# MNW Design System Charter The MNW UI is composed from a small fixed set of primitives. Every screen is assembled from these — like an OS rendering applications, not like a website where each page reinvents its own widgets. If a screen needs something not in this charter, the answer is to extend the charter, not to write a one-off. Source of truth for visual identity: the cross-project brand system (internal). Source of truth for primitives: this file plus `static/style.css`. ## Tokens Every visual value is a token defined in `:root` (`static/style.css`). No raw hex, no off-scale spacing, no bespoke shadow. | Tier | Token group | Where defined | |---|---|---| | Color | `--background`, `--detail`, `--highlight`, `--light-background`, surface family, `--text-muted`, `--border`, semantic (`--success/-bg`, `--warning/-bg/-border`, `--danger/-bg`, `--error/-bg`), `--stripe`, health (`--health-ok/-warn/-error/-unknown`), diff (`--diff-add/-bg`, `--diff-del/-bg`), `--focus-ring`, `--highlight-faint`, `--overlay` | `style.css:40-95` | | Type | `--font-heading` (Young Serif), `--font-mono` (IBM Plex Mono), `--font-body` (Lato) | `style.css:42-44` | | Spacing | `--space-1`...`--space-6` mapping to `0.25 / 0.5 / 0.75 / 1 / 1.5 / 2 rem` | `style.css:96-103` | | Radius | `--radius-sm` (2px), `--radius-md` (4px), `--radius-round` (50%) | `style.css:105-108` | | Shadow | Hard-offset (everyday affordance): `--shadow-raised` (buttons, tabs), `--shadow-card` (cards, panels), `--shadow-inset` (inputs, recessed). Edge colors `--shadow-edge` / `--shadow-edge-soft`. Blurred (true elevation, floats over page): `--shadow-1` (subtle), `--shadow-2` (raised), `--shadow-3` (modal/overlay). | `style.css:208-222` | Pure `#000` and `#fff` are forbidden outside the token table. Bootstrap-derived yellows (`#fff3cd`, `#ffc107`) are forbidden — use `--warning-bg` / `--warning-border`. ## Type tiers (from `brand.md`) Every text element maps to exactly one tier: - **H1 / wordmark / section heads** — `--font-heading` (Young Serif), `normal` weight, color `--detail`. - **H2 / H3 / meta / taglines / footer** — `--font-mono` (IBM Plex Mono), `normal` weight. - **Body / lists / table cells / labels** — `--font-body` (Lato). No fourth typeface. No `font-family` declarations in templates. ## Components — canonical primitive table For each primitive, exactly one canonical class **or** one canonical partial. Variants are class modifiers; nothing else. ### Layout primitives (page scaffolding) | Primitive | Canonical class | Variants | Notes | |---|---|---|---| | Container | `.container` | `--narrow` (600px), `--medium` (800px), `--wide` (900px) | Default `max-width: 1200px` | | Toolbar / header bar | `.stack-row` | `--bordered`, `--tight`, `--top` | Title-left + actions-right | | Inline form row | `.field-row` | child `.form-group.is-grow` / `.is-grow-2` | Input(s) + button bottom-aligned | | Vertical list row | `.list-row` | parts: `-title`, `-meta` | Flex with bottom border | | Two-col form grid | `.form-row` | (none) | `grid-template-columns: 1fr 1fr` | | Cover-image row | `.cover-row` + `.cover-thumb` + `.cover-empty` | (none) | 120×120 thumbnail picker | ### Component primitives (reusable blocks) | Primitive | Canonical class | Variants | States | |---|---|---|---| | Page section box | `.content-section` | (none) | — | | Card | `.card` | `.card-muted` (surface-muted fill), `.card--bordered`, `.card--selectable` | `:hover`, `:focus-within`, `.is-selected` | | Button | `.btn-primary` / `.btn-secondary` / `.btn-danger` | `.btn--large`, `.btn--icon`, `.btn--link`, `.small`, `.saved` | `:hover`, `:focus-visible`, `:disabled`, `.htmx-request` | | Heading | `.brand-h1` (wordmark), `.page-title` (h1), `.subtitle-h2` (auth/wizard h2), `.subsection-title` (default h2), `.section-header` (h2 with bottom border) | — | — | | Input shape | (base `input`/`textarea`/`select`) | `.input--xs`, `.input--sm`, `.input--mono`, `.input--upper`, `.input--numeric` | `:focus`, `:disabled` | | Form field | `.form-group` + `.hint` | `.form-group--error` + `.field-error` | (via `_ui.html` macro `form_field`) | | Section lead | `.section-lead` | with `.mb-3` / `.text-sm` / `.dimmed` utilities | — | | Section divider | `.section-divider` | — | — | | Section grouping label | `.section-group-label` | — | — | | Badge | `.badge` | `.badge-success`, `.badge-warning`, `.badge-danger`, `.free` | `.is-selected`, `.is-faded`, `.active` (= completion, not selection) | | Tag | `.tag` (inside `.tag-input` for editing) | — | — | | Callout (solid-tint inline) | `.callout` | `--danger`, `--warning`, `--solid-warning` | — | | Alert (left-border inline) | `.alert` | `-note`, `-tip`, `-important`, `-warning`, `-caution` | — | | Banner (full-bleed page top) | `.banner` | `--info`, `--warning` | — | | Modal | `.modal-overlay` + `.modal` + `.form-actions` | — | — | | Confirm dialog | `_ui.html` macro `confirm_dialog` | — | — | | Toast | `partials/toast.html` → `.toast` | `--success`, `--error`, `--warning` | — | | Empty state | `.empty-state` (or `_ui.html` macro) | `--compact`, `--chart`, `--lg` | — | | Loading skeleton | `partials/loading_skeleton.html` | row, card, list | — | | Progress bar | `.progress-bar-container` + `.progress-bar` | `--slim` (6px), `--rounded`, `.progress-bar--highlight` (purple, default green) | — | | Upload status block | `.upload-status` + `-row` | `-msg.is-success`, `-msg.is-error` | — | | Status pill | `.field-status` / `.save-status` | `.success`, `.error`, `.saving` | — | | Table | `.data-table` (rich), `.compact-table` (small mono) | `.minw-300..800` for horizontal scroll min-width | `.sortable.ascending`, `.sortable.descending` | | Tabs | `.tabs` + `.tab` | — | `.tab.is-selected` | | Breadcrumb | `.breadcrumb` | — | — | | Pagination | `_ui.html` macro `pagination` → `.pagination` | — | `.active` | Tokens, the four parameterized macros (`empty_state`, `loading_skeleton`, `form_field`, `confirm_dialog`, `pagination`), and the consolidation pass have all shipped. Tokens live in `static/style.css` `:root`. Macros are in `templates/partials/_ui.html`. ### Composition guide for new features Build new UI by composing primitives, not by writing fresh CSS. Order of preference: 1. **Use a utility class** for one-off spacing/sizing — `.mb-4`, `.text-sm`, `.nowrap`, `.danger-text`. 2. **Use a layout primitive** to position content — `.container`, `.stack-row`, `.field-row`, `.list-row`. 3. **Use a component primitive** for a UI element — `.card`, `.callout`, `.badge`, `.progress-bar`, etc. 4. **Extend a primitive with a modifier** — `.card--bordered`, `.callout--warning`, `.stack-row--bordered`, `.input--sm`. 5. **Only then consider a new class** — and add it to the design system table here AND to `style.css` in the matching section. A new class is a smell, not a goal. Three usages without an entry above means a missing primitive, not a license to keep inlining. Page-scoped CSS (`.foo-page .bar`) is a last resort. Most page-scoped rules in `style.css` exist for genuine page-specific layout (e.g. `.item-page .item-layout` grid, `.article-page .article-body` typography). Don't add new ones for shapes that are really cards or list rows in disguise. ### How to use the macros The codebase has two partial conventions: - **`{% include %}` partials** — share the parent template's context. Good for header / nav / chrome that doesn't need parameters. Examples: `partials/site_header.html`, `partials/admin_nav.html`. - **Macros in `partials/_ui.html`** — parameterized primitives. Import once and call: ```jinja {%- import "partials/_ui.html" as ui -%} ... {% call ui::empty_state("No items yet", "Create one to get started.") %} {% call ui::form_field("Title", "title", value, "Up to 80 characters.", error) %} {% call ui::confirm_dialog("Delete item?", "This can't be undone.", "/item/123/delete", "Delete", "/item/123") %} {% call ui::pagination(current_page, total_pages, "/items?page=") %} ``` When a primitive needs an HTMX endpoint (e.g. server-rendered confirm dialogs returned to a swap target), wrap it in a small Rust template struct that calls the macro in its body. ## State vocabulary Exactly one spelling for each interaction state, applied to every interactive primitive: - **Hover** — three options, picked by component type; do not mix on the same component: - Surface lift: shift background one step (`--background` -> `--light-background` -> `--surface-muted`). For inline lists, cards-as-links, table rows, and navigation controls (tabs, pagination, breadcrumbs) — anything that moves the camera without committing. - Depth lift: shadow extends from `var(--shadow-raised)` to `3px 3px var(--shadow-edge)`. Reserved for controls that **cause a state change** — DB write, payment, file upload, delete, save. Pair with `:active { box-shadow: none; transform: translate(2px, 2px); }` so the press feels tactile. Default carriers: solid-fill buttons (`.btn-primary` / `-secondary` / `-danger`, bare `button`, `input[type="submit"]`, `form button`). The shadow is an opt-in signal that "this commits something," so a `.btn-primary` used purely for navigation (e.g. "Go to checkout" link) is the exception, not the rule — those keep depth because they lead to a commit, but pure view-switching uses of these classes should override to `box-shadow: none`. - Opacity fade to `0.6`: reserved for `.btn--link` (semantically a link, no surface). - **Focus** — `:focus-visible` shows the `--focus-ring` violet outline. Custom interactive containers (`.type-card`, `.pricing-card`, sort headers) must opt in by adding `:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }`. - **Selected / active** — `.is-selected` modifier applies `background: var(--highlight-faint)` plus the focus-ring border. Migrate `.tab.active`, `.filter-item.active`, `.view-btn.active`, `.badge.active`, and the `:checked + .type-card-inner` recipe onto this single class. - **Disabled** — `:disabled` and `[aria-disabled="true"]` show `opacity: 0.5` and `cursor: not-allowed`. - **Busy / loading** — HTMX-driven via `.htmx-request` on the trigger, plus a `partials/loading_skeleton.html` for content placeholders. ## Page-level layouts - `.padded-page` — standard content padding (`1.5rem`). - `.centered-page` — landing / login / signup vertical-center layout. - `.container` + `--narrow` / `--medium` / `--wide` modifiers. - Wizard layout in `static/wizard.css` (`.wizard-layout` + `.wizard-sidebar` + `.wizard-content`). - Media layout in `static/media-player.css` (`.media-container`). No new top-level layout containers without an entry in this list. ## Rules templates must follow 1. **No inline `style="..."`.** If you need a one-off, add a utility class or extend a primitive. The single exception: `style="--var: dynamic"` or `style="width: {{ pct }}%"` for server-computed values (progress bars, chart bars, avatar fallback colors). 2. **No page-scoped `