Skip to main content

max / makenotwork

13.3 KB · 161 lines History Blame Raw
1 # MNW Design System Charter
2
3 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.
4
5 Source of truth for visual identity: the cross-project brand system (internal). Source of truth for primitives: this file plus `static/style.css`.
6
7 ## Tokens
8
9 Every visual value is a token defined in `:root` (`static/style.css`). No raw hex, no off-scale spacing, no bespoke shadow.
10
11 | Tier | Token group | Where defined |
12 |---|---|---|
13 | 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` |
14 | Type | `--font-heading` (Young Serif), `--font-mono` (IBM Plex Mono), `--font-body` (Lato) | `style.css:42-44` |
15 | Spacing | `--space-1`...`--space-6` mapping to `0.25 / 0.5 / 0.75 / 1 / 1.5 / 2 rem` | `style.css:96-103` |
16 | Radius | `--radius-sm` (2px), `--radius-md` (4px), `--radius-round` (50%) | `style.css:105-108` |
17 | 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` |
18
19 Pure `#000` and `#fff` are forbidden outside the token table. Bootstrap-derived yellows (`#fff3cd`, `#ffc107`) are forbidden — use `--warning-bg` / `--warning-border`.
20
21 ## Type tiers (from `brand.md`)
22
23 Every text element maps to exactly one tier:
24
25 - **H1 / wordmark / section heads**`--font-heading` (Young Serif), `normal` weight, color `--detail`.
26 - **H2 / H3 / meta / taglines / footer**`--font-mono` (IBM Plex Mono), `normal` weight.
27 - **Body / lists / table cells / labels**`--font-body` (Lato).
28
29 No fourth typeface. No `font-family` declarations in templates.
30
31 ## Components — canonical primitive table
32
33 For each primitive, exactly one canonical class **or** one canonical partial. Variants are class modifiers; nothing else.
34
35 ### Layout primitives (page scaffolding)
36
37 | Primitive | Canonical class | Variants | Notes |
38 |---|---|---|---|
39 | Container | `.container` | `--narrow` (600px), `--medium` (800px), `--wide` (900px) | Default `max-width: 1200px` |
40 | Toolbar / header bar | `.stack-row` | `--bordered`, `--tight`, `--top` | Title-left + actions-right |
41 | Inline form row | `.field-row` | child `.form-group.is-grow` / `.is-grow-2` | Input(s) + button bottom-aligned |
42 | Vertical list row | `.list-row` | parts: `-title`, `-meta` | Flex with bottom border |
43 | Two-col form grid | `.form-row` | (none) | `grid-template-columns: 1fr 1fr` |
44 | Cover-image row | `.cover-row` + `.cover-thumb` + `.cover-empty` | (none) | 120×120 thumbnail picker |
45
46 ### Component primitives (reusable blocks)
47
48 | Primitive | Canonical class | Variants | States |
49 |---|---|---|---|
50 | Page section box | `.content-section` | (none) ||
51 | Card | `.card` | `.card-muted` (surface-muted fill), `.card--bordered`, `.card--selectable` | `:hover`, `:focus-within`, `.is-selected` |
52 | Button | `.btn-primary` / `.btn-secondary` / `.btn-danger` | `.btn--large`, `.btn--icon`, `.btn--link`, `.small`, `.saved` | `:hover`, `:focus-visible`, `:disabled`, `.htmx-request` |
53 | Heading | `.brand-h1` (wordmark), `.page-title` (h1), `.subtitle-h2` (auth/wizard h2), `.subsection-title` (default h2), `.section-header` (h2 with bottom border) |||
54 | Input shape | (base `input`/`textarea`/`select`) | `.input--xs`, `.input--sm`, `.input--mono`, `.input--upper`, `.input--numeric` | `:focus`, `:disabled` |
55 | Form field | `.form-group` + `.hint` | `.form-group--error` + `.field-error` | (via `_ui.html` macro `form_field`) |
56 | Section lead | `.section-lead` | with `.mb-3` / `.text-sm` / `.dimmed` utilities ||
57 | Section divider | `.section-divider` |||
58 | Section grouping label | `.section-group-label` |||
59 | Badge | `.badge` | `.badge-success`, `.badge-warning`, `.badge-danger`, `.free` | `.is-selected`, `.is-faded`, `.active` (= completion, not selection) |
60 | Tag | `.tag` (inside `.tag-input` for editing) |||
61 | Callout (solid-tint inline) | `.callout` | `--danger`, `--warning`, `--solid-warning` ||
62 | Alert (left-border inline) | `.alert` | `-note`, `-tip`, `-important`, `-warning`, `-caution` ||
63 | Banner (full-bleed page top) | `.banner` | `--info`, `--warning` ||
64 | Modal | `.modal-overlay` + `.modal` + `.form-actions` |||
65 | Confirm dialog | `_ui.html` macro `confirm_dialog` |||
66 | Toast | `partials/toast.html``.toast` | `--success`, `--error`, `--warning` ||
67 | Empty state | `.empty-state` (or `_ui.html` macro) | `--compact`, `--chart`, `--lg` ||
68 | Loading skeleton | `partials/loading_skeleton.html` | row, card, list ||
69 | Progress bar | `.progress-bar-container` + `.progress-bar` | `--slim` (6px), `--rounded`, `.progress-bar--highlight` (purple, default green) ||
70 | Upload status block | `.upload-status` + `-row` | `-msg.is-success`, `-msg.is-error` ||
71 | Status pill | `.field-status` / `.save-status` | `.success`, `.error`, `.saving` ||
72 | Table | `.data-table` (rich), `.compact-table` (small mono) | `.minw-300..800` for horizontal scroll min-width | `.sortable.ascending`, `.sortable.descending` |
73 | Tabs | `.tabs` + `.tab` || `.tab.is-selected` |
74 | Breadcrumb | `.breadcrumb` |||
75 | Pagination | `_ui.html` macro `pagination``.pagination` || `.active` |
76
77 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`.
78
79 ### Composition guide for new features
80
81 Build new UI by composing primitives, not by writing fresh CSS. Order of preference:
82
83 1. **Use a utility class** for one-off spacing/sizing — `.mb-4`, `.text-sm`, `.nowrap`, `.danger-text`.
84 2. **Use a layout primitive** to position content — `.container`, `.stack-row`, `.field-row`, `.list-row`.
85 3. **Use a component primitive** for a UI element — `.card`, `.callout`, `.badge`, `.progress-bar`, etc.
86 4. **Extend a primitive with a modifier**`.card--bordered`, `.callout--warning`, `.stack-row--bordered`, `.input--sm`.
87 5. **Only then consider a new class** — and add it to the design system table here AND to `style.css` in the matching section.
88
89 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.
90
91 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.
92
93 ### How to use the macros
94
95 The codebase has two partial conventions:
96
97 - **`{% 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`.
98 - **Macros in `partials/_ui.html`** — parameterized primitives. Import once and call:
99
100 ```jinja
101 {%- import "partials/_ui.html" as ui -%}
102 ...
103 {% call ui::empty_state("No items yet", "Create one to get started.") %}
104 {% call ui::form_field("Title", "title", value, "Up to 80 characters.", error) %}
105 {% call ui::confirm_dialog("Delete item?", "This can't be undone.", "/item/123/delete", "Delete", "/item/123") %}
106 {% call ui::pagination(current_page, total_pages, "/items?page=") %}
107 ```
108
109 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.
110
111 ## State vocabulary
112
113 Exactly one spelling for each interaction state, applied to every interactive primitive:
114
115 - **Hover** — three options, picked by component type; do not mix on the same component:
116 - 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.
117 - 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`.
118 - Opacity fade to `0.6`: reserved for `.btn--link` (semantically a link, no surface).
119 - **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; }`.
120 - **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.
121 - **Disabled**`:disabled` and `[aria-disabled="true"]` show `opacity: 0.5` and `cursor: not-allowed`.
122 - **Busy / loading** — HTMX-driven via `.htmx-request` on the trigger, plus a `partials/loading_skeleton.html` for content placeholders.
123
124 ## Page-level layouts
125
126 - `.padded-page` — standard content padding (`1.5rem`).
127 - `.centered-page` — landing / login / signup vertical-center layout.
128 - `.container` + `--narrow` / `--medium` / `--wide` modifiers.
129 - Wizard layout in `static/wizard.css` (`.wizard-layout` + `.wizard-sidebar` + `.wizard-content`).
130 - Media layout in `static/media-player.css` (`.media-container`).
131
132 No new top-level layout containers without an entry in this list.
133
134 ## Rules templates must follow
135
136 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).
137 2. **No page-scoped `<style>` blocks.** All page-isolated style blocks have been migrated. New templates do not get this exemption.
138 3. **No raw hex.** Use tokens. Adding a new color means adding a token.
139 4. **No checkmarks, emoji, or icon glyphs in copy.** Words only. The diamond mark is the only graphic element. Status uses `.badge`, not ``.
140 5. **No new typefaces.** Three tiers, no exceptions.
141 6. **Empty / error / loading states use the shared partial.** Never assemble these inline.
142 7. **Destructive actions** use `.danger` button class plus the `confirm_dialog` macro. No bare destructive buttons.
143 8. **Spacing values come from `--space-*` tokens.** Off-scale values are bugs.
144 9. **Page-scoped CSS is a last resort.** Default to composing layout + component primitives. Page-scoped sections exist for genuinely page-specific layout (grids, long-form typography, marketing heroes) — not for shapes that are cards or list rows in disguise.
145 10. **No bare `<h1>` / `<h2>` in templates.** Pick a heading class: `.brand-h1` (wordmark), `.page-title` (page h1), `.subtitle-h2` (page subtitle on auth/wizards), `.subsection-title` (default h2 inside dashboards/tabs/partials), or `.section-header` (prose sub-section with bottom border). Bare tags inside server-rendered markdown / user content (e.g. `partials/item_text_editor.html` JS preview) are the only exception.
146 11. **Button class is `.btn-primary` / `.btn-secondary` / `.btn-danger`.** Works on both `<button>` and `<a>` (the `<a><button>` antipattern is banned — use `<a class="btn-primary">…</a>`). The bare `button.primary` shorthand has been retired.
147 12. **Class names use kebab-case only.** No BEM `__` separator (retired 2026-05-20 — was 112 sites across 20 prefixes, now flattened to single-dash). Modifier `--` (e.g. `.card--bordered`) is still allowed.
148
149 ## How to extend
150
151 When a screen genuinely needs something not in this charter:
152
153 1. Propose the primitive in this file (name, canonical class/partial, variants, states).
154 2. Add the class to `style.css` in the matching section, or a partial under `templates/partials/`.
155 3. Update the inventory table above.
156 4. Then use it. Three usages without an entry in the charter means a missing primitive, not a license to inline.
157
158 ## Audit cadence
159
160 Phase 0 establishes the charter. Subsequent phases (tracked internally) audit conformance: every finding in those phases should be expressible as "screen X uses an off-charter primitive Y" or "primitive Y has a gap the charter should address." Free-form aesthetic critique is out of scope — that belongs to brand work, not this charter.
161