@sanity/themer 0.3.16 → 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,20 +1,32 @@
1
- import { a as DEFAULT_BACKGROUND_DARK, c as resolveThemeOptions, i as DEFAULT_ACCENT, l as isColor, n as buildTheme, o as DEFAULT_BACKGROUND_LIGHT, r as buildPalette, s as deriveTextColor, t as presets } from "./presets-By3s-D-S.js";
2
- 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";
3
2
  import { c } from "react-compiler-runtime";
4
- import { definePlugin } from "sanity";
5
- import { Box, Button, Card, Flex, Grid, Layer, Stack, Text, ThemeProvider } from "@sanity/ui";
6
- import { createContext, useContext, useEffect, useState } from "react";
7
- 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";
8
8
  import { CloseIcon } from "@sanity/icons/Close";
9
+ import { CodeBlockIcon } from "@sanity/icons/CodeBlock";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
11
  import { ResetIcon } from "@sanity/icons/Reset";
10
- 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";
11
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";
12
25
  import { registerLanguage } from "react-refractor";
13
26
  import typescript from "refractor/typescript";
14
- import { styled } from "styled-components";
15
- import { jsx, jsxs } from "react/jsx-runtime";
27
+ import { useActor, useSelector } from "@xstate/react";
28
+ import { assign, not, setup } from "xstate";
16
29
  import { ColorWheelIcon } from "@sanity/icons/ColorWheel";
17
- import { Tooltip } from "@sanity/ui/tooltip";
18
30
  /** @internal */
19
31
  const ThemerContext = createContext(null);
20
32
  /** @internal */
@@ -23,20 +35,58 @@ function useThemer() {
23
35
  if (!context) throw Error("useThemer must be used within the `themerTool` plugin");
24
36
  return context;
25
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
+ }
26
73
  /**
27
74
  * Reduces theme options to the minimal object that recreates them through
28
- * `buildTheme`: the text color is dropped when it matches the one derived
29
- * from the accent, and backgrounds and contrast are dropped when they match
30
- * the defaults. Options that boil down to the stock Studio theme reduce to
31
- * `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.
32
80
  *
33
81
  * @internal
34
82
  */
35
83
  function minimizeOptions(options) {
36
- let resolved = resolveThemeOptions(options), minimized = { accent: resolved.accent };
37
- resolved.text !== deriveTextColor(resolved.accent) && (minimized.text = resolved.text);
38
- let dark = resolved.background.dark === DEFAULT_BACKGROUND_DARK ? null : resolved.background.dark, light = resolved.background.light === DEFAULT_BACKGROUND_LIGHT ? null : resolved.background.light;
39
- 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;
40
90
  }
41
91
  /**
42
92
  * Whether two sets of theme options generate the same theme, comparing their
@@ -47,11 +97,1629 @@ function minimizeOptions(options) {
47
97
  */
48
98
  function sameOptions(a, b) {
49
99
  let left = resolveThemeOptions(a), right = resolveThemeOptions(b);
50
- 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;
51
1718
  }
52
1719
  /**
53
1720
  * Serializes theme options into the `buildTheme` call to paste into
54
- * `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.
55
1723
  *
56
1724
  * Options that boil down to the stock Studio theme need nothing from this
57
1725
  * package — so they serialize to a bare `buildTheme()` from `@sanity/ui/theme`
@@ -62,77 +1730,35 @@ function sameOptions(a, b) {
62
1730
  function createThemeSnippet(options) {
63
1731
  let minimized = minimizeOptions(options);
64
1732
  if (minimized === null) return "import {buildTheme} from '@sanity/ui/theme'\n\nexport const theme = buildTheme()\n";
65
- let entries = [` accent: '${minimized.accent}',`];
66
- if (minimized.text !== void 0 && entries.push(` text: '${minimized.text}',`), minimized.background) {
67
- let parts = [];
68
- 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));
69
1737
  }
70
- 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 },`;
71
1744
  }
72
1745
  registerLanguage(typescript);
73
1746
  /**
74
- * `<input type="color">` paints the color into a shadow-DOM swatch that brings
75
- * its own border and padding, which then sits inside ours as a second border.
76
- * Stripping that chrome leaves the themed border as the only one.
77
- */
78
- const Swatch = styled.input.withConfig({
79
- displayName: "Swatch",
80
- componentId: "sc-7kf8xr-0"
81
- })`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({
82
- displayName: "Range",
83
- componentId: "sc-7kf8xr-1"
84
- })`display:block;width:100%;margin:0;accent-color:var(--card-focus-ring-color);`;
85
- /**
86
- * The themer sidebar: presets, the accent/text/background pickers and the
87
- * contrast slider that generate the previewed `buildTheme` theme, plus the
88
- * `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.
89
1750
  *
