docs/internal/web-ui-theme-checklist.md
Purpose: an exhaustive, element-by-element visual checklist for building and
reviewing a web theme (web-ui/css/9x-theme-*.css + tokens in
web-ui/js/15-theme.js). Every native chrome surface the web UI renders is
listed with the visual dimensions that must be deliberately styled —
surface/fill, text, secondary text, font, padding, margin, gaps, borders /
hairlines, radius, icons, shadow, and the hover / selected / disabled / focused
states. Walk it top-to-bottom against a live build in each theme; anything you
can't tick is an unstyled or mis-styled element.
See web-ui/README.md §"Web themes" for the mechanism and
web-ui.md for the architecture. The buffer interior is not a
theme surface — it is painted from the editor's TUI colour theme (see
config-themes-settings.md); this doc covers the
chrome only, plus the handful of buffer-adjacent surfaces a web theme does
own (pane background, caret, scrollbar, separators).
web-ui/test/run.sh starts the bridge; or fresh --web).Ctrl/Cmd+Alt+T, or
window.fresh.setWebTheme('name') from the console).web-ui/test/drive.mjs exercises all of
them and drops screenshots in $SHOTS; capturing per-theme screenshots there
is the fastest way to review.applyTheme() follow the TUI palette).Golden rules (learned the hard way):
var(--bg). --bg is the buffer
background (kept at the TUI theme's dark colour, deliberately). Several
controls default to it — .w-text (search field), .settings-modal .set-pill
/ .set-field — and render as black chips on a light chrome. Repoint them
at a chrome surface.svg.cells is pinned to --mono-family; a
theme may repoint the chrome --font-family at a proportional stack, but must
not touch the cell font. Set letter-spacing:normal on chrome when using a
proportional font (the grid's --cell-tracking is for monospace).place()),
so you cannot inset their highlight with margin — use an inset ::before
pill (see §4b).transparent end fills its box and renders a square (this bit the macOS
traffic lights). Fade to transparent at the radius.A theme is (a) a theme-<name> body class driving structural CSS and (b) an
inline token set layered over applyTheme() by applyWebTheme(). Verify the
token set is complete and internally consistent.
--bg (buffer — usually leave to TUI), --bg2
(popovers/menus base), --bg3 (menu/sidebar base), --shell (frame bars).--fg (label), --muted (secondary label), --status-fg.--surface, --surface-2 (derived from bg/fg by
default; override if the theme wants specific greys).--border, --hairline, --hairline-strong.--hover, --sel, --sel-ring, --menuhi.--accent, --ui-accent, --on-ui-accent, --on-accent,
--on-sel, --ok (status/connection dot green).--r-sm, --r-md, --r-lg, --shadow.--font-family (chrome — safe to change), --mono-family
(buffer — never change), --font-size (live, zoom-driven).--on-* colour is legible on its paired fill
(check the accent-filled primary button, toggles, selected rows).webThemeScale multiplier (Compact uses 0.92);
confirm the grid re-fits after a theme switch.body, and any theme-specific desktop/wallpaper):
fills gaps behind translucent chrome; correct in light & dark.#app geometry: inset (Cosmos bezel) vs full-bleed (macOS/Compact).
If inset for a decorative frame, APPY/APPX still map cells correctly
(hit-testing exact).#device, macOS
#mactitle): shown only for its theme, pointer-events:none, correct height,
hidden on body.mobile; #app inset matches its height.syncMacTitle).letter-spacing
correct for the family; chrome font-size reads right relative to the buffer..menubar, .menu)--shell/theme), bottom border/hairline, height
(one cell row), horizontal padding, clip-path (Cosmos dock gap only)..menu): text colour, weight, per-item horizontal padding,
hover fill (--hover), cell-position alignment intact..menu.open): fill (accent or theme), readable text..dropdown, .dropdown.submenu).mitem, .msep, .mlabel).mitem): text colour, height (line-height:CH), horizontal
padding, gap between label and accelerator..mitem.hi): the inset rounded pill (via ::before, since
items are cell-positioned) — inset amount, radius, accent fill, readable text..check (checkmark column width), .lab gap, .accel
(secondary colour; readable on the highlight), .arrow (submenu chevron)..mitem.disabled): muted text, no highlight on hover..msep): hairline, inset margins..mlabel): muted, italic..tabbar, .tab)--fg), the
active indicator (bottom/top accent rule — height, colour, radius).--fg..tab .dot): only occupies space when dirty; colour..tab .x): shape (macOS = circle), size, glyph
colour, hover fill (stronger than the tab-body hover)..statusbar)--status-fg), padding..txt): colour (--fg), ellipsis..seg): radius, hover fill, transition.::before): --ok green, glow; the reconnect
pill overrides when the socket drops..fileexplorer)--bg3/--shell/sidebar token), right
border/hairline..fx-title): colour (muted), weight, letter-spacing,
case (Title vs UPPER — match the theme's reference), padding..fx-list): side padding (the selection-pill gutter), scroll..fx-row): text colour, line-height (row height / breathing
room), horizontal padding, gap, per-row margin, radius..fx-row.sel): the selection language for a persistent
sidebar (soft neutral vs accent — §20); label colour on it (dark vs
--on-sel)..fx-row:not(.sel):hover): --hover fill..fx-icon, .ficon): size (~14px), file colour (muted),
folder colour (.fx-icon.dir = accent), colour through selection..fx-chev): width, colour, colour through selection..fx-name, .dir): dir weight (600), ellipsis/scroll.The cells are TUI-theme-owned; a web theme owns only the surrounding surfaces.
.pane-content, .pane-gutter): --bg (keep TUI);
any theme separator (e.g. an inset top hairline for a "content well")..caret): colour, width (scales with zoom), blink..scrollbar .thumb + ::-webkit-scrollbar*): track
transparent, thumb colour (hairline-strong), hover, radius, width (density)..separator, .resize-grip): hairline
fill, hover accent.::selection, body.natsel): highlight tint..palette).palette.centered) vs bottom-sheet placement both
styled; the .modal-scrim behind the centered card..ptitle / .ptitlebar + .ptbclose): weight, colour,
divider, the close × (hover)..pinput): background (transparent over the material),
padding, .pmsg/.status/.count muted colours, the query .q + .caret2
blink. The N / M match count (.count) sits right-aligned and muted..prow): padding, gap, .ptext (command name), .pdesc
(inline muted description, ellipsis), .pkey accelerator chip (border,
radius, background, padding) right-aligned..prow.sel): fill (accent for this active surface),
.pdesc readable on it, and the .pkey chip's border re-tints on the
selected row (don't leave a grey chip on an accent fill)..ppreview svg.cells): buffer cells; background --bg..psearchopts, .psopt, .psbox, .pskey):
pill border/radius, on-state accent tint + check, key chip..palette.input-only) (Open File): bottom-hugging
bar, no list..popup).popup-title, .popup-desc): raised band, bottom
hairline, colours..popup-body, .popup-row): padding, .ptext2,
.pdetail (muted), .picon..popup-row.sel): highlight (inset for menu-like popups),
.pdetail readable; disabled rows..popup-line): whitespace-pre, colour..ctxmenu).ctxitem): padding; selection (inset highlight); hover..widget-surface.w-dock)65-widgets.js on the active theme)..fx-title: muted..w-button.primary): accent fill/gradient, border,
text, radius..w-text, .w-text-input, placeholder): chrome
surface (not --bg), border, radius, padding; optional leading
magnifier (::before); placeholder muted..w-button, view:/All ▾ pills):
chrome surface, border, radius; not black chips..w-toggle, .w-box, .on): sliding switch geometry, off
track (neutral gray), on track (accent), knob; label colour, on-label colour..w-divider): hairline, margin..w-tree-row): padding (row height), whitespace-pre
indentation; disclosure (.w-tree-disc) & checkbox (.w-tree-check) colours..w-tree-xrow): muted detail line..w-tree-card, .w-list-card): border, radius,
padding, margin, background..w-list-row, .w-list-cards): as above..w-hintbar, .w-hint b): muted; the accented key hints..w-section, .w-section-label): border, radius, padding,
label colour.view: cycles them) — rows, selection and density in each..ptoolbar) — e.g. live-grep.w-row gaps..widget-surface.w-floatingModal).modal-scrim behind it (dims + blocks the dock)..anchored): a real menu, not a stack of
bordered buttons — borderless rows (.w-button de-bordered), inset highlight
on the active item, near-opaque material; a transparent .scrim-clear
backdrop that still catches outside-clicks..w-text inside a modal): chrome surface, focus
ring..settings-modal).set-title): raised surface, weight, bottom hairline..set-cats, .set-cat, .set-cat-sec): width,
right border, row padding/radius, section colour; selected category
(.set-cat.sel) selection language; focus ring (.set-cats.focus)..set-items, .set-item): padding, per-item radius/margin,
divider handling; focused item (.set-item.sel) — a subtle form-row
selection (tint + dark label), not a saturated bar..set-section): accent, uppercase, letter-spacing..set-item-head, .set-name, .set-desc):
layout gap, name flex, desc muted + wrap..set-ctl-wrap, .set-ctl, .set-dim): right-align,
dim italic for complex types..set-switch, .on): off track neutral (scope :not(.on)),
on track accent, knob colour (--on-accent)..set-step, .set-num-v): border, radius..set-pill, .set-field): chrome surface (not
--bg → black chip), border, radius, ellipsis..set-dd, .set-dd-row, .set-dd-row.sel): popover
material, border; rows borderless; selected row accent..set-item-block .set-list, .set-list-row,
.set-list-head, .set-list-label, .set-list-sub, .set-list-badge,
.set-list-add): the "Name │ value" columns line up, key column width,
value muted, header dimmed, add row (blue "+ Add…"), badge accent, row
hairlines, .sel on rows..set-search, .set-sresult, .set-sresult.sel): accent
mark; result selection scrolls into view..set-dual-*): columns, active column border, row
selection, buttons..set-footer, .btn, .btn.primary,
.btn.danger, .btn.sel): raised band; push buttons (chrome surface,
border); primary accent fill + readable text; danger; focused button..set-entry, .set-overlay): overlay material,
border, fields..kbedit).kb-table, .kb-row, .kb-row.kb-head, .kb-row.kb-section):
header background, section accent, row padding/radius, .sel selection, hover..kb-key, .kb-action, .kb-context, .kb-source,
.kb-chip): key chips (border, on-state accent), monospace where relevant..kb-search, .kb-search b): accent mark, focus ring..kb-btn, .kb-btn.focus, .kb-field,
.kb-field.focus, .kb-ac-row): chrome surfaces, accent focus, readable
on-accent..kb-dialog, .kb-field, .kb-overlay,
.kb-autocomplete): overlay material, fields, autocomplete rows..trustdialog).td-opt, .td-opt.sel, .td-odesc): row selection,
description readable on it..td-path): monospace, ellipsis, colour..td-btn, .td-btn.primary): push vs accent-filled..auxmodal).am-line, .am-line.sel): selection.#themeswitch) — frontend-owned#themebtn, .ts-dot, .ts-name): surface, border, radius,
shadow, the swatch dot; hover; hidden on body.mobile.#thememenu, .ts-head, .ts-row, .ts-row.on, .ts-check,
.ts-label, .ts-desc): popover material, header, row padding/radius,
hover, on-row tint + check, label/desc colours.Decide and verify a consistent language across surfaces:
--on-sel/white.::before), not
edge-to-edge.--hover)..w-toggle.focus, .w-button.focus, .kb-search.focus,
.set-*.focus): soft accent ring, consistent.--r-sm/md/lg) applied consistently to pills, fields,
buttons, cards, panels, rows.--hairline vs
-strong).--shadow); menus lighter than modals if the theme distinguishes them.@supports not (backdrop-filter)
opaque fallback.30-render.js, cosmos/theme CSS)prefers-reduced-motion..modal-scrim, .fx-in): fade; position:fixed;inset:0 for
centered modals in every theme (not gated to one theme)..fx-cut): tone/duration.body.mobile, 70-mobile.css)Desktop chrome is replaced with touch bars; verify the theme's tokens still read:
.m-header, .m-logo, .m-file, .m-icon, .m-icon.on):
surface, spark accent, active-icon accent..m-sheet, .m-sheet-item, .m-sheet-ic): material,
active fill, the theme/view-pref rows..m-bottom, .m-syms, .m-sym, .m-save): surfaces,
save accent..m-mods, .m-mod.on, .m-nav-item.active,
.m-status, .m-seg.trust): armed-modifier accent, active-nav accent,
trust segment accent.applyTheme()).Ctrl+=/Ctrl+0) and density scale: chrome scales with the
buffer; nothing clips or overflows.--bg.svg.cells (buffer)
and .ficon (decorative) are SVG.web-ui/test/run.sh): the theme assertions
(body class, bezel/title-bar swap, buffer-stays-monospace, density, switcher)
plus the 140+ chrome assertions.body.theme-cosmos); a new theme must not regress it.Concrete elements pulled from per-theme screenshots (web-ui/test/drive.mjs
$SHOTS, plus focused captures of the title bar, tabs, toolbar, popup, context
menu and the dock in each view mode). These are the easy-to-miss bits — verify
each renders correctly and legibly in the theme under review.
Status bar (bottom): left cluster Trusted · Local · Ln N, Col N · <message>
and right cluster LF · UTF-8 · Rust · LSP (off) · Palette: Ctrl+P. Mixed
muted (.seg) and full (.txt) weights; the trailing shortcut hint reads as
emphasized. Confirm the message area (File explorer ready / Search cancelled / Cancelled) is muted and doesn't jump.
Palette: title band (Command Palette) + close ×; > prompt; right
N / M count; per-row inline description; per-row accelerator chip; the
selected row's chip re-tints. Frosted (Cosmos, content shows through) vs
near-opaque (macOS).
Tabs: active-tab accent rule — Cosmos draws it on the top edge (merges
with the buffer below), macOS on the bottom. Close × circular (macOS) vs
rounded-square. Dirty dot only when modified.
File explorer: file rows are ◇ name (muted glyph); folders are ▸ 📁 name
bold with an accent folder glyph and a disclosure chevron; the header is the
project name (FRESH upper in Cosmos, fresh Title Case in macOS). Selected
row: teal translucent pill (Cosmos) vs soft neutral pill + dark label (macOS).
Orchestrator dock:
Orchestrator; primary New Task… ▾; search field (with a leading
magnifier in macOS); ▸ Filters disclosure.view: card / view: compact cycle pill, an All ▾
scope pull, all worktrees and show empty sliding toggles, Move… and
Manage buttons.· <name> ▣ <branch>, line 2 ▸ <task/branch> +
a right-aligned git diff stat (+N −M, additions green / deletions red)
or a status word (clean / detached). Selection = soft fill + faint accent
edge; the diff stat colours must stay legible on the selection.· <name> row.↕ switch →← fold / Enter edit F2 menu — plain text
with the keys (Enter, F2) in the accent colour.Live-grep toolbar: Search in: then toggles Files / Ignored / Buffers / Terminals, each a sliding switch + label + a muted Alt+<key> hint; on-toggles
colour their label with the accent. Match: Word / Regex. Provider: a
value pill (git-grep) + Alt+M save matches. Empty result reads 0 / 0.
Status popup (e.g. Remote: Local): bold title row, an action row
(Create Dev Container Config) with the menu-selection highlight, and a muted
Dismiss (Esc) footer row.
Context menu (dock right-click): a muted target header (fresh), action
rows (Visit… / Move to Folder… / Archive / Delete) as borderless menu rows
with an inset highlight on the active one, and a muted Esc to close footer.
Cosmos-only decorative chrome (for reference, gated to .theme-cosmos):
machined bezel with corner screws, top-right status LEDs (green/amber),
engraved side-rail legends + running LED dots, silkscreen SPEC: COSMOS-991 /
PROJECT: INFERNU CORE labels; the glass dock's green New Task pill and
frosted search ⌕. A new theme replaces this frame wholesale — none of it
should leak.
Aspirational, from the macOS reference screenshot (not yet built — candidate polish, evaluate if added): right-aligned count badges on sidebar rows; a monochrome SF-symbol toolbar icon set; translucent vibrancy on sidebars (content behind shows through); rounded translucent cards for floating panels; avatar/initial circles; 2-line previews with progressive muting (title darker than preview). A floating rounded window over a desktop (vs our maximized full-bleed) is a structural variant, essentially what Cosmos already does.
Known-good numbers for a macOS-family theme, for calibration:
#ff5f57 / #febc2e /
#28c840; documented older set (with rims) #ed6a5f/#e24b41,
#f6be50/#e1a73e, #61c555/#2dac2f; unfocused #dddddd. Each dot needs
a transparent stop at the radius (or it renders square) and a subtle
top-lit sheen.#0063e1–#007aff;
dark #0a84ff.labelColor = black @ ~85% (light) / white @ ~85% (dark);
secondaryLabel ≈ 50–55%.~#ececec (systemGray6); dark window/
toolbar ~#2a2a2c, content ~#1e1e1e.inset 0 1px 0 rgba(255,255,255,.22)), not a gradient — cf. the
"Try now" pill and Control Center toggles.References used for calibration: a real Mail window (light, soft-gray sidebar selection, count badges, monochrome toolbar) and a faithful Big Sur clone (light + dark — Control Center cards, dark-sidebar filled-accent selection, flat accent buttons, rounded translucent panels).
| Surface | CSS | Builder (JS) | Scene projection (Rust) |
|---|---|---|---|
| tokens / base | 10-base.css | 15-theme.js, 20-cells.js applyTheme | — |
| menu bar / dropdown | 20-chrome.css | 40-menu.js | menu_view |
| tabs | 20-chrome.css | 45-tabs.js | tab_bar_view |
| status bar | 20-chrome.css | 55-status.js | status_view |
| palette / picker | 30-palette.css | 50-palette.js | palette_view |
| file explorer | 35-editor.css | 70-panels.js | file_explorer_view |
| pane / caret / scrollbar | 35-editor.css | 30-render.js | pane cells |
| popups | 50-popups.css | 60-popups.js | popups_view |
| widgets / dock / settings / kbedit | 40-widgets.css | 65-widgets.js | widgets_view, settings_view, keybinding_editor_view |
| trust dialog | 40-widgets.css | 70-panels.js | trust_dialog_view |
| context menu | 40-widgets.css | 65-widgets.js | context_menu_view |
| polish / hairlines / selection | 60-polish.css | — | — |
| mobile | 70-mobile.css | 75-app.js | (mirrors tabs/status) |
| skin (navy/teal base) | 80-skin.css | — | — |
| Cosmos shell | 90-cosmos.css | 10-core.js (layoutShell) | — |
| theme switcher | 91-theme-switch.css | 15-theme.js | — |
| macOS theme | 92-theme-macos.css | 15-theme.js tokens | — |
| Compact theme | 94-theme-compact.css | 15-theme.js tokens | — |