@gjsify/adwaita-storybook 0.11.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/README.md +61 -0
- package/lib/types/app.d.ts +67 -0
- package/lib/types/controls.d.ts +7 -0
- package/lib/types/index.d.ts +7 -0
- package/lib/types/registry.d.ts +1 -0
- package/lib/types/story-element.d.ts +30 -0
- package/lib/types/styles.d.ts +3 -0
- package/lib/types/types.d.ts +24 -0
- package/package.json +59 -0
- package/src/_virtual/_rolldown/runtime.js +1 -0
- package/src/_virtual/_rolldown/runtime2.js +1 -0
- package/src/app.js +1 -0
- package/src/app.ts +312 -0
- package/src/app2.js +1 -0
- package/src/controls.js +1 -0
- package/src/controls.ts +190 -0
- package/src/controls2.js +1 -0
- package/src/index.js +1 -0
- package/src/index.ts +35 -0
- package/src/registry.js +1 -0
- package/src/registry.ts +8 -0
- package/src/registry2.js +1 -0
- package/src/story-element.js +1 -0
- package/src/story-element.ts +100 -0
- package/src/story-element2.js +1 -0
- package/src/styles.js +198 -0
- package/src/styles.ts +219 -0
- package/src/styles2.js +198 -0
- package/src/types.js +1 -0
- package/src/types.ts +33 -0
- package/src/types2.js +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @gjsify/adwaita-storybook
|
|
2
|
+
|
|
3
|
+
Browser renderer for the [`@gjsify/stories`](../../framework/stories) contract — the web counterpart of [`@gjsify/storybook`](../../framework/storybook). It renders stories as a generic Adwaita component browser built from [`@gjsify/adwaita-web`](../adwaita-web) custom elements, so a web storybook looks and behaves like the native GTK one and the two targets can be compared 1:1 (e.g. via screenshots).
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Author stories as `StoryElement` subclasses (the DOM analog of `@gjsify/storybook`'s `StoryWidget`):
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { ControlType, StoryElement, type StoryMeta, type WebStoryModule } from '@gjsify/adwaita-storybook';
|
|
11
|
+
|
|
12
|
+
export class ButtonStylesWebStory extends StoryElement {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(ButtonStylesWebStory.getMetadata(), 'Default');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static getMetadata(): StoryMeta {
|
|
18
|
+
return {
|
|
19
|
+
title: 'Buttons/Button Styles',
|
|
20
|
+
description: 'adw-button with Adwaita style variants.',
|
|
21
|
+
controls: [
|
|
22
|
+
{ name: 'label', label: 'Label', type: ControlType.TEXT, defaultValue: 'Click me' },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
initialize(): void {
|
|
28
|
+
const button = document.createElement('adw-button');
|
|
29
|
+
button.setAttribute('label', this.args.label as string);
|
|
30
|
+
this.addContent(button);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
updateArgs(): void {
|
|
34
|
+
// react to control changes
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const ButtonStylesWebStories: WebStoryModule = { stories: [ButtonStylesWebStory] };
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Mount the app:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { mountStorybook } from '@gjsify/adwaita-storybook';
|
|
45
|
+
import { ButtonStylesWebStories } from './button-styles.web.js';
|
|
46
|
+
|
|
47
|
+
mountStorybook(document.body, {
|
|
48
|
+
title: 'Adwaita Storybook',
|
|
49
|
+
stories: [ButtonStylesWebStories],
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`mountStorybook` builds the full chrome — a sidebar of stories grouped by category, a preview pane, and a controls panel that renders the story's `controls` as live-bound adwaita-web rows.
|
|
54
|
+
|
|
55
|
+
## Driving it programmatically
|
|
56
|
+
|
|
57
|
+
`mountStorybook` exposes a small control surface on `window.__storybook` (`listStories`, `openStory`, `getCurrentStory`, `setArg`, `getArgs`), so a host browser's MCP `eval_js` (see [`@gjsify/devtools-browser`](../../framework/devtools-browser)) can drive and screenshot stories without an in-page devtools channel.
|
|
58
|
+
|
|
59
|
+
## Sharing metadata with the native renderer
|
|
60
|
+
|
|
61
|
+
A story's `StoryMeta` (its `controls`, defaults, title, description) is renderer-agnostic. Author it once in a `*.meta.ts` module and import it from both the GTK `*.story.ts` and the web `*.web.ts`, so the two renderers are guaranteed to expose identical controls.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import '@gjsify/adwaita-web';
|
|
2
|
+
import type { StoryArgValue } from '@gjsify/stories';
|
|
3
|
+
import { type CategoryGroup, type ControlRow, type StorybookView, type StorySummary } from '@gjsify/storybook-core';
|
|
4
|
+
import type { StoryElement } from './story-element.js';
|
|
5
|
+
import type { WebStoryModule } from './types.js';
|
|
6
|
+
export type { StorySummary } from '@gjsify/storybook-core';
|
|
7
|
+
/** Options that adapt the storybook to a host project. */
|
|
8
|
+
export interface StorybookWebOptions {
|
|
9
|
+
/** Story modules to display. */
|
|
10
|
+
stories: WebStoryModule[];
|
|
11
|
+
/** Window title (shown in the sidebar header). */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Auto-select the first story on mount (default true). */
|
|
14
|
+
openFirst?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class StorybookWebApp implements StorybookView<StoryElement> {
|
|
17
|
+
private _controller;
|
|
18
|
+
private _options;
|
|
19
|
+
private _container;
|
|
20
|
+
private _listEl;
|
|
21
|
+
private _previewEl;
|
|
22
|
+
private _previewTitle;
|
|
23
|
+
private _controlsGroup;
|
|
24
|
+
private _navSplit;
|
|
25
|
+
private _controlsSplit;
|
|
26
|
+
private _backBtn;
|
|
27
|
+
private _rowByTitle;
|
|
28
|
+
constructor(container: HTMLElement, options: StorybookWebOptions);
|
|
29
|
+
/** Build the UI, instantiate stories, and select the first one. */
|
|
30
|
+
mount(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Adapt the two split views to the available width, the way a desktop
|
|
33
|
+
* Adwaita window sheds panes as it narrows. There are two thresholds:
|
|
34
|
+
*
|
|
35
|
+
* - below {@link CONTROLS_BREAKPOINT} the controls panel folds away into an
|
|
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.
|
|
40
|
+
*/
|
|
41
|
+
private _observeBreakpoint;
|
|
42
|
+
private _applyBreakpoint;
|
|
43
|
+
/** The currently-displayed story, or null. */
|
|
44
|
+
get activeStory(): StoryElement | null;
|
|
45
|
+
/** Every story flattened to a summary. */
|
|
46
|
+
listStories(): StorySummary[];
|
|
47
|
+
/** Select + show a story by its full `Category/Name` title. */
|
|
48
|
+
openStoryByTitle(title: string): boolean;
|
|
49
|
+
/** The active story as `{ title, story, args }`, or null. */
|
|
50
|
+
getCurrentStory(): {
|
|
51
|
+
title: string;
|
|
52
|
+
story: string;
|
|
53
|
+
args: Record<string, unknown>;
|
|
54
|
+
} | null;
|
|
55
|
+
/** Set one arg on the active story — drives the same path as the controls. */
|
|
56
|
+
setActiveArg(name: string, value: StoryArgValue): boolean;
|
|
57
|
+
renderSidebar(groups: Array<CategoryGroup<StoryElement>>, onSelect: (instance: StoryElement) => void): void;
|
|
58
|
+
markSelected(title: string): void;
|
|
59
|
+
setPreviewTitle(title: string): void;
|
|
60
|
+
showPreview(instance: StoryElement): void;
|
|
61
|
+
renderControls(rows: Array<ControlRow<unknown>>): void;
|
|
62
|
+
private _buildControls;
|
|
63
|
+
private _buildUI;
|
|
64
|
+
private _exposeGlobal;
|
|
65
|
+
}
|
|
66
|
+
/** Construct, mount, and return a {@link StorybookWebApp}. */
|
|
67
|
+
export declare function mountStorybook(container: HTMLElement, options: StorybookWebOptions): StorybookWebApp;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StoryControl } from '@gjsify/stories';
|
|
2
|
+
import { type ControlRow as CoreControlRow } from '@gjsify/storybook-core';
|
|
3
|
+
import type { StoryElement } from './story-element.js';
|
|
4
|
+
/** A built control: the row element plus a refresher that re-syncs it from args. */
|
|
5
|
+
export type ControlRow = CoreControlRow<HTMLElement>;
|
|
6
|
+
/** Build the row for a single control, or null if it cannot be rendered. */
|
|
7
|
+
export declare function createControlRow(story: StoryElement, control: StoryControl): ControlRow | null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ControlType, argsFromControls, type StoryArgs, type StoryArgValue, type StoryBooleanControl, type StoryColorControl, type StoryControl, type StoryMeta, type StoryNumberControl, type StorySelectControl, type StorySelectOption, type StoryTextControl, } from '@gjsify/stories';
|
|
2
|
+
export { StoryElement, type StoryArgsListener } from './story-element.js';
|
|
3
|
+
export { StoryRegistry } from './registry.js';
|
|
4
|
+
export { createControlRow, type ControlRow } from './controls.js';
|
|
5
|
+
export { StorybookWebApp, mountStorybook, type StorybookWebOptions, type StorySummary } from './app.js';
|
|
6
|
+
export { STORYBOOK_WEB_CSS, injectStorybookStyles } from './styles.js';
|
|
7
|
+
export { isWebStoryModule, type WebStoryConstructor, type WebStoryDecorator, type WebStoryModule } from './types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { StoryRegistry } from '@gjsify/storybook-core';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { StoryArgs, StoryMeta } from '@gjsify/stories';
|
|
2
|
+
import { type StoryChrome, StoryViewBase } from '@gjsify/storybook-core';
|
|
3
|
+
/** Notified whenever a story's args change (the controls panel re-syncs through this). */
|
|
4
|
+
export type StoryArgsListener = (args: StoryArgs) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Base class for browser story elements.
|
|
7
|
+
*
|
|
8
|
+
* Provides default chrome — a clamped boxed group with a title + description
|
|
9
|
+
* above a centered, dashed-framed preview stage — built programmatically. Simple
|
|
10
|
+
* stories compose their preview by calling {@link addContent}; a subclass that
|
|
11
|
+
* needs a bespoke layout passes its own root element to the constructor (then
|
|
12
|
+
* {@link addContent} is a no-op).
|
|
13
|
+
*/
|
|
14
|
+
export declare class StoryElement extends StoryViewBase<HTMLElement> {
|
|
15
|
+
/**
|
|
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
|
+
* @param customRoot Opt out of the default chrome by supplying a root element.
|
|
20
|
+
*/
|
|
21
|
+
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
|
+
*/
|
|
26
|
+
get element(): HTMLElement;
|
|
27
|
+
/** Build the @gjsify/adwaita-web DOM chrome — the single renderer seam. */
|
|
28
|
+
protected createChrome(_meta: StoryMeta, customRoot?: HTMLElement): StoryChrome<HTMLElement>;
|
|
29
|
+
private _installDefaultChrome;
|
|
30
|
+
}
|
|
@@ -0,0 +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";
|
|
2
|
+
/** Inject the chrome stylesheet once (idempotent, browser-only). */
|
|
3
|
+
export declare function injectStorybookStyles(): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { StoryMeta } from '@gjsify/stories';
|
|
2
|
+
import type { StoryModuleLike } from '@gjsify/storybook-core';
|
|
3
|
+
import type { StoryElement } from './story-element.js';
|
|
4
|
+
/** Constructor contract for a web story class: zero-arg `new`, static metadata. */
|
|
5
|
+
export interface WebStoryConstructor {
|
|
6
|
+
new (): StoryElement;
|
|
7
|
+
getMetadata(): StoryMeta;
|
|
8
|
+
}
|
|
9
|
+
/** Wraps a story's content-build step (e.g. to install a shared environment). */
|
|
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
|
+
*/
|
|
16
|
+
export interface WebStoryModule extends StoryModuleLike<StoryElement, WebStoryConstructor> {
|
|
17
|
+
stories: WebStoryConstructor[];
|
|
18
|
+
/** Populated by {@link StoryRegistry.createStoryInstances}. */
|
|
19
|
+
instances?: StoryElement[];
|
|
20
|
+
/** Decorators applied around every story's `initialize()` in this module. */
|
|
21
|
+
decorators?: WebStoryDecorator[];
|
|
22
|
+
}
|
|
23
|
+
/** Narrowing guard — `true` when `value` is a {@link WebStoryModule}. */
|
|
24
|
+
export declare function isWebStoryModule(value: unknown): value is WebStoryModule;
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gjsify/adwaita-storybook",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Browser renderer for the @gjsify/stories contract — the web counterpart of @gjsify/storybook, built on @gjsify/adwaita-web",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"default": "./src/index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"lib"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clear": "rm -rf lib tsconfig.tsbuildinfo || exit 0",
|
|
20
|
+
"check": "gjsify tsc --noEmit",
|
|
21
|
+
"build": "gjsify run build:gjsify && gjsify run build:types",
|
|
22
|
+
"build:gjsify": "gjsify build --library 'src/**/*.ts' --exclude 'src/**/*.spec.ts'",
|
|
23
|
+
"build:types": "gjsify tsc"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"storybook",
|
|
27
|
+
"stories",
|
|
28
|
+
"adwaita",
|
|
29
|
+
"web-components",
|
|
30
|
+
"browser"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@gjsify/adwaita-web": "^0.11.0",
|
|
34
|
+
"@gjsify/stories": "^0.11.0",
|
|
35
|
+
"@gjsify/storybook-core": "^0.11.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@gjsify/cli": "^0.11.0",
|
|
39
|
+
"typescript": "^6.0.3"
|
|
40
|
+
},
|
|
41
|
+
"gjsify": {
|
|
42
|
+
"runtimes": {
|
|
43
|
+
"gjs": "none",
|
|
44
|
+
"node": "none",
|
|
45
|
+
"browser": "polyfill",
|
|
46
|
+
"nativescript": "none"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/gjsify/gjsify.git",
|
|
53
|
+
"directory": "packages/web/adwaita-storybook"
|
|
54
|
+
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/gjsify/gjsify/issues"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-storybook#readme"
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./runtime.js";
|
package/src/app.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{createControlRow as e}from"./controls2.js";import{injectStorybookStyles as t}from"./styles2.js";import"@gjsify/adwaita-web";import{StorybookController as n}from"@gjsify/storybook-core";function h(e,t={},n=[]){let r=document.createElement(e);for(let[e,n]of Object.entries(t))r.setAttribute(e,n);for(let e of n)r.append(e);return r}var StorybookWebApp=class{_controller=new n(this,e=>this._buildControls(e));_options;_container;_listEl;_previewEl;_previewTitle;_controlsGroup;_navSplit;_controlsSplit;_backBtn;_rowByTitle=new Map;constructor(e,t){this._container=e,this._options=t}mount(){t(),this._buildUI(),this._observeBreakpoint(),this._exposeGlobal(),this._controller.mount(this._options.stories,this._options.openFirst!==!1)}_observeBreakpoint(){let apply=()=>this._applyBreakpoint();typeof ResizeObserver<`u`?new ResizeObserver(apply).observe(this._navSplit):globalThis.addEventListener?.(`resize`,apply),apply()}_applyBreakpoint(){let e=this._navSplit.clientWidth;if(e===0)return;let t=e<620,n=e<1e3,r=this._navSplit,i=this._controlsSplit;r.collapsed!==t&&(r.collapsed=t),i.collapsed!==n&&(i.collapsed=n),this._navSplit.removeAttribute(`show-content`),this._backBtn.hidden=!0,i.showSidebar=!n}get activeStory(){return this._controller.activeStory}listStories(){return this._controller.listStories()}openStoryByTitle(e){return this._controller.openStoryByTitle(e)}getCurrentStory(){return this._controller.getCurrentStory()}setActiveArg(e,t){return this._controller.setActiveArg(e,t)}renderSidebar(e,t){this._listEl.replaceChildren(),this._rowByTitle.clear();for(let{category:n,stories:r}of e){this._listEl.append(h(`div`,{class:`sb-category`},[n]));for(let{instance:e,name:n}of r){let r=h(`div`,{class:`sb-story-row`,role:`button`,tabindex:`0`},[n]);r.addEventListener(`click`,()=>t(e)),r.addEventListener(`keydown`,n=>{(n.key===`Enter`||n.key===` `)&&(n.preventDefault(),t(e))}),this._listEl.append(r),this._rowByTitle.set(e.meta.title,r)}}}markSelected(e){for(let[t,n]of this._rowByTitle)n.classList.toggle(`selected`,t===e)}setPreviewTitle(e){this._previewTitle.setAttribute(`title`,e)}showPreview(e){this._previewEl.replaceChildren(e.element),this._navSplit.collapsed&&this._navSplit.setAttribute(`show-content`,``)}renderControls(e){let t=this._controlsGroup.querySelector(`.adw-preferences-group-listbox`);if(t){t.replaceChildren();for(let n of e)t.append(n.view)}}_buildControls(t){let n=[],r=t.meta.controls;if(Array.isArray(r))for(let i of r){if(!i?.name||!i?.type){console.warn(`Invalid control configuration:`,i);continue}let r=e(t,i);r&&n.push(r)}return n}_buildUI(){this._listEl=h(`div`,{class:`sb-sidebar-list navigation-sidebar`});let e=h(`div`,{class:`sb-sidebar-scroll`},[this._listEl]),t=h(`div`,{slot:`sidebar`,class:`sb-sidebar-pane`},[h(`adw-toolbar-view`,{},[h(`adw-header-bar`,{slot:`top`},[h(`adw-window-title`,{slot:`center`,title:this._options.title??`Stories`})]),e])]);this._previewEl=h(`div`,{class:`sb-preview-scroll`}),this._controlsGroup=h(`adw-preferences-group`,{title:`Controls`});let n=h(`div`,{slot:`sidebar`,class:`sb-controls-scroll`},[h(`div`,{class:`sb-controls-page`},[this._controlsGroup])]),r=h(`adw-overlay-split-view`,{"sidebar-position":`end`,"show-sidebar":``,"min-sidebar-width":`280`,"max-sidebar-width":`360`},[h(`div`,{slot:`content`,class:`sb-preview-pane`},[this._previewEl]),n]);this._controlsSplit=r,this._backBtn=h(`button`,{class:`adw-header-btn sb-back-btn`,slot:`start`,title:`Back to stories`,"aria-label":`Back to stories`}),this._backBtn.append(h(`span`,{class:`adw-icon adw-icon--go-previous`})),this._backBtn.hidden=!0,this._backBtn.addEventListener(`click`,()=>this._navSplit.removeAttribute(`show-content`)),this._previewTitle=h(`adw-window-title`,{slot:`center`,title:`Preview`});let i=h(`button`,{class:`adw-header-btn sb-toggle-controls`,title:`Toggle Controls`,"aria-label":`Toggle Controls`});i.append(h(`span`,{class:`adw-icon adw-icon--sidebar-show`})),i.setAttribute(`slot`,`end`),i.addEventListener(`click`,()=>{r.showSidebar=!r.showSidebar});let a=h(`div`,{slot:`content`,class:`sb-content-pane`},[h(`adw-toolbar-view`,{},[h(`adw-header-bar`,{slot:`top`},[this._backBtn,this._previewTitle,i]),r])]);this._navSplit=h(`adw-navigation-split-view`,{"min-sidebar-width":`220`,"max-sidebar-width":`320`},[t,a]);let o=h(`adw-window`,{class:`sb-window`},[this._navSplit]);this._container.replaceChildren(o)}_exposeGlobal(){globalThis.__storybook={listStories:()=>this.listStories(),openStory:e=>this.openStoryByTitle(e),getCurrentStory:()=>this.getCurrentStory(),setArg:(e,t)=>this.setActiveArg(e,t),getArgs:()=>this.activeStory?.args??null}}};function mountStorybook(e,t){let n=new StorybookWebApp(e,t);return n.mount(),n}export{StorybookWebApp,mountStorybook};
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// StorybookWebApp — the browser component browser: a sidebar of stories grouped
|
|
2
|
+
// by category, a preview pane, and a controls panel that renders live-bound
|
|
3
|
+
// adwaita-web rows from each story's controls. The web counterpart of
|
|
4
|
+
// @gjsify/storybook's StorybookWindow, built from @gjsify/adwaita-web custom
|
|
5
|
+
// elements so it looks and behaves like the native GTK storybook.
|
|
6
|
+
//
|
|
7
|
+
// The app state machine (register/instantiate, category grouping, show + wire
|
|
8
|
+
// controls, the MCP control surface) lives in @gjsify/storybook-core's
|
|
9
|
+
// StorybookController. This class is the browser StorybookView<StoryElement>:
|
|
10
|
+
// it owns ONLY the adwaita-web split-views chrome (_buildUI), the browser-only
|
|
11
|
+
// responsive breakpoint fold (_observeBreakpoint/_applyBreakpoint), and the
|
|
12
|
+
// window.__storybook MCP bridge (_exposeGlobal, delegating to the controller).
|
|
13
|
+
|
|
14
|
+
import '@gjsify/adwaita-web'; // registers the custom elements + self-injects the adwaita stylesheet
|
|
15
|
+
import type { StoryArgValue } from '@gjsify/stories';
|
|
16
|
+
import {
|
|
17
|
+
type CategoryGroup,
|
|
18
|
+
type ControlRow,
|
|
19
|
+
StorybookController,
|
|
20
|
+
type StorybookView,
|
|
21
|
+
type StorySummary,
|
|
22
|
+
} from '@gjsify/storybook-core';
|
|
23
|
+
import { createControlRow } from './controls.js';
|
|
24
|
+
import type { StoryElement } from './story-element.js';
|
|
25
|
+
import { injectStorybookStyles } from './styles.js';
|
|
26
|
+
import type { WebStoryModule } from './types.js';
|
|
27
|
+
|
|
28
|
+
export type { StorySummary } from '@gjsify/storybook-core';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Width (px) below which the controls panel folds into an on-demand overlay so
|
|
32
|
+
* the sidebar + preview keep their room. It docks only in a genuinely roomy
|
|
33
|
+
* standalone window — both embeds sit below this (the docs page ~712px and the
|
|
34
|
+
* home-page slideshow ~907px), so they show sidebar + preview with the controls
|
|
35
|
+
* closed by default (reopen via the header button).
|
|
36
|
+
*/
|
|
37
|
+
const CONTROLS_BREAKPOINT = 1000;
|
|
38
|
+
|
|
39
|
+
/** Width (px) below which even the sidebar folds away and nav goes single-pane. */
|
|
40
|
+
const NAV_BREAKPOINT = 620;
|
|
41
|
+
|
|
42
|
+
/** Options that adapt the storybook to a host project. */
|
|
43
|
+
export interface StorybookWebOptions {
|
|
44
|
+
/** Story modules to display. */
|
|
45
|
+
stories: WebStoryModule[];
|
|
46
|
+
/** Window title (shown in the sidebar header). */
|
|
47
|
+
title?: string;
|
|
48
|
+
/** Auto-select the first story on mount (default true). */
|
|
49
|
+
openFirst?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Tiny element builder — sets attributes and appends children. */
|
|
53
|
+
function h(tag: string, attrs: Record<string, string> = {}, children: Array<Node | string> = []): HTMLElement {
|
|
54
|
+
const el = document.createElement(tag);
|
|
55
|
+
for (const [key, value] of Object.entries(attrs)) el.setAttribute(key, value);
|
|
56
|
+
for (const child of children) el.append(child);
|
|
57
|
+
return el;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class StorybookWebApp implements StorybookView<StoryElement> {
|
|
61
|
+
private _controller = new StorybookController<StoryElement>(this, (story) => this._buildControls(story));
|
|
62
|
+
private _options: StorybookWebOptions;
|
|
63
|
+
private _container: HTMLElement;
|
|
64
|
+
|
|
65
|
+
private _listEl!: HTMLElement;
|
|
66
|
+
private _previewEl!: HTMLElement;
|
|
67
|
+
private _previewTitle!: HTMLElement;
|
|
68
|
+
private _controlsGroup!: HTMLElement;
|
|
69
|
+
private _navSplit!: HTMLElement;
|
|
70
|
+
private _controlsSplit!: HTMLElement;
|
|
71
|
+
private _backBtn!: HTMLElement;
|
|
72
|
+
|
|
73
|
+
private _rowByTitle = new Map<string, HTMLElement>();
|
|
74
|
+
|
|
75
|
+
constructor(container: HTMLElement, options: StorybookWebOptions) {
|
|
76
|
+
this._container = container;
|
|
77
|
+
this._options = options;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Build the UI, instantiate stories, and select the first one. */
|
|
81
|
+
mount(): void {
|
|
82
|
+
injectStorybookStyles();
|
|
83
|
+
this._buildUI();
|
|
84
|
+
this._observeBreakpoint();
|
|
85
|
+
this._exposeGlobal();
|
|
86
|
+
this._controller.mount(this._options.stories, this._options.openFirst !== false);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Adapt the two split views to the available width, the way a desktop
|
|
91
|
+
* Adwaita window sheds panes as it narrows. There are two thresholds:
|
|
92
|
+
*
|
|
93
|
+
* - below {@link CONTROLS_BREAKPOINT} the controls panel folds away into an
|
|
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.
|
|
98
|
+
*/
|
|
99
|
+
private _observeBreakpoint(): void {
|
|
100
|
+
const apply = (): void => this._applyBreakpoint();
|
|
101
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
102
|
+
new ResizeObserver(apply).observe(this._navSplit);
|
|
103
|
+
} else {
|
|
104
|
+
globalThis.addEventListener?.('resize', apply);
|
|
105
|
+
}
|
|
106
|
+
apply();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private _applyBreakpoint(): void {
|
|
110
|
+
const width = this._navSplit.clientWidth;
|
|
111
|
+
if (width === 0) return; // not laid out yet — the observer will re-fire
|
|
112
|
+
const navCollapsed = width < NAV_BREAKPOINT;
|
|
113
|
+
const controlsCollapsed = width < CONTROLS_BREAKPOINT;
|
|
114
|
+
const nav = this._navSplit as HTMLElement & { collapsed: boolean };
|
|
115
|
+
const osv = this._controlsSplit as HTMLElement & { collapsed: boolean; showSidebar: boolean };
|
|
116
|
+
if (nav.collapsed !== navCollapsed) nav.collapsed = navCollapsed;
|
|
117
|
+
if (osv.collapsed !== controlsCollapsed) osv.collapsed = controlsCollapsed;
|
|
118
|
+
// Single-pane nav shows the story list first (like native); the back
|
|
119
|
+
// button only appears once a story is pushed onto the content pane.
|
|
120
|
+
this._navSplit.removeAttribute('show-content');
|
|
121
|
+
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
|
+
osv.showSidebar = !controlsCollapsed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- control surface (delegates to the controller; driven by host-level MCP via window.__storybook) ---
|
|
128
|
+
|
|
129
|
+
/** The currently-displayed story, or null. */
|
|
130
|
+
get activeStory(): StoryElement | null {
|
|
131
|
+
return this._controller.activeStory;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Every story flattened to a summary. */
|
|
135
|
+
listStories(): StorySummary[] {
|
|
136
|
+
return this._controller.listStories();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Select + show a story by its full `Category/Name` title. */
|
|
140
|
+
openStoryByTitle(title: string): boolean {
|
|
141
|
+
return this._controller.openStoryByTitle(title);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The active story as `{ title, story, args }`, or null. */
|
|
145
|
+
getCurrentStory(): { title: string; story: string; args: Record<string, unknown> } | null {
|
|
146
|
+
return this._controller.getCurrentStory();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Set one arg on the active story — drives the same path as the controls. */
|
|
150
|
+
setActiveArg(name: string, value: StoryArgValue): boolean {
|
|
151
|
+
return this._controller.setActiveArg(name, value);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- StorybookView<StoryElement> render seams (driven by the controller) ---
|
|
155
|
+
|
|
156
|
+
renderSidebar(
|
|
157
|
+
groups: Array<CategoryGroup<StoryElement>>,
|
|
158
|
+
onSelect: (instance: StoryElement) => void,
|
|
159
|
+
): void {
|
|
160
|
+
this._listEl.replaceChildren();
|
|
161
|
+
this._rowByTitle.clear();
|
|
162
|
+
|
|
163
|
+
for (const { category, stories } of groups) {
|
|
164
|
+
this._listEl.append(h('div', { class: 'sb-category' }, [category]));
|
|
165
|
+
for (const { instance, name } of stories) {
|
|
166
|
+
const row = h('div', { class: 'sb-story-row', role: 'button', tabindex: '0' }, [name]);
|
|
167
|
+
row.addEventListener('click', () => onSelect(instance));
|
|
168
|
+
row.addEventListener('keydown', (e) => {
|
|
169
|
+
if ((e as KeyboardEvent).key === 'Enter' || (e as KeyboardEvent).key === ' ') {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
onSelect(instance);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
this._listEl.append(row);
|
|
175
|
+
this._rowByTitle.set(instance.meta.title, row);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
markSelected(title: string): void {
|
|
181
|
+
for (const [rowTitle, row] of this._rowByTitle) {
|
|
182
|
+
row.classList.toggle('selected', rowTitle === title);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
setPreviewTitle(title: string): void {
|
|
187
|
+
this._previewTitle.setAttribute('title', title);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
showPreview(instance: StoryElement): void {
|
|
191
|
+
this._previewEl.replaceChildren(instance.element);
|
|
192
|
+
// When collapsed (narrow), selecting a story navigates to the content
|
|
193
|
+
// pane (push-navigation), mirroring the native split view.
|
|
194
|
+
if ((this._navSplit as HTMLElement & { collapsed: boolean }).collapsed) {
|
|
195
|
+
this._navSplit.setAttribute('show-content', '');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
renderControls(rows: Array<ControlRow<unknown>>): void {
|
|
200
|
+
const listbox = this._controlsGroup.querySelector('.adw-preferences-group-listbox');
|
|
201
|
+
if (!listbox) return;
|
|
202
|
+
listbox.replaceChildren();
|
|
203
|
+
for (const row of rows) listbox.append(row.view as HTMLElement);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private _buildControls(story: StoryElement): Array<ControlRow<unknown>> {
|
|
207
|
+
const rows: Array<ControlRow<unknown>> = [];
|
|
208
|
+
const controls = story.meta.controls;
|
|
209
|
+
if (Array.isArray(controls)) {
|
|
210
|
+
for (const control of controls) {
|
|
211
|
+
if (!control?.name || !control?.type) {
|
|
212
|
+
console.warn('Invalid control configuration:', control);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const built = createControlRow(story, control);
|
|
216
|
+
if (built) rows.push(built);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return rows;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private _buildUI(): void {
|
|
223
|
+
// --- Sidebar pane: header + scrollable story list ---
|
|
224
|
+
this._listEl = h('div', { class: 'sb-sidebar-list navigation-sidebar' });
|
|
225
|
+
const sidebarScroll = h('div', { class: 'sb-sidebar-scroll' }, [this._listEl]);
|
|
226
|
+
const sidebarHeader = h('adw-header-bar', { slot: 'top' }, [
|
|
227
|
+
h('adw-window-title', { slot: 'center', title: this._options.title ?? 'Stories' }),
|
|
228
|
+
]);
|
|
229
|
+
const sidebarPane = h('div', { slot: 'sidebar', class: 'sb-sidebar-pane' }, [
|
|
230
|
+
h('adw-toolbar-view', {}, [sidebarHeader, sidebarScroll]),
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
// --- Content pane: preview header + (preview | controls) split ---
|
|
234
|
+
this._previewEl = h('div', { class: 'sb-preview-scroll' });
|
|
235
|
+
|
|
236
|
+
this._controlsGroup = h('adw-preferences-group', { title: 'Controls' });
|
|
237
|
+
const controlsScroll = h('div', { slot: 'sidebar', class: 'sb-controls-scroll' }, [
|
|
238
|
+
h('div', { class: 'sb-controls-page' }, [this._controlsGroup]),
|
|
239
|
+
]);
|
|
240
|
+
const previewSlot = h('div', { slot: 'content', class: 'sb-preview-pane' }, [this._previewEl]);
|
|
241
|
+
|
|
242
|
+
const controlsSplit = h(
|
|
243
|
+
'adw-overlay-split-view',
|
|
244
|
+
{
|
|
245
|
+
'sidebar-position': 'end',
|
|
246
|
+
'show-sidebar': '',
|
|
247
|
+
'min-sidebar-width': '280',
|
|
248
|
+
'max-sidebar-width': '360',
|
|
249
|
+
},
|
|
250
|
+
[previewSlot, controlsScroll],
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
this._controlsSplit = controlsSplit;
|
|
254
|
+
|
|
255
|
+
// Back button — only shown when collapsed, returns to the story list.
|
|
256
|
+
this._backBtn = h('button', {
|
|
257
|
+
class: 'adw-header-btn sb-back-btn',
|
|
258
|
+
slot: 'start',
|
|
259
|
+
title: 'Back to stories',
|
|
260
|
+
'aria-label': 'Back to stories',
|
|
261
|
+
});
|
|
262
|
+
this._backBtn.append(h('span', { class: 'adw-icon adw-icon--go-previous' }));
|
|
263
|
+
this._backBtn.hidden = true;
|
|
264
|
+
this._backBtn.addEventListener('click', () => this._navSplit.removeAttribute('show-content'));
|
|
265
|
+
|
|
266
|
+
this._previewTitle = h('adw-window-title', { slot: 'center', title: 'Preview' });
|
|
267
|
+
const toggleControls = h('button', {
|
|
268
|
+
class: 'adw-header-btn sb-toggle-controls',
|
|
269
|
+
title: 'Toggle Controls',
|
|
270
|
+
'aria-label': 'Toggle Controls',
|
|
271
|
+
});
|
|
272
|
+
toggleControls.append(h('span', { class: 'adw-icon adw-icon--sidebar-show' }));
|
|
273
|
+
toggleControls.setAttribute('slot', 'end');
|
|
274
|
+
toggleControls.addEventListener('click', () => {
|
|
275
|
+
(controlsSplit as HTMLElement & { showSidebar: boolean }).showSidebar = !(
|
|
276
|
+
controlsSplit as HTMLElement & { showSidebar: boolean }
|
|
277
|
+
).showSidebar;
|
|
278
|
+
});
|
|
279
|
+
const previewHeader = h('adw-header-bar', { slot: 'top' }, [this._backBtn, this._previewTitle, toggleControls]);
|
|
280
|
+
|
|
281
|
+
const contentPane = h('div', { slot: 'content', class: 'sb-content-pane' }, [
|
|
282
|
+
h('adw-toolbar-view', {}, [previewHeader, controlsSplit]),
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
// --- Outer split + window ---
|
|
286
|
+
this._navSplit = h('adw-navigation-split-view', { 'min-sidebar-width': '220', 'max-sidebar-width': '320' }, [
|
|
287
|
+
sidebarPane,
|
|
288
|
+
contentPane,
|
|
289
|
+
]);
|
|
290
|
+
const window_ = h('adw-window', { class: 'sb-window' }, [this._navSplit]);
|
|
291
|
+
this._container.replaceChildren(window_);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private _exposeGlobal(): void {
|
|
295
|
+
// Expose a tiny control surface so the host browser's MCP `eval_js` can
|
|
296
|
+
// drive stories without an in-page devtools channel.
|
|
297
|
+
(globalThis as unknown as { __storybook?: unknown }).__storybook = {
|
|
298
|
+
listStories: () => this.listStories(),
|
|
299
|
+
openStory: (title: string) => this.openStoryByTitle(title),
|
|
300
|
+
getCurrentStory: () => this.getCurrentStory(),
|
|
301
|
+
setArg: (name: string, value: StoryArgValue) => this.setActiveArg(name, value),
|
|
302
|
+
getArgs: () => this.activeStory?.args ?? null,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Construct, mount, and return a {@link StorybookWebApp}. */
|
|
308
|
+
export function mountStorybook(container: HTMLElement, options: StorybookWebOptions): StorybookWebApp {
|
|
309
|
+
const app = new StorybookWebApp(container, options);
|
|
310
|
+
app.mount();
|
|
311
|
+
return app;
|
|
312
|
+
}
|
package/src/app2.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import"./_virtual/_rolldown/runtime2.js";import{createControlRow as e}from"./controls2.js";import{injectStorybookStyles as t}from"./styles2.js";import"@gjsify/adwaita-web";import{StorybookController as n}from"@gjsify/storybook-core";function h(e,t={},n=[]){let r=document.createElement(e);for(let[e,n]of Object.entries(t))r.setAttribute(e,n);for(let e of n)r.append(e);return r}var StorybookWebApp=class{_controller=new n(this,e=>this._buildControls(e));_options;_container;_listEl;_previewEl;_previewTitle;_controlsGroup;_navSplit;_controlsSplit;_backBtn;_rowByTitle=new Map;constructor(e,t){this._container=e,this._options=t}mount(){t(),this._buildUI(),this._observeBreakpoint(),this._exposeGlobal(),this._controller.mount(this._options.stories,this._options.openFirst!==!1)}_observeBreakpoint(){let apply=()=>this._applyBreakpoint();typeof ResizeObserver<`u`?new ResizeObserver(apply).observe(this._navSplit):globalThis.addEventListener?.(`resize`,apply),apply()}_applyBreakpoint(){let e=this._navSplit.clientWidth;if(e===0)return;let t=e<620,n=e<1e3,r=this._navSplit,i=this._controlsSplit;r.collapsed!==t&&(r.collapsed=t),i.collapsed!==n&&(i.collapsed=n),this._navSplit.removeAttribute(`show-content`),this._backBtn.hidden=!0,i.showSidebar=!n}get activeStory(){return this._controller.activeStory}listStories(){return this._controller.listStories()}openStoryByTitle(e){return this._controller.openStoryByTitle(e)}getCurrentStory(){return this._controller.getCurrentStory()}setActiveArg(e,t){return this._controller.setActiveArg(e,t)}renderSidebar(e,t){this._listEl.replaceChildren(),this._rowByTitle.clear();for(let{category:n,stories:r}of e){this._listEl.append(h(`div`,{class:`sb-category`},[n]));for(let{instance:e,name:n}of r){let r=h(`div`,{class:`sb-story-row`,role:`button`,tabindex:`0`},[n]);r.addEventListener(`click`,()=>t(e)),r.addEventListener(`keydown`,n=>{(n.key===`Enter`||n.key===` `)&&(n.preventDefault(),t(e))}),this._listEl.append(r),this._rowByTitle.set(e.meta.title,r)}}}markSelected(e){for(let[t,n]of this._rowByTitle)n.classList.toggle(`selected`,t===e)}setPreviewTitle(e){this._previewTitle.setAttribute(`title`,e)}showPreview(e){this._previewEl.replaceChildren(e.element),this._navSplit.collapsed&&this._navSplit.setAttribute(`show-content`,``)}renderControls(e){let t=this._controlsGroup.querySelector(`.adw-preferences-group-listbox`);if(t){t.replaceChildren();for(let n of e)t.append(n.view)}}_buildControls(t){let n=[],r=t.meta.controls;if(Array.isArray(r))for(let i of r){if(!i?.name||!i?.type){console.warn(`Invalid control configuration:`,i);continue}let r=e(t,i);r&&n.push(r)}return n}_buildUI(){this._listEl=h(`div`,{class:`sb-sidebar-list navigation-sidebar`});let e=h(`div`,{class:`sb-sidebar-scroll`},[this._listEl]),t=h(`div`,{slot:`sidebar`,class:`sb-sidebar-pane`},[h(`adw-toolbar-view`,{},[h(`adw-header-bar`,{slot:`top`},[h(`adw-window-title`,{slot:`center`,title:this._options.title??`Stories`})]),e])]);this._previewEl=h(`div`,{class:`sb-preview-scroll`}),this._controlsGroup=h(`adw-preferences-group`,{title:`Controls`});let n=h(`div`,{slot:`sidebar`,class:`sb-controls-scroll`},[h(`div`,{class:`sb-controls-page`},[this._controlsGroup])]),r=h(`adw-overlay-split-view`,{"sidebar-position":`end`,"show-sidebar":``,"min-sidebar-width":`280`,"max-sidebar-width":`360`},[h(`div`,{slot:`content`,class:`sb-preview-pane`},[this._previewEl]),n]);this._controlsSplit=r,this._backBtn=h(`button`,{class:`adw-header-btn sb-back-btn`,slot:`start`,title:`Back to stories`,"aria-label":`Back to stories`}),this._backBtn.append(h(`span`,{class:`adw-icon adw-icon--go-previous`})),this._backBtn.hidden=!0,this._backBtn.addEventListener(`click`,()=>this._navSplit.removeAttribute(`show-content`)),this._previewTitle=h(`adw-window-title`,{slot:`center`,title:`Preview`});let i=h(`button`,{class:`adw-header-btn sb-toggle-controls`,title:`Toggle Controls`,"aria-label":`Toggle Controls`});i.append(h(`span`,{class:`adw-icon adw-icon--sidebar-show`})),i.setAttribute(`slot`,`end`),i.addEventListener(`click`,()=>{r.showSidebar=!r.showSidebar});let a=h(`div`,{slot:`content`,class:`sb-content-pane`},[h(`adw-toolbar-view`,{},[h(`adw-header-bar`,{slot:`top`},[this._backBtn,this._previewTitle,i]),r])]);this._navSplit=h(`adw-navigation-split-view`,{"min-sidebar-width":`220`,"max-sidebar-width":`320`},[t,a]);let o=h(`adw-window`,{class:`sb-window`},[this._navSplit]);this._container.replaceChildren(o)}_exposeGlobal(){globalThis.__storybook={listStories:()=>this.listStories(),openStory:e=>this.openStoryByTitle(e),getCurrentStory:()=>this.getCurrentStory(),setArg:(e,t)=>this.setActiveArg(e,t),getArgs:()=>this.activeStory?.args??null}}};function mountStorybook(e,t){let n=new StorybookWebApp(e,t);return n.mount(),n}export{StorybookWebApp,mountStorybook};
|
package/src/controls.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{bindControl as e}from"@gjsify/storybook-core";function createControlRow(n,r){return e(n,r,t)}const t={text(e){let t=document.createElement(`adw-entry-row`);t.setAttribute(`title`,e);let cb=()=>{};return t.addEventListener(`changed`,e=>cb(e.detail.text)),{node:t,get:()=>t.text??``,set:e=>{t.text=e},onChange:e=>{cb=e}}},color(e,t){let n=document.createElement(`adw-action-row`);n.setAttribute(`title`,e),t&&n.setAttribute(`subtitle`,t);let r=document.createElement(`input`);r.type=`color`,r.className=`sb-color-input`,r.slot=`suffix`,n.appendChild(r);let cb=()=>{};return r.addEventListener(`input`,()=>cb(r.value)),{node:n,get:()=>r.value,set:e=>{r.value=e},onChange:e=>{cb=e}}},boolean(e){let t=document.createElement(`adw-switch-row`);t.setAttribute(`title`,e);let cb=()=>{};return t.addEventListener(`notify::active`,e=>cb(e.detail.active)),{node:t,get:()=>t.hasAttribute(`active`),set:e=>{t.toggleAttribute(`active`,e)},onChange:e=>{cb=e}}},number(e){let t=document.createElement(`adw-spin-row`);t.setAttribute(`title`,label(e)),t.setAttribute(`min`,String(e.min??0)),t.setAttribute(`max`,String(e.max??100)),t.setAttribute(`step`,String(e.step??1));let cb=()=>{};return t.addEventListener(`notify::value`,e=>cb(e.detail.value)),{node:t,get:()=>t.value??0,set:e=>{t.value=e},onChange:e=>{cb=e}}},select(e){let t=e.options??[],n=document.createElement(`adw-combo-row`);n.setAttribute(`title`,label(e)),n.setAttribute(`items`,JSON.stringify(t.map(e=>e.label)));let cb=()=>{};return n.addEventListener(`notify::selected`,e=>cb(e.detail.selected)),{node:n,get:()=>n.selected??0,set:e=>{n.selected=e},onChange:e=>{cb=e}}},range(e){let t=e.min??0,n=e.max??100,r=e.step??1,i=Number.isInteger(r),format=e=>i?String(Math.round(e)):e.toFixed(2),a=document.createElement(`div`);a.className=`sb-range-row`;let o=document.createElement(`div`);o.className=`sb-range-header`;let s=document.createElement(`span`);s.className=`sb-range-title`,s.textContent=label(e);let c=document.createElement(`span`);c.className=`sb-range-value`,o.append(s,c);let l=document.createElement(`input`);if(l.type=`range`,l.className=`sb-range-input`,l.min=String(t),l.max=String(n),l.step=String(r),e.description){let t=document.createElement(`span`);t.className=`sb-range-description`,t.textContent=e.description,a.append(o,t,l)}else a.append(o,l);let cb=()=>{};return l.addEventListener(`input`,()=>{let e=parseFloat(l.value);c.textContent=format(e),cb(e)}),{node:a,get:()=>parseFloat(l.value),set:e=>{l.value=String(e),c.textContent=format(e)},onChange:e=>{cb=e}}}};function label(e){return e.label||e.name}export{createControlRow};
|