@sanity/themer 0.3.15 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tool.js CHANGED
@@ -1,21 +1,32 @@
1
- import { a as DEFAULT_BACKGROUND_DARK, c as resolveThemeOptions, i as DEFAULT_ACCENT, n as buildTheme, o as DEFAULT_BACKGROUND_LIGHT, r as buildPalette, s as deriveTextColor, t as presets } from "./presets-DWzYcsLg.js";
2
- import { t as isColor } from "./mix-BfV6_Ke4.js";
3
- import { COLOR_TINTS } from "@sanity/color";
1
+ 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";
4
2
  import { c } from "react-compiler-runtime";
5
- import { definePlugin } from "sanity";
6
- import { Box, Button, Card, Flex, Grid, Layer, Stack, Text, ThemeProvider } from "@sanity/ui";
7
- import { createContext, useContext, useEffect, useState } from "react";
8
- import { ClipboardIcon } from "@sanity/icons/Clipboard";
3
+ import { definePlugin, useColorSchemeValue } from "sanity";
4
+ import { Badge, Box, Button, Card, Dialog, Flex, Layer, Stack, Text, TextInput, ThemeProvider, useTheme_v2 } from "@sanity/ui";
5
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
6
+ import { styled } from "styled-components";
7
+ import { ArrowLeftIcon } from "@sanity/icons/ArrowLeft";
9
8
  import { CloseIcon } from "@sanity/icons/Close";
9
+ import { CodeBlockIcon } from "@sanity/icons/CodeBlock";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
11
  import { ResetIcon } from "@sanity/icons/Reset";
11
- import { Code } from "@sanity/ui/code";
12
+ import { TrashIcon } from "@sanity/icons/Trash";
13
+ import { SparklesIcon } from "@sanity/icons/Sparkles";
14
+ import { Tooltip } from "@sanity/ui/tooltip";
15
+ import { ImageIcon } from "@sanity/icons/Image";
12
16
  import { useToast } from "@sanity/ui/toast";
17
+ import { AddIcon } from "@sanity/icons/Add";
18
+ import { RestoreIcon } from "@sanity/icons/Restore";
19
+ import { CopyIcon } from "@sanity/icons/Copy";
20
+ import { EditIcon } from "@sanity/icons/Edit";
21
+ import { EllipsisHorizontalIcon } from "@sanity/icons/EllipsisHorizontal";
22
+ import { Menu, MenuButton, MenuDivider, MenuItem } from "@sanity/ui/menu";
23
+ import { ClipboardIcon } from "@sanity/icons/Clipboard";
24
+ import { Code } from "@sanity/ui/code";
13
25
  import { registerLanguage } from "react-refractor";
14
26
  import typescript from "refractor/typescript";
15
- import { styled } from "styled-components";
16
- import { jsx, jsxs } from "react/jsx-runtime";
27
+ import { useActor, useSelector } from "@xstate/react";
28
+ import { assign, not, setup } from "xstate";
17
29
  import { ColorWheelIcon } from "@sanity/icons/ColorWheel";
18
- import { Tooltip } from "@sanity/ui/tooltip";
19
30
  /** @internal */
20
31
  const ThemerContext = createContext(null);
21
32
  /** @internal */
@@ -24,20 +35,58 @@ function useThemer() {
24
35
  if (!context) throw Error("useThemer must be used within the `themerTool` plugin");
25
36
  return context;
26
37
  }
38
+ const Root$2 = styled(Box).withConfig({
39
+ displayName: "Root",
40
+ componentId: "sc-6zqwg0-0"
41
+ })`scrollbar-gutter:stable;`;
42
+ /**
43
+ * The scrolling body of a sidebar flow. It always reserves the scrollbar's
44
+ * space, so the content does not shift when the list grows or shrinks past
45
+ * the point where it needs to scroll — and it takes that space out of its
46
+ * right padding, so the content stays centered rather than hugging the left.
47
+ * With overlay scrollbars there is no gutter and the padding stays even.
48
+ *
49
+ * @internal
50
+ */
51
+ function ScrollArea(props) {
52
+ let $ = c(8), { children, padding } = props, { space } = useTheme_v2(), ref = useRef(null), [gutter, setGutter] = useState(0), t0, t1;
53
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = () => {
54
+ let element = ref.current;
55
+ if (!element) return;
56
+ let measure = () => setGutter(element.offsetWidth - element.clientWidth);
57
+ measure();
58
+ let observer = new ResizeObserver(measure);
59
+ return observer.observe(element), () => observer.disconnect();
60
+ }, t1 = [], $[0] = t0, $[1] = t1) : (t0 = $[0], t1 = $[1]), useEffect(t0, t1);
61
+ let t2 = Math.max(0, space[padding] - gutter), t3;
62
+ $[2] === t2 ? t3 = $[3] : (t3 = { paddingRight: t2 }, $[2] = t2, $[3] = t3);
63
+ let t4;
64
+ return $[4] !== children || $[5] !== padding || $[6] !== t3 ? (t4 = /* @__PURE__ */ jsx(Root$2, {
65
+ flex: 1,
66
+ overflow: "auto",
67
+ padding,
68
+ ref,
69
+ style: t3,
70
+ children
71
+ }), $[4] = children, $[5] = padding, $[6] = t3, $[7] = t4) : t4 = $[7], t4;
72
+ }
27
73
  /**
28
74
  * Reduces theme options to the minimal object that recreates them through
29
- * `buildTheme`: the text color is dropped when it matches the one derived
30
- * from the accent, and backgrounds and contrast are dropped when they match
31
- * the defaults. Options that boil down to the stock Studio theme reduce to
32
- * `null` — they need nothing from this package.
75
+ * `buildTheme`: in each scheme the accent is dropped when it is the stock
76
+ * one, the text color when it matches the one derived from the accent, and
77
+ * the background and contrast when they match the defaults — and a scheme
78
+ * with nothing left is dropped altogether. Options that boil down to the
79
+ * stock Studio theme reduce to `null` — they need nothing from this package.
33
80
  *
34
81
  * @internal
35
82
  */
36
83
  function minimizeOptions(options) {
37
- let resolved = resolveThemeOptions(options), minimized = { accent: resolved.accent };
38
- resolved.text !== deriveTextColor(resolved.accent) && (minimized.text = resolved.text);
39
- let dark = resolved.background.dark === DEFAULT_BACKGROUND_DARK ? null : resolved.background.dark, light = resolved.background.light === DEFAULT_BACKGROUND_LIGHT ? null : resolved.background.light;
40
- return (dark !== null || light !== null) && (minimized.background = {}, dark !== null && (minimized.background.dark = dark), light !== null && (minimized.background.light = light)), resolved.contrast !== 85 && (minimized.contrast = resolved.contrast), minimized.accent === DEFAULT_ACCENT && Object.keys(minimized).length === 1 ? null : minimized;
84
+ let resolved = resolveThemeOptions(options), minimized = {};
85
+ for (let scheme of SCHEMES) {
86
+ let values = resolved[scheme], kept = {};
87
+ 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);
88
+ }
89
+ return Object.keys(minimized).length === 0 ? null : minimized;
41
90
  }
42
91
  /**
43
92
  * Whether two sets of theme options generate the same theme, comparing their
@@ -48,11 +97,1629 @@ function minimizeOptions(options) {
48
97
  */
