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).
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.
A theme may now say how each piece of a site is built, not only one radius and one density:
- components (themeComponents.ts): 36 generic parts with clamped tokens (radius, border incl.
98.css-style bevels, fill by color role / glass / gradient, padding, sizes, type, shadow, shape,
hover/active/focus), presets and parents; compiled over a site's part map as soft values
(--wb-part-*) and rigid rules; text on a chosen fill keeps its contrast.
- variants (themeVariants.ts): fixed words on six axes; the owner holds an axis or switches layout
changes off; the look of a variant is the site's skin.
- decoration (themeDecor.ts): dividers, chrome (title bar, masthead, prompt), a focus ring that is
always visible, selection, scrollbars, links, motion under reduced-motion.
- icon packs (themeIcons.ts): a standalone document of glyphs by semantic name (path data, colored
layers or a small PNG, never an SVG document), with license, source and credits; a theme only
names a pack; the owner's icon choice is separate; drawn as CSS masks of the current color over
the site's own svg boxes, behind forced-colors and mask-support guards, per group.
- a theme document gains the optional groups components, variants, decor, icons and
colors.actions; a theme without them is sanitized to exactly what it was.
- theme.schema.json and the new icons.schema.json are generated from the same tables
(scripts/theme-schema.mjs; a test fails when they are stale).
A site may color an icon or text with one of its border grays (X's composer toolbar: 1.7:1 on its
own black). Placed by axis position it landed between a theme's background and its secondary
text, under every contrast a theme guarantees. In text usage that stretch of the axis (above the
most raised surface, below secondary text) now maps to the theme's secondary text; surface-dark
foregrounds (labels on a filled surface), borders and backgrounds are unchanged.
The entry opens the notes in the page; its keywords carry the paused flags of that area. English
for the three new words; the other catalogs follow with the words step.
- create-waybetter: the generated app has a feature table (one area, one feature, three modes), a feature module, the registry in its runtime, the cards and the first-run screen in its panel
- waybetter i18n rename --map <file> [--copy]: keys move or are copied in every catalog with each language's own value
- feature context: win (no globals in a module), text() for a feature's words (t('...') is read as a whole catalog key by the i18n tools)
- probes: `when` (only counts while the part of the page it lives in is there)
- cards: a caret, an empty area says its features come later; the first-run screen takes the whole panel
- featureTable.ts: areas, per-feature flags, core flags, modes as presets; sparse storage, ownership (an area that is off turns off what it owns), mode switching that keeps or drops manual changes, table validation
- featureModules.ts: createFeatureRegistry (flag + surface gating, scoped context, palette commands under the area with a budget of 5, settings dialog, probes, hotfix selector overrides and kill flags)
- settingsForm.ts: a settings schema normalizes stored values and draws the controls from the UI kit
- domHub observeSelector; api.observe goes through the shared hub (a root of the caller's own keeps its observer)
- createFeatureFlags: a delivery is the whole state (a flag it does not name is at its default), resolve() for ownership, current()
Moved from wayBetter for X: 27 messages in 51 languages, and windows/languages/<Language>.nsh for the 17 NSIS languages Tauri does not ship (scripts/installer-languages.mjs writes and checks them).
What every app's page runtime entry did by hand (moved from wayBetter for X's inject/entry.tsx):
- siteRuntime.ts: pageInvoke (the IPC bridge read at call time), createFeatureFlags (parts that
install on enable and dispose on disable, the flags native code delivers, a deck column's
overrides on top of them, recorded at once and applied when <html> exists) and
installSiteRuntime (the runtime object with the members native code calls, the plugin API views,
developer-mode errors, its window names, the queued native deliveries, the command report).
- pageStrings.ts: the page's language (English built in, deliveries checked as data, the
bundled-plugin namespace rule).
- panelButton.ts: the way into the panel from the page (the site's own navigation entry when the
adapter mounts one, else a floating button that hides under the site's overlays and sits above
its bottom bar).
- commandReport.ts: the palette command report and developer-mode start errors.
- tests/siteRuntime.test.ts.
- 668 wb.* messages for the panel (navigation, settings search, accounts, updates, Appearance, the
Theme Builder, fonts, plugins, custom CSS, Make, Power User, settings, backup and sync, developer
mode, the controls), moved with every translation from wayBetter for X's catalogs; `{site}`
stands where the English said "X" (49 keys; the app passes "X", so X reads the same). Keys that
named X got site-neutral names (settings.links.site*, settings.language.followSite*,
builder.leadingSite, builder.liveOnSite, fonts.asSite, make.siteDefaultLeading, features.default).
English source: shell/src/panel/panelMessages.ts.
- One translation could not take `{site}` (hu wb.settings.alerts.hint never named the site) and
falls back to English.
- locales/en.meta.json: the translator notes of the moved keys (40), among them the one fragment a
language may leave empty (wb.make.step2Before); the catalog test reads it.
- runtime/tests/i18n.test.ts: en.json is the kit, shell-model, store, shell-frame and panel source.
The site-independent theme engine moves from wayBetter for X into the framework:
- runtime/themeSpec.ts: theme document types, the schema's defaults (BASE_*), sanitizing,
import, overrides, ensureContrast, font roles/stacks, font choices, backgrounds.
- runtime/themeChoice.ts: ThemeChoice and createThemeCatalog(builtins) (normalize, base,
resolve, palette, all), choiceForId, serializeChoice.
- runtime/themeRuntime.ts: installThemeRuntime(win, site) (live CSS over the site's rules,
--wb-* vars, site color mode, first-frame snapshot), collectRules, resolvedFonts.
- runtime/fonts.ts: bundled fonts and owner-downloaded Google Fonts from bytes.
- themeApi exports hexToRgb, mix and MONO_FAMILIES (one copy: themeTokens and themeSpec use them).
- host theme.rs: ThemeChoice/ThemeScopes over a site's ThemeSite (envelope checks against
theme.schema.json, which theme a tab shows, page color, delivery script).
- 245 wb.* messages for the shell frame (tabs, address bar, new-tab page, palette, account
switcher, decks, workspaces, chips, media panel, pane themes, backup preview), moved with every
translation from wayBetter for X's catalogs; `{site}` stands where the English said "X" (10
keys; the app passes "X", so X reads the same). English source: shell/src/frame/frameMessages.ts.
- wb.install.source.store|link|file: where an install comes from, translated in every locale
(native code's label is English only).
- runtime/tests/i18n.test.ts: en.json is the kit, shell-model, store and shell-frame source.
Moved from wayBetter for X so every wayBetter app gets the Store UI; the app passes its native
bridge, theme adapter, translator and (optionally) its own controls and button classes.
- storeModel.ts: wire types of the native store commands (InstallPreview, StoreCatalogView,
LinkEvent, StoreItem with `apps`), permission badges in the site's words, categories, item state
(install/installed/update/changed locally/built in), search, free theme ids, versions, previews.
- installController.ts: the install screen's state machine (links incl. those queued before the
shell listened, dropped files, store items, cancel, confirm), generic over the app's theme format
(`ThemeAdapter`: parse, ids, facts, save) and its bridge (`StoreBackend`: invoke/listen).
- InstallDialog.tsx (`InstallHost`): the screen (Install enabled after 1 s, Enter never installs,
tabs hidden via setOverlay) and the window-wide drop target. StoreView.tsx: search, kinds,
categories, cards, permission badges, install/update/remove; default controls or the app's.
- store.css: both, themed with framework `--wb-*` tokens (documented; dark defaults).
- storeMessages.ts + every framework catalog: 120 `wb.store.*`, `wb.install.*`, `wb.permission.*`
messages moved from the app catalogs with all existing translations; the six that named X take
`{site}`; generic words the components need copied with their translations.
- npm package: exports for the new modules and store.css, optional react/react-dom peers; @types
for the typecheck. Tests: models, controller flows with a fake bridge, every looked-up key exists.
At document start (where the app installs the X runtime) there is no <html> element yet, so
the lang report never ran and "Follow X" never learned X's language (found live: siteLang
stayed null). It now reads lang again at readystatechange/DOMContentLoaded, and a report
that fails (no bridge yet) is retried with a bounded backoff. Tests.
Kit and shell-model words (18 keys) per locale, machine-drafted except English. Tests cover
locale normalization, fallback chains, direction, ICU-lite formatting and quoting, the runtime,
catalog rules (plural-only arguments, missing forms, allowEmpty), plugin strings, and that every
framework catalog passes the checks.
- styles.ts: adoptStyles(root, css) adopts a constructed stylesheet into a shadow root or document
(not governed by a page's CSP, parsed once, stays with the root when its host is moved);
<style> fallback where constructable sheets are missing or refused.
- UI kit: the kit's shadow root and each dialog's `css` use it (dialog CSS goes with the dialog).
- Plugin API: api.adoptCSS(root, css) and scope.adoptCSS (removed on dispose); injectCSS keeps its
<style> and, when a CSP leaves it without a sheet, applies the same rules as an adopted sheet.
- tests/styles.test.ts.
- host plugins.rs: PERMISSIONS, parse_permissions, ScriptPlugin.permissions/problems (None = legacy,
full API), validate_plugin (file name, 2 MiB, UTF-8 text without binary bytes, header with @name
and @version, valid @permissions, network:none refused when the code calls network APIs),
source_findings, PluginStore::install (atomic rename, never enables, keeps the enabled state on
update) / remove / is_enabled / file_hash. start/stop scripts run the module body, construction,
start() and stop() inside window.<global>.withPlugin(id, grants, fn) when the runtime has it.
- runtime permissions.ts: parsePermissions, scopePluginApi (undeclared gated members throw a
PluginPermissionError; scope().ui gated like api.ui), createPluginContext (the view of the plugin
whose code runs; legacy = full API). A declaration and guard rail, not a sandbox.
- The `commands` permission name comes from the parallel palette-commands work (its API lands there).
The kit draws in one shadow DOM host in the active theme's colors, corners and font and repaints
live; accessible dialogs (focus trap and return, Esc/Enter rules, alertdialog for alerts and
confirms), stacking with inert, toasts with actions, reduced motion, text-only content (ui.h),
and scope.ui closes a plugin's dialogs on dispose. api.toast routes to it.
Site-agnostic parts of the X app, behind a small site adapter:
- crates/waybetter-host: webview-label security policy and tab rect
clamp, account profiles with native-owned accounts (AccountRules),
plugin store with bundled-plugin upgrades (PluginHost), signed
data-only hotfix channel, update scheduling and crash handshake, tab
payloads/link routing/tab-chrome validation, and (feature "tauri")
webview helpers and the Windows system menu.
- packages/runtime: DOM hub, request hub, plugin API core, OKLab theme
color model, split-view rail hiding, link routing and account
reporting, each configured by a SiteAdapter.
- packages/shell: address bar, pane, link-drop, profile, palette and
update-chip models, and window controls with the Tauri API injected.
Tests moved/adapted from the app: 45 Rust (headless), 40 TypeScript.