Skip to content

Redesign the Settings pane: subtabs + readable rows (#69) - #85

Merged
thalida merged 29 commits into
mainfrom
feat/issue-69-settings-subtabs
Jul 4, 2026
Merged

Redesign the Settings pane: subtabs + readable rows (#69)#85
thalida merged 29 commits into
mainfrom
feat/issue-69-settings-subtabs

Conversation

@thalida

@thalida thalida commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Closes #69

A focused redesign of the Settings pane and its surrounding chrome. Scope note: #69 as filed was "pure reorganization onto subtabs"; during design review the owner widened it to a full Settings interaction-model + IA improvement, plus sidebar-wide polish.

Settings pane → 3 tabs (World · Updates · Preview)

  • World — the 9 rendering sections. Keeps the draft + Save/Discard model (its changes trigger expensive city rebuilds, so batching behind Save is the point). The sticky footer (Save · Discard · Reset all) shows only here, and Reset all is scoped to World.
  • Updates · Preview — cheap, single-purpose settings, so they autosave (apply on change, no Save button) and render inline (no one-item accordion). Per-field reset icons apply instantly.

Control rows (layout B)

  • Labels no longer truncate; each control shows a concise one-line description inline (descriptions are optional).
  • Compact inputs sit inline (control to the right of the label, like a checkbox): toggles, colors, numbers, selects. Wide controls stack full-width: sliders, range-pairs.
  • ~128 field descriptions rewritten — one clear line each (effect/constraint/impact), implementation plumbing dropped, no em-dashes (guard test enforces it).

World accordions

  • Collapse depth capped at 2 levels (MAX_COLLAPSE_DEPTH): sections + one subgroup level collapse; deeper groups render as flat labeled clusters, so the panel never becomes a maze of nested accordions.
  • Sticky stacking headers on scroll — the section header pins to the top and an open subgroup header stacks flush below it (opaque hover, inset focus, identical at every level).

Shortcuts & Debug left Settings (they aren't settings)

  • Shortcuts (keyboard/mouse reference) → a ? icon in the header top-right opens a Shortcuts modal.
  • Debug (dev diagnostics) → a Bug icon in the header, shown only when debug mode is on (import.meta.env.DEV for just dev, a VITE_DEBUG build arg, or ?debug in the URL), opens a Debug modal.

Both modals reuse the existing SourcePicker modal pattern (signal-driven via state/stores/ui.ts, mounted from App).

Interaction model

  • Autosave is a store-level write-through flag (markAutosave) on LIVE_UPDATES + SYNTAX_THEME; the draft layer applies their writes immediately and Reset all skips them. World stores stay draft/Save.
  • Opening a modal suppresses scene keyboard shortcuts (Esc no longer clears your building selection while a modal is open).

Sidebar consistency (all panes)

  • Every sidebar pane (Explorer/Tree, Search, Info Overview + Readme, Settings) now frames its content with the same 16px inset on all four sides.
  • CSS comments no longer hardcode pixel sizes (they duplicated the design tokens and went stale).

Accessibility

  • Settings descriptions are associated to their control via aria-describedby and kept OUT of the control's accessible name (screen readers announce "Max floors", then the description separately). Toggle/color rows no longer treat the description text as part of the click target.
  • Subtab + accordion focus rings are inset so they can't be clipped by the scroll container; modals set role="dialog"/aria-modal, close on Esc/backdrop, and focus their close button on open.

Verification

  • just test (app + api) green — 2426 app tests, incl. guards for: stacked rows + inline descriptions, subtab structure + World-only footer, autosave write-through (instant apply, Reset-all skips), the accordion depth cap, the em-dash rule, both modals (open/close/Esc/backdrop), isDebugMode(), modal-open suppresses scene Esc-deselect, and description-out-of-name + aria-describedby wiring.
  • just lint clean. layoutV4 snapshot untouched.
  • Per-task reviews + two opus whole-branch integration reviews; all findings fixed. UI verified in-app by the owner.

🤖 Generated with Claude Code

thalida and others added 7 commits July 3, 2026 23:07
…descriptions (#69)

ThemeRow moves from a single-line [label | control | reset] row to a
vertical head-row + full-width-control + optional description stack
(layout B), so labels stop truncating and stacked controls share one
width. Toggle/Color fields stay inline on the head row since a
full-width control would look odd for them. TierWidthsField and
HueMapField adopt the shared ThemeRow instead of hand-rolled markup.

Drops a stale controlsPane assertion that assumed tips were
hover-only text; several field tips legitimately mention "Rebuild",
which is now visible by design.
)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-dashes (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the rewrite (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thalida thalida linked an issue Jul 4, 2026 that may be closed by this pull request
thalida and others added 20 commits July 4, 2026 01:33
…#69)

Add markAutosave/isAutosave to settingsSchema.ts. setDraft/stageReset now
write through for autosave stores instead of staging a draft;
stageResetAll/anyResettable skip them (Reset-all stays World-only). Marks
LIVE_UPDATES and SYNTAX_THEME autosave, reintroducing the write-through
behavior Phase-1 Task 3 removed, now as a named per-store flag.

Flips the SYNTAX_THEME draft tests to assert instant apply, and fixes a
pre-existing test-isolation leak in settingsDrafts.test.ts (disposable
per-test FOO/BAR stores were never unregistered, leaking non-default
values into later anyResettable() sweeps) via a new _unregisterForTests
hook.
…orld only (#69)

ControlsPane drops the Shortcuts/Debug subtabs (moving to header modals in
Tasks 8/9); Settings is now World (draftable, 9-section accordion, keeps the
Save/Discard/Reset-all footer) plus autosave Updates/Preview, each a single
section rendered inline via a new SectionNode.inline flag (no <details>
wrapper, no section reset — a one-item accordion is bad UX).

onRunCollisionCheck/onRunStemDiagnostic stay wired through ControlsPaneProps
for now; Task 9 removes them once Debug moves to its own modal.
Shortcuts is reference material, not a setting: replace the removed
Controls-pane subtab with a `?` icon in the header that opens a
self-hiding modal (dialog role, Esc/backdrop/close-button dismiss),
reusing the SourcePicker modal chrome. ShortcutsSection is now dead
and removed.
Debug's dev diagnostics (collision check, stem placement) move out of
Settings into a DebugModal opened from a header bug icon, shown only when
isDebugMode() (dev server, VITE_DEBUG, or ?debug) is on. Mirrors the
Shortcuts modal (Task 8). Drops the now-dead onRunCollisionCheck/
onRunStemDiagnostic props threaded through ControlsPane/LeftSidebar.
…ip CSS; modal focus + comment (#69)

Final integration review of the Phase-2 settings work found the scene's
document keydown handler still firing Esc-deselect/R/F underneath open
modals; the shortcuts-chip CSS stranded in ControlsPane.css after the
shortcuts list moved into its own modal; a stale comment claiming
HAS_ANY_NON_DEFAULT drives Reset-all's enabled state (it's anyResettable()
now); and both new modals not moving focus in on open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aria-describedby (#69, #79)

A wrapping <label> around head + control + description made every control's
accessible name "label + full description" and let clicking the description
text activate inline toggle/color controls. ThemeRow now wraps only head +
control in the <label>; the description is a sibling, linked back to the
control via aria-describedby (id generated per-field by Field/TierWidthsField/
HueMapField via useId).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…; drop two redundant hints (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… Updates tab (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ve the footer) (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Depth-driven collapsibility: sections + one subgroup level collapse; deeper
groups render as flat labeled clusters. Fixes a .map(renderChild) call that
passed the array index as the depth arg, flattening every group past the 3rd. (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single wrapper seeds the nesting depth (default level 2) and every path uses it,
so no call site passes a literal depth and nothing maps renderChild directly. (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ch 12→16) (#69)

Info Overview, Tree, Search, Readme, and Settings now share the same horizontal
content inset so the panes line up when switching tabs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Section header pins at the scroll-body top; an open subgroup header stacks just
below it (offset via --cc-controls-header-h). Opaque backgrounds + z-index so
scrolled rows don't bleed through. (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…very pane

Overview, Readme, Tree, and Search now frame their content with the same 16px
inset as Settings (pane-body--padded) on every side, not just the left. (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-list)

.tree-list's padding shorthand (later, equal specificity) was zeroing the
.tree-root top/bottom padding; move the vertical inset onto the two-class
.tree-root.tree-list rule so it wins. Tree content now insets 16px on all sides. (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… top/bottom) (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Scroll body drops its top padding (the first child carries the 16px inset now)
  so a sticky header pins at the true top and rows stop peeking above it.
- Zero the row--bleed margin-bottom on sticky summaries so stacked headers sit
  flush, and tighten the stack offset. (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… at every level

Sticky headers no longer go translucent on hover (scrolled rows were showing
through the nested one); hover is the same tinted gray composited opaquely. Focus
ring is inset so it's fully visible instead of clipped to the bottom edge. (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

// A modal (shortcuts/debug/source picker) owns keyboard input while open —
// don't let scene shortcuts (Esc-deselect, R, F) fire underneath it.
if (document.querySelector('[role="dialog"][aria-modal="true"]')) return;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Maybe there could be some global scene state value that this script uses? This feels pretty brittle.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed — the DOM query was brittle. Replaced it with a computed MODAL_OPEN signal in state/stores/ui.ts (SOURCE_PICKER.visible || SHORTCUTS_OPEN || DEBUG_OPEN); the handler now reads MODAL_OPEN.value. city/ already imports @/state freely, so this is idiomatic. Test updated to drive it via the signal (openShortcuts/closeShortcuts) instead of an injected DOM node. — 42ed71f

describedBy={descId}
onCommit={(v) => commit({ ...map, [k]: v })}
/>
<span class="theme-hue-preview" style={{ background: `hsl(${value}, 80%, 55%)` }} />

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

What's this harcoded preview value? why is it different from the file tag color value? Fix all of these please.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch. The swatch hardcoded hsl(hue, 80%, 55%) — unrelated to how files actually render. Files show the ExtensionBadge color hsl(hue, 60%, 35%), so the preview now uses a shared fileTagHsl() helper (with FILE_TAG_SATURATION/FILE_TAG_LIGHTNESS constants in utils/colors) and Badge.tsx derives its luminance-check values from the same constants — one source now. — 42ed71f

Replaces the brittle document.querySelector('[role=dialog]') check with a
computed MODAL_OPEN signal in the ui store (source picker / shortcuts / debug).
Review feedback. (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-extension preview swatch hardcoded hsl(hue, 80%, 55%), different from the
color files actually show. Now uses fileTagHsl() (hue, 60%, 35% — the same as the
ExtensionBadge), sourced from one shared constant. Review feedback. (#69)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thalida
thalida merged commit 619949e into main Jul 4, 2026
1 check passed
@thalida
thalida deleted the feat/issue-69-settings-subtabs branch July 4, 2026 20:19
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.

Reorganize Settings pane onto PaneTabs subtabs

1 participant