90
1751
  * @internal
91
1752
  */
92
- function ThemerSidebar() {
93
- let $ = c(117), { baseOptions, options, setOptions, setOpen } = useThemer(), toast = useToast(), active = options ?? baseOptions, t0;
94
- $[0] === active ? t0 = $[1] : (t0 = resolveThemeOptions(active), $[0] = active, $[1] = t0);
95
- let resolved = t0, t1;
96
- $[2] === active ? t1 = $[3] : (t1 = buildPalette(active), $[2] = active, $[3] = t1);
97
- let palette = t1, t2;
98
- $[4] === active ? t2 = $[5] : (t2 = presets.find((preset) => sameOptions(preset.options, active))?.slug ?? "", $[4] = active, $[5] = t2);
99
- let activePresetSlug = t2, t3;
100
- $[6] === active ? t3 = $[7] : (t3 = createThemeSnippet(active), $[6] = active, $[7] = t3);
101
- let snippet = t3, t4;
102
- $[8] !== active || $[9] !== setOptions ? (t4 = (changes) => {
103
- setOptions({
104
- ...active,
105
- ...changes
106
- });
107
- }, $[8] = active, $[9] = setOptions, $[10] = t4) : t4 = $[10];
108
- let patch = t4, t5;
109
- $[11] !== active || $[12] !== setOptions ? (t5 = (changes_0) => {
110
- setOptions({
111
- ...active,
112
- background: {
113
- ...active.background,
114
- ...changes_0
115
- }
116
- });
117
- }, $[11] = active, $[12] = setOptions, $[13] = t5) : t5 = $[13];
118
- let patchBackground = t5, t6;
119
- $[14] !== active || $[15] !== setOptions ? (t6 = (field) => {
120
- let next = { ...active };
121
- delete next[field], setOptions(next);
122
- }, $[14] = active, $[15] = setOptions, $[16] = t6) : t6 = $[16];
123
- let clearField = t6, t7;
124
- $[17] !== active || $[18] !== setOptions ? (t7 = (key) => {
125
- let background = { ...active.background };
126
- delete background[key];
127
- let next_0 = { ...active };
128
- background.dark === void 0 && background.light === void 0 ? delete next_0.background : next_0.background = background, setOptions(next_0);
129
- }, $[17] = active, $[18] = setOptions, $[19] = t7) : t7 = $[19];
130
- let clearBackground = t7, t8;
131
- $[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 () => {
132
1758
  try {
133
1759
  await navigator.clipboard.writeText(snippet), toast.push({
134
1760
  status: "success",
135
- title: "Copied theme to the clipboard"
1761
+ title: "Copied the theme to the clipboard"
136
1762
  });
137
1763
  } catch {
138
1764
  toast.push({
@@ -140,515 +1766,705 @@ function ThemerSidebar() {
140
1766
  title: "Could not copy the theme"
141
1767
  });
142
1768
  }
143
- }, $[20] = snippet, $[21] = toast, $[22] = t8) : t8 = $[22];
144
- let handleCopy = t8, t9;
145
- $[23] === Symbol.for("react.memo_cache_sentinel") ? (t9 = /* @__PURE__ */ jsx(Box, {
146
- flex: 1,
147
- paddingLeft: 1,
148
- children: /* @__PURE__ */ jsx(Text, {
149
- size: 1,
150
- weight: "semibold",
151
- children: "Themer"
152
- })
153
- }), $[23] = t9) : t9 = $[23];
154
- let t10;
155
- $[24] === setOpen ? t10 = $[25] : (t10 = /* @__PURE__ */ jsx(Card, {
156
- 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",
157
1774
  padding: 3,
158
- children: /* @__PURE__ */ jsxs(Flex, {
159
- align: "center",
160
- gap: 2,
161
- children: [t9, /* @__PURE__ */ jsx(Button, {
162
- icon: CloseIcon,
163
- mode: "bleed",
164
- onClick: () => setOpen(!1),
165
- padding: 2,
166
- title: "Close themer"
167
- })]
1775
+ children: /* @__PURE__ */ jsx(Button, {
1776
+ icon: ClipboardIcon,
1777
+ mode: "ghost",
1778
+ onClick: () => void handleCopy(),
1779
+ text: "Copy"
168
1780
  })
169
- }), $[24] = setOpen, $[25] = t10);
170
- let t11;
171
- $[26] === Symbol.for("react.memo_cache_sentinel") ? (t11 = /* @__PURE__ */ jsx(Text, {
172
- size: 1,
173
- weight: "medium",
174
- children: "Presets"
175
- }), $[26] = t11) : t11 = $[26];
176
- let t12;
177
- $[27] !== activePresetSlug || $[28] !== setOptions ? (t12 = presets.map((preset_0) => /* @__PURE__ */ jsx(PresetButton, {
178
- active: preset_0.slug === activePresetSlug,
179
- onClick: () => setOptions(preset_0.options),
180
- preset: preset_0
181
- }, preset_0.slug)), $[27] = activePresetSlug, $[28] = setOptions, $[29] = t12) : t12 = $[29];
182
- let t13;
183
- $[30] === t12 ? t13 = $[31] : (t13 = /* @__PURE__ */ jsxs(Stack, {
184
- gap: 3,
185
- children: [t11, /* @__PURE__ */ jsx(Grid, {
186
- gap: 2,
187
- gridTemplateColumns: 2,
188
- children: t12
189
- })]
190
- }), $[30] = t12, $[31] = t13);
191
- let t14;
192
- $[32] === Symbol.for("react.memo_cache_sentinel") ? (t14 = /* @__PURE__ */ jsx(Text, {
193
- size: 1,
194
- weight: "medium",
195
- children: "Colors"
196
- }), $[32] = t14) : t14 = $[32];
197
- let t15;
198
- $[33] === patch ? t15 = $[34] : (t15 = (accent) => patch({ accent }), $[33] = patch, $[34] = t15);
199
- let t16;
200
- $[35] !== palette.blue || $[36] !== resolved.accent || $[37] !== t15 ? (t16 = /* @__PURE__ */ jsx(ColorRow, {
201
- onChange: t15,
202
- tints: palette.blue,
203
- title: "Accent",
204
- value: resolved.accent
205
- }), $[35] = palette.blue, $[36] = resolved.accent, $[37] = t15, $[38] = t16) : t16 = $[38];
206
- let t17 = active.text === void 0, t18;
207
- $[39] === patch ? t18 = $[40] : (t18 = (text) => patch({ text }), $[39] = patch, $[40] = t18);
208
- let t19;
209
- $[41] === clearField ? t19 = $[42] : (t19 = () => clearField("text"), $[41] = clearField, $[42] = t19);
210
- let t20;
211
- $[43] !== palette.gray || $[44] !== resolved.text || $[45] !== t17 || $[46] !== t18 || $[47] !== t19 ? (t20 = /* @__PURE__ */ jsx(ColorRow, {
212
- auto: t17,
213
- onChange: t18,
214
- onClear: t19,
215
- tints: palette.gray,
216
- title: "Text",
217
- value: resolved.text
218
- }), $[43] = palette.gray, $[44] = resolved.text, $[45] = t17, $[46] = t18, $[47] = t19, $[48] = t20) : t20 = $[48];
219
- let t21 = active.background?.dark === void 0, t22;
220
- $[49] === patchBackground ? t22 = $[50] : (t22 = (dark) => patchBackground({ dark }), $[49] = patchBackground, $[50] = t22);
221
- let t23;
222
- $[51] === clearBackground ? t23 = $[52] : (t23 = () => clearBackground("dark"), $[51] = clearBackground, $[52] = t23);
223
- let t24;
224
- $[53] !== palette.black || $[54] !== resolved.background.dark || $[55] !== t21 || $[56] !== t22 || $[57] !== t23 ? (t24 = /* @__PURE__ */ jsx(ColorRow, {
225
- adjusted: palette.black,
226
- auto: t21,
227
- onChange: t22,
228
- onClear: t23,
229
- title: "Background · dark",
230
- value: resolved.background.dark
231
- }), $[53] = palette.black, $[54] = resolved.background.dark, $[55] = t21, $[56] = t22, $[57] = t23, $[58] = t24) : t24 = $[58];
232
- let t25 = active.background?.light === void 0, t26;
233
- $[59] === patchBackground ? t26 = $[60] : (t26 = (light) => patchBackground({ light }), $[59] = patchBackground, $[60] = t26);
234
- let t27;
235
- $[61] === clearBackground ? t27 = $[62] : (t27 = () => clearBackground("light"), $[61] = clearBackground, $[62] = t27);
236
- let t28;
237
- $[63] !== palette.white || $[64] !== resolved.background.light || $[65] !== t25 || $[66] !== t26 || $[67] !== t27 ? (t28 = /* @__PURE__ */ jsx(ColorRow, {
238
- adjusted: palette.white,
239
- auto: t25,
240
- onChange: t26,
241
- onClear: t27,
242
- title: "Background · light",
243
- value: resolved.background.light
244
- }), $[63] = palette.white, $[64] = resolved.background.light, $[65] = t25, $[66] = t26, $[67] = t27, $[68] = t28) : t28 = $[68];
245
- let t29;
246
- $[69] !== t16 || $[70] !== t20 || $[71] !== t24 || $[72] !== t28 ? (t29 = /* @__PURE__ */ jsxs(Stack, {
247
- gap: 3,
248
- children: [t14, /* @__PURE__ */ jsxs(Stack, {
249
- gap: 4,
250
- children: [
251
- t16,
252
- t20,
253
- t24,
254
- t28
255
- ]
256
- })]
257
- }), $[69] = t16, $[70] = t20, $[71] = t24, $[72] = t28, $[73] = t29) : t29 = $[73];
258
- let t30;
259
- $[74] === Symbol.for("react.memo_cache_sentinel") ? (t30 = /* @__PURE__ */ jsx(Text, {
260
- size: 1,
261
- weight: "medium",
262
- children: "Contrast"
263
- }), $[74] = t30) : t30 = $[74];
264
- let t31 = active.contrast === void 0 ? " · auto" : "", t32;
265
- $[75] !== resolved.contrast || $[76] !== t31 ? (t32 = /* @__PURE__ */ jsxs(Stack, {
266
- flex: 1,
267
- gap: 2,
268
- children: [t30, /* @__PURE__ */ jsxs(Text, {
269
- muted: !0,
270
- size: 0,
271
- children: [resolved.contrast, t31]
272
- })]
273
- }), $[75] = resolved.contrast, $[76] = t31, $[77] = t32) : t32 = $[77];
274
- let t33;
275
- $[78] !== active.contrast || $[79] !== clearField ? (t33 = active.contrast !== void 0 && /* @__PURE__ */ jsx(Button, {
276
- icon: ResetIcon,
277
- mode: "bleed",
278
- onClick: () => clearField("contrast"),
279
- padding: 2,
280
- title: "Reset to auto"
281
- }), $[78] = active.contrast, $[79] = clearField, $[80] = t33) : t33 = $[80];
282
- let t34;
283
- $[81] !== t32 || $[82] !== t33 ? (t34 = /* @__PURE__ */ jsxs(Flex, {
284
- align: "center",
285
- gap: 2,
286
- children: [t32, t33]
287
- }), $[81] = t32, $[82] = t33, $[83] = t34) : t34 = $[83];
288
- let t35;
289
- $[84] === patch ? t35 = $[85] : (t35 = (event) => patch({ contrast: Number(event.currentTarget.value) }), $[84] = patch, $[85] = t35);
290
- let t36;
291
- $[86] !== resolved.contrast || $[87] !== t35 ? (t36 = /* @__PURE__ */ jsx(Range, {
292
- "aria-label": "Contrast",
293
- max: 100,
294
- min: 15,
295
- onChange: t35,
296
- step: 1,
297
- type: "range",
298
- value: resolved.contrast
299
- }), $[86] = resolved.contrast, $[87] = t35, $[88] = t36) : t36 = $[88];
300
- let t37;
301
- $[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, {
302
1792
  muted: !0,
303
- size: 0,
304
- children: "100 keeps text and borders neutral — lower values blend in the accent"
305
- }), $[89] = t37) : t37 = $[89];
306
- let t38;
307
- $[90] !== t34 || $[91] !== t36 ? (t38 = /* @__PURE__ */ jsxs(Stack, {
308
- gap: 3,
1793
+ size: 1,
309
1794
  children: [
310
- t34,
311
- t36,
312
- 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."
313
1805
  ]
314
- }), $[90] = t34, $[91] = t36, $[92] = t38) : t38 = $[92];
315
- let t39;
316
- $[93] === Symbol.for("react.memo_cache_sentinel") ? (t39 = /* @__PURE__ */ jsx(Text, {
317
- size: 1,
318
- weight: "medium",
319
- children: "Add to your config"
320
- }), $[93] = t39) : t39 = $[93];
321
- let t40;
322
- $[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, {
323
1809
  border: !0,
324
1810
  overflow: "auto",
325
- padding: 2,
1811
+ padding: 3,
326
1812
  radius: 2,
327
1813
  tone: "transparent",
328
1814
  children: /* @__PURE__ */ jsx(Code, {
329
1815
  language: "ts",
330
- size: 0,
1816
+ size: 1,
331
1817
  children: snippet
332
1818
  })
333
- }), $[94] = snippet, $[95] = t40);
334
- let t41;
335
- $[96] === handleCopy ? t41 = $[97] : (t41 = /* @__PURE__ */ jsx(Button, {
336
- icon: ClipboardIcon,
337
- mode: "ghost",
338
- onClick: () => void handleCopy(),
339
- text: "Copy"
340
- }), $[96] = handleCopy, $[97] = t41);
341
- let t42 = options === null, t43;
342
- $[98] === setOptions ? t43 = $[99] : (t43 = () => setOptions(null), $[98] = setOptions, $[99] = t43);
343
- let t44;
344
- $[100] !== t42 || $[101] !== t43 ? (t44 = /* @__PURE__ */ jsx(Button, {
345
- disabled: t42,
346
- icon: ResetIcon,
347
- mode: "ghost",
348
- onClick: t43,
349
- text: "Reset",
350
- tone: "critical"
351
- }), $[100] = t42, $[101] = t43, $[102] = t44) : t44 = $[102];
352
- let t45;
353
- $[103] !== t41 || $[104] !== t44 ? (t45 = /* @__PURE__ */ jsxs(Flex, {
354
- gap: 2,
355
- children: [t41, t44]
356
- }), $[103] = t41, $[104] = t44, $[105] = t45) : t45 = $[105];
357
- let t46;
358
- $[106] !== t40 || $[107] !== t45 ? (t46 = /* @__PURE__ */ jsxs(Stack, {
359
- gap: 3,
360
- children: [
361
- t39,
362
- t40,
363
- t45
364
- ]
365
- }), $[106] = t40, $[107] = t45, $[108] = t46) : t46 = $[108];
366
- let t47;
367
- $[109] !== t13 || $[110] !== t29 || $[111] !== t38 || $[112] !== t46 ? (t47 = /* @__PURE__ */ jsx(Box, {
368
- flex: 1,
369
- overflow: "auto",
370
- padding: 3,
1819
+ }), $[14] = snippet, $[15] = t8);
1820
+ let t9;
1821
+ $[16] !== t7 || $[17] !== t8 ? (t9 = /* @__PURE__ */ jsx(Box, {
1822
+ padding: 4,
371
1823
  children: /* @__PURE__ */ jsxs(Stack, {
372
1824
  gap: 4,
373
- children: [
374
- t13,
375
- t29,
376
- t38,
377
- t46
378
- ]
379
- })
380
- }), $[109] = t13, $[110] = t29, $[111] = t38, $[112] = t46, $[113] = t47) : t47 = $[113];
381
- let t48;
382
- return $[114] !== t10 || $[115] !== t47 ? (t48 = /* @__PURE__ */ jsx(Card, {
383
- height: "fill",
384
- children: /* @__PURE__ */ jsxs(Flex, {
385
- direction: "column",
386
- height: "fill",
387
- children: [t10, t47]
1825
+ children: [t7, t8]
388
1826
  })
389
- }), $[114] = t10, $[115] = t47, $[116] = t48) : t48 = $[116], t48;
390
- }
391
- const paletteStyle = {
392
- display: "flex",
393
- gap: 1,
394
- background: "var(--card-border-color)",
395
- height: 21,
396
- borderRadius: 3,
397
- overflow: "hidden",
398
- boxShadow: "inset 0 0 0 1px var(--card-border-color)"
399
- };
400
- /** A little palette of the colors a preset would apply */
401
- function PresetButton(props) {
402
- let $ = c(27), { active, onClick, preset } = props, t0;
403
- $[0] === preset.options ? t0 = $[1] : (t0 = resolveThemeOptions(preset.options), $[0] = preset.options, $[1] = t0);
404
- let resolved = t0, t1;
405
- $[2] === resolved.accent ? t1 = $[3] : (t1 = ["accent", resolved.accent], $[2] = resolved.accent, $[3] = t1);
406
- let t2;
407
- $[4] === resolved.text ? t2 = $[5] : (t2 = ["text", resolved.text], $[4] = resolved.text, $[5] = t2);
408
- let t3;
409
- $[6] === resolved.background.dark ? t3 = $[7] : (t3 = ["dark", resolved.background.dark], $[6] = resolved.background.dark, $[7] = t3);
410
- let t4;
411
- $[8] === resolved.background.light ? t4 = $[9] : (t4 = ["light", resolved.background.light], $[8] = resolved.background.light, $[9] = t4);
412
- let t5;
413
- $[10] !== t1 || $[11] !== t2 || $[12] !== t3 || $[13] !== t4 ? (t5 = [
414
- t1,
415
- t2,
416
- t3,
417
- t4
418
- ], $[10] = t1, $[11] = t2, $[12] = t3, $[13] = t4, $[14] = t5) : t5 = $[14];
419
- let swatches = t5, t6;
420
- $[15] === swatches ? t6 = $[16] : (t6 = /* @__PURE__ */ jsx("span", {
421
- style: paletteStyle,
422
- children: swatches.map(_temp$1)
423
- }), $[15] = swatches, $[16] = t6);
424
- let t7;
425
- $[17] === preset.title ? t7 = $[18] : (t7 = /* @__PURE__ */ jsx(Text, {
426
- align: "left",
427
- size: 1,
428
- textOverflow: "ellipsis",
429
- children: preset.title
430
- }), $[17] = preset.title, $[18] = t7);
431
- let t8;
432
- $[19] !== t6 || $[20] !== t7 ? (t8 = /* @__PURE__ */ jsxs(Stack, {
433
- as: "span",
434
- gap: 2,
435
- children: [t6, t7]
436
- }), $[19] = t6, $[20] = t7, $[21] = t8) : t8 = $[21];
437
- let t9;
438
- return $[22] !== active || $[23] !== onClick || $[24] !== preset.title || $[25] !== t8 ? (t9 = /* @__PURE__ */ jsx(Button, {
439
- mode: "ghost",
440
- onClick,
441
- padding: 2,
442
- selected: active,
443
- title: preset.title,
444
- children: t8
445
- }), $[22] = active, $[23] = onClick, $[24] = preset.title, $[25] = t8, $[26] = t9) : t9 = $[26], t9;
446
- }
447
- function _temp$1(t0) {
448
- let [key, background] = t0;
449
- return /* @__PURE__ */ jsx("span", { style: {
450
- flex: 1,
451
- background
452
- } }, 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;
453
1839
  }
454
- const rampStyle = {
455
- ...paletteStyle,
456
- height: 13,
457
- borderRadius: 2
1840
+ const VIEW_TITLES = {
1841
+ list: "Themer",
1842
+ edit: "Edit theme",
1843
+ removed: "Removed themes"
458
1844
  };
459
1845
  /**
460
- * One color of the theme: a swatch with its value, an optional reset-to-auto
461
- * 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
462
1851
  */
463
- function ColorRow(props) {
464
- let $ = c(27), { adjusted, auto, onChange, onClear, tints, title, value } = props, t0;
465
- $[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, {
466
1865
  size: 1,
467
- children: title
468
- }), $[0] = title, $[1] = t0);
469
- let t1 = adjusted !== void 0 && adjusted !== value ? ` → ${adjusted}` : "", t2 = auto ? " · auto" : "", t3;
470
- $[2] !== t1 || $[3] !== t2 || $[4] !== value ? (t3 = /* @__PURE__ */ jsxs(Text, {
471
- muted: !0,
472
- size: 0,
473
1866
  textOverflow: "ellipsis",
474
- children: [
475
- value,
476
- t1,
477
- t2
478
- ]
479
- }), $[2] = t1, $[3] = t2, $[4] = value, $[5] = t3) : t3 = $[5];
480
- let t4;
481
- $[6] !== t0 || $[7] !== t3 ? (t4 = /* @__PURE__ */ jsxs(Stack, {
482
- flex: 1,
483
- gap: 2,
484
- children: [t0, t3]
485
- }), $[6] = t0, $[7] = t3, $[8] = t4) : t4 = $[8];
1867
+ weight: "semibold",
1868
+ children: t3
1869
+ }), $[4] = t3, $[5] = t4);
486
1870
  let t5;
