feat(timetable): keyboard a11y composable + page entry — Session 4 step 11 + ship
useTimetableKeyboard (RFC v0.2 D20): - Arrow ←/→ nudges by SNAP_MIN; Shift+Arrow = ±60min - Arrow ↑/↓ shifts lane; Shift+Arrow ↑/↓ = ±1 stage - [/] cycles stages preserving time + lane - Space starts a "keyboard drag" (announced via aria-live), arrows accumulate the offset, Enter commits, Esc cancels - Enter on a focused block opens the popover; Delete confirms+removes - Pure orchestration — the actual mutation goes through useTimetableMutations so keyboard moves inherit optimistic update + 409 rollback pages/events/[id]/timetable/index.vue: - definePage with organizer context + navActiveLink=events - ?day query param ↔ store.activeDayId in both directions - Composes EventTabsNav, TimeAxis, GridBg, StageHeaderCell, StageRow, Wachtrij, PerformancePopover, AddPerformanceDialog, StageEditor, LineupMatrix, EmptyDayState - Conflict pill in toolbar (header total) per prototype audit §4.8 - Status filter chips applied to canvas blocks via store.isStatusVisible - usePointerDrag + useDragOrClick wires drag to a single move() call; on success flashes pulseSet on cascaded[] for 1.5s (D18 + D21 keyframe) - aria-live region echoes keyboard-drag announcements Tweaks for boundary/lint cleanliness: - Dialog props switched from Ref<T> to T + toRef inside (Vue templates auto-unwrap refs; Ref-typed props clashed with template usage) - Wachtrij counts shadow + sonarjs cleanup - no-void watcher Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import { useTimetableMutations } from '@/composables/api/useTimetableMutations'
|
||||
import { createPerformancePayloadSchema } from '@/schemas/timetable'
|
||||
@@ -9,9 +8,9 @@ import { requiredValidator } from '@core/utils/validators'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
orgId: Ref<string>
|
||||
eventId: Ref<string>
|
||||
dayId: Ref<string | null>
|
||||
orgId: string
|
||||
eventId: string
|
||||
dayId: string | null
|
||||
stages: Stage[]
|
||||
/** Engagements available to schedule (filtered to this event). */
|
||||
engagements: ArtistEngagement[]
|
||||
@@ -24,6 +23,10 @@ const emit = defineEmits<{
|
||||
'created': []
|
||||
}>()
|
||||
|
||||
const orgIdRef = toRef(props, 'orgId')
|
||||
const eventIdRef = toRef(props, 'eventId')
|
||||
const dayIdRef = toRef(props, 'dayId')
|
||||
|
||||
const refForm = ref<VForm>()
|
||||
const errors = ref<Record<string, string>>({})
|
||||
|
||||
@@ -50,7 +53,7 @@ watch(() => props.modelValue, open => {
|
||||
}
|
||||
})
|
||||
|
||||
const mutations = useTimetableMutations({ orgId: props.orgId, eventId: props.eventId, dayId: props.dayId })
|
||||
const mutations = useTimetableMutations({ orgId: orgIdRef, eventId: eventIdRef, dayId: dayIdRef })
|
||||
const isPending = computed(() => mutations.create.isPending.value)
|
||||
|
||||
const engagementOptions = computed(() =>
|
||||
@@ -64,7 +67,7 @@ const engagementOptions = computed(() =>
|
||||
async function submit(): Promise<void> {
|
||||
errors.value = {}
|
||||
|
||||
const eventIdValue = props.dayId.value ?? props.eventId.value
|
||||
const eventIdValue = props.dayId ?? props.eventId
|
||||
|
||||
const payload = {
|
||||
engagement_id: form.value.engagement_id,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import { useTimetableMutations } from '@/composables/api/useTimetableMutations'
|
||||
import type { Stage } from '@/types/timetable'
|
||||
|
||||
@@ -12,9 +11,9 @@ interface SubEvent {
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
orgId: Ref<string>
|
||||
eventId: Ref<string>
|
||||
dayId: Ref<string | null>
|
||||
orgId: string
|
||||
eventId: string
|
||||
dayId: string | null
|
||||
stages: Stage[]
|
||||
subEvents: SubEvent[]
|
||||
}>()
|
||||
@@ -24,6 +23,10 @@ const emit = defineEmits<{
|
||||
'saved': []
|
||||
}>()
|
||||
|
||||
const orgIdRef = toRef(props, 'orgId')
|
||||
const eventIdRef = toRef(props, 'eventId')
|
||||
const dayIdRef = toRef(props, 'dayId')
|
||||
|
||||
const matrix = ref<Record<string, Set<string>>>({})
|
||||
const errors = ref<Record<string, string>>({})
|
||||
|
||||
@@ -48,7 +51,7 @@ function isOn(stageId: string, eventId: string): boolean {
|
||||
return matrix.value[stageId]?.has(eventId) ?? false
|
||||
}
|
||||
|
||||
const mutations = useTimetableMutations({ orgId: props.orgId, eventId: props.eventId, dayId: props.dayId })
|
||||
const mutations = useTimetableMutations({ orgId: orgIdRef, eventId: eventIdRef, dayId: dayIdRef })
|
||||
const isPending = ref(false)
|
||||
|
||||
const dirtyStages = computed(() =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, toRef } from 'vue'
|
||||
import { useEngagement } from '@/composables/api/useTimetable'
|
||||
import { ArtistEngagementStatus, type Performance } from '@/types/timetable'
|
||||
|
||||
@@ -8,7 +8,7 @@ const props = defineProps<{
|
||||
/** Anchor element rect (from PerformanceBlock click). */
|
||||
anchorRect: DOMRect | null
|
||||
performance: Performance | null
|
||||
orgId: import('vue').Ref<string>
|
||||
orgId: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -17,9 +17,11 @@ const emit = defineEmits<{
|
||||
'delete': [perf: Performance]
|
||||
}>()
|
||||
|
||||
const orgIdRef = toRef(props, 'orgId')
|
||||
|
||||
const engagementId = computed(() => props.performance?.engagement_id ?? null)
|
||||
const engagementIdRef = computed<string | null>(() => engagementId.value)
|
||||
const { data: engagement, isLoading } = useEngagement(props.orgId, engagementIdRef)
|
||||
const { data: engagement, isLoading } = useEngagement(orgIdRef, engagementIdRef)
|
||||
|
||||
const advancing = computed(() => {
|
||||
const e = engagement.value ?? props.performance?.engagement
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import { useTimetableMutations } from '@/composables/api/useTimetableMutations'
|
||||
import { createStagePayloadSchema } from '@/schemas/timetable'
|
||||
@@ -9,9 +8,9 @@ import { requiredValidator } from '@core/utils/validators'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
orgId: Ref<string>
|
||||
eventId: Ref<string>
|
||||
dayId: Ref<string | null>
|
||||
orgId: string
|
||||
eventId: string
|
||||
dayId: string | null
|
||||
/** Pass an existing stage to edit; null = create. */
|
||||
stage: Stage | null
|
||||
}>()
|
||||
@@ -22,6 +21,10 @@ const emit = defineEmits<{
|
||||
'deleted': [stage: Stage]
|
||||
}>()
|
||||
|
||||
const orgIdRef = toRef(props, 'orgId')
|
||||
const eventIdRef = toRef(props, 'eventId')
|
||||
const dayIdRef = toRef(props, 'dayId')
|
||||
|
||||
const STAGE_PALETTE = [
|
||||
'#e85d75',
|
||||
'#d9a93c',
|
||||
@@ -57,7 +60,7 @@ watch(() => props.modelValue, open => {
|
||||
}
|
||||
})
|
||||
|
||||
const mutations = useTimetableMutations({ orgId: props.orgId, eventId: props.eventId, dayId: props.dayId })
|
||||
const mutations = useTimetableMutations({ orgId: orgIdRef, eventId: eventIdRef, dayId: dayIdRef })
|
||||
const isPending = computed(() => mutations.createStage.isPending.value || mutations.updateStage.isPending.value)
|
||||
const isDeleting = computed(() => mutations.deleteStage.isPending.value)
|
||||
|
||||
|
||||
@@ -37,25 +37,23 @@ const filtered = computed(() => {
|
||||
|
||||
return props.performances.filter(p => {
|
||||
const status = p.engagement?.booking_status?.value
|
||||
if (!store.isStatusVisible(status))
|
||||
return false
|
||||
if (search && !(p.engagement?.artist?.name ?? '').toLowerCase().includes(search))
|
||||
return false
|
||||
const visible = store.isStatusVisible(status)
|
||||
const matchesSearch = !search || (p.engagement?.artist?.name ?? '').toLowerCase().includes(search)
|
||||
|
||||
return true
|
||||
return visible && matchesSearch
|
||||
})
|
||||
})
|
||||
|
||||
const counts = computed(() => {
|
||||
const counts = new Map<ArtistEngagementStatusType, number>()
|
||||
const out = new Map<ArtistEngagementStatusType, number>()
|
||||
for (const p of props.performances) {
|
||||
const s = p.engagement?.booking_status?.value
|
||||
if (!s)
|
||||
continue
|
||||
counts.set(s, (counts.get(s) ?? 0) + 1)
|
||||
out.set(s, (out.get(s) ?? 0) + 1)
|
||||
}
|
||||
|
||||
return counts
|
||||
return out
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user