@unpunnyfuns/swatchbook-blocks 1.1.1 → 2.0.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/README.md +5 -3
- package/dist/host-e2nSv-sA.mjs +791 -0
- package/dist/host-e2nSv-sA.mjs.map +1 -0
- package/dist/host.d.mts +57 -0
- package/dist/host.mjs +2 -0
- package/dist/index.d.mts +273 -328
- package/dist/index.mjs +536 -1112
- package/dist/index.mjs.map +1 -1
- package/dist/registry-Dm25Go94.d.mts +244 -0
- package/dist/style.css +237 -240
- package/package.json +6 -2
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
import './style.css';
|
|
2
|
+
import { COLOR_FORMATS } from "@unpunnyfuns/swatchbook-core/color-formats";
|
|
3
|
+
import { formatColor } from "@unpunnyfuns/swatchbook-core/format-color";
|
|
4
|
+
import { createContext, useContext, useEffect, useState, useSyncExternalStore } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { formatTokenValue } from "@unpunnyfuns/swatchbook-core/token-value-css";
|
|
7
|
+
import cx from "clsx";
|
|
8
|
+
//#region src/internal/value-to-css.ts
|
|
9
|
+
/**
|
|
10
|
+
* Formats a DTCG dimension-shaped raw value (`number`, pre-formatted
|
|
11
|
+
* string, or `{ value, unit }`) as a CSS length. Shared by the border and
|
|
12
|
+
* shadow presenters, whose `$value` shapes both carry raw dimension fields.
|
|
13
|
+
*/
|
|
14
|
+
function formatLength(raw) {
|
|
15
|
+
if (typeof raw === "number") return `${raw}px`;
|
|
16
|
+
if (typeof raw === "string") return raw;
|
|
17
|
+
if (raw && typeof raw === "object") {
|
|
18
|
+
const v = raw;
|
|
19
|
+
if (typeof v.value === "number" && typeof v.unit === "string") return `${v.value}${v.unit}`;
|
|
20
|
+
}
|
|
21
|
+
return "0px";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Maps a display `ColorFormat` to one CSS can actually render as a color
|
|
25
|
+
* value. `raw` renders a JSON debug string (core's `formatColor`), which
|
|
26
|
+
* isn't valid CSS: the border and shadow presenters substitute `hex` for
|
|
27
|
+
* that one format so the swatch still paints; every other format
|
|
28
|
+
* (rgb/hsl/oklch/hex) is already valid CSS.
|
|
29
|
+
*/
|
|
30
|
+
function cssColorFormat(format) {
|
|
31
|
+
return format === "raw" ? "hex" : format;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/border-preview/BorderSample.tsx
|
|
35
|
+
/** Pure presentation for a single border token's sample. Renders from plain props. */
|
|
36
|
+
function BorderSampleView({ cssVar }) {
|
|
37
|
+
return /* @__PURE__ */ jsx("div", {
|
|
38
|
+
className: "sb-border-sample",
|
|
39
|
+
style: { border: cssVar },
|
|
40
|
+
"aria-hidden": true
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Builds a valid `border` shorthand value straight from a realised
|
|
45
|
+
* `border.$value`. Distinct from core's `formatTokenValue`, whose `raw`
|
|
46
|
+
* color branch emits a display-only JSON string that the browser would
|
|
47
|
+
* reject as a border color.
|
|
48
|
+
*/
|
|
49
|
+
function borderValueToCss(value, colorFormat) {
|
|
50
|
+
if (!value || typeof value !== "object") return "";
|
|
51
|
+
return [
|
|
52
|
+
formatLength(value.width),
|
|
53
|
+
typeof value.style === "string" ? value.style : "solid",
|
|
54
|
+
formatColor(value.color, cssColorFormat(colorFormat)).value
|
|
55
|
+
].join(" ");
|
|
56
|
+
}
|
|
57
|
+
function BorderSample({ token, cssVar, colorFormat }) {
|
|
58
|
+
return /* @__PURE__ */ jsx(BorderSampleView, { cssVar: cssVar ?? borderValueToCss(token.$value, colorFormat) });
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/presenters/ColorSwatch.tsx
|
|
62
|
+
function leafOf(path) {
|
|
63
|
+
return path.split(".").at(-1) ?? path;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Presenter for `$type: color` tokens: a color chip, leaf label, and
|
|
67
|
+
* formatted value with an out-of-gamut marker. The chip's background is
|
|
68
|
+
* `cssVar` when supplied (R3); otherwise it is computed straight from
|
|
69
|
+
* `token.$value`, with `raw` guarded to `hex` (`cssColorFormat`) so the chip
|
|
70
|
+
* always paints a real color even when the display format is a JSON debug
|
|
71
|
+
* string. The value text below the chip uses the actual `colorFormat`,
|
|
72
|
+
* `raw` included. `options.label` lets a grouped consumer (e.g.
|
|
73
|
+
* `ColorPalette`) supply a group-relative label instead of the bare leaf
|
|
74
|
+
* segment, so context lost to grouping (`blue` in `color.palette.blue.50`)
|
|
75
|
+
* still reads on the swatch; standalone usage falls back to `leafOf(path)`.
|
|
76
|
+
*/
|
|
77
|
+
function ColorSwatch({ path, token, cssVar, colorFormat, options }) {
|
|
78
|
+
const chipBackground = cssVar ?? formatColor(token.$value, cssColorFormat(colorFormat)).value;
|
|
79
|
+
const { value, outOfGamut } = formatColor(token.$value, colorFormat);
|
|
80
|
+
const label = typeof options?.["label"] === "string" ? options["label"] : leafOf(path);
|
|
81
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
82
|
+
className: "sb-color-swatch",
|
|
83
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
84
|
+
className: "sb-color-swatch__chip",
|
|
85
|
+
style: { background: chipBackground },
|
|
86
|
+
"aria-hidden": true
|
|
87
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
88
|
+
className: "sb-color-swatch__meta",
|
|
89
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
90
|
+
className: "sb-color-swatch__leaf",
|
|
91
|
+
children: label
|
|
92
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
93
|
+
className: "sb-color-swatch__value",
|
|
94
|
+
children: [value, outOfGamut && /* @__PURE__ */ jsxs("span", {
|
|
95
|
+
title: "Out of sRGB gamut for this format",
|
|
96
|
+
"aria-label": "out of gamut",
|
|
97
|
+
className: "sb-color-swatch__gamut-warn",
|
|
98
|
+
children: [" ", "⚠"]
|
|
99
|
+
})]
|
|
100
|
+
})]
|
|
101
|
+
})]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/dimension-scale/dimension-px.ts
|
|
106
|
+
/**
|
|
107
|
+
* Convert a DTCG dimension `$value` (`{ value, unit }`) to pixels for the
|
|
108
|
+
* purpose of deciding whether to cap the rendered size. `rootFontSizePx`
|
|
109
|
+
* scales `rem` against the rendering context's actual root font-size
|
|
110
|
+
* (default 16 for the no-DOM / SSR path); the bar paints at the same
|
|
111
|
+
* context's `var()`, so passing the measured root keeps the cap decision
|
|
112
|
+
* aligned with what's drawn. Returns `NaN` for anything other than `px` /
|
|
113
|
+
* `rem` — `ex` / `ch` / `%`, and the non-DTCG `em` (the `dimension` type
|
|
114
|
+
* permits only `px | rem`) — which the caller treats as "render at cssVar
|
|
115
|
+
* but don't cap".
|
|
116
|
+
*/
|
|
117
|
+
function toPixels(raw, rootFontSizePx = 16) {
|
|
118
|
+
if (raw == null || typeof raw !== "object") return NaN;
|
|
119
|
+
const v = raw;
|
|
120
|
+
if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
|
|
121
|
+
switch (v.unit) {
|
|
122
|
+
case "px": return v.value;
|
|
123
|
+
case "rem": return v.value * rootFontSizePx;
|
|
124
|
+
default: return NaN;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/internal/use-root-font-size.ts
|
|
129
|
+
function readRootFontSize() {
|
|
130
|
+
if (typeof document === "undefined") return 16;
|
|
131
|
+
const fontSize = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
132
|
+
return Number.isFinite(fontSize) && fontSize > 0 ? fontSize : 16;
|
|
133
|
+
}
|
|
134
|
+
function subscribe$1(onChange) {
|
|
135
|
+
if (typeof window === "undefined") return () => {};
|
|
136
|
+
window.addEventListener("resize", onChange);
|
|
137
|
+
return () => window.removeEventListener("resize", onChange);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Root font-size (px) of the context a block renders in, tracked across
|
|
141
|
+
* viewport changes. `rem` dimension values resolve their `var()` against this
|
|
142
|
+
* root, so the cap math (`toPixels`) and value sort (`sortTokens`) must scale
|
|
143
|
+
* `rem` by it rather than a literal 16: a Storybook Docs page, or a responsive
|
|
144
|
+
* desktop/tablet/mobile breakpoint, can apply a different root to the same
|
|
145
|
+
* token. Falls back to 16 with no DOM.
|
|
146
|
+
*/
|
|
147
|
+
function useRootFontSize() {
|
|
148
|
+
return useSyncExternalStore(subscribe$1, readRootFontSize, () => 16);
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/dimension-scale/DimensionSample.tsx
|
|
152
|
+
function asDimensionVisual(raw) {
|
|
153
|
+
return raw === "length" || raw === "radius" || raw === "size" ? raw : "length";
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Pure derivation of a single dimension token's sample geometry from its
|
|
157
|
+
* realised `$value`. The cap decision always reads the numeric `$value` (so
|
|
158
|
+
* it holds regardless of whether `cssVar` is present); `cssVar`, when given,
|
|
159
|
+
* wins as the rendered value up to that cap.
|
|
160
|
+
*/
|
|
161
|
+
function deriveDimensionSample(props, rootFontSizePx) {
|
|
162
|
+
const realised = formatTokenValue(props.token.$value, "dimension", props.colorFormat);
|
|
163
|
+
const cssVar = props.cssVar ?? realised;
|
|
164
|
+
const pxValue = toPixels(props.token.$value, rootFontSizePx);
|
|
165
|
+
const capped = Number.isFinite(pxValue) && pxValue > 480;
|
|
166
|
+
return {
|
|
167
|
+
cssVar,
|
|
168
|
+
pxValue,
|
|
169
|
+
capped,
|
|
170
|
+
cappedValue: capped ? `480px` : cssVar
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function withCap(visual) {
|
|
174
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
175
|
+
className: "sb-dimension-sample sb-dimension-sample--capped",
|
|
176
|
+
title: `capped at 480px`,
|
|
177
|
+
children: [visual, /* @__PURE__ */ jsx("span", {
|
|
178
|
+
className: "sb-dimension-sample__cap",
|
|
179
|
+
"aria-hidden": true,
|
|
180
|
+
children: "…"
|
|
181
|
+
})]
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/** Pure presentation for a single dimension token's bar/sample. Renders from plain props. */
|
|
185
|
+
function DimensionSampleView({ cssVar, capped, cappedValue, visual }) {
|
|
186
|
+
switch (visual) {
|
|
187
|
+
case "radius": return /* @__PURE__ */ jsx("div", {
|
|
188
|
+
className: "sb-dimension-sample__radius-sample",
|
|
189
|
+
style: { borderRadius: cssVar },
|
|
190
|
+
"aria-hidden": true
|
|
191
|
+
});
|
|
192
|
+
case "size": {
|
|
193
|
+
const sample = /* @__PURE__ */ jsx("div", {
|
|
194
|
+
className: "sb-dimension-sample__size-sample",
|
|
195
|
+
style: {
|
|
196
|
+
width: cappedValue,
|
|
197
|
+
height: cappedValue
|
|
198
|
+
},
|
|
199
|
+
"aria-hidden": true
|
|
200
|
+
});
|
|
201
|
+
return capped ? withCap(sample) : sample;
|
|
202
|
+
}
|
|
203
|
+
default: {
|
|
204
|
+
const bar = /* @__PURE__ */ jsx("div", {
|
|
205
|
+
className: "sb-dimension-sample__bar",
|
|
206
|
+
style: { width: cappedValue },
|
|
207
|
+
"aria-hidden": true
|
|
208
|
+
});
|
|
209
|
+
return capped ? withCap(bar) : bar;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Connected block: renders a realised dimension token's sample, capped for oversized values. */
|
|
214
|
+
function DimensionSample({ token, cssVar, colorFormat, options }) {
|
|
215
|
+
const visual = asDimensionVisual(options?.["visual"]);
|
|
216
|
+
const rootFontSize = useRootFontSize();
|
|
217
|
+
const derived = deriveDimensionSample({
|
|
218
|
+
token,
|
|
219
|
+
cssVar,
|
|
220
|
+
colorFormat
|
|
221
|
+
}, rootFontSize);
|
|
222
|
+
return /* @__PURE__ */ jsx(DimensionSampleView, {
|
|
223
|
+
cssVar: derived.cssVar,
|
|
224
|
+
capped: derived.capped,
|
|
225
|
+
cappedValue: derived.cappedValue,
|
|
226
|
+
visual
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/presenters/FontFamilySpecimen.tsx
|
|
231
|
+
const DEFAULT_SAMPLE$2 = "The quick brown fox jumps over the lazy dog.";
|
|
232
|
+
function stackString(raw) {
|
|
233
|
+
if (typeof raw === "string") return raw;
|
|
234
|
+
if (Array.isArray(raw)) return raw.map(String).join(", ");
|
|
235
|
+
return "";
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Presenter for `$type: fontFamily` tokens: path, resolved stack, a sample
|
|
239
|
+
* line styled per R3, and the css var in use.
|
|
240
|
+
*/
|
|
241
|
+
function FontFamilySpecimen({ path, token, cssVar, options }) {
|
|
242
|
+
const rawSample = options?.["sample"];
|
|
243
|
+
const sample = typeof rawSample === "string" ? rawSample : DEFAULT_SAMPLE$2;
|
|
244
|
+
const stack = stackString(token.$value);
|
|
245
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
246
|
+
className: "sb-font-family-specimen__row",
|
|
247
|
+
children: [
|
|
248
|
+
/* @__PURE__ */ jsxs("div", {
|
|
249
|
+
className: "sb-font-family-specimen__meta",
|
|
250
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
251
|
+
className: "sb-font-family-specimen__path",
|
|
252
|
+
children: path
|
|
253
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
254
|
+
className: "sb-font-family-specimen__stack",
|
|
255
|
+
children: stack
|
|
256
|
+
})]
|
|
257
|
+
}),
|
|
258
|
+
/* @__PURE__ */ jsx("div", {
|
|
259
|
+
className: "sb-font-family-specimen__sample",
|
|
260
|
+
style: { fontFamily: cssVar ?? stack },
|
|
261
|
+
children: sample
|
|
262
|
+
}),
|
|
263
|
+
cssVar && /* @__PURE__ */ jsx("span", {
|
|
264
|
+
className: "sb-font-family-specimen__css-var",
|
|
265
|
+
children: cssVar
|
|
266
|
+
})
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/internal/css-var-style.ts
|
|
272
|
+
/**
|
|
273
|
+
* Coerce a CSS `var(--…)` reference into the numeric slot of a React
|
|
274
|
+
* inline-style property.
|
|
275
|
+
*
|
|
276
|
+
* React's `CSSProperties` types unitless properties (`fontWeight`,
|
|
277
|
+
* `lineHeight`, `zIndex`, …) as `number`. The DOM accepts a string at
|
|
278
|
+
* runtime — the rendered stylesheet just receives whatever React passes —
|
|
279
|
+
* so a `var(--font-weight)` reference works functionally. TypeScript still
|
|
280
|
+
* complains. Centralising the type assertion in one named helper keeps
|
|
281
|
+
* the gap visible (and greppable) instead of scattering casts across
|
|
282
|
+
* block components that want CSS-var-driven typography or layout values.
|
|
283
|
+
*/
|
|
284
|
+
const cssVarAsNumber = (varRef) => varRef;
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/presenters/FontWeightSpecimen.tsx
|
|
287
|
+
const DEFAULT_SAMPLE$1 = "Aa";
|
|
288
|
+
/**
|
|
289
|
+
* Presenter for `$type: fontWeight` tokens: path, the raw weight value, a
|
|
290
|
+
* sample glyph styled per R3, and the css var in use.
|
|
291
|
+
*/
|
|
292
|
+
function FontWeightSpecimen({ path, token, cssVar, options }) {
|
|
293
|
+
const rawSample = options?.["sample"];
|
|
294
|
+
const sample = typeof rawSample === "string" ? rawSample : DEFAULT_SAMPLE$1;
|
|
295
|
+
const display = token.$value == null ? "" : String(token.$value);
|
|
296
|
+
const sampleStyle = { fontWeight: cssVar ? cssVarAsNumber(cssVar) : display };
|
|
297
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
298
|
+
className: "sb-font-weight-specimen__row",
|
|
299
|
+
children: [
|
|
300
|
+
/* @__PURE__ */ jsxs("div", {
|
|
301
|
+
className: "sb-font-weight-specimen__meta",
|
|
302
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
303
|
+
className: "sb-font-weight-specimen__path",
|
|
304
|
+
children: path
|
|
305
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
306
|
+
className: "sb-font-weight-specimen__value",
|
|
307
|
+
children: display
|
|
308
|
+
})]
|
|
309
|
+
}),
|
|
310
|
+
/* @__PURE__ */ jsx("div", {
|
|
311
|
+
className: "sb-font-weight-specimen__sample",
|
|
312
|
+
style: sampleStyle,
|
|
313
|
+
children: sample
|
|
314
|
+
}),
|
|
315
|
+
cssVar && /* @__PURE__ */ jsx("span", {
|
|
316
|
+
className: "sb-font-weight-specimen__css-var",
|
|
317
|
+
children: cssVar
|
|
318
|
+
})
|
|
319
|
+
]
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/presenters/GradientSwatch.tsx
|
|
324
|
+
/**
|
|
325
|
+
* Builds a valid `linear-gradient()` value straight from a realised
|
|
326
|
+
* `gradient.$value` stop list. Distinct from core's `formatTokenValue`,
|
|
327
|
+
* whose `raw` color branch emits a display-only JSON string per stop that
|
|
328
|
+
* the browser would reject as a gradient color.
|
|
329
|
+
*/
|
|
330
|
+
function gradientValueToCss(stops, colorFormat) {
|
|
331
|
+
const format = cssColorFormat(colorFormat);
|
|
332
|
+
return `linear-gradient(90deg, ${stops.map((stop) => {
|
|
333
|
+
return `${formatColor(stop.color, format).value} ${((typeof stop.position === "number" ? stop.position : 0) * 100).toFixed(0)}%`;
|
|
334
|
+
}).join(", ")})`;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Presenter for `$type: gradient` tokens: a gradient chip. Unlike the
|
|
338
|
+
* other composite presenters, a gradient's css var (per Terrazzo's
|
|
339
|
+
* `transformGradient`) resolves to a bare `<color> <pct>, ...` stop list,
|
|
340
|
+
* not a complete property value: DTCG gradients carry no direction, so
|
|
341
|
+
* both branches need one supplied at render time. `90deg` (`to right`)
|
|
342
|
+
* matches the direction the standalone block previously hard-coded.
|
|
343
|
+
*/
|
|
344
|
+
function GradientSwatch({ token, cssVar, colorFormat }) {
|
|
345
|
+
const stops = Array.isArray(token.$value) ? token.$value : [];
|
|
346
|
+
return /* @__PURE__ */ jsx("div", {
|
|
347
|
+
className: "sb-gradient-swatch__chip",
|
|
348
|
+
style: { background: cssVar ? `linear-gradient(90deg, ${cssVar})` : gradientValueToCss(stops, colorFormat) },
|
|
349
|
+
"aria-hidden": true
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/internal/prefers-reduced-motion.ts
|
|
354
|
+
function isChromatic() {
|
|
355
|
+
if (typeof navigator === "undefined") return false;
|
|
356
|
+
return navigator.userAgent.includes("Chromatic");
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Reactive `prefers-reduced-motion: reduce` detector. Returns the current
|
|
360
|
+
* match and updates if the user toggles the OS-level preference. Also
|
|
361
|
+
* returns `true` under Chromatic to keep animated samples deterministic
|
|
362
|
+
* during visual regression capture.
|
|
363
|
+
*/
|
|
364
|
+
function usePrefersReducedMotion() {
|
|
365
|
+
const [reduced, setReduced] = useState(false);
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
if (typeof window === "undefined") return;
|
|
368
|
+
if (isChromatic()) {
|
|
369
|
+
setReduced(true);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
373
|
+
setReduced(query.matches);
|
|
374
|
+
const onChange = (e) => setReduced(e.matches);
|
|
375
|
+
query.addEventListener("change", onChange);
|
|
376
|
+
return () => query.removeEventListener("change", onChange);
|
|
377
|
+
}, []);
|
|
378
|
+
return reduced;
|
|
379
|
+
}
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region src/motion-preview/MotionSample.tsx
|
|
382
|
+
const DEFAULT_DURATION_MS = 300;
|
|
383
|
+
const DEFAULT_EASING = "cubic-bezier(0.2, 0, 0, 1)";
|
|
384
|
+
function extractDurationMs(raw) {
|
|
385
|
+
if (raw == null) return NaN;
|
|
386
|
+
if (typeof raw === "object") {
|
|
387
|
+
const v = raw;
|
|
388
|
+
if (typeof v.value === "number" && typeof v.unit === "string") {
|
|
389
|
+
if (v.unit === "ms") return v.value;
|
|
390
|
+
if (v.unit === "s") return v.value * 1e3;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return NaN;
|
|
394
|
+
}
|
|
395
|
+
function extractCubicBezier(raw) {
|
|
396
|
+
if (Array.isArray(raw) && raw.length === 4 && raw.every((n) => typeof n === "number")) return `cubic-bezier(${raw.map((n) => Number(n).toFixed(3)).join(", ")})`;
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
function asDuration(raw, themeTokens, fallback) {
|
|
400
|
+
const direct = extractDurationMs(raw);
|
|
401
|
+
if (Number.isFinite(direct)) return direct;
|
|
402
|
+
if (typeof raw === "string") {
|
|
403
|
+
const match = raw.match(/^\{([^}]+)\}$/);
|
|
404
|
+
if (match && match[1]) {
|
|
405
|
+
const referenced = themeTokens[match[1]];
|
|
406
|
+
const resolved = extractDurationMs(referenced?.$value);
|
|
407
|
+
if (Number.isFinite(resolved)) return resolved;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return fallback;
|
|
411
|
+
}
|
|
412
|
+
function asEasing(raw, themeTokens, fallback) {
|
|
413
|
+
const direct = extractCubicBezier(raw);
|
|
414
|
+
if (direct) return direct;
|
|
415
|
+
if (typeof raw === "string") {
|
|
416
|
+
const match = raw.match(/^\{([^}]+)\}$/);
|
|
417
|
+
if (match && match[1]) {
|
|
418
|
+
const referenced = themeTokens[match[1]];
|
|
419
|
+
const resolved = extractCubicBezier(referenced?.$value);
|
|
420
|
+
if (resolved) return resolved;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return fallback;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Extracts a duration/easing `Spec` from a token's `$value`, dispatching on
|
|
427
|
+
* `$type` (`transition` / `duration` / `cubicBezier`). `themeTokens` backs
|
|
428
|
+
* `{path}`-form sub-value alias resolution for callers still walking a raw
|
|
429
|
+
* (not-yet-realised) project map; pass `{}` when `token` is already a
|
|
430
|
+
* realised leaf (no alias strings survive resolution).
|
|
431
|
+
*/
|
|
432
|
+
function resolveMotionSpec(token, themeTokens) {
|
|
433
|
+
if (!token) return null;
|
|
434
|
+
const type = token.$type;
|
|
435
|
+
if (type === "transition") {
|
|
436
|
+
const v = token.$value ?? {};
|
|
437
|
+
return {
|
|
438
|
+
durationMs: asDuration(v.duration, themeTokens, DEFAULT_DURATION_MS),
|
|
439
|
+
easing: asEasing(v.timingFunction, themeTokens, DEFAULT_EASING)
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (type === "duration") {
|
|
443
|
+
const durationMs = extractDurationMs(token.$value);
|
|
444
|
+
if (!Number.isFinite(durationMs)) return null;
|
|
445
|
+
return {
|
|
446
|
+
durationMs,
|
|
447
|
+
easing: DEFAULT_EASING
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (type === "cubicBezier") {
|
|
451
|
+
const easing = extractCubicBezier(token.$value);
|
|
452
|
+
if (!easing) return null;
|
|
453
|
+
return {
|
|
454
|
+
durationMs: DEFAULT_DURATION_MS,
|
|
455
|
+
easing
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
/** Pure presentation + animation for a single motion token's sample. Renders from plain props. */
|
|
461
|
+
function MotionSampleView({ spec, speed, runKey }) {
|
|
462
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
463
|
+
const durationMs = spec?.durationMs ?? DEFAULT_DURATION_MS;
|
|
464
|
+
const easing = spec?.easing ?? DEFAULT_EASING;
|
|
465
|
+
const scaledDuration = Math.max(1, durationMs / speed);
|
|
466
|
+
const [phase, setPhase] = useState(0);
|
|
467
|
+
useEffect(() => {
|
|
468
|
+
if (reducedMotion) return;
|
|
469
|
+
setPhase(0);
|
|
470
|
+
const id = requestAnimationFrame(() => setPhase(1));
|
|
471
|
+
const loop = window.setInterval(() => {
|
|
472
|
+
setPhase((p) => p === 0 ? 1 : 0);
|
|
473
|
+
}, scaledDuration * 2);
|
|
474
|
+
return () => {
|
|
475
|
+
cancelAnimationFrame(id);
|
|
476
|
+
window.clearInterval(loop);
|
|
477
|
+
};
|
|
478
|
+
}, [
|
|
479
|
+
scaledDuration,
|
|
480
|
+
runKey,
|
|
481
|
+
reducedMotion
|
|
482
|
+
]);
|
|
483
|
+
if (reducedMotion) return /* @__PURE__ */ jsxs("div", {
|
|
484
|
+
className: "sb-motion-sample__reduced-motion",
|
|
485
|
+
children: [
|
|
486
|
+
"Animation suppressed by ",
|
|
487
|
+
/* @__PURE__ */ jsx("code", { children: "prefers-reduced-motion: reduce" }),
|
|
488
|
+
"."
|
|
489
|
+
]
|
|
490
|
+
});
|
|
491
|
+
return /* @__PURE__ */ jsx("div", {
|
|
492
|
+
className: "sb-motion-sample__track",
|
|
493
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
494
|
+
className: cx("sb-motion-sample__ball", phase === 1 ? "sb-motion-sample__ball--end" : "sb-motion-sample__ball--start"),
|
|
495
|
+
style: { transition: `left ${scaledDuration}ms ${easing}` },
|
|
496
|
+
"aria-hidden": true
|
|
497
|
+
})
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Connected block: renders a realised motion token's animated sample. Both
|
|
502
|
+
* `durationMs` and `easing` come from the realised `$value`; the ball's
|
|
503
|
+
* setInterval loop needs a JS-readable duration, and (see the comment below)
|
|
504
|
+
* `cssVar` cannot stand in for `easing` here. `cssVar` is still accepted, per
|
|
505
|
+
* the uniform `PresenterProps` contract every presenter shares, but this
|
|
506
|
+
* View has nowhere safe to apply it.
|
|
507
|
+
*/
|
|
508
|
+
function MotionSample({ token, cssVar: _cssVar, options }) {
|
|
509
|
+
const rawSpeed = options?.["speed"];
|
|
510
|
+
const speed = typeof rawSpeed === "number" ? rawSpeed : 1;
|
|
511
|
+
const rawRunKey = options?.["runKey"];
|
|
512
|
+
const runKey = typeof rawRunKey === "number" ? rawRunKey : 0;
|
|
513
|
+
return /* @__PURE__ */ jsx(MotionSampleView, {
|
|
514
|
+
spec: resolveMotionSpec(token, {}),
|
|
515
|
+
speed,
|
|
516
|
+
runKey
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/presenters/OpacitySwatch.tsx
|
|
521
|
+
const DEFAULT_SAMPLE_COLOR_VAR = "var(--swatchbook-accent-bg)";
|
|
522
|
+
function opacityValueToCss(value) {
|
|
523
|
+
return typeof value === "number" ? String(value) : "1";
|
|
524
|
+
}
|
|
525
|
+
/** Pure presentation for a single opacity token's chip. Renders from plain props. */
|
|
526
|
+
function OpacitySwatchView({ opacity, sampleColorVar }) {
|
|
527
|
+
return /* @__PURE__ */ jsx("div", {
|
|
528
|
+
className: "sb-opacity-swatch",
|
|
529
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
530
|
+
className: "sb-opacity-swatch__chip",
|
|
531
|
+
style: {
|
|
532
|
+
opacity,
|
|
533
|
+
background: sampleColorVar
|
|
534
|
+
},
|
|
535
|
+
"aria-hidden": true
|
|
536
|
+
})
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Presenter for `$type: number` opacity tokens: a colored chip over a
|
|
541
|
+
* checkerboard backdrop, with the chip's `opacity` set from the token per
|
|
542
|
+
* R3. The number alone (`0.4`) doesn't convey what the token looks like
|
|
543
|
+
* applied to a surface; the chip does.
|
|
544
|
+
*/
|
|
545
|
+
function OpacitySwatch({ token, cssVar, options }) {
|
|
546
|
+
const opacity = cssVar ?? opacityValueToCss(token.$value);
|
|
547
|
+
const rawSampleColorVar = options?.["sampleColorVar"];
|
|
548
|
+
return /* @__PURE__ */ jsx(OpacitySwatchView, {
|
|
549
|
+
opacity,
|
|
550
|
+
sampleColorVar: typeof rawSampleColorVar === "string" ? rawSampleColorVar : DEFAULT_SAMPLE_COLOR_VAR
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region src/presenters/StrokeSample.tsx
|
|
555
|
+
const STRING_STYLES = new Set([
|
|
556
|
+
"solid",
|
|
557
|
+
"dashed",
|
|
558
|
+
"dotted",
|
|
559
|
+
"double",
|
|
560
|
+
"groove",
|
|
561
|
+
"ridge",
|
|
562
|
+
"outset",
|
|
563
|
+
"inset"
|
|
564
|
+
]);
|
|
565
|
+
function isCssStyleKeyword(value) {
|
|
566
|
+
return typeof value === "string" && STRING_STYLES.has(value);
|
|
567
|
+
}
|
|
568
|
+
/** Pure presentation for a single stroke-style token's line. Renders from plain props. */
|
|
569
|
+
function StrokeSampleView({ cssStyle }) {
|
|
570
|
+
if (!cssStyle) return /* @__PURE__ */ jsx("span", {
|
|
571
|
+
className: "sb-stroke-sample__object-fallback",
|
|
572
|
+
children: "Object-form (dashArray + lineCap) — no pure CSS `border-style` equivalent."
|
|
573
|
+
});
|
|
574
|
+
return /* @__PURE__ */ jsx("div", {
|
|
575
|
+
className: "sb-stroke-sample__line",
|
|
576
|
+
style: { borderTopStyle: cssStyle },
|
|
577
|
+
"aria-hidden": true
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Presenter for `$type: strokeStyle` tokens: a line styled with the
|
|
582
|
+
* token's `border-top-style` per R3, or the object-form fallback message
|
|
583
|
+
* when `$value` is a dash pattern with no pure CSS equivalent.
|
|
584
|
+
*/
|
|
585
|
+
function StrokeSample({ token, cssVar }) {
|
|
586
|
+
return /* @__PURE__ */ jsx(StrokeSampleView, { cssStyle: isCssStyleKeyword(token.$value) ? cssVar ?? token.$value : null });
|
|
587
|
+
}
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/presenters/TypeSpecimen.tsx
|
|
590
|
+
const DEFAULT_SAMPLE = "The quick brown fox jumps over the lazy dog.";
|
|
591
|
+
function asFontFamily(raw) {
|
|
592
|
+
if (typeof raw === "string") return raw;
|
|
593
|
+
if (Array.isArray(raw)) return raw.map(String).join(", ");
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Builds the sample's inline style straight from a realised
|
|
597
|
+
* `typography.$value`. `fontSize`/`letterSpacing` share `formatLength` with
|
|
598
|
+
* the border/shadow presenters (same DTCG `{ value, unit }` dimension
|
|
599
|
+
* envelope); `fontWeight`/`lineHeight` are unitless and cast to strings
|
|
600
|
+
* directly.
|
|
601
|
+
*/
|
|
602
|
+
function styleFromValue(value) {
|
|
603
|
+
const style = {};
|
|
604
|
+
const fontFamily = asFontFamily(value.fontFamily);
|
|
605
|
+
if (fontFamily) style.fontFamily = fontFamily;
|
|
606
|
+
if (value.fontSize != null) style.fontSize = formatLength(value.fontSize);
|
|
607
|
+
if (value.fontWeight != null) style.fontWeight = String(value.fontWeight);
|
|
608
|
+
if (value.lineHeight != null) style.lineHeight = String(value.lineHeight);
|
|
609
|
+
if (value.letterSpacing != null) style.letterSpacing = formatLength(value.letterSpacing);
|
|
610
|
+
return style;
|
|
611
|
+
}
|
|
612
|
+
function describeValue(value) {
|
|
613
|
+
return [
|
|
614
|
+
value.fontSize != null ? formatLength(value.fontSize) : void 0,
|
|
615
|
+
value.fontWeight != null ? `w${String(value.fontWeight)}` : void 0,
|
|
616
|
+
value.lineHeight != null ? `lh ${String(value.lineHeight)}` : void 0
|
|
617
|
+
].filter(Boolean).join(" · ");
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Presenter for `$type: typography` tokens: a spec summary and a sample line
|
|
621
|
+
* styled per R3. Token identity (the path) is the consuming block's
|
|
622
|
+
* responsibility, not this presenter's; see `TypographyScale`.
|
|
623
|
+
*/
|
|
624
|
+
function TypeSpecimen({ token, cssVar: _cssVar, options }) {
|
|
625
|
+
const rawSample = options?.["sample"];
|
|
626
|
+
const sample = typeof rawSample === "string" ? rawSample : DEFAULT_SAMPLE;
|
|
627
|
+
const value = token.$value ?? {};
|
|
628
|
+
const sampleStyle = styleFromValue(value);
|
|
629
|
+
const specs = describeValue(value);
|
|
630
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
631
|
+
className: "sb-type-specimen",
|
|
632
|
+
children: [specs && /* @__PURE__ */ jsx("span", {
|
|
633
|
+
className: "sb-type-specimen__description",
|
|
634
|
+
children: specs
|
|
635
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
636
|
+
className: "sb-type-specimen__sample",
|
|
637
|
+
style: sampleStyle,
|
|
638
|
+
children: sample
|
|
639
|
+
})]
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
//#endregion
|
|
643
|
+
//#region src/shadow-preview/ShadowSample.tsx
|
|
644
|
+
/** Pure presentation for a single shadow token's sample. Renders from plain props. */
|
|
645
|
+
function ShadowSampleView({ cssVar }) {
|
|
646
|
+
return /* @__PURE__ */ jsx("div", {
|
|
647
|
+
className: "sb-shadow-sample",
|
|
648
|
+
style: { boxShadow: cssVar },
|
|
649
|
+
"aria-hidden": true
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Builds a valid `box-shadow` value straight from a realised `shadow.$value`
|
|
654
|
+
* (single layer or array). Distinct from core's `formatTokenValue`, whose
|
|
655
|
+
* `raw` color branch emits a display-only JSON string that the browser
|
|
656
|
+
* would reject as a box-shadow color.
|
|
657
|
+
*/
|
|
658
|
+
function shadowValueToCss(value, colorFormat) {
|
|
659
|
+
const layers = Array.isArray(value) ? value : [value];
|
|
660
|
+
const format = cssColorFormat(colorFormat);
|
|
661
|
+
return layers.map((layer) => {
|
|
662
|
+
if (!layer || typeof layer !== "object") return "";
|
|
663
|
+
const s = layer;
|
|
664
|
+
const pieces = [
|
|
665
|
+
formatLength(s.offsetX),
|
|
666
|
+
formatLength(s.offsetY),
|
|
667
|
+
formatLength(s.blur),
|
|
668
|
+
formatLength(s.spread),
|
|
669
|
+
formatColor(s.color, format).value
|
|
670
|
+
];
|
|
671
|
+
if (s.inset) pieces.push("inset");
|
|
672
|
+
return pieces.join(" ");
|
|
673
|
+
}).filter((layer) => layer !== "").join(", ");
|
|
674
|
+
}
|
|
675
|
+
function ShadowSample({ token, cssVar, colorFormat }) {
|
|
676
|
+
return /* @__PURE__ */ jsx(ShadowSampleView, { cssVar: cssVar ?? shadowValueToCss(token.$value, colorFormat) });
|
|
677
|
+
}
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/presenters/registry.ts
|
|
680
|
+
/**
|
|
681
|
+
* Built-in presenters, keyed by DTCG `$type`. Phase C registers each entry as
|
|
682
|
+
* its presenter lands. A consumer's `presenters` override merges over this.
|
|
683
|
+
*/
|
|
684
|
+
const DEFAULT_PRESENTERS = {
|
|
685
|
+
color: ColorSwatch,
|
|
686
|
+
shadow: ShadowSample,
|
|
687
|
+
border: BorderSample,
|
|
688
|
+
dimension: DimensionSample,
|
|
689
|
+
gradient: GradientSwatch,
|
|
690
|
+
transition: MotionSample,
|
|
691
|
+
typography: TypeSpecimen,
|
|
692
|
+
fontFamily: FontFamilySpecimen,
|
|
693
|
+
fontWeight: FontWeightSpecimen,
|
|
694
|
+
number: OpacitySwatch,
|
|
695
|
+
strokeStyle: StrokeSample
|
|
696
|
+
};
|
|
697
|
+
/** Overrides win per `$type`; unlisted types keep the built-in. */
|
|
698
|
+
function mergePresenters(overrides) {
|
|
699
|
+
return overrides ? {
|
|
700
|
+
...DEFAULT_PRESENTERS,
|
|
701
|
+
...overrides
|
|
702
|
+
} : DEFAULT_PRESENTERS;
|
|
703
|
+
}
|
|
704
|
+
/** The active merged registry for the subtree. */
|
|
705
|
+
const PresenterContext = createContext(DEFAULT_PRESENTERS);
|
|
706
|
+
let ambientPresenters = DEFAULT_PRESENTERS;
|
|
707
|
+
/**
|
|
708
|
+
* Set the ambient presenter registry consulted by blocks that render with
|
|
709
|
+
* no `SwatchbookProvider` or `PresenterContext.Provider` above them (MDX
|
|
710
|
+
* doc blocks, autodocs). A host registers this once at preview-init; it is
|
|
711
|
+
* NOT reactive, so register before first render. Passing no argument (or
|
|
712
|
+
* `undefined`) resets to the built-ins. An explicit provider/context
|
|
713
|
+
* override still wins for its subtree; this only fills the provider-less
|
|
714
|
+
* gap.
|
|
715
|
+
*/
|
|
716
|
+
function registerPresenters(overrides) {
|
|
717
|
+
ambientPresenters = mergePresenters(overrides);
|
|
718
|
+
}
|
|
719
|
+
/** The presenter for `type`, or `undefined` if none is registered. */
|
|
720
|
+
function usePresenter(type) {
|
|
721
|
+
const ctx = useContext(PresenterContext);
|
|
722
|
+
return (ctx === DEFAULT_PRESENTERS ? ambientPresenters : ctx)[type];
|
|
723
|
+
}
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region src/host.ts
|
|
726
|
+
let source = {
|
|
727
|
+
axes: [],
|
|
728
|
+
presets: [],
|
|
729
|
+
diagnostics: [],
|
|
730
|
+
css: "",
|
|
731
|
+
cssVarPrefix: "",
|
|
732
|
+
indicators: {},
|
|
733
|
+
listing: {},
|
|
734
|
+
tokenGraph: {
|
|
735
|
+
nodes: {},
|
|
736
|
+
axes: [],
|
|
737
|
+
axisDefaults: {},
|
|
738
|
+
axisContexts: {}
|
|
739
|
+
},
|
|
740
|
+
defaultTuple: {},
|
|
741
|
+
defaultColorFormat: "hex",
|
|
742
|
+
activeAxes: null,
|
|
743
|
+
version: 0
|
|
744
|
+
};
|
|
745
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
746
|
+
/**
|
|
747
|
+
* Host adapter API. Push a (partial) project snapshot into the ambient
|
|
748
|
+
* store. Omitted fields fall back to the current source, so a host can
|
|
749
|
+
* call this repeatedly with incremental patches (e.g. one per HMR event
|
|
750
|
+
* or axis flip) without re-sending the whole snapshot each time.
|
|
751
|
+
*/
|
|
752
|
+
function registerProjectSource(patch) {
|
|
753
|
+
source = {
|
|
754
|
+
axes: patch.axes ?? source.axes,
|
|
755
|
+
presets: patch.presets ?? source.presets,
|
|
756
|
+
diagnostics: patch.diagnostics ?? source.diagnostics,
|
|
757
|
+
css: patch.css ?? source.css,
|
|
758
|
+
cssVarPrefix: patch.cssVarPrefix ?? source.cssVarPrefix,
|
|
759
|
+
indicators: patch.indicators ?? source.indicators,
|
|
760
|
+
listing: patch.listing ?? source.listing,
|
|
761
|
+
tokenGraph: patch.tokenGraph ?? source.tokenGraph,
|
|
762
|
+
defaultTuple: patch.defaultTuple ?? source.defaultTuple,
|
|
763
|
+
defaultColorFormat: patch.defaultColorFormat ?? source.defaultColorFormat,
|
|
764
|
+
activeAxes: "activeAxes" in patch ? patch.activeAxes ?? null : source.activeAxes,
|
|
765
|
+
version: source.version + 1
|
|
766
|
+
};
|
|
767
|
+
for (const cb of listeners) cb();
|
|
768
|
+
}
|
|
769
|
+
function subscribe(cb) {
|
|
770
|
+
listeners.add(cb);
|
|
771
|
+
return () => {
|
|
772
|
+
listeners.delete(cb);
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function getSnapshot() {
|
|
776
|
+
return source;
|
|
777
|
+
}
|
|
778
|
+
function getServerSnapshot() {
|
|
779
|
+
return source;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Host adapter API. Read the live, ambient project source. Re-renders in
|
|
783
|
+
* place on each {@link registerProjectSource} call.
|
|
784
|
+
*/
|
|
785
|
+
function useProjectSource() {
|
|
786
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
789
|
+
export { COLOR_FORMATS as C, BorderSample as S, cssVarAsNumber as _, mergePresenters as a, useRootFontSize as b, ShadowSample as c, OpacitySwatch as d, MotionSample as f, FontWeightSpecimen as g, GradientSwatch as h, PresenterContext as i, TypeSpecimen as l, usePrefersReducedMotion as m, useProjectSource as n, registerPresenters as o, resolveMotionSpec as p, DEFAULT_PRESENTERS as r, usePresenter as s, registerProjectSource as t, StrokeSample as u, FontFamilySpecimen as v, formatColor as w, ColorSwatch as x, DimensionSample as y };
|
|
790
|
+
|
|
791
|
+
//# sourceMappingURL=host-e2nSv-sA.mjs.map
|