docs: rework design guide per mara's review

Addresses all 7 line comments from her REQUEST_CHANGES review:
- drop the issue-#3444 history pointer and any issue-number tracking
  refs throughout (a design guide states expectations, it isn't a
  change log or a status report)
- stop naming specific rejected technical solutions (SSE/WS) for a
  design constraint — state the chosen shape only
- flip the component-first heuristic: build the primitive first/
  alongside its first real caller, not after — the point is giving the
  next thing built ready-made blocks, not lagging behind usage
- drop the 'Open questions' section entirely — that's what the issue
  thread is for, not a doc
- stop leading Visual language with 'basis: Material Design' and then
  immediately carving out big exceptions — lead with what we actually
  want, mention Material as a minor closing influence instead
- drop 'future work'/timeline framing everywhere (motion override,
  PWA-as-future-slice, 'not shipped yet' theming caveats) — state the
  target design as the expectation, not its current build status
- reworded the stylix-wins theming bullet to drop 'build time', which
  reads wrong from a frontend dev's perspective (stylix supplies the
  palette separately, it isn't decided by the frontend's own build)
This commit is contained in:
iris 2026-08-18 21:01:37 +02:00 committed by mara
commit 61b7bb4b4d

View file

@ -11,16 +11,8 @@ of truth for appearance and this doc as the source of truth for intent.
Colour variables specifically are `docs/web-ui/css-vars.md`'s job, not Colour variables specifically are `docs/web-ui/css-vars.md`'s job, not
repeated here. repeated here.
Distilled from the design-language discussion in issue #3444 — read that
thread for the full reasoning behind any rule below that needs more
context than fits here.
## Visual language ## Visual language
- **Basis: Material Design, the Material You generation** (Android's
current design language) — but swarm-ui strays fairly far from its
theming approach; base16/stylix (below) is the actual colour contract,
not Material's dynamic-colour system.
- **Frosted / tinted glass** where it fits — translucency over flat - **Frosted / tinted glass** where it fits — translucency over flat
panels, not a hard rule for every surface. panels, not a hard rule for every surface.
- **Playful use of the multiple accent colours**, without tipping into - **Playful use of the multiple accent colours**, without tipping into
@ -39,6 +31,10 @@ context than fits here.
swarm-ui, but the standard to avoid): the per-hive agent terminal's swarm-ui, but the standard to avoid): the per-hive agent terminal's
model selector living in a three-dot overflow menu instead of next to model selector living in a three-dot overflow menu instead of next to
the thing that shows the current model. the thing that shows the current model.
- Loosely influenced by Material Design's current (Material You)
generation, mainly for shape/elevation ideas — colour theming is
governed entirely by the base16/stylix contract below, not Material's
dynamic-colour system.
## Motion ## Motion
@ -51,11 +47,6 @@ though it lives in the dashboard package rather than swarm-ui; every
future swarm-ui animation follows the same pattern, not just whimsy future swarm-ui animation follows the same pattern, not just whimsy
pieces. pieces.
An in-app override (independent of the OS-level media query, for
someone who wants motion on despite a system-wide reduced-motion
setting, or vice versa) is tracked separately — see #3456, which needs
the client-settings surface from #3453 first.
Prefer CSS-driven animation over JS-driven where possible, and avoid Prefer CSS-driven animation over JS-driven where possible, and avoid
jarring content swaps (layout shift, hard cuts) where a transition can jarring content swaps (layout shift, hard cuts) where a transition can
smooth them instead. smooth them instead.
@ -68,24 +59,20 @@ read that for the how. This section is the *policy* layered on top:
- **User-theming compatible by construction.** The whole point of the - **User-theming compatible by construction.** The whole point of the
base16/`colors.css` swap contract is that a user's own theme (stylix base16/`colors.css` swap contract is that a user's own theme (stylix
today) takes over with zero swarm-ui code changes — a generator today) takes over with zero swarm-ui code changes. Don't build
replaces exactly one file. Don't build anything that assumes a anything that assumes a specific palette's exact colours (contrast
specific palette's exact colours (contrast ratios, "this accent is ratios, "this accent is always purple") beyond what the semantic var
always purple") beyond what the semantic var names promise. names promise.
- **stylix wins outright when it's active, no in-between state.** - **stylix wins outright when it's active, no in-between state.** When
`nix/host-modules/hive-c0re/theme.nix` decides at *build time* whether an operator has stylix supplying swarm-ui's palette, that palette is
a hive is stylix-themed; when it is, that palette is authoritative — authoritative — swarm-ui's own light/dark preference (below) doesn't
swarm-ui's own light/dark logic (below) doesn't run at all rather than override it. This is a deliberate simplification, not a limitation to
trying to negotiate with it. This is a deliberate simplification, not fix: a stylix-using operator has already made their choice.
a limitation to fix: a stylix-using operator has already made their - **Light theme is accessibility, not a cosmetic extra.** Some people
choice. need light for contrast/low-vision reasons, others need dark for
- **Light theme is accessibility, not a cosmetic extra** — confirmed photosensitivity — there's no universally-correct default. The OS
position on #3444/#3452: some people need light for contrast/low-vision preference (`prefers-color-scheme`) drives the default, with an
reasons, others need dark for photosensitivity, so "OS preference by explicit user override available, same shape as motion above.
default + explicit override" is the right shape, same as motion above.
**Not shipped yet** — tracked as #3452 (light palette itself), #3453
(the client-settings surface an override setting needs), #3454 (the
override setting). Until #3452 lands, swarm-ui is dark-only.
## Data freshness & refresh ## Data freshness & refresh
@ -94,22 +81,18 @@ The governing question for anything that shows time-sensitive data:
needs a refresh story; a value that's silently gone stale with no way to needs a refresh story; a value that's silently gone stale with no way to
tell is worse than one that's visibly stale. tell is worse than one that's visibly stale.
- **Relative-time labels must actually tick.** A `StatusChip`-style - **Relative-time labels must actually tick.** A label like "fresh (5s
"fresh (5s ago)" computed once at fetch and never updated again reads ago)" is computed from a stored timestamp and re-renders itself on an
as current when it isn't. Tracked as #3445 (`RelativeTime` component: interval — never a value frozen at fetch time that quietly goes stale
takes a UTC datetime, not a precomputed age, and re-renders itself on while still reading as current. Pauses while its tab is hidden.
an internal interval; pauses while its tab is hidden).
- **Pages that poll get a refresh-interval control, not silent - **Pages that poll get a refresh-interval control, not silent
staleness and not push.** SSE/WebSocket push was considered and staleness.** A small, Grafana-like "refresh every: off / 10s / 30s /
rejected for now — "overkill, leads to the same mess we have in 1m / …" control the operator sets per page, paused while the tab is
core." Instead: a small, Grafana-like "refresh every: off / 10s / 30s backgrounded and resumed on foreground.
/ 1m / …" control the operator sets per page, paused while the tab is
backgrounded and resumed on foreground. Tracked as #3446
(`OverviewPage`'s hive-status roster is the first real caller).
- **A refresh must never clobber input the operator is mid-edit on.** - **A refresh must never clobber input the operator is mid-edit on.**
Any future polling component's contract needs to make this the Any polling component's contract needs to make this the caller's
caller's problem to opt out of correctly, not something the next problem to opt out of correctly, not something the next adopter
adopter discovers by shipping a bug. discovers by shipping a bug.
## Errors ## Errors
@ -140,13 +123,11 @@ just bare column headers floating over nothing.
not a desktop-only app. Concrete floor: every interactive control from not a desktop-only app. Concrete floor: every interactive control from
the shared `ui/` kit (buttons, inputs, selects) carries a minimum the shared `ui/` kit (buttons, inputs, selects) carries a minimum
touch target (`2.75em` ≈ 44px, WCAG 2.5.5) by default, so this isn't touch target (`2.75em` ≈ 44px, WCAG 2.5.5) by default, so this isn't
something each page has to remember. Tracked follow-through: something each page has to remember.
#3447 (page-level layout — nav wrap, no fixed-width assumptions). - **Phone is a second-class citizen, not an unsupported one.** Things
- **Phone is a second-class citizen for now, not an unsupported one.** must not *break* at phone width, but don't over-invest in phone
Things must not *break* at phone width, but don't over-invest in phone optimization beyond that. swarm-ui is installable as a PWA, so a
optimization beyond that yet. A PWA manifest (installable, basic phone can check status or do basic interactions.
status-check/interaction use case) is wanted eventually but is its own
future slice, not implied by "supports touch" today.
- **A narrow viewport isn't only a phone** — a tiling-window-manager - **A narrow viewport isn't only a phone** — a tiling-window-manager
user with swarm-ui in a narrow tile hits the same layout constraints a user with swarm-ui in a narrow tile hits the same layout constraints a
phone does. Design for the constraint (narrow viewport), not the phone does. Design for the constraint (narrow viewport), not the
@ -154,27 +135,21 @@ just bare column headers floating over nothing.
## Component-first design ## Component-first design
When something needs a table, a form field, a button — build (or adopt) Build the `src/ui/` primitive before or alongside the first real page
a `src/ui/` primitive that enforces consistent theming and behaviour, that needs it, not after it's been styled inline and left for later —
rather than styling it inline on the page that happens to need it first. a page reaching for a component that's already there has ready-made
blocks to build with, instead of the next contributor duplicating
ad-hoc styling that someone then has to hunt down and consolidate.
`Panel`/`StatusChip`/`Table`/`TextField`/`SelectField`/`Button` are the `Panel`/`StatusChip`/`Table`/`TextField`/`SelectField`/`Button` are the
primitives that exist as of this writing, each with its own primitives that exist; `/components` always has the current, complete
`/components` demo; `/components` always has the current, complete list list — this doc won't try to keep a duplicate inventory in sync.
— this doc won't try to keep a duplicate inventory in sync.
Default heuristic for *when* to promote page-scoped styling into a
shared primitive: the day a second real page needs the same thing, not
speculatively ahead of one. (Exception: when a peer explicitly asks for
a primitive ahead of a second caller, as happened with the form kit —
that's a judgement call each time, not a rule change.)
**Every new `ui/` component gets a demo section on `/components` the **Every new `ui/` component gets a demo section on `/components` the
same day it lands** — no primitive without a place to see it. One same day it lands** — no primitive without a place to see it. One
deliberate exception exists today: `FormField`, the internal label+ deliberate exception exists today: `FormField`, the internal label+
control wrapper `TextField`/`SelectField` share, isn't itself a control wrapper `TextField`/`SelectField` share, isn't itself a
primitive a page reaches for directly, so it has no demo of its own — primitive a page reaches for directly, so it has no demo of its own —
that's a real, considered exception (see its own doc comment), not an a considered exception, not an oversight this rule missed.
oversight this rule missed.
## Attention ## Attention
@ -182,15 +157,3 @@ oversight this rule missed.
attention to where it actually matters (an error, a state change worth attention to where it actually matters (an error, a state change worth
noticing) — not attention-optimized in the engagement/growth sense of noticing) — not attention-optimized in the engagement/growth sense of
maximizing time-on-page. maximizing time-on-page.
## Open questions
Tracked separately rather than asserted here as settled:
| Question | Tracked in |
| --- | --- |
| Light theme (palette + `prefers-color-scheme` default) | #3452 |
| Client-local settings surface (page + storage) | #3453 |
| Theme override setting | #3454 |
| Reduced-motion override setting | #3456 |
| PWA manifest / installable phone experience | not yet filed — mentioned in #3444, no concrete scope agreed |