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:
parent
892de6cbf4
commit
068168469c
49 changed files with 6442 additions and 6 deletions
170
packages/shell/src/frame/site.ts
Normal file
170
packages/shell/src/frame/site.ts
Normal 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'));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue