@unpunnyfuns/swatchbook-integrations 0.61.0 → 0.62.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/css-in-js.d.mts +6 -1
- package/dist/css-in-js.mjs +27 -9
- package/dist/css-in-js.mjs.map +1 -1
- package/dist/tailwind.mjs +9 -1
- package/dist/tailwind.mjs.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Preview-side adapters that let your Storybook stories use [Tailwind v4](./src/tailwind.ts) or a [CSS-in-JS library](./src/css-in-js.ts) against [swatchbook](https://github.com/unpunnyfuns/swatchbook)'s tokens.
|
|
4
4
|
|
|
5
|
-
Not a replacement for your production build. Integrations are preview-only
|
|
5
|
+
Not a replacement for your production build. Integrations are preview-only: they let `bg-sb-surface-default` or `theme.color.accent.bg` resolve correctly inside Storybook; for production artifacts, run [Terrazzo](https://terrazzo.app/)'s CLI against the same DTCG sources.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -19,6 +19,7 @@ npm install -D tailwindcss @tailwindcss/vite
|
|
|
19
19
|
## Usage
|
|
20
20
|
|
|
21
21
|
```ts title=".storybook/main.ts"
|
|
22
|
+
import { defineMain } from '@storybook/react-vite/node';
|
|
22
23
|
import tailwindIntegration from '@unpunnyfuns/swatchbook-integrations/tailwind';
|
|
23
24
|
import cssInJsIntegration from '@unpunnyfuns/swatchbook-integrations/css-in-js';
|
|
24
25
|
|
|
@@ -35,13 +36,13 @@ export default defineMain({
|
|
|
35
36
|
});
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
Per-integration wiring details live in the [Integrations guide](https://unpunnyfuns.github.io/swatchbook/guides/integrations/)
|
|
39
|
+
Per-integration wiring details live in the [Integrations guide](https://unpunnyfuns.github.io/swatchbook/guides/integrations/): Tailwind's Vite plugin setup, the CSS-in-JS `virtual:swatchbook/theme` import, role-map overrides, and how to write your own integration against the `SwatchbookIntegration` contract.
|
|
39
40
|
|
|
40
41
|
## Boundaries
|
|
41
42
|
|
|
42
43
|
- ✅ Preview-side use inside Storybook. Stories that style with utility classes or theme accessors pick up the swatchbook toolbar's axis flips via CSS cascade.
|
|
43
|
-
- ❌ No MUI / Vuetify / Bootstrap SCSS factories. Those need resolved values per named theme
|
|
44
|
+
- ❌ No MUI / Vuetify / Bootstrap SCSS factories. Those need resolved values per named theme; run Terrazzo's CLI with `@terrazzo/plugin-js` for that case.
|
|
44
45
|
|
|
45
46
|
## Documentation
|
|
46
47
|
|
|
47
|
-
[unpunnyfuns.github.io/swatchbook](https://unpunnyfuns.github.io/swatchbook/)
|
|
48
|
+
[unpunnyfuns.github.io/swatchbook](https://unpunnyfuns.github.io/swatchbook/): concepts, guides, and full API reference.
|
package/dist/css-in-js.d.mts
CHANGED
|
@@ -53,6 +53,11 @@ interface CssInJsIntegrationOptions {
|
|
|
53
53
|
* ```
|
|
54
54
|
*/
|
|
55
55
|
declare function cssInJsIntegration(options?: CssInJsIntegrationOptions): SwatchbookIntegration;
|
|
56
|
+
interface TreeNode {
|
|
57
|
+
[key: string]: TreeNode | string;
|
|
58
|
+
}
|
|
59
|
+
declare function buildTree(sortedPaths: readonly string[], leafFor: (path: string) => string): TreeNode;
|
|
60
|
+
declare function uniqueIdents(names: readonly string[]): Map<string, string>;
|
|
56
61
|
//#endregion
|
|
57
|
-
export { CssInJsIntegrationOptions, cssInJsIntegration as default };
|
|
62
|
+
export { CssInJsIntegrationOptions, TreeNode, buildTree, cssInJsIntegration as default, uniqueIdents };
|
|
58
63
|
//# sourceMappingURL=css-in-js.d.mts.map
|
package/dist/css-in-js.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cssVarName } from "@unpunnyfuns/swatchbook-core/css-var";
|
|
1
2
|
import { listPaths } from "@unpunnyfuns/swatchbook-core/graph";
|
|
2
3
|
//#region src/css-in-js.ts
|
|
3
4
|
/**
|
|
@@ -52,12 +53,11 @@ function cssInJsIntegration(options = {}) {
|
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
function renderTheme(project) {
|
|
55
|
-
const
|
|
56
|
-
const varPrefix = prefix ? `${prefix}-` : "";
|
|
57
|
-
const tree = buildTree(collectPaths(project), (path) => `var(--${varPrefix}${path.replaceAll(".", "-")})`);
|
|
56
|
+
const tree = buildTree(collectPaths(project), (path) => `var(${cssVarName(path, project)})`);
|
|
58
57
|
const groupNames = Object.keys(tree).toSorted();
|
|
59
|
-
const
|
|
60
|
-
const
|
|
58
|
+
const idents = uniqueIdents(groupNames);
|
|
59
|
+
const groupExports = groupNames.map((name) => `export const ${idents.get(name)} = ${renderNode(tree[name], 1)};`);
|
|
60
|
+
const aggregate = `export const theme = { ${groupNames.map((n) => idents.get(n)).join(", ")} };`;
|
|
61
61
|
return [
|
|
62
62
|
"/* Synthesized by @unpunnyfuns/swatchbook-integrations/css-in-js for preview.",
|
|
63
63
|
" * Served via `virtual:swatchbook/theme` — rebuilt on token changes. */",
|
|
@@ -72,20 +72,25 @@ function collectPaths(project) {
|
|
|
72
72
|
return [...listPaths(project.tokenGraph)];
|
|
73
73
|
}
|
|
74
74
|
function buildTree(sortedPaths, leafFor) {
|
|
75
|
-
const root =
|
|
75
|
+
const root = Object.create(null);
|
|
76
76
|
for (const path of sortedPaths) {
|
|
77
77
|
const segments = path.split(".");
|
|
78
78
|
let node = root;
|
|
79
|
+
let collided = false;
|
|
79
80
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
80
81
|
const seg = segments[i];
|
|
81
82
|
const existing = node[seg];
|
|
82
|
-
if (typeof existing === "string")
|
|
83
|
+
if (typeof existing === "string") {
|
|
84
|
+
collided = true;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
83
87
|
if (existing === void 0) {
|
|
84
|
-
const next =
|
|
88
|
+
const next = Object.create(null);
|
|
85
89
|
node[seg] = next;
|
|
86
90
|
node = next;
|
|
87
91
|
} else node = existing;
|
|
88
92
|
}
|
|
93
|
+
if (collided) continue;
|
|
89
94
|
const leafKey = segments.at(-1);
|
|
90
95
|
if (node[leafKey] === void 0) node[leafKey] = leafFor(path);
|
|
91
96
|
}
|
|
@@ -105,7 +110,20 @@ function safeKey(key) {
|
|
|
105
110
|
function safeIdent(key) {
|
|
106
111
|
return /^[A-Za-z_$][\w$]*$/.test(key) ? key : `_${key.replaceAll(/[^\w$]/g, "_")}`;
|
|
107
112
|
}
|
|
113
|
+
function uniqueIdents(names) {
|
|
114
|
+
const used = /* @__PURE__ */ new Set();
|
|
115
|
+
const out = /* @__PURE__ */ new Map();
|
|
116
|
+
for (const name of names) {
|
|
117
|
+
const base = safeIdent(name);
|
|
118
|
+
let ident = base;
|
|
119
|
+
let n = 2;
|
|
120
|
+
while (used.has(ident)) ident = `${base}_${n++}`;
|
|
121
|
+
used.add(ident);
|
|
122
|
+
out.set(name, ident);
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
108
126
|
//#endregion
|
|
109
|
-
export { cssInJsIntegration as default };
|
|
127
|
+
export { buildTree, cssInJsIntegration as default, uniqueIdents };
|
|
110
128
|
|
|
111
129
|
//# sourceMappingURL=css-in-js.mjs.map
|
package/dist/css-in-js.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"css-in-js.mjs","names":[],"sources":["../src/css-in-js.ts"],"sourcesContent":["import type { Project, SwatchbookIntegration } from '@unpunnyfuns/swatchbook-core';\nimport { listPaths } from '@unpunnyfuns/swatchbook-core/graph';\n\nexport interface CssInJsIntegrationOptions {\n /**\n * Virtual module ID the addon serves. Default:\n * `'virtual:swatchbook/theme'`. Consumers import this string from\n * their preview (or stories) to receive a typed accessor object whose\n * leaves are `var(--<cssVarPrefix>-*)` references.\n */\n virtualId?: string;\n}\n\n/**\n * Preview-only CSS-in-JS integration for swatchbook's Storybook addon.\n * Contributes a virtual JS module exporting a nested accessor that mirrors\n * the project's token tree, every leaf a `var(--<cssVarPrefix>-*)` reference.\n * Stories import `{ theme }` the way their production components do, but\n * backed by swatchbook's runtime-switchable cascade rather than a concrete\n * theme object.\n *\n * The accessor is stable across tuples: its values are `var(...)` refs, so\n * the toolbar's `data-*` axis flips repaint through the cascade without\n * rebuilding the object. Consumers wire it into a provider once.\n *\n * Not a transform step. Consumers needing resolved-value permutations\n * (MUI `createTheme`, Vuetify factories) are out of scope — that's the\n * production CSS-in-JS emit, not this preview shim.\n *\n * ```ts\n * // .storybook/main.ts\n * import cssInJsIntegration from '@unpunnyfuns/swatchbook-integrations/css-in-js';\n *\n * export default defineMain({\n * addons: [\n * {\n * name: '@unpunnyfuns/swatchbook-addon',\n * options: {\n * configPath: '../swatchbook.config.ts',\n * integrations: [cssInJsIntegration()],\n * },\n * },\n * ],\n * });\n *\n * // Any story / component\n * import { theme, color, space } from 'virtual:swatchbook/theme';\n *\n * // styled-components / emotion\n * <ThemeProvider theme={theme}>...</ThemeProvider>\n *\n * // direct ref\n * const bg = color.surface.default; // -> \"var(--sb-color-surface-default)\"\n * ```\n */\nexport default function cssInJsIntegration(\n options: CssInJsIntegrationOptions = {},\n): SwatchbookIntegration {\n const virtualId = options.virtualId ?? 'virtual:swatchbook/theme';\n return {\n name: 'css-in-js',\n virtualModule: {\n virtualId,\n render: renderTheme,\n },\n };\n}\n\n// Render the virtual module source: one accessor export per top-level group,\n// plus the aggregate `theme`.\nfunction renderTheme(project: Project): string {\n const
|
|
1
|
+
{"version":3,"file":"css-in-js.mjs","names":[],"sources":["../src/css-in-js.ts"],"sourcesContent":["import type { Project, SwatchbookIntegration } from '@unpunnyfuns/swatchbook-core';\nimport { cssVarName } from '@unpunnyfuns/swatchbook-core/css-var';\nimport { listPaths } from '@unpunnyfuns/swatchbook-core/graph';\n\nexport interface CssInJsIntegrationOptions {\n /**\n * Virtual module ID the addon serves. Default:\n * `'virtual:swatchbook/theme'`. Consumers import this string from\n * their preview (or stories) to receive a typed accessor object whose\n * leaves are `var(--<cssVarPrefix>-*)` references.\n */\n virtualId?: string;\n}\n\n/**\n * Preview-only CSS-in-JS integration for swatchbook's Storybook addon.\n * Contributes a virtual JS module exporting a nested accessor that mirrors\n * the project's token tree, every leaf a `var(--<cssVarPrefix>-*)` reference.\n * Stories import `{ theme }` the way their production components do, but\n * backed by swatchbook's runtime-switchable cascade rather than a concrete\n * theme object.\n *\n * The accessor is stable across tuples: its values are `var(...)` refs, so\n * the toolbar's `data-*` axis flips repaint through the cascade without\n * rebuilding the object. Consumers wire it into a provider once.\n *\n * Not a transform step. Consumers needing resolved-value permutations\n * (MUI `createTheme`, Vuetify factories) are out of scope — that's the\n * production CSS-in-JS emit, not this preview shim.\n *\n * ```ts\n * // .storybook/main.ts\n * import cssInJsIntegration from '@unpunnyfuns/swatchbook-integrations/css-in-js';\n *\n * export default defineMain({\n * addons: [\n * {\n * name: '@unpunnyfuns/swatchbook-addon',\n * options: {\n * configPath: '../swatchbook.config.ts',\n * integrations: [cssInJsIntegration()],\n * },\n * },\n * ],\n * });\n *\n * // Any story / component\n * import { theme, color, space } from 'virtual:swatchbook/theme';\n *\n * // styled-components / emotion\n * <ThemeProvider theme={theme}>...</ThemeProvider>\n *\n * // direct ref\n * const bg = color.surface.default; // -> \"var(--sb-color-surface-default)\"\n * ```\n */\nexport default function cssInJsIntegration(\n options: CssInJsIntegrationOptions = {},\n): SwatchbookIntegration {\n const virtualId = options.virtualId ?? 'virtual:swatchbook/theme';\n return {\n name: 'css-in-js',\n virtualModule: {\n virtualId,\n render: renderTheme,\n },\n };\n}\n\n// Render the virtual module source: one accessor export per top-level group,\n// plus the aggregate `theme`.\nfunction renderTheme(project: Project): string {\n const paths = collectPaths(project);\n const tree = buildTree(paths, (path) => `var(${cssVarName(path, project)})`);\n\n const groupNames = Object.keys(tree).toSorted();\n // Distinct group names can sanitize to the same identifier (e.g. `a-b` and\n // `a.b` both → `a_b`), which would emit duplicate top-level exports — a\n // SyntaxError in the virtual module. Suffix collisions to keep each unique.\n const idents = uniqueIdents(groupNames);\n const groupExports = groupNames.map(\n (name) => `export const ${idents.get(name)!} = ${renderNode(tree[name]!, 1)};`,\n );\n const aggregate = `export const theme = { ${groupNames.map((n) => idents.get(n)!).join(', ')} };`;\n\n return [\n '/* Synthesized by @unpunnyfuns/swatchbook-integrations/css-in-js for preview.',\n ' * Served via `virtual:swatchbook/theme` — rebuilt on token changes. */',\n '',\n ...groupExports,\n '',\n aggregate,\n '',\n ].join('\\n');\n}\n\nfunction collectPaths(project: Project): string[] {\n // Mutable copy; listPaths returns a readonly array.\n return [...listPaths(project.tokenGraph)];\n}\n\nexport interface TreeNode {\n [key: string]: TreeNode | string;\n}\n\n// Build a nested object tree from a sorted path list; leaves hold leafFor(path).\n// On a leaf/branch collision (a short path's leaf shares a key a longer path\n// wants to nest under) the leaf wins — real DTCG trees don't hit this, but\n// explicit beats silent UB.\nexport function buildTree(\n sortedPaths: readonly string[],\n leafFor: (path: string) => string,\n): TreeNode {\n // Null-prototype nodes: token path segments become object keys, so a\n // `__proto__` segment from untrusted token input would otherwise mutate\n // Object.prototype. With no prototype, such a key is just an own property.\n const root: TreeNode = Object.create(null);\n for (const path of sortedPaths) {\n const segments = path.split('.');\n let node = root;\n let collided = false;\n for (let i = 0; i < segments.length - 1; i++) {\n const seg = segments[i]!;\n const existing = node[seg];\n // A token already occupies this segment as a leaf, so the deeper path\n // can't nest under it (a key can't be both a string and an object).\n // Drop the deeper path rather than misfiling it under the truncated\n // key the loop happened to stop on.\n if (typeof existing === 'string') {\n collided = true;\n break;\n }\n if (existing === undefined) {\n const next: TreeNode = Object.create(null);\n node[seg] = next;\n node = next;\n } else {\n node = existing;\n }\n }\n if (collided) continue;\n const leafKey = segments.at(-1)!;\n if (node[leafKey] === undefined) node[leafKey] = leafFor(path);\n }\n return root;\n}\n\nfunction renderNode(node: TreeNode | string, depth: number): string {\n if (typeof node === 'string') return JSON.stringify(node);\n const indent = ' '.repeat(depth);\n const closing = ' '.repeat(depth - 1);\n const entries = Object.keys(node)\n .toSorted()\n .map((key) => `${indent}${safeKey(key)}: ${renderNode(node[key]!, depth + 1)}`);\n return `{\\n${entries.join(',\\n')},\\n${closing}}`;\n}\n\n// Bare identifier or canonical integer literal when safe, quoted otherwise.\n// Leading-zero numerics like \"050\" stay quoted — bare 050 is octal in strict mode.\nfunction safeKey(key: string): string {\n if (/^[A-Za-z_$][\\w$]*$/.test(key)) return key;\n if (/^(0|[1-9]\\d*)$/.test(key)) return key;\n return JSON.stringify(key);\n}\n\n// Top-level exports must be valid JS identifiers.\nfunction safeIdent(key: string): string {\n return /^[A-Za-z_$][\\w$]*$/.test(key) ? key : `_${key.replaceAll(/[^\\w$]/g, '_')}`;\n}\n\n// Map each name to a unique JS identifier, suffixing on collision so two\n// names that sanitize to the same ident don't produce duplicate exports.\nexport function uniqueIdents(names: readonly string[]): Map<string, string> {\n const used = new Set<string>();\n const out = new Map<string, string>();\n for (const name of names) {\n const base = safeIdent(name);\n let ident = base;\n let n = 2;\n while (used.has(ident)) ident = `${base}_${n++}`;\n used.add(ident);\n out.set(name, ident);\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,SAAwB,mBACtB,UAAqC,EAAE,EAChB;AAEvB,QAAO;EACL,MAAM;EACN,eAAe;GACb,WAJc,QAAQ,aAAa;GAKnC,QAAQ;GACT;EACF;;AAKH,SAAS,YAAY,SAA0B;CAE7C,MAAM,OAAO,UADC,aAAa,QAAQ,GACJ,SAAS,OAAO,WAAW,MAAM,QAAQ,CAAC,GAAG;CAE5E,MAAM,aAAa,OAAO,KAAK,KAAK,CAAC,UAAU;CAI/C,MAAM,SAAS,aAAa,WAAW;CACvC,MAAM,eAAe,WAAW,KAC7B,SAAS,gBAAgB,OAAO,IAAI,KAAK,CAAE,KAAK,WAAW,KAAK,OAAQ,EAAE,CAAC,GAC7E;CACD,MAAM,YAAY,0BAA0B,WAAW,KAAK,MAAM,OAAO,IAAI,EAAE,CAAE,CAAC,KAAK,KAAK,CAAC;AAE7F,QAAO;EACL;EACA;EACA;EACA,GAAG;EACH;EACA;EACA;EACD,CAAC,KAAK,KAAK;;AAGd,SAAS,aAAa,SAA4B;AAEhD,QAAO,CAAC,GAAG,UAAU,QAAQ,WAAW,CAAC;;AAW3C,SAAgB,UACd,aACA,SACU;CAIV,MAAM,OAAiB,OAAO,OAAO,KAAK;AAC1C,MAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,WAAW,KAAK,MAAM,IAAI;EAChC,IAAI,OAAO;EACX,IAAI,WAAW;AACf,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,SAAS,GAAG,KAAK;GAC5C,MAAM,MAAM,SAAS;GACrB,MAAM,WAAW,KAAK;AAKtB,OAAI,OAAO,aAAa,UAAU;AAChC,eAAW;AACX;;AAEF,OAAI,aAAa,KAAA,GAAW;IAC1B,MAAM,OAAiB,OAAO,OAAO,KAAK;AAC1C,SAAK,OAAO;AACZ,WAAO;SAEP,QAAO;;AAGX,MAAI,SAAU;EACd,MAAM,UAAU,SAAS,GAAG,GAAG;AAC/B,MAAI,KAAK,aAAa,KAAA,EAAW,MAAK,WAAW,QAAQ,KAAK;;AAEhE,QAAO;;AAGT,SAAS,WAAW,MAAyB,OAAuB;AAClE,KAAI,OAAO,SAAS,SAAU,QAAO,KAAK,UAAU,KAAK;CACzD,MAAM,SAAS,KAAK,OAAO,MAAM;CACjC,MAAM,UAAU,KAAK,OAAO,QAAQ,EAAE;AAItC,QAAO,MAHS,OAAO,KAAK,KAAK,CAC9B,UAAU,CACV,KAAK,QAAQ,GAAG,SAAS,QAAQ,IAAI,CAAC,IAAI,WAAW,KAAK,MAAO,QAAQ,EAAE,GAAG,CAC5D,KAAK,MAAM,CAAC,KAAK,QAAQ;;AAKhD,SAAS,QAAQ,KAAqB;AACpC,KAAI,qBAAqB,KAAK,IAAI,CAAE,QAAO;AAC3C,KAAI,iBAAiB,KAAK,IAAI,CAAE,QAAO;AACvC,QAAO,KAAK,UAAU,IAAI;;AAI5B,SAAS,UAAU,KAAqB;AACtC,QAAO,qBAAqB,KAAK,IAAI,GAAG,MAAM,IAAI,IAAI,WAAW,WAAW,IAAI;;AAKlF,SAAgB,aAAa,OAA+C;CAC1E,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,sBAAM,IAAI,KAAqB;AACrC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,UAAU,KAAK;EAC5B,IAAI,QAAQ;EACZ,IAAI,IAAI;AACR,SAAO,KAAK,IAAI,MAAM,CAAE,SAAQ,GAAG,KAAK,GAAG;AAC3C,OAAK,IAAI,MAAM;AACf,MAAI,IAAI,MAAM,MAAM;;AAEtB,QAAO"}
|
package/dist/tailwind.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cssVarName } from "@unpunnyfuns/swatchbook-core/css-var";
|
|
1
2
|
//#region src/tailwind.ts
|
|
2
3
|
/**
|
|
3
4
|
* Preview-only Tailwind v4 integration for the swatchbook Storybook
|
|
@@ -54,7 +55,7 @@ function renderTailwindTheme(project, roles) {
|
|
|
54
55
|
if (names.length === 0) continue;
|
|
55
56
|
entries.push(` /* ${scale} */`);
|
|
56
57
|
for (const [themeKey, sourcePath] of names) {
|
|
57
|
-
const sourceVar =
|
|
58
|
+
const sourceVar = cssVarName(sourcePath, project);
|
|
58
59
|
const themeVar = `--${scale}-${varPrefix}${themeKey}`;
|
|
59
60
|
entries.push(` ${themeVar}: var(${sourceVar});`);
|
|
60
61
|
}
|
|
@@ -101,6 +102,12 @@ const RADIUS_ROOTS = [
|
|
|
101
102
|
"borderRadius",
|
|
102
103
|
"border-radius"
|
|
103
104
|
];
|
|
105
|
+
const FONT_SIZE_ROOTS = [
|
|
106
|
+
"fontSize",
|
|
107
|
+
"font-size",
|
|
108
|
+
"textSize",
|
|
109
|
+
"text-size"
|
|
110
|
+
];
|
|
104
111
|
const FONT_PREFIXES = [
|
|
105
112
|
"font.family.",
|
|
106
113
|
"fontFamily.",
|
|
@@ -135,6 +142,7 @@ function classify(path, type) {
|
|
|
135
142
|
scale: "spacing",
|
|
136
143
|
role: pathToRole(path.slice(head.length + 1))
|
|
137
144
|
};
|
|
145
|
+
if (FONT_SIZE_ROOTS.includes(head)) return null;
|
|
138
146
|
if (head === "font" && /size|text/i.test(path)) return null;
|
|
139
147
|
if (head === "text") return null;
|
|
140
148
|
return {
|
package/dist/tailwind.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tailwind.mjs","names":[],"sources":["../src/tailwind.ts"],"sourcesContent":["import type { Project, SwatchbookIntegration } from '@unpunnyfuns/swatchbook-core';\n\nexport interface TailwindIntegrationOptions {\n /**\n * Virtual module ID the addon serves. Storybook users import this\n * string from their preview to receive the rendered `@theme` block.\n * Default: `'virtual:swatchbook/tailwind.css'`.\n */\n virtualId?: string;\n /**\n * Override the auto-derived role map. Keys are Tailwind scale names\n * (`color`, `spacing`, `radius`, `shadow`, `font`); values are ordered\n * `[tailwindEntryName, dtcgPath]` pairs. Supplied map **replaces** the\n * derived one — pass your own to pin exact entries, restrict the\n * universe of emitted utilities, or name scales the derivation doesn't\n * cover.\n *\n * Omit to let the integration derive a role map from the project at\n * render time: every `color` token lands under the `color` scale,\n * every `dimension` token under `spacing` / `radius` based on its\n * path prefix, every `shadow` under `shadow`, every `fontFamily` under\n * `font`. Works for any DTCG project without a configuration step.\n */\n roles?: Readonly<Record<string, readonly (readonly [string, string])[]>>;\n}\n\ntype RoleMap = Readonly<Record<string, readonly (readonly [string, string])[]>>;\n\n/**\n * Preview-only Tailwind v4 integration for the swatchbook Storybook\n * addon. Contributes a virtual CSS module whose `@theme` block aliases\n * Tailwind utility scales to the project's DTCG tokens via\n * `var(--<cssVarPrefix>-*)` references, so stories authored with\n * utility classes render correctly inside Storybook and react to the\n * toolbar's axis flips. This is not a replacement for the consumer's\n * production Tailwind build.\n *\n * Every entry is nested under the project's own `cssVarPrefix` so it\n * never collides with Tailwind's shipped scales: with\n * `cssVarPrefix: 'sb'` you get `--color-sb-surface-default`,\n * `--spacing-sb-md`, etc., generating utilities `bg-sb-surface-default`\n * and `p-sb-md` that coexist with `bg-red-500` / `p-4` / `max-w-md`.\n *\n * ```ts\n * // .storybook/main.ts\n * import tailwindIntegration from '@unpunnyfuns/swatchbook-integrations/tailwind';\n *\n * export default defineMain({\n * addons: [\n * {\n * name: '@unpunnyfuns/swatchbook-addon',\n * options: {\n * configPath: '../swatchbook.config.ts',\n * integrations: [tailwindIntegration()],\n * },\n * },\n * ],\n * });\n *\n * // .storybook/preview.tsx\n * import 'virtual:swatchbook/tailwind.css';\n * ```\n */\nexport default function tailwindIntegration(\n options: TailwindIntegrationOptions = {},\n): SwatchbookIntegration {\n const virtualId = options.virtualId ?? 'virtual:swatchbook/tailwind.css';\n const userRoles = options.roles;\n\n return {\n name: 'tailwind',\n virtualModule: {\n virtualId,\n render: (project) => renderTailwindTheme(project, userRoles ?? deriveRoles(project)),\n // Tailwind's `@theme` block is a global stylesheet — exactly the\n // kind of payload the addon should auto-inject into the preview,\n // so consumers don't hand-write a second `import` line after\n // plugging the integration in.\n autoInject: true,\n },\n };\n}\n\nfunction renderTailwindTheme(project: Project, roles: RoleMap): string {\n const prefix = project.config.cssVarPrefix ?? '';\n const varPrefix = prefix ? `${prefix}-` : '';\n\n const entries: string[] = [];\n for (const [scale, names] of Object.entries(roles)) {\n if (names.length === 0) continue;\n entries.push(` /* ${scale} */`);\n for (const [themeKey, sourcePath] of names) {\n const sourceVar =
|
|
1
|
+
{"version":3,"file":"tailwind.mjs","names":[],"sources":["../src/tailwind.ts"],"sourcesContent":["import type { Project, SwatchbookIntegration } from '@unpunnyfuns/swatchbook-core';\nimport { cssVarName } from '@unpunnyfuns/swatchbook-core/css-var';\n\nexport interface TailwindIntegrationOptions {\n /**\n * Virtual module ID the addon serves. Storybook users import this\n * string from their preview to receive the rendered `@theme` block.\n * Default: `'virtual:swatchbook/tailwind.css'`.\n */\n virtualId?: string;\n /**\n * Override the auto-derived role map. Keys are Tailwind scale names\n * (`color`, `spacing`, `radius`, `shadow`, `font`); values are ordered\n * `[tailwindEntryName, dtcgPath]` pairs. Supplied map **replaces** the\n * derived one — pass your own to pin exact entries, restrict the\n * universe of emitted utilities, or name scales the derivation doesn't\n * cover.\n *\n * Omit to let the integration derive a role map from the project at\n * render time: every `color` token lands under the `color` scale,\n * every `dimension` token under `spacing` / `radius` based on its\n * path prefix, every `shadow` under `shadow`, every `fontFamily` under\n * `font`. Works for any DTCG project without a configuration step.\n */\n roles?: Readonly<Record<string, readonly (readonly [string, string])[]>>;\n}\n\ntype RoleMap = Readonly<Record<string, readonly (readonly [string, string])[]>>;\n\n/**\n * Preview-only Tailwind v4 integration for the swatchbook Storybook\n * addon. Contributes a virtual CSS module whose `@theme` block aliases\n * Tailwind utility scales to the project's DTCG tokens via\n * `var(--<cssVarPrefix>-*)` references, so stories authored with\n * utility classes render correctly inside Storybook and react to the\n * toolbar's axis flips. This is not a replacement for the consumer's\n * production Tailwind build.\n *\n * Every entry is nested under the project's own `cssVarPrefix` so it\n * never collides with Tailwind's shipped scales: with\n * `cssVarPrefix: 'sb'` you get `--color-sb-surface-default`,\n * `--spacing-sb-md`, etc., generating utilities `bg-sb-surface-default`\n * and `p-sb-md` that coexist with `bg-red-500` / `p-4` / `max-w-md`.\n *\n * ```ts\n * // .storybook/main.ts\n * import tailwindIntegration from '@unpunnyfuns/swatchbook-integrations/tailwind';\n *\n * export default defineMain({\n * addons: [\n * {\n * name: '@unpunnyfuns/swatchbook-addon',\n * options: {\n * configPath: '../swatchbook.config.ts',\n * integrations: [tailwindIntegration()],\n * },\n * },\n * ],\n * });\n *\n * // .storybook/preview.tsx\n * import 'virtual:swatchbook/tailwind.css';\n * ```\n */\nexport default function tailwindIntegration(\n options: TailwindIntegrationOptions = {},\n): SwatchbookIntegration {\n const virtualId = options.virtualId ?? 'virtual:swatchbook/tailwind.css';\n const userRoles = options.roles;\n\n return {\n name: 'tailwind',\n virtualModule: {\n virtualId,\n render: (project) => renderTailwindTheme(project, userRoles ?? deriveRoles(project)),\n // Tailwind's `@theme` block is a global stylesheet — exactly the\n // kind of payload the addon should auto-inject into the preview,\n // so consumers don't hand-write a second `import` line after\n // plugging the integration in.\n autoInject: true,\n },\n };\n}\n\nfunction renderTailwindTheme(project: Project, roles: RoleMap): string {\n const prefix = project.config.cssVarPrefix ?? '';\n const varPrefix = prefix ? `${prefix}-` : '';\n\n const entries: string[] = [];\n for (const [scale, names] of Object.entries(roles)) {\n if (names.length === 0) continue;\n entries.push(` /* ${scale} */`);\n for (const [themeKey, sourcePath] of names) {\n const sourceVar = cssVarName(sourcePath, project);\n const themeVar = `--${scale}-${varPrefix}${themeKey}`;\n entries.push(` ${themeVar}: var(${sourceVar});`);\n }\n entries.push('');\n }\n while (entries.length > 0 && entries.at(-1) === '') entries.pop();\n\n return [\n '/* Synthesized by @unpunnyfuns/swatchbook-integrations/tailwind for preview.',\n ' * Served via `virtual:swatchbook/tailwind.css` — rebuilt on token changes. */',\n \"@import 'tailwindcss';\",\n '',\n '@theme {',\n ...entries,\n '}',\n '',\n ].join('\\n');\n}\n\n// Classify each default-theme token into a Tailwind scale, dropping empty\n// scales. Returns the same shape as the `roles` option; per-token rules and\n// skip reasons live in classify().\nfunction deriveRoles(project: Project): RoleMap {\n const scales: Record<string, [string, string][]> = {\n color: [],\n spacing: [],\n radius: [],\n shadow: [],\n font: [],\n };\n\n for (const [path, token] of Object.entries(project.defaultTokens)) {\n const classification = classify(path, token.$type);\n if (!classification) continue;\n const { scale, role } = classification;\n if (!role) continue;\n scales[scale]?.push([role, path]);\n }\n\n const out: Record<string, readonly (readonly [string, string])[]> = {};\n for (const [scale, entries] of Object.entries(scales)) {\n if (entries.length === 0) continue;\n entries.sort(([a], [b]) => a.localeCompare(b, 'en'));\n out[scale] = entries;\n }\n return out;\n}\n\nconst SPACING_ROOTS = ['space', 'spacing'] as const;\nconst RADIUS_ROOTS = ['radius', 'borderRadius', 'border-radius'] as const;\n// Single-segment font-size roots — camelCase/dashed forms the dotted\n// `font.size.*` check below misses, which otherwise bucket into spacing.\nconst FONT_SIZE_ROOTS = ['fontSize', 'font-size', 'textSize', 'text-size'] as const;\nconst FONT_PREFIXES = ['font.family.', 'fontFamily.', 'font.'] as const;\n\n// Map one token's $type + path to its Tailwind { scale, role }, or null to skip.\nfunction classify(path: string, type: string | undefined): { scale: string; role: string } | null {\n switch (type) {\n case 'color':\n return { scale: 'color', role: stripPrefix(path, 'color.') };\n case 'shadow':\n return { scale: 'shadow', role: stripPrefix(path, 'shadow.') };\n case 'fontFamily': {\n for (const prefix of FONT_PREFIXES) {\n if (path.startsWith(prefix)) {\n return { scale: 'font', role: pathToRole(path.slice(prefix.length)) };\n }\n }\n return { scale: 'font', role: pathToRole(path) };\n }\n case 'dimension': {\n const head = path.split('.', 1)[0] ?? '';\n if ((RADIUS_ROOTS as readonly string[]).includes(head)) {\n return { scale: 'radius', role: pathToRole(path.slice(head.length + 1)) };\n }\n if ((SPACING_ROOTS as readonly string[]).includes(head)) {\n return { scale: 'spacing', role: pathToRole(path.slice(head.length + 1)) };\n }\n // Font-size-ish dimensions are skipped — Tailwind's `--text-*` entries\n // expect a size+line-height pair that this integration doesn't build.\n if ((FONT_SIZE_ROOTS as readonly string[]).includes(head)) return null;\n if (head === 'font' && /size|text/i.test(path)) return null;\n if (head === 'text') return null;\n // Default-bucket any other dimension under spacing — safer than guessing.\n return { scale: 'spacing', role: pathToRole(path) };\n }\n default:\n return null;\n }\n}\n\nfunction stripPrefix(path: string, prefix: string): string {\n return pathToRole(path.startsWith(prefix) ? path.slice(prefix.length) : path);\n}\n\nfunction pathToRole(remainder: string): string {\n return remainder.replaceAll('.', '-');\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unpunnyfuns/swatchbook-integrations",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.2",
|
|
4
4
|
"description": "Display-side integrations for the swatchbook Storybook addon. Each subpath (./tailwind, …) exports a factory that plugs into the addon's options as a SwatchbookIntegration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "unpunnyfuns <unpunnyfuns@gmail.com>",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@unpunnyfuns/swatchbook-core": "0.
|
|
47
|
+
"@unpunnyfuns/swatchbook-core": "0.62.2"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^25.6.0",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"build": "tsdown src/tailwind.ts src/css-in-js.ts --format esm --dts --clean --sourcemap",
|
|
58
58
|
"typecheck": "tsc --noEmit",
|
|
59
59
|
"test": "vitest run",
|
|
60
|
+
"coverage": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=text --coverage.include='src/**' --coverage.all",
|
|
60
61
|
"test:watch": "vitest",
|
|
61
62
|
"lint": "oxlint --deny-warnings -c ../../.oxlintrc.json src test",
|
|
62
63
|
"format": "oxfmt -c ../../.oxfmtrc.json src test",
|