@unpunnyfuns/swatchbook-addon 0.1.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.
@@ -0,0 +1,903 @@
1
+ import { a as GLOBAL_KEY, d as TOOL_ID, n as AXES_GLOBAL_KEY, o as INIT_EVENT, r as COLOR_FORMAT_GLOBAL_KEY, s as PANEL_ID, t as ADDON_ID } from "./constants-1plfdgh7.mjs";
2
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { IconButton, Placeholder, ScrollArea, WithTooltipPure } from "storybook/internal/components";
4
+ import { addons, types, useGlobals, useStorybookApi } from "storybook/manager-api";
5
+ //#region src/panel.tsx
6
+ /** `React.createElement` alias so the manager bundle avoids `react/jsx-runtime`. */
7
+ const h$1 = React.createElement;
8
+ function usePayload() {
9
+ const [payload, setPayload] = useState(null);
10
+ useEffect(() => {
11
+ const channel = addons.getChannel();
12
+ const onInit = (next) => setPayload(next);
13
+ channel.on(INIT_EVENT, onInit);
14
+ return () => {
15
+ channel.off(INIT_EVENT, onInit);
16
+ };
17
+ }, []);
18
+ return payload;
19
+ }
20
+ function makeCssVarName(path, prefix) {
21
+ const tail = path.replaceAll(".", "-");
22
+ return prefix ? `--${prefix}-${tail}` : `--${tail}`;
23
+ }
24
+ async function copy(text) {
25
+ try {
26
+ await navigator.clipboard.writeText(text);
27
+ } catch {}
28
+ }
29
+ /** Format a token `$value` into a short display string. */
30
+ function formatValue(value) {
31
+ if (value == null) return "";
32
+ if (typeof value === "string" || typeof value === "number") return String(value);
33
+ if (typeof value === "object") {
34
+ const v = value;
35
+ if (typeof v["hex"] === "string") return v["hex"];
36
+ if ("value" in v && "unit" in v) return `${String(v["value"])}${String(v["unit"])}`;
37
+ return JSON.stringify(value).slice(0, 80);
38
+ }
39
+ return String(value);
40
+ }
41
+ const containerStyle = {
42
+ display: "flex",
43
+ flexDirection: "column",
44
+ height: "100%"
45
+ };
46
+ const headerStyle = {
47
+ padding: "8px 12px",
48
+ borderBottom: "1px solid rgba(128,128,128,0.2)",
49
+ display: "flex",
50
+ flexDirection: "column",
51
+ gap: 6
52
+ };
53
+ const axisIndicatorStyle = {
54
+ fontSize: 11,
55
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace",
56
+ opacity: .7
57
+ };
58
+ const treeWrapperStyle = {
59
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
60
+ fontSize: 12,
61
+ padding: 8
62
+ };
63
+ const groupRowStyle = {
64
+ display: "flex",
65
+ alignItems: "center",
66
+ gap: 6,
67
+ padding: "4px 6px",
68
+ borderRadius: 4,
69
+ cursor: "pointer",
70
+ userSelect: "none",
71
+ outline: "none"
72
+ };
73
+ const leafRowStyle = {
74
+ display: "flex",
75
+ alignItems: "center",
76
+ gap: 8,
77
+ padding: "4px 6px",
78
+ borderRadius: 4,
79
+ cursor: "pointer",
80
+ border: "none",
81
+ background: "transparent",
82
+ color: "inherit",
83
+ width: "100%",
84
+ textAlign: "left",
85
+ fontFamily: "inherit",
86
+ fontSize: "inherit",
87
+ outline: "none"
88
+ };
89
+ const caretStyle = {
90
+ display: "inline-block",
91
+ width: 12,
92
+ textAlign: "center",
93
+ opacity: .6
94
+ };
95
+ const treeUlStyle = {
96
+ listStyle: "none",
97
+ margin: 0,
98
+ padding: 0
99
+ };
100
+ const nestedUlStyle = {
101
+ listStyle: "none",
102
+ margin: 0,
103
+ paddingLeft: 18,
104
+ borderLeft: "1px solid rgba(128,128,128,0.2)"
105
+ };
106
+ const typePillStyle = {
107
+ display: "inline-block",
108
+ padding: "1px 6px",
109
+ borderRadius: 4,
110
+ fontSize: 10,
111
+ letterSpacing: .5,
112
+ textTransform: "uppercase",
113
+ background: "rgba(128,128,128,0.15)"
114
+ };
115
+ const valueStyle = {
116
+ marginLeft: "auto",
117
+ opacity: .7,
118
+ fontSize: 11,
119
+ maxWidth: "40%",
120
+ overflow: "hidden",
121
+ textOverflow: "ellipsis",
122
+ whiteSpace: "nowrap"
123
+ };
124
+ const countStyle = {
125
+ marginLeft: "auto",
126
+ fontSize: 11,
127
+ opacity: .7
128
+ };
129
+ const swatchStyle = {
130
+ display: "inline-block",
131
+ width: 14,
132
+ height: 14,
133
+ borderRadius: 3,
134
+ border: "1px solid rgba(128,128,128,0.3)",
135
+ marginLeft: 8
136
+ };
137
+ const searchInputStyle = {
138
+ width: "100%",
139
+ padding: "4px 8px",
140
+ fontSize: 12,
141
+ border: "1px solid rgba(128,128,128,0.3)",
142
+ borderRadius: 4,
143
+ background: "transparent",
144
+ color: "inherit"
145
+ };
146
+ function buildTree(resolved) {
147
+ const rootNode = {
148
+ kind: "group",
149
+ segment: "",
150
+ path: "",
151
+ children: []
152
+ };
153
+ for (const [path, token] of Object.entries(resolved)) {
154
+ const segments = path.split(".");
155
+ let node = rootNode;
156
+ for (let i = 0; i < segments.length - 1; i += 1) {
157
+ const seg = segments[i];
158
+ const prefix = segments.slice(0, i + 1).join(".");
159
+ let child = node.children.find((c) => c.kind === "group" && c.segment === seg);
160
+ if (!child) {
161
+ child = {
162
+ kind: "group",
163
+ segment: seg,
164
+ path: prefix,
165
+ children: []
166
+ };
167
+ node.children.push(child);
168
+ }
169
+ node = child;
170
+ }
171
+ const leafSegment = segments[segments.length - 1];
172
+ node.children.push({
173
+ kind: "leaf",
174
+ segment: leafSegment,
175
+ path,
176
+ token
177
+ });
178
+ }
179
+ sortTree(rootNode);
180
+ return rootNode.children;
181
+ }
182
+ function sortTree(node) {
183
+ node.children.sort((a, b) => {
184
+ if (a.kind !== b.kind) return a.kind === "group" ? -1 : 1;
185
+ return a.segment.localeCompare(b.segment);
186
+ });
187
+ for (const c of node.children) if (c.kind === "group") sortTree(c);
188
+ }
189
+ function collectInitialExpanded(nodes, remainingDepth, out) {
190
+ if (remainingDepth <= 0) return;
191
+ for (const node of nodes) {
192
+ if (node.kind !== "group") continue;
193
+ out.add(node.path);
194
+ collectInitialExpanded(node.children, remainingDepth - 1, out);
195
+ }
196
+ }
197
+ function countLeaves(node) {
198
+ if (node.kind === "leaf") return 1;
199
+ let n = 0;
200
+ for (const c of node.children) n += countLeaves(c);
201
+ return n;
202
+ }
203
+ function filterTree(nodes, query) {
204
+ if (!query) return nodes;
205
+ const out = [];
206
+ for (const node of nodes) {
207
+ if (node.kind === "leaf") {
208
+ if (node.path.toLowerCase().includes(query)) out.push(node);
209
+ continue;
210
+ }
211
+ const filteredChildren = filterTree(node.children, query);
212
+ if (filteredChildren.length > 0) out.push({
213
+ ...node,
214
+ children: filteredChildren
215
+ });
216
+ }
217
+ return out;
218
+ }
219
+ function collectAllGroupPaths(nodes, out) {
220
+ for (const node of nodes) if (node.kind === "group") {
221
+ out.add(node.path);
222
+ collectAllGroupPaths(node.children, out);
223
+ }
224
+ }
225
+ function DesignTokensPanel({ active }) {
226
+ const payload = usePayload();
227
+ const [globals] = useGlobals();
228
+ const [query, setQuery] = useState("");
229
+ const axes = useMemo(() => payload?.axes ?? [], [payload]);
230
+ const themes = useMemo(() => payload?.themes ?? [], [payload]);
231
+ const globalAxes = globals[AXES_GLOBAL_KEY];
232
+ const globalTheme = globals[GLOBAL_KEY];
233
+ const tuple = useMemo(() => {
234
+ const out = {};
235
+ for (const axis of axes) out[axis.name] = axis.default;
236
+ if (globalAxes && typeof globalAxes === "object") {
237
+ for (const axis of axes) {
238
+ const candidate = globalAxes[axis.name];
239
+ if (candidate && axis.contexts.includes(candidate)) out[axis.name] = candidate;
240
+ }
241
+ return out;
242
+ }
243
+ if (globalTheme) {
244
+ const match = themes.find((t) => t.name === globalTheme);
245
+ if (match) for (const axis of axes) {
246
+ const candidate = match.input[axis.name];
247
+ if (candidate && axis.contexts.includes(candidate)) out[axis.name] = candidate;
248
+ }
249
+ }
250
+ return out;
251
+ }, [
252
+ axes,
253
+ themes,
254
+ globalAxes,
255
+ globalTheme
256
+ ]);
257
+ const themeName = useMemo(() => {
258
+ return themes.find((t) => {
259
+ const input = t.input;
260
+ return Object.keys(input).every((k) => input[k] === tuple[k]);
261
+ })?.name ?? globalTheme ?? payload?.defaultTheme ?? "";
262
+ }, [
263
+ themes,
264
+ tuple,
265
+ globalTheme,
266
+ payload
267
+ ]);
268
+ const prefix = payload?.cssVarPrefix ?? "";
269
+ const tokens = useMemo(() => payload?.themesResolved[themeName] ?? {}, [payload, themeName]);
270
+ const tokenCount = Object.keys(tokens).length;
271
+ const tree = useMemo(() => buildTree(tokens), [tokens]);
272
+ const lowerQuery = query.toLowerCase();
273
+ const filtered = useMemo(() => filterTree(tree, lowerQuery), [tree, lowerQuery]);
274
+ const initialExpanded = useMemo(() => {
275
+ const out = /* @__PURE__ */ new Set();
276
+ collectInitialExpanded(tree, 1, out);
277
+ return out;
278
+ }, [tree]);
279
+ const [expanded, setExpanded] = useState(initialExpanded);
280
+ useEffect(() => {
281
+ setExpanded(initialExpanded);
282
+ }, [initialExpanded]);
283
+ const displayExpanded = useMemo(() => {
284
+ if (!lowerQuery) return expanded;
285
+ const all = /* @__PURE__ */ new Set();
286
+ collectAllGroupPaths(filtered, all);
287
+ return all;
288
+ }, [
289
+ expanded,
290
+ filtered,
291
+ lowerQuery
292
+ ]);
293
+ const toggle = useCallback((path) => {
294
+ setExpanded((prev) => {
295
+ const next = new Set(prev);
296
+ if (next.has(path)) next.delete(path);
297
+ else next.add(path);
298
+ return next;
299
+ });
300
+ }, []);
301
+ const handleLeafClick = useCallback((path) => {
302
+ copy(`var(${makeCssVarName(path, prefix)})`);
303
+ }, [prefix]);
304
+ if (!active) return null;
305
+ if (!payload) return h$1(Placeholder, null, "Waiting for swatchbook preview…");
306
+ const showAxisIndicator = axes.length > 1 || axes.length === 1 && axes[0]?.source !== "synthetic";
307
+ const axisIndicatorText = axes.map((axis) => `${axis.name}: ${tuple[axis.name] ?? axis.default}`).join(" · ");
308
+ const disabledAxes = payload?.disabledAxes ?? [];
309
+ const pinnedSample = themes[0]?.input ?? {};
310
+ const disabledIndicatorText = disabledAxes.map((name) => `${name}: ${pinnedSample[name] ?? "?"} · pinned`).join(" · ");
311
+ return h$1("div", { style: containerStyle }, h$1("div", { style: headerStyle }, showAxisIndicator && h$1("div", {
312
+ style: axisIndicatorStyle,
313
+ "data-testid": "design-tokens-panel-axis-indicator"
314
+ }, axisIndicatorText), disabledAxes.length > 0 && h$1("div", {
315
+ style: {
316
+ ...axisIndicatorStyle,
317
+ opacity: .5
318
+ },
319
+ "data-testid": "design-tokens-panel-disabled-axes-indicator"
320
+ }, disabledIndicatorText), h$1("input", {
321
+ style: searchInputStyle,
322
+ type: "search",
323
+ placeholder: `Search ${tokenCount} tokens in ${themeName}…`,
324
+ value: query,
325
+ onChange: (e) => setQuery(e.target.value)
326
+ })), h$1(DiagnosticsSection, { diagnostics: payload.diagnostics }), h$1(ScrollArea, { vertical: true }, filtered.length === 0 ? h$1(Placeholder, null, query ? "No tokens match this filter." : "No tokens in this theme.") : h$1("div", { style: treeWrapperStyle }, h$1("ul", {
327
+ style: treeUlStyle,
328
+ role: "tree"
329
+ }, filtered.map((node) => h$1(TreeRow, {
330
+ key: node.path || node.segment,
331
+ node,
332
+ expanded: displayExpanded,
333
+ onToggle: toggle,
334
+ onLeafClick: handleLeafClick,
335
+ prefix
336
+ }))))));
337
+ }
338
+ function TreeRow({ node, expanded, onToggle, onLeafClick, prefix }) {
339
+ if (node.kind === "leaf") return h$1(LeafRow, {
340
+ node,
341
+ onLeafClick,
342
+ prefix
343
+ });
344
+ const isOpen = expanded.has(node.path);
345
+ const onKey = (e) => {
346
+ if (e.key === "Enter" || e.key === " ") {
347
+ e.preventDefault();
348
+ onToggle(node.path);
349
+ }
350
+ };
351
+ return h$1("li", {
352
+ role: "treeitem",
353
+ "aria-expanded": isOpen
354
+ }, h$1("div", {
355
+ role: "button",
356
+ tabIndex: 0,
357
+ style: groupRowStyle,
358
+ onClick: () => onToggle(node.path),
359
+ onKeyDown: onKey,
360
+ "data-path": node.path,
361
+ "data-testid": "design-tokens-panel-group"
362
+ }, h$1("span", {
363
+ style: caretStyle,
364
+ "aria-hidden": true
365
+ }, isOpen ? "▾" : "▸"), h$1("span", null, node.segment), h$1("span", { style: countStyle }, countLeaves(node))), isOpen && h$1("ul", {
366
+ style: nestedUlStyle,
367
+ role: "group"
368
+ }, node.children.map((c) => h$1(TreeRow, {
369
+ key: c.path || c.segment,
370
+ node: c,
371
+ expanded,
372
+ onToggle,
373
+ onLeafClick,
374
+ prefix
375
+ }))));
376
+ }
377
+ function LeafRow({ node, onLeafClick, prefix }) {
378
+ const type = node.token.$type ?? "";
379
+ const value = node.token.$value;
380
+ const displayValue = formatValue(value);
381
+ const colorPreview = type === "color" && typeof value === "object" && value !== null && typeof value["hex"] === "string" ? value["hex"] : null;
382
+ return h$1("li", { role: "treeitem" }, h$1("button", {
383
+ type: "button",
384
+ style: leafRowStyle,
385
+ onClick: () => onLeafClick(node.path),
386
+ title: `Click to copy var(${makeCssVarName(node.path, prefix)})`,
387
+ "data-path": node.path,
388
+ "data-testid": "design-tokens-panel-leaf"
389
+ }, h$1("span", {
390
+ style: caretStyle,
391
+ "aria-hidden": true
392
+ }, "•"), h$1("span", null, node.segment), type && h$1("span", { style: typePillStyle }, type), h$1("span", { style: valueStyle }, displayValue), colorPreview && h$1("span", {
393
+ style: {
394
+ ...swatchStyle,
395
+ background: colorPreview
396
+ },
397
+ "aria-hidden": true
398
+ })));
399
+ }
400
+ const severityStyle = {
401
+ error: { color: "#d64545" },
402
+ warn: { color: "#b08900" },
403
+ info: { opacity: .6 }
404
+ };
405
+ const severityLabel = {
406
+ error: "ERROR",
407
+ warn: "WARN",
408
+ info: "INFO"
409
+ };
410
+ const diagnosticsSectionStyle = { borderBottom: "1px solid rgba(128,128,128,0.2)" };
411
+ const diagnosticsSummaryStyle = {
412
+ padding: "10px 12px",
413
+ fontSize: 12,
414
+ cursor: "pointer",
415
+ userSelect: "none",
416
+ listStyle: "none",
417
+ display: "flex",
418
+ alignItems: "center",
419
+ gap: 8
420
+ };
421
+ const diagnosticRowStyle = {
422
+ display: "grid",
423
+ gridTemplateColumns: "60px 1fr",
424
+ gap: 12,
425
+ padding: "8px 12px",
426
+ fontSize: 12,
427
+ borderTop: "1px solid rgba(128,128,128,0.12)"
428
+ };
429
+ function DiagnosticsSection({ diagnostics }) {
430
+ const counts = diagnostics.reduce((acc, d) => {
431
+ acc[d.severity] = (acc[d.severity] ?? 0) + 1;
432
+ return acc;
433
+ }, {
434
+ error: 0,
435
+ warn: 0,
436
+ info: 0
437
+ });
438
+ const hasErrorsOrWarnings = counts.error > 0 || counts.warn > 0;
439
+ const summaryText = (() => {
440
+ if (diagnostics.length === 0) return "✔ OK · no diagnostics";
441
+ const parts = [];
442
+ if (counts.error > 0) parts.push(`✖ ${counts.error} error${counts.error === 1 ? "" : "s"}`);
443
+ if (counts.warn > 0) parts.push(`⚠ ${counts.warn} warning${counts.warn === 1 ? "" : "s"}`);
444
+ if (counts.info > 0) parts.push(`${counts.info} info`);
445
+ return parts.join(" · ");
446
+ })();
447
+ const summaryColor = (() => {
448
+ if (diagnostics.length === 0) return "#30a46c";
449
+ if (counts.error > 0) return "#d64545";
450
+ if (counts.warn > 0) return "#b08900";
451
+ return "inherit";
452
+ })();
453
+ return h$1("details", {
454
+ style: diagnosticsSectionStyle,
455
+ open: hasErrorsOrWarnings,
456
+ "data-testid": "design-tokens-panel-diagnostics"
457
+ }, h$1("summary", { style: {
458
+ ...diagnosticsSummaryStyle,
459
+ color: summaryColor,
460
+ fontWeight: 600
461
+ } }, h$1("span", null, "Diagnostics"), h$1("span", { style: { fontWeight: 400 } }, summaryText)), diagnostics.length === 0 ? null : h$1("div", null, diagnostics.map((d, i) => h$1("div", {
462
+ key: `${d.group}-${i}`,
463
+ style: diagnosticRowStyle
464
+ }, h$1("span", { style: {
465
+ ...severityStyle[d.severity],
466
+ fontWeight: 600,
467
+ fontSize: 10
468
+ } }, severityLabel[d.severity]), h$1("div", null, h$1("div", null, d.message), (d.filename || d.group) && h$1("div", { style: {
469
+ opacity: .5,
470
+ fontSize: 10,
471
+ marginTop: 4
472
+ } }, [
473
+ d.group,
474
+ d.filename,
475
+ d.line ? `:${d.line}` : ""
476
+ ].filter(Boolean).join(" · ")))))));
477
+ }
478
+ //#endregion
479
+ //#region src/manager.tsx
480
+ /**
481
+ * Use explicit `React.createElement` rather than JSX so the manager bundle
482
+ * doesn't take a hard dependency on `react/jsx-runtime`. Storybook's manager
483
+ * page injects its own React as a runtime global; `react/jsx-runtime` isn't
484
+ * always part of that exposure, which breaks JSX with
485
+ * "Cannot read properties of undefined (reading 'recentlyCreatedOwnerStacks')".
486
+ * Mirrors the pattern `@storybook/addon-a11y` uses in its manager.
487
+ */
488
+ const h = React.createElement;
489
+ const EMPTY_AXES = [];
490
+ const EMPTY_PRESETS = [];
491
+ const EMPTY_THEMES = [];
492
+ /**
493
+ * Root toolbar glyph — a split-circle ("yinyang") mark: a faint filled
494
+ * disc for the full-swatch silhouette, with a darker half-and-inset-disc
495
+ * path reading as a pair of theme variants swapped in place.
496
+ */
497
+ function SwatchbookIcon() {
498
+ return h("svg", {
499
+ width: 14,
500
+ height: 14,
501
+ viewBox: "0 0 14 14",
502
+ "aria-hidden": true
503
+ }, h("circle", {
504
+ cx: 7,
505
+ cy: 7,
506
+ r: 6,
507
+ fill: "currentColor",
508
+ opacity: .15
509
+ }), h("path", {
510
+ d: "M7 1a6 6 0 0 0 0 12 3 3 0 0 0 0-6 3 3 0 0 1 0-6Z",
511
+ fill: "currentColor"
512
+ }));
513
+ }
514
+ function tupleMatchesInput(tuple, input) {
515
+ const keys = Object.keys(input);
516
+ if (keys.length === 0) return false;
517
+ return keys.every((k) => input[k] === tuple[k]);
518
+ }
519
+ function composedNameFor(tuple, themes, fallback) {
520
+ return themes.find((t) => tupleMatchesInput(tuple, t.input))?.name ?? fallback;
521
+ }
522
+ function defaultTupleFor(axes) {
523
+ const out = {};
524
+ for (const axis of axes) out[axis.name] = axis.default;
525
+ return out;
526
+ }
527
+ /**
528
+ * Treat the `{ name: 'theme', source: 'synthetic' }` axis — the one core
529
+ * fabricates for single-theme projects with no resolver — as a special case
530
+ * that uses the label "Theme" instead of the axis name. Authored single-axis
531
+ * resolvers keep their real axis name (e.g. `mode`).
532
+ */
533
+ function displayLabelFor(axis) {
534
+ if (axis.source === "synthetic" && axis.name === "theme") return "Theme";
535
+ return axis.name;
536
+ }
537
+ /**
538
+ * Compose a preset's sanitized partial tuple with the axis defaults, so
539
+ * applying a preset that only names some axes leaves the omitted ones at
540
+ * their defaults (not blank). Matches the preview decorator's own fallback
541
+ * logic so what the toolbar sends out is what the decorator honors.
542
+ */
543
+ function presetTuple(preset, axes, defaults) {
544
+ const out = { ...defaults };
545
+ for (const axis of axes) {
546
+ const candidate = preset.axes[axis.name];
547
+ if (candidate !== void 0 && axis.contexts.includes(candidate)) out[axis.name] = candidate;
548
+ }
549
+ return out;
550
+ }
551
+ function tuplesEqual(a, b, axes) {
552
+ for (const axis of axes) if (a[axis.name] !== b[axis.name]) return false;
553
+ return true;
554
+ }
555
+ const SECTION_LABEL_STYLE = {
556
+ padding: "8px 12px 4px",
557
+ fontSize: 11,
558
+ textTransform: "uppercase",
559
+ letterSpacing: .5,
560
+ opacity: .6
561
+ };
562
+ const SECTION_BODY_STYLE = {
563
+ padding: "0 12px 10px",
564
+ display: "flex",
565
+ flexWrap: "wrap",
566
+ gap: 4
567
+ };
568
+ const AXIS_ROW_STYLE = {
569
+ padding: "0 12px 10px",
570
+ display: "grid",
571
+ gridTemplateColumns: "max-content 1fr",
572
+ columnGap: 12,
573
+ rowGap: 4,
574
+ alignItems: "center"
575
+ };
576
+ const AXIS_LABEL_STYLE = {
577
+ fontSize: 12,
578
+ fontWeight: 600,
579
+ opacity: .85
580
+ };
581
+ const AXIS_PILLS_STYLE = {
582
+ display: "flex",
583
+ flexWrap: "wrap",
584
+ gap: 4
585
+ };
586
+ const OPTION_PILL_BASE = {
587
+ padding: "3px 8px",
588
+ borderRadius: 4,
589
+ fontSize: 12,
590
+ lineHeight: "18px",
591
+ borderWidth: 1,
592
+ borderStyle: "solid",
593
+ borderColor: "transparent",
594
+ background: "transparent",
595
+ cursor: "pointer",
596
+ color: "inherit",
597
+ outline: "none",
598
+ boxShadow: "none"
599
+ };
600
+ const OPTION_PILL_ACTIVE = {
601
+ ...OPTION_PILL_BASE,
602
+ fontWeight: 600,
603
+ background: "rgba(0, 122, 255, 0.12)",
604
+ borderColor: "rgba(0, 122, 255, 0.45)"
605
+ };
606
+ const PRESET_PILL_MODIFIED = {
607
+ display: "inline-block",
608
+ width: 6,
609
+ height: 6,
610
+ marginLeft: 6,
611
+ borderRadius: "50%",
612
+ background: "currentColor",
613
+ opacity: .6,
614
+ verticalAlign: "middle"
615
+ };
616
+ const DIVIDER_STYLE = {
617
+ height: 1,
618
+ background: "currentColor",
619
+ opacity: .1,
620
+ margin: "2px 8px"
621
+ };
622
+ function OptionPill({ label, active, title, onClick, trailing }) {
623
+ return h("button", {
624
+ type: "button",
625
+ title,
626
+ onClick,
627
+ onMouseDown: (event) => event.preventDefault(),
628
+ style: active ? OPTION_PILL_ACTIVE : OPTION_PILL_BASE
629
+ }, label, trailing ?? null);
630
+ }
631
+ function PresetsSection({ presets, axes, defaults, activeTuple, lastApplied, onApply }) {
632
+ return h("div", null, h("div", { style: SECTION_LABEL_STYLE }, "Presets"), h("div", { style: SECTION_BODY_STYLE }, ...presets.map((preset) => {
633
+ const matches = tuplesEqual(presetTuple(preset, axes, defaults), activeTuple, axes);
634
+ const modified = !matches && preset.name === lastApplied;
635
+ const title = preset.description ? `${preset.name} — ${preset.description}` : preset.name;
636
+ return h(OptionPill, {
637
+ key: `${TOOL_ID}/preset/${preset.name}`,
638
+ label: preset.name,
639
+ active: matches,
640
+ title,
641
+ onClick: () => onApply(preset),
642
+ trailing: modified ? h("span", {
643
+ "aria-hidden": true,
644
+ style: PRESET_PILL_MODIFIED
645
+ }) : null
646
+ });
647
+ })));
648
+ }
649
+ function AxisSection({ axis, active, onSelect }) {
650
+ const label = displayLabelFor(axis);
651
+ return h("div", { style: AXIS_ROW_STYLE }, h("div", {
652
+ style: AXIS_LABEL_STYLE,
653
+ title: axis.description
654
+ }, label), h("div", { style: AXIS_PILLS_STYLE }, ...axis.contexts.map((ctx) => h(OptionPill, {
655
+ key: `${TOOL_ID}/${axis.name}/${ctx}`,
656
+ label: ctx,
657
+ active: ctx === active,
658
+ onClick: () => onSelect(ctx)
659
+ }))));
660
+ }
661
+ const COLOR_FORMAT_OPTIONS = [
662
+ {
663
+ id: "hex",
664
+ label: "Hex"
665
+ },
666
+ {
667
+ id: "rgb",
668
+ label: "RGB"
669
+ },
670
+ {
671
+ id: "hsl",
672
+ label: "HSL"
673
+ },
674
+ {
675
+ id: "oklch",
676
+ label: "OKLCH"
677
+ },
678
+ {
679
+ id: "raw",
680
+ label: "Raw (JSON)"
681
+ }
682
+ ];
683
+ function ColorFormatSection({ active, onSelect }) {
684
+ 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, {
685
+ key: `${TOOL_ID}/color-format/${opt.id}`,
686
+ label: opt.label,
687
+ active: opt.id === active,
688
+ onClick: () => onSelect(opt.id)
689
+ }))));
690
+ }
691
+ function PopoverBody(props) {
692
+ const { axes, presets, defaults, activeTuple, activeColorFormat, lastApplied, onApplyPreset, onSelectAxis, onSelectColorFormat, onKeyDown } = props;
693
+ const sections = [];
694
+ if (presets.length > 0) sections.push(h(PresetsSection, {
695
+ key: "presets",
696
+ presets,
697
+ axes,
698
+ defaults,
699
+ activeTuple,
700
+ lastApplied,
701
+ onApply: onApplyPreset
702
+ }), h("div", {
703
+ key: "presets-divider",
704
+ style: DIVIDER_STYLE
705
+ }));
706
+ axes.forEach((axis, idx) => {
707
+ sections.push(h(AxisSection, {
708
+ key: `axis-${axis.name}`,
709
+ axis,
710
+ active: activeTuple[axis.name] ?? axis.default,
711
+ onSelect: (next) => onSelectAxis(axis.name, next)
712
+ }));
713
+ if (idx === axes.length - 1) sections.push(h("div", {
714
+ key: "axes-divider",
715
+ style: DIVIDER_STYLE
716
+ }));
717
+ });
718
+ sections.push(h(ColorFormatSection, {
719
+ key: "color-format",
720
+ active: activeColorFormat,
721
+ onSelect: onSelectColorFormat
722
+ }));
723
+ return h("div", {
724
+ role: "menu",
725
+ tabIndex: -1,
726
+ onKeyDown,
727
+ style: {
728
+ minWidth: 260,
729
+ padding: "4px 0",
730
+ outline: "none"
731
+ },
732
+ "data-testid": "swatchbook-toolbar-popover"
733
+ }, ...sections);
734
+ }
735
+ function AxesToolbar() {
736
+ const [globals, updateGlobals] = useGlobals();
737
+ const api = useStorybookApi();
738
+ const [payload, setPayload] = useState(null);
739
+ const [open, setOpen] = useState(false);
740
+ const bodyRef = useRef(null);
741
+ useEffect(() => {
742
+ const channel = addons.getChannel();
743
+ const onInit = (next) => setPayload(next);
744
+ channel.on(INIT_EVENT, onInit);
745
+ return () => {
746
+ channel.off(INIT_EVENT, onInit);
747
+ };
748
+ }, []);
749
+ const axes = payload?.axes ?? EMPTY_AXES;
750
+ const presets = payload?.presets ?? EMPTY_PRESETS;
751
+ const themes = payload?.themes ?? EMPTY_THEMES;
752
+ const defaults = useMemo(() => defaultTupleFor(axes), [axes]);
753
+ const [lastApplied, setLastApplied] = useState(null);
754
+ const globalTuple = globals[AXES_GLOBAL_KEY];
755
+ const activeColorFormat = globals["swatchbookColorFormat"] ?? "hex";
756
+ const activeTuple = useMemo(() => {
757
+ const out = { ...defaults };
758
+ if (globalTuple) for (const axis of axes) {
759
+ const candidate = globalTuple[axis.name];
760
+ if (candidate !== void 0 && axis.contexts.includes(candidate)) out[axis.name] = candidate;
761
+ }
762
+ return out;
763
+ }, [
764
+ axes,
765
+ defaults,
766
+ globalTuple
767
+ ]);
768
+ const setAxis = useCallback((axisName, next) => {
769
+ const tuple = {
770
+ ...activeTuple,
771
+ [axisName]: next
772
+ };
773
+ const composed = composedNameFor(tuple, themes, payload?.defaultTheme ?? themes[0]?.name ?? "");
774
+ updateGlobals({
775
+ [AXES_GLOBAL_KEY]: tuple,
776
+ [GLOBAL_KEY]: composed
777
+ });
778
+ }, [
779
+ activeTuple,
780
+ themes,
781
+ payload?.defaultTheme,
782
+ updateGlobals
783
+ ]);
784
+ const applyPreset = useCallback((preset) => {
785
+ const tuple = presetTuple(preset, axes, defaults);
786
+ const composed = composedNameFor(tuple, themes, payload?.defaultTheme ?? themes[0]?.name ?? "");
787
+ updateGlobals({
788
+ [AXES_GLOBAL_KEY]: tuple,
789
+ [GLOBAL_KEY]: composed
790
+ });
791
+ setLastApplied(preset.name);
792
+ }, [
793
+ axes,
794
+ defaults,
795
+ themes,
796
+ payload?.defaultTheme,
797
+ updateGlobals
798
+ ]);
799
+ useEffect(() => {
800
+ if (axes.length === 0) return;
801
+ /**
802
+ * alt+T cycles the primary (first) axis's contexts, keeping the rest
803
+ * of the tuple pinned. With multi-axis projects this makes the shortcut
804
+ * predictable — you always know which wheel you're spinning. For
805
+ * single-axis projects the behavior is identical to the pre-N-dropdown
806
+ * toolbar (cycle through every theme).
807
+ */
808
+ const primary = axes[0];
809
+ if (!primary) return;
810
+ api.setAddonShortcut(ADDON_ID, {
811
+ label: `Cycle swatchbook ${displayLabelFor(primary)}`,
812
+ defaultShortcut: ["alt", "T"],
813
+ actionName: "cycleAxis",
814
+ showInMenu: true,
815
+ action: () => {
816
+ const current = activeTuple[primary.name] ?? primary.default;
817
+ const idx = primary.contexts.indexOf(current);
818
+ const next = primary.contexts[(idx + 1) % primary.contexts.length];
819
+ if (next !== void 0) setAxis(primary.name, next);
820
+ }
821
+ });
822
+ }, [
823
+ api,
824
+ axes,
825
+ activeTuple,
826
+ setAxis
827
+ ]);
828
+ const handleKeyDown = useCallback((event) => {
829
+ if (event.key === "Escape") {
830
+ event.stopPropagation();
831
+ setOpen(false);
832
+ }
833
+ }, []);
834
+ /**
835
+ * Escape closes even when focus hasn't entered the popover yet (e.g. the
836
+ * user opened it via click and the mouse is still over the canvas). We
837
+ * attach a document-level listener when open.
838
+ */
839
+ useEffect(() => {
840
+ if (!open) return;
841
+ const onDocKey = (e) => {
842
+ if (e.key === "Escape") setOpen(false);
843
+ };
844
+ document.addEventListener("keydown", onDocKey);
845
+ return () => document.removeEventListener("keydown", onDocKey);
846
+ }, [open]);
847
+ if (axes.length === 0) return h(IconButton, {
848
+ key: TOOL_ID,
849
+ title: "Swatchbook theme (loading…)",
850
+ disabled: true
851
+ }, h(SwatchbookIcon));
852
+ const button = h(IconButton, {
853
+ key: TOOL_ID,
854
+ title: `Swatchbook · ${axes.map((a) => activeTuple[a.name] ?? a.default).join(" · ")}`,
855
+ active: open,
856
+ onClick: () => setOpen((prev) => !prev)
857
+ }, h(SwatchbookIcon));
858
+ const tooltipBody = h(PopoverBody, {
859
+ axes,
860
+ presets,
861
+ defaults,
862
+ activeTuple,
863
+ activeColorFormat,
864
+ lastApplied,
865
+ onApplyPreset: applyPreset,
866
+ onSelectAxis: setAxis,
867
+ onSelectColorFormat: (next) => updateGlobals({ [COLOR_FORMAT_GLOBAL_KEY]: next }),
868
+ onKeyDown: handleKeyDown
869
+ });
870
+ return h("span", {
871
+ ref: bodyRef,
872
+ style: {
873
+ display: "inline-flex",
874
+ alignItems: "center"
875
+ }
876
+ }, h(WithTooltipPure, {
877
+ placement: "bottom",
878
+ trigger: "click",
879
+ visible: open,
880
+ onVisibleChange: (next) => setOpen(next),
881
+ closeOnOutsideClick: true,
882
+ tooltip: tooltipBody,
883
+ children: button
884
+ }));
885
+ }
886
+ addons.register(ADDON_ID, () => {
887
+ addons.add(TOOL_ID, {
888
+ type: types.TOOL,
889
+ title: "Swatchbook theme",
890
+ match: ({ viewMode, tabId }) => !tabId && (viewMode === "story" || viewMode === "docs"),
891
+ render: () => h(AxesToolbar)
892
+ });
893
+ addons.add(PANEL_ID, {
894
+ type: types.PANEL,
895
+ title: "Design Tokens",
896
+ match: ({ viewMode }) => viewMode === "story",
897
+ render: ({ active }) => h(DesignTokensPanel, { active: !!active })
898
+ });
899
+ });
900
+ //#endregion
901
+ export {};
902
+
903
+ //# sourceMappingURL=manager.mjs.map