shell: the shell frame, createShell(site) (moved from wayBetter for X)

packages/shell/src/frame (waybetter/shell/frame): the whole shell of an app. An app describes its
site (ShellSite: names and pages, account rules, search, address chips and completions, its panel
tab, the new-tab page, palette items, deck column kinds and flags and an optional compose dock, the
media panel's composer rule, a theme store, overlays, the native features it serves) and passes
its Tauri API and i18n instance; createShell returns the root component.

- Shell.tsx: dockview host (tabs in the titlebar row, splits, drag-link drops, shortcuts, context
  menu, deck back-and-forth, session restore, workspace on start, overlay raise); state.ts (pane
  placeholders, tabs_sync one-in-flight via syncGate, shell_raise layer, profiles, layout).
- WebPanel (address bar), NewTabPage, CommandPalette + paletteModel (frame groups; site items),
  DeckView + AddColumnDialog + deckStore (site column kinds), PaneTheme + themeStore
  (createThemeStore over the app's ThemeAdapter) + chromeTheme (ChromeTheme input) + themeScopes,
  Titlebar + TitlebarExtras + icons + brand (wB mark data URI), AccountSwitcher + accountModel,
  workspaces + workspaceModel, backupModel, syncHost, devHost + devModel, pluginCommandStore,
  MediaPanel + mediaModel, searchInput, hooks, bridge (installPreviewMocks), i18n, boot.ts
  (bootScript: the first-frame boot.js), CSS themed with --wb-* tokens.
- paneModel: paneVisibility, raiseSupported, cssRgb; deckModel: isSourcePage.
- Includes the live-QA titlebar fix (dock ResizeObserver + rAF titlebar marks, dock sized by its
  insets).
- tests/frame.test.ts: every frame key has English and is used, palette order with a site,
  address fallback, layout gate, boot script, chrome variables, switcher rules, workspaces,
  deck settings with site flags.
- npm package 0.4.0: exports ./shell/frame, ./shell/frame/*, ./shell/frame/*.css (copied by the
  build); dockview and acorn optional peers; root devDependencies dockview, acorn.
This commit is contained in:
CRTkafa 2026-09-29 12:47:10 +07:00
parent 892de6cbf4
commit 068168469c
49 changed files with 6442 additions and 6 deletions

View file

@ -0,0 +1,170 @@
// What a wayBetter app tells the shell frame about its site: names, pages, the address bar chips,
// the new-tab page, the palette's site items, deck column kinds, the app's own panel and the
// overlays it adds. `createShell(site)` sets it; every frame module reads it through `site()`.
// Everything here is data or small functions; the frame owns the behavior (tabs, splits, the
// titlebar, overlays, layout sync, workspaces, sync, developer mode).
import type { ComponentType } from 'react';
import type { AddressModel } from '../addressModel.ts';
import type { ColumnSource, DeckColumn } from '../deckModel.ts';
import type { AccountRules } from '../profileModel.ts';
import type { IconName } from './icons.tsx';
import type { PaletteItem } from './paletteModel.ts';
import type { ThemeStore } from './themeStore.ts';
/** An inline completion for a text field (the address bar, a column's value). */
export type Suggestion = { label: string; insert: string; hint: string };
/** Completions for what is being typed and how accepting one rewrites the text. */
export type Completer = {
/** Up to `max` completions for the token at `caret`. */
suggest(text: string, caret: number, max: number): Suggestion[];
apply(text: string, caret: number, insert: string): { text: string; caret: number };
/** Accessible name of the chips ("X search operators"). */
label(): string;
};
/** One kind of deck column (Home, Notifications, a search, a profile, ...). */
export type ColumnKind = {
kind: string;
label: string;
icon: IconName;
hint: string;
/** What the value field asks for (none = no value). */
value?: { label: string; placeholder: string };
};
/** A deck column flag the page applies (a site tweak), with its label for settings. */
export type ColumnFlag = { key: string; label: string; hint: string };
/** A ready-made set of columns ("Monitor", "Two accounts"). */
export type DeckPreset = {
id: string;
label(): string;
/** Disabled (with this hint) when only one account exists. */
needsTwoAccounts?: () => string;
columns(profiles: readonly string[]): Array<Pick<DeckColumn, 'profile' | 'source'>>;
};
export type DeckSite = {
kinds: readonly ColumnKind[];
/** The page a column loads, or null when its value is not valid for its kind. */
urlFor(source: ColumnSource): string | null;
/** Header title and subtitle for a column. */
title(source: ColumnSource, custom?: string): { title: string; detail?: string };
/** The typed value → the stored value, or null when it is not valid for that kind. */
parseValue(kind: string, input: string): string | null;
/** Why a typed value is not valid for `kind` (shown under the field). */
invalid(kind: string): string;
/** Completions in a kind's value field (e.g. search operators for a search column). */
completer?(kind: string): Completer | null;
/** Flags a column may override (listed in settings), and flags the page accepts that are not listed. */
flags: readonly ColumnFlag[];
extraFlags?: readonly string[];
/** Flags every column starts with. */
defaultFlags: Record<string, boolean>;
/** Flags a column's kind always sets (e.g. X's Following column selects X's own Following tab). */
kindFlags?(source: ColumnSource): Record<string, boolean>;
presets?: readonly DeckPreset[];
/** A page docked at the right of a deck to write in (X: its composer). */
compose?: {
url: string;
width: number;
/** The docked page's chrome and placeholder (it registers its placeholder with the frame). */
Dock: ComponentType<{ deckId: string; left: number }>;
/** The dock's webview reported a page (e.g. load a fresh composer once one was sent). */
navigated?(tabId: string, url: string | undefined, loading: boolean | undefined): void;
};
/** A page title without the site's suffix (" / X"). */
pageTitle?(title: string): string;
};
export type PaletteSite = {
/** The field's placeholder. */
placeholder(): string;
/** The literal search for the query (listed first; kept as the last resort when ranking). */
search?(text: string): PaletteItem | null;
/** Completions ranked above everything else while typing (they rewrite the query). */
completions?(text: string): PaletteItem[];
/** Sections of the app's panel ("Themes", "Plugins", ...). */
sections: ReadonlyArray<{ id: string; label(): string; keywords: string; icon: IconName }>;
/** The site's own pages and actions (after the tabs, decks and the app's items). */
items?(): PaletteItem[];
/** Heading for a site group (`PaletteItem.group`); the frame names its own groups. */
groupLabel?(group: string): string | undefined;
};
export type ShellSite = {
/** The site's name in messages ("X"). */
name: string;
/** The app's name ("wayBetter for X"): the update chip, the brand button, the palette's group. */
product: string;
/** The app's logo (an image URL): titlebar, the panel tab, the new-tab page, the site chip. */
logo: string;
/** The site's home feed: the first tab, and what an account opens beside. */
homeUrl: string;
/** The site's own sign-in and sign-out pages (accounts are signed in and out on the site). */
loginUrl: string;
logoutUrl: string;
/** A page of the site itself (https, the site's hosts). */
isSiteUrl(url: string): boolean;
/** Account handle and avatar rules (as the native `AccountRules`). */
accounts: AccountRules;
/** Text typed in the address bar or the new-tab page that is not an address: the site's search. */
searchUrl(text: string): string;
/** Address bar chips for a URL (null: show the raw address). */
address(url: string): AddressModel | null;
/** Completions while the address is edited. */
addressCompleter?: Completer;
/** The app's own tab: its settings panel, opened at a section (`openPanelSection`). */
panel: {
/** Tab id (kept in saved layouts and workspaces). */
id: string;
title: string;
component: ComponentType<{ section?: { name: string; nonce: number } }>;
};
newTab: {
/** The search field's placeholder. */
search(): string;
/** Quick links (their `url` gets the `homeUrl` setting). */
links: ReadonlyArray<{ id: string; label(): string; icon: IconName; url(homeUrl: string): string }>;
};
palette: PaletteSite;
/** Decks of site columns (Power User Mode); omit for none. */
deck?: DeckSite;
/** Gallery and clipboard panel: which tabs have a composer to attach to; omit for none. */
media?: { canAttach(url: string): boolean };
/** The app's theme store (createThemeStore): chrome and pane themes; omit to keep the frame's own style. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
themes?: ThemeStore<any, any>;
/**
* Native features the app serves (waybetter-host modules it wires up): workspaces (`workspaces_*`),
* encrypted folder sync (`sync_*`), developer mode (`dev_*`) and plugin palette commands
* (`tab_run_command`). Each is on unless set to false; off, the frame shows nothing of it.
*/
features?: { workspaces?: boolean; sync?: boolean; developer?: boolean; pluginCommands?: boolean };
/** Components rendered on top of the shell (they listen to their own events). */
overlays?: ReadonlyArray<ComponentType>;
/** Called once the dock exists (read site settings, listen to site events). Returns a cleanup. */
start?(): (() => void) | undefined;
};
let current: ShellSite | null = null;
/** Sets the site (createShell). */
export function configureSite(value: ShellSite) {
current = value;
}
export function site(): ShellSite {
if (!current) throw new Error('wayBetter shell: createShell(site) first');
return current;
}
/** Whether the app serves a native feature (`site.features`, on by default). */
export const hasFeature = (name: keyof NonNullable<ShellSite['features']>) => site().features?.[name] !== false;
/** The site's runtime text changed (X: the Twitter/X wording): address bars and new-tab pages re-render. */
export function siteChanged() {
window.dispatchEvent(new CustomEvent('bt-site'));
}