Skip to main content

max / audiofiles

12.7 KB · 163 lines History Blame Raw
1 # audiofiles Design System
2
3 **Status:** Charter (phase-0 deliverable, 2026-05-19). Defines the canonical primitive set for the egui UI. Implementations follow during consolidation against the internal UX audit's divergence map.
4
5 **Scope:** `crates/audiofiles-browser/src/ui/`. Native egui dispatch only — this app has no web layer.
6
7 **Authority:** When this document and an inline implementation disagree, this document wins. New UI code must use a primitive from this charter; if no primitive fits, add one here first and then implement it.
8
9 ---
10
11 ## Layering rules
12
13 | Layer | File | Owns |
14 |------------------|---------------------------------------|----------------------------------------------------------------------|
15 | Palette + visuals | `ui/theme.rs` | `ThemeColors`, `apply_theme`, all `Color32` and spacing tokens, theme TOML loading. **No widgets here.** |
16 | Widgets | `ui/widgets.rs` | Every shared visual recipe (rows, headers, buttons, modals, banners, pills, empty states). |
17 | Panels / screens | `ui/sidebar.rs`, `toolbar.rs`, etc. | Composition only — call `widgets::*` and `theme::*`; never reinvent. |
18
19 **Hard rules:**
20
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 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.
24 4. **No inline `Window::new(...).collapsible(false).resizable(false).anchor(CENTER_CENTER, [0,0])`** — use `widgets::modal_window` or `widgets::confirm_modal`.
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
27 ---
28
29 ## Tokens (`theme.rs`)
30
31 ### Colour tokens
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.
34
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. |
57
58 ### Spacing tokens
59
60 Replace every literal `add_space(N.0)` with one of these. New code may not introduce new spacing magnitudes without adding a token here.
61
62 | Name | Value | Use |
63 |--------------------|-------|--------------------------------------------------------------------|
64 | `space::xs` | 2.0 | Tight inline (button-icon gap, tag chip internal). |
65 | `space::sm` | 4.0 | After a label, before its control (52 occurrences today). |
66 | `space::md` | 8.0 | Default between unrelated controls in a row (58 today). |
67 | `space::lg` | 12.0 | Between minor sections within a panel (19 today). |
68 | `space::section` | 16.0 | Between major sections (= existing `section_spacing` token). |
69 | `space::xl` | 20.0 | Reserved for headline padding in empty states. |
70
71 The existing TOML keys `section_spacing`, `grid_row_spacing`, `item_spacing_x/y`, `button_padding_x/y`, `rounding` remain; the new `space::*` constants live in `theme.rs` as `pub const` and are theme-independent.
72
73 ### Stroke / radius
74
75 | Token | Value (default) | Use |
76 |--------------------|-----------------|--------------------------------------------------|
77 | `rounding` | 4.0 (TOML) | All widget corner radii. |
78 | `border_thin` | 0.5 px | Separators, inactive widget border. |
79 | `border_default` | 1.0 px | Hovered/active widget border, window stroke. |
80 | `focus_ring` | 1.5 px `accent_blue` | New token — focus outline on text fields. |
81
82 ---
83
84 ## Widgets (`widgets.rs`)
85
86 The canonical helper for every recurring UI pattern in the codebase. Names are normative — implementations rename existing inline copies to match.
87
88 ### Rows and selection
89
90 - **`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, …)`.
91 - **`tree_node(ui, label, active, has_active_descendant, children)`** — for the sidebar tag tree; collapsing header variant of `selectable_row`.
92
93 ### Headers and section structure
94
95 - **`section_header(ui, "Vaults")`**`.strong()`, `text_secondary()`, followed by `ui.separator()` and `space::sm`.
96 - **`subsection_label(ui, "Save as Collection")`** — same colour, no separator. For sub-blocks inside an already-headed section.
97 - **`filter_section(ui, label, active, |ui| { … })`**`CollapsingHeader` with `"* "` marker when active and `default_open(active)`. Replaces the 5-way duplication in `filter_panel.rs`.
98
99 ### Buttons
100
101 - **`primary_button(ui, label) -> Response`** — bold weight, default fill. Singular per modal/dialog.
102 - **`secondary_button(ui, label) -> Response`** — current default button. Cancel and peer actions use this.
103 - **`danger_button(ui, label) -> Response`**`accent_red` text or fill (TBD during impl). For Delete / Purge / Discard.
104 - **`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).
105 - **`icon_button(ui, glyph_or_word, tooltip) -> Response`** — small, non-toggle. (`x`, `+`, settings gear once renamed.)
106
107 ### Pills, chips, badges
108
109 - **`tag_chip(ui, tag) -> Response`** — existing.
110 - **`tag_chip_removable(ui, tag) -> bool`** — existing.
111 - **`classification_badge(ui, class)`** — existing.
112 - **`toggle_pills(ui, current, options: &[(value, label, tooltip)]) -> Option<value>`** — segmented control for mutually-exclusive choices: Folder/All, Exact/Compatible, Add/Remove tag, help tabs.
113
114 ### Inputs
115
116 - **`inline_text_submit(ui, buf, opts) -> SubmitOutcome`** — text field that commits on Enter, cancels on button/Escape. `SubmitOutcome::{None, Submitted(String), Cancelled}`. Used by sidebar rename, sidebar create, detail tag add, toolbar save-collection.
117 - **`search_field(ui, buf, hint, on_change)`** — search-box recipe used by the toolbar and the sidebar tag filter. Hint text, optional leading glyph (subject to brand rule).
118
119 ### Empty states
120
121 - **`empty_state(ui, EmptyState { icon: Option<&str>, heading: &str, body: Option<&str>, cta: Option<(&str, Response sink)> })`** — centered, vertical, consistent spacing. Replaces the six divergent empty states in `file_list.rs`, `detail.rs`, and `sidebar.rs`. (Per brand rule, `icon` defaults to a word like "Empty" rather than an emoji.)
122
123 ### Modals and overlays
124
125 - **`modal_window(ctx, title, |ui| { … })`** — pre-configured `Window::new(title).collapsible(false).resizable(false).anchor(CENTER_CENTER, [0,0])` with consistent inner padding. All bulk modals, help overlay, and warning overlays compose this.
126 - **`confirm_modal(ctx, prompt, danger: bool, on_confirm, on_cancel)`** — unified destructive-confirm scaffold. Confirm label is `danger_button` when `danger=true`. Replaces both `draw_confirm_dialog` and `draw_unsafe_warning`'s ad-hoc Window.
127 - **`name_modal(...)`** — existing; keep, lift to `widgets.rs` so callers outside `overlays.rs` can use it.
128
129 ### Banners and notifications
130
131 - **`info_banner(ui, body)`** — frame with `bg_tertiary()`, `corner_radius(4)`, `space::md` inset; used by the existing VFS first-run banner and any future inline tips.
132 - **`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.
133 - **`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.
134
135 ### Forms and grids
136
137 - **`metadata_grid(ui, id, rows: impl Iterator<Item=(label, value)>)`** — the detail-panel `Grid` recipe; reusable for any read-only "key / value" block. `grid_row_spacing` token applies.
138 - **`preview_grid(ui, id, headers, rows)`** — striped two-column grid used by bulk-rename preview; could be reused by export dry-run.
139
140 ---
141
142 ## Panel shapes
143
144 These are de-facto today; the charter codifies them so the surface audits (Phase 1+) can rely on them.
145
146 | Shape | Egui type | Conventions |
147 |----------------------|----------------------------|-----------------------------------------------------------------------|
148 | Left sidebar | `SidePanel::left` | Sections separated by `section_header` + `space::lg` between groups. |
149 | Top toolbar | `TopBottomPanel::top` | Two rows max (breadcrumb + search). Right-aligned actions via `Layout::right_to_left`. |
150 | Right detail | `SidePanel::right` | Sections paced by `space::section`. Waveform always first. |
151 | Central | `CentralPanel::default` | Either the file list table or a full-screen wizard (import/export). |
152 | Bottom footer | `TopBottomPanel::bottom` | One row. Transport on the left, status text on the right. |
153 | Modal | `modal_window` widget | Centered, fixed-size unless explicitly resizable (bulk rename only). |
154 | Inline banner | `info_banner` widget | Inside another panel; for one-time tips or unobtrusive state. |
155
156 ---
157
158 ## Migration phasing
159
160 This charter is the target. Migration into ordered batches with success criteria is tracked in the internal remediation plan; surface audits do not begin until consolidation lands.
161
162 In short: build the missing widgets first (§Widgets), then rewrite panels to call them, then audit the user-facing surfaces.
163