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).
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.
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.
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.
- 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.
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.
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.
themeDoctor(theme) names the lighter mechanism for what a theme's CSS does
(radius, bevel, hiding, literal colors, fonts), its performance costs (:has,
universal selectors, blur, size) and accessibility costs (outline removed,
unguarded motion, small post text, moved navigation), plus contrast fixes.
Theme import adds the count and the first hint to its message. Tested.
Appearance > Icons gets Import icon pack: a *.icons.json file is parsed by the
runtime sanitizer and shown on an install screen (name, author, licence,
source, credits, every glyph, size, data only) before it joins the persisted
theme choice (packs, at most 8, validated natively for shape and size) and
the Icons picker. Imported packs can be removed again.
With the logo as the menu's only titlebar trigger (`<AppMenu trigger={false}/>`), `--bt-tb-menu` still kept 38px free at the end of the titlebar-hosted tab strip. It is 0 when there is no `.bt-app-button`, so the right side is back to its layout before the menu.
The top-right ⋮ and the logo opened the same menu: one of them was redundant.
The logo stays as the only titlebar trigger (the app's own mark, where an app
menu lives); the right side is back to workspaces, accounts and the window
buttons. Alt+F, F10, the palette and the account menu's last row open the
menu under the logo. Apps that want a separate button keep <AppMenu/>'s
default trigger.
- ThemeChoice gains an optional icons field (absent: follow the theme's pack; site: the site's
own icons; else a pack id), validated natively as a slug (waybetter-host theme.rs, with a
test) and kept by per-theme choices; variants is an override group natively too.
- Appearance: an Icons row (Follow the theme / the site's own icons / the site's packs) when the
site lists packs; English words in the catalog.
- The chrome's glyphs follow the owner's pick as well as the theme's suggestion.
For the macOS reports (the menu never shows and the window goes inactive on a click; the find field gets no keyboard): off unless the app is started with `--diag` or WAYBETTER_DIAG=1. Then native code and the shell append to `diag.log` in the app's log folder (truncated at launch): menu open requests and every close with its reason, overlay counts, raise/lower and `shell_raise` results, native `shell_focus` calls and their result, the shell document's focus/blur/visibility, and where the find bar's focus is right after it opens and 300 ms later. A line is an event and a few words in plain ASCII (capped); never a URL, a query or page text. `diag_get` / `diag_log` are shell-only (in commands::SHELL and the template's list).
A site's theme adapter may name the pack of the global theme (iconPack); the frame's Icon draws
that pack's glyph for the same meaning (home, search, bell, mail, bookmark, user, ...) from its
validated path data, and its own line glyph where the pack has none. Rendered as React SVG
elements, never markup.
Seen live: the report said "WebView2 154.0.0.0", Chromium's reduced user agent version. The engine's brand list (`fullVersionList`) gives the full one, which the report now uses when it is a plain version (tested).
menuMessages.en.json stays the English source of the menu's words; en.json now carries them too
(the i18n test takes them into the source union) and 48 catalogs translate all 51 menu words and
the seven new mode words (plural forms per language for the match count).
Release blocker (macOS 1.0.0: both panes stayed on the shell's loading shape). After `shell_ready` the startup cover waited, with no limit, for every visible site tab to report that it painted. A page whose report never comes (its paint check is frame-driven and gives up after 10 s, and an engine may hold a hidden window's frames back) kept the shell raised over the panes, showing the skeleton, for good. The wait is now bounded (8 s after the window is shown); the page then shows as it is.
The app menu asked for the keyboard natively (`shell_focus`) on every open. It now does so only when the shell does not have it (keys pressed in a tab); a click on the shell already gave it the keyboard.
The mode cards' default picture was shrunk to 16px by the shell's base glyph rule ("a few dots"); it keeps its size.
WebView2 runs document-start scripts before the document has an element, so
injectCSS threw ("reading 'appendChild'") for every CSS feature that was on at
start, e.g. hiding promoted posts, until the next flags delivery restarted it.
Such styles now wait (one MutationObserver for all of them), are attached as
soon as the document has a root, and get their CSP check then; a style cleared
while waiting is never attached.
A mode is a starting point, never a lock and never applied again on its own. modeStatus() (runtime)
compares the flags with the mode's preset: custom, what re-applying turns on and off, and which of
those the preset gained in an update since it was applied. mode_set keeps the preset of that moment
(modes.json "preset", flag names only, bounded; forgotten when the mode changes). Settings, the
first-run screen and the app menu's Mode row show Custom; Re-apply lists what it turns on and off
before it writes anything; a preset that grew is mentioned once on the mode row (Review), never applied.
Tests: exact match, changed by the owner, preset grew, choice made before the preset was kept.
Ctrl+F pressed in a deck column (or in the shell while a deck is in front, for its focused column) opens that column's own find bar, laid over the column's header; F3 and Shift+F3 step through it. Same bar, same `tab_find`.
The app menu closed on any window blur. Taking the keyboard from a focused tab (Alt+F / F10 forwarded by the tab, then `shell_focus`) can pass through a blur on its way to the shell, which closed the menu a few milliseconds after it opened. It now closes on blur only if the focus is still away a moment later.