# audiofiles Design System **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. **Scope:** `crates/audiofiles-browser/src/ui/`. Native egui dispatch only — this app has no web layer. **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. --- ## Layering rules | Layer | File | Owns | |------------------|---------------------------------------|----------------------------------------------------------------------| | Palette + visuals | `ui/theme.rs` | `ThemeColors`, `apply_theme`, all `Color32` and spacing tokens, theme TOML loading. **No widgets here.** | | Widgets | `ui/widgets.rs` | Every shared visual recipe (rows, headers, buttons, modals, banners, pills, empty states). | | Panels / screens | `ui/sidebar.rs`, `toolbar.rs`, etc. | Composition only — call `widgets::*` and `theme::*`; never reinvent. | **Hard rules:** 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. 2. **No raw `add_space(N.0)` outside `widgets.rs` and `theme.rs`** — use named spacing tokens (§Spacing). 3. **No inline `selectable_label(active, RichText::new(label).strong().color(accent_blue()))`** — use `widgets::selectable_row` or one of its specializations. 4. **No inline `Window::new(...).collapsible(false).resizable(false).anchor(CENTER_CENTER, [0,0])`** — use `widgets::modal_window` or `widgets::confirm_modal`. 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. --- ## Tokens (`theme.rs`) ### Colour tokens All UI colour reads must go through these accessors. The 15 base slots come from `ThemeColors`; the derived slots are computed at runtime. | Token | Kind | Use | |--------------------|----------|-----------------------------------------------------------------------| | `bg_primary` | base | Deepest layer (table body, scroll background). | | `bg_secondary` | base | Panels, sidebars, window fill. | | `bg_tertiary` | base | Hover background, highlighted regions. | | `bg_surface` | base | Cards, popovers, tag chips. | | `fg_primary` | base | Primary text. | | `fg_secondary` | base | Secondary text, labels. | | `fg_muted` | base | Placeholders, disabled items, empty-state hints. | | `accent_blue` | base | Selection/active state. *The* affordance colour. | | `accent_red` | base | Errors, danger buttons, destructive confirms. | | `accent_green` | base | Success, ready state. | | `accent_yellow` | base | Warnings (not yet used as such — reserved). | | `accent_purple` | base | Reserved (classification palette). | | `accent_cyan` | base | Reserved (classification palette). | | `border_default` | base | Separator, panel boundary. | | `bg_row_even` | derived | Striped row alternation (even). | | `bg_row_odd` | derived | Striped row alternation (odd) = `bg_primary`. | | `bg_hover` | derived | = `bg_tertiary`. Shared row/button hover. | | `bg_selected` | derived | Selection fill (= `lerp(bg_primary, accent_blue, 0.3)`). | | `classification_color(c)` | domain | Sample-class palette. Stable across themes for muscle memory. | | `piano_white_key()`, `piano_black_key()` | domain | Instrument panel only. | ### Spacing tokens Replace every literal `add_space(N.0)` with one of these. New code may not introduce new spacing magnitudes without adding a token here. | Name | Value | Use | |--------------------|-------|--------------------------------------------------------------------| | `space::xs` | 2.0 | Tight inline (button-icon gap, tag chip internal). | | `space::sm` | 4.0 | After a label, before its control (52 occurrences today). | | `space::md` | 8.0 | Default between unrelated controls in a row (58 today). | | `space::lg` | 12.0 | Between minor sections within a panel (19 today). | | `space::section` | 16.0 | Between major sections (= existing `section_spacing` token). | | `space::xl` | 20.0 | Reserved for headline padding in empty states. | 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. ### Stroke / radius | Token | Value (default) | Use | |--------------------|-----------------|--------------------------------------------------| | `rounding` | 4.0 (TOML) | All widget corner radii. | | `border_thin` | 0.5 px | Separators, inactive widget border. | | `border_default` | 1.0 px | Hovered/active widget border, window stroke. | | `focus_ring` | 1.5 px `accent_blue` | New token — focus outline on text fields. | --- ## Widgets (`widgets.rs`) The canonical helper for every recurring UI pattern in the codebase. Names are normative — implementations rename existing inline copies to match. ### Rows and selection - **`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, …)`. - **`tree_node(ui, label, active, has_active_descendant, children)`** — for the sidebar tag tree; collapsing header variant of `selectable_row`. ### Headers and section structure - **`section_header(ui, "Vaults")`** — `.strong()`, `text_secondary()`, followed by `ui.separator()` and `space::sm`. - **`subsection_label(ui, "Save as Collection")`** — same colour, no separator. For sub-blocks inside an already-headed section. - **`filter_section(ui, label, active, |ui| { … })`** — `CollapsingHeader` with `"* "` marker when active and `default_open(active)`. Replaces the 5-way duplication in `filter_panel.rs`. ### Buttons - **`primary_button(ui, label) -> Response`** — bold weight, default fill. Singular per modal/dialog. - **`secondary_button(ui, label) -> Response`** — current default button. Cancel and peer actions use this. - **`danger_button(ui, label) -> Response`** — `accent_red` text or fill (TBD during impl). For Delete / Purge / Discard. - **`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). - **`icon_button(ui, glyph_or_word, tooltip) -> Response`** — small, non-toggle. (`x`, `+`, settings gear once renamed.) ### Pills, chips, badges - **`tag_chip(ui, tag) -> Response`** — existing. - **`tag_chip_removable(ui, tag) -> bool`** — existing. - **`classification_badge(ui, class)`** — existing. - **`toggle_pills(ui, current, options: &[(value, label, tooltip)]) -> Option`** — segmented control for mutually-exclusive choices: Folder/All, Exact/Compatible, Add/Remove tag, help tabs. ### Inputs - **`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. - **`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). ### Empty states - **`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.) ### Modals and overlays - **`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. - **`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. - **`name_modal(...)`** — existing; keep, lift to `widgets.rs` so callers outside `overlays.rs` can use it. ### Banners and notifications - **`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. - **`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. - **`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. ### Forms and grids - **`metadata_grid(ui, id, rows: impl Iterator)`** — the detail-panel `Grid` recipe; reusable for any read-only "key / value" block. `grid_row_spacing` token applies. - **`preview_grid(ui, id, headers, rows)`** — striped two-column grid used by bulk-rename preview; could be reused by export dry-run. --- ## Panel shapes These are de-facto today; the charter codifies them so the surface audits (Phase 1+) can rely on them. | Shape | Egui type | Conventions | |----------------------|----------------------------|-----------------------------------------------------------------------| | Left sidebar | `SidePanel::left` | Sections separated by `section_header` + `space::lg` between groups. | | Top toolbar | `TopBottomPanel::top` | Two rows max (breadcrumb + search). Right-aligned actions via `Layout::right_to_left`. | | Right detail | `SidePanel::right` | Sections paced by `space::section`. Waveform always first. | | Central | `CentralPanel::default` | Either the file list table or a full-screen wizard (import/export). | | Bottom footer | `TopBottomPanel::bottom` | One row. Transport on the left, status text on the right. | | Modal | `modal_window` widget | Centered, fixed-size unless explicitly resizable (bulk rename only). | | Inline banner | `info_banner` widget | Inside another panel; for one-time tips or unobtrusive state. | --- ## Migration phasing 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. In short: build the missing widgets first (§Widgets), then rewrite panels to call them, then audit the user-facing surfaces.