docs(product): spec Phase L L7 — capture ordering, card metadata, and selection styling

Persisted gap-preserving per-bank display order + drag-drop reorder (model change, JSON round-trip + migration), decorative bars.beats/s.ms overlay, tertiary-border selection restyle. Model work separated from draw work; forks flagged (meter source, M9 substrate overlap, drag disambiguation).
This commit is contained in:
2026-07-27 01:11:05 -04:00
parent 9decd25d88
commit e4bcc8f075
2 changed files with 267 additions and 4 deletions
+119 -4
View File
@@ -372,17 +372,123 @@ work.
state — never a pulse, per the speed constraint) as the signature surface; VST3 class UID
unchanged. (Gated.)
## L7 — capture ordering, card metadata, and selection styling (ungated, after L6)
**Goal:** Three grid-facing improvements to the dock panel, drawn through the L1 kit in the
settled DS-2 palette. (1) **Persisted deterministic capture order + drag-drop reorder + sparse
placement:** each bank (and the pool) carries an explicit, persisted per-sample order, changed
by dragging cards within the grid; a card may sit in a grid slot that leaves gaps (an empty
first row above an occupied second row). (2) **Decorative metadata over the peaks:** each card
overlays capture length as **bars.beats.subdivisions in the bottom-LEFT** and **seconds.ms in
the bottom-RIGHT**, subtle/legible over the waveform in the kit's micro / value-mono type
classes. (3) **Selection restyle:** a selected card drops the inverted accent-fill and instead
draws the *normal* cell + an **`accent/tertiary` (pastel purple `#C2AAE8`) border**; hover,
focus, and drag-highlight stay visually distinct. **No new capture/placement behavior** — this
is model + layout + draw only; the "capture ≠ placement" principle is untouched.
> **This is a persisted-model change, not pure layout.** Feature (1) adds an explicit
> per-sample ordinal/slot to the persisted state and its JSON round-trip + a migration for
> existing projects — scoped honestly below, model work separated from draw work.
> **Forks for Daniel (surface before build — see CONTEXT.md §L7):**
> - **F1 — bars.beats source.** `Sample` already carries `captureTempo` (BPM at capture) and
> `lengthBeats`, but **no time-signature field.** bars.beats.subdivisions needs a
> time-signature reference. **Recommendation: a capture-time stamp** (add `captureTimeSigNum`
> /`captureTimeSigDenom` to `Sample`, stamped on the capture path) so the label is stable as
> the project tempo/meter later changes — matching the existing `captureTempo` stamp
> philosophy. This adds a **capture-path stamp beyond draw work** (its own checkbox below,
> flagged). Alternative (live project meter at draw time) is rejected: the label would drift
> under the card as the project changes, and a bank sample outlives the project state it was
> captured under.
> - **F2 — M9 un-deferral awareness.** M9 "slots" (capture-to-slot-N / insert-slot-N,
> MIDI-bindable, MPC-style) is **explicitly deferred (Daniel, 2026-07-26).** L7's sparse
> placement is a **partial overlap** — both need addressable grid positions. Building L7's
> sparse-slot model effectively lays the *position substrate* M9 would sit on, but L7 does
> **not** add slot-numbered capture/insert actions or MIDI bindings. Flagged so Daniel knows
> L7 un-defers the *coordinate model* portion of M9, not the *action* portion. If Daniel wants
> the slot substrate to be explicitly M9-shaped (numbered addressable slots vs. a plain
> gap-preserving ordinal), that is a fork to settle before the model checkbox.
> - **F3 — drag-disambiguation rule (proposed, Daniel to confirm).** The panel already has
> internal bank-move/copy drag and OS drag-out. In-grid reorder must coexist. **Proposed
> rule:** a drag whose drop lands **within the SAME bank's own grid** = **reorder-to-slot**;
> a drop on **a tab / the other region's bank** = **move/copy** (as today); a drag that
> **leaves the client rect** = **OS drag-out** (as today). One clean precedence: leave-client
> wins → else other-bank wins → else same-bank-grid = reorder. See CONTEXT.md §L7.
**Verify (in DAW):** capture several samples; drag a card to a new position within its bank —
the order persists across project save/reload and Ctrl-Z restores the prior order; drag the
first card below the second so the first row is empty — the gap holds across reload; each card
shows bars.beats.subdivisions bottom-left and s.ms bottom-right, legible over the peaks; a
selected card shows the normal cell with a purple (`accent/tertiary`) border — no inversion —
and hover / focus / drag-target remain distinct; dragging a card to another bank's tab still
moves/copies (not reorder), and dragging out of the panel still starts an OS file drag; an
existing project (saved before L7) loads cleanly with cards in their prior insertion order.
**Depends on:** L1 (the kit) for all draw; sequences **after L6** (both rework the same
`bank_panel` grid). **Ungated by Phase S** (the dock panel is on dev); independent of the L3
gate. No `capture` / `insert` behavior change; no palette/font decision re-opened.
**(a) Persisted order + sparse-position model (pure, hard-tested) — model work, separate from draw:**
- [ ] Add an explicit per-sample **display position** to the persisted bank state (gap-preserving
ordinal or slot coordinate — settle the shape against F2). Decide the home: `bank_model`
is stated untouched by `bank_book`'s wrapping — position is a **per-bank display concern**, so
it belongs with the bank's membership, not on `Sample`. Candidate: a per-`Bank` ordered/keyed
position map in `bank_book` (id → slot), leaving `bank_model` / `Sample` untouched. Settle the
exact carrier at build.
- [ ] JSON round-trip for the position data (`serialize`/`deserialize` remain lossless:
`deserialize(serialize(x)) == x` including positions).
- [ ] **Migration for existing projects** (old blobs with no position data): load cleanly with a
**default = current insertion order, densely packed** (no gaps). Spec the default explicitly so
a pre-L7 project is visually identical on first load post-L7.
- [ ] **Gap semantics on insert / delete / prune** (pure, tested): a new capture takes the next
free slot after the last occupied one (append); a delete/remove/prune **leaves a gap** (does
not re-pack) so positions stay stable; define whether an empty trailing tail is trimmed.
- [ ] **Reorder mutator** (pure): move sample `id` to target slot within its bank, gap-preserving;
deterministic; unit-tested.
- [ ] Undo integration: a reorder is one Ctrl-Z (batched undo point in the actions/shell layer,
matching the existing bank-verb undo discipline).
**(b) Reorder-drag gesture + slot hit-test (pure geometry + shell wiring):**
- [ ] **Sparse-aware grid layout** (pure, extend `bank_grid`): map per-slot positions (with gaps)
to `CellRect`s — an empty slot is drawn empty (or skipped), not back-filled. Slot ↔ pixel-rect
math + point→slot hit-test stay pure and unit-tested (mirror `mode_switch`/`bank_grid`).
- [ ] **Drag-disambiguation** (F3): drop within the same bank's grid = reorder-to-slot; drop on a
tab / other bank = move/copy (unchanged); leave client = OS drag-out (unchanged). Precedence
encoded in a **pure decision helper** (mirror `drag_out::decideGesture`); shell reads live
pointer + region + client rect and calls it.
- [ ] Shell wiring: on a same-bank reorder drop, resolve the target slot via the pure hit-test and
call the reorder mutator; drop-target slot highlight during the drag (distinct from the existing
region/tab drop highlights).
**(c) Metadata overlay (pure formatting + kit draw):**
- [ ] **Pure formatters, unit-tested:** `bars.beats.subdivisions` from `lengthSeconds` +
`captureTempo` + capture-time signature (F1) and `seconds.milliseconds` from `lengthSeconds`.
Deterministic string output; edge cases (zero length, missing tempo → graceful blank/fallback).
- [ ] **[FLAGGED — F1, capture-path stamp beyond draw work] Capture-time signature stamp:** add
`captureTimeSigNum` / `captureTimeSigDenom` to `Sample` + its JSON round-trip, stamped on the
capture path (read the project meter at capture). Only needed if F1 resolves to the capture-time
stamp (recommended). Old samples with no stamp fall back gracefully in the formatter.
- [ ] **Kit draw:** overlay the two strings on the card — bars.beats bottom-LEFT, s.ms
bottom-RIGHT — via the kit `text()` in the micro / value-mono class, `text/dim` (or a subtle
shadowed variant for legibility over the waveform); **decorative, non-interactive** (no
hit-test, no hover). Respects the speed constraint (no animation).
**(d) Selection restyle (tertiary border):**
- [ ] `drawThumbnail`: a selected card draws the **normal cell fill** (Rest/Hover surface, not
`InteractionState::Active`) + an **`accent/tertiary` border** (pastel purple `#C2AAE8`); the
waveform draws in its normal accent color (drop the inverted `bg/base` wave). Focus ring, hover
state, and drag-target highlight stay **distinct** from the selection border (keep them
coherent — spec the four states' visual separation in CONTEXT.md §L7).
## Phase L — sequencing
```
L1 (shared kit) ──► L2 (dock-panel layout redesign) [LANDED]
├─────────► L3 (VST editor + embed-strip restyle; GATED on Phase S landing on dev)
└─────────► L4 (dock-panel button layout enhancement) [LANDED]
└────► L5 (dock-panel button refinements; ungated, after L4) [LANDED]
Phase S (separate team, ungated): S-work builds with current drawing, adopts L1 when it lands.
└────► L6 (toolbar polish; ungated, after L5) [LANDED]
└────► L7 (capture ordering + card metadata + selection styling; ungated, after L6)
```
L1, L2, L4, and L5 have landed. L3 waits for Phase S to reach dev.
Phase S feature work runs in parallel, ungated by Phase L.
L1, L2, L4, L5, and L6 have landed. L7 is the next ungated dock-panel work. L3 waits for
Phase S to reach dev. Phase S feature work runs in parallel, ungated by Phase L.
## Phase L — must-verify-before-build
- **LICE design-kit surfaces (L1)** — `LICE_GradRect`, `LICE_RoundRect`, AA
@@ -407,6 +513,15 @@ Phase S feature work runs in parallel, ungated by Phase L.
pure module; sourced from the registered action phrase, prefix stripped at draw time); item-move
and track-tag action ids confirmed; active-mode read confirmed via the same
`view().activeModeId()` the footer toggle uses.
- **L7 model + tempo surface (L7)** — settle at L7 build/spec-review: (1) **position carrier**
where the per-sample display position lives (candidate: a per-`Bank` id→slot map in `bank_book`,
keeping `bank_model`/`Sample` untouched per CLAUDE.md); (2) **F1 bars.beats source** — confirm
the capture-time signature stamp with Daniel (recommended) vs. live project meter; the stamp adds
a capture-path write (its own flagged checkbox); (3) **F2 M9 overlap** — confirm L7's sparse-slot
substrate is not mis-scoped as M9's slot *actions*; (4) **F3 drag rule** — confirm the
same-bank-grid = reorder / other-bank = move-copy / leave-client = OS-drag precedence. Verify the
REAPER meter-read API for the stamp (`TimeMap_GetTimeSigAtTime` or equivalent) against the SDK at
build.
---