@forgebook/svelte 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 +21 -0
- package/README.md +93 -2
- package/dist/src/StoryHost.svelte +34 -0
- package/dist/src/clock.d.ts +29 -0
- package/dist/src/clock.js +54 -0
- package/dist/src/mountFrame.d.ts +14 -0
- package/dist/src/mountFrame.js +15 -0
- package/dist/src/snippetArgs.d.ts +20 -0
- package/dist/src/snippetArgs.js +22 -0
- package/dist/src/stories.d.ts +92 -0
- package/dist/src/stories.js +0 -0
- package/dist/src/storyCell.d.ts +17 -0
- package/dist/src/storyCell.js +39 -0
- package/dist/src/storyMarkup.d.ts +24 -0
- package/dist/src/storyMarkup.js +89 -0
- package/dist/src/storyNode.d.ts +39 -0
- package/dist/src/storyNode.js +66 -0
- package/dist/src/storyView.d.ts +28 -0
- package/dist/src/storyView.js +10 -0
- package/dist/src/svelteRenderer.d.ts +19 -0
- package/dist/src/svelteRenderer.js +196 -0
- package/dist/src/textSnippet.d.ts +25 -0
- package/dist/src/textSnippet.js +30 -0
- package/dist/src/texts.d.ts +18 -0
- package/dist/src/texts.js +22 -0
- package/dist/src/vite/componentProps.d.ts +23 -0
- package/dist/src/vite/componentProps.js +132 -0
- package/dist/src/vite/extractDocs.d.ts +37 -0
- package/dist/src/vite/extractDocs.js +249 -0
- package/dist/src/vite/svelte.d.ts +20 -0
- package/dist/src/vite/svelte.js +41 -0
- package/dist/src/vite/svelteScript.d.ts +13 -0
- package/dist/src/vite/svelteScript.js +61 -0
- package/dist/src/vite/svelteSources.d.ts +18 -0
- package/dist/src/vite/svelteSources.js +127 -0
- package/package.json +84 -3
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,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @forgebook/svelte
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Svelte 5 for [Forgebook](https://www.npmjs.com/package/forgebook): the story types, the context helpers, the renderer the
|
|
4
|
+
frame mounts a story with, and the adapter that reads a component's props, snippets and callback props from its types.
|
|
5
|
+
|
|
6
|
+
**Requirements:** Svelte 5.46 · `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/svelte/LICENSE)
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install --save-dev forgebook @forgebook/svelte react react-dom
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The catalogue's configuration names it as its framework:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// forgebook.config.ts
|
|
17
|
+
import { svelte } from "@forgebook/svelte/vite";
|
|
18
|
+
import { defineConfig } from "forgebook";
|
|
19
|
+
|
|
20
|
+
export default defineConfig({ framework: svelte(), title: "My components", stories: ["src/**/*.stories.ts"] });
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
and the stories take their types from it:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import type { Meta, Story } from "@forgebook/svelte";
|
|
27
|
+
|
|
28
|
+
import Badge from "./Badge.svelte";
|
|
29
|
+
|
|
30
|
+
export default { title: "Components/Badge", component: Badge } satisfies Meta<typeof Badge>;
|
|
31
|
+
|
|
32
|
+
// the component with its args: its props, its callback props (`onremove`) among them
|
|
33
|
+
export const Default: Story<typeof Badge> = { args: { label: "New" } };
|
|
34
|
+
|
|
35
|
+
// a snippet filled with a text, as `#name`: `#children` is the content
|
|
36
|
+
export const WithIcon: Story<typeof Badge> = { args: { label: "New", "#icon": "*", "#children": "Details" } };
|
|
37
|
+
|
|
38
|
+
// a render, returning the component with props of its choosing, as Storybook's Svelte stories do
|
|
39
|
+
export const Danger: Story<typeof Badge> = {
|
|
40
|
+
render: (args) => ({ Component: Badge, props: { ...args, tone: "danger" } })
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A story is written in a `.ts` file, as CSF 3: a render returns a component, drawn with the args, or
|
|
45
|
+
`{ Component, props }`. A story that needs markup of its own (a component inside a layout, a `bind:`) is a small
|
|
46
|
+
`.svelte` file the render returns. A decorator returns the same, and the story reaches its component as `children`:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// forgebook.preview.ts
|
|
50
|
+
import type { Preview } from "@forgebook/svelte";
|
|
51
|
+
|
|
52
|
+
import ThemeFrame from "./ThemeFrame.svelte"; // sets its context, then {@render children()}
|
|
53
|
+
|
|
54
|
+
export default {
|
|
55
|
+
decorators: [(_story, context) => ({ Component: ThemeFrame, props: { theme: context.globals["theme"] } })]
|
|
56
|
+
} satisfies Preview;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A snippet a story passes itself goes under the prop's own name (`children: createRawSnippet(…)`); the panel edits a
|
|
60
|
+
snippet as the text that fills it. The panel records a story's callback props in Events. A story file checked with `tsc`
|
|
61
|
+
sees your `*.svelte` shim; `svelte-check` checks its args against the real component.
|
|
62
|
+
|
|
63
|
+
| Import | What it is |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `@forgebook/svelte` | `Meta`, `Story`, `Decorator`, `Preview`, `LogicMeta`: CSF 3 typed for Svelte, the args from the component's props |
|
|
66
|
+
| `@forgebook/svelte/vite` | `svelte()`, the framework adapter of `forgebook.config.ts`, with Svelte's Vite plugin |
|
|
67
|
+
| `@forgebook/svelte/texts`, `@forgebook/svelte/clock` | `getStoryText()`, a story's texts in the catalogue's language; `getStoryClock()` and `getStoryNow()`, the story clock, read as `.current` |
|
|
68
|
+
|
|
69
|
+
## What the catalogue reads
|
|
70
|
+
|
|
71
|
+
Props come from the component's types: `svelte2tsx`, which Svelte's language server and `svelte-check` use, writes each
|
|
72
|
+
`.svelte` file as TypeScript, so a `$props()` typed in TypeScript and one typed with JSDoc read the same, and the
|
|
73
|
+
destructuring gives each prop's default (`$bindable` included). The `<!-- @component -->` comment describes the
|
|
74
|
+
component, with `@useWhen` and the other tags; a TSDoc block that opens its script, with `@module`, is its summary. A
|
|
75
|
+
snippet is listed as the arg that fills it (`#icon`), a function prop named `on…` as an event, and what the component
|
|
76
|
+
passes on to an element (`HTMLAttributes`) is left out. A function with runes in a `.svelte.ts` module is documented by its
|
|
77
|
+
signature in a story about logic (`LogicMeta`).
|
|
78
|
+
|
|
79
|
+
Svelte's own plugin is the adapter's, and it reads your `svelte.config.js`, so don't add `@sveltejs/vite-plugin-svelte` to
|
|
80
|
+
a catalogue; in a Vite or Vitest configuration of your own that already has it, add `forgebookPlugin` without the
|
|
81
|
+
adapter's `vitePlugins()`. `svelte({ plugin })` passes options to Svelte's plugin, over what it reads from
|
|
82
|
+
`svelte.config.js`; they are `@sveltejs/vite-plugin-svelte`'s own, so they follow its major version. A SvelteKit app's components, which read `$app/*`, are drawn by
|
|
83
|
+
[`@forgebook/sveltekit`](https://www.npmjs.com/package/@forgebook/sveltekit), built on this adapter. The frame says a story is drawn once Svelte has settled, so a component that awaits (`experimental.async`)
|
|
84
|
+
is drawn before its play runs. Generated server tests take `svelteRenderer` from `@forgebook/svelte/renderer` and run in
|
|
85
|
+
Node (`environment: "node"`), since Svelte compiles a component apart for the server; the catalogue's
|
|
86
|
+
`forgebook new`, `forgebook docs --framework svelte` and `forgebook init --from storybook` (from `@storybook/svelte-vite`)
|
|
87
|
+
read Svelte too.
|
|
88
|
+
|
|
89
|
+
A snippet made from a text is drawn in a `span`, since Svelte draws a snippet made outside a component from one element.
|
|
90
|
+
Stories written in `.stories.svelte` files (Svelte CSF) are not read: write them in CSF 3, as above. "Used in" follows
|
|
91
|
+
React's JSX, not Svelte markup, so it lists nothing yet.
|
|
92
|
+
|
|
93
|
+
The rest of its exports are what the catalogue loads itself. Getting started, the catalogue's features and its commands:
|
|
94
|
+
[Forgebook's README](https://github.com/ensiria/forgebook/blob/develop/packages/forgebook/README.md).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
What the renderer mounts in a story's canvas, and renders on the server: the story's tree of components (`storyNode.ts`),
|
|
4
|
+
each drawn with its props and the node inside it as its `children`, in an error boundary.
|
|
5
|
+
|
|
6
|
+
The state lives in the renderer (`svelteRenderer.ts`), in values Svelte follows: new args draw again what read them, and a
|
|
7
|
+
component whose node keeps its component keeps its state. A new try after an error draws the story anew. An error in an
|
|
8
|
+
event handler is the handler's, as in a page: a boundary does not catch it. On the server a boundary catches nothing, so
|
|
9
|
+
an error fails the render. The file imports nothing but types, so it is published as it is.
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import type { StoryNode } from "./storyNode.ts";
|
|
13
|
+
import type { StoryScene } from "./svelteRenderer.ts";
|
|
14
|
+
|
|
15
|
+
const { scene }: { readonly scene: StoryScene } = $props();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#snippet draw(node: StoryNode)}
|
|
19
|
+
{#if node.inner === undefined}
|
|
20
|
+
<node.component {...node.props} />
|
|
21
|
+
{:else}
|
|
22
|
+
{@const inner = node.inner}
|
|
23
|
+
<node.component {...node.props}>{@render draw(inner)}</node.component>
|
|
24
|
+
{/if}
|
|
25
|
+
{/snippet}
|
|
26
|
+
|
|
27
|
+
{#key scene.attempt}
|
|
28
|
+
{#if !scene.isFailed}
|
|
29
|
+
<svelte:boundary onerror={scene.fail}>
|
|
30
|
+
{@const node = scene.node}
|
|
31
|
+
{@render draw(node)}
|
|
32
|
+
</svelte:boundary>
|
|
33
|
+
{/if}
|
|
34
|
+
{/key}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The story clock for Svelte 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 Svelte component reads it,
|
|
7
|
+
* each value a `current` that a template follows, as Svelte's own reactive values are read. The moment follows the
|
|
8
|
+
* clock's ticks through `createSubscriber`, so it listens only while something reads it, and not at all on the server.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { type StoryClock } from "@forgebook/story-format/clock";
|
|
13
|
+
/** A value of the moment, read as Svelte's reactive values are. */
|
|
14
|
+
export type StoryValue<TValue> = {
|
|
15
|
+
/** The value; a template or an effect that reads it follows it. */
|
|
16
|
+
readonly current: TValue;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The story's clock, for a component inside a story; called while the component is initialised.
|
|
20
|
+
*
|
|
21
|
+
* @returns the clock the catalogue runs for the story, which is replaced when the story starts over.
|
|
22
|
+
*/
|
|
23
|
+
export declare const getStoryClock: () => StoryValue<StoryClock>;
|
|
24
|
+
/**
|
|
25
|
+
* The story's current moment, which follows each tick of the clock; called while the component is initialised.
|
|
26
|
+
*
|
|
27
|
+
* @returns the moment, in milliseconds since the Unix epoch.
|
|
28
|
+
*/
|
|
29
|
+
export declare const getStoryNow: () => StoryValue<number>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The story clock for Svelte 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 Svelte component reads it,
|
|
7
|
+
* each value a `current` that a template follows, as Svelte's own reactive values are read. The moment follows the
|
|
8
|
+
* clock's ticks through `createSubscriber`, so it listens only while something reads it, and not at all on the server.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { heldStoryClock } from "@forgebook/story-format/clock";
|
|
13
|
+
import { createSubscriber } from "svelte/reactivity";
|
|
14
|
+
import { storyView } from "./storyView.js";
|
|
15
|
+
/** The clock outside a catalogue. */
|
|
16
|
+
const HELD_CLOCK = heldStoryClock(0);
|
|
17
|
+
/**
|
|
18
|
+
* The story's clock, for a component inside a story; called while the component is initialised.
|
|
19
|
+
*
|
|
20
|
+
* @returns the clock the catalogue runs for the story, which is replaced when the story starts over.
|
|
21
|
+
*/
|
|
22
|
+
export const getStoryClock = () => {
|
|
23
|
+
const view = storyView();
|
|
24
|
+
return {
|
|
25
|
+
get current() {
|
|
26
|
+
return view?.clock ?? HELD_CLOCK;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The story's current moment, which follows each tick of the clock; called while the component is initialised.
|
|
32
|
+
*
|
|
33
|
+
* @returns the moment, in milliseconds since the Unix epoch.
|
|
34
|
+
*/
|
|
35
|
+
export const getStoryNow = () => {
|
|
36
|
+
const clock = getStoryClock();
|
|
37
|
+
// one subscriber per clock: a story that starts over has a new clock, and what read the old one stops listening
|
|
38
|
+
const subscribers = new WeakMap();
|
|
39
|
+
const subscriberOf = (current) => {
|
|
40
|
+
const kept = subscribers.get(current);
|
|
41
|
+
if (kept !== undefined)
|
|
42
|
+
return kept;
|
|
43
|
+
const subscriber = createSubscriber((update) => current.subscribe(update));
|
|
44
|
+
subscribers.set(current, subscriber);
|
|
45
|
+
return subscriber;
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
get current() {
|
|
49
|
+
const current = clock.current;
|
|
50
|
+
subscriberOf(current)();
|
|
51
|
+
return current.now();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame page of a Svelte catalogue: Forgebook's frame, drawing its stories with Svelte. Called by the page the plugin
|
|
3
|
+
* 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 Svelte.
|
|
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 Svelte catalogue: Forgebook's frame, drawing its stories with Svelte. Called by the page the plugin
|
|
3
|
+
* generates from the catalogue's configuration, not by an app.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { mountFrame as mountForgebookFrame } from "forgebook/frame";
|
|
8
|
+
import { svelteRenderer } from "./svelteRenderer.js";
|
|
9
|
+
/**
|
|
10
|
+
* Shows the story the address names in the given element, drawn with Svelte.
|
|
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: svelteRenderer });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which args fill a snippet with a text: `#name`, the snippet prop `name` drawn as that text.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* A snippet is a prop like any other in Svelte 5, and a text passed to it would reach `{@render children()}` as a
|
|
6
|
+
* string, which throws: an arg has to say it is a snippet's text, so the renderer can make the snippet. A story that
|
|
7
|
+
* passes a snippet of its own passes it under the prop's name. Free of Svelte, since the shell's code writer reads it too
|
|
8
|
+
* and the shell loads no Svelte.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/** What an arg's name starts with when it fills a snippet with a text. */
|
|
13
|
+
export declare const SNIPPET_MARK = "#";
|
|
14
|
+
/**
|
|
15
|
+
* The snippet prop an arg fills with a text.
|
|
16
|
+
*
|
|
17
|
+
* @param name - the arg's name.
|
|
18
|
+
* @returns the snippet prop's name, or none for any other arg.
|
|
19
|
+
*/
|
|
20
|
+
export declare const snippetOfArg: (name: string) => string | undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which args fill a snippet with a text: `#name`, the snippet prop `name` drawn as that text.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* A snippet is a prop like any other in Svelte 5, and a text passed to it would reach `{@render children()}` as a
|
|
6
|
+
* string, which throws: an arg has to say it is a snippet's text, so the renderer can make the snippet. A story that
|
|
7
|
+
* passes a snippet of its own passes it under the prop's name. Free of Svelte, since the shell's code writer reads it too
|
|
8
|
+
* and the shell loads no Svelte.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/** What an arg's name starts with when it fills a snippet with a text. */
|
|
13
|
+
export const SNIPPET_MARK = "#";
|
|
14
|
+
/**
|
|
15
|
+
* The snippet prop an arg fills with a text.
|
|
16
|
+
*
|
|
17
|
+
* @param name - the arg's name.
|
|
18
|
+
* @returns the snippet prop's name, or none for any other arg.
|
|
19
|
+
*/
|
|
20
|
+
export const snippetOfArg = (name) => name.startsWith(SNIPPET_MARK) && name.length > SNIPPET_MARK.length
|
|
21
|
+
? name.slice(SNIPPET_MARK.length)
|
|
22
|
+
: undefined;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The story types for Svelte: what a story file of a Svelte library is written with
|
|
3
|
+
* (`import type { Meta, Story } from "@forgebook/svelte"`).
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The format's types (`@forgebook/story-format/csf`) are generic over a framework; these name them for Svelte 5, so a
|
|
7
|
+
* meta writes `satisfies Meta<typeof Button>` and its stories' args are the component's props (its callback props
|
|
8
|
+
* among them), a prop that takes a text also as `storyText("key")`, and a snippet's text as `#name`. Types only: a
|
|
9
|
+
* library that writes stories depends on this package, never on the catalogue that shows them. A `.svelte` file has its
|
|
10
|
+
* real types under `svelte-check`; plain `tsc` sees the project's `*.svelte` shim.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import type { Decorator as CsfDecorator, CsfMeta, CsfPreview, CsfStory, StoryParameters, WrittenArg } from "@forgebook/story-format/csf";
|
|
15
|
+
import type { Component, Snippet } from "svelte";
|
|
16
|
+
/**
|
|
17
|
+
* Any Svelte 5 component: what a `.svelte` file exports, whatever its props. Its props are `never` because a component
|
|
18
|
+
* that takes any props at all can stand for it.
|
|
19
|
+
*/
|
|
20
|
+
export type AnyComponent = Component<never, Record<string, unknown>, string>;
|
|
21
|
+
/** A component drawn with these props: what a render returns to draw a component with props of its choosing. */
|
|
22
|
+
export type StoryComponent = {
|
|
23
|
+
/** The component. */
|
|
24
|
+
readonly Component: AnyComponent;
|
|
25
|
+
/** Its props; a snippet's text as `#name`, as in the args. None draws it with none. */
|
|
26
|
+
readonly props?: Readonly<Record<string, unknown>>;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* What a story draws: a component, drawn with the args, or a component with props of its choosing
|
|
30
|
+
* (`{ Component: Button, props: { ...args, tone: "danger" } }`), as Storybook's Svelte stories write it. What a
|
|
31
|
+
* decorator returns is drawn around the story, which reaches its component as `children`.
|
|
32
|
+
*/
|
|
33
|
+
export type SvelteStoryResult = AnyComponent | StoryComponent;
|
|
34
|
+
/** Svelte, as the format's types read it. */
|
|
35
|
+
export type SvelteRenderer = {
|
|
36
|
+
/** A component. */
|
|
37
|
+
readonly component: AnyComponent;
|
|
38
|
+
/** What a story draws. */
|
|
39
|
+
readonly storyResult: SvelteStoryResult;
|
|
40
|
+
};
|
|
41
|
+
/** A component's props, as its function is called with them. */
|
|
42
|
+
type PropsOf<TComponent extends AnyComponent> = TComponent extends (internals: never, props: infer TProps) => unknown ? TProps : Readonly<Record<string, never>>;
|
|
43
|
+
/** Whether a prop takes a snippet: what it renders is what a snippet returns. */
|
|
44
|
+
type IsSnippet<TValue> = NonNullable<TValue> extends (...parameters: never) => infer TResult ? TResult extends ReturnType<Snippet> ? true : false : false;
|
|
45
|
+
/** The args that fill a component's snippets with a text, each as `#name`. */
|
|
46
|
+
type SnippetArgsOf<TComponent extends AnyComponent> = {
|
|
47
|
+
[TName in keyof PropsOf<TComponent> as TName extends string ? IsSnippet<PropsOf<TComponent>[TName]> extends true ? `#${TName}` : never : never]?: WrittenArg<string>;
|
|
48
|
+
};
|
|
49
|
+
/** A component's args, every one optional: what a story's args may set, and what its render receives. */
|
|
50
|
+
export type ArgsOf<TComponent extends AnyComponent> = Partial<PropsOf<TComponent>> & SnippetArgsOf<TComponent>;
|
|
51
|
+
/**
|
|
52
|
+
* What a story's `args` may write: the component's props, every one optional, a prop that takes a text also as
|
|
53
|
+
* `storyText("key")`, and its snippets' texts. The render and the play function receive `ArgsOf`, the text already in
|
|
54
|
+
* the shown language.
|
|
55
|
+
*/
|
|
56
|
+
export type WrittenArgsOf<TComponent extends AnyComponent> = {
|
|
57
|
+
[TName in keyof PropsOf<TComponent>]?: WrittenArg<PropsOf<TComponent>[TName]>;
|
|
58
|
+
} & SnippetArgsOf<TComponent>;
|
|
59
|
+
/** A page with no component (a foundations page): no args. */
|
|
60
|
+
export type NoComponent = Component<Record<string, never>>;
|
|
61
|
+
/** A Svelte story file's default export: `{ title, component, args… } satisfies Meta<typeof Button>`. */
|
|
62
|
+
export type Meta<TComponent extends AnyComponent = NoComponent> = CsfMeta<SvelteRenderer, TComponent, ArgsOf<TComponent>, WrittenArgsOf<TComponent>>;
|
|
63
|
+
/** A Svelte story: a named export of a story file. */
|
|
64
|
+
export type Story<TComponent extends AnyComponent = NoComponent> = CsfStory<SvelteRenderer, ArgsOf<TComponent>, WrittenArgsOf<TComponent>>;
|
|
65
|
+
/** Any function a story about logic documents; its parameter is `never` so any signature is assignable. */
|
|
66
|
+
export type AnyFunction = (...parameters: never[]) => unknown;
|
|
67
|
+
/** No args: a story about logic draws a scene of its own, which takes none. */
|
|
68
|
+
type NoArgs = Readonly<Record<string, never>>;
|
|
69
|
+
/**
|
|
70
|
+
* The default export of a story file about logic, written `satisfies LogicMeta<typeof createToggle>`: `parameters` must
|
|
71
|
+
* say `logic: true`, so a function's file is never taken for a component's. Its component is the function (in a
|
|
72
|
+
* `.svelte.ts` module, with runes), whose signature the catalogue documents.
|
|
73
|
+
*/
|
|
74
|
+
export type LogicMeta<TFunction extends AnyFunction> = CsfMeta<SvelteRenderer, TFunction, NoArgs, NoArgs> & {
|
|
75
|
+
/** The catalogue's settings, `logic: true` among them. */
|
|
76
|
+
readonly parameters: StoryParameters & {
|
|
77
|
+
readonly logic: true;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* A story about logic: a `render` that draws a scene using the function, required (without one, the catalogue would
|
|
82
|
+
* draw the function as a component), and a `play` that tries it.
|
|
83
|
+
*/
|
|
84
|
+
export type LogicStory = CsfStory<SvelteRenderer, NoArgs, NoArgs> & Required<Pick<CsfStory<SvelteRenderer, NoArgs, NoArgs>, "render">>;
|
|
85
|
+
/** Wraps a Svelte story: a provider, a container of a given width, drawn around it. */
|
|
86
|
+
export type Decorator<TArgs> = CsfDecorator<SvelteRenderer, TArgs>;
|
|
87
|
+
/**
|
|
88
|
+
* A Svelte catalogue's preview (`forgebook.preview.ts`): decorators around every story (its providers, the language's
|
|
89
|
+
* reading `context.globals.lang`), parameters under every meta's, a setup before any story renders.
|
|
90
|
+
*/
|
|
91
|
+
export type Preview = CsfPreview<SvelteRenderer>;
|
|
92
|
+
export {};
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A value Svelte follows. */
|
|
2
|
+
export type StoryCell<TValue> = {
|
|
3
|
+
/** The value; a template, a derived value or an effect that reads it follows it. */
|
|
4
|
+
readonly current: TValue;
|
|
5
|
+
/**
|
|
6
|
+
* Replaces the value; what read it is drawn again, unless the new value is the same.
|
|
7
|
+
*
|
|
8
|
+
* @param value - the new value.
|
|
9
|
+
*/
|
|
10
|
+
readonly set: (value: TValue) => void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A value Svelte follows.
|
|
14
|
+
*
|
|
15
|
+
* @param initial - its first value.
|
|
16
|
+
*/
|
|
17
|
+
export declare const storyCell: <TValue>(initial: TValue) => StoryCell<TValue>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value Svelte follows, held outside a component: what the renderer keeps of a drawn story (its args, its scene, its
|
|
3
|
+
* texts), so the host's template draws again what read a value that changed, and only that.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Svelte's `$state` exists only in files its compiler reads; `createSubscriber` is its way for plain code to be followed,
|
|
7
|
+
* so the renderer stays a TypeScript module, built and published as one.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { createSubscriber } from "svelte/reactivity";
|
|
12
|
+
/**
|
|
13
|
+
* A value Svelte follows.
|
|
14
|
+
*
|
|
15
|
+
* @param initial - its first value.
|
|
16
|
+
*/
|
|
17
|
+
export const storyCell = (initial) => {
|
|
18
|
+
let value = initial;
|
|
19
|
+
// set while something reads the value: no one is told when no one reads it
|
|
20
|
+
let notify = () => undefined;
|
|
21
|
+
const subscribe = createSubscriber((update) => {
|
|
22
|
+
notify = update;
|
|
23
|
+
return () => {
|
|
24
|
+
notify = () => undefined;
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
get current() {
|
|
29
|
+
subscribe();
|
|
30
|
+
return value;
|
|
31
|
+
},
|
|
32
|
+
set: (next) => {
|
|
33
|
+
if (Object.is(next, value))
|
|
34
|
+
return;
|
|
35
|
+
value = next;
|
|
36
|
+
notify();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markup of a story drawn from its args: the component with the values the story and the props panel set, as a
|
|
3
|
+
* developer would write it in a Svelte file, for the Code card.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Attributes are written as Svelte writes them: a text in quotes, or as an expression when it holds a brace or a quote
|
|
7
|
+
* (`label={"a {b}"}`); a number, `null`, `false` or a list as an expression (`count={3}`, `options={["a", "b"]}`); `true`
|
|
8
|
+
* the bare name; a named value by its name (`icon={IconFolder}`). A snippet's text goes between the tags: `#children` as
|
|
9
|
+
* the content, any other in a `{#snippet name()}` block. Only the values that cross from the frame are known (texts,
|
|
10
|
+
* numbers, booleans, `null`, lists of texts and numbers, named values): a function the story passes is left out. Pure.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { type PanelArgs } from "forgebook/messages";
|
|
15
|
+
import type { StoryCodeWriter } from "forgebook/renderer";
|
|
16
|
+
/**
|
|
17
|
+
* A component's markup with these args.
|
|
18
|
+
*
|
|
19
|
+
* @param component - the component's name (`IconTile`).
|
|
20
|
+
* @param args - the values, in the order they are written.
|
|
21
|
+
*/
|
|
22
|
+
export declare const storyMarkup: (component: string, args: PanelArgs) => string;
|
|
23
|
+
/** The code writer a Svelte catalogue's shell writes a story's code with: the name the generated pages import. */
|
|
24
|
+
export declare const codeOf: StoryCodeWriter;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markup of a story drawn from its args: the component with the values the story and the props panel set, as a
|
|
3
|
+
* developer would write it in a Svelte file, for the Code card.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Attributes are written as Svelte writes them: a text in quotes, or as an expression when it holds a brace or a quote
|
|
7
|
+
* (`label={"a {b}"}`); a number, `null`, `false` or a list as an expression (`count={3}`, `options={["a", "b"]}`); `true`
|
|
8
|
+
* the bare name; a named value by its name (`icon={IconFolder}`). A snippet's text goes between the tags: `#children` as
|
|
9
|
+
* the content, any other in a `{#snippet name()}` block. Only the values that cross from the frame are known (texts,
|
|
10
|
+
* numbers, booleans, `null`, lists of texts and numbers, named values): a function the story passes is left out. Pure.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { isNamedArg } from "forgebook/messages";
|
|
15
|
+
import { snippetOfArg } from "./snippetArgs.js";
|
|
16
|
+
/** The snippet a component's content fills. */
|
|
17
|
+
const CHILDREN = "children";
|
|
18
|
+
/** The longest line the attributes stay on one line for, in characters. */
|
|
19
|
+
const ONE_LINE = 80;
|
|
20
|
+
/** A text that reads the same between quotes: no brace, which opens an expression, and nothing to escape. */
|
|
21
|
+
const isPlainText = (text) => !/[{}"&\n]/.test(text);
|
|
22
|
+
/** A value as a JavaScript expression. */
|
|
23
|
+
const expression = (value) => JSON.stringify(value);
|
|
24
|
+
/** One attribute as Svelte writes it. */
|
|
25
|
+
const attribute = (name, value) => {
|
|
26
|
+
if (value === true)
|
|
27
|
+
return name;
|
|
28
|
+
if (typeof value === "string")
|
|
29
|
+
return isPlainText(value) ? `${name}="${value}"` : `${name}={${expression(value)}}`;
|
|
30
|
+
// a named value is written by its name, as the source's module exports it (`icon={IconFolder}`)
|
|
31
|
+
if (isNamedArg(value))
|
|
32
|
+
return `${name}={${value.named}}`;
|
|
33
|
+
// a multiple choice's values, as the source writes a list
|
|
34
|
+
if (Array.isArray(value))
|
|
35
|
+
return `${name}={[${value.map(expression).join(", ")}]}`;
|
|
36
|
+
return `${name}={${String(value)}}`;
|
|
37
|
+
};
|
|
38
|
+
/** A text as Svelte's markup holds it: a tag or an expression it writes stays text. */
|
|
39
|
+
const contentText = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/\{/g, "{").replace(/\}/g, "}");
|
|
40
|
+
/** Whether a value fills a snippet: a text or a number, which is all the renderer draws in one. */
|
|
41
|
+
const isShown = (value) => typeof value === "string" || typeof value === "number";
|
|
42
|
+
/** A snippet's content as markup. */
|
|
43
|
+
const snippetContent = (value) => contentText(String(value));
|
|
44
|
+
/**
|
|
45
|
+
* What goes between a component's tags: each named snippet in its block, then the content.
|
|
46
|
+
*
|
|
47
|
+
* @param snippets - the args that fill a snippet, by the snippet's name.
|
|
48
|
+
*/
|
|
49
|
+
const contentOf = (snippets) => {
|
|
50
|
+
const shown = snippets.flatMap(([name, value]) => isShown(value) ? [[name, value]] : []);
|
|
51
|
+
if (shown.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
const [only] = shown;
|
|
54
|
+
if (shown.length === 1 && only?.[0] === CHILDREN)
|
|
55
|
+
return snippetContent(only[1]);
|
|
56
|
+
const named = shown
|
|
57
|
+
.filter(([name]) => name !== CHILDREN)
|
|
58
|
+
.map(([name, value]) => ` {#snippet ${name}()}${snippetContent(value)}{/snippet}`);
|
|
59
|
+
const children = shown
|
|
60
|
+
.filter(([name]) => name === CHILDREN)
|
|
61
|
+
.map(([, value]) => ` ${snippetContent(value)}`);
|
|
62
|
+
return `\n${[...named, ...children].join("\n")}\n`;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* A component's markup with these args.
|
|
66
|
+
*
|
|
67
|
+
* @param component - the component's name (`IconTile`).
|
|
68
|
+
* @param args - the values, in the order they are written.
|
|
69
|
+
*/
|
|
70
|
+
export const storyMarkup = (component, args) => {
|
|
71
|
+
const entries = Object.entries(args);
|
|
72
|
+
const attributes = entries
|
|
73
|
+
.filter(([name]) => snippetOfArg(name) === undefined)
|
|
74
|
+
.map(([name, value]) => attribute(name, value));
|
|
75
|
+
const content = contentOf(entries.flatMap(([name, value]) => {
|
|
76
|
+
const snippet = snippetOfArg(name);
|
|
77
|
+
return snippet === undefined ? [] : [[snippet, value]];
|
|
78
|
+
}));
|
|
79
|
+
const oneLine = [component, ...attributes].join(" ");
|
|
80
|
+
const isOneLine = oneLine.length + 2 <= ONE_LINE;
|
|
81
|
+
const opening = isOneLine
|
|
82
|
+
? `<${oneLine}`
|
|
83
|
+
: `<${component}\n${attributes.map((text) => ` ${text}`).join("\n")}\n`;
|
|
84
|
+
if (content === undefined)
|
|
85
|
+
return `${opening}${isOneLine ? " " : ""}/>`;
|
|
86
|
+
return `${opening}>${content}</${component}>`;
|
|
87
|
+
};
|
|
88
|
+
/** The code writer a Svelte catalogue's shell writes a story's code with: the name the generated pages import. */
|
|
89
|
+
export const codeOf = storyMarkup;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A story as Svelte draws it: its render (or the meta's component) inside its decorators, the story's, the meta's and the
|
|
3
|
+
* catalogue's (its preview's), in that order outward, as a tree of components with their props. The story's canvas, each
|
|
4
|
+
* cell of its matrix and the server draw it the same way, through `StoryHost.svelte`.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Svelte has no function that draws a component from code, so the tree is data and the host's template draws it: each
|
|
8
|
+
* node's component with its props, the node inside it as its `children`. A render returns a component, drawn with the
|
|
9
|
+
* args, or `{ Component, props }`, as Storybook's Svelte stories do; a decorator returns the same, drawn around the story.
|
|
10
|
+
* A decorator's `story()` gives the story's node: one that returns it as it is adds nothing around it. A `#name` prop
|
|
11
|
+
* becomes the snippet of its text (`snippetArgs.ts`).
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { StoryContext } from "@forgebook/story-format/csf";
|
|
16
|
+
import type { LoadedArgs, LoadedDecorator, LoadedStory } from "forgebook/story-module";
|
|
17
|
+
import type { Component } from "svelte";
|
|
18
|
+
import type { TextSnippets } from "./textSnippet.ts";
|
|
19
|
+
/** A component as the host draws it: with whatever props the story gives. */
|
|
20
|
+
export type DrawnComponent = Component<Record<string, unknown>>;
|
|
21
|
+
/** One component of a story's tree, with its props and what it draws inside. */
|
|
22
|
+
export type StoryNode = {
|
|
23
|
+
/** The component. */
|
|
24
|
+
readonly component: DrawnComponent;
|
|
25
|
+
/** Its props, the snippets of `#name` texts made. */
|
|
26
|
+
readonly props: Readonly<Record<string, unknown>>;
|
|
27
|
+
/** What it draws as its `children`: the story inside a decorator; none for the story itself. */
|
|
28
|
+
readonly inner?: StoryNode;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The story with these args: its render (or the meta's component), inside its decorators.
|
|
32
|
+
*
|
|
33
|
+
* @param loaded - the story.
|
|
34
|
+
* @param context - the args and the canvas its render, play and decorators read.
|
|
35
|
+
* @param outer - the catalogue's decorators, outside the story's and the meta's.
|
|
36
|
+
* @param snippetOf - makes the snippet of a `#name` text.
|
|
37
|
+
* @throws when the story has nothing to draw, or a render or a decorator returns what cannot be drawn.
|
|
38
|
+
*/
|
|
39
|
+
export declare const storyNode: (loaded: LoadedStory, context: StoryContext<LoadedArgs>, outer: readonly LoadedDecorator[], snippetOf: TextSnippets) => StoryNode;
|