@unpunnyfuns/swatchbook-addon 0.53.0 → 0.55.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.
@@ -21,15 +21,19 @@ interface TokenInfo {
21
21
  }
22
22
  /**
23
23
  * Read a DTCG token for the currently active theme. Re-reads on theme
24
- * switch via the addon's `PermutationContext`. Returns `{ value, cssVar, type,
25
- * description }`.
24
+ * switch via the addon's `SwatchbookContext`. Returns `{ value, cssVar,
25
+ * type, description }`.
26
26
  *
27
27
  * Typed paths appear automatically once `.swatchbook/tokens.d.ts` is
28
28
  * generated (happens on first storybook start/build). Until then
29
29
  * `TokenPath` is `string`.
30
30
  *
31
31
  * Safe to call in autodocs / MDX renders — uses plain React context, not
32
- * Storybook's preview-only hooks.
32
+ * Storybook's preview-only hooks. Reads from the addon-provided
33
+ * `SwatchbookContext` when present (preferred — uses the lifted
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.
33
37
  */
34
38
  declare function useToken(path: TokenPath): TokenInfo;
35
39
  //#endregion
@@ -1,27 +1,44 @@
1
- import { cssVarPrefix, defaultPermutation, permutationsResolved } from "virtual:swatchbook/tokens";
2
- import { useActivePermutation } from "@unpunnyfuns/swatchbook-blocks";
1
+ import { buildResolveAt } from "@unpunnyfuns/swatchbook-core/resolve-at";
2
+ import { axes, cells, cssVarPrefix, defaultTuple, jointOverrides } from "virtual:swatchbook/tokens";
3
+ import { useActiveAxes, useOptionalSwatchbookData } from "@unpunnyfuns/swatchbook-blocks";
3
4
  //#region src/hooks/use-token.ts
5
+ /**
6
+ * Module-scope `resolveAt` for the no-provider fallback path. Built
7
+ * once from the stable virtual-module exports — mirrors what the
8
+ * preview decorator does for its `previewResolveAt` but lives in this
9
+ * file so the hook can be called outside the addon's preview wrapper
10
+ * (autodocs / MDX renders).
11
+ */
12
+ const fallbackResolveAt = buildResolveAt(axes, cells, new Map(jointOverrides), defaultTuple);
4
13
  function makeCssVar(path, prefix) {
5
14
  const tail = path.replaceAll(".", "-");
6
15
  return prefix ? `var(--${prefix}-${tail})` : `var(--${tail})`;
7
16
  }
8
17
  /**
9
18
  * Read a DTCG token for the currently active theme. Re-reads on theme
10
- * switch via the addon's `PermutationContext`. Returns `{ value, cssVar, type,
11
- * description }`.
19
+ * switch via the addon's `SwatchbookContext`. Returns `{ value, cssVar,
20
+ * type, description }`.
12
21
  *
13
22
  * Typed paths appear automatically once `.swatchbook/tokens.d.ts` is
14
23
  * generated (happens on first storybook start/build). Until then
15
24
  * `TokenPath` is `string`.
16
25
  *
17
26
  * Safe to call in autodocs / MDX renders — uses plain React context, not
18
- * Storybook's preview-only hooks.
27
+ * Storybook's preview-only hooks. Reads from the addon-provided
28
+ * `SwatchbookContext` when present (preferred — uses the lifted
29
+ * `resolveAt` accessor and the live active tuple); falls back to the
30
+ * virtual module's eager `permutationsResolved` lookup keyed by the
31
+ * default permutation name when no provider is mounted.
19
32
  */
