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:
2026-05-09 01:58:56 +02:00
parent 288aebcd69
commit 43572a7812
7 changed files with 868 additions and 30 deletions

View File

@@ -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