Design system — desktop surface
Tokens and rules for the Meltemi desktop client (desktop/, Tauri webview).
Normative source:
design-system/— the maintainer’s design system (Meltemi Design System, UI Kit Desktop, Compañero móvil). This document records the same decisions in the form the code consumes (desktop/ui/src/app.css); on any conflict,design-system/wins and this document is corrected (gui-clase-mundial design D11).
Derived from brand V2 (brand/README.md). The same tokens are reused by the
phase-3 mobile companion without a redesign. The transversal rule inherited
from the terminal surface applies everywhere: every state is encoded as
symbol + word; color is never the only carrier of meaning.
Brand palette
Section titled “Brand palette”| Token | Value | Source |
|---|---|---|
--mel-aegean |
#2563EB |
brand V2 gradient start (Aegean blue) |
--mel-wind |
#22D3EE |
brand V2 gradient end (wind cyan) |
The gradient (aegean → wind, lower-left → upper-right) is reserved for brand marks: the app icon and the onboarding lockup. UI chrome never paints the gradient; interactive accents use the solid tokens below. Per brand rules: no glow, no shadows-as-decoration, no circular containers around the mark.
One exception, and only one (compositor-que-trabaja, 2026-08-09): the
working indicator may wear the gradient. It is not chrome — it is the
state signal of this product, the one moment where the interface says an agent
is working right now, and the brand’s own image is what it says: the wind
running along the edge while the sails work. Nothing else in the surface may
claim this exception; a second thing wearing the gradient makes both of them
decoration.
Color tokens
Section titled “Color tokens”Semantic tokens as CSS custom properties; both themes ship from day one and follow the OS, with a config override. All text/background pairs meet WCAG AA (≥ 4.5:1 normal text, ≥ 3:1 large text and UI glyphs).
| Token | Light | Dark | Role |
|---|---|---|---|
--bg |
#F8FAFC |
#0B1220 |
window background |
--surface |
#FFFFFF |
#111A2E |
cards, panels, trays |
--surface-2 |
#EEF2F7 |
#1A2540 |
nested panels, table stripes |
--text |
#0F172A |
#E2E8F0 |
primary text |
--text-muted |
#475569 |
#94A3B8 |
secondary text, hints |
--border |
#CBD5E1 |
#2C3A57 |
hairlines, dividers |
--accent |
#2563EB |
#60A5FA |
interactive, links, selection |
--focus |
#1D4ED8 |
#22D3EE |
focus ring (see Focus) |
--ok |
#15803D |
#4ADE80 |
success, active session |
--warn |
#B45309 |
#FBBF24 |
pending permission, degraded |
--danger |
#B91C1C |
#F87171 |
daemon down, errors, destructive |
--info |
#0E7490 |
#67E8F9 |
notices, streaming activity |
Structural and tint tokens formalized by gui-clase-mundial (they were already used de facto by the mockup):
| Token | Light | Dark | Role |
|---|---|---|---|
--panel |
#F1F5F9 |
#0E1526 |
structural chrome: sidebar, status bar, drawer |
--hair |
#E2E8F0 |
#1A2540 |
inner divider, fainter than --border |
--text-faint |
#64748B |
#5B6B8C |
third text level: column headers, binary paths, metadata |
--tint-ok |
#DCFCE7 |
#0F2E22 |
faint pill background for a correct state |
--tint-warn |
#FEF3C7 |
#3A2A12 |
the pending-permission counter, the chrome’s one always-visible signal |
--tint-danger |
#FEE2E2 |
#3B1618 |
inline session-error notices, short of the full-width banner |
--tint-info |
#CFFAFE |
#0C3A45 |
streaming activity and informational notices |
--touch-min |
44px |
44px |
the mobile companion’s minimum touch target |
Shell architecture
Section titled “Shell architecture”Three persistent zones (gui-clase-mundial; reference mockup
docs/ux/mockups/shell-clase-mundial.html):
- Sidebar (
--panel, 216 px): active project on top (switchable), views with icon, label and live counters (sessions, pending permissions), Settings at the bottom. It is the visible form of the keymap, not its replacement: 1–4,:/Ctrl+K,a,?and Esc keep working. - Top bar: view title and meta, the palette’s visible search affordance with its shortcut, and the primary action (“New session” — the brand gradient lives here and nowhere else).
- Status bar (
--panel): connection state, daemon version, endpoint and the session/permission summary.
A detail drawer (268 px, --panel) opens from a selected row without
losing the list; Esc closes the drawer before acting on the view.
Inside each project node of the sidebar, sessions are grouped into four buckets
in signal order — waiting on your decision, ready for your instruction, working,
stopped — each with glyph, word and count, and an empty one is not drawn
(sesiones-en-la-barra, 2026-09-15). The bar also lists the open session tabs
in a section of its own: the same set, the same order and the same close as the
strip, which remains the way to every tab when the bar is folded to its rail.
Elevation and density
Section titled “Elevation and density”Settings groups the existing controls into Appearance, Tools,
Project, Connections and About (ajustes-por-pestanas). The shared
tab strip stays above the scrolling category. Panels remain mounted while
hidden so an unsaved external-editor template, a connection test or a diagnosis
survives navigation. About carries the full privacy declaration. Arrow keys,
Home and End move focus with selection; narrow layouts scroll the category
labels and keep touch targets at least 44 px high.
Density is the feature — this is a control plane, not a landing page.
| Token | Value | Use |
|---|---|---|
--row-h |
32 px | table row height |
--cell-pad |
8 px | table cell padding |
--panel-pad |
16 px | panel/view padding |
--radius-control |
4 px | buttons, inputs |
--radius-panel |
8 px | panels, cards |
--shadow-overlay |
one level | overlays only — nothing else floats |
Hairlines are 1 px --hair inside a surface and --border at its edge. Pills
(.pill) are rounded (9 px) because they are labels, not controls; there
are no pill buttons. Counters use min-width, never width, so i18n
expansion cannot clip them. One solid button per zone; the brand gradient is
reserved for the shell’s primary action.
Hard rule (design system): the permission tray and the signal banners never animate their layout — nothing moves under the cursor while a permission is being decided.
Entity identity
Section titled “Entity identity”Each agent shows an initials avatar whose tone is stable for its id: the
ten known fleet entries use the assignments in design-system/, and any other
id (custom agents, profiles) hashes deterministically into the same palette
(FNV-1a → index). Same agent, same color, in every view and every session —
no state, no configuration (desktop/ui/src/lib/agents.ts).
Iconography
Section titled “Iconography”Own line icons (desktop/ui/src/lib/icons.ts): 24×24 viewBox, 1.5 px stroke,
rounded terminals, geometry echoing the mark. Inline SVG only — no icon font,
no platform emoji, nothing remote. Any icon carrying state is paired with a
visible or accessible label.
Status vocabulary
Section titled “Status vocabulary”Shared with the TUI so both surfaces speak one language. Each status renders
glyph + label (localized); the glyph has an ASCII twin (TUI rule) and the
desktop uses the same shapes in its iconography. Two rows were added by
sesiones-en-la-barra (2026-09-15), when the bar started heading a bucket for
each of them, and the last two by identidad-propia (2026-10-01).
Presence is about a machine of the user’s mesh, not about a session, and it is read-only: a machine being online says nothing about being able to do anything to it, and Meltemi offers nothing that would. Its two shapes are the ones already learned for filled-is-here and hollow-is-not, with their own names in the glyph table so neither meaning drifts when the other changes.
| Status | Glyph | ASCII twin | Color token |
|---|---|---|---|
| starting / connecting | ◌ |
~ |
--info |
| active / streaming | ▸ |
> |
--ok |
| waiting_permission | ● + count + word |
! |
--warn |
| waiting_instruction | ❯ |
> |
--text-muted |
| ended / done | ■ |
x |
--text-muted |
| interrupted (stopped) | ■ |
x |
--text-muted |
| error / unreachable | ▲ |
! |
--danger |
| mesh machine online | ● + word |
* |
--ok |
| mesh machine offline | ○ + word |
o |
--text-faint |
Signal priority
Section titled “Signal priority”Visual weight follows the daemon signal order and never inverts it:
- Daemon unreachable — full-width persistent banner (
--danger), above all content. - Permission pending — tray indicator: symbol + counter + word, always visible in the chrome.
- Session error / unexpected end — inline persistent notice on the affected session.
- Streaming — passive activity glyph; never a popup.
A lower-priority signal never occludes a higher one.
Typography
Section titled “Typography”- UI:
Inter, "Segoe UI", "SF Pro Text", Roboto, system-ui, sans-serif. Inter is preferred, never required (the brand wordmark is outlined; no font dependency ships with the app). - Code, transcripts, diffs:
"Cascadia Mono", "SF Mono", "JetBrains Mono", Consolas, monospace. - Scale (rem): 0.75 (caption) / 0.8125 (dense table) / 0.875 (body, default) / 1.0 (section) / 1.25 (view title). Line height 1.45 body, 1.3 dense.
- Body text is never lighter than 400; dense tables use tabular numerals
(
font-variant-numeric: tabular-nums).
Spacing, density, shape
Section titled “Spacing, density, shape”- 4 px base unit; scale: 4 / 8 / 12 / 16 / 24 / 32.
- Density target is a control plane, not a marketing page: tables at 32 px rows, 8 px cell padding; panels at 16 px padding.
- Radii: 4 px controls, 8 px panels/cards. No pill buttons.
- Elevation: 1 px
--borderhairlines; a single soft shadow level for overlays only (0 4px 16px rgb(0 0 0 / 0.18)); nothing else floats.
Focus and keyboard
Section titled “Focus and keyboard”- Focus ring: 2 px solid
--focuswith 2 px offset, on every focusable element, always visible when navigating by keyboard (:focus-visible). - Focus is additionally marked by shape where rows/panels are selected
(leading
▸marker), so selection survives monochrome and high contrast. - Full keyboard operation is a requirement (
gui-shellspec); pointer-only affordances (hover menus without keyboard path) are forbidden. - High contrast: honor
prefers-contrast/ forced-colors mode — in forced-colors, rely on system colors and the symbol+word rule. - In a composer,
Ctrl/Cmd+Enterdispatches now andCtrl/Cmd+Shift+Enterqueues behind the turn in flight; each control carries its chord in akbdbeside its label, and with a turn in flight no control is named “Send” (sesiones-en-la-barra, 2026-09-15).
Motion
Section titled “Motion”- Durations come from tokens, never from literals:
--dur-1(120 ms, arrival),--dur-2(160 ms, state change),--dur-pulse(1.2 s, skeleton),--dur-ambient(2.4 s, the ambient ring) and--dur-mark(600 ms, the mark’s outline, once), all with--ease-outexcept the ring’s linear turn. - Opacity/transform only, with one named exception: the mark’s outline
(class H) is drawn with
stroke-dashoffset, which is paint and not layout. Never height, position or background color. prefers-reduced-motion: reducesuppresses every non-essential animation (spinners become static glyphs with a textual “working…” label). Each file that animates withdraws its own animation in its own reduced-motion branch; the global kill-switch inapp.cssis a backstop, not the design.- Never animate the permission tray, a permission card, the notices, the signal banners or the sidebar: they arrive still.
Motion vocabulary
Section titled “Motion vocabulary”Every animation belongs to a class, and the named animations are a closed list
—composer-wind, arrive, skeleton-pulse, mark-draw— that
desktop/tests/scenarios_shell.rs enforces: a name outside it, a body that
animates anything else, a literal duration or a file without its reduced-motion
branch fails the build (senales-de-vida, 2026-09-26).
| Class | Where it applies | Duration | Without motion |
|---|---|---|---|
| A ambient | the composer ring; one per view | --dur-ambient |
removed; the accent frame carries the state |
| B arrival | new transcript items (turns, system lines, empty turns — never a permission card) and the palette as it opens | --dur-1 |
appears at once |
| C state change | the glyph of a status badge when its state changes | --dur-2 |
changes at once |
| D skeleton | waiting for data, only past 150 ms; opacity pulse, never a sweeping gradient | --dur-pulse |
still bars and the visible “loading…” text |
| E flow | streamed text arrives as it arrives, with no typing effect; a fixed, non-blinking cursor marks its edge | — | the same |
| F tools | reserved for herramientas-a-la-vista |
— | — |
| G terminal | the TUI header spinner, braille with an ASCII twin of its own cycle, armed only while a session works | ~100 ms per frame | the ASCII twin |
| H brand | the mark’s outline, drawn once in the welcome | --dur-mark, once |
already drawn |
| I window | hidden until the interface exists, capped at 500 ms | — | the same |
Arrival does not apply to switching views: views mount by branch, several have no single root to carry the class without breaking their height, and the tray is one of them.
Ambient working indicator
Section titled “Ambient working indicator”A class of its own, added because the rule above is about transitions and
this is not one (compositor-que-trabaja, 2026-08-09). An ambient indicator
says an agent is working right now, and it may run a slow loop — 2–3 s — for as
long as that is true. Its constraints, which are what keep it from becoming a
licence to animate:
- One per view. Two ambient loops on one screen are decoration, not signal.
- Transform or opacity only, and never layout. Nothing may move because something is working.
- It runs only while an agent works, and stops when the session is waiting on the user: waiting is not working, and an indicator that keeps moving while the agent is stopped lies about who has to act next.
- Under
prefers-reduced-motionit is removed, not frozen. The global kill-switch shortens durations; it does not clear a painted background, so an animation left to it stays visible, stopped, in an arbitrary position — a state nobody designed and nobody asked to see. The indicator withdraws and its static equivalent, with a word, carries the state.
Cross-webview CSS budget
Section titled “Cross-webview CSS budget”Target engines: WebView2 (Chromium), WKWebView (Safari), WebKitGTK. Keep to
the conservative intersection: flexbox, grid, custom properties, :focus-visible,
prefers-* media queries. Avoid: backdrop-filter as a load-bearing effect,
subgrid, scroll-driven animations, exotic selectors. Fonts, images and styles
are bundled — the webview never loads remote content (CSP).
Iconography and app icon
Section titled “Iconography and app icon”- Line icons, 1.5 px stroke, rounded terminals matching the mark’s geometry; every icon that conveys state is paired with a visible or accessible label.
- App/launcher icon:
brand/meltemi-app-icon.svg(512 px PNG export already inbrand/); tray/taskbar monochrome variants use the mono marks.
Internationalization
Section titled “Internationalization”- ES and EN from day one; every visible string goes through the message
catalog (lint-enforced). Layouts tolerate +30 % text expansion; no text
baked into images; dates/numbers via
Intlwith the app locale.