3.7 KiB
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 consumevar(--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#1eafb1vs{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.
DraggableBlockis presentational. Onpointerdown(primary button) it callssetPointerCapture, tracks a 3px move threshold (TimetableGrid parity), emitsdragstart: [e: PointerEvent]once threshold is crossed, and onpointerup/lostpointercaptureemitsdragend: [delta: { x: number; y: number }](clientX/Yminus start). It performs zero snap/lane/px-min math.TimetableGridkeepsstartDragMove's math but is driven by@dragstart/@dragendinstead of its ownmousedown.CueTimelineEditordrops HTML5 drag and adopts the same emits.clickis emitted only when no drag occurred (threshold not crossed).vuedraggableis 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.tsMUST include anArtistBlockstory (TimetableGrid usage expressed in the §7.1 contract) and aCueBlockstory (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).