@gjsify/adwaita-storybook 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/types/app.d.ts +14 -9
- package/lib/types/appearance.d.ts +15 -0
- package/lib/types/story-element.d.ts +5 -12
- package/lib/types/styles.d.ts +1 -1
- package/lib/types/types.d.ts +1 -5
- package/package.json +6 -5
- package/src/app.ts +50 -35
- package/src/appearance.ts +182 -0
- package/src/controls.ts +7 -11
- package/src/index.ts +11 -16
- package/src/registry.ts +2 -6
- package/src/story-element.ts +12 -26
- package/src/styles.ts +83 -4
- package/src/types.ts +1 -5
package/lib/types/app.d.ts
CHANGED
|
@@ -17,6 +17,15 @@ export declare class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
17
17
|
private _controller;
|
|
18
18
|
private _options;
|
|
19
19
|
private _container;
|
|
20
|
+
/**
|
|
21
|
+
* Colour scheme + accent for the whole storybook, not for one story.
|
|
22
|
+
*
|
|
23
|
+
* Scoped to the storybook's own container rather than `documentElement`: this
|
|
24
|
+
* app is EMBEDDED by the website, and stamping `.theme-dark` on the document
|
|
25
|
+
* would re-theme the host page around it. `_theme.scss` scopes on the class
|
|
26
|
+
* wherever it sits, so a subtree works.
|
|
27
|
+
*/
|
|
28
|
+
private _appearance;
|
|
20
29
|
private _listEl;
|
|
21
30
|
private _previewEl;
|
|
22
31
|
private _previewTitle;
|
|
@@ -29,18 +38,14 @@ export declare class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
29
38
|
/** Build the UI, instantiate stories, and select the first one. */
|
|
30
39
|
mount(): void;
|
|
31
40
|
/**
|
|
32
|
-
* Adapt the two split views to the available width, the way a desktop
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* on-demand overlay, so the sidebar + preview keep their room (this is
|
|
37
|
-
* what a medium embed — e.g. the docs page — wants);
|
|
38
|
-
* - below {@link NAV_BREAKPOINT} there isn't room for the sidebar either,
|
|
39
|
-
* so the whole thing goes single-pane and navigates one column at a time.
|
|
41
|
+
* Adapt the two split views to the available width, the way a desktop Adwaita
|
|
42
|
+
* window sheds panes as it narrows: below {@link CONTROLS_BREAKPOINT} the controls
|
|
43
|
+
* panel folds into an on-demand overlay so sidebar + preview keep their room, and
|
|
44
|
+
* below {@link NAV_BREAKPOINT} the sidebar goes too and nav is single-pane.
|
|
40
45
|
*/
|
|
41
46
|
private _observeBreakpoint;
|
|
42
47
|
private _applyBreakpoint;
|
|
43
|
-
/** The currently-displayed story, or null. */
|
|
48
|
+
/** The currently-displayed story, or null. Also the MCP surface via `window.__storybook`. */
|
|
44
49
|
get activeStory(): StoryElement | null;
|
|
45
50
|
/** Every story flattened to a summary. */
|
|
46
51
|
listStories(): StorySummary[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StorybookSettings } from '@gjsify/storybook-core';
|
|
2
|
+
/** Owns the browser side of {@link StorybookSettings}: the theme scope and the accent. */
|
|
3
|
+
export declare class StorybookWebAppearance {
|
|
4
|
+
readonly settings: StorybookSettings;
|
|
5
|
+
private readonly _root;
|
|
6
|
+
constructor(root?: HTMLElement);
|
|
7
|
+
private _apply;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The appearance dialog: the colour scheme, and the accent behind a switch.
|
|
11
|
+
*
|
|
12
|
+
* Built once and reused — unlike the GTK `Adw.Dialog`, `<adw-preferences-dialog>`
|
|
13
|
+
* reopens after a dismissal, so there is nothing to rebuild.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildAppearanceDialog(appearance: StorybookWebAppearance): HTMLElement;
|
|
@@ -5,24 +5,17 @@ export type StoryArgsListener = (args: StoryArgs) => void;
|
|
|
5
5
|
/**
|
|
6
6
|
* Base class for browser story elements.
|
|
7
7
|
*
|
|
8
|
-
* Provides default chrome — a clamped boxed group with a title + description
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* {@link addContent} is a no-op).
|
|
8
|
+
* Provides default chrome — a clamped boxed group with a title + description above a
|
|
9
|
+
* centered, dashed-framed preview stage. Simple stories compose their preview by
|
|
10
|
+
* calling {@link addContent}; a subclass needing a bespoke layout passes its own root
|
|
11
|
+
* element to the constructor, and {@link addContent} is then a no-op.
|
|
13
12
|
*/
|
|
14
13
|
export declare class StoryElement extends StoryViewBase<HTMLElement> {
|
|
15
14
|
/**
|
|
16
|
-
* @param meta The story's metadata (the same renderer-agnostic object
|
|
17
|
-
* its GTK twin uses).
|
|
18
|
-
* @param story Variant name (defaults to "Default").
|
|
19
15
|
* @param customRoot Opt out of the default chrome by supplying a root element.
|
|
20
16
|
*/
|
|
21
17
|
constructor(meta: StoryMeta, story?: string, customRoot?: HTMLElement);
|
|
22
|
-
/**
|
|
23
|
-
* The root node inserted into the storybook's preview area. Alias for the
|
|
24
|
-
* base's {@link root} getter — kept because consumers reference `.element`.
|
|
25
|
-
*/
|
|
18
|
+
/** Alias for the base's {@link root} getter — kept because consumers use `.element`. */
|
|
26
19
|
get element(): HTMLElement;
|
|
27
20
|
/** Build the @gjsify/adwaita-web DOM chrome — the single renderer seam. */
|
|
28
21
|
protected createChrome(_meta: StoryMeta, customRoot?: HTMLElement): StoryChrome<HTMLElement>;
|
package/lib/types/styles.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const STORYBOOK_WEB_CSS = "\n.sb-window {\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n}\n\n/* Each pane in the outer split view is a flex column that fills its height. */\n.sb-sidebar-pane,\n.sb-content-pane {\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n flex: 1 1 auto;\n}\n\n/* The toolbar-view content holds either a scroller (sidebar) or the split view\n (content); the inner panes own the scrolling, so this level never scrolls. */\n.sb-sidebar-pane adw-toolbar-view > .adw-toolbar-view-content,\n.sb-content-pane adw-toolbar-view > .adw-toolbar-view-content {\n overflow: hidden;\n}\n\n/* Background scheme: the content header + preview + controls all sit on the\n window (story) background as one surface; only the left story-list column\n (its header + list) keeps the distinct sidebar shade. Matches the native\n renderer. The default adw-header-bar background is its own headerbar shade,\n so override it per pane here. */\n.sb-content-pane adw-header-bar {\n background-color: var(--window-bg-color);\n}\n.sb-sidebar-pane adw-header-bar {\n background-color: var(--sidebar-bg-color);\n}\n\n.sb-sidebar-scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n background-color: var(--sidebar-bg-color);\n}\n\n/* --- Story list (mirrors GTK .navigation-sidebar) --- */\n.sb-sidebar-list {\n padding: var(--spacing-xs);\n display: flex;\n flex-direction: column;\n gap: 1px;\n}\n\n.sb-category {\n font-size: var(--font-size-small);\n font-weight: 700;\n opacity: var(--dim-opacity);\n padding: 10px var(--spacing-m) 4px;\n}\n\n.sb-story-row {\n padding: var(--spacing-xs) var(--spacing-m);\n margin: 0 var(--spacing-xs);\n border-radius: var(--button-radius);\n cursor: pointer;\n color: var(--window-fg-color);\n font-size: var(--font-size-base);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sb-story-row:hover {\n background-color: var(--button-bg-color);\n}\n\n.sb-story-row.selected {\n background-color: var(--accent-bg-color);\n color: var(--accent-fg-color);\n}\n\n/* --- Preview area --- */\n.sb-preview-scroll {\n flex: 1 1 auto;\n min-height: 0;\n min-width: 0;\n overflow: auto;\n background-color: var(--window-bg-color);\n}\n\n.sb-story-page {\n display: flex;\n justify-content: center;\n padding: var(--spacing-xl) var(--spacing-m);\n min-height: 100%;\n box-sizing: border-box;\n}\n\n.sb-story-clamp {\n width: 100%;\n max-width: 600px;\n}\n\n.sb-story-group-header {\n padding: var(--spacing-xs) 2px;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sb-story-group-title {\n font-size: var(--font-size-heading);\n font-weight: 700;\n color: var(--window-fg-color);\n}\n\n.sb-story-group-description {\n font-size: var(--font-size-base);\n opacity: var(--dim-opacity);\n color: var(--window-fg-color);\n}\n\n/* Centered, dashed-framed preview stage (mirrors the native .story-stage). */\n.story-stage {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-m);\n padding: var(--spacing-l);\n margin-top: var(--spacing-s);\n border: 1px dashed rgba(127, 127, 127, 0.28);\n border-radius: var(--card-radius);\n min-height: 80px;\n}\n\n/* --- Controls panel --- */\n.sb-controls-scroll {\n height: 100%;\n overflow-y: auto;\n background-color: var(--window-bg-color);\n}\n\n.sb-controls-page {\n padding: var(--spacing-l);\n}\n\n/* Range-control card (mirrors the native .story-range-row). */\n.sb-range-row {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-xs);\n padding: var(--spacing-m);\n border-bottom: 1px solid var(--card-shade-color);\n}\n\n.sb-range-row:last-child {\n border-bottom: none;\n}\n\n.sb-range-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: var(--spacing-m);\n}\n\n.sb-range-title {\n font-size: var(--font-size-base);\n font-weight: 700;\n color: var(--card-fg-color);\n}\n\n.sb-range-value {\n font-size: var(--font-size-base);\n font-variant-numeric: tabular-nums;\n opacity: var(--dim-opacity);\n}\n\n.sb-range-description {\n font-size: var(--font-size-small);\n opacity: var(--dim-opacity);\n}\n\n.sb-range-input {\n width: 100%;\n accent-color: var(--accent-bg-color);\n}\n\n.sb-color-input {\n width: 42px;\n height: 28px;\n padding: 0;\n border: 1px solid var(--card-shade-color);\n border-radius: var(--button-radius);\n background: none;\n cursor: pointer;\n}\n";
|
|
1
|
+
export declare const STORYBOOK_WEB_CSS = "\n/* Appearance dialog \u2014 round swatches with a selection ring. The web twin of the GTK\n renderer's rules, themselves adapted from Learn6502.\n\n The CARD is not decoration: in the dark scheme the \"dark\" swatch is a near-black\n circle on a near-black dialog and is invisible without a lighter surface behind\n it. Centring happens INSIDE the full-width card, so the row does not read as\n left-aligned under the group title. */\n.sb-swatch-card {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 8px;\n padding: 12px;\n}\n/* Squaring the card's top alone leaves a notch: the row above lives in the group's\n own rounded list, so BOTH sides have to be squared for the join to read as one\n list. The attached card and the group rule below are the two halves of that. */\n.sb-swatch-card--attached {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.sb-attached-group .boxed-list {\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n}\n/* Desaturated rather than merely dimmed: while the accent follows the desktop the\n palette must read as unavailable, not as nine slightly faded choices. */\n.sb-swatch-card--disabled {\n filter: saturate(0.4) brightness(1.05);\n opacity: 0.7;\n}\n/* A radio input, so grouping and arrow-key navigation are the platform's. Its own\n glyph is switched off because the swatch IS the indicator \u2014 a dot drawn on a\n colour competes with the ring that already says which one is selected. */\n.sb-swatch {\n appearance: none;\n -webkit-appearance: none;\n margin: 0;\n width: 24px;\n height: 24px;\n border-radius: 9999px;\n box-shadow: 0 0 0 3px transparent;\n cursor: pointer;\n /* A hairline on the fill itself. Without it the LIGHT scheme swatch is a\n near-white circle on a near-white card and vanishes \u2014 the exact mirror of the\n dark swatch on a dark dialog, and a card alone only fixes one direction.\n --border-color is a currentColor mix, so it holds in both schemes. */\n border: 1px solid var(--border-color);\n}\n.sb-swatch:checked {\n box-shadow: 0 0 0 3px var(--accent-bg-color);\n}\n.sb-swatch:focus-visible {\n outline: 2px solid var(--accent-color);\n outline-offset: 3px;\n}\n.sb-swatch:disabled {\n cursor: default;\n}\n/* The scheme swatches are larger \u2014 they are the dialog's primary choice. */\n.sb-swatch--scheme {\n width: 44px;\n height: 44px;\n}\n/* \"Follow system\" is the two schemes meeting on a diagonal, so the option looks like\n what it does instead of needing a word for it. */\n/* light_2 / dark_4 from libadwaita's palette (_palette.scss:37,44), which is what\n Learn6502's selector uses. NOT light_1 (#ffffff): pure white is invisible against\n a light card even with the hairline, and light_2 is the shade the palette offers\n for exactly this. */\n.sb-swatch--scheme-system {\n background: linear-gradient(to bottom right, #f6f5f4 49.99%, #241f31 50.01%);\n}\n.sb-swatch--scheme-light {\n background-color: #f6f5f4;\n}\n.sb-swatch--scheme-dark {\n background-color: #241f31;\n}\n\n.sb-window {\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n}\n\n/* Each pane in the outer split view is a flex column that fills its height. */\n.sb-sidebar-pane,\n.sb-content-pane {\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n flex: 1 1 auto;\n}\n\n/* The toolbar-view content holds either a scroller (sidebar) or the split view\n (content); the inner panes own the scrolling, so this level never scrolls. */\n.sb-sidebar-pane adw-toolbar-view > .adw-toolbar-view-content,\n.sb-content-pane adw-toolbar-view > .adw-toolbar-view-content {\n overflow: hidden;\n}\n\n/* Background scheme: the content header + preview + controls all sit on the\n window (story) background as one surface; only the left story-list column\n (its header + list) keeps the distinct sidebar shade. Matches the native\n renderer. The default adw-header-bar background is its own headerbar shade,\n so override it per pane here. */\n.sb-content-pane adw-header-bar {\n background-color: var(--window-bg-color);\n}\n.sb-sidebar-pane adw-header-bar {\n background-color: var(--sidebar-bg-color);\n}\n\n.sb-sidebar-scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n background-color: var(--sidebar-bg-color);\n}\n\n/* --- Story list (mirrors GTK .navigation-sidebar) --- */\n.sb-sidebar-list {\n padding: var(--spacing-xs);\n display: flex;\n flex-direction: column;\n gap: 1px;\n}\n\n.sb-category {\n font-size: var(--font-size-small);\n font-weight: 700;\n opacity: var(--dim-opacity);\n padding: 10px var(--spacing-m) 4px;\n}\n\n.sb-story-row {\n padding: var(--spacing-xs) var(--spacing-m);\n margin: 0 var(--spacing-xs);\n border-radius: var(--button-radius);\n cursor: pointer;\n color: var(--window-fg-color);\n font-size: var(--font-size-base);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sb-story-row:hover {\n background-color: var(--button-bg-color);\n}\n\n.sb-story-row.selected {\n background-color: var(--accent-bg-color);\n color: var(--accent-fg-color);\n}\n\n/* --- Preview area --- */\n.sb-preview-scroll {\n flex: 1 1 auto;\n min-height: 0;\n min-width: 0;\n overflow: auto;\n background-color: var(--window-bg-color);\n}\n\n.sb-story-page {\n display: flex;\n justify-content: center;\n padding: var(--spacing-xl) var(--spacing-m);\n min-height: 100%;\n box-sizing: border-box;\n}\n\n.sb-story-clamp {\n width: 100%;\n max-width: 600px;\n}\n\n.sb-story-group-header {\n padding: var(--spacing-xs) 2px;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sb-story-group-title {\n font-size: var(--font-size-heading);\n font-weight: 700;\n color: var(--window-fg-color);\n}\n\n.sb-story-group-description {\n font-size: var(--font-size-base);\n opacity: var(--dim-opacity);\n color: var(--window-fg-color);\n}\n\n/* Centered, dashed-framed preview stage (mirrors the native .story-stage). */\n.story-stage {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-m);\n padding: var(--spacing-l);\n margin-top: var(--spacing-s);\n border: 1px dashed rgba(127, 127, 127, 0.28);\n border-radius: var(--card-radius);\n min-height: 80px;\n}\n\n/* --- Controls panel --- */\n.sb-controls-scroll {\n height: 100%;\n overflow-y: auto;\n background-color: var(--window-bg-color);\n}\n\n.sb-controls-page {\n padding: var(--spacing-l);\n}\n\n/* Range-control card (mirrors the native .story-range-row). */\n.sb-range-row {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-xs);\n padding: var(--spacing-m);\n border-bottom: 1px solid var(--card-shade-color);\n}\n\n.sb-range-row:last-child {\n border-bottom: none;\n}\n\n.sb-range-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: var(--spacing-m);\n}\n\n.sb-range-title {\n font-size: var(--font-size-base);\n font-weight: 700;\n color: var(--card-fg-color);\n}\n\n.sb-range-value {\n font-size: var(--font-size-base);\n font-variant-numeric: tabular-nums;\n opacity: var(--dim-opacity);\n}\n\n.sb-range-description {\n font-size: var(--font-size-small);\n opacity: var(--dim-opacity);\n}\n\n.sb-range-input {\n width: 100%;\n accent-color: var(--accent-bg-color);\n}\n\n.sb-color-input {\n width: 42px;\n height: 28px;\n padding: 0;\n border: 1px solid var(--card-shade-color);\n border-radius: var(--button-radius);\n background: none;\n cursor: pointer;\n}\n";
|
|
2
2
|
/** Inject the chrome stylesheet once (idempotent, browser-only). */
|
|
3
3
|
export declare function injectStorybookStyles(): void;
|
package/lib/types/types.d.ts
CHANGED
|
@@ -8,11 +8,7 @@ export interface WebStoryConstructor {
|
|
|
8
8
|
}
|
|
9
9
|
/** Wraps a story's content-build step (e.g. to install a shared environment). */
|
|
10
10
|
export type WebStoryDecorator = (build: () => void, story: StoryElement) => void;
|
|
11
|
-
/**
|
|
12
|
-
* A group of related web stories, optionally wrapped by module-level decorators.
|
|
13
|
-
* The DOM binding of {@link StoryModuleLike} over {@link StoryElement} +
|
|
14
|
-
* {@link WebStoryConstructor}.
|
|
15
|
-
*/
|
|
11
|
+
/** A group of related web stories, optionally wrapped by module-level decorators. */
|
|
16
12
|
export interface WebStoryModule extends StoryModuleLike<StoryElement, WebStoryConstructor> {
|
|
17
13
|
stories: WebStoryConstructor[];
|
|
18
14
|
/** Populated by {@link StoryRegistry.createStoryInstances}. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/adwaita-storybook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Browser renderer for the @gjsify/stories contract — the web counterpart of @gjsify/storybook, built on @gjsify/adwaita-web",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -29,12 +29,13 @@
|
|
|
29
29
|
"browser"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@gjsify/adwaita-
|
|
33
|
-
"@gjsify/
|
|
34
|
-
"@gjsify/
|
|
32
|
+
"@gjsify/adwaita-core": "^0.38.0",
|
|
33
|
+
"@gjsify/adwaita-web": "^0.38.0",
|
|
34
|
+
"@gjsify/stories": "^0.38.0",
|
|
35
|
+
"@gjsify/storybook-core": "^0.38.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@gjsify/cli": "^0.
|
|
38
|
+
"@gjsify/cli": "^0.38.0",
|
|
38
39
|
"typescript": "^6.0.3"
|
|
39
40
|
},
|
|
40
41
|
"gjsify": {
|
package/src/app.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// @gjsify/storybook's StorybookWindow, built from @gjsify/adwaita-web custom
|
|
5
|
-
// elements so it looks and behaves like the native GTK storybook.
|
|
1
|
+
// The browser component browser — sidebar of stories by category, preview pane, and a
|
|
2
|
+
// controls panel of live-bound adwaita-web rows; the web counterpart of
|
|
3
|
+
// `@gjsify/storybook`'s `StorybookWindow`.
|
|
6
4
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// responsive breakpoint fold (_observeBreakpoint/_applyBreakpoint), and the
|
|
12
|
-
// window.__storybook MCP bridge (_exposeGlobal, delegating to the controller).
|
|
5
|
+
// `StorybookController` in `@gjsify/storybook-core` owns the app state machine. This
|
|
6
|
+
// class is the browser `StorybookView<StoryElement>` and owns ONLY the split-views
|
|
7
|
+
// chrome (`_buildUI`), the browser-only responsive fold (`_observeBreakpoint` /
|
|
8
|
+
// `_applyBreakpoint`) and the `window.__storybook` MCP bridge (`_exposeGlobal`).
|
|
13
9
|
|
|
14
10
|
import '@gjsify/adwaita-web'; // registers the custom elements + self-injects the adwaita stylesheet
|
|
15
11
|
import type { StoryArgValue } from '@gjsify/stories';
|
|
@@ -20,6 +16,7 @@ import {
|
|
|
20
16
|
type StorybookView,
|
|
21
17
|
type StorySummary,
|
|
22
18
|
} from '@gjsify/storybook-core';
|
|
19
|
+
import { buildAppearanceDialog, StorybookWebAppearance } from './appearance.js';
|
|
23
20
|
import { createControlRow } from './controls.js';
|
|
24
21
|
import type { StoryElement } from './story-element.js';
|
|
25
22
|
import { injectStorybookStyles } from './styles.js';
|
|
@@ -61,6 +58,15 @@ export class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
61
58
|
private _controller = new StorybookController<StoryElement>(this, (story) => this._buildControls(story));
|
|
62
59
|
private _options: StorybookWebOptions;
|
|
63
60
|
private _container: HTMLElement;
|
|
61
|
+
/**
|
|
62
|
+
* Colour scheme + accent for the whole storybook, not for one story.
|
|
63
|
+
*
|
|
64
|
+
* Scoped to the storybook's own container rather than `documentElement`: this
|
|
65
|
+
* app is EMBEDDED by the website, and stamping `.theme-dark` on the document
|
|
66
|
+
* would re-theme the host page around it. `_theme.scss` scopes on the class
|
|
67
|
+
* wherever it sits, so a subtree works.
|
|
68
|
+
*/
|
|
69
|
+
private _appearance: StorybookWebAppearance;
|
|
64
70
|
|
|
65
71
|
private _listEl!: HTMLElement;
|
|
66
72
|
private _previewEl!: HTMLElement;
|
|
@@ -75,6 +81,7 @@ export class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
75
81
|
constructor(container: HTMLElement, options: StorybookWebOptions) {
|
|
76
82
|
this._container = container;
|
|
77
83
|
this._options = options;
|
|
84
|
+
this._appearance = new StorybookWebAppearance(container);
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
/** Build the UI, instantiate stories, and select the first one. */
|
|
@@ -87,14 +94,10 @@ export class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
/**
|
|
90
|
-
* Adapt the two split views to the available width, the way a desktop
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* on-demand overlay, so the sidebar + preview keep their room (this is
|
|
95
|
-
* what a medium embed — e.g. the docs page — wants);
|
|
96
|
-
* - below {@link NAV_BREAKPOINT} there isn't room for the sidebar either,
|
|
97
|
-
* so the whole thing goes single-pane and navigates one column at a time.
|
|
97
|
+
* Adapt the two split views to the available width, the way a desktop Adwaita
|
|
98
|
+
* window sheds panes as it narrows: below {@link CONTROLS_BREAKPOINT} the controls
|
|
99
|
+
* panel folds into an on-demand overlay so sidebar + preview keep their room, and
|
|
100
|
+
* below {@link NAV_BREAKPOINT} the sidebar goes too and nav is single-pane.
|
|
98
101
|
*/
|
|
99
102
|
private _observeBreakpoint(): void {
|
|
100
103
|
const apply = (): void => this._applyBreakpoint();
|
|
@@ -115,18 +118,14 @@ export class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
115
118
|
const osv = this._controlsSplit as HTMLElement & { collapsed: boolean; showSidebar: boolean };
|
|
116
119
|
if (nav.collapsed !== navCollapsed) nav.collapsed = navCollapsed;
|
|
117
120
|
if (osv.collapsed !== controlsCollapsed) osv.collapsed = controlsCollapsed;
|
|
118
|
-
// Single-pane nav shows the story list first
|
|
119
|
-
//
|
|
121
|
+
// Single-pane nav shows the story list first, as native does; the back button
|
|
122
|
+
// only appears once a story is pushed onto the content pane.
|
|
120
123
|
this._navSplit.removeAttribute('show-content');
|
|
121
124
|
this._backBtn.hidden = true;
|
|
122
|
-
// The controls panel docks only when there's room; otherwise it's an
|
|
123
|
-
// on-demand overlay so the sidebar + preview keep their space.
|
|
124
125
|
osv.showSidebar = !controlsCollapsed;
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
/** The currently-displayed story, or null. */
|
|
128
|
+
/** The currently-displayed story, or null. Also the MCP surface via `window.__storybook`. */
|
|
130
129
|
get activeStory(): StoryElement | null {
|
|
131
130
|
return this._controller.activeStory;
|
|
132
131
|
}
|
|
@@ -151,8 +150,6 @@ export class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
151
150
|
return this._controller.setActiveArg(name, value);
|
|
152
151
|
}
|
|
153
152
|
|
|
154
|
-
// --- StorybookView<StoryElement> render seams (driven by the controller) ---
|
|
155
|
-
|
|
156
153
|
renderSidebar(groups: Array<CategoryGroup<StoryElement>>, onSelect: (instance: StoryElement) => void): void {
|
|
157
154
|
this._listEl.replaceChildren();
|
|
158
155
|
this._rowByTitle.clear();
|
|
@@ -217,7 +214,6 @@ export class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
217
214
|
}
|
|
218
215
|
|
|
219
216
|
private _buildUI(): void {
|
|
220
|
-
// --- Sidebar pane: header + scrollable story list ---
|
|
221
217
|
this._listEl = h('div', { class: 'sb-sidebar-list navigation-sidebar' });
|
|
222
218
|
const sidebarScroll = h('div', { class: 'sb-sidebar-scroll' }, [this._listEl]);
|
|
223
219
|
const sidebarHeader = h('adw-header-bar', { slot: 'top' }, [
|
|
@@ -227,7 +223,6 @@ export class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
227
223
|
h('adw-toolbar-view', {}, [sidebarHeader, sidebarScroll]),
|
|
228
224
|
]);
|
|
229
225
|
|
|
230
|
-
// --- Content pane: preview header + (preview | controls) split ---
|
|
231
226
|
this._previewEl = h('div', { class: 'sb-preview-scroll' });
|
|
232
227
|
|
|
233
228
|
this._controlsGroup = h('adw-preferences-group', { title: 'Controls' });
|
|
@@ -249,7 +244,6 @@ export class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
249
244
|
|
|
250
245
|
this._controlsSplit = controlsSplit;
|
|
251
246
|
|
|
252
|
-
// Back button — only shown when collapsed, returns to the story list.
|
|
253
247
|
this._backBtn = h('button', {
|
|
254
248
|
class: 'adw-header-btn sb-back-btn',
|
|
255
249
|
slot: 'start',
|
|
@@ -273,13 +267,34 @@ export class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
273
267
|
controlsSplit as HTMLElement & { showSidebar: boolean }
|
|
274
268
|
).showSidebar;
|
|
275
269
|
});
|
|
276
|
-
|
|
270
|
+
// Appearance is a property of the STORYBOOK, not of any story — every story
|
|
271
|
+
// is previewed under it — so it belongs in the chrome, not in the controls.
|
|
272
|
+
const appearanceBtn = h('button', {
|
|
273
|
+
class: 'adw-header-btn sb-appearance-btn',
|
|
274
|
+
slot: 'end',
|
|
275
|
+
title: 'Appearance',
|
|
276
|
+
'aria-label': 'Appearance',
|
|
277
|
+
});
|
|
278
|
+
appearanceBtn.append(h('span', { class: 'adw-icon adw-icon--preferences-system' }));
|
|
279
|
+
const appearanceDialog = buildAppearanceDialog(this._appearance);
|
|
280
|
+
appearanceBtn.addEventListener('click', () => {
|
|
281
|
+
// Mounted on first press: a dialog nobody opens should not sit in the
|
|
282
|
+
// tree of every storybook that never asks for it.
|
|
283
|
+
if (!appearanceDialog.isConnected) this._container.append(appearanceDialog);
|
|
284
|
+
appearanceDialog.toggleAttribute('open', true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const previewHeader = h('adw-header-bar', { slot: 'top' }, [
|
|
288
|
+
this._backBtn,
|
|
289
|
+
this._previewTitle,
|
|
290
|
+
appearanceBtn,
|
|
291
|
+
toggleControls,
|
|
292
|
+
]);
|
|
277
293
|
|
|
278
294
|
const contentPane = h('div', { slot: 'content', class: 'sb-content-pane' }, [
|
|
279
295
|
h('adw-toolbar-view', {}, [previewHeader, controlsSplit]),
|
|
280
296
|
]);
|
|
281
297
|
|
|
282
|
-
// --- Outer split + window ---
|
|
283
298
|
this._navSplit = h('adw-navigation-split-view', { 'min-sidebar-width': '220', 'max-sidebar-width': '320' }, [
|
|
284
299
|
sidebarPane,
|
|
285
300
|
contentPane,
|
|
@@ -289,8 +304,8 @@ export class StorybookWebApp implements StorybookView<StoryElement> {
|
|
|
289
304
|
}
|
|
290
305
|
|
|
291
306
|
private _exposeGlobal(): void {
|
|
292
|
-
//
|
|
293
|
-
//
|
|
307
|
+
// A control surface for the host browser's MCP `eval_js`, so stories can be
|
|
308
|
+
// driven without an in-page devtools channel.
|
|
294
309
|
(globalThis as unknown as { __storybook?: unknown }).__storybook = {
|
|
295
310
|
listStories: () => this.listStories(),
|
|
296
311
|
openStory: (title: string) => this.openStoryByTitle(title),
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Applying the storybook's appearance settings in the browser, and the dialog
|
|
2
|
+
// that drives them — the web twin of `@gjsify/storybook`'s `appearance.ts`.
|
|
3
|
+
//
|
|
4
|
+
// The model is `@gjsify/storybook-core`'s `StorybookSettings`, so this target and
|
|
5
|
+
// the GTK one agree on what a colour scheme and an accent ARE. Only the two halves
|
|
6
|
+
// below are per-target:
|
|
7
|
+
//
|
|
8
|
+
// scheme → `.theme-dark` / `.theme-light` on the root, which is what
|
|
9
|
+
// `_theme.scss` scopes on. NEITHER class means "follow the desktop",
|
|
10
|
+
// so `'system'` removes both and lets `prefers-color-scheme` decide.
|
|
11
|
+
// accent → the two custom properties, through `@gjsify/adwaita-web`'s
|
|
12
|
+
// `applyAdwaitaAccent`.
|
|
13
|
+
//
|
|
14
|
+
// Original implementation.
|
|
15
|
+
|
|
16
|
+
import { ADW_ACCENT_COLOR_NAMES, adwaitaAccentBgColor, type AdwAccentColorName } from '@gjsify/adwaita-core';
|
|
17
|
+
import { applyAdwaitaAccent, clearAdwaitaAccent } from '@gjsify/adwaita-web';
|
|
18
|
+
import { STORYBOOK_COLOR_SCHEMES, StorybookSettings, type StorybookColorScheme } from '@gjsify/storybook-core';
|
|
19
|
+
|
|
20
|
+
const SCHEME_LABELS: Record<StorybookColorScheme, string> = {
|
|
21
|
+
system: 'Follow system',
|
|
22
|
+
light: 'Light',
|
|
23
|
+
dark: 'Dark',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const ACCENT_LABELS: Record<AdwAccentColorName, string> = {
|
|
27
|
+
blue: 'Blue',
|
|
28
|
+
teal: 'Teal',
|
|
29
|
+
green: 'Green',
|
|
30
|
+
yellow: 'Yellow',
|
|
31
|
+
orange: 'Orange',
|
|
32
|
+
red: 'Red',
|
|
33
|
+
pink: 'Pink',
|
|
34
|
+
purple: 'Purple',
|
|
35
|
+
slate: 'Slate',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Owns the browser side of {@link StorybookSettings}: the theme scope and the accent. */
|
|
39
|
+
export class StorybookWebAppearance {
|
|
40
|
+
readonly settings: StorybookSettings;
|
|
41
|
+
private readonly _root: HTMLElement;
|
|
42
|
+
|
|
43
|
+
constructor(root: HTMLElement = document.documentElement) {
|
|
44
|
+
this._root = root;
|
|
45
|
+
const query = globalThis.matchMedia?.('(prefers-color-scheme: dark)') ?? null;
|
|
46
|
+
|
|
47
|
+
this.settings = new StorybookSettings(() => query?.matches ?? false);
|
|
48
|
+
this.settings.subscribe(() => this._apply());
|
|
49
|
+
// Wired unconditionally — `systemChanged` no-ops unless the preference is
|
|
50
|
+
// `'system'`, so the guard lives in one place.
|
|
51
|
+
query?.addEventListener('change', () => this.settings.systemChanged());
|
|
52
|
+
|
|
53
|
+
this._apply();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private _apply(): void {
|
|
57
|
+
const scheme = this.settings.colorScheme;
|
|
58
|
+
// Both classes off is the "follow the desktop" state, not a third theme:
|
|
59
|
+
// `_theme.scss` applies its dark palette from `prefers-color-scheme` when
|
|
60
|
+
// no explicit scope is present.
|
|
61
|
+
this._root.classList.toggle('theme-dark', scheme === 'dark');
|
|
62
|
+
this._root.classList.toggle('theme-light', scheme === 'light');
|
|
63
|
+
|
|
64
|
+
const accent = this.settings.resolvedAccent;
|
|
65
|
+
if (accent) applyAdwaitaAccent(accent, { target: this._root, dark: this.settings.resolvedDark });
|
|
66
|
+
else clearAdwaitaAccent(this._root);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A round swatch: a radio input, so grouping and keyboard nav come for free. */
|
|
71
|
+
function buildSwatch(name: string, value: string, label: string, classes: readonly string[]): HTMLInputElement {
|
|
72
|
+
const swatch = document.createElement('input');
|
|
73
|
+
swatch.type = 'radio';
|
|
74
|
+
swatch.name = name;
|
|
75
|
+
swatch.value = value;
|
|
76
|
+
swatch.title = label;
|
|
77
|
+
// A coloured circle has no text; without this a screen reader reads unnamed
|
|
78
|
+
// radios, and `title` is not an accessible name.
|
|
79
|
+
swatch.setAttribute('aria-label', label);
|
|
80
|
+
swatch.classList.add('sb-swatch', ...classes);
|
|
81
|
+
return swatch;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The appearance dialog: the colour scheme, and the accent behind a switch.
|
|
86
|
+
*
|
|
87
|
+
* Built once and reused — unlike the GTK `Adw.Dialog`, `<adw-preferences-dialog>`
|
|
88
|
+
* reopens after a dismissal, so there is nothing to rebuild.
|
|
89
|
+
*/
|
|
90
|
+
export function buildAppearanceDialog(appearance: StorybookWebAppearance): HTMLElement {
|
|
91
|
+
const settings = appearance.settings;
|
|
92
|
+
|
|
93
|
+
const dialog = document.createElement('adw-preferences-dialog');
|
|
94
|
+
dialog.setAttribute('title', 'Appearance');
|
|
95
|
+
const page = document.createElement('adw-preferences-page');
|
|
96
|
+
|
|
97
|
+
// --- Colour scheme ---
|
|
98
|
+
const schemeGroup = document.createElement('adw-preferences-group');
|
|
99
|
+
schemeGroup.setAttribute('title', 'Style');
|
|
100
|
+
|
|
101
|
+
const schemeCard = document.createElement('div');
|
|
102
|
+
schemeCard.className = 'card sb-swatch-card';
|
|
103
|
+
const schemeSwatches = new Map<string, HTMLInputElement>();
|
|
104
|
+
|
|
105
|
+
for (const scheme of STORYBOOK_COLOR_SCHEMES) {
|
|
106
|
+
const swatch = buildSwatch('sb-scheme', scheme, SCHEME_LABELS[scheme], [
|
|
107
|
+
'sb-swatch--scheme',
|
|
108
|
+
`sb-swatch--scheme-${scheme}`,
|
|
109
|
+
]);
|
|
110
|
+
swatch.checked = settings.colorScheme === scheme;
|
|
111
|
+
swatch.addEventListener('change', () => {
|
|
112
|
+
if (swatch.checked) settings.colorScheme = scheme;
|
|
113
|
+
});
|
|
114
|
+
schemeSwatches.set(scheme, swatch);
|
|
115
|
+
schemeCard.append(swatch);
|
|
116
|
+
}
|
|
117
|
+
schemeGroup.append(schemeCard);
|
|
118
|
+
page.append(schemeGroup);
|
|
119
|
+
|
|
120
|
+
// --- Accent ---
|
|
121
|
+
const accentGroup = document.createElement('adw-preferences-group');
|
|
122
|
+
accentGroup.setAttribute('title', 'Accent colour');
|
|
123
|
+
// Squares the group list's BOTTOM corners. The attached card squares its top;
|
|
124
|
+
// without both, the row above keeps its round bottom and the two meet with a
|
|
125
|
+
// notch at each end.
|
|
126
|
+
accentGroup.classList.add('sb-attached-group');
|
|
127
|
+
|
|
128
|
+
const accentSwitch = document.createElement('adw-switch-row');
|
|
129
|
+
accentSwitch.setAttribute('title', 'Use a custom accent colour');
|
|
130
|
+
accentSwitch.setAttribute('subtitle', 'Off follows the desktop, which owns the accent');
|
|
131
|
+
accentSwitch.toggleAttribute('active', settings.accentMode === 'custom');
|
|
132
|
+
accentGroup.append(accentSwitch);
|
|
133
|
+
|
|
134
|
+
// Attached: the switch row and the palette read as one boxed list rather than
|
|
135
|
+
// two stacked cards with a seam between them.
|
|
136
|
+
const accentCard = document.createElement('div');
|
|
137
|
+
accentCard.className = 'card sb-swatch-card sb-swatch-card--attached';
|
|
138
|
+
const accentSwatches = new Map<string, HTMLInputElement>();
|
|
139
|
+
|
|
140
|
+
for (const accent of ADW_ACCENT_COLOR_NAMES) {
|
|
141
|
+
const swatch = buildSwatch('sb-accent', accent, ACCENT_LABELS[accent], []);
|
|
142
|
+
// The fill comes from core, per swatch — one source of truth for the nine
|
|
143
|
+
// colours, and a tenth upstream would need no CSS here.
|
|
144
|
+
swatch.style.backgroundColor = adwaitaAccentBgColor(accent);
|
|
145
|
+
swatch.checked = settings.accent === accent;
|
|
146
|
+
swatch.addEventListener('change', () => {
|
|
147
|
+
if (swatch.checked) settings.accent = accent;
|
|
148
|
+
});
|
|
149
|
+
accentSwatches.set(accent, swatch);
|
|
150
|
+
accentCard.append(swatch);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const syncAccentEnabled = (custom: boolean): void => {
|
|
154
|
+
accentCard.classList.toggle('sb-swatch-card--disabled', !custom);
|
|
155
|
+
for (const swatch of accentSwatches.values()) swatch.disabled = !custom;
|
|
156
|
+
};
|
|
157
|
+
syncAccentEnabled(settings.accentMode === 'custom');
|
|
158
|
+
|
|
159
|
+
accentSwitch.addEventListener('notify::active', () => {
|
|
160
|
+
settings.accentMode = accentSwitch.hasAttribute('active') ? 'custom' : 'system';
|
|
161
|
+
});
|
|
162
|
+
accentGroup.append(accentCard);
|
|
163
|
+
page.append(accentGroup);
|
|
164
|
+
|
|
165
|
+
dialog.append(page);
|
|
166
|
+
|
|
167
|
+
// One-way sync FROM the model, so the dialog keeps no second copy of the state
|
|
168
|
+
// and a change made anywhere else still shows here.
|
|
169
|
+
settings.subscribe((state) => {
|
|
170
|
+
const scheme = schemeSwatches.get(state.colorScheme);
|
|
171
|
+
if (scheme && !scheme.checked) scheme.checked = true;
|
|
172
|
+
|
|
173
|
+
const accent = accentSwatches.get(state.accent);
|
|
174
|
+
if (accent && !accent.checked) accent.checked = true;
|
|
175
|
+
|
|
176
|
+
const custom = state.accentMode === 'custom';
|
|
177
|
+
accentSwitch.toggleAttribute('active', custom);
|
|
178
|
+
syncAccentEnabled(custom);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return dialog;
|
|
182
|
+
}
|
package/src/controls.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// RANGE/NUMBER formatting + type guards, the refresh "set only if differs"
|
|
7
|
-
// guard) now lives in @gjsify/storybook-core's bindControl. This file supplies
|
|
8
|
-
// ONLY the adwaita-web leaf-widget factory — the single renderer seam.
|
|
1
|
+
// Renders each `@gjsify/stories` ControlType as an `@gjsify/adwaita-web` row, wired
|
|
2
|
+
// two-way to a StoryElement's args. Per-kind value coercion (initial-value seeding,
|
|
3
|
+
// SELECT index↔value, RANGE/NUMBER formatting + guards, the refresh "set only if
|
|
4
|
+
// differs" guard) belongs to `bindControl` in `@gjsify/storybook-core`; this file
|
|
5
|
+
// supplies ONLY the adwaita-web leaf-widget factory, the single renderer seam.
|
|
9
6
|
|
|
10
7
|
import type { StoryControl, StoryNumberControl, StorySelectControl } from '@gjsify/stories';
|
|
11
8
|
import {
|
|
@@ -25,9 +22,8 @@ export function createControlRow(story: StoryElement, control: StoryControl): Co
|
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* widgets emit. The only renderer-specific seam; all coercion is in bindControl.
|
|
25
|
+
* One `adw-*` (or range/color) element per control kind, with get/set/onChange over the
|
|
26
|
+
* DOM events those widgets emit. The only renderer-specific seam.
|
|
31
27
|
*/
|
|
32
28
|
const ADWAITA_WEB_FACTORY: ControlWidgetFactory<HTMLElement> = {
|
|
33
29
|
text(label: string): ControlWidget<HTMLElement, string> {
|
package/src/index.ts
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// built from @gjsify/adwaita-web, looking and behaving like the native GTK
|
|
5
|
-
// storybook.
|
|
1
|
+
// Browser renderer for the `@gjsify/stories` contract, the web counterpart of
|
|
2
|
+
// `@gjsify/storybook`: `*.web.ts` files author `StoryElement` subclasses, rendered in
|
|
3
|
+
// an Adwaita component browser built from `@gjsify/adwaita-web`.
|
|
6
4
|
//
|
|
7
|
-
// A
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// (story base, registry, control binding, app controller) lives in core; this
|
|
13
|
-
// package keeps only the @gjsify/adwaita-web view layer.
|
|
5
|
+
// A thin DOM adapter over `@gjsify/storybook-core`, which owns the renderer-agnostic
|
|
6
|
+
// logic (story base, registry, control binding, app controller) — this package is only
|
|
7
|
+
// the view layer. Nothing compares the two renderers by screenshot (#1052); what holds
|
|
8
|
+
// their behaviour together is the `@gjsify/adwaita-core/conformance` vector tables both
|
|
9
|
+
// renderer suites assert their real widgets against.
|
|
14
10
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// generic ones from @gjsify/stories.
|
|
11
|
+
// The authoring contract is re-exported so a story needs one import only. The
|
|
12
|
+
// web-specific WebStoryModule / WebStoryDecorator intentionally shadow the generic
|
|
13
|
+
// ones from `@gjsify/stories`, because they reference `StoryElement`.
|
|
19
14
|
export {
|
|
20
15
|
ControlType,
|
|
21
16
|
argsFromControls,
|
package/src/registry.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// The registration/instantiation logic (validate, filter, decorator reduceRight
|
|
4
|
-
// fold over initialize()) is renderer-agnostic and now lives in
|
|
5
|
-
// @gjsify/storybook-core. This file just re-exports the generic class; callers
|
|
6
|
-
// type it as `StoryRegistry<StoryElement>`.
|
|
1
|
+
// Re-export of the renderer-agnostic registry; web callers type it as
|
|
2
|
+
// `StoryRegistry<StoryElement>`.
|
|
7
3
|
|
|
8
4
|
export { StoryRegistry } from '@gjsify/storybook-core';
|
package/src/story-element.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// exposes the same authoring surface (getMetadata, initialize, updateArgs,
|
|
5
|
-
// teardown, addContent, meta/story/args) so a story is a near-1:1 port of its
|
|
6
|
-
// GTK twin. That is a claim about the story SOURCE, which is readable here; that
|
|
7
|
-
// the two then render identically is not measured anywhere (#1052).
|
|
1
|
+
// The web counterpart of `@gjsify/storybook`'s `StoryWidget` (an `Adw.Bin`): same
|
|
2
|
+
// authoring surface, DOM instead of GTK widgets, so a story is a near-1:1 port of its
|
|
3
|
+
// GTK twin in SOURCE. Whether the two then render identically is not measured (#1052).
|
|
8
4
|
//
|
|
9
|
-
// Thin DOM adapter over
|
|
10
|
-
//
|
|
11
|
-
// initialize/updateArgs/teardown/addContent surface) lives in the base; this
|
|
12
|
-
// class supplies ONLY the @gjsify/adwaita-web DOM chrome via createChrome().
|
|
5
|
+
// Thin DOM adapter over `StoryViewBase`, which owns the renderer-agnostic state; this
|
|
6
|
+
// class supplies ONLY the `@gjsify/adwaita-web` DOM chrome, via `createChrome()`.
|
|
13
7
|
|
|
14
8
|
import type { StoryArgs, StoryMeta } from '@gjsify/stories';
|
|
15
9
|
import { type StoryChrome, StoryViewBase } from '@gjsify/storybook-core';
|
|
@@ -20,17 +14,13 @@ export type StoryArgsListener = (args: StoryArgs) => void;
|
|
|
20
14
|
/**
|
|
21
15
|
* Base class for browser story elements.
|
|
22
16
|
*
|
|
23
|
-
* Provides default chrome — a clamped boxed group with a title + description
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* {@link addContent} is a no-op).
|
|
17
|
+
* Provides default chrome — a clamped boxed group with a title + description above a
|
|
18
|
+
* centered, dashed-framed preview stage. Simple stories compose their preview by
|
|
19
|
+
* calling {@link addContent}; a subclass needing a bespoke layout passes its own root
|
|
20
|
+
* element to the constructor, and {@link addContent} is then a no-op.
|
|
28
21
|
*/
|
|
29
22
|
export class StoryElement extends StoryViewBase<HTMLElement> {
|
|
30
23
|
/**
|
|
31
|
-
* @param meta The story's metadata (the same renderer-agnostic object
|
|
32
|
-
* its GTK twin uses).
|
|
33
|
-
* @param story Variant name (defaults to "Default").
|
|
34
24
|
* @param customRoot Opt out of the default chrome by supplying a root element.
|
|
35
25
|
*/
|
|
36
26
|
constructor(meta: StoryMeta, story = 'Default', customRoot?: HTMLElement) {
|
|
@@ -38,10 +28,7 @@ export class StoryElement extends StoryViewBase<HTMLElement> {
|
|
|
38
28
|
this.initBase(meta, story, customRoot);
|
|
39
29
|
}
|
|
40
30
|
|
|
41
|
-
/**
|
|
42
|
-
* The root node inserted into the storybook's preview area. Alias for the
|
|
43
|
-
* base's {@link root} getter — kept because consumers reference `.element`.
|
|
44
|
-
*/
|
|
31
|
+
/** Alias for the base's {@link root} getter — kept because consumers use `.element`. */
|
|
45
32
|
get element(): HTMLElement {
|
|
46
33
|
return this.root;
|
|
47
34
|
}
|
|
@@ -77,9 +64,8 @@ export class StoryElement extends StoryViewBase<HTMLElement> {
|
|
|
77
64
|
descEl.className = 'sb-story-group-description';
|
|
78
65
|
header.append(titleEl, descEl);
|
|
79
66
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// .story-stage in @gjsify/storybook).
|
|
67
|
+
// Dashed frame so the preview's bounds stay locatable even when the widget is
|
|
68
|
+
// transparent or empty (mirrors the native `.story-stage`).
|
|
83
69
|
const stage = document.createElement('div');
|
|
84
70
|
stage.className = 'story-stage';
|
|
85
71
|
|
package/src/styles.ts
CHANGED
|
@@ -1,9 +1,88 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// properties so it follows the active light/dark theme.
|
|
1
|
+
// Chrome styles, shipped as a string and injected once on mount (as adwaita-web's own
|
|
2
|
+
// stylesheet and the native renderer's STORYBOOK_CSS are). Driven entirely by the
|
|
3
|
+
// adwaita-web CSS custom properties, so it follows the active light/dark theme.
|
|
5
4
|
|
|
6
5
|
export const STORYBOOK_WEB_CSS = `
|
|
6
|
+
/* Appearance dialog — round swatches with a selection ring. The web twin of the GTK
|
|
7
|
+
renderer's rules, themselves adapted from Learn6502.
|
|
8
|
+
|
|
9
|
+
The CARD is not decoration: in the dark scheme the "dark" swatch is a near-black
|
|
10
|
+
circle on a near-black dialog and is invisible without a lighter surface behind
|
|
11
|
+
it. Centring happens INSIDE the full-width card, so the row does not read as
|
|
12
|
+
left-aligned under the group title. */
|
|
13
|
+
.sb-swatch-card {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-wrap: wrap;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
gap: 8px;
|
|
18
|
+
padding: 12px;
|
|
19
|
+
}
|
|
20
|
+
/* Squaring the card's top alone leaves a notch: the row above lives in the group's
|
|
21
|
+
own rounded list, so BOTH sides have to be squared for the join to read as one
|
|
22
|
+
list. The attached card and the group rule below are the two halves of that. */
|
|
23
|
+
.sb-swatch-card--attached {
|
|
24
|
+
border-top-left-radius: 0;
|
|
25
|
+
border-top-right-radius: 0;
|
|
26
|
+
}
|
|
27
|
+
.sb-attached-group .boxed-list {
|
|
28
|
+
border-bottom-left-radius: 0;
|
|
29
|
+
border-bottom-right-radius: 0;
|
|
30
|
+
}
|
|
31
|
+
/* Desaturated rather than merely dimmed: while the accent follows the desktop the
|
|
32
|
+
palette must read as unavailable, not as nine slightly faded choices. */
|
|
33
|
+
.sb-swatch-card--disabled {
|
|
34
|
+
filter: saturate(0.4) brightness(1.05);
|
|
35
|
+
opacity: 0.7;
|
|
36
|
+
}
|
|
37
|
+
/* A radio input, so grouping and arrow-key navigation are the platform's. Its own
|
|
38
|
+
glyph is switched off because the swatch IS the indicator — a dot drawn on a
|
|
39
|
+
colour competes with the ring that already says which one is selected. */
|
|
40
|
+
.sb-swatch {
|
|
41
|
+
appearance: none;
|
|
42
|
+
-webkit-appearance: none;
|
|
43
|
+
margin: 0;
|
|
44
|
+
width: 24px;
|
|
45
|
+
height: 24px;
|
|
46
|
+
border-radius: 9999px;
|
|
47
|
+
box-shadow: 0 0 0 3px transparent;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
/* A hairline on the fill itself. Without it the LIGHT scheme swatch is a
|
|
50
|
+
near-white circle on a near-white card and vanishes — the exact mirror of the
|
|
51
|
+
dark swatch on a dark dialog, and a card alone only fixes one direction.
|
|
52
|
+
--border-color is a currentColor mix, so it holds in both schemes. */
|
|
53
|
+
border: 1px solid var(--border-color);
|
|
54
|
+
}
|
|
55
|
+
.sb-swatch:checked {
|
|
56
|
+
box-shadow: 0 0 0 3px var(--accent-bg-color);
|
|
57
|
+
}
|
|
58
|
+
.sb-swatch:focus-visible {
|
|
59
|
+
outline: 2px solid var(--accent-color);
|
|
60
|
+
outline-offset: 3px;
|
|
61
|
+
}
|
|
62
|
+
.sb-swatch:disabled {
|
|
63
|
+
cursor: default;
|
|
64
|
+
}
|
|
65
|
+
/* The scheme swatches are larger — they are the dialog's primary choice. */
|
|
66
|
+
.sb-swatch--scheme {
|
|
67
|
+
width: 44px;
|
|
68
|
+
height: 44px;
|
|
69
|
+
}
|
|
70
|
+
/* "Follow system" is the two schemes meeting on a diagonal, so the option looks like
|
|
71
|
+
what it does instead of needing a word for it. */
|
|
72
|
+
/* light_2 / dark_4 from libadwaita's palette (_palette.scss:37,44), which is what
|
|
73
|
+
Learn6502's selector uses. NOT light_1 (#ffffff): pure white is invisible against
|
|
74
|
+
a light card even with the hairline, and light_2 is the shade the palette offers
|
|
75
|
+
for exactly this. */
|
|
76
|
+
.sb-swatch--scheme-system {
|
|
77
|
+
background: linear-gradient(to bottom right, #f6f5f4 49.99%, #241f31 50.01%);
|
|
78
|
+
}
|
|
79
|
+
.sb-swatch--scheme-light {
|
|
80
|
+
background-color: #f6f5f4;
|
|
81
|
+
}
|
|
82
|
+
.sb-swatch--scheme-dark {
|
|
83
|
+
background-color: #241f31;
|
|
84
|
+
}
|
|
85
|
+
|
|
7
86
|
.sb-window {
|
|
8
87
|
width: 100%;
|
|
9
88
|
height: 100%;
|
package/src/types.ts
CHANGED
|
@@ -14,11 +14,7 @@ export interface WebStoryConstructor {
|
|
|
14
14
|
/** Wraps a story's content-build step (e.g. to install a shared environment). */
|
|
15
15
|
export type WebStoryDecorator = (build: () => void, story: StoryElement) => void;
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* A group of related web stories, optionally wrapped by module-level decorators.
|
|
19
|
-
* The DOM binding of {@link StoryModuleLike} over {@link StoryElement} +
|
|
20
|
-
* {@link WebStoryConstructor}.
|
|
21
|
-
*/
|
|
17
|
+
/** A group of related web stories, optionally wrapped by module-level decorators. */
|
|
22
18
|
export interface WebStoryModule extends StoryModuleLike<StoryElement, WebStoryConstructor> {
|
|
23
19
|
stories: WebStoryConstructor[];
|
|
24
20
|
/** Populated by {@link StoryRegistry.createStoryInstances}. */
|