docs: swarm-ui design guide
Distills the design-language discussion on #3444 into a durable reference: visual language, motion, theming policy, data-freshness/ refresh, error UX, empty states, layout/viewport, component-first design, and attention — plus an open-questions table pointing at the still-unbuilt issues (#3452/#3453/#3454/#3456) instead of asserting them as settled. Mirrors css-vars.md's shape and links to it + to /components rather than duplicating either. First draft for comment, not a final spec.
This commit is contained in:
parent
d2fb4bff79
commit
7dc3e710b4
2 changed files with 190 additions and 0 deletions
|
|
@ -29,6 +29,11 @@ dashboard rather than how it's built, start at
|
|||
- **[CSS theme variables](web-ui/css-vars.md)** — the Catppuccin
|
||||
Mocha custom properties declared once in `base.css` and the rule
|
||||
that per-page stylesheets reference (never redeclare) them.
|
||||
- **[swarm-ui design guide](web-ui/design-guide.md)** — visual
|
||||
language, motion, theming, error-UX, and component-first principles
|
||||
for the swarm-level Preact app specifically (not this doc's
|
||||
dashboard/agent UIs). `/components` on a running swarm-ui is the
|
||||
companion living demo of every primitive it references.
|
||||
|
||||
## Reading paths
|
||||
|
||||
|
|
|
|||
185
docs/web-ui/design-guide.md
Normal file
185
docs/web-ui/design-guide.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# swarm-ui design guide
|
||||
|
||||
Scope: **swarm-ui only** (the swarm-level Preact app — not the per-hive
|
||||
dashboard, which has its own older visual language). This doc is the
|
||||
*why*: the principles behind how swarm-ui looks and behaves, and the
|
||||
concrete rules that follow from them. It deliberately doesn't show what
|
||||
things look like — that's `ComponentsPage` (`/components`), the living,
|
||||
always-current demo of every primitive in `src/ui/`. Code can't go stale
|
||||
the way a doc's screenshots can, so treat `ComponentsPage` as the source
|
||||
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
|
||||
visual noise — the base16 palette has several accent slots
|
||||
(`--purple`, `--cyan`, `--pink`, …); use more than one where it adds
|
||||
distinction, not decoration for its own sake.
|
||||
- **Whimsy** — small, delightful touches are welcome (the home page's
|
||||
matrix-rain background is the reference example). Whimsy still has to
|
||||
clear the accessibility bar below (motion, in particular).
|
||||
- **Efficient navigation** — minimize clicks/hops for a common task.
|
||||
- **Avoid junk drawers.** A control belongs next to the thing it
|
||||
affects, not tucked into a catch-all menu. Concrete anti-example (not
|
||||
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.
|
||||
|
||||
## Motion
|
||||
|
||||
**General rule, not case-by-case: every non-essential CSS animation
|
||||
gates on `prefers-reduced-motion` and pauses when its tab/section is
|
||||
hidden.** The matrix-rain background is the existing reference
|
||||
implementation of this pattern — every future animation follows it, 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.
|
||||
|
||||
## Theming
|
||||
|
||||
The mechanical contract (base16 slots, semantic vars, what a page's CSS
|
||||
is and isn't allowed to reference) lives in `docs/web-ui/css-vars.md` —
|
||||
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.
|
||||
|
||||
## Data freshness & refresh
|
||||
|
||||
The governing question for anything that shows time-sensitive data:
|
||||
**would a user returning to this tab expect current data?** If yes, it
|
||||
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).
|
||||
- **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).
|
||||
- **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.
|
||||
|
||||
## Errors
|
||||
|
||||
**`ApiErrorPanel` + the RFC 9457 `ProblemDetails` shape it renders is
|
||||
the canonical error surface** for every API failure in swarm-ui, not
|
||||
just its original callers — nobody should build a softer/quieter error
|
||||
UI later. Concretely: full untruncated `detail`, a copy button, no
|
||||
"something went wrong, try reloading."
|
||||
|
||||
This follows from **power-user-first**, one of the standing principles:
|
||||
swarm-ui instances are mostly self-hosted and operators are techies, so
|
||||
errors should give them what they need to actually diagnose a problem
|
||||
rather than a friendly wall. Power-user-first doesn't mean
|
||||
newcomer-hostile — the UI should still be self-explanatory, warn or ask
|
||||
for confirmation before a destructive/dangerous action, and offer
|
||||
helpful hints — it just shouldn't get in the way of someone who already
|
||||
knows what they're doing.
|
||||
|
||||
## Empty states
|
||||
|
||||
Graceful fallbacks, always — a table with zero rows should show
|
||||
something that represents "no rows" (a real empty-state message), not
|
||||
just bare column headers floating over nothing.
|
||||
|
||||
## Layout & viewport
|
||||
|
||||
- **swarm-ui adapts to screen size and fully supports touch** — this is
|
||||
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.
|
||||
- **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
|
||||
device.
|
||||
|
||||
## 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.
|
||||
`Panel`/`StatusChip`/`Table`/`FormField`/`TextField`/`SelectField`/
|
||||
`Button` are the primitives that exist as of this writing; `/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.)
|
||||
|
||||
**Every new `ui/` component gets a demo section on `/components` the
|
||||
same day it lands** — no primitive without a place to see it.
|
||||
|
||||
## Attention
|
||||
|
||||
"Attention-optimized" here means the system should lead the operator's
|
||||
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 |
|
||||
Loading…
Reference in a new issue