spike: module face, panel, and play mode UX (M2) - #248
Draft
Yona-Appletree wants to merge 16 commits into
Draft
Conversation
The DTO layer for docs/design/modules.md R8 and docs/design/panel.md P1/P2: - UiPanelControlState — the three states P-Q2 requires to be visibly distinct (Read-at-default, Read-following-automation, Engaged/Latch). - UiPanelControlView — one channel on one panel: the existing UiPanelControl payload (so knob v2 / fader / toggle are reused, not forked) plus the panel state and its Read-state provenance caption. - UiPanelGroup — a scope's channel list plus nested child-module groups (R8's presentation recursion), with the engaged counts a per-module reset gesture needs. - UiModuleFace / UiModuleChild — the face worn at every depth: output mirror (R7), panel, children nested inside, bus-as-wiring drawer, provenance (§8), and the P11 auto-save flag. Spike posture: these are fed by mock fixtures. Deriving them from real scope data is M4's; nothing in the project layer builds them yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One additive `engaged` prop per widget, defaulting to false, so every existing face renders byte-identically. When set, the value arc / fill / pill ring wears the amber status-attention family instead of the violet bound family. Engaged deliberately OUTRANKS bound: a captured control has stopped following the thing it is wired to, and panel.md P6 says that has to be visible at a glance — bound means "wired", engaged means "captured". Green stays valid-only, so it was never a candidate. A dedicated status-engaged token family is the eventual home; amber is the spike's proposal for the G2 gate to judge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The spike's components, parallel to app/node/face/ rather than merged into it so the seam is easy to promote or delete: - ModuleFace — output hero, panel, children nested INSIDE the card, the bus wiring drawer, provenance. An embedded module child renders the same component one level in, which is the "one face at every depth" claim made literal (modules.md §5). - ModulePanel / NestedPanelGroup — a scope's controls plus each child module's panel as a hairline-boxed, collapsible group (R8). The header row carries the two module-level gestures: reset-N (present only when something is held) and the P11 auto-save toggle. - ModulePanelControl — the three panel states on the production widgets, with the per-control reset glyph appearing only while engaged. - PlayModeSurface — panels only, no faces (P12). - PanelGesture — the component-level seam standing in for panel.md P8's PanelWrite/PanelClear until M4 adds them; the variants are shaped like the two wire ops so the mapping is mechanical. UiNodeFace gains a Module arm so the face renders inside the real NodePane card chrome, and NodePane threads the panel-gesture handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six stories carrying G2 questions 1-3, on mock fixtures that reproduce the design doc's worked examples rather than inventing content: E3's drop-in embed, E4's shared control and detach, and panel.md's scarf. - root_card — the root module as the single top-level workspace card (flat-root reversal). Walkable: the fixture state is real, so a knob drag actually engages the control and a reset actually lets go. - author_zoom — every child expanded, both embedded modules wearing the same face one level in. - embedded_module — the artist zoom level: one effect as its own card. - nested_groups — two instances of one effect side by side, one detached and one following, on the root panel. - wiring_drawer_open / workspace_no_bus_pane — the bus split: controls on the face, writers/readers in a drawer, sidebar pane gone. The fixtures keep a pristine Read face as the clear-target and apply "already held" controls through the same transition a live touch takes, so a reset in the walkable stories lands somewhere honest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
G2 question 5, and panel.md P-Q2's "confirm the three states are visibly distinct" turned into something judgeable in pixels: - three_states — all three states across knob, fader, and toggle. - engaged_vs_bound — amber against violet on one channel, one range. - reset_gestures — per-control glyph vs per-module chip, with the chip counting everything under the scope including nested groups. - auto_save_off — the P11 toggle in the panel header, on the module that owns the scope rather than in app settings. - latch_walkthrough — walkable Read to Latch to Clear from a pristine panel, so the first touch is the thing being felt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The playlist's face presents the ACTIVE entry's panel, and switching entries re-derives the control from whatever is bound in the newly active sink scope: 0-1 "Drift" becomes 0-10 "Whirl" on the same channel name. entry_drift and entry_whirl are the two static ends; meta_switch is walkable — each entry gets its OWN state holder, which is the model's separation and not a story convenience, so tweaking Drift, switching to Whirl, and switching back finds the tweak still there. Entry activation has no wire op today, so the switch rides plain buttons; what is under test is what happens to the panel, not how the entry gets activated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
G2 question 4. Panels only, no faces (panel.md P12): the root module's panel with its nested effect groups over a slim output banner — no hero-dominant preview, no children, no wiring, no card chrome. Reset and auto-save stay, because the scarf scenario (P10) is an end user's. Three widths/densities: desktop, phone (375px), and phone with the nested groups collapsed — the last one asking which should be the default when a project embeds several effects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From the first capture pass: at 375px the nested group header squeezed
"plasma" down to "P" and the scope path to "/aurora.m…". The header row
now wraps instead of shrinking, so the summary and reset chip drop to a
second line and the group keeps its name. Control captions get a few more
characters before truncating ("held · was inherited …" instead of
"held · was inhe…").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A module's children are not part of its face. They render below the card as full sibling cards through the existing UiNodeChild path — the same grammar the playlist's active child and the old project node use — so the module contributes no new nesting rules of its own. Drops UiModuleChild (children are UiNodeChild now) and adds UiNodeFace::Controls for a leaf whose whole face is its bound slots: the clock, the control node, the fixture. Its group carries the ENCLOSING scope, because a leaf introduces no scope — which is what keeps a fixture's brightness and the module panel's brightness one control with one panel state (panel.md P1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
G2 revision 1. The module face keeps output hero, panel (nested child-module groups included), wiring drawer, and provenance; every child — clock, control node, embedded modules, fixture — now renders BELOW the card on the NodeChildren rail, the same grammar the playlist's active child and the old project node already use. All of them render: module children are collaborators, not branches, so there is no active-child filtering here. An embedded module therefore wears its face in a card of its own, which makes "one face at every depth" literal instead of illustrative, and keeps the host card a readable fixed height. Its controls still appear twice — nested group above, own card below — and that is kept deliberately: it is the playlist precedent, one control with two views (panel.md P1), held in lockstep by the shared (scope, channel) identity. PanelSpike now walks the whole node view rather than one face, because that identity genuinely spans two cards now: holding brightness on the panel has to move the fixture card below it, and a test pins that. auto_save becomes Option<bool> — only the module that presents the switch carries it, since persistence is per project folder (P11). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rome G2 revision 2. The all-caps "RESET 2" / "AUTO-SAVE" chips read as loud as the controls they sit above. They become small icon affordances in the panel's upper right: the revert glyph alone with the count as a tiny superscript, and a small switch for auto-save, both with the full sentence in the tooltip so the chrome does not have to carry it. Nested group resets wear the same treatment on the right of their heading row. Per-control reset glyphs are unchanged — they already appear only while engaged, which is the point of them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two claims the revision rests on: a module's children are sibling cards on the rail (all of them, at every depth), and a leaf's control and the module panel's are one control that moves together even though they now live on two different cards. Also fixes the host-target test build, which `just check` does not compile — auto_save is Option<bool> now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Groundwork for "think actual control panel": a control on the face is about to become widget + label + value and nothing else, so everything it currently says in a caption needs a home. UiPanelControlView::detail_aspects and UiPanelGroup::detail_aspects build that home out of the existing slot aspect vocabulary — the state section, the (scope, channel) identity, and then the backing slot's own aspects, so the panel popup is a superset of the slot popup rather than a fork of it. Adds UiSlotAspectKind::PanelState, the one kind that titles itself: "Held" / "Following" / "At default" IS the section, so there is nothing for the shared summary machinery to derive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Think actual control panel — minimize distraction, maximize clarity."
A control is now a widget, a LABEL, and a value; every caption under it
("control · Master speed", "held · was inherited · …", "authored
default", "no writer yet") is gone from the face.
None of that information is lost — it moved behind the label, which is
now the detail trigger, reusing the node face's SlotDetailButton
machinery verbatim: the whole control is the popover's outline anchor, so
opening merges the control into the aspect card and reads as diving into
it. State stays legible on the face through color alone plus the reset
glyph, whose presence is itself the "something is held here" signal.
Values render in tabular numerals in a reserved slot and faders get a
fixed footprint, because a panel row that reflows while you drag a fader
is not a control panel. That was a real bug on root-card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Amended G2 round-3 item 3. A nested module group is a light hairline box with its name embedded in the top border — a real fieldset/legend, so the notch is the browser's rather than a stack of CSS tricks, which keeps the "— PLASMA 1 —" reading Yona liked while making the group a bordered CLUSTER. ModulePanel lays the groups out in a wrapping flex row, so two effect boxes sit next to each other when the width allows: function groups on hardware, which is the whole "actual control panel" image. No collapse. UiPanelGroup::collapsed and PanelGesture::ToggleGroup are deleted rather than left unused: wrapping is the density mechanism, and a panel you have to unfold before you can play it is not a control panel. The scope path and the control tally leave the header for the label's detail popup, so the border carries only the instance name — which is why the fixtures now label the groups "plasma 1" / "plasma 2" instead of both "plasma". A test pins that two side-by-side groups stay tellable apart and that the path is still reachable. play-mode/groups-collapsed is repurposed to play-mode/groups-wrapped: the same panel at a width where the row wraps, which is now the density story worth having next to the side-by-side desktop one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reset button is h-5 while the bare label line is shorter, so a box with a held control grew taller than its unheld sibling in the same flex row. Pin the legend line box to h-5 either way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This work is unfinished and parked. It is a storybook-only UX spike —
UiNodeFace::Moduleis never produced bynode_face_builder, so none ofthis is reachable by opening a project. It passed its visual gate (G2) and
is preserved for harvesting during implementation; it is not intended
to merge as-is.
Plan: lp2025/2026-07-31-1002-modules-buses-panels
Path: ~/.photomancer/planning/lp2025/2026-07-31-1002-modules-buses-panels/plan.md
Handoff document (read first):
~/.photomancer/planning/lp2025/2026-07-31-1002-modules-buses-panels/2026-08-01-0757-handoff.mdWhat works
19 stories under
studio/module/{module-face,panel-state,playlist-panel,play-mode},57 PNGs, all green locally (
just check,just test-studio-host228,cargo test -p lpa-studio-core --lib780).provenance, with children below as sibling cards (playlist grammar).
state text lives in per-control detail popups (reuses
SlotDetailButtonUiSlotAspectKind::PanelState).<fieldset>/<legend>boxes in awrapping flex row — no collapse; legend pinned
h-5so sibling boxesstay level with or without a reset button.
(violet), engaged (amber + ↺). Reset per control and per module;
auto-save toggle upper-right.
UiNodeFace::Controlsfor leaf nodes carries the enclosing scope, soa fixture's brightness and the module panel's brightness are one control
across two cards.
Dev server:
.claude/launch.jsonin the spike worktree, port 30283.What is NOT done
SlotEditOp::SetValueat mock addresses(deliberate spike shortcut, explicitly not a proposal — panel writes get
their own wire ops per
docs/design/panel.mdP8).hover/focus-only); the group reset sits inside the
<legend>becauseright-aligning in a content-sized legend is impossible.
Next session
Do not continue this branch. Run
yona-planfor the implementationroadmap (M3) per the handoff document, then harvest these components during
the Studio milestone. CI has not been run on this branch — it was never
intended to gate.
🤖 Generated with Claude Code