@cssints/stories 0.0.0-stage → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -2
- package/dist/index.d.mts +95 -0
- package/dist/index.mjs +76 -0
- package/package.json +30 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexei Accio
|
|
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,52 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cssints/stories
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The CSF 3 loader of a cssints storybook: the modules an app globs in, the entries with the ids Storybook builds out, and
|
|
4
|
+
the controls a story declares.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
// src/registry/button/button.stories.tsx — a Storybook CSF 3 file, unchanged
|
|
8
|
+
import type { Meta, Story } from "@cssints/stories";
|
|
9
|
+
import { Button, type ButtonProps } from "./button.tsx";
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Button",
|
|
13
|
+
component: Button,
|
|
14
|
+
args: { children: "Save", tone: "neutral" },
|
|
15
|
+
argTypes: {
|
|
16
|
+
tone: { type: "select", options: ["neutral", "accent", "quiet"] },
|
|
17
|
+
disabled: { type: "boolean" },
|
|
18
|
+
},
|
|
19
|
+
} satisfies Meta<ButtonProps>;
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
export const Default: Story<ButtonProps> = {};
|
|
23
|
+
export const Publish: Story<ButtonProps> = { args: { tone: "accent", children: "Publish" } };
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// the app
|
|
28
|
+
import { entries, parseHash, storyOfId, type Module } from "@cssints/stories";
|
|
29
|
+
|
|
30
|
+
const modules = import.meta.glob<Module>("../registry/*/*.stories.tsx", { eager: true });
|
|
31
|
+
export const stories = entries(modules); // [{ id: "button--default", item: "button", view, args, argTypes, … }]
|
|
32
|
+
|
|
33
|
+
parseHash("#button--publish"); // { name: "button", story: "button--publish" }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What it decides, and what it leaves to the app
|
|
37
|
+
|
|
38
|
+
- **The id.** `button--publish`, the item's name and the story's, kebab-cased — what Storybook builds, so a story file
|
|
39
|
+
moves either way. Within a file the order is the module namespace's, alphabetical, which is Storybook's default.
|
|
40
|
+
- **The args.** The meta's, the story's over them. A control in the host writes an arg and re-renders the view; the loader
|
|
41
|
+
does not care how.
|
|
42
|
+
- **The controls.** `argTypes` is the model (`boolean`, `select` with `options`, `text`, `number`, `range` with
|
|
43
|
+
`min`/`max`/`step`). Storybook infers a control from a component's props; a Solid component carries none at run time, so
|
|
44
|
+
a story declares what it wants by hand. The panel is the app's: a row per declared arg.
|
|
45
|
+
- **Nothing else.** No DOM, no styles, no routing: the chrome (a rail, a tablist, a native window) is the app's, and so is
|
|
46
|
+
the renderer — `entries<Node>()` returns views of whatever the app renders, so the same loader serves the web site, the
|
|
47
|
+
lab and cssints-native.
|
|
48
|
+
|
|
49
|
+
## Tests
|
|
50
|
+
|
|
51
|
+
`bun run test` — `test/stories.mjs` on the loader alone: ids, the arg merge, the exports it skips, the order, a component
|
|
52
|
+
view through `createComponent` and a render view, and the hash split.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** A control a story declares for one of its args: only what is declared gets a row in the panel. */
|
|
2
|
+
export type Control = {
|
|
3
|
+
readonly type: "boolean";
|
|
4
|
+
} | {
|
|
5
|
+
readonly type: "select";
|
|
6
|
+
readonly options: ReadonlyArray<string>;
|
|
7
|
+
} | {
|
|
8
|
+
readonly type: "text";
|
|
9
|
+
} | {
|
|
10
|
+
readonly type: "number";
|
|
11
|
+
readonly min?: number;
|
|
12
|
+
readonly max?: number;
|
|
13
|
+
readonly step?: number;
|
|
14
|
+
} | {
|
|
15
|
+
readonly type: "range";
|
|
16
|
+
readonly min: number;
|
|
17
|
+
readonly max: number;
|
|
18
|
+
readonly step?: number;
|
|
19
|
+
};
|
|
20
|
+
export type ArgTypes = Readonly<Record<string, Control>>;
|
|
21
|
+
/**
|
|
22
|
+
* The part of CSF 3 that is read: a default `meta` export (title, component, args, argTypes) and one object per named export
|
|
23
|
+
* for a story. Storybook 11 keeps CSF 3 support, so a story file moves into a real Storybook unchanged; the ids this module
|
|
24
|
+
* builds are the ones Storybook builds (`button--primary`).
|
|
25
|
+
*
|
|
26
|
+
* `argTypes` is the one extension: Storybook infers a control from a component's props, which a Solid component does not
|
|
27
|
+
* carry at run time, so a story declares the controls it wants by hand.
|
|
28
|
+
*/
|
|
29
|
+
export interface Meta<Props extends object = Record<string, unknown>, Node = unknown> {
|
|
30
|
+
readonly title: string;
|
|
31
|
+
readonly component?: (props: Props) => Node;
|
|
32
|
+
/** Arguments every story of the file starts from. */
|
|
33
|
+
readonly args?: Partial<Props>;
|
|
34
|
+
readonly argTypes?: ArgTypes;
|
|
35
|
+
}
|
|
36
|
+
export interface Story<Props extends object = Record<string, unknown>, Node = unknown> {
|
|
37
|
+
/** The name in the rail, instead of the export's own. */
|
|
38
|
+
readonly name?: string;
|
|
39
|
+
/** Arguments this story adds to the meta's. */
|
|
40
|
+
readonly args?: Partial<Props>;
|
|
41
|
+
/** Markup instead of the meta's component: a story that composes several widgets. */
|
|
42
|
+
readonly render?: (args: Props) => Node;
|
|
43
|
+
}
|
|
44
|
+
/** The values a story renders with: the meta's args, the story's, and whatever a control wrote since. */
|
|
45
|
+
export type Args = Record<string, unknown>;
|
|
46
|
+
/** One story: the id Storybook would give it, its args and controls, and the view that draws it. */
|
|
47
|
+
export interface Entry<Node = unknown> {
|
|
48
|
+
/** `button--primary`: the item's name and the story's, kebab-cased, as Storybook builds a story id. */
|
|
49
|
+
readonly id: string;
|
|
50
|
+
/** The item the file documents (`button`), from its path; in an app without a registry, the file's own name. */
|
|
51
|
+
readonly item: string;
|
|
52
|
+
readonly title: string;
|
|
53
|
+
readonly name: string;
|
|
54
|
+
/** The meta's args with the story's over them, before any control moves them. */
|
|
55
|
+
readonly args: Args;
|
|
56
|
+
/** The controls this story declares; a story with none gets no panel. */
|
|
57
|
+
readonly argTypes: ArgTypes;
|
|
58
|
+
/** Renders the story with the args it is given. */
|
|
59
|
+
readonly view: (args: Args) => Node;
|
|
60
|
+
}
|
|
61
|
+
/** A CSF module: the `import.meta.glob` result an app passes to `entries`. */
|
|
62
|
+
export type Module<Node = unknown> = {
|
|
63
|
+
readonly default: Meta<Record<string, unknown>, Node>;
|
|
64
|
+
} & Record<string, unknown>;
|
|
65
|
+
/**
|
|
66
|
+
* Every story of every CSF module given, sorted by item and then by id.
|
|
67
|
+
*
|
|
68
|
+
* Within a file an ES module namespace lists its exports alphabetically, which is the order Storybook gives a component's
|
|
69
|
+
* stories by default, and the sort keeps it.
|
|
70
|
+
*
|
|
71
|
+
* @param {Readonly<Record<string, Module<Node>>>} modules the CSF modules, keyed by their path (an `import.meta.glob` result)
|
|
72
|
+
* @param {(path: string) => string} itemOf the item a module documents, from its path; a story beside its component names
|
|
73
|
+
* the directory, which is the default
|
|
74
|
+
* @returns {ReadonlyArray<Entry<Node>>} the stories, sorted
|
|
75
|
+
*/
|
|
76
|
+
export declare const entries: <Node = unknown>(modules: Readonly<Record<string, Module<Node>>>, itemOf?: (path: string) => string) => ReadonlyArray<Entry<Node>>;
|
|
77
|
+
/**
|
|
78
|
+
* The story an id names.
|
|
79
|
+
*
|
|
80
|
+
* @param {ReadonlyArray<Entry<Node>>} stories the stories of the app
|
|
81
|
+
* @param {string} id the id, `button--primary`
|
|
82
|
+
* @returns {Entry<Node> | undefined} the story, or nothing when no file declares it
|
|
83
|
+
*/
|
|
84
|
+
export declare const storyOfId: <Node>(stories: ReadonlyArray<Entry<Node>>, id: string) => Entry<Node> | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* A hash split into the page it names and the story, if it names one: `#button--primary` is the button's primary story,
|
|
87
|
+
* `#button` its first, `#installation` a guide. The split is on the first `--`, which no item or guide name holds.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} hash the raw `location.hash`, with or without its `#`
|
|
90
|
+
* @returns {{ name: string, story: string | undefined }} the page and the full story id
|
|
91
|
+
*/
|
|
92
|
+
export declare const parseHash: (hash: string) => {
|
|
93
|
+
name: string;
|
|
94
|
+
story: string | undefined;
|
|
95
|
+
};
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createComponent } from "solid-js";
|
|
2
|
+
// src/index.ts
|
|
3
|
+
const kebab = (text) => text.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9]+/g, "-").toLowerCase();
|
|
4
|
+
const viewOf = (meta, story) => {
|
|
5
|
+
if (story.render) {
|
|
6
|
+
const render = story.render;
|
|
7
|
+
return (args) => render(args);
|
|
8
|
+
}
|
|
9
|
+
const component = meta.component;
|
|
10
|
+
if (!component) throw new Error(`stories: a story of "${meta.title}" has no render and its meta has no component`);
|
|
11
|
+
return (args) => createComponent(component, args);
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Every story of every CSF module given, sorted by item and then by id.
|
|
15
|
+
*
|
|
16
|
+
* Within a file an ES module namespace lists its exports alphabetically, which is the order Storybook gives a component's
|
|
17
|
+
* stories by default, and the sort keeps it.
|
|
18
|
+
*
|
|
19
|
+
* @param {Readonly<Record<string, Module<Node>>>} modules the CSF modules, keyed by their path (an `import.meta.glob` result)
|
|
20
|
+
* @param {(path: string) => string} itemOf the item a module documents, from its path; a story beside its component names
|
|
21
|
+
* the directory, which is the default
|
|
22
|
+
* @returns {ReadonlyArray<Entry<Node>>} the stories, sorted
|
|
23
|
+
*/
|
|
24
|
+
const entries = (modules, itemOf = (path) => path.split("/").at(-2) ?? "") => {
|
|
25
|
+
const found = [];
|
|
26
|
+
for (const [path, module] of Object.entries(modules)) {
|
|
27
|
+
const meta = module.default;
|
|
28
|
+
const item = itemOf(path);
|
|
29
|
+
for (const [exported, value] of Object.entries(module)) {
|
|
30
|
+
if (exported === "default") continue;
|
|
31
|
+
if (typeof value !== "object" || value === null) continue;
|
|
32
|
+
const story = value;
|
|
33
|
+
const name = story.name ?? exported;
|
|
34
|
+
found.push({
|
|
35
|
+
id: `${kebab(item)}--${kebab(name)}`,
|
|
36
|
+
item,
|
|
37
|
+
title: meta.title,
|
|
38
|
+
name,
|
|
39
|
+
args: {
|
|
40
|
+
...meta.args,
|
|
41
|
+
...story.args
|
|
42
|
+
},
|
|
43
|
+
argTypes: meta.argTypes ?? {},
|
|
44
|
+
view: viewOf(meta, story)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return found.sort((a, b) => a.item.localeCompare(b.item) || a.id.localeCompare(b.id));
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The story an id names.
|
|
52
|
+
*
|
|
53
|
+
* @param {ReadonlyArray<Entry<Node>>} stories the stories of the app
|
|
54
|
+
* @param {string} id the id, `button--primary`
|
|
55
|
+
* @returns {Entry<Node> | undefined} the story, or nothing when no file declares it
|
|
56
|
+
*/
|
|
57
|
+
const storyOfId = (stories, id) => stories.find((entry) => entry.id === id);
|
|
58
|
+
/**
|
|
59
|
+
* A hash split into the page it names and the story, if it names one: `#button--primary` is the button's primary story,
|
|
60
|
+
* `#button` its first, `#installation` a guide. The split is on the first `--`, which no item or guide name holds.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} hash the raw `location.hash`, with or without its `#`
|
|
63
|
+
* @returns {{ name: string, story: string | undefined }} the page and the full story id
|
|
64
|
+
*/
|
|
65
|
+
const parseHash = (hash) => {
|
|
66
|
+
const raw = hash.replace(/^#/, "");
|
|
67
|
+
const at = raw.indexOf("--");
|
|
68
|
+
return at === -1 ? {
|
|
69
|
+
name: raw,
|
|
70
|
+
story: void 0
|
|
71
|
+
} : {
|
|
72
|
+
name: raw.slice(0, at),
|
|
73
|
+
story: raw
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
export { entries, parseHash, storyOfId };
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cssints/stories",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.28.0",
|
|
4
|
+
"description": "The CSF 3 loader of a cssints storybook: the modules an app globs in, the entries with Storybook's ids out, and the controls a story declares",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/alexeiaccio/css-in-ts-compiler.git",
|
|
9
|
+
"directory": "packages/stories"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://cssints.dev",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"default": "./dist/index.mjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"solid-js": "^2.0.0-rc.13"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|