@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 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
- # Temporary Holding Version
1
+ # @cssints/stories
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
+ 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.
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }