@unpunnyfuns/swatchbook-addon 0.52.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as AddonOptions } from "./options-Bcekz7uL.mjs";
1
+ import { t as AddonOptions } from "./options-q4CiYIs4.mjs";
2
2
  import { definePreviewAddon } from "storybook/internal/csf";
3
3
  export * from "@unpunnyfuns/swatchbook-blocks";
4
4
  export * from "@unpunnyfuns/swatchbook-switcher";
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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"}
@@ -0,0 +1,51 @@
1
+ import { Config, SwatchbookIntegration } from "@unpunnyfuns/swatchbook-core";
2
+
3
+ //#region src/options.d.ts
4
+ /**
5
+ * Options accepted by the swatchbook preset. Either pass a full {@link Config}
6
+ * as `config`, or set `configPath` pointing at a module whose default export
7
+ * is a `Config` (supports `.ts`, `.mts`, `.js`, `.mjs` via jiti).
8
+ */
9
+ interface AddonOptions {
10
+ /** Inline swatchbook config. Mutually exclusive with `configPath`. */
11
+ config?: Config;
12
+ /** Path to a config module, relative to the Storybook `configDir`. */
13
+ configPath?: string;
14
+ /**
15
+ * Display-side integrations that plug into the addon's Vite plugin.
16
+ * Each integration typically contributes a virtual module the
17
+ * preview imports — e.g. the Tailwind integration serves
18
+ * `virtual:swatchbook/tailwind.css`. The addon itself is
19
+ * tool-agnostic; integrations ship as separate packages.
20
+ */
21
+ integrations?: SwatchbookIntegration[];
22
+ /**
23
+ * Which CSS emitter populates the `css` export of
24
+ * `virtual:swatchbook/tokens`. Defaults to `'projected'`.
25
+ *
26
+ * - `'projected'` (default) — smart axis-projected emit
27
+ * (`emitAxisProjectedCss`). One `:root` baseline block, one
28
+ * `[data-<axis>="<ctx>"]` block per non-default cell (deltas only
29
+ * for tokens that axis touches), and compound `[data-A][data-B]`
30
+ * blocks for joint-variant tokens that need cartesian-correct
31
+ * values at specific joint tuples. Output size scales with
32
+ * `Σ(axes × non-default contexts × touching tokens) + joint
33
+ * compound blocks` — dramatically smaller than cartesian for
34
+ * typical fixtures. Spec-faithful for any DTCG-compliant
35
+ * resolver: orthogonal tokens project, joint-variant tokens fall
36
+ * back to compound selectors automatically.
37
+ * - `'cartesian'` — explicit fan-out (`projectCss`). One block per
38
+ * cartesian tuple, scoped by compound
39
+ * `[data-<axis>="<ctx>"][data-…]` selectors. Output scales with
40
+ * the cartesian product. Pick this when you want explicit
41
+ * per-tuple blocks for debugging, regression-comparison against
42
+ * the projected output, or because your tooling reads them
43
+ * directly. Not an escape hatch for large cardinality — at the
44
+ * scales where the projection analysis is expensive, the
45
+ * cartesian output is just as unmanageable.
46
+ */
47
+ emitMode?: 'cartesian' | 'projected';
48
+ }
49
+ //#endregion
50
+ export { AddonOptions as t };
51
+ //# sourceMappingURL=options-q4CiYIs4.d.mts.map
package/dist/preset.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as AddonOptions } from "./options-Bcekz7uL.mjs";
1
+ import { t as AddonOptions } from "./options-q4CiYIs4.mjs";
2
2
  import { Project } from "@unpunnyfuns/swatchbook-core";
3
3
  import { InlineConfig } from "vite";
4
4
 
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, projectCss } from "@unpunnyfuns/swatchbook-core";
6
6
  import { createJiti } from "jiti";
7
7
  import { watch } from "node:fs";
8
8
  import "picomatch";
@@ -17,12 +17,12 @@ function resolvedId(virtualId) {
17
17
  * and diagnostics. Watches the token files + resolver for changes and
18
18
  * invalidates the module so HMR reloads the preview with fresh data.
19
19
  */
20
- function swatchbookTokensPlugin({ config, cwd, integrations = [] }) {
20
+ function swatchbookTokensPlugin({ config, cwd, integrations = [], emitMode = "projected" }) {
21
21
  let project;
22
22
  let css = "";
23
23
  async function refresh() {
24
24
  project = await loadProject(config, cwd);
25
- css = projectCss(project);
25
+ css = composeProjectCss(project, emitMode);
26
26
  }
27
27
  /** Map of resolvedId → integration, indexed once. */
28
28
  const integrationById = /* @__PURE__ */ new Map();
@@ -159,6 +159,21 @@ function swatchbookTokensPlugin({ config, cwd, integrations = [] }) {
159
159
  };
160
160
  }
161
161
  /**
162
+ * Dispatch between the two CSS emitters based on `emitMode`. Extracted
163
+ * from the plugin's closure so unit tests can verify the dispatch
164
+ * without booting Vite — pass a project + mode, get the matching CSS.
165
+ *
166
+ * The plugin defaults to `'projected'` (the smart axis-projected
167
+ * emitter); `'cartesian'` calls `projectCss` for explicit per-tuple
168
+ * fan-out.
169
+ *
170
+ * @internal Exported for tests; not part of the public API.
171
+ */
172
+ function composeProjectCss(project, emitMode = "projected") {
173
+ if (emitMode === "cartesian") return projectCss(project);
174
+ return emitAxisProjectedCss(project);
175
+ }
176
+ /**
162
177
  * Reduce the full Token Listing surface down to the fields blocks read.
163
178
  * Drops `originalValue` (large, not needed for display), `$value`, `$type`,
164
179
  * `mode`, `subtype` for now — the blocks don't consume them yet. Keeps the
@@ -191,7 +206,8 @@ async function viteFinal(viteConfig, options) {
191
206
  plugins.push(swatchbookTokensPlugin({
192
207
  config,
193
208
  cwd,
194
- ...options.integrations !== void 0 && { integrations: options.integrations }
209
+ ...options.integrations !== void 0 && { integrations: options.integrations },
210
+ ...options.emitMode !== void 0 && { emitMode: options.emitMode }
195
211
  }));
196
212
  return {
197
213
  ...viteConfig,
@@ -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, 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 * Which CSS emitter to use for the virtual module's `css` export.\n * `'projected'` (default) calls the smart `emitAxisProjectedCss`;\n * `'cartesian'` calls `projectCss` for explicit per-tuple fan-out.\n * See `AddonOptions.emitMode` for trade-offs.\n */\n emitMode?: 'cartesian' | 'projected';\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 emitMode = 'projected',\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 = composeProjectCss(project, emitMode);\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\n/**\n * Dispatch between the two CSS emitters based on `emitMode`. Extracted\n * from the plugin's closure so unit tests can verify the dispatch\n * without booting Vite — pass a project + mode, get the matching CSS.\n *\n * The plugin defaults to `'projected'` (the smart axis-projected\n * emitter); `'cartesian'` calls `projectCss` for explicit per-tuple\n * fan-out.\n *\n * @internal Exported for tests; not part of the public API.\n */\nexport function composeProjectCss(\n project: Project,\n emitMode: 'cartesian' | 'projected' = 'projected',\n): string {\n if (emitMode === 'cartesian') return projectCss(project);\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 ...(options.emitMode !== undefined && { emitMode: options.emitMode }),\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":";;;;;;;;;;AAmCA,SAAS,WAAW,WAA2B;AAC7C,QAAO,KAAK;;;;;;;;AASd,SAAgB,uBAAuB,EACrC,QACA,KACA,eAAe,EAAE,EACjB,WAAW,eACuB;CAClC,IAAI;CACJ,IAAI,MAAM;CAEV,eAAe,UAAyB;AACtC,YAAU,MAAM,YAAY,QAAQ,IAAI;AACxC,QAAM,kBAAkB,SAAS,SAAS;;;CAI5C,MAAM,kCAAkB,IAAI,KAAoC;;CAEhE,MAAM,gBAA0B,EAAE;AAClC,MAAK,MAAM,eAAe,cAAc;EACtC,MAAM,KAAK,YAAY;AACvB,MAAI,CAAC,GAAI;AACT,kBAAgB,IAAI,WAAW,GAAG,UAAU,EAAE,YAAY;AAC1D,MAAI,GAAG,WAAY,eAAc,KAAK,GAAG,UAAU;;AAGrD,QAAO;EACL,MAAM;EACN,SAAS;EAET,MAAM,aAAa;AACjB,SAAM,SAAS;;EAGjB,UAAU,IAAI;AACZ,OAAI,OAAA,4BAA0B,QAAO;AACrC,OAAI,OAAA,8CACF,QAAO;AAET,QAAK,MAAM,eAAe,aACxB,KAAI,YAAY,eAAe,cAAc,GAC3C,QAAO,WAAW,YAAY,cAAc,UAAU;AAG1D,UAAO;;EAGT,KAAK,IAAI;AACP,OAAI,OAAO,6CAGT,QAAO,cAAc,KAAK,QAAQ,UAAU,KAAK,UAAU,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK;GAEhF,MAAM,cAAc,gBAAgB,IAAI,GAAG;AAC3C,OAAI,aAAa,eAAe;AAC9B,QAAI,CAAC,QAAS,QAAO;AACrB,WAAO,YAAY,cAAc,OAAO,QAAQ;;AAElD,OAAI,OAAO,2BAA4B,QAAO;AAC9C,OAAI,CAAC,QAAS,QAAO;AAErB,UAAO;IACL;IACA,uBAAuB,KAAK,UAAU,QAAQ,KAAK,CAAC;IACpD,0BAA0B,KAAK,UAAU,QAAQ,QAAQ,CAAC;IAC1D,+BAA+B,KAAK,UAAU,QAAQ,aAAa,CAAC;IACpE,+BAA+B,KAAK,UAAU,QAAQ,aAAa,CAAC;IACpE,qCAAqC,KAAK,UAAU,QAAQ,aAAa,IAAI,QAAQ,KAAK,CAAC;IAC3F,uCAAuC,KAAK,UAAU,QAAQ,qBAAqB,CAAC;IACpF,8BAA8B,KAAK,UAAU,QAAQ,YAAY,CAAC;IAClE,sBAAsB,KAAK,UAAU,IAAI,CAAC;IAC1C,+BAA+B,KAAK,UAAU,OAAO,gBAAgB,GAAG,CAAC;IACzE,0BAA0B,KAAK,UAAU,YAAY,QAAQ,QAAQ,CAAC,CAAC;IACxE,CAAC,KAAK,KAAK;;EAGd,MAAM,gBAAgB,QAAQ;AAO5B,OAAI,CAAC,QAAS,OAAM,SAAS;;;;;;;GAQ7B,IAAI,UAAgD;GACpD,MAAM,mBAAyB;AAC7B,QAAI,QAAS,cAAa,QAAQ;AAClC,cAAU,iBAAiB;AACzB,eAAU;AACV,MAAM,YAAY;AAChB,YAAM,SAAS;AACf,UAAI,CAAC,QAAS;MACd,MAAM,aAAa,OAAO,KACxB,QAAQ,qBAAqB,QAAQ,aAAa,IAAI,QAAQ,OAAO,EAAE,CACxE,CAAC;MACF,MAAM,YAAY,QAAQ,YAAY;AACtC,aAAO,OAAO,OAAO,KACnB,iDAAiD,WAAW,WAAW,UAAU,aAAa,cAAc,IAAI,KAAK,OACrH;OAAE,OAAO;OAAO,WAAW;OAAM,CAClC;MACD,MAAM,MAAM,OAAO,YAAY,cAAc,2BAA2B;AACxE,UAAI,IAAK,QAAO,YAAY,iBAAiB,IAAI;AAIjD,WAAK,MAAM,yBAAyB,gBAAgB,MAAM,EAAE;OAC1D,MAAM,IAAI,OAAO,YAAY,cAAc,sBAAsB;AACjE,WAAI,EAAG,QAAO,YAAY,iBAAiB,EAAE;;;;;;;;;;AAU/C,aAAO,GAAG,KAAK;OACb,MAAM;OACN,OAAO;OACP,MAAM;QACJ,MAAM,QAAQ;QACd,cAAc,QAAQ;QACtB,SAAS,QAAQ;QACjB,cAAc,QAAQ;QACtB,oBAAoB,QAAQ,aAAa,IAAI,QAAQ;QACrD,sBAAsB,QAAQ;QAC9B,aAAa,QAAQ;QACrB;QACA,cAAc,OAAO,gBAAgB;QACrC,SAAS,YAAY,QAAQ,QAAQ;QACtC;OACF,CAAC;SACA;OACH,IAAI;;;;;;;;;;;;;;GAeT,MAAM,wBAAQ,IAAI,KAA0B;AAC5C,QAAK,MAAM,QAAQ,SAAS,eAAe,EAAE,EAAE;IAC7C,MAAM,MAAM,QAAQ,KAAK;IACzB,MAAM,MAAM,MAAM,IAAI,IAAI,oBAAI,IAAI,KAAa;AAC/C,QAAI,IAAI,SAAS,KAAK,CAAC;AACvB,UAAM,IAAI,KAAK,IAAI;;GAGrB,MAAM,eAA4B,EAAE;AACpC,QAAK,MAAM,CAAC,KAAK,UAAU,MACzB,KAAI;IACF,MAAM,IAAIA,MAAQ,KAAK,EAAE,YAAY,OAAO,GAAG,WAAW,aAAa;AACrE,SAAI,CAAC,SAAU;AACf,SAAI,CAAC,MAAM,IAAI,SAAS,CAAE;AAC1B,SAAI,cAAc,YAAY,cAAc,SAAU,aAAY;MAClE;AACF,iBAAa,KAAK,EAAE;WACd;AAIV,UAAO,YAAY,KAAK,eAAe;AACrC,SAAK,MAAM,KAAK,aAAc,GAAE,OAAO;KACvC;;EAEL;;;;;;;;;;;;;AAiDH,SAAgB,kBACd,SACA,WAAsC,aAC9B;AACR,KAAI,aAAa,YAAa,QAAO,WAAW,QAAQ;AACxD,QAAO,qBAAqB,QAAQ;;;;;;;;;AAetC,SAAS,YAAY,SAA8D;CACjF,MAAM,MAAuC,EAAE;AAC/C,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,EAAE;EACnD,MAAM,MAAM,MAAM,YAAY;EAC9B,MAAM,OAAwB,EAAE,OAAO,IAAI,OAAO;AAClD,MAAI,IAAI,iBAAiB,KAAA,EAAW,MAAK,eAAe,IAAI;AAC5D,MAAI,IAAI,WAAW,KAAA,EAAW,MAAK,SAAS,IAAI;AAChD,MAAI,QAAQ;;AAEd,QAAO;;;;;;;;;;AC1RT,eAAsB,UACpB,YACA,SACuB;CACvB,MAAM,EAAE,QAAQ,QAAQ,MAAM,cAAc,QAAQ;AAKpD,OAAM,kBAAkB,QAAQ,KAAK,QAAQ;CAE7C,MAAM,UAAU,MAAM,QAAQ,WAAW,QAAQ,GAAG,CAAC,GAAG,WAAW,QAAQ,GAAG,EAAE;AAChF,SAAQ,KACN,uBAAuB;EACrB;EACA;EACA,GAAI,QAAQ,iBAAiB,KAAA,KAAa,EAAE,cAAc,QAAQ,cAAc;EAChF,GAAI,QAAQ,aAAa,KAAA,KAAa,EAAE,UAAU,QAAQ,UAAU;EACrE,CAAC,CACH;AAED,QAAO;EAAE,GAAG;EAAY;EAAS;;;AAInC,SAAgB,eAAe,QAAkB,EAAE,EAAY;CAC7D,MAAM,aAAa,OAAO,KAAK,QAAQ,wCAAwC;AAC/E,QAAO,CAAC,GAAG,OAAO,cAAc,WAAW,CAAC;;AAG9C,eAAe,cAAc,SAAkE;CAC7F,MAAM,cAAc,QAAQ,QAAQ,WAAW,KAAK;AAEpD,KAAI,QAAQ,OACV,QAAO;EAAE,QAAQ,QAAQ;EAAQ,KAAK;EAAa;CAGrD,MAAM,OAAO,QAAQ,cAAc;CACnC,MAAM,WAAW,WAAW,KAAK,GAAG,OAAO,QAAQ,QAAQ,WAAW,KAAK;AAU3E,QAAO;EAAE,QAJO,MAJH,WAAW,cAAc,QAAQ,UAAU,CAAC,MAAM;GAC7D,gBAAgB;GAChB,aAAa;GACd,CAAC,CACyB,OAAO,UAAU,EAAE,SAAS,MAAM,CAAC;EAIrC,KADb,QAAQ,SAAS;EACC;;AAGhC,eAAe,kBACb,QACA,KACA,SACe;CACf,MAAM,UAAU,MAAM,YAAY,QAAQ,IAAI;CAE9C,MAAM,SAAS,QADK,QAAQ,QAAQ,WAAW,KAAK,EAChB,OAAO,UAAU,cAAc;AACnE,OAAM,MAAM,QAAQ,EAAE,WAAW,MAAM,CAAC;CACxC,MAAM,UAAU,iBAAiB,QAAQ;AACzC,OAAM,UAAU,QAAQ,QAAQ,cAAc,EAAE,QAAQ;;;AAI1D,SAAgB,iBAAiB,SAA0B;CACzD,MAAM,wBAAQ,IAAI,KAAa;AAC/B,MAAK,MAAM,SAAS,QAAQ,cAAc;EACxC,MAAM,SAAS,QAAQ,qBAAqB,MAAM;AAClD,MAAI,CAAC,OAAQ;AACb,OAAK,MAAM,QAAQ,OAAO,KAAK,OAAO,CAAE,OAAM,IAAI,KAAK;;CAGzD,MAAM,eADS,CAAC,GAAG,MAAM,CAAC,UAAU,CACR,KAAK,MAAM,OAAO,KAAK,UAAU,EAAE,CAAC,WAAW;CAC3E,MAAM,aACJ,QAAQ,aAAa,KAAK,MAAM,KAAK,UAAU,EAAE,KAAK,CAAC,CAAC,KAAK,MAAM,IAAI;AAEzE,QAAO;EACL;EACA;EACA;EACA,GAAG;EACH;EACA;EACA,6CAA6C,WAAW;EACxD;EACA;EACD,CAAC,KAAK,KAAK"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unpunnyfuns/swatchbook-addon",
3
- "version": "0.52.0",
3
+ "version": "0.54.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.52.0",
79
- "@unpunnyfuns/swatchbook-core": "0.52.0",
80
- "@unpunnyfuns/swatchbook-switcher": "0.52.0"
78
+ "@unpunnyfuns/swatchbook-blocks": "0.54.0",
79
+ "@unpunnyfuns/swatchbook-core": "0.54.0",
80
+ "@unpunnyfuns/swatchbook-switcher": "0.54.0"
81
81
  },
82
82
  "peerDependencies": {
83
83
  "@storybook/react-vite": "^10.3.5",
@@ -93,14 +93,17 @@
93
93
  "@types/picomatch": "^4.0.3",
94
94
  "@types/react": "^19.2.14",
95
95
  "@vitejs/plugin-react": "^6.0.1",
96
- "jsdom": "^29.0.2",
96
+ "@vitest/browser": "^4.1.4",
97
+ "@vitest/browser-playwright": "^4.1.4",
98
+ "playwright": "^1.59.1",
97
99
  "react": "^19.2.4",
98
100
  "react-dom": "^19.2.4",
99
101
  "storybook": "^10.3.5",
100
102
  "tsdown": "^0.21.9",
101
103
  "typescript": "^6.0.0",
102
104
  "vite": "^8.0.4",
103
- "vitest": "^4.1.4"
105
+ "vitest": "^4.1.4",
106
+ "@unpunnyfuns/swatchbook-tokens": "0.0.0"
104
107
  },
105
108
  "scripts": {
106
109
  "build": "tsdown",
@@ -1,25 +0,0 @@
1
- import { Config, SwatchbookIntegration } from "@unpunnyfuns/swatchbook-core";
2
-
3
- //#region src/options.d.ts
4
- /**
5
- * Options accepted by the swatchbook preset. Either pass a full {@link Config}
6
- * as `config`, or set `configPath` pointing at a module whose default export
7
- * is a `Config` (supports `.ts`, `.mts`, `.js`, `.mjs` via jiti).
8
- */
9
- interface AddonOptions {
10
- /** Inline swatchbook config. Mutually exclusive with `configPath`. */
11
- config?: Config;
12
- /** Path to a config module, relative to the Storybook `configDir`. */
13
- configPath?: string;
14
- /**
15
- * Display-side integrations that plug into the addon's Vite plugin.
16
- * Each integration typically contributes a virtual module the
17
- * preview imports — e.g. the Tailwind integration serves
18
- * `virtual:swatchbook/tailwind.css`. The addon itself is
19
- * tool-agnostic; integrations ship as separate packages.
20
- */
21
- integrations?: SwatchbookIntegration[];
22
- }
23
- //#endregion
24
- export { AddonOptions as t };
25
- //# sourceMappingURL=options-Bcekz7uL.d.mts.map