@unpunnyfuns/swatchbook-addon 0.60.8 → 0.61.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.
@@ -1,7 +1,6 @@
1
1
  //#region src/constants.ts
2
2
  const ADDON_ID = "swatchbook";
3
3
  const TOOL_ID = `${ADDON_ID}/theme-switcher`;
4
- const PARAM_KEY = "swatchbook";
5
4
  /** Canonical active-permutation tuple: `Record<axisName, contextName>`. Read by toolbar, panel, blocks. */
6
5
  const AXES_GLOBAL_KEY = "swatchbookAxes";
7
6
  /** Display-only color format for blocks (`hex` | `rgb` | `hsl` | `oklch` | `raw`). Emitted CSS is unaffected. */
@@ -20,7 +19,9 @@ const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
20
19
  const INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID = "virtual:swatchbook/integration-side-effects";
21
20
  const RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID = `\0${INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID}`;
22
21
  const STYLE_ELEMENT_ID = "swatchbook-tokens";
23
- /** Channel event: preview → manager, carries theme list + mode. */
22
+ /** Channel event: preview → manager, carries the init payload:
23
+ * axes / presets / disabledAxes / diagnostics / cssVarPrefix /
24
+ * defaultTuple. */
24
25
  const INIT_EVENT = "swatchbook/init";
25
26
  /** Channel event: manager → preview, asks preview to re-emit INIT_EVENT.
26
27
  * Covers the race where the manager subscribes after the preview's
@@ -39,10 +40,10 @@ const PREVIEW_MOUSEDOWN_EVENT = "swatchbook/preview-mousedown";
39
40
  const TOKENS_UPDATED_EVENT = "swatchbook/tokens-updated";
40
41
  /** Custom Vite HMR event: plugin → preview. Preview forwards it to the
41
42
  * Storybook channel as {@link TOKENS_UPDATED_EVENT} so blocks can
42
- * update their snapshot. Kept distinct from the channel event so the
43
- * plugin doesn't need a Storybook-channel dependency. */
44
- const HMR_EVENT = "swatchbook/tokens-updated";
43
+ * update their snapshot. A distinct wire string from the channel event
44
+ * so the plugin doesn't need a Storybook-channel dependency. */
45
+ const HMR_EVENT = "swatchbook/hmr-tokens";
45
46
  //#endregion
46
- export { INIT_EVENT as a, PARAM_KEY as c, RESOLVED_VIRTUAL_MODULE_ID as d, STYLE_ELEMENT_ID as f, VIRTUAL_MODULE_ID as h, HMR_EVENT as i, PREVIEW_MOUSEDOWN_EVENT as l, TOOL_ID as m, AXES_GLOBAL_KEY as n, INIT_REQUEST_EVENT as o, TOKENS_UPDATED_EVENT as p, COLOR_FORMAT_GLOBAL_KEY as r, INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID as s, ADDON_ID as t, RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID as u };
47
+ export { INIT_EVENT as a, PREVIEW_MOUSEDOWN_EVENT as c, STYLE_ELEMENT_ID as d, TOKENS_UPDATED_EVENT as f, HMR_EVENT as i, RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID as l, VIRTUAL_MODULE_ID as m, AXES_GLOBAL_KEY as n, INIT_REQUEST_EVENT as o, TOOL_ID as p, COLOR_FORMAT_GLOBAL_KEY as r, INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID as s, ADDON_ID as t, RESOLVED_VIRTUAL_MODULE_ID as u };
47
48
 
48
- //# sourceMappingURL=constants-B31xFInv.mjs.map
49
+ //# sourceMappingURL=constants-BuGxPJys.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants-BuGxPJys.mjs","names":[],"sources":["../src/constants.ts"],"sourcesContent":["export const ADDON_ID = 'swatchbook';\nexport const TOOL_ID = `${ADDON_ID}/theme-switcher`;\n/** Canonical active-permutation tuple: `Record<axisName, contextName>`. Read by toolbar, panel, blocks. */\nexport const AXES_GLOBAL_KEY = 'swatchbookAxes';\n/** Display-only color format for blocks (`hex` | `rgb` | `hsl` | `oklch` | `raw`). Emitted CSS is unaffected. */\nexport const COLOR_FORMAT_GLOBAL_KEY = 'swatchbookColorFormat';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:swatchbook/tokens';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\n/**\n * Aggregate virtual module the addon's preview always imports. Its body\n * is a sequence of side-effect imports — one per integration that\n * declared `virtualModule.autoInject: true`. Integrations contributing\n * global stylesheets (Tailwind's `@theme` block, a rules-heavy CSS\n * file) can opt into this path so consumers never hand-write an\n * `import 'virtual:swatchbook/…'` line themselves; the body is empty\n * when no integration opts in.\n */\nexport const INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID = 'virtual:swatchbook/integration-side-effects';\nexport const RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID = `\\0${INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID}`;\n\nexport const STYLE_ELEMENT_ID = 'swatchbook-tokens';\n\n/** Channel event: preview → manager, carries the init payload:\n * axes / presets / disabledAxes / diagnostics / cssVarPrefix /\n * defaultTuple. */\nexport const INIT_EVENT = 'swatchbook/init';\n/** Channel event: manager → preview, asks preview to re-emit INIT_EVENT.\n * Covers the race where the manager subscribes after the preview's\n * initial broadcast — without it the toolbar stays in \"loading…\" until\n * the user triggers anything that re-fires INIT_EVENT. */\nexport const INIT_REQUEST_EVENT = 'swatchbook/init-request';\n/** Channel event: preview → manager, fires once per `mousedown` on the\n * preview document. The toolbar popover listens for it so clicks landing\n * inside the preview iframe close the popover — a plain document-level\n * listener on the manager can't see iframe events. */\nexport const PREVIEW_MOUSEDOWN_EVENT = 'swatchbook/preview-mousedown';\n\n/** Channel event: preview → blocks, carries the fresh virtual-module\n * payload after a dev-time token refresh so blocks can re-render in\n * place without a full iframe reload. Fired by the preview in response\n * to the `HMR_EVENT` below. */\nexport const TOKENS_UPDATED_EVENT = 'swatchbook/tokens-updated';\n\n/** Custom Vite HMR event: plugin → preview. Preview forwards it to the\n * Storybook channel as {@link TOKENS_UPDATED_EVENT} so blocks can\n * update their snapshot. A distinct wire string from the channel event\n * so the plugin doesn't need a Storybook-channel dependency. */\nexport const HMR_EVENT = 'swatchbook/hmr-tokens';\n"],"mappings":";AAAA,MAAa,WAAW;AACxB,MAAa,UAAU,GAAG,SAAS;;AAEnC,MAAa,kBAAkB;;AAE/B,MAAa,0BAA0B;AAEvC,MAAa,oBAAoB;AACjC,MAAa,6BAA6B,KAAK;;;;;;;;;;AAW/C,MAAa,sCAAsC;AACnD,MAAa,+CAA+C,KAAK;AAEjE,MAAa,mBAAmB;;;;AAKhC,MAAa,aAAa;;;;;AAK1B,MAAa,qBAAqB;;;;;AAKlC,MAAa,0BAA0B;;;;;AAMvC,MAAa,uBAAuB;;;;;AAMpC,MAAa,YAAY"}
@@ -32,8 +32,9 @@ interface TokenInfo {
32
32
  * Storybook's preview-only hooks. Reads from the addon-provided
33
33
  * `SwatchbookContext` when present (preferred — uses the lifted
34
34
  * `resolveAt` accessor and the live active tuple); falls back to the
35
- * virtual module's eager `permutationsResolved` lookup keyed by the
36
- * default permutation name when no provider is mounted.
35
+ * module-scope `fallbackResolveAt` (`resolveAllAt(virtualTokenGraph,
36
+ * tuple)`) over the virtual module's default tuple when no provider is
37
+ * mounted.
37
38
  */
38
39
  declare function useToken(path: TokenPath): TokenInfo;
39
40
  //#endregion
@@ -3,13 +3,6 @@ import { cssVarPrefix, defaultTuple, tokenGraph } from "virtual:swatchbook/token
3
3
  import { useActiveAxes, useOptionalSwatchbookData } from "@unpunnyfuns/swatchbook-blocks";
4
4
  import { makeCssVar } from "@unpunnyfuns/swatchbook-core/css-var";
5
5
  //#region src/hooks/use-token.ts
6
- /**
7
- * Module-scope `resolveAt` for the no-provider fallback path. Built
8
- * once from the stable virtual-module exports — mirrors what the
9
- * preview decorator does for its `previewResolveAt` but lives in this
10
- * file so the hook can be called outside the addon's preview wrapper
11
- * (autodocs / MDX renders).
12
- */
13
6
  const fallbackResolveAt = (tuple) => resolveAllAt(tokenGraph, tuple);
14
7
  /**
15
8
  * Read a DTCG token for the currently active theme. Re-reads on theme
@@ -24,8 +17,9 @@ const fallbackResolveAt = (tuple) => resolveAllAt(tokenGraph, tuple);
24
17
  * Storybook's preview-only hooks. Reads from the addon-provided
25
18
  * `SwatchbookContext` when present (preferred — uses the lifted
26
19
  * `resolveAt` accessor and the live active tuple); falls back to the
27
- * virtual module's eager `permutationsResolved` lookup keyed by the
28
- * default permutation name when no provider is mounted.
20
+ * module-scope `fallbackResolveAt` (`resolveAllAt(virtualTokenGraph,
21
+ * tuple)`) over the virtual module's default tuple when no provider is
22
+ * mounted.
29
23
  */
30
24
  function useToken(path) {
31
25
  const snapshot = useOptionalSwatchbookData();
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["virtualTokenGraph","virtualCssVarPrefix","virtualDefaultTuple"],"sources":["../../src/hooks/use-token.ts"],"sourcesContent":["import {\n cssVarPrefix as virtualCssVarPrefix,\n defaultTuple as virtualDefaultTuple,\n tokenGraph as virtualTokenGraph,\n} from 'virtual:swatchbook/tokens';\nimport { resolveAllAt } from '@unpunnyfuns/swatchbook-core/graph';\nimport { makeCssVar } from '@unpunnyfuns/swatchbook-core/css-var';\nimport { useActiveAxes, useOptionalSwatchbookData } from '@unpunnyfuns/swatchbook-blocks';\n\n/**\n * Module-scope `resolveAt` for the no-provider fallback path. Built\n * once from the stable virtual-module exports — mirrors what the\n * preview decorator does for its `previewResolveAt` but lives in this\n * file so the hook can be called outside the addon's preview wrapper\n * (autodocs / MDX renders).\n */\nconst fallbackResolveAt = (tuple: Record<string, string>) => resolveAllAt(virtualTokenGraph, tuple);\n\n/**\n * Consumers augment this interface (via the addon's generated\n * `.swatchbook/tokens.d.ts`) to narrow {@link useToken}'s first parameter\n * to their project's actual token paths. Without augmentation it's empty\n * and {@link TokenPath} falls back to `string`.\n */\nexport interface SwatchbookTokenMap {}\n\ntype KnownPath = keyof SwatchbookTokenMap;\n\n/** Union of known token paths, or `string` when the addon codegen hasn't run. */\nexport type TokenPath = [KnownPath] extends [never] ? string : KnownPath;\n\nexport interface TokenInfo {\n /** The resolved DTCG `$value`. Shape varies by `$type`. */\n value: unknown;\n /** `var(--prefix-token-path)` reference, ready to drop into any CSS value. */\n cssVar: string;\n /** DTCG `$type` of the token, if known. */\n type?: string;\n /** Optional DTCG `$description`. */\n description?: string;\n}\n\n/**\n * Read a DTCG token for the currently active theme. Re-reads on theme\n * switch via the addon's `SwatchbookContext`. Returns `{ value, cssVar,\n * type, description }`.\n *\n * Typed paths appear automatically once `.swatchbook/tokens.d.ts` is\n * generated (happens on first storybook start/build). Until then\n * `TokenPath` is `string`.\n *\n * Safe to call in autodocs / MDX renders — uses plain React context, not\n * Storybook's preview-only hooks. Reads from the addon-provided\n * `SwatchbookContext` when present (preferred — uses the lifted\n * `resolveAt` accessor and the live active tuple); falls back to the\n * virtual module's eager `permutationsResolved` lookup keyed by the\n * default permutation name when no provider is mounted.\n */\nexport function useToken(path: TokenPath): TokenInfo {\n const snapshot = useOptionalSwatchbookData();\n const contextAxes = useActiveAxes();\n const hasContextAxes = Object.keys(contextAxes).length > 0;\n\n const prefix = snapshot?.cssVarPrefix ?? virtualCssVarPrefix;\n const resolver = snapshot?.resolveAt ?? fallbackResolveAt;\n const tuple = hasContextAxes\n ? (contextAxes as Record<string, string>)\n : (snapshot?.defaultTuple ?? virtualDefaultTuple);\n const token = resolver(tuple)[path] as\n | { $value?: unknown; $type?: string; $description?: string }\n | undefined;\n\n const info: TokenInfo = {\n value: token?.$value,\n cssVar: makeCssVar(path, prefix),\n };\n if (token?.$type) info.type = token.$type;\n if (token?.$description) info.description = token.$description;\n return info;\n}\n"],"mappings":";;;;;;;;;;;;AAgBA,MAAM,qBAAqB,UAAkC,aAAaA,YAAmB,MAAM;;;;;;;;;;;;;;;;;AA0CnG,SAAgB,SAAS,MAA4B;CACnD,MAAM,WAAW,2BAA2B;CAC5C,MAAM,cAAc,eAAe;CACnC,MAAM,iBAAiB,OAAO,KAAK,YAAY,CAAC,SAAS;CAEzD,MAAM,SAAS,UAAU,gBAAgBC;CAKzC,MAAM,SAJW,UAAU,aAAa,mBAC1B,iBACT,cACA,UAAU,gBAAgBC,aACF,CAAC;CAI9B,MAAM,OAAkB;EACtB,OAAO,OAAO;EACd,QAAQ,WAAW,MAAM,OAAO;EACjC;AACD,KAAI,OAAO,MAAO,MAAK,OAAO,MAAM;AACpC,KAAI,OAAO,aAAc,MAAK,cAAc,MAAM;AAClD,QAAO"}
1
+ {"version":3,"file":"index.mjs","names":["virtualTokenGraph","virtualCssVarPrefix","virtualDefaultTuple"],"sources":["../../src/hooks/use-token.ts"],"sourcesContent":["import {\n cssVarPrefix as virtualCssVarPrefix,\n defaultTuple as virtualDefaultTuple,\n tokenGraph as virtualTokenGraph,\n} from 'virtual:swatchbook/tokens';\nimport { resolveAllAt } from '@unpunnyfuns/swatchbook-core/graph';\nimport { makeCssVar } from '@unpunnyfuns/swatchbook-core/css-var';\nimport { useActiveAxes, useOptionalSwatchbookData } from '@unpunnyfuns/swatchbook-blocks';\n\n// Module-scope `resolveAt` for the no-provider fallback path. Built\n// once from the stable virtual-module exports — mirrors what the\n// preview decorator does for its `previewResolveAt` but lives in this\n// file so the hook can be called outside the addon's preview wrapper\n// (autodocs / MDX renders).\nconst fallbackResolveAt = (tuple: Record<string, string>) => resolveAllAt(virtualTokenGraph, tuple);\n\n/**\n * Consumers augment this interface (via the addon's generated\n * `.swatchbook/tokens.d.ts`) to narrow {@link useToken}'s first parameter\n * to their project's actual token paths. Without augmentation it's empty\n * and {@link TokenPath} falls back to `string`.\n */\nexport interface SwatchbookTokenMap {}\n\ntype KnownPath = keyof SwatchbookTokenMap;\n\n/** Union of known token paths, or `string` when the addon codegen hasn't run. */\nexport type TokenPath = [KnownPath] extends [never] ? string : KnownPath;\n\nexport interface TokenInfo {\n /** The resolved DTCG `$value`. Shape varies by `$type`. */\n value: unknown;\n /** `var(--prefix-token-path)` reference, ready to drop into any CSS value. */\n cssVar: string;\n /** DTCG `$type` of the token, if known. */\n type?: string;\n /** Optional DTCG `$description`. */\n description?: string;\n}\n\n/**\n * Read a DTCG token for the currently active theme. Re-reads on theme\n * switch via the addon's `SwatchbookContext`. Returns `{ value, cssVar,\n * type, description }`.\n *\n * Typed paths appear automatically once `.swatchbook/tokens.d.ts` is\n * generated (happens on first storybook start/build). Until then\n * `TokenPath` is `string`.\n *\n * Safe to call in autodocs / MDX renders — uses plain React context, not\n * Storybook's preview-only hooks. Reads from the addon-provided\n * `SwatchbookContext` when present (preferred — uses the lifted\n * `resolveAt` accessor and the live active tuple); falls back to the\n * module-scope `fallbackResolveAt` (`resolveAllAt(virtualTokenGraph,\n * tuple)`) over the virtual module's default tuple when no provider is\n * mounted.\n */\nexport function useToken(path: TokenPath): TokenInfo {\n const snapshot = useOptionalSwatchbookData();\n const contextAxes = useActiveAxes();\n const hasContextAxes = Object.keys(contextAxes).length > 0;\n\n const prefix = snapshot?.cssVarPrefix ?? virtualCssVarPrefix;\n const resolver = snapshot?.resolveAt ?? fallbackResolveAt;\n const tuple = hasContextAxes\n ? (contextAxes as Record<string, string>)\n : (snapshot?.defaultTuple ?? virtualDefaultTuple);\n const token = resolver(tuple)[path] as\n | { $value?: unknown; $type?: string; $description?: string }\n | undefined;\n\n const info: TokenInfo = {\n value: token?.$value,\n cssVar: makeCssVar(path, prefix),\n };\n if (token?.$type) info.type = token.$type;\n if (token?.$description) info.description = token.$description;\n return info;\n}\n"],"mappings":";;;;;AAcA,MAAM,qBAAqB,UAAkC,aAAaA,YAAmB,MAAM;;;;;;;;;;;;;;;;;;AA2CnG,SAAgB,SAAS,MAA4B;CACnD,MAAM,WAAW,2BAA2B;CAC5C,MAAM,cAAc,eAAe;CACnC,MAAM,iBAAiB,OAAO,KAAK,YAAY,CAAC,SAAS;CAEzD,MAAM,SAAS,UAAU,gBAAgBC;CAKzC,MAAM,SAJW,UAAU,aAAa,mBAC1B,iBACT,cACA,UAAU,gBAAgBC,aACF,CAAC;CAI9B,MAAM,OAAkB;EACtB,OAAO,OAAO;EACd,QAAQ,WAAW,MAAM,OAAO;EACjC;AACD,KAAI,OAAO,MAAO,MAAK,OAAO,MAAM;AACpC,KAAI,OAAO,aAAc,MAAK,cAAc,MAAM;AAClD,QAAO"}
package/dist/index.d.mts CHANGED
@@ -6,7 +6,6 @@ export * from "@unpunnyfuns/swatchbook-switcher";
6
6
  //#region src/constants.d.ts
7
7
  declare const ADDON_ID = "swatchbook";
8
8
  declare const TOOL_ID = "swatchbook/theme-switcher";
9
- declare const PARAM_KEY = "swatchbook";
10
9
  /** Canonical active-permutation tuple: `Record<axisName, contextName>`. Read by toolbar, panel, blocks. */
11
10
  declare const AXES_GLOBAL_KEY = "swatchbookAxes";
12
11
  /** Display-only color format for blocks (`hex` | `rgb` | `hsl` | `oklch` | `raw`). Emitted CSS is unaffected. */
@@ -21,5 +20,5 @@ declare const VIRTUAL_MODULE_ID = "virtual:swatchbook/tokens";
21
20
  */
22
21
  declare function swatchbookAddon(): ReturnType<typeof definePreviewAddon>;
23
22
  //#endregion
24
- export { ADDON_ID, AXES_GLOBAL_KEY, type AddonOptions, COLOR_FORMAT_GLOBAL_KEY, PARAM_KEY, TOOL_ID, VIRTUAL_MODULE_ID, swatchbookAddon as default };
23
+ export { ADDON_ID, AXES_GLOBAL_KEY, type AddonOptions, COLOR_FORMAT_GLOBAL_KEY, TOOL_ID, VIRTUAL_MODULE_ID, swatchbookAddon as default };
25
24
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { i as preview_exports } from "./preview-EthytSmK.mjs";
2
- import { c as PARAM_KEY, h as VIRTUAL_MODULE_ID, m as TOOL_ID, n as AXES_GLOBAL_KEY, r as COLOR_FORMAT_GLOBAL_KEY, t as ADDON_ID } from "./constants-B31xFInv.mjs";
1
+ import { i as preview_exports } from "./preview-B6Sy1z-D.mjs";
2
+ import { m as VIRTUAL_MODULE_ID, n as AXES_GLOBAL_KEY, p as TOOL_ID, r as COLOR_FORMAT_GLOBAL_KEY, t as ADDON_ID } from "./constants-BuGxPJys.mjs";
3
3
  import { definePreviewAddon } from "storybook/internal/csf";
4
4
  export * from "@unpunnyfuns/swatchbook-blocks";
5
5
  export * from "@unpunnyfuns/swatchbook-switcher";
@@ -13,6 +13,6 @@ function swatchbookAddon() {
13
13
  return definePreviewAddon(preview_exports);
14
14
  }
15
15
  //#endregion
16
- export { ADDON_ID, AXES_GLOBAL_KEY, COLOR_FORMAT_GLOBAL_KEY, PARAM_KEY, TOOL_ID, VIRTUAL_MODULE_ID, swatchbookAddon as default };
16
+ export { ADDON_ID, AXES_GLOBAL_KEY, COLOR_FORMAT_GLOBAL_KEY, TOOL_ID, VIRTUAL_MODULE_ID, swatchbookAddon as default };
17
17
 
18
18
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["previewExports"],"sources":["../src/index.ts"],"sourcesContent":["import { definePreviewAddon } from 'storybook/internal/csf';\nimport * as previewExports from '#/preview.tsx';\n\nexport type { AddonOptions } from '#/options.ts';\n\n/**\n * Public namespace constants — addon ID, parameter / global keys, the\n * canonical virtual module ID. Useful for consumer code that needs to\n * reach into the addon's namespace (custom toolbar registrations,\n * channel events, manager hooks that need a stable handle on these keys).\n */\nexport {\n ADDON_ID,\n AXES_GLOBAL_KEY,\n COLOR_FORMAT_GLOBAL_KEY,\n PARAM_KEY,\n TOOL_ID,\n VIRTUAL_MODULE_ID,\n} from '#/constants.ts';\n\n/**\n * Re-export the full user-facing surface from `@unpunnyfuns/swatchbook-blocks`\n * and `@unpunnyfuns/swatchbook-switcher` so consumers can\n * `import { TokenTable, ThemeSwitcher, useToken } from '@unpunnyfuns/swatchbook-addon'`\n * without adding the sibling packages to their own package.json. Both are\n * declared as regular dependencies of the addon, so they come along for the\n * ride. Subpath entries (`./preset`, `./manager`, `./preview`, `./hooks`)\n * still exist for Storybook's preset loader — this meta re-export is just\n * for the React / MDX consumer path.\n */\nexport * from '@unpunnyfuns/swatchbook-blocks';\nexport * from '@unpunnyfuns/swatchbook-switcher';\n\n/**\n * CSF Next factory. Consumers call this inside\n * `definePreview({ addons: [swatchbookAddon()] })` so the preview annotations\n * (decorator, globalTypes, initialGlobals) are added to the preview bundle.\n */\nexport default function swatchbookAddon(): ReturnType<typeof definePreviewAddon> {\n return definePreviewAddon(previewExports);\n}\n"],"mappings":";;;;;;;;;;;AAsCA,SAAwB,kBAAyD;AAC/E,QAAO,mBAAmBA,gBAAe"}
1
+ {"version":3,"file":"index.mjs","names":["previewExports"],"sources":["../src/index.ts"],"sourcesContent":["import { definePreviewAddon } from 'storybook/internal/csf';\nimport * as previewExports from '#/preview.tsx';\n\nexport type { AddonOptions } from '#/options.ts';\n\n/**\n * Public namespace constants — addon ID, parameter / global keys, the\n * canonical virtual module ID. Useful for consumer code that needs to\n * reach into the addon's namespace (custom toolbar registrations,\n * channel events, manager hooks that need a stable handle on these keys).\n */\nexport {\n ADDON_ID,\n AXES_GLOBAL_KEY,\n COLOR_FORMAT_GLOBAL_KEY,\n TOOL_ID,\n VIRTUAL_MODULE_ID,\n} from '#/constants.ts';\n\n/**\n * Re-export the full user-facing surface from `@unpunnyfuns/swatchbook-blocks`\n * and `@unpunnyfuns/swatchbook-switcher` so consumers can\n * `import { TokenTable, ThemeSwitcher, useToken } from '@unpunnyfuns/swatchbook-addon'`\n * without adding the sibling packages to their own package.json. Both are\n * declared as regular dependencies of the addon, so they come along for the\n * ride. Subpath entries (`./preset`, `./manager`, `./preview`, `./hooks`)\n * still exist for Storybook's preset loader — this meta re-export is just\n * for the React / MDX consumer path.\n */\nexport * from '@unpunnyfuns/swatchbook-blocks';\nexport * from '@unpunnyfuns/swatchbook-switcher';\n\n/**\n * CSF Next factory. Consumers call this inside\n * `definePreview({ addons: [swatchbookAddon()] })` so the preview annotations\n * (decorator, globalTypes, initialGlobals) are added to the preview bundle.\n */\nexport default function swatchbookAddon(): ReturnType<typeof definePreviewAddon> {\n return definePreviewAddon(previewExports);\n}\n"],"mappings":";;;;;;;;;;;AAqCA,SAAwB,kBAAyD;AAC/E,QAAO,mBAAmBA,gBAAe"}
package/dist/manager.mjs CHANGED
@@ -1,25 +1,10 @@
1
- import { a as INIT_EVENT, l as PREVIEW_MOUSEDOWN_EVENT, m as TOOL_ID, n as AXES_GLOBAL_KEY, o as INIT_REQUEST_EVENT, r as COLOR_FORMAT_GLOBAL_KEY, t as ADDON_ID } from "./constants-B31xFInv.mjs";
1
+ import { a as INIT_EVENT, c as PREVIEW_MOUSEDOWN_EVENT, n as AXES_GLOBAL_KEY, o as INIT_REQUEST_EVENT, p as TOOL_ID, r as COLOR_FORMAT_GLOBAL_KEY, t as ADDON_ID } from "./constants-BuGxPJys.mjs";
2
2
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { ThemeSwitcher, presetTuple } from "@unpunnyfuns/swatchbook-switcher";
4
4
  import { Button, ToggleButton, WithTooltip } from "storybook/internal/components";
5
5
  import { addons, types, useGlobals, useStorybookApi } from "storybook/manager-api";
6
6
  //#region src/ColorFormatSelector.tsx
7
7
  /**
8
- * Storybook-addon-specific pill row for picking how color sub-values
9
- * render in swatchbook blocks (hex / rgb / hsl / oklch / raw). Lives in
10
- * the addon rather than the shared switcher because color format is a
11
- * blocks-rendering concern, not a theming one — the docs-site navbar
12
- * switcher has no consumer for it.
13
- *
14
- * Reuses the `sb-switcher__*` class names so styling stays consistent
15
- * with the rest of the toolbar popover. The switcher's CSS is already
16
- * loaded on the page because this selector only renders inside
17
- * `<ThemeSwitcher>`'s `footer` prop.
18
- *
19
- * Uses `React.createElement` (via `h`) to survive embedding in Storybook's
20
- * manager bundle, which doesn't expose `react/jsx-runtime`.
21
- */
22
- /**
23
8
  * Runtime list of valid color formats. Declared here (alongside
24
9
  * `COLOR_FORMAT_OPTIONS`) rather than imported from blocks because the
25
10
  * manager bundle can't pull from `@unpunnyfuns/swatchbook-blocks` — that
@@ -79,26 +64,9 @@ function ColorFormatSelector({ active, onSelect }) {
79
64
  }
80
65
  //#endregion
81
66
  //#region src/manager.tsx
82
- /**
83
- * Use explicit `React.createElement` rather than JSX so the manager bundle
84
- * doesn't take a hard dependency on `react/jsx-runtime`. Storybook's manager
85
- * page injects its own React as a runtime global; `react/jsx-runtime` isn't
86
- * always part of that exposure, which breaks JSX with
87
- * "Cannot read properties of undefined (reading 'recentlyCreatedOwnerStacks')".
88
- * Mirrors the pattern `@storybook/addon-a11y` uses in its manager.
89
- *
90
- * The imported `<ThemeSwitcher>` from `@unpunnyfuns/swatchbook-switcher`
91
- * compiles with classic JSX (`React.createElement`) specifically so it
92
- * survives embedding in the manager bundle the same way.
93
- */
94
67
  const h = React.createElement;
95
68
  const EMPTY_AXES = [];
96
69
  const EMPTY_PRESETS = [];