487
- $[9] !== auto || $[10] !== onClear ? (t5 = onClear && !auto && /* @__PURE__ */ jsx(Button, {
488
- 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,
489
1880
  mode: "bleed",
490
- onClick: onClear,
1881
+ onClick: () => setSnippetOpen(!0),
491
1882
  padding: 2,
492
- title: "Reset to auto"
493
- }), $[9] = auto, $[10] = onClear, $[11] = t5) : t5 = $[11];
494
- let t6 = `${title} color`, t7;
495
- $[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);
496
1893
  let t8;
497
- $[14] !== t6 || $[15] !== t7 || $[16] !== value ? (t8 = /* @__PURE__ */ jsx(Swatch, {
498
- "aria-label": t6,
499
- onChange: t7,
500
- type: "color",
501
- value
502
- }), $[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];
503
1907
  let t9;
504
- $[18] !== t4 || $[19] !== t5 || $[20] !== t8 ? (t9 = /* @__PURE__ */ jsxs(Flex, {
505
- align: "center",
506
- gap: 2,
507
- children: [
508
- t4,
509
- t5,
510
- t8
511
- ]
512
- }), $[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);
513
1909
  let t10;
514
- $[22] === tints ? t10 = $[23] : (t10 = tints && /* @__PURE__ */ jsx("span", {
515
- style: rampStyle,
516
- children: COLOR_TINTS.map((tint) => /* @__PURE__ */ jsx("span", { style: {
517
- flex: 1,
518
- background: tints[tint]
519
- } }, tint))
520
- }), $[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];
521
1914
  let t11;
522
- return $[24] !== t10 || $[25] !== t9 ? (t11 = /* @__PURE__ */ jsxs(Stack, {
523
- gap: 2,
524
- children: [t9, t10]
525
- }), $[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;
526
1937
  }
527
1938
  /**
528
- * Narrow enough to leave the studio preview as much room as possible: it fits
529
- * the widest picker label next to its swatch, and the code snippet scrolls
530
- * 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.
531
1942
  */
532
- const sidebarStyle = {
533
- width: 240,
534
- flex: "none",
535
- borderLeft: "1px solid var(--card-border-color)",
536
- boxSizing: "border-box",
537
- overflow: "hidden"
538
- };
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;}`;
539
1971
  /**
540
1972
  * Renders the themer sidebar next to the active tool, so the user can browse
541
- * 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.
542
1976
  *
543
1977
  * @internal
544
1978
  */
545
1979
  function ThemerActiveToolLayout(props) {
546
- let $ = c(9), { open } = useThemer(), t0;
547
- $[0] === props ? t0 = $[1] : (t0 = props.renderDefault(props), $[0] = props, $[1] = t0);
548
- let t1;
549
- $[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, {
550
2012
  flex: 1,
551
2013
  height: "fill",
552
2014
  overflow: "auto",
553
- children: t0
554
- }), $[2] = t0, $[3] = t1);
555
- let t2;
556
- $[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, {
557
2019
  height: "fill",
558
- style: sidebarStyle,
2020
+ style: { width },
559
2021
  zOffset: 100,
560
- children: /* @__PURE__ */ jsx(ThemerSidebar, {})
561
- }), $[4] = open, $[5] = t2);
562
- let t3;
563
- 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, {
564
2045
  height: "fill",
565
2046
  sizing: "border",
566
- children: [t1, t2]
567
- }), $[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
+ };
568
2253
  }
569
- const STORAGE_KEY = "sanityStudio:themer:options";
2254
+ const STORAGE_KEY = "sanityStudio:themer:state", LEGACY_STORAGE_KEY = "sanityStudio:themer:options";
570
2255
  function sanitizeColor(value) {
571
2256
  return typeof value == "string" && isColor(value) ? value.toLowerCase() : null;
572
2257
  }
573
- function sanitizeOptions(value) {
574
- 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) {
575
2273
  let accent = sanitizeColor(Reflect.get(value, "accent"));
576
2274
  if (!accent) return null;
577
- let options = { accent }, text = sanitizeColor(Reflect.get(value, "text"));
578
- text && (options.text = text);
579
- let background = Reflect.get(value, "background");
580
- if (background && typeof background == "object") {
581
- let dark = sanitizeColor(Reflect.get(background, "dark")), light = sanitizeColor(Reflect.get(background, "light"));
582
- (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++;
583
2320
  }
584
- let contrast = Reflect.get(value, "contrast");
585
- 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
+ };
586
2350
  }
587
2351
  /**
588
- * Restores draft theme options from localStorage, so theme drafts survive
589
- * 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.
590
2355
  *
591
2356
  * @internal
592
2357
  */
593
- function readStoredOptions() {
2358
+ function readStoredState() {
594
2359
  try {
595
- if (typeof localStorage > "u") return null;
2360
+ if (typeof localStorage > "u") return initialThemerState;
596
2361
  let raw = localStorage.getItem(STORAGE_KEY);
597
- return raw ? sanitizeOptions(JSON.parse(raw)) : null;
2362
+ return raw ? sanitizeState(JSON.parse(raw)) : readLegacyState() ?? initialThemerState;
598
2363
  } catch {
599
- return null;
2364
+ return initialThemerState;
600
2365
  }
601
2366
  }
602
2367
  /** @internal */
603
- function writeStoredOptions(options) {
2368
+ function writeStoredState(state) {
604
2369
  try {
605
2370
  if (typeof localStorage > "u") return;
606
- 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);
607
2372
  } catch {}
608
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
+ }
609
2388
  /**
610
- * Wraps the whole Studio so that draft themes generated by the themer sidebar
611
- * apply everywhere while the user browses around, and hosts the state that the
612
- * 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.
613
2393
  *
614
- * 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
615
2395
  * preview follows the appearance setting (light/dark/system) like any other
616
2396
  * theme.
617
2397
  *
618
2398
  * @internal
619
2399
  */
620
2400
  function ThemerLayout(props) {
621
- let $ = c(13), { baseOptions, ...layoutProps } = props, [open, setOpen] = useState(!1), [options, setOptions] = useState(readStoredOptions), t0, t1;
622
- $[0] === options ? (t0 = $[1], t1 = $[2]) : (t0 = () => writeStoredOptions(options), t1 = [options], $[0] = options, $[1] = t0, $[2] = t1), useEffect(t0, t1);
623
- let t2;
624
- $[3] === options ? t2 = $[4] : (t2 = options === null ? null : buildTheme(options), $[3] = options, $[4] = t2);
625
- let theme = t2, t3;
626
- $[5] !== baseOptions || $[6] !== open || $[7] !== options ? (t3 = {
2401
+ let $ = c(38), { baseOptions, ...layoutProps } = props, t0;
2402
+ $[0] === baseOptions ? t0 = $[1] : (t0 = () => ({
627
2403
  baseOptions,
628
- options,
629
- 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,
630
2445
  open,
631
- setOpen
632
- }, $[5] = baseOptions, $[6] = open, $[7] = options, $[8] = t3) : t3 = $[8];
633
- 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, {
634
2449
  theme,
635
2450
  children: layoutProps.renderDefault(layoutProps)
636
- }), t6;
637
- 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, {
638
2453
  value: context,
639
- children: t5
640
- }), $[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;
641
2456
  }
642
2457
  function ThemerNavbarButton() {
643
- let $ = c(7), { open, setOpen } = useThemer(), t0;
2458
+ let $ = c(6), { open, send } = useThemer(), t0;
644
2459
  $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = /* @__PURE__ */ jsx(Text, {
645
2460
  size: 1,
646
2461
  children: "Themer"
647
2462
  }), $[0] = t0) : t0 = $[0];
648
2463
  let t1;
649
- $[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);
650
2465
  let t2;
651
- return $[4] !== open || $[5] !== t1 ? (t2 = /* @__PURE__ */ jsx(Tooltip, {
2466
+ return $[3] !== open || $[4] !== t1 ? (t2 = /* @__PURE__ */ jsx(Tooltip, {
2467
+ animate: !0,
652
2468
  content: t0,
653
2469
  portal: !0,
654
2470
  children: /* @__PURE__ */ jsx(Button, {
@@ -659,7 +2475,7 @@ function ThemerNavbarButton() {
659
2475
  padding: 2,
660
2476
  selected: open
661
2477
  })
662
- }), $[4] = open, $[5] = t1, $[6] = t2) : t2 = $[6], t2;
2478
+ }), $[3] = open, $[4] = t1, $[5] = t2) : t2 = $[5], t2;
663
2479
  }
664
2480
  /**
665
2481
  * Adds the toggle that opens and closes the themer sidebar to the Studio
@@ -669,12 +2485,12 @@ function ThemerNavbarButton() {
669
2485
  * @internal
670
2486
  */
671
2487
  function ThemerNavbar(props) {
672
- let $ = c(10), { open, setOpen } = useThemer(), t0;
2488
+ let $ = c(9), { open, send } = useThemer(), t0;
673
2489
  $[0] === props.__internal_actions ? t0 = $[1] : (t0 = props.__internal_actions ?? [], $[0] = props.__internal_actions, $[1] = t0);
674
2490
  let t1;
675
- if ($[2] !== open || $[3] !== props || $[4] !== setOpen || $[5] !== t0) {
2491
+ if ($[2] !== open || $[3] !== props || $[4] !== send || $[5] !== t0) {
676
2492
  let t2;
677
- $[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({
678
2494
  ...props,
679
2495
  __internal_actions: [
680
2496
  ...t0,
@@ -692,7 +2508,7 @@ function ThemerNavbar(props) {
692
2508
  title: "Themer"
693
2509
  }
694
2510
  ]
695
- }), $[2] = open, $[3] = props, $[4] = setOpen, $[5] = t0, $[6] = t1;
2511
+ }), $[2] = open, $[3] = props, $[4] = send, $[5] = t0, $[6] = t1;
696
2512
  } else t1 = $[6];
697
2513
  return t1;
698
2514
  }
@@ -701,11 +2517,15 @@ function _temp() {
701
2517
  }
702
2518
  /**
703
2519
  * A Studio plugin that adds a themer sidebar for `buildTheme` themes: a
704
- * navbar toggle opens the sidebar next to the active tool, where presets, the
705
- * accent/text/background pickers and the contrast slider preview a
706
- * `buildTheme` theme live on the whole Studio while you browse around.
707
- * Toggle between light and dark mode with the regular appearance menu — the
708
- * 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.
709
2529
  *
710
2530
  * ```ts
711
2531
  * import {themerTool} from '@sanity/themer/tool'
@@ -723,7 +2543,7 @@ function _temp() {
723
2543
  * @alpha
724
2544
  */
725
2545
  const themerTool = definePlugin((options) => {
726
- let baseOptions = options?.config ?? { accent: DEFAULT_ACCENT };
2546
+ let baseOptions = options?.config ?? {};
727
2547
  function ThemerLayoutWithOptions(props) {
728
2548
  let $ = c(2), t0;
729
2549
  return $[0] === props ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(ThemerLayout, {