diff --git a/dev-docs/superpowers/plans/2026-05-17-gui-redesign-tier1-primitives-DRAGGABLEBLOCK-CONTRACT.md b/dev-docs/superpowers/plans/2026-05-17-gui-redesign-tier1-primitives-DRAGGABLEBLOCK-CONTRACT.md index 5098386b..ac1aa0ee 100644 --- a/dev-docs/superpowers/plans/2026-05-17-gui-redesign-tier1-primitives-DRAGGABLEBLOCK-CONTRACT.md +++ b/dev-docs/superpowers/plans/2026-05-17-gui-redesign-tier1-primitives-DRAGGABLEBLOCK-CONTRACT.md @@ -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).