97
- /**
98
- * Root toolbar glyph — a split-circle ("yinyang") mark: a faint filled
99
- * disc for the full-swatch silhouette, with a darker half-and-inset-disc
100
- * path reading as a pair of theme variants swapped in place.
101
- */
102
70
  function SwatchbookIcon() {
103
71
  return h("svg", {
104
72
  width: 14,
@@ -131,12 +99,6 @@ function AxesToolbar() {
131
99
  const channel = addons.getChannel();
132
100
  const onInit = (next) => setPayload(next);
133
101
  channel.on(INIT_EVENT, onInit);
134
- /**
135
- * Ask the preview to (re-)emit INIT_EVENT in case it already broadcast
136
- * before this effect subscribed. Without this request, a late-mounting
137
- * manager (story navigation, docs reload) can stay in "loading…" until
138
- * the user triggers a globals change.
139
- */
140
102
  channel.emit(INIT_REQUEST_EVENT);
141
103
  return () => {
142
104
  channel.off(INIT_EVENT, onInit);
@@ -206,11 +168,6 @@ function AxesToolbar() {
206
168
  setOpen(false);
207
169
  }
208
170
  }, []);
209
- /**
210
- * Escape closes even when focus hasn't entered the popover yet (e.g. the
211
- * user opened it via click and the mouse is still over the canvas). We
212
- * attach a document-level listener when open.
213
- */
214
171
  useEffect(() => {
215
172
  if (!open) return;
216
173
  const onDocKey = (e) => {
@@ -219,12 +176,6 @@ function AxesToolbar() {
219
176
  document.addEventListener("keydown", onDocKey);
220
177
  return () => document.removeEventListener("keydown", onDocKey);
221
178
  }, [open]);
222
- /**
223
- * `WithTooltip`'s built-in `closeOnOutsideClick` misses some cases
224
- * (portaled popover + manager iframe boundaries). Belt-and-suspenders:
225
- * close when the user mouses down anywhere that isn't the trigger wrapper
226
- * or the popover body.
227
- */
228
179
  useEffect(() => {
229
180
  if (!open) return;
230
181
  const onDocMouseDown = (e) => {
@@ -234,12 +185,6 @@ function AxesToolbar() {
234
185
  if (target.closest("[data-testid=\"swatchbook-switcher\"]")) return;
235
186
  setOpen(false);
236
187
  };
237
- /**
238
- * The manager's document-level listener above can't see mousedowns
239
- * inside the preview iframe. Preview emits PREVIEW_MOUSEDOWN_EVENT on
240
- * every mousedown over its own document; listen for it here so
241
- * clicking the canvas / docs page also closes the popover.
242
- */
243
188
  const channel = addons.getChannel();
244
189
  const onPreviewMouseDown = () => setOpen(false);
245
190
  document.addEventListener("mousedown", onDocMouseDown);
@@ -1 +1 @@
1
- {"version":3,"file":"manager.mjs","names":["h"],"sources":["../src/ColorFormatSelector.tsx","../src/manager.tsx"],"sourcesContent":["import type { ColorFormat } from '@unpunnyfuns/swatchbook-blocks';\nimport React from 'react';\nimport type { ReactElement } from 'react';\n\nexport type { ColorFormat };\n\n/**\n * Storybook-addon-specific pill row for picking how color sub-values\n * render in swatchbook blocks (hex / rgb / hsl / oklch / raw). Lives in\n * the addon rather than the shared switcher because color format is a\n * blocks-rendering concern, not a theming one — the docs-site navbar\n * switcher has no consumer for it.\n *\n * Reuses the `sb-switcher__*` class names so styling stays consistent\n * with the rest of the toolbar popover. The switcher's CSS is already\n * loaded on the page because this selector only renders inside\n * `<ThemeSwitcher>`'s `footer` prop.\n *\n * Uses `React.createElement` (via `h`) to survive embedding in Storybook's\n * manager bundle, which doesn't expose `react/jsx-runtime`.\n */\n\n/**\n * Runtime list of valid color formats. Declared here (alongside\n * `COLOR_FORMAT_OPTIONS`) rather than imported from blocks because the\n * manager bundle can't pull from `@unpunnyfuns/swatchbook-blocks` — that\n * package's barrel has a top-level `import 'virtual:swatchbook/tokens'`\n * which the manager has no resolver for. The `ColorFormat` type above\n * stays imported (type-only erases at compile time).\n */\nexport const COLOR_FORMATS: readonly ColorFormat[] = ['hex', 'rgb', 'hsl', 'oklch', 'raw'];\n\nconst COLOR_FORMAT_OPTIONS: readonly { id: ColorFormat; label: string }[] = [\n { id: 'hex', label: 'Hex' },\n { id: 'rgb', label: 'RGB' },\n { id: 'hsl', label: 'HSL' },\n { id: 'oklch', label: 'OKLCH' },\n { id: 'raw', label: 'Raw (JSON)' },\n];\n\nconst h = React.createElement;\n\nexport interface ColorFormatSelectorProps {\n active: ColorFormat;\n onSelect(next: ColorFormat): void;\n}\n\nexport function ColorFormatSelector({ active, onSelect }: ColorFormatSelectorProps): ReactElement {\n return h(\n 'div',\n null,\n h(\n 'div',\n { className: 'sb-switcher__section-label', id: 'sb-color-format-label' },\n 'Color format',\n ),\n h(\n 'div',\n {\n className: 'sb-switcher__section-body',\n role: 'group',\n 'aria-labelledby': 'sb-color-format-label',\n },\n ...COLOR_FORMAT_OPTIONS.map((opt) => {\n const isActive = opt.id === active;\n return h(\n 'button',\n {\n key: `color-format/${opt.id}`,\n type: 'button',\n 'aria-pressed': isActive,\n 'aria-label': `${opt.label} color format`,\n onClick: () => onSelect(opt.id),\n className: isActive\n ? 'sb-switcher__pill sb-switcher__pill--active'\n : 'sb-switcher__pill',\n },\n opt.label,\n );\n }),\n ),\n );\n}\n","import { presetTuple, ThemeSwitcher } from '@unpunnyfuns/swatchbook-switcher';\nimport { COLOR_FORMATS, ColorFormatSelector } from '#/ColorFormatSelector.tsx';\nimport type { ColorFormat } from '#/ColorFormatSelector.tsx';\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { ReactElement } from 'react';\nimport { Button, ToggleButton, WithTooltip } from 'storybook/internal/components';\nimport { addons, types, useGlobals, useStorybookApi } from 'storybook/manager-api';\nimport type {\n InitPayload,\n VirtualAxis as AxisEntry,\n VirtualPreset as PresetEntry,\n} from '#/channel-types.ts';\nimport {\n ADDON_ID,\n AXES_GLOBAL_KEY,\n COLOR_FORMAT_GLOBAL_KEY,\n INIT_EVENT,\n INIT_REQUEST_EVENT,\n PREVIEW_MOUSEDOWN_EVENT,\n TOOL_ID,\n} from '#/constants.ts';\n\n/**\n * Use explicit `React.createElement` rather than JSX so the manager bundle\n * doesn't take a hard dependency on `react/jsx-runtime`. Storybook's manager\n * page injects its own React as a runtime global; `react/jsx-runtime` isn't\n * always part of that exposure, which breaks JSX with\n * \"Cannot read properties of undefined (reading 'recentlyCreatedOwnerStacks')\".\n * Mirrors the pattern `@storybook/addon-a11y` uses in its manager.\n *\n * The imported `<ThemeSwitcher>` from `@unpunnyfuns/swatchbook-switcher`\n * compiles with classic JSX (`React.createElement`) specifically so it\n * survives embedding in the manager bundle the same way.\n */\nconst h = React.createElement;\n\nconst EMPTY_AXES: readonly AxisEntry[] = [];\nconst EMPTY_PRESETS: readonly PresetEntry[] = [];\n\n/**\n * Root toolbar glyph — a split-circle (\"yinyang\") mark: a faint filled\n * disc for the full-swatch silhouette, with a darker half-and-inset-disc\n * path reading as a pair of theme variants swapped in place.\n */\nfunction SwatchbookIcon(): ReactElement {\n return h(\n 'svg',\n { width: 14, height: 14, viewBox: '0 0 14 14', 'aria-hidden': true },\n h('circle', { cx: 7, cy: 7, r: 6, fill: 'currentColor', opacity: 0.15 }),\n h('path', {\n d: 'M7 1a6 6 0 0 0 0 12 3 3 0 0 0 0-6 3 3 0 0 1 0-6Z',\n fill: 'currentColor',\n }),\n );\n}\n\nfunction defaultTupleFor(axes: readonly AxisEntry[]): Record<string, string> {\n const out: Record<string, string> = {};\n for (const axis of axes) out[axis.name] = axis.default;\n return out;\n}\n\nfunction AxesToolbar(): ReactElement {\n const [globals, updateGlobals] = useGlobals();\n const api = useStorybookApi();\n const [payload, setPayload] = useState<InitPayload | null>(null);\n const [open, setOpen] = useState(false);\n const bodyRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n const channel = addons.getChannel();\n const onInit = (next: InitPayload): void => setPayload(next);\n channel.on(INIT_EVENT, onInit);\n /**\n * Ask the preview to (re-)emit INIT_EVENT in case it already broadcast\n * before this effect subscribed. Without this request, a late-mounting\n * manager (story navigation, docs reload) can stay in \"loading…\" until\n * the user triggers a globals change.\n */\n channel.emit(INIT_REQUEST_EVENT);\n return () => {\n channel.off(INIT_EVENT, onInit);\n };\n }, []);\n\n const axes = payload?.axes ?? EMPTY_AXES;\n const presets = payload?.presets ?? EMPTY_PRESETS;\n const defaults = useMemo(() => defaultTupleFor(axes), [axes]);\n const [lastApplied, setLastApplied] = useState<string | null>(null);\n const rawTuple = globals[AXES_GLOBAL_KEY];\n const globalTuple =\n rawTuple && typeof rawTuple === 'object' ? (rawTuple as Record<string, string>) : undefined;\n const rawColorFormat = globals[COLOR_FORMAT_GLOBAL_KEY];\n const activeColorFormat: ColorFormat =\n typeof rawColorFormat === 'string' &&\n (COLOR_FORMATS as readonly string[]).includes(rawColorFormat)\n ? (rawColorFormat as ColorFormat)\n : 'hex';\n\n const activeTuple = useMemo<Record<string, string>>(() => {\n const out: Record<string, string> = { ...defaults };\n if (globalTuple) {\n for (const axis of axes) {\n const candidate = globalTuple[axis.name];\n if (candidate !== undefined && axis.contexts.includes(candidate)) {\n out[axis.name] = candidate;\n }\n }\n }\n return out;\n }, [axes, defaults, globalTuple]);\n\n const setAxis = useCallback(\n (axisName: string, next: string): void => {\n const tuple: Record<string, string> = { ...activeTuple, [axisName]: next };\n updateGlobals({ [AXES_GLOBAL_KEY]: tuple });\n },\n [activeTuple, updateGlobals],\n );\n\n const applyPreset = useCallback(\n (preset: PresetEntry): void => {\n const tuple = presetTuple(preset, axes, defaults);\n updateGlobals({ [AXES_GLOBAL_KEY]: tuple });\n setLastApplied(preset.name);\n },\n [axes, defaults, updateGlobals],\n );\n\n useEffect(() => {\n if (presets.length === 0) return;\n // `alt+shift+C` rather than `alt+T` — the latter conflicts with\n // Storybook's built-in \"toggle addon panel\" binding on some\n // platforms. Rebindable from Storybook's keyboard-shortcuts panel.\n api.setAddonShortcut(ADDON_ID, {\n label: `Cycle swatchbook presets (${presets.length})`,\n defaultShortcut: ['alt', 'shift', 'C'],\n actionName: 'cyclePreset',\n showInMenu: true,\n action: () => {\n const currentIdx = lastApplied\n ? presets.findIndex((preset) => preset.name === lastApplied)\n : -1;\n const next = presets[(currentIdx + 1) % presets.length];\n if (next) applyPreset(next);\n },\n });\n }, [api, presets, lastApplied, applyPreset]);\n\n const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>): void => {\n if (event.key === 'Escape') {\n event.stopPropagation();\n setOpen(false);\n }\n }, []);\n\n /**\n * Escape closes even when focus hasn't entered the popover yet (e.g. the\n * user opened it via click and the mouse is still over the canvas). We\n * attach a document-level listener when open.\n */\n useEffect(() => {\n if (!open) return;\n const onDocKey = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') setOpen(false);\n };\n document.addEventListener('keydown', onDocKey);\n return () => document.removeEventListener('keydown', onDocKey);\n }, [open]);\n\n /**\n * `WithTooltip`'s built-in `closeOnOutsideClick` misses some cases\n * (portaled popover + manager iframe boundaries). Belt-and-suspenders:\n * close when the user mouses down anywhere that isn't the trigger wrapper\n * or the popover body.\n */\n useEffect(() => {\n if (!open) return;\n const onDocMouseDown = (e: MouseEvent): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (bodyRef.current?.contains(target)) return;\n if (target.closest('[data-testid=\"swatchbook-switcher\"]')) return;\n setOpen(false);\n };\n /**\n * The manager's document-level listener above can't see mousedowns\n * inside the preview iframe. Preview emits PREVIEW_MOUSEDOWN_EVENT on\n * every mousedown over its own document; listen for it here so\n * clicking the canvas / docs page also closes the popover.\n */\n const channel = addons.getChannel();\n const onPreviewMouseDown = (): void => setOpen(false);\n document.addEventListener('mousedown', onDocMouseDown);\n channel.on(PREVIEW_MOUSEDOWN_EVENT, onPreviewMouseDown);\n return () => {\n document.removeEventListener('mousedown', onDocMouseDown);\n channel.off(PREVIEW_MOUSEDOWN_EVENT, onPreviewMouseDown);\n };\n }, [open]);\n\n if (axes.length === 0) {\n return h(\n Button,\n {\n key: TOOL_ID,\n ariaLabel: 'Swatchbook theme (loading…)',\n tooltip: 'Swatchbook theme (loading…)',\n disabled: true,\n },\n h(SwatchbookIcon),\n );\n }\n\n const summary = axes.map((a) => activeTuple[a.name] ?? a.default).join(' · ');\n const label = `Swatchbook · ${summary}`;\n\n const button = h(\n ToggleButton,\n {\n key: TOOL_ID,\n ariaLabel: label,\n tooltip: label,\n pressed: open,\n onClick: () => setOpen((prev) => !prev),\n /**\n * Screen-reader disclosure semantics for the popover trigger. We\n * don't set `aria-controls` because the popover is portaled by\n * Storybook's `WithTooltip` with a dynamically-generated id we\n * don't have a stable handle on; `aria-haspopup` + `aria-expanded`\n * is the practical subset of the disclosure pattern.\n *\n * `aria-haspopup=\"true\"` (generic) rather than `\"dialog\"`: the\n * switcher body is `role=\"group\"` (it's a settings panel of\n * independent controls, not a modal dialog), so promising\n * `\"dialog\"` would misalign the trigger with what AT finds when\n * focus enters the popover.\n */\n 'aria-haspopup': true as const,\n 'aria-expanded': open,\n },\n h(SwatchbookIcon),\n );\n\n const tooltipBody = h(ThemeSwitcher, {\n axes,\n presets,\n activeTuple,\n defaults,\n lastApplied,\n onAxisChange: setAxis,\n onPresetApply: applyPreset,\n onKeyDown: handleKeyDown,\n /**\n * Color format is addon-local chrome — drives how swatchbook blocks\n * stringify colors inside stories and docs. Slotted through the\n * switcher's `footer` escape hatch so shared theming UI stays free\n * of this concern.\n */\n footer: h(ColorFormatSelector, {\n active: activeColorFormat,\n onSelect: (next: ColorFormat) => updateGlobals({ [COLOR_FORMAT_GLOBAL_KEY]: next }),\n }),\n });\n\n return h(\n 'span',\n { ref: bodyRef, style: { display: 'inline-flex', alignItems: 'center' } },\n h(WithTooltip, {\n placement: 'bottom',\n trigger: 'click',\n visible: open,\n onVisibleChange: (next: boolean) => setOpen(next),\n closeOnOutsideClick: true,\n tooltip: tooltipBody,\n children: button,\n }),\n );\n}\n\naddons.register(ADDON_ID, () => {\n addons.add(TOOL_ID, {\n type: types.TOOL,\n title: 'Swatchbook theme',\n match: ({ viewMode, tabId }) => !tabId && (viewMode === 'story' || viewMode === 'docs'),\n render: () => h(AxesToolbar),\n });\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"manager.mjs","names":["h"],"sources":["../src/ColorFormatSelector.tsx","../src/manager.tsx"],"sourcesContent":["import type { ColorFormat } from '@unpunnyfuns/swatchbook-blocks';\nimport React from 'react';\nimport type { ReactElement } from 'react';\n\nexport type { ColorFormat };\n\n// Storybook-addon-specific pill row for picking how color sub-values\n// render in swatchbook blocks (hex / rgb / hsl / oklch / raw). Lives in\n// the addon rather than the shared switcher because color format is a\n// blocks-rendering concern, not a theming one — the docs-site navbar\n// switcher has no consumer for it.\n//\n// Reuses the `sb-switcher__*` class names so styling stays consistent\n// with the rest of the toolbar popover. The switcher's CSS is already\n// loaded on the page because this selector only renders inside\n// `<ThemeSwitcher>`'s `footer` prop.\n//\n// Uses `React.createElement` (via `h`) to survive embedding in Storybook's\n// manager bundle, which doesn't expose `react/jsx-runtime`.\n\n/**\n * Runtime list of valid color formats. Declared here (alongside\n * `COLOR_FORMAT_OPTIONS`) rather than imported from blocks because the\n * manager bundle can't pull from `@unpunnyfuns/swatchbook-blocks` — that\n * package's barrel has a top-level `import 'virtual:swatchbook/tokens'`\n * which the manager has no resolver for. The `ColorFormat` type above\n * stays imported (type-only erases at compile time).\n */\nexport const COLOR_FORMATS: readonly ColorFormat[] = ['hex', 'rgb', 'hsl', 'oklch', 'raw'];\n\nconst COLOR_FORMAT_OPTIONS: readonly { id: ColorFormat; label: string }[] = [\n { id: 'hex', label: 'Hex' },\n { id: 'rgb', label: 'RGB' },\n { id: 'hsl', label: 'HSL' },\n { id: 'oklch', label: 'OKLCH' },\n { id: 'raw', label: 'Raw (JSON)' },\n];\n\nconst h = React.createElement;\n\ninterface ColorFormatSelectorProps {\n active: ColorFormat;\n onSelect(next: ColorFormat): void;\n}\n\nexport function ColorFormatSelector({ active, onSelect }: ColorFormatSelectorProps): ReactElement {\n return h(\n 'div',\n null,\n h(\n 'div',\n { className: 'sb-switcher__section-label', id: 'sb-color-format-label' },\n 'Color format',\n ),\n h(\n 'div',\n {\n className: 'sb-switcher__section-body',\n role: 'group',\n 'aria-labelledby': 'sb-color-format-label',\n },\n ...COLOR_FORMAT_OPTIONS.map((opt) => {\n const isActive = opt.id === active;\n return h(\n 'button',\n {\n key: `color-format/${opt.id}`,\n type: 'button',\n 'aria-pressed': isActive,\n 'aria-label': `${opt.label} color format`,\n onClick: () => onSelect(opt.id),\n className: isActive\n ? 'sb-switcher__pill sb-switcher__pill--active'\n : 'sb-switcher__pill',\n },\n opt.label,\n );\n }),\n ),\n );\n}\n","import { presetTuple, ThemeSwitcher } from '@unpunnyfuns/swatchbook-switcher';\nimport { COLOR_FORMATS, ColorFormatSelector } from '#/ColorFormatSelector.tsx';\nimport type { ColorFormat } from '#/ColorFormatSelector.tsx';\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { ReactElement } from 'react';\nimport { Button, ToggleButton, WithTooltip } from 'storybook/internal/components';\nimport { addons, types, useGlobals, useStorybookApi } from 'storybook/manager-api';\nimport type {\n InitPayload,\n VirtualAxis as AxisEntry,\n VirtualPreset as PresetEntry,\n} from '#/channel-types.ts';\nimport {\n ADDON_ID,\n AXES_GLOBAL_KEY,\n COLOR_FORMAT_GLOBAL_KEY,\n INIT_EVENT,\n INIT_REQUEST_EVENT,\n PREVIEW_MOUSEDOWN_EVENT,\n TOOL_ID,\n} from '#/constants.ts';\n\n// Use explicit `React.createElement` rather than JSX so the manager bundle\n// doesn't take a hard dependency on `react/jsx-runtime`. Storybook's manager\n// page injects its own React as a runtime global; `react/jsx-runtime` isn't\n// always part of that exposure, which breaks JSX with\n// \"Cannot read properties of undefined (reading 'recentlyCreatedOwnerStacks')\".\n// Mirrors the pattern `@storybook/addon-a11y` uses in its manager.\n//\n// The imported `<ThemeSwitcher>` from `@unpunnyfuns/swatchbook-switcher`\n// compiles with classic JSX (`React.createElement`) specifically so it\n// survives embedding in the manager bundle the same way.\nconst h = React.createElement;\n\nconst EMPTY_AXES: readonly AxisEntry[] = [];\nconst EMPTY_PRESETS: readonly PresetEntry[] = [];\n\n// Root toolbar glyph — a split-circle (\"yinyang\") mark: a faint filled\n// disc for the full-swatch silhouette, with a darker half-and-inset-disc\n// path reading as a pair of theme variants swapped in place.\nfunction SwatchbookIcon(): ReactElement {\n return h(\n 'svg',\n { width: 14, height: 14, viewBox: '0 0 14 14', 'aria-hidden': true },\n h('circle', { cx: 7, cy: 7, r: 6, fill: 'currentColor', opacity: 0.15 }),\n h('path', {\n d: 'M7 1a6 6 0 0 0 0 12 3 3 0 0 0 0-6 3 3 0 0 1 0-6Z',\n fill: 'currentColor',\n }),\n );\n}\n\nfunction defaultTupleFor(axes: readonly AxisEntry[]): Record<string, string> {\n const out: Record<string, string> = {};\n for (const axis of axes) out[axis.name] = axis.default;\n return out;\n}\n\nfunction AxesToolbar(): ReactElement {\n const [globals, updateGlobals] = useGlobals();\n const api = useStorybookApi();\n const [payload, setPayload] = useState<InitPayload | null>(null);\n const [open, setOpen] = useState(false);\n const bodyRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n const channel = addons.getChannel();\n const onInit = (next: InitPayload): void => setPayload(next);\n channel.on(INIT_EVENT, onInit);\n // Ask the preview to (re-)emit INIT_EVENT in case it already broadcast\n // before this effect subscribed. Without this request, a late-mounting\n // manager (story navigation, docs reload) can stay in \"loading…\" until\n // the user triggers a globals change.\n channel.emit(INIT_REQUEST_EVENT);\n return () => {\n channel.off(INIT_EVENT, onInit);\n };\n }, []);\n\n const axes = payload?.axes ?? EMPTY_AXES;\n const presets = payload?.presets ?? EMPTY_PRESETS;\n const defaults = useMemo(() => defaultTupleFor(axes), [axes]);\n const [lastApplied, setLastApplied] = useState<string | null>(null);\n const rawTuple = globals[AXES_GLOBAL_KEY];\n const globalTuple =\n rawTuple && typeof rawTuple === 'object' ? (rawTuple as Record<string, string>) : undefined;\n const rawColorFormat = globals[COLOR_FORMAT_GLOBAL_KEY];\n const activeColorFormat: ColorFormat =\n typeof rawColorFormat === 'string' &&\n (COLOR_FORMATS as readonly string[]).includes(rawColorFormat)\n ? (rawColorFormat as ColorFormat)\n : 'hex';\n\n const activeTuple = useMemo<Record<string, string>>(() => {\n const out: Record<string, string> = { ...defaults };\n if (globalTuple) {\n for (const axis of axes) {\n const candidate = globalTuple[axis.name];\n if (candidate !== undefined && axis.contexts.includes(candidate)) {\n out[axis.name] = candidate;\n }\n }\n }\n return out;\n }, [axes, defaults, globalTuple]);\n\n const setAxis = useCallback(\n (axisName: string, next: string): void => {\n const tuple: Record<string, string> = { ...activeTuple, [axisName]: next };\n updateGlobals({ [AXES_GLOBAL_KEY]: tuple });\n },\n [activeTuple, updateGlobals],\n );\n\n const applyPreset = useCallback(\n (preset: PresetEntry): void => {\n const tuple = presetTuple(preset, axes, defaults);\n updateGlobals({ [AXES_GLOBAL_KEY]: tuple });\n setLastApplied(preset.name);\n },\n [axes, defaults, updateGlobals],\n );\n\n useEffect(() => {\n if (presets.length === 0) return;\n // `alt+shift+C` rather than `alt+T` — the latter conflicts with\n // Storybook's built-in \"toggle addon panel\" binding on some\n // platforms. Rebindable from Storybook's keyboard-shortcuts panel.\n api.setAddonShortcut(ADDON_ID, {\n label: `Cycle swatchbook presets (${presets.length})`,\n defaultShortcut: ['alt', 'shift', 'C'],\n actionName: 'cyclePreset',\n showInMenu: true,\n action: () => {\n const currentIdx = lastApplied\n ? presets.findIndex((preset) => preset.name === lastApplied)\n : -1;\n const next = presets[(currentIdx + 1) % presets.length];\n if (next) applyPreset(next);\n },\n });\n }, [api, presets, lastApplied, applyPreset]);\n\n const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>): void => {\n if (event.key === 'Escape') {\n event.stopPropagation();\n setOpen(false);\n }\n }, []);\n\n // Escape closes even when focus hasn't entered the popover yet (e.g. the\n // user opened it via click and the mouse is still over the canvas). We\n // attach a document-level listener when open.\n useEffect(() => {\n if (!open) return;\n const onDocKey = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') setOpen(false);\n };\n document.addEventListener('keydown', onDocKey);\n return () => document.removeEventListener('keydown', onDocKey);\n }, [open]);\n\n // `WithTooltip`'s built-in `closeOnOutsideClick` misses some cases\n // (portaled popover + manager iframe boundaries). Belt-and-suspenders:\n // close when the user mouses down anywhere that isn't the trigger wrapper\n // or the popover body.\n useEffect(() => {\n if (!open) return;\n const onDocMouseDown = (e: MouseEvent): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (bodyRef.current?.contains(target)) return;\n if (target.closest('[data-testid=\"swatchbook-switcher\"]')) return;\n setOpen(false);\n };\n // The manager's document-level listener above can't see mousedowns\n // inside the preview iframe. Preview emits PREVIEW_MOUSEDOWN_EVENT on\n // every mousedown over its own document; listen for it here so\n // clicking the canvas / docs page also closes the popover.\n const channel = addons.getChannel();\n const onPreviewMouseDown = (): void => setOpen(false);\n document.addEventListener('mousedown', onDocMouseDown);\n channel.on(PREVIEW_MOUSEDOWN_EVENT, onPreviewMouseDown);\n return () => {\n document.removeEventListener('mousedown', onDocMouseDown);\n channel.off(PREVIEW_MOUSEDOWN_EVENT, onPreviewMouseDown);\n };\n }, [open]);\n\n if (axes.length === 0) {\n return h(\n Button,\n {\n key: TOOL_ID,\n ariaLabel: 'Swatchbook theme (loading…)',\n tooltip: 'Swatchbook theme (loading…)',\n disabled: true,\n },\n h(SwatchbookIcon),\n );\n }\n\n const summary = axes.map((a) => activeTuple[a.name] ?? a.default).join(' · ');\n const label = `Swatchbook · ${summary}`;\n\n const button = h(\n ToggleButton,\n {\n key: TOOL_ID,\n ariaLabel: label,\n tooltip: label,\n pressed: open,\n onClick: () => setOpen((prev) => !prev),\n // Screen-reader disclosure semantics for the popover trigger. We\n // don't set `aria-controls` because the popover is portaled by\n // Storybook's `WithTooltip` with a dynamically-generated id we\n // don't have a stable handle on; `aria-haspopup` + `aria-expanded`\n // is the practical subset of the disclosure pattern.\n //\n // `aria-haspopup=\"true\"` (generic) rather than `\"dialog\"`: the\n // switcher body is `role=\"group\"` (it's a settings panel of\n // independent controls, not a modal dialog), so promising\n // `\"dialog\"` would misalign the trigger with what AT finds when\n // focus enters the popover.\n 'aria-haspopup': true as const,\n 'aria-expanded': open,\n },\n h(SwatchbookIcon),\n );\n\n const tooltipBody = h(ThemeSwitcher, {\n axes,\n presets,\n activeTuple,\n defaults,\n lastApplied,\n onAxisChange: setAxis,\n onPresetApply: applyPreset,\n onKeyDown: handleKeyDown,\n // Color format is addon-local chrome — drives how swatchbook blocks\n // stringify colors inside stories and docs. Slotted through the\n // switcher's `footer` escape hatch so shared theming UI stays free\n // of this concern.\n footer: h(ColorFormatSelector, {\n active: activeColorFormat,\n onSelect: (next: ColorFormat) => updateGlobals({ [COLOR_FORMAT_GLOBAL_KEY]: next }),\n }),\n });\n\n return h(\n 'span',\n { ref: bodyRef, style: { display: 'inline-flex', alignItems: 'center' } },\n h(WithTooltip, {\n placement: 'bottom',\n trigger: 'click',\n visible: open,\n onVisibleChange: (next: boolean) => setOpen(next),\n closeOnOutsideClick: true,\n tooltip: tooltipBody,\n children: button,\n }),\n );\n}\n\naddons.register(ADDON_ID, () => {\n addons.add(TOOL_ID, {\n type: types.TOOL,\n title: 'Swatchbook theme',\n match: ({ viewMode, tabId }) => !tabId && (viewMode === 'story' || viewMode === 'docs'),\n render: () => h(AxesToolbar),\n });\n});\n"],"mappings":"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"}
package/dist/preset.d.mts CHANGED
@@ -4,7 +4,6 @@ import { InlineConfig } from "vite";
4
4
 
5
5
  //#region src/preset.d.ts
6
6
  interface PresetOptions extends AddonOptions {
7
- /** Storybook injects this — the `.storybook` directory absolute path. */
8
7
  configDir: string;
9
8
  }
10
9
  /**
package/dist/preset.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { d as RESOLVED_VIRTUAL_MODULE_ID, h as VIRTUAL_MODULE_ID, i as HMR_EVENT, s as INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID, u as RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID } from "./constants-B31xFInv.mjs";
1
+ import { i as HMR_EVENT, l as RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID, m as VIRTUAL_MODULE_ID, s as INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID, u as RESOLVED_VIRTUAL_MODULE_ID } from "./constants-BuGxPJys.mjs";
2
2
  import { listPaths } from "@unpunnyfuns/swatchbook-core/graph";
3
3
  import { enumerateThemes } from "@unpunnyfuns/swatchbook-core/themes";
4
4
  import { mkdir, writeFile } from "node:fs/promises";
@@ -10,7 +10,6 @@ import { snapshotForWire } from "@unpunnyfuns/swatchbook-core/snapshot-for-wire"
10
10
  import { watch } from "node:fs";
11
11
  import "picomatch";
12
12
  //#region src/virtual/plugin.ts
13
- /** `\0<virtualId>` — Vite convention for resolved virtual module IDs. */
14
13
  function resolvedId(virtualId) {
15
14
  return `\0${virtualId}`;
16
15
  }
@@ -27,9 +26,7 @@ function swatchbookTokensPlugin({ config, cwd, integrations = [], initialProject
27
26
  project = await loadProject(config, cwd);
28
27
  css = emitAxisProjectedCss(project);
29
28
  }
30
- /** Map of resolvedId → integration, indexed once. */
31
29
  const integrationById = /* @__PURE__ */ new Map();
32
- /** Virtual IDs the preview auto-imports as side effects (global CSS). */
33
30
  const autoInjectIds = [];
34
31
  for (const integration of integrations) {
35
32
  const vm = integration.virtualModule;
@@ -78,12 +75,6 @@ function swatchbookTokensPlugin({ config, cwd, integrations = [], initialProject
78
75
  },
79
76
  async configureServer(server) {
80
77
  if (!project) await refresh();
81
- /**
82
- * Editors typically emit two or three filesystem events per save
83
- * (atomic rename + rewrite + metadata). A 100 ms trailing debounce
84
- * coalesces those into a single reload while staying well under
85
- * user-perceptible latency.
86
- */
87
78
  let pending = null;
88
79
  const invalidate = () => {
89
80
  if (pending) clearTimeout(pending);
@@ -104,14 +95,6 @@ function swatchbookTokensPlugin({ config, cwd, integrations = [], initialProject
104
95
  const m = server.moduleGraph.getModuleById(resolvedIntegrationId);
105
96
  if (m) server.moduleGraph.invalidateModule(m);
106
97
  }
107
- /**
108
- * Send the fresh snapshot as a custom HMR event instead of a
109
- * full-reload. The preview subscribes and re-broadcasts to
110
- * blocks via the Storybook channel so the React tree
111
- * re-renders in place without losing toolbar / args / scroll
112
- * state. Field shape matches the INIT_EVENT payload so the
113
- * preview can hand it straight through.
114
- */
115
98
  server.ws.send({
116
99
  type: "custom",
117
100
  event: HMR_EVENT,
@@ -120,18 +103,6 @@ function swatchbookTokensPlugin({ config, cwd, integrations = [], initialProject
120
103
  })();
121
104
  }, 100);
122
105
  };
123
- /**
124
- * Watch each source file's *parent directory* rather than the file
125
- * itself. File-level `fs.watch` is fragile: atomic-save editors
126
- * unlink the old inode and write a new one, so the original
127
- * watcher either fires a one-shot 'rename' and goes deaf, or on
128
- * some platforms loops on ghost events for the old inode. Watching
129
- * the dir sidesteps both — the dir inode is stable across the
130
- * rename dance — and filename filtering keeps event volume low.
131
- *
132
- * Vite's `server.watcher` still wouldn't carry these events across
133
- * pnpm symlink boundaries, so we keep running our own watchers.
134
- */
135
106
  const byDir = /* @__PURE__ */ new Map();
136
107
  for (const file of project?.sourceFiles ?? []) {
137
108
  const dir = dirname(file);
@@ -1 +1 @@
1
- {"version":3,"file":"preset.mjs","names":["fsWatch"],"sources":["../src/virtual/plugin.ts","../src/preset.ts"],"sourcesContent":["import type { Config, Project, SwatchbookIntegration } from '@unpunnyfuns/swatchbook-core';\nimport { emitAxisProjectedCss, loadProject } from '@unpunnyfuns/swatchbook-core';\nimport { listPaths } from '@unpunnyfuns/swatchbook-core/graph';\nimport { snapshotForWire } from '@unpunnyfuns/swatchbook-core/snapshot-for-wire';\nimport { watch as fsWatch } from 'node:fs';\nimport type { FSWatcher } from 'node:fs';\nimport { basename, dirname, isAbsolute, resolve as resolvePath } from 'node:path';\nimport picomatch from 'picomatch';\nimport type { Plugin } from 'vite';\nimport {\n HMR_EVENT,\n INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID,\n RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID,\n RESOLVED_VIRTUAL_MODULE_ID,\n VIRTUAL_MODULE_ID,\n} from '#/constants.ts';\n\nexport interface SwatchbookPluginOptions {\n config: Config;\n cwd: string;\n /** Display-side integrations — each may contribute a virtual module the preview imports. */\n integrations?: readonly SwatchbookIntegration[];\n /**\n * Pre-loaded project to use for the first `buildStart` instead of\n * calling `loadProject` again. Lets `preset.viteFinal` share its\n * single `loadProject` call (originally needed for codegen) with the\n * plugin, eliminating a redundant parse pass at Storybook startup.\n * HMR-triggered reloads still call `loadProject` directly.\n */\n initialProject?: Project;\n}\n\n/** `\\0<virtualId>` — Vite convention for resolved virtual module IDs. */\nfunction resolvedId(virtualId: string): string {\n return `\\0${virtualId}`;\n}\n\n/**\n * Vite plugin that serves the virtual `virtual:swatchbook/tokens` module —\n * a single source of truth for permutations, resolved token maps, per-theme CSS,\n * and diagnostics. Watches the token files + resolver for changes and\n * invalidates the module so HMR reloads the preview with fresh data.\n */\nexport function swatchbookTokensPlugin({\n config,\n cwd,\n integrations = [],\n initialProject,\n}: SwatchbookPluginOptions): Plugin {\n let project: Project | undefined = initialProject;\n let css = project ? emitAxisProjectedCss(project) : '';\n\n async function refresh(): Promise<void> {\n project = await loadProject(config, cwd);\n css = emitAxisProjectedCss(project);\n }\n\n /** Map of resolvedId → integration, indexed once. */\n const integrationById = new Map<string, SwatchbookIntegration>();\n /** Virtual IDs the preview auto-imports as side effects (global CSS). */\n const autoInjectIds: string[] = [];\n for (const integration of integrations) {\n const vm = integration.virtualModule;\n if (!vm) continue;\n integrationById.set(resolvedId(vm.virtualId), integration);\n if (vm.autoInject) autoInjectIds.push(vm.virtualId);\n }\n\n return {\n name: 'swatchbook:virtual-tokens',\n enforce: 'pre',\n\n /**\n * Vite uses esbuild for `optimizeDeps` pre-bundling (development\n * mode), and esbuild doesn't see Rollup-style `resolveId` hooks.\n * Without this exclusion, a preview file that imports\n * `virtual:swatchbook/tokens` (directly or via the addon's\n * `useToken` hook) can get pulled into pre-bundling, hitting\n * esbuild with no resolver registered and failing with\n * `Could not resolve \"virtual:swatchbook/tokens\"`.\n *\n * Excluding the virtual IDs tells Vite to route them through the\n * Rollup-style pipeline at request time — our `resolveId` / `load`\n * hooks above handle the resolution. Build mode is unaffected\n * (build uses Rollup throughout).\n */\n config() {\n return {\n optimizeDeps: {\n exclude: [VIRTUAL_MODULE_ID, INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID],\n },\n };\n },\n\n async buildStart() {\n // Skip the redundant load when preset.viteFinal already supplied\n // a freshly-loaded project via `initialProject`. The first HMR\n // reload (or a manual `refresh()`) calls `loadProject` as usual.\n if (project) return;\n await refresh();\n },\n\n resolveId(id) {\n if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;\n if (id === INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID) {\n return RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID;\n }\n for (const integration of integrations) {\n if (integration.virtualModule?.virtualId === id) {\n return resolvedId(integration.virtualModule.virtualId);\n }\n }\n return null;\n },\n\n load(id) {\n if (id === RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID) {\n // Aggregate side-effect imports. Empty when no integration\n // opted in — still a valid ESM module, just a no-op.\n return autoInjectIds.map((vid) => `import ${JSON.stringify(vid)};`).join('\\n');\n }\n const integration = integrationById.get(id);\n if (integration?.virtualModule) {\n if (!project) return '';\n return integration.virtualModule.render(project);\n }\n if (id !== RESOLVED_VIRTUAL_MODULE_ID) return null;\n if (!project) return 'export default null;';\n // Emit a typed ESM module. `snapshotForWire` does the field set +\n // Map-to-Object conversion in one place; we destructure here and\n // JSON-stringify each field for ESM export.\n const snap = snapshotForWire(project, css);\n return [\n `/* swatchbook virtual module — generated */`,\n `export const axes = ${JSON.stringify(snap.axes)};`,\n `export const presets = ${JSON.stringify(snap.presets)};`,\n `export const disabledAxes = ${JSON.stringify(snap.disabledAxes)};`,\n `export const diagnostics = ${JSON.stringify(snap.diagnostics)};`,\n `export const css = ${JSON.stringify(snap.css)};`,\n `export const cssVarPrefix = ${JSON.stringify(snap.cssVarPrefix)};`,\n `export const listing = ${JSON.stringify(snap.listing)};`,\n `export const defaultTuple = ${JSON.stringify(snap.defaultTuple)};`,\n `export const tokenGraph = ${JSON.stringify(snap.tokenGraph)};`,\n ].join('\\n');\n },\n\n async configureServer(server) {\n // `configureServer` fires before `buildStart` in Vite's plugin\n // lifecycle, so `project` is still undefined when consumers only\n // set `config.resolver` (no `tokens` glob). Force an initial load\n // here so the watcher setup below sees a populated `sourceFiles`\n // list — otherwise only the resolver file itself gets watched,\n // and saves to any `$ref` target silently drop.\n if (!project) await refresh();\n\n /**\n * Editors typically emit two or three filesystem events per save\n * (atomic rename + rewrite + metadata). A 100 ms trailing debounce\n * coalesces those into a single reload while staying well under\n * user-perceptible latency.\n */\n let pending: ReturnType<typeof setTimeout> | null = null;\n const invalidate = (): void => {\n if (pending) clearTimeout(pending);\n pending = setTimeout(() => {\n pending = null;\n void (async () => {\n await refresh();\n if (!project) return;\n const tokenCount = [...listPaths(project.tokenGraph)].length;\n const diagCount = project.diagnostics.length;\n server.config.logger.info(\n `\\x1b[36m[swatchbook]\\x1b[0m tokens reloaded — ${tokenCount} tokens, ${diagCount} diagnostic${diagCount === 1 ? '' : 's'}`,\n { clear: false, timestamp: true },\n );\n const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID);\n if (mod) server.moduleGraph.invalidateModule(mod);\n // Invalidate every integration-contributed virtual module so\n // its body re-renders against the fresh project on the next\n // request.\n for (const resolvedIntegrationId of integrationById.keys()) {\n const m = server.moduleGraph.getModuleById(resolvedIntegrationId);\n if (m) server.moduleGraph.invalidateModule(m);\n }\n /**\n * Send the fresh snapshot as a custom HMR event instead of a\n * full-reload. The preview subscribes and re-broadcasts to\n * blocks via the Storybook channel so the React tree\n * re-renders in place without losing toolbar / args / scroll\n * state. Field shape matches the INIT_EVENT payload so the\n * preview can hand it straight through.\n */\n server.ws.send({\n type: 'custom',\n event: HMR_EVENT,\n data: snapshotForWire(project, css),\n });\n })();\n }, 100);\n };\n\n /**\n * Watch each source file's *parent directory* rather than the file\n * itself. File-level `fs.watch` is fragile: atomic-save editors\n * unlink the old inode and write a new one, so the original\n * watcher either fires a one-shot 'rename' and goes deaf, or on\n * some platforms loops on ghost events for the old inode. Watching\n * the dir sidesteps both — the dir inode is stable across the\n * rename dance — and filename filtering keeps event volume low.\n *\n * Vite's `server.watcher` still wouldn't carry these events across\n * pnpm symlink boundaries, so we keep running our own watchers.\n */\n const byDir = new Map<string, Set<string>>();\n for (const file of project?.sourceFiles ?? []) {\n const dir = dirname(file);\n const set = byDir.get(dir) ?? new Set<string>();\n set.add(basename(file));\n byDir.set(dir, set);\n }\n\n const fileWatchers: FSWatcher[] = [];\n for (const [dir, names] of byDir) {\n try {\n const w = fsWatch(dir, { persistent: false }, (eventType, filename) => {\n if (!filename) return;\n if (!names.has(filename)) return;\n if (eventType === 'change' || eventType === 'rename') invalidate();\n });\n fileWatchers.push(w);\n } catch {\n // unwatchable dir — skip. Next loadProject pass will report it.\n }\n }\n server.httpServer?.once('close', () => {\n for (const w of fileWatchers) w.close();\n });\n },\n };\n}\n\n/**\n * Collect the set of filesystem paths the dev server should watch for\n * HMR. When `config.tokens` is set, use its globs (stripped to their\n * base directories) — users opt in to broader watching this way. When\n * absent, use the resolver file + every `$ref` target it pulled in, as\n * tracked on `project.sourceFiles` — which stays correct as the resolver\n * evolves without requiring a parallel `tokens` glob.\n */\n/** @internal Exported for tests; not part of the public API. */\nexport function collectWatchPaths(\n config: Config,\n project: Project | undefined,\n cwd: string,\n): string[] {\n const paths: string[] = [];\n if (config.tokens && config.tokens.length > 0) {\n for (const glob of config.tokens) {\n // `picomatch.scan` yields the longest literal prefix before any glob\n // metachar, so it handles brace expansion, nested globstars, and the\n // other shapes the hand-rolled regex missed.\n const { base } = picomatch.scan(glob);\n paths.push(resolveFromCwd(base || '.', cwd));\n }\n } else if (project?.sourceFiles) {\n for (const file of project.sourceFiles) paths.push(dirname(file));\n }\n if (config.resolver) paths.push(resolveFromCwd(config.resolver, cwd));\n return [...new Set(paths)];\n}\n\nfunction resolveFromCwd(p: string, cwd: string): string {\n if (isAbsolute(p)) return p;\n return resolvePath(cwd, p);\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { Config, Project } from '@unpunnyfuns/swatchbook-core';\nimport { loadProject } from '@unpunnyfuns/swatchbook-core';\nimport { listPaths } from '@unpunnyfuns/swatchbook-core/graph';\nimport { enumerateThemes } from '@unpunnyfuns/swatchbook-core/themes';\nimport { createJiti } from 'jiti';\nimport type { InlineConfig } from 'vite';\nimport type { AddonOptions } from '#/options.ts';\nimport { swatchbookTokensPlugin } from '#/virtual/plugin.ts';\n\ninterface PresetOptions extends AddonOptions {\n /** Storybook injects this — the `.storybook` directory absolute path. */\n configDir: string;\n}\n\n/**\n * Storybook preset entry. Called by Storybook at config time; extends Vite's\n * plugin list with our virtual-module plugin so the preview can import\n * `virtual:swatchbook/tokens`. Also writes the typed token-path codegen so\n * `useToken()` autocompletes against the loaded project.\n */\nexport async function viteFinal(\n viteConfig: InlineConfig,\n options: PresetOptions,\n): Promise<InlineConfig> {\n const { config, cwd } = await resolveConfig(options);\n\n // One `loadProject` call shared between codegen (needs the resolved\n // shape to render typed token paths) and the Vite plugin (uses it\n // for its first virtual-module render). Without sharing, the addon\n // calls `loadProject` twice at Storybook startup — once here for\n // codegen, once again inside the plugin's `buildStart`.\n const project = await loadProject(config, cwd);\n await writeTokenCodegen(project, options);\n\n const plugins = Array.isArray(viteConfig.plugins) ? [...viteConfig.plugins] : [];\n plugins.push(\n swatchbookTokensPlugin({\n config,\n cwd,\n initialProject: project,\n ...(options.integrations !== undefined && { integrations: options.integrations }),\n }),\n );\n\n return { ...viteConfig, plugins };\n}\n\nasync function resolveConfig(options: PresetOptions): Promise<{ config: Config; cwd: string }> {\n const projectRoot = resolve(options.configDir, '..');\n\n if (options.config) {\n return { config: options.config, cwd: projectRoot };\n }\n\n const path = options.configPath ?? 'swatchbook.config.ts';\n const absolute = isAbsolute(path) ? path : resolve(options.configDir, path);\n\n const jiti = createJiti(pathToFileURL(options.configDir).href, {\n interopDefault: true,\n moduleCache: false,\n });\n const loaded = (await jiti.import(absolute, { default: true })) as Config;\n\n // If the config file isn't at projectRoot, still resolve globs from its dir.\n const cwd = dirname(absolute);\n return { config: loaded, cwd };\n}\n\nasync function writeTokenCodegen(project: Project, options: PresetOptions): Promise<void> {\n const projectRoot = resolve(options.configDir, '..');\n const outDir = resolve(projectRoot, project.config.outDir ?? '.swatchbook');\n await mkdir(outDir, { recursive: true });\n const content = renderTokenTypes(project);\n await writeFile(resolve(outDir, 'tokens.d.ts'), content);\n}\n\n/** @internal Exported for tests; not part of the public API. */\nexport function renderTokenTypes(project: Project): string {\n const sorted = [...listPaths(project.tokenGraph)].toSorted();\n const tokenEntries = sorted.map((p) => ` ${JSON.stringify(p)}: string;`);\n const themeNames = project.axes.length === 0 ? [] : enumerateThemes(project).map((t) => t.name);\n const themeUnion =\n themeNames.length > 0 ? themeNames.map((n) => JSON.stringify(n)).join(' | ') : 'string';\n\n return [\n '// Generated by @unpunnyfuns/swatchbook-addon. Do not edit.',\n \"declare module '@unpunnyfuns/swatchbook-addon/hooks' {\",\n ' interface SwatchbookTokenMap {',\n ...tokenEntries,\n ' }',\n '',\n ` export type SwatchbookPermutationName = ${themeUnion};`,\n '}',\n '',\n ].join('\\n');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"preset.mjs","names":["fsWatch"],"sources":["../src/virtual/plugin.ts","../src/preset.ts"],"sourcesContent":["import type { Config, Project, SwatchbookIntegration } from '@unpunnyfuns/swatchbook-core';\nimport { emitAxisProjectedCss, loadProject } from '@unpunnyfuns/swatchbook-core';\nimport { listPaths } from '@unpunnyfuns/swatchbook-core/graph';\nimport { snapshotForWire } from '@unpunnyfuns/swatchbook-core/snapshot-for-wire';\nimport { watch as fsWatch } from 'node:fs';\nimport type { FSWatcher } from 'node:fs';\nimport { basename, dirname, isAbsolute, resolve as resolvePath } from 'node:path';\nimport picomatch from 'picomatch';\nimport type { Plugin } from 'vite';\nimport {\n HMR_EVENT,\n INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID,\n RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID,\n RESOLVED_VIRTUAL_MODULE_ID,\n VIRTUAL_MODULE_ID,\n} from '#/constants.ts';\n\nexport interface SwatchbookTokensPluginOptions {\n config: Config;\n cwd: string;\n /** Display-side integrations — each may contribute a virtual module the preview imports. */\n integrations?: readonly SwatchbookIntegration[];\n /**\n * Pre-loaded project to use for the first `buildStart` instead of\n * calling `loadProject` again. Shares `preset.viteFinal`'s single\n * `loadProject` call with the plugin so Storybook startup doesn't parse\n * twice. HMR-triggered reloads still call `loadProject` directly.\n */\n initialProject?: Project;\n}\n\n// `\\0<virtualId>` — Vite convention for resolved virtual module IDs.\nfunction resolvedId(virtualId: string): string {\n return `\\0${virtualId}`;\n}\n\n/**\n * Vite plugin that serves the virtual `virtual:swatchbook/tokens` module —\n * a single source of truth for permutations, resolved token maps, per-theme CSS,\n * and diagnostics. Watches the token files + resolver for changes and\n * invalidates the module so HMR reloads the preview with fresh data.\n */\nexport function swatchbookTokensPlugin({\n config,\n cwd,\n integrations = [],\n initialProject,\n}: SwatchbookTokensPluginOptions): Plugin {\n let project: Project | undefined = initialProject;\n let css = project ? emitAxisProjectedCss(project) : '';\n\n async function refresh(): Promise<void> {\n project = await loadProject(config, cwd);\n css = emitAxisProjectedCss(project);\n }\n\n // Map of resolvedId → integration, indexed once.\n const integrationById = new Map<string, SwatchbookIntegration>();\n // Virtual IDs the preview auto-imports as side effects (global CSS).\n const autoInjectIds: string[] = [];\n for (const integration of integrations) {\n const vm = integration.virtualModule;\n if (!vm) continue;\n integrationById.set(resolvedId(vm.virtualId), integration);\n if (vm.autoInject) autoInjectIds.push(vm.virtualId);\n }\n\n return {\n name: 'swatchbook:virtual-tokens',\n enforce: 'pre',\n\n // Vite uses esbuild for `optimizeDeps` pre-bundling (development\n // mode), and esbuild doesn't see Rollup-style `resolveId` hooks.\n // Without this exclusion, a preview file that imports\n // `virtual:swatchbook/tokens` (directly or via the addon's\n // `useToken` hook) can get pulled into pre-bundling, hitting\n // esbuild with no resolver registered and failing with\n // `Could not resolve \"virtual:swatchbook/tokens\"`.\n //\n // Excluding the virtual IDs tells Vite to route them through the\n // Rollup-style pipeline at request time — our `resolveId` / `load`\n // hooks above handle the resolution. Build mode is unaffected\n // (build uses Rollup throughout).\n config() {\n return {\n optimizeDeps: {\n exclude: [VIRTUAL_MODULE_ID, INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID],\n },\n };\n },\n\n async buildStart() {\n // Skip the redundant load when preset.viteFinal already supplied\n // a freshly-loaded project via `initialProject`. The first HMR\n // reload (or a manual `refresh()`) calls `loadProject` as usual.\n if (project) return;\n await refresh();\n },\n\n resolveId(id) {\n if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;\n if (id === INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID) {\n return RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID;\n }\n for (const integration of integrations) {\n if (integration.virtualModule?.virtualId === id) {\n return resolvedId(integration.virtualModule.virtualId);\n }\n }\n return null;\n },\n\n load(id) {\n if (id === RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID) {\n // Aggregate side-effect imports. Empty when no integration\n // opted in — still a valid ESM module, just a no-op.\n return autoInjectIds.map((vid) => `import ${JSON.stringify(vid)};`).join('\\n');\n }\n const integration = integrationById.get(id);\n if (integration?.virtualModule) {\n if (!project) return '';\n return integration.virtualModule.render(project);\n }\n if (id !== RESOLVED_VIRTUAL_MODULE_ID) return null;\n if (!project) return 'export default null;';\n // Emit a typed ESM module. `snapshotForWire` does the field set +\n // Map-to-Object conversion in one place; we destructure here and\n // JSON-stringify each field for ESM export.\n const snap = snapshotForWire(project, css);\n return [\n `/* swatchbook virtual module — generated */`,\n `export const axes = ${JSON.stringify(snap.axes)};`,\n `export const presets = ${JSON.stringify(snap.presets)};`,\n `export const disabledAxes = ${JSON.stringify(snap.disabledAxes)};`,\n `export const diagnostics = ${JSON.stringify(snap.diagnostics)};`,\n `export const css = ${JSON.stringify(snap.css)};`,\n `export const cssVarPrefix = ${JSON.stringify(snap.cssVarPrefix)};`,\n `export const listing = ${JSON.stringify(snap.listing)};`,\n `export const defaultTuple = ${JSON.stringify(snap.defaultTuple)};`,\n `export const tokenGraph = ${JSON.stringify(snap.tokenGraph)};`,\n ].join('\\n');\n },\n\n async configureServer(server) {\n // `configureServer` fires before `buildStart` in Vite's plugin\n // lifecycle, so `project` is still undefined when consumers only\n // set `config.resolver` (no `tokens` glob). Force an initial load\n // here so the watcher setup below sees a populated `sourceFiles`\n // list — otherwise only the resolver file itself gets watched,\n // and saves to any `$ref` target silently drop.\n if (!project) await refresh();\n\n // Editors typically emit two or three filesystem events per save\n // (atomic rename + rewrite + metadata). A 100 ms trailing debounce\n // coalesces those into a single reload while staying well under\n // user-perceptible latency.\n let pending: ReturnType<typeof setTimeout> | null = null;\n const invalidate = (): void => {\n if (pending) clearTimeout(pending);\n pending = setTimeout(() => {\n pending = null;\n void (async () => {\n await refresh();\n if (!project) return;\n const tokenCount = [...listPaths(project.tokenGraph)].length;\n const diagCount = project.diagnostics.length;\n server.config.logger.info(\n `\\x1b[36m[swatchbook]\\x1b[0m tokens reloaded — ${tokenCount} tokens, ${diagCount} diagnostic${diagCount === 1 ? '' : 's'}`,\n { clear: false, timestamp: true },\n );\n const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID);\n if (mod) server.moduleGraph.invalidateModule(mod);\n // Invalidate every integration-contributed virtual module so\n // its body re-renders against the fresh project on the next\n // request.\n for (const resolvedIntegrationId of integrationById.keys()) {\n const m = server.moduleGraph.getModuleById(resolvedIntegrationId);\n if (m) server.moduleGraph.invalidateModule(m);\n }\n // Send the fresh snapshot as a custom HMR event instead of a\n // full-reload. The preview subscribes and re-broadcasts to\n // blocks via the Storybook channel so the React tree\n // re-renders in place without losing toolbar / args / scroll\n // state. Field shape matches the INIT_EVENT payload so the\n // preview can hand it straight through.\n server.ws.send({\n type: 'custom',\n event: HMR_EVENT,\n data: snapshotForWire(project, css),\n });\n })();\n }, 100);\n };\n\n // Watch each source file's *parent directory* rather than the file\n // itself. File-level `fs.watch` is fragile: atomic-save editors\n // unlink the old inode and write a new one, so the original\n // watcher either fires a one-shot 'rename' and goes deaf, or on\n // some platforms loops on ghost events for the old inode. Watching\n // the dir sidesteps both — the dir inode is stable across the\n // rename dance — and filename filtering keeps event volume low.\n //\n // Vite's `server.watcher` still wouldn't carry these events across\n // pnpm symlink boundaries, so we keep running our own watchers.\n const byDir = new Map<string, Set<string>>();\n for (const file of project?.sourceFiles ?? []) {\n const dir = dirname(file);\n const set = byDir.get(dir) ?? new Set<string>();\n set.add(basename(file));\n byDir.set(dir, set);\n }\n\n const fileWatchers: FSWatcher[] = [];\n for (const [dir, names] of byDir) {\n try {\n const w = fsWatch(dir, { persistent: false }, (eventType, filename) => {\n if (!filename) return;\n if (!names.has(filename)) return;\n if (eventType === 'change' || eventType === 'rename') invalidate();\n });\n fileWatchers.push(w);\n } catch {\n // unwatchable dir — skip. Next loadProject pass will report it.\n }\n }\n server.httpServer?.once('close', () => {\n for (const w of fileWatchers) w.close();\n });\n },\n };\n}\n\n/**\n * Collect the set of filesystem paths the dev server should watch for\n * HMR. When `config.tokens` is set, use its globs (stripped to their\n * base directories) — users opt in to broader watching this way. When\n * absent, use the resolver file + every `$ref` target it pulled in, as\n * tracked on `project.sourceFiles` — which stays correct as the resolver\n * evolves without requiring a parallel `tokens` glob.\n *\n * @internal Exported for tests; not part of the public API.\n */\nexport function collectWatchPaths(\n config: Config,\n project: Project | undefined,\n cwd: string,\n): string[] {\n const paths: string[] = [];\n if (config.tokens && config.tokens.length > 0) {\n for (const glob of config.tokens) {\n // `picomatch.scan` yields the longest literal prefix before any glob\n // metachar, so it handles brace expansion, nested globstars, and the\n // other shapes the hand-rolled regex missed.\n const { base } = picomatch.scan(glob);\n paths.push(resolveFromCwd(base || '.', cwd));\n }\n } else if (project?.sourceFiles) {\n for (const file of project.sourceFiles) paths.push(dirname(file));\n }\n if (config.resolver) paths.push(resolveFromCwd(config.resolver, cwd));\n return [...new Set(paths)];\n}\n\nfunction resolveFromCwd(p: string, cwd: string): string {\n if (isAbsolute(p)) return p;\n return resolvePath(cwd, p);\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { Config, Project } from '@unpunnyfuns/swatchbook-core';\nimport { loadProject } from '@unpunnyfuns/swatchbook-core';\nimport { listPaths } from '@unpunnyfuns/swatchbook-core/graph';\nimport { enumerateThemes } from '@unpunnyfuns/swatchbook-core/themes';\nimport { createJiti } from 'jiti';\nimport type { InlineConfig } from 'vite';\nimport type { AddonOptions } from '#/options.ts';\nimport { swatchbookTokensPlugin } from '#/virtual/plugin.ts';\n\ninterface PresetOptions extends AddonOptions {\n // Storybook injects this — the `.storybook` directory absolute path.\n configDir: string;\n}\n\n/**\n * Storybook preset entry. Called by Storybook at config time; extends Vite's\n * plugin list with our virtual-module plugin so the preview can import\n * `virtual:swatchbook/tokens`. Also writes the typed token-path codegen so\n * `useToken()` autocompletes against the loaded project.\n */\nexport async function viteFinal(\n viteConfig: InlineConfig,\n options: PresetOptions,\n): Promise<InlineConfig> {\n const { config, cwd } = await resolveConfig(options);\n\n // One `loadProject` call shared between codegen (needs the resolved\n // shape to render typed token paths) and the Vite plugin (uses it\n // for its first virtual-module render). Without sharing, the addon\n // calls `loadProject` twice at Storybook startup — once here for\n // codegen, once again inside the plugin's `buildStart`.\n const project = await loadProject(config, cwd);\n await writeTokenCodegen(project, options);\n\n const plugins = Array.isArray(viteConfig.plugins) ? [...viteConfig.plugins] : [];\n plugins.push(\n swatchbookTokensPlugin({\n config,\n cwd,\n initialProject: project,\n ...(options.integrations !== undefined && { integrations: options.integrations }),\n }),\n );\n\n return { ...viteConfig, plugins };\n}\n\nasync function resolveConfig(options: PresetOptions): Promise<{ config: Config; cwd: string }> {\n const projectRoot = resolve(options.configDir, '..');\n\n if (options.config) {\n return { config: options.config, cwd: projectRoot };\n }\n\n const path = options.configPath ?? 'swatchbook.config.ts';\n const absolute = isAbsolute(path) ? path : resolve(options.configDir, path);\n\n const jiti = createJiti(pathToFileURL(options.configDir).href, {\n interopDefault: true,\n moduleCache: false,\n });\n const loaded = (await jiti.import(absolute, { default: true })) as Config;\n\n // If the config file isn't at projectRoot, still resolve globs from its dir.\n const cwd = dirname(absolute);\n return { config: loaded, cwd };\n}\n\nasync function writeTokenCodegen(project: Project, options: PresetOptions): Promise<void> {\n const projectRoot = resolve(options.configDir, '..');\n const outDir = resolve(projectRoot, project.config.outDir ?? '.swatchbook');\n await mkdir(outDir, { recursive: true });\n const content = renderTokenTypes(project);\n await writeFile(resolve(outDir, 'tokens.d.ts'), content);\n}\n\n/** @internal Exported for tests; not part of the public API. */\nexport function renderTokenTypes(project: Project): string {\n const sorted = [...listPaths(project.tokenGraph)].toSorted();\n const tokenEntries = sorted.map((p) => ` ${JSON.stringify(p)}: string;`);\n const themeNames = project.axes.length === 0 ? [] : enumerateThemes(project).map((t) => t.name);\n const themeUnion =\n themeNames.length > 0 ? themeNames.map((n) => JSON.stringify(n)).join(' | ') : 'string';\n\n return [\n '// Generated by @unpunnyfuns/swatchbook-addon. Do not edit.',\n \"declare module '@unpunnyfuns/swatchbook-addon/hooks' {\",\n ' interface SwatchbookTokenMap {',\n ...tokenEntries,\n ' }',\n '',\n ` export type SwatchbookPermutationName = ${themeUnion};`,\n '}',\n '',\n ].join('\\n');\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { a as INIT_EVENT, f as STYLE_ELEMENT_ID, i as HMR_EVENT, l as PREVIEW_MOUSEDOWN_EVENT, n as AXES_GLOBAL_KEY, o as INIT_REQUEST_EVENT, p as TOKENS_UPDATED_EVENT, r as COLOR_FORMAT_GLOBAL_KEY } from "./constants-B31xFInv.mjs";
1
+ import { a as INIT_EVENT, c as PREVIEW_MOUSEDOWN_EVENT, d as STYLE_ELEMENT_ID, f as TOKENS_UPDATED_EVENT, i as HMR_EVENT, n as AXES_GLOBAL_KEY, o as INIT_REQUEST_EVENT, r as COLOR_FORMAT_GLOBAL_KEY } from "./constants-BuGxPJys.mjs";
2
2
  import { resolveAllAt } from "@unpunnyfuns/swatchbook-core/graph";
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { addons } from "storybook/preview-api";
@@ -27,12 +27,6 @@ var preview_exports = /* @__PURE__ */ __exportAll({
27
27
  globalTypes: () => globalTypes,
28
28
  initialGlobals: () => initialGlobals
29
29
  });
30
- /**
31
- * Standard visually-hidden style for the theme-flip live region.
32
- * Keeps the announcement element discoverable by SR but out of visual
33
- * + pointer flow. The clip-path / `position: absolute` combination is
34
- * the canonical sr-only pattern.
35
- */
36
30
  const SR_ONLY_STYLE = {
37
31
  position: "absolute",
38
32
  width: 1,
@@ -44,12 +38,6 @@ const SR_ONLY_STYLE = {
44
38
  whiteSpace: "nowrap",
45
39
  border: 0
46
40
  };
47
- /**
48
- * The `html, body { ... }` rules that paint the iframe's own chrome
49
- * (outside any decorator wrapper — Docs mode, autodocs, empty gutters)
50
- * with the active theme's surface + text vars. Composed alongside the
51
- * emitted token CSS so both load through the same `<style>` element.
52
- */
53
41
  function iframeChromeRules(prefix) {
54
42
  return `
55
43
  html, body {
@@ -59,45 +47,19 @@ html, body {
59
47
  }
60
48
  `;
61
49
  }
62
- /**
63
- * Inject the per-theme stylesheet plus the iframe-chrome block. Shared
64
- * with the HMR re-emit path below so a token refresh updates the
65
- * iframe's chrome rules from the same source.
66
- */
67
50
  function ensureStylesheet(cssText, prefix) {
68
51
  ensureStyleElement(STYLE_ELEMENT_ID, `${cssText}\n${iframeChromeRules(prefix)}`);
69
52
  }
70
- /**
71
- * Apply `cb(axisName, value)` for every pinned (disabled) axis whose
72
- * default-tuple value is set. `virtualDefaultTuple` carries the
73
- * post-filter axis defaults; disabled axes don't appear in
74
- * `virtualAxes` but their pinned context value still lives here, so
75
- * sampling it gives the same result the old "first permutation's
76
- * input" lookup did.
77
- */
78
53
  function forEachPinnedAxis(cb) {
79
54
  for (const name of disabledAxes) {
80
55
  const value = defaultTuple[name];
81
56
  if (value !== void 0) cb(name, value);
82
57
  }
83
58
  }
84
- /**
85
- * Compose a stable theme name from a tuple — `axisValues.join(' · ')`
86
- * in axis order. Used for the `ThemeContext` value the blocks read and
87
- * the addon-channel signals downstream consumers subscribe to. Returns
88
- * empty string when there are no axes (no name to write).
89
- */
90
- function matchThemeName(tuple) {
59
+ function composeThemeName(tuple) {
91
60
  if (axes.length === 0) return "";
92
61
  return tupleToName(axes, tuple);
93
62
  }
94
- /**
95
- * Write one `data-<prefix>-<axis>=<context>` per axis on `<html>`.
96
- * The smart CSS emitter targets these single-axis selectors (and
97
- * joint compounds across multiple) — that's the actual scoping
98
- * surface the cascade resolves through. Prefix follows `cssVarPrefix`
99
- * so attr namespace and emitted selectors stay in lockstep.
100
- */
101
63
  function setRootAxes(tuple) {
102
64
  if (typeof document === "undefined") return;
103
65
  const root = document.documentElement;
@@ -111,7 +73,7 @@ function setRootAxes(tuple) {
111
73
  root.setAttribute(dataAttr(cssVarPrefix, name), value);
112
74
  });
113
75
  }
114
- function pickInitFields(source) {
76
+ function toInitPayload(source) {
115
77
  return {
116
78
  axes: source.axes,
117
79
  disabledAxes: source.disabledAxes,
@@ -121,13 +83,8 @@ function pickInitFields(source) {
121
83
  defaultTuple: source.defaultTuple
122
84
  };
123
85
  }
124
- /**
125
- * Emit the full virtual-module payload to the manager over Storybook's
126
- * channel so the toolbar + panel (which run in the manager bundle and
127
- * can't import our virtual module) can render from it.
128
- */
129
86
  function broadcastInit() {
130
- addons.getChannel().emit(INIT_EVENT, pickInitFields({
87
+ addons.getChannel().emit(INIT_EVENT, toInitPayload({
131
88
  axes,
132
89
  disabledAxes,
133
90
  presets,
@@ -136,19 +93,11 @@ function broadcastInit() {
136
93
  defaultTuple
137
94
  }));
138
95
  }
139
- /** Axis-default tuple, used as the baseline before overrides. */
140
96
  function defaultTuple$1() {
141
97
  const out = {};
142
98
  for (const axis of axes) out[axis.name] = axis.default;
143
99
  return out;
144
100
  }
145
- /**
146
- * Reverse-engineer a tuple from a `Light · Brand A · Normal`-shape
147
- * theme name. Splits on ` · ` and zips with `virtualAxes` in declared
148
- * order — matches `matchThemeName`'s production direction so a
149
- * round-trip is lossless. Returns `undefined` when the segment count
150
- * doesn't match the axis count.
151
- */
152
101
  function tupleForName(name) {
153
102
  if (!name) return void 0;
154
103
  const parts = name.split(" · ");
@@ -162,11 +111,6 @@ function tupleForName(name) {
162
111
  }
163
112
  return out;
164
113
  }
165
- /**
166
- * Merge a partial tuple onto the axis defaults, dropping keys for axes that
167
- * don't exist and silently falling back to the default for contexts that
168
- * aren't listed on the axis.
169
- */
170
114
  function normalizeTuple(partial) {
171
115
  const out = defaultTuple$1();
172
116
  for (const axis of axes) {
@@ -175,18 +119,11 @@ function normalizeTuple(partial) {
175
119
  }
176
120
  return out;
177
121
  }
178
- /**
179
- * Resolve the active tuple from all input channels, in priority order:
180
- * 1. `parameters.swatchbook.axes` — per-story tuple.
181
- * 2. `parameters.swatchbook.permutation` — per-story composed name.
182
- * 3. `globals.swatchbookAxes` — toolbar-set tuple.
183
- * 4. virtual module default.
184
- */
185
122
  function resolveTuple(axesGlobal, paramSwatchbook) {
186
123
  const paramAxes = paramSwatchbook?.axes;
187
124
  if (paramAxes) return normalizeTuple(paramAxes);
188
- if (paramSwatchbook?.permutation) {
189
- const hit = tupleForName(paramSwatchbook.permutation);
125
+ if (paramSwatchbook?.themeName) {
126
+ const hit = tupleForName(paramSwatchbook.themeName);
190
127
  if (hit) return normalizeTuple(hit);
191
128
  }
192
129
  if (axesGlobal && typeof axesGlobal === "object") return normalizeTuple(axesGlobal);
@@ -196,14 +133,6 @@ function resolveColorFormat(raw) {
196
133
  if (typeof raw === "string" && COLOR_FORMATS.includes(raw)) return raw;
197
134
  return "hex";
198
135
  }
199
- /**
200
- * Single shared `resolveAt` instance for the lifetime of the preview
201
- * iframe. `virtualTokenGraph` is a module-level virtual-module export
202
- * with stable identity, so this closure never needs to rebuild;
203
- * downstream `ProjectSnapshot` consumers can key memos on the snapshot
204
- * wrapper without worrying about `resolveAt` churning when Storybook
205
- * recreates `context.globals`.
206
- */
207
136
  const previewResolveAt = (tuple) => resolveAllAt(tokenGraph, tuple);
208
137
  const themedDecorator = (Story, context) => {
209
138
  const globals = context.globals;
@@ -213,7 +142,7 @@ const themedDecorator = (Story, context) => {
213
142
  const paramSwatchbook = parameters.swatchbook;
214
143
  const tuple = useMemo(() => resolveTuple(axesGlobal, paramSwatchbook), [axesGlobal, paramSwatchbook]);
215
144
  const colorFormat = useMemo(() => resolveColorFormat(colorFormatGlobal), [colorFormatGlobal]);
216
- const themeName = useMemo(() => matchThemeName(tuple), [tuple]);
145
+ const themeName = useMemo(() => composeThemeName(tuple), [tuple]);
217
146
  useEffect(() => {
218
147
  ensureStylesheet(css, cssVarPrefix);
219
148
  broadcastInit();
@@ -242,8 +171,6 @@ const themedDecorator = (Story, context) => {
242
171
  });
243
172
  const snapshot = useMemo(() => ({
244
173
  axes,
245
- disabledAxes,
246
- presets,
247
174
  activeTheme: themeName,
248
175
  activeAxes: tuple,
249
176
  cssVarPrefix,
@@ -288,7 +215,7 @@ const decorators = [themedDecorator];
288
215
  const globalTypes = {
289
216
  [AXES_GLOBAL_KEY]: {
290
217
  name: "Axes",
291
- description: "Per-axis context selection — the active permutation tuple."
218
+ description: "Per-axis context selection — the active theme name tuple."
292
219
  },
293
220
  [COLOR_FORMAT_GLOBAL_KEY]: {
294
221
  name: "Color format",
@@ -304,35 +231,11 @@ const initialGlobals = {
304
231
  [AXES_GLOBAL_KEY]: buildInitialAxes(),
305
232
  [COLOR_FORMAT_GLOBAL_KEY]: "hex"
306
233
  };
307
- /**
308
- * Module-level channel subscription: writes the active tuple's attributes
309
- * onto `<html>` regardless of whether a story decorator is rendering.
310
- *
311
- * The {@link themedDecorator} already sets these inside story renders, but
312
- * it never runs on MDX docs pages that embed blocks without `<Story />`.
313
- * Without attrs on an ancestor, the per-tuple CSS selectors
314
- * (`[data-mode="Dark"][data-brand="…"]`) don't match and everything falls
315
- * back to the `:root` default tuple — so colors stay defaults even after
316
- * the toolbar switches axes. Subscribing globally fixes MDX docs at the
317
- * cost of one idempotent redundant write per story render.
318
- */
319
234
  function installGlobalAxisApplier() {
320
235
  if (typeof document === "undefined") return;
321
236
  const channel = addons.getChannel();
322
- /**
323
- * Inject the stylesheet and emit the init payload once on module load so
324
- * the manager's toolbar populates and CSS vars are available even when no
325
- * story/decorator ever runs (bare MDX docs pages). Without these, the
326
- * toolbar sits in its disabled "loading…" state and nothing is styled.
327
- */
328
237
  ensureStylesheet(css, cssVarPrefix);
329
238
  broadcastInit();
330
- /**
331
- * If the manager subscribes to INIT_EVENT after our initial broadcast,
332
- * it misses the payload and the toolbar stays in its "loading…" state
333
- * until something else re-fires it. Honor an explicit request event so
334
- * a late-mounting manager can ask for the payload.
335
- */
336
239
  channel.on(INIT_REQUEST_EVENT, broadcastInit);
337
240
  const apply = (globals) => {
338
241
  ensureStylesheet(css, cssVarPrefix);
@@ -341,9 +244,9 @@ function installGlobalAxisApplier() {
341
244
  let lastApplied = "";
342
245
  const onGlobals = (payload) => {
343
246
  if (!payload.globals) return;
344
- const fingerprint = matchThemeName(resolveTuple(payload.globals[AXES_GLOBAL_KEY], void 0));
345
- if (fingerprint === lastApplied) return;
346
- lastApplied = fingerprint;
247
+ const themeKey = composeThemeName(resolveTuple(payload.globals[AXES_GLOBAL_KEY], void 0));
248
+ if (themeKey === lastApplied) return;
249
+ lastApplied = themeKey;
347
250
  apply(payload.globals);
348
251
  };
349
252
  channel.on("globalsUpdated", onGlobals);
@@ -351,13 +254,6 @@ function installGlobalAxisApplier() {
351
254
  channel.on("updateGlobals", onGlobals);
352
255
  }
353
256
  installGlobalAxisApplier();
354
- /**
355
- * Bridge `mousedown` inside the preview iframe to the manager via a
356
- * dedicated channel event. The toolbar popover's outside-click listener
357
- * runs on the manager's document, which can't observe mousedowns inside
358
- * the preview; without this bridge, clicking the canvas leaves the
359
- * popover open. Idempotent: fires at most once per real mousedown.
360
- */
361
257
  function installPreviewMouseDownBridge() {
362
258
  if (typeof document === "undefined") return;
363
259
  const channel = addons.getChannel();
@@ -369,10 +265,10 @@ installPreviewMouseDownBridge();
369
265
  if (import.meta.hot) import.meta.hot.on(HMR_EVENT, (payload) => {
370
266
  ensureStylesheet(payload.css, payload.cssVarPrefix);
371
267
  const channel = addons.getChannel();
372
- channel.emit(INIT_EVENT, pickInitFields(payload));
268
+ channel.emit(INIT_EVENT, toInitPayload(payload));
373
269
  channel.emit(TOKENS_UPDATED_EVENT, payload);
374
270
  });
375
271
  //#endregion
376
272
  export { preview_exports as i, globalTypes as n, initialGlobals as r, decorators as t };
377
273
 
378
- //# sourceMappingURL=preview-EthytSmK.mjs.map
274
+ //# sourceMappingURL=preview-B6Sy1z-D.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-B6Sy1z-D.mjs","names":["virtualDisabledAxes","virtualDefaultTuple","virtualAxes","virtualPresets","defaultTuple","virtualTokenGraph","virtualListing"],"sources":["../src/preview.tsx"],"sourcesContent":["/// <reference types=\"vite/client\" />\nimport { resolveAllAt } from '@unpunnyfuns/swatchbook-core/graph';\nimport type { TokenMap } from '@unpunnyfuns/swatchbook-core';\nimport type { Decorator, Preview } from '@storybook/react-vite';\nimport type { CSSProperties } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { addons } from 'storybook/preview-api';\nimport { dataAttr } from '@unpunnyfuns/swatchbook-core/data-attr';\nimport { ensureStyleElement } from '@unpunnyfuns/swatchbook-core/style-element';\nimport { tupleToName } from '@unpunnyfuns/swatchbook-core/themes';\n// Side-effect import for integrations that opted into `autoInject`\n// (e.g. Tailwind's `@theme` block). When no integration opts in, the\n// virtual module body is empty — still a valid no-op.\nimport 'virtual:swatchbook/integration-side-effects';\nimport {\n axes as virtualAxes,\n css,\n cssVarPrefix,\n defaultTuple as virtualDefaultTuple,\n diagnostics,\n disabledAxes as virtualDisabledAxes,\n listing as virtualListing,\n presets as virtualPresets,\n tokenGraph as virtualTokenGraph,\n} from 'virtual:swatchbook/tokens';\nimport {\n AxesContext,\n COLOR_FORMATS,\n ColorFormatContext,\n SwatchbookContext,\n ThemeContext,\n} from '@unpunnyfuns/swatchbook-blocks';\nimport type { ColorFormat, ProjectSnapshot } from '@unpunnyfuns/swatchbook-blocks';\nimport {\n AXES_GLOBAL_KEY,\n COLOR_FORMAT_GLOBAL_KEY,\n HMR_EVENT,\n INIT_EVENT,\n INIT_REQUEST_EVENT,\n PREVIEW_MOUSEDOWN_EVENT,\n STYLE_ELEMENT_ID,\n TOKENS_UPDATED_EVENT,\n} from '#/constants.ts';\nimport type { InitPayload } from '#/channel-types.ts';\nimport type { StoryParameters, SwatchbookGlobals } from '#/globals.ts';\n\n// Standard visually-hidden style for the theme-flip live region.\n// Keeps the announcement element discoverable by SR but out of visual\n// + pointer flow. The clip-path / `position: absolute` combination is\n// the canonical sr-only pattern.\nconst SR_ONLY_STYLE: CSSProperties = {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0, 0, 0, 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n// The `html, body { ... }` rules that paint the iframe's own chrome\n// (outside any decorator wrapper — Docs mode, autodocs, empty gutters)\n// with the active theme's surface + text vars. Composed alongside the\n// emitted token CSS so both load through the same `<style>` element.\nfunction iframeChromeRules(prefix: string): string {\n const surface = prefix ? `--${prefix}-color-surface-default` : '--color-surface-default';\n const text = prefix ? `--${prefix}-color-text-default` : '--color-text-default';\n return `\nhtml, body {\n background: var(${surface}, Canvas);\n color: var(${text}, CanvasText);\n margin: 0;\n}\n`;\n}\n\n// Inject the per-theme stylesheet plus the iframe-chrome block. Shared\n// with the HMR re-emit path below so a token refresh updates the\n// iframe's chrome rules from the same source.\nfunction ensureStylesheet(cssText: string, prefix: string): void {\n ensureStyleElement(STYLE_ELEMENT_ID, `${cssText}\\n${iframeChromeRules(prefix)}`);\n}\n\n// Apply `cb(axisName, value)` for every pinned (disabled) axis whose\n// default-tuple value is set. `virtualDefaultTuple` carries the\n// post-filter axis defaults; disabled axes don't appear in\n// `virtualAxes` but their pinned context value still lives here, so\n// sampling it yields each pinned axis's active context.\nfunction forEachPinnedAxis(cb: (name: string, value: string) => void): void {\n for (const name of virtualDisabledAxes) {\n const value = virtualDefaultTuple[name];\n if (value !== undefined) cb(name, value);\n }\n}\n\n// Compose a stable theme name from a tuple — `axisValues.join(' · ')`\n// in axis order. Used for the `ThemeContext` value the blocks read and\n// the addon-channel signals downstream consumers subscribe to. Returns\n// empty string when there are no axes (no name to write).\nfunction composeThemeName(tuple: Readonly<Record<string, string>>): string {\n if (virtualAxes.length === 0) return '';\n return tupleToName(virtualAxes, tuple);\n}\n\n// Write one `data-<prefix>-<axis>=<context>` per axis on `<html>`.\n// The smart CSS emitter targets these single-axis selectors (and\n// joint compounds across multiple) — that's the actual scoping\n// surface the cascade resolves through. Prefix follows `cssVarPrefix`\n// so attr namespace and emitted selectors stay in lockstep.\nfunction setRootAxes(tuple: Readonly<Record<string, string>>): void {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n for (const axis of virtualAxes) {\n const attr = dataAttr(cssVarPrefix, axis.name);\n const value = tuple[axis.name];\n if (value === undefined) {\n root.removeAttribute(attr);\n } else {\n root.setAttribute(attr, value);\n }\n }\n forEachPinnedAxis((name, value) => {\n root.setAttribute(dataAttr(cssVarPrefix, name), value);\n });\n}\n\n// Project the INIT_EVENT fields the manager bundle needs out of a wider\n// source object. Both `broadcastInit` (module-level virtual exports) and\n// the HMR re-emit (`payload`-shaped) feed it, so the two compose the same\n// `InitPayload` from the same field set.\nfunction toInitPayload(source: InitPayload): InitPayload {\n return {\n axes: source.axes,\n disabledAxes: source.disabledAxes,\n presets: source.presets,\n diagnostics: source.diagnostics,\n cssVarPrefix: source.cssVarPrefix,\n defaultTuple: source.defaultTuple,\n };\n}\n\n// Emit the full virtual-module payload to the manager over Storybook's\n// channel so the toolbar + panel (which run in the manager bundle and\n// can't import our virtual module) can render from it.\nfunction broadcastInit(): void {\n const channel = addons.getChannel();\n channel.emit(\n INIT_EVENT,\n toInitPayload({\n axes: virtualAxes,\n disabledAxes: virtualDisabledAxes,\n presets: virtualPresets,\n diagnostics,\n cssVarPrefix,\n defaultTuple: virtualDefaultTuple,\n }),\n );\n}\n\n// Axis-default tuple, used as the baseline before overrides.\nfunction defaultTuple(): Record<string, string> {\n const out: Record<string, string> = {};\n for (const axis of virtualAxes) out[axis.name] = axis.default;\n return out;\n}\n\n// Reverse-engineer a tuple from a `Light · Brand A · Normal`-shape\n// theme name. Splits on ` · ` and zips with `virtualAxes` in declared\n// order — matches `composeThemeName`'s production direction so a\n// round-trip is lossless. Returns `undefined` when the segment count\n// doesn't match the axis count.\nfunction tupleForName(name: string): Record<string, string> | undefined {\n if (!name) return undefined;\n const parts = name.split(' · ');\n if (parts.length !== virtualAxes.length) return undefined;\n const out: Record<string, string> = {};\n for (let i = 0; i < virtualAxes.length; i++) {\n const axis = virtualAxes[i] as (typeof virtualAxes)[number];\n const value = parts[i];\n if (value === undefined) return undefined;\n out[axis.name] = value;\n }\n return out;\n}\n\n// Merge a partial tuple onto the axis defaults, dropping keys for axes that\n// don't exist and silently falling back to the default for contexts that\n// aren't listed on the axis.\nfunction normalizeTuple(partial: Readonly<Record<string, string>>): Record<string, string> {\n const out = defaultTuple();\n for (const axis of virtualAxes) {\n const candidate = partial[axis.name];\n if (candidate !== undefined && axis.contexts.includes(candidate)) {\n out[axis.name] = candidate;\n }\n }\n return out;\n}\n\n// Resolve the active tuple from all input channels, in priority order:\n// 1. `parameters.swatchbook.axes` — per-story tuple.\n// 2. `parameters.swatchbook.themeName` — per-story composed theme name.\n// 3. `globals.swatchbookAxes` — toolbar-set tuple.\n// 4. virtual module default.\nfunction resolveTuple(\n axesGlobal: SwatchbookGlobals[typeof AXES_GLOBAL_KEY],\n paramSwatchbook: StoryParameters['swatchbook'],\n): Record<string, string> {\n const paramAxes = paramSwatchbook?.axes;\n if (paramAxes) {\n return normalizeTuple(paramAxes);\n }\n if (paramSwatchbook?.themeName) {\n const hit = tupleForName(paramSwatchbook.themeName);\n if (hit) return normalizeTuple(hit);\n }\n if (axesGlobal && typeof axesGlobal === 'object') {\n return normalizeTuple(axesGlobal as Record<string, string>);\n }\n return defaultTuple();\n}\n\nfunction resolveColorFormat(raw: SwatchbookGlobals[typeof COLOR_FORMAT_GLOBAL_KEY]): ColorFormat {\n if (typeof raw === 'string' && (COLOR_FORMATS as readonly string[]).includes(raw)) {\n return raw as ColorFormat;\n }\n return 'hex';\n}\n\n// Single shared `resolveAt` instance for the lifetime of the preview\n// iframe. `virtualTokenGraph` is a module-level virtual-module export\n// with stable identity, so this closure never needs to rebuild;\n// downstream `ProjectSnapshot` consumers can key memos on the snapshot\n// wrapper without worrying about `resolveAt` churning when Storybook\n// recreates `context.globals`.\nconst previewResolveAt = (tuple: Record<string, string>): TokenMap =>\n resolveAllAt(virtualTokenGraph, tuple);\n\nconst themedDecorator: Decorator = (Story, context) => {\n const globals = context.globals as SwatchbookGlobals;\n const parameters = context.parameters as StoryParameters;\n const axesGlobal = globals[AXES_GLOBAL_KEY];\n const colorFormatGlobal = globals[COLOR_FORMAT_GLOBAL_KEY];\n const paramSwatchbook = parameters.swatchbook;\n const tuple = useMemo(\n () => resolveTuple(axesGlobal, paramSwatchbook),\n [axesGlobal, paramSwatchbook],\n );\n const colorFormat = useMemo(() => resolveColorFormat(colorFormatGlobal), [colorFormatGlobal]);\n const themeName = useMemo(() => composeThemeName(tuple), [tuple]);\n\n useEffect(() => {\n ensureStylesheet(css, cssVarPrefix);\n broadcastInit();\n }, []);\n\n useEffect(() => {\n setRootAxes(tuple);\n }, [tuple]);\n\n // Page-level live region announces theme/axis flips to SR users.\n // Initial mount stays silent (no spurious announcement on every story\n // load); subsequent `themeName` changes schedule a debounced update so\n // rapid axis flips (or per-story tuple overrides while paging through\n // a Storybook docs index) collapse into one announcement.\n const [announcement, setAnnouncement] = useState('');\n const initialThemeRef = useRef(themeName);\n useEffect(() => {\n if (themeName === initialThemeRef.current) return;\n const timer = setTimeout(() => {\n setAnnouncement(themeName ? `Theme: ${themeName}` : '');\n }, 250);\n return () => {\n clearTimeout(timer);\n };\n }, [themeName]);\n\n const wrapperAttrs: Record<string, string> = {};\n for (const axis of virtualAxes) {\n const value = tuple[axis.name];\n if (value !== undefined) wrapperAttrs[dataAttr(cssVarPrefix, axis.name)] = value;\n }\n forEachPinnedAxis((name, value) => {\n wrapperAttrs[dataAttr(cssVarPrefix, name)] = value;\n });\n\n const snapshot = useMemo<ProjectSnapshot>(\n () => ({\n axes: virtualAxes,\n activeTheme: themeName,\n activeAxes: tuple,\n cssVarPrefix,\n diagnostics,\n css,\n listing: virtualListing,\n tokenGraph: virtualTokenGraph,\n defaultTuple: virtualDefaultTuple,\n resolveAt: previewResolveAt,\n }),\n [themeName, tuple],\n );\n\n return (\n <SwatchbookContext.Provider value={snapshot}>\n <ThemeContext.Provider value={themeName}>\n <AxesContext.Provider value={tuple}>\n <ColorFormatContext.Provider value={colorFormat}>\n <div\n {...wrapperAttrs}\n style={{\n padding: '1rem',\n minHeight: '100%',\n }}\n >\n <Story />\n </div>\n <div role=\"status\" aria-live=\"polite\" style={SR_ONLY_STYLE}>\n {announcement}\n </div>\n </ColorFormatContext.Provider>\n </AxesContext.Provider>\n </ThemeContext.Provider>\n </SwatchbookContext.Provider>\n );\n};\n\n/**\n * Named exports consumed by `definePreviewAddon(previewExports)` in the\n * addon's CSF Next factory (`src/index.ts`).\n */\nexport const decorators: NonNullable<Preview['decorators']> = [themedDecorator];\n\nexport const globalTypes: NonNullable<Preview['globalTypes']> = {\n [AXES_GLOBAL_KEY]: {\n name: 'Axes',\n description: 'Per-axis context selection — the active theme name tuple.',\n },\n [COLOR_FORMAT_GLOBAL_KEY]: {\n name: 'Color format',\n description: 'Display format for color tokens in blocks. Emitted CSS is unaffected.',\n },\n};\n\nfunction buildInitialAxes(): Record<string, string> {\n const out: Record<string, string> = {};\n for (const axis of virtualAxes) out[axis.name] = axis.default;\n return out;\n}\n\nexport const initialGlobals: NonNullable<Preview['initialGlobals']> = {\n [AXES_GLOBAL_KEY]: buildInitialAxes(),\n [COLOR_FORMAT_GLOBAL_KEY]: 'hex',\n};\n\n// Module-level channel subscription: writes the active tuple's attributes\n// onto `<html>` regardless of whether a story decorator is rendering.\n//\n// The themedDecorator already sets these inside story renders, but it\n// never runs on MDX docs pages that embed blocks without `<Story />`.\n// Without attrs on an ancestor, the per-tuple CSS selectors\n// (`[data-mode=\"Dark\"][data-brand=\"…\"]`) don't match and everything falls\n// back to the `:root` default tuple — so colors stay defaults even after\n// the toolbar switches axes. Subscribing globally fixes MDX docs at the\n// cost of one idempotent redundant write per story render.\nfunction installGlobalAxisApplier(): void {\n if (typeof document === 'undefined') return;\n const channel = addons.getChannel();\n // Inject the stylesheet and emit the init payload once on module load so\n // the manager's toolbar populates and CSS vars are available even when no\n // story/decorator ever runs (bare MDX docs pages). Without these, the\n // toolbar sits in its disabled \"loading…\" state and nothing is styled.\n ensureStylesheet(css, cssVarPrefix);\n broadcastInit();\n // If the manager subscribes to INIT_EVENT after our initial broadcast,\n // it misses the payload and the toolbar stays in its \"loading…\" state\n // until something else re-fires it. Honor an explicit request event so\n // a late-mounting manager can ask for the payload.\n channel.on(INIT_REQUEST_EVENT, broadcastInit);\n const apply = (globals: SwatchbookGlobals): void => {\n ensureStylesheet(css, cssVarPrefix);\n const tuple = resolveTuple(globals[AXES_GLOBAL_KEY], undefined);\n setRootAxes(tuple);\n };\n // Storybook fires `globalsUpdated`, `setGlobals`, and `updateGlobals`\n // for the same logical change (preview init + every toolbar tick).\n // Subscribing to all three is intentional — `setGlobals` carries the\n // initial URL-persisted globals; `updateGlobals` is the toolbar\n // signal; `globalsUpdated` is the cross-frame echo. Apply the same\n // handler to all three but dedupe via a stringified-tuple guard so\n // downstream `setRootAxes` + `useSyncExternalStore` consumers\n // re-render at most once per real change instead of three times per\n // tick.\n let lastApplied = '';\n const onGlobals = (payload: { globals?: SwatchbookGlobals }): void => {\n if (!payload.globals) return;\n const tuple = resolveTuple(payload.globals[AXES_GLOBAL_KEY], undefined);\n const themeKey = composeThemeName(tuple);\n if (themeKey === lastApplied) return;\n lastApplied = themeKey;\n apply(payload.globals);\n };\n channel.on('globalsUpdated', onGlobals);\n channel.on('setGlobals', onGlobals);\n channel.on('updateGlobals', onGlobals);\n}\n\ninstallGlobalAxisApplier();\n\n// Bridge `mousedown` inside the preview iframe to the manager via a\n// dedicated channel event. The toolbar popover's outside-click listener\n// runs on the manager's document, which can't observe mousedowns inside\n// the preview; without this bridge, clicking the canvas leaves the\n// popover open. Idempotent: fires at most once per real mousedown.\nfunction installPreviewMouseDownBridge(): void {\n if (typeof document === 'undefined') return;\n const channel = addons.getChannel();\n document.addEventListener('mousedown', () => {\n channel.emit(PREVIEW_MOUSEDOWN_EVENT);\n });\n}\n\ninstallPreviewMouseDownBridge();\n\n// Wire the dev-time token-refresh HMR path. The plugin emits `HMR_EVENT`\n// with the fresh virtual-module payload whenever a watched source file\n// changes; we re-inject the stylesheet and forward to the Storybook\n// channel so the toolbar re-renders and blocks can re-subscribe with\n// the new snapshot — no full preview reload, so args / scroll / open\n// overlays survive the refresh. No-ops in production where\n// `import.meta.hot` is undefined.\ninterface HmrSnapshot {\n axes: typeof virtualAxes;\n disabledAxes: typeof virtualDisabledAxes;\n presets: typeof virtualPresets;\n diagnostics: typeof diagnostics;\n css: string;\n cssVarPrefix: string;\n listing: typeof virtualListing;\n tokenGraph: typeof virtualTokenGraph;\n defaultTuple: typeof virtualDefaultTuple;\n}\nif (import.meta.hot) {\n import.meta.hot.on(HMR_EVENT, (payload: HmrSnapshot) => {\n ensureStylesheet(payload.css, payload.cssVarPrefix);\n const channel = addons.getChannel();\n channel.emit(INIT_EVENT, toInitPayload(payload));\n channel.emit(TOKENS_UPDATED_EVENT, payload);\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAM,gBAA+B;CACnC,UAAU;CACV,OAAO;CACP,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,UAAU;CACV,MAAM;CACN,YAAY;CACZ,QAAQ;CACT;AAMD,SAAS,kBAAkB,QAAwB;AAGjD,QAAO;;oBAFS,SAAS,KAAK,OAAO,0BAA0B,0BAIrC;eAHb,SAAS,KAAK,OAAO,uBAAuB,uBAIvC;;;;;AASpB,SAAS,iBAAiB,SAAiB,QAAsB;AAC/D,oBAAmB,kBAAkB,GAAG,QAAQ,IAAI,kBAAkB,OAAO,GAAG;;AAQlF,SAAS,kBAAkB,IAAiD;AAC1E,MAAK,MAAM,QAAQA,cAAqB;EACtC,MAAM,QAAQC,aAAoB;AAClC,MAAI,UAAU,KAAA,EAAW,IAAG,MAAM,MAAM;;;AAQ5C,SAAS,iBAAiB,OAAiD;AACzE,KAAIC,KAAY,WAAW,EAAG,QAAO;AACrC,QAAO,YAAYA,MAAa,MAAM;;AAQxC,SAAS,YAAY,OAA+C;AAClE,KAAI,OAAO,aAAa,YAAa;CACrC,MAAM,OAAO,SAAS;AACtB,MAAK,MAAM,QAAQA,MAAa;EAC9B,MAAM,OAAO,SAAS,cAAc,KAAK,KAAK;EAC9C,MAAM,QAAQ,MAAM,KAAK;AACzB,MAAI,UAAU,KAAA,EACZ,MAAK,gBAAgB,KAAK;MAE1B,MAAK,aAAa,MAAM,MAAM;;AAGlC,oBAAmB,MAAM,UAAU;AACjC,OAAK,aAAa,SAAS,cAAc,KAAK,EAAE,MAAM;GACtD;;AAOJ,SAAS,cAAc,QAAkC;AACvD,QAAO;EACL,MAAM,OAAO;EACb,cAAc,OAAO;EACrB,SAAS,OAAO;EAChB,aAAa,OAAO;EACpB,cAAc,OAAO;EACrB,cAAc,OAAO;EACtB;;AAMH,SAAS,gBAAsB;AACb,QAAO,YAAY,CAC3B,KACN,YACA,cAAc;EACNA;EACQF;EACLG;EACT;EACA;EACcF;EACf,CAAC,CACH;;AAIH,SAASG,iBAAuC;CAC9C,MAAM,MAA8B,EAAE;AACtC,MAAK,MAAM,QAAQF,KAAa,KAAI,KAAK,QAAQ,KAAK;AACtD,QAAO;;AAQT,SAAS,aAAa,MAAkD;AACtE,KAAI,CAAC,KAAM,QAAO,KAAA;CAClB,MAAM,QAAQ,KAAK,MAAM,MAAM;AAC/B,KAAI,MAAM,WAAWA,KAAY,OAAQ,QAAO,KAAA;CAChD,MAAM,MAA8B,EAAE;AACtC,MAAK,IAAI,IAAI,GAAG,IAAIA,KAAY,QAAQ,KAAK;EAC3C,MAAM,OAAOA,KAAY;EACzB,MAAM,QAAQ,MAAM;AACpB,MAAI,UAAU,KAAA,EAAW,QAAO,KAAA;AAChC,MAAI,KAAK,QAAQ;;AAEnB,QAAO;;AAMT,SAAS,eAAe,SAAmE;CACzF,MAAM,MAAME,gBAAc;AAC1B,MAAK,MAAM,QAAQF,MAAa;EAC9B,MAAM,YAAY,QAAQ,KAAK;AAC/B,MAAI,cAAc,KAAA,KAAa,KAAK,SAAS,SAAS,UAAU,CAC9D,KAAI,KAAK,QAAQ;;AAGrB,QAAO;;AAQT,SAAS,aACP,YACA,iBACwB;CACxB,MAAM,YAAY,iBAAiB;AACnC,KAAI,UACF,QAAO,eAAe,UAAU;AAElC,KAAI,iBAAiB,WAAW;EAC9B,MAAM,MAAM,aAAa,gBAAgB,UAAU;AACnD,MAAI,IAAK,QAAO,eAAe,IAAI;;AAErC,KAAI,cAAc,OAAO,eAAe,SACtC,QAAO,eAAe,WAAqC;AAE7D,QAAOE,gBAAc;;AAGvB,SAAS,mBAAmB,KAAqE;AAC/F,KAAI,OAAO,QAAQ,YAAa,cAAoC,SAAS,IAAI,CAC/E,QAAO;AAET,QAAO;;AAST,MAAM,oBAAoB,UACxB,aAAaC,YAAmB,MAAM;AAExC,MAAM,mBAA8B,OAAO,YAAY;CACrD,MAAM,UAAU,QAAQ;CACxB,MAAM,aAAa,QAAQ;CAC3B,MAAM,aAAa,QAAQ;CAC3B,MAAM,oBAAoB,QAAQ;CAClC,MAAM,kBAAkB,WAAW;CACnC,MAAM,QAAQ,cACN,aAAa,YAAY,gBAAgB,EAC/C,CAAC,YAAY,gBAAgB,CAC9B;CACD,MAAM,cAAc,cAAc,mBAAmB,kBAAkB,EAAE,CAAC,kBAAkB,CAAC;CAC7F,MAAM,YAAY,cAAc,iBAAiB,MAAM,EAAE,CAAC,MAAM,CAAC;AAEjE,iBAAgB;AACd,mBAAiB,KAAK,aAAa;AACnC,iBAAe;IACd,EAAE,CAAC;AAEN,iBAAgB;AACd,cAAY,MAAM;IACjB,CAAC,MAAM,CAAC;CAOX,MAAM,CAAC,cAAc,mBAAmB,SAAS,GAAG;CACpD,MAAM,kBAAkB,OAAO,UAAU;AACzC,iBAAgB;AACd,MAAI,cAAc,gBAAgB,QAAS;EAC3C,MAAM,QAAQ,iBAAiB;AAC7B,mBAAgB,YAAY,UAAU,cAAc,GAAG;KACtD,IAAI;AACP,eAAa;AACX,gBAAa,MAAM;;IAEpB,CAAC,UAAU,CAAC;CAEf,MAAM,eAAuC,EAAE;AAC/C,MAAK,MAAM,QAAQH,MAAa;EAC9B,MAAM,QAAQ,MAAM,KAAK;AACzB,MAAI,UAAU,KAAA,EAAW,cAAa,SAAS,cAAc,KAAK,KAAK,IAAI;;AAE7E,oBAAmB,MAAM,UAAU;AACjC,eAAa,SAAS,cAAc,KAAK,IAAI;GAC7C;CAEF,MAAM,WAAW,eACR;EACCA;EACN,aAAa;EACb,YAAY;EACZ;EACA;EACA;EACSI;EACGD;EACEJ;EACd,WAAW;EACZ,GACD,CAAC,WAAW,MAAM,CACnB;AAED,QACE,oBAAC,kBAAkB,UAAnB;EAA4B,OAAO;YACjC,oBAAC,aAAa,UAAd;GAAuB,OAAO;aAC5B,oBAAC,YAAY,UAAb;IAAsB,OAAO;cAC3B,qBAAC,mBAAmB,UAApB;KAA6B,OAAO;eAApC,CACE,oBAAC,OAAD;MACE,GAAI;MACJ,OAAO;OACL,SAAS;OACT,WAAW;OACZ;gBAED,oBAAC,OAAD,EAAS,CAAA;MACL,CAAA,EACN,oBAAC,OAAD;MAAK,MAAK;MAAS,aAAU;MAAS,OAAO;gBAC1C;MACG,CAAA,CACsB;;IACT,CAAA;GACD,CAAA;EACG,CAAA;;;;;;AAQjC,MAAa,aAAiD,CAAC,gBAAgB;AAE/E,MAAa,cAAmD;EAC7D,kBAAkB;EACjB,MAAM;EACN,aAAa;EACd;EACA,0BAA0B;EACzB,MAAM;EACN,aAAa;EACd;CACF;AAED,SAAS,mBAA2C;CAClD,MAAM,MAA8B,EAAE;AACtC,MAAK,MAAM,QAAQC,KAAa,KAAI,KAAK,QAAQ,KAAK;AACtD,QAAO;;AAGT,MAAa,iBAAyD;EACnE,kBAAkB,kBAAkB;EACpC,0BAA0B;CAC5B;AAYD,SAAS,2BAAiC;AACxC,KAAI,OAAO,aAAa,YAAa;CACrC,MAAM,UAAU,OAAO,YAAY;AAKnC,kBAAiB,KAAK,aAAa;AACnC,gBAAe;AAKf,SAAQ,GAAG,oBAAoB,cAAc;CAC7C,MAAM,SAAS,YAAqC;AAClD,mBAAiB,KAAK,aAAa;AAEnC,cADc,aAAa,QAAQ,kBAAkB,KAAA,EAAU,CAC7C;;CAWpB,IAAI,cAAc;CAClB,MAAM,aAAa,YAAmD;AACpE,MAAI,CAAC,QAAQ,QAAS;EAEtB,MAAM,WAAW,iBADH,aAAa,QAAQ,QAAQ,kBAAkB,KAAA,EAAU,CAC/B;AACxC,MAAI,aAAa,YAAa;AAC9B,gBAAc;AACd,QAAM,QAAQ,QAAQ;;AAExB,SAAQ,GAAG,kBAAkB,UAAU;AACvC,SAAQ,GAAG,cAAc,UAAU;AACnC,SAAQ,GAAG,iBAAiB,UAAU;;AAGxC,0BAA0B;AAO1B,SAAS,gCAAsC;AAC7C,KAAI,OAAO,aAAa,YAAa;CACrC,MAAM,UAAU,OAAO,YAAY;AACnC,UAAS,iBAAiB,mBAAmB;AAC3C,UAAQ,KAAK,wBAAwB;GACrC;;AAGJ,+BAA+B;AAoB/B,IAAI,OAAO,KAAK,IACd,QAAO,KAAK,IAAI,GAAG,YAAY,YAAyB;AACtD,kBAAiB,QAAQ,KAAK,QAAQ,aAAa;CACnD,MAAM,UAAU,OAAO,YAAY;AACnC,SAAQ,KAAK,YAAY,cAAc,QAAQ,CAAC;AAChD,SAAQ,KAAK,sBAAsB,QAAQ;EAC3C"}
package/dist/preview.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as globalTypes, r as initialGlobals, t as decorators } from "./preview-EthytSmK.mjs";
1
+ import { n as globalTypes, r as initialGlobals, t as decorators } from "./preview-B6Sy1z-D.mjs";
2
2
  export { decorators, globalTypes, initialGlobals };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unpunnyfuns/swatchbook-addon",
3
- "version": "0.60.8",
3
+ "version": "0.61.0",
4
4
  "description": "Storybook addon for DTCG design tokens — toolbar, panel, and useToken hook.",
5
5
  "license": "MIT",
6
6
  "author": "unpunnyfuns <unpunnyfuns@gmail.com>",
@@ -75,9 +75,9 @@
75
75
  "dependencies": {
76
76
  "jiti": "^2.4.0",
77
77
  "picomatch": "^4.0.4",
78
- "@unpunnyfuns/swatchbook-blocks": "0.60.8",
79
- "@unpunnyfuns/swatchbook-core": "0.60.8",
80
- "@unpunnyfuns/swatchbook-switcher": "0.60.8"
78
+ "@unpunnyfuns/swatchbook-blocks": "0.61.0",
79
+ "@unpunnyfuns/swatchbook-switcher": "0.61.0",
80
+ "@unpunnyfuns/swatchbook-core": "0.61.0"
81
81
  },
82
82
  "peerDependencies": {
83
83
  "@storybook/react-vite": "^10.3.5",
@@ -93,8 +93,8 @@
93
93
  "@types/picomatch": "^4.0.3",
94
94
  "@types/react": "^19.2.14",
95
95
  "@vitejs/plugin-react": "^6.0.1",
96
- "@vitest/browser": "^4.1.4",
97
- "@vitest/browser-playwright": "^4.1.4",
96
+ "@vitest/browser": "^4.1.7",
97
+ "@vitest/browser-playwright": "^4.1.7",
98
98
  "playwright": "^1.59.1",
99
99
  "react": "^19.2.4",
100
100
  "react-dom": "^19.2.4",
@@ -102,7 +102,7 @@
102
102
  "tsdown": "^0.21.9",
103
103
  "typescript": "^6.0.0",
104
104
  "vite": "^8.0.4",
105
- "vitest": "^4.1.4",
105
+ "vitest": "^4.1.7",
106
106
  "@unpunnyfuns/swatchbook-tokens": "0.0.0"
107
107
  },
108
108
  "scripts": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"constants-B31xFInv.mjs","names":[],"sources":["../src/constants.ts"],"sourcesContent":["export const ADDON_ID = 'swatchbook';\nexport const TOOL_ID = `${ADDON_ID}/theme-switcher`;\nexport const PARAM_KEY = 'swatchbook';\n/** Canonical active-permutation tuple: `Record<axisName, contextName>`. Read by toolbar, panel, blocks. */\nexport const AXES_GLOBAL_KEY = 'swatchbookAxes';\n/** Display-only color format for blocks (`hex` | `rgb` | `hsl` | `oklch` | `raw`). Emitted CSS is unaffected. */\nexport const COLOR_FORMAT_GLOBAL_KEY = 'swatchbookColorFormat';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:swatchbook/tokens';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\n/**\n * Aggregate virtual module the addon's preview always imports. Its body\n * is a sequence of side-effect imports — one per integration that\n * declared `virtualModule.autoInject: true`. Integrations contributing\n * global stylesheets (Tailwind's `@theme` block, a rules-heavy CSS\n * file) can opt into this path so consumers never hand-write an\n * `import 'virtual:swatchbook/…'` line themselves; the body is empty\n * when no integration opts in.\n */\nexport const INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID = 'virtual:swatchbook/integration-side-effects';\nexport const RESOLVED_INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID = `\\0${INTEGRATION_SIDE_EFFECTS_VIRTUAL_ID}`;\n\nexport const STYLE_ELEMENT_ID = 'swatchbook-tokens';\n\n/** Channel event: preview → manager, carries theme list + mode. */\nexport const INIT_EVENT = 'swatchbook/init';\n/** Channel event: manager → preview, asks preview to re-emit INIT_EVENT.\n * Covers the race where the manager subscribes after the preview's\n * initial broadcast — without it the toolbar stays in \"loading…\" until\n * the user triggers anything that re-fires INIT_EVENT. */\nexport const INIT_REQUEST_EVENT = 'swatchbook/init-request';\n/** Channel event: preview → manager, fires once per `mousedown` on the\n * preview document. The toolbar popover listens for it so clicks landing\n * inside the preview iframe close the popover — a plain document-level\n * listener on the manager can't see iframe events. */\nexport const PREVIEW_MOUSEDOWN_EVENT = 'swatchbook/preview-mousedown';\n\n/** Channel event: preview → blocks, carries the fresh virtual-module\n * payload after a dev-time token refresh so blocks can re-render in\n * place without a full iframe reload. Fired by the preview in response\n * to the `HMR_EVENT` below. */\nexport const TOKENS_UPDATED_EVENT = 'swatchbook/tokens-updated';\n\n/** Custom Vite HMR event: plugin → preview. Preview forwards it to the\n * Storybook channel as {@link TOKENS_UPDATED_EVENT} so blocks can\n * update their snapshot. Kept distinct from the channel event so the\n * plugin doesn't need a Storybook-channel dependency. */\nexport const HMR_EVENT = 'swatchbook/tokens-updated';\n"],"mappings":";AAAA,MAAa,WAAW;AACxB,MAAa,UAAU,GAAG,SAAS;AACnC,MAAa,YAAY;;AAEzB,MAAa,kBAAkB;;AAE/B,MAAa,0BAA0B;AAEvC,MAAa,oBAAoB;AACjC,MAAa,6BAA6B,KAAK;;;;;;;;;;AAW/C,MAAa,sCAAsC;AACnD,MAAa,+CAA+C,KAAK;AAEjE,MAAa,mBAAmB;;AAGhC,MAAa,aAAa;;;;;AAK1B,MAAa,qBAAqB;;;;;AAKlC,MAAa,0BAA0B;;;;;AAMvC,MAAa,uBAAuB;;;;;AAMpC,MAAa,YAAY"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"preview-EthytSmK.mjs","names":["virtualDisabledAxes","virtualDefaultTuple","virtualAxes","virtualPresets","defaultTuple","virtualTokenGraph","virtualListing"],"sources":["../src/preview.tsx"],"sourcesContent":["/// <reference types=\"vite/client\" />\nimport { resolveAllAt } from '@unpunnyfuns/swatchbook-core/graph';\nimport type { TokenMap } from '@unpunnyfuns/swatchbook-core';\nimport type { Decorator, Preview } from '@storybook/react-vite';\nimport type { CSSProperties } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { addons } from 'storybook/preview-api';\nimport { dataAttr } from '@unpunnyfuns/swatchbook-core/data-attr';\nimport { ensureStyleElement } from '@unpunnyfuns/swatchbook-core/style-element';\nimport { tupleToName } from '@unpunnyfuns/swatchbook-core/themes';\n// Side-effect import for integrations that opted into `autoInject`\n// (e.g. Tailwind's `@theme` block). When no integration opts in, the\n// virtual module body is empty — still a valid no-op.\nimport 'virtual:swatchbook/integration-side-effects';\nimport {\n axes as virtualAxes,\n css,\n cssVarPrefix,\n defaultTuple as virtualDefaultTuple,\n diagnostics,\n disabledAxes as virtualDisabledAxes,\n listing as virtualListing,\n presets as virtualPresets,\n tokenGraph as virtualTokenGraph,\n} from 'virtual:swatchbook/tokens';\nimport {\n AxesContext,\n COLOR_FORMATS,\n ColorFormatContext,\n SwatchbookContext,\n ThemeContext,\n} from '@unpunnyfuns/swatchbook-blocks';\nimport type { ColorFormat, ProjectSnapshot } from '@unpunnyfuns/swatchbook-blocks';\nimport {\n AXES_GLOBAL_KEY,\n COLOR_FORMAT_GLOBAL_KEY,\n HMR_EVENT,\n INIT_EVENT,\n INIT_REQUEST_EVENT,\n PREVIEW_MOUSEDOWN_EVENT,\n STYLE_ELEMENT_ID,\n TOKENS_UPDATED_EVENT,\n} from '#/constants.ts';\nimport type { StoryParameters, SwatchbookGlobals } from '#/globals.ts';\n\n/**\n * Standard visually-hidden style for the theme-flip live region.\n * Keeps the announcement element discoverable by SR but out of visual\n * + pointer flow. The clip-path / `position: absolute` combination is\n * the canonical sr-only pattern.\n */\nconst SR_ONLY_STYLE: CSSProperties = {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0, 0, 0, 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * The `html, body { ... }` rules that paint the iframe's own chrome\n * (outside any decorator wrapper — Docs mode, autodocs, empty gutters)\n * with the active theme's surface + text vars. Composed alongside the\n * emitted token CSS so both load through the same `<style>` element.\n */\nfunction iframeChromeRules(prefix: string): string {\n const surface = prefix ? `--${prefix}-color-surface-default` : '--color-surface-default';\n const text = prefix ? `--${prefix}-color-text-default` : '--color-text-default';\n return `\nhtml, body {\n background: var(${surface}, Canvas);\n color: var(${text}, CanvasText);\n margin: 0;\n}\n`;\n}\n\n/**\n * Inject the per-theme stylesheet plus the iframe-chrome block. Shared\n * with the HMR re-emit path below so a token refresh updates the\n * iframe's chrome rules from the same source.\n */\nfunction ensureStylesheet(cssText: string, prefix: string): void {\n ensureStyleElement(STYLE_ELEMENT_ID, `${cssText}\\n${iframeChromeRules(prefix)}`);\n}\n\n/**\n * Apply `cb(axisName, value)` for every pinned (disabled) axis whose\n * default-tuple value is set. `virtualDefaultTuple` carries the\n * post-filter axis defaults; disabled axes don't appear in\n * `virtualAxes` but their pinned context value still lives here, so\n * sampling it gives the same result the old \"first permutation's\n * input\" lookup did.\n */\nfunction forEachPinnedAxis(cb: (name: string, value: string) => void): void {\n for (const name of virtualDisabledAxes) {\n const value = virtualDefaultTuple[name];\n if (value !== undefined) cb(name, value);\n }\n}\n\n/**\n * Compose a stable theme name from a tuple — `axisValues.join(' · ')`\n * in axis order. Used for the `ThemeContext` value the blocks read and\n * the addon-channel signals downstream consumers subscribe to. Returns\n * empty string when there are no axes (no name to write).\n */\nfunction matchThemeName(tuple: Readonly<Record<string, string>>): string {\n if (virtualAxes.length === 0) return '';\n return tupleToName(virtualAxes, tuple);\n}\n\n/**\n * Write one `data-<prefix>-<axis>=<context>` per axis on `<html>`.\n * The smart CSS emitter targets these single-axis selectors (and\n * joint compounds across multiple) — that's the actual scoping\n * surface the cascade resolves through. Prefix follows `cssVarPrefix`\n * so attr namespace and emitted selectors stay in lockstep.\n */\nfunction setRootAxes(tuple: Readonly<Record<string, string>>): void {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n for (const axis of virtualAxes) {\n const attr = dataAttr(cssVarPrefix, axis.name);\n const value = tuple[axis.name];\n if (value === undefined) {\n root.removeAttribute(attr);\n } else {\n root.setAttribute(attr, value);\n }\n }\n forEachPinnedAxis((name, value) => {\n root.setAttribute(dataAttr(cssVarPrefix, name), value);\n });\n}\n\n/**\n * Subset of an INIT_EVENT-shaped object the manager bundle needs. The\n * fields are the union of what the toolbar and panel read; named so\n * `broadcastInit` (module-level virtual exports) and the HMR re-emit\n * (`payload`-shaped) compose the same payload from the same shape.\n */\ninterface InitFieldsSource {\n axes: typeof virtualAxes;\n disabledAxes: typeof virtualDisabledAxes;\n presets: typeof virtualPresets;\n diagnostics: typeof diagnostics;\n cssVarPrefix: string;\n defaultTuple: typeof virtualDefaultTuple;\n}\n\nfunction pickInitFields(source: InitFieldsSource): InitFieldsSource {\n return {\n axes: source.axes,\n disabledAxes: source.disabledAxes,\n presets: source.presets,\n diagnostics: source.diagnostics,\n cssVarPrefix: source.cssVarPrefix,\n defaultTuple: source.defaultTuple,\n };\n}\n\n/**\n * Emit the full virtual-module payload to the manager over Storybook's\n * channel so the toolbar + panel (which run in the manager bundle and\n * can't import our virtual module) can render from it.\n */\nfunction broadcastInit(): void {\n const channel = addons.getChannel();\n channel.emit(\n INIT_EVENT,\n pickInitFields({\n axes: virtualAxes,\n disabledAxes: virtualDisabledAxes,\n presets: virtualPresets,\n diagnostics,\n cssVarPrefix,\n defaultTuple: virtualDefaultTuple,\n }),\n );\n}\n\n/** Axis-default tuple, used as the baseline before overrides. */\nfunction defaultTuple(): Record<string, string> {\n const out: Record<string, string> = {};\n for (const axis of virtualAxes) out[axis.name] = axis.default;\n return out;\n}\n\n/**\n * Reverse-engineer a tuple from a `Light · Brand A · Normal`-shape\n * theme name. Splits on ` · ` and zips with `virtualAxes` in declared\n * order — matches `matchThemeName`'s production direction so a\n * round-trip is lossless. Returns `undefined` when the segment count\n * doesn't match the axis count.\n */\nfunction tupleForName(name: string): Record<string, string> | undefined {\n if (!name) return undefined;\n const parts = name.split(' · ');\n if (parts.length !== virtualAxes.length) return undefined;\n const out: Record<string, string> = {};\n for (let i = 0; i < virtualAxes.length; i++) {\n const axis = virtualAxes[i] as (typeof virtualAxes)[number];\n const value = parts[i];\n if (value === undefined) return undefined;\n out[axis.name] = value;\n }\n return out;\n}\n\n/**\n * Merge a partial tuple onto the axis defaults, dropping keys for axes that\n * don't exist and silently falling back to the default for contexts that\n * aren't listed on the axis.\n */\nfunction normalizeTuple(partial: Readonly<Record<string, string>>): Record<string, string> {\n const out = defaultTuple();\n for (const axis of virtualAxes) {\n const candidate = partial[axis.name];\n if (candidate !== undefined && axis.contexts.includes(candidate)) {\n out[axis.name] = candidate;\n }\n }\n return out;\n}\n\n/**\n * Resolve the active tuple from all input channels, in priority order:\n * 1. `parameters.swatchbook.axes` — per-story tuple.\n * 2. `parameters.swatchbook.permutation` — per-story composed name.\n * 3. `globals.swatchbookAxes` — toolbar-set tuple.\n * 4. virtual module default.\n */\nfunction resolveTuple(\n axesGlobal: SwatchbookGlobals[typeof AXES_GLOBAL_KEY],\n paramSwatchbook: StoryParameters['swatchbook'],\n): Record<string, string> {\n const paramAxes = paramSwatchbook?.axes;\n if (paramAxes) {\n return normalizeTuple(paramAxes);\n }\n if (paramSwatchbook?.permutation) {\n const hit = tupleForName(paramSwatchbook.permutation);\n if (hit) return normalizeTuple(hit);\n }\n if (axesGlobal && typeof axesGlobal === 'object') {\n return normalizeTuple(axesGlobal as Record<string, string>);\n }\n return defaultTuple();\n}\n\nfunction resolveColorFormat(raw: SwatchbookGlobals[typeof COLOR_FORMAT_GLOBAL_KEY]): ColorFormat {\n if (typeof raw === 'string' && (COLOR_FORMATS as readonly string[]).includes(raw)) {\n return raw as ColorFormat;\n }\n return 'hex';\n}\n\n/**\n * Single shared `resolveAt` instance for the lifetime of the preview\n * iframe. `virtualTokenGraph` is a module-level virtual-module export\n * with stable identity, so this closure never needs to rebuild;\n * downstream `ProjectSnapshot` consumers can key memos on the snapshot\n * wrapper without worrying about `resolveAt` churning when Storybook\n * recreates `context.globals`.\n */\nconst previewResolveAt = (tuple: Record<string, string>): TokenMap =>\n resolveAllAt(virtualTokenGraph, tuple);\n\nconst themedDecorator: Decorator = (Story, context) => {\n const globals = context.globals as SwatchbookGlobals;\n const parameters = context.parameters as StoryParameters;\n const axesGlobal = globals[AXES_GLOBAL_KEY];\n const colorFormatGlobal = globals[COLOR_FORMAT_GLOBAL_KEY];\n const paramSwatchbook = parameters.swatchbook;\n const tuple = useMemo(\n () => resolveTuple(axesGlobal, paramSwatchbook),\n [axesGlobal, paramSwatchbook],\n );\n const colorFormat = useMemo(() => resolveColorFormat(colorFormatGlobal), [colorFormatGlobal]);\n const themeName = useMemo(() => matchThemeName(tuple), [tuple]);\n\n useEffect(() => {\n ensureStylesheet(css, cssVarPrefix);\n broadcastInit();\n }, []);\n\n useEffect(() => {\n setRootAxes(tuple);\n }, [tuple]);\n\n // Page-level live region announces theme/axis flips to SR users.\n // Initial mount stays silent (no spurious announcement on every story\n // load); subsequent `themeName` changes schedule a debounced update so\n // rapid axis flips (or per-story tuple overrides while paging through\n // a Storybook docs index) collapse into one announcement.\n const [announcement, setAnnouncement] = useState('');\n const initialThemeRef = useRef(themeName);\n useEffect(() => {\n if (themeName === initialThemeRef.current) return;\n const timer = setTimeout(() => {\n setAnnouncement(themeName ? `Theme: ${themeName}` : '');\n }, 250);\n return () => {\n clearTimeout(timer);\n };\n }, [themeName]);\n\n const wrapperAttrs: Record<string, string> = {};\n for (const axis of virtualAxes) {\n const value = tuple[axis.name];\n if (value !== undefined) wrapperAttrs[dataAttr(cssVarPrefix, axis.name)] = value;\n }\n forEachPinnedAxis((name, value) => {\n wrapperAttrs[dataAttr(cssVarPrefix, name)] = value;\n });\n\n const snapshot = useMemo<ProjectSnapshot>(\n () => ({\n axes: virtualAxes,\n disabledAxes: virtualDisabledAxes,\n presets: virtualPresets,\n activeTheme: themeName,\n activeAxes: tuple,\n cssVarPrefix,\n diagnostics,\n css,\n listing: virtualListing,\n tokenGraph: virtualTokenGraph,\n defaultTuple: virtualDefaultTuple,\n resolveAt: previewResolveAt,\n }),\n [themeName, tuple],\n );\n\n return (\n <SwatchbookContext.Provider value={snapshot}>\n <ThemeContext.Provider value={themeName}>\n <AxesContext.Provider value={tuple}>\n <ColorFormatContext.Provider value={colorFormat}>\n <div\n {...wrapperAttrs}\n style={{\n padding: '1rem',\n minHeight: '100%',\n }}\n >\n <Story />\n </div>\n <div role=\"status\" aria-live=\"polite\" style={SR_ONLY_STYLE}>\n {announcement}\n </div>\n </ColorFormatContext.Provider>\n </AxesContext.Provider>\n </ThemeContext.Provider>\n </SwatchbookContext.Provider>\n );\n};\n\n/**\n * Named exports consumed by `definePreviewAddon(previewExports)` in the\n * addon's CSF Next factory (`src/index.ts`).\n */\nexport const decorators: NonNullable<Preview['decorators']> = [themedDecorator];\n\nexport const globalTypes: NonNullable<Preview['globalTypes']> = {\n [AXES_GLOBAL_KEY]: {\n name: 'Axes',\n description: 'Per-axis context selection — the active permutation tuple.',\n },\n [COLOR_FORMAT_GLOBAL_KEY]: {\n name: 'Color format',\n description: 'Display format for color tokens in blocks. Emitted CSS is unaffected.',\n },\n};\n\nfunction buildInitialAxes(): Record<string, string> {\n const out: Record<string, string> = {};\n for (const axis of virtualAxes) out[axis.name] = axis.default;\n return out;\n}\n\nexport const initialGlobals: NonNullable<Preview['initialGlobals']> = {\n [AXES_GLOBAL_KEY]: buildInitialAxes(),\n [COLOR_FORMAT_GLOBAL_KEY]: 'hex',\n};\n\n/**\n * Module-level channel subscription: writes the active tuple's attributes\n * onto `<html>` regardless of whether a story decorator is rendering.\n *\n * The {@link themedDecorator} already sets these inside story renders, but\n * it never runs on MDX docs pages that embed blocks without `<Story />`.\n * Without attrs on an ancestor, the per-tuple CSS selectors\n * (`[data-mode=\"Dark\"][data-brand=\"…\"]`) don't match and everything falls\n * back to the `:root` default tuple — so colors stay defaults even after\n * the toolbar switches axes. Subscribing globally fixes MDX docs at the\n * cost of one idempotent redundant write per story render.\n */\nfunction installGlobalAxisApplier(): void {\n if (typeof document === 'undefined') return;\n const channel = addons.getChannel();\n /**\n * Inject the stylesheet and emit the init payload once on module load so\n * the manager's toolbar populates and CSS vars are available even when no\n * story/decorator ever runs (bare MDX docs pages). Without these, the\n * toolbar sits in its disabled \"loading…\" state and nothing is styled.\n */\n ensureStylesheet(css, cssVarPrefix);\n broadcastInit();\n /**\n * If the manager subscribes to INIT_EVENT after our initial broadcast,\n * it misses the payload and the toolbar stays in its \"loading…\" state\n * until something else re-fires it. Honor an explicit request event so\n * a late-mounting manager can ask for the payload.\n */\n channel.on(INIT_REQUEST_EVENT, broadcastInit);\n const apply = (globals: SwatchbookGlobals): void => {\n ensureStylesheet(css, cssVarPrefix);\n const tuple = resolveTuple(globals[AXES_GLOBAL_KEY], undefined);\n setRootAxes(tuple);\n };\n // Storybook fires `globalsUpdated`, `setGlobals`, and `updateGlobals`\n // for the same logical change (preview init + every toolbar tick).\n // Subscribing to all three is intentional — `setGlobals` carries the\n // initial URL-persisted globals; `updateGlobals` is the toolbar\n // signal; `globalsUpdated` is the cross-frame echo. Apply the same\n // handler to all three but dedupe via a stringified-tuple guard so\n // downstream `setRootAxes` + `useSyncExternalStore` consumers\n // re-render at most once per real change instead of three times per\n // tick.\n let lastApplied = '';\n const onGlobals = (payload: { globals?: SwatchbookGlobals }): void => {\n if (!payload.globals) return;\n const tuple = resolveTuple(payload.globals[AXES_GLOBAL_KEY], undefined);\n const fingerprint = matchThemeName(tuple);\n if (fingerprint === lastApplied) return;\n lastApplied = fingerprint;\n apply(payload.globals);\n };\n channel.on('globalsUpdated', onGlobals);\n channel.on('setGlobals', onGlobals);\n channel.on('updateGlobals', onGlobals);\n}\n\ninstallGlobalAxisApplier();\n\n/**\n * Bridge `mousedown` inside the preview iframe to the manager via a\n * dedicated channel event. The toolbar popover's outside-click listener\n * runs on the manager's document, which can't observe mousedowns inside\n * the preview; without this bridge, clicking the canvas leaves the\n * popover open. Idempotent: fires at most once per real mousedown.\n */\nfunction installPreviewMouseDownBridge(): void {\n if (typeof document === 'undefined') return;\n const channel = addons.getChannel();\n document.addEventListener('mousedown', () => {\n channel.emit(PREVIEW_MOUSEDOWN_EVENT);\n });\n}\n\ninstallPreviewMouseDownBridge();\n\n/**\n * Wire the dev-time token-refresh HMR path. The plugin emits `HMR_EVENT`\n * with the fresh virtual-module payload whenever a watched source file\n * changes; we re-inject the stylesheet and forward to the Storybook\n * channel so the toolbar re-renders and blocks can re-subscribe with\n * the new snapshot — no full preview reload, so args / scroll / open\n * overlays survive the refresh. No-ops in production where\n * `import.meta.hot` is undefined.\n */\ninterface HmrSnapshot {\n axes: typeof virtualAxes;\n disabledAxes: typeof virtualDisabledAxes;\n presets: typeof virtualPresets;\n diagnostics: typeof diagnostics;\n css: string;\n cssVarPrefix: string;\n listing: typeof virtualListing;\n tokenGraph: typeof virtualTokenGraph;\n defaultTuple: typeof virtualDefaultTuple;\n}\nif (import.meta.hot) {\n import.meta.hot.on(HMR_EVENT, (payload: HmrSnapshot) => {\n ensureStylesheet(payload.css, payload.cssVarPrefix);\n const channel = addons.getChannel();\n channel.emit(INIT_EVENT, pickInitFields(payload));\n channel.emit(TOKENS_UPDATED_EVENT, payload);\n });\n}\n"],"mappings":"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"}