Skip to content

Settings: rows read like macOS System Settings rows - #201

Open
T0mSIlver wants to merge 1 commit into
settings-sidebar-chromefrom
settings-row-idiom
Open

Settings: rows read like macOS System Settings rows#201
T0mSIlver wants to merge 1 commit into
settings-sidebar-chromefrom
settings-row-idiom

Conversation

@T0mSIlver

@T0mSIlver T0mSIlver commented Jul 27, 2026

Copy link
Copy Markdown
Owner

What

Stacked on #199 — this PR targets settings-sidebar-chrome, not main.
Do not delete #199's branch before this one is retargeted; GitHub auto-closes
children when a base branch disappears. Review #199 first.

Presentation-only rewrite of the shared row/group primitives in
SettingsView.swift. Same controls, same bindings, same behavior — rendered in
the row idiom the rest of macOS uses: label leading, control trailing,
explanation on its own full-width line underneath.

The old row put the label in a fixed 128pt column with everything else to its
right. Long labels ("Use Claude Code project files as polish context") wrapped
inside that column while short ones ("Enable") left a gutter, and every help
sentence started at the column's edge — so each card had a second, ragged left
margin. Now the label takes the leftover width (layoutPriority(0), so controls
keep their intrinsic size) and the explanation is a row of its own, aligned to
the label.

Also:

  • Page-level .toggleStyle(.switch), so 12 call sites stop repeating it.
  • SettingsGroup: corner radius 18 → 8, .quinary fill with a .quaternary
    border, .headline header (down from 17pt semibold), and a Divider() between
    rows.
  • The last divider is removed geometrically — the row container is laid out
    1pt short and clipped, so the final divider falls outside the clip. The
    alternative was teaching the card to enumerate its children, which are
    heterogeneous and arrive wrapped in Group/if branches.

Deviations from the brief (with reasons)

  1. Help text became a help: parameter — the one call-site change. The brief
    asked for no call-site changes, but most call sites nest the help text inside
    their own VStack, and a row cannot pull a nested view out of its control
    column — which is exactly where this text must not be. The zero-call-site
    alternative is hoisting the string with a PreferenceKey, which puts every
    row's layout one frame behind its content and fights Swift 6's @Sendable
    onPreferenceChange (the closure would capture a non-Sendable generic
    View). The migration is mechanical, and it shrinks the call sites — the
    wrapper VStacks disappear.
  2. No page-level .controlSize(.mini). Applied at page level it shrinks every
    button, picker, and text field on the page, not just switches — and macOS
    System Settings uses regular-size switches. Says so here rather than shipping
    a pane of miniature buttons; happy to reverse if the owner wants the smaller
    switch specifically.
  3. Added layout: .stacked for composite rows (see below). Not in the brief,
    but it is the fix for the exact rows the brief asked me to hand-check.
  4. Row label/control alignment stays .top rather than centering: several rows
    have tall controls, and a centered short label beside those reads as
    misaligned.

The composite rows the brief flagged — what I found and did

Hand-check here = careful code reading plus a clean build; none of it has been
seen on screen (see below).

Row Under trailing alignment What I did
ShortcutRecorderField + Reset/Clear button (:578-632) Fine — fixed 132pt field + button is a bounded, intrinsically-sized HStack. The SettingsInlineMessage ("Not set. Record one to enable.") now sits under the control, trailing-aligned, instead of under the label. Left as-is; validation text belongs next to the control it validates. Flagged for the owner's eye.
Sliders (:558, :651) Broken. A Slider reports no intrinsic width, so in a trailing column it collapses to nothing. Both get an explicit SettingsLayout.sliderWidth (190pt) track.
ManagedBackendStatusRow (:386) Fine — dot + optional progress + short caption. Unchanged.
ClaudePluginSettingsRow Broken. Two buttons + spinner + result line ≈ 400pt of control; the label would be squeezed to a wrapped stub. layout: .stacked.
ClaudeRemoteHostsSettingsRow Broken, worse — host rows (3 buttons each) plus an enrollment form of two text fields and a button. layout: .stacked.
SettingsFileNotes ("Included files") Full-width list of file names, not a control. layout: .stacked.
PermissionRowsView Shared verbatim with the onboarding wizard, and the card no longer supplies inner padding. Wrapped in SettingsGroupRow at the call site rather than modified — onboarding is untouched.
SettingsAvailabilityCard Same padding issue. Renders inside SettingsGroupRow; radius 16 → 8.

