waybetter
v0.7.0
Published
wayBetter: the framework for apps that run the apps you already use in a light native (Tauri) window and extend them in place. Page runtime (DOM/request hubs, plugins, plugin UI kit with themed dialogs and toasts, theme API and theme JSON Schema, theme co
Downloads
401
Maintainers
Readme
waybetter
The framework behind the wayBetter apps: the apps you already use in a light native (Tauri) window, extended in place the way BetterDiscord extends Discord. Websites and services that ship a heavy Electron client alike: every wayBetter app is the same shell, runtime and one small adapter.
This package is the TypeScript half and the command line. The native half is the Rust crate
waybetter-host, which is not on npm: an app takes it from the
repository as a git submodule or a path dependency. The
repository README explains how the three layers fit together and shows a minimal adapter;
docs/ARCHITECTURE.md there lists every module.
waybetter/runtime/*: the page runtime. One DOM-mutation/animation-frame hub, one fetch/XHR hub, a plugin API core with a themed UI kit (toasts, alerts, confirmations, prompts, dialogs), a theme API, an OKLab theme color model, split-pane rail hiding, link routing and account change reporting.waybetter/theme.schema.json: the JSON Schema of wayBetter theme files.waybetter/shell/*: pure models for the app chrome around the pages: address bar chips, dock/split-pane rules, dragged links, account profiles, the update chip, command palette ranking and window controls. The Tauri window API is passed in by the caller, never imported. And the wayBetter Store: the install screen and the Store view as React components (React is an optional peer; everything else has no dependencies), their controller and models.waybetter/shell/frame: the whole shell of an app,createShell(site, { native, i18n }): tabs in the titlebar, split panes, the smart address bar, the new-tab page, the command palette, the account switcher, decks of site columns, workspaces, pane themes, overlays and the native layout sync. The app describes its site (pages, address chips, palette items, column kinds, its own panel tab); React, React DOM and dockview are optional peers, the Tauri API is passed in.waybetter store build|sign|verifyandwaybetter/store-catalog.schema.json: the tooling and JSON Schema of the shared, signed wayBetter Store catalog.
Plain ESM with .d.ts types. Zero dependencies (the React parts use your React; the shell frame also dockview).
Install
npm install waybetterUsage
Every module is its own subpath export, so you only pull in what you use:
import { configureRuntime, type SiteAdapter } from 'waybetter/runtime/site';
import { watchDom } from 'waybetter/runtime/domHub';
import { buildColorModel, deriveColors } from 'waybetter/runtime/themeTokens';
import { parseAddress } from 'waybetter/shell/addressModel';
import { fuzzyScore, rankItems } from 'waybetter/shell/palette';runtime/site is the composition root: an app is mostly one adapter object built from these
pieces. The source comments document each module's contract.
Modules
Runtime
accountReport: reports the signed-in account seen in the page's own UI.siteRuntime: composes a runtime entry:pageInvoke,createFeatureFlags(flag-controlled parts, a deck column's overrides),installSiteRuntime(the runtime object underwindow.wayBetterand the app's names, the members native code calls, queued native deliveries, the palette command report).pageStrings: the page's language (English built in; native deliveries are checked as data).panelButton: the way into the app's panel from the page: the adapter's navigation entry, or a floating button.commandReport: reports plugin palette commands to the shell; developer-mode start errors.column: a site tab as a narrow deck column: hides the site's chrome, lets its content fill the width, per-column filters (keywords,/regex/, replies, reposts, media only) and an unread count from what the site rendered. Makes no requests.domHub: oneMutationObserverand one animation frame shared by every DOM feature.linkRouting: routes clicks on the app's own links per the "links to app pages" setting.netHub: one shared fetch/XHR hook for URL rewrites, gates and passive JSON taps.paneNav: hides a redundant navigation rail when two panes show the same account.pluginApi: small helpers shared by user JS plugins, includinguiandtheme.site: the page-side composition root for an adapter.themeApi: the active theme as values (ResolvedTheme),--wb-*CSS variables, contrast helpers.themeTokens: the OKLab color model themes are built on.ui: the plugin UI kit (see below).
Shell
addressModel:parseAddress(url, site)for the address bar's breadcrumb chips.chrome: layout constants for the custom titlebar and toolbar.deckModel: decks of site columns (TweetDeck-style): bounded model, paging layout (whole columns per page, parked off-page columns), focus moves, unread totals.linkDrop: a dragged link dropped into the dock opens as a tab or split pane.palette: command palette fuzzy scoring, ranking and selection movement.paneModel: pane geometry: where a "beside" link opens, which panes keep the rail.profileModel: the account profile model.updateModel: the titlebar update chip model.windowControls: native window calls for the titlebar, resize grips and caption buttons.storeModel,storeMessages: Store wire types, item states (install, update, changed locally, built in), permission badges, search; the English of everywb.store.*,wb.install.*,wb.permission.*message (translations inwaybetter/locales/*.json).installController: the install screen's state (install links, dropped files, store items, confirm), given the app's native bridge and theme adapter.InstallDialog(InstallHost),StoreView,store.css: the install screen, the window drop target and the Store view, themed with--wb-*tokens. See docs/STORE.md in the repository.
Shell frame (waybetter/shell/frame, modules also as waybetter/shell/frame/*, styles as waybetter/shell/frame/*.css)
import { createShell, type ShellSite } from 'waybetter/shell/frame';
const Shell = createShell(site, { native: { invoke, listen, currentWindow: getCurrentWindow }, i18n });
createRoot(document.getElementById('root')!).render(<Shell/>);ShellSite(frame/site): the site's names and pages,address(url)chips, slots for the app's panel tab, the new-tab page, palette items, deck column kinds, the media panel, themes (createThemeStore) and overlays, and the nativefeaturesthe app serves.i18n: the app'screateI18ninstance withwaybetter/locales/*.jsonmerged in; the frame's words arewb.*messages in 51 languages.bootScript()(frame/boot): the first-frameboot.js(the last theme's chrome colors before the modules load).installPreviewMocks(handlers): native commands answered from memory, for development previews in a plain browser.createLanguage(options)(frame/language): which language the app speaks (follow the site, follow the system, or one language), switched live and handed to native code and site tabs (i18n_set);createAppI18n(options)(frame/appI18n): the app's i18n instance with typed keys,useT, dates and numbers.- Everything else is exported for apps that build their own settings on it: the shared state (
shell,setOverlay,scheduleSync), stores (decks, workspaces, sync, developer mode), models (palette, accounts, workspaces, backup, media) and hooks.
Panel (waybetter/shell/panel, modules also as waybetter/shell/panel/*, styles as waybetter/shell/panel/*.css)
import { createPanel, type PanelSite } from 'waybetter/shell/panel';
const Panel = createPanel({ brand: { name: 'wayBetter', logo }, home, themes, sections, make, languages });
const Shell = createShell({ ...site, panel: { id: 'app', title: 'wayBetter', component: Panel } }, options);- The app's own tab: home, Appearance and the Theme Builder (with the Google Fonts browser), plugins and custom CSS, the Store, Make, accounts, Power User, settings, backup and sync, developer mode, updates, and a search across every setting. Each section shows only when the app serves what it needs.
PanelSite(panel/site): the brand and the home page's words, the app's ownsectionsand theirorder,themes(the theme store and catalog, the preview component a theme document is drawn in, the site font),make(example plugins, an example theme, bundled docs),store,languages, and what its native side serves (settings.alerts,backup,updates).- For the app's own sections: the panel's controls (
panel/controls:Section,Row,Toggle,Segmented,Select,Slider,ColorField,Button, ...),useFeaturesandFeatureGroups(panel/FeatureToggles: on/off rows over the host'sfeatures_get/set),compareLines. - The panel's words are
wb.*messages in 51 languages (waybetter/locales/*.json; translator notes inwaybetter/locales/en.meta.json).
Command line
waybetter store build --config store.config.mjs: builds an app's part of the shared store folder (its items only, validated with the apps' own rules).waybetter store sign [--init] --key <private key>/waybetter store verify --pub <key file>: Ed25519 signature over the exact catalog bytes; any listed public key verifies (rotation). The private key is never printed or copied.waybetter strings check <pack> [--inventory <file>] [--verbose]: validates a string pack (data that replaces the wrapped site's own UI strings:waybetter/runtime/stringPacks,waybetter/runtime/stringPackHook,waybetter/runtime/textNet) against the site's string inventory and prints its coverage; the inventories otherwise come fromwaybetter.strings.jsonin the app's root.waybetter i18n check [--write] [--verbose] [locale…]: checks an app's catalogs (and the framework's in the same languages) with thewaybetter/i18n/checkrules, bundled plugins' inline English and the generated files;--writeregenerateslanguages.jsonand the native localization files (macOS, Linux).waybetter i18n extract [--missing]: keys the code looks up that English lacks, and English keys without a literal lookup. Both readwaybetter.i18n.jsonin the app's root (locales,sources,nativeSources,bundledPlugins,runtimeKeys,supported,native; schema:i18n-config.schema.json).supportedlists the languages in the supported tier; every other catalog getstier: "community"in the generatedlanguages.json(languages.schema.json), and an app resolves its automatic language choices only to supported ones (createLanguage({ supported })).waybetter release windows|macos: signed native releases and the update manifest (waybetter.release.json). The Windows build runs path-free (below);WAYBETTER_RELEASE_CHECKcan name a command run on every artifact before anything is copied or signed.waybetter build-env -- <command>: runs a build (tauri build,cargo build) so the binary holds none of the machine's paths: the home folder, the workspace,CARGO_HOME,RUSTUP_HOMEandCARGO_TARGET_DIRbecome/home,/src,/cargo,/rustupand/target(--remap-path-prefixappended to the flags already inRUSTFLAGS/CARGO_ENCODED_RUSTFLAGS,/PDBALTPATHon MSVC; for C and C++ a build script compiles,-ffile-prefix-mapon gcc and clang and, on MSVC,/d1trimfile:<root>inCLand/PDBALTPATHinLINK, which reach the cc crate, CMake and MSBuild alike). Without a command it prints that environment (--json).windows/languages/<Language>.nsh: the Windows installer's messages for the languages Tauri does not ship, next to the installer template (windows/installer.nsi).
Plugin UI kit (runtime/ui)
Plugins get it as api.ui (and scope.ui, which closes whatever it opened on scope.dispose()).
Everything is drawn in one Shadow DOM host, so the site's CSS cannot restyle it, and it takes its
colors, corners and font from the active theme (api.theme), live.
const ui = this.scope.ui;
ui.toast('Link copied'); // tones: neutral, info, success, warning, danger
ui.toast('Post hidden', { action: { label: 'Undo', onClick: undo } });
await ui.alert({ title: 'Saved', message: 'Kept on this device only.', tone: 'success' });
if (await ui.confirm({ title: 'Delete this note?', confirmLabel: 'Delete', danger: true })) remove();
const name = await ui.prompt({ title: 'Rename', value: 'Reading', validate: v => v.trim() ? null : 'Required' });
const dialog = ui.dialog({
title: 'Settings',
content: body => body.append(ui.h('label', { class: 'wb-check' }, ui.h('input', { type: 'checkbox' }), 'Hide trends')),
actions: [{ label: 'Cancel' }, { label: 'Save', tone: 'primary', default: true, onClick: save }],
});modal(options)is the primitive under all of them;dialogis arole="dialog",alert/confirmarerole="alertdialog". All arearia-modal, labelled by their title and described by their message.- Focus moves in, Tab/Shift+Tab wrap inside, and focus returns to where it was. Esc dismisses;
Enter in a single-line field (Ctrl/Cmd+Enter in a textarea) runs the
defaultaction. A destructiveconfirmstarts on Cancel. - Backdrop clicks dismiss alerts and confirmations only (a press that started in a field and ended on the backdrop does not count). Keys typed in a dialog never reach the page's own shortcuts.
- Dialogs stack (lower ones become
inert); toasts stack (four at most), pause while hovered or focused and are a polite live region (role="alert"for danger). prefers-reduced-motionturns off the animations. Text is always set as text;ui.h(tag, props, ...children)builds content withoutinnerHTML, string handlers orjavascript:URLs.- Classes for content:
wb-field,wb-label,wb-hint,wb-check,wb-input,wb-mono,wb-divider,wb-row,wb-muted; per-dialog CSS goes incss.
createUiKit(window, { theme }) builds a kit outside the plugin API; theme is a ThemeApi from
createThemeApi(window, source), where source is the site's { current(), onChange() }.
Themes
api.theme.current() returns the active theme resolved: colors (bg, surface, elevated,
border, text, muted, accent, onAccent, inverse, onInverse, page), radius,
buttonRadius, font, mode and siteDefault (true when the site's own colors show).
api.theme.onChange(fn) fires after it changes. The same values are CSS custom properties on
:root (--wb-bg, --wb-text, --wb-accent, --wb-radius, --wb-font, …), so plugin CSS can
use var(--wb-accent).
Theme files are JSON; theme.schema.json describes every field and its limits (the app clamps
out-of-range values when it imports a file). Point a theme at it for editor completion and checks:
{ "$schema": "https://unpkg.com/waybetter/theme.schema.json", "name": "Midnight Mint", "colors": { "bg": "#0b1110", "text": "#e8f3ef", "accent": "#3ddc97" } }or, in a project that installs the package, "$schema": "./node_modules/waybetter/theme.schema.json".
Build your own
npm create waybetter plugin # *.plugin.js (or --ts) using api.scope, storage, ui and theme
npm create waybetter theme # theme JSON with "$schema" pointing at theme.schema.json
npm create waybetter app myapp -- --site youtube.com # a new wayBetter app (Tauri 2 + waybetter-host)create-waybetter generates code against this
package's real API and schema; --help lists every option.
Projects using wayBetter
| Project | Target | What it is | | --- | --- | --- | | wayBetter for X | x.com (website) | Desktop app around the live X: tabs, split panes, real accounts side by side, themes, plugins, drafts and fixes for X's own bugs. The first wayBetter app. |
Built something on wayBetter? Add it to this list with a pull request: name and link, what it extends, one line on what it does.
License
GPL-3.0-or-later. See LICENSE.
