Skip to main content

max / audiofiles

Bring the design-system and architecture docs onto the intent vocabulary The colour-token table still listed bg_primary, fg_muted and accent_blue, which have been surface_page, content_muted and action since the intent names landed. A reader hits that table before anything else, so it was teaching the wrong vocabulary in the one document that claims authority over the code. Names, kinds and derivations now match theme.rs: 14 base slots, the two genuine derivations separated from the two aliases that were filed as derived, and trim_mute_overlay listed at all.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-27 13:51 UTC
Signed with PGP, not checked
Commit: 58ac453f87354a39218274ea568bf620760d8610
Parent: ce6f6aa
2 files changed, +38 insertions, -31 deletions
@@ -110,7 +110,9 @@
110 110
111 111 The browser supports multiple color themes. Themes are TOML files declaring colors by intent, with `[meta]`, `[surface]`, `[content]`, `[action]`, `[status]`, `[line]`, and `[category]` sections; interactive states are derived perceptually rather than authored. Bundled themes come from the shared `makeover` crate, embedded at compile time; users can add custom themes to `<config>/audiofiles/themes/` which override bundled themes by ID.
112 112
113 - The active theme is stored in a global `RwLock<ThemeColors>`. Public accessor functions (e.g., `bg_primary()`, `text_secondary()`, `accent_blue()`) read through the lock. Derived colors (row stripes, selection highlights, hover states) are computed by linear interpolation between base palette slots.
113 + The active theme is stored in a global `RwLock<ThemeColors>`. Public accessor functions (e.g., `surface_page()`, `content_secondary()`, `action()`) read through the lock and are named for intent, matching the TOML sections. Derived colors (row stripes, selection highlights, the trim wash) are blended in OKLab once per theme install and cached alongside the palette, rather than recomputed per read.
114 +
115 + Which theme is active is two questions. `theme::chosen()` is the user's standing selection, `Follow` or `Fixed(id)`; `theme::active_id()` is what it resolves to against the desktop's current light/dark appearance. Only the first is stored, under `ConfigKey::Theme`, as `"system"` or an id.
114 116
115 117 Sample classification colors are hardcoded (not theme-driven) so that semantic categories maintain consistent visual identity across themes.
116 118
@@ -20,7 +20,7 @@
20 20
21 21 1. **No `Color32::from_rgb(...)` outside `theme.rs`.** Test by `grep -rE 'Color32::(from_rgb|WHITE|BLACK|GRAY|RED|GREEN|BLUE|YELLOW|DARK_GRAY)' src/ui/ | grep -v theme.rs`, must be empty.
22 22 2. **No raw `add_space(N.0)` outside `widgets.rs` and `theme.rs`**: use named spacing tokens (§Spacing).
23 - 3. **No inline `selectable_label(active, RichText::new(label).strong().color(accent_blue()))`**: use `widgets::selectable_row` or one of its specializations.
23 + 3. **No inline `selectable_label(active, RichText::new(label).strong().color(action()))`**: use `widgets::selectable_row` or one of its specializations.
24 24 4. **No inline `Window::new(...).collapsible(false).resizable(false).anchor(CENTER_CENTER, [0,0])`**: use `widgets::modal_window` or `widgets::confirm_modal`.
25 25 5. **No emoji or checkmark glyphs in user-facing strings** (CLAUDE.md brand rule). Use words. Test by `grep -rE '\\u\{1F[0-9A-Fa-f]{3}\}|\\u\{2[0-9A-Fa-f]{3}\}'` against UI files: flagged glyphs require a documented exception comment.
26 26
@@ -30,30 +30,35 @@
30 30
31 31 ### Colour tokens
32 32
33 - All UI colour reads must go through these accessors. The 15 base slots come from `ThemeColors`; the derived slots are computed at runtime.
33 + All UI colour reads must go through these accessors. Every name is an intent, not
34 + an appearance: `action` says what the colour is for, `accent_blue` said what it
35 + looked like on one theme and lied on the next. The 14 base slots come from
36 + `ThemeColors`, parsed from the theme TOML. Derived slots are blended once per
37 + theme install (`DerivedColors`) rather than per read.
34 38
35 - | Token | Kind | Use |
36 - |--------------------|----------|-----------------------------------------------------------------------|
37 - | `bg_primary` | base | Deepest layer (table body, scroll background). |
38 - | `bg_secondary` | base | Panels, sidebars, window fill. |
39 - | `bg_tertiary` | base | Hover background, highlighted regions. |
40 - | `bg_surface` | base | Cards, popovers, tag chips. |
41 - | `fg_primary` | base | Primary text. |
42 - | `fg_secondary` | base | Secondary text, labels. |
43 - | `fg_muted` | base | Placeholders, disabled items, empty-state hints. |
44 - | `accent_blue` | base | Selection/active state. *The* affordance colour. |
45 - | `accent_red` | base | Errors, danger buttons, destructive confirms. |
46 - | `accent_green` | base | Success, ready state. |
47 - | `accent_yellow` | base | Warnings (not yet used as such, reserved). |
48 - | `accent_purple` | base | Reserved (classification palette). |
49 - | `accent_cyan` | base | Reserved (classification palette). |
50 - | `border_default` | base | Separator, panel boundary. |
51 - | `bg_row_even` | derived | Striped row alternation (even). |
52 - | `bg_row_odd` | derived | Striped row alternation (odd) = `bg_primary`. |
53 - | `bg_hover` | derived | = `bg_tertiary`. Shared row/button hover. |
54 - | `bg_selected` | derived | Selection fill (= `lerp(bg_primary, accent_blue, 0.3)`). |
55 - | `classification_color(c)` | domain | Sample-class palette. Stable across themes for muscle memory. |
56 - | `piano_white_key()`, `piano_black_key()` | domain | Instrument panel only. |
39 + | Token | Kind | Use |
40 + |-----------------------|----------|--------------------------------------------------------------------|
41 + | `surface_page()` | base | Deepest layer (table body, scroll background). |
42 + | `surface_overlay()` | base | Panels, sidebars, window fill. |
43 + | `surface_sunken()` | base | Hover background, highlighted and recessed regions. |
44 + | `surface_raised()` | base | Cards, popovers, tag chips. |
45 + | `content()` | base | Primary text. |
46 + | `content_secondary()` | base | Secondary text, labels. |
47 + | `content_muted()` | base | Placeholders, disabled items, empty-state hints. |
48 + | `action()` | base | Selection/active state. *The* affordance colour. |
49 + | `danger()` | base | Errors, danger buttons, destructive confirms. |
50 + | `success()` | base | Success, ready state. |
51 + | `warning()` | base | Warnings. |
52 + | `category_five()` | base | Classification palette only. |
53 + | `category_six()` | base | Classification palette only. |
54 + | `border()` | base | Separator, panel boundary. |
55 + | `row_even()` | derived | Striped row alternation (even) = `lerp(page, overlay, 0.3)`. |
56 + | `row_odd()` | alias | Striped row alternation (odd) = `surface_page`. |
57 + | `hover_surface()` | alias | = `surface_sunken`. Shared row/button hover. |
58 + | `selection()` | derived | Selection fill = `lerp(page, action, 0.3)`. |
59 + | `trim_mute_overlay()` | derived | Edit panel's trimmed-region wash; dims toward the theme's own page colour. |
60 + | `classification_color(c)` | domain | Sample-class palette. Stable across themes for muscle memory. |
61 + | `piano_white_key()`, `piano_black_key()` | domain | Instrument panel only. |
57 62
58 63 ### Theme selection
59 64
@@ -100,7 +105,7 @@
100 105 | `rounding` | 4.0 (TOML) | All widget corner radii. |
101 106 | `border_thin` | 0.5 px | Separators, inactive widget border. |
102 107 | `border_default` | 1.0 px | Hovered/active widget border, window stroke. |
103 - | `focus_ring` | 1.5 px `accent_blue` | New token: focus outline on text fields. |
108 + | `focus_ring` | 1.5 px `action` | New token: focus outline on text fields. |
104 109
105 110 ---
106 111
@@ -110,12 +115,12 @@
110 115
111 116 ### Rows and selection
112 117
113 - - **`selectable_row(ui, active, label) -> Response`**: single source of truth for every "list item that highlights when active in `accent_blue`." Replaces sidebar VFS rows, collection rows, breadcrumb segments, tag tree leaves, sort headers. Internally: builds RichText with `.strong()` + `accent_blue()` when active; `text_secondary()` otherwise; delegates to `ui.selectable_label(active, …)`.
118 + - **`selectable_row(ui, active, label) -> Response`**: single source of truth for every "list item that highlights when active in `action`." Replaces sidebar VFS rows, collection rows, breadcrumb segments, tag tree leaves, sort headers. Internally: builds RichText with `.strong()` + `action()` when active; `content_secondary()` otherwise; delegates to `ui.selectable_label(active, …)`.
114 119 - **`tree_node(ui, label, active, has_active_descendant, children)`**: for the sidebar tag tree; collapsing header variant of `selectable_row`.
115 120
116 121 ### Headers and section structure
117 122
118 - - **`section_header(ui, "Vaults")`**: `.strong()`, `text_secondary()`, followed by `ui.separator()` and `space::bound()`.
123 + - **`section_header(ui, "Vaults")`**: `.strong()`, `content_secondary()`, followed by `ui.separator()` and `space::bound()`.
119 124 - **`subsection_label(ui, "Save as Collection")`**: same colour, no separator. For sub-blocks inside an already-headed section.
120 125 - **`filter_section(ui, label, active, |ui| { … })`**: `CollapsingHeader` with `"* "` marker when active and `default_open(active)`. Replaces the 5-way duplication in `filter_panel.rs`.
121 126
@@ -123,8 +128,8 @@
123 128
124 129 - **`primary_button(ui, label) -> Response`**: bold weight, default fill. Singular per modal/dialog.
125 130 - **`secondary_button(ui, label) -> Response`**: current default button. Cancel and peer actions use this.
126 - - **`danger_button(ui, label) -> Response`**: `accent_red` text or fill (TBD during impl). For Delete / Purge / Discard.
127 - - **`toolbar_toggle(ui, glyph_or_word, active, tooltip) -> bool`**: used six times in `toolbar.rs:138–204`. Active state colours via `accent_blue` / `text_muted`. Replace the emoji glyphs with words during the rollout (see brand rule).
131 + - **`danger_button(ui, label) -> Response`**: `danger` text or fill (TBD during impl). For Delete / Purge / Discard.
132 + - **`toolbar_toggle(ui, glyph_or_word, active, tooltip) -> bool`**: used six times in `toolbar.rs:138–204`. Active state colours via `action` / `content_muted`. Replace the emoji glyphs with words during the rollout (see brand rule).
128 133 - **`icon_button(ui, glyph_or_word, tooltip) -> Response`**: small, non-toggle. (`x`, `+`, settings gear once renamed.)
129 134
130 135 ### Pills, chips, badges
@@ -151,7 +156,7 @@
151 156
152 157 ### Banners and notifications
153 158
154 - - **`info_banner(ui, body)`**: frame with `bg_tertiary()`, `corner_radius(4)`, `space::group()` inset; used by the existing VFS first-run banner and any future inline tips.
159 + - **`info_banner(ui, body)`**: frame with `surface_sunken()`, `corner_radius(4)`, `space::group()` inset; used by the existing VFS first-run banner and any future inline tips.
155 160 - **`toast(ctx, severity, body)`**: *new* primitive. Timed transient notification surfaced from a state-owned queue. Replaces the current "set `state.status = '...'`" pattern for errors and ephemeral confirmations (rename success, copy-to-clipboard). The existing footer status label is retained for *persistent* state ("Sync: 3 pending") but should no longer be the channel for transient error feedback.
156 161 - **`loading_spinner(ui)` / `busy_indicator(ui, label)`**: *new* primitive for in-flight operations that aren't full-screen (sidebar refresh, sync running). Import has its own progress screen and is out of scope here.
157 162