@sanity/themer 0.7.1 → 0.8.1

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/tool.js CHANGED
@@ -1,457 +1,18 @@
1
1
  import "@sanity/themer/bundle.css";
2
- import { a as DEFAULT_BACKGROUND, c as resolveThemeOptions, d as hslToHex, f as relativeLuminance, i as DEFAULT_ACCENT, l as isColor, n as buildTheme, o as SCHEMES, r as buildPalette, s as deriveTextColor, t as presets, u as hexToHsl } from "./presets-CqBgS3A-.js";
2
+ import { l as isColor, n as buildTheme, o as SCHEMES, t as presets } from "./presets-CqBgS3A-.js";
3
+ import { a as duplicateTitle, i as displayTitle, l as IMAGE_PALETTE_KEYS, o as initialThemerState, r as createCustomTheme, s as resolveThemes, v as ThemerContext, y as useThemer } from "./ResizableSidebar.css.ts.vanilla-M9EuPu-q.js";
4
+ import { blue, cyan, green, magenta, orange, purple, red, yellow } from "@sanity/color";
3
5
  import { c } from "react/compiler-runtime";
4
6
  import { definePlugin, useColorSchemeValue } from "sanity";
5
- import { Badge, Box, Button, Card, Dialog, Flex, Layer, Stack, Text, TextInput, ThemeProvider, useMediaIndex, useRootTheme, useTheme_v2 } from "@sanity/ui";
7
+ import { Button, Card, Flex, Text, ThemeProvider, useMediaIndex, usePrefersReducedMotion } from "@sanity/ui";
6
8
  import { useActor, useSelector } from "@xstate/react";
7
- import { Activity, ViewTransition, addTransitionType, createContext, startTransition, useContext, useDeferredValue, useEffect, useRef, useState, useSyncExternalStore } from "react";
9
+ import { Activity, ViewTransition, addTransitionType, lazy, startTransition, useDeferredValue, useEffect, useRef, useState, useSyncExternalStore } from "react";
8
10
  import { assign, not, or, setup, stateIn } from "xstate";
9
- import { ArrowLeftIcon } from "@sanity/icons/ArrowLeft";
10
- import { CloseIcon } from "@sanity/icons/Close";
11
- import { CodeBlockIcon } from "@sanity/icons/CodeBlock";
12
- import { SplitVerticalIcon } from "@sanity/icons/SplitVertical";
13
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
- import { assignInlineVars } from "@vanilla-extract/dynamic";
15
- import { ResetIcon } from "@sanity/icons/Reset";
16
- import { TrashIcon } from "@sanity/icons/Trash";
17
- import { SparklesIcon } from "@sanity/icons/Sparkles";
18
- import { Tooltip } from "@sanity/ui/tooltip";
19
- import { ImageIcon } from "@sanity/icons/Image";
20
- import { useToast } from "@sanity/ui/toast";
21
- import { AddIcon } from "@sanity/icons/Add";
22
- import { ClipboardIcon } from "@sanity/icons/Clipboard";
23
- import { RestoreIcon } from "@sanity/icons/Restore";
24
- import { MotionConfig, Reorder, useDragControls } from "motion/react";
25
- import { ArrowDownIcon } from "@sanity/icons/ArrowDown";
26
- import { ArrowUpIcon } from "@sanity/icons/ArrowUp";
27
- import { CopyIcon } from "@sanity/icons/Copy";
28
- import { EditIcon } from "@sanity/icons/Edit";
29
- import { EllipsisHorizontalIcon } from "@sanity/icons/EllipsisHorizontal";
30
- import { ShareIcon } from "@sanity/icons/Share";
31
- import { Menu, MenuButton, MenuDivider, MenuItem } from "@sanity/ui/menu";
32
- import scrollIntoView from "scroll-into-view-if-needed";
33
- import { Code } from "@sanity/ui/code";
34
- import { registerLanguage } from "react-refractor";
35
- import typescript from "refractor/typescript";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
36
12
  import { ColorWheelIcon } from "@sanity/icons/ColorWheel";
37
- /** @internal */
38
- const ThemerContext = createContext(null);
39
- /** @internal */
40
- function useThemer() {
41
- let context = useContext(ThemerContext);
42
- if (!context) throw Error("useThemer must be used within the `themerTool` plugin");
43
- return context;
44
- }
45
- /** @internal */
46
- const IMAGE_PALETTE_KEYS = [
47
- "vibrant",
48
- "lightVibrant",
49
- "darkVibrant",
50
- "muted",
51
- "lightMuted",
52
- "darkMuted",
53
- "dominant"
54
- ], IMAGE_PALETTE_VARIANTS = [
55
- "muted",
56
- "vibrant",
57
- "lightMuted",
58
- "lightVibrant",
59
- "darkMuted",
60
- "darkVibrant"
61
- ], IMAGE_PALETTE_TITLES = {
62
- dominant: "Dominant",
63
- vibrant: "Vibrant",
64
- lightVibrant: "Light vibrant",
65
- darkVibrant: "Dark vibrant",
66
- muted: "Muted",
67
- lightMuted: "Light muted",
68
- darkMuted: "Dark muted"
69
- }, TARGETS = [
70
- ["vibrant", {
71
- lightness: [
72
- .5,
73
- .3,
74
- .7
75
- ],
76
- saturation: [
77
- 1,
78
- .35,
79
- 1
80
- ]
81
- }],
82
- ["lightVibrant", {
83
- lightness: [
84
- .74,
85
- .55,
86
- 1
87
- ],
88
- saturation: [
89
- 1,
90
- .35,
91
- 1
92
- ]
93
- }],
94
- ["darkVibrant", {
95
- lightness: [
96
- .26,
97
- 0,
98
- .45
99
- ],
100
- saturation: [
101
- 1,
102
- .35,
103
- 1
104
- ]
105
- }],
106
- ["muted", {
107
- lightness: [
108
- .5,
109
- .3,
110
- .7
111
- ],
112
- saturation: [
113
- .3,
114
- 0,
115
- .4
116
- ]
117
- }],
118
- ["lightMuted", {
119
- lightness: [
120
- .74,
121
- .55,
122
- 1
123
- ],
124
- saturation: [
125
- .3,
126
- 0,
127
- .4
128
- ]
129
- }],
130
- ["darkMuted", {
131
- lightness: [
132
- .26,
133
- 0,
134
- .45
135
- ],
136
- saturation: [
137
- .3,
138
- 0,
139
- .4
140
- ]
141
- }]
142
- ];
143
- /**
144
- * Extracts the color palette of an image from its pixels, the way the Vibrant
145
- * algorithm does: the colors are quantized into a coarse histogram, and each
146
- * swatch claims the color that comes closest to its target lightness and
147
- * saturation — weighted by how much of the image it covers — that no other
148
- * swatch has claimed yet. The dominant swatch is simply the most common
149
- * color. Runs anywhere pixels come from, so it can run on device.
150
- *
151
- * @internal
152
- */
153
- function extractImagePalette(pixels) {
154
- let candidates = quantize(pixels), palette = {
155
- dominant: null,
156
- vibrant: null,
157
- lightVibrant: null,
158
- darkVibrant: null,
159
- muted: null,
160
- lightMuted: null,
161
- darkMuted: null
162
- };
163
- if (candidates.length === 0) return palette;
164
- let maximumPopulation = Math.max(...candidates.map((candidate) => candidate.population)), claimed = /* @__PURE__ */ new Set();
165
- for (let [key, target] of TARGETS) {
166
- let best = null, bestScore = -Infinity;
167
- for (let candidate of candidates) {
168
- if (claimed.has(candidate.hex) || !matchesTarget(candidate, target)) continue;
169
- let score = scoreCandidate(candidate, target, maximumPopulation);
170
- score > bestScore && (best = candidate, bestScore = score);
171
- }
172
- best && (palette[key] = best.hex, claimed.add(best.hex));
173
- }
174
- return palette.dominant = candidates.reduce((most, candidate) => candidate.population > most.population ? candidate : most).hex, palette;
175
- }
176
- /**
177
- * Turns the pixels into a histogram of quantized colors, each averaged over
178
- * the pixels it stands for, leaving out transparent pixels and the near-black
179
- * and near-white ones that are backdrop rather than color.
180
- */
181
- function quantize(pixels) {
182
- let { data } = pixels, buckets = /* @__PURE__ */ new Map(), length = Math.min(data.length, pixels.width * pixels.height * 4);
183
- for (let offset = 0; offset + 3 < length; offset += 4) {
184
- let r = data[offset], g = data[offset + 1], b = data[offset + 2];
185
- if (data[offset + 3] < 125) continue;
186
- let lightness = (Math.max(r, g, b) + Math.min(r, g, b)) / 510;
187
- if (lightness < .05 || lightness > .95) continue;
188
- let key = r >> 3 << 10 | g >> 3 << 5 | b >> 3, bucket = buckets.get(key);
189
- bucket ? (bucket.count++, bucket.r += r, bucket.g += g, bucket.b += b) : buckets.set(key, {
190
- count: 1,
191
- r,
192
- g,
193
- b
194
- });
195
- }
196
- let candidates = [];
197
- for (let bucket of buckets.values()) {
198
- let r = Math.round(bucket.r / bucket.count), g = Math.round(bucket.g / bucket.count), b = Math.round(bucket.b / bucket.count), { saturation, lightness } = saturationAndLightness(r, g, b);
199
- candidates.push({
200
- hex: rgbToHex(r, g, b),
201
- population: bucket.count,
202
- saturation,
203
- lightness
204
- });
205
- }
206
- return candidates;
207
- }
208
- function matchesTarget(candidate, target) {
209
- return candidate.lightness >= target.lightness[1] && candidate.lightness <= target.lightness[2] && candidate.saturation >= target.saturation[1] && candidate.saturation <= target.saturation[2];
210
- }
211
- function scoreCandidate(candidate, target, maximumPopulation) {
212
- return 3 * (1 - Math.abs(candidate.saturation - target.saturation[0])) + 6.5 * (1 - Math.abs(candidate.lightness - target.lightness[0])) + .5 * (candidate.population / maximumPopulation);
213
- }
214
- function saturationAndLightness(r, g, b) {
215
- let max = Math.max(r, g, b) / 255, min = Math.min(r, g, b) / 255, lightness = (max + min) / 2, delta = max - min;
216
- return {
217
- saturation: delta === 0 ? 0 : lightness > .5 ? delta / (2 - max - min) : delta / (max + min),
218
- lightness
219
- };
220
- }
221
- function rgbToHex(r, g, b) {
222
- return `#${[
223
- r,
224
- g,
225
- b
226
- ].map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
227
- }
228
- /** Reads `#rgb` and `#rrggbb` alike — stored palettes may hold either */
229
- function hexToRgb(hex) {
230
- let expanded = hex.length === 4 ? `${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex.slice(1);
231
- return [
232
- parseInt(expanded.slice(0, 2), 16),
233
- parseInt(expanded.slice(2, 4), 16),
234
- parseInt(expanded.slice(4, 6), 16)
235
- ];
236
- }
237
- /** Mixes `amount` of `color` into `base` */
238
- function tint(base, color, amount) {
239
- let [r1, g1, b1] = hexToRgb(base), [r2, g2, b2] = hexToRgb(color);
240
- return rgbToHex(Math.round(r1 + (r2 - r1) * amount), Math.round(g1 + (g2 - g1) * amount), Math.round(b1 + (b2 - b1) * amount));
241
- }
242
- /** How much of the palette tints each scheme's stock background */
243
- const BACKGROUND_TINT = {
244
- light: .08,
245
- dark: .3
246
- }, BUTTON_LABEL = {
247
- light: "#ffffff",
248
- dark: "#000000"
249
- }, ACCENT_LIGHTNESS_STEP = .01;
250
- function contrastRatio(a, b) {
251
- let [lighter, darker] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
252
- return (lighter + .05) / (darker + .05);
253
- }
254
- /**
255
- * Makes an accent readable as a button in the given scheme: a light scheme
256
- * accent is darkened until white text on it reaches AA contrast, a dark
257
- * scheme accent lightened until black text does — as far as the accent's
258
- * hue and saturation allow. Accents that already pass come back untouched.
259
- *
260
- * @internal
261
- */
262
- function readableAccent(hex, scheme) {
263
- let label = BUTTON_LABEL[scheme], { h, s } = hexToHsl(hex), { l } = hexToHsl(hex), current = hex;
264
- for (; contrastRatio(current, label) < 4.5;) {
265
- let next = scheme === "light" ? l - ACCENT_LIGHTNESS_STEP : l + ACCENT_LIGHTNESS_STEP;
266
- if (next < 0 || next > 1) break;
267
- l = next, current = hslToHex({
268
- h,
269
- s,
270
- l
271
- });
272
- }
273
- return current;
274
- }
275
- /** The accent a variant gives a scheme, made readable on a button */
276
- function variantAccent(palette, variant, scheme) {
277
- let swatch = (variant ? palette[variant] : null) ?? palette.vibrant ?? (scheme === "light" ? palette.darkVibrant : palette.lightVibrant) ?? palette.dominant;
278
- return swatch === null ? null : readableAccent(swatch, scheme);
279
- }
280
- /**
281
- * Derives theme options from an image palette: the accent of both schemes is
282
- * the swatch of the chosen variant — or, without one, the vibrant swatch,
283
- * each scheme falling back to the vibrant swatch of its own end of the scale —
284
- * adjusted until it is readable as a button. The muted swatch becomes the text color, and the light and dark muted
285
- * swatches tint the stock backgrounds of their scheme — enough to carry the
286
- * image's tone without giving up a usable canvas. Whatever the palette lacks
287
- * stays at the theme defaults.
288
- *
289
- * @internal
290
- */
291
- function optionsFromImagePalette(palette, variant) {
292
- let options = {}, light = schemeFromPalette(variantAccent(palette, variant ?? null, "light"), palette.muted ?? palette.darkMuted, palette.lightMuted ?? palette.muted ?? palette.lightVibrant, "light"), dark = schemeFromPalette(variantAccent(palette, variant ?? null, "dark"), palette.muted ?? palette.lightMuted, palette.darkMuted ?? palette.muted ?? palette.darkVibrant, "dark");
293
- return light && (options.light = light), dark && (options.dark = dark), options;
294
- }
295
- function schemeFromPalette(accent, text, backgroundTint, scheme) {
296
- let options = {};
297
- return accent && (options.accent = accent), text && (options.text = text), backgroundTint && (options.background = tint(DEFAULT_BACKGROUND[scheme], backgroundTint, BACKGROUND_TINT[scheme])), Object.keys(options).length > 0 ? options : null;
298
- }
299
- /**
300
- * Applies an image palette to existing theme options: the colors the palette
301
- * provides replace the current ones scheme by scheme, and whatever it does
302
- * not touch — like the contrast — stays as it is.
303
- *
304
- * @internal
305
- */
306
- function applyImagePalette(options, palette, variant) {
307
- let derived = optionsFromImagePalette(palette, variant), next = { ...options };
308
- for (let scheme of SCHEMES) derived[scheme] && (next[scheme] = {
309
- ...options[scheme],
310
- ...derived[scheme]
311
- });
312
- return next;
313
- }
314
- /**
315
- * The variant a theme is built around: the one whose swatch is the theme's
316
- * accent, or `null` when the accent comes from somewhere else.
317
- *
318
- * @internal
319
- */
320
- function currentImageVariant(options, palette) {
321
- let accent = options.light?.accent;
322
- return accent ? IMAGE_PALETTE_VARIANTS.find((variant) => palette[variant] !== null && variantAccent(palette, variant, "light") === accent) ?? null : null;
323
- }
324
- /**
325
- * How much a swatch has going for it as an accent: saturation first, then
326
- * closeness to mid lightness — the range where an accent reads best.
327
- */
328
- function interest(hex) {
329
- let { s, l } = hexToHsl(hex);
330
- return .1 + .55 * s + .35 * (1 - Math.abs(l - .5) * 2);
331
- }
332
- /**
333
- * Picks a variant for the "I'm feeling lucky" button: a random one, weighted
334
- * by how interesting its swatch is as an accent, and never the one the theme
335
- * already uses while there are others to pick from.
336
- *
337
- * @internal
338
- */
339
- function pickLuckyVariant(palette, options = {}) {
340
- let { exclude = null, random = Math.random } = options, candidates = IMAGE_PALETTE_VARIANTS.filter((variant) => palette[variant] !== null);
341
- if (candidates.length > 1 && (candidates = candidates.filter((variant) => variant !== exclude)), candidates.length === 0) return null;
342
- let weights = candidates.map((variant) => interest(palette[variant])), total = weights.reduce((sum, weight) => sum + weight, 0), pick = random() * total;
343
- for (let [index, variant] of candidates.entries()) if (pick -= weights[index], pick < 0) return variant;
344
- return candidates.at(-1) ?? null;
345
- }
346
- /** A theme title for the image a palette came from, e.g. `sunset-beach.jpg` → `sunset beach` @internal */
347
- function titleFromFileName(name) {
348
- return name.replace(/\.[^.]+$/, "").replace(/[-_]+/g, " ").trim() || "Image theme";
349
- }
350
- /**
351
- * Reduces theme options to the minimal object that recreates them through
352
- * `buildTheme`: in each scheme the accent is dropped when it is the stock
353
- * one, the text color when it matches the one derived from the accent, and
354
- * the background and contrast when they match the defaults — and a scheme
355
- * with nothing left is dropped altogether. Options that boil down to the
356
- * stock Studio theme reduce to `null` — they need nothing from this package.
357
- *
358
- * @internal
359
- */
360
- function minimizeOptions(options) {
361
- let resolved = resolveThemeOptions(options), minimized = {};
362
- for (let scheme of SCHEMES) {
363
- let values = resolved[scheme], kept = {};
364
- values.accent !== DEFAULT_ACCENT && (kept.accent = values.accent), values.text !== deriveTextColor(values.accent) && (kept.text = values.text), values.background !== DEFAULT_BACKGROUND[scheme] && (kept.background = values.background), values.contrast !== 85 && (kept.contrast = values.contrast), Object.keys(kept).length > 0 && (minimized[scheme] = kept);
365
- }
366
- return Object.keys(minimized).length === 0 ? null : minimized;
367
- }
368
- /**
369
- * Whether two sets of theme options generate the same theme, comparing their
370
- * resolved forms so that derived and explicit values (and hex casing) don't
371
- * matter.
372
- *
373
- * @internal
374
- */
375
- function sameOptions(a, b) {
376
- let left = resolveThemeOptions(a), right = resolveThemeOptions(b);
377
- return SCHEMES.every((scheme) => sameScheme(left[scheme], right[scheme]));
378
- }
379
- function sameScheme(a, b) {
380
- return a.accent === b.accent && a.text === b.text && a.background === b.background && a.contrast === b.contrast;
381
- }
382
- /** The slug of the theme the Studio config was generated from @internal */
383
- const initialThemerState = {
384
- active: null,
385
- custom: [],
386
- removed: [],
387
- order: []
388
- };
389
- /**
390
- * Resolves the list of themes: the configured theme first, then the presets
391
- * (minus the ones that would only repeat the configured theme), then the
392
- * user's own themes — rearranged into the order the user dragged them into,
393
- * with themes that order does not know about kept in that default order after
394
- * the ones it does. Removed themes are set aside so they can be restored, and
395
- * the active theme falls back to the configured one when its slug no longer
396
- * resolves.
397
- *
398
- * @internal
399
- */
400
- function resolveThemes(state, baseOptions) {
401
- let config = {
402
- slug: "config",
403
- title: "Studio config",
404
- options: baseOptions,
405
- source: "config"
406
- }, all = [config];
407
- for (let preset of presets) sameOptions(preset.options, baseOptions) || all.push({
408
- slug: preset.slug,
409
- title: preset.title,
410
- options: preset.options,
411
- source: "preset"
412
- });
413
- for (let theme of state.custom) all.push({
414
- slug: theme.slug,
415
- title: theme.title,
416
- options: theme.options,
417
- source: "custom",
418
- ...theme.palette ? { palette: theme.palette } : {}
419
- });
420
- let position = new Map(state.order.map((slug, index) => [slug, index])), rank = (theme) => position.get(theme.slug) ?? state.order.length;
421
- all.sort((a, b) => rank(a) - rank(b));
422
- let removedSlugs = new Set(state.removed), themes = all.filter((theme) => !removedSlugs.has(theme.slug));
423
- return {
424
- themes,
425
- removed: all.filter((theme) => removedSlugs.has(theme.slug)),
426
- active: themes.find((theme) => theme.slug === state.active) ?? config
427
- };
428
- }
429
- /**
430
- * Creates a custom theme with a slug that cannot collide with the presets or
431
- * the configured theme.
432
- *
433
- * @internal
434
- */
435
- function createCustomTheme(title, options, palette) {
436
- return {
437
- slug: `custom-${randomId()}`,
438
- title,
439
- options,
440
- ...palette ? { palette } : {}
441
- };
442
- }
443
- /** The title of a duplicated theme @internal */
444
- function duplicateTitle(title) {
445
- return `${displayTitle(title)} copy`;
446
- }
447
- /** The title to show for a theme, falling back when the user cleared it @internal */
448
- function displayTitle(title) {
449
- return title.trim() || "Untitled theme";
450
- }
451
- function randomId() {
452
- return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
453
- }
454
- const STORAGE_KEY = "sanityStudio:themer:state", LEGACY_STORAGE_KEY = "sanityStudio:themer:options";
13
+ import { Tooltip } from "@sanity/ui/tooltip";
14
+ import { animate, motion, useMotionValue, useTransform } from "motion/react";
15
+ const STORAGE_KEY = "sanityStudio:themer:state", LEGACY_STORAGE_KEY = "sanityStudio:themer:options", VISITED_STORAGE_KEY = "sanityStudio:themer:visited";
455
16
  function sanitizeColor(value) {
456
17
  return typeof value == "string" && isColor(value) ? value.toLowerCase() : null;
457
18
  }
@@ -576,6 +137,31 @@ function writeStoredState(state) {
576
137
  } catch {}
577
138
  }
578
139
  /**
140
+ * Whether the themer sidebar has been opened before, in any session — the
141
+ * cue for the navbar to stop introducing the tool.
142
+ *
143
+ * @internal
144
+ */
145
+ function hasVisited() {
146
+ try {
147
+ return typeof localStorage < "u" && localStorage.getItem(VISITED_STORAGE_KEY) !== null;
148
+ } catch {
149
+ return !1;
150
+ }
151
+ }
152
+ /**
153
+ * Notes that the themer sidebar has been opened, keeping the time it first
154
+ * was.
155
+ *
156
+ * @internal
157
+ */
158
+ function markVisited() {
159
+ try {
160
+ if (typeof localStorage > "u" || localStorage.getItem(VISITED_STORAGE_KEY) !== null) return;
161
+ localStorage.setItem(VISITED_STORAGE_KEY, (/* @__PURE__ */ new Date()).toISOString());
162
+ } catch {}
163
+ }
164
+ /**
579
165
  * How long the layout's motions — the panel's and the split preview's, and
580
166
  * the cross-fade to another theme — are taken to last when the layout never
581
167
  * says: it reports every view transition it starts, so this only ends a
@@ -1030,2119 +616,118 @@ function selectStoredState(snapshot) {
1030
616
  order
1031
617
  };
1032
618
  }
1033
- var root$2 = "_1o50s7t0";
1034
- const ScrollAreaInsetContext = createContext({
1035
- left: 0,
1036
- right: 0,
1037
- bottom: 0
1038
- });
1039
- /**
1040
- * The gutter the last scroll area measured. It is the same for every scroll
1041
- * area (the reserved gutter does not depend on the content), so the next one
1042
- * starts out with the right padding instead of shifting after its first
1043
- * measurement — which would also throw off anything scrolling into view as
1044
- * the content mounts.
1045
- */
1046
- let lastGutter = 0;
1047
- /**
1048
- * The scrolling body of a sidebar flow. It always reserves the scrollbar's
1049
- * space, so the content does not shift when the list grows or shrinks past
1050
- * the point where it needs to scroll — and it takes that space out of its
1051
- * right padding, so the content stays centered rather than hugging the left.
1052
- * With overlay scrollbars there is no gutter and the padding stays even.
1053
- *
1054
- * @internal
1055
- */
1056
- function ScrollArea(props) {
1057
- let $ = c(15), { children, padding } = props, { space } = useTheme_v2(), ref = useRef(null), [gutter, setGutter] = useState(_temp$8), t0, t1;
1058
- $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = () => {
1059
- let element = ref.current;
1060
- if (!element) return;
1061
- let measure = () => {
1062
- lastGutter = element.offsetWidth - element.clientWidth, setGutter(lastGutter);
1063
- };
1064
- measure();
1065
- let observer = new ResizeObserver(measure);
1066
- return observer.observe(element), () => observer.disconnect();
1067
- }, t1 = [], $[0] = t0, $[1] = t1) : (t0 = $[0], t1 = $[1]), useEffect(t0, t1);
1068
- let t2 = space[padding], t3 = Math.max(0, space[padding] - gutter), t4 = space[padding], t5;
1069
- $[2] !== t2 || $[3] !== t3 || $[4] !== t4 ? (t5 = {
1070
- left: t2,
1071
- right: t3,
1072
- bottom: t4
1073
- }, $[2] = t2, $[3] = t3, $[4] = t4, $[5] = t5) : t5 = $[5];
1074
- let inset = t5, t6;
1075
- $[6] === inset.right ? t6 = $[7] : (t6 = { paddingRight: inset.right }, $[6] = inset.right, $[7] = t6);
1076
- let t7;
1077
- $[8] !== children || $[9] !== inset ? (t7 = /* @__PURE__ */ jsx(ScrollAreaInsetContext.Provider, {
1078
- value: inset,
1079
- children
1080
- }), $[8] = children, $[9] = inset, $[10] = t7) : t7 = $[10];
1081
- let t8;
1082
- return $[11] !== padding || $[12] !== t6 || $[13] !== t7 ? (t8 = /* @__PURE__ */ jsx(Box, {
1083
- className: root$2,
1084
- flex: 1,
1085
- overflow: "auto",
1086
- padding,
1087
- ref,
1088
- style: t6,
1089
- children: t7
1090
- }), $[11] = padding, $[12] = t6, $[13] = t7, $[14] = t8) : t8 = $[14], t8;
619
+ const QUERY = "(prefers-reduced-motion: reduce)";
620
+ function subscribe(onChange) {
621
+ let media = matchMedia(QUERY);
622
+ return media.addEventListener("change", onChange), () => media.removeEventListener("change", onChange);
1091
623
  }
1092
- function _temp$8() {
1093
- return lastGutter;
624
+ function getSnapshot() {
625
+ return matchMedia(QUERY).matches;
1094
626
  }
1095
- /**
1096
- * Content that runs edge to edge in a scroll area — up to the scrollbar —
1097
- * instead of sitting inside its padding. It goes last: it runs down to the
1098
- * bottom edge too.
1099
- *
1100
- * @internal
1101
- */
1102
- function ScrollAreaBleed(props) {
1103
- let $ = c(7), { left, right, bottom } = useContext(ScrollAreaInsetContext), t0 = -left, t1 = -right, t2 = -bottom, t3;
1104
- $[0] !== t0 || $[1] !== t1 || $[2] !== t2 ? (t3 = {
1105
- marginLeft: t0,
1106
- marginRight: t1,
1107
- marginBottom: t2
1108
- }, $[0] = t0, $[1] = t1, $[2] = t2, $[3] = t3) : t3 = $[3];
1109
- let t4;
1110
- return $[4] !== props.children || $[5] !== t3 ? (t4 = /* @__PURE__ */ jsx("div", {
1111
- style: t3,
1112
- children: props.children
1113
- }), $[4] = props.children, $[5] = t3, $[6] = t4) : t4 = $[6], t4;
627
+ function getServerSnapshot() {
628
+ return !1;
1114
629
  }
1115
- var avatar = "_1aa9huuh", body = "_1aa9huui", button = "_1aa9huuq", colors = {
1116
- bg: "var(--_1aa9huu0)",
1117
- fg: "var(--_1aa9huu1)",
1118
- mutedFg: "var(--_1aa9huu2)",
1119
- border: "var(--_1aa9huu3)",
1120
- accent: "var(--_1aa9huu4)",
1121
- avatar: "var(--_1aa9huu5)",
1122
- selectedBg: "var(--_1aa9huu6)",
1123
- selectedFg: "var(--_1aa9huu7)",
1124
- inputBg: "var(--_1aa9huu8)",
1125
- inputBorder: "var(--_1aa9huu9)"
1126
- }, field = "_1aa9huun", footer = "_1aa9huup", form = "_1aa9huum", input = "_1aa9huuo", line = {
1127
- fg: "_1aa9huus _1aa9huur",
1128
- muted: "_1aa9huut _1aa9huur",
1129
- selected: "_1aa9huuu _1aa9huur"
1130
- }, list = "_1aa9huuj", listRow = "_1aa9huuk", logo = "_1aa9huuf", navbar = "_1aa9huue", pane = "_1aa9huul", root$1 = "_1aa9huua", spacer = "_1aa9huug", studio = {
1131
- light: "_1aa9huub",
1132
- dark: "_1aa9huud _1aa9huub"
1133
- };
1134
- /** The document rows in the list pane — the widths of their title lines */
1135
- const LIST_ROWS = [
1136
- "62%",
1137
- "46%",
1138
- "70%",
1139
- "52%"
1140
- ], FIELDS$1 = ["32%", "24%"];
1141
630
  /**
1142
- * A tiny, lo-fi mockup of a Studio in the given theme — the navbar over the
1143
- * structure tool with a list of documents on the left and a document form on
1144
- * the right — in both color schemes at once: the light scheme on the left and
1145
- * the dark scheme on the right, split diagonally. It only paints colors the
1146
- * theme options change (backgrounds, text, borders, the accent) so that a
1147
- * glance shows what the theme does to the Studio — with few and bold enough
1148
- * elements to still read when scaled down to a thumbnail.
631
+ * Whether the user prefers reduced motion — live, so that turning the
632
+ * setting on while the Studio is open takes effect right away.
1149
633
  *
1150
634
  * @internal
1151
635
  */
1152
- function ThemePreview(props) {
1153
- let $ = c(11), { options } = props, t0;
1154
- $[0] === options ? t0 = $[1] : (t0 = buildTheme(options), $[0] = options, $[1] = t0);
1155
- let theme = t0, t1;
1156
- $[2] === Symbol.for("react.memo_cache_sentinel") ? (t1 = /* @__PURE__ */ jsx(MockStudio, { scheme: "light" }), $[2] = t1) : t1 = $[2];
1157
- let t2;
1158
- $[3] === theme ? t2 = $[4] : (t2 = /* @__PURE__ */ jsx(ThemeProvider, {
1159
- scheme: "light",
1160
- theme,
1161
- tone: "default",
1162
- children: t1
1163
- }), $[3] = theme, $[4] = t2);
1164
- let t3;
1165
- $[5] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx(MockStudio, { scheme: "dark" }), $[5] = t3) : t3 = $[5];
1166
- let t4;
1167
- $[6] === theme ? t4 = $[7] : (t4 = /* @__PURE__ */ jsx(ThemeProvider, {
1168
- scheme: "dark",
1169
- theme,
1170
- tone: "default",
1171
- children: t3
1172
- }), $[6] = theme, $[7] = t4);
1173
- let t5;
1174
- return $[8] !== t2 || $[9] !== t4 ? (t5 = /* @__PURE__ */ jsxs("div", {
1175
- "aria-hidden": !0,
1176
- className: root$1,
1177
- children: [t2, t4]
1178
- }), $[8] = t2, $[9] = t4, $[10] = t5) : t5 = $[10], t5;
1179
- }
1180
- function MockStudio(props) {
1181
- let $ = c(19), { scheme } = props, { color } = useTheme_v2(), selected = color.selectable.default.selected, inputColor = color.input.default.enabled, t0 = studio[scheme], t1;
1182
- $[0] !== color.avatar.blue.bg || $[1] !== color.bg || $[2] !== color.border || $[3] !== color.button.default.primary.enabled.bg || $[4] !== color.fg || $[5] !== color.muted.fg || $[6] !== inputColor.bg || $[7] !== inputColor.border || $[8] !== selected.bg || $[9] !== selected.fg ? (t1 = assignInlineVars({
1183
- [colors.bg]: color.bg,
1184
- [colors.fg]: color.fg,
1185
- [colors.mutedFg]: color.muted.fg,
1186
- [colors.border]: color.border,
1187
- [colors.accent]: color.button.default.primary.enabled.bg,
1188
- [colors.avatar]: color.avatar.blue.bg,
1189
- [colors.selectedBg]: selected.bg,
1190
- [colors.selectedFg]: selected.fg,
1191
- [colors.inputBg]: inputColor.bg,
1192
- [colors.inputBorder]: inputColor.border
1193
- }), $[0] = color.avatar.blue.bg, $[1] = color.bg, $[2] = color.border, $[3] = color.button.default.primary.enabled.bg, $[4] = color.fg, $[5] = color.muted.fg, $[6] = inputColor.bg, $[7] = inputColor.border, $[8] = selected.bg, $[9] = selected.fg, $[10] = t1) : t1 = $[10];
1194
- let t2;
1195
- $[11] === Symbol.for("react.memo_cache_sentinel") ? (t2 = /* @__PURE__ */ jsx("div", { className: logo }), $[11] = t2) : t2 = $[11];
1196
- let t3;
1197
- $[12] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsxs("div", {
1198
- className: navbar,
1199
- children: [
1200
- t2,
1201
- /* @__PURE__ */ jsx("div", {
1202
- className: line.fg,
1203
- style: { width: "22%" }
1204
- }),
1205
- /* @__PURE__ */ jsx("div", { className: spacer }),
1206
- /* @__PURE__ */ jsx("div", { className: avatar })
1207
- ]
1208
- }), $[12] = t3) : t3 = $[12];
1209
- let t4;
1210
- $[13] === Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx("div", {
1211
- className: list,
1212
- children: LIST_ROWS.map(_temp$7)
1213
- }), $[13] = t4) : t4 = $[13];
1214
- let t5;
1215
- $[14] === Symbol.for("react.memo_cache_sentinel") ? (t5 = /* @__PURE__ */ jsxs("div", {
1216
- className: form,
1217
- children: [/* @__PURE__ */ jsx("div", {
1218
- className: line.fg,
1219
- style: { width: "45%" }
1220
- }), FIELDS$1.map(_temp2)]
1221
- }), $[14] = t5) : t5 = $[14];
1222
- let t6;
1223
- $[15] === Symbol.for("react.memo_cache_sentinel") ? (t6 = /* @__PURE__ */ jsxs("div", {
1224
- className: body,
1225
- children: [t4, /* @__PURE__ */ jsxs("div", {
1226
- className: pane,
1227
- children: [t5, /* @__PURE__ */ jsx("div", {
1228
- className: footer,
1229
- children: /* @__PURE__ */ jsx("div", { className: button })
1230
- })]
1231
- })]
1232
- }), $[15] = t6) : t6 = $[15];
1233
- let t7;
1234
- return $[16] !== t0 || $[17] !== t1 ? (t7 = /* @__PURE__ */ jsxs("div", {
1235
- className: t0,
1236
- style: t1,
1237
- children: [t3, t6]
1238
- }), $[16] = t0, $[17] = t1, $[18] = t7) : t7 = $[18], t7;
1239
- }
1240
- function _temp2(width_0) {
1241
- return /* @__PURE__ */ jsxs("div", {
1242
- className: field,
1243
- children: [/* @__PURE__ */ jsx("div", {
1244
- className: line.muted,
1245
- style: { width: width_0 }
1246
- }), /* @__PURE__ */ jsx("div", { className: input })]
1247
- }, width_0);
1248
- }
1249
- function _temp$7(width, index) {
1250
- let isSelected = index === 1;
1251
- return /* @__PURE__ */ jsx("div", {
1252
- className: listRow,
1253
- "data-selected": isSelected,
1254
- children: /* @__PURE__ */ jsx("div", {
1255
- className: isSelected ? line.selected : line.muted,
1256
- style: { width }
1257
- })
1258
- }, width);
636
+ function usePrefersReducedMotion$1() {
637
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
1259
638
  }
1260
639
  /**
1261
- * The floating preview card of a theme: the lo-fi studio mockup on a raised,
1262
- * rounded card, like the appearance previews in macOS System Settings.
640
+ * The Studio navbar's root, the `Card` that draws its bottom border — its
641
+ * test id is stable and unique to the navbar, unlike its `data-ui` name
1263
642
  *
1264
643
  * @internal
1265
644
  */
1266
- function ThemeThumbnail(props) {
1267
- let $ = c(2), t0;
1268
- return $[0] === props.options ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(Card, {
1269
- overflow: "hidden",
1270
- radius: 3,
1271
- shadow: 1,
1272
- children: /* @__PURE__ */ jsx(ThemePreview, { options: props.options })
1273
- }), $[0] = props.options, $[1] = t0), t0;
1274
- }
1275
- var removedThumbnail = "_1vdkp0v0";
645
+ const NAVBAR_SELECTOR = "[data-testid=\"studio-navbar\"]";
1276
646
  /**
1277
- * The flow for restoring removed themes: the presets and custom themes the
1278
- * user took out of the list, each with a button to put it back — and, for
1279
- * custom themes, one to delete it for good. The machine leaves the flow once
1280
- * the last one is gone.
647
+ * The height of the Studio navbar inside the given element, in pixels — kept
648
+ * up to date as the navbar renders, remounts and resizes, and `null` until
649
+ * there is one. The themer's header takes this height, so that its bottom
650
+ * border lines up with the navbar's whatever height the Studio version, its
651
+ * breakpoint or a custom navbar gives it.
1281
652
  *
1282
653
  * @internal
1283
654
  */
1284
- function RemovedThemes() {
1285
- let $ = c(7), { removed, send } = useThemer(), t0;
1286
- if ($[0] !== removed || $[1] !== send) {
1287
- let t1;
1288
- $[3] === send ? t1 = $[4] : (t1 = (theme) => /* @__PURE__ */ jsxs(Stack, {
1289
- gap: 3,
1290
- children: [
1291
- /* @__PURE__ */ jsx(Box, {
1292
- className: removedThumbnail,
1293
- children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: theme.options })
1294
- }),
1295
- /* @__PURE__ */ jsx(Text, {
1296
- align: "center",
1297
- size: 1,
1298
- textOverflow: "ellipsis",
1299
- children: displayTitle(theme.title)
1300
- }),
1301
- /* @__PURE__ */ jsxs(Flex, {
1302
- gap: 2,
1303
- justify: "center",
1304
- children: [/* @__PURE__ */ jsx(Button, {
1305
- mode: "ghost",
1306
- onClick: () => send({
1307
- type: "theme.restore",
1308
- slug: theme.slug
1309
- }),
1310
- padding: 2,
1311
- text: "Restore"
1312
- }), theme.source === "custom" && /* @__PURE__ */ jsx(Button, {
1313
- mode: "ghost",
1314
- onClick: () => send({
1315
- type: "theme.delete",
1316
- slug: theme.slug
1317
- }),
1318
- padding: 2,
1319
- text: "Delete",
1320
- tone: "critical"
1321
- })]
1322
- })
1323
- ]
1324
- }, theme.slug), $[3] = send, $[4] = t1), t0 = removed.map(t1), $[0] = removed, $[1] = send, $[2] = t0;
1325
- } else t0 = $[2];
1326
- let t1;
1327
- return $[5] === t0 ? t1 = $[6] : (t1 = /* @__PURE__ */ jsx(ScrollArea, {
1328
- padding: 3,
1329
- children: /* @__PURE__ */ jsx(Stack, {
1330
- gap: 5,
1331
- children: t0
1332
- })
1333
- }), $[5] = t0, $[6] = t1), t1;
655
+ function useStudioNavbarHeight(ref) {
656
+ let $ = c(3), [height, setHeight] = useState(null), t0, t1;
657
+ return $[0] === ref ? (t0 = $[1], t1 = $[2]) : (t0 = () => {
658
+ let root = ref.current;
659
+ if (!root) return;
660
+ let navbar = null, resizeObserver = new ResizeObserver(() => {
661
+ navbar && setHeight(navbar.getBoundingClientRect().height);
662
+ }), observeNavbar = () => {
663
+ let next = root.querySelector(NAVBAR_SELECTOR);
664
+ next !== navbar && (navbar && resizeObserver.unobserve(navbar), navbar = next, navbar ? (resizeObserver.observe(navbar), setHeight(navbar.getBoundingClientRect().height)) : setHeight(null));
665
+ }, mutationObserver = new MutationObserver(observeNavbar);
666
+ return mutationObserver.observe(root, {
667
+ childList: !0,
668
+ subtree: !0
669
+ }), observeNavbar(), () => {
670
+ mutationObserver.disconnect(), resizeObserver.disconnect();
671
+ };
672
+ }, t1 = [ref], $[0] = ref, $[1] = t0, $[2] = t1), useEffect(t0, t1), height;
1334
673
  }
1335
674
  /**
1336
- * A button with an animated tooltip instead of a `title` attribute, sized like
1337
- * the Studio's own buttons (`padding` and `gap` of 2). Icon-only buttons take
1338
- * the tooltip as their accessible name.
675
+ * `useDeferredValue` with a transition type: the returned value lags behind
676
+ * `value` like a deferred one — rendered in a transition, which React
677
+ * animates — and the view transition React starts for it carries the given
678
+ * type (`document.startViewTransition({types})`), so a stylesheet tells it
679
+ * from any other transition on the page through
680
+ * `:active-view-transition-type()`.
1339
681
  *
1340
- * @internal
1341
- */
1342
- function TooltipButton(props) {
1343
- let $ = c(11), buttonProps, tooltip;
1344
- $[0] === props ? (buttonProps = $[1], tooltip = $[2]) : ({tooltip, ...buttonProps} = props, $[0] = props, $[1] = buttonProps, $[2] = tooltip);
1345
- let t0;
1346
- $[3] === tooltip ? t0 = $[4] : (t0 = /* @__PURE__ */ jsx(Text, {
1347
- size: 1,
1348
- children: tooltip
1349
- }), $[3] = tooltip, $[4] = t0);
1350
- let t1 = buttonProps.text === void 0 ? tooltip : void 0, t2;
1351
- $[5] !== buttonProps || $[6] !== t1 ? (t2 = /* @__PURE__ */ jsx(Button, {
1352
- "aria-label": t1,
1353
- gap: 2,
1354
- padding: 2,
1355
- ...buttonProps
1356
- }), $[5] = buttonProps, $[6] = t1, $[7] = t2) : t2 = $[7];
1357
- let t3;
1358
- return $[8] !== t0 || $[9] !== t2 ? (t3 = /* @__PURE__ */ jsx(Tooltip, {
1359
- animate: !0,
1360
- content: t0,
1361
- placement: "bottom",
1362
- portal: !0,
1363
- children: t2
1364
- }), $[8] = t0, $[9] = t2, $[10] = t3) : t3 = $[10], t3;
1365
- }
1366
- /**
1367
- * A button that opens the file picker for an image, and hands the chosen file
1368
- * over — the picker is a hidden file input, so nothing is uploaded anywhere.
682
+ * React only takes types from `startTransition`, and a deferred render is
683
+ * not one. So while the deferred value lags, a transition of the type joins
684
+ * it, with a state update of its own for the type to belong to: React renders
685
+ * every pending transition in one go, and the commit — the view transition
686
+ * — carries the types of all of them. The value itself stays with
687
+ * `useDeferredValue`, which reads the latest `value` as the deferred render
688
+ * runs: should `value` change back before that render commits, nothing
689
+ * changes and nothing animates — a queued state update would commit the
690
+ * stale value and animate it, only to animate back.
1369
691
  *
1370
692
  * @internal
1371
693
  */
1372
- function ImageFileButton(props) {
1373
- let $ = c(11), buttonProps, onFile;
1374
- $[0] === props ? (buttonProps = $[1], onFile = $[2]) : ({onFile, ...buttonProps} = props, $[0] = props, $[1] = buttonProps, $[2] = onFile);
1375
- let inputRef = useRef(null), t0;
1376
- $[3] === Symbol.for("react.memo_cache_sentinel") ? (t0 = () => inputRef.current?.click(), $[3] = t0) : t0 = $[3];
1377
- let t1;
1378
- $[4] === buttonProps ? t1 = $[5] : (t1 = /* @__PURE__ */ jsx(TooltipButton, {
1379
- ...buttonProps,
1380
- icon: ImageIcon,
1381
- onClick: t0
1382
- }), $[4] = buttonProps, $[5] = t1);
1383
- let t2;
1384
- $[6] === onFile ? t2 = $[7] : (t2 = /* @__PURE__ */ jsx("input", {
1385
- accept: "image/*",
1386
- hidden: !0,
1387
- onChange: (event) => {
1388
- let file = event.currentTarget.files?.[0];
1389
- event.currentTarget.value = "", file && onFile(file);
1390
- },
1391
- ref: inputRef,
1392
- type: "file"
1393
- }), $[6] = onFile, $[7] = t2);
1394
- let t3;
1395
- return $[8] !== t1 || $[9] !== t2 ? (t3 = /* @__PURE__ */ jsxs(Fragment, { children: [t1, t2] }), $[8] = t1, $[9] = t2, $[10] = t3) : t3 = $[10], t3;
694
+ function useTypedDeferredValue(value, type) {
695
+ let $ = c(5), deferred = useDeferredValue(value), [, setTransitions] = useState(0), t0, t1;
696
+ return $[0] !== deferred || $[1] !== type || $[2] !== value ? (t0 = () => {
697
+ Object.is(deferred, value) || startTransition(() => {
698
+ addTransitionType(type), setTransitions(_temp);
699
+ });
700
+ }, t1 = [
701
+ deferred,
702
+ type,
703
+ value
704
+ ], $[0] = deferred, $[1] = type, $[2] = value, $[3] = t0, $[4] = t1) : (t0 = $[3], t1 = $[4]), useEffect(t0, t1), deferred;
705
+ }
706
+ function _temp(count) {
707
+ return count + 1;
1396
708
  }
709
+ var layout = "_1yq4ar78", layoutTransitionType = "sanity-themer", panelTransitionClasses = {
710
+ slideIn: "_1yq4ar76",
711
+ slideOut: "_1yq4ar77"
712
+ }, splitTransitionClasses = {
713
+ resize: "_1yq4ar70",
714
+ crossfade: "_1yq4ar71",
715
+ slideIn: "_1yq4ar72",
716
+ slideOut: "_1yq4ar73",
717
+ dropIn: "_1yq4ar74",
718
+ dropOut: "_1yq4ar75"
719
+ }, studioScheme = {
720
+ light: "_1yq4ar79",
721
+ dark: "_1yq4ar7a"
722
+ };
1397
723
  /**
1398
- * Reads the pixels of an image file on device: the image is decoded, drawn
1399
- * onto a small canvas and read back, so nothing leaves the browser.
1400
- *
1401
- * @internal
724
+ * Below this media index the Studio collapses its navbar into a drawer, and
725
+ * the themer follows suit: the sidebar covers the Studio instead of standing
726
+ * next to it, and the split preview stacks instead of sitting side by side.
1402
727
  */
1403
- async function readImagePixels(file) {
1404
- let image = await decodeImage(file);
1405
- try {
1406
- let scale = Math.min(1, 96 / Math.max(image.width, image.height)), width = Math.max(1, Math.round(image.width * scale)), height = Math.max(1, Math.round(image.height * scale)), canvas = document.createElement("canvas");
1407
- canvas.width = width, canvas.height = height;
1408
- let context = canvas.getContext("2d", { willReadFrequently: !0 });
1409
- if (!context) throw Error("Could not draw the image");
1410
- return context.drawImage(image, 0, 0, width, height), {
1411
- data: context.getImageData(0, 0, width, height).data,
1412
- width,
1413
- height
1414
- };
1415
- } finally {
1416
- typeof ImageBitmap < "u" && image instanceof ImageBitmap && image.close();
1417
- }
1418
- }
1419
- async function decodeImage(file) {
1420
- if (typeof createImageBitmap == "function") try {
1421
- return await createImageBitmap(file);
1422
- } catch {}
1423
- let url = URL.createObjectURL(file);
1424
- try {
1425
- return await new Promise((resolve, reject) => {
1426
- let image = new Image();
1427
- image.addEventListener("load", () => resolve(image), { once: !0 }), image.addEventListener("error", () => reject(/* @__PURE__ */ Error("Could not decode the image")), { once: !0 }), image.src = url;
1428
- });
1429
- } finally {
1430
- URL.revokeObjectURL(url);
1431
- }
1432
- }
1433
- /**
1434
- * Extracts the palette of image files on device and hands it to `onPalette`,
1435
- * along with the file it came from. Failures end up as a toast.
1436
- *
1437
- * @internal
1438
- */
1439
- function useImagePalette(onPalette) {
1440
- let $ = c(10), toast = useToast(), [busy, setBusy] = useState(!1), onPaletteRef = useRef(onPalette), mountedRef = useRef(!0), latestPickRef = useRef(0), t0, t1;
1441
- $[0] === onPalette ? (t0 = $[1], t1 = $[2]) : (t0 = () => {
1442
- onPaletteRef.current = onPalette;
1443
- }, t1 = [onPalette], $[0] = onPalette, $[1] = t0, $[2] = t1), useEffect(t0, t1);
1444
- let t2, t3;
1445
- $[3] === Symbol.for("react.memo_cache_sentinel") ? (t2 = () => (mountedRef.current = !0, () => {
1446
- mountedRef.current = !1;
1447
- }), t3 = [], $[3] = t2, $[4] = t3) : (t2 = $[3], t3 = $[4]), useEffect(t2, t3);
1448
- let t4;
1449
- $[5] === toast ? t4 = $[6] : (t4 = (file) => {
1450
- let pick = latestPickRef.current += 1;
1451
- setBusy(!0);
1452
- let isCurrent = () => mountedRef.current && latestPickRef.current === pick, extract = async () => {
1453
- let palette = extractImagePalette(await readImagePixels(file));
1454
- isCurrent() && (Object.values(palette).every(_temp$6) ? toast.push({
1455
- status: "warning",
1456
- title: "Found no colors in that image"
1457
- }) : onPaletteRef.current(palette, file));
1458
- };
1459
- extract().catch(() => {
1460
- isCurrent() && toast.push({
1461
- status: "error",
1462
- title: "Could not read that image"
1463
- });
1464
- }).finally(() => {
1465
- isCurrent() && setBusy(!1);
1466
- });
1467
- }, $[5] = toast, $[6] = t4);
1468
- let pickImage = t4, t5;
1469
- return $[7] !== busy || $[8] !== pickImage ? (t5 = {
1470
- busy,
1471
- pickImage
1472
- }, $[7] = busy, $[8] = pickImage, $[9] = t5) : t5 = $[9], t5;
1473
- }
1474
- function _temp$6(swatch) {
1475
- return swatch === null;
1476
- }
1477
- var paletteGrid = "_17s80v42", tile = "_17s80v44", variantButton = "_17s80v46", variantFrame = "_17s80v47", variantRow = "_17s80v45", variantScale = "var(--_17s80v41)", variantScaleBox = "_17s80v48", variantWidth = "var(--_17s80v40)";
1478
- /** The swatches on display: the vibrant family over the muted one */
1479
- const TILES = [
1480
- "vibrant",
1481
- "lightVibrant",
1482
- "darkVibrant",
1483
- "muted",
1484
- "lightMuted",
1485
- "darkMuted"
1486
- ];
1487
- /**
1488
- * Takes the colors of a theme from an image, on device: the image is read
1489
- * into a canvas and its palette extracted right there — nothing is uploaded.
1490
- * Shows the image (while the session lasts) next to its palette, offers a
1491
- * theme variant built around each swatch as a small preview, and a lucky
1492
- * button that picks an interesting one.
1493
- *
1494
- * @internal
1495
- */
1496
- function ImagePaletteSection(props) {
1497
- let $ = c(38), { imageUrl, onPalette, onVariant, onLucky, options, palette } = props, { busy, pickImage } = useImagePalette(onPalette), [dragging, setDragging] = useState(!1), dragDepth = useRef(0), t0;
1498
- $[0] !== options || $[1] !== palette ? (t0 = palette ? currentImageVariant(options, palette) : null, $[0] = options, $[1] = palette, $[2] = t0) : t0 = $[2];
1499
- let current = t0, rowRef = useRef(null), [thumbnailWidth, setThumbnailWidth] = useState(58), t1;
1500
- $[3] === Symbol.for("react.memo_cache_sentinel") ? (t1 = () => {
1501
- let row = rowRef.current;
1502
- if (!row) return;
1503
- let measure = () => {
1504
- let visible = row.clientWidth - 12 - 16;
1505
- setThumbnailWidth(Math.max(24, Math.floor(visible / 2.5) - 8));
1506
- };
1507
- measure();
1508
- let observer = new ResizeObserver(measure);
1509
- return observer.observe(row), () => observer.disconnect();
1510
- }, $[3] = t1) : t1 = $[3];
1511
- let t2;
1512
- $[4] === palette ? t2 = $[5] : (t2 = [palette], $[4] = palette, $[5] = t2), useEffect(t1, t2);
1513
- let t3;
1514
- bb0: {
1515
- if (!palette) {
1516
- let t4;
1517
- $[6] === Symbol.for("react.memo_cache_sentinel") ? (t4 = [], $[6] = t4) : t4 = $[6], t3 = t4;
1518
- break bb0;
1519
- }
1520
- let t4;
1521
- $[7] !== options || $[8] !== palette ? (t4 = IMAGE_PALETTE_VARIANTS.flatMap((variant) => palette[variant] === null ? [] : [{
1522
- variant,
1523
- title: IMAGE_PALETTE_TITLES[variant],
1524
- options: applyImagePalette(options, palette, variant)
1525
- }]), $[7] = options, $[8] = palette, $[9] = t4) : t4 = $[9], t3 = t4;
1526
- }
1527
- let variants = t3, t4, t5;
1528
- $[10] === Symbol.for("react.memo_cache_sentinel") ? (t4 = (event) => {
1529
- event.preventDefault(), dragDepth.current += 1, setDragging(!0);
1530
- }, t5 = () => {
1531
- dragDepth.current = Math.max(0, dragDepth.current - 1), dragDepth.current === 0 && setDragging(!1);
1532
- }, $[10] = t4, $[11] = t5) : (t4 = $[10], t5 = $[11]);
1533
- let t6;
1534
- $[12] === pickImage ? t6 = $[13] : (t6 = (event_1) => {
1535
- event_1.preventDefault(), dragDepth.current = 0, setDragging(!1);
1536
- let file = event_1.dataTransfer.files[0];
1537
- file && pickImage(file);
1538
- }, $[12] = pickImage, $[13] = t6);
1539
- let t7 = dragging ? "primary" : "default", t8;
1540
- $[14] === Symbol.for("react.memo_cache_sentinel") ? (t8 = /* @__PURE__ */ jsx(Box, {
1541
- flex: 1,
1542
- children: /* @__PURE__ */ jsx(Text, {
1543
- size: 1,
1544
- weight: "medium",
1545
- children: "Image palette"
1546
- })
1547
- }), $[14] = t8) : t8 = $[14];
1548
- let t9;
1549
- $[15] !== busy || $[16] !== palette || $[17] !== pickImage ? (t9 = palette && /* @__PURE__ */ jsx(ImageFileButton, {
1550
- loading: busy,
1551
- mode: "bleed",
1552
- onFile: pickImage,
1553
- padding: 2,
1554
- tooltip: "Pick another image"
1555
- }), $[15] = busy, $[16] = palette, $[17] = pickImage, $[18] = t9) : t9 = $[18];
1556
- let t10;
1557
- $[19] === t9 ? t10 = $[20] : (t10 = /* @__PURE__ */ jsxs(Flex, {
1558
- align: "center",
1559
- gap: 2,
1560
- children: [t8, t9]
1561
- }), $[19] = t9, $[20] = t10);
1562
- let t11;
1563
- $[21] !== busy || $[22] !== current || $[23] !== imageUrl || $[24] !== onLucky || $[25] !== onVariant || $[26] !== palette || $[27] !== pickImage || $[28] !== thumbnailWidth || $[29] !== variants ? (t11 = palette ? /* @__PURE__ */ jsxs(Fragment, { children: [
1564
- /* @__PURE__ */ jsxs("div", {
1565
- className: paletteGrid,
1566
- "data-with-image": imageUrl !== void 0,
1567
- children: [imageUrl && /* @__PURE__ */ jsx("img", {
1568
- alt: "",
1569
- className: "_17s80v43",
1570
- src: imageUrl
1571
- }), TILES.map((key) => {
1572
- let hex = palette[key], title = IMAGE_PALETTE_TITLES[key];
1573
- return /* @__PURE__ */ jsx(Tooltip, {
1574
- animate: !0,
1575
- content: /* @__PURE__ */ jsx(Text, {
1576
- size: 1,
1577
- children: hex ? `${title} · ${hex}` : `No ${title.toLowerCase()} color`
1578
- }),
1579
- placement: "bottom",
1580
- portal: !0,
1581
- children: /* @__PURE__ */ jsx("span", {
1582
- className: tile,
1583
- "data-empty": hex === null,
1584
- style: { background: hex ?? void 0 }
1585
- })
1586
- }, key);
1587
- })]
1588
- }),
1589
- /* @__PURE__ */ jsxs(Stack, {
1590
- gap: 2,
1591
- children: [/* @__PURE__ */ jsx(Text, {
1592
- muted: !0,
1593
- size: 0,
1594
- children: "Pick the swatch to build the theme around"
1595
- }), /* @__PURE__ */ jsx("div", {
1596
- className: variantRow,
1597
- ref: rowRef,
1598
- style: assignInlineVars({
1599
- [variantWidth]: `${thumbnailWidth}px`,
1600
- [variantScale]: String(thumbnailWidth / 144)
1601
- }),
1602
- children: variants.map((t12) => {
1603
- let { variant: variant_0, title: title_0, options: variantOptions } = t12;
1604
- return /* @__PURE__ */ jsxs("button", {
1605
- "aria-pressed": variant_0 === current,
1606
- className: variantButton,
1607
- onClick: () => onVariant(variant_0),
1608
- type: "button",
1609
- children: [/* @__PURE__ */ jsx("span", {
1610
- className: variantFrame,
1611
- children: /* @__PURE__ */ jsx("span", {
1612
- className: variantScaleBox,
1613
- children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: variantOptions })
1614
- })
1615
- }), /* @__PURE__ */ jsx(Box, {
1616
- as: "span",
1617
- display: "block",
1618
- paddingTop: 1,
1619
- children: /* @__PURE__ */ jsx(Text, {
1620
- align: "center",
1621
- as: "span",
1622
- size: 0,
1623
- textOverflow: "ellipsis",
1624
- weight: variant_0 === current ? "medium" : "regular",
1625
- children: title_0
1626
- })
1627
- })]
1628
- }, variant_0);
1629
- })
1630
- })]
1631
- }),
1632
- /* @__PURE__ */ jsx(TooltipButton, {
1633
- icon: SparklesIcon,
1634
- mode: "ghost",
1635
- onClick: onLucky,
1636
- text: "I'm feeling lucky",
1637
- tooltip: "Build the theme around a swatch that looks interesting",
1638
- width: "fill"
1639
- })
1640
- ] }) : /* @__PURE__ */ jsxs(Stack, {
1641
- gap: 3,
1642
- children: [/* @__PURE__ */ jsx(Text, {
1643
- muted: !0,
1644
- size: 0,
1645
- children: "Take the accent, text and background colors from an image — read on device, nothing is uploaded."
1646
- }), /* @__PURE__ */ jsx(ImageFileButton, {
1647
- loading: busy,
1648
- mode: "ghost",
1649
- onFile: pickImage,
1650
- text: "Pick an image",
1651
- tooltip: "Take the colors from an image on your device",
1652
- width: "fill"
1653
- })]
1654
- }), $[21] = busy, $[22] = current, $[23] = imageUrl, $[24] = onLucky, $[25] = onVariant, $[26] = palette, $[27] = pickImage, $[28] = thumbnailWidth, $[29] = variants, $[30] = t11) : t11 = $[30];
1655
- let t12;
1656
- $[31] !== t10 || $[32] !== t11 ? (t12 = /* @__PURE__ */ jsxs(Stack, {
1657
- gap: 3,
1658
- children: [t10, t11]
1659
- }), $[31] = t10, $[32] = t11, $[33] = t12) : t12 = $[33];
1660
- let t13;
1661
- return $[34] !== t12 || $[35] !== t6 || $[36] !== t7 ? (t13 = /* @__PURE__ */ jsx(Card, {
1662
- border: !0,
1663
- onDragEnter: t4,
1664
- onDragLeave: t5,
1665
- onDragOver: _temp$5,
1666
- onDrop: t6,
1667
- padding: 3,
1668
- radius: 3,
1669
- tone: t7,
1670
- children: t12
1671
- }), $[34] = t12, $[35] = t6, $[36] = t7, $[37] = t13) : t13 = $[37], t13;
1672
- }
1673
- function _temp$5(event_0) {
1674
- event_0.preventDefault();
1675
- }
1676
- var colorRowText = "os3rj2", range = "os3rj1", swatch = "os3rj0";
1677
- const SCHEME_TITLES = {
1678
- dark: "Dark mode",
1679
- light: "Light mode"
1680
- };
1681
- /**
1682
- * Where the split preview shows a scheme: the Studio's own scheme stays next
1683
- * to the sidebar (or at the bottom, stacked on small screens), the other one
1684
- * takes the far side
1685
- */
1686
- function splitHint(own, mobile) {
1687
- return mobile ? own ? "Shown at the bottom" : "Shown at the top" : own ? "Shown on the right, next to the themer" : "Shown on the left";
1688
- }
1689
- const SCHEME_OPTION_KEYS = [
1690
- "accent",
1691
- "text",
1692
- "background",
1693
- "contrast"
1694
- ];
1695
- /**
1696
- * `<input type="color">` paints the color into a shadow-DOM swatch that brings
1697
- * its own border and padding, which then sits inside ours as a second border.
1698
- * Stripping that chrome leaves the themed border as the only one.
1699
- */
1700
- /**
1701
- * The flow for editing one of the user's own themes: its title, a live
1702
- * preview, and a card per color scheme with the accent/text/background
1703
- * pickers and the contrast slider of that scheme. Every change applies to the
1704
- * whole Studio right away — in the scheme the Studio is showing, or in both
1705
- * side by side in the split preview.
1706
- *
1707
- * @internal
1708
- */
1709
- function ThemeEditor(props) {
1710
- let $ = c(42), { focusTitle, slug } = props, { themes, images, split, mobile, send } = useThemer(), t0;
1711
- if ($[0] !== slug || $[1] !== themes) {
1712
- let t1;
1713
- $[3] === slug ? t1 = $[4] : (t1 = (candidate) => candidate.slug === slug && candidate.source === "custom", $[3] = slug, $[4] = t1), t0 = themes.find(t1), $[0] = slug, $[1] = themes, $[2] = t0;
1714
- } else t0 = $[2];
1715
- let theme = t0;
1716
- if (!theme) return null;
1717
- let t1;
1718
- $[5] !== send || $[6] !== slug || $[7] !== theme.options || $[8] !== theme.palette ? (t1 = (variant) => {
1719
- theme.palette && variant !== null && send({
1720
- type: "theme.update",
1721
- slug,
1722
- options: applyImagePalette(theme.options, theme.palette, variant)
1723
- });
1724
- }, $[5] = send, $[6] = slug, $[7] = theme.options, $[8] = theme.palette, $[9] = t1) : t1 = $[9];
1725
- let applyVariant = t1, t2 = images[slug], t3;
1726
- $[10] === send ? t3 = $[11] : (t3 = () => send({ type: "flow.list" }), $[10] = send, $[11] = t3);
1727
- let t4;
1728
- $[12] !== applyVariant || $[13] !== theme.options || $[14] !== theme.palette ? (t4 = () => {
1729
- theme.palette && applyVariant(pickLuckyVariant(theme.palette, { exclude: currentImageVariant(theme.options, theme.palette) }));
1730
- }, $[12] = applyVariant, $[13] = theme.options, $[14] = theme.palette, $[15] = t4) : t4 = $[15];
1731
- let t5;
1732
- $[16] !== send || $[17] !== slug ? (t5 = (options) => send({
1733
- type: "theme.update",
1734
- slug,
1735
- options
1736
- }), $[16] = send, $[17] = slug, $[18] = t5) : t5 = $[18];
1737
- let t6;
1738
- $[19] !== send || $[20] !== slug || $[21] !== theme.options ? (t6 = (palette, file) => send({
1739
- type: "theme.update",
1740
- slug,
1741
- options: applyImagePalette(theme.options, palette),
1742
- palette,
1743
- imageUrl: URL.createObjectURL(file)
1744
- }), $[19] = send, $[20] = slug, $[21] = theme.options, $[22] = t6) : t6 = $[22];
1745
- let t7, t8;
1746
- $[23] !== send || $[24] !== slug ? (t7 = () => send({
1747
- type: "theme.remove",
1748
- slug
1749
- }), t8 = (title) => send({
1750
- type: "theme.update",
1751
- slug,
1752
- title
1753
- }), $[23] = send, $[24] = slug, $[25] = t7, $[26] = t8) : (t7 = $[25], t8 = $[26]);
1754
- let t9;
1755
- return $[27] !== applyVariant || $[28] !== focusTitle || $[29] !== mobile || $[30] !== split || $[31] !== t2 || $[32] !== t3 || $[33] !== t4 || $[34] !== t5 || $[35] !== t6 || $[36] !== t7 || $[37] !== t8 || $[38] !== theme.options || $[39] !== theme.palette || $[40] !== theme.title ? (t9 = /* @__PURE__ */ jsx(ThemeEditorForm, {
1756
- focusTitle,
1757
- imageUrl: t2,
1758
- onDone: t3,
1759
- onLucky: t4,
1760
- onOptionsChange: t5,
1761
- onPalette: t6,
1762
- onRemove: t7,
1763
- onTitleChange: t8,
1764
- onVariant: applyVariant,
1765
- options: theme.options,
1766
- palette: theme.palette,
1767
- split,
1768
- mobile,
1769
- title: theme.title
1770
- }), $[27] = applyVariant, $[28] = focusTitle, $[29] = mobile, $[30] = split, $[31] = t2, $[32] = t3, $[33] = t4, $[34] = t5, $[35] = t6, $[36] = t7, $[37] = t8, $[38] = theme.options, $[39] = theme.palette, $[40] = theme.title, $[41] = t9) : t9 = $[41], t9;
1771
- }
1772
- function ThemeEditorForm(props) {
1773
- let $ = c(50), { focusTitle, imageUrl, onDone, onLucky, onOptionsChange, onPalette, onRemove, onTitleChange, onVariant, options, palette, split, mobile, title } = props, studioScheme = useColorSchemeValue(), t0;
1774
- $[0] === options ? t0 = $[1] : (t0 = resolveThemeOptions(options), $[0] = options, $[1] = t0);
1775
- let resolved = t0, t1;
1776
- $[2] === options ? t1 = $[3] : (t1 = buildPalette(options), $[2] = options, $[3] = t1);
1777
- let palettes = t1, t2;
1778
- $[4] !== onOptionsChange || $[5] !== options ? (t2 = (scheme, changes) => {
1779
- let values = {
1780
- ...options[scheme],
1781
- ...changes
1782
- };
1783
- for (let key of SCHEME_OPTION_KEYS) values[key] === void 0 && delete values[key];
1784
- let next = { ...options };
1785
- Object.keys(values).length === 0 ? delete next[scheme] : next[scheme] = values, onOptionsChange(next);
1786
- }, $[4] = onOptionsChange, $[5] = options, $[6] = t2) : t2 = $[6];
1787
- let patchScheme = t2, t3;
1788
- $[7] === options ? t3 = $[8] : (t3 = /* @__PURE__ */ jsx(ThemeThumbnail, { options }), $[7] = options, $[8] = t3);
1789
- let t4;
1790
- $[9] === Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx(Text, {
1791
- size: 1,
1792
- weight: "medium",
1793
- children: "Title"
1794
- }), $[9] = t4) : t4 = $[9];
1795
- let t5;
1796
- $[10] === onTitleChange ? t5 = $[11] : (t5 = (event) => onTitleChange(event.currentTarget.value), $[10] = onTitleChange, $[11] = t5);
1797
- let t6 = focusTitle ? _temp$4 : void 0, t7;
1798
- $[12] !== focusTitle || $[13] !== t5 || $[14] !== t6 || $[15] !== title ? (t7 = /* @__PURE__ */ jsxs(Stack, {
1799
- gap: 2,
1800
- children: [t4, /* @__PURE__ */ jsx(TextInput, {
1801
- autoFocus: focusTitle,
1802
- fontSize: 1,
1803
- onChange: t5,
1804
- onFocus: t6,
1805
- padding: 2,
1806
- placeholder: "Untitled theme",
1807
- value: title
1808
- })]
1809
- }), $[12] = focusTitle, $[13] = t5, $[14] = t6, $[15] = title, $[16] = t7) : t7 = $[16];
1810
- let t8;
1811
- $[17] !== imageUrl || $[18] !== onLucky || $[19] !== onPalette || $[20] !== onVariant || $[21] !== options || $[22] !== palette ? (t8 = /* @__PURE__ */ jsx(ImagePaletteSection, {
1812
- imageUrl,
1813
- onLucky,
1814
- onPalette,
1815
- onVariant,
1816
- options,
1817
- palette
1818
- }), $[17] = imageUrl, $[18] = onLucky, $[19] = onPalette, $[20] = onVariant, $[21] = options, $[22] = palette, $[23] = t8) : t8 = $[23];
1819
- let t9;
1820
- $[24] !== mobile || $[25] !== options || $[26] !== palettes || $[27] !== patchScheme || $[28] !== resolved || $[29] !== split || $[30] !== studioScheme ? (t9 = SCHEMES.map((scheme_0) => /* @__PURE__ */ jsx(SchemeCard, {
1821
- active: split || scheme_0 === studioScheme,
1822
- onChange: (changes_0) => patchScheme(scheme_0, changes_0),
1823
- options: options[scheme_0] ?? {},
1824
- palette: palettes[scheme_0],
1825
- resolved: resolved[scheme_0],
1826
- scheme: scheme_0,
1827
- split,
1828
- splitHint: splitHint(scheme_0 === studioScheme, mobile)
1829
- }, scheme_0)), $[24] = mobile, $[25] = options, $[26] = palettes, $[27] = patchScheme, $[28] = resolved, $[29] = split, $[30] = studioScheme, $[31] = t9) : t9 = $[31];
1830
- let t10;
1831
- $[32] === t9 ? t10 = $[33] : (t10 = /* @__PURE__ */ jsx(ScrollAreaBleed, { children: t9 }), $[32] = t9, $[33] = t10);
1832
- let t11;
1833
- $[34] !== t10 || $[35] !== t3 || $[36] !== t7 || $[37] !== t8 ? (t11 = /* @__PURE__ */ jsx(ScrollArea, {
1834
- padding: 3,
1835
- children: /* @__PURE__ */ jsxs(Stack, {
1836
- gap: 4,
1837
- children: [
1838
- t3,
1839
- t7,
1840
- t8,
1841
- t10
1842
- ]
1843
- })
1844
- }), $[34] = t10, $[35] = t3, $[36] = t7, $[37] = t8, $[38] = t11) : t11 = $[38];
1845
- let t12;
1846
- $[39] === onRemove ? t12 = $[40] : (t12 = /* @__PURE__ */ jsx(TooltipButton, {
1847
- icon: TrashIcon,
1848
- mode: "ghost",
1849
- onClick: onRemove,
1850
- text: "Remove",
1851
- tone: "critical",
1852
- tooltip: "Remove the theme — it can be restored until it is deleted"
1853
- }), $[39] = onRemove, $[40] = t12);
1854
- let t13;
1855
- $[41] === Symbol.for("react.memo_cache_sentinel") ? (t13 = /* @__PURE__ */ jsx(Box, { flex: 1 }), $[41] = t13) : t13 = $[41];
1856
- let t14;
1857
- $[42] === onDone ? t14 = $[43] : (t14 = /* @__PURE__ */ jsx(Button, {
1858
- mode: "ghost",
1859
- onClick: onDone,
1860
- padding: 2,
1861
- text: "Done"
1862
- }), $[42] = onDone, $[43] = t14);
1863
- let t15;
1864
- $[44] !== t12 || $[45] !== t14 ? (t15 = /* @__PURE__ */ jsx(Card, {
1865
- borderTop: !0,
1866
- padding: 3,
1867
- children: /* @__PURE__ */ jsxs(Flex, {
1868
- gap: 2,
1869
- children: [
1870
- t12,
1871
- t13,
1872
- t14
1873
- ]
1874
- })
1875
- }), $[44] = t12, $[45] = t14, $[46] = t15) : t15 = $[46];
1876
- let t16;
1877
- return $[47] !== t11 || $[48] !== t15 ? (t16 = /* @__PURE__ */ jsxs(Fragment, { children: [t11, t15] }), $[47] = t11, $[48] = t15, $[49] = t16) : t16 = $[49], t16;
1878
- }
1879
- function _temp$4(event_0) {
1880
- return event_0.currentTarget.select();
1881
- }
1882
- /**
1883
- * The colors of one scheme. The cards span the sidebar edge to edge, one
1884
- * after the other with a border between them, in the sidebar's own scheme —
1885
- * the swatches show the colors they edit. The scheme the Studio is showing is
1886
- * marked as active: its changes show up in the Studio right away, the other
1887
- * scheme's only once the appearance setting switches to it — or both are
1888
- * active, in the split preview.
1889
- */
1890
- function SchemeCard(props) {
1891
- let $ = c(66), { active, onChange, options, palette, resolved, scheme, split, splitHint } = props, name = SCHEME_TITLES[scheme], t0;
1892
- $[0] === name ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(Box, {
1893
- flex: 1,
1894
- children: /* @__PURE__ */ jsx(Text, {
1895
- size: 1,
1896
- weight: "medium",
1897
- children: name
1898
- })
1899
- }), $[0] = name, $[1] = t0);
1900
- let t1;
1901
- $[2] === active ? t1 = $[3] : (t1 = active && /* @__PURE__ */ jsx(Badge, {
1902
- fontSize: 0,
1903
- tone: "primary",
1904
- children: "Active"
1905
- }), $[2] = active, $[3] = t1);
1906
- let t2;
1907
- $[4] !== t0 || $[5] !== t1 ? (t2 = /* @__PURE__ */ jsxs(Flex, {
1908
- align: "center",
1909
- gap: 2,
1910
- children: [t0, t1]
1911
- }), $[4] = t0, $[5] = t1, $[6] = t2) : t2 = $[6];
1912
- let t3 = split ? splitHint : active ? "Shown in the Studio right now" : `Shown once the Studio appearance is ${scheme}`, t4;
1913
- $[7] === t3 ? t4 = $[8] : (t4 = /* @__PURE__ */ jsx(Text, {
1914
- muted: !0,
1915
- size: 0,
1916
- children: t3
1917
- }), $[7] = t3, $[8] = t4);
1918
- let t5;
1919
- $[9] !== t2 || $[10] !== t4 ? (t5 = /* @__PURE__ */ jsxs(Stack, {
1920
- gap: 2,
1921
- children: [t2, t4]
1922
- }), $[9] = t2, $[10] = t4, $[11] = t5) : t5 = $[11];
1923
- let t6 = options.accent === void 0, t7 = `${name} accent`, t8, t9;
1924
- $[12] === onChange ? (t8 = $[13], t9 = $[14]) : (t8 = (accent) => onChange({ accent }), t9 = () => onChange({ accent: void 0 }), $[12] = onChange, $[13] = t8, $[14] = t9);
1925
- let t10;
1926
- $[15] !== resolved.accent || $[16] !== t6 || $[17] !== t7 || $[18] !== t8 || $[19] !== t9 ? (t10 = /* @__PURE__ */ jsx(ColorRow, {
1927
- auto: t6,
1928
- autoLabel: "default",
1929
- label: t7,
1930
- onChange: t8,
1931
- onClear: t9,
1932
- title: "Accent",
1933
- value: resolved.accent
1934
- }), $[15] = resolved.accent, $[16] = t6, $[17] = t7, $[18] = t8, $[19] = t9, $[20] = t10) : t10 = $[20];
1935
- let t11 = options.text === void 0, t12 = `${name} text`, t13, t14;
1936
- $[21] === onChange ? (t13 = $[22], t14 = $[23]) : (t13 = (text) => onChange({ text }), t14 = () => onChange({ text: void 0 }), $[21] = onChange, $[22] = t13, $[23] = t14);
1937
- let t15;
1938
- $[24] !== resolved.text || $[25] !== t11 || $[26] !== t12 || $[27] !== t13 || $[28] !== t14 ? (t15 = /* @__PURE__ */ jsx(ColorRow, {
1939
- auto: t11,
1940
- label: t12,
1941
- onChange: t13,
1942
- onClear: t14,
1943
- title: "Text",
1944
- value: resolved.text
1945
- }), $[24] = resolved.text, $[25] = t11, $[26] = t12, $[27] = t13, $[28] = t14, $[29] = t15) : t15 = $[29];
1946
- let t16 = scheme === "dark" ? palette.black : palette.white, t17 = options.background === void 0, t18 = `${name} background`, t19, t20;
1947
- $[30] === onChange ? (t19 = $[31], t20 = $[32]) : (t19 = (background) => onChange({ background }), t20 = () => onChange({ background: void 0 }), $[30] = onChange, $[31] = t19, $[32] = t20);
1948
- let t21;
1949
- $[33] !== resolved.background || $[34] !== t16 || $[35] !== t17 || $[36] !== t18 || $[37] !== t19 || $[38] !== t20 ? (t21 = /* @__PURE__ */ jsx(ColorRow, {
1950
- adjusted: t16,
1951
- auto: t17,
1952
- label: t18,
1953
- onChange: t19,
1954
- onClear: t20,
1955
- title: "Background",
1956
- value: resolved.background
1957
- }), $[33] = resolved.background, $[34] = t16, $[35] = t17, $[36] = t18, $[37] = t19, $[38] = t20, $[39] = t21) : t21 = $[39];
1958
- let t22;
1959
- $[40] === Symbol.for("react.memo_cache_sentinel") ? (t22 = /* @__PURE__ */ jsx(Text, {
1960
- size: 1,
1961
- children: "Contrast"
1962
- }), $[40] = t22) : t22 = $[40];
1963
- let t23 = options.contrast === void 0 ? " · auto" : "", t24;
1964
- $[41] !== resolved.contrast || $[42] !== t23 ? (t24 = /* @__PURE__ */ jsxs(Stack, {
1965
- flex: 1,
1966
- gap: 2,
1967
- children: [t22, /* @__PURE__ */ jsxs(Text, {
1968
- muted: !0,
1969
- size: 0,
1970
- children: [resolved.contrast, t23]
1971
- })]
1972
- }), $[41] = resolved.contrast, $[42] = t23, $[43] = t24) : t24 = $[43];
1973
- let t25;
1974
- $[44] !== onChange || $[45] !== options.contrast ? (t25 = options.contrast !== void 0 && /* @__PURE__ */ jsx(TooltipButton, {
1975
- icon: ResetIcon,
1976
- mode: "bleed",
1977
- onClick: () => onChange({ contrast: void 0 }),
1978
- padding: 2,
1979
- tooltip: "Reset to auto"
1980
- }), $[44] = onChange, $[45] = options.contrast, $[46] = t25) : t25 = $[46];
1981
- let t26;
1982
- $[47] !== t24 || $[48] !== t25 ? (t26 = /* @__PURE__ */ jsxs(Flex, {
1983
- align: "center",
1984
- gap: 2,
1985
- children: [t24, t25]
1986
- }), $[47] = t24, $[48] = t25, $[49] = t26) : t26 = $[49];
1987
- let t27 = `${name} contrast`, t28;
1988
- $[50] === onChange ? t28 = $[51] : (t28 = (event) => onChange({ contrast: Number(event.currentTarget.value) }), $[50] = onChange, $[51] = t28);
1989
- let t29;
1990
- $[52] !== resolved.contrast || $[53] !== t27 || $[54] !== t28 ? (t29 = /* @__PURE__ */ jsx("input", {
1991
- "aria-label": t27,
1992
- className: range,
1993
- max: 100,
1994
- min: 15,
1995
- onChange: t28,
1996
- step: 1,
1997
- type: "range",
1998
- value: resolved.contrast
1999
- }), $[52] = resolved.contrast, $[53] = t27, $[54] = t28, $[55] = t29) : t29 = $[55];
2000
- let t30;
2001
- $[56] === Symbol.for("react.memo_cache_sentinel") ? (t30 = /* @__PURE__ */ jsx(Text, {
2002
- muted: !0,
2003
- size: 0,
2004
- children: "100 keeps text and borders neutral — lower values blend in the accent"
2005
- }), $[56] = t30) : t30 = $[56];
2006
- let t31;
2007
- $[57] !== t26 || $[58] !== t29 ? (t31 = /* @__PURE__ */ jsxs(Stack, {
2008
- gap: 3,
2009
- children: [
2010
- t26,
2011
- t29,
2012
- t30
2013
- ]
2014
- }), $[57] = t26, $[58] = t29, $[59] = t31) : t31 = $[59];
2015
- let t32;
2016
- return $[60] !== t10 || $[61] !== t15 || $[62] !== t21 || $[63] !== t31 || $[64] !== t5 ? (t32 = /* @__PURE__ */ jsx(Card, {
2017
- borderTop: !0,
2018
- padding: 3,
2019
- children: /* @__PURE__ */ jsxs(Stack, {
2020
- gap: 4,
2021
- children: [
2022
- t5,
2023
- t10,
2024
- t15,
2025
- t21,
2026
- t31
2027
- ]
2028
- })
2029
- }), $[60] = t10, $[61] = t15, $[62] = t21, $[63] = t31, $[64] = t5, $[65] = t32) : t32 = $[65], t32;
2030
- }
2031
- /** One color of a scheme: a swatch with its value and an optional reset button */
2032
- function ColorRow(props) {
2033
- let $ = c(23), { adjusted, auto, autoLabel: t0, label, onChange, onClear, title, value } = props, autoLabel = t0 === void 0 ? "auto" : t0, t1;
2034
- $[0] === onChange ? t1 = $[1] : (t1 = (event) => onChange(event.currentTarget.value), $[0] = onChange, $[1] = t1);
2035
- let t2;
2036
- $[2] !== label || $[3] !== t1 || $[4] !== value ? (t2 = /* @__PURE__ */ jsx("input", {
2037
- "aria-label": label,
2038
- className: swatch,
2039
- onChange: t1,
2040
- type: "color",
2041
- value
2042
- }), $[2] = label, $[3] = t1, $[4] = value, $[5] = t2) : t2 = $[5];
2043
- let t3;
2044
- $[6] === title ? t3 = $[7] : (t3 = /* @__PURE__ */ jsx(Text, {
2045
- size: 1,
2046
- children: title
2047
- }), $[6] = title, $[7] = t3);
2048
- let t4 = adjusted !== void 0 && adjusted !== value ? ` → ${adjusted}` : "", t5 = auto ? ` · ${autoLabel}` : "", t6;
2049
- $[8] !== t4 || $[9] !== t5 || $[10] !== value ? (t6 = /* @__PURE__ */ jsxs(Text, {
2050
- muted: !0,
2051
- size: 0,
2052
- textOverflow: "ellipsis",
2053
- children: [
2054
- value,
2055
- t4,
2056
- t5
2057
- ]
2058
- }), $[8] = t4, $[9] = t5, $[10] = value, $[11] = t6) : t6 = $[11];
2059
- let t7;
2060
- $[12] !== t3 || $[13] !== t6 ? (t7 = /* @__PURE__ */ jsxs(Stack, {
2061
- className: colorRowText,
2062
- flex: 1,
2063
- gap: 2,
2064
- children: [t3, t6]
2065
- }), $[12] = t3, $[13] = t6, $[14] = t7) : t7 = $[14];
2066
- let t8;
2067
- $[15] !== auto || $[16] !== autoLabel || $[17] !== onClear ? (t8 = onClear && !auto && /* @__PURE__ */ jsx(TooltipButton, {
2068
- icon: ResetIcon,
2069
- mode: "bleed",
2070
- onClick: onClear,
2071
- padding: 2,
2072
- tooltip: `Reset to ${autoLabel}`
2073
- }), $[15] = auto, $[16] = autoLabel, $[17] = onClear, $[18] = t8) : t8 = $[18];
2074
- let t9;
2075
- return $[19] !== t2 || $[20] !== t7 || $[21] !== t8 ? (t9 = /* @__PURE__ */ jsxs(Flex, {
2076
- align: "center",
2077
- gap: 2,
2078
- children: [
2079
- t2,
2080
- t7,
2081
- t8
2082
- ]
2083
- }), $[19] = t2, $[20] = t7, $[21] = t8, $[22] = t9) : t9 = $[22], t9;
2084
- }
2085
- /**
2086
- * The version of the code format, the first byte of every code — bumped if
2087
- * the layout below ever changes, so older codes can still be told apart
2088
- */
2089
- const FIELDS = [
2090
- "accent",
2091
- "text",
2092
- "background",
2093
- "contrast"
2094
- ];
2095
- /**
2096
- * Turns a theme into a short code to share in a message: `title`, and the
2097
- * colors and contrasts of both schemes, packed into bytes — one flag byte says
2098
- * which of the eight fields are set, each color is its three RGB bytes and a
2099
- * contrast one byte, and the title takes up the rest — then base64url'd, so
2100
- * it is one word without padding, safe in URLs and chat. A theme setting
2101
- * everything comes to about 30 bytes plus its title.
2102
- *
2103
- * @internal
2104
- */
2105
- function encodeTheme(theme) {
2106
- let bytes = [1, 0], flags = 0;
2107
- return SCHEMES.forEach((scheme, schemeIndex) => {
2108
- let options = theme.options[scheme] ?? {};
2109
- FIELDS.forEach((field, fieldIndex) => {
2110
- let value = options[field];
2111
- if (value !== void 0) {
2112
- if (field === "contrast") {
2113
- if (typeof value != "number" || !Number.isFinite(value)) return;
2114
- bytes.push(Math.round(Math.min(100, Math.max(15, value))));
2115
- } else {
2116
- if (typeof value != "string" || !isColor(value)) return;
2117
- bytes.push(...rgbBytes(value));
2118
- }
2119
- flags |= 1 << schemeIndex * FIELDS.length + fieldIndex;
2120
- }
2121
- });
2122
- }), bytes[1] = flags, bytes.push(...new TextEncoder().encode(theme.title.trim())), toBase64Url(Uint8Array.from(bytes));
2123
- }
2124
- /**
2125
- * Reads a theme back out of a code — as pasted, so surrounding whitespace and
2126
- * words around the code are fine: the first word that is a code counts. `null`
2127
- * when nothing in the text is one.
2128
- *
2129
- * @internal
2130
- */
2131
- function decodeTheme(text) {
2132
- for (let word of text.trim().split(/\s+/)) {
2133
- let theme = decodeCode(word);
2134
- if (theme) return theme;
2135
- }
2136
- return null;
2137
- }
2138
- function decodeCode(code) {
2139
- let bytes = fromBase64Url(code);
2140
- if (!bytes || bytes.length < 2 || bytes[0] !== 1) return null;
2141
- let flags = bytes[1], options = {}, offset = 2;
2142
- for (let [schemeIndex, scheme] of SCHEMES.entries()) {
2143
- let schemeOptions = {};
2144
- for (let [fieldIndex, field] of FIELDS.entries()) if (flags & 1 << schemeIndex * FIELDS.length + fieldIndex) {
2145
- if (field === "contrast") {
2146
- if (offset + 1 > bytes.length) return null;
2147
- schemeOptions.contrast = Math.min(100, Math.max(15, bytes[offset])), offset += 1;
2148
- } else {
2149
- if (offset + 3 > bytes.length) return null;
2150
- schemeOptions[field] = hexColor(bytes.subarray(offset, offset + 3)), offset += 3;
2151
- }
2152
- }
2153
- Object.keys(schemeOptions).length > 0 && (options[scheme] = schemeOptions);
2154
- }
2155
- let title;
2156
- try {
2157
- title = new TextDecoder("utf-8", { fatal: !0 }).decode(bytes.subarray(offset));
2158
- } catch {
2159
- return null;
2160
- }
2161
- return {
2162
- title: displayTitle(title),
2163
- options
2164
- };
2165
- }
2166
- /** The three RGB bytes of a `#rgb` or `#rrggbb` color */
2167
- function rgbBytes(color) {
2168
- let hex = color.length === 4 ? color.slice(1).replaceAll(/./g, "$&$&") : color.slice(1);
2169
- return [
2170
- 0,
2171
- 2,
2172
- 4
2173
- ].map((start) => parseInt(hex.slice(start, start + 2), 16));
2174
- }
2175
- function hexColor(rgb) {
2176
- return `#${[...rgb].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2177
- }
2178
- function toBase64Url(bytes) {
2179
- let binary = "";
2180
- for (let byte of bytes) binary += String.fromCharCode(byte);
2181
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
2182
- }
2183
- function fromBase64Url(code) {
2184
- if (!/^[A-Za-z0-9_-]+$/.test(code)) return null;
2185
- let base64 = code.replaceAll("-", "+").replaceAll("_", "/");
2186
- try {
2187
- return Uint8Array.from(atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "=")), (char) => char.charCodeAt(0));
2188
- } catch {
2189
- return null;
2190
- }
2191
- }
2192
- /**
2193
- * Copying a theme as a code to share, and adding a theme from a code someone
2194
- * shared — with toasts telling how it went.
2195
- *
2196
- * @internal
2197
- */
2198
- function useThemeCodes() {
2199
- let $ = c(11), { send } = useThemer(), toast = useToast(), t0;
2200
- $[0] === toast ? t0 = $[1] : (t0 = async (theme) => {
2201
- let title = displayTitle(theme.title);
2202
- try {
2203
- await navigator.clipboard.writeText(encodeTheme(theme)), toast.push({
2204
- status: "success",
2205
- title: `Copied ${title} as a code`,
2206
- description: "Paste it in the themer of another Studio to add the theme there"
2207
- });
2208
- } catch {
2209
- toast.push({
2210
- status: "error",
2211
- title: `Could not copy ${title}`
2212
- });
2213
- }
2214
- }, $[0] = toast, $[1] = t0);
2215
- let copyTheme = t0, t1;
2216
- $[2] !== send || $[3] !== toast ? (t1 = (text) => {
2217
- let theme_0 = decodeTheme(text);
2218
- return theme_0 ? (send({
2219
- type: "theme.import",
2220
- title: theme_0.title,
2221
- options: theme_0.options
2222
- }), toast.push({
2223
- status: "success",
2224
- title: `Added ${theme_0.title}`
2225
- }), !0) : !1;
2226
- }, $[2] = send, $[3] = toast, $[4] = t1) : t1 = $[4];
2227
- let addThemeFromText = t1, t2;
2228
- $[5] === addThemeFromText ? t2 = $[6] : (t2 = async () => {
2229
- let text_0;
2230
- try {
2231
- text_0 = await navigator.clipboard.readText();
2232
- } catch {
2233
- return "no-clipboard";
2234
- }
2235
- return addThemeFromText(text_0) ? "added" : "no-theme";
2236
- }, $[5] = addThemeFromText, $[6] = t2);
2237
- let addThemeFromClipboard = t2, t3;
2238
- return $[7] !== addThemeFromClipboard || $[8] !== addThemeFromText || $[9] !== copyTheme ? (t3 = {
2239
- copyTheme,
2240
- addThemeFromText,
2241
- addThemeFromClipboard
2242
- }, $[7] = addThemeFromClipboard, $[8] = addThemeFromText, $[9] = copyTheme, $[10] = t3) : t3 = $[10], t3;
2243
- }
2244
- /**
2245
- * Adds a theme pasted anywhere in the Studio while the themer's list is
2246
- * showing — pastes into inputs and other editable things are theirs.
2247
- *
2248
- * @internal
2249
- */
2250
- function usePasteThemeCodes() {
2251
- let $ = c(5), { open, send } = useThemer(), toast = useToast(), t0, t1;
2252
- $[0] !== open || $[1] !== send || $[2] !== toast ? (t0 = () => {
2253
- if (!open) return;
2254
- let handlePaste = (event) => {
2255
- if (isEditable(event.target)) return;
2256
- let theme = decodeTheme(event.clipboardData?.getData("text/plain") ?? "");
2257
- theme && (event.preventDefault(), send({
2258
- type: "theme.import",
2259
- title: theme.title,
2260
- options: theme.options
2261
- }), toast.push({
2262
- status: "success",
2263
- title: `Added ${theme.title}`
2264
- }));
2265
- };
2266
- return document.addEventListener("paste", handlePaste), () => document.removeEventListener("paste", handlePaste);
2267
- }, t1 = [
2268
- open,
2269
- send,
2270
- toast
2271
- ], $[0] = open, $[1] = send, $[2] = toast, $[3] = t0, $[4] = t1) : (t0 = $[3], t1 = $[4]), useEffect(t0, t1);
2272
- }
2273
- function isEditable(target) {
2274
- return target instanceof HTMLElement && (target.isContentEditable || target.matches("input, textarea, select"));
2275
- }
2276
- /**
2277
- * Where a theme code someone shared gets pasted by hand — for browsers that
2278
- * do not hand the clipboard over, and for codes that are not on it.
2279
- *
2280
- * @internal
2281
- */
2282
- function PasteThemeDialog(props) {
2283
- let $ = c(35), { onClose } = props, { addThemeFromText } = useThemeCodes(), [text, setText] = useState(""), [invalid, setInvalid] = useState(!1), t0;
2284
- $[0] !== addThemeFromText || $[1] !== onClose || $[2] !== text ? (t0 = () => {
2285
- addThemeFromText(text) ? onClose() : setInvalid(!0);
2286
- }, $[0] = addThemeFromText, $[1] = onClose, $[2] = text, $[3] = t0) : t0 = $[3];
2287
- let add = t0, t1;
2288
- $[4] === onClose ? t1 = $[5] : (t1 = /* @__PURE__ */ jsx(Button, {
2289
- mode: "ghost",
2290
- onClick: onClose,
2291
- padding: 2,
2292
- text: "Cancel"
2293
- }), $[4] = onClose, $[5] = t1);
2294
- let t2;
2295
- $[6] === text ? t2 = $[7] : (t2 = text.trim(), $[6] = text, $[7] = t2);
2296
- let t3 = t2 === "", t4;
2297
- $[8] !== add || $[9] !== t3 ? (t4 = /* @__PURE__ */ jsx(Button, {
2298
- disabled: t3,
2299
- onClick: add,
2300
- padding: 2,
2301
- text: "Add theme",
2302
- tone: "primary"
2303
- }), $[8] = add, $[9] = t3, $[10] = t4) : t4 = $[10];
2304
- let t5;
2305
- $[11] !== t1 || $[12] !== t4 ? (t5 = /* @__PURE__ */ jsxs(Flex, {
2306
- gap: 2,
2307
- justify: "flex-end",
2308
- padding: 3,
2309
- children: [t1, t4]
2310
- }), $[11] = t1, $[12] = t4, $[13] = t5) : t5 = $[13];
2311
- let t6;
2312
- $[14] === add ? t6 = $[15] : (t6 = (event) => {
2313
- event.preventDefault(), add();
2314
- }, $[14] = add, $[15] = t6);
2315
- let t7;
2316
- $[16] === Symbol.for("react.memo_cache_sentinel") ? (t7 = /* @__PURE__ */ jsx(Text, {
2317
- muted: !0,
2318
- size: 1,
2319
- children: "Paste the code someone copied from their themer — the theme is added to your list and applied."
2320
- }), $[16] = t7) : t7 = $[16];
2321
- let t8 = invalid ? "themer-paste-error" : void 0, t9 = invalid ? "That is not a theme code" : void 0, t10;
2322
- $[17] === Symbol.for("react.memo_cache_sentinel") ? (t10 = (event_0) => {
2323
- setText(event_0.currentTarget.value), setInvalid(!1);
2324
- }, $[17] = t10) : t10 = $[17];
2325
- let t11;
2326
- $[18] !== invalid || $[19] !== t8 || $[20] !== t9 || $[21] !== text ? (t11 = /* @__PURE__ */ jsx(TextInput, {
2327
- "aria-describedby": t8,
2328
- "aria-invalid": invalid,
2329
- "aria-label": "Theme code",
2330
- autoFocus: !0,
2331
- customValidity: t9,
2332
- fontSize: 1,
2333
- onChange: t10,
2334
- placeholder: "AQ…",
2335
- value: text
2336
- }), $[18] = invalid, $[19] = t8, $[20] = t9, $[21] = text, $[22] = t11) : t11 = $[22];
2337
- let t12;
2338
- $[23] === invalid ? t12 = $[24] : (t12 = invalid && /* @__PURE__ */ jsx(Card, {
2339
- id: "themer-paste-error",
2340
- padding: 3,
2341
- radius: 2,
2342
- role: "alert",
2343
- tone: "critical",
2344
- children: /* @__PURE__ */ jsx(Text, {
2345
- size: 1,
2346
- children: "That is not a theme code — codes are one word, copied from a theme's menu."
2347
- })
2348
- }), $[23] = invalid, $[24] = t12);
2349
- let t13;
2350
- $[25] !== t11 || $[26] !== t12 ? (t13 = /* @__PURE__ */ jsxs(Stack, {
2351
- gap: 3,
2352
- padding: 4,
2353
- children: [
2354
- t7,
2355
- t11,
2356
- t12
2357
- ]
2358
- }), $[25] = t11, $[26] = t12, $[27] = t13) : t13 = $[27];
2359
- let t14;
2360
- $[28] !== t13 || $[29] !== t6 ? (t14 = /* @__PURE__ */ jsx("form", {
2361
- onSubmit: t6,
2362
- children: t13
2363
- }), $[28] = t13, $[29] = t6, $[30] = t14) : t14 = $[30];
2364
- let t15;
2365
- return $[31] !== onClose || $[32] !== t14 || $[33] !== t5 ? (t15 = /* @__PURE__ */ jsx(Dialog, {
2366
- animate: !0,
2367
- footer: t5,
2368
- header: "Add a shared theme",
2369
- id: "themer-paste",
2370
- onClickOutside: onClose,
2371
- onClose,
2372
- width: 0,
2373
- children: t14
2374
- }), $[31] = onClose, $[32] = t14, $[33] = t5, $[34] = t15) : t15 = $[34], t15;
2375
- }
2376
- var frame$1 = "_1wsn7882", menuCard = "_1wsn7884", menuSlot = "_1wsn7883", pickButton = "_1wsn7881", root = "_1wsn7880";
2377
- /**
2378
- * Scrolls the applied theme's card into view when it is not already, as a
2379
- * ref: the list scrolls to it as it mounts — coming back from the editor or
2380
- * the removed themes — and when another theme gets applied
2381
- */
2382
- function scrollRefIntoViewIfNeeded(node) {
2383
- node && scrollIntoView(node, {
2384
- block: "nearest",
2385
- scrollMode: "if-needed"
2386
- });
2387
- }
2388
- /**
2389
- * One theme in the list: the floating preview with the title below it, like
2390
- * an appearance option in macOS System Settings. Clicking it applies the
2391
- * theme to the whole Studio, dragging it moves the theme to another place in
2392
- * the list; its menu holds the move, edit, duplicate and remove flows that
2393
- * apply to the theme.
2394
- *
2395
- * @internal
2396
- */
2397
- function ThemeCard(props) {
2398
- let $ = c(85), { active, theme } = props, { themes, send } = useThemer(), { copyTheme } = useThemeCodes(), { scheme } = useRootTheme(), [menuOpen, setMenuOpen] = useState(!1), dragControls = useDragControls(), dragged = useRef(!1), t0;
2399
- $[0] === theme.title ? t0 = $[1] : (t0 = displayTitle(theme.title), $[0] = theme.title, $[1] = t0);
2400
- let title = t0, t1;
2401
- $[2] === theme.slug ? t1 = $[3] : (t1 = (candidate) => candidate.slug === theme.slug, $[2] = theme.slug, $[3] = t1);
2402
- let index = themes.findIndex(t1), t2;
2403
- $[4] !== index || $[5] !== send || $[6] !== themes ? (t2 = (to) => {
2404
- let order = themes.map(_temp$3);
2405
- order.splice(to, 0, ...order.splice(index, 1)), send({
2406
- type: "theme.reorder",
2407
- order
2408
- });
2409
- }, $[4] = index, $[5] = send, $[6] = themes, $[7] = t2) : t2 = $[7];
2410
- let moveTo = t2, t3, t4;
2411
- $[8] === Symbol.for("react.memo_cache_sentinel") ? (t3 = () => {
2412
- requestAnimationFrame(() => {
2413
- dragged.current = !1;
2414
- });
2415
- }, t4 = () => {
2416
- dragged.current = !0;
2417
- }, $[8] = t3, $[9] = t4) : (t3 = $[8], t4 = $[9]);
2418
- let t5 = active ? scrollRefIntoViewIfNeeded : void 0, t6;
2419
- $[10] === Symbol.for("react.memo_cache_sentinel") ? (t6 = { scale: 1.04 }, $[10] = t6) : t6 = $[10];
2420
- let t7;
2421
- $[11] !== send || $[12] !== theme.slug ? (t7 = () => {
2422
- dragged.current || send({
2423
- type: "theme.pick",
2424
- slug: theme.slug
2425
- });
2426
- }, $[11] = send, $[12] = theme.slug, $[13] = t7) : t7 = $[13];
2427
- let t8;
2428
- $[14] === dragControls ? t8 = $[15] : (t8 = (event) => {
2429
- event.pointerType !== "touch" && dragControls.start(event);
2430
- }, $[14] = dragControls, $[15] = t8);
2431
- let t9;
2432
- $[16] === theme.options ? t9 = $[17] : (t9 = /* @__PURE__ */ jsx("span", {
2433
- className: frame$1,
2434
- children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: theme.options })
2435
- }), $[16] = theme.options, $[17] = t9);
2436
- let t10 = active ? "medium" : "regular", t11;
2437
- $[18] !== t10 || $[19] !== title ? (t11 = /* @__PURE__ */ jsx(Box, {
2438
- as: "span",
2439
- display: "block",
2440
- paddingTop: 2,
2441
- paddingX: 2,
2442
- children: /* @__PURE__ */ jsx(Text, {
2443
- align: "center",
2444
- as: "span",
2445
- size: 1,
2446
- textOverflow: "ellipsis",
2447
- weight: t10,
2448
- children: title
2449
- })
2450
- }), $[18] = t10, $[19] = title, $[20] = t11) : t11 = $[20];
2451
- let t12;
2452
- $[21] !== active || $[22] !== t11 || $[23] !== t7 || $[24] !== t8 || $[25] !== t9 ? (t12 = /* @__PURE__ */ jsxs("button", {
2453
- "aria-pressed": active,
2454
- className: pickButton,
2455
- onClick: t7,
2456
- onPointerDown: t8,
2457
- type: "button",
2458
- children: [t9, t11]
2459
- }), $[21] = active, $[22] = t11, $[23] = t7, $[24] = t8, $[25] = t9, $[26] = t12) : t12 = $[26];
2460
- let t13;
2461
- $[27] === Symbol.for("react.memo_cache_sentinel") ? (t13 = /* @__PURE__ */ jsx(Text, {
2462
- size: 1,
2463
- children: "Show more"
2464
- }), $[27] = t13) : t13 = $[27];
2465
- let t14 = `Show more for ${title}`, t15;
2466
- $[28] === t14 ? t15 = $[29] : (t15 = /* @__PURE__ */ jsx(Button, {
2467
- "aria-label": t14,
2468
- fontSize: 1,
2469
- icon: EllipsisHorizontalIcon,
2470
- mode: "ghost",
2471
- padding: 1,
2472
- radius: 2
2473
- }), $[28] = t14, $[29] = t15);
2474
- let t16 = `themer-theme-${theme.slug}`, t17 = index <= 0, t18;
2475
- $[30] !== index || $[31] !== moveTo ? (t18 = () => moveTo(index - 1), $[30] = index, $[31] = moveTo, $[32] = t18) : t18 = $[32];
2476
- let t19;
2477
- $[33] !== t17 || $[34] !== t18 ? (t19 = /* @__PURE__ */ jsx(MenuItem, {
2478
- disabled: t17,
2479
- icon: ArrowUpIcon,
2480
- onClick: t18,
2481
- text: "Move up"
2482
- }), $[33] = t17, $[34] = t18, $[35] = t19) : t19 = $[35];
2483
- let t20 = index >= themes.length - 1, t21;
2484
- $[36] !== index || $[37] !== moveTo ? (t21 = () => moveTo(index + 1), $[36] = index, $[37] = moveTo, $[38] = t21) : t21 = $[38];
2485
- let t22;
2486
- $[39] !== t20 || $[40] !== t21 ? (t22 = /* @__PURE__ */ jsx(MenuItem, {
2487
- disabled: t20,
2488
- icon: ArrowDownIcon,
2489
- onClick: t21,
2490
- text: "Move down"
2491
- }), $[39] = t20, $[40] = t21, $[41] = t22) : t22 = $[41];
2492
- let t23;
2493
- $[42] === Symbol.for("react.memo_cache_sentinel") ? (t23 = /* @__PURE__ */ jsx(MenuDivider, {}), $[42] = t23) : t23 = $[42];
2494
- let t24;
2495
- $[43] !== send || $[44] !== theme.slug || $[45] !== theme.source ? (t24 = theme.source === "custom" && /* @__PURE__ */ jsx(MenuItem, {
2496
- icon: EditIcon,
2497
- onClick: () => send({
2498
- type: "theme.edit",
2499
- slug: theme.slug
2500
- }),
2501
- text: "Edit"
2502
- }), $[43] = send, $[44] = theme.slug, $[45] = theme.source, $[46] = t24) : t24 = $[46];
2503
- let t25;
2504
- $[47] !== send || $[48] !== theme.slug ? (t25 = () => send({
2505
- type: "theme.duplicate",
2506
- slug: theme.slug
2507
- }), $[47] = send, $[48] = theme.slug, $[49] = t25) : t25 = $[49];
2508
- let t26 = theme.source === "custom" ? "Duplicate" : "Duplicate to edit", t27;
2509
- $[50] !== t25 || $[51] !== t26 ? (t27 = /* @__PURE__ */ jsx(MenuItem, {
2510
- icon: CopyIcon,
2511
- onClick: t25,
2512
- text: t26
2513
- }), $[50] = t25, $[51] = t26, $[52] = t27) : t27 = $[52];
2514
- let t28;
2515
- $[53] !== copyTheme || $[54] !== theme ? (t28 = /* @__PURE__ */ jsx(MenuItem, {
2516
- icon: ShareIcon,
2517
- onClick: () => void copyTheme(theme),
2518
- text: "Copy to share"
2519
- }), $[53] = copyTheme, $[54] = theme, $[55] = t28) : t28 = $[55];
2520
- let t29;
2521
- $[56] !== send || $[57] !== theme.slug || $[58] !== theme.source ? (t29 = theme.source !== "config" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuDivider, {}), /* @__PURE__ */ jsx(MenuItem, {
2522
- icon: TrashIcon,
2523
- onClick: () => send({
2524
- type: "theme.remove",
2525
- slug: theme.slug
2526
- }),
2527
- text: "Remove",
2528
- tone: "critical"
2529
- })] }), $[56] = send, $[57] = theme.slug, $[58] = theme.source, $[59] = t29) : t29 = $[59];
2530
- let t30;
2531
- $[60] !== t19 || $[61] !== t22 || $[62] !== t24 || $[63] !== t27 || $[64] !== t28 || $[65] !== t29 ? (t30 = /* @__PURE__ */ jsxs(Menu, { children: [
2532
- t19,
2533
- t22,
2534
- t23,
2535
- t24,
2536
- t27,
2537
- t28,
2538
- t29
2539
- ] }), $[60] = t19, $[61] = t22, $[62] = t24, $[63] = t27, $[64] = t28, $[65] = t29, $[66] = t30) : t30 = $[66];
2540
- let t31, t32;
2541
- $[67] === Symbol.for("react.memo_cache_sentinel") ? (t31 = () => setMenuOpen(!1), t32 = () => setMenuOpen(!0), $[67] = t31, $[68] = t32) : (t31 = $[67], t32 = $[68]);
2542
- let t33;
2543
- $[69] === scheme ? t33 = $[70] : (t33 = {
2544
- animate: !0,
2545
- placement: "bottom-end",
2546
- portal: !0,
2547
- scheme
2548
- }, $[69] = scheme, $[70] = t33);
2549
- let t34;
2550
- $[71] !== t15 || $[72] !== t16 || $[73] !== t30 || $[74] !== t33 ? (t34 = /* @__PURE__ */ jsx(Tooltip, {
2551
- animate: !0,
2552
- content: t13,
2553
- placement: "bottom",
2554
- portal: !0,
2555
- children: /* @__PURE__ */ jsx(Card, {
2556
- className: menuCard,
2557
- radius: 2,
2558
- scheme: "dark",
2559
- children: /* @__PURE__ */ jsx(MenuButton, {
2560
- button: t15,
2561
- id: t16,
2562
- menu: t30,
2563
- onClose: t31,
2564
- onOpen: t32,
2565
- popover: t33
2566
- })
2567
- })
2568
- }), $[71] = t15, $[72] = t16, $[73] = t30, $[74] = t33, $[75] = t34) : t34 = $[75];
2569
- let t35;
2570
- $[76] !== menuOpen || $[77] !== t34 ? (t35 = /* @__PURE__ */ jsx("div", {
2571
- className: menuSlot,
2572
- "data-visible": menuOpen,
2573
- children: t34
2574
- }), $[76] = menuOpen, $[77] = t34, $[78] = t35) : t35 = $[78];
2575
- let t36;
2576
- return $[79] !== dragControls || $[80] !== t12 || $[81] !== t35 || $[82] !== t5 || $[83] !== theme.slug ? (t36 = /* @__PURE__ */ jsxs(Reorder.Item, {
2577
- as: "div",
2578
- className: root,
2579
- dragControls,
2580
- dragListener: !1,
2581
- onDragEnd: t3,
2582
- onDragStart: t4,
2583
- ref: t5,
2584
- value: theme.slug,
2585
- whileDrag: t6,
2586
- children: [t12, t35]
2587
- }), $[79] = dragControls, $[80] = t12, $[81] = t35, $[82] = t5, $[83] = theme.slug, $[84] = t36) : t36 = $[84], t36;
2588
- }
2589
- function _temp$3(candidate_0) {
2590
- return candidate_0.slug;
2591
- }
2592
- var cardGrid = "twjqgh0";
2593
- /**
2594
- * One column at the sidebar's default width; wider sidebars — and the
2595
- * overlay on small screens — fit more cards per row
2596
- */
2597
- /**
2598
- * The flow for picking a theme: a grid of theme cards — the configured theme,
2599
- * the presets and the user's own themes — that drag into the order the user
2600
- * wants, with the entry points to the add and restore flows below. A theme
2601
- * code someone shared pastes right into the list, or through the paste button.
2602
- *
2603
- * @internal
2604
- */
2605
- function ThemeList() {
2606
- let $ = c(38), { themes, removed, active, send } = useThemer(), { addThemeFromClipboard } = useThemeCodes(), [pasting, setPasting] = useState(!1);
2607
- usePasteThemeCodes();
2608
- let t0;
2609
- $[0] === send ? t0 = $[1] : (t0 = (palette, file) => send({
2610
- type: "theme.add",
2611
- title: titleFromFileName(file.name),
2612
- options: optionsFromImagePalette(palette),
2613
- palette,
2614
- imageUrl: URL.createObjectURL(file)
2615
- }), $[0] = send, $[1] = t0);
2616
- let { busy, pickImage } = useImagePalette(t0), t1;
2617
- $[2] === send ? t1 = $[3] : (t1 = (order) => send({
2618
- type: "theme.reorder",
2619
- order
2620
- }), $[2] = send, $[3] = t1);
2621
- let t2;
2622
- $[4] === themes ? t2 = $[5] : (t2 = themes.map(_temp$2), $[4] = themes, $[5] = t2);
2623
- let t3;
2624
- if ($[6] !== active || $[7] !== themes) {
2625
- let t4;
2626
- $[9] === active ? t4 = $[10] : (t4 = (theme_0) => /* @__PURE__ */ jsx(ThemeCard, {
2627
- active: theme_0.slug === active.slug,
2628
- theme: theme_0
2629
- }, theme_0.slug), $[9] = active, $[10] = t4), t3 = themes.map(t4), $[6] = active, $[7] = themes, $[8] = t3;
2630
- } else t3 = $[8];
2631
- let t4;
2632
- $[11] !== t1 || $[12] !== t2 || $[13] !== t3 ? (t4 = /* @__PURE__ */ jsx(MotionConfig, {
2633
- reducedMotion: "user",
2634
- children: /* @__PURE__ */ jsx(Reorder.Group, {
2635
- as: "div",
2636
- className: cardGrid,
2637
- onReorder: t1,
2638
- values: t2,
2639
- children: t3
2640
- })
2641
- }), $[11] = t1, $[12] = t2, $[13] = t3, $[14] = t4) : t4 = $[14];
2642
- let t5;
2643
- $[15] !== removed.length || $[16] !== send ? (t5 = removed.length > 0 && /* @__PURE__ */ jsx(Button, {
2644
- gap: 2,
2645
- icon: RestoreIcon,
2646
- mode: "bleed",
2647
- onClick: () => send({ type: "flow.removed" }),
2648
- padding: 2,
2649
- text: `Show removed (${removed.length})`,
2650
- width: "fill"
2651
- }), $[15] = removed.length, $[16] = send, $[17] = t5) : t5 = $[17];
2652
- let t6;
2653
- $[18] !== t4 || $[19] !== t5 ? (t6 = /* @__PURE__ */ jsx(ScrollArea, {
2654
- padding: 3,
2655
- children: /* @__PURE__ */ jsxs(Stack, {
2656
- gap: 4,
2657
- children: [t4, t5]
2658
- })
2659
- }), $[18] = t4, $[19] = t5, $[20] = t6) : t6 = $[20];
2660
- let t7;
2661
- $[21] === send ? t7 = $[22] : (t7 = /* @__PURE__ */ jsx(Box, {
2662
- flex: 1,
2663
- children: /* @__PURE__ */ jsx(TooltipButton, {
2664
- icon: AddIcon,
2665
- mode: "ghost",
2666
- onClick: () => send({ type: "theme.add" }),
2667
- text: "Add theme",
2668
- tooltip: "Add a theme based on the applied one",
2669
- width: "fill"
2670
- })
2671
- }), $[21] = send, $[22] = t7);
2672
- let t8;
2673
- $[23] !== busy || $[24] !== pickImage ? (t8 = /* @__PURE__ */ jsx(ImageFileButton, {
2674
- loading: busy,
2675
- mode: "ghost",
2676
- onFile: pickImage,
2677
- tooltip: "Add a theme from the colors of an image"
2678
- }), $[23] = busy, $[24] = pickImage, $[25] = t8) : t8 = $[25];
2679
- let t9;
2680
- $[26] === addThemeFromClipboard ? t9 = $[27] : (t9 = /* @__PURE__ */ jsx(TooltipButton, {
2681
- icon: ClipboardIcon,
2682
- mode: "ghost",
2683
- onClick: async () => {
2684
- await addThemeFromClipboard() !== "added" && setPasting(!0);
2685
- },
2686
- tooltip: "Add a theme from a code someone shared"
2687
- }), $[26] = addThemeFromClipboard, $[27] = t9);
2688
- let t10;
2689
- $[28] !== t7 || $[29] !== t8 || $[30] !== t9 ? (t10 = /* @__PURE__ */ jsx(Card, {
2690
- borderTop: !0,
2691
- padding: 3,
2692
- children: /* @__PURE__ */ jsxs(Flex, {
2693
- gap: 2,
2694
- children: [
2695
- t7,
2696
- t8,
2697
- t9
2698
- ]
2699
- })
2700
- }), $[28] = t7, $[29] = t8, $[30] = t9, $[31] = t10) : t10 = $[31];
2701
- let t11;
2702
- $[32] === pasting ? t11 = $[33] : (t11 = pasting && /* @__PURE__ */ jsx(PasteThemeDialog, { onClose: () => setPasting(!1) }), $[32] = pasting, $[33] = t11);
2703
- let t12;
2704
- return $[34] !== t10 || $[35] !== t11 || $[36] !== t6 ? (t12 = /* @__PURE__ */ jsxs(Fragment, { children: [
2705
- t6,
2706
- t10,
2707
- t11
2708
- ] }), $[34] = t10, $[35] = t11, $[36] = t6, $[37] = t12) : t12 = $[37], t12;
2709
- }
2710
- function _temp$2(theme) {
2711
- return theme.slug;
2712
- }
2713
- /**
2714
- * Serializes theme options into the `buildTheme` call to paste into
2715
- * `sanity.config.ts`, keeping only what differs from the derived defaults in
2716
- * each scheme.
2717
- *
2718
- * Options that boil down to the stock Studio theme need nothing from this
2719
- * package — so they serialize to a bare `buildTheme()` from `@sanity/ui/theme`
2720
- * instead.
2721
- *
2722
- * @internal
2723
- */
2724
- function createThemeSnippet(options) {
2725
- let minimized = minimizeOptions(options);
2726
- if (minimized === null) return "import {buildTheme} from '@sanity/ui/theme'\n\nexport const theme = buildTheme()\n";
2727
- let lines = [];
2728
- for (let scheme of SCHEMES) {
2729
- let values = minimized[scheme];
2730
- values && lines.push(serializeScheme(scheme, values));
2731
- }
2732
- return `import {buildTheme} from '@sanity/themer'\n\nexport const theme = buildTheme({\n${lines.join("\n")}\n})\n`;
2733
- }
2734
- /** A scheme with up to two options fits on one line, more spread out */
2735
- function serializeScheme(scheme, values) {
2736
- let entries = [];
2737
- return values.accent !== void 0 && entries.push(`accent: '${values.accent}'`), values.text !== void 0 && entries.push(`text: '${values.text}'`), values.background !== void 0 && entries.push(`background: '${values.background}'`), values.contrast !== void 0 && entries.push(`contrast: ${values.contrast}`), entries.length <= 2 ? ` ${scheme}: {${entries.join(", ")}},` : ` ${scheme}: {\n${entries.map((entry) => ` ${entry},`).join("\n")}\n },`;
2738
- }
2739
- registerLanguage(typescript);
2740
- /**
2741
- * Shows the `buildTheme` snippet that makes a theme permanent, in a dialog so
2742
- * that it does not take up sidebar space or make the sidebar scroll as the
2743
- * snippet changes shape with the theme.
2744
- *
2745
- * @internal
2746
- */
2747
- function ThemeSnippetDialog(props) {
2748
- let $ = c(23), { onClose, theme } = props, toast = useToast(), t0;
2749
- $[0] === theme.options ? t0 = $[1] : (t0 = createThemeSnippet(theme.options), $[0] = theme.options, $[1] = t0);
2750
- let snippet = t0, t1;
2751
- $[2] !== snippet || $[3] !== toast ? (t1 = async () => {
2752
- try {
2753
- await navigator.clipboard.writeText(snippet), toast.push({
2754
- status: "success",
2755
- title: "Copied the theme to the clipboard"
2756
- });
2757
- } catch {
2758
- toast.push({
2759
- status: "error",
2760
- title: "Could not copy the theme"
2761
- });
2762
- }
2763
- }, $[2] = snippet, $[3] = toast, $[4] = t1) : t1 = $[4];
2764
- let handleCopy = t1, t2;
2765
- $[5] === handleCopy ? t2 = $[6] : (t2 = /* @__PURE__ */ jsx(Flex, {
2766
- gap: 2,
2767
- justify: "flex-end",
2768
- padding: 3,
2769
- children: /* @__PURE__ */ jsx(Button, {
2770
- gap: 2,
2771
- icon: ClipboardIcon,
2772
- mode: "ghost",
2773
- onClick: () => void handleCopy(),
2774
- padding: 2,
2775
- text: "Copy"
2776
- })
2777
- }), $[5] = handleCopy, $[6] = t2);
2778
- let t3;
2779
- $[7] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx("code", { children: "sanity.config.ts" }), $[7] = t3) : t3 = $[7];
2780
- let t4;
2781
- $[8] === Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx("code", { children: "theme" }), $[8] = t4) : t4 = $[8];
2782
- let t5;
2783
- $[9] === Symbol.for("react.memo_cache_sentinel") ? (t5 = /* @__PURE__ */ jsx("code", { children: "defineConfig" }), $[9] = t5) : t5 = $[9];
2784
- let t6;
2785
- $[10] === theme.title ? t6 = $[11] : (t6 = displayTitle(theme.title), $[10] = theme.title, $[11] = t6);
2786
- let t7;
2787
- $[12] === t6 ? t7 = $[13] : (t7 = /* @__PURE__ */ jsxs(Text, {
2788
- muted: !0,
2789
- size: 1,
2790
- children: [
2791
- "Export this from ",
2792
- t3,
2793
- " and pass it as the ",
2794
- t4,
2795
- " of",
2796
- " ",
2797
- t5,
2798
- " to make “",
2799
- t6,
2800
- "” the Studio theme without this tool."
2801
- ]
2802
- }), $[12] = t6, $[13] = t7);
2803
- let t8;
2804
- $[14] === snippet ? t8 = $[15] : (t8 = /* @__PURE__ */ jsx(Card, {
2805
- border: !0,
2806
- overflow: "auto",
2807
- padding: 3,
2808
- radius: 2,
2809
- tone: "transparent",
2810
- children: /* @__PURE__ */ jsx(Code, {
2811
- language: "ts",
2812
- size: 1,
2813
- children: snippet
2814
- })
2815
- }), $[14] = snippet, $[15] = t8);
2816
- let t9;
2817
- $[16] !== t7 || $[17] !== t8 ? (t9 = /* @__PURE__ */ jsx(Box, {
2818
- padding: 4,
2819
- children: /* @__PURE__ */ jsxs(Stack, {
2820
- gap: 4,
2821
- children: [t7, t8]
2822
- })
2823
- }), $[16] = t7, $[17] = t8, $[18] = t9) : t9 = $[18];
2824
- let t10;
2825
- return $[19] !== onClose || $[20] !== t2 || $[21] !== t9 ? (t10 = /* @__PURE__ */ jsx(Dialog, {
2826
- animate: !0,
2827
- footer: t2,
2828
- header: "Add to your config",
2829
- id: "themer-snippet",
2830
- onClickOutside: onClose,
2831
- onClose,
2832
- width: 1,
2833
- children: t9
2834
- }), $[19] = onClose, $[20] = t2, $[21] = t9, $[22] = t10) : t10 = $[22], t10;
2835
- }
2836
- var title = "c2pqej0";
2837
- const VIEW_TITLES = {
2838
- list: "Themer",
2839
- edit: "Edit theme",
2840
- removed: "Removed themes"
2841
- };
2842
- /**
2843
- * The themer sidebar: a header that navigates between the flows and toggles
2844
- * the split preview, the flow itself — picking a theme, editing one, or
2845
- * restoring removed ones — and the dialog with the `buildTheme` snippet of
2846
- * the applied theme.
2847
- *
2848
- * @internal
2849
- */
2850
- function ThemerSidebar() {
2851
- let $ = c(42), { active, split, view, navbarHeight, send } = useThemer(), [snippetOpen, setSnippetOpen] = useState(!1), inList = view.name === "list", t0;
2852
- $[0] === navbarHeight ? t0 = $[1] : (t0 = navbarHeight === null ? void 0 : { height: navbarHeight }, $[0] = navbarHeight, $[1] = t0);
2853
- let t1;
2854
- $[2] !== inList || $[3] !== send ? (t1 = !inList && /* @__PURE__ */ jsx(TooltipButton, {
2855
- icon: ArrowLeftIcon,
2856
- mode: "bleed",
2857
- onClick: () => send({ type: "flow.list" }),
2858
- padding: 2,
2859
- tooltip: "Back to the themes"
2860
- }), $[2] = inList, $[3] = send, $[4] = t1) : t1 = $[4];
2861
- let t2 = VIEW_TITLES[view.name], t3;
2862
- $[5] === t2 ? t3 = $[6] : (t3 = /* @__PURE__ */ jsx(Box, {
2863
- className: title,
2864
- flex: 1,
2865
- children: /* @__PURE__ */ jsx(Text, {
2866
- size: 1,
2867
- textOverflow: "ellipsis",
2868
- weight: "semibold",
2869
- children: t2
2870
- })
2871
- }), $[5] = t2, $[6] = t3);
2872
- let t4;
2873
- $[7] !== send || $[8] !== split || $[9] !== view.name ? (t4 = view.name !== "removed" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TooltipButton, {
2874
- "aria-pressed": split,
2875
- icon: SplitVerticalIcon,
2876
- mode: "bleed",
2877
- onClick: () => send({ type: "preview.toggle" }),
2878
- padding: 2,
2879
- selected: split,
2880
- tooltip: "Show light and dark side by side"
2881
- }), /* @__PURE__ */ jsx(TooltipButton, {
2882
- icon: CodeBlockIcon,
2883
- mode: "bleed",
2884
- onClick: () => setSnippetOpen(!0),
2885
- padding: 2,
2886
- tooltip: "Show the code for the applied theme"
2887
- })] }), $[7] = send, $[8] = split, $[9] = view.name, $[10] = t4) : t4 = $[10];
2888
- let t5;
2889
- $[11] === send ? t5 = $[12] : (t5 = /* @__PURE__ */ jsx(TooltipButton, {
2890
- icon: CloseIcon,
2891
- mode: "bleed",
2892
- onClick: () => send({ type: "sidebar.close" }),
2893
- padding: 2,
2894
- tooltip: "Close themer"
2895
- }), $[11] = send, $[12] = t5);
2896
- let t6;
2897
- $[13] !== t4 || $[14] !== t5 ? (t6 = /* @__PURE__ */ jsxs(Flex, { children: [t4, t5] }), $[13] = t4, $[14] = t5, $[15] = t6) : t6 = $[15];
2898
- let t7;
2899
- $[16] !== t1 || $[17] !== t3 || $[18] !== t6 ? (t7 = /* @__PURE__ */ jsxs(Flex, {
2900
- align: "center",
2901
- gap: 1,
2902
- height: "fill",
2903
- children: [
2904
- t1,
2905
- t3,
2906
- t6
2907
- ]
2908
- }), $[16] = t1, $[17] = t3, $[18] = t6, $[19] = t7) : t7 = $[19];
2909
- let t8;
2910
- $[20] !== t0 || $[21] !== t7 ? (t8 = /* @__PURE__ */ jsx(Card, {
2911
- borderBottom: !0,
2912
- padding: 3,
2913
- sizing: "border",
2914
- style: t0,
2915
- children: t7
2916
- }), $[20] = t0, $[21] = t7, $[22] = t8) : t8 = $[22];
2917
- let t9;
2918
- $[23] === view.name ? t9 = $[24] : (t9 = view.name === "list" && /* @__PURE__ */ jsx(ThemeList, {}), $[23] = view.name, $[24] = t9);
2919
- let t10;
2920
- $[25] !== view.focusTitle || $[26] !== view.name || $[27] !== view.slug ? (t10 = view.name === "edit" && /* @__PURE__ */ jsx(ThemeEditor, {
2921
- focusTitle: view.focusTitle,
2922
- slug: view.slug
2923
- }), $[25] = view.focusTitle, $[26] = view.name, $[27] = view.slug, $[28] = t10) : t10 = $[28];
2924
- let t11;
2925
- $[29] === view.name ? t11 = $[30] : (t11 = view.name === "removed" && /* @__PURE__ */ jsx(RemovedThemes, {}), $[29] = view.name, $[30] = t11);
2926
- let t12;
2927
- $[31] !== t10 || $[32] !== t11 || $[33] !== t8 || $[34] !== t9 ? (t12 = /* @__PURE__ */ jsxs(Flex, {
2928
- direction: "column",
2929
- height: "fill",
2930
- children: [
2931
- t8,
2932
- t9,
2933
- t10,
2934
- t11
2935
- ]
2936
- }), $[31] = t10, $[32] = t11, $[33] = t8, $[34] = t9, $[35] = t12) : t12 = $[35];
2937
- let t13;
2938
- $[36] !== active || $[37] !== snippetOpen ? (t13 = snippetOpen && /* @__PURE__ */ jsx(ThemeSnippetDialog, {
2939
- onClose: () => setSnippetOpen(!1),
2940
- theme: active
2941
- }), $[36] = active, $[37] = snippetOpen, $[38] = t13) : t13 = $[38];
2942
- let t14;
2943
- return $[39] !== t12 || $[40] !== t13 ? (t14 = /* @__PURE__ */ jsxs(Card, {
2944
- height: "fill",
2945
- children: [t12, t13]
2946
- }), $[39] = t12, $[40] = t13, $[41] = t14) : t14 = $[41], t14;
2947
- }
2948
- var content = "bgn4np4", frame = "bgn4np3", sidebar = "bgn4np2";
2949
- /** How far one arrow key press resizes the sidebar */
2950
- const WIDTH_STORAGE_KEY = "sanityStudio:themer:width";
2951
- function clampWidth(width) {
2952
- return Math.min(400, Math.max(200, Math.round(width)));
2953
- }
2954
- function readStoredWidth() {
2955
- try {
2956
- let stored = Number(localStorage.getItem(WIDTH_STORAGE_KEY));
2957
- return Number.isFinite(stored) && stored > 0 ? clampWidth(stored) : 200;
2958
- } catch {
2959
- return 200;
2960
- }
2961
- }
2962
- function writeStoredWidth(width) {
2963
- try {
2964
- width === 200 ? localStorage.removeItem(WIDTH_STORAGE_KEY) : localStorage.setItem(WIDTH_STORAGE_KEY, String(width));
2965
- } catch {}
2966
- }
2967
- /**
2968
- * The themer sidebar, in a layer along the right edge of the Studio. Its left
2969
- * edge drags (or arrow-keys) it wider, up to twice its default width — the
2970
- * width sticks between sessions. As an overlay (small screens) it covers the
2971
- * Studio edge to edge instead, with nothing to resize.
2972
- *
2973
- * @internal
2974
- */
2975
- function ResizableSidebar(props) {
2976
- let $ = c(24), { overlay } = props, [width, setWidth] = useState(readStoredWidth), [dragging, setDragging] = useState(!1), drag = useRef(null), t0, t1;
2977
- $[0] === width ? (t0 = $[1], t1 = $[2]) : (t0 = () => writeStoredWidth(width), t1 = [width], $[0] = width, $[1] = t0, $[2] = t1), useEffect(t0, t1);
2978
- let t2;
2979
- $[3] === width ? t2 = $[4] : (t2 = (event) => {
2980
- event.button === 0 && (event.preventDefault(), event.currentTarget.setPointerCapture(event.pointerId), drag.current = {
2981
- pointerId: event.pointerId,
2982
- startX: event.clientX,
2983
- startWidth: width
2984
- }, setDragging(!0));
2985
- }, $[3] = width, $[4] = t2);
2986
- let handlePointerDown = t2, t3;
2987
- $[5] === Symbol.for("react.memo_cache_sentinel") ? (t3 = (event_0) => {
2988
- drag.current && drag.current.pointerId === event_0.pointerId && setWidth(clampWidth(drag.current.startWidth - (event_0.clientX - drag.current.startX)));
2989
- }, $[5] = t3) : t3 = $[5];
2990
- let handlePointerMove = t3, t4;
2991
- $[6] === Symbol.for("react.memo_cache_sentinel") ? (t4 = (event_1) => {
2992
- drag.current && drag.current.pointerId === event_1.pointerId && (drag.current = null, setDragging(!1));
2993
- }, $[6] = t4) : t4 = $[6];
2994
- let handlePointerUp = t4, t5;
2995
- $[7] === Symbol.for("react.memo_cache_sentinel") ? (t5 = (event_2) => {
2996
- let step = {
2997
- ArrowLeft: 16,
2998
- ArrowRight: -16,
2999
- Home: 400,
3000
- End: -400
3001
- }[event_2.key];
3002
- step !== void 0 && (event_2.preventDefault(), setWidth((current) => clampWidth(current + step)));
3003
- }, $[7] = t5) : t5 = $[7];
3004
- let handleKeyDown = t5, t6;
3005
- $[8] !== overlay || $[9] !== width ? (t6 = overlay ? void 0 : { width }, $[8] = overlay, $[9] = width, $[10] = t6) : t6 = $[10];
3006
- let t7 = !overlay, t8;
3007
- $[11] !== dragging || $[12] !== handlePointerDown || $[13] !== overlay || $[14] !== width ? (t8 = !overlay && /* @__PURE__ */ jsx("div", {
3008
- "aria-label": "Resize the themer",
3009
- className: "bgn4np5",
3010
- "aria-orientation": "vertical",
3011
- "aria-valuemax": 400,
3012
- "aria-valuemin": 200,
3013
- "aria-valuenow": width,
3014
- "data-dragging": dragging,
3015
- onDoubleClick: () => setWidth(200),
3016
- onKeyDown: handleKeyDown,
3017
- onPointerCancel: handlePointerUp,
3018
- onPointerDown: handlePointerDown,
3019
- onPointerMove: handlePointerMove,
3020
- onPointerUp: handlePointerUp,
3021
- role: "separator",
3022
- tabIndex: 0
3023
- }), $[11] = dragging, $[12] = handlePointerDown, $[13] = overlay, $[14] = width, $[15] = t8) : t8 = $[15];
3024
- let t9;
3025
- $[16] === Symbol.for("react.memo_cache_sentinel") ? (t9 = /* @__PURE__ */ jsx(Box, {
3026
- className: content,
3027
- height: "fill",
3028
- overflow: "hidden",
3029
- children: /* @__PURE__ */ jsx(ThemerSidebar, {})
3030
- }), $[16] = t9) : t9 = $[16];
3031
- let t10;
3032
- $[17] !== t7 || $[18] !== t8 ? (t10 = /* @__PURE__ */ jsxs(Card, {
3033
- borderLeft: t7,
3034
- className: frame,
3035
- height: "fill",
3036
- children: [t8, t9]
3037
- }), $[17] = t7, $[18] = t8, $[19] = t10) : t10 = $[19];
3038
- let t11;
3039
- return $[20] !== overlay || $[21] !== t10 || $[22] !== t6 ? (t11 = /* @__PURE__ */ jsx(Layer, {
3040
- className: sidebar,
3041
- "data-overlay": overlay,
3042
- style: t6,
3043
- zOffset: 100,
3044
- children: t10
3045
- }), $[20] = overlay, $[21] = t10, $[22] = t6, $[23] = t11) : t11 = $[23], t11;
3046
- }
3047
- const QUERY = "(prefers-reduced-motion: reduce)";
3048
- function subscribe(onChange) {
3049
- let media = matchMedia(QUERY);
3050
- return media.addEventListener("change", onChange), () => media.removeEventListener("change", onChange);
3051
- }
3052
- function getSnapshot() {
3053
- return matchMedia(QUERY).matches;
3054
- }
3055
- function getServerSnapshot() {
3056
- return !1;
3057
- }
3058
- /**
3059
- * Whether the user prefers reduced motion — live, so that turning the
3060
- * setting on while the Studio is open takes effect right away.
3061
- *
3062
- * @internal
3063
- */
3064
- function usePrefersReducedMotion() {
3065
- return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
3066
- }
3067
- /**
3068
- * The height of the Studio navbar inside the given element, in pixels — kept
3069
- * up to date as the navbar renders, remounts and resizes, and `null` until
3070
- * there is one. The themer's header takes this height, so that its bottom
3071
- * border lines up with the navbar's whatever height the Studio version, its
3072
- * breakpoint or a custom navbar gives it.
3073
- *
3074
- * @internal
3075
- */
3076
- function useStudioNavbarHeight(ref) {
3077
- let $ = c(3), [height, setHeight] = useState(null), t0, t1;
3078
- return $[0] === ref ? (t0 = $[1], t1 = $[2]) : (t0 = () => {
3079
- let root = ref.current;
3080
- if (!root) return;
3081
- let navbar = null, resizeObserver = new ResizeObserver(() => {
3082
- navbar && setHeight(navbar.getBoundingClientRect().height);
3083
- }), observeNavbar = () => {
3084
- let next = root.querySelector("[data-testid=\"studio-navbar\"]");
3085
- next !== navbar && (navbar && resizeObserver.unobserve(navbar), navbar = next, navbar ? (resizeObserver.observe(navbar), setHeight(navbar.getBoundingClientRect().height)) : setHeight(null));
3086
- }, mutationObserver = new MutationObserver(observeNavbar);
3087
- return mutationObserver.observe(root, {
3088
- childList: !0,
3089
- subtree: !0
3090
- }), observeNavbar(), () => {
3091
- mutationObserver.disconnect(), resizeObserver.disconnect();
3092
- };
3093
- }, t1 = [ref], $[0] = ref, $[1] = t0, $[2] = t1), useEffect(t0, t1), height;
3094
- }
3095
- /**
3096
- * `useDeferredValue` with a transition type: the returned value lags behind
3097
- * `value` like a deferred one — rendered in a transition, which React
3098
- * animates — and the view transition React starts for it carries the given
3099
- * type (`document.startViewTransition({types})`), so a stylesheet tells it
3100
- * from any other transition on the page through
3101
- * `:active-view-transition-type()`.
3102
- *
3103
- * React only takes types from `startTransition`, and a deferred render is
3104
- * not one. So while the deferred value lags, a transition of the type joins
3105
- * it, with a state update of its own for the type to belong to: React renders
3106
- * every pending transition in one go, and the commit — the view transition
3107
- * — carries the types of all of them. The value itself stays with
3108
- * `useDeferredValue`, which reads the latest `value` as the deferred render
3109
- * runs: should `value` change back before that render commits, nothing
3110
- * changes and nothing animates — a queued state update would commit the
3111
- * stale value and animate it, only to animate back.
3112
- *
3113
- * @internal
3114
- */
3115
- function useTypedDeferredValue(value, type) {
3116
- let $ = c(5), deferred = useDeferredValue(value), [, setTransitions] = useState(0), t0, t1;
3117
- return $[0] !== deferred || $[1] !== type || $[2] !== value ? (t0 = () => {
3118
- Object.is(deferred, value) || startTransition(() => {
3119
- addTransitionType(type), setTransitions(_temp$1);
3120
- });
3121
- }, t1 = [
3122
- deferred,
3123
- type,
3124
- value
3125
- ], $[0] = deferred, $[1] = type, $[2] = value, $[3] = t0, $[4] = t1) : (t0 = $[3], t1 = $[4]), useEffect(t0, t1), deferred;
3126
- }
3127
- function _temp$1(count) {
3128
- return count + 1;
3129
- }
3130
- var layout = "_1yq4ar78", layoutTransitionType = "sanity-themer", panelTransitionClasses = {
3131
- slideIn: "_1yq4ar76",
3132
- slideOut: "_1yq4ar77"
3133
- }, splitTransitionClasses = {
3134
- resize: "_1yq4ar70",
3135
- crossfade: "_1yq4ar71",
3136
- slideIn: "_1yq4ar72",
3137
- slideOut: "_1yq4ar73",
3138
- dropIn: "_1yq4ar74",
3139
- dropOut: "_1yq4ar75"
3140
- }, studioScheme = {
3141
- light: "_1yq4ar79",
3142
- dark: "_1yq4ar7a"
3143
- };
3144
- function sameStoredState(a, b) {
3145
- return a.active === b.active && a.custom === b.custom && a.removed === b.removed && a.order === b.order;
728
+ const ResizableSidebar = lazy(() => import("./ResizableSidebar-DD787kQr.js"));
729
+ function sameStoredState(a, b) {
730
+ return a.active === b.active && a.custom === b.custom && a.removed === b.removed && a.order === b.order;
3146
731
  }
3147
732
  function selectView(snapshot) {
3148
733
  let { editing } = snapshot.context;
@@ -3184,7 +769,7 @@ function sameView(a, b) {
3184
769
  * @internal
3185
770
  */
3186
771
  function ThemerLayout(props) {
3187
- let $ = c(85), { baseOptions, ...layoutProps } = props, t0;
772
+ let $ = c(87), { baseOptions, ...layoutProps } = props, t0;
3188
773
  $[0] === baseOptions ? t0 = $[1] : (t0 = () => ({
3189
774
  baseOptions,
3190
775
  stored: readStoredState()
@@ -3199,7 +784,9 @@ function ThemerLayout(props) {
3199
784
  $[8] === snapshot ? t4 = $[9] : (t4 = snapshot.hasTag("moving"), $[8] = snapshot, $[9] = t4);
3200
785
  let moving = t4, t5;
3201
786
  $[10] === snapshot ? t5 = $[11] : (t5 = snapshot.hasTag("switching"), $[10] = snapshot, $[11] = t5);
3202
- let switching = t5, { images } = snapshot.context, mobile = useMediaIndex() <= 1, studioRef = useRef(null), navbarHeight = useStudioNavbarHeight(studioRef), reduceMotion = usePrefersReducedMotion(), deferredOpen = useTypedDeferredValue(reduceMotion ? null : open, layoutTransitionType), deferredSplit = useTypedDeferredValue(reduceMotion ? null : split, layoutTransitionType), shownOpen = deferredOpen ?? open, shownSplit = deferredSplit ?? split, t6;
787
+ let switching = t5, { images } = snapshot.context, mobile = useMediaIndex() <= 1, studioRef = useRef(null), navbarHeight = useStudioNavbarHeight(studioRef), reduceMotion = usePrefersReducedMotion$1(), deferredOpen = useTypedDeferredValue(reduceMotion ? null : open, layoutTransitionType), deferredSplit = useTypedDeferredValue(reduceMotion ? null : split, layoutTransitionType), shownOpen = deferredOpen ?? open, shownSplit = deferredSplit ?? split, [panelShown, setPanelShown] = useState(!1), panelWanted = open || panelShown, panelMounted = useTypedDeferredValue(panelWanted, layoutTransitionType);
788
+ panelMounted && !panelShown && setPanelShown(!0);
789
+ let loading = panelWanted !== panelMounted, t6;
3203
790
  $[12] !== baseOptions || $[13] !== stored ? (t6 = resolveThemes(stored, baseOptions), $[12] = baseOptions, $[13] = stored, $[14] = t6) : t6 = $[14];
3204
791
  let { themes, removed, active } = t6, activeOptions = active.source === "config" ? null : active.options, t7;
3205
792
  $[15] === activeOptions ? t7 = $[16] : (t7 = activeOptions === null ? null : buildTheme(activeOptions), $[15] = activeOptions, $[16] = t7);
@@ -3219,7 +806,7 @@ function ThemerLayout(props) {
3219
806
  for (let url of Object.values(actorRef.getSnapshot().context.images)) URL.revokeObjectURL(url);
3220
807
  }, t11 = [actorRef], $[23] = actorRef, $[24] = t10, $[25] = t11), useEffect(t10, t11);
3221
808
  let t12;
3222
- $[26] !== active || $[27] !== baseOptions || $[28] !== images || $[29] !== mobile || $[30] !== navbarHeight || $[31] !== open || $[32] !== removed || $[33] !== send || $[34] !== split || $[35] !== themes || $[36] !== view ? (t12 = {
809
+ $[26] !== active || $[27] !== baseOptions || $[28] !== images || $[29] !== loading || $[30] !== mobile || $[31] !== navbarHeight || $[32] !== open || $[33] !== removed || $[34] !== send || $[35] !== split || $[36] !== themes || $[37] !== view ? (t12 = {
3223
810
  baseOptions,
3224
811
  themes,
3225
812
  removed,
@@ -3227,15 +814,16 @@ function ThemerLayout(props) {
3227
814
  images,
3228
815
  view,
3229
816
  open,
817
+ loading,
3230
818
  split,
3231
819
  mobile,
3232
820
  navbarHeight,
3233
821
  send
3234
- }, $[26] = active, $[27] = baseOptions, $[28] = images, $[29] = mobile, $[30] = navbarHeight, $[31] = open, $[32] = removed, $[33] = send, $[34] = split, $[35] = themes, $[36] = view, $[37] = t12) : t12 = $[37];
822
+ }, $[26] = active, $[27] = baseOptions, $[28] = images, $[29] = loading, $[30] = mobile, $[31] = navbarHeight, $[32] = open, $[33] = removed, $[34] = send, $[35] = split, $[36] = themes, $[37] = view, $[38] = t12) : t12 = $[38];
3235
823
  let context = t12, studio = layoutProps.renderDefault(layoutProps), t13;
3236
- $[38] === send ? t13 = $[39] : (t13 = () => send({ type: "layout.transitioned" }), $[38] = send, $[39] = t13);
824
+ $[39] === send ? t13 = $[40] : (t13 = () => send({ type: "layout.transitioned" }), $[39] = send, $[40] = t13);
3237
825
  let transitioned = t13, t14;
3238
- $[40] !== mobile || $[41] !== moving || $[42] !== reduceMotion || $[43] !== switching ? (t14 = reduceMotion ? {
826
+ $[41] !== mobile || $[42] !== moving || $[43] !== reduceMotion || $[44] !== switching ? (t14 = reduceMotion ? {
3239
827
  copyEnter: "none",
3240
828
  copyExit: "none",
3241
829
  panelEnter: "none",
@@ -3249,9 +837,9 @@ function ThemerLayout(props) {
3249
837
  panelExit: panelTransitionClasses.slideOut,
3250
838
  update: switching ? splitTransitionClasses.crossfade : "none",
3251
839
  studioUpdate: moving ? splitTransitionClasses.resize : switching ? splitTransitionClasses.crossfade : "none"
3252
- }, $[40] = mobile, $[41] = moving, $[42] = reduceMotion, $[43] = switching, $[44] = t14) : t14 = $[44];
840
+ }, $[41] = mobile, $[42] = moving, $[43] = reduceMotion, $[44] = switching, $[45] = t14) : t14 = $[45];
3253
841
  let classes = t14, t15 = mobile ? "column" : "row", t16;
3254
- $[45] !== classes.copyEnter || $[46] !== classes.copyExit || $[47] !== classes.update || $[48] !== mobile || $[49] !== oppositeScheme || $[50] !== shownOpen || $[51] !== shownSplit || $[52] !== shownTheme || $[53] !== studio || $[54] !== transitioned ? (t16 = shownOpen && /* @__PURE__ */ jsx(Activity, {
842
+ $[46] !== classes.copyEnter || $[47] !== classes.copyExit || $[48] !== classes.update || $[49] !== mobile || $[50] !== oppositeScheme || $[51] !== shownOpen || $[52] !== shownSplit || $[53] !== shownTheme || $[54] !== studio || $[55] !== transitioned ? (t16 = shownOpen && /* @__PURE__ */ jsx(Activity, {
3255
843
  mode: shownSplit ? "visible" : "hidden",
3256
844
  children: /* @__PURE__ */ jsx(ViewTransition, {
3257
845
  enter: classes.copyEnter,
@@ -3268,28 +856,28 @@ function ThemerLayout(props) {
3268
856
  children: studio
3269
857
  })
3270
858
  }, "opposite")
3271
- }), $[45] = classes.copyEnter, $[46] = classes.copyExit, $[47] = classes.update, $[48] = mobile, $[49] = oppositeScheme, $[50] = shownOpen, $[51] = shownSplit, $[52] = shownTheme, $[53] = studio, $[54] = transitioned, $[55] = t16) : t16 = $[55];
859
+ }), $[46] = classes.copyEnter, $[47] = classes.copyExit, $[48] = classes.update, $[49] = mobile, $[50] = oppositeScheme, $[51] = shownOpen, $[52] = shownSplit, $[53] = shownTheme, $[54] = studio, $[55] = transitioned, $[56] = t16) : t16 = $[56];
3272
860
  let t17;
3273
- $[56] !== shownTheme || $[57] !== studio ? (t17 = /* @__PURE__ */ jsx(StudioPreview, {
861
+ $[57] !== shownTheme || $[58] !== studio ? (t17 = /* @__PURE__ */ jsx(StudioPreview, {
3274
862
  ref: studioRef,
3275
863
  theme: shownTheme,
3276
864
  children: studio
3277
- }), $[56] = shownTheme, $[57] = studio, $[58] = t17) : t17 = $[58];
865
+ }), $[57] = shownTheme, $[58] = studio, $[59] = t17) : t17 = $[59];
3278
866
  let t18;
3279
- $[59] !== classes.studioUpdate || $[60] !== t17 || $[61] !== transitioned ? (t18 = /* @__PURE__ */ jsx(ViewTransition, {
867
+ $[60] !== classes.studioUpdate || $[61] !== t17 || $[62] !== transitioned ? (t18 = /* @__PURE__ */ jsx(ViewTransition, {
3280
868
  onUpdate: transitioned,
3281
869
  update: classes.studioUpdate,
3282
870
  children: t17
3283
- }, "primary"), $[59] = classes.studioUpdate, $[60] = t17, $[61] = transitioned, $[62] = t18) : t18 = $[62];
871
+ }, "primary"), $[60] = classes.studioUpdate, $[61] = t17, $[62] = transitioned, $[63] = t18) : t18 = $[63];
3284
872
  let t19 = shownOpen ? "visible" : "hidden", t20 = shownTheme ?? void 0, t21;
3285
- $[63] === mobile ? t21 = $[64] : (t21 = /* @__PURE__ */ jsx(ResizableSidebar, { overlay: mobile }), $[63] = mobile, $[64] = t21);
873
+ $[64] !== mobile || $[65] !== panelMounted ? (t21 = panelMounted && /* @__PURE__ */ jsx(ResizableSidebar, { overlay: mobile }), $[64] = mobile, $[65] = panelMounted, $[66] = t21) : t21 = $[66];
3286
874
  let t22;
3287
- $[65] !== t20 || $[66] !== t21 ? (t22 = /* @__PURE__ */ jsx(ThemeProvider, {
875
+ $[67] !== t20 || $[68] !== t21 ? (t22 = /* @__PURE__ */ jsx(ThemeProvider, {
3288
876
  theme: t20,
3289
877
  children: t21
3290
- }), $[65] = t20, $[66] = t21, $[67] = t22) : t22 = $[67];
878
+ }), $[67] = t20, $[68] = t21, $[69] = t22) : t22 = $[69];
3291
879
  let t23;
3292
- $[68] !== classes.panelEnter || $[69] !== classes.panelExit || $[70] !== classes.update || $[71] !== t22 || $[72] !== transitioned ? (t23 = /* @__PURE__ */ jsx(ViewTransition, {
880
+ $[70] !== classes.panelEnter || $[71] !== classes.panelExit || $[72] !== classes.update || $[73] !== t22 || $[74] !== transitioned ? (t23 = /* @__PURE__ */ jsx(ViewTransition, {
3293
881
  enter: classes.panelEnter,
3294
882
  exit: classes.panelExit,
3295
883
  onEnter: transitioned,
@@ -3297,14 +885,14 @@ function ThemerLayout(props) {
3297
885
  onUpdate: transitioned,
3298
886
  update: classes.update,
3299
887
  children: t22
3300
- }, "panel"), $[68] = classes.panelEnter, $[69] = classes.panelExit, $[70] = classes.update, $[71] = t22, $[72] = transitioned, $[73] = t23) : t23 = $[73];
888
+ }, "panel"), $[70] = classes.panelEnter, $[71] = classes.panelExit, $[72] = classes.update, $[73] = t22, $[74] = transitioned, $[75] = t23) : t23 = $[75];
3301
889
  let t24;
3302
- $[74] !== t19 || $[75] !== t23 ? (t24 = /* @__PURE__ */ jsx(Activity, {
890
+ $[76] !== t19 || $[77] !== t23 ? (t24 = /* @__PURE__ */ jsx(Activity, {
3303
891
  mode: t19,
3304
892
  children: t23
3305
- }), $[74] = t19, $[75] = t23, $[76] = t24) : t24 = $[76];
893
+ }), $[76] = t19, $[77] = t23, $[78] = t24) : t24 = $[78];
3306
894
  let t25;
3307
- $[77] !== t15 || $[78] !== t16 || $[79] !== t18 || $[80] !== t24 ? (t25 = /* @__PURE__ */ jsxs(Flex, {
895
+ $[79] !== t15 || $[80] !== t16 || $[81] !== t18 || $[82] !== t24 ? (t25 = /* @__PURE__ */ jsxs(Flex, {
3308
896
  className: layout,
3309
897
  direction: t15,
3310
898
  height: "fill",
@@ -3314,12 +902,12 @@ function ThemerLayout(props) {
3314
902
  t18,
3315
903
  t24
3316
904
  ]
3317
- }), $[77] = t15, $[78] = t16, $[79] = t18, $[80] = t24, $[81] = t25) : t25 = $[81];
905
+ }), $[79] = t15, $[80] = t16, $[81] = t18, $[82] = t24, $[83] = t25) : t25 = $[83];
3318
906
  let t26;
3319
- return $[82] !== context || $[83] !== t25 ? (t26 = /* @__PURE__ */ jsx(ThemerContext.Provider, {
907
+ return $[84] !== context || $[85] !== t25 ? (t26 = /* @__PURE__ */ jsx(ThemerContext.Provider, {
3320
908
  value: context,
3321
909
  children: t25
3322
- }), $[82] = context, $[83] = t25, $[84] = t26) : t26 = $[84], t26;
910
+ }), $[84] = context, $[85] = t25, $[86] = t26) : t26 = $[86], t26;
3323
911
  }
3324
912
  /**
3325
913
  * One copy of the Studio, in the given color scheme — or, without one, in the
@@ -3351,28 +939,235 @@ function StudioPreview(props) {
3351
939
  children: t2
3352
940
  }), $[6] = scheme, $[7] = t0, $[8] = t2, $[9] = t3) : t3 = $[9], t3;
3353
941
  }
942
+ /**
943
+ * The colors the slices fill with, clockwise from 12 o'clock: the palette's
944
+ * hues in rainbow order, like a real color swatch
945
+ */
946
+ const SLICE_COLORS = [
947
+ red[500].hex,
948
+ orange[500].hex,
949
+ yellow[400].hex,
950
+ green[500].hex,
951
+ cyan[500].hex,
952
+ blue[500].hex,
953
+ purple[500].hex,
954
+ magenta[500].hex
955
+ ], TOTAL_ACTS = 2 + (1 - Math.cbrt(1 / 8)), ANIMATION_DURATION = .55 * TOTAL_ACTS;
956
+ function actAt(progress) {
957
+ return progress * TOTAL_ACTS;
958
+ }
959
+ /**
960
+ * How far the wheel has turned at `progress` (0–1), in degrees.
961
+ *
962
+ * It stands still while the top half of the slices pop in. As the 6 o'clock
963
+ * slice pops in it starts spinning — one full turn with a cubic ease-in, from
964
+ * rest to top speed as the bottom half pop in — and it stands still again the
965
+ * moment it is back upright: the clearing of {@link isSliceFilled} takes off
966
+ * at the speed the spin reached, as if the wheel kept turning and eased out
967
+ * over a second turn, so the two read as one movement.
968
+ */
969
+ function wheelRotation(progress) {
970
+ let spin = actAt(progress) - 1;
971
+ return spin <= 0 || spin >= 1 ? 0 : 360 * spin ** 3;
972
+ }
973
+ /**
974
+ * Whether slice `index` (clockwise from 12 o'clock) shows its color at
975
+ * `progress` (0–1). A slice pops in whole as the fill reaches its leading
976
+ * edge, and pops out whole as the clearing does — stepped, never partial.
977
+ *
978
+ * The fill goes clockwise at a steady pace over the first two acts, half of
979
+ * the slices in each. The clearing follows in the third act, the same way
980
+ * round, on a cubic ease-out that starts at the speed the spin of
981
+ * {@link wheelRotation} ends with: the slices at first pop out as quickly as
982
+ * they were passing by, then ever more slowly.
983
+ */
984
+ function isSliceFilled(progress, index) {
985
+ let act = actAt(progress);
986
+ if (act * 8 / 2 <= index) return !1;
987
+ let clearing = act - 2;
988
+ return clearing <= 0 || (1 - (1 - clearing) ** 3) * 8 < index;
989
+ }
990
+ /**
991
+ * The geometry of `ColorWheelIcon` from `@sanity/icons`: a ring between two
992
+ * circles around the center of a 25×25 viewBox, cut into eight slices by the
993
+ * spokes of its outline path
994
+ */
995
+ const CENTER = 12.5, INNER_RADIUS = 3.5;
996
+ /** A point on a circle around the center, at `degrees` clockwise from 3 o'clock */
997
+ function point(radius, degrees) {
998
+ let radians = degrees * Math.PI / 180;
999
+ return `${CENTER + radius * Math.cos(radians)} ${CENTER + radius * Math.sin(radians)}`;
1000
+ }
1001
+ /**
1002
+ * The part of the ring a slice colors in: between its two spokes, from the
1003
+ * inner circle out to the outer. The 12 o'clock slice is centered on 12, so
1004
+ * it starts half a slice before it.
1005
+ */
1006
+ function slicePath(index) {
1007
+ let start = index * 45 - 45 / 2 - 90, end = start + 45;
1008
+ return [
1009
+ `M${point(8, start)}`,
1010
+ `A8 8 0 0 1 ${point(8, end)}`,
1011
+ `L${point(INNER_RADIUS, end)}`,
1012
+ `A${INNER_RADIUS} ${INNER_RADIUS} 0 0 0 ${point(INNER_RADIUS, start)}`,
1013
+ "Z"
1014
+ ].join("");
1015
+ }
1016
+ const SLICE_PATHS = SLICE_COLORS.map((_, index) => slicePath(index));
1017
+ /**
1018
+ * A filled shape, not a stroked one: the icon styles of `@sanity/ui` force
1019
+ * `vector-effect: non-scaling-stroke` on every path in an icon, which fills
1020
+ * are none the wiser about
1021
+ */
1022
+ function Slice(props) {
1023
+ let $ = c(8), { index, progress } = props, t0;
1024
+ $[0] === index ? t0 = $[1] : (t0 = (value) => +!!isSliceFilled(value, index), $[0] = index, $[1] = t0);
1025
+ let opacity = useTransform(progress, t0), t1 = SLICE_PATHS[index], t2 = SLICE_COLORS[index], t3;
1026
+ $[2] === opacity ? t3 = $[3] : (t3 = { opacity }, $[2] = opacity, $[3] = t3);
1027
+ let t4;
1028
+ return $[4] !== t1 || $[5] !== t2 || $[6] !== t3 ? (t4 = /* @__PURE__ */ jsx(motion.path, {
1029
+ d: t1,
1030
+ fill: t2,
1031
+ style: t3
1032
+ }), $[4] = t1, $[5] = t2, $[6] = t3, $[7] = t4) : t4 = $[7], t4;
1033
+ }
1034
+ /**
1035
+ * The `ColorWheelIcon` of `@sanity/icons`, with an animation driven by
1036
+ * `progress` (0–1): the slices pop in with color one at a time, clockwise
1037
+ * from 12 o'clock like the segments of a classic spinner, the wheel spins once
1038
+ * as the 6 o'clock slice pops in, and the colors pop out the same way round,
1039
+ * carrying the spin's speed to a rest (see `colorWheel.ts`). At rest — at
1040
+ * either end — it looks exactly like the icon it stands in for. With
1041
+ * `prefers-reduced-motion: reduce` the wheel keeps still; only the colors come
1042
+ * and go.
1043
+ *
1044
+ * @internal
1045
+ */
1046
+ function AnimatedColorWheelIcon(props) {
1047
+ let $ = c(10), { progress } = props, prefersReducedMotion = usePrefersReducedMotion(), t0;
1048
+ $[0] === prefersReducedMotion ? t0 = $[1] : (t0 = (value) => prefersReducedMotion ? 0 : wheelRotation(value), $[0] = prefersReducedMotion, $[1] = t0);
1049
+ let rotate = useTransform(progress, t0), t1;
1050
+ $[2] === rotate ? t1 = $[3] : (t1 = { rotate }, $[2] = rotate, $[3] = t1);
1051
+ let t2;
1052
+ $[4] === progress ? t2 = $[5] : (t2 = SLICE_COLORS.map((color, index) => /* @__PURE__ */ jsx(Slice, {
1053
+ index,
1054
+ progress
1055
+ }, color)), $[4] = progress, $[5] = t2);
1056
+ let t3;
1057
+ $[6] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx("path", {
1058
+ d: "M9.43853 5.10896L11.1606 9.26642M13.8394 15.7336L15.5615 19.891M15.7336 11.1606L19.891 9.43853M9.26642 13.8394L5.10896 15.5615M5.3139 9.52342L9.23359 11.147M15.7664 13.853L19.6861 15.4766M13.853 9.23359L15.4766 5.3139M9.52342 19.6861L11.147 15.7664M20.5 12.5C20.5 16.9183 16.9183 20.5 12.5 20.5C8.08172 20.5 4.5 16.9183 4.5 12.5C4.5 8.08172 8.08172 4.5 12.5 4.5C16.9183 4.5 20.5 8.08172 20.5 12.5ZM16 12.5C16 14.433 14.433 16 12.5 16C10.567 16 9 14.433 9 12.5C9 10.567 10.567 9 12.5 9C14.433 9 16 10.567 16 12.5Z",
1059
+ stroke: "currentColor",
1060
+ strokeLinejoin: "round",
1061
+ strokeWidth: 1.2
1062
+ }), $[6] = t3) : t3 = $[6];
1063
+ let t4;
1064
+ return $[7] !== t1 || $[8] !== t2 ? (t4 = /* @__PURE__ */ jsxs(motion.svg, {
1065
+ "data-sanity-icon": "color-wheel",
1066
+ fill: "none",
1067
+ height: "1em",
1068
+ style: t1,
1069
+ viewBox: "0 0 25 25",
1070
+ width: "1em",
1071
+ xmlns: "http://www.w3.org/2000/svg",
1072
+ children: [t2, t3]
1073
+ }), $[7] = t1, $[8] = t2, $[9] = t4) : t4 = $[9], t4;
1074
+ }
1075
+ /**
1076
+ * Starts loading the sidebar's code as the toggle is about to be pressed —
1077
+ * the same import `React.lazy` makes, which then waits for this one
1078
+ */
1079
+ function preloadSidebar() {
1080
+ import("./ResizableSidebar-DD787kQr.js").catch(() => {});
1081
+ }
1082
+ /**
1083
+ * Opens or closes the sidebar — and notes the visit, which is what stops the
1084
+ * navbar from introducing the tool
1085
+ */
1086
+ function toggleSidebar(send) {
1087
+ markVisited(), send({ type: "sidebar.toggle" });
1088
+ }
1089
+ /**
1090
+ * Whether the navbar has had its one chance per page load to introduce the
1091
+ * tool. Module state rather than component state on purpose: the button
1092
+ * remounts with the navbar (switching workspaces, crossing the narrow-screen
1093
+ * breakpoint) and renders once per Studio copy in the split preview, and none
1094
+ * of those should earn another introduction — only loading the page again does.
1095
+ */
1096
+ let introduced = !1;
1097
+ /**
1098
+ * The topbar toggle, with an icon that plays its color wheel animation: on
1099
+ * hovering the button, every time; on hovering the navbar it sits in, once —
1100
+ * a "hey, look, new tool!" for anyone who has never opened the sidebar, which
1101
+ * the visit noted by {@link toggleSidebar} ends for good; and lap after lap
1102
+ * while the sidebar's code loads, the way a spinner would.
1103
+ */
3354
1104
  function ThemerNavbarButton() {
3355
- let $ = c(6), { open, send } = useThemer(), t0;
3356
- $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = /* @__PURE__ */ jsx(Text, {
1105
+ let $ = c(26), { open, loading, send } = useThemer(), progress = useMotionValue(0), [button, setButton] = useState(null), t0;
1106
+ $[0] === progress ? t0 = $[1] : (t0 = () => {
1107
+ progress.isAnimating() || animate(progress, [0, 1], {
1108
+ duration: ANIMATION_DURATION,
1109
+ ease: "linear"
1110
+ });
1111
+ }, $[0] = progress, $[1] = t0);
1112
+ let play = t0, t1, t2;
1113
+ $[2] !== loading || $[3] !== progress ? (t1 = () => {
1114
+ if (!loading) return;
1115
+ let looping = !0, lap = (from) => {
1116
+ animate(progress, [from, 1], {
1117
+ duration: (1 - from) * ANIMATION_DURATION,
1118
+ ease: "linear",
1119
+ onComplete: () => {
1120
+ looping && lap(0);
1121
+ }
1122
+ });
1123
+ };
1124
+ return lap(progress.get()), () => {
1125
+ looping = !1;
1126
+ };
1127
+ }, t2 = [loading, progress], $[2] = loading, $[3] = progress, $[4] = t1, $[5] = t2) : (t1 = $[4], t2 = $[5]), useEffect(t1, t2);
1128
+ let t3, t4;
1129
+ $[6] === progress ? (t3 = $[7], t4 = $[8]) : (t3 = () => () => progress.stop(), t4 = [progress], $[6] = progress, $[7] = t3, $[8] = t4), useEffect(t3, t4);
1130
+ let t5, t6;
1131
+ $[9] !== button || $[10] !== play ? (t5 = () => {
1132
+ let navbar = button?.closest(NAVBAR_SELECTOR);
1133
+ if (!navbar || introduced) return;
1134
+ let introduce = () => {
1135
+ introduced || (introduced = !0, hasVisited() || play());
1136
+ };
1137
+ return navbar.addEventListener("mouseenter", introduce, { once: !0 }), () => navbar.removeEventListener("mouseenter", introduce);
1138
+ }, t6 = [button, play], $[9] = button, $[10] = play, $[11] = t5, $[12] = t6) : (t5 = $[11], t6 = $[12]), useEffect(t5, t6);
1139
+ let t7;
1140
+ $[13] === Symbol.for("react.memo_cache_sentinel") ? (t7 = /* @__PURE__ */ jsx(Text, {
3357
1141
  size: 1,
3358
1142
  children: "Themer"
3359
- }), $[0] = t0) : t0 = $[0];
3360
- let t1;
3361
- $[1] === send ? t1 = $[2] : (t1 = () => send({ type: "sidebar.toggle" }), $[1] = send, $[2] = t1);
3362
- let t2;
3363
- return $[3] !== open || $[4] !== t1 ? (t2 = /* @__PURE__ */ jsx(Tooltip, {
1143
+ }), $[13] = t7) : t7 = $[13];
1144
+ let t8;
1145
+ $[14] === progress ? t8 = $[15] : (t8 = /* @__PURE__ */ jsx(AnimatedColorWheelIcon, { progress }), $[14] = progress, $[15] = t8);
1146
+ let t9;
1147
+ $[16] === send ? t9 = $[17] : (t9 = () => toggleSidebar(send), $[16] = send, $[17] = t9);
1148
+ let t10;
1149
+ $[18] === play ? t10 = $[19] : (t10 = () => {
1150
+ preloadSidebar(), play();
1151
+ }, $[18] = play, $[19] = t10);
1152
+ let t11;
1153
+ return $[20] !== loading || $[21] !== open || $[22] !== t10 || $[23] !== t8 || $[24] !== t9 ? (t11 = /* @__PURE__ */ jsx(Tooltip, {
3364
1154
  animate: !0,
3365
- content: t0,
1155
+ content: t7,
3366
1156
  portal: !0,
3367
1157
  children: /* @__PURE__ */ jsx(Button, {
1158
+ "aria-busy": loading,
3368
1159
  "aria-label": "Themer",
3369
- icon: ColorWheelIcon,
1160
+ disabled: loading,
1161
+ icon: t8,
3370
1162
  mode: "bleed",
3371
- onClick: t1,
1163
+ onClick: t9,
1164
+ onFocus: preloadSidebar,
1165
+ onMouseEnter: t10,
3372
1166
  padding: 2,
1167
+ ref: setButton,
3373
1168
  selected: open
3374
1169
  })
3375
- }), $[3] = open, $[4] = t1, $[5] = t2) : t2 = $[5], t2;
1170
+ }), $[20] = loading, $[21] = open, $[22] = t10, $[23] = t8, $[24] = t9, $[25] = t11) : t11 = $[25], t11;
3376
1171
  }
3377
1172
  /**
3378
1173
  * Adds the toggle that opens and closes the themer sidebar to the Studio
@@ -3390,14 +1185,14 @@ function ThemerNavbar(props) {
3390
1185
  let t1;
3391
1186
  if ($[2] !== open || $[3] !== props || $[4] !== send || $[5] !== t0) {
3392
1187
  let t2;
3393
- $[7] === send ? t2 = $[8] : (t2 = () => send({ type: "sidebar.toggle" }), $[7] = send, $[8] = t2), t1 = props.renderDefault({
1188
+ $[7] === send ? t2 = $[8] : (t2 = () => toggleSidebar(send), $[7] = send, $[8] = t2), t1 = props.renderDefault({
3394
1189
  ...props,
3395
1190
  __internal_actions: [
3396
1191
  ...t0,
3397
1192
  {
3398
1193
  location: "topbar",
3399
1194
  name: "themer-topbar",
3400
- render: _temp
1195
+ render: ThemerNavbarButton
3401
1196
  },
3402
1197
  {
3403
1198
  icon: ColorWheelIcon,
@@ -3412,9 +1207,6 @@ function ThemerNavbar(props) {
3412
1207
  } else t1 = $[6];
3413
1208
  return t1;
3414
1209
  }
3415
- function _temp() {
3416
- return /* @__PURE__ */ jsx(ThemerNavbarButton, {});
3417
- }
3418
1210
  /**
3419
1211
  * A Studio plugin that adds a themer sidebar for `buildTheme` themes: a
3420
1212
  * navbar toggle opens the sidebar next to the Studio, with a list of themes —