# Plan 3 — DraggableBlock contract & reconciliation gates ## Theme alignment The parity harness spans two PrimeVue installations that configure dark-mode differently. The divergence and its resolution are recorded here as spec-of-record. > 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).