Skip to content

spike: module face, panel, and play mode UX (M2) - #248

Draft
Yona-Appletree wants to merge 16 commits into
mainfrom
claude/module-panel-ux-spike
Draft

spike: module face, panel, and play mode UX (M2)#248
Yona-Appletree wants to merge 16 commits into
mainfrom
claude/module-panel-ux-spike

Conversation

@Yona-Appletree

Copy link
Copy Markdown
Member

This work is unfinished and parked. It is a storybook-only UX spike —
UiNodeFace::Module is never produced by node_face_builder, so none of
this 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.md

What works

19 stories under studio/module/{module-face,panel-state,playlist-panel,play-mode},
57 PNGs, all green locally (just check, just test-studio-host 228,
cargo test -p lpa-studio-core --lib 780).

  • Module face at every depth: output hero → panel → wiring drawer →
    provenance, with children below as sibling cards (playlist grammar).
  • Panel controls are widget + label + value only; all provenance and
    state text lives in per-control detail popups (reuses SlotDetailButton
    • the contiguous-outline pattern; adds UiSlotAspectKind::PanelState).
  • Nested module groups are light <fieldset>/<legend> boxes in a
    wrapping flex row — no collapse; legend pinned h-5 so sibling boxes
    stay level with or without a reset button.
  • Three control states visibly distinct: at-default, following
    (violet), engaged (amber + ↺). Reset per control and per module;
    auto-save toggle upper-right.
  • Play mode: root panel only, desktop / mobile / wrapped.
  • UiNodeFace::Controls for leaf nodes carries the enclosing scope, so
    a fixture's brightness and the module panel's brightness are one control
    across two cards.

Dev server: .claude/launch.json in the spike worktree, port 30283.

What is NOT done

  • No engine or DTO derivation — mock fixtures only.
  • The real sidebar bus pane is mock-removed, not deleted.
  • Knob drags still dispatch SlotEditOp::SetValue at mock addresses
    (deliberate spike shortcut, explicitly not a proposal — panel writes get
    their own wire ops per docs/design/panel.md P8).
  • Carried UX items: the ⓘ glyph repeats on every control label (candidate:
    hover/focus-only); the group reset sits inside the <legend> because
    right-aligning in a content-sized legend is impossible.

Next session

Do not continue this branch. Run yona-plan for the implementation
roadmap (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

Yona-Appletree and others added 16 commits July 31, 2026 14:56
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>
@Yona-Appletree Yona-Appletree changed the title [HANDOFF] spike: module face, panel, and play mode UX (M2) spike: module face, panel, and play mode UX (M2) Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant