docs(plan): Plan 3 Task A2 — DraggableBlock canonical API reconciled from 2 consumers

This commit is contained in:
2026-05-18 10:19:18 +02:00
parent 1561024ead
commit dd45e89990

View File

@@ -7,3 +7,39 @@ The parity harness spans two PrimeVue installations that configure dark-mode dif
> crewli-starter: `darkModeSelector: '[data-theme="dark"]'`, dark primary hardcoded `#1eafb1`, bespoke surface block.
> apps/app: `darkModeSelector: '.dark'` (RFC-WS-FRONTEND-PRIMEVUE AD-2, matches Vuexy), dark primary `{primary.400}` Aura ramp, RFC Appendix B surface tokens.
> **Decision (option b — normalise at the harness):** v2 components NEVER hardcode a dark selector or a semantic hex; they consume `var(--p-*)` Aura tokens only, which resolve correctly under apps/app's `.dark`. The parity harness renders the crewli-starter reference under `[data-theme="dark"]` and the v2 component under `.dark`; the human parity-check compares **rendered pixels**, not selector strings. The `#1eafb1` vs `{primary.400}` ramp delta is **accepted** (same teal family; RFC AD-2 owns the apps/app ramp) and explicitly recorded so a dark-mode parity diff is read as theme-by-design, not a component bug.
## DraggableBlock dual-consumer reconciliation
Spec §7.1 is the canonical DraggableBlock contract. Two crewli-starter consumers use fundamentally different drag models — `TimetableGrid` uses a `mousedown``window` `mousemove`/`mouseup` pointer approach, while `CueTimelineEditor` uses native HTML5 drag (`draggable="true"`, `dataTransfer.setData`). Both map cleanly to the §7.1 prop/emit contract as shown below.
```
TimetableGrid performance block → §7.1
artist.name → line1Left.text
status tag (engagement) → line1Left.tag {label, severity}
genre → line1Right.pill
capacity/conflict warn → line1Right.tag {label:'!', severity:'danger'}
blockTime(start,end) → line2Left
advanceCount done/total → line2Right.progress (0..1 → 0..100)
selected (selectedId) → selected
drag.value!=null → dragging
baseRowHeight 56/64/76 → density compact|regular|comfy
CueTimelineEditor cue block → §7.1
cue.label → line1Left.text
cue.kind tag → line1Left.tag {label, severity}
cue.dest pill → line1Right.pill
(none) → line1Right.tag
cue.time → line2Left
(none) → line2Right (null — no progress on cues)
selectedCueId===cue.id → selected
drag?.id===cue.id → dragging
fixed 'regular' → density
```
### Drag model
> **Drag model = PointerEvents, parent owns positioning.** `DraggableBlock` is presentational. On `pointerdown` (primary button) it calls `setPointerCapture`, tracks a 3px move threshold (TimetableGrid parity), emits `dragstart: [e: PointerEvent]` once threshold is crossed, and on `pointerup`/`lostpointercapture` emits `dragend: [delta: { x: number; y: number }]` (`clientX/Y` minus start). It performs **zero** snap/lane/px-min math. `TimetableGrid` keeps `startDragMove`'s math but is driven by `@dragstart`/`@dragend` instead of its own `mousedown`. `CueTimelineEditor` drops HTML5 drag and adopts the same emits. `click` is emitted only when no drag occurred (threshold not crossed). `vuedraggable` is not used (wrong abstraction for free-position blocks; spec §7.1).
### Retrofit proof
> Plan 3 is incomplete without a retrofit proof per consumer: `DraggableBlock.stories.ts` MUST include an `ArtistBlock` story (TimetableGrid usage expressed in the §7.1 contract) and a `CueBlock` story (CueTimelineEditor usage in the §7.1 contract). These prove the abstraction expresses both consumers without either consumer's page existing yet (Tier-4 defers the pages, not this proof).