Skip to content

Commit d282fdb

Browse files
committed
Extract two reusable Ark dropdown primitives (ui/Select, ui/Combobox) as the dropdown-uniformization foundation
M0 of the dropdown-uniformization effort: one clean, macOS-y dropdown architecture every in-scope picker can converge onto, so the app stops carrying several unrelated dropdown idioms. - **`lib/ui/Select.svelte`**: presentational, items-driven single-select on Ark `Select`. The house dropdown (native-`<select>` replacement). Items carry `value` + `label` + optional `description` + optional `group` (Ark `ItemGroup`/`ItemGroupLabel` bucketing for the viewer's encoding picker). Forwards `onChange`, `onHighlightChange`, `disabled`, `placeholder`, `ariaLabel`, and a content-level `contentClass` hook. Keeps the stable `.select-trigger` / `.select-item` / `.select-content` / `.option-description` class contract that `SettingSelect`'s focus-restore and the a11y-contrast checker key on. Standardized Lucide `chevron-down` with a real hit-area replaces the tiny `▼` glyph. - **`lib/ui/Combobox.svelte`**: presentational text-field-with-suggestions on Ark `Combobox` for the AI model picker. Text is driven by a controlled `inputValue` (separate from `value`), with `selectionBehavior="preserve"` + `allowCustomValue`, so it never blanks on a cold-start / mid-fetch empty list or a typed custom model name (the load-bearing finding #4 trap). Open-on-focus wired via controlled `open`; own in-field spinner overlay for `loading` (Ark has none); graceful empty-state row. - **`SettingSelect.svelte`** now renders `ui/Select`: keeps its registry reads, the `allowCustom` "Custom…" inline-number flow (incl. the load-bearing `setTimeout(0)` focus-restore), immediate-apply-on-highlight, and the `custom-highlighted` content class. The `__custom__` sentinel stays in `SettingSelect`; `ui/Select` never sees it. Behavior is unchanged for the three consumers. - Cataloged both in Debug > Components (new `SelectSection` + `ComboboxSection`, wired into both the catalog page and the debug-window sidebar), with the Combobox demoing the empty / loading / free-text states. - Tests: tier-3 axe a11y for both primitives, plus a functional Combobox test asserting the field keeps its `inputValue` with an empty list and shows non-member custom values (finding #4 guard). - Docs: both primitives in `lib/ui/DETAILS.md` + must-know lines in `lib/ui/CLAUDE.md` (stable `.select-*` contract, the Combobox value-model trap); `settings/components/CLAUDE.md` notes `SettingSelect` wraps `ui/Select`. No new IPC, no native `<select>` migrated yet (M1–M3 own those). `pnpm check` green.
1 parent 5cb5cbd commit d282fdb

14 files changed

Lines changed: 1191 additions & 317 deletions

File tree

apps/desktop/src/lib/settings/components/CLAUDE.md

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,23 @@ Parent: [`../CLAUDE.md`](../CLAUDE.md) for the registry, store, sections, and se
99

1010
## File map
1111

12-
| File | Use for |
13-
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
14-
| `SettingsSidebar.svelte` | Left-column nav: search input, top-level + nested section list, "section has matches" highlight. Declares `TOP_LEVEL_ORDER` (keep in sync with the E2E test in `settings.spec.ts`) |
15-
| `SettingsContent.svelte` | Right-pane router: maps `selectedSection` to one of the 18 `sections/*.svelte` components, or renders `SectionSummary` for the four top-level sections that have subsections (`Appearance`, `Behavior`, `File systems`, `Developer`) |
16-
| `SettingsSection.svelte` | Shared section wrapper: h2 title + slot. One per `sections/*.svelte` so the title styling lives in one place |
17-
| `SectionSummary.svelte` | Card grid shown when a top-level section with subsections is selected; each card deep-links into a subsection |
18-
| `SettingRow.svelte` | Label + description + control + reset-pip + restart-required badge wrapper. Carries `split` (see below) and `searchQuery` (for `<mark>`-style label highlighting via `highlightMatches`) |
19-
| `SettingSwitch.svelte` | Primary boolean toggle (Ark UI `Switch`) |
20-
| `SettingCheckbox.svelte` | Less prominent boolean control (Ark UI `Checkbox`). Use for secondary booleans that hang off a switch or live inside a denser layout |
21-
| `SettingSelect.svelte` | Dropdown for enum settings (Ark UI `Select`). Supports `allowCustom` from the registry, which renders an inline text input when the user picks `Custom…` |
22-
| `SettingToggleGroup.svelte` | Segmented control for short enum lists. Thin wrapper around `lib/ui/ToggleGroup` with `semantics="toggles"`. `labelOverrides` lets a button label track another reactive setting (for example, `kB``KB` on binary/SI switch) |
23-
| `SettingRadioGroup.svelte` | Vertical radio list for longer enum lists or when each option needs a `customContent` snippet (description, sublabel, preview) |
24-
| `SettingSlider.svelte` | Slider + paired number input (Ark UI `Slider` + `NumberInput`). Reads `min` / `max` / `step` / `sliderStops` from the registry constraints |
25-
| `SettingNumberInput.svelte` | Standalone number input for settings without a slider (small ranges, exact values). Clamps to registry `min`/`max` on change |
26-
| `SettingPasswordInput.svelte` | Masked text input with reveal toggle. Two modes (see § "Password-input modes" below) |
27-
| `SettingColorSwatchPicker.svelte` | Circle trigger + 4×4 swatch popover for picking a tint color. Used by the per-volume-type pane tints under `Appearance > Colors and formats` |
28-
| `swatch-keyboard.ts` | Pure arrow-key/Home/End/PageUp/PageDown index resolver for the swatch grid. Extracted from the picker so the traversal arithmetic is unit-testable without a DOM |
12+
| File | Use for |
13+
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
14+
| `SettingsSidebar.svelte` | Left-column nav: search input, top-level + nested section list, "section has matches" highlight. Declares `TOP_LEVEL_ORDER` (keep in sync with the E2E test in `settings.spec.ts`) |
15+
| `SettingsContent.svelte` | Right-pane router: maps `selectedSection` to one of the 18 `sections/*.svelte` components, or renders `SectionSummary` for the four top-level sections that have subsections (`Appearance`, `Behavior`, `File systems`, `Developer`) |
16+
| `SettingsSection.svelte` | Shared section wrapper: h2 title + slot. One per `sections/*.svelte` so the title styling lives in one place |
17+
| `SectionSummary.svelte` | Card grid shown when a top-level section with subsections is selected; each card deep-links into a subsection |
18+
| `SettingRow.svelte` | Label + description + control + reset-pip + restart-required badge wrapper. Carries `split` (see below) and `searchQuery` (for `<mark>`-style label highlighting via `highlightMatches`) |
19+
| `SettingSwitch.svelte` | Primary boolean toggle (Ark UI `Switch`) |
20+
| `SettingCheckbox.svelte` | Less prominent boolean control (Ark UI `Checkbox`). Use for secondary booleans that hang off a switch or live inside a denser layout |
21+
| `SettingSelect.svelte` | Dropdown for enum settings: a thin registry wrapper around `lib/ui/Select`. Builds the items array (incl. its `__custom__` sentinel row) and owns the `allowCustom` inline-number flow; `ui/Select` never sees `__custom__`. Supports `allowCustom` from the registry, which renders an inline text input when the user picks `Custom…` |
22+
| `SettingToggleGroup.svelte` | Segmented control for short enum lists. Thin wrapper around `lib/ui/ToggleGroup` with `semantics="toggles"`. `labelOverrides` lets a button label track another reactive setting (for example, `kB``KB` on binary/SI switch) |
23+
| `SettingRadioGroup.svelte` | Vertical radio list for longer enum lists or when each option needs a `customContent` snippet (description, sublabel, preview) |
24+
| `SettingSlider.svelte` | Slider + paired number input (Ark UI `Slider` + `NumberInput`). Reads `min` / `max` / `step` / `sliderStops` from the registry constraints |
25+
| `SettingNumberInput.svelte` | Standalone number input for settings without a slider (small ranges, exact values). Clamps to registry `min`/`max` on change |
26+
| `SettingPasswordInput.svelte` | Masked text input with reveal toggle. Two modes (see § "Password-input modes" below) |
27+
| `SettingColorSwatchPicker.svelte` | Circle trigger + 4×4 swatch popover for picking a tint color. Used by the per-volume-type pane tints under `Appearance > Colors and formats` |
28+
| `swatch-keyboard.ts` | Pure arrow-key/Home/End/PageUp/PageDown index resolver for the swatch grid. Extracted from the picker so the traversal arithmetic is unit-testable without a DOM |
2929

3030
Each `.svelte` ships with `*.a11y.test.ts` (axe-core tier-3 audit). `SettingColorSwatchPicker` and `swatch-keyboard`
3131
also have functional `*.test.ts` files.

0 commit comments

Comments
 (0)