Every consent-critical help string (the polish-context toggles, the non-local
endpoint toggle) is byte-identical, and the rationale comments that explain each
one moved with them.

Proof

  • Unit suite green — ./scripts/remote-build.sh
Test Suite 'localvoxtralPackageTests.xctest' passed at 2026-07-28 00:17:59.858.
	 Executed 2021 tests, with 2 tests skipped and 0 failures (0 unexpected) in 71.535 (71.637) seconds

No new warnings (.build/last-remote.log has only the pre-existing
ClaudePluginInstallService.swift:226 and ClaudeHookPublisherTests ones).

CI build-test on this branch: pass (4m38s). #199 picked up review hardening
after this branch was cut (da5d653, drill scripts only — no app code), so this
branch is one commit behind its base; nothing in it touches the same files.

  • AX drill: the drill's needles are group titles ("Permissions", "Polishing",
    "Start dictation with", …) and field labels ("Endpoint", "API key"), all
    unchanged by this PR, and Settings: hand-rolled sidebar chrome in place of the TabView #199's scoped selectors are unaffected. The drill
    could not run tonight — see Settings: hand-rolled sidebar chrome in place of the TabView #199's Proof for the two runs and why (an existing
    app instance on the runner's GUI session would not quit; the guard was not
    weakened).
  • LLM lanes: not run, and not required — SwiftUI layout of existing controls.
    Nothing here changes prompts, model pins, sampling, or anything that reaches
    the model; scripts/ci/llm-lane-filter.sh matches nothing in the diff.

UI change — what was verified, and what was not

Verified: clean build under Swift 6.2 strict concurrency, full unit suite, and
the row-by-row reading in the table above.

Not verified — needs the owner's eyes (./scripts/try-pr.sh <this PR>), in
priority order:

  1. The Text Processing pane, which has the longest labels and the two stacked
    composite rows: do the toggles line up on the right, and does the explanation
    under each one read as belonging to it?
  2. The Dictation pane's two shortcut rows and the 190pt sliders — the two
    spots where I changed geometry on a guess.
  3. That the group cards show a divider between every row and no stray line
    above the bottom edge (the 1pt clip trick).
  4. That .quinary on .windowBackgroundColor is still visible as a card in both
    light and dark appearance.

Presentation only — the same controls, bound to the same state, in the row
idiom the rest of the system uses: label leading, control trailing,
explanation on its own full-width line underneath.

The old row put the label in a fixed 128pt column and everything else to
its right. Long labels ("Use Claude Code project files as polish context")
wrapped inside that column while short ones left a gutter, and each help
sentence started at the column's edge, so every card had a second, ragged
left margin. Now the label takes the leftover width and the explanation is
a row of its own, aligned to the label.

Help text becomes a `help:` parameter instead of a view inside the control
closure. This is the one call-site change, and it is forced: most call
sites nest the text inside their own VStack, and a row cannot pull a view
out of its control column — which is precisely where this text must not
be. Hoisting it with a PreferenceKey instead would put every row's layout
a frame behind its content and fight Swift 6's @sendable
onPreferenceChange, to keep a diff that is mechanical either way.

Composite rows (plugin button bar, enrolled SSH hosts, config file list)
opt into `layout: .stacked` — beside a 400pt-wide control the label would
be squeezed into a wrapped stub. Sliders get an explicit width because a
Slider reports no intrinsic one and would collapse in a trailing column.

Groups: radius 18 -> 8, .quinary fill with a .quaternary border, .headline
header, and a divider between rows. The last divider is removed
geometrically (container 1pt short, clipped) rather than by teaching the
card to enumerate heterogeneous children.
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