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
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
- **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
panels, not a hard rule for every surface.
- **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
model selector living in a three-dot overflow menu instead of next to
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
@ -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
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
jarring content swaps (layout shift, hard cuts) where a transition can
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
base16/`colors.css` swap contract is that a user's own theme (stylix
today) takes over with zero swarm-ui code changes — a generator
replaces exactly one file. Don't build anything that assumes a
specific palette's exact colours (contrast ratios, "this accent is
always purple") beyond what the semantic var names promise.
- **stylix wins outright when it's active, no in-between state.**
`nix/host-modules/hive-c0re/theme.nix` decides at *build time* whether
a hive is stylix-themed; when it is, that palette is authoritative —
swarm-ui's own light/dark logic (below) doesn't run at all rather than
trying to negotiate with it. This is a deliberate simplification, not
a limitation to fix: a stylix-using operator has already made their
choice.
- **Light theme is accessibility, not a cosmetic extra** — confirmed
position on #3444/#3452: some people need light for contrast/low-vision
reasons, others need dark for photosensitivity, so "OS preference by
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.
today) takes over with zero swarm-ui code changes. Don't build
anything that assumes a specific palette's exact colours (contrast
ratios, "this accent is always purple") beyond what the semantic var
names promise.
- **stylix wins outright when it's active, no in-between state.** When
an operator has stylix supplying swarm-ui's palette, that palette is
authoritative — swarm-ui's own light/dark preference (below) doesn't
override it. This is a deliberate simplification, not a limitation to
fix: a stylix-using operator has already made their choice.
- **Light theme is accessibility, not a cosmetic extra.** Some people
need light for contrast/low-vision reasons, others need dark for
photosensitivity — there's no universally-correct default. The OS
preference (`prefers-color-scheme`) drives the default, with an
explicit user override available, same shape as motion above.
## 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
tell is worse than one that's visibly stale.
- **Relative-time labels must actually tick.** A `StatusChip`-style
"fresh (5s ago)" computed once at fetch and never updated again reads
as current when it isn't. Tracked as #3445 (`RelativeTime` component:
takes a UTC datetime, not a precomputed age, and re-renders itself on
an internal interval; pauses while its tab is hidden).
- **Relative-time labels must actually tick.** A label like "fresh (5s
ago)" is computed from a stored timestamp and re-renders itself on an
interval — never a value frozen at fetch time that quietly goes stale
while still reading as current. Pauses while its tab is hidden.
- **Pages that poll get a refresh-interval control, not silent
staleness and not push.** SSE/WebSocket push was considered and
rejected for now — "overkill, leads to the same mess we have in
core." Instead: a small, Grafana-like "refresh every: off / 10s / 30s
/ 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).
staleness.** A small, Grafana-like "refresh every: off / 10s / 30s /
1m / …" control the operator sets per page, paused while the tab is
backgrounded and resumed on foreground.
- **A refresh must never clobber input the operator is mid-edit on.**
Any future polling component's contract needs to make this the
caller's problem to opt out of correctly, not something the next
adopter discovers by shipping a bug.
Any polling component's contract needs to make this the caller's
problem to opt out of correctly, not something the next adopter
discovers by shipping a bug.
## Errors
@ -140,13 +123,11 @@ just bare column headers floating over nothing.
not a desktop-only app. Concrete floor: every interactive control from
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
something each page has to remember. Tracked follow-through:
#3447 (page-level layout — nav wrap, no fixed-width assumptions).
- **Phone is a second-class citizen for now, not an unsupported one.**
Things must not *break* at phone width, but don't over-invest in phone
optimization beyond that yet. A PWA manifest (installable, basic
status-check/interaction use case) is wanted eventually but is its own
future slice, not implied by "supports touch" today.
something each page has to remember.
- **Phone is a second-class citizen, not an unsupported one.** 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
phone can check status or do basic interactions.
- **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
phone does. Design for the constraint (narrow viewport), not the
@ -154,27 +135,21 @@ just bare column headers floating over nothing.
## Component-first design
When something needs a table, a form field, a button — build (or adopt)
a `src/ui/` primitive that enforces consistent theming and behaviour,
rather than styling it inline on the page that happens to need it first.
Build the `src/ui/` primitive before or alongside the first real page
that needs it, not after it's been styled inline and left for later —
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
primitives that exist as of this writing, each with its own
`/components` demo; `/components` always has the current, complete list
— 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.)
primitives that exist; `/components` always has the current, complete
list — this doc won't try to keep a duplicate inventory in sync.
**Every new `ui/` component gets a demo section on `/components` the
same day it lands** — no primitive without a place to see it. One
deliberate exception exists today: `FormField`, the internal label+
control wrapper `TextField`/`SelectField` share, isn't itself a
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
oversight this rule missed.
a considered exception, not an oversight this rule missed.
## Attention
@ -182,15 +157,3 @@ oversight this rule missed.
attention to where it actually matters (an error, a state change worth
noticing) — not attention-optimized in the engagement/growth sense of
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 |