@unpunnyfuns/swatchbook-addon 0.6.2 → 0.8.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/{constants-0XmrhOl_.mjs → constants-B3vYm529.mjs} +12 -2
- package/dist/{constants-0XmrhOl_.mjs.map → constants-B3vYm529.mjs.map} +1 -1
- package/dist/index.mjs +2 -2
- package/dist/manager.mjs +52 -217
- package/dist/manager.mjs.map +1 -1
- package/dist/preset.mjs +83 -43
- package/dist/preset.mjs.map +1 -1
- package/dist/{preview-C91uAAG-.mjs → preview-hz8hF3q0.mjs} +33 -2
- package/dist/preview-hz8hF3q0.mjs.map +1 -0
- package/dist/preview.mjs +1 -1
- package/package.json +4 -3
- package/dist/preview-C91uAAG-.mjs.map +0 -1
|
@@ -23,7 +23,17 @@ const INIT_REQUEST_EVENT = "swatchbook/init-request";
|
|
|
23
23
|
* inside the preview iframe close the popover — a plain document-level
|
|
24
24
|
* listener on the manager can't see iframe events. */
|
|
25
25
|
const PREVIEW_MOUSEDOWN_EVENT = "swatchbook/preview-mousedown";
|
|
26
|
+
/** Channel event: preview → blocks, carries the fresh virtual-module
|
|
27
|
+
* payload after a dev-time token refresh so blocks can re-render in
|
|
28
|
+
* place without a full iframe reload. Fired by the preview in response
|
|
29
|
+
* to the `HMR_EVENT` below. */
|
|
30
|
+
const TOKENS_UPDATED_EVENT = "swatchbook/tokens-updated";
|
|
31
|
+
/** Custom Vite HMR event: plugin → preview. Preview forwards it to the
|
|
32
|
+
* Storybook channel as {@link TOKENS_UPDATED_EVENT} so blocks can
|
|
33
|
+
* update their snapshot. Kept distinct from the channel event so the
|
|
34
|
+
* plugin doesn't need a Storybook-channel dependency. */
|
|
35
|
+
const HMR_EVENT = "swatchbook/tokens-updated";
|
|
26
36
|
//#endregion
|
|
27
|
-
export {
|
|
37
|
+
export { HMR_EVENT as a, PARAM_KEY as c, STYLE_ELEMENT_ID as d, TOKENS_UPDATED_EVENT as f, GLOBAL_KEY as i, PREVIEW_MOUSEDOWN_EVENT as l, VIRTUAL_MODULE_ID as m, AXES_GLOBAL_KEY as n, INIT_EVENT as o, TOOL_ID as p, COLOR_FORMAT_GLOBAL_KEY as r, INIT_REQUEST_EVENT as s, ADDON_ID as t, RESOLVED_VIRTUAL_MODULE_ID as u };
|
|
28
38
|
|
|
29
|
-
//# sourceMappingURL=constants-
|
|
39
|
+
//# sourceMappingURL=constants-B3vYm529.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants-
|
|
1
|
+
{"version":3,"file":"constants-B3vYm529.mjs","names":[],"sources":["../src/constants.ts"],"sourcesContent":["export const ADDON_ID = 'swatchbook';\nexport const TOOL_ID = `${ADDON_ID}/theme-switcher`;\nexport const PARAM_KEY = 'swatchbook';\n/** Canonical active-theme global: composed permutation ID (string). Read by toolbar, panel, blocks. */\nexport const GLOBAL_KEY = 'swatchbookTheme';\n/** Tuple companion: `Record<axisName, contextName>`. Optional — when present, takes precedence over the string global. */\nexport const AXES_GLOBAL_KEY = 'swatchbookAxes';\n/** Display-only color format for blocks (`hex` | `rgb` | `hsl` | `oklch` | `raw`). Emitted CSS is unaffected. */\nexport const COLOR_FORMAT_GLOBAL_KEY = 'swatchbookColorFormat';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:swatchbook/tokens';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\nexport const STYLE_ELEMENT_ID = 'swatchbook-tokens';\n\n/** Channel event: preview → manager, carries theme list + mode. */\nexport const INIT_EVENT = 'swatchbook/init';\n/** Channel event: manager → preview, asks preview to re-emit INIT_EVENT.\n * Covers the race where the manager subscribes after the preview's\n * initial broadcast — without it the toolbar stays in \"loading…\" until\n * the user triggers anything that re-fires INIT_EVENT. */\nexport const INIT_REQUEST_EVENT = 'swatchbook/init-request';\n/** Channel event: preview → manager, fires once per `mousedown` on the\n * preview document. The toolbar popover listens for it so clicks landing\n * inside the preview iframe close the popover — a plain document-level\n * listener on the manager can't see iframe events. */\nexport const PREVIEW_MOUSEDOWN_EVENT = 'swatchbook/preview-mousedown';\n\n/** Channel event: preview → blocks, carries the fresh virtual-module\n * payload after a dev-time token refresh so blocks can re-render in\n * place without a full iframe reload. Fired by the preview in response\n * to the `HMR_EVENT` below. */\nexport const TOKENS_UPDATED_EVENT = 'swatchbook/tokens-updated';\n\n/** Custom Vite HMR event: plugin → preview. Preview forwards it to the\n * Storybook channel as {@link TOKENS_UPDATED_EVENT} so blocks can\n * update their snapshot. Kept distinct from the channel event so the\n * plugin doesn't need a Storybook-channel dependency. */\nexport const HMR_EVENT = 'swatchbook/tokens-updated';\n"],"mappings":";AAAA,MAAa,WAAW;AACxB,MAAa,UAAU,GAAG,SAAS;AACnC,MAAa,YAAY;;AAEzB,MAAa,aAAa;;AAE1B,MAAa,kBAAkB;;AAE/B,MAAa,0BAA0B;AAEvC,MAAa,oBAAoB;AACjC,MAAa,6BAA6B,KAAK;AAE/C,MAAa,mBAAmB;;AAGhC,MAAa,aAAa;;;;;AAK1B,MAAa,qBAAqB;;;;;AAKlC,MAAa,0BAA0B;;;;;AAMvC,MAAa,uBAAuB;;;;;AAMpC,MAAa,YAAY"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as preview_exports } from "./preview-
|
|
2
|
-
import {
|
|
1
|
+
import { i as preview_exports } from "./preview-hz8hF3q0.mjs";
|
|
2
|
+
import { c as PARAM_KEY, i as GLOBAL_KEY, m as VIRTUAL_MODULE_ID, n as AXES_GLOBAL_KEY, t as ADDON_ID } from "./constants-B3vYm529.mjs";
|
|
3
3
|
import { definePreviewAddon } from "storybook/internal/csf";
|
|
4
4
|
//#region src/index.ts
|
|
5
5
|
/**
|
package/dist/manager.mjs
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as GLOBAL_KEY, l as PREVIEW_MOUSEDOWN_EVENT, n as AXES_GLOBAL_KEY, o as INIT_EVENT, p as TOOL_ID, r as COLOR_FORMAT_GLOBAL_KEY, s as INIT_REQUEST_EVENT, t as ADDON_ID } from "./constants-B3vYm529.mjs";
|
|
2
2
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { ThemeSwitcher } from "@unpunnyfuns/swatchbook-switcher";
|
|
3
4
|
import { IconButton, WithTooltipPure } from "storybook/internal/components";
|
|
4
5
|
import { addons, types, useGlobals, useStorybookApi } from "storybook/manager-api";
|
|
6
|
+
//#region src/ColorFormatSelector.tsx
|
|
7
|
+
const COLOR_FORMAT_OPTIONS = [
|
|
8
|
+
{
|
|
9
|
+
id: "hex",
|
|
10
|
+
label: "Hex"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "rgb",
|
|
14
|
+
label: "RGB"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "hsl",
|
|
18
|
+
label: "HSL"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "oklch",
|
|
22
|
+
label: "OKLCH"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "raw",
|
|
26
|
+
label: "Raw (JSON)"
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
const h$1 = React.createElement;
|
|
30
|
+
function ColorFormatSelector({ active, onSelect }) {
|
|
31
|
+
return h$1("div", null, h$1("div", { className: "sb-switcher__section-label" }, "Color format"), h$1("div", { className: "sb-switcher__section-body" }, ...COLOR_FORMAT_OPTIONS.map((opt) => h$1("button", {
|
|
32
|
+
key: `color-format/${opt.id}`,
|
|
33
|
+
type: "button",
|
|
34
|
+
onClick: () => onSelect(opt.id),
|
|
35
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
36
|
+
className: opt.id === active ? "sb-switcher__pill sb-switcher__pill--active" : "sb-switcher__pill"
|
|
37
|
+
}, opt.label))));
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
5
40
|
//#region src/manager.tsx
|
|
6
41
|
/**
|
|
7
42
|
* Use explicit `React.createElement` rather than JSX so the manager bundle
|
|
@@ -10,6 +45,10 @@ import { addons, types, useGlobals, useStorybookApi } from "storybook/manager-ap
|
|
|
10
45
|
* always part of that exposure, which breaks JSX with
|
|
11
46
|
* "Cannot read properties of undefined (reading 'recentlyCreatedOwnerStacks')".
|
|
12
47
|
* Mirrors the pattern `@storybook/addon-a11y` uses in its manager.
|
|
48
|
+
*
|
|
49
|
+
* The imported `<ThemeSwitcher>` from `@unpunnyfuns/swatchbook-switcher`
|
|
50
|
+
* compiles with classic JSX (`React.createElement`) specifically so it
|
|
51
|
+
* survives embedding in the manager bundle the same way.
|
|
13
52
|
*/
|
|
14
53
|
const h = React.createElement;
|
|
15
54
|
const EMPTY_AXES = [];
|
|
@@ -51,19 +90,9 @@ function defaultTupleFor(axes) {
|
|
|
51
90
|
return out;
|
|
52
91
|
}
|
|
53
92
|
/**
|
|
54
|
-
* Treat the `{ name: 'theme', source: 'synthetic' }` axis — the one core
|
|
55
|
-
* fabricates for single-theme projects with no resolver — as a special case
|
|
56
|
-
* that uses the label "Theme" instead of the axis name. Authored single-axis
|
|
57
|
-
* resolvers keep their real axis name (e.g. `mode`).
|
|
58
|
-
*/
|
|
59
|
-
function displayLabelFor(axis) {
|
|
60
|
-
if (axis.source === "synthetic" && axis.name === "theme") return "Theme";
|
|
61
|
-
return axis.name;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
93
|
* Compose a preset's sanitized partial tuple with the axis defaults, so
|
|
65
94
|
* applying a preset that only names some axes leaves the omitted ones at
|
|
66
|
-
* their defaults (not blank).
|
|
95
|
+
* their defaults (not blank). Mirrors the preview decorator's own fallback
|
|
67
96
|
* logic so what the toolbar sends out is what the decorator honors.
|
|
68
97
|
*/
|
|
69
98
|
function presetTuple(preset, axes, defaults) {
|
|
@@ -74,190 +103,6 @@ function presetTuple(preset, axes, defaults) {
|
|
|
74
103
|
}
|
|
75
104
|
return out;
|
|
76
105
|
}
|
|
77
|
-
function tuplesEqual(a, b, axes) {
|
|
78
|
-
for (const axis of axes) if (a[axis.name] !== b[axis.name]) return false;
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
const SECTION_LABEL_STYLE = {
|
|
82
|
-
padding: "8px 12px 4px",
|
|
83
|
-
fontSize: 11,
|
|
84
|
-
textTransform: "uppercase",
|
|
85
|
-
letterSpacing: .5,
|
|
86
|
-
opacity: .6
|
|
87
|
-
};
|
|
88
|
-
const SECTION_BODY_STYLE = {
|
|
89
|
-
padding: "0 12px 10px",
|
|
90
|
-
display: "flex",
|
|
91
|
-
flexWrap: "wrap",
|
|
92
|
-
gap: 4
|
|
93
|
-
};
|
|
94
|
-
const AXIS_ROW_STYLE = {
|
|
95
|
-
padding: "0 12px 10px",
|
|
96
|
-
display: "grid",
|
|
97
|
-
gridTemplateColumns: "max-content 1fr",
|
|
98
|
-
columnGap: 12,
|
|
99
|
-
rowGap: 4,
|
|
100
|
-
alignItems: "center"
|
|
101
|
-
};
|
|
102
|
-
const AXIS_LABEL_STYLE = {
|
|
103
|
-
fontSize: 12,
|
|
104
|
-
fontWeight: 600,
|
|
105
|
-
opacity: .85
|
|
106
|
-
};
|
|
107
|
-
const AXIS_PILLS_STYLE = {
|
|
108
|
-
display: "flex",
|
|
109
|
-
flexWrap: "wrap",
|
|
110
|
-
gap: 4
|
|
111
|
-
};
|
|
112
|
-
const OPTION_PILL_BASE = {
|
|
113
|
-
padding: "3px 8px",
|
|
114
|
-
borderRadius: 4,
|
|
115
|
-
fontSize: 12,
|
|
116
|
-
lineHeight: "18px",
|
|
117
|
-
borderWidth: 1,
|
|
118
|
-
borderStyle: "solid",
|
|
119
|
-
borderColor: "transparent",
|
|
120
|
-
background: "transparent",
|
|
121
|
-
cursor: "pointer",
|
|
122
|
-
color: "inherit",
|
|
123
|
-
outline: "none",
|
|
124
|
-
boxShadow: "none"
|
|
125
|
-
};
|
|
126
|
-
const OPTION_PILL_ACTIVE = {
|
|
127
|
-
...OPTION_PILL_BASE,
|
|
128
|
-
fontWeight: 600,
|
|
129
|
-
background: "rgba(0, 122, 255, 0.12)",
|
|
130
|
-
borderColor: "rgba(0, 122, 255, 0.45)"
|
|
131
|
-
};
|
|
132
|
-
const PRESET_PILL_MODIFIED = {
|
|
133
|
-
display: "inline-block",
|
|
134
|
-
width: 6,
|
|
135
|
-
height: 6,
|
|
136
|
-
marginLeft: 6,
|
|
137
|
-
borderRadius: "50%",
|
|
138
|
-
background: "currentColor",
|
|
139
|
-
opacity: .6,
|
|
140
|
-
verticalAlign: "middle"
|
|
141
|
-
};
|
|
142
|
-
const DIVIDER_STYLE = {
|
|
143
|
-
height: 1,
|
|
144
|
-
background: "currentColor",
|
|
145
|
-
opacity: .1,
|
|
146
|
-
margin: "2px 8px"
|
|
147
|
-
};
|
|
148
|
-
function OptionPill({ label, active, title, onClick, trailing }) {
|
|
149
|
-
return h("button", {
|
|
150
|
-
type: "button",
|
|
151
|
-
title,
|
|
152
|
-
onClick,
|
|
153
|
-
onMouseDown: (event) => event.preventDefault(),
|
|
154
|
-
style: active ? OPTION_PILL_ACTIVE : OPTION_PILL_BASE
|
|
155
|
-
}, label, trailing ?? null);
|
|
156
|
-
}
|
|
157
|
-
function PresetsSection({ presets, axes, defaults, activeTuple, lastApplied, onApply }) {
|
|
158
|
-
return h("div", null, h("div", { style: SECTION_LABEL_STYLE }, "Presets"), h("div", { style: SECTION_BODY_STYLE }, ...presets.map((preset) => {
|
|
159
|
-
const matches = tuplesEqual(presetTuple(preset, axes, defaults), activeTuple, axes);
|
|
160
|
-
const modified = !matches && preset.name === lastApplied;
|
|
161
|
-
const title = preset.description ? `${preset.name} — ${preset.description}` : preset.name;
|
|
162
|
-
return h(OptionPill, {
|
|
163
|
-
key: `${TOOL_ID}/preset/${preset.name}`,
|
|
164
|
-
label: preset.name,
|
|
165
|
-
active: matches,
|
|
166
|
-
title,
|
|
167
|
-
onClick: () => onApply(preset),
|
|
168
|
-
trailing: modified ? h("span", {
|
|
169
|
-
"aria-hidden": true,
|
|
170
|
-
style: PRESET_PILL_MODIFIED
|
|
171
|
-
}) : null
|
|
172
|
-
});
|
|
173
|
-
})));
|
|
174
|
-
}
|
|
175
|
-
function AxisSection({ axis, active, onSelect }) {
|
|
176
|
-
const label = displayLabelFor(axis);
|
|
177
|
-
return h("div", { style: AXIS_ROW_STYLE }, h("div", {
|
|
178
|
-
style: AXIS_LABEL_STYLE,
|
|
179
|
-
title: axis.description
|
|
180
|
-
}, label), h("div", { style: AXIS_PILLS_STYLE }, ...axis.contexts.map((ctx) => h(OptionPill, {
|
|
181
|
-
key: `${TOOL_ID}/${axis.name}/${ctx}`,
|
|
182
|
-
label: ctx,
|
|
183
|
-
active: ctx === active,
|
|
184
|
-
onClick: () => onSelect(ctx)
|
|
185
|
-
}))));
|
|
186
|
-
}
|
|
187
|
-
const COLOR_FORMAT_OPTIONS = [
|
|
188
|
-
{
|
|
189
|
-
id: "hex",
|
|
190
|
-
label: "Hex"
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
id: "rgb",
|
|
194
|
-
label: "RGB"
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
id: "hsl",
|
|
198
|
-
label: "HSL"
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
id: "oklch",
|
|
202
|
-
label: "OKLCH"
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
id: "raw",
|
|
206
|
-
label: "Raw (JSON)"
|
|
207
|
-
}
|
|
208
|
-
];
|
|
209
|
-
function ColorFormatSection({ active, onSelect }) {
|
|
210
|
-
return h("div", null, h("div", { style: SECTION_LABEL_STYLE }, "Color format"), h("div", { style: SECTION_BODY_STYLE }, ...COLOR_FORMAT_OPTIONS.map((opt) => h(OptionPill, {
|
|
211
|
-
key: `${TOOL_ID}/color-format/${opt.id}`,
|
|
212
|
-
label: opt.label,
|
|
213
|
-
active: opt.id === active,
|
|
214
|
-
onClick: () => onSelect(opt.id)
|
|
215
|
-
}))));
|
|
216
|
-
}
|
|
217
|
-
function PopoverBody(props) {
|
|
218
|
-
const { axes, presets, defaults, activeTuple, activeColorFormat, lastApplied, onApplyPreset, onSelectAxis, onSelectColorFormat, onKeyDown } = props;
|
|
219
|
-
const sections = [];
|
|
220
|
-
if (presets.length > 0) sections.push(h(PresetsSection, {
|
|
221
|
-
key: "presets",
|
|
222
|
-
presets,
|
|
223
|
-
axes,
|
|
224
|
-
defaults,
|
|
225
|
-
activeTuple,
|
|
226
|
-
lastApplied,
|
|
227
|
-
onApply: onApplyPreset
|
|
228
|
-
}), h("div", {
|
|
229
|
-
key: "presets-divider",
|
|
230
|
-
style: DIVIDER_STYLE
|
|
231
|
-
}));
|
|
232
|
-
axes.forEach((axis, idx) => {
|
|
233
|
-
sections.push(h(AxisSection, {
|
|
234
|
-
key: `axis-${axis.name}`,
|
|
235
|
-
axis,
|
|
236
|
-
active: activeTuple[axis.name] ?? axis.default,
|
|
237
|
-
onSelect: (next) => onSelectAxis(axis.name, next)
|
|
238
|
-
}));
|
|
239
|
-
if (idx === axes.length - 1) sections.push(h("div", {
|
|
240
|
-
key: "axes-divider",
|
|
241
|
-
style: DIVIDER_STYLE
|
|
242
|
-
}));
|
|
243
|
-
});
|
|
244
|
-
sections.push(h(ColorFormatSection, {
|
|
245
|
-
key: "color-format",
|
|
246
|
-
active: activeColorFormat,
|
|
247
|
-
onSelect: onSelectColorFormat
|
|
248
|
-
}));
|
|
249
|
-
return h("div", {
|
|
250
|
-
role: "menu",
|
|
251
|
-
tabIndex: -1,
|
|
252
|
-
onKeyDown,
|
|
253
|
-
style: {
|
|
254
|
-
minWidth: 260,
|
|
255
|
-
padding: "4px 0",
|
|
256
|
-
outline: "none"
|
|
257
|
-
},
|
|
258
|
-
"data-testid": "swatchbook-toolbar-popover"
|
|
259
|
-
}, ...sections);
|
|
260
|
-
}
|
|
261
106
|
function AxesToolbar() {
|
|
262
107
|
const [globals, updateGlobals] = useGlobals();
|
|
263
108
|
const api = useStorybookApi();
|
|
@@ -331,19 +176,6 @@ function AxesToolbar() {
|
|
|
331
176
|
]);
|
|
332
177
|
useEffect(() => {
|
|
333
178
|
if (presets.length === 0) return;
|
|
334
|
-
/**
|
|
335
|
-
* `alt+shift+C` cycles through the project's presets, applying the
|
|
336
|
-
* next one each press. When no preset has been applied yet, starts
|
|
337
|
-
* from the first. Stays out of Storybook's core shortcut surface —
|
|
338
|
-
* the earlier default (`alt+T`) collided with the built-in "toggle
|
|
339
|
-
* addon panel" binding on some platforms. Users can still rebind it
|
|
340
|
-
* through Storybook's own keyboard-shortcuts panel.
|
|
341
|
-
*
|
|
342
|
-
* Only registers when the project actually defines presets. For
|
|
343
|
-
* projects without presets the cycle would have nothing to cycle
|
|
344
|
-
* through, so the shortcut disappears entirely rather than sitting
|
|
345
|
-
* dead in the menu.
|
|
346
|
-
*/
|
|
347
179
|
api.setAddonShortcut(ADDON_ID, {
|
|
348
180
|
label: `Cycle swatchbook presets (${presets.length})`,
|
|
349
181
|
defaultShortcut: [
|
|
@@ -395,7 +227,7 @@ function AxesToolbar() {
|
|
|
395
227
|
const target = e.target;
|
|
396
228
|
if (!(target instanceof Element)) return;
|
|
397
229
|
if (bodyRef.current?.contains(target)) return;
|
|
398
|
-
if (target.closest("[data-testid=\"swatchbook-
|
|
230
|
+
if (target.closest("[data-testid=\"swatchbook-switcher\"]")) return;
|
|
399
231
|
setOpen(false);
|
|
400
232
|
};
|
|
401
233
|
/**
|
|
@@ -424,17 +256,20 @@ function AxesToolbar() {
|
|
|
424
256
|
active: open,
|
|
425
257
|
onClick: () => setOpen((prev) => !prev)
|
|
426
258
|
}, h(SwatchbookIcon));
|
|
427
|
-
const tooltipBody = h(
|
|
259
|
+
const tooltipBody = h(ThemeSwitcher, {
|
|
428
260
|
axes,
|
|
429
261
|
presets,
|
|
430
|
-
|
|
262
|
+
themes,
|
|
431
263
|
activeTuple,
|
|
432
|
-
|
|
264
|
+
defaults,
|
|
433
265
|
lastApplied,
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
266
|
+
onAxisChange: setAxis,
|
|
267
|
+
onPresetApply: applyPreset,
|
|
268
|
+
onKeyDown: handleKeyDown,
|
|
269
|
+
footer: h(ColorFormatSelector, {
|
|
270
|
+
active: activeColorFormat,
|
|
271
|
+
onSelect: (next) => updateGlobals({ [COLOR_FORMAT_GLOBAL_KEY]: next })
|
|
272
|
+
})
|
|
438
273
|
});
|
|
439
274
|
return h("span", {
|
|
440
275
|
ref: bodyRef,
|
package/dist/manager.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.mjs","names":[],"sources":["../src/manager.tsx"],"sourcesContent":["import 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 type VirtualTheme as ThemeEntry,\n} from '#/channel-types.ts';\nimport {\n ADDON_ID,\n AXES_GLOBAL_KEY,\n COLOR_FORMAT_GLOBAL_KEY,\n 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 */\nconst h = React.createElement;\n\nconst EMPTY_AXES: readonly AxisEntry[] = [];\nconst EMPTY_PRESETS: readonly PresetEntry[] = [];\nconst EMPTY_THEMES: readonly ThemeEntry[] = [];\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 tupleMatchesInput(\n tuple: Readonly<Record<string, string>>,\n input: Readonly<Record<string, string>>,\n): boolean {\n const keys = Object.keys(input);\n if (keys.length === 0) return false;\n return keys.every((k) => input[k] === tuple[k]);\n}\n\nfunction composedNameFor(\n tuple: Readonly<Record<string, string>>,\n themes: readonly ThemeEntry[],\n fallback: string,\n): string {\n const match = themes.find((t) => tupleMatchesInput(tuple, t.input));\n return match?.name ?? fallback;\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 * Treat the `{ name: 'theme', source: 'synthetic' }` axis — the one core\n * fabricates for single-theme projects with no resolver — as a special case\n * that uses the label \"Theme\" instead of the axis name. Authored single-axis\n * resolvers keep their real axis name (e.g. `mode`).\n */\nfunction displayLabelFor(axis: AxisEntry): string {\n if (axis.source === 'synthetic' && axis.name === 'theme') return 'Theme';\n return axis.name;\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). Matches 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 tuplesEqual(\n a: Readonly<Record<string, string>>,\n b: Readonly<Record<string, string>>,\n axes: readonly AxisEntry[],\n): boolean {\n for (const axis of axes) {\n if (a[axis.name] !== b[axis.name]) return false;\n }\n return true;\n}\n\nconst SECTION_LABEL_STYLE: React.CSSProperties = {\n padding: '8px 12px 4px',\n fontSize: 11,\n textTransform: 'uppercase',\n letterSpacing: 0.5,\n opacity: 0.6,\n};\n\nconst SECTION_BODY_STYLE: React.CSSProperties = {\n padding: '0 12px 10px',\n display: 'flex',\n flexWrap: 'wrap',\n gap: 4,\n};\n\nconst AXIS_ROW_STYLE: React.CSSProperties = {\n padding: '0 12px 10px',\n display: 'grid',\n gridTemplateColumns: 'max-content 1fr',\n columnGap: 12,\n rowGap: 4,\n alignItems: 'center',\n};\n\nconst AXIS_LABEL_STYLE: React.CSSProperties = {\n fontSize: 12,\n fontWeight: 600,\n opacity: 0.85,\n};\n\nconst AXIS_PILLS_STYLE: React.CSSProperties = {\n display: 'flex',\n flexWrap: 'wrap',\n gap: 4,\n};\n\nconst OPTION_PILL_BASE: React.CSSProperties = {\n padding: '3px 8px',\n borderRadius: 4,\n fontSize: 12,\n lineHeight: '18px',\n // Longhand border properties (not the `border` shorthand) so\n // active → inactive only updates `borderColor`'s value instead of\n // *removing* the key from inline style. Removing it lets Storybook's\n // theme paint a stray border-color on the previously-selected pill.\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: 'transparent',\n background: 'transparent',\n cursor: 'pointer',\n color: 'inherit',\n outline: 'none',\n boxShadow: 'none',\n};\n\nconst OPTION_PILL_ACTIVE: React.CSSProperties = {\n ...OPTION_PILL_BASE,\n fontWeight: 600,\n background: 'rgba(0, 122, 255, 0.12)',\n borderColor: 'rgba(0, 122, 255, 0.45)',\n};\n\nconst PRESET_PILL_MODIFIED: React.CSSProperties = {\n display: 'inline-block',\n width: 6,\n height: 6,\n marginLeft: 6,\n borderRadius: '50%',\n background: 'currentColor',\n opacity: 0.6,\n verticalAlign: 'middle',\n};\n\nconst DIVIDER_STYLE: React.CSSProperties = {\n height: 1,\n background: 'currentColor',\n opacity: 0.1,\n margin: '2px 8px',\n};\n\ninterface OptionPillProps {\n label: string;\n active: boolean;\n title?: string;\n onClick: () => void;\n trailing?: ReactElement | null;\n}\n\nfunction OptionPill({ label, active, title, onClick, trailing }: OptionPillProps): ReactElement {\n return h(\n 'button',\n {\n type: 'button',\n title,\n onClick,\n // Skip focus on mouse click so Storybook's `:focus` border-color\n // theming doesn't stick on the previously-clicked pill. Keyboard\n // tabbing still lands focus normally — preventDefault on mousedown\n // only blocks the implicit focus-on-click behavior.\n onMouseDown: (event) => event.preventDefault(),\n style: active ? OPTION_PILL_ACTIVE : OPTION_PILL_BASE,\n },\n label,\n trailing ?? null,\n );\n}\n\ninterface PresetsSectionProps {\n presets: readonly PresetEntry[];\n axes: readonly AxisEntry[];\n defaults: Readonly<Record<string, string>>;\n activeTuple: Readonly<Record<string, string>>;\n lastApplied: string | null;\n onApply: (preset: PresetEntry) => void;\n}\n\nfunction PresetsSection({\n presets,\n axes,\n defaults,\n activeTuple,\n lastApplied,\n onApply,\n}: PresetsSectionProps): ReactElement {\n return h(\n 'div',\n null,\n h('div', { style: SECTION_LABEL_STYLE }, 'Presets'),\n h(\n 'div',\n { style: SECTION_BODY_STYLE },\n ...presets.map((preset) => {\n const tuple = presetTuple(preset, axes, defaults);\n const matches = tuplesEqual(tuple, activeTuple, axes);\n const modified = !matches && preset.name === lastApplied;\n const title = preset.description ? `${preset.name} — ${preset.description}` : preset.name;\n return h(OptionPill, {\n key: `${TOOL_ID}/preset/${preset.name}`,\n label: preset.name,\n active: matches,\n title,\n onClick: () => onApply(preset),\n trailing: modified\n ? h('span', { 'aria-hidden': true, style: PRESET_PILL_MODIFIED })\n : null,\n });\n }),\n ),\n );\n}\n\ninterface AxisSectionProps {\n axis: AxisEntry;\n active: string;\n onSelect: (next: string) => void;\n}\n\nfunction AxisSection({ axis, active, onSelect }: AxisSectionProps): ReactElement {\n const label = displayLabelFor(axis);\n return h(\n 'div',\n { style: AXIS_ROW_STYLE },\n h('div', { style: AXIS_LABEL_STYLE, title: axis.description }, label),\n h(\n 'div',\n { style: AXIS_PILLS_STYLE },\n ...axis.contexts.map((ctx) =>\n h(OptionPill, {\n key: `${TOOL_ID}/${axis.name}/${ctx}`,\n label: ctx,\n active: ctx === active,\n onClick: () => onSelect(ctx),\n }),\n ),\n ),\n );\n}\n\nconst COLOR_FORMAT_OPTIONS: readonly { id: string; 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\ninterface ColorFormatSectionProps {\n active: string;\n onSelect: (next: string) => void;\n}\n\nfunction ColorFormatSection({ active, onSelect }: ColorFormatSectionProps): ReactElement {\n return h(\n 'div',\n null,\n h('div', { style: SECTION_LABEL_STYLE }, 'Color format'),\n h(\n 'div',\n { style: SECTION_BODY_STYLE },\n ...COLOR_FORMAT_OPTIONS.map((opt) =>\n h(OptionPill, {\n key: `${TOOL_ID}/color-format/${opt.id}`,\n label: opt.label,\n active: opt.id === active,\n onClick: () => onSelect(opt.id),\n }),\n ),\n ),\n );\n}\n\ninterface PopoverBodyProps {\n axes: readonly AxisEntry[];\n presets: readonly PresetEntry[];\n defaults: Readonly<Record<string, string>>;\n activeTuple: Readonly<Record<string, string>>;\n activeColorFormat: string;\n lastApplied: string | null;\n onApplyPreset: (preset: PresetEntry) => void;\n onSelectAxis: (axisName: string, next: string) => void;\n onSelectColorFormat: (next: string) => void;\n onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void;\n}\n\nfunction PopoverBody(props: PopoverBodyProps): ReactElement {\n const {\n axes,\n presets,\n defaults,\n activeTuple,\n activeColorFormat,\n lastApplied,\n onApplyPreset,\n onSelectAxis,\n onSelectColorFormat,\n onKeyDown,\n } = props;\n const sections: ReactElement[] = [];\n if (presets.length > 0) {\n sections.push(\n h(PresetsSection, {\n key: 'presets',\n presets,\n axes,\n defaults,\n activeTuple,\n lastApplied,\n onApply: onApplyPreset,\n }),\n h('div', { key: 'presets-divider', style: DIVIDER_STYLE }),\n );\n }\n axes.forEach((axis, idx) => {\n sections.push(\n h(AxisSection, {\n key: `axis-${axis.name}`,\n axis,\n active: activeTuple[axis.name] ?? axis.default,\n onSelect: (next) => onSelectAxis(axis.name, next),\n }),\n );\n if (idx === axes.length - 1) {\n sections.push(h('div', { key: 'axes-divider', style: DIVIDER_STYLE }));\n }\n });\n sections.push(\n h(ColorFormatSection, {\n key: 'color-format',\n active: activeColorFormat,\n onSelect: onSelectColorFormat,\n }),\n );\n return h(\n 'div',\n {\n role: 'menu',\n tabIndex: -1,\n onKeyDown,\n style: { minWidth: 260, padding: '4px 0', outline: 'none' },\n 'data-testid': 'swatchbook-toolbar-popover',\n },\n ...sections,\n );\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 themes = payload?.themes ?? EMPTY_THEMES;\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) ?? 'hex';\n\n const activeTuple = useMemo<Record<string, string>>(() => {\n const out: Record<string, string> = { ...defaults };\n if (globalTuple) {\n for (const axis of axes) {\n const candidate = globalTuple[axis.name];\n if (candidate !== undefined && axis.contexts.includes(candidate)) {\n out[axis.name] = candidate;\n }\n }\n }\n return out;\n }, [axes, defaults, globalTuple]);\n\n const setAxis = useCallback(\n (axisName: string, next: string): void => {\n const tuple: Record<string, string> = { ...activeTuple, [axisName]: next };\n const fallback = payload?.defaultTheme ?? themes[0]?.name ?? '';\n const composed = composedNameFor(tuple, themes, fallback);\n updateGlobals({ [AXES_GLOBAL_KEY]: tuple, [GLOBAL_KEY]: composed });\n },\n [activeTuple, themes, payload?.defaultTheme, updateGlobals],\n );\n\n const applyPreset = useCallback(\n (preset: PresetEntry): void => {\n const tuple = presetTuple(preset, axes, defaults);\n const fallback = payload?.defaultTheme ?? themes[0]?.name ?? '';\n const composed = composedNameFor(tuple, themes, fallback);\n updateGlobals({ [AXES_GLOBAL_KEY]: tuple, [GLOBAL_KEY]: composed });\n setLastApplied(preset.name);\n },\n [axes, defaults, themes, payload?.defaultTheme, updateGlobals],\n );\n\n useEffect(() => {\n if (presets.length === 0) return;\n /**\n * `alt+shift+C` cycles through the project's presets, applying the\n * next one each press. When no preset has been applied yet, starts\n * from the first. Stays out of Storybook's core shortcut surface —\n * the earlier default (`alt+T`) collided with the built-in \"toggle\n * addon panel\" binding on some platforms. Users can still rebind it\n * through Storybook's own keyboard-shortcuts panel.\n *\n * Only registers when the project actually defines presets. For\n * projects without presets the cycle would have nothing to cycle\n * through, so the shortcut disappears entirely rather than sitting\n * dead in the menu.\n */\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-toolbar-popover\"]')) 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 },\n h(SwatchbookIcon),\n );\n\n const tooltipBody = h(PopoverBody, {\n axes,\n presets,\n defaults,\n activeTuple,\n activeColorFormat,\n lastApplied,\n onApplyPreset: applyPreset,\n onSelectAxis: setAxis,\n onSelectColorFormat: (next: string) => updateGlobals({ [COLOR_FORMAT_GLOBAL_KEY]: next }),\n onKeyDown: handleKeyDown,\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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"}
|
|
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 type VirtualTheme as ThemeEntry,\n} from '#/channel-types.ts';\nimport {\n ADDON_ID,\n AXES_GLOBAL_KEY,\n COLOR_FORMAT_GLOBAL_KEY,\n 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_THEMES: readonly ThemeEntry[] = [];\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 tupleMatchesInput(\n tuple: Readonly<Record<string, string>>,\n input: Readonly<Record<string, string>>,\n): boolean {\n const keys = Object.keys(input);\n if (keys.length === 0) return false;\n return keys.every((k) => input[k] === tuple[k]);\n}\n\nfunction composedNameFor(\n tuple: Readonly<Record<string, string>>,\n themes: readonly ThemeEntry[],\n fallback: string,\n): string {\n const match = themes.find((t) => tupleMatchesInput(tuple, t.input));\n return match?.name ?? fallback;\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 themes = payload?.themes ?? EMPTY_THEMES;\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 const fallback = payload?.defaultTheme ?? themes[0]?.name ?? '';\n const composed = composedNameFor(tuple, themes, fallback);\n updateGlobals({ [AXES_GLOBAL_KEY]: tuple, [GLOBAL_KEY]: composed });\n },\n [activeTuple, themes, payload?.defaultTheme, updateGlobals],\n );\n\n const applyPreset = useCallback(\n (preset: PresetEntry): void => {\n const tuple = presetTuple(preset, axes, defaults);\n const fallback = payload?.defaultTheme ?? themes[0]?.name ?? '';\n const composed = composedNameFor(tuple, themes, fallback);\n updateGlobals({ [AXES_GLOBAL_KEY]: tuple, [GLOBAL_KEY]: composed });\n setLastApplied(preset.name);\n },\n [axes, defaults, themes, payload?.defaultTheme, 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 },\n h(SwatchbookIcon),\n );\n\n const tooltipBody = h(ThemeSwitcher, {\n axes,\n presets,\n themes,\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;;;;;;;;;;;;;;;;AC1BH,MAAM,IAAI,MAAM;AAEhB,MAAM,aAAmC,EAAE;AAC3C,MAAM,gBAAwC,EAAE;AAChD,MAAM,eAAsC,EAAE;;;;;;AAO9C,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,kBACP,OACA,OACS;CACT,MAAM,OAAO,OAAO,KAAK,MAAM;AAC/B,KAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAO,KAAK,OAAO,MAAM,MAAM,OAAO,MAAM,GAAG;;AAGjD,SAAS,gBACP,OACA,QACA,UACQ;AAER,QADc,OAAO,MAAM,MAAM,kBAAkB,OAAO,EAAE,MAAM,CAAC,EACrD,QAAQ;;AAGxB,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,SAAS,SAAS,UAAU;CAClC,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;EAE1E,MAAM,WAAW,gBAAgB,OAAO,QADvB,SAAS,gBAAgB,OAAO,IAAI,QAAQ,GACJ;AACzD,gBAAc;IAAG,kBAAkB;IAAQ,aAAa;GAAU,CAAC;IAErE;EAAC;EAAa;EAAQ,SAAS;EAAc;EAAc,CAC5D;CAED,MAAM,cAAc,aACjB,WAA8B;EAC7B,MAAM,QAAQ,YAAY,QAAQ,MAAM,SAAS;EAEjD,MAAM,WAAW,gBAAgB,OAAO,QADvB,SAAS,gBAAgB,OAAO,IAAI,QAAQ,GACJ;AACzD,gBAAc;IAAG,kBAAkB;IAAQ,aAAa;GAAU,CAAC;AACnE,iBAAe,OAAO,KAAK;IAE7B;EAAC;EAAM;EAAU;EAAQ,SAAS;EAAc;EAAc,CAC/D;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;EACxC,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"}
|
package/dist/preset.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as HMR_EVENT, u as RESOLVED_VIRTUAL_MODULE_ID } from "./constants-B3vYm529.mjs";
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, isAbsolute, resolve
|
|
3
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { loadProject, projectCss } from "@unpunnyfuns/swatchbook-core";
|
|
6
6
|
import { createJiti } from "jiti";
|
|
7
|
-
import
|
|
7
|
+
import { watch } from "node:fs";
|
|
8
|
+
import "picomatch";
|
|
8
9
|
//#region src/virtual/plugin.ts
|
|
9
10
|
/**
|
|
10
11
|
* Vite plugin that serves the virtual `virtual:swatchbook/tokens` module —
|
|
@@ -45,51 +46,90 @@ function swatchbookTokensPlugin({ config, cwd }) {
|
|
|
45
46
|
`export const cssVarPrefix = ${JSON.stringify(config.cssVarPrefix ?? "")};`
|
|
46
47
|
].join("\n");
|
|
47
48
|
},
|
|
48
|
-
configureServer(server) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
async configureServer(server) {
|
|
50
|
+
if (!project) await refresh();
|
|
51
|
+
/**
|
|
52
|
+
* Editors typically emit two or three filesystem events per save
|
|
53
|
+
* (atomic rename + rewrite + metadata). A 100 ms trailing debounce
|
|
54
|
+
* coalesces those into a single reload while staying well under
|
|
55
|
+
* user-perceptible latency.
|
|
56
|
+
*/
|
|
57
|
+
let pending = null;
|
|
58
|
+
const invalidate = () => {
|
|
59
|
+
if (pending) clearTimeout(pending);
|
|
60
|
+
pending = setTimeout(() => {
|
|
61
|
+
pending = null;
|
|
62
|
+
(async () => {
|
|
63
|
+
await refresh();
|
|
64
|
+
if (!project) return;
|
|
65
|
+
const tokenCount = Object.keys(project.themesResolved[project.themes[0]?.name ?? ""] ?? {}).length;
|
|
66
|
+
const diagCount = project.diagnostics.length;
|
|
67
|
+
server.config.logger.info(`\x1b[36m[swatchbook]\x1b[0m tokens reloaded — ${tokenCount} tokens, ${diagCount} diagnostic${diagCount === 1 ? "" : "s"}`, {
|
|
68
|
+
clear: false,
|
|
69
|
+
timestamp: true
|
|
70
|
+
});
|
|
71
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID);
|
|
72
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
73
|
+
/**
|
|
74
|
+
* Send the fresh snapshot as a custom HMR event instead of a
|
|
75
|
+
* full-reload. The preview subscribes and re-broadcasts to
|
|
76
|
+
* blocks via the Storybook channel so the React tree
|
|
77
|
+
* re-renders in place without losing toolbar / args / scroll
|
|
78
|
+
* state. Field shape matches the INIT_EVENT payload so the
|
|
79
|
+
* preview can hand it straight through.
|
|
80
|
+
*/
|
|
81
|
+
server.ws.send({
|
|
82
|
+
type: "custom",
|
|
83
|
+
event: HMR_EVENT,
|
|
84
|
+
data: {
|
|
85
|
+
axes: project.axes,
|
|
86
|
+
disabledAxes: project.disabledAxes,
|
|
87
|
+
presets: project.presets,
|
|
88
|
+
themes: project.themes,
|
|
89
|
+
defaultTheme: project.themes[0]?.name ?? null,
|
|
90
|
+
themesResolved: project.themesResolved,
|
|
91
|
+
diagnostics: project.diagnostics,
|
|
92
|
+
css,
|
|
93
|
+
cssVarPrefix: config.cssVarPrefix ?? ""
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
})();
|
|
97
|
+
}, 100);
|
|
56
98
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Watch each source file's *parent directory* rather than the file
|
|
101
|
+
* itself. File-level `fs.watch` is fragile: atomic-save editors
|
|
102
|
+
* unlink the old inode and write a new one, so the original
|
|
103
|
+
* watcher either fires a one-shot 'rename' and goes deaf, or on
|
|
104
|
+
* some platforms loops on ghost events for the old inode. Watching
|
|
105
|
+
* the dir sidesteps both — the dir inode is stable across the
|
|
106
|
+
* rename dance — and filename filtering keeps event volume low.
|
|
107
|
+
*
|
|
108
|
+
* Vite's `server.watcher` still wouldn't carry these events across
|
|
109
|
+
* pnpm symlink boundaries, so we keep running our own watchers.
|
|
110
|
+
*/
|
|
111
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
112
|
+
for (const file of project?.sourceFiles ?? []) {
|
|
113
|
+
const dir = dirname(file);
|
|
114
|
+
const set = byDir.get(dir) ?? /* @__PURE__ */ new Set();
|
|
115
|
+
set.add(basename(file));
|
|
116
|
+
byDir.set(dir, set);
|
|
117
|
+
}
|
|
118
|
+
const fileWatchers = [];
|
|
119
|
+
for (const [dir, names] of byDir) try {
|
|
120
|
+
const w = watch(dir, { persistent: false }, (eventType, filename) => {
|
|
121
|
+
if (!filename) return;
|
|
122
|
+
if (!names.has(filename)) return;
|
|
123
|
+
if (eventType === "change" || eventType === "rename") invalidate();
|
|
124
|
+
});
|
|
125
|
+
fileWatchers.push(w);
|
|
126
|
+
} catch {}
|
|
127
|
+
server.httpServer?.once("close", () => {
|
|
128
|
+
for (const w of fileWatchers) w.close();
|
|
66
129
|
});
|
|
67
130
|
}
|
|
68
131
|
};
|
|
69
132
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Collect the set of filesystem paths the dev server should watch for
|
|
72
|
-
* HMR. When `config.tokens` is set, use its globs (stripped to their
|
|
73
|
-
* base directories) — users opt in to broader watching this way. When
|
|
74
|
-
* absent, use the resolver file + every `$ref` target it pulled in, as
|
|
75
|
-
* tracked on `project.sourceFiles` — which stays correct as the resolver
|
|
76
|
-
* evolves without requiring a parallel `tokens` glob.
|
|
77
|
-
*/
|
|
78
|
-
/** @internal Exported for tests; not part of the public API. */
|
|
79
|
-
function collectWatchPaths(config, project, cwd) {
|
|
80
|
-
const paths = [];
|
|
81
|
-
if (config.tokens && config.tokens.length > 0) for (const glob of config.tokens) {
|
|
82
|
-
const { base } = picomatch.scan(glob);
|
|
83
|
-
paths.push(resolveFromCwd(base || ".", cwd));
|
|
84
|
-
}
|
|
85
|
-
else if (project?.sourceFiles) for (const file of project.sourceFiles) paths.push(dirname(file));
|
|
86
|
-
if (config.resolver) paths.push(resolveFromCwd(config.resolver, cwd));
|
|
87
|
-
return [...new Set(paths)];
|
|
88
|
-
}
|
|
89
|
-
function resolveFromCwd(p, cwd) {
|
|
90
|
-
if (isAbsolute(p)) return p;
|
|
91
|
-
return resolve(cwd, p);
|
|
92
|
-
}
|
|
93
133
|
//#endregion
|
|
94
134
|
//#region src/preset.ts
|
|
95
135
|
/**
|
package/dist/preset.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preset.mjs","names":["resolvePath"],"sources":["../src/virtual/plugin.ts","../src/preset.ts"],"sourcesContent":["import type { Config, Project } from '@unpunnyfuns/swatchbook-core';\nimport { loadProject, projectCss } from '@unpunnyfuns/swatchbook-core';\nimport { dirname, isAbsolute, resolve as resolvePath, sep } from 'node:path';\nimport picomatch from 'picomatch';\nimport type { Plugin } from 'vite';\nimport { RESOLVED_VIRTUAL_MODULE_ID, VIRTUAL_MODULE_ID } from '#/constants.ts';\n\nexport interface SwatchbookPluginOptions {\n config: Config;\n cwd: string;\n}\n\n/**\n * Vite plugin that serves the virtual `virtual:swatchbook/tokens` module —\n * a single source of truth for themes, 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({ config, cwd }: 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 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 return null;\n },\n\n load(id) {\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 themes = ${JSON.stringify(project.themes)};`,\n `export const defaultTheme = ${JSON.stringify(project.themes[0]?.name ?? null)};`,\n `export const themesResolved = ${JSON.stringify(project.themesResolved)};`,\n `export const diagnostics = ${JSON.stringify(project.diagnostics)};`,\n `export const css = ${JSON.stringify(css)};`,\n `export const cssVarPrefix = ${JSON.stringify(config.cssVarPrefix ?? '')};`,\n ].join('\\n');\n },\n\n configureServer(server) {\n const watchPaths = collectWatchPaths(config, project, cwd);\n for (const p of watchPaths) server.watcher.add(p);\n\n const invalidate = async (): Promise<void> => {\n await refresh();\n const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID);\n if (mod) server.moduleGraph.invalidateModule(mod);\n server.ws.send({ type: 'full-reload' });\n };\n\n const matches = (changed: string): boolean =>\n watchPaths.some((p) => changed === p || changed.startsWith(`${p}${sep}`));\n\n server.watcher.on('change', (changed) => {\n if (matches(changed)) void invalidate();\n });\n server.watcher.on('add', (changed) => {\n if (matches(changed)) void invalidate();\n });\n server.watcher.on('unlink', (changed) => {\n if (matches(changed)) void invalidate();\n });\n },\n };\n}\n\n/**\n * Collect the set of filesystem paths the dev server should watch for\n * HMR. When `config.tokens` is set, use its globs (stripped to their\n * base directories) — users opt in to broader watching this way. When\n * absent, use the resolver file + every `$ref` target it pulled in, as\n * tracked on `project.sourceFiles` — which stays correct as the resolver\n * evolves without requiring a parallel `tokens` glob.\n */\n/** @internal Exported for tests; not part of the public API. */\nexport function collectWatchPaths(\n config: Config,\n project: Project | undefined,\n cwd: string,\n): string[] {\n const paths: string[] = [];\n if (config.tokens && config.tokens.length > 0) {\n for (const glob of config.tokens) {\n // `picomatch.scan` yields the longest literal prefix before any glob\n // metachar, so it handles brace expansion, nested globstars, and the\n // other shapes the hand-rolled regex missed.\n const { base } = picomatch.scan(glob);\n paths.push(resolveFromCwd(base || '.', cwd));\n }\n } else if (project?.sourceFiles) {\n for (const file of project.sourceFiles) paths.push(dirname(file));\n }\n if (config.resolver) paths.push(resolveFromCwd(config.resolver, cwd));\n return [...new Set(paths)];\n}\n\nfunction resolveFromCwd(p: string, cwd: string): string {\n if (isAbsolute(p)) return p;\n return resolvePath(cwd, p);\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, resolve } from 'node:path';\nimport { 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(swatchbookTokensPlugin({ config, cwd }));\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.themes) {\n const tokens = project.themesResolved[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 = project.themes.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 SwatchbookThemeName = ${themeUnion};`,\n '}',\n '',\n ].join('\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"preset.mjs","names":["fsWatch"],"sources":["../src/virtual/plugin.ts","../src/preset.ts"],"sourcesContent":["import type { Config, Project } 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 { HMR_EVENT, RESOLVED_VIRTUAL_MODULE_ID, VIRTUAL_MODULE_ID } from '#/constants.ts';\n\nexport interface SwatchbookPluginOptions {\n config: Config;\n cwd: string;\n}\n\n/**\n * Vite plugin that serves the virtual `virtual:swatchbook/tokens` module —\n * a single source of truth for themes, 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({ config, cwd }: 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 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 return null;\n },\n\n load(id) {\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 themes = ${JSON.stringify(project.themes)};`,\n `export const defaultTheme = ${JSON.stringify(project.themes[0]?.name ?? null)};`,\n `export const themesResolved = ${JSON.stringify(project.themesResolved)};`,\n `export const diagnostics = ${JSON.stringify(project.diagnostics)};`,\n `export const css = ${JSON.stringify(css)};`,\n `export const cssVarPrefix = ${JSON.stringify(config.cssVarPrefix ?? '')};`,\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.themesResolved[project.themes[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 /**\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 themes: project.themes,\n defaultTheme: project.themes[0]?.name ?? null,\n themesResolved: project.themesResolved,\n diagnostics: project.diagnostics,\n css,\n cssVarPrefix: config.cssVarPrefix ?? '',\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","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(swatchbookTokensPlugin({ config, cwd }));\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.themes) {\n const tokens = project.themesResolved[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 = project.themes.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 SwatchbookThemeName = ${themeUnion};`,\n '}',\n '',\n ].join('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as HMR_EVENT, c as PARAM_KEY, d as STYLE_ELEMENT_ID, f as TOKENS_UPDATED_EVENT, i as GLOBAL_KEY, l as PREVIEW_MOUSEDOWN_EVENT, n as AXES_GLOBAL_KEY, o as INIT_EVENT, r as COLOR_FORMAT_GLOBAL_KEY, s as INIT_REQUEST_EVENT } from "./constants-B3vYm529.mjs";
|
|
2
2
|
import { useEffect, useMemo } from "react";
|
|
3
3
|
import { addons } from "storybook/preview-api";
|
|
4
4
|
import { axes, css, cssVarPrefix, defaultTheme, diagnostics, disabledAxes, presets, themes, themesResolved } from "virtual:swatchbook/tokens";
|
|
@@ -319,7 +319,38 @@ function installPreviewMouseDownBridge() {
|
|
|
319
319
|
});
|
|
320
320
|
}
|
|
321
321
|
installPreviewMouseDownBridge();
|
|
322
|
+
if (import.meta.hot) import.meta.hot.on(HMR_EVENT, (payload) => {
|
|
323
|
+
if (typeof document !== "undefined") {
|
|
324
|
+
const bodyRules = `
|
|
325
|
+
html, body {
|
|
326
|
+
background: var(${payload.cssVarPrefix ? `--${payload.cssVarPrefix}-` : "--"}color-surface-default, Canvas);
|
|
327
|
+
color: var(${payload.cssVarPrefix ? `--${payload.cssVarPrefix}-` : "--"}color-text-default, CanvasText);
|
|
328
|
+
margin: 0;
|
|
329
|
+
}
|
|
330
|
+
`;
|
|
331
|
+
const text = `${payload.css}\n${bodyRules}`;
|
|
332
|
+
let style = document.getElementById(STYLE_ELEMENT_ID);
|
|
333
|
+
if (!style) {
|
|
334
|
+
style = document.createElement("style");
|
|
335
|
+
style.id = STYLE_ELEMENT_ID;
|
|
336
|
+
document.head.appendChild(style);
|
|
337
|
+
}
|
|
338
|
+
if (style.textContent !== text) style.textContent = text;
|
|
339
|
+
}
|
|
340
|
+
const channel = addons.getChannel();
|
|
341
|
+
channel.emit(INIT_EVENT, {
|
|
342
|
+
axes: payload.axes,
|
|
343
|
+
disabledAxes: payload.disabledAxes,
|
|
344
|
+
presets: payload.presets,
|
|
345
|
+
themes: payload.themes,
|
|
346
|
+
defaultTheme: payload.defaultTheme,
|
|
347
|
+
themesResolved: payload.themesResolved,
|
|
348
|
+
diagnostics: payload.diagnostics,
|
|
349
|
+
cssVarPrefix: payload.cssVarPrefix
|
|
350
|
+
});
|
|
351
|
+
channel.emit(TOKENS_UPDATED_EVENT, payload);
|
|
352
|
+
});
|
|
322
353
|
//#endregion
|
|
323
354
|
export { preview_exports as i, globalTypes as n, initialGlobals as r, decorators as t };
|
|
324
355
|
|
|
325
|
-
//# sourceMappingURL=preview-
|
|
356
|
+
//# sourceMappingURL=preview-hz8hF3q0.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-hz8hF3q0.mjs","names":["virtualDisabledAxes","virtualAxes","virtualPresets"],"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';\nimport {\n axes as virtualAxes,\n css,\n cssVarPrefix,\n defaultTheme,\n diagnostics,\n disabledAxes as virtualDisabledAxes,\n presets as virtualPresets,\n themes,\n themesResolved,\n} from 'virtual:swatchbook/tokens';\nimport {\n AxesContext,\n COLOR_FORMATS,\n type ColorFormat,\n ColorFormatContext,\n type ProjectSnapshot,\n SwatchbookContext,\n ThemeContext,\n} from '@unpunnyfuns/swatchbook-blocks';\nimport {\n AXES_GLOBAL_KEY,\n COLOR_FORMAT_GLOBAL_KEY,\n GLOBAL_KEY,\n HMR_EVENT,\n INIT_EVENT,\n INIT_REQUEST_EVENT,\n PREVIEW_MOUSEDOWN_EVENT,\n PARAM_KEY,\n STYLE_ELEMENT_ID,\n TOKENS_UPDATED_EVENT,\n} from '#/constants.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 = themes[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 `defaultTheme` and then\n * the first theme. Returns empty string when the project has no themes so\n * callers can omit the attr instead of writing a made-up context name.\n */\nfunction matchThemeName(tuple: Readonly<Record<string, string>>): string {\n const match = themes.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 ?? defaultTheme ?? themes[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 themes,\n defaultTheme,\n themesResolved,\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 `Theme.input` by composed name. Returns `undefined` if no theme matches. */\nfunction tupleForName(name: string): Record<string, string> | undefined {\n const match = themes.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 four input channels, in priority order:\n * 1. `parameters.swatchbook.axes` — per-story tuple.\n * 2. `parameters.swatchbook.theme` — per-story composed name (legacy).\n * 3. `globals.swatchbookAxes` — toolbar-set tuple.\n * 4. `globals.swatchbookTheme` — toolbar-set composed name.\n * 5. virtual module default.\n */\nfunction resolveTuple(\n globals: Record<string, unknown>,\n parameters: Record<string, Record<string, unknown>>,\n): Record<string, string> {\n const param = parameters[PARAM_KEY];\n const paramAxes = param?.['axes'];\n if (paramAxes && typeof paramAxes === 'object') {\n return normalizeTuple(paramAxes as Record<string, string>);\n }\n const paramTheme = param?.['theme'];\n if (typeof paramTheme === 'string') {\n const hit = tupleForName(paramTheme);\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 const globalTheme = globals[GLOBAL_KEY];\n if (typeof globalTheme === 'string') {\n const hit = tupleForName(globalTheme);\n if (hit) return normalizeTuple(hit);\n }\n return defaultTuple();\n}\n\nfunction resolveColorFormat(globals: Record<string, unknown>): 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 tuple = useMemo(\n () =>\n resolveTuple(\n context.globals as Record<string, unknown>,\n context.parameters as Record<string, Record<string, unknown>>,\n ),\n [context.globals, context.parameters],\n );\n const colorFormat = useMemo(\n () => resolveColorFormat(context.globals as Record<string, unknown>),\n [context.globals],\n );\n const themeName = useMemo(() => matchThemeName(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 themes,\n themesResolved,\n activeTheme: themeName,\n activeAxes: tuple,\n cssVarPrefix,\n diagnostics,\n css,\n }),\n [themeName, tuple],\n );\n\n return (\n <SwatchbookContext.Provider value={snapshot}>\n <ThemeContext.Provider value={themeName}>\n <AxesContext.Provider value={tuple}>\n <ColorFormatContext.Provider value={colorFormat}>\n <div\n {...wrapperAttrs}\n style={{\n padding: '1rem',\n minHeight: '100%',\n }}\n >\n <Story />\n </div>\n </ColorFormatContext.Provider>\n </AxesContext.Provider>\n </ThemeContext.Provider>\n </SwatchbookContext.Provider>\n );\n};\n\n/**\n * Named exports consumed by `definePreviewAddon(previewExports)` in the\n * addon's CSF Next factory (`src/index.ts`).\n */\nexport const decorators: NonNullable<Preview['decorators']> = [themedDecorator];\n\nexport const globalTypes: NonNullable<Preview['globalTypes']> = {\n [GLOBAL_KEY]: {\n name: 'Theme',\n description: 'Active swatchbook theme (composed permutation ID).',\n },\n [AXES_GLOBAL_KEY]: {\n name: 'Axes',\n description: 'Per-axis context selection. Takes precedence over the composed theme name.',\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 [GLOBAL_KEY]: defaultTheme ?? themes[0]?.name ?? '',\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: Record<string, unknown>): void => {\n ensureStylesheet();\n const tuple = resolveTuple(globals, {});\n setRootAxes(matchThemeName(tuple), tuple);\n };\n const onGlobals = (payload: { globals?: Record<string, unknown> }): 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 themes: typeof themes;\n defaultTheme: typeof defaultTheme;\n themesResolved: typeof themesResolved;\n diagnostics: typeof diagnostics;\n css: string;\n cssVarPrefix: string;\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 themes: payload.themes,\n defaultTheme: payload.defaultTheme,\n themesResolved: payload.themesResolved,\n diagnostics: payload.diagnostics,\n cssVarPrefix: payload.cssVarPrefix,\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-
|
|
1
|
+
import { n as globalTypes, r as initialGlobals, t as decorators } from "./preview-hz8hF3q0.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.
|
|
3
|
+
"version": "0.8.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>",
|
|
@@ -70,8 +70,9 @@
|
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"jiti": "^2.4.0",
|
|
72
72
|
"picomatch": "^4.0.4",
|
|
73
|
-
"@unpunnyfuns/swatchbook-blocks": "0.
|
|
74
|
-
"@unpunnyfuns/swatchbook-core": "0.
|
|
73
|
+
"@unpunnyfuns/swatchbook-blocks": "0.8.0",
|
|
74
|
+
"@unpunnyfuns/swatchbook-core": "0.8.0",
|
|
75
|
+
"@unpunnyfuns/swatchbook-switcher": "0.8.0"
|
|
75
76
|
},
|
|
76
77
|
"peerDependencies": {
|
|
77
78
|
"react": ">=18",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"preview-C91uAAG-.mjs","names":["virtualDisabledAxes","virtualAxes","virtualPresets"],"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","import type { Decorator, Preview } from '@storybook/react-vite';\nimport { useEffect, useMemo } from 'react';\nimport { addons } from 'storybook/preview-api';\nimport { dataAttr } from '#/data-attr.ts';\nimport {\n axes as virtualAxes,\n css,\n cssVarPrefix,\n defaultTheme,\n diagnostics,\n disabledAxes as virtualDisabledAxes,\n presets as virtualPresets,\n themes,\n themesResolved,\n} from 'virtual:swatchbook/tokens';\nimport {\n AxesContext,\n COLOR_FORMATS,\n type ColorFormat,\n ColorFormatContext,\n type ProjectSnapshot,\n SwatchbookContext,\n ThemeContext,\n} from '@unpunnyfuns/swatchbook-blocks';\nimport {\n AXES_GLOBAL_KEY,\n COLOR_FORMAT_GLOBAL_KEY,\n GLOBAL_KEY,\n INIT_EVENT,\n INIT_REQUEST_EVENT,\n PREVIEW_MOUSEDOWN_EVENT,\n PARAM_KEY,\n STYLE_ELEMENT_ID,\n} from '#/constants.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 = themes[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 `defaultTheme` and then\n * the first theme. Returns empty string when the project has no themes so\n * callers can omit the attr instead of writing a made-up context name.\n */\nfunction matchThemeName(tuple: Readonly<Record<string, string>>): string {\n const match = themes.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 ?? defaultTheme ?? themes[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 themes,\n defaultTheme,\n themesResolved,\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 `Theme.input` by composed name. Returns `undefined` if no theme matches. */\nfunction tupleForName(name: string): Record<string, string> | undefined {\n const match = themes.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 four input channels, in priority order:\n * 1. `parameters.swatchbook.axes` — per-story tuple.\n * 2. `parameters.swatchbook.theme` — per-story composed name (legacy).\n * 3. `globals.swatchbookAxes` — toolbar-set tuple.\n * 4. `globals.swatchbookTheme` — toolbar-set composed name.\n * 5. virtual module default.\n */\nfunction resolveTuple(\n globals: Record<string, unknown>,\n parameters: Record<string, Record<string, unknown>>,\n): Record<string, string> {\n const param = parameters[PARAM_KEY];\n const paramAxes = param?.['axes'];\n if (paramAxes && typeof paramAxes === 'object') {\n return normalizeTuple(paramAxes as Record<string, string>);\n }\n const paramTheme = param?.['theme'];\n if (typeof paramTheme === 'string') {\n const hit = tupleForName(paramTheme);\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 const globalTheme = globals[GLOBAL_KEY];\n if (typeof globalTheme === 'string') {\n const hit = tupleForName(globalTheme);\n if (hit) return normalizeTuple(hit);\n }\n return defaultTuple();\n}\n\nfunction resolveColorFormat(globals: Record<string, unknown>): 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 tuple = useMemo(\n () =>\n resolveTuple(\n context.globals as Record<string, unknown>,\n context.parameters as Record<string, Record<string, unknown>>,\n ),\n [context.globals, context.parameters],\n );\n const colorFormat = useMemo(\n () => resolveColorFormat(context.globals as Record<string, unknown>),\n [context.globals],\n );\n const themeName = useMemo(() => matchThemeName(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 themes,\n themesResolved,\n activeTheme: themeName,\n activeAxes: tuple,\n cssVarPrefix,\n diagnostics,\n css,\n }),\n [themeName, tuple],\n );\n\n return (\n <SwatchbookContext.Provider value={snapshot}>\n <ThemeContext.Provider value={themeName}>\n <AxesContext.Provider value={tuple}>\n <ColorFormatContext.Provider value={colorFormat}>\n <div\n {...wrapperAttrs}\n style={{\n padding: '1rem',\n minHeight: '100%',\n }}\n >\n <Story />\n </div>\n </ColorFormatContext.Provider>\n </AxesContext.Provider>\n </ThemeContext.Provider>\n </SwatchbookContext.Provider>\n );\n};\n\n/**\n * Named exports consumed by `definePreviewAddon(previewExports)` in the\n * addon's CSF Next factory (`src/index.ts`).\n */\nexport const decorators: NonNullable<Preview['decorators']> = [themedDecorator];\n\nexport const globalTypes: NonNullable<Preview['globalTypes']> = {\n [GLOBAL_KEY]: {\n name: 'Theme',\n description: 'Active swatchbook theme (composed permutation ID).',\n },\n [AXES_GLOBAL_KEY]: {\n name: 'Axes',\n description: 'Per-axis context selection. Takes precedence over the composed theme name.',\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 [GLOBAL_KEY]: defaultTheme ?? themes[0]?.name ?? '',\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: Record<string, unknown>): void => {\n ensureStylesheet();\n const tuple = resolveTuple(globals, {});\n setRootAxes(matchThemeName(tuple), tuple);\n };\n const onGlobals = (payload: { globals?: Record<string, unknown> }): 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"],"mappings":"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"}
|