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
+148
View File
@@ -1643,6 +1643,154 @@ predicate). The `TrackPopupMenu` popup, the `Main_OnCommand` dispatch, and the t
timer + LICE overlay draw are the only DAW-bound pieces; the L1 kit draws. **L5 resolves its timer + LICE overlay draw are the only DAW-bound pieces; the L1 kit draws. **L5 resolves its
menu/tooltip/action-id specifics against the landed L4 `bank_panel`** (build-time confirmation). menu/tooltip/action-id specifics against the landed L4 `bank_panel`** (build-time confirmation).
## L7 capture ordering · card metadata · selection styling (ordering model · overlay · tertiary-border selection)
L7 is the **next ungated dock-panel pass over the same `bank_panel` grid** L4L6 built. Unlike
L4/L5 (pure layout over existing actions), **L7 (1) is a persisted-model change** — it adds an
explicit per-sample display order with gap-preserving sparse placement to the persisted bank
state and its JSON round-trip. (2) and (3) are draw-only. **No new capture/placement behavior;**
the "capture ≠ placement" principle is untouched. Drawn through the L1 kit in the DS-2
grey-neutral + three-accent-pastel palette; **no palette/font decision is re-opened.** Ungated
by Phase S; independent of the L3 gate. Sequences AFTER L6.
**Research baseline (dev, confirmed at spec time).** The grid draws in **`BankIndex` insertion
order**: `BankIndex` holds `std::vector<Sample> samples_` in insertion order (`all()`), and
`bank_grid::computeCellRects` tiles exactly one contiguous rect per sample left-to-right,
top-to-bottom — **no explicit ordinal, no sparse slots today.** `Sample` already carries
`lengthSeconds`, `lengthBeats`, and `captureTempo` (BPM at capture) but **no time-signature
field.** The selected cell is drawn (`bank_panel::drawThumbnail`) as
`InteractionState::Active` (accent-fill surface) + an `accent/primary` border + an *inverted*
waveform (`bg/base`), with focus as a `text/primary` double-line ring. Drag today: press on a
selected cell arms a drag; crossing a threshold begins it; leaving the client rect hands off to
OS drag-out (`drag_out::decideGesture`); a drop on a tab / the other region moves (or copies on
Ctrl) the samples to that bank. **There is no same-bank in-grid reorder today.**
### 1. Persisted deterministic order + sparse (gap-preserving) placement
Each bank (and the pool) carries an **explicit, persisted display position per sample** — the
grid no longer derives order from insertion order. Positions are **gap-preserving**: a sample
may occupy a slot that leaves earlier slots empty (an empty first row above an occupied second
row is a valid, persisted state).
**Where the position data lives (the CLAUDE.md constraint).** `bank_model` / `Sample` are stated
**untouched by `bank_book`'s wrapping** (no `bankId` on `Sample`; a bank is a logical grouping
over the shared pool). Display position is a **per-bank display concern**, so it belongs with the
bank's membership, **not on `Sample`** — a copy of a sample into two banks can sit at different
slots. **Recommended carrier: a per-`Bank` ordered position map in `bank_book`** (sample id →
slot), leaving `bank_model` untouched. The exact carrier (ordered id list with gaps vs. explicit
id→slot map) is settled at build; the *contract* below holds either way.
**Ordering contract (deterministic, gap-preserving):**
- **Deterministic:** the grid iterates positions in ascending slot order; ties are impossible
(one sample per slot). The order is fully determined by the persisted position data, not by
insertion order or hash.
- **Insert (new capture):** a new capture takes the **next free slot after the last occupied
slot** (append). It never fills an earlier gap automatically — a gap is a user's deliberate
layout, not a hole to be plugged.
- **Delete / remove / prune:** removing a sample **leaves its slot empty** (does not re-pack) so
every other sample keeps its position. Define at build whether a purely-empty trailing tail is
trimmed for content-height purposes (the recommendation: trim trailing empties for scroll
extent, keep interior gaps).
- **Reorder:** the user drags a card to a target slot within its bank; the pure reorder mutator
moves that sample's position to the target slot, gap-preserving. (Whether a drop onto an
occupied slot displaces/swaps vs. inserts-and-shifts is settled at build — recommendation:
**drop-into-empty-slot places there; drop-onto-occupied inserts before and shifts the tail**,
matching common file-manager reorder.)
**JSON round-trip + migration (load-bearing).** `serialize`/`deserialize` stay lossless
including positions (`deserialize(serialize(x)) == x`). **A pre-L7 project blob has no position
data** → on load it defaults to **current insertion order, densely packed (no gaps)**, so a
project saved before L7 is visually identical on first post-L7 load. This default is the
migration; it is one-way (once re-saved, the position data is authoritative). Pure and
hard-tested — this is a persisted-model change, tested to the same bar as `bank_book`'s existing
round-trip + legacy migration.
**Undo.** A reorder is **one Ctrl-Z** — the actions/shell layer wraps the mutation in a batched
undo point (`Undo_BeginBlock2`/`EndBlock2`), matching every existing bank-verb's undo discipline.
**M9 overlap (flagged — Daniel's awareness).** M9 "slots" (capture-to-slot-N / insert-slot-N,
MIDI-bindable, MPC-style) is **explicitly deferred (Daniel, 2026-07-26).** L7's sparse-slot
model is a **partial overlap:** it builds the *addressable-position substrate* M9 would sit on,
but L7 adds **no** slot-numbered capture/insert actions and **no** MIDI bindings. Building L7
un-defers the *coordinate model* portion of M9, not the *action* portion. If Daniel wants the
substrate to be explicitly M9-shaped (fixed numbered slots vs. a plain gap-preserving ordinal),
that is a fork to settle before the model is built.
### 2. Decorative metadata overlay (bars.beats · s.ms)
Each card overlays, on the waveform, two decorative read-outs of **capture length**:
- **bottom-LEFT:** length in **bars.beats.subdivisions** (musical).
- **bottom-RIGHT:** length in **seconds.milliseconds** (wall-clock).
**Contract:** decorative and **non-interactive** — no hit-test, no hover, no selection role.
Drawn via the L1 kit `text()` in the **micro / value-mono** type class, in `text/dim` (or a
subtle shadowed variant for legibility over the peaks), subordinate to the waveform. **Respects
the speed constraint (no animation).** Pure formatting helpers (below) are unit-tested; only the
kit draw is shell.
**Bars.beats source (FORK F1 — Daniel to confirm).** bars.beats.subdivisions requires a
**tempo + time-signature** reference. `Sample` already carries `captureTempo` (BPM at capture)
and `lengthBeats`, but **no time-signature.** Two options:
- **(recommended) Capture-time stamp:** add `captureTimeSigNum` / `captureTimeSigDenom` to
`Sample` + its JSON round-trip, stamped on the capture path (read the project meter at capture
via the REAPER meter API — verify `TimeMap_GetTimeSigAtTime` / equivalent against the SDK at
build). *Why recommended:* the label is **stable** as the project's tempo/meter later changes;
a bank sample outlives the project state it was captured under, matching the existing
`captureTempo` stamp philosophy. **Cost:** this is a **capture-path write beyond draw work** —
its own flagged checkbox; old samples with no stamp fall back gracefully (blank musical
read-out, or a documented assumed 4/4).
- **(rejected) Live project meter at draw time:** the label would drift under the card as the
project tempo/meter changes, and would be wrong for any sample captured under a different
meter than the project's current one.
**Formatting helpers (pure, tested).** `bars.beats.subdivisions` from `lengthSeconds` +
`captureTempo` + capture-time signature; `seconds.milliseconds` from `lengthSeconds`.
Deterministic; graceful on edge cases (zero length → both read empty/`0`; missing tempo → blank
musical read-out, keep the s.ms read-out).
### 3. Selection styling — tertiary border replaces inversion
A selected card **drops the inverted accent-fill treatment** and instead draws the **normal
cell** (Rest or Hover surface) + an **`accent/tertiary` border** (pastel purple `#C2AAE8`); the
**waveform draws in its normal accent color** (the inverted `bg/base` wave is removed). The four
grid-card interaction states stay **visually distinct and coherent:**
- **Selected:** normal cell surface + **`accent/tertiary` (purple) 1px border**. No fill change,
no wave inversion.
- **Focus (caret):** the existing distinct inner ring — keep it separate from the selection
border so a focused *and* selected card reads both (e.g. purple outer border + a
`text/primary` inner focus ring). Settle the exact inner treatment at build so focus is
legible on top of the selection border.
- **Hover:** the kit Hover surface (unchanged) — a fill-state change, orthogonal to the purple
border, so a hovered selected card still reads as selected.
- **Drag-target slot:** the reorder drop-target highlight (a distinct accent — recommend
`accent/hot` outline on the target slot) must not be confusable with the purple selection
border; spec the exact treatment at build.
### Drag disambiguation (FORK F3 — proposed rule, Daniel to confirm)
In-grid reorder must coexist with the existing internal bank-move/copy drag and OS drag-out.
**Proposed precedence (one clean rule, evaluated live during the drag):**
1. **Pointer leaves the client rect → OS drag-out** (unchanged; `drag_out::decideGesture` wins
first — the existing invariant #4 boundary).
2. **Else drop lands on a tab / the OTHER region's bank → move/copy** (unchanged; Ctrl = copy).
3. **Else drop lands within the SAME bank's own grid → reorder-to-slot** (new).
So: leave-client wins → else other-bank wins → else same-bank-grid = reorder. The precedence is
encoded in a **pure decision helper** (mirror `drag_out::decideGesture`); the shell reads the
live pointer + focused region + client rect and calls it. This keeps the reorder gesture from
ever stealing an intended bank-move or OS-drag, and keeps a same-bank in-grid drag from being
mis-read as a no-op (today a same-bank drop is a no-op; L7 gives it reorder meaning).
### Pure/shell discipline (L7)
Model: the position carrier + gap semantics + reorder mutator + JSON round-trip/migration are
**pure** (in `bank_book`, CTest-covered to the bar of its existing round-trip). Layout: the
sparse-aware slot↔rect math + point→slot hit-test + the drag-disambiguation decision are **pure**
(extend `bank_grid`; mirror `mode_switch` / `drag_out::decideGesture`). Formatting: the
bars.beats and s.ms formatters are **pure**. Shell (DAW-bound): the reorder drag wiring +
drop-target highlight, the capture-time-signature stamp (F1) read on the capture path, and the
kit overlay/selection-border draw. The L1 kit draws; no palette/font decision re-opened.
## The L3 gate + Phase S coordination contract ## The L3 gate + Phase S coordination contract
**L3 (VST editor + embed-strip restyle) is GATED on Phase S landing on dev.** The VST **L3 (VST editor + embed-strip restyle) is GATED on Phase S landing on dev.** The VST
+119 -4
View File
@@ -372,17 +372,123 @@ work.
state — never a pulse, per the speed constraint) as the signature surface; VST3 class UID state — never a pulse, per the speed constraint) as the signature surface; VST3 class UID
unchanged. (Gated.) 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 ## Phase L — sequencing
``` ```
L1 (shared kit) ──► L2 (dock-panel layout redesign) [LANDED] L1 (shared kit) ──► L2 (dock-panel layout redesign) [LANDED]
├─────────► L3 (VST editor + embed-strip restyle; GATED on Phase S landing on dev) ├─────────► L3 (VST editor + embed-strip restyle; GATED on Phase S landing on dev)
└─────────► L4 (dock-panel button layout enhancement) [LANDED] └─────────► L4 (dock-panel button layout enhancement) [LANDED]
└────► L5 (dock-panel button refinements; ungated, after L4) [LANDED] └────► L5 (dock-panel button refinements; ungated, after L4) [LANDED]
└────► L6 (toolbar polish; ungated, after L5) [LANDED]
Phase S (separate team, ungated): S-work builds with current drawing, adopts L1 when it lands. └────► 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. L1, L2, L4, L5, and L6 have landed. L7 is the next ungated dock-panel work. L3 waits for
Phase S feature work runs in parallel, ungated by Phase L. Phase S to reach dev. Phase S feature work runs in parallel, ungated by Phase L.
## Phase L — must-verify-before-build ## Phase L — must-verify-before-build
- **LICE design-kit surfaces (L1)** — `LICE_GradRect`, `LICE_RoundRect`, AA - **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 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 and track-tag action ids confirmed; active-mode read confirmed via the same
`view().activeModeId()` the footer toggle uses. `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.
--- ---