@forgebook/solid 0.0.0-stage → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ensiria
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,69 @@
1
- # Temporary Holding Version
1
+ # @forgebook/solid
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Solid for [Forgebook](https://www.npmjs.com/package/forgebook): the story types, the primitives, the renderer the frame
4
+ mounts a story with, and the adapter that reads a component's props from its TypeScript types.
5
+
6
+ **Requirements:** Solid 1.9 · `forgebook` · Vite 8 · React in the project, for the catalogue's own interface (the frames
7
+ that draw your components load none) · [MIT licence](https://github.com/ensiria/forgebook/blob/develop/packages/solid/LICENSE)
8
+
9
+ ```sh
10
+ npm install --save-dev forgebook @forgebook/solid react react-dom
11
+ ```
12
+
13
+ The catalogue's configuration names it as its framework:
14
+
15
+ ```ts
16
+ // forgebook.config.ts
17
+ import { solid } from "@forgebook/solid/vite";
18
+ import { defineConfig } from "forgebook";
19
+
20
+ export default defineConfig({ framework: solid(), title: "My components", stories: ["src/**/*.stories.tsx"] });
21
+ ```
22
+
23
+ and the stories take their types from it:
24
+
25
+ ```tsx
26
+ import type { Meta, Story } from "@forgebook/solid";
27
+
28
+ import { Badge } from "./Badge";
29
+
30
+ export default { title: "Components/Badge", component: Badge } satisfies Meta<typeof Badge>;
31
+
32
+ export const Default: Story<typeof Badge> = { args: { label: "New" } };
33
+
34
+ // a render runs once: read the args inside its JSX, or spread them, and each new value reaches the story in place
35
+ export const Inline: Story<typeof Badge> = {
36
+ render: (args) => (
37
+ <p>
38
+ Status: <Badge label="" {...args} />
39
+ </p>
40
+ )
41
+ };
42
+ ```
43
+
44
+ | Import | What it is |
45
+ |---|---|
46
+ | `@forgebook/solid` | `Meta`, `Story`, `Decorator`, `Preview`, `LogicMeta`: CSF 3 typed for Solid, the args from the component's props |
47
+ | `@forgebook/solid/vite` | `solid()`, the framework adapter of `forgebook.config.ts`, with Solid's Vite plugin |
48
+ | `@forgebook/solid/texts`, `@forgebook/solid/clock` | A story's texts in the catalogue's language (`useStoryText`), and the story clock as accessors (`useStoryNow()`) |
49
+
50
+ ## What the catalogue does with Solid
51
+
52
+ A story runs once, as a Solid component does. New args from the panel, Replay or the events do not draw it again: each arg
53
+ is a signal of its own, and only what reads it updates, so a counter keeps its count and an open menu stays open. An arg
54
+ holds the value the story passed (an option object is that object, not a store's copy). The language, the switches a
55
+ decorator reads in `context.globals` and the story clock follow the same way. Read the args where Solid can follow them:
56
+ inside JSX or spread, never destructured at the top of a render. A story that reads a resource is ready, for its play
57
+ function, once the resource has loaded; what a story or its effects throw is shown in place of the story, and the next
58
+ args try it again.
59
+
60
+ Solid's plugin is the adapter's, so don't add `vite-plugin-solid` to a catalogue: it compiles your stories and
61
+ components, refreshes an edited one in its frame, and leaves the catalogue's own React alone.
62
+
63
+ Generated server tests run on Node, with Solid's server build: in the Vitest configuration they run in, give the plugin
64
+ `solid({ ssr: true })` and `environment: "node"`, then take `solidRenderer` from `@forgebook/solid/renderer`. They wait
65
+ for a story's resources. `forgebook new`, `forgebook docs --framework solid` and `forgebook init --from storybook` (from
66
+ `storybook-solidjs-vite`) read Solid too.
67
+
68
+ The rest of its exports are what the catalogue loads itself. Getting started, the catalogue's features and its commands:
69
+ [Forgebook's README](https://github.com/ensiria/forgebook/blob/develop/packages/forgebook/README.md).
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The story clock for Solid components inside a story: the clock the catalogue runs, held at the Unix epoch outside a
3
+ * catalogue.
4
+ *
5
+ * @remarks
6
+ * The clock itself is the format's (`@forgebook/story-format/clock`); this module is how a Solid component reads it,
7
+ * as accessors, Solid's way: the moment read inside JSX follows the clock's ticks and nothing else runs again. The
8
+ * frame's renderer provides it (`solidRenderer.ts`).
9
+ *
10
+ * @module
11
+ */
12
+ import { type StoryClock } from "@forgebook/story-format/clock";
13
+ import { type Accessor } from "solid-js";
14
+ /** The story clock of the frame: held at the Unix epoch outside a catalogue. */
15
+ export declare const StoryClockContext: import("solid-js").Context<Accessor<StoryClock>>;
16
+ /**
17
+ * The story's clock, for a component inside a story.
18
+ *
19
+ * @returns the clock the catalogue runs for the story, as an accessor.
20
+ */
21
+ export declare const useStoryClock: () => Accessor<StoryClock>;
22
+ /**
23
+ * The story's current moment, following each tick of the clock.
24
+ *
25
+ * @returns the moment, in milliseconds since the Unix epoch, as an accessor.
26
+ */
27
+ export declare const useStoryNow: () => Accessor<number>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The story clock for Solid components inside a story: the clock the catalogue runs, held at the Unix epoch outside a
3
+ * catalogue.
4
+ *
5
+ * @remarks
6
+ * The clock itself is the format's (`@forgebook/story-format/clock`); this module is how a Solid component reads it,
7
+ * as accessors, Solid's way: the moment read inside JSX follows the clock's ticks and nothing else runs again. The
8
+ * frame's renderer provides it (`solidRenderer.ts`).
9
+ *
10
+ * @module
11
+ */
12
+ import { heldStoryClock } from "@forgebook/story-format/clock";
13
+ import { createContext, createRenderEffect, createSignal, onCleanup, useContext } from "solid-js";
14
+ /** The story clock of the frame: held at the Unix epoch outside a catalogue. */
15
+ export const StoryClockContext = createContext(() => heldStoryClock(0));
16
+ /**
17
+ * The story's clock, for a component inside a story.
18
+ *
19
+ * @returns the clock the catalogue runs for the story, as an accessor.
20
+ */
21
+ export const useStoryClock = () => useContext(StoryClockContext);
22
+ /**
23
+ * The story's current moment, following each tick of the clock.
24
+ *
25
+ * @returns the moment, in milliseconds since the Unix epoch, as an accessor.
26
+ */
27
+ export const useStoryNow = () => {
28
+ const clock = useStoryClock();
29
+ const [now, setNow] = createSignal(clock().now());
30
+ createRenderEffect(() => {
31
+ const current = clock();
32
+ setNow(current.now());
33
+ onCleanup(current.subscribe(() => setNow(current.now())));
34
+ });
35
+ return now;
36
+ };
@@ -0,0 +1,3 @@
1
+ import type { StoryCodeWriter } from "forgebook/renderer";
2
+ /** The code writer a Solid catalogue's shell writes a story's code with. */
3
+ export declare const codeOf: StoryCodeWriter;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The code writer a Solid catalogue's shell writes a story's code with: the JSX every framework written in JSX shares
3
+ * (`forgebook/jsx-code`), under the name the generated pages import.
4
+ *
5
+ * @module
6
+ */
7
+ import { jsxCode } from "forgebook/jsx-code";
8
+ /** The code writer a Solid catalogue's shell writes a story's code with. */
9
+ export const codeOf = jsxCode;
@@ -0,0 +1,17 @@
1
+ /** A record followed value by value, and how to set it. */
2
+ export type LiveRecord<TValue> = {
3
+ /** The record: reading a value inside a reactive scope follows it. */
4
+ readonly record: Readonly<Record<string, TValue>>;
5
+ /**
6
+ * Makes the record equal to new values: a value that did not change notifies no one.
7
+ *
8
+ * @param next - the new values.
9
+ */
10
+ readonly assign: (next: Readonly<Record<string, TValue>>) => void;
11
+ };
12
+ /**
13
+ * A record followed value by value.
14
+ *
15
+ * @param initial - its first values.
16
+ */
17
+ export declare const liveRecord: <TValue>(initial: Readonly<Record<string, TValue>>) => LiveRecord<TValue>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * A record whose every value is a signal of its own: what the Solid renderer hands a story as its args and its
3
+ * `globals`, so new values reach the places that read them and nothing else runs again.
4
+ *
5
+ * @remarks
6
+ * Solid's store would do the same, but it wraps every plain object and array inside as a store too: an option a story
7
+ * passes (`value: OPTIONS[0]`) would no longer be the object the component compares it with. Here the values are kept
8
+ * as they are, only one level deep. The record carries Solid's `$PROXY` mark, so `mergeProps` and `splitProps` (what
9
+ * `<Button {...args} />` compiles to) read it lazily as they read a store, and a value set later, or a key added or
10
+ * removed, still reaches a spread.
11
+ *
12
+ * @module
13
+ */
14
+ import { $PROXY, batch, createSignal } from "solid-js";
15
+ /** Two lists of keys hold the same names in the same order. */
16
+ const isSameKeys = (current, next) => current.length === next.length && current.every((key, index) => key === next[index]);
17
+ /**
18
+ * A record followed value by value.
19
+ *
20
+ * @param initial - its first values.
21
+ */
22
+ export const liveRecord = (initial) => {
23
+ const values = new Map();
24
+ const [keys, setKeys] = createSignal(Object.keys(initial), { equals: isSameKeys });
25
+ /** The signal of a key, made the first time it is read or set. */
26
+ const signalOf = (key) => {
27
+ const existing = values.get(key);
28
+ if (existing !== undefined)
29
+ return existing;
30
+ const made = createSignal(initial[key]);
31
+ values.set(key, made);
32
+ return made;
33
+ };
34
+ const record = new Proxy({}, {
35
+ get: (_target, key) => {
36
+ if (key === $PROXY)
37
+ return record;
38
+ if (typeof key !== "string")
39
+ return undefined;
40
+ // what every object inherits (`toString`, `hasOwnProperty`) stays the object's, unless an arg takes the name
41
+ if (!keys().includes(key) && key in Object.prototype)
42
+ return Reflect.get(Object.prototype, key);
43
+ return signalOf(key)[0]();
44
+ },
45
+ has: (_target, key) => key === $PROXY || (typeof key === "string" && keys().includes(key)),
46
+ ownKeys: () => [...keys()],
47
+ getOwnPropertyDescriptor: (_target, key) => typeof key === "string" && keys().includes(key)
48
+ ? { configurable: true, enumerable: true, value: signalOf(key)[0]() }
49
+ : undefined,
50
+ set: () => false,
51
+ deleteProperty: () => false
52
+ });
53
+ const assign = (next) => {
54
+ // one update for every value: what reads two args runs once
55
+ batch(() => {
56
+ for (const key of keys())
57
+ if (!(key in next))
58
+ signalOf(key)[1](undefined);
59
+ // a function is a value here (an event's handler), not an update of the old one
60
+ for (const [key, value] of Object.entries(next))
61
+ signalOf(key)[1](() => value);
62
+ setKeys(Object.keys(next));
63
+ });
64
+ };
65
+ return { record, assign };
66
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The frame page of a Solid catalogue: Forgebook's frame, drawing its stories with Solid. Called by the page the
3
+ * plugin generates from the catalogue's configuration, not by an app.
4
+ *
5
+ * @module
6
+ */
7
+ import { type FrameConfig } from "forgebook/frame";
8
+ /**
9
+ * Shows the story the address names in the given element, drawn with Solid.
10
+ *
11
+ * @param container - the element the story renders into.
12
+ * @param config - the sources and the languages, as the shell's configuration gives them.
13
+ */
14
+ export declare const mountFrame: (container: HTMLElement, config: Omit<FrameConfig, "renderer">) => Promise<void>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The frame page of a Solid catalogue: Forgebook's frame, drawing its stories with Solid. Called by the page the
3
+ * plugin generates from the catalogue's configuration, not by an app.
4
+ *
5
+ * @module
6
+ */
7
+ import { mountFrame as mountForgebookFrame } from "forgebook/frame";
8
+ import { solidRenderer } from "./solidRenderer.js";
9
+ /**
10
+ * Shows the story the address names in the given element, drawn with Solid.
11
+ *
12
+ * @param container - the element the story renders into.
13
+ * @param config - the sources and the languages, as the shell's configuration gives them.
14
+ */
15
+ export const mountFrame = (container, config) => mountForgebookFrame(container, { ...config, renderer: solidRenderer });
@@ -0,0 +1,3 @@
1
+ import type { StoryRenderer } from "forgebook/renderer";
2
+ /** Solid as the frame draws with it. */
3
+ export declare const solidRenderer: StoryRenderer;
@@ -0,0 +1,159 @@
1
+ import { storyTextOf } from "@forgebook/story-format/texts";
2
+ import { storyContextOf, untracedStep } from "forgebook/renderer";
3
+ import { createComponent, createSignal, ErrorBoundary, onMount, Show, Suspense } from "solid-js";
4
+ import { render, renderToStringAsync } from "solid-js/web";
5
+ import { StoryClockContext } from "./clock.js";
6
+ import { liveRecord } from "./liveRecord.js";
7
+ import { storyNode } from "./storyNode.js";
8
+ import { StoryTextsContext } from "./texts.js";
9
+ /** An error as the frame takes it. */
10
+ const toError = (error) => (error instanceof Error ? error : new Error(String(error)));
11
+ /**
12
+ * Whether two drawings draw the same story: by its id, as the frame names it. A drawing of the same story is the same
13
+ * story drawn again with other values, however it was read (the frame reads it once; a test may read it anew), so its
14
+ * args, switches and texts go into what it holds; another story starts over.
15
+ *
16
+ * @param current - the drawing on screen.
17
+ * @param next - the new one.
18
+ */
19
+ const isSameStory = (current, next) => current.identity.id === next.identity.id;
20
+ /**
21
+ * The story in the texts' and the clock's providers; the language's provider is a decorator of the catalogue's
22
+ * preview, inside these.
23
+ *
24
+ * @param texts - the shown language's texts.
25
+ * @param clock - the story's clock.
26
+ * @param story - draws the story.
27
+ */
28
+ const inProviders = (texts, clock, story) => createComponent(StoryTextsContext.Provider, {
29
+ value: texts,
30
+ get children() {
31
+ return createComponent(StoryClockContext.Provider, {
32
+ value: clock,
33
+ get children() {
34
+ return story();
35
+ }
36
+ });
37
+ }
38
+ });
39
+ /**
40
+ * What a render and a decorator receive, reading the moment's values when they are read: what reads them inside JSX
41
+ * follows them.
42
+ *
43
+ * @param args - the live args.
44
+ * @param view - the live view.
45
+ * @param canvas - the story's canvas.
46
+ */
47
+ const liveContext = (args, view, canvas) => ({
48
+ args,
49
+ step: untracedStep,
50
+ canvasElement: canvas,
51
+ get text() {
52
+ return (key, values) => storyTextOf(view.texts())(key, values);
53
+ },
54
+ get clock() {
55
+ return view.clock();
56
+ },
57
+ globals: view.globals,
58
+ get loaded() {
59
+ return view.loaded();
60
+ },
61
+ get id() {
62
+ return view.scene().identity.id;
63
+ },
64
+ get name() {
65
+ return view.scene().identity.name;
66
+ },
67
+ get title() {
68
+ return view.scene().identity.title;
69
+ },
70
+ get parameters() {
71
+ return view.scene().story.parameters;
72
+ }
73
+ });
74
+ /** Solid as the frame draws with it. */
75
+ export const solidRenderer = {
76
+ mount: (canvas, first, onError) => {
77
+ const args = liveRecord(first.args);
78
+ const globals = liveRecord(first.globals);
79
+ const [texts, setTexts] = createSignal(first.texts);
80
+ const [clock, setClock] = createSignal(first.clock);
81
+ const [loaded, setLoaded] = createSignal(first.loaded);
82
+ const [scene, setScene] = createSignal(first);
83
+ /** Whether the story threw, and how to try again. */
84
+ let failure;
85
+ let markDrawn = () => undefined;
86
+ const drawn = new Promise((resolve) => {
87
+ markDrawn = resolve;
88
+ });
89
+ const view = { texts, clock, globals: globals.record, loaded, scene };
90
+ const context = liveContext(args.record, view, canvas);
91
+ /** Says the story is drawn, once its effects ran without an error; a component of no output. */
92
+ const Drawn = () => {
93
+ onMount(() => {
94
+ // after the effects of this drain: one of the story's own may still throw in it, and a story that threw is not drawn
95
+ queueMicrotask(() => {
96
+ if (failure === undefined)
97
+ markDrawn();
98
+ });
99
+ });
100
+ return null;
101
+ };
102
+ /** The story on screen, made again only for another story. */
103
+ const KeyedStory = () => Show({
104
+ get when() {
105
+ return scene();
106
+ },
107
+ keyed: true,
108
+ children: (current) => [
109
+ storyNode(current.story, context, current.decorators),
110
+ createComponent(Drawn, {})
111
+ ]
112
+ });
113
+ const dispose = render(() => inProviders(texts, clock, () => createComponent(ErrorBoundary, {
114
+ fallback: (error, reset) => {
115
+ if (failure === undefined)
116
+ onError(toError(error));
117
+ failure = { retry: reset };
118
+ return null;
119
+ },
120
+ // read again after an error, so a try starts the story anew
121
+ get children() {
122
+ return createComponent(Suspense, {
123
+ get children() {
124
+ // new args and switches reach the story in place
125
+ return createComponent(KeyedStory, {});
126
+ }
127
+ });
128
+ }
129
+ })), canvas);
130
+ return {
131
+ drawn,
132
+ update: (next) => {
133
+ // only an error that stood before these values is tried again: one they cause is reported, and stands
134
+ const standing = failure;
135
+ args.assign(next.args);
136
+ globals.assign(next.globals);
137
+ setTexts(() => next.texts);
138
+ setClock(() => next.clock);
139
+ setLoaded(() => next.loaded);
140
+ if (!isSameStory(scene(), next))
141
+ setScene(() => next);
142
+ if (standing !== undefined && failure === standing) {
143
+ failure = undefined;
144
+ standing.retry();
145
+ }
146
+ },
147
+ unmount: dispose
148
+ };
149
+ },
150
+ renderToString: async (drawing) => {
151
+ const context = storyContextOf(drawing, drawing.args, () => document.body);
152
+ try {
153
+ return await renderToStringAsync(() => inProviders(() => drawing.texts, () => drawing.clock, () => storyNode(drawing.story, context, drawing.decorators)));
154
+ }
155
+ catch (error) {
156
+ throw toError(error);
157
+ }
158
+ }
159
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The story types for Solid: what a story file of a Solid library is written with
3
+ * (`import type { Meta, Story } from "@forgebook/solid"`).
4
+ *
5
+ * @remarks
6
+ * The format's types (`@forgebook/story-format/csf`) are generic over a framework; these name them for Solid, so a
7
+ * meta writes `satisfies Meta<typeof Button>` and its stories' args are the component's props, a prop that takes a
8
+ * text also as `storyText("key")`. A render receives the args as props: reading `args.label` inside JSX follows it,
9
+ * and spreading them (`<Button {...args} />`) keeps every one live. Types only: a library that writes stories depends
10
+ * on this package, never on the catalogue that shows them.
11
+ *
12
+ * @module
13
+ */
14
+ import type { Decorator as CsfDecorator, CsfMeta, CsfPreview, CsfStory, StoryParameters, WrittenArg } from "@forgebook/story-format/csf";
15
+ import type { JSX } from "solid-js";
16
+ /**
17
+ * Any component: a function of its props, run once (Solid's `Component`). The props are what the stories' args fill
18
+ * in; they are `never` here so a component of any props is assignable.
19
+ */
20
+ export type AnyComponent = (props: never) => JSX.Element;
21
+ /** Solid, as the format's types read it. */
22
+ export type SolidRenderer = {
23
+ /** A component. */
24
+ readonly component: AnyComponent;
25
+ /** What a story draws. */
26
+ readonly storyResult: JSX.Element;
27
+ };
28
+ /** A component's props, as its function takes them. */
29
+ type PropsOf<TComponent> = TComponent extends (props: infer TProps) => unknown ? TProps : never;
30
+ /** A component's props, every one optional: what a story's args may set, and what its render receives. */
31
+ export type ArgsOf<TComponent extends AnyComponent> = Partial<PropsOf<TComponent>>;
32
+ /**
33
+ * What a story's `args` may write: the component's props, every one optional, a prop that takes a text also as
34
+ * `storyText("key")`. The render and the play function receive `ArgsOf`, the text already in the shown language.
35
+ */
36
+ export type WrittenArgsOf<TComponent extends AnyComponent> = {
37
+ [TName in keyof PropsOf<TComponent>]?: WrittenArg<PropsOf<TComponent>[TName]>;
38
+ };
39
+ /** A page with no component (a foundations page): no args. */
40
+ export type NoComponent = () => null;
41
+ /** A Solid story file's default export: `{ title, component, args… } satisfies Meta<typeof Button>`. */
42
+ export type Meta<TComponent extends AnyComponent = NoComponent> = CsfMeta<SolidRenderer, TComponent, ArgsOf<TComponent>, WrittenArgsOf<TComponent>>;
43
+ /** A Solid story: a named export of a story file. */
44
+ export type Story<TComponent extends AnyComponent = NoComponent> = CsfStory<SolidRenderer, ArgsOf<TComponent>, WrittenArgsOf<TComponent>>;
45
+ /** Any primitive or function a story about logic documents (`createUndo`); its parameter is `never` so any signature is assignable. */
46
+ export type AnyFunction = (...parameters: never[]) => unknown;
47
+ /** No args: a story about logic draws a scene of its own, which takes none. */
48
+ type NoArgs = Readonly<Record<string, never>>;
49
+ /**
50
+ * The default export of a story file about logic, written `satisfies LogicMeta<typeof createUndo>`: `parameters` must say
51
+ * `logic: true`, so a primitive's file is never taken for a component's. Its component is the primitive or the function, whose
52
+ * signature the catalogue documents.
53
+ */
54
+ export type LogicMeta<TFunction extends AnyFunction> = CsfMeta<SolidRenderer, TFunction, NoArgs, NoArgs> & {
55
+ /** The catalogue's settings, `logic: true` among them. */
56
+ readonly parameters: StoryParameters & {
57
+ readonly logic: true;
58
+ };
59
+ };
60
+ /**
61
+ * A story about logic: a `render` that draws a scene using the primitive, required (without one, the catalogue would
62
+ * draw the primitive as a component), and a `play` that tries it.
63
+ */
64
+ export type LogicStory = CsfStory<SolidRenderer, NoArgs, NoArgs> & Required<Pick<CsfStory<SolidRenderer, NoArgs, NoArgs>, "render">>;
65
+ /** Wraps a Solid story: a provider, a container of a given width. */
66
+ export type Decorator<TArgs> = CsfDecorator<SolidRenderer, TArgs>;
67
+ /**
68
+ * A Solid catalogue's preview (`forgebook.preview.ts`): decorators around every story (its providers, the language's
69
+ * reading `context.globals.lang`), parameters under every meta's, a setup before any story renders.
70
+ */
71
+ export type Preview = CsfPreview<SolidRenderer>;
72
+ export {};
File without changes
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A story drawn as Solid: its render (or the meta's component) inside its decorators, the story's, the meta's and
3
+ * the catalogue's (its preview's), in that order outward. The story's canvas and each cell of its matrix draw it the
4
+ * same way, in a page and on a server.
5
+ *
6
+ * @module
7
+ */
8
+ import type { StoryContext } from "@forgebook/story-format/csf";
9
+ import type { LoadedArgs, LoadedDecorator, LoadedStory } from "forgebook/story-module";
10
+ import { type JSX } from "solid-js";
11
+ /**
12
+ * The story with these args: its render (or the meta's component, with the args as its props), inside its
13
+ * decorators. Each part runs once, as a Solid component does; what it reads of the args follows them.
14
+ *
15
+ * @param loaded - the story.
16
+ * @param context - the args and the canvas its render, play and decorators read.
17
+ * @param outer - the catalogue's decorators, outside the story's and the meta's.
18
+ */
19
+ export declare const storyNode: (loaded: LoadedStory, context: StoryContext<LoadedArgs>, outer: readonly LoadedDecorator[]) => JSX.Element;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A story drawn as Solid: its render (or the meta's component) inside its decorators, the story's, the meta's and
3
+ * the catalogue's (its preview's), in that order outward. The story's canvas and each cell of its matrix draw it the
4
+ * same way, in a page and on a server.
5
+ *
6
+ * @module
7
+ */
8
+ import { createComponent, untrack } from "solid-js";
9
+ /**
10
+ * A loaded story read as Solid's: the one place Solid's types meet the story file's module.
11
+ *
12
+ * @remarks
13
+ * The frame reads a story file without its types (`forgebook/story-module`: a function is taken with the signature
14
+ * CSF gives it), so what its functions return is `unknown` there. A Solid story file is written against
15
+ * `@forgebook/solid`'s `Meta` and `Story`, and its catalogue's preview against its `Preview`, whose components,
16
+ * renders and decorators return `JSX.Element`: that is what this view takes on trust, as the frame takes the
17
+ * functions' parameters.
18
+ *
19
+ * @param story - the loaded story.
20
+ */
21
+ const asSolidStory = (story) => story;
22
+ /**
23
+ * The story with these args: its render (or the meta's component, with the args as its props), inside its
24
+ * decorators. Each part runs once, as a Solid component does; what it reads of the args follows them.
25
+ *
26
+ * @param loaded - the story.
27
+ * @param context - the args and the canvas its render, play and decorators read.
28
+ * @param outer - the catalogue's decorators, outside the story's and the meta's.
29
+ */
30
+ export const storyNode = (loaded, context, outer) => {
31
+ const story = asSolidStory({ ...loaded, decorators: [...loaded.decorators, ...outer] });
32
+ const { component, render } = story;
33
+ // each part runs untracked, as a component's body does: a decorator writing `{story()}` in its JSX calls it inside
34
+ // an effect, which would otherwise make the whole story again whenever the render read an arg at its top level
35
+ const renderStory = () => untrack(() => {
36
+ if (render !== undefined)
37
+ return render(context.args, context);
38
+ // the args themselves are the props, not a copy: a component reads each one as it changes
39
+ return component === undefined ? null : createComponent(component, context.args);
40
+ });
41
+ const decorated = story.decorators.reduce((inner, decorator) => () => untrack(() => decorator(inner, context)), renderStory);
42
+ return decorated();
43
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The texts inside the stories for Solid components inside a story: the shown language's table, as the frame provides
3
+ * it, read with `useStoryText()`.
4
+ *
5
+ * @remarks
6
+ * The table and the text function are the format's (`@forgebook/story-format/texts`); this module is how a Solid
7
+ * component reads them. The frame's renderer provides the table as an accessor (`solidRenderer.ts`), so a text read
8
+ * inside JSX follows the language without the story running again; outside a catalogue there is none, and a text is
9
+ * its key in brackets.
10
+ *
11
+ * @module
12
+ */
13
+ import { type StoryTextFunction } from "@forgebook/story-format/texts";
14
+ import { type Accessor } from "solid-js";
15
+ /** The texts of the shown language, as the catalogue's frame provides them; none outside a catalogue. */
16
+ export declare const StoryTextsContext: import("solid-js").Context<Accessor<Readonly<Record<string, string>>>>;
17
+ /**
18
+ * The text function of the shown language, for a component inside a story: each call reads the table of the moment.
19
+ *
20
+ * @returns `text(key, values?)`.
21
+ */
22
+ export declare const useStoryText: () => StoryTextFunction;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The texts inside the stories for Solid components inside a story: the shown language's table, as the frame provides
3
+ * it, read with `useStoryText()`.
4
+ *
5
+ * @remarks
6
+ * The table and the text function are the format's (`@forgebook/story-format/texts`); this module is how a Solid
7
+ * component reads them. The frame's renderer provides the table as an accessor (`solidRenderer.ts`), so a text read
8
+ * inside JSX follows the language without the story running again; outside a catalogue there is none, and a text is
9
+ * its key in brackets.
10
+ *
11
+ * @module
12
+ */
13
+ import { storyTextOf } from "@forgebook/story-format/texts";
14
+ import { createContext, useContext } from "solid-js";
15
+ /** The texts of the shown language, as the catalogue's frame provides them; none outside a catalogue. */
16
+ export const StoryTextsContext = createContext(() => ({}));
17
+ /**
18
+ * The text function of the shown language, for a component inside a story: each call reads the table of the moment.
19
+ *
20
+ * @returns `text(key, values?)`.
21
+ */
22
+ export const useStoryText = () => {
23
+ const table = useContext(StoryTextsContext);
24
+ return (key, values) => storyTextOf(table())(key, values);
25
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The docs module of `@forgebook/solid`: the plugin's worker and `forgebook-docs` load it through the adapter
3
+ * (`forgebook/vite/adapter`). A Solid component is a function of its props, run once, read as every such framework's is
4
+ * (`forgebook/vite/function-component-docs`): its props from the first parameter's type, events as `on*` props, its
5
+ * parts from its `classNames`, `styles` and `refs` maps.
6
+ *
7
+ * @module
8
+ */
9
+ import type { ComponentDocs, NoComponentDocs } from "forgebook/component-docs";
10
+ import type { ExtractFileComponent, ExtractStoryComponent } from "forgebook/vite/adapter";
11
+ /** The module a Solid story file imports its types from (`Meta`, `Story`), for the stories `forgebook new` writes. */
12
+ export declare const storyTypesModule = "@forgebook/solid";
13
+ /** Reads a story file's component with the file that declares it. */
14
+ export declare const extractStoryComponent: ExtractStoryComponent;
15
+ /** Reads a component a file exports, for writing its first stories (`forgebook new`). */
16
+ export declare const extractFileComponent: ExtractFileComponent;
17
+ /**
18
+ * The docs of a story file's component.
19
+ *
20
+ * @param storyFile - the story file's absolute path.
21
+ * @returns the component's docs, `{ name: null }` when the meta names no component, or a message saying why the file
22
+ * cannot be read.
23
+ */
24
+ export declare const extractComponentDocs: (storyFile: string) => ComponentDocs | NoComponentDocs | string;
@@ -0,0 +1,15 @@
1
+ import { functionComponentDocs, functionFileComponent, functionStoryComponent } from "forgebook/vite/function-component-docs";
2
+ /** The module a Solid story file imports its types from (`Meta`, `Story`), for the stories `forgebook new` writes. */
3
+ export const storyTypesModule = "@forgebook/solid";
4
+ /** Reads a story file's component with the file that declares it. */
5
+ export const extractStoryComponent = functionStoryComponent;
6
+ /** Reads a component a file exports, for writing its first stories (`forgebook new`). */
7
+ export const extractFileComponent = functionFileComponent;
8
+ /**
9
+ * The docs of a story file's component.
10
+ *
11
+ * @param storyFile - the story file's absolute path.
12
+ * @returns the component's docs, `{ name: null }` when the meta names no component, or a message saying why the file
13
+ * cannot be read.
14
+ */
15
+ export const extractComponentDocs = (storyFile) => functionComponentDocs(storyFile);
@@ -0,0 +1,21 @@
1
+ import { type FrameworkAdapter } from "forgebook/vite/adapter";
2
+ /** Options of Solid's own Vite plugin, as the adapter passes them on. */
3
+ export type SolidOptions = {
4
+ /**
5
+ * Compile the stories for a server render as well (`vite-plugin-solid`'s `ssr`): what the generated server tests
6
+ * need, run by Vitest on Node, where Solid's JSX must become strings rather than DOM nodes. A catalogue needs none.
7
+ * `false` by default.
8
+ */
9
+ readonly ssr?: boolean;
10
+ };
11
+ /**
12
+ * Solid, as the plugin reads its components: the docs module the docs' worker loads is this package's `extractDocs.ts`
13
+ * (the props from the first parameter's type, events as `on*` props); the generated pages draw with its renderer and
14
+ * write code with the JSX writer; Vite gets Solid's plugin, which compiles the stories' JSX into the DOM operations its
15
+ * signals update and refreshes an edited component in its frame. The core's own files are left to Vite: its shell is
16
+ * React.
17
+ *
18
+ * @param options - Solid's plugin's options; none for a catalogue.
19
+ * @returns the adapter.
20
+ */
21
+ export declare const solid: (options?: SolidOptions) => FrameworkAdapter;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Forgebook's adapter for Solid, for the Vite plugin: `forgebookPlugin({ framework: solid() })`.
3
+ *
4
+ * @module
5
+ */
6
+ import { extname } from "node:path";
7
+ import { FORGEBOOK_FILES } from "forgebook/vite/adapter";
8
+ /**
9
+ * Solid, as the plugin reads its components: the docs module the docs' worker loads is this package's `extractDocs.ts`
10
+ * (the props from the first parameter's type, events as `on*` props); the generated pages draw with its renderer and
11
+ * write code with the JSX writer; Vite gets Solid's plugin, which compiles the stories' JSX into the DOM operations its
12
+ * signals update and refreshes an edited component in its frame. The core's own files are left to Vite: its shell is
13
+ * React.
14
+ *
15
+ * @param options - Solid's plugin's options; none for a catalogue.
16
+ * @returns the adapter.
17
+ */
18
+ export const solid = (options = {}) => ({
19
+ name: "solid",
20
+ // beside this module, with its extension: `.ts` in the sources, `.js` once built for npm
21
+ docsModule: new URL(`./extractDocs${extname(import.meta.url)}`, import.meta.url).href,
22
+ pages: { frame: "@forgebook/solid/frame", code: "@forgebook/solid/code" },
23
+ browserDependencies: ["solid-js", "solid-js/web"],
24
+ vitePlugins: async () => {
25
+ const { default: plugin } = await import("vite-plugin-solid");
26
+ return [plugin({ exclude: [FORGEBOOK_FILES], ssr: options.ssr ?? false })];
27
+ }
28
+ });
package/package.json CHANGED
@@ -1,6 +1,82 @@
1
1
  {
2
2
  "name": "@forgebook/solid",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.1",
4
+ "description": "Forgebook for Solid: the story types and primitives, the renderer the frame mounts a story with, args updating in place as signals do, and the adapter that reads a Solid component's props.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ensiria/forgebook.git",
9
+ "directory": "packages/solid"
10
+ },
11
+ "type": "module",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "engines": {
16
+ "node": ">=22.18"
17
+ },
18
+ "sideEffects": false,
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/src/stories.d.ts",
22
+ "default": "./dist/src/stories.js"
23
+ },
24
+ "./clock": {
25
+ "types": "./dist/src/clock.d.ts",
26
+ "default": "./dist/src/clock.js"
27
+ },
28
+ "./texts": {
29
+ "types": "./dist/src/texts.d.ts",
30
+ "default": "./dist/src/texts.js"
31
+ },
32
+ "./renderer": {
33
+ "types": "./dist/src/solidRenderer.d.ts",
34
+ "default": "./dist/src/solidRenderer.js"
35
+ },
36
+ "./frame": {
37
+ "types": "./dist/src/mountFrame.d.ts",
38
+ "default": "./dist/src/mountFrame.js"
39
+ },
40
+ "./code": {
41
+ "types": "./dist/src/code.d.ts",
42
+ "default": "./dist/src/code.js"
43
+ },
44
+ "./vite": {
45
+ "types": "./dist/src/vite/solid.d.ts",
46
+ "default": "./dist/src/vite/solid.js"
47
+ },
48
+ "./docs": {
49
+ "types": "./dist/src/vite/extractDocs.d.ts",
50
+ "default": "./dist/src/vite/extractDocs.js"
51
+ }
52
+ },
53
+ "dependencies": {
54
+ "vite-plugin-solid": "2.11.15",
55
+ "@forgebook/story-format": "0.2.1"
56
+ },
57
+ "peerDependencies": {
58
+ "solid-js": "^1.9.0",
59
+ "forgebook": "^0.3.0"
60
+ },
61
+ "devDependencies": {
62
+ "@testing-library/jest-dom": "6.9.1",
63
+ "@types/node": "24.19.2",
64
+ "happy-dom": "21.0.4",
65
+ "solid-js": "1.9.18",
66
+ "vite": "8.3.5",
67
+ "vitest": "5.0.5",
68
+ "@forgebook/tools": "0.0.0",
69
+ "@forgebook/testing": "0.2.1",
70
+ "forgebook": "0.3.0",
71
+ "@forgebook/typescript-config": "0.0.0"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public"
75
+ },
76
+ "scripts": {
77
+ "build": "package-build build",
78
+ "manifest": "package-build manifest",
79
+ "test": "vitest run",
80
+ "typecheck": "tsc -p src && tsc -p src/vite && tsc -p test"
81
+ }
6
82
  }