Skip to content

feat(layout): reveal/collapse/disclosure — cycle action arm + group sugar (2de.4)#105

Merged
brandon-fryslie merged 4 commits into
mainfrom
feat/2de.4-groups-cycle-actions
Jun 12, 2026
Merged

feat(layout): reveal/collapse/disclosure — cycle action arm + group sugar (2de.4)#105
brandon-fryslie merged 4 commits into
mainfrom
feat/2de.4-groups-cycle-actions

Conversation

@brandon-fryslie

Copy link
Copy Markdown
Contributor

Closes the design+build for brandon-layout-substrate-2de.4 — the feature layer atop the container|segment + actions foundation. Two additions, zero render-engine work; everything desugars to the shipped grammar.

1. cycle action arm (the enumerated-domain stepper)

{ set: key, cycle: [v0, v1, …] } writes the successor of the current value, wrapping; a current value outside the domain counts as the first member (order members default-state-first). The gate derives as an allow-list of the members through the existing same-key merge — no validator plumbing, the vocabulary grows by arms.

{{ action }} grows a variadic display form for cycle actions: one display per member, positionally matched — {{ action "toggle" "▸ details" "▾ details" }} — or one static display. The current member's display renders; the click writes the successor. Non-cycle kinds keep display [boundValue]; wrong arity is a loud render error.

Deliberate contrast with set-bounded: a cycle emits the absolute successor computed at render (the glyph names the current state, so the click delivers exactly the transition the glyph promised — [LAW:one-source-of-truth], display and write derive from one read), where the bounded stepper emits a relative nudge (rapid clicks must accumulate).

2. group layout sugar (input-only node kind)

{ kind: "group", name, label, open?, direction?, key?, bg?, fg?, when?, children } lowers at the loader to canonical container/segment nodes and synthesizes its state var + cycle action + toggle segment under the reserved groups. namespace — one declaration, every derived artifact single-sourced ([LAW:one-source-of-truth]; a user name under the prefix is a load error). Group is never a canonical LayoutNode kind ([LAW:one-type-per-behavior] — arranging + gating are behaviors container already has).

  • Independent toggle: omit key.
  • Accordion: siblings share a key — one key holds one open name, so opening a sibling auto-closes the rest. Not a mode, a shared value ([LAW:dataflow-not-control-flow]); the gate is the union of the sibling cycles via the existing merge. No path grammar, no new action arm needed — the accordion semantics fall out of cycle's "unknown current counts as the first member" rule.
  • Nested disclosure: nested groups with distinct keys; a closed parent's when hides the subtree, child state persists invisibly. Ancestor/descendant sharing a key is a load error.

Verification

  • 32 new tests through the real spine + real daemon verb handlers (toggle round trip, accordion auto-close, nested persistence, every loader invariant), schema GOOD/BAD parity cases; full suite 1171/1171.
  • Live-path verified on the built dist with an isolated daemon: rendered closed toggles → url-handle click → ▾ + body → clicking the sibling auto-closed the first. Real OSC-8 wire URLs through the real set-state gate.
  • Schema regenerated from the loader declarations (check:schema green); check:protocol green.
  • Folds the pending CLAUDE.md merge-vs-replace doc-drift fix.

Out of scope (by design review with the user): auto-indent for nested groups → brandon-layout-polish-atn; a {{ toggle }} helper — subsumed by the variadic display form.

…e.4 slice 1)

{ set, cycle: [v0, v1, ...] } writes the SUCCESSOR of the current value,
wrapping; a current value outside the domain counts as the first member.
Gate derives as an allow-list of the members through the existing merge —
two cycles sharing a key union, which IS the accordion's writable path set.

{{ action }} grows the variadic display form: one display per cycle member
(positionally matched, current member's display renders) or one static
display; non-cycle kinds keep (display, boundValue) with arity >2 loud.

// [LAW:one-type-per-behavior] the bounded stepper's sibling: range vs
// enumerated domain — toggles, N-state cyclers, accordion paths are values
// of one arm, not new modes.
…closure (2de.4 slice 2)

{ kind: "group", name, label, open?, direction?, key?, bg?, fg?, when?,
children } is an INPUT-only node: it lowers at the loader to canonical
container/segment nodes and synthesizes its state var + cycle action +
toggle segment under the reserved groups.* namespace — one declaration,
every derived artifact single-sourced from it.

Accordion is key-sharing, not a mode: sibling groups naming one key each
toggle cycle [closed, ownName]; one key holds one open name, so opening a
sibling auto-closes the rest and the gate is the union of the sibling
cycles via the existing same-key merge. Nested disclosure nests groups
with distinct keys (a closed parent's when hides the subtree; child state
persists invisibly). An ancestor/descendant sharing a key is a load error.

// [LAW:one-type-per-behavior] group is never a canonical LayoutNode kind
// [LAW:dataflow-not-control-flow] accordion = a shared key value, no mode
…t (2de.4 slice 3)

Documents the cycle value-source arm, the variadic {{ action }} display
form, and the group sugar (independent toggle / accordion-by-shared-key /
nested disclosure). Folds the pending doc-drift fix: the architecture
recap now states the by-name merge for variables/segments/actions/helpers
and root-only wholesale replacement, matching mergeWithDefault.

@brandon-fryslie brandon-fryslie left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review

The two new features — the cycle action arm and group layout sugar — are well-structured and internally consistent. The type system correctly models both forms, the gate derivation is sound (allow-list union for accordion), the 'unknown current counts as first member' rule is applied uniformly in both display selection and write computation, and the synthesis pass enforces the documented invariants with loud errors. One concrete defect: escapeTemplateLiteral does not neutralise newline or carriage-return characters in group labels, so a label containing a literal \n (legal in JSON5 string values) produces a broken Go template string literal at synthesis time.

Comment thread src/config/loader/layout.ts
… boundary

Go-template string literals forbid literal newlines; a label containing \n
would survive escapeTemplateLiteral and produce an unparseable synthesized
template. Reject at the validator so the broken state is unrepresentable
before synthesis runs. [LAW:no-silent-failure]
@brandon-fryslie brandon-fryslie merged commit 52fc13b into main Jun 12, 2026
6 of 7 checks passed
@brandon-fryslie brandon-fryslie deleted the feat/2de.4-groups-cycle-actions branch June 12, 2026 06:36
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.

2 participants