@unpunnyfuns/swatchbook-integrations 0.55.0 → 0.56.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.
@@ -72,12 +72,7 @@ function renderTheme(project) {
72
72
  ].join("\n");
73
73
  }
74
74
  function collectPaths(project) {
75
- const all = /* @__PURE__ */ new Set();
76
- for (const theme of project.permutations) {
77
- const tokens = project.resolveAt(theme.input);
78
- for (const path of Object.keys(tokens)) all.add(path);
79
- }
80
- return [...all].toSorted();
75
+ return [...project.varianceByPath.keys()].toSorted();
81
76
  }
82
77
  /**
83
78
  * Build a nested object tree from a sorted path list. Leaves hold the
@@ -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';\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 that exports a nested accessor\n * mirroring the project's token tree — every leaf is a `var(...)`\n * reference carrying the project's `cssVarPrefix`. Lets stories import\n * `{ theme }` the same way their production components do, but backed\n * by swatchbook's runtime-switchable cascade rather than a concrete\n * theme object. Not a replacement for the consumer's production\n * CSS-in-JS emit step.\n *\n * Swatchbook's toolbar still does the flipping via compound `data-*`\n * attributes; the accessor's values don't change across tuples because\n * the cascade resolves the var.\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 *\n * The theme object is stable across tuples — consumers wire it into a\n * provider *once*; runtime switching happens entirely through CSS cascade\n * when swatchbook's toolbar toggles `data-<prefix>-<axis>` on `<html>`.\n * Consumers who need resolved-value permutations (MUI `createTheme`, Vuetify\n * factories) are not covered — that's a different emission story.\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\nfunction renderTheme(project: Project): string {\n const prefix = project.config.cssVarPrefix ?? '';\n const varPrefix = prefix ? `${prefix}-` : '';\n const paths = collectPaths(project);\n const tree = buildTree(paths, (path) => `var(--${varPrefix}${path.replaceAll('.', '-')})`);\n\n const groupNames = Object.keys(tree).toSorted();\n const groupExports = groupNames.map(\n (name) => `export const ${safeIdent(name)} = ${renderNode(tree[name]!, 1)};`,\n );\n const aggregate = `export const theme = { ${groupNames.map(safeIdent).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 const all = new Set<string>();\n for (const theme of project.permutations) {\n const tokens = project.resolveAt(theme.input as Record<string, string>);\n for (const path of Object.keys(tokens)) all.add(path);\n }\n return [...all].toSorted();\n}\n\ntype TreeNode = { [key: string]: TreeNode | string };\n\n/**\n * Build a nested object tree from a sorted path list. Leaves hold the\n * emitted value from `leafFor(path)`. Leaf/branch collisions (a shorter\n * path emits a leaf while a longer path wants to nest under the same\n * key) are resolved by keeping the leaf — realistic DTCG trees don't\n * hit this, but the explicit behaviour beats silent UB.\n */\nfunction buildTree(sortedPaths: readonly string[], leafFor: (path: string) => string): TreeNode {\n const root: TreeNode = {};\n for (const path of sortedPaths) {\n const segments = path.split('.');\n let node = root;\n for (let i = 0; i < segments.length - 1; i++) {\n const seg = segments[i]!;\n const existing = node[seg];\n if (typeof existing === 'string') break;\n if (existing === undefined) {\n const next: TreeNode = {};\n node[seg] = next;\n node = next;\n } else {\n node = existing;\n }\n }\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/**\n * Bare identifier (or canonical non-leading-zero integer literal) if\n * safe; quoted string otherwise. Leading-zero numerics like `\"050\"`\n * stay quoted because bare `050` is an octal under strict mode.\n */\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,SAAwB,mBACtB,UAAqC,EAAE,EAChB;AAEvB,QAAO;EACL,MAAM;EACN,eAAe;GACb,WAJc,QAAQ,aAAa;GAKnC,QAAQ;GACT;EACF;;AAGH,SAAS,YAAY,SAA0B;CAC7C,MAAM,SAAS,QAAQ,OAAO,gBAAgB;CAC9C,MAAM,YAAY,SAAS,GAAG,OAAO,KAAK;CAE1C,MAAM,OAAO,UADC,aAAa,QAAQ,GACJ,SAAS,SAAS,YAAY,KAAK,WAAW,KAAK,IAAI,CAAC,GAAG;CAE1F,MAAM,aAAa,OAAO,KAAK,KAAK,CAAC,UAAU;CAC/C,MAAM,eAAe,WAAW,KAC7B,SAAS,gBAAgB,UAAU,KAAK,CAAC,KAAK,WAAW,KAAK,OAAQ,EAAE,CAAC,GAC3E;CACD,MAAM,YAAY,0BAA0B,WAAW,IAAI,UAAU,CAAC,KAAK,KAAK,CAAC;AAEjF,QAAO;EACL;EACA;EACA;EACA,GAAG;EACH;EACA;EACA;EACD,CAAC,KAAK,KAAK;;AAGd,SAAS,aAAa,SAA4B;CAChD,MAAM,sBAAM,IAAI,KAAa;AAC7B,MAAK,MAAM,SAAS,QAAQ,cAAc;EACxC,MAAM,SAAS,QAAQ,UAAU,MAAM,MAAgC;AACvE,OAAK,MAAM,QAAQ,OAAO,KAAK,OAAO,CAAE,KAAI,IAAI,KAAK;;AAEvD,QAAO,CAAC,GAAG,IAAI,CAAC,UAAU;;;;;;;;;AAY5B,SAAS,UAAU,aAAgC,SAA6C;CAC9F,MAAM,OAAiB,EAAE;AACzB,MAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,WAAW,KAAK,MAAM,IAAI;EAChC,IAAI,OAAO;AACX,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,SAAS,GAAG,KAAK;GAC5C,MAAM,MAAM,SAAS;GACrB,MAAM,WAAW,KAAK;AACtB,OAAI,OAAO,aAAa,SAAU;AAClC,OAAI,aAAa,KAAA,GAAW;IAC1B,MAAM,OAAiB,EAAE;AACzB,SAAK,OAAO;AACZ,WAAO;SAEP,QAAO;;EAGX,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;;;;;;;AAQhD,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"}
1
+ {"version":3,"file":"css-in-js.mjs","names":[],"sources":["../src/css-in-js.ts"],"sourcesContent":["import type { Project, SwatchbookIntegration } from '@unpunnyfuns/swatchbook-core';\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 that exports a nested accessor\n * mirroring the project's token tree — every leaf is a `var(...)`\n * reference carrying the project's `cssVarPrefix`. Lets stories import\n * `{ theme }` the same way their production components do, but backed\n * by swatchbook's runtime-switchable cascade rather than a concrete\n * theme object. Not a replacement for the consumer's production\n * CSS-in-JS emit step.\n *\n * Swatchbook's toolbar still does the flipping via compound `data-*`\n * attributes; the accessor's values don't change across tuples because\n * the cascade resolves the var.\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 *\n * The theme object is stable across tuples — consumers wire it into a\n * provider *once*; runtime switching happens entirely through CSS cascade\n * when swatchbook's toolbar toggles `data-<prefix>-<axis>` on `<html>`.\n * Consumers who need resolved-value permutations (MUI `createTheme`, Vuetify\n * factories) are not covered — that's a different emission story.\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\nfunction renderTheme(project: Project): string {\n const prefix = project.config.cssVarPrefix ?? '';\n const varPrefix = prefix ? `${prefix}-` : '';\n const paths = collectPaths(project);\n const tree = buildTree(paths, (path) => `var(--${varPrefix}${path.replaceAll('.', '-')})`);\n\n const groupNames = Object.keys(tree).toSorted();\n const groupExports = groupNames.map(\n (name) => `export const ${safeIdent(name)} = ${renderNode(tree[name]!, 1)};`,\n );\n const aggregate = `export const theme = { ${groupNames.map(safeIdent).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 return [...project.varianceByPath.keys()].toSorted();\n}\n\ntype TreeNode = { [key: string]: TreeNode | string };\n\n/**\n * Build a nested object tree from a sorted path list. Leaves hold the\n * emitted value from `leafFor(path)`. Leaf/branch collisions (a shorter\n * path emits a leaf while a longer path wants to nest under the same\n * key) are resolved by keeping the leaf — realistic DTCG trees don't\n * hit this, but the explicit behaviour beats silent UB.\n */\nfunction buildTree(sortedPaths: readonly string[], leafFor: (path: string) => string): TreeNode {\n const root: TreeNode = {};\n for (const path of sortedPaths) {\n const segments = path.split('.');\n let node = root;\n for (let i = 0; i < segments.length - 1; i++) {\n const seg = segments[i]!;\n const existing = node[seg];\n if (typeof existing === 'string') break;\n if (existing === undefined) {\n const next: TreeNode = {};\n node[seg] = next;\n node = next;\n } else {\n node = existing;\n }\n }\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/**\n * Bare identifier (or canonical non-leading-zero integer literal) if\n * safe; quoted string otherwise. Leading-zero numerics like `\"050\"`\n * stay quoted because bare `050` is an octal under strict mode.\n */\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,SAAwB,mBACtB,UAAqC,EAAE,EAChB;AAEvB,QAAO;EACL,MAAM;EACN,eAAe;GACb,WAJc,QAAQ,aAAa;GAKnC,QAAQ;GACT;EACF;;AAGH,SAAS,YAAY,SAA0B;CAC7C,MAAM,SAAS,QAAQ,OAAO,gBAAgB;CAC9C,MAAM,YAAY,SAAS,GAAG,OAAO,KAAK;CAE1C,MAAM,OAAO,UADC,aAAa,QAAQ,GACJ,SAAS,SAAS,YAAY,KAAK,WAAW,KAAK,IAAI,CAAC,GAAG;CAE1F,MAAM,aAAa,OAAO,KAAK,KAAK,CAAC,UAAU;CAC/C,MAAM,eAAe,WAAW,KAC7B,SAAS,gBAAgB,UAAU,KAAK,CAAC,KAAK,WAAW,KAAK,OAAQ,EAAE,CAAC,GAC3E;CACD,MAAM,YAAY,0BAA0B,WAAW,IAAI,UAAU,CAAC,KAAK,KAAK,CAAC;AAEjF,QAAO;EACL;EACA;EACA;EACA,GAAG;EACH;EACA;EACA;EACD,CAAC,KAAK,KAAK;;AAGd,SAAS,aAAa,SAA4B;AAChD,QAAO,CAAC,GAAG,QAAQ,eAAe,MAAM,CAAC,CAAC,UAAU;;;;;;;;;AAYtD,SAAS,UAAU,aAAgC,SAA6C;CAC9F,MAAM,OAAiB,EAAE;AACzB,MAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,WAAW,KAAK,MAAM,IAAI;EAChC,IAAI,OAAO;AACX,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,SAAS,GAAG,KAAK;GAC5C,MAAM,MAAM,SAAS;GACrB,MAAM,WAAW,KAAK;AACtB,OAAI,OAAO,aAAa,SAAU;AAClC,OAAI,aAAa,KAAA,GAAW;IAC1B,MAAM,OAAiB,EAAE;AACzB,SAAK,OAAO;AACZ,WAAO;SAEP,QAAO;;EAGX,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;;;;;;;AAQhD,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"}
package/dist/tailwind.mjs CHANGED
@@ -99,7 +99,7 @@ function deriveRoles(project) {
99
99
  shadow: [],
100
100
  font: []
101
101
  };
102
- for (const [path, token] of Object.entries(project.graph)) {
102
+ for (const [path, token] of Object.entries(project.defaultTokens)) {
103
103
  const classification = classify(path, token.$type);
104
104
  if (!classification) continue;
105
105
  const { scale, role } = classification;
@@ -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 sourcePrefix = prefix ? `${prefix}-` : '';\n const scopePrefix = 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 = `--${sourcePrefix}${sourcePath.replaceAll('.', '-')}`;\n const themeVar = `--${scale}-${scopePrefix}${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/**\n * Walk the project's default-theme token graph and classify each entry\n * into a Tailwind scale based on its `$type` + path. Returns a role map\n * shaped the same as a user-supplied `roles` option.\n *\n * Scale assignment:\n * - `$type: 'color'` → `color`, role = path minus `color.` prefix\n * - `$type: 'dimension'` → `spacing` (default), `radius` (if path starts with\n * `radius.`, `borderRadius.`, or `border-radius.`), or nothing (skipped)\n * for dimensions that look like font sizes (`font.size.*`, `text.*`), since\n * Tailwind's `--text-*` scale needs size + line-height pairs this preview\n * integration doesn't synthesize\n * - `$type: 'shadow'` → `shadow`, role = path minus `shadow.` prefix\n * - `$type: 'fontFamily'` → `font`, role = path minus `font.` / `font.family.` / `fontFamily.` prefix\n *\n * Other `$type`s are skipped — they don't have a natural single-value\n * Tailwind utility and would produce broken output.\n */\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.graph)) {\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;\nconst FONT_PREFIXES = ['font.family.', 'fontFamily.', 'font.'] as const;\n\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 (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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"}
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 sourcePrefix = prefix ? `${prefix}-` : '';\n const scopePrefix = 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 = `--${sourcePrefix}${sourcePath.replaceAll('.', '-')}`;\n const themeVar = `--${scale}-${scopePrefix}${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/**\n * Walk the project's default-theme token graph and classify each entry\n * into a Tailwind scale based on its `$type` + path. Returns a role map\n * shaped the same as a user-supplied `roles` option.\n *\n * Scale assignment:\n * - `$type: 'color'` → `color`, role = path minus `color.` prefix\n * - `$type: 'dimension'` → `spacing` (default), `radius` (if path starts with\n * `radius.`, `borderRadius.`, or `border-radius.`), or nothing (skipped)\n * for dimensions that look like font sizes (`font.size.*`, `text.*`), since\n * Tailwind's `--text-*` scale needs size + line-height pairs this preview\n * integration doesn't synthesize\n * - `$type: 'shadow'` → `shadow`, role = path minus `shadow.` prefix\n * - `$type: 'fontFamily'` → `font`, role = path minus `font.` / `font.family.` / `fontFamily.` prefix\n *\n * Other `$type`s are skipped — they don't have a natural single-value\n * Tailwind utility and would produce broken output.\n */\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;\nconst FONT_PREFIXES = ['font.family.', 'fontFamily.', 'font.'] as const;\n\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 (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.55.0",
3
+ "version": "0.56.0",
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.55.0"
47
+ "@unpunnyfuns/swatchbook-core": "0.56.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^25.6.0",