0024 — Settings, re-segmented into vertical tabs¶
- Status: IMPLEMENTED (2026-07-16) — open questions RESOLVED: Q1 → go
granular (per-concern leaves grouped under rail headers, not three mega-tabs —
"we'll add settings, not remove them"), Q2 → yes, extend
Icon.svelte, Q3 → default to Image. See §7 for the shipped shape. First of the post-0022chrome trilogy (0024settings tabs ·0025notifications bell ·0026help panel). Build order 0024 → 0025 → 0026; each is independently shippable. - Why now: the "Pro Dark" studio (
0022) stabilised the chrome, and the global Settings modal (GlobalSettings.svelte, 518 lines) is now the most cluttered surface in it — one long scroll stacking six unrelated sections (default cull profile, object removal, eye detection, AI review, updates, shut down). A photographer looking for "where do I paste my API key" scrolls past thresholds and eraser backends to find it. Segmenting it makes each concern findable and shrinks the reading surface. - Depends on:
0022(the token system + modal styling this reuses), the existing settings API (/api/v1/settings/{profile,face,remove,review},/api/v1/version,/api/v1/update,/api/v1/shutdown— all unchanged). - Non-goals: no new settings, no API change, no behaviour change. This is
a layout re-segmentation of exactly the controls that exist today. No new
cull judgement (
0006), no provider change (0005). Per-shoot cull overrides (Settings.svelte) are out of scope — this is the global modal only.
1. What we have¶
GlobalSettings.svelte is a single scrolling .modal with six <section>s in
DOM order: Default cull profile (8 threshold sliders), Object removal
(ONNX inpaint enable + backend + runtime path), Eye detection (ONNX face
enable + runtime path), AI review (cloud provider + keychain key + model),
Updates (version + check), Shut down (danger confirm). Each section owns
its own load/save against its own endpoint; there is no shared "save all" — saves
are already per-section. That independence is what makes tabbing them safe: a tab
is just a section (or group) shown one at a time.
2. The tab rail — granular leaves under group headers (Q1 RESOLVED)¶
Review decision: the more granular the better — settings will grow, not shrink, so each concern gets its own selectable leaf rather than being lumped into a "Models & downloads" mega-tab. The rail stays legible by grouping leaves under quiet section headers; a new setting later is just a new leaf under the right group (that's the scalability the granularity buys).
A vertical rail on the left (grouped), a scrolling panel on the right — one leaf shown at a time:
| Group | Leaf | Icon | Contains (existing section, verbatim) |
|---|---|---|---|
| Judgement | Image | aperture | Default cull profile (the 8 threshold sliders) |
| Models & providers | Object removal | eraser | Inpaint eraser enable · backend · runtime path |
| Eye detection | focus | Face-model enable · runtime path | |
| AI review | ai | Cloud provider · keychain key · model | |
| System | Updates | download | Version + check for updates |
| Shut down | power | Stop-the-server danger confirm |
Six leaves, three groups; every control from today's six sections lands in exactly one leaf, none moved out of the modal. Rationale: Judgement acts on pixels (thresholds); Models & providers is one leaf per heavy backend — each local ONNX model with its own runtime-path/auto-download notes, and the cloud critic with its key — so "where does this model live / where's my key" is a direct click, and adding a future provider is a new leaf here; System is app lifecycle, updates and shutdown kept distinct (different intents).
3. Shape & structure¶
- Two-pane modal. Left: a vertical list of tab buttons (icon + label, active
state, keyboard/arrow navigable,
role="tab"); right: the active tab's panel. Reuse the0022.modal/.scrimshell,--surface/--linetokens, and the existing field styles — this should read as the same modal, re-organised. - Split the monolith, one component per leaf. Extract the shell into
GlobalSettings.svelte(owns the rail, the active-leaf$state, the group→leaf structure, the scrim/header/Esc, and nothing else); each leaf becomes its own small component —SettingsImage,SettingsObjectRemoval,SettingsEyeDetection,SettingsAIReview,SettingsUpdates,SettingsShutdown— owning only its own load/save (moving the existing code, not rewriting it). One-component-per-leaf is deliberate: it's what makes "add a setting" a purely additive change (new leaf component + one rail entry), matching the granular intent. Drops the 518-line file to a thin shell + focused leaves. - The rail structure is data. Define the groups/leaves as a small array
(
{group, leaf, id, icon, component}) the shell maps over, so adding a leaf is one array entry, not markup surgery. - Deep-link (optional, cheap). Let the parent open the modal on a chosen tab
(a prop /
openSettings(tab)), so0025's "update available" notification and0026's help links can jump straight to Update & shutdown or Models. - Responsive. On a narrow viewport the rail collapses to a horizontal strip
above the panel (the modal already caps at 540px; the rail must not force
horizontal scroll —
0022responsive rule).
4. Requirements¶
- R-SET-1 Every control present before the change is present after it, in one of the three tabs, wired to the same endpoint with the same payload. No setting added or removed.
- R-SET-2 Per-section save semantics are preserved — each save button saves only its section and shows its own "Saved ✓"; no cross-tab "save all".
- R-SET-3 Tabs are keyboard-operable (tab into the rail, arrow between tabs,
Esccloses the modal) and carryrole="tab"/aria-selectedfor a11y; both themes meet WCAG AA (0022). - R-SET-4 The parent can open the modal focused on a named tab (for
0025/0026deep-links); default is Image. - R-SET-5 No API/behaviour change — a request-diff of every save before/after is byte-identical; existing settings e2e/unit tests pass unchanged (updated only for the new selectors).
5. Testing¶
- Playwright: open Settings → assert the six grouped leaves in the rail; switch
leaves → assert the right pane renders and the others don't; save on each leaf →
assert the same request fires as today;
Esc/arrow-key navigation. - Unit: the shell renders the active leaf's component; deep-link prop selects the leaf; the rail is built from the leaf array.
- Existing
GlobalSettingsselectors in current specs updated to the tabbed DOM.
6. Open questions — RESOLVED¶
- Q1 — granularity → RESOLVED: go granular. Per-concern leaves grouped under rail headers (§2), not three mega-tabs: settings will grow, and a granular rail scales additively. (Rejected the "Models & downloads" lump in favour of one leaf per backend.)
- Q2 — icons → RESOLVED: yes. Extend
Icon.svelte(0023) with the rail glyphs (aperture,eraser,download,power—focus/aialready exist), one icon vocabulary across the studio. - Q3 — tab persistence → RESOLVED: default to Image. Deep-links (R-SET-4) cover jumping elsewhere; no last-used-tab persistence for now.
7. As built (IMPLEMENTED 2026-07-16)¶
The 518-line GlobalSettings.svelte is now a ~135-line shell: a data-driven rail
(groups → leaves, {group,leaf,id,icon,comp}) + an active-leaf $derived (a
local override wins over the openTab deep-link) rendering the current leaf's
component. Each concern is its own component under pkg/studio/web/src/settings/
— SettingsImage, SettingsObjectRemoval, SettingsEyeDetection,
SettingsAIReview, SettingsUpdates, SettingsShutdown — owning only its own
load/save against the unchanged endpoint. Six leaves under Judgement /
Models & providers / System; opens on Image; role="tablist"/tab/tabpanel,
roving tabindex, arrow-key nav, Esc to close. Icon.svelte gained aperture,
eye, download, power (reusing remove/ai); shared form styles live under
.setleaf in app.css. App.svelte gained openSettings(tab) +
openTab/settingsTab for 0025/0026 deep-links.
Verified: 24 Playwright e2e (4 new: rail leaves, one-pane-at-a-time,
per-leaf save fires the same PUT, Esc/arrow nav), 71 vitest unit, svelte-check
0 errors 0 warnings (also cleared 3 pre-existing label-a11y warnings via
for/id), eslint + prettier clean. Screenshotted live (headless) in both
themes — grouped rail, gold-accented active leaf + icons, gold checkbox. No Go
change beyond re-embedding the built bundle; no API/behaviour change (R-SET-5).