@gjsify/storybook 0.7.5

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 ADDED
@@ -0,0 +1,100 @@
1
+ # @gjsify/storybook
2
+
3
+ A reusable **GTK/Adwaita storybook** for GJS. Author component previews as `*.story.ts` files anywhere in your project, then browse them in a generic Adwaita component explorer — a sidebar grouped by category, a live preview pane, and an auto-generated controls panel with two-way binding. No per-project storybook *application* to maintain.
4
+
5
+ It is the GTK renderer for the runtime-agnostic [`@gjsify/stories`](../stories) contract (re-exported here, so a story needs only one import).
6
+
7
+ ## Author a story
8
+
9
+ ```ts
10
+ import GObject from '@girs/gobject-2.0';
11
+ import { ControlType, StoryWidget, type StoryArgs, type StoryMeta, type StoryModule } from '@gjsify/storybook';
12
+ import { MyButton } from './my-button.js';
13
+
14
+ export class MyButtonStory extends StoryWidget {
15
+ private _button: MyButton | null = null;
16
+ static {
17
+ GObject.registerClass({ GTypeName: 'MyButtonStory' }, MyButtonStory);
18
+ }
19
+
20
+ constructor() {
21
+ super(StoryWidget.fromMeta(MyButtonStory.getMetadata(), 'Default')); // defaults derived from controls
22
+ }
23
+
24
+ static getMetadata(): StoryMeta {
25
+ return {
26
+ title: 'UI/My Button', // "UI" groups the sidebar
27
+ component: MyButton.$gtype,
28
+ controls: [
29
+ { name: 'label', label: 'Label', type: ControlType.TEXT, defaultValue: 'Click me' },
30
+ { name: 'size', label: 'Size', type: ControlType.RANGE, min: 16, max: 96, step: 2, defaultValue: 42 },
31
+ ],
32
+ };
33
+ }
34
+
35
+ initialize(): void {
36
+ this._button = new MyButton({ label: this.args.label as string });
37
+ this.addContent(this._button);
38
+ }
39
+
40
+ updateArgs(_args: StoryArgs): void {
41
+ if (this._button && typeof this.args.label === 'string') this._button.label = this.args.label;
42
+ }
43
+ }
44
+
45
+ GObject.type_ensure(MyButtonStory.$gtype);
46
+
47
+ export const MyButtonStories: StoryModule = { stories: [MyButtonStory] };
48
+ ```
49
+
50
+ Subclasses that supply their own composite `.blp` template have full layout control; otherwise `addContent()` drops the preview into default chrome. The optional `withActionGroup(prefix, actions)` decorator (`StoryModule.decorators`) installs a stubbed `Gio.SimpleActionGroup` so a previewed widget's buttons resolve in the sandbox.
51
+
52
+ ## Run it
53
+
54
+ The easy way — let the CLI discover and launch every `*.story.ts`:
55
+
56
+ ```bash
57
+ gjsify storybook
58
+ ```
59
+
60
+ Configure it in `package.json`:
61
+
62
+ ```jsonc
63
+ "gjsify": {
64
+ "storybook": {
65
+ "applicationId": "org.example.Storybook",
66
+ "title": "Example Storybook",
67
+ "stories": "src/**/*.story.ts",
68
+ "globals": "auto,dom" // add ,dom when stories use canvas/DOM widgets
69
+ }
70
+ }
71
+ ```
72
+
73
+ Or wire a launcher yourself with `runStorybook` + `collectStoryModules`:
74
+
75
+ ```ts
76
+ import { collectStoryModules, runStorybook } from '@gjsify/storybook';
77
+ import * as button from './widgets/my-button.story.js';
78
+
79
+ await runStorybook({
80
+ applicationId: 'org.example.Storybook',
81
+ stories: collectStoryModules([button]),
82
+ });
83
+ ```
84
+
85
+ ## Exports
86
+
87
+ - `StoryWidget` — Adw.Bin base class; `fromMeta()`, `addContent()`, `initialize()`/`updateArgs()`/`teardown()` hooks
88
+ - `StoryModule` / `StoryWidgetConstructor` — registration shape (with optional module `decorators`)
89
+ - `StoryRegistryService` — collects modules and instantiates their widgets
90
+ - `StorybookApplication` / `StorybookWindow` — the Adwaita host + component browser
91
+ - `runStorybook(options)` / `collectStoryModules(namespaces)` — launcher + discovery
92
+ - `withActionGroup` / `installActionGroup` / `StoryAction` — action-group decorator helpers
93
+ - re-exported contract: `ControlType`, `StoryControl` (+ per-kind types), `StoryMeta`, `StoryArgs`, `argsFromControls`
94
+
95
+ ## Build / test
96
+
97
+ ```bash
98
+ gjsify workspace @gjsify/storybook build
99
+ gjsify workspace @gjsify/storybook test
100
+ ```
@@ -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"./_virtual/_rolldown/runtime.js";import{StoryRegistryService as e}from"./registry.js";import{STORYBOOK_CSS as t}from"./styles.js";import{StorybookWindow as n}from"./window.js";import r from"@girs/adw-1";import i from"@girs/gdk-4.0";import a from"@girs/gio-2.0";import o from"@girs/gobject-2.0";import s from"@girs/gtk-4.0";var c=class StorybookApplication extends r.Application{static{o.registerClass({GTypeName:`StorybookApplication`},StorybookApplication)}constructor(t){super({application_id:t.applicationId,flags:a.ApplicationFlags.DEFAULT_FLAGS}),this._window=null,this._registry=new e,this._options=t,this.connect(`startup`,()=>this._initStyles()),this.connect(`activate`,()=>this._onActivate())}_initStyles(){let e=new s.CssProvider;e.load_from_string(`${t}\n${this._options.css??``}`);let n=i.Display.get_default();if(!n){console.error(`No display found`);return}s.StyleContext.add_provider_for_display(n,e,s.STYLE_PROVIDER_PRIORITY_APPLICATION)}_onActivate(){if(!this._window){this._window=new n({application:this}),this._options.title&&this._window.set_title(this._options.title),this._registry.registerStories(this._options.stories);try{let e=this._registry.createStoryInstances();this._window.populateSidebar(e)}catch(e){console.error(`Failed to create story instances:`,e)}}this._window.present()}};o.type_ensure(c.$gtype);export{c as StorybookApplication};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import e from"@girs/gio-2.0";import t from"@girs/glib-2.0";function installActionGroup(n,r,i){let a=new e.SimpleActionGroup;for(let n of i)if(typeof n.state==`boolean`){let r=e.SimpleAction.new_stateful(n.name,null,t.Variant.new_boolean(n.state));r.connect(`change-state`,(e,t)=>{e.set_state(t),n.onChange?.(t)}),a.add_action(r)}else if(typeof n.state==`string`){let r=e.SimpleAction.new_stateful(n.name,t.VariantType.new(`s`),t.Variant.new_string(n.state));r.connect(`change-state`,(e,t)=>{e.set_state(t),n.onChange?.(t)}),a.add_action(r)}else{let t=new e.SimpleAction({name:n.name});t.connect(`activate`,(e,t)=>n.onChange?.(t??null)),a.add_action(t)}return n.insert_action_group(r,a),a}function withActionGroup(e,t){return(n,r)=>{installActionGroup(r,e,t),n()}}export{installActionGroup,withActionGroup};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{isStoryModule as e}from"@gjsify/stories";function collectStoryModules(t){let n=[];for(let r of t)if(!(!r||typeof r!=`object`))for(let t of Object.values(r))e(t)&&n.push(t);return n}export{collectStoryModules};
@@ -0,0 +1 @@
1
+ import{StoryRegistryService as e}from"./registry.js";import{StorybookWindow as t}from"./window.js";import{StorybookApplication as n}from"./application.js";import{installActionGroup as r,withActionGroup as i}from"./decorators.js";import{collectStoryModules as a}from"./discover.js";import{StoryWidget as o}from"./story-widget.js";import{runStorybook as s}from"./run.js";import{ControlType as c,argsFromControls as l,isStoryModule as u}from"@gjsify/stories";export{c as ControlType,e as StoryRegistryService,o as StoryWidget,n as StorybookApplication,t as StorybookWindow,l as argsFromControls,a as collectStoryModules,r as installActionGroup,u as isStoryModule,s as runStorybook,i as withActionGroup};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";var StoryRegistryService=class{constructor(){this.modules=[]}registerStory(e){if(!e||!Array.isArray(e.stories)){console.warn(`Invalid story module provided to registerStory`);return}this.modules.push(e)}registerStories(e){if(!Array.isArray(e)){console.warn(`Invalid story modules array provided to registerStories`);return}let t=e.filter(e=>e&&Array.isArray(e.stories));this.modules.push(...t)}getStories(){return[...this.modules]}createStoryInstances(){for(let e of this.modules)this.instantiateStoriesForModule(e);return[...this.modules]}instantiateStoriesForModule(e){e.instances||=[];let t=e.decorators??[];for(let n of e.stories)try{let r=new n,build=()=>{typeof r.initialize==`function`&&r.initialize()};t.reduceRight((e,t)=>()=>t(e,r),build)(),e.instances.push(r)}catch(e){console.error(`Failed to instantiate story widget: ${n.name||`Unknown`}`,e)}}};export{StoryRegistryService};
package/lib/esm/run.js ADDED
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{StorybookApplication as e}from"./application.js";async function runStorybook(t){return new e(t).runAsync([imports.system.programInvocationName,...ARGV])}export{runStorybook};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import e from"@girs/adw-1";import t from"@girs/gobject-2.0";import n from"@girs/gtk-4.0";import{argsFromControls as r}from"@gjsify/stories";var i=class StoryWidget extends e.Bin{static{t.registerClass({GTypeName:`StoryWidget`,Properties:{meta:t.ParamSpec.object(`meta`,`Meta`,`Story metadata`,t.ParamFlags.READWRITE,t.Object),story:t.ParamSpec.string(`story`,`Story Name`,`Story name`,t.ParamFlags.READWRITE,``),args:t.ParamSpec.object(`args`,`Args`,`Story arguments`,t.ParamFlags.READWRITE,t.Object)}},StoryWidget)}constructor(e,t={}){super(t),this._story=``,this._storyContent=null,this._group=null,this._meta=e.meta,this._story=e.story,this._args=e.args,this.get_child()??this._installDefaultChrome()}static fromMeta(e,t){return{meta:e,story:t,args:r(e.controls)}}get meta(){return this._meta}set meta(e){this._meta!==e&&(this._meta=e,this.notify(`meta`),this._refreshChromeText())}get story(){return this._story}set story(e){this._story!==e&&(this._story=e,this.notify(`story`),this._refreshChromeText())}get args(){return this._args}set args(e){this._args!==e&&(this._args=e,this.notify(`args`),this.updateArgs(e))}initialize(){}updateArgs(e){}teardown(){}addContent(e){if(!this._storyContent)return;let t=this._storyContent.get_first_child();for(;t;)t.unparent(),t=this._storyContent.get_first_child();this._storyContent.append(e)}_installDefaultChrome(){let t=new e.PreferencesPage,r=new e.PreferencesGroup,i=new n.Box({orientation:n.Orientation.VERTICAL,halign:n.Align.CENTER,valign:n.Align.CENTER,margin_top:12,margin_bottom:12,spacing:12,hexpand:!0});r.add(i),t.add(r),this.set_child(t),this._group=r,this._storyContent=i,this._refreshChromeText()}_refreshChromeText(){if(!this._group)return;let e=this._meta?this._story?`${this._meta.title} — ${this._story}`:this._meta.title:this._story,t=this._meta?.description??``;this._group.set_title(e),this._group.set_description(t)}};t.type_ensure(i.$gtype);export{i as StoryWidget};
@@ -0,0 +1,10 @@
1
+ const e=`
2
+ /* Range-control card in the controls sidebar — keeps the label + description
3
+ from being squashed into a single column when the sidebar is narrow. */
4
+ .story-range-row {
5
+ padding: 0;
6
+ }
7
+ .story-range-row > box {
8
+ border-radius: 12px;
9
+ }
10
+ `;export{e as STORYBOOK_CSS};
File without changes
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import e from"@girs/adw-1";import t from"@girs/gdk-4.0";import n from"@girs/gobject-2.0";import r from"@girs/gtk-4.0";import{ControlType as i}from"@gjsify/stories";var a=class StorybookWindow extends e.ApplicationWindow{static{n.registerClass({GTypeName:`StorybookWindow`},StorybookWindow)}constructor(e){super(e),this._controlRows=[],this._controlRefreshers=[],this._activeStoryHandlerId=0,this._activeStory=null,this.set_default_size(1200,800),this.set_size_request(360,320),this.set_title(`Storybook`),this._buildUI(),this._sidebar_list.connect(`row-selected`,this._onStorySelected.bind(this)),this._show_controls_button.connect(`toggled`,this._onToggleControls.bind(this)),this._controls_split_view.set_show_sidebar(!0)}_buildUI(){this._sidebar_list=new r.ListBox({selection_mode:r.SelectionMode.SINGLE}),this._sidebar_list.add_css_class(`navigation-sidebar`);let t=new r.ScrolledWindow({hexpand:!0,vexpand:!0,child:this._sidebar_list}),n=new e.HeaderBar({title_widget:new e.WindowTitle({title:`Stories`}),show_end_title_buttons:!1}),i=new e.ToolbarView({content:t});i.add_top_bar(n);let a=new e.NavigationPage({title:`Stories`,tag:`stories`,child:i});this._content_area=new e.Bin({hexpand:!0,vexpand:!0});let o=new r.ScrolledWindow({hexpand:!0,vexpand:!0,child:this._content_area});this._control_panel=new e.PreferencesGroup({title:`Controls`});let s=new e.PreferencesPage;s.add(this._control_panel);let c=new r.ScrolledWindow({vexpand:!0,child:s});this._controls_split_view=new e.OverlaySplitView({show_sidebar:!0,sidebar_position:r.PackType.END,min_sidebar_width:280,max_sidebar_width:360,content:o,sidebar:c}),this._preview_title=new e.WindowTitle({title:`Preview`}),this._show_controls_button=new r.ToggleButton({icon_name:`sidebar-show-right-symbolic`,tooltip_text:`Toggle Controls`,active:!0});let l=new e.HeaderBar({title_widget:this._preview_title});l.pack_end(this._show_controls_button);let u=new e.ToolbarView({content:this._controls_split_view});u.add_top_bar(l);let d=new e.NavigationPage({title:`Preview`,tag:`preview`,child:u});this._main_split_view=new e.NavigationSplitView({min_sidebar_width:220,max_sidebar_width:320,sidebar:a,content:d}),this.set_content(this._main_split_view);let f=e.BreakpointCondition.parse(`max-width: 720sp`);if(f){let t=new e.Breakpoint({condition:f});t.connect(`apply`,()=>{this._main_split_view.collapsed=!0,this._controls_split_view.collapsed=!0}),t.connect(`unapply`,()=>{this._main_split_view.collapsed=!1,this._controls_split_view.collapsed=!1}),this.add_breakpoint(t)}}_onToggleControls(e){this._controls_split_view.set_show_sidebar(e.get_active())}populateSidebar(e){if(this._clearSidebar(),!e.some(e=>e.instances?.length)){console.error(`Story modules do not have instances. Call createStoryInstances first.`);return}this._groupStoriesByCategory(e).forEach((e,t)=>{this._addCategoryToSidebar(t),e.forEach(e=>{this._addStoryToSidebar(e)})})}_clearSidebar(){let e=this._sidebar_list.get_first_child();for(;e;){let t=e.get_next_sibling();e.unparent(),e=t}}_groupStoriesByCategory(e){let t=new Map;return e.forEach(e=>{e.instances?.length&&e.instances.forEach(e=>{let[n]=e.meta.title.split(`/`);t.has(n)||t.set(n,[]),t.get(n).push(e)})}),t}_addCategoryToSidebar(e){let t=new r.ListBoxRow({selectable:!1}),n=new r.Label({label:e,halign:r.Align.START,margin_top:10,margin_bottom:4,margin_start:12,margin_end:12});n.add_css_class(`heading`),n.add_css_class(`dim-label`),t.set_child(n),this._sidebar_list.append(t)}_addStoryToSidebar(e){let t=e.meta.title.split(`/`),n=t.length>1?t[1]:e.meta.title,i=new r.ListBoxRow,a=new r.Label({label:n||`Unnamed Story`,halign:r.Align.START,margin_start:20,margin_top:6,margin_bottom:6});i.set_child(a),this._sidebar_list.append(i),i.storyWidget=e}_onStorySelected(e,t){if(!t)return;let n=t;n.storyWidget&&(this._showStory(n.storyWidget),this._main_split_view.get_collapsed()&&this._main_split_view.set_show_content(!0))}_showStory(e){this._preview_title.set_title(`${e.meta.title} - ${e.story}`),this._content_area.set_child(e),this._updateControlPanel(e)}_updateControlPanel(e){this._clearControlPanel();let t=e.meta.controls;Array.isArray(t)&&(t.forEach(t=>{if(!t?.name||!t?.type){console.warn(`Invalid control configuration:`,t);return}let n=this._createControlRow(e,t);n&&(this._control_panel.add(n),this._controlRows.push(n))}),this._activeStory&&this._activeStoryHandlerId&&this._activeStory.disconnect(this._activeStoryHandlerId),this._activeStory=e,this._activeStoryHandlerId=e.connect(`notify::args`,()=>{for(let t of this._controlRefreshers)t(e.args)}),this._show_controls_button.set_active(!0))}_clearControlPanel(){for(let e of this._controlRows)this._control_panel.remove(e);this._controlRows=[],this._controlRefreshers=[]}_createControlRow(e,t){let n=e.args[t.name];switch(t.type){case i.TEXT:return this._createTextRow(e,t,typeof n==`string`?n:``);case i.NUMBER:return this._createNumberRow(e,t,typeof n==`number`?n:t.min??0);case i.BOOLEAN:return this._createBooleanRow(e,t,typeof n==`boolean`?n:!1);case i.RANGE:return this._createRangeRow(e,t,typeof n==`number`?n:t.min??0);case i.SELECT:return this._createSelectRow(e,t,n??null);case i.COLOR:return this._createColorRow(e,t,typeof n==`string`?n:`#000000`);default:return console.warn(`Unsupported control type: ${t.type}`),null}}_writeArg(e,t,n){e.args={...e.args,[t]:n}}_createTextRow(t,n,r){let i=new e.EntryRow({title:n.label||n.name});return i.set_text(r),n.description&&i.set_tooltip_text(n.description),i.connect(`changed`,()=>this._writeArg(t,n.name,i.get_text())),this._controlRefreshers.push(e=>{let t=typeof e[n.name]==`string`?e[n.name]:``;i.get_text()!==t&&i.set_text(t)}),i}_createNumberRow(t,n,r){let i=e.SpinRow.new_with_range(n.min??0,n.max??100,n.step??1);return i.set_title(n.label||n.name),n.description&&i.set_subtitle(n.description),i.set_value(r),i.connect(`changed`,()=>this._writeArg(t,n.name,i.get_value())),this._controlRefreshers.push(e=>{let t=typeof e[n.name]==`number`?e[n.name]:0;i.get_value()!==t&&i.set_value(t)}),i}_createBooleanRow(t,n,r){let i=new e.SwitchRow({title:n.label||n.name,subtitle:n.description??``,active:r});return i.connect(`notify::active`,()=>this._writeArg(t,n.name,i.get_active())),this._controlRefreshers.push(e=>{let t=!!e[n.name];i.get_active()!==t&&i.set_active(t)}),i}_createRangeRow(e,t,n){let i=t.min??0,a=t.max??100,o=t.step??1,s=Number.isInteger(o)&&Number.isInteger(n),c=new r.Adjustment({lower:i,upper:a,step_increment:o,value:n});c.step_increment=o;let l=new r.ListBoxRow({selectable:!1,activatable:!1});l.add_css_class(`story-range-row`);let u=new r.Box({orientation:r.Orientation.VERTICAL,spacing:6,margin_top:12,margin_bottom:12,margin_start:12,margin_end:12}),d=new r.Box({orientation:r.Orientation.HORIZONTAL,spacing:12}),f=new r.Label({label:t.label||t.name,halign:r.Align.START,hexpand:!0,ellipsize:3});f.add_css_class(`heading`);let p=new r.Label({label:this._formatRangeValue(n,s),halign:r.Align.END});if(p.add_css_class(`numeric`),p.add_css_class(`dim-label`),d.append(f),d.append(p),u.append(d),t.description){let e=new r.Label({label:t.description,halign:r.Align.START,xalign:0,wrap:!0,max_width_chars:32});e.add_css_class(`caption`),e.add_css_class(`dim-label`),u.append(e)}let m=new r.Scale({orientation:r.Orientation.HORIZONTAL,adjustment:c,draw_value:!1,hexpand:!0});return m.connect(`value-changed`,()=>{let n=m.get_value();if(s){let e=Math.round(n);if(e!==n){m.set_value(e);return}n=e}p.set_label(this._formatRangeValue(n,s)),this._writeArg(e,t.name,n)}),u.append(m),l.set_child(u),this._controlRefreshers.push(e=>{let r=typeof e[t.name]==`number`?e[t.name]:n;m.get_value()!==r&&(m.set_value(r),p.set_label(this._formatRangeValue(r,s)))}),l}_formatRangeValue(e,t){return t?String(Math.round(e)):e.toFixed(2)}_createSelectRow(t,n,i){let a=n.options;if(!a?.length)return console.warn(`SELECT control "${n.name}" has no options`),null;let o=r.StringList.new(a.map(e=>e.label)),s=new e.ComboRow({title:n.label||n.name,subtitle:n.description??``,model:o}),c=a.findIndex(e=>e.value===i);return c>=0&&s.set_selected(c),s.connect(`notify::selected`,()=>{let e=s.get_selected();e>=0&&e<a.length&&this._writeArg(t,n.name,a[e].value)}),this._controlRefreshers.push(e=>{let t=e[n.name],r=a.findIndex(e=>e.value===t);r>=0&&s.get_selected()!==r&&s.set_selected(r)}),s}_createColorRow(n,i,a){let o=new e.ActionRow({title:i.label||i.name,subtitle:i.description??``}),s=new r.ColorDialogButton({dialog:new r.ColorDialog({title:i.label||i.name}),valign:r.Align.CENTER}),c=new t.RGBA;return c.parse(a)&&s.set_rgba(c),s.connect(`notify::rgba`,()=>{this._writeArg(n,i.name,this._rgbaToHex(s.get_rgba()))}),o.add_suffix(s),o.set_activatable_widget(s),o}_rgbaToHex(e){let channel=e=>Math.round(Math.max(0,Math.min(1,e))*255).toString(16).padStart(2,`0`);return`#${channel(e.red)}${channel(e.green)}${channel(e.blue)}`}};n.type_ensure(a.$gtype);export{a as StorybookWindow};
@@ -0,0 +1,26 @@
1
+ import Adw from '@girs/adw-1';
2
+ import type { StoryModule } from './story-widget.js';
3
+ /** Options that adapt the generic storybook to a host project. */
4
+ export interface StorybookOptions {
5
+ /** GApplication id, e.g. `org.example.Storybook`. */
6
+ applicationId: string;
7
+ /** Window title (defaults to the `.blp` template's "Storybook"). */
8
+ title?: string;
9
+ /** Story modules to display. */
10
+ stories: StoryModule[];
11
+ /** Consumer's own widget stylesheet, layered on top of the built-in chrome CSS. */
12
+ css?: string;
13
+ }
14
+ /**
15
+ * Hosts the storybook: loads styles on startup, builds the window and populates
16
+ * it from a fresh {@link StoryRegistryService} on activate. One registry per
17
+ * application instance (no shared singleton).
18
+ */
19
+ export declare class StorybookApplication extends Adw.Application {
20
+ private _window;
21
+ private _registry;
22
+ private _options;
23
+ constructor(options: StorybookOptions);
24
+ private _initStyles;
25
+ private _onActivate;
26
+ }
@@ -0,0 +1,28 @@
1
+ import Gio from '@girs/gio-2.0';
2
+ import GLib from '@girs/glib-2.0';
3
+ import type Gtk from '@girs/gtk-4.0';
4
+ import type { StoryDecorator } from './story-widget.js';
5
+ /** Declarative description of one action to install in a story sandbox. */
6
+ export interface StoryAction {
7
+ /** Action name (without the group prefix). */
8
+ name: string;
9
+ /**
10
+ * Initial state for a stateful action. `boolean` → a toggle, `string` → a
11
+ * radio/enum action. Omit for a plain (parameterless, stateless) action.
12
+ */
13
+ state?: boolean | string;
14
+ /** Called when the action activates (plain) or changes state (stateful). */
15
+ onChange?: (value: GLib.Variant | null) => void;
16
+ }
17
+ /**
18
+ * Install a `Gio.SimpleActionGroup` on a widget so a story's buttons reach
19
+ * stubbed actions in the sandbox — replaces the hand-rolled
20
+ * `Gio.SimpleAction.new_stateful(...)` boilerplate that every interactive
21
+ * story used to repeat.
22
+ */
23
+ export declare function installActionGroup(widget: Gtk.Widget, prefix: string, actions: StoryAction[]): Gio.SimpleActionGroup;
24
+ /**
25
+ * Decorator that installs an action group before the story builds — so the
26
+ * actions exist by the time the previewed widget wires its buttons.
27
+ */
28
+ export declare function withActionGroup(prefix: string, actions: StoryAction[]): StoryDecorator;
@@ -0,0 +1,8 @@
1
+ import type { StoryModule as GtkStoryModule } from './story-widget.js';
2
+ /**
3
+ * Flatten a list of module namespaces (the `import * as m from './x.story.js'`
4
+ * objects the `gjsify storybook` command generates) into the story modules
5
+ * they export — regardless of the export name. Any exported value shaped like a
6
+ * {@link StoryModule} (an object with a `stories` array) is collected.
7
+ */
8
+ export declare function collectStoryModules(namespaces: Array<Record<string, unknown>>): GtkStoryModule[];
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,10 @@
1
+ export { ControlType, argsFromControls, isStoryModule, 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 { StoryWidget } from './story-widget.js';
3
+ export type { StoryDecorator, StoryModule, StoryWidgetConstructor } from './story-widget.js';
4
+ export { StoryRegistryService } from './registry.js';
5
+ export { installActionGroup, withActionGroup, type StoryAction } from './decorators.js';
6
+ export { StorybookWindow } from './window.js';
7
+ export { StorybookApplication, type StorybookOptions } from './application.js';
8
+ export { runStorybook } from './run.js';
9
+ export { collectStoryModules } from './discover.js';
10
+ export type { StoryRow } from './types.js';
@@ -0,0 +1,22 @@
1
+ import type { StoryModule } from './story-widget.js';
2
+ /**
3
+ * Manages story-module registration and instantiation. Unlike the original
4
+ * PixelRPG implementation there is no exported singleton — each
5
+ * {@link runStorybook} run owns exactly one registry, which removes the
6
+ * double-registration footgun.
7
+ */
8
+ export declare class StoryRegistryService {
9
+ private modules;
10
+ /** Register a single story module. */
11
+ registerStory(storyModule: StoryModule): void;
12
+ /** Register multiple story modules (invalid ones are filtered out). */
13
+ registerStories(storyModules: StoryModule[]): void;
14
+ /** All registered story modules (a shallow copy). */
15
+ getStories(): StoryModule[];
16
+ /**
17
+ * Instantiate every registered story widget and prepare it for display.
18
+ * Call once GTK is ready.
19
+ */
20
+ createStoryInstances(): StoryModule[];
21
+ private instantiateStoriesForModule;
22
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { type StorybookOptions } from './application.js';
2
+ /**
3
+ * Construct and run a storybook application, resolving with its exit code when
4
+ * the window closes. The whole per-project launcher collapses to:
5
+ *
6
+ * ```ts
7
+ * import { runStorybook, collectStoryModules } from '@gjsify/storybook';
8
+ * import * as bar from './widgets/bar.story.js';
9
+ * await runStorybook({ applicationId: 'org.example.Storybook', stories: collectStoryModules([bar]) });
10
+ * ```
11
+ */
12
+ export declare function runStorybook(options: StorybookOptions): Promise<number>;
@@ -0,0 +1,68 @@
1
+ import Adw from '@girs/adw-1';
2
+ import GObject from '@girs/gobject-2.0';
3
+ import Gtk from '@girs/gtk-4.0';
4
+ import { type StoryArgs, type StoryMeta } from '@gjsify/stories';
5
+ export declare namespace StoryWidget {
6
+ interface ConstructorProps {
7
+ meta: StoryMeta;
8
+ story: string;
9
+ args: StoryArgs;
10
+ }
11
+ }
12
+ /**
13
+ * Base class for GJS story widgets.
14
+ *
15
+ * Provides default chrome — a centered `Adw.PreferencesPage` /
16
+ * `Adw.PreferencesGroup` with title + description above a content slot — built
17
+ * programmatically (no `Template`). Simple stories compose their preview by
18
+ * calling {@link addContent}; elaborate subclasses can override the layout by
19
+ * providing their own composite template (then {@link addContent} is a no-op).
20
+ */
21
+ export declare class StoryWidget extends Adw.Bin {
22
+ private _meta;
23
+ private _story;
24
+ private _args;
25
+ private _storyContent;
26
+ private _group;
27
+ constructor(params: StoryWidget.ConstructorProps, adwParams?: Partial<Adw.Bin.ConstructorProps>);
28
+ /**
29
+ * Build the constructor params from a story's metadata, deriving the initial
30
+ * args from the controls' default values (so defaults are spelled once).
31
+ */
32
+ static fromMeta(meta: StoryMeta, story: string): StoryWidget.ConstructorProps;
33
+ get meta(): StoryMeta;
34
+ set meta(value: StoryMeta);
35
+ get story(): string;
36
+ set story(value: string);
37
+ get args(): StoryArgs;
38
+ set args(value: StoryArgs);
39
+ /** Override in subclasses — build the preview. Runs in the registry init path. */
40
+ initialize(): void;
41
+ /** Override in subclasses — react to a control change. */
42
+ updateArgs(_args: StoryArgs): void;
43
+ /** Override in subclasses to release resources / disconnect signals on deselect. */
44
+ teardown(): void;
45
+ /**
46
+ * Add a widget to the default story preview slot. No-op if the subclass
47
+ * installed its own composite template.
48
+ */
49
+ addContent(widget: Gtk.Widget): void;
50
+ private _installDefaultChrome;
51
+ private _refreshChromeText;
52
+ }
53
+ /** Wraps a story's content-build step (e.g. to install a shared action group). */
54
+ export type StoryDecorator = (build: () => void, widget: StoryWidget) => void;
55
+ /** Constructor contract for a story class: zero-arg `new`, static metadata. */
56
+ export interface StoryWidgetConstructor {
57
+ new (): StoryWidget;
58
+ $gtype: GObject.GType;
59
+ getMetadata(): StoryMeta;
60
+ }
61
+ /** A group of related stories, optionally wrapped by module-level decorators. */
62
+ export interface StoryModule {
63
+ stories: StoryWidgetConstructor[];
64
+ /** Populated by {@link StoryRegistryService.createStoryInstances}. */
65
+ instances?: StoryWidget[];
66
+ /** Decorators applied around every story's `initialize()` in this module. */
67
+ decorators?: StoryDecorator[];
68
+ }
@@ -0,0 +1 @@
1
+ export declare const STORYBOOK_CSS = "\n/* Range-control card in the controls sidebar \u2014 keeps the label + description\n from being squashed into a single column when the sidebar is narrow. */\n.story-range-row {\n padding: 0;\n}\n.story-range-row > box {\n border-radius: 12px;\n}\n";
@@ -0,0 +1,6 @@
1
+ import type Gtk from '@girs/gtk-4.0';
2
+ import type { StoryWidget } from './story-widget.js';
3
+ /** A sidebar row that remembers which story widget it selects. */
4
+ export interface StoryRow extends Gtk.ListBoxRow {
5
+ storyWidget: StoryWidget;
6
+ }
@@ -0,0 +1,44 @@
1
+ import Adw from '@girs/adw-1';
2
+ import type { StoryModule } from './story-widget.js';
3
+ /**
4
+ * Main window for the storybook. The sidebar lists story instances grouped by
5
+ * the `Category/Name` title prefix; the preview pane holds the active
6
+ * {@link StoryWidget} and an `Adw.PreferencesGroup` of controls drives its
7
+ * `args` with two-way binding.
8
+ */
9
+ export declare class StorybookWindow extends Adw.ApplicationWindow {
10
+ private _sidebar_list;
11
+ private _content_area;
12
+ private _control_panel;
13
+ private _preview_title;
14
+ private _show_controls_button;
15
+ private _controls_split_view;
16
+ private _main_split_view;
17
+ private _controlRows;
18
+ private _controlRefreshers;
19
+ private _activeStoryHandlerId;
20
+ private _activeStory;
21
+ constructor(params: Partial<Adw.ApplicationWindow.ConstructorProps>);
22
+ private _buildUI;
23
+ private _onToggleControls;
24
+ /** Populate the sidebar with story instances grouped by category. */
25
+ populateSidebar(storyModules: StoryModule[]): void;
26
+ private _clearSidebar;
27
+ private _groupStoriesByCategory;
28
+ private _addCategoryToSidebar;
29
+ private _addStoryToSidebar;
30
+ private _onStorySelected;
31
+ private _showStory;
32
+ private _updateControlPanel;
33
+ private _clearControlPanel;
34
+ private _createControlRow;
35
+ private _writeArg;
36
+ private _createTextRow;
37
+ private _createNumberRow;
38
+ private _createBooleanRow;
39
+ private _createRangeRow;
40
+ private _formatRangeValue;
41
+ private _createSelectRow;
42
+ private _createColorRow;
43
+ private _rgbaToHex;
44
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@gjsify/storybook",
3
+ "version": "0.7.5",
4
+ "description": "Reusable GTK/Adwaita storybook for GJS — author *.story.ts and browse them in a generic component explorer",
5
+ "type": "module",
6
+ "module": "lib/esm/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/esm/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "lib"
16
+ ],
17
+ "scripts": {
18
+ "clear": "rm -rf lib tmp tsconfig.tsbuildinfo test.gjs.mjs || exit 0",
19
+ "check": "gjsify tsc --noEmit",
20
+ "build": "gjsify run build:gjsify && gjsify run build:types",
21
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
22
+ "build:types": "gjsify tsc",
23
+ "build:test": "gjsify run build:test:gjs",
24
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
25
+ "test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
26
+ "test:gjs": "gjsify run test.gjs.mjs"
27
+ },
28
+ "keywords": [
29
+ "gjs",
30
+ "storybook",
31
+ "stories",
32
+ "gtk",
33
+ "adwaita",
34
+ "ui"
35
+ ],
36
+ "dependencies": {
37
+ "@girs/adw-1": "1.10.0-4.0.4",
38
+ "@girs/gdk-4.0": "4.0.0-4.0.4",
39
+ "@girs/gio-2.0": "2.88.0-4.0.4",
40
+ "@girs/gjs": "4.0.4",
41
+ "@girs/glib-2.0": "2.88.0-4.0.4",
42
+ "@girs/gobject-2.0": "2.88.0-4.0.4",
43
+ "@girs/gtk-4.0": "4.23.0-4.0.4",
44
+ "@gjsify/stories": "^0.7.5"
45
+ },
46
+ "devDependencies": {
47
+ "@gjsify/cli": "^0.7.5",
48
+ "@gjsify/unit": "^0.7.5",
49
+ "@types/node": "^25.9.2",
50
+ "typescript": "^6.0.3"
51
+ },
52
+ "gjsify": {
53
+ "runtimes": {
54
+ "gjs": "polyfill",
55
+ "node": "none",
56
+ "browser": "none",
57
+ "nativescript": "none"
58
+ }
59
+ },
60
+ "license": "MIT",
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/gjsify/gjsify.git",
64
+ "directory": "packages/framework/storybook"
65
+ },
66
+ "bugs": {
67
+ "url": "https://github.com/gjsify/gjsify/issues"
68
+ },
69
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/framework/storybook#readme"
70
+ }