49
98
  function sameOptions(a, b) {
50
99
  let left = resolveThemeOptions(a), right = resolveThemeOptions(b);
51
- return left.accent === right.accent && left.text === right.text && left.background.dark === right.background.dark && left.background.light === right.background.light && left.contrast === right.contrast;
100
+ return SCHEMES.every((scheme) => sameScheme(left[scheme], right[scheme]));
101
+ }
102
+ function sameScheme(a, b) {
103
+ return a.accent === b.accent && a.text === b.text && a.background === b.background && a.contrast === b.contrast;
104
+ }
105
+ /** The slug of the theme the Studio config was generated from @internal */
106
+ const initialThemerState = {
107
+ active: null,
108
+ custom: [],
109
+ removed: []
110
+ };
111
+ /**
112
+ * Resolves the list of themes: the configured theme first, then the presets
113
+ * (minus the ones that would only repeat the configured theme), then the
114
+ * user's own themes. Removed themes are set aside so they can be restored,
115
+ * and the active theme falls back to the configured one when its slug no
116
+ * longer resolves.
117
+ *
118
+ * @internal
119
+ */
120
+ function resolveThemes(state, baseOptions) {
121
+ let config = {
122
+ slug: "config",
123
+ title: "Studio config",
124
+ options: baseOptions,
125
+ source: "config"
126
+ }, all = [config];
127
+ for (let preset of presets) sameOptions(preset.options, baseOptions) || all.push({
128
+ slug: preset.slug,
129
+ title: preset.title,
130
+ options: preset.options,
131
+ source: "preset"
132
+ });
133
+ for (let theme of state.custom) all.push({
134
+ slug: theme.slug,
135
+ title: theme.title,
136
+ options: theme.options,
137
+ source: "custom",
138
+ ...theme.palette ? { palette: theme.palette } : {}
139
+ });
140
+ let removedSlugs = new Set(state.removed), themes = all.filter((theme) => !removedSlugs.has(theme.slug));
141
+ return {
142
+ themes,
143
+ removed: all.filter((theme) => removedSlugs.has(theme.slug)),
144
+ active: themes.find((theme) => theme.slug === state.active) ?? config
145
+ };
146
+ }
147
+ /**
148
+ * Creates a custom theme with a slug that cannot collide with the presets or
149
+ * the configured theme.
150
+ *
151
+ * @internal
152
+ */
153
+ function createCustomTheme(title, options, palette) {
154
+ return {
155
+ slug: `custom-${randomId()}`,
156
+ title,
157
+ options,
158
+ ...palette ? { palette } : {}
159
+ };
160
+ }
161
+ /** The title of a duplicated theme @internal */
162
+ function duplicateTitle(title) {
163
+ return `${displayTitle(title)} copy`;
164
+ }
165
+ /** The title to show for a theme, falling back when the user cleared it @internal */
166
+ function displayTitle(title) {
167
+ return title.trim() || "Untitled theme";
168
+ }
169
+ function randomId() {
170
+ return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
171
+ }
172
+ /**
173
+ * The dark scheme covers the right side of the preview behind a diagonal cut,
174
+ * like the "Auto" appearance preview in macOS System Settings shows both
175
+ * schemes at once.
176
+ */
177
+ const Root$1 = styled.div.withConfig({
178
+ displayName: "Root",
179
+ componentId: "sc-w55yz9-0"
180
+ })`position:relative;aspect-ratio:16 / 9;overflow:hidden;`, LIST_ROWS = [
181
+ "62%",
182
+ "46%",
183
+ "70%",
184
+ "52%"
185
+ ], FIELDS = ["32%", "24%"];
186
+ /**
187
+ * A tiny, lo-fi mockup of a Studio in the given theme — the navbar over the
188
+ * structure tool with a list of documents on the left and a document form on
189
+ * the right — in both color schemes at once: the light scheme on the left and
190
+ * the dark scheme on the right, split diagonally. It only paints colors the
191
+ * theme options change (backgrounds, text, borders, the accent) so that a
192
+ * glance shows what the theme does to the Studio — with few and bold enough
193
+ * elements to still read when scaled down to a thumbnail.
194
+ *
195
+ * @internal
196
+ */
197
+ function ThemePreview(props) {
198
+ let $ = c(11), { options } = props, t0;
199
+ $[0] === options ? t0 = $[1] : (t0 = buildTheme(options), $[0] = options, $[1] = t0);
200
+ let theme = t0, t1;
201
+ $[2] === Symbol.for("react.memo_cache_sentinel") ? (t1 = /* @__PURE__ */ jsx(MockStudio, {}), $[2] = t1) : t1 = $[2];
202
+ let t2;
203
+ $[3] === theme ? t2 = $[4] : (t2 = /* @__PURE__ */ jsx(ThemeProvider, {
204
+ scheme: "light",
205
+ theme,
206
+ tone: "default",
207
+ children: t1
208
+ }), $[3] = theme, $[4] = t2);
209
+ let t3;
210
+ $[5] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx(MockStudio, { style: { clipPath: "polygon(58% 0, 100% 0, 100% 100%, 42% 100%)" } }), $[5] = t3) : t3 = $[5];
211
+ let t4;
212
+ $[6] === theme ? t4 = $[7] : (t4 = /* @__PURE__ */ jsx(ThemeProvider, {
213
+ scheme: "dark",
214
+ theme,
215
+ tone: "default",
216
+ children: t3
217
+ }), $[6] = theme, $[7] = t4);
218
+ let t5;
219
+ return $[8] !== t2 || $[9] !== t4 ? (t5 = /* @__PURE__ */ jsxs(Root$1, {
220
+ "aria-hidden": !0,
221
+ children: [t2, t4]
222
+ }), $[8] = t2, $[9] = t4, $[10] = t5) : t5 = $[10], t5;
223
+ }
224
+ function MockStudio(props) {
225
+ let $ = c(53), { color } = useTheme_v2(), primary = color.button.default.primary.enabled, selected = color.selectable.default.selected, input = color.input.default.enabled, border = `1px solid ${color.border}`, line = _temp$4, t0;
226
+ $[0] !== color.bg || $[1] !== props.style ? (t0 = {
227
+ position: "absolute",
228
+ inset: 0,
229
+ display: "flex",
230
+ flexDirection: "column",
231
+ background: color.bg,
232
+ ...props.style
233
+ }, $[0] = color.bg, $[1] = props.style, $[2] = t0) : t0 = $[2];
234
+ let t1;
235
+ $[3] === border ? t1 = $[4] : (t1 = {
236
+ flex: "none",
237
+ display: "flex",
238
+ alignItems: "center",
239
+ gap: 5,
240
+ height: "18%",
241
+ padding: "0 5%",
242
+ borderBottom: border
243
+ }, $[3] = border, $[4] = t1);
244
+ let t2;
245
+ $[5] === primary.bg ? t2 = $[6] : (t2 = /* @__PURE__ */ jsx("div", { style: {
246
+ flex: "none",
247
+ width: 8,
248
+ height: 8,
249
+ borderRadius: 2,
250
+ background: primary.bg
251
+ } }), $[5] = primary.bg, $[6] = t2);
252
+ let t3;
253
+ $[7] === color.fg ? t3 = $[8] : (t3 = /* @__PURE__ */ jsx("div", { style: line("22%", color.fg) }), $[7] = color.fg, $[8] = t3);
254
+ let t4;
255
+ $[9] === Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx("div", { style: { flex: 1 } }), $[9] = t4) : t4 = $[9];
256
+ let t5;
257
+ $[10] === color.avatar.blue.bg ? t5 = $[11] : (t5 = /* @__PURE__ */ jsx("div", { style: {
258
+ flex: "none",
259
+ width: 8,
260
+ height: 8,
261
+ borderRadius: "50%",
262
+ background: color.avatar.blue.bg
263
+ } }), $[10] = color.avatar.blue.bg, $[11] = t5);
264
+ let t6;
265
+ $[12] !== t1 || $[13] !== t2 || $[14] !== t3 || $[15] !== t5 ? (t6 = /* @__PURE__ */ jsxs("div", {
266
+ style: t1,
267
+ children: [
268
+ t2,
269
+ t3,
270
+ t4,
271
+ t5
272
+ ]
273
+ }), $[12] = t1, $[13] = t2, $[14] = t3, $[15] = t5, $[16] = t6) : t6 = $[16];
274
+ let t7;
275
+ $[17] === Symbol.for("react.memo_cache_sentinel") ? (t7 = {
276
+ flex: 1,
277
+ display: "flex",
278
+ minHeight: 0
279
+ }, $[17] = t7) : t7 = $[17];
280
+ let t8;
281
+ $[18] === border ? t8 = $[19] : (t8 = {
282
+ flex: "none",
283
+ display: "flex",
284
+ flexDirection: "column",
285
+ width: "36%",
286
+ borderRight: border
287
+ }, $[18] = border, $[19] = t8);
288
+ let t9;
289
+ $[20] !== color.muted || $[21] !== selected ? (t9 = LIST_ROWS.map((width_0, index) => {
290
+ let isSelected = index === 1;
291
+ return /* @__PURE__ */ jsx("div", {
292
+ style: {
293
+ flex: "none",
294
+ display: "flex",
295
+ alignItems: "center",
296
+ height: "18%",
297
+ padding: "0 9%",
298
+ background: isSelected ? selected.bg : void 0
299
+ },
300
+ children: /* @__PURE__ */ jsx("div", { style: line(width_0, isSelected ? selected.fg : color.muted.fg) })
301
+ }, width_0);
302
+ }), $[20] = color.muted, $[21] = selected, $[22] = t9) : t9 = $[22];
303
+ let t10;
304
+ $[23] !== t8 || $[24] !== t9 ? (t10 = /* @__PURE__ */ jsx("div", {
305
+ style: t8,
306
+ children: t9
307
+ }), $[23] = t8, $[24] = t9, $[25] = t10) : t10 = $[25];
308
+ let t11, t12;
309
+ $[26] === Symbol.for("react.memo_cache_sentinel") ? (t11 = {
310
+ flex: 1,
311
+ display: "flex",
312
+ flexDirection: "column",
313
+ minWidth: 0
314
+ }, t12 = {
315
+ flex: 1,
316
+ display: "flex",
317
+ flexDirection: "column",
318
+ gap: 7,
319
+ padding: "9% 8% 0",
320
+ minHeight: 0,
321
+ overflow: "hidden"
322
+ }, $[26] = t11, $[27] = t12) : (t11 = $[26], t12 = $[27]);
323
+ let t13;
324
+ $[28] === color.fg ? t13 = $[29] : (t13 = /* @__PURE__ */ jsx("div", { style: line("45%", color.fg) }), $[28] = color.fg, $[29] = t13);
325
+ let t14;
326
+ $[30] !== color.muted || $[31] !== input ? (t14 = FIELDS.map((width_1) => /* @__PURE__ */ jsxs("div", {
327
+ style: {
328
+ flex: "none",
329
+ display: "flex",
330
+ flexDirection: "column",
331
+ gap: 3
332
+ },
333
+ children: [/* @__PURE__ */ jsx("div", { style: line(width_1, color.muted.fg) }), /* @__PURE__ */ jsx("div", { style: {
334
+ height: 12,
335
+ borderRadius: 3,
336
+ background: input.bg,
337
+ boxShadow: `inset 0 0 0 1px ${input.border}`
338
+ } })]
339
+ }, width_1)), $[30] = color.muted, $[31] = input, $[32] = t14) : t14 = $[32];
340
+ let t15;
341
+ $[33] !== t13 || $[34] !== t14 ? (t15 = /* @__PURE__ */ jsxs("div", {
342
+ style: t12,
343
+ children: [t13, t14]
344
+ }), $[33] = t13, $[34] = t14, $[35] = t15) : t15 = $[35];
345
+ let t16;
346
+ $[36] === border ? t16 = $[37] : (t16 = {
347
+ flex: "none",
348
+ display: "flex",
349
+ alignItems: "center",
350
+ justifyContent: "flex-end",
351
+ height: "20%",
352
+ padding: "0 8%",
353
+ borderTop: border
354
+ }, $[36] = border, $[37] = t16);
355
+ let t17;
356
+ $[38] === primary.bg ? t17 = $[39] : (t17 = /* @__PURE__ */ jsx("div", { style: {
357
+ width: "32%",
358
+ height: "50%",
359
+ borderRadius: 3,
360
+ background: primary.bg
361
+ } }), $[38] = primary.bg, $[39] = t17);
362
+ let t18;
363
+ $[40] !== t16 || $[41] !== t17 ? (t18 = /* @__PURE__ */ jsx("div", {
364
+ style: t16,
365
+ children: t17
366
+ }), $[40] = t16, $[41] = t17, $[42] = t18) : t18 = $[42];
367
+ let t19;
368
+ $[43] !== t15 || $[44] !== t18 ? (t19 = /* @__PURE__ */ jsxs("div", {
369
+ style: t11,
370
+ children: [t15, t18]
371
+ }), $[43] = t15, $[44] = t18, $[45] = t19) : t19 = $[45];
372
+ let t20;
373
+ $[46] !== t10 || $[47] !== t19 ? (t20 = /* @__PURE__ */ jsxs("div", {
374
+ style: t7,
375
+ children: [t10, t19]
376
+ }), $[46] = t10, $[47] = t19, $[48] = t20) : t20 = $[48];
377
+ let t21;
378
+ return $[49] !== t0 || $[50] !== t20 || $[51] !== t6 ? (t21 = /* @__PURE__ */ jsxs("div", {
379
+ style: t0,
380
+ children: [t6, t20]
381
+ }), $[49] = t0, $[50] = t20, $[51] = t6, $[52] = t21) : t21 = $[52], t21;
382
+ }
383
+ function _temp$4(width, background) {
384
+ return {
385
+ flex: "none",
386
+ width,
387
+ height: 4,
388
+ borderRadius: 2,
389
+ background
390
+ };
391
+ }
392
+ /**
393
+ * The floating preview card of a theme: the lo-fi studio mockup on a raised,
394
+ * rounded card, like the appearance previews in macOS System Settings.
395
+ *
396
+ * @internal
397
+ */
398
+ function ThemeThumbnail(props) {
399
+ let $ = c(2), t0;
400
+ return $[0] === props.options ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(Card, {
401
+ overflow: "hidden",
402
+ radius: 3,
403
+ shadow: 1,
404
+ children: /* @__PURE__ */ jsx(ThemePreview, { options: props.options })
405
+ }), $[0] = props.options, $[1] = t0), t0;
406
+ }
407
+ /**
408
+ * The flow for restoring removed themes: the presets and custom themes the
409
+ * user took out of the list, each with a button to put it back — and, for
410
+ * custom themes, one to delete it for good. The machine leaves the flow once
411
+ * the last one is gone.
412
+ *
413
+ * @internal
414
+ */
415
+ function RemovedThemes() {
416
+ let $ = c(7), { removed, send } = useThemer(), t0;
417
+ if ($[0] !== removed || $[1] !== send) {
418
+ let t1;
419
+ $[3] === send ? t1 = $[4] : (t1 = (theme) => /* @__PURE__ */ jsxs(Stack, {
420
+ gap: 3,
421
+ children: [
422
+ /* @__PURE__ */ jsx(Box, {
423
+ style: { opacity: .6 },
424
+ children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: theme.options })
425
+ }),
426
+ /* @__PURE__ */ jsx(Text, {
427
+ align: "center",
428
+ size: 1,
429
+ textOverflow: "ellipsis",
430
+ children: displayTitle(theme.title)
431
+ }),
432
+ /* @__PURE__ */ jsxs(Flex, {
433
+ gap: 2,
434
+ justify: "center",
435
+ children: [/* @__PURE__ */ jsx(Button, {
436
+ fontSize: 1,
437
+ mode: "ghost",
438
+ onClick: () => send({
439
+ type: "theme.restore",
440
+ slug: theme.slug
441
+ }),
442
+ padding: 2,
443
+ text: "Restore"
444
+ }), theme.source === "custom" && /* @__PURE__ */ jsx(Button, {
445
+ fontSize: 1,
446
+ mode: "ghost",
447
+ onClick: () => send({
448
+ type: "theme.delete",
449
+ slug: theme.slug
450
+ }),
451
+ padding: 2,
452
+ text: "Delete",
453
+ tone: "critical"
454
+ })]
455
+ })
456
+ ]
457
+ }, theme.slug), $[3] = send, $[4] = t1), t0 = removed.map(t1), $[0] = removed, $[1] = send, $[2] = t0;
458
+ } else t0 = $[2];
459
+ let t1;
460
+ return $[5] === t0 ? t1 = $[6] : (t1 = /* @__PURE__ */ jsx(ScrollArea, {
461
+ padding: 3,
462
+ children: /* @__PURE__ */ jsx(Stack, {
463
+ gap: 5,
464
+ children: t0
465
+ })
466
+ }), $[5] = t0, $[6] = t1), t1;
467
+ }
468
+ /** @internal */
469
+ const IMAGE_PALETTE_KEYS = [
470
+ "vibrant",
471
+ "lightVibrant",
472
+ "darkVibrant",
473
+ "muted",
474
+ "lightMuted",
475
+ "darkMuted",
476
+ "dominant"
477
+ ], IMAGE_PALETTE_VARIANTS = [
478
+ "muted",
479
+ "vibrant",
480
+ "lightMuted",
481
+ "lightVibrant",
482
+ "darkMuted",
483
+ "darkVibrant"
484
+ ], IMAGE_PALETTE_TITLES = {
485
+ dominant: "Dominant",
486
+ vibrant: "Vibrant",
487
+ lightVibrant: "Light vibrant",
488
+ darkVibrant: "Dark vibrant",
489
+ muted: "Muted",
490
+ lightMuted: "Light muted",
491
+ darkMuted: "Dark muted"
492
+ }, TARGETS = [
493
+ ["vibrant", {
494
+ lightness: [
495
+ .5,
496
+ .3,
497
+ .7
498
+ ],
499
+ saturation: [
500
+ 1,
501
+ .35,
502
+ 1
503
+ ]
504
+ }],
505
+ ["lightVibrant", {
506
+ lightness: [
507
+ .74,
508
+ .55,
509
+ 1
510
+ ],
511
+ saturation: [
512
+ 1,
513
+ .35,
514
+ 1
515
+ ]
516
+ }],
517
+ ["darkVibrant", {
518
+ lightness: [
519
+ .26,
520
+ 0,
521
+ .45
522
+ ],
523
+ saturation: [
524
+ 1,
525
+ .35,
526
+ 1
527
+ ]
528
+ }],
529
+ ["muted", {
530
+ lightness: [
531
+ .5,
532
+ .3,
533
+ .7
534
+ ],
535
+ saturation: [
536
+ .3,
537
+ 0,
538
+ .4
539
+ ]
540
+ }],
541
+ ["lightMuted", {
542
+ lightness: [
543
+ .74,
544
+ .55,
545
+ 1
546
+ ],
547
+ saturation: [
548
+ .3,
549
+ 0,
550
+ .4
551
+ ]
552
+ }],
553
+ ["darkMuted", {
554
+ lightness: [
555
+ .26,
556
+ 0,
557
+ .45
558
+ ],
559
+ saturation: [
560
+ .3,
561
+ 0,
562
+ .4
563
+ ]
564
+ }]
565
+ ];
566
+ /**
567
+ * Extracts the color palette of an image from its pixels, the way the Vibrant
568
+ * algorithm does: the colors are quantized into a coarse histogram, and each
569
+ * swatch claims the color that comes closest to its target lightness and
570
+ * saturation — weighted by how much of the image it covers — that no other
571
+ * swatch has claimed yet. The dominant swatch is simply the most common
572
+ * color. Runs anywhere pixels come from, so it can run on device.
573
+ *
574
+ * @internal
575
+ */
576
+ function extractImagePalette(pixels) {
577
+ let candidates = quantize(pixels), palette = {
578
+ dominant: null,
579
+ vibrant: null,
580
+ lightVibrant: null,
581
+ darkVibrant: null,
582
+ muted: null,
583
+ lightMuted: null,
584
+ darkMuted: null
585
+ };
586
+ if (candidates.length === 0) return palette;
587
+ let maximumPopulation = Math.max(...candidates.map((candidate) => candidate.population)), claimed = /* @__PURE__ */ new Set();
588
+ for (let [key, target] of TARGETS) {
589
+ let best = null, bestScore = -Infinity;
590
+ for (let candidate of candidates) {
591
+ if (claimed.has(candidate.hex) || !matchesTarget(candidate, target)) continue;
592
+ let score = scoreCandidate(candidate, target, maximumPopulation);
593
+ score > bestScore && (best = candidate, bestScore = score);
594
+ }
595
+ best && (palette[key] = best.hex, claimed.add(best.hex));
596
+ }
597
+ return palette.dominant = candidates.reduce((most, candidate) => candidate.population > most.population ? candidate : most).hex, palette;
598
+ }
599
+ /**
600
+ * Turns the pixels into a histogram of quantized colors, each averaged over
601
+ * the pixels it stands for, leaving out transparent pixels and the near-black
602
+ * and near-white ones that are backdrop rather than color.
603
+ */
604
+ function quantize(pixels) {
605
+ let { data } = pixels, buckets = /* @__PURE__ */ new Map(), length = Math.min(data.length, pixels.width * pixels.height * 4);
606
+ for (let offset = 0; offset + 3 < length; offset += 4) {
607
+ let r = data[offset], g = data[offset + 1], b = data[offset + 2];
608
+ if (data[offset + 3] < 125) continue;
609
+ let lightness = (Math.max(r, g, b) + Math.min(r, g, b)) / 510;
610
+ if (lightness < .05 || lightness > .95) continue;
611
+ let key = r >> 3 << 10 | g >> 3 << 5 | b >> 3, bucket = buckets.get(key);
612
+ bucket ? (bucket.count++, bucket.r += r, bucket.g += g, bucket.b += b) : buckets.set(key, {
613
+ count: 1,
614
+ r,
615
+ g,
616
+ b
617
+ });
618
+ }
619
+ let candidates = [];
620
+ for (let bucket of buckets.values()) {
621
+ 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);
622
+ candidates.push({
623
+ hex: rgbToHex(r, g, b),
624
+ population: bucket.count,
625
+ saturation,
626
+ lightness
627
+ });
628
+ }
629
+ return candidates;
630
+ }
631
+ function matchesTarget(candidate, target) {
632
+ return candidate.lightness >= target.lightness[1] && candidate.lightness <= target.lightness[2] && candidate.saturation >= target.saturation[1] && candidate.saturation <= target.saturation[2];
633
+ }
634
+ function scoreCandidate(candidate, target, maximumPopulation) {
635
+ return 3 * (1 - Math.abs(candidate.saturation - target.saturation[0])) + 6.5 * (1 - Math.abs(candidate.lightness - target.lightness[0])) + .5 * (candidate.population / maximumPopulation);
636
+ }
637
+ function saturationAndLightness(r, g, b) {
638
+ let max = Math.max(r, g, b) / 255, min = Math.min(r, g, b) / 255, lightness = (max + min) / 2, delta = max - min;
639
+ return {
640
+ saturation: delta === 0 ? 0 : lightness > .5 ? delta / (2 - max - min) : delta / (max + min),
641
+ lightness
642
+ };
643
+ }
644
+ function rgbToHex(r, g, b) {
645
+ return `#${[
646
+ r,
647
+ g,
648
+ b
649
+ ].map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
650
+ }
651
+ /** Reads `#rgb` and `#rrggbb` alike — stored palettes may hold either */
652
+ function hexToRgb(hex) {
653
+ let expanded = hex.length === 4 ? `${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex.slice(1);
654
+ return [
655
+ parseInt(expanded.slice(0, 2), 16),
656
+ parseInt(expanded.slice(2, 4), 16),
657
+ parseInt(expanded.slice(4, 6), 16)
658
+ ];
659
+ }
660
+ /** Mixes `amount` of `color` into `base` */
661
+ function tint(base, color, amount) {
662
+ let [r1, g1, b1] = hexToRgb(base), [r2, g2, b2] = hexToRgb(color);
663
+ return rgbToHex(Math.round(r1 + (r2 - r1) * amount), Math.round(g1 + (g2 - g1) * amount), Math.round(b1 + (b2 - b1) * amount));
664
+ }
665
+ /** How much of the palette tints each scheme's stock background */
666
+ const BACKGROUND_TINT = {
667
+ light: .08,
668
+ dark: .3
669
+ }, BUTTON_LABEL = {
670
+ light: "#ffffff",
671
+ dark: "#000000"
672
+ }, ACCENT_LIGHTNESS_STEP = .01;
673
+ function contrastRatio(a, b) {
674
+ let [lighter, darker] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
675
+ return (lighter + .05) / (darker + .05);
676
+ }
677
+ /**
678
+ * Makes an accent readable as a button in the given scheme: a light scheme
679
+ * accent is darkened until white text on it reaches AA contrast, a dark
680
+ * scheme accent lightened until black text does — as far as the accent's
681
+ * hue and saturation allow. Accents that already pass come back untouched.
682
+ *
683
+ * @internal
684
+ */
685
+ function readableAccent(hex, scheme) {
686
+ let label = BUTTON_LABEL[scheme], { h, s } = hexToHsl(hex), { l } = hexToHsl(hex), current = hex;
687
+ for (; contrastRatio(current, label) < 4.5;) {
688
+ let next = scheme === "light" ? l - ACCENT_LIGHTNESS_STEP : l + ACCENT_LIGHTNESS_STEP;
689
+ if (next < 0 || next > 1) break;
690
+ l = next, current = hslToHex({
691
+ h,
692
+ s,
693
+ l
694
+ });
695
+ }
696
+ return current;
697
+ }
698
+ /** The accent a variant gives a scheme, made readable on a button */
699
+ function variantAccent(palette, variant, scheme) {
700
+ let swatch = (variant ? palette[variant] : null) ?? palette.vibrant ?? (scheme === "light" ? palette.darkVibrant : palette.lightVibrant) ?? palette.dominant;
701
+ return swatch === null ? null : readableAccent(swatch, scheme);
702
+ }
703
+ /**
704
+ * Derives theme options from an image palette: the accent of both schemes is
705
+ * the swatch of the chosen variant — or, without one, the vibrant swatch,
706
+ * each scheme falling back to the vibrant swatch of its own end of the scale —
707
+ * adjusted until it is readable as a button. The muted swatch becomes the text color, and the light and dark muted
708
+ * swatches tint the stock backgrounds of their scheme — enough to carry the
709
+ * image's tone without giving up a usable canvas. Whatever the palette lacks
710
+ * stays at the theme defaults.
711
+ *
712
+ * @internal
713
+ */
714
+ function optionsFromImagePalette(palette, variant) {
715
+ 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");
716
+ return light && (options.light = light), dark && (options.dark = dark), options;
717
+ }
718
+ function schemeFromPalette(accent, text, backgroundTint, scheme) {
719
+ let options = {};
720
+ 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;
721
+ }
722
+ /**
723
+ * Applies an image palette to existing theme options: the colors the palette
724
+ * provides replace the current ones scheme by scheme, and whatever it does
725
+ * not touch — like the contrast — stays as it is.
726
+ *
727
+ * @internal
728
+ */
729
+ function applyImagePalette(options, palette, variant) {
730
+ let derived = optionsFromImagePalette(palette, variant), next = { ...options };
731
+ for (let scheme of SCHEMES) derived[scheme] && (next[scheme] = {
732
+ ...options[scheme],
733
+ ...derived[scheme]
734
+ });
735
+ return next;
736
+ }
737
+ /**
738
+ * The variant a theme is built around: the one whose swatch is the theme's
739
+ * accent, or `null` when the accent comes from somewhere else.
740
+ *
741
+ * @internal
742
+ */
743
+ function currentImageVariant(options, palette) {
744
+ let accent = options.light?.accent;
745
+ return accent ? IMAGE_PALETTE_VARIANTS.find((variant) => palette[variant] !== null && variantAccent(palette, variant, "light") === accent) ?? null : null;
746
+ }
747
+ /**
748
+ * How much a swatch has going for it as an accent: saturation first, then
749
+ * closeness to mid lightness — the range where an accent reads best.
750
+ */
751
+ function interest(hex) {
752
+ let { s, l } = hexToHsl(hex);
753
+ return .1 + .55 * s + .35 * (1 - Math.abs(l - .5) * 2);
754
+ }
755
+ /**
756
+ * Picks a variant for the "I'm feeling lucky" button: a random one, weighted
757
+ * by how interesting its swatch is as an accent, and never the one the theme
758
+ * already uses while there are others to pick from.
759
+ *
760
+ * @internal
761
+ */
762
+ function pickLuckyVariant(palette, options = {}) {
763
+ let { exclude = null, random = Math.random } = options, candidates = IMAGE_PALETTE_VARIANTS.filter((variant) => palette[variant] !== null);
764
+ if (candidates.length > 1 && (candidates = candidates.filter((variant) => variant !== exclude)), candidates.length === 0) return null;
765
+ let weights = candidates.map((variant) => interest(palette[variant])), total = weights.reduce((sum, weight) => sum + weight, 0), pick = random() * total;
766
+ for (let [index, variant] of candidates.entries()) if (pick -= weights[index], pick < 0) return variant;
767
+ return candidates.at(-1) ?? null;
768
+ }
769
+ /** A theme title for the image a palette came from, e.g. `sunset-beach.jpg` → `sunset beach` @internal */
770
+ function titleFromFileName(name) {
771
+ return name.replace(/\.[^.]+$/, "").replace(/[-_]+/g, " ").trim() || "Image theme";
772
+ }
773
+ /**
774
+ * A button with an animated tooltip instead of a `title` attribute. Icon-only
775
+ * buttons take the tooltip as their accessible name.
776
+ *
777
+ * @internal
778
+ */
779
+ function TooltipButton(props) {
780
+ let $ = c(11), buttonProps, tooltip;
781
+ $[0] === props ? (buttonProps = $[1], tooltip = $[2]) : ({tooltip, ...buttonProps} = props, $[0] = props, $[1] = buttonProps, $[2] = tooltip);
782
+ let t0;
783
+ $[3] === tooltip ? t0 = $[4] : (t0 = /* @__PURE__ */ jsx(Text, {
784
+ size: 1,
785
+ children: tooltip
786
+ }), $[3] = tooltip, $[4] = t0);
787
+ let t1 = buttonProps.text === void 0 ? tooltip : void 0, t2;
788
+ $[5] !== buttonProps || $[6] !== t1 ? (t2 = /* @__PURE__ */ jsx(Button, {
789
+ "aria-label": t1,
790
+ ...buttonProps
791
+ }), $[5] = buttonProps, $[6] = t1, $[7] = t2) : t2 = $[7];
792
+ let t3;
793
+ return $[8] !== t0 || $[9] !== t2 ? (t3 = /* @__PURE__ */ jsx(Tooltip, {
794
+ animate: !0,
795
+ content: t0,
796
+ placement: "bottom",
797
+ portal: !0,
798
+ children: t2
799
+ }), $[8] = t0, $[9] = t2, $[10] = t3) : t3 = $[10], t3;
800
+ }
801
+ /**
802
+ * A button that opens the file picker for an image, and hands the chosen file
803
+ * over — the picker is a hidden file input, so nothing is uploaded anywhere.
804
+ *
805
+ * @internal
806
+ */
807
+ function ImageFileButton(props) {
808
+ let $ = c(11), buttonProps, onFile;
809
+ $[0] === props ? (buttonProps = $[1], onFile = $[2]) : ({onFile, ...buttonProps} = props, $[0] = props, $[1] = buttonProps, $[2] = onFile);
810
+ let inputRef = useRef(null), t0;
811
+ $[3] === Symbol.for("react.memo_cache_sentinel") ? (t0 = () => inputRef.current?.click(), $[3] = t0) : t0 = $[3];
812
+ let t1;
813
+ $[4] === buttonProps ? t1 = $[5] : (t1 = /* @__PURE__ */ jsx(TooltipButton, {
814
+ ...buttonProps,
815
+ icon: ImageIcon,
816
+ onClick: t0
817
+ }), $[4] = buttonProps, $[5] = t1);
818
+ let t2;
819
+ $[6] === onFile ? t2 = $[7] : (t2 = /* @__PURE__ */ jsx("input", {
820
+ accept: "image/*",
821
+ hidden: !0,
822
+ onChange: (event) => {
823
+ let file = event.currentTarget.files?.[0];
824
+ event.currentTarget.value = "", file && onFile(file);
825
+ },
826
+ ref: inputRef,
827
+ type: "file"
828
+ }), $[6] = onFile, $[7] = t2);
829
+ let t3;
830
+ return $[8] !== t1 || $[9] !== t2 ? (t3 = /* @__PURE__ */ jsxs(Fragment, { children: [t1, t2] }), $[8] = t1, $[9] = t2, $[10] = t3) : t3 = $[10], t3;
831
+ }
832
+ /**
833
+ * Reads the pixels of an image file on device: the image is decoded, drawn
834
+ * onto a small canvas and read back, so nothing leaves the browser.
835
+ *
836
+ * @internal
837
+ */
838
+ async function readImagePixels(file) {
839
+ let image = await decodeImage(file);
840
+ try {
841
+ 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");
842
+ canvas.width = width, canvas.height = height;
843
+ let context = canvas.getContext("2d", { willReadFrequently: !0 });
844
+ if (!context) throw Error("Could not draw the image");
845
+ return context.drawImage(image, 0, 0, width, height), {
846
+ data: context.getImageData(0, 0, width, height).data,
847
+ width,
848
+ height
849
+ };
850
+ } finally {
851
+ typeof ImageBitmap < "u" && image instanceof ImageBitmap && image.close();
852
+ }
853
+ }
854
+ async function decodeImage(file) {
855
+ if (typeof createImageBitmap == "function") try {
856
+ return await createImageBitmap(file);
857
+ } catch {}
858
+ let url = URL.createObjectURL(file);
859
+ try {
860
+ return await new Promise((resolve, reject) => {
861
+ let image = new Image();
862
+ image.addEventListener("load", () => resolve(image), { once: !0 }), image.addEventListener("error", () => reject(/* @__PURE__ */ Error("Could not decode the image")), { once: !0 }), image.src = url;
863
+ });
864
+ } finally {
865
+ URL.revokeObjectURL(url);
866
+ }
867
+ }
868
+ /**
869
+ * Extracts the palette of image files on device and hands it to `onPalette`,
870
+ * along with the file it came from. Failures end up as a toast.
871
+ *
872
+ * @internal
873
+ */
874
+ function useImagePalette(onPalette) {
875
+ let $ = c(10), toast = useToast(), [busy, setBusy] = useState(!1), onPaletteRef = useRef(onPalette), mountedRef = useRef(!0), latestPickRef = useRef(0), t0, t1;
876
+ $[0] === onPalette ? (t0 = $[1], t1 = $[2]) : (t0 = () => {
877
+ onPaletteRef.current = onPalette;
878
+ }, t1 = [onPalette], $[0] = onPalette, $[1] = t0, $[2] = t1), useEffect(t0, t1);
879
+ let t2, t3;
880
+ $[3] === Symbol.for("react.memo_cache_sentinel") ? (t2 = () => (mountedRef.current = !0, () => {
881
+ mountedRef.current = !1;
882
+ }), t3 = [], $[3] = t2, $[4] = t3) : (t2 = $[3], t3 = $[4]), useEffect(t2, t3);
883
+ let t4;
884
+ $[5] === toast ? t4 = $[6] : (t4 = (file) => {
885
+ let pick = latestPickRef.current += 1;
886
+ setBusy(!0);
887
+ let isCurrent = () => mountedRef.current && latestPickRef.current === pick, extract = async () => {
888
+ let palette = extractImagePalette(await readImagePixels(file));
889
+ isCurrent() && (Object.values(palette).every(_temp$3) ? toast.push({
890
+ status: "warning",
891
+ title: "Found no colors in that image"
892
+ }) : onPaletteRef.current(palette, file));
893
+ };
894
+ extract().catch(() => {
895
+ isCurrent() && toast.push({
896
+ status: "error",
897
+ title: "Could not read that image"
898
+ });
899
+ }).finally(() => {
900
+ isCurrent() && setBusy(!1);
901
+ });
902
+ }, $[5] = toast, $[6] = t4);
903
+ let pickImage = t4, t5;
904
+ return $[7] !== busy || $[8] !== pickImage ? (t5 = {
905
+ busy,
906
+ pickImage
907
+ }, $[7] = busy, $[8] = pickImage, $[9] = t5) : t5 = $[9], t5;
908
+ }
909
+ function _temp$3(swatch) {
910
+ return swatch === null;
911
+ }
912
+ /** The swatches on display: the vibrant family over the muted one */
913
+ const TILES = [
914
+ "vibrant",
915
+ "lightVibrant",
916
+ "darkVibrant",
917
+ "muted",
918
+ "lightMuted",
919
+ "darkMuted"
920
+ ], PaletteGrid = styled.div.withConfig({
921
+ displayName: "PaletteGrid",
922
+ componentId: "sc-j2tqre-0"
923
+ })`display:grid;grid-template-columns:repeat(3,${24}px);grid-auto-rows:${24}px;gap:${4}px;&[data-with-image='true']{grid-template-columns:${52}px repeat(3,${24}px);}`, ImageTile = styled.img.withConfig({
924
+ displayName: "ImageTile",
925
+ componentId: "sc-j2tqre-1"
926
+ })`display:block;grid-row:span 2;width:${52}px;height:${52}px;border-radius:3px;object-fit:cover;box-shadow:inset 0 0 0 1px var(--card-border-color);`, Tile = styled.span.withConfig({
927
+ displayName: "Tile",
928
+ componentId: "sc-j2tqre-2"
929
+ })`display:block;border-radius:3px;box-shadow:inset 0 0 0 1px var(--card-border-color);&[data-empty='true']{background:repeating-linear-gradient( -45deg,transparent 0 3px,var(--card-border-color) 3px 4px );}`, VariantRow = styled.div.withConfig({
930
+ displayName: "VariantRow",
931
+ componentId: "sc-j2tqre-3"
932
+ })`display:flex;gap:${8}px;margin:0 -${12}px;padding:0 ${12}px;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-left:${12}px;scrollbar-width:thin;`, VariantButton = styled.button.withConfig({
933
+ displayName: "VariantButton",
934
+ componentId: "sc-j2tqre-4"
935
+ })`appearance:none;flex:none;display:block;box-sizing:border-box;width:calc(var(--variant-width) + 8px);margin:0;padding:0;border:0;background:none;color:inherit;font:inherit;text-align:center;cursor:pointer;scroll-snap-align:start;&:focus{outline:none;}`, VariantFrame = styled.span.withConfig({
936
+ displayName: "VariantFrame",
937
+ componentId: "sc-j2tqre-5"
938
+ })`display:block;box-sizing:border-box;width:calc(var(--variant-width) + 8px);height:calc(var(--variant-width) * 9 / 16 + 8px);padding:4px;border-radius:7px;overflow:hidden;transition:box-shadow 100ms;${VariantButton}:hover &{box-shadow:inset 0 0 0 2px var(--card-border-color);}${VariantButton}[aria-pressed='true'] &{box-shadow:inset 0 0 0 2px var(--card-focus-ring-color);}${VariantButton}:focus-visible &{outline:2px solid var(--card-focus-ring-color);outline-offset:-2px;}`, VariantScale = styled.span.withConfig({
939
+ displayName: "VariantScale",
940
+ componentId: "sc-j2tqre-6"
941
+ })`display:block;width:${144}px;transform:scale(var(--variant-scale));transform-origin:top left;`;
942
+ /**
943
+ * Takes the colors of a theme from an image, on device: the image is read
944
+ * into a canvas and its palette extracted right there — nothing is uploaded.
945
+ * Shows the image (while the session lasts) next to its palette, offers a
946
+ * theme variant built around each swatch as a small preview, and a lucky
947
+ * button that picks an interesting one.
948
+ *
949
+ * @internal
950
+ */
951
+ function ImagePaletteSection(props) {
952
+ let $ = c(38), { imageUrl, onPalette, onVariant, onLucky, options, palette } = props, { busy, pickImage } = useImagePalette(onPalette), [dragging, setDragging] = useState(!1), dragDepth = useRef(0), t0;
953
+ $[0] !== options || $[1] !== palette ? (t0 = palette ? currentImageVariant(options, palette) : null, $[0] = options, $[1] = palette, $[2] = t0) : t0 = $[2];
954
+ let current = t0, rowRef = useRef(null), [variantWidth, setVariantWidth] = useState(58), t1;
955
+ $[3] === Symbol.for("react.memo_cache_sentinel") ? (t1 = () => {
956
+ let row = rowRef.current;
957
+ if (!row) return;
958
+ let measure = () => {
959
+ let visible = row.clientWidth - 12 - 16;
960
+ setVariantWidth(Math.max(24, Math.floor(visible / 2.5) - 8));
961
+ };
962
+ measure();
963
+ let observer = new ResizeObserver(measure);
964
+ return observer.observe(row), () => observer.disconnect();
965
+ }, $[3] = t1) : t1 = $[3];
966
+ let t2;
967
+ $[4] === palette ? t2 = $[5] : (t2 = [palette], $[4] = palette, $[5] = t2), useEffect(t1, t2);
968
+ let t3;
969
+ bb0: {
970
+ if (!palette) {
971
+ let t4;
972
+ $[6] === Symbol.for("react.memo_cache_sentinel") ? (t4 = [], $[6] = t4) : t4 = $[6], t3 = t4;
973
+ break bb0;
974
+ }
975
+ let t4;
976
+ $[7] !== options || $[8] !== palette ? (t4 = IMAGE_PALETTE_VARIANTS.flatMap((variant) => palette[variant] === null ? [] : [{
977
+ variant,
978
+ options: applyImagePalette(options, palette, variant)
979
+ }]), $[7] = options, $[8] = palette, $[9] = t4) : t4 = $[9], t3 = t4;
980
+ }
981
+ let variants = t3, t4, t5;
982
+ $[10] === Symbol.for("react.memo_cache_sentinel") ? (t4 = (event) => {
983
+ event.preventDefault(), dragDepth.current += 1, setDragging(!0);
984
+ }, t5 = () => {
985
+ dragDepth.current = Math.max(0, dragDepth.current - 1), dragDepth.current === 0 && setDragging(!1);
986
+ }, $[10] = t4, $[11] = t5) : (t4 = $[10], t5 = $[11]);
987
+ let t6;
988
+ $[12] === pickImage ? t6 = $[13] : (t6 = (event_1) => {
989
+ event_1.preventDefault(), dragDepth.current = 0, setDragging(!1);
990
+ let file = event_1.dataTransfer.files[0];
991
+ file && pickImage(file);
992
+ }, $[12] = pickImage, $[13] = t6);
993
+ let t7 = dragging ? "primary" : "default", t8;
994
+ $[14] === Symbol.for("react.memo_cache_sentinel") ? (t8 = /* @__PURE__ */ jsx(Box, {
995
+ flex: 1,
996
+ children: /* @__PURE__ */ jsx(Text, {
997
+ size: 1,
998
+ weight: "medium",
999
+ children: "Image palette"
1000
+ })
1001
+ }), $[14] = t8) : t8 = $[14];
1002
+ let t9;
1003
+ $[15] !== busy || $[16] !== palette || $[17] !== pickImage ? (t9 = palette && /* @__PURE__ */ jsx(ImageFileButton, {
1004
+ loading: busy,
1005
+ mode: "bleed",
1006
+ onFile: pickImage,
1007
+ padding: 2,
1008
+ tooltip: "Pick another image"
1009
+ }), $[15] = busy, $[16] = palette, $[17] = pickImage, $[18] = t9) : t9 = $[18];
1010
+ let t10;
1011
+ $[19] === t9 ? t10 = $[20] : (t10 = /* @__PURE__ */ jsxs(Flex, {
1012
+ align: "center",
1013
+ gap: 2,
1014
+ children: [t8, t9]
1015
+ }), $[19] = t9, $[20] = t10);
1016
+ let t11;
1017
+ $[21] !== busy || $[22] !== current || $[23] !== imageUrl || $[24] !== onLucky || $[25] !== onVariant || $[26] !== palette || $[27] !== pickImage || $[28] !== variantWidth || $[29] !== variants ? (t11 = palette ? /* @__PURE__ */ jsxs(Fragment, { children: [
1018
+ /* @__PURE__ */ jsxs(PaletteGrid, {
1019
+ "data-with-image": imageUrl !== void 0,
1020
+ children: [imageUrl && /* @__PURE__ */ jsx(ImageTile, {
1021
+ alt: "",
1022
+ src: imageUrl
1023
+ }), TILES.map((key) => {
1024
+ let hex = palette[key], title = IMAGE_PALETTE_TITLES[key];
1025
+ return /* @__PURE__ */ jsx(Tooltip, {
1026
+ animate: !0,
1027
+ content: /* @__PURE__ */ jsx(Text, {
1028
+ size: 1,
1029
+ children: hex ? `${title} · ${hex}` : `No ${title.toLowerCase()} color`
1030
+ }),
1031
+ placement: "bottom",
1032
+ portal: !0,
1033
+ children: /* @__PURE__ */ jsx(Tile, {
1034
+ "data-empty": hex === null,
1035
+ style: { background: hex ?? void 0 }
1036
+ })
1037
+ }, key);
1038
+ })]
1039
+ }),
1040
+ /* @__PURE__ */ jsxs(Stack, {
1041
+ gap: 2,
1042
+ children: [/* @__PURE__ */ jsx(Text, {
1043
+ muted: !0,
1044
+ size: 0,
1045
+ children: "Pick the swatch to build the theme around"
1046
+ }), /* @__PURE__ */ jsx(VariantRow, {
1047
+ ref: rowRef,
1048
+ style: {
1049
+ "--variant-width": `${variantWidth}px`,
1050
+ "--variant-scale": variantWidth / 144
1051
+ },
1052
+ children: variants.map((t12) => {
1053
+ let { variant: variant_0, options: variantOptions } = t12;
1054
+ return /* @__PURE__ */ jsxs(VariantButton, {
1055
+ "aria-pressed": variant_0 === current,
1056
+ onClick: () => onVariant(variant_0),
1057
+ type: "button",
1058
+ children: [/* @__PURE__ */ jsx(VariantFrame, { children: /* @__PURE__ */ jsx(VariantScale, { children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: variantOptions }) }) }), /* @__PURE__ */ jsx(Box, {
1059
+ paddingTop: 1,
1060
+ children: /* @__PURE__ */ jsx(Text, {
1061
+ align: "center",
1062
+ size: 0,
1063
+ textOverflow: "ellipsis",
1064
+ weight: variant_0 === current ? "medium" : "regular",
1065
+ children: IMAGE_PALETTE_TITLES[variant_0]
1066
+ })
1067
+ })]
1068
+ }, variant_0);
1069
+ })
1070
+ })]
1071
+ }),
1072
+ /* @__PURE__ */ jsx(TooltipButton, {
1073
+ icon: SparklesIcon,
1074
+ mode: "ghost",
1075
+ onClick: onLucky,
1076
+ text: "I'm feeling lucky",
1077
+ tooltip: "Build the theme around a swatch that looks interesting",
1078
+ width: "fill"
1079
+ })
1080
+ ] }) : /* @__PURE__ */ jsxs(Stack, {
1081
+ gap: 3,
1082
+ children: [/* @__PURE__ */ jsx(Text, {
1083
+ muted: !0,
1084
+ size: 0,
1085
+ children: "Take the accent, text and background colors from an image — read on device, nothing is uploaded."
1086
+ }), /* @__PURE__ */ jsx(ImageFileButton, {
1087
+ loading: busy,
1088
+ mode: "ghost",
1089
+ onFile: pickImage,
1090
+ text: "Pick an image",
1091
+ tooltip: "Take the colors from an image on your device",
1092
+ width: "fill"
1093
+ })]
1094
+ }), $[21] = busy, $[22] = current, $[23] = imageUrl, $[24] = onLucky, $[25] = onVariant, $[26] = palette, $[27] = pickImage, $[28] = variantWidth, $[29] = variants, $[30] = t11) : t11 = $[30];
1095
+ let t12;
1096
+ $[31] !== t10 || $[32] !== t11 ? (t12 = /* @__PURE__ */ jsxs(Stack, {
1097
+ gap: 3,
1098
+ children: [t10, t11]
1099
+ }), $[31] = t10, $[32] = t11, $[33] = t12) : t12 = $[33];
1100
+ let t13;
1101
+ return $[34] !== t12 || $[35] !== t6 || $[36] !== t7 ? (t13 = /* @__PURE__ */ jsx(Card, {
1102
+ border: !0,
1103
+ onDragEnter: t4,
1104
+ onDragLeave: t5,
1105
+ onDragOver: _temp$2,
1106
+ onDrop: t6,
1107
+ padding: 3,
1108
+ radius: 3,
1109
+ tone: t7,
1110
+ children: t12
1111
+ }), $[34] = t12, $[35] = t6, $[36] = t7, $[37] = t13) : t13 = $[37], t13;
1112
+ }
1113
+ function _temp$2(event_0) {
1114
+ event_0.preventDefault();
1115
+ }
1116
+ const SCHEME_TITLES = {
1117
+ dark: "Dark mode",
1118
+ light: "Light mode"
1119
+ }, SCHEME_OPTION_KEYS = [
1120
+ "accent",
1121
+ "text",
1122
+ "background",
1123
+ "contrast"
1124
+ ], Swatch = styled.input.withConfig({
1125
+ displayName: "Swatch",
1126
+ componentId: "sc-vs5143-0"
1127
+ })`box-sizing:border-box;flex:none;width:33px;height:33px;padding:0;border:1px solid var(--card-border-color);border-radius:4px;background:none;cursor:pointer;&::-webkit-color-swatch-wrapper{padding:0;}&::-webkit-color-swatch{border:none;border-radius:3px;}&::-moz-color-swatch{border:none;border-radius:3px;}`, Range = styled.input.withConfig({
1128
+ displayName: "Range",
1129
+ componentId: "sc-vs5143-1"
1130
+ })`display:block;width:100%;margin:0;accent-color:var(--card-focus-ring-color);`;
1131
+ /**
1132
+ * The flow for editing one of the user's own themes: its title, a live
1133
+ * preview, and a card per color scheme with the accent/text/background
1134
+ * pickers and the contrast slider of that scheme. Every change applies to the
1135
+ * whole Studio right away — in the scheme the Studio is showing.
1136
+ *
1137
+ * @internal
1138
+ */
1139
+ function ThemeEditor(props) {
1140
+ let $ = c(40), { focusTitle, slug } = props, { themes, images, send } = useThemer(), t0;
1141
+ if ($[0] !== slug || $[1] !== themes) {
1142
+ let t1;
1143
+ $[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;
1144
+ } else t0 = $[2];
1145
+ let theme = t0;
1146
+ if (!theme) return null;
1147
+ let t1;
1148
+ $[5] !== send || $[6] !== slug || $[7] !== theme.options || $[8] !== theme.palette ? (t1 = (variant) => {
1149
+ theme.palette && variant !== null && send({
1150
+ type: "theme.update",
1151
+ slug,
1152
+ options: applyImagePalette(theme.options, theme.palette, variant)
1153
+ });
1154
+ }, $[5] = send, $[6] = slug, $[7] = theme.options, $[8] = theme.palette, $[9] = t1) : t1 = $[9];
1155
+ let applyVariant = t1, t2 = images[slug], t3;
1156
+ $[10] === send ? t3 = $[11] : (t3 = () => send({ type: "flow.list" }), $[10] = send, $[11] = t3);
1157
+ let t4;
1158
+ $[12] !== applyVariant || $[13] !== theme.options || $[14] !== theme.palette ? (t4 = () => {
1159
+ theme.palette && applyVariant(pickLuckyVariant(theme.palette, { exclude: currentImageVariant(theme.options, theme.palette) }));
1160
+ }, $[12] = applyVariant, $[13] = theme.options, $[14] = theme.palette, $[15] = t4) : t4 = $[15];
1161
+ let t5;
1162
+ $[16] !== send || $[17] !== slug ? (t5 = (options) => send({
1163
+ type: "theme.update",
1164
+ slug,
1165
+ options
1166
+ }), $[16] = send, $[17] = slug, $[18] = t5) : t5 = $[18];
1167
+ let t6;
1168
+ $[19] !== send || $[20] !== slug || $[21] !== theme.options ? (t6 = (palette, file) => send({
1169
+ type: "theme.update",
1170
+ slug,
1171
+ options: applyImagePalette(theme.options, palette),
1172
+ palette,
1173
+ imageUrl: URL.createObjectURL(file)
1174
+ }), $[19] = send, $[20] = slug, $[21] = theme.options, $[22] = t6) : t6 = $[22];
1175
+ let t7, t8;
1176
+ $[23] !== send || $[24] !== slug ? (t7 = () => send({
1177
+ type: "theme.remove",
1178
+ slug
1179
+ }), t8 = (title) => send({
1180
+ type: "theme.update",
1181
+ slug,
1182
+ title
1183
+ }), $[23] = send, $[24] = slug, $[25] = t7, $[26] = t8) : (t7 = $[25], t8 = $[26]);
1184
+ let t9;
1185
+ return $[27] !== applyVariant || $[28] !== focusTitle || $[29] !== t2 || $[30] !== t3 || $[31] !== t4 || $[32] !== t5 || $[33] !== t6 || $[34] !== t7 || $[35] !== t8 || $[36] !== theme.options || $[37] !== theme.palette || $[38] !== theme.title ? (t9 = /* @__PURE__ */ jsx(ThemeEditorForm, {
1186
+ focusTitle,
1187
+ imageUrl: t2,
1188
+ onDone: t3,
1189
+ onLucky: t4,
1190
+ onOptionsChange: t5,
1191
+ onPalette: t6,
1192
+ onRemove: t7,
1193
+ onTitleChange: t8,
1194
+ onVariant: applyVariant,
1195
+ options: theme.options,
1196
+ palette: theme.palette,
1197
+ title: theme.title
1198
+ }), $[27] = applyVariant, $[28] = focusTitle, $[29] = t2, $[30] = t3, $[31] = t4, $[32] = t5, $[33] = t6, $[34] = t7, $[35] = t8, $[36] = theme.options, $[37] = theme.palette, $[38] = theme.title, $[39] = t9) : t9 = $[39], t9;
1199
+ }
1200
+ function ThemeEditorForm(props) {
1201
+ let $ = c(46), { focusTitle, imageUrl, onDone, onLucky, onOptionsChange, onPalette, onRemove, onTitleChange, onVariant, options, palette, title } = props, studioScheme = useColorSchemeValue(), t0;
1202
+ $[0] === options ? t0 = $[1] : (t0 = resolveThemeOptions(options), $[0] = options, $[1] = t0);
1203
+ let resolved = t0, t1;
1204
+ $[2] === options ? t1 = $[3] : (t1 = buildPalette(options), $[2] = options, $[3] = t1);
1205
+ let palettes = t1, t2;
1206
+ $[4] !== onOptionsChange || $[5] !== options ? (t2 = (scheme, changes) => {
1207
+ let values = {
1208
+ ...options[scheme],
1209
+ ...changes
1210
+ };
1211
+ for (let key of SCHEME_OPTION_KEYS) values[key] === void 0 && delete values[key];
1212
+ let next = { ...options };
1213
+ Object.keys(values).length === 0 ? delete next[scheme] : next[scheme] = values, onOptionsChange(next);
1214
+ }, $[4] = onOptionsChange, $[5] = options, $[6] = t2) : t2 = $[6];
1215
+ let patchScheme = t2, t3;
1216
+ $[7] === options ? t3 = $[8] : (t3 = /* @__PURE__ */ jsx(ThemeThumbnail, { options }), $[7] = options, $[8] = t3);
1217
+ let t4;
1218
+ $[9] === Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx(Text, {
1219
+ size: 1,
1220
+ weight: "medium",
1221
+ children: "Title"
1222
+ }), $[9] = t4) : t4 = $[9];
1223
+ let t5;
1224
+ $[10] === onTitleChange ? t5 = $[11] : (t5 = (event) => onTitleChange(event.currentTarget.value), $[10] = onTitleChange, $[11] = t5);
1225
+ let t6 = focusTitle ? _temp$1 : void 0, t7;
1226
+ $[12] !== focusTitle || $[13] !== t5 || $[14] !== t6 || $[15] !== title ? (t7 = /* @__PURE__ */ jsxs(Stack, {
1227
+ gap: 2,
1228
+ children: [t4, /* @__PURE__ */ jsx(TextInput, {
1229
+ autoFocus: focusTitle,
1230
+ fontSize: 1,
1231
+ onChange: t5,
1232
+ onFocus: t6,
1233
+ padding: 2,
1234
+ placeholder: "Untitled theme",
1235
+ value: title
1236
+ })]
1237
+ }), $[12] = focusTitle, $[13] = t5, $[14] = t6, $[15] = title, $[16] = t7) : t7 = $[16];
1238
+ let t8;
1239
+ $[17] !== imageUrl || $[18] !== onLucky || $[19] !== onPalette || $[20] !== onVariant || $[21] !== options || $[22] !== palette ? (t8 = /* @__PURE__ */ jsx(ImagePaletteSection, {
1240
+ imageUrl,
1241
+ onLucky,
1242
+ onPalette,
1243
+ onVariant,
1244
+ options,
1245
+ palette
1246
+ }), $[17] = imageUrl, $[18] = onLucky, $[19] = onPalette, $[20] = onVariant, $[21] = options, $[22] = palette, $[23] = t8) : t8 = $[23];
1247
+ let t9;
1248
+ $[24] !== options || $[25] !== palettes || $[26] !== patchScheme || $[27] !== resolved || $[28] !== studioScheme ? (t9 = SCHEMES.map((scheme_0) => /* @__PURE__ */ jsx(SchemeCard, {
1249
+ active: scheme_0 === studioScheme,
1250
+ onChange: (changes_0) => patchScheme(scheme_0, changes_0),
1251
+ options: options[scheme_0] ?? {},
1252
+ palette: palettes[scheme_0],
1253
+ resolved: resolved[scheme_0],
1254
+ scheme: scheme_0
1255
+ }, scheme_0)), $[24] = options, $[25] = palettes, $[26] = patchScheme, $[27] = resolved, $[28] = studioScheme, $[29] = t9) : t9 = $[29];
1256
+ let t10;
1257
+ $[30] !== t3 || $[31] !== t7 || $[32] !== t8 || $[33] !== t9 ? (t10 = /* @__PURE__ */ jsx(ScrollArea, {
1258
+ padding: 3,
1259
+ children: /* @__PURE__ */ jsxs(Stack, {
1260
+ gap: 4,
1261
+ children: [
1262
+ t3,
1263
+ t7,
1264
+ t8,
1265
+ t9
1266
+ ]
1267
+ })
1268
+ }), $[30] = t3, $[31] = t7, $[32] = t8, $[33] = t9, $[34] = t10) : t10 = $[34];
1269
+ let t11;
1270
+ $[35] === onRemove ? t11 = $[36] : (t11 = /* @__PURE__ */ jsx(TooltipButton, {
1271
+ icon: TrashIcon,
1272
+ mode: "ghost",
1273
+ onClick: onRemove,
1274
+ text: "Remove",
1275
+ tone: "critical",
1276
+ tooltip: "Remove the theme — it can be restored until it is deleted"
1277
+ }), $[35] = onRemove, $[36] = t11);
1278
+ let t12;
1279
+ $[37] === Symbol.for("react.memo_cache_sentinel") ? (t12 = /* @__PURE__ */ jsx(Box, { flex: 1 }), $[37] = t12) : t12 = $[37];
1280
+ let t13;
1281
+ $[38] === onDone ? t13 = $[39] : (t13 = /* @__PURE__ */ jsx(Button, {
1282
+ mode: "ghost",
1283
+ onClick: onDone,
1284
+ text: "Done"
1285
+ }), $[38] = onDone, $[39] = t13);
1286
+ let t14;
1287
+ $[40] !== t11 || $[41] !== t13 ? (t14 = /* @__PURE__ */ jsx(Card, {
1288
+ borderTop: !0,
1289
+ padding: 3,
1290
+ children: /* @__PURE__ */ jsxs(Flex, {
1291
+ gap: 2,
1292
+ children: [
1293
+ t11,
1294
+ t12,
1295
+ t13
1296
+ ]
1297
+ })
1298
+ }), $[40] = t11, $[41] = t13, $[42] = t14) : t14 = $[42];
1299
+ let t15;
1300
+ return $[43] !== t10 || $[44] !== t14 ? (t15 = /* @__PURE__ */ jsxs(Fragment, { children: [t10, t14] }), $[43] = t10, $[44] = t14, $[45] = t15) : t15 = $[45], t15;
1301
+ }
1302
+ function _temp$1(event_0) {
1303
+ return event_0.currentTarget.select();
1304
+ }
1305
+ /**
1306
+ * The colors of one scheme, on a card painted in that scheme so that each
1307
+ * card shows the colors it edits. The scheme the Studio is showing is marked
1308
+ * as active: its changes show up in the Studio right away, the other scheme's
1309
+ * only once the appearance setting switches to it.
1310
+ */
1311
+ function SchemeCard(props) {
1312
+ let $ = c(69), { active, onChange, options, palette, resolved, scheme } = props, name = SCHEME_TITLES[scheme], t0;
1313
+ $[0] === name ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(Box, {
1314
+ flex: 1,
1315
+ children: /* @__PURE__ */ jsx(Text, {
1316
+ size: 1,
1317
+ weight: "medium",
1318
+ children: name
1319
+ })
1320
+ }), $[0] = name, $[1] = t0);
1321
+ let t1;
1322
+ $[2] === active ? t1 = $[3] : (t1 = active && /* @__PURE__ */ jsx(Badge, {
1323
+ fontSize: 0,
1324
+ tone: "primary",
1325
+ children: "Active"
1326
+ }), $[2] = active, $[3] = t1);
1327
+ let t2;
1328
+ $[4] !== t0 || $[5] !== t1 ? (t2 = /* @__PURE__ */ jsxs(Flex, {
1329
+ align: "center",
1330
+ gap: 2,
1331
+ children: [t0, t1]
1332
+ }), $[4] = t0, $[5] = t1, $[6] = t2) : t2 = $[6];
1333
+ let t3 = active ? "Shown in the Studio right now" : `Shown once the Studio appearance is ${scheme}`, t4;
1334
+ $[7] === t3 ? t4 = $[8] : (t4 = /* @__PURE__ */ jsx(Text, {
1335
+ muted: !0,
1336
+ size: 0,
1337
+ children: t3
1338
+ }), $[7] = t3, $[8] = t4);
1339
+ let t5;
1340
+ $[9] !== t2 || $[10] !== t4 ? (t5 = /* @__PURE__ */ jsxs(Stack, {
1341
+ gap: 2,
1342
+ children: [t2, t4]
1343
+ }), $[9] = t2, $[10] = t4, $[11] = t5) : t5 = $[11];
1344
+ let t6 = options.accent === void 0, t7 = `${name} accent`, t8, t9;
1345
+ $[12] === onChange ? (t8 = $[13], t9 = $[14]) : (t8 = (accent) => onChange({ accent }), t9 = () => onChange({ accent: void 0 }), $[12] = onChange, $[13] = t8, $[14] = t9);
1346
+ let t10;
1347
+ $[15] !== resolved.accent || $[16] !== t6 || $[17] !== t7 || $[18] !== t8 || $[19] !== t9 ? (t10 = /* @__PURE__ */ jsx(ColorRow, {
1348
+ auto: t6,
1349
+ autoLabel: "default",
1350
+ label: t7,
1351
+ onChange: t8,
1352
+ onClear: t9,
1353
+ title: "Accent",
1354
+ value: resolved.accent
1355
+ }), $[15] = resolved.accent, $[16] = t6, $[17] = t7, $[18] = t8, $[19] = t9, $[20] = t10) : t10 = $[20];
1356
+ let t11 = options.text === void 0, t12 = `${name} text`, t13, t14;
1357
+ $[21] === onChange ? (t13 = $[22], t14 = $[23]) : (t13 = (text) => onChange({ text }), t14 = () => onChange({ text: void 0 }), $[21] = onChange, $[22] = t13, $[23] = t14);
1358
+ let t15;
1359
+ $[24] !== resolved.text || $[25] !== t11 || $[26] !== t12 || $[27] !== t13 || $[28] !== t14 ? (t15 = /* @__PURE__ */ jsx(ColorRow, {
1360
+ auto: t11,
1361
+ label: t12,
1362
+ onChange: t13,
1363
+ onClear: t14,
1364
+ title: "Text",
1365
+ value: resolved.text
1366
+ }), $[24] = resolved.text, $[25] = t11, $[26] = t12, $[27] = t13, $[28] = t14, $[29] = t15) : t15 = $[29];
1367
+ let t16 = scheme === "dark" ? palette.black : palette.white, t17 = options.background === void 0, t18 = `${name} background`, t19, t20;
1368
+ $[30] === onChange ? (t19 = $[31], t20 = $[32]) : (t19 = (background) => onChange({ background }), t20 = () => onChange({ background: void 0 }), $[30] = onChange, $[31] = t19, $[32] = t20);
1369
+ let t21;
1370
+ $[33] !== resolved.background || $[34] !== t16 || $[35] !== t17 || $[36] !== t18 || $[37] !== t19 || $[38] !== t20 ? (t21 = /* @__PURE__ */ jsx(ColorRow, {
1371
+ adjusted: t16,
1372
+ auto: t17,
1373
+ label: t18,
1374
+ onChange: t19,
1375
+ onClear: t20,
1376
+ title: "Background",
1377
+ value: resolved.background
1378
+ }), $[33] = resolved.background, $[34] = t16, $[35] = t17, $[36] = t18, $[37] = t19, $[38] = t20, $[39] = t21) : t21 = $[39];
1379
+ let t22;
1380
+ $[40] === Symbol.for("react.memo_cache_sentinel") ? (t22 = /* @__PURE__ */ jsx(Text, {
1381
+ size: 1,
1382
+ children: "Contrast"
1383
+ }), $[40] = t22) : t22 = $[40];
1384
+ let t23 = options.contrast === void 0 ? " · auto" : "", t24;
1385
+ $[41] !== resolved.contrast || $[42] !== t23 ? (t24 = /* @__PURE__ */ jsxs(Stack, {
1386
+ flex: 1,
1387
+ gap: 2,
1388
+ children: [t22, /* @__PURE__ */ jsxs(Text, {
1389
+ muted: !0,
1390
+ size: 0,
1391
+ children: [resolved.contrast, t23]
1392
+ })]
1393
+ }), $[41] = resolved.contrast, $[42] = t23, $[43] = t24) : t24 = $[43];
1394
+ let t25;
1395
+ $[44] !== onChange || $[45] !== options.contrast ? (t25 = options.contrast !== void 0 && /* @__PURE__ */ jsx(TooltipButton, {
1396
+ icon: ResetIcon,
1397
+ mode: "bleed",
1398
+ onClick: () => onChange({ contrast: void 0 }),
1399
+ padding: 2,
1400
+ tooltip: "Reset to auto"
1401
+ }), $[44] = onChange, $[45] = options.contrast, $[46] = t25) : t25 = $[46];
1402
+ let t26;
1403
+ $[47] !== t24 || $[48] !== t25 ? (t26 = /* @__PURE__ */ jsxs(Flex, {
1404
+ align: "center",
1405
+ gap: 2,
1406
+ children: [t24, t25]
1407
+ }), $[47] = t24, $[48] = t25, $[49] = t26) : t26 = $[49];
1408
+ let t27 = `${name} contrast`, t28;
1409
+ $[50] === onChange ? t28 = $[51] : (t28 = (event) => onChange({ contrast: Number(event.currentTarget.value) }), $[50] = onChange, $[51] = t28);
1410
+ let t29;
1411
+ $[52] !== resolved.contrast || $[53] !== t27 || $[54] !== t28 ? (t29 = /* @__PURE__ */ jsx(Range, {
1412
+ "aria-label": t27,
1413
+ max: 100,
1414
+ min: 15,
1415
+ onChange: t28,
1416
+ step: 1,
1417
+ type: "range",
1418
+ value: resolved.contrast
1419
+ }), $[52] = resolved.contrast, $[53] = t27, $[54] = t28, $[55] = t29) : t29 = $[55];
1420
+ let t30;
1421
+ $[56] === Symbol.for("react.memo_cache_sentinel") ? (t30 = /* @__PURE__ */ jsx(Text, {
1422
+ muted: !0,
1423
+ size: 0,
1424
+ children: "100 keeps text and borders neutral — lower values blend in the accent"
1425
+ }), $[56] = t30) : t30 = $[56];
1426
+ let t31;
1427
+ $[57] !== t26 || $[58] !== t29 ? (t31 = /* @__PURE__ */ jsxs(Stack, {
1428
+ gap: 3,
1429
+ children: [
1430
+ t26,
1431
+ t29,
1432
+ t30
1433
+ ]
1434
+ }), $[57] = t26, $[58] = t29, $[59] = t31) : t31 = $[59];
1435
+ let t32;
1436
+ $[60] !== t10 || $[61] !== t15 || $[62] !== t21 || $[63] !== t31 || $[64] !== t5 ? (t32 = /* @__PURE__ */ jsxs(Stack, {
1437
+ gap: 4,
1438
+ children: [
1439
+ t5,
1440
+ t10,
1441
+ t15,
1442
+ t21,
1443
+ t31
1444
+ ]
1445
+ }), $[60] = t10, $[61] = t15, $[62] = t21, $[63] = t31, $[64] = t5, $[65] = t32) : t32 = $[65];
1446
+ let t33;
1447
+ return $[66] !== scheme || $[67] !== t32 ? (t33 = /* @__PURE__ */ jsx(Card, {
1448
+ border: !0,
1449
+ padding: 3,
1450
+ radius: 3,
1451
+ scheme,
1452
+ tone: "default",
1453
+ children: t32
1454
+ }), $[66] = scheme, $[67] = t32, $[68] = t33) : t33 = $[68], t33;
1455
+ }
1456
+ /** One color of a scheme: a swatch with its value and an optional reset button */
1457
+ function ColorRow(props) {
1458
+ let $ = c(24), { adjusted, auto, autoLabel: t0, label, onChange, onClear, title, value } = props, autoLabel = t0 === void 0 ? "auto" : t0, t1;
1459
+ $[0] === onChange ? t1 = $[1] : (t1 = (event) => onChange(event.currentTarget.value), $[0] = onChange, $[1] = t1);
1460
+ let t2;
1461
+ $[2] !== label || $[3] !== t1 || $[4] !== value ? (t2 = /* @__PURE__ */ jsx(Swatch, {
1462
+ "aria-label": label,
1463
+ onChange: t1,
1464
+ type: "color",
1465
+ value
1466
+ }), $[2] = label, $[3] = t1, $[4] = value, $[5] = t2) : t2 = $[5];
1467
+ let t3;
1468
+ $[6] === Symbol.for("react.memo_cache_sentinel") ? (t3 = { minWidth: 0 }, $[6] = t3) : t3 = $[6];
1469
+ let t4;
1470
+ $[7] === title ? t4 = $[8] : (t4 = /* @__PURE__ */ jsx(Text, {
1471
+ size: 1,
1472
+ children: title
1473
+ }), $[7] = title, $[8] = t4);
1474
+ let t5 = adjusted !== void 0 && adjusted !== value ? ` → ${adjusted}` : "", t6 = auto ? ` · ${autoLabel}` : "", t7;
1475
+ $[9] !== t5 || $[10] !== t6 || $[11] !== value ? (t7 = /* @__PURE__ */ jsxs(Text, {
1476
+ muted: !0,
1477
+ size: 0,
1478
+ textOverflow: "ellipsis",
1479
+ children: [
1480
+ value,
1481
+ t5,
1482
+ t6
1483
+ ]
1484
+ }), $[9] = t5, $[10] = t6, $[11] = value, $[12] = t7) : t7 = $[12];
1485
+ let t8;
1486
+ $[13] !== t4 || $[14] !== t7 ? (t8 = /* @__PURE__ */ jsxs(Stack, {
1487
+ flex: 1,
1488
+ gap: 2,
1489
+ style: t3,
1490
+ children: [t4, t7]
1491
+ }), $[13] = t4, $[14] = t7, $[15] = t8) : t8 = $[15];
1492
+ let t9;
1493
+ $[16] !== auto || $[17] !== autoLabel || $[18] !== onClear ? (t9 = onClear && !auto && /* @__PURE__ */ jsx(TooltipButton, {
1494
+ icon: ResetIcon,
1495
+ mode: "bleed",
1496
+ onClick: onClear,
1497
+ padding: 2,
1498
+ tooltip: `Reset to ${autoLabel}`
1499
+ }), $[16] = auto, $[17] = autoLabel, $[18] = onClear, $[19] = t9) : t9 = $[19];
1500
+ let t10;
1501
+ return $[20] !== t2 || $[21] !== t8 || $[22] !== t9 ? (t10 = /* @__PURE__ */ jsxs(Flex, {
1502
+ align: "center",
1503
+ gap: 2,
1504
+ children: [
1505
+ t2,
1506
+ t8,
1507
+ t9
1508
+ ]
1509
+ }), $[20] = t2, $[21] = t8, $[22] = t9, $[23] = t10) : t10 = $[23], t10;
1510
+ }
1511
+ const Root = styled.div.withConfig({
1512
+ displayName: "Root",
1513
+ componentId: "sc-a1a6d4-0"
1514
+ })`position:relative;`, PickButton = styled.button.withConfig({
1515
+ displayName: "PickButton",
1516
+ componentId: "sc-a1a6d4-1"
1517
+ })`appearance:none;display:block;box-sizing:border-box;width:100%;margin:0;padding:0;border:0;background:none;color:inherit;font:inherit;text-align:center;cursor:pointer;&:focus{outline:none;}`, Frame = styled.span.withConfig({
1518
+ displayName: "Frame",
1519
+ componentId: "sc-a1a6d4-2"
1520
+ })`display:block;padding:4px;border-radius:10px;transition:box-shadow 100ms;${PickButton}:hover &{box-shadow:inset 0 0 0 2px var(--card-border-color);}${PickButton}[aria-pressed='true'] &{box-shadow:inset 0 0 0 2px var(--card-focus-ring-color);}${PickButton}:focus-visible &{outline:2px solid var(--card-focus-ring-color);outline-offset:-2px;}`, MenuSlot = styled.div.withConfig({
1521
+ displayName: "MenuSlot",
1522
+ componentId: "sc-a1a6d4-3"
1523
+ })`position:absolute;top:10px;right:10px;opacity:0;transition:opacity 100ms;${Root}:hover &,${Root}:focus-within &,&[data-visible='true']{opacity:1;}`;
1524
+ /**
1525
+ * One theme in the list: the floating preview with the title below it, like
1526
+ * an appearance option in macOS System Settings. Clicking it applies the
1527
+ * theme to the whole Studio; its menu holds the edit, duplicate and remove
1528
+ * flows that apply to the theme.
1529
+ *
1530
+ * @internal
1531
+ */
1532
+ function ThemeCard(props) {
1533
+ let $ = c(50), { active, theme } = props, { send } = useThemer(), [menuOpen, setMenuOpen] = useState(!1), t0;
1534
+ $[0] === theme.title ? t0 = $[1] : (t0 = displayTitle(theme.title), $[0] = theme.title, $[1] = t0);
1535
+ let title = t0, t1;
1536
+ $[2] !== send || $[3] !== theme.slug ? (t1 = () => send({
1537
+ type: "theme.pick",
1538
+ slug: theme.slug
1539
+ }), $[2] = send, $[3] = theme.slug, $[4] = t1) : t1 = $[4];
1540
+ let t2;
1541
+ $[5] === theme.options ? t2 = $[6] : (t2 = /* @__PURE__ */ jsx(Frame, { children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: theme.options }) }), $[5] = theme.options, $[6] = t2);
1542
+ let t3 = active ? "medium" : "regular", t4;
1543
+ $[7] !== t3 || $[8] !== title ? (t4 = /* @__PURE__ */ jsx(Box, {
1544
+ as: "span",
1545
+ display: "block",
1546
+ paddingTop: 2,
1547
+ paddingX: 2,
1548
+ children: /* @__PURE__ */ jsx(Text, {
1549
+ align: "center",
1550
+ as: "span",
1551
+ size: 1,
1552
+ textOverflow: "ellipsis",
1553
+ weight: t3,
1554
+ children: title
1555
+ })
1556
+ }), $[7] = t3, $[8] = title, $[9] = t4) : t4 = $[9];
1557
+ let t5;
1558
+ $[10] !== active || $[11] !== t1 || $[12] !== t2 || $[13] !== t4 ? (t5 = /* @__PURE__ */ jsxs(PickButton, {
1559
+ "aria-pressed": active,
1560
+ onClick: t1,
1561
+ type: "button",
1562
+ children: [t2, t4]
1563
+ }), $[10] = active, $[11] = t1, $[12] = t2, $[13] = t4, $[14] = t5) : t5 = $[14];
1564
+ let t6;
1565
+ $[15] === Symbol.for("react.memo_cache_sentinel") ? (t6 = /* @__PURE__ */ jsx(Text, {
1566
+ size: 1,
1567
+ children: "Show more"
1568
+ }), $[15] = t6) : t6 = $[15];
1569
+ let t7;
1570
+ $[16] === Symbol.for("react.memo_cache_sentinel") ? (t7 = { display: "inline-block" }, $[16] = t7) : t7 = $[16];
1571
+ let t8 = `Show more for ${title}`, t9;
1572
+ $[17] === t8 ? t9 = $[18] : (t9 = /* @__PURE__ */ jsx(Button, {
1573
+ "aria-label": t8,
1574
+ fontSize: 1,
1575
+ icon: EllipsisHorizontalIcon,
1576
+ mode: "ghost",
1577
+ padding: 1,
1578
+ radius: 2
1579
+ }), $[17] = t8, $[18] = t9);
1580
+ let t10 = `themer-theme-${theme.slug}`, t11;
1581
+ $[19] !== send || $[20] !== theme.slug || $[21] !== theme.source ? (t11 = theme.source === "custom" && /* @__PURE__ */ jsx(MenuItem, {
1582
+ icon: EditIcon,
1583
+ onClick: () => send({
1584
+ type: "theme.edit",
1585
+ slug: theme.slug
1586
+ }),
1587
+ text: "Edit"
1588
+ }), $[19] = send, $[20] = theme.slug, $[21] = theme.source, $[22] = t11) : t11 = $[22];
1589
+ let t12;
1590
+ $[23] !== send || $[24] !== theme.slug ? (t12 = () => send({
1591
+ type: "theme.duplicate",
1592
+ slug: theme.slug
1593
+ }), $[23] = send, $[24] = theme.slug, $[25] = t12) : t12 = $[25];
1594
+ let t13 = theme.source === "custom" ? "Duplicate" : "Duplicate to edit", t14;
1595
+ $[26] !== t12 || $[27] !== t13 ? (t14 = /* @__PURE__ */ jsx(MenuItem, {
1596
+ icon: CopyIcon,
1597
+ onClick: t12,
1598
+ text: t13
1599
+ }), $[26] = t12, $[27] = t13, $[28] = t14) : t14 = $[28];
1600
+ let t15;
1601
+ $[29] !== send || $[30] !== theme.slug || $[31] !== theme.source ? (t15 = theme.source !== "config" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuDivider, {}), /* @__PURE__ */ jsx(MenuItem, {
1602
+ icon: TrashIcon,
1603
+ onClick: () => send({
1604
+ type: "theme.remove",
1605
+ slug: theme.slug
1606
+ }),
1607
+ text: "Remove",
1608
+ tone: "critical"
1609
+ })] }), $[29] = send, $[30] = theme.slug, $[31] = theme.source, $[32] = t15) : t15 = $[32];
1610
+ let t16;
1611
+ $[33] !== t11 || $[34] !== t14 || $[35] !== t15 ? (t16 = /* @__PURE__ */ jsxs(Menu, { children: [
1612
+ t11,
1613
+ t14,
1614
+ t15
1615
+ ] }), $[33] = t11, $[34] = t14, $[35] = t15, $[36] = t16) : t16 = $[36];
1616
+ let t17, t18, t19;
1617
+ $[37] === Symbol.for("react.memo_cache_sentinel") ? (t17 = () => setMenuOpen(!1), t18 = () => setMenuOpen(!0), t19 = {
1618
+ animate: !0,
1619
+ placement: "bottom-end",
1620
+ portal: !0
1621
+ }, $[37] = t17, $[38] = t18, $[39] = t19) : (t17 = $[37], t18 = $[38], t19 = $[39]);
1622
+ let t20;
1623
+ $[40] !== t10 || $[41] !== t16 || $[42] !== t9 ? (t20 = /* @__PURE__ */ jsx(Tooltip, {
1624
+ animate: !0,
1625
+ content: t6,
1626
+ placement: "bottom",
1627
+ portal: !0,
1628
+ children: /* @__PURE__ */ jsx("span", {
1629
+ style: t7,
1630
+ children: /* @__PURE__ */ jsx(MenuButton, {
1631
+ button: t9,
1632
+ id: t10,
1633
+ menu: t16,
1634
+ onClose: t17,
1635
+ onOpen: t18,
1636
+ popover: t19
1637
+ })
1638
+ })
1639
+ }), $[40] = t10, $[41] = t16, $[42] = t9, $[43] = t20) : t20 = $[43];
1640
+ let t21;
1641
+ $[44] !== menuOpen || $[45] !== t20 ? (t21 = /* @__PURE__ */ jsx(MenuSlot, {
1642
+ "data-visible": menuOpen,
1643
+ children: t20
1644
+ }), $[44] = menuOpen, $[45] = t20, $[46] = t21) : t21 = $[46];
1645
+ let t22;
1646
+ return $[47] !== t21 || $[48] !== t5 ? (t22 = /* @__PURE__ */ jsxs(Root, { children: [t5, t21] }), $[47] = t21, $[48] = t5, $[49] = t22) : t22 = $[49], t22;
1647
+ }
1648
+ /**
1649
+ * The flow for picking a theme: one column of theme cards — the configured
1650
+ * theme, the presets and the user's own themes — with the entry points to
1651
+ * the add and restore flows below.
1652
+ *
1653
+ * @internal
1654
+ */
1655
+ function ThemeList() {
1656
+ let $ = c(24), { themes, removed, active, send } = useThemer(), t0;
1657
+ $[0] === send ? t0 = $[1] : (t0 = (palette, file) => send({
1658
+ type: "theme.add",
1659
+ title: titleFromFileName(file.name),
1660
+ options: optionsFromImagePalette(palette),
1661
+ palette,
1662
+ imageUrl: URL.createObjectURL(file)
1663
+ }), $[0] = send, $[1] = t0);
1664
+ let { busy, pickImage } = useImagePalette(t0), t1;
1665
+ if ($[2] !== active || $[3] !== themes) {
1666
+ let t2;
1667
+ $[5] === active ? t2 = $[6] : (t2 = (theme) => /* @__PURE__ */ jsx(ThemeCard, {
1668
+ active: theme.slug === active.slug,
1669
+ theme
1670
+ }, theme.slug), $[5] = active, $[6] = t2), t1 = themes.map(t2), $[2] = active, $[3] = themes, $[4] = t1;
1671
+ } else t1 = $[4];
1672
+ let t2;
1673
+ $[7] !== removed.length || $[8] !== send ? (t2 = removed.length > 0 && /* @__PURE__ */ jsx(Button, {
1674
+ icon: RestoreIcon,
1675
+ mode: "bleed",
1676
+ onClick: () => send({ type: "flow.removed" }),
1677
+ text: `Show removed (${removed.length})`,
1678
+ width: "fill"
1679
+ }), $[7] = removed.length, $[8] = send, $[9] = t2) : t2 = $[9];
1680
+ let t3;
1681
+ $[10] !== t1 || $[11] !== t2 ? (t3 = /* @__PURE__ */ jsx(ScrollArea, {
1682
+ padding: 3,
1683
+ children: /* @__PURE__ */ jsxs(Stack, {
1684
+ gap: 4,
1685
+ children: [t1, t2]
1686
+ })
1687
+ }), $[10] = t1, $[11] = t2, $[12] = t3) : t3 = $[12];
1688
+ let t4;
1689
+ $[13] === send ? t4 = $[14] : (t4 = /* @__PURE__ */ jsx(Box, {
1690
+ flex: 1,
1691
+ children: /* @__PURE__ */ jsx(TooltipButton, {
1692
+ icon: AddIcon,
1693
+ mode: "ghost",
1694
+ onClick: () => send({ type: "theme.add" }),
1695
+ text: "Add theme",
1696
+ tooltip: "Add a theme based on the applied one",
1697
+ width: "fill"
1698
+ })
1699
+ }), $[13] = send, $[14] = t4);
1700
+ let t5;
1701
+ $[15] !== busy || $[16] !== pickImage ? (t5 = /* @__PURE__ */ jsx(ImageFileButton, {
1702
+ loading: busy,
1703
+ mode: "ghost",
1704
+ onFile: pickImage,
1705
+ tooltip: "Add a theme from the colors of an image"
1706
+ }), $[15] = busy, $[16] = pickImage, $[17] = t5) : t5 = $[17];
1707
+ let t6;
1708
+ $[18] !== t4 || $[19] !== t5 ? (t6 = /* @__PURE__ */ jsx(Card, {
1709
+ borderTop: !0,
1710
+ padding: 3,
1711
+ children: /* @__PURE__ */ jsxs(Flex, {
1712
+ gap: 2,
1713
+ children: [t4, t5]
1714
+ })
1715
+ }), $[18] = t4, $[19] = t5, $[20] = t6) : t6 = $[20];
1716
+ let t7;
1717
+ return $[21] !== t3 || $[22] !== t6 ? (t7 = /* @__PURE__ */ jsxs(Fragment, { children: [t3, t6] }), $[21] = t3, $[22] = t6, $[23] = t7) : t7 = $[23], t7;
52
1718
  }
53
1719
  /**
54
1720
  * Serializes theme options into the `buildTheme` call to paste into
55
- * `sanity.config.ts`, keeping only what differs from the derived defaults.
1721
+ * `sanity.config.ts`, keeping only what differs from the derived defaults in
1722
+ * each scheme.
56
1723
  *
57
1724
  * Options that boil down to the stock Studio theme need nothing from this
58
1725
  * package — so they serialize to a bare `buildTheme()` from `@sanity/ui/theme`
@@ -63,77 +1730,35 @@ function sameOptions(a, b) {
63
1730
  function createThemeSnippet(options) {
64
1731
  let minimized = minimizeOptions(options);
65
1732
  if (minimized === null) return "import {buildTheme} from '@sanity/ui/theme'\n\nexport const theme = buildTheme()\n";
66
- let entries = [` accent: '${minimized.accent}',`];
67
- if (minimized.text !== void 0 && entries.push(` text: '${minimized.text}',`), minimized.background) {
68
- let parts = [];
69
- minimized.background.dark !== void 0 && parts.push(`dark: '${minimized.background.dark}'`), minimized.background.light !== void 0 && parts.push(`light: '${minimized.background.light}'`), entries.push(` background: {${parts.join(", ")}},`);
1733
+ let lines = [];
1734
+ for (let scheme of SCHEMES) {
1735
+ let values = minimized[scheme];
1736
+ values && lines.push(serializeScheme(scheme, values));
70
1737
  }
71
- return minimized.contrast !== void 0 && entries.push(` contrast: ${minimized.contrast},`), `import {buildTheme} from '@sanity/themer'\n\nexport const theme = buildTheme({\n${entries.join("\n")}\n})\n`;
1738
+ return `import {buildTheme} from '@sanity/themer'\n\nexport const theme = buildTheme({\n${lines.join("\n")}\n})\n`;
1739
+ }
1740
+ /** A scheme with up to two options fits on one line, more spread out */
1741
+ function serializeScheme(scheme, values) {
1742
+ let entries = [];
1743
+ 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 },`;
72
1744
  }
73
1745
  registerLanguage(typescript);
74
1746
  /**
75
- * `<input type="color">` paints the color into a shadow-DOM swatch that brings
76
- * its own border and padding, which then sits inside ours as a second border.
77
- * Stripping that chrome leaves the themed border as the only one.
78
- */
79
- const Swatch = styled.input.withConfig({
80
- displayName: "Swatch",
81
- componentId: "sc-7kf8xr-0"
82
- })`box-sizing:border-box;flex:none;width:33px;height:33px;padding:0;border:1px solid var(--card-border-color);border-radius:4px;background:none;cursor:pointer;&::-webkit-color-swatch-wrapper{padding:0;}&::-webkit-color-swatch{border:none;border-radius:3px;}&::-moz-color-swatch{border:none;border-radius:3px;}`, Range = styled.input.withConfig({
83
- displayName: "Range",
84
- componentId: "sc-7kf8xr-1"
85
- })`display:block;width:100%;margin:0;accent-color:var(--card-focus-ring-color);`;
86
- /**
87
- * The themer sidebar: presets, the accent/text/background pickers and the
88
- * contrast slider that generate the previewed `buildTheme` theme, plus the
89
- * `buildTheme` snippet to make it permanent.
1747
+ * Shows the `buildTheme` snippet that makes a theme permanent, in a dialog so
1748
+ * that it does not take up sidebar space or make the sidebar scroll as the
1749
+ * snippet changes shape with the theme.
90
1750
  *
91
1751
  * @internal
92
1752
  */
93
- function ThemerSidebar() {
94
- let $ = c(117), { baseOptions, options, setOptions, setOpen } = useThemer(), toast = useToast(), active = options ?? baseOptions, t0;
95
- $[0] === active ? t0 = $[1] : (t0 = resolveThemeOptions(active), $[0] = active, $[1] = t0);
96
- let resolved = t0, t1;
97
- $[2] === active ? t1 = $[3] : (t1 = buildPalette(active), $[2] = active, $[3] = t1);
98
- let palette = t1, t2;
99
- $[4] === active ? t2 = $[5] : (t2 = presets.find((preset) => sameOptions(preset.options, active))?.slug ?? "", $[4] = active, $[5] = t2);
100
- let activePresetSlug = t2, t3;
101
- $[6] === active ? t3 = $[7] : (t3 = createThemeSnippet(active), $[6] = active, $[7] = t3);
102
- let snippet = t3, t4;
103
- $[8] !== active || $[9] !== setOptions ? (t4 = (changes) => {
104
- setOptions({
105
- ...active,
106
- ...changes
107
- });
108
- }, $[8] = active, $[9] = setOptions, $[10] = t4) : t4 = $[10];
109
- let patch = t4, t5;
110
- $[11] !== active || $[12] !== setOptions ? (t5 = (changes_0) => {
111
- setOptions({
112
- ...active,
113
- background: {
114
- ...active.background,
115
- ...changes_0
116
- }
117
- });
118
- }, $[11] = active, $[12] = setOptions, $[13] = t5) : t5 = $[13];
119
- let patchBackground = t5, t6;
120
- $[14] !== active || $[15] !== setOptions ? (t6 = (field) => {
121
- let next = { ...active };
122
- delete next[field], setOptions(next);
123
- }, $[14] = active, $[15] = setOptions, $[16] = t6) : t6 = $[16];
124
- let clearField = t6, t7;
125
- $[17] !== active || $[18] !== setOptions ? (t7 = (key) => {
126
- let background = { ...active.background };
127
- delete background[key];
128
- let next_0 = { ...active };
129
- background.dark === void 0 && background.light === void 0 ? delete next_0.background : next_0.background = background, setOptions(next_0);
130
- }, $[17] = active, $[18] = setOptions, $[19] = t7) : t7 = $[19];
131
- let clearBackground = t7, t8;
132
- $[20] !== snippet || $[21] !== toast ? (t8 = async () => {
1753
+ function ThemeSnippetDialog(props) {
1754
+ let $ = c(23), { onClose, theme } = props, toast = useToast(), t0;
1755
+ $[0] === theme.options ? t0 = $[1] : (t0 = createThemeSnippet(theme.options), $[0] = theme.options, $[1] = t0);
1756
+ let snippet = t0, t1;
1757
+ $[2] !== snippet || $[3] !== toast ? (t1 = async () => {
133
1758
  try {
134
1759
  await navigator.clipboard.writeText(snippet), toast.push({
135
1760
  status: "success",
136
- title: "Copied theme to the clipboard"
1761
+ title: "Copied the theme to the clipboard"
137
1762
  });
138
1763
  } catch {
139
1764
  toast.push({
@@ -141,515 +1766,705 @@ function ThemerSidebar() {
141
1766
  title: "Could not copy the theme"
142
1767
  });
143
1768
  }
144
- }, $[20] = snippet, $[21] = toast, $[22] = t8) : t8 = $[22];
145
- let handleCopy = t8, t9;
146
- $[23] === Symbol.for("react.memo_cache_sentinel") ? (t9 = /* @__PURE__ */ jsx(Box, {
147
- flex: 1,
148
- paddingLeft: 1,
149
- children: /* @__PURE__ */ jsx(Text, {
150
- size: 1,
151
- weight: "semibold",
152
- children: "Themer"
153
- })
154
- }), $[23] = t9) : t9 = $[23];
155
- let t10;
156
- $[24] === setOpen ? t10 = $[25] : (t10 = /* @__PURE__ */ jsx(Card, {
157
- borderBottom: !0,
1769
+ }, $[2] = snippet, $[3] = toast, $[4] = t1) : t1 = $[4];
1770
+ let handleCopy = t1, t2;
1771
+ $[5] === handleCopy ? t2 = $[6] : (t2 = /* @__PURE__ */ jsx(Flex, {
1772
+ gap: 2,
1773
+ justify: "flex-end",
158
1774
  padding: 3,
159
- children: /* @__PURE__ */ jsxs(Flex, {
160
- align: "center",
161
- gap: 2,
162
- children: [t9, /* @__PURE__ */ jsx(Button, {
163
- icon: CloseIcon,
164
- mode: "bleed",
165
- onClick: () => setOpen(!1),
166
- padding: 2,
167
- title: "Close themer"
168
- })]
1775
+ children: /* @__PURE__ */ jsx(Button, {
1776
+ icon: ClipboardIcon,
1777
+ mode: "ghost",
1778
+ onClick: () => void handleCopy(),
1779
+ text: "Copy"
169
1780
  })
170
- }), $[24] = setOpen, $[25] = t10);
171
- let t11;
172
- $[26] === Symbol.for("react.memo_cache_sentinel") ? (t11 = /* @__PURE__ */ jsx(Text, {
173
- size: 1,
174
- weight: "medium",
175
- children: "Presets"
176
- }), $[26] = t11) : t11 = $[26];
177
- let t12;
178
- $[27] !== activePresetSlug || $[28] !== setOptions ? (t12 = presets.map((preset_0) => /* @__PURE__ */ jsx(PresetButton, {
179
- active: preset_0.slug === activePresetSlug,
180
- onClick: () => setOptions(preset_0.options),
181
- preset: preset_0
182
- }, preset_0.slug)), $[27] = activePresetSlug, $[28] = setOptions, $[29] = t12) : t12 = $[29];
183
- let t13;
184
- $[30] === t12 ? t13 = $[31] : (t13 = /* @__PURE__ */ jsxs(Stack, {
185
- gap: 3,
186
- children: [t11, /* @__PURE__ */ jsx(Grid, {
187
- gap: 2,
188
- gridTemplateColumns: 2,
189
- children: t12
190
- })]
191
- }), $[30] = t12, $[31] = t13);
192
- let t14;
193
- $[32] === Symbol.for("react.memo_cache_sentinel") ? (t14 = /* @__PURE__ */ jsx(Text, {
194
- size: 1,
195
- weight: "medium",
196
- children: "Colors"
197
- }), $[32] = t14) : t14 = $[32];
198
- let t15;
199
- $[33] === patch ? t15 = $[34] : (t15 = (accent) => patch({ accent }), $[33] = patch, $[34] = t15);
200
- let t16;
201
- $[35] !== palette.blue || $[36] !== resolved.accent || $[37] !== t15 ? (t16 = /* @__PURE__ */ jsx(ColorRow, {
202
- onChange: t15,
203
- tints: palette.blue,
204
- title: "Accent",
205
- value: resolved.accent
206
- }), $[35] = palette.blue, $[36] = resolved.accent, $[37] = t15, $[38] = t16) : t16 = $[38];
207
- let t17 = active.text === void 0, t18;
208
- $[39] === patch ? t18 = $[40] : (t18 = (text) => patch({ text }), $[39] = patch, $[40] = t18);
209
- let t19;
210
- $[41] === clearField ? t19 = $[42] : (t19 = () => clearField("text"), $[41] = clearField, $[42] = t19);
211
- let t20;
212
- $[43] !== palette.gray || $[44] !== resolved.text || $[45] !== t17 || $[46] !== t18 || $[47] !== t19 ? (t20 = /* @__PURE__ */ jsx(ColorRow, {
213
- auto: t17,
214
- onChange: t18,
215
- onClear: t19,
216
- tints: palette.gray,
217
- title: "Text",
218
- value: resolved.text
219
- }), $[43] = palette.gray, $[44] = resolved.text, $[45] = t17, $[46] = t18, $[47] = t19, $[48] = t20) : t20 = $[48];
220
- let t21 = active.background?.dark === void 0, t22;
221
- $[49] === patchBackground ? t22 = $[50] : (t22 = (dark) => patchBackground({ dark }), $[49] = patchBackground, $[50] = t22);
222
- let t23;
223
- $[51] === clearBackground ? t23 = $[52] : (t23 = () => clearBackground("dark"), $[51] = clearBackground, $[52] = t23);
224
- let t24;
225
- $[53] !== palette.black || $[54] !== resolved.background.dark || $[55] !== t21 || $[56] !== t22 || $[57] !== t23 ? (t24 = /* @__PURE__ */ jsx(ColorRow, {
226
- adjusted: palette.black,
227
- auto: t21,
228
- onChange: t22,
229
- onClear: t23,
230
- title: "Background · dark",
231
- value: resolved.background.dark
232
- }), $[53] = palette.black, $[54] = resolved.background.dark, $[55] = t21, $[56] = t22, $[57] = t23, $[58] = t24) : t24 = $[58];
233
- let t25 = active.background?.light === void 0, t26;
234
- $[59] === patchBackground ? t26 = $[60] : (t26 = (light) => patchBackground({ light }), $[59] = patchBackground, $[60] = t26);
235
- let t27;
236
- $[61] === clearBackground ? t27 = $[62] : (t27 = () => clearBackground("light"), $[61] = clearBackground, $[62] = t27);
237
- let t28;
238
- $[63] !== palette.white || $[64] !== resolved.background.light || $[65] !== t25 || $[66] !== t26 || $[67] !== t27 ? (t28 = /* @__PURE__ */ jsx(ColorRow, {
239
- adjusted: palette.white,
240
- auto: t25,
241
- onChange: t26,
242
- onClear: t27,
243
- title: "Background · light",
244
- value: resolved.background.light
245
- }), $[63] = palette.white, $[64] = resolved.background.light, $[65] = t25, $[66] = t26, $[67] = t27, $[68] = t28) : t28 = $[68];
246
- let t29;
247
- $[69] !== t16 || $[70] !== t20 || $[71] !== t24 || $[72] !== t28 ? (t29 = /* @__PURE__ */ jsxs(Stack, {
248
- gap: 3,
249
- children: [t14, /* @__PURE__ */ jsxs(Stack, {
250
- gap: 4,
251
- children: [
252
- t16,
253
- t20,
254
- t24,
255
- t28
256
- ]
257
- })]
258
- }), $[69] = t16, $[70] = t20, $[71] = t24, $[72] = t28, $[73] = t29) : t29 = $[73];
259
- let t30;
260
- $[74] === Symbol.for("react.memo_cache_sentinel") ? (t30 = /* @__PURE__ */ jsx(Text, {
261
- size: 1,
262
- weight: "medium",
263
- children: "Contrast"
264
- }), $[74] = t30) : t30 = $[74];
265
- let t31 = active.contrast === void 0 ? " · auto" : "", t32;
266
- $[75] !== resolved.contrast || $[76] !== t31 ? (t32 = /* @__PURE__ */ jsxs(Stack, {
267
- flex: 1,
268
- gap: 2,
269
- children: [t30, /* @__PURE__ */ jsxs(Text, {
270
- muted: !0,
271
- size: 0,
272
- children: [resolved.contrast, t31]
273
- })]
274
- }), $[75] = resolved.contrast, $[76] = t31, $[77] = t32) : t32 = $[77];
275
- let t33;
276
- $[78] !== active.contrast || $[79] !== clearField ? (t33 = active.contrast !== void 0 && /* @__PURE__ */ jsx(Button, {
277
- icon: ResetIcon,
278
- mode: "bleed",
279
- onClick: () => clearField("contrast"),
280
- padding: 2,
281
- title: "Reset to auto"
282
- }), $[78] = active.contrast, $[79] = clearField, $[80] = t33) : t33 = $[80];
283
- let t34;
284
- $[81] !== t32 || $[82] !== t33 ? (t34 = /* @__PURE__ */ jsxs(Flex, {
285
- align: "center",
286
- gap: 2,
287
- children: [t32, t33]
288
- }), $[81] = t32, $[82] = t33, $[83] = t34) : t34 = $[83];
289
- let t35;
290
- $[84] === patch ? t35 = $[85] : (t35 = (event) => patch({ contrast: Number(event.currentTarget.value) }), $[84] = patch, $[85] = t35);
291
- let t36;
292
- $[86] !== resolved.contrast || $[87] !== t35 ? (t36 = /* @__PURE__ */ jsx(Range, {
293
- "aria-label": "Contrast",
294
- max: 100,
295
- min: 15,
296
- onChange: t35,
297
- step: 1,
298
- type: "range",
299
- value: resolved.contrast
300
- }), $[86] = resolved.contrast, $[87] = t35, $[88] = t36) : t36 = $[88];
301
- let t37;
302
- $[89] === Symbol.for("react.memo_cache_sentinel") ? (t37 = /* @__PURE__ */ jsx(Text, {
1781
+ }), $[5] = handleCopy, $[6] = t2);
1782
+ let t3;
1783
+ $[7] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx("code", { children: "sanity.config.ts" }), $[7] = t3) : t3 = $[7];
1784
+ let t4;
1785
+ $[8] === Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx("code", { children: "theme" }), $[8] = t4) : t4 = $[8];
1786
+ let t5;
1787
+ $[9] === Symbol.for("react.memo_cache_sentinel") ? (t5 = /* @__PURE__ */ jsx("code", { children: "defineConfig" }), $[9] = t5) : t5 = $[9];
1788
+ let t6;
1789
+ $[10] === theme.title ? t6 = $[11] : (t6 = displayTitle(theme.title), $[10] = theme.title, $[11] = t6);
1790
+ let t7;
1791
+ $[12] === t6 ? t7 = $[13] : (t7 = /* @__PURE__ */ jsxs(Text, {
303
1792
  muted: !0,
304
- size: 0,
305
- children: "100 keeps text and borders neutral — lower values blend in the accent"
306
- }), $[89] = t37) : t37 = $[89];
307
- let t38;
308
- $[90] !== t34 || $[91] !== t36 ? (t38 = /* @__PURE__ */ jsxs(Stack, {
309
- gap: 3,
1793
+ size: 1,
310
1794
  children: [
311
- t34,
312
- t36,
313
- t37
1795
+ "Export this from ",
1796
+ t3,
1797
+ " and pass it as the ",
1798
+ t4,
1799
+ " of",
1800
+ " ",
1801
+ t5,
1802
+ " to make “",
1803
+ t6,
1804
+ "” the Studio theme without this tool."
314
1805
  ]
315
- }), $[90] = t34, $[91] = t36, $[92] = t38) : t38 = $[92];
316
- let t39;
317
- $[93] === Symbol.for("react.memo_cache_sentinel") ? (t39 = /* @__PURE__ */ jsx(Text, {
318
- size: 1,
319
- weight: "medium",
320
- children: "Add to your config"
321
- }), $[93] = t39) : t39 = $[93];
322
- let t40;
323
- $[94] === snippet ? t40 = $[95] : (t40 = /* @__PURE__ */ jsx(Card, {
1806
+ }), $[12] = t6, $[13] = t7);
1807
+ let t8;
1808
+ $[14] === snippet ? t8 = $[15] : (t8 = /* @__PURE__ */ jsx(Card, {
324
1809
  border: !0,
325
1810
  overflow: "auto",
326
- padding: 2,
1811
+ padding: 3,
327
1812
  radius: 2,
328
1813
  tone: "transparent",
329
1814
  children: /* @__PURE__ */ jsx(Code, {
330
1815
  language: "ts",
331
- size: 0,
1816
+ size: 1,
332
1817
  children: snippet
333
1818
  })
334
- }), $[94] = snippet, $[95] = t40);
335
- let t41;
336
- $[96] === handleCopy ? t41 = $[97] : (t41 = /* @__PURE__ */ jsx(Button, {
337
- icon: ClipboardIcon,
338
- mode: "ghost",
339
- onClick: () => void handleCopy(),
340
- text: "Copy"
341
- }), $[96] = handleCopy, $[97] = t41);
342
- let t42 = options === null, t43;
343
- $[98] === setOptions ? t43 = $[99] : (t43 = () => setOptions(null), $[98] = setOptions, $[99] = t43);
344
- let t44;
345
- $[100] !== t42 || $[101] !== t43 ? (t44 = /* @__PURE__ */ jsx(Button, {
346
- disabled: t42,
347
- icon: ResetIcon,
348
- mode: "ghost",
349
- onClick: t43,
350
- text: "Reset",
351
- tone: "critical"
352
- }), $[100] = t42, $[101] = t43, $[102] = t44) : t44 = $[102];
353
- let t45;
354
- $[103] !== t41 || $[104] !== t44 ? (t45 = /* @__PURE__ */ jsxs(Flex, {
355
- gap: 2,
356
- children: [t41, t44]
357
- }), $[103] = t41, $[104] = t44, $[105] = t45) : t45 = $[105];
358
- let t46;
359
- $[106] !== t40 || $[107] !== t45 ? (t46 = /* @__PURE__ */ jsxs(Stack, {
360
- gap: 3,
361
- children: [
362
- t39,
363
- t40,
364
- t45
365
- ]
366
- }), $[106] = t40, $[107] = t45, $[108] = t46) : t46 = $[108];
367
- let t47;
368
- $[109] !== t13 || $[110] !== t29 || $[111] !== t38 || $[112] !== t46 ? (t47 = /* @__PURE__ */ jsx(Box, {
369
- flex: 1,
370
- overflow: "auto",
371
- padding: 3,
1819
+ }), $[14] = snippet, $[15] = t8);
1820
+ let t9;
1821
+ $[16] !== t7 || $[17] !== t8 ? (t9 = /* @__PURE__ */ jsx(Box, {
1822
+ padding: 4,
372
1823
  children: /* @__PURE__ */ jsxs(Stack, {
373
1824
  gap: 4,
374
- children: [
375
- t13,
376
- t29,
377
- t38,
378
- t46
379
- ]
380
- })
381
- }), $[109] = t13, $[110] = t29, $[111] = t38, $[112] = t46, $[113] = t47) : t47 = $[113];
382
- let t48;
383
- return $[114] !== t10 || $[115] !== t47 ? (t48 = /* @__PURE__ */ jsx(Card, {
384
- height: "fill",
385
- children: /* @__PURE__ */ jsxs(Flex, {
386
- direction: "column",
387
- height: "fill",
388
- children: [t10, t47]
1825
+ children: [t7, t8]
389
1826
  })
390
- }), $[114] = t10, $[115] = t47, $[116] = t48) : t48 = $[116], t48;
391
- }
392
- const paletteStyle = {
393
- display: "flex",
394
- gap: 1,
395
- background: "var(--card-border-color)",
396
- height: 21,
397
- borderRadius: 3,
398
- overflow: "hidden",
399
- boxShadow: "inset 0 0 0 1px var(--card-border-color)"
400
- };
401
- /** A little palette of the colors a preset would apply */
402
- function PresetButton(props) {
403
- let $ = c(27), { active, onClick, preset } = props, t0;
404
- $[0] === preset.options ? t0 = $[1] : (t0 = resolveThemeOptions(preset.options), $[0] = preset.options, $[1] = t0);
405
- let resolved = t0, t1;
406
- $[2] === resolved.accent ? t1 = $[3] : (t1 = ["accent", resolved.accent], $[2] = resolved.accent, $[3] = t1);
407
- let t2;
408
- $[4] === resolved.text ? t2 = $[5] : (t2 = ["text", resolved.text], $[4] = resolved.text, $[5] = t2);
409
- let t3;
410
- $[6] === resolved.background.dark ? t3 = $[7] : (t3 = ["dark", resolved.background.dark], $[6] = resolved.background.dark, $[7] = t3);
411
- let t4;
412
- $[8] === resolved.background.light ? t4 = $[9] : (t4 = ["light", resolved.background.light], $[8] = resolved.background.light, $[9] = t4);
413
- let t5;
414
- $[10] !== t1 || $[11] !== t2 || $[12] !== t3 || $[13] !== t4 ? (t5 = [
415
- t1,
416
- t2,
417
- t3,
418
- t4
419
- ], $[10] = t1, $[11] = t2, $[12] = t3, $[13] = t4, $[14] = t5) : t5 = $[14];
420
- let swatches = t5, t6;
421
- $[15] === swatches ? t6 = $[16] : (t6 = /* @__PURE__ */ jsx("span", {
422
- style: paletteStyle,
423
- children: swatches.map(_temp$1)
424
- }), $[15] = swatches, $[16] = t6);
425
- let t7;
426
- $[17] === preset.title ? t7 = $[18] : (t7 = /* @__PURE__ */ jsx(Text, {
427
- align: "left",
428
- size: 1,
429
- textOverflow: "ellipsis",
430
- children: preset.title
431
- }), $[17] = preset.title, $[18] = t7);
432
- let t8;
433
- $[19] !== t6 || $[20] !== t7 ? (t8 = /* @__PURE__ */ jsxs(Stack, {
434
- as: "span",
435
- gap: 2,
436
- children: [t6, t7]
437
- }), $[19] = t6, $[20] = t7, $[21] = t8) : t8 = $[21];
438
- let t9;
439
- return $[22] !== active || $[23] !== onClick || $[24] !== preset.title || $[25] !== t8 ? (t9 = /* @__PURE__ */ jsx(Button, {
440
- mode: "ghost",
441
- onClick,
442
- padding: 2,
443
- selected: active,
444
- title: preset.title,
445
- children: t8
446
- }), $[22] = active, $[23] = onClick, $[24] = preset.title, $[25] = t8, $[26] = t9) : t9 = $[26], t9;
447
- }
448
- function _temp$1(t0) {
449
- let [key, background] = t0;
450
- return /* @__PURE__ */ jsx("span", { style: {
451
- flex: 1,
452
- background
453
- } }, key);
1827
+ }), $[16] = t7, $[17] = t8, $[18] = t9) : t9 = $[18];
1828
+ let t10;
1829
+ return $[19] !== onClose || $[20] !== t2 || $[21] !== t9 ? (t10 = /* @__PURE__ */ jsx(Dialog, {
1830
+ animate: !0,
1831
+ footer: t2,
1832
+ header: "Add to your config",
1833
+ id: "themer-snippet",
1834
+ onClickOutside: onClose,
1835
+ onClose,
1836
+ width: 1,
1837
+ children: t9
1838
+ }), $[19] = onClose, $[20] = t2, $[21] = t9, $[22] = t10) : t10 = $[22], t10;
454
1839
  }
455
- const rampStyle = {
456
- ...paletteStyle,
457
- height: 13,
458
- borderRadius: 2
1840
+ const VIEW_TITLES = {
1841
+ list: "Themer",
1842
+ edit: "Edit theme",
1843
+ removed: "Removed themes"
459
1844
  };
460
1845
  /**
461
- * One color of the theme: a swatch with its value, an optional reset-to-auto
462
- * button, and the generated 50–950 tint ramp for the scales it anchors.
1846
+ * The themer sidebar: a header that navigates between the flows, the flow
1847
+ * itself — picking a theme, editing one, or restoring removed ones — and the
1848
+ * dialog with the `buildTheme` snippet of the applied theme.
1849
+ *
1850
+ * @internal
463
1851
  */
464
- function ColorRow(props) {
465
- let $ = c(27), { adjusted, auto, onChange, onClear, tints, title, value } = props, t0;
466
- $[0] === title ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(Text, {
1852
+ function ThemerSidebar() {
1853
+ let $ = c(37), { active, view, send } = useThemer(), [snippetOpen, setSnippetOpen] = useState(!1), inList = view.name === "list", t0;
1854
+ $[0] !== inList || $[1] !== send ? (t0 = !inList && /* @__PURE__ */ jsx(TooltipButton, {
1855
+ icon: ArrowLeftIcon,
1856
+ mode: "bleed",
1857
+ onClick: () => send({ type: "flow.list" }),
1858
+ padding: 2,
1859
+ tooltip: "Back to the themes"
1860
+ }), $[0] = inList, $[1] = send, $[2] = t0) : t0 = $[2];
1861
+ let t1 = +!!inList, t2;
1862
+ $[3] === Symbol.for("react.memo_cache_sentinel") ? (t2 = { minWidth: 0 }, $[3] = t2) : t2 = $[3];
1863
+ let t3 = VIEW_TITLES[view.name], t4;
1864
+ $[4] === t3 ? t4 = $[5] : (t4 = /* @__PURE__ */ jsx(Text, {
467
1865
  size: 1,
468
- children: title
469
- }), $[0] = title, $[1] = t0);
470
- let t1 = adjusted !== void 0 && adjusted !== value ? ` → ${adjusted}` : "", t2 = auto ? " · auto" : "", t3;
471
- $[2] !== t1 || $[3] !== t2 || $[4] !== value ? (t3 = /* @__PURE__ */ jsxs(Text, {
472
- muted: !0,
473
- size: 0,
474
1866
  textOverflow: "ellipsis",
475
- children: [
476
- value,
477
- t1,
478
- t2
479
- ]
480
- }), $[2] = t1, $[3] = t2, $[4] = value, $[5] = t3) : t3 = $[5];
481
- let t4;
482
- $[6] !== t0 || $[7] !== t3 ? (t4 = /* @__PURE__ */ jsxs(Stack, {
483
- flex: 1,
484
- gap: 2,
485
- children: [t0, t3]
486
- }), $[6] = t0, $[7] = t3, $[8] = t4) : t4 = $[8];
1867
+ weight: "semibold",
1868
+ children: t3
1869
+ }), $[4] = t3, $[5] = t4);
487
1870
  let t5;
488
- $[9] !== auto || $[10] !== onClear ? (t5 = onClear && !auto && /* @__PURE__ */ jsx(Button, {
489
- icon: ResetIcon,
1871
+ $[6] !== t1 || $[7] !== t4 ? (t5 = /* @__PURE__ */ jsx(Box, {
1872
+ flex: 1,
1873
+ paddingLeft: t1,
1874
+ style: t2,
1875
+ children: t4
1876
+ }), $[6] = t1, $[7] = t4, $[8] = t5) : t5 = $[8];
1877
+ let t6;
1878
+ $[9] === view.name ? t6 = $[10] : (t6 = view.name !== "removed" && /* @__PURE__ */ jsx(TooltipButton, {
1879
+ icon: CodeBlockIcon,
490
1880
  mode: "bleed",
491
- onClick: onClear,
1881
+ onClick: () => setSnippetOpen(!0),
492
1882
  padding: 2,
493
- title: "Reset to auto"
494
- }), $[9] = auto, $[10] = onClear, $[11] = t5) : t5 = $[11];
495
- let t6 = `${title} color`, t7;
496
- $[12] === onChange ? t7 = $[13] : (t7 = (event) => onChange(event.currentTarget.value), $[12] = onChange, $[13] = t7);
1883
+ tooltip: "Show the code for the applied theme"
1884
+ }), $[9] = view.name, $[10] = t6);
1885
+ let t7;
1886
+ $[11] === send ? t7 = $[12] : (t7 = /* @__PURE__ */ jsx(TooltipButton, {
1887
+ icon: CloseIcon,
1888
+ mode: "bleed",
1889
+ onClick: () => send({ type: "sidebar.close" }),
1890
+ padding: 2,
1891
+ tooltip: "Close themer"
1892
+ }), $[11] = send, $[12] = t7);
497
1893
  let t8;
498
- $[14] !== t6 || $[15] !== t7 || $[16] !== value ? (t8 = /* @__PURE__ */ jsx(Swatch, {
499
- "aria-label": t6,
500
- onChange: t7,
501
- type: "color",
502
- value
503
- }), $[14] = t6, $[15] = t7, $[16] = value, $[17] = t8) : t8 = $[17];
1894
+ $[13] !== t0 || $[14] !== t5 || $[15] !== t6 || $[16] !== t7 ? (t8 = /* @__PURE__ */ jsx(Card, {
1895
+ padding: 2,
1896
+ children: /* @__PURE__ */ jsxs(Flex, {
1897
+ align: "center",
1898
+ gap: 1,
1899
+ children: [
1900
+ t0,
1901
+ t5,
1902
+ t6,
1903
+ t7
1904
+ ]
1905
+ })
1906
+ }), $[13] = t0, $[14] = t5, $[15] = t6, $[16] = t7, $[17] = t8) : t8 = $[17];
504
1907
  let t9;
505
- $[18] !== t4 || $[19] !== t5 || $[20] !== t8 ? (t9 = /* @__PURE__ */ jsxs(Flex, {
506
- align: "center",
507
- gap: 2,
508
- children: [
509
- t4,
510
- t5,
511
- t8
512
- ]
513
- }), $[18] = t4, $[19] = t5, $[20] = t8, $[21] = t9) : t9 = $[21];
1908
+ $[18] === view.name ? t9 = $[19] : (t9 = view.name === "list" && /* @__PURE__ */ jsx(ThemeList, {}), $[18] = view.name, $[19] = t9);
514
1909
  let t10;
515
- $[22] === tints ? t10 = $[23] : (t10 = tints && /* @__PURE__ */ jsx("span", {
516
- style: rampStyle,
517
- children: COLOR_TINTS.map((tint) => /* @__PURE__ */ jsx("span", { style: {
518
- flex: 1,
519
- background: tints[tint]
520
- } }, tint))
521
- }), $[22] = tints, $[23] = t10);
1910
+ $[20] !== view.focusTitle || $[21] !== view.name || $[22] !== view.slug ? (t10 = view.name === "edit" && /* @__PURE__ */ jsx(ThemeEditor, {
1911
+ focusTitle: view.focusTitle,
1912
+ slug: view.slug
1913
+ }), $[20] = view.focusTitle, $[21] = view.name, $[22] = view.slug, $[23] = t10) : t10 = $[23];
522
1914
  let t11;
523
- return $[24] !== t10 || $[25] !== t9 ? (t11 = /* @__PURE__ */ jsxs(Stack, {
524
- gap: 2,
525
- children: [t9, t10]
526
- }), $[24] = t10, $[25] = t9, $[26] = t11) : t11 = $[26], t11;
1915
+ $[24] === view.name ? t11 = $[25] : (t11 = view.name === "removed" && /* @__PURE__ */ jsx(RemovedThemes, {}), $[24] = view.name, $[25] = t11);
1916
+ let t12;
1917
+ $[26] !== t10 || $[27] !== t11 || $[28] !== t8 || $[29] !== t9 ? (t12 = /* @__PURE__ */ jsxs(Flex, {
1918
+ direction: "column",
1919
+ height: "fill",
1920
+ children: [
1921
+ t8,
1922
+ t9,
1923
+ t10,
1924
+ t11
1925
+ ]
1926
+ }), $[26] = t10, $[27] = t11, $[28] = t8, $[29] = t9, $[30] = t12) : t12 = $[30];
1927
+ let t13;
1928
+ $[31] !== active || $[32] !== snippetOpen ? (t13 = snippetOpen && /* @__PURE__ */ jsx(ThemeSnippetDialog, {
1929
+ onClose: () => setSnippetOpen(!1),
1930
+ theme: active
1931
+ }), $[31] = active, $[32] = snippetOpen, $[33] = t13) : t13 = $[33];
1932
+ let t14;
1933
+ return $[34] !== t12 || $[35] !== t13 ? (t14 = /* @__PURE__ */ jsxs(Card, {
1934
+ height: "fill",
1935
+ children: [t12, t13]
1936
+ }), $[34] = t12, $[35] = t13, $[36] = t14) : t14 = $[36], t14;
527
1937
  }
528
1938
  /**
529
- * Narrow enough to leave the studio preview as much room as possible: it fits
530
- * the widest picker label next to its swatch, and the code snippet scrolls
531
- * horizontally rather than widening the sidebar.
1939
+ * The narrowest the sidebar goes — and its default: narrow enough to leave the
1940
+ * studio preview as much room as possible, while the themes stack in a single
1941
+ * column of preview cards and the editor fits a swatch next to its label.
532
1942
  */
533
- const sidebarStyle = {
534
- width: 240,
535
- flex: "none",
536
- borderLeft: "1px solid var(--card-border-color)",
537
- boxSizing: "border-box",
538
- overflow: "hidden"
539
- };
1943
+ const WIDTH_STORAGE_KEY = "sanityStudio:themer:width";
1944
+ function clampWidth(width) {
1945
+ return Math.min(400, Math.max(200, Math.round(width)));
1946
+ }
1947
+ function readStoredWidth() {
1948
+ try {
1949
+ let stored = Number(localStorage.getItem(WIDTH_STORAGE_KEY));
1950
+ return Number.isFinite(stored) && stored > 0 ? clampWidth(stored) : 200;
1951
+ } catch {
1952
+ return 200;
1953
+ }
1954
+ }
1955
+ function writeStoredWidth(width) {
1956
+ try {
1957
+ width === 200 ? localStorage.removeItem(WIDTH_STORAGE_KEY) : localStorage.setItem(WIDTH_STORAGE_KEY, String(width));
1958
+ } catch {}
1959
+ }
1960
+ /**
1961
+ * The sidebar itself does not clip, so the resize handle can straddle its
1962
+ * border — the content is clipped one level down instead
1963
+ */
1964
+ const Sidebar = styled(Layer).withConfig({
1965
+ displayName: "Sidebar",
1966
+ componentId: "sc-xkgsr1-0"
1967
+ })`position:relative;flex:none;box-sizing:border-box;border-left:1px solid var(--card-border-color);`, ResizeHandle = styled.div.withConfig({
1968
+ displayName: "ResizeHandle",
1969
+ componentId: "sc-xkgsr1-1"
1970
+ })`position:absolute;top:0;bottom:0;left:-3px;z-index:1;width:7px;cursor:col-resize;touch-action:none;&::after{content:'';position:absolute;top:0;bottom:0;left:2px;width:3px;background:var(--card-focus-ring-color);opacity:0;transition:opacity 100ms;}&:hover::after,&:focus-visible::after,&[data-dragging='true']::after{opacity:1;}&:focus{outline:none;}`;
540
1971
  /**
541
1972
  * Renders the themer sidebar next to the active tool, so the user can browse
542
- * around their own studio while tweaking the theme.
1973
+ * around their own studio while tweaking the theme. The sidebar's left edge
1974
+ * drags (or arrow-keys) it wider, up to twice its default width — the width
1975
+ * sticks between sessions.
543
1976
  *
544
1977
  * @internal
545
1978
  */
546
1979
  function ThemerActiveToolLayout(props) {
547
- let $ = c(9), { open } = useThemer(), t0;
548
- $[0] === props ? t0 = $[1] : (t0 = props.renderDefault(props), $[0] = props, $[1] = t0);
549
- let t1;
550
- $[2] === t0 ? t1 = $[3] : (t1 = /* @__PURE__ */ jsx(Box, {
1980
+ let $ = c(20), { open } = useThemer(), [width, setWidth] = useState(readStoredWidth), [dragging, setDragging] = useState(!1), drag = useRef(null), t0, t1;
1981
+ $[0] === width ? (t0 = $[1], t1 = $[2]) : (t0 = () => writeStoredWidth(width), t1 = [width], $[0] = width, $[1] = t0, $[2] = t1), useEffect(t0, t1);
1982
+ let t2;
1983
+ $[3] === width ? t2 = $[4] : (t2 = (event) => {
1984
+ event.button === 0 && (event.preventDefault(), event.currentTarget.setPointerCapture(event.pointerId), drag.current = {
1985
+ pointerId: event.pointerId,
1986
+ startX: event.clientX,
1987
+ startWidth: width
1988
+ }, setDragging(!0));
1989
+ }, $[3] = width, $[4] = t2);
1990
+ let handlePointerDown = t2, t3;
1991
+ $[5] === Symbol.for("react.memo_cache_sentinel") ? (t3 = (event_0) => {
1992
+ drag.current && drag.current.pointerId === event_0.pointerId && setWidth(clampWidth(drag.current.startWidth - (event_0.clientX - drag.current.startX)));
1993
+ }, $[5] = t3) : t3 = $[5];
1994
+ let handlePointerMove = t3, t4;
1995
+ $[6] === Symbol.for("react.memo_cache_sentinel") ? (t4 = (event_1) => {
1996
+ drag.current && drag.current.pointerId === event_1.pointerId && (drag.current = null, setDragging(!1));
1997
+ }, $[6] = t4) : t4 = $[6];
1998
+ let handlePointerUp = t4, t5;
1999
+ $[7] === Symbol.for("react.memo_cache_sentinel") ? (t5 = (event_2) => {
2000
+ let step = {
2001
+ ArrowLeft: 16,
2002
+ ArrowRight: -16,
2003
+ Home: 400,
2004
+ End: -400
2005
+ }[event_2.key];
2006
+ step !== void 0 && (event_2.preventDefault(), setWidth((current) => clampWidth(current + step)));
2007
+ }, $[7] = t5) : t5 = $[7];
2008
+ let handleKeyDown = t5, t6;
2009
+ $[8] === props ? t6 = $[9] : (t6 = props.renderDefault(props), $[8] = props, $[9] = t6);
2010
+ let t7;
2011
+ $[10] === t6 ? t7 = $[11] : (t7 = /* @__PURE__ */ jsx(Box, {
551
2012
  flex: 1,
552
2013
  height: "fill",
553
2014
  overflow: "auto",
554
- children: t0
555
- }), $[2] = t0, $[3] = t1);
556
- let t2;
557
- $[4] === open ? t2 = $[5] : (t2 = open && /* @__PURE__ */ jsx(Layer, {
2015
+ children: t6
2016
+ }), $[10] = t6, $[11] = t7);
2017
+ let t8;
2018
+ $[12] !== dragging || $[13] !== handlePointerDown || $[14] !== open || $[15] !== width ? (t8 = open && /* @__PURE__ */ jsxs(Sidebar, {
558
2019
  height: "fill",
559
- style: sidebarStyle,
2020
+ style: { width },
560
2021
  zOffset: 100,
561
- children: /* @__PURE__ */ jsx(ThemerSidebar, {})
562
- }), $[4] = open, $[5] = t2);
563
- let t3;
564
- return $[6] !== t1 || $[7] !== t2 ? (t3 = /* @__PURE__ */ jsxs(Flex, {
2022
+ children: [/* @__PURE__ */ jsx(ResizeHandle, {
2023
+ "aria-label": "Resize the themer",
2024
+ "aria-orientation": "vertical",
2025
+ "aria-valuemax": 400,
2026
+ "aria-valuemin": 200,
2027
+ "aria-valuenow": width,
2028
+ "data-dragging": dragging,
2029
+ onDoubleClick: () => setWidth(200),
2030
+ onKeyDown: handleKeyDown,
2031
+ onPointerCancel: handlePointerUp,
2032
+ onPointerDown: handlePointerDown,
2033
+ onPointerMove: handlePointerMove,
2034
+ onPointerUp: handlePointerUp,
2035
+ role: "separator",
2036
+ tabIndex: 0
2037
+ }), /* @__PURE__ */ jsx(Box, {
2038
+ height: "fill",
2039
+ overflow: "hidden",
2040
+ children: /* @__PURE__ */ jsx(ThemerSidebar, {})
2041
+ })]
2042
+ }), $[12] = dragging, $[13] = handlePointerDown, $[14] = open, $[15] = width, $[16] = t8) : t8 = $[16];
2043
+ let t9;
2044
+ return $[17] !== t7 || $[18] !== t8 ? (t9 = /* @__PURE__ */ jsxs(Flex, {
565
2045
  height: "fill",
566
2046
  sizing: "border",
567
- children: [t1, t2]
568
- }), $[6] = t1, $[7] = t2, $[8] = t3) : t3 = $[8], t3;
2047
+ children: [t7, t8]
2048
+ }), $[17] = t7, $[18] = t8, $[19] = t9) : t9 = $[19], t9;
2049
+ }
2050
+ function themesOf(context) {
2051
+ return resolveThemes(context, context.baseOptions);
2052
+ }
2053
+ /**
2054
+ * The state of the themer tool: two parallel regions, whether the `sidebar`
2055
+ * is open and which `flow` it is in — picking a theme from the `list`,
2056
+ * `edit`ing one of the user's own themes, or restoring `removed` ones. The
2057
+ * context carries the persisted state (the applied theme, the user's themes
2058
+ * and what was removed) alongside what the flows need.
2059
+ *
2060
+ * Theme operations are handled in every flow, and the flows leave on their
2061
+ * own when they lose their subject: the editor when its theme is removed or
2062
+ * deleted, and the removed view when its last theme is restored or deleted.
2063
+ *
2064
+ * @internal
2065
+ */
2066
+ const themerMachine = setup({
2067
+ types: {
2068
+ context: {},
2069
+ events: {},
2070
+ input: {}
2071
+ },
2072
+ guards: {
2073
+ isCustomTheme: ({ context }, params) => themesOf(context).themes.some((theme) => theme.slug === params.slug && theme.source === "custom"),
2074
+ isEditingListedTheme: ({ context }) => context.editing !== null && themesOf(context).themes.some((theme) => theme.slug === context.editing?.slug && theme.source === "custom"),
2075
+ hasRemovedThemes: ({ context }) => themesOf(context).removed.length > 0
2076
+ },
2077
+ actions: {
2078
+ pick: assign((_, params) => ({ active: params.slug === "config" ? null : params.slug })),
2079
+ add: assign(({ context }, params) => {
2080
+ let theme = createCustomTheme(params.title ?? "Untitled theme", params.options ?? themesOf(context).active.options, params.palette);
2081
+ return {
2082
+ active: theme.slug,
2083
+ custom: [...context.custom, theme],
2084
+ editing: {
2085
+ slug: theme.slug,
2086
+ focusTitle: params.title === void 0
2087
+ },
2088
+ images: params.imageUrl ? {
2089
+ ...context.images,
2090
+ [theme.slug]: params.imageUrl
2091
+ } : context.images
2092
+ };
2093
+ }),
2094
+ duplicate: assign(({ context }, params) => {
2095
+ let { themes, removed } = themesOf(context), source = [...themes, ...removed].find((theme) => theme.slug === params.slug);
2096
+ if (!source) return {};
2097
+ let theme = createCustomTheme(duplicateTitle(source.title), source.options, source.palette);
2098
+ return {
2099
+ active: theme.slug,
2100
+ custom: [...context.custom, theme],
2101
+ editing: {
2102
+ slug: theme.slug,
2103
+ focusTitle: !0
2104
+ }
2105
+ };
2106
+ }),
2107
+ startEditing: assign((_, params) => ({
2108
+ active: params.slug,
2109
+ editing: {
2110
+ slug: params.slug,
2111
+ focusTitle: !1
2112
+ }
2113
+ })),
2114
+ stopEditing: assign({ editing: null }),
2115
+ update: assign(({ context }, params) => ({
2116
+ custom: context.custom.map((theme) => theme.slug === params.slug ? {
2117
+ ...theme,
2118
+ title: params.title ?? theme.title,
2119
+ options: params.options ?? theme.options,
2120
+ ...params.palette ? { palette: params.palette } : {}
2121
+ } : theme),
2122
+ images: params.imageUrl && context.custom.some((theme) => theme.slug === params.slug) ? {
2123
+ ...context.images,
2124
+ [params.slug]: params.imageUrl
2125
+ } : context.images
2126
+ })),
2127
+ remove: assign(({ context }, params) => params.slug === "config" || context.removed.includes(params.slug) ? {} : {
2128
+ active: context.active === params.slug ? null : context.active,
2129
+ removed: [...context.removed, params.slug]
2130
+ }),
2131
+ restore: assign(({ context }, params) => ({ removed: context.removed.filter((slug) => slug !== params.slug) })),
2132
+ delete: assign(({ context }, params) => {
2133
+ if (!context.custom.some((theme) => theme.slug === params.slug)) return {};
2134
+ let { [params.slug]: _deleted, ...images } = context.images;
2135
+ return {
2136
+ active: context.active === params.slug ? null : context.active,
2137
+ custom: context.custom.filter((theme) => theme.slug !== params.slug),
2138
+ removed: context.removed.filter((slug) => slug !== params.slug),
2139
+ images
2140
+ };
2141
+ })
2142
+ }
2143
+ }).createMachine({
2144
+ id: "themer",
2145
+ context: ({ input }) => ({
2146
+ ...input.stored,
2147
+ baseOptions: input.baseOptions,
2148
+ editing: null,
2149
+ images: {}
2150
+ }),
2151
+ type: "parallel",
2152
+ states: {
2153
+ sidebar: {
2154
+ initial: "closed",
2155
+ states: {
2156
+ closed: { on: { "sidebar.toggle": "open" } },
2157
+ open: { on: {
2158
+ "sidebar.toggle": "closed",
2159
+ "sidebar.close": "closed"
2160
+ } }
2161
+ }
2162
+ },
2163
+ flow: {
2164
+ initial: "list",
2165
+ on: {
2166
+ "theme.pick": { actions: {
2167
+ type: "pick",
2168
+ params: ({ event }) => ({ slug: event.slug })
2169
+ } },
2170
+ "theme.add": {
2171
+ target: ".edit",
2172
+ actions: {
2173
+ type: "add",
2174
+ params: ({ event }) => ({
2175
+ title: event.title,
2176
+ options: event.options,
2177
+ palette: event.palette,
2178
+ imageUrl: event.imageUrl
2179
+ })
2180
+ }
2181
+ },
2182
+ "theme.duplicate": {
2183
+ target: ".edit",
2184
+ actions: {
2185
+ type: "duplicate",
2186
+ params: ({ event }) => ({ slug: event.slug })
2187
+ }
2188
+ },
2189
+ "theme.edit": {
2190
+ target: ".edit",
2191
+ guard: {
2192
+ type: "isCustomTheme",
2193
+ params: ({ event }) => ({ slug: event.slug })
2194
+ },
2195
+ actions: {
2196
+ type: "startEditing",
2197
+ params: ({ event }) => ({ slug: event.slug })
2198
+ }
2199
+ },
2200
+ "theme.update": { actions: {
2201
+ type: "update",
2202
+ params: ({ event }) => ({
2203
+ slug: event.slug,
2204
+ title: event.title,
2205
+ options: event.options,
2206
+ palette: event.palette,
2207
+ imageUrl: event.imageUrl
2208
+ })
2209
+ } },
2210
+ "theme.remove": { actions: {
2211
+ type: "remove",
2212
+ params: ({ event }) => ({ slug: event.slug })
2213
+ } },
2214
+ "theme.restore": { actions: {
2215
+ type: "restore",
2216
+ params: ({ event }) => ({ slug: event.slug })
2217
+ } },
2218
+ "theme.delete": { actions: {
2219
+ type: "delete",
2220
+ params: ({ event }) => ({ slug: event.slug })
2221
+ } },
2222
+ "flow.list": ".list",
2223
+ "flow.removed": {
2224
+ target: ".removed",
2225
+ guard: "hasRemovedThemes"
2226
+ }
2227
+ },
2228
+ states: {
2229
+ list: {},
2230
+ edit: {
2231
+ exit: "stopEditing",
2232
+ always: {
2233
+ guard: not("isEditingListedTheme"),
2234
+ target: "list"
2235
+ }
2236
+ },
2237
+ removed: { always: {
2238
+ guard: not("hasRemovedThemes"),
2239
+ target: "list"
2240
+ } }
2241
+ }
2242
+ }
2243
+ }
2244
+ });
2245
+ /** The part of the machine context that is persisted between sessions @internal */
2246
+ function selectStoredState(snapshot) {
2247
+ let { active, custom, removed } = snapshot.context;
2248
+ return {
2249
+ active,
2250
+ custom,
2251
+ removed
2252
+ };
569
2253
  }
570
- const STORAGE_KEY = "sanityStudio:themer:options";
2254
+ const STORAGE_KEY = "sanityStudio:themer:state", LEGACY_STORAGE_KEY = "sanityStudio:themer:options";
571
2255
  function sanitizeColor(value) {
572
2256
  return typeof value == "string" && isColor(value) ? value.toLowerCase() : null;
573
2257
  }
574
- function sanitizeOptions(value) {
575
- if (!value || typeof value != "object") return null;
2258
+ function sanitizeContrast(value) {
2259
+ return typeof value == "number" && Number.isFinite(value) ? Math.min(100, Math.max(15, value)) : null;
2260
+ }
2261
+ function sanitizeSchemeOptions(value) {
2262
+ let options = {};
2263
+ if (!value || typeof value != "object") return options;
2264
+ let accent = sanitizeColor(Reflect.get(value, "accent")), text = sanitizeColor(Reflect.get(value, "text")), background = sanitizeColor(Reflect.get(value, "background")), contrast = sanitizeContrast(Reflect.get(value, "contrast"));
2265
+ return accent && (options.accent = accent), text && (options.text = text), background && (options.background = background), contrast !== null && (options.contrast = contrast), options;
2266
+ }
2267
+ /**
2268
+ * Options from before they were grouped by scheme: one accent, text color and
2269
+ * contrast for both schemes, and a background per scheme. Both schemes get
2270
+ * the shared colors, so the theme keeps looking the same.
2271
+ */
2272
+ function sanitizeFlatOptions(value) {
576
2273
  let accent = sanitizeColor(Reflect.get(value, "accent"));
577
2274
  if (!accent) return null;
578
- let options = { accent }, text = sanitizeColor(Reflect.get(value, "text"));
579
- text && (options.text = text);
580
- let background = Reflect.get(value, "background");
581
- if (background && typeof background == "object") {
582
- let dark = sanitizeColor(Reflect.get(background, "dark")), light = sanitizeColor(Reflect.get(background, "light"));
583
- (dark || light) && (options.background = {}, dark && (options.background.dark = dark), light && (options.background.light = light));
2275
+ let shared = { accent }, text = sanitizeColor(Reflect.get(value, "text")), contrast = sanitizeContrast(Reflect.get(value, "contrast"));
2276
+ text && (shared.text = text), contrast !== null && (shared.contrast = contrast);
2277
+ let backgrounds = Reflect.get(value, "background"), options = {};
2278
+ for (let scheme of SCHEMES) {
2279
+ let background = backgrounds && typeof backgrounds == "object" ? sanitizeColor(Reflect.get(backgrounds, scheme)) : null;
2280
+ options[scheme] = background ? {
2281
+ ...shared,
2282
+ background
2283
+ } : { ...shared };
2284
+ }
2285
+ return options;
2286
+ }
2287
+ function sanitizeOptions(value) {
2288
+ if (!value || typeof value != "object") return null;
2289
+ if ("accent" in value) return sanitizeFlatOptions(value);
2290
+ let options = {};
2291
+ for (let scheme of SCHEMES) {
2292
+ let schemeOptions = sanitizeSchemeOptions(Reflect.get(value, scheme));
2293
+ Object.keys(schemeOptions).length > 0 && (options[scheme] = schemeOptions);
2294
+ }
2295
+ return options;
2296
+ }
2297
+ function isReservedSlug(slug) {
2298
+ return slug === "config" || presets.some((preset) => preset.slug === slug);
2299
+ }
2300
+ function sanitizeCustomTheme(value) {
2301
+ if (!value || typeof value != "object") return null;
2302
+ let slug = Reflect.get(value, "slug");
2303
+ if (typeof slug != "string" || !slug || isReservedSlug(slug)) return null;
2304
+ let options = sanitizeOptions(Reflect.get(value, "options"));
2305
+ if (!options) return null;
2306
+ let title = Reflect.get(value, "title"), palette = sanitizePalette(Reflect.get(value, "palette"));
2307
+ return {
2308
+ slug,
2309
+ title: displayTitle(typeof title == "string" ? title : ""),
2310
+ options,
2311
+ ...palette ? { palette } : {}
2312
+ };
2313
+ }
2314
+ function sanitizePalette(value) {
2315
+ if (!value || typeof value != "object") return null;
2316
+ let palette = {}, swatches = 0;
2317
+ for (let key of IMAGE_PALETTE_KEYS) {
2318
+ let swatch = sanitizeColor(Reflect.get(value, key));
2319
+ palette[key] = swatch, swatch && swatches++;
584
2320
  }
585
- let contrast = Reflect.get(value, "contrast");
586
- return typeof contrast == "number" && Number.isFinite(contrast) && (options.contrast = Math.min(100, Math.max(15, contrast))), options;
2321
+ return swatches > 0 ? palette : null;
2322
+ }
2323
+ function sanitizeState(value) {
2324
+ if (!value || typeof value != "object") return initialThemerState;
2325
+ let custom = [], rawCustom = Reflect.get(value, "custom");
2326
+ if (Array.isArray(rawCustom)) for (let item of rawCustom) {
2327
+ let theme = sanitizeCustomTheme(item);
2328
+ theme && !custom.some((existing) => existing.slug === theme.slug) && custom.push(theme);
2329
+ }
2330
+ let removed = [], rawRemoved = Reflect.get(value, "removed");
2331
+ if (Array.isArray(rawRemoved)) for (let slug of rawRemoved) typeof slug == "string" && slug !== "config" && (presets.some((preset) => preset.slug === slug) || custom.some((theme) => theme.slug === slug)) && !removed.includes(slug) && removed.push(slug);
2332
+ let rawActive = Reflect.get(value, "active");
2333
+ return {
2334
+ active: typeof rawActive == "string" ? rawActive : null,
2335
+ custom,
2336
+ removed
2337
+ };
2338
+ }
2339
+ function readLegacyState() {
2340
+ let raw = localStorage.getItem(LEGACY_STORAGE_KEY);
2341
+ if (!raw) return null;
2342
+ let options = sanitizeOptions(JSON.parse(raw));
2343
+ if (!options) return null;
2344
+ let theme = createCustomTheme("Draft theme", options);
2345
+ return {
2346
+ active: theme.slug,
2347
+ custom: [theme],
2348
+ removed: []
2349
+ };
587
2350
  }
588
2351
  /**
589
- * Restores draft theme options from localStorage, so theme drafts survive
590
- * studio reloads.
2352
+ * Restores the themer state from localStorage, so the user's themes survive
2353
+ * studio reloads. Falls back to the initial state when nothing usable is
2354
+ * stored.
591
2355
  *
592
2356
  * @internal
593
2357
  */
594
- function readStoredOptions() {
2358
+ function readStoredState() {
595
2359
  try {
596
- if (typeof localStorage > "u") return null;
2360
+ if (typeof localStorage > "u") return initialThemerState;
597
2361
  let raw = localStorage.getItem(STORAGE_KEY);
598
- return raw ? sanitizeOptions(JSON.parse(raw)) : null;
2362
+ return raw ? sanitizeState(JSON.parse(raw)) : readLegacyState() ?? initialThemerState;
599
2363
  } catch {
600
- return null;
2364
+ return initialThemerState;
601
2365
  }
602
2366
  }
603
2367
  /** @internal */
604
- function writeStoredOptions(options) {
2368
+ function writeStoredState(state) {
605
2369
  try {
606
2370
  if (typeof localStorage > "u") return;
607
- options === null ? localStorage.removeItem(STORAGE_KEY) : localStorage.setItem(STORAGE_KEY, JSON.stringify(options));
2371
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)), localStorage.removeItem(LEGACY_STORAGE_KEY);
608
2372
  } catch {}
609
2373
  }
2374
+ function sameStoredState(a, b) {
2375
+ return a.active === b.active && a.custom === b.custom && a.removed === b.removed;
2376
+ }
2377
+ function selectView(snapshot) {
2378
+ let { editing } = snapshot.context;
2379
+ return snapshot.matches({ flow: "edit" }) && editing ? {
2380
+ name: "edit",
2381
+ slug: editing.slug,
2382
+ focusTitle: editing.focusTitle
2383
+ } : snapshot.matches({ flow: "removed" }) ? { name: "removed" } : { name: "list" };
2384
+ }
2385
+ function sameView(a, b) {
2386
+ return a.name === b.name ? a.name !== "edit" || b.name !== "edit" || a.slug === b.slug && a.focusTitle === b.focusTitle : !1;
2387
+ }
610
2388
  /**
611
- * Wraps the whole Studio so that draft themes generated by the themer sidebar
612
- * apply everywhere while the user browses around, and hosts the state that the
613
- * navbar toggle and the sidebar share.
2389
+ * Wraps the whole Studio so that the theme picked in the themer sidebar
2390
+ * applies everywhere while the user browses around, and runs the themer
2391
+ * machine that the navbar toggle and the sidebar share: the user's themes,
2392
+ * which one is applied, and which flow the sidebar is in.
614
2393
  *
615
- * The draft theme provider inherits the color scheme from the Studio, so the
2394
+ * The theme provider inherits the color scheme from the Studio, so the
616
2395
  * preview follows the appearance setting (light/dark/system) like any other
617
2396
  * theme.
618
2397
  *
619
2398
  * @internal
620
2399
  */
621
2400
  function ThemerLayout(props) {
622
- let $ = c(13), { baseOptions, ...layoutProps } = props, [open, setOpen] = useState(!1), [options, setOptions] = useState(readStoredOptions), t0, t1;
623
- $[0] === options ? (t0 = $[1], t1 = $[2]) : (t0 = () => writeStoredOptions(options), t1 = [options], $[0] = options, $[1] = t0, $[2] = t1), useEffect(t0, t1);
624
- let t2;
625
- $[3] === options ? t2 = $[4] : (t2 = options === null ? null : buildTheme(options), $[3] = options, $[4] = t2);
626
- let theme = t2, t3;
627
- $[5] !== baseOptions || $[6] !== open || $[7] !== options ? (t3 = {
2401
+ let $ = c(38), { baseOptions, ...layoutProps } = props, t0;
2402
+ $[0] === baseOptions ? t0 = $[1] : (t0 = () => ({
628
2403
  baseOptions,
629
- options,
630
- setOptions,
2404
+ stored: readStoredState()
2405
+ }), $[0] = baseOptions, $[1] = t0);
2406
+ let [input] = useState(t0), t1;
2407
+ $[2] === input ? t1 = $[3] : (t1 = { input }, $[2] = input, $[3] = t1);
2408
+ let [snapshot, send, actorRef] = useActor(themerMachine, t1), stored = useSelector(actorRef, selectStoredState, sameStoredState), view = useSelector(actorRef, selectView, sameView), t2;
2409
+ $[4] === snapshot ? t2 = $[5] : (t2 = snapshot.matches({ sidebar: "open" }), $[4] = snapshot, $[5] = t2);
2410
+ let open = t2, { images } = snapshot.context, t3, t4;
2411
+ $[6] === stored ? (t3 = $[7], t4 = $[8]) : (t3 = () => writeStoredState(stored), t4 = [stored], $[6] = stored, $[7] = t3, $[8] = t4), useEffect(t3, t4);
2412
+ let previousImages = useRef(images), t5, t6;
2413
+ $[9] === images ? (t5 = $[10], t6 = $[11]) : (t5 = () => {
2414
+ let current = new Set(Object.values(images));
2415
+ for (let url of Object.values(previousImages.current)) current.has(url) || URL.revokeObjectURL(url);
2416
+ previousImages.current = images;
2417
+ }, t6 = [images], $[9] = images, $[10] = t5, $[11] = t6), useEffect(t5, t6);
2418
+ let t7, t8;
2419
+ $[12] === Symbol.for("react.memo_cache_sentinel") ? (t7 = () => () => {
2420
+ for (let url_0 of Object.values(previousImages.current)) URL.revokeObjectURL(url_0);
2421
+ }, t8 = [], $[12] = t7, $[13] = t8) : (t7 = $[12], t8 = $[13]), useEffect(t7, t8);
2422
+ let t9;
2423
+ $[14] !== baseOptions || $[15] !== stored ? (t9 = resolveThemes(stored, baseOptions), $[14] = baseOptions, $[15] = stored, $[16] = t9) : t9 = $[16];
2424
+ let { themes, removed, active } = t9, activeOptions = active.source === "config" ? null : active.options, t10;
2425
+ $[17] === activeOptions ? t10 = $[18] : (t10 = activeOptions === null ? null : buildTheme(activeOptions), $[17] = activeOptions, $[18] = t10);
2426
+ let theme = t10, scheme = useColorSchemeValue(), t11;
2427
+ $[19] !== scheme || $[20] !== theme?.v2?.color ? (t11 = () => {
2428
+ let background = theme?.v2?.color[scheme].default.bg;
2429
+ if (background === void 0) return;
2430
+ let { style } = document.body, previous = style.backgroundColor;
2431
+ return style.backgroundColor = background, () => {
2432
+ style.backgroundColor = previous;
2433
+ };
2434
+ }, $[19] = scheme, $[20] = theme?.v2?.color, $[21] = t11) : t11 = $[21];
2435
+ let t12;
2436
+ $[22] !== scheme || $[23] !== theme ? (t12 = [scheme, theme], $[22] = scheme, $[23] = theme, $[24] = t12) : t12 = $[24], useEffect(t11, t12);
2437
+ let t13;
2438
+ $[25] !== active || $[26] !== baseOptions || $[27] !== images || $[28] !== open || $[29] !== removed || $[30] !== send || $[31] !== themes || $[32] !== view ? (t13 = {
2439
+ baseOptions,
2440
+ themes,
2441
+ removed,
2442
+ active,
2443
+ images,
2444
+ view,
631
2445
  open,
632
- setOpen
633
- }, $[5] = baseOptions, $[6] = open, $[7] = options, $[8] = t3) : t3 = $[8];
634
- let context = t3, t4 = ThemerContext, t5 = theme === null ? layoutProps.renderDefault(layoutProps) : /* @__PURE__ */ jsx(ThemeProvider, {
2446
+ send
2447
+ }, $[25] = active, $[26] = baseOptions, $[27] = images, $[28] = open, $[29] = removed, $[30] = send, $[31] = themes, $[32] = view, $[33] = t13) : t13 = $[33];
2448
+ let context = t13, t14 = ThemerContext, t15 = theme === null ? layoutProps.renderDefault(layoutProps) : /* @__PURE__ */ jsx(ThemeProvider, {
635
2449
  theme,
636
2450
  children: layoutProps.renderDefault(layoutProps)
637
- }), t6;
638
- return $[9] !== context || $[10] !== t4.Provider || $[11] !== t5 ? (t6 = /* @__PURE__ */ jsx(t4.Provider, {
2451
+ }), t16;
2452
+ return $[34] !== context || $[35] !== t14.Provider || $[36] !== t15 ? (t16 = /* @__PURE__ */ jsx(t14.Provider, {
639
2453
  value: context,
640
- children: t5
641
- }), $[9] = context, $[10] = t4.Provider, $[11] = t5, $[12] = t6) : t6 = $[12], t6;
2454
+ children: t15
2455
+ }), $[34] = context, $[35] = t14.Provider, $[36] = t15, $[37] = t16) : t16 = $[37], t16;
642
2456
  }
643
2457
  function ThemerNavbarButton() {
644
- let $ = c(7), { open, setOpen } = useThemer(), t0;
2458
+ let $ = c(6), { open, send } = useThemer(), t0;
645
2459
  $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = /* @__PURE__ */ jsx(Text, {
646
2460
  size: 1,
647
2461
  children: "Themer"
648
2462
  }), $[0] = t0) : t0 = $[0];
649
2463
  let t1;
650
- $[1] !== open || $[2] !== setOpen ? (t1 = () => setOpen(!open), $[1] = open, $[2] = setOpen, $[3] = t1) : t1 = $[3];
2464
+ $[1] === send ? t1 = $[2] : (t1 = () => send({ type: "sidebar.toggle" }), $[1] = send, $[2] = t1);
651
2465
  let t2;
652
- return $[4] !== open || $[5] !== t1 ? (t2 = /* @__PURE__ */ jsx(Tooltip, {
2466
+ return $[3] !== open || $[4] !== t1 ? (t2 = /* @__PURE__ */ jsx(Tooltip, {
2467
+ animate: !0,
653
2468
  content: t0,
654
2469
  portal: !0,
655
2470
  children: /* @__PURE__ */ jsx(Button, {
@@ -660,7 +2475,7 @@ function ThemerNavbarButton() {
660
2475
  padding: 2,
661
2476
  selected: open
662
2477
  })
663
- }), $[4] = open, $[5] = t1, $[6] = t2) : t2 = $[6], t2;
2478
+ }), $[3] = open, $[4] = t1, $[5] = t2) : t2 = $[5], t2;
664
2479
  }
665
2480
  /**
666
2481
  * Adds the toggle that opens and closes the themer sidebar to the Studio
@@ -670,12 +2485,12 @@ function ThemerNavbarButton() {
670
2485
  * @internal
671
2486
  */
672
2487
  function ThemerNavbar(props) {
673
- let $ = c(10), { open, setOpen } = useThemer(), t0;
2488
+ let $ = c(9), { open, send } = useThemer(), t0;
674
2489
  $[0] === props.__internal_actions ? t0 = $[1] : (t0 = props.__internal_actions ?? [], $[0] = props.__internal_actions, $[1] = t0);
675
2490
  let t1;
676
- if ($[2] !== open || $[3] !== props || $[4] !== setOpen || $[5] !== t0) {
2491
+ if ($[2] !== open || $[3] !== props || $[4] !== send || $[5] !== t0) {
677
2492
  let t2;
678
- $[7] !== open || $[8] !== setOpen ? (t2 = () => setOpen(!open), $[7] = open, $[8] = setOpen, $[9] = t2) : t2 = $[9], t1 = props.renderDefault({
2493
+ $[7] === send ? t2 = $[8] : (t2 = () => send({ type: "sidebar.toggle" }), $[7] = send, $[8] = t2), t1 = props.renderDefault({
679
2494
  ...props,
680
2495
  __internal_actions: [
681
2496
  ...t0,
@@ -693,7 +2508,7 @@ function ThemerNavbar(props) {
693
2508
  title: "Themer"
694
2509
  }
695
2510
  ]
696
- }), $[2] = open, $[3] = props, $[4] = setOpen, $[5] = t0, $[6] = t1;
2511
+ }), $[2] = open, $[3] = props, $[4] = send, $[5] = t0, $[6] = t1;
697
2512
  } else t1 = $[6];
698
2513
  return t1;
699
2514
  }
@@ -702,11 +2517,15 @@ function _temp() {
702
2517
  }
703
2518
  /**
704
2519
  * A Studio plugin that adds a themer sidebar for `buildTheme` themes: a
705
- * navbar toggle opens the sidebar next to the active tool, where presets, the
706
- * accent/text/background pickers and the contrast slider preview a
707
- * `buildTheme` theme live on the whole Studio while you browse around.
708
- * Toggle between light and dark mode with the regular appearance menu — the
709
- * preview follows it.
2520
+ * navbar toggle opens the sidebar next to the active tool, with a list of
2521
+ * themes — the configured theme, the presets and your own — each previewed
2522
+ * as a tiny Studio in both color schemes. Picking one applies it live to the
2523
+ * whole Studio while you browse around; your own themes can be edited with
2524
+ * accent/text/background pickers and a contrast slider per scheme, added,
2525
+ * duplicated, given the colors of an image (its palette is read on device),
2526
+ * removed and restored, and a dialog shows the `buildTheme` snippet that
2527
+ * makes the applied theme permanent. Toggle between light and dark mode with
2528
+ * the regular appearance menu — the preview follows it.
710
2529
  *
711
2530
  * ```ts
712
2531
  * import {themerTool} from '@sanity/themer/tool'
@@ -724,7 +2543,7 @@ function _temp() {
724
2543
  * @alpha
725
2544
  */
726
2545
  const themerTool = definePlugin((options) => {
727
- let baseOptions = options?.config ?? { accent: DEFAULT_ACCENT };
2546
+ let baseOptions = options?.config ?? {};
728
2547
  function ThemerLayoutWithOptions(props) {
729
2548
  let $ = c(2), t0;
730
2549
  return $[0] === props ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(ThemerLayout, {