themeGuards.ts: watches the site's layer container; every new popover (and one that the
site moves or that grows) is measured in an idle callback and shifted into the viewport
minus the bar insets with a translate (data-wb-clamped, --wb-clamp-x/y); taller than the
viewport it scrolls. Bounded: only nodes that hold a popover are measured, at most 4 per
pass; a moved popover is seen through its own style attribute and a ResizeObserver.
clampShift/viewportBox are pure and unit tested.
themeDerive.ts: canvas, content, card, chrome, raised, panel, strong, accentFill and
tooltip, each with ink, inkSoft, link, line, hover, selected and focus solved on its
fill (both gradient ends). Hover and selected step back toward the fill until every
ink still reads on them; hover is neutral unless the theme sets one (no accent wash);
a strong gradient's end moves until one ink reads on both; targets no ink can reach
on a mid-tone fill are capped and noted. 5,000-palette property test.
chromeTheme.ts: legible() is the solver; the bevel title bar, selection and face inks
come from the strong and chrome surfaces. Chrome vars of the 13 built-ins: only the
faint/warn/danger inks move (closer to the theme colour, still 4.5:1).
A pure measurement over a rendered document (fixtures and live): text and icon contrast over the
painted backdrop, X colour leakage, truncated bar/nav/button labels, bar geometry and hit tests, X line
icons under a pack, empty and nested frames, popover bounds, target sizes. Grew out of theme-qa/probe.js.
themeInk.ts: OKLCH lightness search toward the pole with headroom, hue kept, chroma
cut only for the gamut; several fills (gradient ends, glass grounds); compositing.
reach() now runs on it (8% headroom when a colour has to move, for the site's tints
mixed from these surfaces). Property test: 5,000 random pairs reach 4.5:1 and 3:1;
the live 1.0.2 cases ("New" pill 1.19:1, HoverLabel 2.7:1, like pink 3.84:1) pass.
A literal #3ecf8e green read 1.03:1 on the Windows 98 silver card (theme QA r1). Status text now mixes
the status hue toward --wb-text; store.css may hold no literal text colours outside its token defaults.
A save timer queued before the async startup (settings, profiles, workspaces) finished fired on the still-empty dock and overwrote the stored layout, so a shell reload came back with one fresh Home tab. Saves now wait for layoutReady.
Owner, live 2026-10-02: the account switcher's rows overlapped (names and handles collided) and
"Not signed in yet" was unreadable. Causes: edges.css forced every menu button to 26px, which squashed the
two-line account rows; the status inks (#f0b14a signed out, #ff8a80 errors) and the hint ink (a 38% fade of
the secondary text) were Night Ink literals, 2.1 to 3.6:1 on other themes' menus. Now the forced height is
gone, status, hint and secondary inks are measured on the menu surface (the face in bevelled themes) at
4.5:1 (--wb-c-warn, --wb-c-danger-ink, --wb-c-faint, --wb-c-muted), every line of a selected bevelled row
takes the selection's ink, and wayBetter's own default hint ink is legible. Checked: every shell menu
(accounts, app menu, tab menu, workspaces, palette) under 14 themes with an overlap and contrast scan
(verification/design-pass/1.0.2/menus-shots.mjs); new frame test for the inks.
A glyph made only of axis-aligned rectangles (M, h, v, z) is pixel art: its SVG gets
shape-rendering="crispEdges", so a 16-cell glyph stays sharp at sizes that are not a whole multiple
of its grid (X's rail draws 26px icons, the action bar 18.75px).
A preview (the Theme Builder's, developer mode's, or one an automation left on through theme_preview)
replaced every site tab's theme, pane themes included, also on reload; "Theme for this pane" then
showed its choice checked while the page kept the preview. Reproduced live: a preview of Windows 98
turned a Paper pane into Windows 98 until the preview ended. Now a pane with its own theme keeps it;
every other tab shows the preview, as before.
Live 2026-10-01: X's search-field icon is a 28x16 box with 12px of padding; sized at 100% of that width a
pixel glyph lost its top and bottom and sat left of X's own icon. Glyphs are now sized with contain (a
scaled pack by the box's height) and placed in the content box, for masks and images alike.
An app that does not know a catalog item's kind refuses the whole signed file, so
the icons and builtin kinds can never go into catalog.json. `waybetter store build`
now writes catalog.json (format 2: theme, plugin, strings only; what every app
reads today) and catalog.v3.json (format 3: every kind) from the same items;
`sign` and `verify` handle the pair; the rules refuse a format 2 file that lists a
newer kind (CLI, JSON Schema, host). The host reads catalog.v3.json first and falls
back to catalog.json only when the newer file is not there (a newer file that does
not verify is an error). An item of a kind a version does not know is skipped
instead of refusing the file, so the next new kind costs current apps nothing.
themeStore marks every dock group whose theme (the pane's own, else the global one) is bevelled with
.wb-bevel; edges.css draws tabs, toolbar, address field, wB button and the settings panel per group.
The window-level parts (title bar, caption buttons, menus, palette, scrollbars) keep following the
global theme. A Windows 98 pane is bevelled inside a flat window, and a flat pane stays flat inside a
Windows 98 window.
popover_host owns the one popover webview of an app: creation (recreated above tabs created after
it), placement through popover::place, the session (seq, source tab, app meta flattened into the
JSON the page reads), the toggle-closed window, close on layout change, navigation (the app says
whether the context moved on), shell raise, and the popover_only / current caller checks. The app
passes a PopoverConfig (page url, open event, macOS ground). Tests: layout staleness and the
session JSON shape.
With an ancestor in front (:root:root ::selection) every node the site inserts cost extra style
recalculation: Terminal measured 4 elements per insertion on the theme matrix (6 after a bar theme,
over the budget of 5); a bare ::selection measures 2 (3 after a bar theme). The rule did not predate
the design pass's changes; it was in Terminal's decor from the start.
Pure placement of a trusted local card under a button of a site tab, kept inside the tab, flipped
above when short, with the caret on the button; moved from the X app with its seven tests.
Open With: store_commands keeps each existing file the OS hands over (launch
argument, a second launch forwarded by single-instance with its working
directory, a macOS file:// open-URL) under a random id (Store::receive_file:
name says what it is, regular file, size cap, 8 kept, 15 minutes) and queues
LinkEvent.opened {id, name, kind}. The shell-only install_stage_opened {id}
reads that one file once and stages it exactly like a dropped file; it takes
an id only (testkit asserts that), so the shell cannot name a path. The shell
shows the same install screen; nothing installs without it. The NSIS template
lists the app under "Open with" for the declared extensions (OpenWithProgids)
and never makes it the default program, unlike Tauri's own macro.
Catalog kinds: icons (an icon pack, *.icons.json, at most 120 KB; validate_icons,
staged and committed as text for the shell, the install screen draws every icon,
.icons.json is no longer read as a theme) and builtin (a feature the app
already has, such as an area plugin: no url, sha256, size or file; never staged;
the Store view lists it as Built in with the app's own switch via the areas
prop). Rust: Kind, CatalogItem.check, verify_item, install links refuse builtin.
JS: storeModel, installController (requestOpened, icons adapter), InstallDialog,
StoreView, StoreSection (remove an icon pack, area switches), words. CLI and
JSON Schema: kinds, optional url/sha256/size on builtin, icons size cap; the
test schema validator learns not and if/then/else. docs/STORE.md.
tab_commands keeps the list each site tab reported (validated by plugins::commands, accepted only
from a tab on the site's origin), clears it when a new document starts, tells the shell, and checks
the shell-only tab_run_command: only a command that tab reported, only while it is on the site.
tab_run_command joins commands::SHELL (guard checked by assert_host_guards); the app's tab_report
hands its `commands` to tab_commands::reported.
- Rich sentences (a link or button inside text) can be replaced by data: the inventory gives a rich entry `slots`, a pack value places the site's own elements with {e0}, {e1}, ... (each once, the last may be left out; no markup, plural or other parameters). The engine returns a pieces + element order template; the page inventory carries `rich` slot counts.
- Packs may carry `when: { languages }` (applies only while the site shows one of them; `en` matches `en-GB`) and `rewrite: { words }` (whole-word rewording of the site's own text, messages keep their parameters' values; no sentences of the site in the pack). The hook tells the engine the site's language; a rewording pack makes the delivery carry the whole inventory.
- Apps can ship built-in packs (BuiltInPacks) delivered before installed ones; syncStringPacks hands the tabs a delivery at start only when it changed.
- Translate mode words are a function the app supplies (asked at each draw) and gain a `plural` word; host validates `when` and `rewrite` envelopes.
- Theme doctor: every finding says where (token path or CSS rule), what and how to fix, as catalog keys wb.doctor.*; the Theme Builder shows the full report; the import hint is translated. Doctor words are in the 17 supported catalogs.
- waybetter strings check describes conditions and rewrites.
alerts_commands keeps the owner's AlertSettings (saved in app data, validated by alerts), serves
the shell-only alerts_get / alerts_set (testkit::assert_alerts_guards) and builds the script that
hands the page runtime the keywords to watch. The app names the file and the runtime member
(AlertsConfig); store()/script() are the app's own writers (backup import).
theme_commands serves theme_get / theme_set / theme_preview / theme_scopes_get / theme_scopes_set
(shell-only, each checks its caller first; testkit::assert_theme_guards) over the theme state the
host now keeps: the owner's theme, account and pane themes, the Theme Builder preview. The app
only names its built-in themes and the two files (ThemeConfig). apply() themes every site tab
(script, cached Google font bytes, native background); page_scripts()/tab_background() serve a new
document; set_theme/set_scopes/set_preview are the app's own writers (backup import, dev mode).
A font install or removal re-themes the tabs in the host itself.
Every radius in the chrome and the panel scales with the theme's own radius (--wb-c-rs = radius / 12,
0 to 1.5): a square theme gets a square window, a round one a rounder window; themes that keep the
site's colors keep wayBetter's own corners. A theme built from raised and sunken edges (ChromeTheme
edges: 'bevel') gets frame/edges.css on html[data-wb-edges=bevel], also from the first frame: a face
color under tabs, toolbar, menus and the panel, 98.css's four edge lines derived from that face,
the active title bar's gradient across the tab row, tabs as a tab control, small raised caption
buttons, a sunken address field, selected menu rows in the title bar's color, raised and default
buttons, etched group boxes and classic scrollbars in the panel.
A state saved on a larger or rearranged screen (2720x1800 at x=3347 on a smaller second monitor)
left the titlebar off-screen and the window could not be moved. After the window-state restore,
the window is moved inside, and shrunk to, the work area of the monitor it is mostly on (the
primary one when that monitor is gone). Maximized and full-screen windows are left to the OS.
window_fit::fit is pure and unit tested.
ShellMenu.trigger chooses the trigger: 'pane' (default; each pane's wB button opens the menu under
itself, lined up with its end edge and kept inside the window), 'button' (titlebar) or 'logo'.
Alt+F, F10, the palette and the account menu's last row open it under the focused pane's wB (the
first one on screen otherwise). The menu's Settings row and the other panel rows still open the
panel, and Ctrl+Shift+W still does. The logo is part of the drag region again, as before 715f6f2.
A theme rule gated by html:has(...) / body:not(:has(...)) made the browser
restyle the whole page for every node the site inserted. Measured live on
x.com under any theme: about 440 elements restyled per insertion, against 1.5
without a theme; with the gates gone, 2.5.
Sites name page states as selectors (ThemeRuntimeSite.flags). The engine
keeps html[data-wb-<name>] in step, checked once per DOM hub frame and also
inside the site's open shadow roots. It passes the flags that hold to css(),
so CSS for one state can be left out of the other. Changing flags rebuild the
theme (they are part of its render key). Tests: pageFlags, flagAttribute.
Live on X's chat embed: --x-white is declared only inside the embed's root, so
the document's values left .text-white unresolved and the sent bubble's label
stayed white on a light accent (2:1 under Nord). Each root's theme now reads
the site's primitive variables from the root's own content (our sheet off
while reading), falling back to the document's.
On macOS the shell can hold the keyboard while its own settings tab is active; Ctrl+F then did
nothing, so typed keys and Enter went to X's search field. Find now targets the last web tab that
was in front (or the first one on screen).
The keys that open the find bar are often pressed in the page. The bar asks for the keyboard (`shell_focus`) and focuses its field; if the shell still does not have the keyboard 250 ms later, it asks once more and focuses the field again, so typing never lands in the page (seen on macOS: text typed right after Cmd+F was lost). Diagnostics lines record both steps.
X renders its chat inside the open shadow root of its embed host since
2026-10-01; the theme's <style> in <head> never reached it. For each root the
DOM hub names (domRoots), the engine now builds the theme from that root's own
rules (collectRootRules: adopted sheets and <style> elements, its own sheet
excluded), adds the static sheet, adopts it last among the root's sheets,
rebuilds it when the root's rules change and takes it off when the theme is
off or the root is gone. Test: collectRootRules.
X renders its chat inside an open shadow root since 2026-10-01 (the XChatEmbed
host [data-testid=xchatEmbedRoute]); the hub's document observer saw nothing
of it, selector observers found nothing, feature sheets in <head> did not
reach it and probes could not see it.
A site adapter may name shadowHosts. The hub's one MutationObserver then also
observes each such root (picked up on the next page mutation, or a few timed
retries while a host has no root yet; dropped with the observer), selector
observers search the roots too, domRoots(win) gives features the roots, a
feature's ctx.css sheet is also adopted by each root (CSP-proof, updated in
place, taken off on stop), and probes count matches inside the roots.
Tests: hub observes and searches a root that appears later with one
observer; registry adopts and removes the sheet and a probe inside a root is
never a miss.