20
33
  function useToken(path) {
21
- const token = (permutationsResolved[useActivePermutation() || (defaultPermutation ?? "")] ?? {})[path];
34
+ const snapshot = useOptionalSwatchbookData();
35
+ const contextAxes = useActiveAxes();
36
+ const hasContextAxes = Object.keys(contextAxes).length > 0;
37
+ const prefix = snapshot?.cssVarPrefix ?? cssVarPrefix;
38
+ const token = (snapshot?.resolveAt ?? fallbackResolveAt)(hasContextAxes ? contextAxes : snapshot?.defaultTuple ?? defaultTuple)[path];
22
39
  const info = {
23
40
  value: token?.$value,
24
- cssVar: makeCssVar(path, cssVarPrefix)
41
+ cssVar: makeCssVar(path, prefix)
25
42
  };
26
43
  if (token?.$type) info.type = token.$type;
27
44
  if (token?.$description) info.description = token.$description;
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/hooks/use-token.ts"],"sourcesContent":["import { cssVarPrefix, defaultPermutation, permutationsResolved } from 'virtual:swatchbook/tokens';\nimport { useActivePermutation } from '@unpunnyfuns/swatchbook-blocks';\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\nfunction makeCssVar(path: string, prefix: string): string {\n const tail = path.replaceAll('.', '-');\n return prefix ? `var(--${prefix}-${tail})` : `var(--${tail})`;\n}\n\n/**\n * Read a DTCG token for the currently active theme. Re-reads on theme\n * switch via the addon's `PermutationContext`. Returns `{ value, cssVar, type,\n * 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.\n */\nexport function useToken(path: TokenPath): TokenInfo {\n const contextPermutation = useActivePermutation();\n const permutationName = contextPermutation || (defaultPermutation ?? '');\n const tokens = permutationsResolved[permutationName] ?? {};\n const token = tokens[path];\n const info: TokenInfo = {\n value: token?.$value,\n cssVar: makeCssVar(path, cssVarPrefix),\n };\n if (token?.$type) info.type = token.$type;\n if (token?.$description) info.description = token.$description;\n return info;\n}\n"],"mappings":";;;AA2BA,SAAS,WAAW,MAAc,QAAwB;CACxD,MAAM,OAAO,KAAK,WAAW,KAAK,IAAI;AACtC,QAAO,SAAS,SAAS,OAAO,GAAG,KAAK,KAAK,SAAS,KAAK;;;;;;;;;;;;;;AAe7D,SAAgB,SAAS,MAA4B;CAInD,MAAM,SADS,qBAFY,sBAAsB,KACF,sBAAsB,QACb,EAAE,EACrC;CACrB,MAAM,OAAkB;EACtB,OAAO,OAAO;EACd,QAAQ,WAAW,MAAM,aAAa;EACvC;AACD,KAAI,OAAO,MAAO,MAAK,OAAO,MAAM;AACpC,KAAI,OAAO,aAAc,MAAK,cAAc,MAAM;AAClD,QAAO"}
1
+ {"version":3,"file":"index.mjs","names":["virtualAxes","virtualCells","virtualJointOverrides","virtualDefaultTuple","virtualCssVarPrefix"],"sources":["../../src/hooks/use-token.ts"],"sourcesContent":["import {\n axes as virtualAxes,\n cells as virtualCells,\n cssVarPrefix as virtualCssVarPrefix,\n defaultTuple as virtualDefaultTuple,\n jointOverrides as virtualJointOverrides,\n} from 'virtual:swatchbook/tokens';\nimport { buildResolveAt } from '@unpunnyfuns/swatchbook-core/resolve-at';\nimport type {\n Axis as CoreAxis,\n Cells as CoreCells,\n JointOverride,\n JointOverrides,\n} from '@unpunnyfuns/swatchbook-core';\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 = buildResolveAt(\n virtualAxes as readonly CoreAxis[],\n virtualCells as CoreCells,\n new Map(virtualJointOverrides as readonly (readonly [string, JointOverride])[]) as JointOverrides,\n virtualDefaultTuple,\n);\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\nfunction makeCssVar(path: string, prefix: string): string {\n const tail = path.replaceAll('.', '-');\n return prefix ? `var(--${prefix}-${tail})` : `var(--${tail})`;\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":";;;;;;;;;;;AAuBA,MAAM,oBAAoB,eACxBA,MACAC,OACA,IAAI,IAAIC,eAAuE,EAC/EC,aACD;AA0BD,SAAS,WAAW,MAAc,QAAwB;CACxD,MAAM,OAAO,KAAK,WAAW,KAAK,IAAI;AACtC,QAAO,SAAS,SAAS,OAAO,GAAG,KAAK,KAAK,SAAS,KAAK;;;;;;;;;;;;;;;;;;AAmB7D,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,gBAAgBD,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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as preview_exports } from "./preview-ZVsSsSIf.mjs";
1
+ import { i as preview_exports } from "./preview-DvtUsEMq.mjs";
2
2
  import { c as PARAM_KEY, h as VIRTUAL_MODULE_ID, n as AXES_GLOBAL_KEY, t as ADDON_ID } from "./constants-B31xFInv.mjs";
3
3
  import { definePreviewAddon } from "storybook/internal/csf";
4
4
  export * from "@unpunnyfuns/swatchbook-blocks";
package/dist/manager.mjs CHANGED
@@ -53,7 +53,6 @@ function ColorFormatSelector({ active, onSelect }) {
53
53
  const h = React.createElement;
54
54
  const EMPTY_AXES = [];
55
55
  const EMPTY_PRESETS = [];
56
- const EMPTY_PERMUTATIONS = [];
57
56
  /**
58
57
  * Root toolbar glyph — a split-circle ("yinyang") mark: a faint filled
59
58
  * disc for the full-swatch silhouette, with a darker half-and-inset-disc
@@ -118,7 +117,6 @@ function AxesToolbar() {
118
117
  }, []);
119
118
  const axes = payload?.axes ?? EMPTY_AXES;
120
119
  const presets = payload?.presets ?? EMPTY_PRESETS;
121
- const permutations = payload?.permutations ?? EMPTY_PERMUTATIONS;
122
120
  const defaults = useMemo(() => defaultTupleFor(axes), [axes]);
123
121
  const [lastApplied, setLastApplied] = useState(null);
124
122
  const globalTuple = globals[AXES_GLOBAL_KEY];
@@ -238,7 +236,6 @@ function AxesToolbar() {
238
236
  const tooltipBody = h(ThemeSwitcher, {
239
237
  axes,
240
238
  presets,
241
- permutations,
242
239
  activeTuple,
243
240
  defaults,
244
241
  lastApplied,
@@ -1 +1 @@
1
- {"version":3,"file":"manager.mjs","names":["h"],"sources":["../src/ColorFormatSelector.tsx","../src/manager.tsx"],"sourcesContent":["import React, { type ReactElement } from 'react';\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\nexport type 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('div', { className: 'sb-switcher__section-label' }, 'Color format'),\n h(\n 'div',\n { className: 'sb-switcher__section-body' },\n ...COLOR_FORMAT_OPTIONS.map((opt) =>\n h(\n 'button',\n {\n key: `color-format/${opt.id}`,\n type: 'button',\n onClick: () => onSelect(opt.id),\n onMouseDown: (event: React.MouseEvent) => event.preventDefault(),\n className:\n opt.id === active\n ? 'sb-switcher__pill sb-switcher__pill--active'\n : 'sb-switcher__pill',\n },\n opt.label,\n ),\n ),\n ),\n );\n}\n","import { ThemeSwitcher } from '@unpunnyfuns/swatchbook-switcher';\nimport { type ColorFormat, ColorFormatSelector } from '#/ColorFormatSelector.tsx';\nimport React, { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from 'react';\nimport { IconButton, WithTooltipPure } from 'storybook/internal/components';\nimport { addons, types, useGlobals, useStorybookApi } from 'storybook/manager-api';\nimport {\n type InitPayload,\n type VirtualAxis as AxisEntry,\n type VirtualPermutation as PermutationEntry,\n type 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[] = [];\nconst EMPTY_PERMUTATIONS: readonly PermutationEntry[] = [];\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\n/**\n * Compose a preset's sanitized partial tuple with the axis defaults, so\n * applying a preset that only names some axes leaves the omitted ones at\n * their defaults (not blank). Mirrors the preview decorator's own fallback\n * logic so what the toolbar sends out is what the decorator honors.\n */\nfunction presetTuple(\n preset: PresetEntry,\n axes: readonly AxisEntry[],\n defaults: Readonly<Record<string, string>>,\n): Record<string, string> {\n const out: Record<string, string> = { ...defaults };\n for (const axis of axes) {\n const candidate = preset.axes[axis.name];\n if (candidate !== undefined && axis.contexts.includes(candidate)) {\n out[axis.name] = candidate;\n }\n }\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 permutations = payload?.permutations ?? EMPTY_PERMUTATIONS;\n const defaults = useMemo(() => defaultTupleFor(axes), [axes]);\n const [lastApplied, setLastApplied] = useState<string | null>(null);\n const globalTuple = globals[AXES_GLOBAL_KEY] as Record<string, string> | undefined;\n const activeColorFormat = ((globals[COLOR_FORMAT_GLOBAL_KEY] as string | undefined) ??\n 'hex') as ColorFormat;\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 * `WithTooltipPure`'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 IconButton,\n { key: TOOL_ID, title: 'Swatchbook theme (loading…)', disabled: true },\n h(SwatchbookIcon),\n );\n }\n\n const summary = axes.map((a) => activeTuple[a.name] ?? a.default).join(' · ');\n const title = `Swatchbook · ${summary}`;\n\n const button = h(\n IconButton,\n {\n key: TOOL_ID,\n title,\n active: 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 `WithTooltipPure` 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 'aria-haspopup': 'dialog' as const,\n 'aria-expanded': open,\n },\n h(SwatchbookIcon),\n );\n\n const tooltipBody = h(ThemeSwitcher, {\n axes,\n presets,\n permutations,\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(WithTooltipPure, {\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":";;;;;;AAoBA,MAAM,uBAAsE;CAC1E;EAAE,IAAI;EAAO,OAAO;EAAO;CAC3B;EAAE,IAAI;EAAO,OAAO;EAAO;CAC3B;EAAE,IAAI;EAAO,OAAO;EAAO;CAC3B;EAAE,IAAI;EAAS,OAAO;EAAS;CAC/B;EAAE,IAAI;EAAO,OAAO;EAAc;CACnC;AAED,MAAMA,MAAI,MAAM;AAOhB,SAAgB,oBAAoB,EAAE,QAAQ,YAAoD;AAChG,QAAOA,IACL,OACA,MACAA,IAAE,OAAO,EAAE,WAAW,8BAA8B,EAAE,eAAe,EACrEA,IACE,OACA,EAAE,WAAW,6BAA6B,EAC1C,GAAG,qBAAqB,KAAK,QAC3BA,IACE,UACA;EACE,KAAK,gBAAgB,IAAI;EACzB,MAAM;EACN,eAAe,SAAS,IAAI,GAAG;EAC/B,cAAc,UAA4B,MAAM,gBAAgB;EAChE,WACE,IAAI,OAAO,SACP,gDACA;EACP,EACD,IAAI,MACL,CACF,CACF,CACF;;;;;;;;;;;;;;;;AC3BH,MAAM,IAAI,MAAM;AAEhB,MAAM,aAAmC,EAAE;AAC3C,MAAM,gBAAwC,EAAE;AAChD,MAAM,qBAAkD,EAAE;;;;;;AAO1D,SAAS,iBAA+B;AACtC,QAAO,EACL,OACA;EAAE,OAAO;EAAI,QAAQ;EAAI,SAAS;EAAa,eAAe;EAAM,EACpE,EAAE,UAAU;EAAE,IAAI;EAAG,IAAI;EAAG,GAAG;EAAG,MAAM;EAAgB,SAAS;EAAM,CAAC,EACxE,EAAE,QAAQ;EACR,GAAG;EACH,MAAM;EACP,CAAC,CACH;;AAGH,SAAS,gBAAgB,MAAoD;CAC3E,MAAM,MAA8B,EAAE;AACtC,MAAK,MAAM,QAAQ,KAAM,KAAI,KAAK,QAAQ,KAAK;AAC/C,QAAO;;;;;;;;AAST,SAAS,YACP,QACA,MACA,UACwB;CACxB,MAAM,MAA8B,EAAE,GAAG,UAAU;AACnD,MAAK,MAAM,QAAQ,MAAM;EACvB,MAAM,YAAY,OAAO,KAAK,KAAK;AACnC,MAAI,cAAc,KAAA,KAAa,KAAK,SAAS,SAAS,UAAU,CAC9D,KAAI,KAAK,QAAQ;;AAGrB,QAAO;;AAGT,SAAS,cAA4B;CACnC,MAAM,CAAC,SAAS,iBAAiB,YAAY;CAC7C,MAAM,MAAM,iBAAiB;CAC7B,MAAM,CAAC,SAAS,cAAc,SAA6B,KAAK;CAChE,MAAM,CAAC,MAAM,WAAW,SAAS,MAAM;CACvC,MAAM,UAAU,OAA8B,KAAK;AAEnD,iBAAgB;EACd,MAAM,UAAU,OAAO,YAAY;EACnC,MAAM,UAAU,SAA4B,WAAW,KAAK;AAC5D,UAAQ,GAAG,YAAY,OAAO;;;;;;;AAO9B,UAAQ,KAAK,mBAAmB;AAChC,eAAa;AACX,WAAQ,IAAI,YAAY,OAAO;;IAEhC,EAAE,CAAC;CAEN,MAAM,OAAO,SAAS,QAAQ;CAC9B,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,eAAe,SAAS,gBAAgB;CAC9C,MAAM,WAAW,cAAc,gBAAgB,KAAK,EAAE,CAAC,KAAK,CAAC;CAC7D,MAAM,CAAC,aAAa,kBAAkB,SAAwB,KAAK;CACnE,MAAM,cAAc,QAAQ;CAC5B,MAAM,oBAAsB,QAAA,4BAC1B;CAEF,MAAM,cAAc,cAAsC;EACxD,MAAM,MAA8B,EAAE,GAAG,UAAU;AACnD,MAAI,YACF,MAAK,MAAM,QAAQ,MAAM;GACvB,MAAM,YAAY,YAAY,KAAK;AACnC,OAAI,cAAc,KAAA,KAAa,KAAK,SAAS,SAAS,UAAU,CAC9D,KAAI,KAAK,QAAQ;;AAIvB,SAAO;IACN;EAAC;EAAM;EAAU;EAAY,CAAC;CAEjC,MAAM,UAAU,aACb,UAAkB,SAAuB;EACxC,MAAM,QAAgC;GAAE,GAAG;IAAc,WAAW;GAAM;AAC1E,gBAAc,GAAG,kBAAkB,OAAO,CAAC;IAE7C,CAAC,aAAa,cAAc,CAC7B;CAED,MAAM,cAAc,aACjB,WAA8B;EAC7B,MAAM,QAAQ,YAAY,QAAQ,MAAM,SAAS;AACjD,gBAAc,GAAG,kBAAkB,OAAO,CAAC;AAC3C,iBAAe,OAAO,KAAK;IAE7B;EAAC;EAAM;EAAU;EAAc,CAChC;AAED,iBAAgB;AACd,MAAI,QAAQ,WAAW,EAAG;AAI1B,MAAI,iBAAiB,UAAU;GAC7B,OAAO,6BAA6B,QAAQ,OAAO;GACnD,iBAAiB;IAAC;IAAO;IAAS;IAAI;GACtC,YAAY;GACZ,YAAY;GACZ,cAAc;IAIZ,MAAM,OAAO,UAHM,cACf,QAAQ,WAAW,WAAW,OAAO,SAAS,YAAY,GAC1D,MAC+B,KAAK,QAAQ;AAChD,QAAI,KAAM,aAAY,KAAK;;GAE9B,CAAC;IACD;EAAC;EAAK;EAAS;EAAa;EAAY,CAAC;CAE5C,MAAM,gBAAgB,aAAa,UAAqD;AACtF,MAAI,MAAM,QAAQ,UAAU;AAC1B,SAAM,iBAAiB;AACvB,WAAQ,MAAM;;IAEf,EAAE,CAAC;;;;;;AAON,iBAAgB;AACd,MAAI,CAAC,KAAM;EACX,MAAM,YAAY,MAA2B;AAC3C,OAAI,EAAE,QAAQ,SAAU,SAAQ,MAAM;;AAExC,WAAS,iBAAiB,WAAW,SAAS;AAC9C,eAAa,SAAS,oBAAoB,WAAW,SAAS;IAC7D,CAAC,KAAK,CAAC;;;;;;;AAQV,iBAAgB;AACd,MAAI,CAAC,KAAM;EACX,MAAM,kBAAkB,MAAwB;GAC9C,MAAM,SAAS,EAAE;AACjB,OAAI,EAAE,kBAAkB,SAAU;AAClC,OAAI,QAAQ,SAAS,SAAS,OAAO,CAAE;AACvC,OAAI,OAAO,QAAQ,wCAAsC,CAAE;AAC3D,WAAQ,MAAM;;;;;;;;EAQhB,MAAM,UAAU,OAAO,YAAY;EACnC,MAAM,2BAAiC,QAAQ,MAAM;AACrD,WAAS,iBAAiB,aAAa,eAAe;AACtD,UAAQ,GAAG,yBAAyB,mBAAmB;AACvD,eAAa;AACX,YAAS,oBAAoB,aAAa,eAAe;AACzD,WAAQ,IAAI,yBAAyB,mBAAmB;;IAEzD,CAAC,KAAK,CAAC;AAEV,KAAI,KAAK,WAAW,EAClB,QAAO,EACL,YACA;EAAE,KAAK;EAAS,OAAO;EAA+B,UAAU;EAAM,EACtE,EAAE,eAAe,CAClB;CAMH,MAAM,SAAS,EACb,YACA;EACE,KAAK;EACL,OANU,gBADE,KAAK,KAAK,MAAM,YAAY,EAAE,SAAS,EAAE,QAAQ,CAAC,KAAK,MAAM;EAQzE,QAAQ;EACR,eAAe,SAAS,SAAS,CAAC,KAAK;EAMvC,iBAAiB;EACjB,iBAAiB;EAClB,EACD,EAAE,eAAe,CAClB;CAED,MAAM,cAAc,EAAE,eAAe;EACnC;EACA;EACA;EACA;EACA;EACA;EACA,cAAc;EACd,eAAe;EACf,WAAW;EAOX,QAAQ,EAAE,qBAAqB;GAC7B,QAAQ;GACR,WAAW,SAAsB,cAAc,GAAG,0BAA0B,MAAM,CAAC;GACpF,CAAC;EACH,CAAC;AAEF,QAAO,EACL,QACA;EAAE,KAAK;EAAS,OAAO;GAAE,SAAS;GAAe,YAAY;GAAU;EAAE,EACzE,EAAE,iBAAiB;EACjB,WAAW;EACX,SAAS;EACT,SAAS;EACT,kBAAkB,SAAkB,QAAQ,KAAK;EACjD,qBAAqB;EACrB,SAAS;EACT,UAAU;EACX,CAAC,CACH;;AAGH,OAAO,SAAS,gBAAgB;AAC9B,QAAO,IAAI,SAAS;EAClB,MAAM,MAAM;EACZ,OAAO;EACP,QAAQ,EAAE,UAAU,YAAY,CAAC,UAAU,aAAa,WAAW,aAAa;EAChF,cAAc,EAAE,YAAY;EAC7B,CAAC;EACF"}
1
+ {"version":3,"file":"manager.mjs","names":["h"],"sources":["../src/ColorFormatSelector.tsx","../src/manager.tsx"],"sourcesContent":["import React, { type ReactElement } from 'react';\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\nexport type 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('div', { className: 'sb-switcher__section-label' }, 'Color format'),\n h(\n 'div',\n { className: 'sb-switcher__section-body' },\n ...COLOR_FORMAT_OPTIONS.map((opt) =>\n h(\n 'button',\n {\n key: `color-format/${opt.id}`,\n type: 'button',\n onClick: () => onSelect(opt.id),\n onMouseDown: (event: React.MouseEvent) => event.preventDefault(),\n className:\n opt.id === active\n ? 'sb-switcher__pill sb-switcher__pill--active'\n : 'sb-switcher__pill',\n },\n opt.label,\n ),\n ),\n ),\n );\n}\n","import { ThemeSwitcher } from '@unpunnyfuns/swatchbook-switcher';\nimport { type ColorFormat, ColorFormatSelector } from '#/ColorFormatSelector.tsx';\nimport React, { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from 'react';\nimport { IconButton, WithTooltipPure } from 'storybook/internal/components';\nimport { addons, types, useGlobals, useStorybookApi } from 'storybook/manager-api';\nimport {\n type InitPayload,\n type VirtualAxis as AxisEntry,\n type 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\n/**\n * Compose a preset's sanitized partial tuple with the axis defaults, so\n * applying a preset that only names some axes leaves the omitted ones at\n * their defaults (not blank). Mirrors the preview decorator's own fallback\n * logic so what the toolbar sends out is what the decorator honors.\n */\nfunction presetTuple(\n preset: PresetEntry,\n axes: readonly AxisEntry[],\n defaults: Readonly<Record<string, string>>,\n): Record<string, string> {\n const out: Record<string, string> = { ...defaults };\n for (const axis of axes) {\n const candidate = preset.axes[axis.name];\n if (candidate !== undefined && axis.contexts.includes(candidate)) {\n out[axis.name] = candidate;\n }\n }\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 globalTuple = globals[AXES_GLOBAL_KEY] as Record<string, string> | undefined;\n const activeColorFormat = ((globals[COLOR_FORMAT_GLOBAL_KEY] as string | undefined) ??\n 'hex') as ColorFormat;\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 * `WithTooltipPure`'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 IconButton,\n { key: TOOL_ID, title: 'Swatchbook theme (loading…)', disabled: true },\n h(SwatchbookIcon),\n );\n }\n\n const summary = axes.map((a) => activeTuple[a.name] ?? a.default).join(' · ');\n const title = `Swatchbook · ${summary}`;\n\n const button = h(\n IconButton,\n {\n key: TOOL_ID,\n title,\n active: 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 `WithTooltipPure` 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 'aria-haspopup': 'dialog' 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(WithTooltipPure, {\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.mjs CHANGED
@@ -2,7 +2,7 @@ import { d as RESOLVED_VIRTUAL_MODULE_ID, i as HMR_EVENT, u as RESOLVED_INTEGRAT
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, resolve } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
- import { loadProject, projectCss } from "@unpunnyfuns/swatchbook-core";
5
+ import { emitAxisProjectedCss, loadProject } from "@unpunnyfuns/swatchbook-core";
6
6
  import { createJiti } from "jiti";
7
7
  import { watch } from "node:fs";
8
8
  import "picomatch";
@@ -22,7 +22,7 @@ function swatchbookTokensPlugin({ config, cwd, integrations = [] }) {
22
22
  let css = "";
23
23
  async function refresh() {
24
24
  project = await loadProject(config, cwd);
25
- css = projectCss(project);
25
+ css = emitAxisProjectedCss(project);
26
26
  }
27
27
  /** Map of resolvedId → integration, indexed once. */
28
28
  const integrationById = /* @__PURE__ */ new Map();
@@ -55,18 +55,21 @@ function swatchbookTokensPlugin({ config, cwd, integrations = [] }) {
55
55
  }
56
56
  if (id !== RESOLVED_VIRTUAL_MODULE_ID) return null;
57
57
  if (!project) return "export default null;";
58
+ const jointOverridesArr = [...project.jointOverrides.entries()];
59
+ const varianceByPathObj = Object.fromEntries(project.varianceByPath.entries());
58
60
  return [
59
61
  `/* swatchbook virtual module — generated */`,
60
62
  `export const axes = ${JSON.stringify(project.axes)};`,
61
63
  `export const presets = ${JSON.stringify(project.presets)};`,
62
64
  `export const disabledAxes = ${JSON.stringify(project.disabledAxes)};`,
63
- `export const permutations = ${JSON.stringify(project.permutations)};`,
64
- `export const defaultPermutation = ${JSON.stringify(project.permutations[0]?.name ?? null)};`,
65
- `export const permutationsResolved = ${JSON.stringify(project.permutationsResolved)};`,
66
65
  `export const diagnostics = ${JSON.stringify(project.diagnostics)};`,
67
66
  `export const css = ${JSON.stringify(css)};`,
68
67
  `export const cssVarPrefix = ${JSON.stringify(config.cssVarPrefix ?? "")};`,
69
- `export const listing = ${JSON.stringify(slimListing(project.listing))};`
68
+ `export const listing = ${JSON.stringify(slimListing(project.listing))};`,
69
+ `export const cells = ${JSON.stringify(project.cells)};`,
70
+ `export const jointOverrides = ${JSON.stringify(jointOverridesArr)};`,
71
+ `export const varianceByPath = ${JSON.stringify(varianceByPathObj)};`,
72
+ `export const defaultTuple = ${JSON.stringify(project.defaultTuple)};`
70
73
  ].join("\n");
71
74
  },
72
75
  async configureServer(server) {
@@ -85,7 +88,7 @@ function swatchbookTokensPlugin({ config, cwd, integrations = [] }) {
85
88
  (async () => {
86
89
  await refresh();
87
90
  if (!project) return;
88
- const tokenCount = Object.keys(project.permutationsResolved[project.permutations[0]?.name ?? ""] ?? {}).length;
91
+ const tokenCount = project.varianceByPath.size;
89
92
  const diagCount = project.diagnostics.length;
90
93
  server.config.logger.info(`\x1b[36m[swatchbook]\x1b[0m tokens reloaded — ${tokenCount} tokens, ${diagCount} diagnostic${diagCount === 1 ? "" : "s"}`, {
91
94
  clear: false,
@@ -112,13 +115,14 @@ function swatchbookTokensPlugin({ config, cwd, integrations = [] }) {
112
115
  axes: project.axes,
113
116
  disabledAxes: project.disabledAxes,
114
117
  presets: project.presets,
115
- permutations: project.permutations,
116
- defaultPermutation: project.permutations[0]?.name ?? null,
117
- permutationsResolved: project.permutationsResolved,
118
118
  diagnostics: project.diagnostics,
119
119
  css,
120
120
  cssVarPrefix: config.cssVarPrefix ?? "",
121
- listing: slimListing(project.listing)
121
+ listing: slimListing(project.listing),
122
+ cells: project.cells,
123
+ jointOverrides: [...project.jointOverrides.entries()],
124
+ varianceByPath: Object.fromEntries(project.varianceByPath.entries()),
125
+ defaultTuple: project.defaultTuple
122
126
  }
123
127
  });
124
128
  })();
@@ -229,7 +233,8 @@ async function writeTokenCodegen(config, cwd, options) {
229
233
  /** @internal Exported for tests; not part of the public API. */
230
234
  function renderTokenTypes(project) {
231
235
  const paths = /* @__PURE__ */ new Set();
232
- for (const theme of project.permutations) {
236
+ if (project.varianceByPath && project.varianceByPath.size > 0) for (const p of project.varianceByPath.keys()) paths.add(p);
237
+ else for (const theme of project.permutations) {
233
238
  const tokens = project.permutationsResolved[theme.name];
234
239
  if (!tokens) continue;
235
240
  for (const path of Object.keys(tokens)) paths.add(path);
@@ -1 +1 @@
1
- {"version":3,"file":"preset.mjs","names":["fsWatch"],"sources":["../src/virtual/plugin.ts","../src/preset.ts"],"sourcesContent":["import type {\n Config,\n ListedToken,\n Project,\n SwatchbookIntegration,\n TokenListingByPath,\n} from '@unpunnyfuns/swatchbook-core';\nimport { loadProject, projectCss } from '@unpunnyfuns/swatchbook-core';\nimport { type FSWatcher, watch as fsWatch } 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\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}: SwatchbookPluginOptions): Plugin {\n let project: Project | undefined;\n let css = '';\n\n async function refresh(): Promise<void> {\n project = await loadProject(config, cwd);\n css = projectCss(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 async buildStart() {\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. Values are JSON-stringified for stability.\n return [\n `/* swatchbook virtual module — generated */`,\n `export const axes = ${JSON.stringify(project.axes)};`,\n `export const presets = ${JSON.stringify(project.presets)};`,\n `export const disabledAxes = ${JSON.stringify(project.disabledAxes)};`,\n `export const permutations = ${JSON.stringify(project.permutations)};`,\n `export const defaultPermutation = ${JSON.stringify(project.permutations[0]?.name ?? null)};`,\n `export const permutationsResolved = ${JSON.stringify(project.permutationsResolved)};`,\n `export const diagnostics = ${JSON.stringify(project.diagnostics)};`,\n `export const css = ${JSON.stringify(css)};`,\n `export const cssVarPrefix = ${JSON.stringify(config.cssVarPrefix ?? '')};`,\n `export const listing = ${JSON.stringify(slimListing(project.listing))};`,\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 = Object.keys(\n project.permutationsResolved[project.permutations[0]?.name ?? ''] ?? {},\n ).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: {\n axes: project.axes,\n disabledAxes: project.disabledAxes,\n presets: project.presets,\n permutations: project.permutations,\n defaultPermutation: project.permutations[0]?.name ?? null,\n permutationsResolved: project.permutationsResolved,\n diagnostics: project.diagnostics,\n css,\n cssVarPrefix: config.cssVarPrefix ?? '',\n listing: slimListing(project.listing),\n },\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\ntype SlimListedToken = Pick<\n ListedToken['$extensions']['app.terrazzo.listing'],\n 'names' | 'previewValue' | 'source'\n>;\n\n/**\n * Reduce the full Token Listing surface down to the fields blocks read.\n * Drops `originalValue` (large, not needed for display), `$value`, `$type`,\n * `mode`, `subtype` for now — the blocks don't consume them yet. Keeps the\n * virtual module payload lean, especially for large projects where each\n * token's raw listing entry can weigh a few KB.\n */\nfunction slimListing(listing: TokenListingByPath): Record<string, SlimListedToken> {\n const out: Record<string, SlimListedToken> = {};\n for (const [path, entry] of Object.entries(listing)) {\n const ext = entry.$extensions['app.terrazzo.listing'];\n const slim: SlimListedToken = { names: ext.names };\n if (ext.previewValue !== undefined) slim.previewValue = ext.previewValue;\n if (ext.source !== undefined) slim.source = ext.source;\n out[path] = slim;\n }\n return out;\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport type { Config, Project } from '@unpunnyfuns/swatchbook-core';\nimport { loadProject } from '@unpunnyfuns/swatchbook-core';\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 // Codegen runs once at Vite startup. The virtual module plugin still\n // owns the live reload path via its HMR watcher; this file just gives\n // TS autocomplete for `useToken('…')` in consumer stories.\n await writeTokenCodegen(config, cwd, options);\n\n const plugins = Array.isArray(viteConfig.plugins) ? [...viteConfig.plugins] : [];\n plugins.push(\n swatchbookTokensPlugin({\n config,\n cwd,\n ...(options.integrations !== undefined && { integrations: options.integrations }),\n }),\n );\n\n return { ...viteConfig, plugins };\n}\n\n/** Storybook appends this module into the manager bundle so our toolbar tool registers. */\nexport function managerEntries(entry: string[] = []): string[] {\n const managerUrl = import.meta.resolve('@unpunnyfuns/swatchbook-addon/manager');\n return [...entry, fileURLToPath(managerUrl)];\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(\n config: Config,\n cwd: string,\n options: PresetOptions,\n): Promise<void> {\n const project = await loadProject(config, cwd);\n const projectRoot = resolve(options.configDir, '..');\n const outDir = resolve(projectRoot, 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 paths = new Set<string>();\n for (const theme of project.permutations) {\n const tokens = project.permutationsResolved[theme.name];\n if (!tokens) continue;\n for (const path of Object.keys(tokens)) paths.add(path);\n }\n const sorted = [...paths].toSorted();\n const tokenEntries = sorted.map((p) => ` ${JSON.stringify(p)}: string;`);\n const themeUnion =\n project.permutations.map((t) => JSON.stringify(t.name)).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 {\n Config,\n ListedToken,\n Project,\n SwatchbookIntegration,\n TokenListingByPath,\n} from '@unpunnyfuns/swatchbook-core';\nimport { emitAxisProjectedCss, loadProject } from '@unpunnyfuns/swatchbook-core';\nimport { type FSWatcher, watch as fsWatch } 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\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}: SwatchbookPluginOptions): Plugin {\n let project: Project | undefined;\n let css = '';\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 async buildStart() {\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. Values are JSON-stringified for stability.\n // `jointOverrides` ships as an array of `[key, entry]` pairs because\n // `Map` doesn't survive `JSON.stringify`; the block side reconstructs\n // the Map (or just reads the array, depending on the consumer).\n const jointOverridesArr = [...project.jointOverrides.entries()];\n const varianceByPathObj = Object.fromEntries(project.varianceByPath.entries());\n return [\n `/* swatchbook virtual module — generated */`,\n `export const axes = ${JSON.stringify(project.axes)};`,\n `export const presets = ${JSON.stringify(project.presets)};`,\n `export const disabledAxes = ${JSON.stringify(project.disabledAxes)};`,\n `export const diagnostics = ${JSON.stringify(project.diagnostics)};`,\n `export const css = ${JSON.stringify(css)};`,\n `export const cssVarPrefix = ${JSON.stringify(config.cssVarPrefix ?? '')};`,\n `export const listing = ${JSON.stringify(slimListing(project.listing))};`,\n `export const cells = ${JSON.stringify(project.cells)};`,\n `export const jointOverrides = ${JSON.stringify(jointOverridesArr)};`,\n `export const varianceByPath = ${JSON.stringify(varianceByPathObj)};`,\n `export const defaultTuple = ${JSON.stringify(project.defaultTuple)};`,\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 = project.varianceByPath.size;\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: {\n axes: project.axes,\n disabledAxes: project.disabledAxes,\n presets: project.presets,\n diagnostics: project.diagnostics,\n css,\n cssVarPrefix: config.cssVarPrefix ?? '',\n listing: slimListing(project.listing),\n cells: project.cells,\n jointOverrides: [...project.jointOverrides.entries()],\n varianceByPath: Object.fromEntries(project.varianceByPath.entries()),\n defaultTuple: project.defaultTuple,\n },\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\n/** @internal Tests use this so they don't have to spin up Vite. */\nexport function composeProjectCss(project: Project): string {\n return emitAxisProjectedCss(project);\n}\n\ntype SlimListedToken = Pick<\n ListedToken['$extensions']['app.terrazzo.listing'],\n 'names' | 'previewValue' | 'source'\n>;\n\n/**\n * Reduce the full Token Listing surface down to the fields blocks read.\n * Drops `originalValue` (large, not needed for display), `$value`, `$type`,\n * `mode`, `subtype` for now — the blocks don't consume them yet. Keeps the\n * virtual module payload lean, especially for large projects where each\n * token's raw listing entry can weigh a few KB.\n */\nfunction slimListing(listing: TokenListingByPath): Record<string, SlimListedToken> {\n const out: Record<string, SlimListedToken> = {};\n for (const [path, entry] of Object.entries(listing)) {\n const ext = entry.$extensions['app.terrazzo.listing'];\n const slim: SlimListedToken = { names: ext.names };\n if (ext.previewValue !== undefined) slim.previewValue = ext.previewValue;\n if (ext.source !== undefined) slim.source = ext.source;\n out[path] = slim;\n }\n return out;\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport type { Config, Project } from '@unpunnyfuns/swatchbook-core';\nimport { loadProject } from '@unpunnyfuns/swatchbook-core';\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 // Codegen runs once at Vite startup. The virtual module plugin still\n // owns the live reload path via its HMR watcher; this file just gives\n // TS autocomplete for `useToken('…')` in consumer stories.\n await writeTokenCodegen(config, cwd, options);\n\n const plugins = Array.isArray(viteConfig.plugins) ? [...viteConfig.plugins] : [];\n plugins.push(\n swatchbookTokensPlugin({\n config,\n cwd,\n ...(options.integrations !== undefined && { integrations: options.integrations }),\n }),\n );\n\n return { ...viteConfig, plugins };\n}\n\n/** Storybook appends this module into the manager bundle so our toolbar tool registers. */\nexport function managerEntries(entry: string[] = []): string[] {\n const managerUrl = import.meta.resolve('@unpunnyfuns/swatchbook-addon/manager');\n return [...entry, fileURLToPath(managerUrl)];\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(\n config: Config,\n cwd: string,\n options: PresetOptions,\n): Promise<void> {\n const project = await loadProject(config, cwd);\n const projectRoot = resolve(options.configDir, '..');\n const outDir = resolve(projectRoot, 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 // `varianceByPath` covers every path that appears in any\n // permutation's resolved map (by construction in\n // `buildVarianceByPath`), so it's the right source for the token\n // path union — independent of the cartesian materialization the\n // chain is in the process of dropping. Hand-built test fixtures\n // that don't populate `varianceByPath` fall through to the\n // legacy per-permutation scan.\n const paths = new Set<string>();\n if (project.varianceByPath && project.varianceByPath.size > 0) {\n for (const p of project.varianceByPath.keys()) paths.add(p);\n } else {\n for (const theme of project.permutations) {\n const tokens = project.permutationsResolved[theme.name];\n if (!tokens) continue;\n for (const path of Object.keys(tokens)) paths.add(path);\n }\n }\n const sorted = [...paths].toSorted();\n const tokenEntries = sorted.map((p) => ` ${JSON.stringify(p)}: string;`);\n const themeUnion =\n project.permutations.map((t) => JSON.stringify(t.name)).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,8 +1,9 @@
1
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";
2
+ import { buildResolveAt } from "@unpunnyfuns/swatchbook-core/resolve-at";
2
3
  import { useEffect, useMemo } from "react";
3
4
  import { addons } from "storybook/preview-api";
4
5
  import "virtual:swatchbook/integration-side-effects";
5
- import { axes, css, cssVarPrefix, defaultPermutation, diagnostics, disabledAxes, listing, permutations, permutationsResolved, presets } from "virtual:swatchbook/tokens";
6
+ import { axes, cells, css, cssVarPrefix, defaultTuple, diagnostics, disabledAxes, jointOverrides, listing, presets, varianceByPath } from "virtual:swatchbook/tokens";
6
7
  import { AxesContext, COLOR_FORMATS, ColorFormatContext, PermutationContext, SwatchbookContext } from "@unpunnyfuns/swatchbook-blocks";
7
8
  import { jsx } from "react/jsx-runtime";
8
9
  //#region \0rolldown/runtime.js
@@ -62,30 +63,29 @@ html, body {
62
63
  if (style.textContent !== text) style.textContent = text;
63
64
  }
64
65
  /**
65
- * Apply `cb(axisName, value)` for every pinned (disabled) axis whose value
66
- * is present on any surviving theme's `input`. Disabled axes don't appear
67
- * in `virtualAxes`, but CSS may still reference their pinned value on
68
- * compound selectors every theme that survived filtering carries the
69
- * same pinned context per disabled axis, so sampling any theme works.
66
+ * Apply `cb(axisName, value)` for every pinned (disabled) axis whose
67
+ * default-tuple value is set. `virtualDefaultTuple` carries the
68
+ * post-filter axis defaults; disabled axes don't appear in
69
+ * `virtualAxes` but their pinned context value still lives here, so
70
+ * sampling it gives the same result the old "first permutation's
71
+ * input" lookup did.
70
72
  */
71
73
  function forEachPinnedAxis(cb) {
72
- const pinnedSample = permutations[0]?.input;
73
- if (!pinnedSample) return;
74
74
  for (const name of disabledAxes) {
75
- const value = pinnedSample[name];
75
+ const value = defaultTuple[name];
76
76
  if (value !== void 0) cb(name, value);
77
77
  }
78
78
  }
79
79
  /**
80
- * Pick the theme name for a tuple, falling back to `defaultPermutation` and then
81
- * the first theme. Returns empty string when the project has no permutations so
82
- * callers can omit the attr instead of writing a made-up context name.
80
+ * Compose a stable theme name from a tuple the same `Light · Brand A
81
+ * · Normal` form `permutationID` produced server-side. Used for the
82
+ * `data-<prefix>-theme` attribute and the `swatchbook/theme` channel
83
+ * signal. Returns empty string when there are no axes (no name to
84
+ * write).
83
85
  */
84
86
  function matchPermutationName(tuple) {
85
- return permutations.find((t) => {
86
- const input = t.input;
87
- return Object.keys(input).every((k) => input[k] === tuple[k]);
88
- })?.name ?? defaultPermutation ?? permutations[0]?.name ?? "";
87
+ if (axes.length === 0) return "";
88
+ return axes.map((axis) => tuple[axis.name] ?? axis.default).join(" · ");
89
89
  }
90
90
  /**
91
91
  * Write the composed permutation ID to `data-<prefix>-theme` plus one
@@ -119,22 +119,39 @@ function broadcastInit() {
119
119
  axes,
120
120
  disabledAxes,
121
121
  presets,
122
- permutations,
123
- defaultPermutation,
124
- permutationsResolved,
125
122
  diagnostics,
126
- cssVarPrefix
123
+ cssVarPrefix,
124
+ cells,
125
+ jointOverrides,
126
+ varianceByPath,
127
+ defaultTuple
127
128
  });
128
129
  }
129
130
  /** Axis-default tuple, used as the baseline before overrides. */
130
- function defaultTuple() {
131
+ function defaultTuple$1() {
131
132
  const out = {};
132
133
  for (const axis of axes) out[axis.name] = axis.default;
133
134
  return out;
134
135
  }
135
- /** Look up a `Permutation.input` by composed name. Returns `undefined` if no theme matches. */
136
+ /**
137
+ * Reverse-engineer a tuple from a `Light · Brand A · Normal`-shape
138
+ * theme name. Splits on ` · ` and zips with `virtualAxes` in declared
139
+ * order — matches `matchPermutationName`'s production direction so a
140
+ * round-trip is lossless. Returns `undefined` when the segment count
141
+ * doesn't match the axis count.
142
+ */
136
143
  function tupleForName(name) {
137
- return permutations.find((t) => t.name === name)?.input;
144
+ if (!name) return void 0;
145
+ const parts = name.split(" · ");
146
+ if (parts.length !== axes.length) return void 0;
147
+ const out = {};
148
+ for (let i = 0; i < axes.length; i++) {
149
+ const axis = axes[i];
150
+ const value = parts[i];
151
+ if (value === void 0) return void 0;
152
+ out[axis.name] = value;
153
+ }
154
+ return out;
138
155
  }
139
156
  /**
140
157
  * Merge a partial tuple onto the axis defaults, dropping keys for axes that
@@ -142,7 +159,7 @@ function tupleForName(name) {
142
159
  * aren't listed on the axis.
143
160
  */
144
161
  function normalizeTuple(partial) {
145
- const out = defaultTuple();
162
+ const out = defaultTuple$1();
146
163
  for (const axis of axes) {
147
164
  const candidate = partial[axis.name];
148
165
  if (candidate !== void 0 && axis.contexts.includes(candidate)) out[axis.name] = candidate;
@@ -166,13 +183,25 @@ function resolveTuple(globals, parameters) {
166
183
  }
167
184
  const globalAxes = globals[AXES_GLOBAL_KEY];
168
185
  if (globalAxes && typeof globalAxes === "object") return normalizeTuple(globalAxes);
169
- return defaultTuple();
186
+ return defaultTuple$1();
170
187
  }
171
188
  function resolveColorFormat(globals) {
172
189
  const raw = globals[COLOR_FORMAT_GLOBAL_KEY];
173
190
  if (typeof raw === "string" && COLOR_FORMATS.includes(raw)) return raw;
174
191
  return "hex";
175
192
  }
193
+ /**
194
+ * Single shared `resolveAt` instance for the lifetime of the preview
195
+ * iframe. The inputs (`virtualAxes`, `virtualCells`, …) are all
196
+ * module-level virtual-module exports with stable identity, so this
197
+ * never needs to rebuild; downstream `ProjectSnapshot` consumers can
198
+ * key memos on the snapshot wrapper without worrying about
199
+ * `resolveAt` churning when Storybook recreates `context.globals`.
200
+ *
201
+ * Replaces the per-render `useMemo(makeResolveAt(...))` dance the
202
+ * blocks side used to do in `useProject`.
203
+ */
204
+ const previewResolveAt = buildResolveAt(axes, cells, new Map(jointOverrides), defaultTuple);
176
205
  const themedDecorator = (Story, context) => {
177
206
  const globals = context.globals;
178
207
  const parameters = context.parameters;
@@ -199,14 +228,17 @@ const themedDecorator = (Story, context) => {
199
228
  axes,
200
229
  disabledAxes,
201
230
  presets,
202
- permutations,
203
- permutationsResolved,
204
231
  activePermutation: themeName,
205
232
  activeAxes: tuple,
206
233
  cssVarPrefix,
207
234
  diagnostics,
208
235
  css,
209
- listing
236
+ listing,
237
+ cells,
238
+ jointOverrides,
239
+ varianceByPath,
240
+ defaultTuple,
241
+ resolveAt: previewResolveAt
210
242
  }), [themeName, tuple]);
211
243
  return /* @__PURE__ */ jsx(SwatchbookContext.Provider, {
212
244
  value: snapshot,
@@ -334,15 +366,16 @@ html, body {
334
366
  axes: payload.axes,
335
367
  disabledAxes: payload.disabledAxes,
336
368
  presets: payload.presets,
337
- permutations: payload.permutations,
338
- defaultPermutation: payload.defaultPermutation,
339
- permutationsResolved: payload.permutationsResolved,
340
369
  diagnostics: payload.diagnostics,
341
- cssVarPrefix: payload.cssVarPrefix
370
+ cssVarPrefix: payload.cssVarPrefix,
371
+ cells: payload.cells,
372
+ jointOverrides: payload.jointOverrides,
373
+ varianceByPath: payload.varianceByPath,
374
+ defaultTuple: payload.defaultTuple
342
375
  });
343
376
  channel.emit(TOKENS_UPDATED_EVENT, payload);
344
377
  });
345
378
  //#endregion
346
379
  export { preview_exports as i, globalTypes as n, initialGlobals as r, decorators as t };
347
380
 
348
- //# sourceMappingURL=preview-ZVsSsSIf.mjs.map
381
+ //# sourceMappingURL=preview-DvtUsEMq.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-DvtUsEMq.mjs","names":["virtualDisabledAxes","virtualDefaultTuple","virtualAxes","virtualPresets","virtualCells","virtualJointOverrides","virtualVarianceByPath","defaultTuple","virtualListing"],"sources":["../src/data-attr.ts","../src/preview.tsx"],"sourcesContent":["/**\n * Produce a prefixed `data-*` attribute name when `prefix` is set, bare\n * `data-<key>` otherwise. Duplicated from `@unpunnyfuns/swatchbook-core`'s\n * `dataAttr` so the preview bundle doesn't have to import from core —\n * core's top-level entry pulls in Node-only modules (`node:fs/promises`\n * via the loader) that throw when evaluated in the browser.\n */\nexport function dataAttr(prefix: string, key: string): string {\n return prefix ? `data-${prefix}-${key}` : `data-${key}`;\n}\n","/// <reference types=\"vite/client\" />\nimport { buildResolveAt } from '@unpunnyfuns/swatchbook-core/resolve-at';\nimport type {\n Axis as CoreAxis,\n Cells as CoreCells,\n JointOverride,\n JointOverrides,\n} from '@unpunnyfuns/swatchbook-core';\nimport type { Decorator, Preview } from '@storybook/react-vite';\nimport { useEffect, useMemo } from 'react';\nimport { addons } from 'storybook/preview-api';\nimport { dataAttr } from '#/data-attr.ts';\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 cells as virtualCells,\n css,\n cssVarPrefix,\n defaultTuple as virtualDefaultTuple,\n diagnostics,\n disabledAxes as virtualDisabledAxes,\n jointOverrides as virtualJointOverrides,\n listing as virtualListing,\n presets as virtualPresets,\n varianceByPath as virtualVarianceByPath,\n} from 'virtual:swatchbook/tokens';\nimport {\n AxesContext,\n COLOR_FORMATS,\n type ColorFormat,\n ColorFormatContext,\n type ProjectSnapshot,\n SwatchbookContext,\n PermutationContext,\n} 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/** CSS var name with the active prefix applied. */\nfunction v(name: string): string {\n return cssVarPrefix ? `--${cssVarPrefix}-${name}` : `--${name}`;\n}\n\n/**\n * Inject the per-theme stylesheet plus a tiny `html, body { ... }` block so\n * the iframe's own chrome (outside any decorator wrapper — Docs mode,\n * autodocs, empty gutters) also picks up the active theme.\n */\nfunction ensureStylesheet(): void {\n if (typeof document === 'undefined') return;\n const bodyRules = `\nhtml, body {\n background: var(${v('color-surface-default')}, Canvas);\n color: var(${v('color-text-default')}, CanvasText);\n margin: 0;\n}\n`;\n const text = `${css}\\n${bodyRules}`;\n let style = document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement | null;\n if (!style) {\n style = document.createElement('style');\n style.id = STYLE_ELEMENT_ID;\n document.head.appendChild(style);\n }\n if (style.textContent !== text) style.textContent = text;\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 — the same `Light · Brand A\n * · Normal` form `permutationID` produced server-side. Used for the\n * `data-<prefix>-theme` attribute and the `swatchbook/theme` channel\n * signal. Returns empty string when there are no axes (no name to\n * write).\n */\nfunction matchPermutationName(tuple: Readonly<Record<string, string>>): string {\n if (virtualAxes.length === 0) return '';\n return virtualAxes.map((axis) => tuple[axis.name] ?? axis.default).join(' · ');\n}\n\n/**\n * Write the composed permutation ID to `data-<prefix>-theme` plus one\n * `data-<prefix>-<axis>=<context>` per axis. Prefix follows `cssVarPrefix`\n * so the attr namespace and the emitted-CSS selectors stay in lockstep;\n * empty prefix keeps the bare `data-theme` form.\n */\nfunction setRootAxes(themeName: string, tuple: Readonly<Record<string, string>>): void {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n const themeAttr = dataAttr(cssVarPrefix, 'theme');\n if (themeName) root.setAttribute(themeAttr, themeName);\n else root.removeAttribute(themeAttr);\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 * 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(INIT_EVENT, {\n axes: virtualAxes,\n disabledAxes: virtualDisabledAxes,\n presets: virtualPresets,\n diagnostics,\n cssVarPrefix,\n cells: virtualCells,\n jointOverrides: virtualJointOverrides,\n varianceByPath: virtualVarianceByPath,\n defaultTuple: virtualDefaultTuple,\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 `matchPermutationName`'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 globals: SwatchbookGlobals,\n parameters: StoryParameters,\n): Record<string, string> {\n const param = parameters.swatchbook;\n const paramAxes = param?.axes;\n if (paramAxes) {\n return normalizeTuple(paramAxes);\n }\n if (param?.permutation) {\n const hit = tupleForName(param.permutation);\n if (hit) return normalizeTuple(hit);\n }\n const globalAxes = globals[AXES_GLOBAL_KEY];\n if (globalAxes && typeof globalAxes === 'object') {\n return normalizeTuple(globalAxes as Record<string, string>);\n }\n return defaultTuple();\n}\n\nfunction resolveColorFormat(globals: SwatchbookGlobals): ColorFormat {\n const raw = globals[COLOR_FORMAT_GLOBAL_KEY];\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. The inputs (`virtualAxes`, `virtualCells`, …) are all\n * module-level virtual-module exports with stable identity, so this\n * never needs to rebuild; downstream `ProjectSnapshot` consumers can\n * key memos on the snapshot wrapper without worrying about\n * `resolveAt` churning when Storybook recreates `context.globals`.\n *\n * Replaces the per-render `useMemo(makeResolveAt(...))` dance the\n * blocks side used to do in `useProject`.\n */\nconst previewResolveAt = buildResolveAt(\n virtualAxes as readonly CoreAxis[],\n virtualCells as CoreCells,\n new Map(virtualJointOverrides as readonly (readonly [string, JointOverride])[]) as JointOverrides,\n virtualDefaultTuple,\n);\n\nconst themedDecorator: Decorator = (Story, context) => {\n const globals = context.globals as SwatchbookGlobals;\n const parameters = context.parameters as StoryParameters;\n const tuple = useMemo(() => resolveTuple(globals, parameters), [globals, parameters]);\n const colorFormat = useMemo(() => resolveColorFormat(globals), [globals]);\n const themeName = useMemo(() => matchPermutationName(tuple), [tuple]);\n\n useEffect(() => {\n ensureStylesheet();\n broadcastInit();\n }, []);\n\n useEffect(() => {\n setRootAxes(themeName, tuple);\n }, [themeName, tuple]);\n\n const wrapperAttrs: Record<string, string> = {};\n if (themeName) wrapperAttrs[dataAttr(cssVarPrefix, 'theme')] = themeName;\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 activePermutation: themeName,\n activeAxes: tuple,\n cssVarPrefix,\n diagnostics,\n css,\n listing: virtualListing,\n cells: virtualCells,\n jointOverrides: virtualJointOverrides,\n varianceByPath: virtualVarianceByPath,\n defaultTuple: virtualDefaultTuple,\n // Cast: `buildResolveAt` returns `TokenMap` (Terrazzo's\n // `TokenNormalized`), while the block-side snapshot type uses\n // its own narrower `VirtualTokenShape`. The shapes are\n // structurally a subset; the cast covers the\n // `exactOptionalPropertyTypes` mismatch between\n // `string | undefined` and `string`.\n resolveAt: previewResolveAt as unknown as NonNullable<ProjectSnapshot['resolveAt']>,\n }),\n [themeName, tuple],\n );\n\n return (\n <SwatchbookContext.Provider value={snapshot}>\n <PermutationContext.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 </ColorFormatContext.Provider>\n </AxesContext.Provider>\n </PermutationContext.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();\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();\n const tuple = resolveTuple(globals, {});\n setRootAxes(matchPermutationName(tuple), tuple);\n };\n const onGlobals = (payload: { globals?: SwatchbookGlobals }): void => {\n if (payload.globals) 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 cells: typeof virtualCells;\n jointOverrides: typeof virtualJointOverrides;\n varianceByPath: typeof virtualVarianceByPath;\n defaultTuple: typeof virtualDefaultTuple;\n}\nif (import.meta.hot) {\n import.meta.hot.on(HMR_EVENT, (payload: HmrSnapshot) => {\n if (typeof document !== 'undefined') {\n const bodyRules = `\nhtml, body {\n background: var(${payload.cssVarPrefix ? `--${payload.cssVarPrefix}-` : '--'}color-surface-default, Canvas);\n color: var(${payload.cssVarPrefix ? `--${payload.cssVarPrefix}-` : '--'}color-text-default, CanvasText);\n margin: 0;\n}\n`;\n const text = `${payload.css}\\n${bodyRules}`;\n let style = document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement | null;\n if (!style) {\n style = document.createElement('style');\n style.id = STYLE_ELEMENT_ID;\n document.head.appendChild(style);\n }\n if (style.textContent !== text) style.textContent = text;\n }\n const channel = addons.getChannel();\n channel.emit(INIT_EVENT, {\n axes: payload.axes,\n disabledAxes: payload.disabledAxes,\n presets: payload.presets,\n diagnostics: payload.diagnostics,\n cssVarPrefix: payload.cssVarPrefix,\n cells: payload.cells,\n jointOverrides: payload.jointOverrides,\n varianceByPath: payload.varianceByPath,\n defaultTuple: payload.defaultTuple,\n });\n channel.emit(TOKENS_UPDATED_EVENT, payload);\n });\n}\n"],"mappings":"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"}
package/dist/preview.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as globalTypes, r as initialGlobals, t as decorators } from "./preview-ZVsSsSIf.mjs";
1
+ import { n as globalTypes, r as initialGlobals, t as decorators } from "./preview-DvtUsEMq.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.53.0",
3
+ "version": "0.55.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.53.0",
79
- "@unpunnyfuns/swatchbook-core": "0.53.0",
80
- "@unpunnyfuns/swatchbook-switcher": "0.53.0"
78
+ "@unpunnyfuns/swatchbook-blocks": "0.55.0",
79
+ "@unpunnyfuns/swatchbook-core": "0.55.0",
80
+ "@unpunnyfuns/swatchbook-switcher": "0.55.0"
81
81
  },
82
82
  "peerDependencies": {
83
83
  "@storybook/react-vite": "^10.3.5",
@@ -102,7 +102,8 @@
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.4",
106
+ "@unpunnyfuns/swatchbook-tokens": "0.0.0"
106
107
  },
107
108
  "scripts": {
108
109
  "build": "tsdown",
@@ -1 +0,0 @@
1
- {"version":3,"file":"preview-ZVsSsSIf.mjs","names":["virtualDisabledAxes","virtualAxes","virtualPresets","virtualListing"],"sources":["../src/data-attr.ts","../src/preview.tsx"],"sourcesContent":["/**\n * Produce a prefixed `data-*` attribute name when `prefix` is set, bare\n * `data-<key>` otherwise. Duplicated from `@unpunnyfuns/swatchbook-core`'s\n * `dataAttr` so the preview bundle doesn't have to import from core —\n * core's top-level entry pulls in Node-only modules (`node:fs/promises`\n * via the loader) that throw when evaluated in the browser.\n */\nexport function dataAttr(prefix: string, key: string): string {\n return prefix ? `data-${prefix}-${key}` : `data-${key}`;\n}\n","/// <reference types=\"vite/client\" />\nimport type { Decorator, Preview } from '@storybook/react-vite';\nimport { useEffect, useMemo } from 'react';\nimport { addons } from 'storybook/preview-api';\nimport { dataAttr } from '#/data-attr.ts';\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 defaultPermutation,\n diagnostics,\n disabledAxes as virtualDisabledAxes,\n listing as virtualListing,\n presets as virtualPresets,\n permutations,\n permutationsResolved,\n} from 'virtual:swatchbook/tokens';\nimport {\n AxesContext,\n COLOR_FORMATS,\n type ColorFormat,\n ColorFormatContext,\n type ProjectSnapshot,\n SwatchbookContext,\n PermutationContext,\n} 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/** CSS var name with the active prefix applied. */\nfunction v(name: string): string {\n return cssVarPrefix ? `--${cssVarPrefix}-${name}` : `--${name}`;\n}\n\n/**\n * Inject the per-theme stylesheet plus a tiny `html, body { ... }` block so\n * the iframe's own chrome (outside any decorator wrapper — Docs mode,\n * autodocs, empty gutters) also picks up the active theme.\n */\nfunction ensureStylesheet(): void {\n if (typeof document === 'undefined') return;\n const bodyRules = `\nhtml, body {\n background: var(${v('color-surface-default')}, Canvas);\n color: var(${v('color-text-default')}, CanvasText);\n margin: 0;\n}\n`;\n const text = `${css}\\n${bodyRules}`;\n let style = document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement | null;\n if (!style) {\n style = document.createElement('style');\n style.id = STYLE_ELEMENT_ID;\n document.head.appendChild(style);\n }\n if (style.textContent !== text) style.textContent = text;\n}\n\n/**\n * Apply `cb(axisName, value)` for every pinned (disabled) axis whose value\n * is present on any surviving theme's `input`. Disabled axes don't appear\n * in `virtualAxes`, but CSS may still reference their pinned value on\n * compound selectors — every theme that survived filtering carries the\n * same pinned context per disabled axis, so sampling any theme works.\n */\nfunction forEachPinnedAxis(cb: (name: string, value: string) => void): void {\n const pinnedSample = permutations[0]?.input;\n if (!pinnedSample) return;\n for (const name of virtualDisabledAxes) {\n const value = pinnedSample[name];\n if (value !== undefined) cb(name, value);\n }\n}\n\n/**\n * Pick the theme name for a tuple, falling back to `defaultPermutation` and then\n * the first theme. Returns empty string when the project has no permutations so\n * callers can omit the attr instead of writing a made-up context name.\n */\nfunction matchPermutationName(tuple: Readonly<Record<string, string>>): string {\n const match = permutations.find((t) => {\n const input = t.input as Record<string, string>;\n return Object.keys(input).every((k) => input[k] === tuple[k]);\n });\n return match?.name ?? defaultPermutation ?? permutations[0]?.name ?? '';\n}\n\n/**\n * Write the composed permutation ID to `data-<prefix>-theme` plus one\n * `data-<prefix>-<axis>=<context>` per axis. Prefix follows `cssVarPrefix`\n * so the attr namespace and the emitted-CSS selectors stay in lockstep;\n * empty prefix keeps the bare `data-theme` form.\n */\nfunction setRootAxes(themeName: string, tuple: Readonly<Record<string, string>>): void {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n const themeAttr = dataAttr(cssVarPrefix, 'theme');\n if (themeName) root.setAttribute(themeAttr, themeName);\n else root.removeAttribute(themeAttr);\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 * 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(INIT_EVENT, {\n axes: virtualAxes,\n disabledAxes: virtualDisabledAxes,\n presets: virtualPresets,\n permutations,\n defaultPermutation,\n permutationsResolved,\n diagnostics,\n cssVarPrefix,\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/** Look up a `Permutation.input` by composed name. Returns `undefined` if no theme matches. */\nfunction tupleForName(name: string): Record<string, string> | undefined {\n const match = permutations.find((t) => t.name === name);\n return match?.input;\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 globals: SwatchbookGlobals,\n parameters: StoryParameters,\n): Record<string, string> {\n const param = parameters.swatchbook;\n const paramAxes = param?.axes;\n if (paramAxes) {\n return normalizeTuple(paramAxes);\n }\n if (param?.permutation) {\n const hit = tupleForName(param.permutation);\n if (hit) return normalizeTuple(hit);\n }\n const globalAxes = globals[AXES_GLOBAL_KEY];\n if (globalAxes && typeof globalAxes === 'object') {\n return normalizeTuple(globalAxes as Record<string, string>);\n }\n return defaultTuple();\n}\n\nfunction resolveColorFormat(globals: SwatchbookGlobals): ColorFormat {\n const raw = globals[COLOR_FORMAT_GLOBAL_KEY];\n if (typeof raw === 'string' && (COLOR_FORMATS as readonly string[]).includes(raw)) {\n return raw as ColorFormat;\n }\n return 'hex';\n}\n\nconst themedDecorator: Decorator = (Story, context) => {\n const globals = context.globals as SwatchbookGlobals;\n const parameters = context.parameters as StoryParameters;\n const tuple = useMemo(() => resolveTuple(globals, parameters), [globals, parameters]);\n const colorFormat = useMemo(() => resolveColorFormat(globals), [globals]);\n const themeName = useMemo(() => matchPermutationName(tuple), [tuple]);\n\n useEffect(() => {\n ensureStylesheet();\n broadcastInit();\n }, []);\n\n useEffect(() => {\n setRootAxes(themeName, tuple);\n }, [themeName, tuple]);\n\n const wrapperAttrs: Record<string, string> = {};\n if (themeName) wrapperAttrs[dataAttr(cssVarPrefix, 'theme')] = themeName;\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 permutations,\n permutationsResolved,\n activePermutation: themeName,\n activeAxes: tuple,\n cssVarPrefix,\n diagnostics,\n css,\n listing: virtualListing,\n }),\n [themeName, tuple],\n );\n\n return (\n <SwatchbookContext.Provider value={snapshot}>\n <PermutationContext.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 </ColorFormatContext.Provider>\n </AxesContext.Provider>\n </PermutationContext.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();\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();\n const tuple = resolveTuple(globals, {});\n setRootAxes(matchPermutationName(tuple), tuple);\n };\n const onGlobals = (payload: { globals?: SwatchbookGlobals }): void => {\n if (payload.globals) 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 permutations: typeof permutations;\n defaultPermutation: typeof defaultPermutation;\n permutationsResolved: typeof permutationsResolved;\n diagnostics: typeof diagnostics;\n css: string;\n cssVarPrefix: string;\n listing: typeof virtualListing;\n}\nif (import.meta.hot) {\n import.meta.hot.on(HMR_EVENT, (payload: HmrSnapshot) => {\n if (typeof document !== 'undefined') {\n const bodyRules = `\nhtml, body {\n background: var(${payload.cssVarPrefix ? `--${payload.cssVarPrefix}-` : '--'}color-surface-default, Canvas);\n color: var(${payload.cssVarPrefix ? `--${payload.cssVarPrefix}-` : '--'}color-text-default, CanvasText);\n margin: 0;\n}\n`;\n const text = `${payload.css}\\n${bodyRules}`;\n let style = document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement | null;\n if (!style) {\n style = document.createElement('style');\n style.id = STYLE_ELEMENT_ID;\n document.head.appendChild(style);\n }\n if (style.textContent !== text) style.textContent = text;\n }\n const channel = addons.getChannel();\n channel.emit(INIT_EVENT, {\n axes: payload.axes,\n disabledAxes: payload.disabledAxes,\n presets: payload.presets,\n permutations: payload.permutations,\n defaultPermutation: payload.defaultPermutation,\n permutationsResolved: payload.permutationsResolved,\n diagnostics: payload.diagnostics,\n cssVarPrefix: payload.cssVarPrefix,\n });\n channel.emit(TOKENS_UPDATED_EVENT, payload);\n });\n}\n"],"mappings":"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"}