shell: the panel, createPanel(site) (moved from wayBetter for X)

packages/shell/src/panel (waybetter/shell/panel): an app's own tab. An app describes itself
(PanelSite: brand and the home page's words, its own sections and their order, its theme store,
catalog and the preview component a theme document is drawn in, Make's examples and docs, the
install controller, its languages, what its native side serves) and createPanel returns the
component for ShellSite.panel. It is in the shell package because it is drawn in the frame and
shares its stores, native bridge and language.

- Panel.tsx (header, settings search over every settings section, navigation, toasts; Accounts
  and Updates), nav.ts (which built-in sections an app gets: each shows only when the app serves
  what it needs; the app's sections in its order), Home.tsx (hero, highlights, About from the
  app's data), controls.tsx + panel.css.
- Appearance.tsx, ThemeBuilder.tsx + builderModel.ts + builder.css, fonts/ (FontBrowser, FontRoles,
  catalog, the offline Google Fonts list; scripts/google-fonts-catalog.mjs regenerates it):
  the theme documents are the runtime's, the store, catalog, accents, draft palette, file suffix
  and preview are the app's (PanelSite.themes).
- Plugins.tsx (the plugins list and custom CSS over the host's plugins_*/custom_css_*),
  StoreSection.tsx, MakePanel.tsx + makeModel.ts + MarkdownDoc.tsx + markdown.ts + make.css (the
  examples, the theme file and the docs are the app's), Settings.tsx (links and session over
  settings_get/set with `siteLinks`, keyword alerts, the media panel), FeatureToggles.tsx
  (useFeatures + FeatureGroups over features_get/set: the app names its flags and groups),
  PowerSettings.tsx (deck defaults from site.deck: its flags and presets; DeckPreset gets `name`
  and `hint`), BackupPanel.tsx + backup.css, DeveloperPanel.tsx, LanguageSettings.tsx, compare.ts.
- frame: language.ts (createLanguage: follow the site / the system / one language, i18n_set;
  moved from the app), appI18n.ts (createAppI18n: the app's typed i18n instance, useT, dates and
  numbers; the app passes React's useSyncExternalStore), formatDate in i18n.ts (ShellI18n.date).
- tests/panel.test.ts: every panel key has English and is used, sections per app, the builder
  model, the font catalog, plugin header rules, the UI kit demos, Markdown, compare.
- npm package: exports ./shell/panel, ./shell/panel/*, ./shell/panel/*.css; the build is
  scripts/build.mjs (the shell is compiled with the runtime sources it imports under one root and
  its imports are pointed at dist/runtime; stylesheets and the font list are copied).
This commit is contained in:
CRTkafa 2026-09-30 14:03:40 +07:00
parent 81ab5bd9fc
commit b2767a8e56
44 changed files with 4864 additions and 2 deletions

View file

@ -0,0 +1,187 @@
// What a wayBetter app tells the panel (its own tab: settings, themes, plugins, the Store, Make,
// accounts, backup, developer mode, updates): the brand and the home page's words, its theme
// catalog and preview, its own sections and what its native side serves. `createPanel(site)` sets
// it; every panel module reads it through `panelSite()`. The shell frame's site (`site()`,
// frame/site.ts) is read too: the deck (Power User), the media panel and the native features.
import type { ComponentType } from 'react';
import type { ThemeCatalog, ThemeChoice } from '../../../runtime/src/themeChoice.ts';
import type { FontChoice, ThemeColors, ThemeSpec } from '../../../runtime/src/themeSpec.ts';
import type { ResolvedTheme } from '../../../runtime/src/themeApi.ts';
import type { InstallController } from '../installController.ts';
import type { ThemeStore } from '../frame/themeStore.ts';
/** A short message at the bottom of the panel (`error`: stays longer, announced as an alert). */
export type Notify = (text: string, error?: boolean) => void;
/** What every section gets. */
export type SectionProps = {
notify: Notify;
/** Shows another section of the panel (built-in or the site's). */
openSection(id: string): void;
};
/** The sections the framework brings; each shows only when the app serves what it needs. */
export const BUILTIN_SECTIONS = ['home', 'themes', 'builder', 'plugins', 'store', 'make', 'accounts', 'power', 'settings', 'backup', 'developer', 'updates'] as const;
export type BuiltinSection = typeof BUILTIN_SECTIONS[number];
/** A section of the app's own (X: Tweaks, Drafts, My Space). */
export type PanelSection = {
id: string;
label(): string;
/** The nav icon: a 16x16 stroke path (`d`). */
icon: string;
component: ComponentType<SectionProps>;
/** Its rows also show in the settings search (they filter themselves with `useSettingsQuery`). */
searchable?: boolean;
};
/** The home page: the hero, the highlight cards and the About block, in the app's words. */
export type PanelHome = {
/** The line above the title: the brand name, then what it is for ("wayBetter" + " for X"). */
kicker: { name: string; rest: string };
/** The title in three parts; the middle one is emphasized. */
title(): [string, string, string];
lede(): string;
/** Chips after the version chip ("Free & open source", "GPL-3.0"). */
chips?(): string[];
/** Hero buttons: each opens a section; the first is the primary one. */
actions?: ReadonlyArray<{ section: string; label(): string }>;
/** Cards under the hero: each opens a section and carries that section's icon. */
highlights?: ReadonlyArray<{ section: string; title(): string; text(): string }>;
about?: {
title(): string;
text(): string;
/** A numbered list (the framework's layers). */
layers?: ReadonlyArray<{ name(): string; text(): string }>;
/** "Made by @someone · site": the link opens in the system browser. */
foot?: { text(): string; by: string; link?: { label: string; url: string } };
};
};
/** The app's themes in the panel: Appearance, the Theme Builder, font pickers, Make's theme view. */
export type PanelThemes = {
/** The app's theme store (the same one the frame gets as `site.themes`). */
store: ThemeStore<ThemeChoice, ThemeSpec>;
/** The site's built-in themes (`createThemeCatalog`); the first is the site's own look. */
catalog: ThemeCatalog;
/** Accent swatches offered in Appearance. */
accents: readonly string[];
/** A miniature of the site drawn from a theme document (the gallery, the builder, the install screen). */
Preview: ComponentType<{ theme: ThemeSpec; className?: string }>;
/** The name of the site's own typeface, shown where a font role keeps it ("X's Chirp"). */
siteFont: string;
/** Named font stacks offered besides Google Fonts (default: the runtime's `FONT_CHOICES`). */
fontChoices?: readonly FontChoice[];
/** The palette a draft starts from when a theme keeps the site's own colors. */
draftColors: Pick<ThemeColors, 'bg' | 'text' | 'accent'>;
/** Swatches for the builder's three base colors (defaults are neutral). */
baseSwatches?: { bg: string[]; text: string[]; accent: string[] };
/** File name suffix of an exported theme (default `.theme.json`). */
fileSuffix?: string;
/** The builder's chrome preview: tab titles and the address chip. */
chromePreview?: { tabs: [string, string, string]; chip: string; page: string };
/**
* Make's theme view: the active theme as plugins see it. `modes` are the site's own display modes
* (X: Default, Dim, Lights out), offered while the theme keeps the site's colors.
*/
resolved?(choice: ThemeChoice, mode: string): ResolvedTheme;
modes?: { label(): string; initial: string; options: ReadonlyArray<{ value: string; label(): string }> };
};
/** A plugin example in Make's editor. */
export type PluginExample = { id: string; title: string; blurb: string; file: string; source: string };
/** Make: example plugins, a theme file and the bundled docs, all the app's. */
export type PanelMake = {
examples: readonly PluginExample[];
/** An example theme file (JSON text) and the name it downloads as. */
themeExample?: { file: string; source: string };
/** Bundled Markdown docs by file name; the first is opened by default. */
docs?: Readonly<Record<string, string>>;
/** The page runtime's global in the code samples (default `wayBetter`). */
global?: string;
};
/** A feature flag group for `FeatureToggles` (the app's page tweaks). */
export type FeatureGroup = { id: string; features: readonly string[] };
/** One language the app has a catalog for (the app's `languages.json`). */
export type PanelLanguage = {
code: string;
name: string;
englishName: string;
dir: 'ltr' | 'rtl';
status: 'source' | 'machine' | 'reviewed';
coverage: number;
};
/** The app's language state (frame/language.ts `createLanguage`). */
export type PanelLanguages = {
list: readonly PanelLanguage[];
state(): { preference: string; siteLang: string | null; system: readonly string[]; locale: string };
subscribe(listener: () => void): () => void;
set(preference: string): Promise<void>;
/** The contributor guide ("Help translate"), opened in the system browser. */
translateUrl?: string;
};
export type PanelSite = {
/** The header: the brand's name and logo (an image URL). */
brand: { name: string; logo: string };
home: PanelHome;
/** The app's version for the home page's chip. */
version?(): Promise<string>;
/** The app's own sections. */
sections?: readonly PanelSection[];
/** Nav order (built-in and site ids). Default: the built-in ones, then the site's. */
order?: readonly string[];
/** Themes (Appearance, Theme Builder); omit for an app without them. */
themes?: PanelThemes;
/** The install screen's controller: the Store section, and removing a plugin from the list. */
store?: { controller: InstallController<ThemeSpec> };
make?: PanelMake;
/** The language picker in Settings; omit for an app in one language. */
languages?: PanelLanguages;
/** Settings: keyword alerts (`alerts_get/set`) and what the app adds under the framework's rows. */
settings?: {
alerts?: boolean;
extra?: ReadonlyArray<ComponentType<SectionProps>>;
/** Extra search words for the "links to the site's pages" row ("x twitter"). */
linkKeywords?: string;
};
/** Placeholder CSS in the editors, written with the site's own selectors: custom CSS, a theme's CSS, the builder's. */
cssExamples?: { custom?: string; theme?: string; builder?: string };
/** Power User (with `site.deck`): what the settings list besides the frame's own. */
power?: {
/** Column flags offered as deck defaults (default: the flags `site.deck.defaultFlags` names). */
defaultFlags?: readonly string[];
/** What one column was measured to take, in MB (shown with the columns-per-page guard). */
memory?: { perColumn: number; range: string };
/** A line above the deck keys (which of the site's own keys keep working in a column). */
keysHint?(): string;
};
/** Backup & export (`backup_*`); the sync part needs the frame's `sync` feature. */
backup?: boolean;
/** The native updater (`app_ready`, `update_status`, `apply_update`). */
updates?: boolean;
};
let current: PanelSite | null = null;
/** Sets the panel's site (createPanel). */
export function configurePanel(value: PanelSite) {
current = value;
}
export function panelSite(): PanelSite {
if (!current) throw new Error('wayBetter panel: createPanel(site) first');
return current;
}
/** The app's themes (Appearance, the builder and the font pickers only render with them). */
export function panelThemes(): PanelThemes {
const themes = panelSite().themes;
if (!themes) throw new Error('wayBetter panel: this app has no themes (PanelSite.themes)');
return themes;
}