@sanity/themer 0.5.0 → 0.7.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,16 +1,17 @@
1
+ import "@sanity/themer/bundle.css";
1
2
  import { a as DEFAULT_BACKGROUND, c as resolveThemeOptions, d as hslToHex, f as relativeLuminance, i as DEFAULT_ACCENT, l as isColor, n as buildTheme, o as SCHEMES, r as buildPalette, s as deriveTextColor, t as presets, u as hexToHsl } from "./presets-CqBgS3A-.js";
2
- import { c } from "react-compiler-runtime";
3
+ import { c } from "react/compiler-runtime";
3
4
  import { definePlugin, useColorSchemeValue } from "sanity";
4
- import { Badge, Box, Button, Card, Dialog, Flex, Layer, Stack, Text, TextInput, ThemeProvider, useRootTheme, useTheme_v2 } from "@sanity/ui";
5
+ import { Badge, Box, Button, Card, Dialog, Flex, Layer, Stack, Text, TextInput, ThemeProvider, useMediaIndex, useRootTheme, useTheme_v2 } from "@sanity/ui";
5
6
  import { useActor, useSelector } from "@xstate/react";
6
- import { createContext, useContext, useEffect, useRef, useState } from "react";
7
- import { assign, not, setup } from "xstate";
8
- import { styled } from "styled-components";
7
+ import { Activity, ViewTransition, createContext, useContext, useDeferredValue, useEffect, useRef, useState, useSyncExternalStore } from "react";
8
+ import { assign, not, or, setup, stateIn } from "xstate";
9
9
  import { ArrowLeftIcon } from "@sanity/icons/ArrowLeft";
10
10
  import { CloseIcon } from "@sanity/icons/Close";
11
11
  import { CodeBlockIcon } from "@sanity/icons/CodeBlock";
12
12
  import { SplitVerticalIcon } from "@sanity/icons/SplitVertical";
13
13
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
+ import { assignInlineVars } from "@vanilla-extract/dynamic";
14
15
  import { ResetIcon } from "@sanity/icons/Reset";
15
16
  import { TrashIcon } from "@sanity/icons/Trash";
16
17
  import { SparklesIcon } from "@sanity/icons/Sparkles";
@@ -18,12 +19,17 @@ import { Tooltip } from "@sanity/ui/tooltip";
18
19
  import { ImageIcon } from "@sanity/icons/Image";
19
20
  import { useToast } from "@sanity/ui/toast";
20
21
  import { AddIcon } from "@sanity/icons/Add";
22
+ import { ClipboardIcon } from "@sanity/icons/Clipboard";
21
23
  import { RestoreIcon } from "@sanity/icons/Restore";
24
+ import { MotionConfig, Reorder, useDragControls } from "motion/react";
25
+ import { ArrowDownIcon } from "@sanity/icons/ArrowDown";
26
+ import { ArrowUpIcon } from "@sanity/icons/ArrowUp";
22
27
  import { CopyIcon } from "@sanity/icons/Copy";
23
28
  import { EditIcon } from "@sanity/icons/Edit";
24
29
  import { EllipsisHorizontalIcon } from "@sanity/icons/EllipsisHorizontal";
30
+ import { ShareIcon } from "@sanity/icons/Share";
25
31
  import { Menu, MenuButton, MenuDivider, MenuItem } from "@sanity/ui/menu";
26
- import { ClipboardIcon } from "@sanity/icons/Clipboard";
32
+ import scrollIntoView from "scroll-into-view-if-needed";
27
33
  import { Code } from "@sanity/ui/code";
28
34
  import { registerLanguage } from "react-refractor";
29
35
  import typescript from "refractor/typescript";
@@ -36,992 +42,1300 @@ function useThemer() {
36
42
  if (!context) throw Error("useThemer must be used within the `themerTool` plugin");
37
43
  return context;
38
44
  }
45
+ /** @internal */
46
+ const IMAGE_PALETTE_KEYS = [
47
+ "vibrant",
48
+ "lightVibrant",
49
+ "darkVibrant",
50
+ "muted",
51
+ "lightMuted",
52
+ "darkMuted",
53
+ "dominant"
54
+ ], IMAGE_PALETTE_VARIANTS = [
55
+ "muted",
56
+ "vibrant",
57
+ "lightMuted",
58
+ "lightVibrant",
59
+ "darkMuted",
60
+ "darkVibrant"
61
+ ], IMAGE_PALETTE_TITLES = {
62
+ dominant: "Dominant",
63
+ vibrant: "Vibrant",
64
+ lightVibrant: "Light vibrant",
65
+ darkVibrant: "Dark vibrant",
66
+ muted: "Muted",
67
+ lightMuted: "Light muted",
68
+ darkMuted: "Dark muted"
69
+ }, TARGETS = [
70
+ ["vibrant", {
71
+ lightness: [
72
+ .5,
73
+ .3,
74
+ .7
75
+ ],
76
+ saturation: [
77
+ 1,
78
+ .35,
79
+ 1
80
+ ]
81
+ }],
82
+ ["lightVibrant", {
83
+ lightness: [
84
+ .74,
85
+ .55,
86
+ 1
87
+ ],
88
+ saturation: [
89
+ 1,
90
+ .35,
91
+ 1
92
+ ]
93
+ }],
94
+ ["darkVibrant", {
95
+ lightness: [
96
+ .26,
97
+ 0,
98
+ .45
99
+ ],
100
+ saturation: [
101
+ 1,
102
+ .35,
103
+ 1
104
+ ]
105
+ }],
106
+ ["muted", {
107
+ lightness: [
108
+ .5,
109
+ .3,
110
+ .7
111
+ ],
112
+ saturation: [
113
+ .3,
114
+ 0,
115
+ .4
116
+ ]
117
+ }],
118
+ ["lightMuted", {
119
+ lightness: [
120
+ .74,
121
+ .55,
122
+ 1
123
+ ],
124
+ saturation: [
125
+ .3,
126
+ 0,
127
+ .4
128
+ ]
129
+ }],
130
+ ["darkMuted", {
131
+ lightness: [
132
+ .26,
133
+ 0,
134
+ .45
135
+ ],
136
+ saturation: [
137
+ .3,
138
+ 0,
139
+ .4
140
+ ]
141
+ }]
142
+ ];
39
143
  /**
40
- * Reduces theme options to the minimal object that recreates them through
41
- * `buildTheme`: in each scheme the accent is dropped when it is the stock
42
- * one, the text color when it matches the one derived from the accent, and
43
- * the background and contrast when they match the defaults — and a scheme
44
- * with nothing left is dropped altogether. Options that boil down to the
45
- * stock Studio theme reduce to `null` — they need nothing from this package.
144
+ * Extracts the color palette of an image from its pixels, the way the Vibrant
145
+ * algorithm does: the colors are quantized into a coarse histogram, and each
146
+ * swatch claims the color that comes closest to its target lightness and
147
+ * saturation — weighted by how much of the image it covers — that no other
148
+ * swatch has claimed yet. The dominant swatch is simply the most common
149
+ * color. Runs anywhere pixels come from, so it can run on device.
46
150
  *
47
151
  * @internal
48
152
  */
49
- function minimizeOptions(options) {
50
- let resolved = resolveThemeOptions(options), minimized = {};
51
- for (let scheme of SCHEMES) {
52
- let values = resolved[scheme], kept = {};
53
- 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);
153
+ function extractImagePalette(pixels) {
154
+ let candidates = quantize(pixels), palette = {
155
+ dominant: null,
156
+ vibrant: null,
157
+ lightVibrant: null,
158
+ darkVibrant: null,
159
+ muted: null,
160
+ lightMuted: null,
161
+ darkMuted: null
162
+ };
163
+ if (candidates.length === 0) return palette;
164
+ let maximumPopulation = Math.max(...candidates.map((candidate) => candidate.population)), claimed = /* @__PURE__ */ new Set();
165
+ for (let [key, target] of TARGETS) {
166
+ let best = null, bestScore = -Infinity;
167
+ for (let candidate of candidates) {
168
+ if (claimed.has(candidate.hex) || !matchesTarget(candidate, target)) continue;
169
+ let score = scoreCandidate(candidate, target, maximumPopulation);
170
+ score > bestScore && (best = candidate, bestScore = score);
171
+ }
172
+ best && (palette[key] = best.hex, claimed.add(best.hex));
54
173
  }
55
- return Object.keys(minimized).length === 0 ? null : minimized;
174
+ return palette.dominant = candidates.reduce((most, candidate) => candidate.population > most.population ? candidate : most).hex, palette;
56
175
  }
57
176
  /**
58
- * Whether two sets of theme options generate the same theme, comparing their
59
- * resolved forms so that derived and explicit values (and hex casing) don't
60
- * matter.
61
- *
62
- * @internal
177
+ * Turns the pixels into a histogram of quantized colors, each averaged over
178
+ * the pixels it stands for, leaving out transparent pixels and the near-black
179
+ * and near-white ones that are backdrop rather than color.
63
180
  */
64
- function sameOptions(a, b) {
65
- let left = resolveThemeOptions(a), right = resolveThemeOptions(b);
66
- return SCHEMES.every((scheme) => sameScheme(left[scheme], right[scheme]));
181
+ function quantize(pixels) {
182
+ let { data } = pixels, buckets = /* @__PURE__ */ new Map(), length = Math.min(data.length, pixels.width * pixels.height * 4);
183
+ for (let offset = 0; offset + 3 < length; offset += 4) {
184
+ let r = data[offset], g = data[offset + 1], b = data[offset + 2];
185
+ if (data[offset + 3] < 125) continue;
186
+ let lightness = (Math.max(r, g, b) + Math.min(r, g, b)) / 510;
187
+ if (lightness < .05 || lightness > .95) continue;
188
+ let key = r >> 3 << 10 | g >> 3 << 5 | b >> 3, bucket = buckets.get(key);
189
+ bucket ? (bucket.count++, bucket.r += r, bucket.g += g, bucket.b += b) : buckets.set(key, {
190
+ count: 1,
191
+ r,
192
+ g,
193
+ b
194
+ });
195
+ }
196
+ let candidates = [];
197
+ for (let bucket of buckets.values()) {
198
+ let r = Math.round(bucket.r / bucket.count), g = Math.round(bucket.g / bucket.count), b = Math.round(bucket.b / bucket.count), { saturation, lightness } = saturationAndLightness(r, g, b);
199
+ candidates.push({
200
+ hex: rgbToHex(r, g, b),
201
+ population: bucket.count,
202
+ saturation,
203
+ lightness
204
+ });
205
+ }
206
+ return candidates;
67
207
  }
68
- function sameScheme(a, b) {
69
- return a.accent === b.accent && a.text === b.text && a.background === b.background && a.contrast === b.contrast;
208
+ function matchesTarget(candidate, target) {
209
+ return candidate.lightness >= target.lightness[1] && candidate.lightness <= target.lightness[2] && candidate.saturation >= target.saturation[1] && candidate.saturation <= target.saturation[2];
70
210
  }
71
- /** The slug of the theme the Studio config was generated from @internal */
72
- const initialThemerState = {
73
- active: null,
74
- custom: [],
75
- removed: []
76
- };
77
- /**
78
- * Resolves the list of themes: the configured theme first, then the presets
79
- * (minus the ones that would only repeat the configured theme), then the
80
- * user's own themes. Removed themes are set aside so they can be restored,
81
- * and the active theme falls back to the configured one when its slug no
82
- * longer resolves.
83
- *
84
- * @internal
85
- */
86
- function resolveThemes(state, baseOptions) {
87
- let config = {
88
- slug: "config",
89
- title: "Studio config",
90
- options: baseOptions,
91
- source: "config"
92
- }, all = [config];
93
- for (let preset of presets) sameOptions(preset.options, baseOptions) || all.push({
94
- slug: preset.slug,
95
- title: preset.title,
96
- options: preset.options,
97
- source: "preset"
98
- });
99
- for (let theme of state.custom) all.push({
100
- slug: theme.slug,
101
- title: theme.title,
102
- options: theme.options,
103
- source: "custom",
104
- ...theme.palette ? { palette: theme.palette } : {}
105
- });
106
- let removedSlugs = new Set(state.removed), themes = all.filter((theme) => !removedSlugs.has(theme.slug));
107
- return {
108
- themes,
109
- removed: all.filter((theme) => removedSlugs.has(theme.slug)),
110
- active: themes.find((theme) => theme.slug === state.active) ?? config
111
- };
211
+ function scoreCandidate(candidate, target, maximumPopulation) {
212
+ return 3 * (1 - Math.abs(candidate.saturation - target.saturation[0])) + 6.5 * (1 - Math.abs(candidate.lightness - target.lightness[0])) + .5 * (candidate.population / maximumPopulation);
112
213
  }
113
- /**
114
- * Creates a custom theme with a slug that cannot collide with the presets or
115
- * the configured theme.
116
- *
117
- * @internal
118
- */
119
- function createCustomTheme(title, options, palette) {
214
+ function saturationAndLightness(r, g, b) {
215
+ let max = Math.max(r, g, b) / 255, min = Math.min(r, g, b) / 255, lightness = (max + min) / 2, delta = max - min;
120
216
  return {
121
- slug: `custom-${randomId()}`,
122
- title,
123
- options,
124
- ...palette ? { palette } : {}
217
+ saturation: delta === 0 ? 0 : lightness > .5 ? delta / (2 - max - min) : delta / (max + min),
218
+ lightness
125
219
  };
126
220
  }
127
- /** The title of a duplicated theme @internal */
128
- function duplicateTitle(title) {
129
- return `${displayTitle(title)} copy`;
221
+ function rgbToHex(r, g, b) {
222
+ return `#${[
223
+ r,
224
+ g,
225
+ b
226
+ ].map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
130
227
  }
131
- /** The title to show for a theme, falling back when the user cleared it @internal */
132
- function displayTitle(title) {
133
- return title.trim() || "Untitled theme";
228
+ /** Reads `#rgb` and `#rrggbb` alike — stored palettes may hold either */
229
+ function hexToRgb(hex) {
230
+ let expanded = hex.length === 4 ? `${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex.slice(1);
231
+ return [
232
+ parseInt(expanded.slice(0, 2), 16),
233
+ parseInt(expanded.slice(2, 4), 16),
234
+ parseInt(expanded.slice(4, 6), 16)
235
+ ];
134
236
  }
135
- function randomId() {
136
- return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
237
+ /** Mixes `amount` of `color` into `base` */
238
+ function tint(base, color, amount) {
239
+ let [r1, g1, b1] = hexToRgb(base), [r2, g2, b2] = hexToRgb(color);
240
+ return rgbToHex(Math.round(r1 + (r2 - r1) * amount), Math.round(g1 + (g2 - g1) * amount), Math.round(b1 + (b2 - b1) * amount));
137
241
  }
138
- function themesOf(context) {
139
- return resolveThemes(context, context.baseOptions);
242
+ /** How much of the palette tints each scheme's stock background */
243
+ const BACKGROUND_TINT = {
244
+ light: .08,
245
+ dark: .3
246
+ }, BUTTON_LABEL = {
247
+ light: "#ffffff",
248
+ dark: "#000000"
249
+ }, ACCENT_LIGHTNESS_STEP = .01;
250
+ function contrastRatio(a, b) {
251
+ let [lighter, darker] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
252
+ return (lighter + .05) / (darker + .05);
140
253
  }
141
254
  /**
142
- * The state of the themer tool: three parallel regions, whether the
143
- * `sidebar` is open, which `flow` it is in — picking a theme from the `list`,
144
- * `edit`ing one of the user's own themes, or restoring `removed` ones — and
145
- * whether the `preview` shows the Studio once (`single`) or in light and dark
146
- * side by side (`split`). The context carries the persisted state (the
147
- * applied theme, the user's themes and what was removed) alongside what the
148
- * flows need.
149
- *
150
- * Theme operations are handled in every flow, and the flows leave on their
151
- * own when they lose their subject: the editor when its theme is removed or
152
- * deleted, and the removed view when its last theme is restored or deleted.
255
+ * Makes an accent readable as a button in the given scheme: a light scheme
256
+ * accent is darkened until white text on it reaches AA contrast, a dark
257
+ * scheme accent lightened until black text does — as far as the accent's
258
+ * hue and saturation allow. Accents that already pass come back untouched.
153
259
  *
154
260
  * @internal
155
261
  */
156
- const themerMachine = setup({
157
- types: {
158
- context: {},
159
- events: {},
160
- input: {}
161
- },
162
- guards: {
163
- isCustomTheme: ({ context }, params) => themesOf(context).themes.some((theme) => theme.slug === params.slug && theme.source === "custom"),
164
- isEditingListedTheme: ({ context }) => context.editing !== null && themesOf(context).themes.some((theme) => theme.slug === context.editing?.slug && theme.source === "custom"),
165
- hasRemovedThemes: ({ context }) => themesOf(context).removed.length > 0
166
- },
167
- actions: {
168
- pick: assign((_, params) => ({ active: params.slug === "config" ? null : params.slug })),
169
- add: assign(({ context }, params) => {
170
- let theme = createCustomTheme(params.title ?? "Untitled theme", params.options ?? themesOf(context).active.options, params.palette);
171
- return {
172
- active: theme.slug,
173
- custom: [...context.custom, theme],
174
- editing: {
175
- slug: theme.slug,
176
- focusTitle: params.title === void 0
177
- },
178
- images: params.imageUrl ? {
179
- ...context.images,
180
- [theme.slug]: params.imageUrl
181
- } : context.images
182
- };
183
- }),
184
- duplicate: assign(({ context }, params) => {
185
- let { themes, removed } = themesOf(context), source = [...themes, ...removed].find((theme) => theme.slug === params.slug);
186
- if (!source) return {};
187
- let theme = createCustomTheme(duplicateTitle(source.title), source.options, source.palette);
188
- return {
189
- active: theme.slug,
190
- custom: [...context.custom, theme],
191
- editing: {
192
- slug: theme.slug,
193
- focusTitle: !0
194
- }
195
- };
196
- }),
197
- startEditing: assign((_, params) => ({
198
- active: params.slug,
199
- editing: {
200
- slug: params.slug,
201
- focusTitle: !1
202
- }
203
- })),
204
- stopEditing: assign({ editing: null }),
205
- update: assign(({ context }, params) => ({
206
- custom: context.custom.map((theme) => theme.slug === params.slug ? {
207
- ...theme,
208
- title: params.title ?? theme.title,
209
- options: params.options ?? theme.options,
210
- ...params.palette ? { palette: params.palette } : {}
211
- } : theme),
212
- images: params.imageUrl && context.custom.some((theme) => theme.slug === params.slug) ? {
213
- ...context.images,
214
- [params.slug]: params.imageUrl
215
- } : context.images
216
- })),
217
- remove: assign(({ context }, params) => params.slug === "config" || context.removed.includes(params.slug) ? {} : {
218
- active: context.active === params.slug ? null : context.active,
219
- removed: [...context.removed, params.slug]
220
- }),
221
- restore: assign(({ context }, params) => ({ removed: context.removed.filter((slug) => slug !== params.slug) })),
222
- delete: assign(({ context }, params) => {
223
- if (!context.custom.some((theme) => theme.slug === params.slug)) return {};
224
- let { [params.slug]: _deleted, ...images } = context.images;
225
- return {
226
- active: context.active === params.slug ? null : context.active,
227
- custom: context.custom.filter((theme) => theme.slug !== params.slug),
228
- removed: context.removed.filter((slug) => slug !== params.slug),
229
- images
230
- };
231
- })
232
- }
233
- }).createMachine({
234
- id: "themer",
235
- context: ({ input }) => ({
236
- ...input.stored,
237
- baseOptions: input.baseOptions,
238
- editing: null,
239
- images: {}
240
- }),
241
- type: "parallel",
242
- states: {
243
- sidebar: {
244
- initial: "closed",
245
- states: {
246
- closed: { on: { "sidebar.toggle": "open" } },
247
- open: { on: {
248
- "sidebar.toggle": "closed",
249
- "sidebar.close": "closed"
250
- } }
251
- }
252
- },
253
- preview: {
254
- initial: "single",
255
- states: {
256
- single: { on: { "preview.toggle": "split" } },
257
- split: { on: { "preview.toggle": "single" } }
258
- }
259
- },
260
- flow: {
261
- initial: "list",
262
- on: {
263
- "theme.pick": { actions: {
264
- type: "pick",
265
- params: ({ event }) => ({ slug: event.slug })
266
- } },
267
- "theme.add": {
268
- target: ".edit",
269
- actions: {
270
- type: "add",
271
- params: ({ event }) => ({
272
- title: event.title,
273
- options: event.options,
274
- palette: event.palette,
275
- imageUrl: event.imageUrl
276
- })
277
- }
278
- },
279
- "theme.duplicate": {
280
- target: ".edit",
281
- actions: {
282
- type: "duplicate",
283
- params: ({ event }) => ({ slug: event.slug })
284
- }
285
- },
286
- "theme.edit": {
287
- target: ".edit",
288
- guard: {
289
- type: "isCustomTheme",
290
- params: ({ event }) => ({ slug: event.slug })
291
- },
292
- actions: {
293
- type: "startEditing",
294
- params: ({ event }) => ({ slug: event.slug })
295
- }
296
- },
297
- "theme.update": { actions: {
298
- type: "update",
299
- params: ({ event }) => ({
300
- slug: event.slug,
301
- title: event.title,
302
- options: event.options,
303
- palette: event.palette,
304
- imageUrl: event.imageUrl
305
- })
306
- } },
307
- "theme.remove": { actions: {
308
- type: "remove",
309
- params: ({ event }) => ({ slug: event.slug })
310
- } },
311
- "theme.restore": { actions: {
312
- type: "restore",
313
- params: ({ event }) => ({ slug: event.slug })
314
- } },
315
- "theme.delete": { actions: {
316
- type: "delete",
317
- params: ({ event }) => ({ slug: event.slug })
318
- } },
319
- "flow.list": ".list",
320
- "flow.removed": {
321
- target: ".removed",
322
- guard: "hasRemovedThemes"
323
- }
324
- },
325
- states: {
326
- list: {},
327
- edit: {
328
- exit: "stopEditing",
329
- always: {
330
- guard: not("isEditingListedTheme"),
331
- target: "list"
332
- }
333
- },
334
- removed: { always: {
335
- guard: not("hasRemovedThemes"),
336
- target: "list"
337
- } }
338
- }
339
- }
262
+ function readableAccent(hex, scheme) {
263
+ let label = BUTTON_LABEL[scheme], { h, s } = hexToHsl(hex), { l } = hexToHsl(hex), current = hex;
264
+ for (; contrastRatio(current, label) < 4.5;) {
265
+ let next = scheme === "light" ? l - ACCENT_LIGHTNESS_STEP : l + ACCENT_LIGHTNESS_STEP;
266
+ if (next < 0 || next > 1) break;
267
+ l = next, current = hslToHex({
268
+ h,
269
+ s,
270
+ l
271
+ });
340
272
  }
341
- });
342
- /** The part of the machine context that is persisted between sessions @internal */
343
- function selectStoredState(snapshot) {
344
- let { active, custom, removed } = snapshot.context;
345
- return {
346
- active,
347
- custom,
348
- removed
349
- };
273
+ return current;
274
+ }
275
+ /** The accent a variant gives a scheme, made readable on a button */
276
+ function variantAccent(palette, variant, scheme) {
277
+ let swatch = (variant ? palette[variant] : null) ?? palette.vibrant ?? (scheme === "light" ? palette.darkVibrant : palette.lightVibrant) ?? palette.dominant;
278
+ return swatch === null ? null : readableAccent(swatch, scheme);
350
279
  }
351
- const Root$2 = styled(Box).withConfig({
352
- displayName: "Root",
353
- componentId: "sc-6lslk9-0"
354
- })`scrollbar-gutter:stable;`, ScrollAreaInsetContext = createContext({
355
- left: 0,
356
- right: 0,
357
- bottom: 0
358
- });
359
280
  /**
360
- * The scrolling body of a sidebar flow. It always reserves the scrollbar's
361
- * space, so the content does not shift when the list grows or shrinks past
362
- * the point where it needs to scroll — and it takes that space out of its
363
- * right padding, so the content stays centered rather than hugging the left.
364
- * With overlay scrollbars there is no gutter and the padding stays even.
281
+ * Derives theme options from an image palette: the accent of both schemes is
282
+ * the swatch of the chosen variant — or, without one, the vibrant swatch,
283
+ * each scheme falling back to the vibrant swatch of its own end of the scale —
284
+ * adjusted until it is readable as a button. The muted swatch becomes the text color, and the light and dark muted
285
+ * swatches tint the stock backgrounds of their scheme — enough to carry the
286
+ * image's tone without giving up a usable canvas. Whatever the palette lacks
287
+ * stays at the theme defaults.
365
288
  *
366
289
  * @internal
367
290
  */
368
- function ScrollArea(props) {
369
- let $ = c(15), { children, padding } = props, { space } = useTheme_v2(), ref = useRef(null), [gutter, setGutter] = useState(0), t0, t1;
370
- $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = () => {
371
- let element = ref.current;
372
- if (!element) return;
373
- let measure = () => setGutter(element.offsetWidth - element.clientWidth);
374
- measure();
375
- let observer = new ResizeObserver(measure);
376
- return observer.observe(element), () => observer.disconnect();
377
- }, t1 = [], $[0] = t0, $[1] = t1) : (t0 = $[0], t1 = $[1]), useEffect(t0, t1);
378
- let t2 = space[padding], t3 = Math.max(0, space[padding] - gutter), t4 = space[padding], t5;
379
- $[2] !== t2 || $[3] !== t3 || $[4] !== t4 ? (t5 = {
380
- left: t2,
381
- right: t3,
382
- bottom: t4
383
- }, $[2] = t2, $[3] = t3, $[4] = t4, $[5] = t5) : t5 = $[5];
384
- let inset = t5, t6;
385
- $[6] === inset.right ? t6 = $[7] : (t6 = { paddingRight: inset.right }, $[6] = inset.right, $[7] = t6);
386
- let t7;
387
- $[8] !== children || $[9] !== inset ? (t7 = /* @__PURE__ */ jsx(ScrollAreaInsetContext.Provider, {
388
- value: inset,
389
- children
390
- }), $[8] = children, $[9] = inset, $[10] = t7) : t7 = $[10];
391
- let t8;
392
- return $[11] !== padding || $[12] !== t6 || $[13] !== t7 ? (t8 = /* @__PURE__ */ jsx(Root$2, {
393
- flex: 1,
394
- overflow: "auto",
395
- padding,
396
- ref,
397
- style: t6,
398
- children: t7
399
- }), $[11] = padding, $[12] = t6, $[13] = t7, $[14] = t8) : t8 = $[14], t8;
291
+ function optionsFromImagePalette(palette, variant) {
292
+ let options = {}, light = schemeFromPalette(variantAccent(palette, variant ?? null, "light"), palette.muted ?? palette.darkMuted, palette.lightMuted ?? palette.muted ?? palette.lightVibrant, "light"), dark = schemeFromPalette(variantAccent(palette, variant ?? null, "dark"), palette.muted ?? palette.lightMuted, palette.darkMuted ?? palette.muted ?? palette.darkVibrant, "dark");
293
+ return light && (options.light = light), dark && (options.dark = dark), options;
294
+ }
295
+ function schemeFromPalette(accent, text, backgroundTint, scheme) {
296
+ let options = {};
297
+ return accent && (options.accent = accent), text && (options.text = text), backgroundTint && (options.background = tint(DEFAULT_BACKGROUND[scheme], backgroundTint, BACKGROUND_TINT[scheme])), Object.keys(options).length > 0 ? options : null;
400
298
  }
401
299
  /**
402
- * Content that runs edge to edge in a scroll area — up to the scrollbar —
403
- * instead of sitting inside its padding. It goes last: it runs down to the
404
- * bottom edge too.
300
+ * Applies an image palette to existing theme options: the colors the palette
301
+ * provides replace the current ones scheme by scheme, and whatever it does
302
+ * not touch — like the contrast — stays as it is.
405
303
  *
406
304
  * @internal
407
305
  */
408
- function ScrollAreaBleed(props) {
409
- let $ = c(7), { left, right, bottom } = useContext(ScrollAreaInsetContext), t0 = -left, t1 = -right, t2 = -bottom, t3;
410
- $[0] !== t0 || $[1] !== t1 || $[2] !== t2 ? (t3 = {
411
- marginLeft: t0,
412
- marginRight: t1,
413
- marginBottom: t2
414
- }, $[0] = t0, $[1] = t1, $[2] = t2, $[3] = t3) : t3 = $[3];
415
- let t4;
416
- return $[4] !== props.children || $[5] !== t3 ? (t4 = /* @__PURE__ */ jsx("div", {
417
- style: t3,
418
- children: props.children
419
- }), $[4] = props.children, $[5] = t3, $[6] = t4) : t4 = $[6], t4;
306
+ function applyImagePalette(options, palette, variant) {
307
+ let derived = optionsFromImagePalette(palette, variant), next = { ...options };
308
+ for (let scheme of SCHEMES) derived[scheme] && (next[scheme] = {
309
+ ...options[scheme],
310
+ ...derived[scheme]
311
+ });
312
+ return next;
420
313
  }
421
314
  /**
422
- * The dark scheme covers the right side of the preview behind a diagonal cut,
423
- * like the "Auto" appearance preview in macOS System Settings shows both
424
- * schemes at once.
315
+ * The variant a theme is built around: the one whose swatch is the theme's
316
+ * accent, or `null` when the accent comes from somewhere else.
317
+ *
318
+ * @internal
425
319
  */
426
- const Root$1 = styled.div.withConfig({
427
- displayName: "Root",
428
- componentId: "sc-w55yz9-0"
429
- })`position:relative;aspect-ratio:16 / 9;overflow:hidden;`, LIST_ROWS = [
430
- "62%",
431
- "46%",
432
- "70%",
433
- "52%"
434
- ], FIELDS = ["32%", "24%"];
320
+ function currentImageVariant(options, palette) {
321
+ let accent = options.light?.accent;
322
+ return accent ? IMAGE_PALETTE_VARIANTS.find((variant) => palette[variant] !== null && variantAccent(palette, variant, "light") === accent) ?? null : null;
323
+ }
435
324
  /**
436
- * A tiny, lo-fi mockup of a Studio in the given theme — the navbar over the
437
- * structure tool with a list of documents on the left and a document form on
438
- * the right — in both color schemes at once: the light scheme on the left and
439
- * the dark scheme on the right, split diagonally. It only paints colors the
440
- * theme options change (backgrounds, text, borders, the accent) so that a
441
- * glance shows what the theme does to the Studio — with few and bold enough
442
- * elements to still read when scaled down to a thumbnail.
325
+ * How much a swatch has going for it as an accent: saturation first, then
326
+ * closeness to mid lightness — the range where an accent reads best.
327
+ */
328
+ function interest(hex) {
329
+ let { s, l } = hexToHsl(hex);
330
+ return .1 + .55 * s + .35 * (1 - Math.abs(l - .5) * 2);
331
+ }
332
+ /**
333
+ * Picks a variant for the "I'm feeling lucky" button: a random one, weighted
334
+ * by how interesting its swatch is as an accent, and never the one the theme
335
+ * already uses while there are others to pick from.
443
336
  *
444
337
  * @internal
445
338
  */
446
- function ThemePreview(props) {
447
- let $ = c(11), { options } = props, t0;
448
- $[0] === options ? t0 = $[1] : (t0 = buildTheme(options), $[0] = options, $[1] = t0);
449
- let theme = t0, t1;
450
- $[2] === Symbol.for("react.memo_cache_sentinel") ? (t1 = /* @__PURE__ */ jsx(MockStudio, {}), $[2] = t1) : t1 = $[2];
451
- let t2;
452
- $[3] === theme ? t2 = $[4] : (t2 = /* @__PURE__ */ jsx(ThemeProvider, {
453
- scheme: "light",
454
- theme,
455
- tone: "default",
456
- children: t1
457
- }), $[3] = theme, $[4] = t2);
458
- let t3;
459
- $[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];
460
- let t4;
461
- $[6] === theme ? t4 = $[7] : (t4 = /* @__PURE__ */ jsx(ThemeProvider, {
462
- scheme: "dark",
463
- theme,
464
- tone: "default",
465
- children: t3
466
- }), $[6] = theme, $[7] = t4);
467
- let t5;
468
- return $[8] !== t2 || $[9] !== t4 ? (t5 = /* @__PURE__ */ jsxs(Root$1, {
469
- "aria-hidden": !0,
470
- children: [t2, t4]
471
- }), $[8] = t2, $[9] = t4, $[10] = t5) : t5 = $[10], t5;
472
- }
473
- function MockStudio(props) {
474
- 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;
475
- $[0] !== color.bg || $[1] !== props.style ? (t0 = {
476
- position: "absolute",
477
- inset: 0,
478
- display: "flex",
479
- flexDirection: "column",
480
- background: color.bg,
481
- ...props.style
482
- }, $[0] = color.bg, $[1] = props.style, $[2] = t0) : t0 = $[2];
483
- let t1;
484
- $[3] === border ? t1 = $[4] : (t1 = {
485
- flex: "none",
486
- display: "flex",
487
- alignItems: "center",
488
- gap: 5,
489
- height: "18%",
490
- padding: "0 5%",
491
- borderBottom: border
492
- }, $[3] = border, $[4] = t1);
493
- let t2;
494
- $[5] === primary.bg ? t2 = $[6] : (t2 = /* @__PURE__ */ jsx("div", { style: {
495
- flex: "none",
496
- width: 8,
497
- height: 8,
498
- borderRadius: 2,
499
- background: primary.bg
500
- } }), $[5] = primary.bg, $[6] = t2);
501
- let t3;
502
- $[7] === color.fg ? t3 = $[8] : (t3 = /* @__PURE__ */ jsx("div", { style: line("22%", color.fg) }), $[7] = color.fg, $[8] = t3);
503
- let t4;
504
- $[9] === Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx("div", { style: { flex: 1 } }), $[9] = t4) : t4 = $[9];
505
- let t5;
506
- $[10] === color.avatar.blue.bg ? t5 = $[11] : (t5 = /* @__PURE__ */ jsx("div", { style: {
507
- flex: "none",
508
- width: 8,
509
- height: 8,
510
- borderRadius: "50%",
511
- background: color.avatar.blue.bg
512
- } }), $[10] = color.avatar.blue.bg, $[11] = t5);
513
- let t6;
514
- $[12] !== t1 || $[13] !== t2 || $[14] !== t3 || $[15] !== t5 ? (t6 = /* @__PURE__ */ jsxs("div", {
515
- style: t1,
516
- children: [
517
- t2,
518
- t3,
519
- t4,
520
- t5
521
- ]
522
- }), $[12] = t1, $[13] = t2, $[14] = t3, $[15] = t5, $[16] = t6) : t6 = $[16];
523
- let t7;
524
- $[17] === Symbol.for("react.memo_cache_sentinel") ? (t7 = {
525
- flex: 1,
526
- display: "flex",
527
- minHeight: 0
528
- }, $[17] = t7) : t7 = $[17];
529
- let t8;
530
- $[18] === border ? t8 = $[19] : (t8 = {
531
- flex: "none",
532
- display: "flex",
533
- flexDirection: "column",
534
- width: "36%",
535
- borderRight: border
536
- }, $[18] = border, $[19] = t8);
537
- let t9;
538
- $[20] !== color.muted || $[21] !== selected ? (t9 = LIST_ROWS.map((width_0, index) => {
539
- let isSelected = index === 1;
540
- return /* @__PURE__ */ jsx("div", {
541
- style: {
542
- flex: "none",
543
- display: "flex",
544
- alignItems: "center",
545
- height: "18%",
546
- padding: "0 9%",
547
- background: isSelected ? selected.bg : void 0
548
- },
549
- children: /* @__PURE__ */ jsx("div", { style: line(width_0, isSelected ? selected.fg : color.muted.fg) })
550
- }, width_0);
551
- }), $[20] = color.muted, $[21] = selected, $[22] = t9) : t9 = $[22];
552
- let t10;
553
- $[23] !== t8 || $[24] !== t9 ? (t10 = /* @__PURE__ */ jsx("div", {
554
- style: t8,
555
- children: t9
556
- }), $[23] = t8, $[24] = t9, $[25] = t10) : t10 = $[25];
557
- let t11, t12;
558
- $[26] === Symbol.for("react.memo_cache_sentinel") ? (t11 = {
559
- flex: 1,
560
- display: "flex",
561
- flexDirection: "column",
562
- minWidth: 0
563
- }, t12 = {
564
- flex: 1,
565
- display: "flex",
566
- flexDirection: "column",
567
- gap: 7,
568
- padding: "9% 8% 0",
569
- minHeight: 0,
570
- overflow: "hidden"
571
- }, $[26] = t11, $[27] = t12) : (t11 = $[26], t12 = $[27]);
572
- let t13;
573
- $[28] === color.fg ? t13 = $[29] : (t13 = /* @__PURE__ */ jsx("div", { style: line("45%", color.fg) }), $[28] = color.fg, $[29] = t13);
574
- let t14;
575
- $[30] !== color.muted || $[31] !== input ? (t14 = FIELDS.map((width_1) => /* @__PURE__ */ jsxs("div", {
576
- style: {
577
- flex: "none",
578
- display: "flex",
579
- flexDirection: "column",
580
- gap: 3
581
- },
582
- children: [/* @__PURE__ */ jsx("div", { style: line(width_1, color.muted.fg) }), /* @__PURE__ */ jsx("div", { style: {
583
- height: 12,
584
- borderRadius: 3,
585
- background: input.bg,
586
- boxShadow: `inset 0 0 0 1px ${input.border}`
587
- } })]
588
- }, width_1)), $[30] = color.muted, $[31] = input, $[32] = t14) : t14 = $[32];
589
- let t15;
590
- $[33] !== t13 || $[34] !== t14 ? (t15 = /* @__PURE__ */ jsxs("div", {
591
- style: t12,
592
- children: [t13, t14]
593
- }), $[33] = t13, $[34] = t14, $[35] = t15) : t15 = $[35];
594
- let t16;
595
- $[36] === border ? t16 = $[37] : (t16 = {
596
- flex: "none",
597
- display: "flex",
598
- alignItems: "center",
599
- justifyContent: "flex-end",
600
- height: "20%",
601
- padding: "0 8%",
602
- borderTop: border
603
- }, $[36] = border, $[37] = t16);
604
- let t17;
605
- $[38] === primary.bg ? t17 = $[39] : (t17 = /* @__PURE__ */ jsx("div", { style: {
606
- width: "32%",
607
- height: "50%",
608
- borderRadius: 3,
609
- background: primary.bg
610
- } }), $[38] = primary.bg, $[39] = t17);
611
- let t18;
612
- $[40] !== t16 || $[41] !== t17 ? (t18 = /* @__PURE__ */ jsx("div", {
613
- style: t16,
614
- children: t17
615
- }), $[40] = t16, $[41] = t17, $[42] = t18) : t18 = $[42];
616
- let t19;
617
- $[43] !== t15 || $[44] !== t18 ? (t19 = /* @__PURE__ */ jsxs("div", {
618
- style: t11,
619
- children: [t15, t18]
620
- }), $[43] = t15, $[44] = t18, $[45] = t19) : t19 = $[45];
621
- let t20;
622
- $[46] !== t10 || $[47] !== t19 ? (t20 = /* @__PURE__ */ jsxs("div", {
623
- style: t7,
624
- children: [t10, t19]
625
- }), $[46] = t10, $[47] = t19, $[48] = t20) : t20 = $[48];
626
- let t21;
627
- return $[49] !== t0 || $[50] !== t20 || $[51] !== t6 ? (t21 = /* @__PURE__ */ jsxs("div", {
628
- style: t0,
629
- children: [t6, t20]
630
- }), $[49] = t0, $[50] = t20, $[51] = t6, $[52] = t21) : t21 = $[52], t21;
339
+ function pickLuckyVariant(palette, options = {}) {
340
+ let { exclude = null, random = Math.random } = options, candidates = IMAGE_PALETTE_VARIANTS.filter((variant) => palette[variant] !== null);
341
+ if (candidates.length > 1 && (candidates = candidates.filter((variant) => variant !== exclude)), candidates.length === 0) return null;
342
+ let weights = candidates.map((variant) => interest(palette[variant])), total = weights.reduce((sum, weight) => sum + weight, 0), pick = random() * total;
343
+ for (let [index, variant] of candidates.entries()) if (pick -= weights[index], pick < 0) return variant;
344
+ return candidates.at(-1) ?? null;
631
345
  }
632
- function _temp$4(width, background) {
633
- return {
634
- flex: "none",
635
- width,
636
- height: 4,
637
- borderRadius: 2,
638
- background
639
- };
346
+ /** A theme title for the image a palette came from, e.g. `sunset-beach.jpg` → `sunset beach` @internal */
347
+ function titleFromFileName(name) {
348
+ return name.replace(/\.[^.]+$/, "").replace(/[-_]+/g, " ").trim() || "Image theme";
640
349
  }
641
350
  /**
642
- * The floating preview card of a theme: the lo-fi studio mockup on a raised,
643
- * rounded card, like the appearance previews in macOS System Settings.
351
+ * Reduces theme options to the minimal object that recreates them through
352
+ * `buildTheme`: in each scheme the accent is dropped when it is the stock
353
+ * one, the text color when it matches the one derived from the accent, and
354
+ * the background and contrast when they match the defaults — and a scheme
355
+ * with nothing left is dropped altogether. Options that boil down to the
356
+ * stock Studio theme reduce to `null` — they need nothing from this package.
644
357
  *
645
358
  * @internal
646
359
  */
647
- function ThemeThumbnail(props) {
648
- let $ = c(2), t0;
649
- return $[0] === props.options ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(Card, {
650
- overflow: "hidden",
651
- radius: 3,
652
- shadow: 1,
653
- children: /* @__PURE__ */ jsx(ThemePreview, { options: props.options })
654
- }), $[0] = props.options, $[1] = t0), t0;
360
+ function minimizeOptions(options) {
361
+ let resolved = resolveThemeOptions(options), minimized = {};
362
+ for (let scheme of SCHEMES) {
363
+ let values = resolved[scheme], kept = {};
364
+ values.accent !== DEFAULT_ACCENT && (kept.accent = values.accent), values.text !== deriveTextColor(values.accent) && (kept.text = values.text), values.background !== DEFAULT_BACKGROUND[scheme] && (kept.background = values.background), values.contrast !== 85 && (kept.contrast = values.contrast), Object.keys(kept).length > 0 && (minimized[scheme] = kept);
365
+ }
366
+ return Object.keys(minimized).length === 0 ? null : minimized;
655
367
  }
656
368
  /**
657
- * The flow for restoring removed themes: the presets and custom themes the
658
- * user took out of the list, each with a button to put it back — and, for
659
- * custom themes, one to delete it for good. The machine leaves the flow once
660
- * the last one is gone.
369
+ * Whether two sets of theme options generate the same theme, comparing their
370
+ * resolved forms so that derived and explicit values (and hex casing) don't
371
+ * matter.
661
372
  *
662
373
  * @internal
663
374
  */
664
- function RemovedThemes() {
665
- let $ = c(7), { removed, send } = useThemer(), t0;
666
- if ($[0] !== removed || $[1] !== send) {
667
- let t1;
668
- $[3] === send ? t1 = $[4] : (t1 = (theme) => /* @__PURE__ */ jsxs(Stack, {
669
- gap: 3,
670
- children: [
671
- /* @__PURE__ */ jsx(Box, {
672
- style: { opacity: .6 },
673
- children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: theme.options })
674
- }),
675
- /* @__PURE__ */ jsx(Text, {
676
- align: "center",
677
- size: 1,
678
- textOverflow: "ellipsis",
679
- children: displayTitle(theme.title)
680
- }),
681
- /* @__PURE__ */ jsxs(Flex, {
682
- gap: 2,
683
- justify: "center",
684
- children: [/* @__PURE__ */ jsx(Button, {
685
- fontSize: 1,
686
- mode: "ghost",
687
- onClick: () => send({
688
- type: "theme.restore",
689
- slug: theme.slug
690
- }),
691
- padding: 2,
692
- text: "Restore"
693
- }), theme.source === "custom" && /* @__PURE__ */ jsx(Button, {
694
- fontSize: 1,
695
- mode: "ghost",
696
- onClick: () => send({
697
- type: "theme.delete",
698
- slug: theme.slug
699
- }),
700
- padding: 2,
701
- text: "Delete",
702
- tone: "critical"
703
- })]
704
- })
705
- ]
706
- }, theme.slug), $[3] = send, $[4] = t1), t0 = removed.map(t1), $[0] = removed, $[1] = send, $[2] = t0;
707
- } else t0 = $[2];
708
- let t1;
709
- return $[5] === t0 ? t1 = $[6] : (t1 = /* @__PURE__ */ jsx(ScrollArea, {
710
- padding: 3,
711
- children: /* @__PURE__ */ jsx(Stack, {
712
- gap: 5,
713
- children: t0
714
- })
715
- }), $[5] = t0, $[6] = t1), t1;
375
+ function sameOptions(a, b) {
376
+ let left = resolveThemeOptions(a), right = resolveThemeOptions(b);
377
+ return SCHEMES.every((scheme) => sameScheme(left[scheme], right[scheme]));
716
378
  }
717
- /** @internal */
718
- const IMAGE_PALETTE_KEYS = [
719
- "vibrant",
720
- "lightVibrant",
721
- "darkVibrant",
722
- "muted",
723
- "lightMuted",
724
- "darkMuted",
725
- "dominant"
726
- ], IMAGE_PALETTE_VARIANTS = [
727
- "muted",
728
- "vibrant",
729
- "lightMuted",
730
- "lightVibrant",
731
- "darkMuted",
732
- "darkVibrant"
733
- ], IMAGE_PALETTE_TITLES = {
734
- dominant: "Dominant",
735
- vibrant: "Vibrant",
736
- lightVibrant: "Light vibrant",
737
- darkVibrant: "Dark vibrant",
738
- muted: "Muted",
739
- lightMuted: "Light muted",
740
- darkMuted: "Dark muted"
741
- }, TARGETS = [
742
- ["vibrant", {
743
- lightness: [
744
- .5,
745
- .3,
746
- .7
747
- ],
748
- saturation: [
749
- 1,
750
- .35,
751
- 1
752
- ]
753
- }],
754
- ["lightVibrant", {
755
- lightness: [
756
- .74,
757
- .55,
758
- 1
759
- ],
760
- saturation: [
761
- 1,
762
- .35,
763
- 1
764
- ]
765
- }],
766
- ["darkVibrant", {
767
- lightness: [
768
- .26,
769
- 0,
770
- .45
771
- ],
772
- saturation: [
773
- 1,
774
- .35,
775
- 1
776
- ]
777
- }],
778
- ["muted", {
779
- lightness: [
780
- .5,
781
- .3,
782
- .7
783
- ],
784
- saturation: [
785
- .3,
786
- 0,
787
- .4
788
- ]
789
- }],
790
- ["lightMuted", {
791
- lightness: [
792
- .74,
793
- .55,
794
- 1
795
- ],
796
- saturation: [
797
- .3,
798
- 0,
799
- .4
800
- ]
801
- }],
802
- ["darkMuted", {
803
- lightness: [
804
- .26,
805
- 0,
806
- .45
807
- ],
808
- saturation: [
809
- .3,
810
- 0,
811
- .4
812
- ]
813
- }]
814
- ];
379
+ function sameScheme(a, b) {
380
+ return a.accent === b.accent && a.text === b.text && a.background === b.background && a.contrast === b.contrast;
381
+ }
382
+ /** The slug of the theme the Studio config was generated from @internal */
383
+ const initialThemerState = {
384
+ active: null,
385
+ custom: [],
386
+ removed: [],
387
+ order: []
388
+ };
815
389
  /**
816
- * Extracts the color palette of an image from its pixels, the way the Vibrant
817
- * algorithm does: the colors are quantized into a coarse histogram, and each
818
- * swatch claims the color that comes closest to its target lightness and
819
- * saturation — weighted by how much of the image it covers — that no other
820
- * swatch has claimed yet. The dominant swatch is simply the most common
821
- * color. Runs anywhere pixels come from, so it can run on device.
390
+ * Resolves the list of themes: the configured theme first, then the presets
391
+ * (minus the ones that would only repeat the configured theme), then the
392
+ * user's own themes — rearranged into the order the user dragged them into,
393
+ * with themes that order does not know about kept in that default order after
394
+ * the ones it does. Removed themes are set aside so they can be restored, and
395
+ * the active theme falls back to the configured one when its slug no longer
396
+ * resolves.
822
397
  *
823
398
  * @internal
824
399
  */
825
- function extractImagePalette(pixels) {
826
- let candidates = quantize(pixels), palette = {
827
- dominant: null,
828
- vibrant: null,
829
- lightVibrant: null,
830
- darkVibrant: null,
831
- muted: null,
832
- lightMuted: null,
833
- darkMuted: null
400
+ function resolveThemes(state, baseOptions) {
401
+ let config = {
402
+ slug: "config",
403
+ title: "Studio config",
404
+ options: baseOptions,
405
+ source: "config"
406
+ }, all = [config];
407
+ for (let preset of presets) sameOptions(preset.options, baseOptions) || all.push({
408
+ slug: preset.slug,
409
+ title: preset.title,
410
+ options: preset.options,
411
+ source: "preset"
412
+ });
413
+ for (let theme of state.custom) all.push({
414
+ slug: theme.slug,
415
+ title: theme.title,
416
+ options: theme.options,
417
+ source: "custom",
418
+ ...theme.palette ? { palette: theme.palette } : {}
419
+ });
420
+ let position = new Map(state.order.map((slug, index) => [slug, index])), rank = (theme) => position.get(theme.slug) ?? state.order.length;
421
+ all.sort((a, b) => rank(a) - rank(b));
422
+ let removedSlugs = new Set(state.removed), themes = all.filter((theme) => !removedSlugs.has(theme.slug));
423
+ return {
424
+ themes,
425
+ removed: all.filter((theme) => removedSlugs.has(theme.slug)),
426
+ active: themes.find((theme) => theme.slug === state.active) ?? config
834
427
  };
835
- if (candidates.length === 0) return palette;
836
- let maximumPopulation = Math.max(...candidates.map((candidate) => candidate.population)), claimed = /* @__PURE__ */ new Set();
837
- for (let [key, target] of TARGETS) {
838
- let best = null, bestScore = -Infinity;
839
- for (let candidate of candidates) {
840
- if (claimed.has(candidate.hex) || !matchesTarget(candidate, target)) continue;
841
- let score = scoreCandidate(candidate, target, maximumPopulation);
842
- score > bestScore && (best = candidate, bestScore = score);
428
+ }
429
+ /**
430
+ * Creates a custom theme with a slug that cannot collide with the presets or
431
+ * the configured theme.
432
+ *
433
+ * @internal
434
+ */
435
+ function createCustomTheme(title, options, palette) {
436
+ return {
437
+ slug: `custom-${randomId()}`,
438
+ title,
439
+ options,
440
+ ...palette ? { palette } : {}
441
+ };
442
+ }
443
+ /** The title of a duplicated theme @internal */
444
+ function duplicateTitle(title) {
445
+ return `${displayTitle(title)} copy`;
446
+ }
447
+ /** The title to show for a theme, falling back when the user cleared it @internal */
448
+ function displayTitle(title) {
449
+ return title.trim() || "Untitled theme";
450
+ }
451
+ function randomId() {
452
+ return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
453
+ }
454
+ const STORAGE_KEY = "sanityStudio:themer:state", LEGACY_STORAGE_KEY = "sanityStudio:themer:options";
455
+ function sanitizeColor(value) {
456
+ return typeof value == "string" && isColor(value) ? value.toLowerCase() : null;
457
+ }
458
+ function sanitizeContrast(value) {
459
+ return typeof value == "number" && Number.isFinite(value) ? Math.min(100, Math.max(15, value)) : null;
460
+ }
461
+ function sanitizeSchemeOptions(value) {
462
+ let options = {};
463
+ if (!value || typeof value != "object") return options;
464
+ 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"));
465
+ return accent && (options.accent = accent), text && (options.text = text), background && (options.background = background), contrast !== null && (options.contrast = contrast), options;
466
+ }
467
+ /**
468
+ * Options from before they were grouped by scheme: one accent, text color and
469
+ * contrast for both schemes, and a background per scheme. Both schemes get
470
+ * the shared colors, so the theme keeps looking the same.
471
+ */
472
+ function sanitizeFlatOptions(value) {
473
+ let accent = sanitizeColor(Reflect.get(value, "accent"));
474
+ if (!accent) return null;
475
+ let shared = { accent }, text = sanitizeColor(Reflect.get(value, "text")), contrast = sanitizeContrast(Reflect.get(value, "contrast"));
476
+ text && (shared.text = text), contrast !== null && (shared.contrast = contrast);
477
+ let backgrounds = Reflect.get(value, "background"), options = {};
478
+ for (let scheme of SCHEMES) {
479
+ let background = backgrounds && typeof backgrounds == "object" ? sanitizeColor(Reflect.get(backgrounds, scheme)) : null;
480
+ options[scheme] = background ? {
481
+ ...shared,
482
+ background
483
+ } : { ...shared };
484
+ }
485
+ return options;
486
+ }
487
+ function sanitizeOptions(value) {
488
+ if (!value || typeof value != "object") return null;
489
+ if ("accent" in value) return sanitizeFlatOptions(value);
490
+ let options = {};
491
+ for (let scheme of SCHEMES) {
492
+ let schemeOptions = sanitizeSchemeOptions(Reflect.get(value, scheme));
493
+ Object.keys(schemeOptions).length > 0 && (options[scheme] = schemeOptions);
494
+ }
495
+ return options;
496
+ }
497
+ function isReservedSlug(slug) {
498
+ return slug === "config" || presets.some((preset) => preset.slug === slug);
499
+ }
500
+ function sanitizeCustomTheme(value) {
501
+ if (!value || typeof value != "object") return null;
502
+ let slug = Reflect.get(value, "slug");
503
+ if (typeof slug != "string" || !slug || isReservedSlug(slug)) return null;
504
+ let options = sanitizeOptions(Reflect.get(value, "options"));
505
+ if (!options) return null;
506
+ let title = Reflect.get(value, "title"), palette = sanitizePalette(Reflect.get(value, "palette"));
507
+ return {
508
+ slug,
509
+ title: displayTitle(typeof title == "string" ? title : ""),
510
+ options,
511
+ ...palette ? { palette } : {}
512
+ };
513
+ }
514
+ function sanitizePalette(value) {
515
+ if (!value || typeof value != "object") return null;
516
+ let palette = {}, swatches = 0;
517
+ for (let key of IMAGE_PALETTE_KEYS) {
518
+ let swatch = sanitizeColor(Reflect.get(value, key));
519
+ palette[key] = swatch, swatch && swatches++;
520
+ }
521
+ return swatches > 0 ? palette : null;
522
+ }
523
+ function sanitizeState(value) {
524
+ if (!value || typeof value != "object") return initialThemerState;
525
+ let custom = [], rawCustom = Reflect.get(value, "custom");
526
+ if (Array.isArray(rawCustom)) for (let item of rawCustom) {
527
+ let theme = sanitizeCustomTheme(item);
528
+ theme && !custom.some((existing) => existing.slug === theme.slug) && custom.push(theme);
529
+ }
530
+ let removed = [], rawRemoved = Reflect.get(value, "removed");
531
+ 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);
532
+ let order = [], rawOrder = Reflect.get(value, "order");
533
+ if (Array.isArray(rawOrder)) for (let slug of rawOrder) typeof slug == "string" && (slug === "config" || presets.some((preset) => preset.slug === slug) || custom.some((theme) => theme.slug === slug)) && !order.includes(slug) && order.push(slug);
534
+ let rawActive = Reflect.get(value, "active");
535
+ return {
536
+ active: typeof rawActive == "string" ? rawActive : null,
537
+ custom,
538
+ removed,
539
+ order
540
+ };
541
+ }
542
+ function readLegacyState() {
543
+ let raw = localStorage.getItem(LEGACY_STORAGE_KEY);
544
+ if (!raw) return null;
545
+ let options = sanitizeOptions(JSON.parse(raw));
546
+ if (!options) return null;
547
+ let theme = createCustomTheme("Draft theme", options);
548
+ return {
549
+ active: theme.slug,
550
+ custom: [theme],
551
+ removed: [],
552
+ order: []
553
+ };
554
+ }
555
+ /**
556
+ * Restores the themer state from localStorage, so the user's themes survive
557
+ * studio reloads. Falls back to the initial state when nothing usable is
558
+ * stored.
559
+ *
560
+ * @internal
561
+ */
562
+ function readStoredState() {
563
+ try {
564
+ if (typeof localStorage > "u") return initialThemerState;
565
+ let raw = localStorage.getItem(STORAGE_KEY);
566
+ return raw ? sanitizeState(JSON.parse(raw)) : readLegacyState() ?? initialThemerState;
567
+ } catch {
568
+ return initialThemerState;
569
+ }
570
+ }
571
+ /** @internal */
572
+ function writeStoredState(state) {
573
+ try {
574
+ if (typeof localStorage > "u") return;
575
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)), localStorage.removeItem(LEGACY_STORAGE_KEY);
576
+ } catch {}
577
+ }
578
+ /**
579
+ * How long the layout's motions — the panel's and the split preview's, and
580
+ * the cross-fade to another theme — are taken to last when the layout never
581
+ * says: it reports every view transition it starts, so this only ends a
582
+ * motion that has none (reduced motion, a browser without view transitions),
583
+ * where the `moving` and `switching` tags have nothing to affect. Generous,
584
+ * so that it never runs out before a slow deferred commit starts the real one.
585
+ *
586
+ * @internal
587
+ */
588
+ const MOTION_DURATION = 2e3;
589
+ function themesOf(context) {
590
+ return resolveThemes(context, context.baseOptions);
591
+ }
592
+ function revokeObjectUrl(url) {
593
+ typeof URL < "u" && typeof URL.revokeObjectURL == "function" && URL.revokeObjectURL(url);
594
+ }
595
+ /**
596
+ * The state of the themer tool: three parallel regions, whether the
597
+ * `sidebar` is open, which `flow` it is in — picking a theme from the `list`,
598
+ * `edit`ing one of the user's own themes, or restoring `removed` ones — and
599
+ * whether the `preview` shows the Studio once (`single`) or in light and dark
600
+ * side by side (`split`). The context carries the persisted state (the
601
+ * applied theme, the user's themes and what was removed) alongside what the
602
+ * flows need.
603
+ *
604
+ * The sidebar and the preview pass through `opening`/`closing` and
605
+ * `splitting`/`unsplitting` on their way, states tagged for the UI: `panel`
606
+ * and `split` say what shows (the panel from `opening` on, the split copy from
607
+ * `splitting` on), `moving` that the Studio is about to give way or take room
608
+ * — which is when, and only when, the layout lets a view transition animate
609
+ * it. A fourth region, `theme`, is `switching` (its tag) right after an event
610
+ * that applies another theme — picking one, adding, duplicating, importing or
611
+ * editing one, removing or deleting the applied one — when the layout
612
+ * cross-fades to it; edits to the applied theme's colors are not switches,
613
+ * they follow the pointer. These states last until the layout reports its
614
+ * transition under way — every boundary reports, the panel's and the copy's
615
+ * entering and leaving as much as the Studio's morphing — or for
616
+ * `MOTION_DURATION` when nothing animates.
617
+ *
618
+ * Theme operations are handled in every flow, and the flows leave on their
619
+ * own when they lose their subject: the editor when its theme is removed or
620
+ * deleted, and the removed view when its last theme is restored or deleted.
621
+ *
622
+ * @internal
623
+ */
624
+ const themerMachine = setup({
625
+ types: {
626
+ context: {},
627
+ events: {},
628
+ input: {}
629
+ },
630
+ guards: {
631
+ isCustomTheme: ({ context }, params) => themesOf(context).themes.some((theme) => theme.slug === params.slug && theme.source === "custom"),
632
+ isEditingListedTheme: ({ context }) => context.editing !== null && themesOf(context).themes.some((theme) => theme.slug === context.editing?.slug && theme.source === "custom"),
633
+ hasRemovedThemes: ({ context }) => themesOf(context).removed.length > 0,
634
+ sidebarShown: or([stateIn({ sidebar: "opening" }), stateIn({ sidebar: "open" })]),
635
+ isAnotherTheme: ({ context }, params) => params.slug !== (context.active ?? "config"),
636
+ isAppliedTheme: ({ context }, params) => params.slug === context.active,
637
+ hasOptions: (_, params) => params.options !== void 0
638
+ },
639
+ actions: {
640
+ persist: ({ context }) => {
641
+ let { active, custom, removed, order } = context;
642
+ writeStoredState({
643
+ active,
644
+ custom,
645
+ removed,
646
+ order
647
+ });
648
+ },
649
+ revokeImage: (_, params) => {
650
+ params.url && revokeObjectUrl(params.url);
651
+ },
652
+ pick: assign((_, params) => ({ active: params.slug === "config" ? null : params.slug })),
653
+ add: assign(({ context }, params) => {
654
+ let theme = createCustomTheme(params.title ?? "Untitled theme", params.options ?? themesOf(context).active.options, params.palette);
655
+ return {
656
+ active: theme.slug,
657
+ custom: [...context.custom, theme],
658
+ editing: {
659
+ slug: theme.slug,
660
+ focusTitle: params.title === void 0
661
+ },
662
+ images: params.imageUrl ? {
663
+ ...context.images,
664
+ [theme.slug]: params.imageUrl
665
+ } : context.images
666
+ };
667
+ }),
668
+ import: assign(({ context }, params) => {
669
+ let theme = createCustomTheme(params.title, params.options);
670
+ return {
671
+ active: theme.slug,
672
+ custom: [...context.custom, theme]
673
+ };
674
+ }),
675
+ duplicate: assign(({ context }, params) => {
676
+ let { themes, removed } = themesOf(context), source = [...themes, ...removed].find((theme) => theme.slug === params.slug);
677
+ if (!source) return {};
678
+ let theme = createCustomTheme(duplicateTitle(source.title), source.options, source.palette);
679
+ return {
680
+ active: theme.slug,
681
+ custom: [...context.custom, theme],
682
+ editing: {
683
+ slug: theme.slug,
684
+ focusTitle: !0
685
+ }
686
+ };
687
+ }),
688
+ startEditing: assign((_, params) => ({
689
+ active: params.slug,
690
+ editing: {
691
+ slug: params.slug,
692
+ focusTitle: !1
693
+ }
694
+ })),
695
+ stopEditing: assign({ editing: null }),
696
+ update: assign(({ context }, params) => ({
697
+ custom: context.custom.map((theme) => theme.slug === params.slug ? {
698
+ ...theme,
699
+ title: params.title ?? theme.title,
700
+ options: params.options ?? theme.options,
701
+ ...params.palette ? { palette: params.palette } : {}
702
+ } : theme),
703
+ images: params.imageUrl && context.custom.some((theme) => theme.slug === params.slug) ? {
704
+ ...context.images,
705
+ [params.slug]: params.imageUrl
706
+ } : context.images
707
+ })),
708
+ remove: assign(({ context }, params) => params.slug === "config" || context.removed.includes(params.slug) ? {} : {
709
+ active: context.active === params.slug ? null : context.active,
710
+ removed: [...context.removed, params.slug]
711
+ }),
712
+ restore: assign(({ context }, params) => ({ removed: context.removed.filter((slug) => slug !== params.slug) })),
713
+ reorder: assign(({ context }, params) => {
714
+ let listed = new Set(themesOf(context).themes.map((theme) => theme.slug)), order = params.order.filter((slug) => listed.has(slug));
715
+ return { order: [...order, ...context.order.filter((slug) => !order.includes(slug))] };
716
+ }),
717
+ delete: assign(({ context }, params) => {
718
+ if (!context.custom.some((theme) => theme.slug === params.slug)) return {};
719
+ let { [params.slug]: _deleted, ...images } = context.images;
720
+ return {
721
+ active: context.active === params.slug ? null : context.active,
722
+ custom: context.custom.filter((theme) => theme.slug !== params.slug),
723
+ removed: context.removed.filter((slug) => slug !== params.slug),
724
+ images
725
+ };
726
+ })
727
+ }
728
+ }).createMachine({
729
+ id: "themer",
730
+ context: ({ input }) => ({
731
+ ...input.stored,
732
+ baseOptions: input.baseOptions,
733
+ editing: null,
734
+ images: {}
735
+ }),
736
+ entry: "persist",
737
+ type: "parallel",
738
+ states: {
739
+ sidebar: {
740
+ initial: "closed",
741
+ states: {
742
+ closed: { on: { "sidebar.toggle": "opening" } },
743
+ opening: {
744
+ tags: ["panel", "moving"],
745
+ after: { [MOTION_DURATION]: "open" },
746
+ on: {
747
+ "layout.transitioned": "open",
748
+ "sidebar.toggle": "closing",
749
+ "sidebar.close": "closing"
750
+ }
751
+ },
752
+ open: {
753
+ tags: ["panel"],
754
+ on: {
755
+ "sidebar.toggle": "closing",
756
+ "sidebar.close": "closing"
757
+ }
758
+ },
759
+ closing: {
760
+ tags: ["moving"],
761
+ after: { [MOTION_DURATION]: "closed" },
762
+ on: {
763
+ "layout.transitioned": "closed",
764
+ "sidebar.toggle": "opening"
765
+ }
766
+ }
767
+ }
768
+ },
769
+ preview: {
770
+ initial: "single",
771
+ states: {
772
+ single: { on: { "preview.toggle": "splitting" } },
773
+ splitting: {
774
+ tags: ["split", "moving"],
775
+ after: { [MOTION_DURATION]: "split" },
776
+ on: {
777
+ "layout.transitioned": "split",
778
+ "preview.toggle": "unsplitting",
779
+ "sidebar.close": "unsplitting",
780
+ "sidebar.toggle": {
781
+ guard: "sidebarShown",
782
+ target: "unsplitting"
783
+ }
784
+ }
785
+ },
786
+ split: {
787
+ tags: ["split"],
788
+ on: {
789
+ "preview.toggle": "unsplitting",
790
+ "sidebar.close": "unsplitting",
791
+ "sidebar.toggle": {
792
+ guard: "sidebarShown",
793
+ target: "unsplitting"
794
+ }
795
+ }
796
+ },
797
+ unsplitting: {
798
+ tags: ["moving"],
799
+ after: { [MOTION_DURATION]: "single" },
800
+ on: {
801
+ "layout.transitioned": "single",
802
+ "preview.toggle": "splitting"
803
+ }
804
+ }
805
+ }
806
+ },
807
+ theme: {
808
+ initial: "applied",
809
+ states: {
810
+ applied: { on: {
811
+ "theme.pick": {
812
+ guard: {
813
+ type: "isAnotherTheme",
814
+ params: ({ event }) => ({ slug: event.slug })
815
+ },
816
+ target: "switching"
817
+ },
818
+ "theme.edit": {
819
+ guard: {
820
+ type: "isAnotherTheme",
821
+ params: ({ event }) => ({ slug: event.slug })
822
+ },
823
+ target: "switching"
824
+ },
825
+ "theme.duplicate": {
826
+ guard: {
827
+ type: "isAnotherTheme",
828
+ params: ({ event }) => ({ slug: event.slug })
829
+ },
830
+ target: "switching"
831
+ },
832
+ "theme.remove": {
833
+ guard: {
834
+ type: "isAppliedTheme",
835
+ params: ({ event }) => ({ slug: event.slug })
836
+ },
837
+ target: "switching"
838
+ },
839
+ "theme.delete": {
840
+ guard: {
841
+ type: "isAppliedTheme",
842
+ params: ({ event }) => ({ slug: event.slug })
843
+ },
844
+ target: "switching"
845
+ },
846
+ "theme.add": {
847
+ guard: {
848
+ type: "hasOptions",
849
+ params: ({ event }) => ({ options: event.options })
850
+ },
851
+ target: "switching"
852
+ },
853
+ "theme.import": "switching"
854
+ } },
855
+ switching: {
856
+ tags: ["switching"],
857
+ after: { [MOTION_DURATION]: "applied" },
858
+ on: {
859
+ "layout.transitioned": "applied",
860
+ "theme.pick": {
861
+ guard: {
862
+ type: "isAnotherTheme",
863
+ params: ({ event }) => ({ slug: event.slug })
864
+ },
865
+ target: "switching",
866
+ reenter: !0
867
+ },
868
+ "theme.edit": {
869
+ guard: {
870
+ type: "isAnotherTheme",
871
+ params: ({ event }) => ({ slug: event.slug })
872
+ },
873
+ target: "switching",
874
+ reenter: !0
875
+ },
876
+ "theme.duplicate": {
877
+ guard: {
878
+ type: "isAnotherTheme",
879
+ params: ({ event }) => ({ slug: event.slug })
880
+ },
881
+ target: "switching",
882
+ reenter: !0
883
+ },
884
+ "theme.remove": {
885
+ guard: {
886
+ type: "isAppliedTheme",
887
+ params: ({ event }) => ({ slug: event.slug })
888
+ },
889
+ target: "switching",
890
+ reenter: !0
891
+ },
892
+ "theme.delete": {
893
+ guard: {
894
+ type: "isAppliedTheme",
895
+ params: ({ event }) => ({ slug: event.slug })
896
+ },
897
+ target: "switching",
898
+ reenter: !0
899
+ },
900
+ "theme.add": {
901
+ guard: {
902
+ type: "hasOptions",
903
+ params: ({ event }) => ({ options: event.options })
904
+ },
905
+ target: "switching",
906
+ reenter: !0
907
+ },
908
+ "theme.import": {
909
+ target: "switching",
910
+ reenter: !0
911
+ }
912
+ }
913
+ }
914
+ }
915
+ },
916
+ flow: {
917
+ initial: "list",
918
+ on: {
919
+ "theme.pick": { actions: [{
920
+ type: "pick",
921
+ params: ({ event }) => ({ slug: event.slug })
922
+ }, "persist"] },
923
+ "theme.add": {
924
+ target: ".edit",
925
+ actions: [{
926
+ type: "add",
927
+ params: ({ event }) => ({
928
+ title: event.title,
929
+ options: event.options,
930
+ palette: event.palette,
931
+ imageUrl: event.imageUrl
932
+ })
933
+ }, "persist"]
934
+ },
935
+ "theme.duplicate": {
936
+ target: ".edit",
937
+ actions: [{
938
+ type: "duplicate",
939
+ params: ({ event }) => ({ slug: event.slug })
940
+ }, "persist"]
941
+ },
942
+ "theme.edit": {
943
+ target: ".edit",
944
+ guard: {
945
+ type: "isCustomTheme",
946
+ params: ({ event }) => ({ slug: event.slug })
947
+ },
948
+ actions: [{
949
+ type: "startEditing",
950
+ params: ({ event }) => ({ slug: event.slug })
951
+ }, "persist"]
952
+ },
953
+ "theme.update": { actions: [
954
+ {
955
+ type: "revokeImage",
956
+ params: ({ context, event }) => ({ url: event.imageUrl ? context.images[event.slug] : void 0 })
957
+ },
958
+ {
959
+ type: "update",
960
+ params: ({ event }) => ({
961
+ slug: event.slug,
962
+ title: event.title,
963
+ options: event.options,
964
+ palette: event.palette,
965
+ imageUrl: event.imageUrl
966
+ })
967
+ },
968
+ "persist"
969
+ ] },
970
+ "theme.remove": { actions: [{
971
+ type: "remove",
972
+ params: ({ event }) => ({ slug: event.slug })
973
+ }, "persist"] },
974
+ "theme.restore": { actions: [{
975
+ type: "restore",
976
+ params: ({ event }) => ({ slug: event.slug })
977
+ }, "persist"] },
978
+ "theme.delete": { actions: [
979
+ {
980
+ type: "revokeImage",
981
+ params: ({ context, event }) => ({ url: context.images[event.slug] })
982
+ },
983
+ {
984
+ type: "delete",
985
+ params: ({ event }) => ({ slug: event.slug })
986
+ },
987
+ "persist"
988
+ ] },
989
+ "theme.reorder": { actions: [{
990
+ type: "reorder",
991
+ params: ({ event }) => ({ order: event.order })
992
+ }, "persist"] },
993
+ "theme.import": { actions: [{
994
+ type: "import",
995
+ params: ({ event }) => ({
996
+ title: event.title,
997
+ options: event.options
998
+ })
999
+ }, "persist"] },
1000
+ "flow.list": ".list",
1001
+ "flow.removed": {
1002
+ target: ".removed",
1003
+ guard: "hasRemovedThemes"
1004
+ }
1005
+ },
1006
+ states: {
1007
+ list: {},
1008
+ edit: {
1009
+ exit: "stopEditing",
1010
+ always: {
1011
+ guard: not("isEditingListedTheme"),
1012
+ target: "list"
1013
+ }
1014
+ },
1015
+ removed: { always: {
1016
+ guard: not("hasRemovedThemes"),
1017
+ target: "list"
1018
+ } }
1019
+ }
843
1020
  }
844
- best && (palette[key] = best.hex, claimed.add(best.hex));
845
- }
846
- return palette.dominant = candidates.reduce((most, candidate) => candidate.population > most.population ? candidate : most).hex, palette;
847
- }
848
- /**
849
- * Turns the pixels into a histogram of quantized colors, each averaged over
850
- * the pixels it stands for, leaving out transparent pixels and the near-black
851
- * and near-white ones that are backdrop rather than color.
852
- */
853
- function quantize(pixels) {
854
- let { data } = pixels, buckets = /* @__PURE__ */ new Map(), length = Math.min(data.length, pixels.width * pixels.height * 4);
855
- for (let offset = 0; offset + 3 < length; offset += 4) {
856
- let r = data[offset], g = data[offset + 1], b = data[offset + 2];
857
- if (data[offset + 3] < 125) continue;
858
- let lightness = (Math.max(r, g, b) + Math.min(r, g, b)) / 510;
859
- if (lightness < .05 || lightness > .95) continue;
860
- let key = r >> 3 << 10 | g >> 3 << 5 | b >> 3, bucket = buckets.get(key);
861
- bucket ? (bucket.count++, bucket.r += r, bucket.g += g, bucket.b += b) : buckets.set(key, {
862
- count: 1,
863
- r,
864
- g,
865
- b
866
- });
867
- }
868
- let candidates = [];
869
- for (let bucket of buckets.values()) {
870
- 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);
871
- candidates.push({
872
- hex: rgbToHex(r, g, b),
873
- population: bucket.count,
874
- saturation,
875
- lightness
876
- });
877
1021
  }
878
- return candidates;
879
- }
880
- function matchesTarget(candidate, target) {
881
- return candidate.lightness >= target.lightness[1] && candidate.lightness <= target.lightness[2] && candidate.saturation >= target.saturation[1] && candidate.saturation <= target.saturation[2];
882
- }
883
- function scoreCandidate(candidate, target, maximumPopulation) {
884
- return 3 * (1 - Math.abs(candidate.saturation - target.saturation[0])) + 6.5 * (1 - Math.abs(candidate.lightness - target.lightness[0])) + .5 * (candidate.population / maximumPopulation);
885
- }
886
- function saturationAndLightness(r, g, b) {
887
- let max = Math.max(r, g, b) / 255, min = Math.min(r, g, b) / 255, lightness = (max + min) / 2, delta = max - min;
1022
+ });
1023
+ /** The part of the machine context that is persisted between sessions @internal */
1024
+ function selectStoredState(snapshot) {
1025
+ let { active, custom, removed, order } = snapshot.context;
888
1026
  return {
889
- saturation: delta === 0 ? 0 : lightness > .5 ? delta / (2 - max - min) : delta / (max + min),
890
- lightness
1027
+ active,
1028
+ custom,
1029
+ removed,
1030
+ order
891
1031
  };
892
1032
  }
893
- function rgbToHex(r, g, b) {
894
- return `#${[
895
- r,
896
- g,
897
- b
898
- ].map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
899
- }
900
- /** Reads `#rgb` and `#rrggbb` alike — stored palettes may hold either */
901
- function hexToRgb(hex) {
902
- let expanded = hex.length === 4 ? `${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex.slice(1);
903
- return [
904
- parseInt(expanded.slice(0, 2), 16),
905
- parseInt(expanded.slice(2, 4), 16),
906
- parseInt(expanded.slice(4, 6), 16)
907
- ];
908
- }
909
- /** Mixes `amount` of `color` into `base` */
910
- function tint(base, color, amount) {
911
- let [r1, g1, b1] = hexToRgb(base), [r2, g2, b2] = hexToRgb(color);
912
- return rgbToHex(Math.round(r1 + (r2 - r1) * amount), Math.round(g1 + (g2 - g1) * amount), Math.round(b1 + (b2 - b1) * amount));
913
- }
914
- /** How much of the palette tints each scheme's stock background */
915
- const BACKGROUND_TINT = {
916
- light: .08,
917
- dark: .3
918
- }, BUTTON_LABEL = {
919
- light: "#ffffff",
920
- dark: "#000000"
921
- }, ACCENT_LIGHTNESS_STEP = .01;
922
- function contrastRatio(a, b) {
923
- let [lighter, darker] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
924
- return (lighter + .05) / (darker + .05);
925
- }
1033
+ var root$2 = "_1o50s7t0";
1034
+ const ScrollAreaInsetContext = createContext({
1035
+ left: 0,
1036
+ right: 0,
1037
+ bottom: 0
1038
+ });
926
1039
  /**
927
- * Makes an accent readable as a button in the given scheme: a light scheme
928
- * accent is darkened until white text on it reaches AA contrast, a dark
929
- * scheme accent lightened until black text does — as far as the accent's
930
- * hue and saturation allow. Accents that already pass come back untouched.
1040
+ * The gutter the last scroll area measured. It is the same for every scroll
1041
+ * area (the reserved gutter does not depend on the content), so the next one
1042
+ * starts out with the right padding instead of shifting after its first
1043
+ * measurement — which would also throw off anything scrolling into view as
1044
+ * the content mounts.
1045
+ */
1046
+ let lastGutter = 0;
1047
+ /**
1048
+ * The scrolling body of a sidebar flow. It always reserves the scrollbar's
1049
+ * space, so the content does not shift when the list grows or shrinks past
1050
+ * the point where it needs to scroll — and it takes that space out of its
1051
+ * right padding, so the content stays centered rather than hugging the left.
1052
+ * With overlay scrollbars there is no gutter and the padding stays even.
931
1053
  *
932
1054
  * @internal
933
1055
  */
934
- function readableAccent(hex, scheme) {
935
- let label = BUTTON_LABEL[scheme], { h, s } = hexToHsl(hex), { l } = hexToHsl(hex), current = hex;
936
- for (; contrastRatio(current, label) < 4.5;) {
937
- let next = scheme === "light" ? l - ACCENT_LIGHTNESS_STEP : l + ACCENT_LIGHTNESS_STEP;
938
- if (next < 0 || next > 1) break;
939
- l = next, current = hslToHex({
940
- h,
941
- s,
942
- l
943
- });
944
- }
945
- return current;
1056
+ function ScrollArea(props) {
1057
+ let $ = c(15), { children, padding } = props, { space } = useTheme_v2(), ref = useRef(null), [gutter, setGutter] = useState(_temp$7), t0, t1;
1058
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = () => {
1059
+ let element = ref.current;
1060
+ if (!element) return;
1061
+ let measure = () => {
1062
+ lastGutter = element.offsetWidth - element.clientWidth, setGutter(lastGutter);
1063
+ };
1064
+ measure();
1065
+ let observer = new ResizeObserver(measure);
1066
+ return observer.observe(element), () => observer.disconnect();
1067
+ }, t1 = [], $[0] = t0, $[1] = t1) : (t0 = $[0], t1 = $[1]), useEffect(t0, t1);
1068
+ let t2 = space[padding], t3 = Math.max(0, space[padding] - gutter), t4 = space[padding], t5;
1069
+ $[2] !== t2 || $[3] !== t3 || $[4] !== t4 ? (t5 = {
1070
+ left: t2,
1071
+ right: t3,
1072
+ bottom: t4
1073
+ }, $[2] = t2, $[3] = t3, $[4] = t4, $[5] = t5) : t5 = $[5];
1074
+ let inset = t5, t6;
1075
+ $[6] === inset.right ? t6 = $[7] : (t6 = { paddingRight: inset.right }, $[6] = inset.right, $[7] = t6);
1076
+ let t7;
1077
+ $[8] !== children || $[9] !== inset ? (t7 = /* @__PURE__ */ jsx(ScrollAreaInsetContext.Provider, {
1078
+ value: inset,
1079
+ children
1080
+ }), $[8] = children, $[9] = inset, $[10] = t7) : t7 = $[10];
1081
+ let t8;
1082
+ return $[11] !== padding || $[12] !== t6 || $[13] !== t7 ? (t8 = /* @__PURE__ */ jsx(Box, {
1083
+ className: root$2,
1084
+ flex: 1,
1085
+ overflow: "auto",
1086
+ padding,
1087
+ ref,
1088
+ style: t6,
1089
+ children: t7
1090
+ }), $[11] = padding, $[12] = t6, $[13] = t7, $[14] = t8) : t8 = $[14], t8;
946
1091
  }
947
- /** The accent a variant gives a scheme, made readable on a button */
948
- function variantAccent(palette, variant, scheme) {
949
- let swatch = (variant ? palette[variant] : null) ?? palette.vibrant ?? (scheme === "light" ? palette.darkVibrant : palette.lightVibrant) ?? palette.dominant;
950
- return swatch === null ? null : readableAccent(swatch, scheme);
1092
+ function _temp$7() {
1093
+ return lastGutter;
951
1094
  }
952
1095
  /**
953
- * Derives theme options from an image palette: the accent of both schemes is
954
- * the swatch of the chosen variant — or, without one, the vibrant swatch,
955
- * each scheme falling back to the vibrant swatch of its own end of the scale —
956
- * adjusted until it is readable as a button. The muted swatch becomes the text color, and the light and dark muted
957
- * swatches tint the stock backgrounds of their scheme — enough to carry the
958
- * image's tone without giving up a usable canvas. Whatever the palette lacks
959
- * stays at the theme defaults.
1096
+ * Content that runs edge to edge in a scroll area — up to the scrollbar —
1097
+ * instead of sitting inside its padding. It goes last: it runs down to the
1098
+ * bottom edge too.
960
1099
  *
961
1100
  * @internal
962
1101
  */
963
- function optionsFromImagePalette(palette, variant) {
964
- 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");
965
- return light && (options.light = light), dark && (options.dark = dark), options;
966
- }
967
- function schemeFromPalette(accent, text, backgroundTint, scheme) {
968
- let options = {};
969
- 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;
1102
+ function ScrollAreaBleed(props) {
1103
+ let $ = c(7), { left, right, bottom } = useContext(ScrollAreaInsetContext), t0 = -left, t1 = -right, t2 = -bottom, t3;
1104
+ $[0] !== t0 || $[1] !== t1 || $[2] !== t2 ? (t3 = {
1105
+ marginLeft: t0,
1106
+ marginRight: t1,
1107
+ marginBottom: t2
1108
+ }, $[0] = t0, $[1] = t1, $[2] = t2, $[3] = t3) : t3 = $[3];
1109
+ let t4;
1110
+ return $[4] !== props.children || $[5] !== t3 ? (t4 = /* @__PURE__ */ jsx("div", {
1111
+ style: t3,
1112
+ children: props.children
1113
+ }), $[4] = props.children, $[5] = t3, $[6] = t4) : t4 = $[6], t4;
970
1114
  }
1115
+ var avatar = "_1aa9huuh", body = "_1aa9huui", button = "_1aa9huuq", colors = {
1116
+ bg: "var(--_1aa9huu0)",
1117
+ fg: "var(--_1aa9huu1)",
1118
+ mutedFg: "var(--_1aa9huu2)",
1119
+ border: "var(--_1aa9huu3)",
1120
+ accent: "var(--_1aa9huu4)",
1121
+ avatar: "var(--_1aa9huu5)",
1122
+ selectedBg: "var(--_1aa9huu6)",
1123
+ selectedFg: "var(--_1aa9huu7)",
1124
+ inputBg: "var(--_1aa9huu8)",
1125
+ inputBorder: "var(--_1aa9huu9)"
1126
+ }, field = "_1aa9huun", footer = "_1aa9huup", form = "_1aa9huum", input = "_1aa9huuo", line = {
1127
+ fg: "_1aa9huus _1aa9huur",
1128
+ muted: "_1aa9huut _1aa9huur",
1129
+ selected: "_1aa9huuu _1aa9huur"
1130
+ }, list = "_1aa9huuj", listRow = "_1aa9huuk", logo = "_1aa9huuf", navbar = "_1aa9huue", pane = "_1aa9huul", root$1 = "_1aa9huua", spacer = "_1aa9huug", studio = {
1131
+ light: "_1aa9huub",
1132
+ dark: "_1aa9huud _1aa9huub"
1133
+ };
1134
+ /** The document rows in the list pane — the widths of their title lines */
1135
+ const LIST_ROWS = [
1136
+ "62%",
1137
+ "46%",
1138
+ "70%",
1139
+ "52%"
1140
+ ], FIELDS$1 = ["32%", "24%"];
971
1141
  /**
972
- * Applies an image palette to existing theme options: the colors the palette
973
- * provides replace the current ones scheme by scheme, and whatever it does
974
- * not touch — like the contrast — stays as it is.
1142
+ * A tiny, lo-fi mockup of a Studio in the given theme — the navbar over the
1143
+ * structure tool with a list of documents on the left and a document form on
1144
+ * the right — in both color schemes at once: the light scheme on the left and
1145
+ * the dark scheme on the right, split diagonally. It only paints colors the
1146
+ * theme options change (backgrounds, text, borders, the accent) so that a
1147
+ * glance shows what the theme does to the Studio — with few and bold enough
1148
+ * elements to still read when scaled down to a thumbnail.
975
1149
  *
976
1150
  * @internal
977
1151
  */
978
- function applyImagePalette(options, palette, variant) {
979
- let derived = optionsFromImagePalette(palette, variant), next = { ...options };
980
- for (let scheme of SCHEMES) derived[scheme] && (next[scheme] = {
981
- ...options[scheme],
982
- ...derived[scheme]
983
- });
984
- return next;
1152
+ function ThemePreview(props) {
1153
+ let $ = c(11), { options } = props, t0;
1154
+ $[0] === options ? t0 = $[1] : (t0 = buildTheme(options), $[0] = options, $[1] = t0);
1155
+ let theme = t0, t1;
1156
+ $[2] === Symbol.for("react.memo_cache_sentinel") ? (t1 = /* @__PURE__ */ jsx(MockStudio, { scheme: "light" }), $[2] = t1) : t1 = $[2];
1157
+ let t2;
1158
+ $[3] === theme ? t2 = $[4] : (t2 = /* @__PURE__ */ jsx(ThemeProvider, {
1159
+ scheme: "light",
1160
+ theme,
1161
+ tone: "default",
1162
+ children: t1
1163
+ }), $[3] = theme, $[4] = t2);
1164
+ let t3;
1165
+ $[5] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx(MockStudio, { scheme: "dark" }), $[5] = t3) : t3 = $[5];
1166
+ let t4;
1167
+ $[6] === theme ? t4 = $[7] : (t4 = /* @__PURE__ */ jsx(ThemeProvider, {
1168
+ scheme: "dark",
1169
+ theme,
1170
+ tone: "default",
1171
+ children: t3
1172
+ }), $[6] = theme, $[7] = t4);
1173
+ let t5;
1174
+ return $[8] !== t2 || $[9] !== t4 ? (t5 = /* @__PURE__ */ jsxs("div", {
1175
+ "aria-hidden": !0,
1176
+ className: root$1,
1177
+ children: [t2, t4]
1178
+ }), $[8] = t2, $[9] = t4, $[10] = t5) : t5 = $[10], t5;
1179
+ }
1180
+ function MockStudio(props) {
1181
+ let $ = c(19), { scheme } = props, { color } = useTheme_v2(), selected = color.selectable.default.selected, inputColor = color.input.default.enabled, t0 = studio[scheme], t1;
1182
+ $[0] !== color.avatar.blue.bg || $[1] !== color.bg || $[2] !== color.border || $[3] !== color.button.default.primary.enabled.bg || $[4] !== color.fg || $[5] !== color.muted.fg || $[6] !== inputColor.bg || $[7] !== inputColor.border || $[8] !== selected.bg || $[9] !== selected.fg ? (t1 = assignInlineVars({
1183
+ [colors.bg]: color.bg,
1184
+ [colors.fg]: color.fg,
1185
+ [colors.mutedFg]: color.muted.fg,
1186
+ [colors.border]: color.border,
1187
+ [colors.accent]: color.button.default.primary.enabled.bg,
1188
+ [colors.avatar]: color.avatar.blue.bg,
1189
+ [colors.selectedBg]: selected.bg,
1190
+ [colors.selectedFg]: selected.fg,
1191
+ [colors.inputBg]: inputColor.bg,
1192
+ [colors.inputBorder]: inputColor.border
1193
+ }), $[0] = color.avatar.blue.bg, $[1] = color.bg, $[2] = color.border, $[3] = color.button.default.primary.enabled.bg, $[4] = color.fg, $[5] = color.muted.fg, $[6] = inputColor.bg, $[7] = inputColor.border, $[8] = selected.bg, $[9] = selected.fg, $[10] = t1) : t1 = $[10];
1194
+ let t2;
1195
+ $[11] === Symbol.for("react.memo_cache_sentinel") ? (t2 = /* @__PURE__ */ jsx("div", { className: logo }), $[11] = t2) : t2 = $[11];
1196
+ let t3;
1197
+ $[12] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsxs("div", {
1198
+ className: navbar,
1199
+ children: [
1200
+ t2,
1201
+ /* @__PURE__ */ jsx("div", {
1202
+ className: line.fg,
1203
+ style: { width: "22%" }
1204
+ }),
1205
+ /* @__PURE__ */ jsx("div", { className: spacer }),
1206
+ /* @__PURE__ */ jsx("div", { className: avatar })
1207
+ ]
1208
+ }), $[12] = t3) : t3 = $[12];
1209
+ let t4;
1210
+ $[13] === Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx("div", {
1211
+ className: list,
1212
+ children: LIST_ROWS.map(_temp$6)
1213
+ }), $[13] = t4) : t4 = $[13];
1214
+ let t5;
1215
+ $[14] === Symbol.for("react.memo_cache_sentinel") ? (t5 = /* @__PURE__ */ jsxs("div", {
1216
+ className: form,
1217
+ children: [/* @__PURE__ */ jsx("div", {
1218
+ className: line.fg,
1219
+ style: { width: "45%" }
1220
+ }), FIELDS$1.map(_temp2)]
1221
+ }), $[14] = t5) : t5 = $[14];
1222
+ let t6;
1223
+ $[15] === Symbol.for("react.memo_cache_sentinel") ? (t6 = /* @__PURE__ */ jsxs("div", {
1224
+ className: body,
1225
+ children: [t4, /* @__PURE__ */ jsxs("div", {
1226
+ className: pane,
1227
+ children: [t5, /* @__PURE__ */ jsx("div", {
1228
+ className: footer,
1229
+ children: /* @__PURE__ */ jsx("div", { className: button })
1230
+ })]
1231
+ })]
1232
+ }), $[15] = t6) : t6 = $[15];
1233
+ let t7;
1234
+ return $[16] !== t0 || $[17] !== t1 ? (t7 = /* @__PURE__ */ jsxs("div", {
1235
+ className: t0,
1236
+ style: t1,
1237
+ children: [t3, t6]
1238
+ }), $[16] = t0, $[17] = t1, $[18] = t7) : t7 = $[18], t7;
1239
+ }
1240
+ function _temp2(width_0) {
1241
+ return /* @__PURE__ */ jsxs("div", {
1242
+ className: field,
1243
+ children: [/* @__PURE__ */ jsx("div", {
1244
+ className: line.muted,
1245
+ style: { width: width_0 }
1246
+ }), /* @__PURE__ */ jsx("div", { className: input })]
1247
+ }, width_0);
1248
+ }
1249
+ function _temp$6(width, index) {
1250
+ let isSelected = index === 1;
1251
+ return /* @__PURE__ */ jsx("div", {
1252
+ className: listRow,
1253
+ "data-selected": isSelected,
1254
+ children: /* @__PURE__ */ jsx("div", {
1255
+ className: isSelected ? line.selected : line.muted,
1256
+ style: { width }
1257
+ })
1258
+ }, width);
985
1259
  }
986
1260
  /**
987
- * The variant a theme is built around: the one whose swatch is the theme's
988
- * accent, or `null` when the accent comes from somewhere else.
1261
+ * The floating preview card of a theme: the lo-fi studio mockup on a raised,
1262
+ * rounded card, like the appearance previews in macOS System Settings.
989
1263
  *
990
1264
  * @internal
991
1265
  */
992
- function currentImageVariant(options, palette) {
993
- let accent = options.light?.accent;
994
- return accent ? IMAGE_PALETTE_VARIANTS.find((variant) => palette[variant] !== null && variantAccent(palette, variant, "light") === accent) ?? null : null;
995
- }
996
- /**
997
- * How much a swatch has going for it as an accent: saturation first, then
998
- * closeness to mid lightness — the range where an accent reads best.
999
- */
1000
- function interest(hex) {
1001
- let { s, l } = hexToHsl(hex);
1002
- return .1 + .55 * s + .35 * (1 - Math.abs(l - .5) * 2);
1266
+ function ThemeThumbnail(props) {
1267
+ let $ = c(2), t0;
1268
+ return $[0] === props.options ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(Card, {
1269
+ overflow: "hidden",
1270
+ radius: 3,
1271
+ shadow: 1,
1272
+ children: /* @__PURE__ */ jsx(ThemePreview, { options: props.options })
1273
+ }), $[0] = props.options, $[1] = t0), t0;
1003
1274
  }
1275
+ var removedThumbnail = "_1vdkp0v0";
1004
1276
  /**
1005
- * Picks a variant for the "I'm feeling lucky" button: a random one, weighted
1006
- * by how interesting its swatch is as an accent, and never the one the theme
1007
- * already uses while there are others to pick from.
1277
+ * The flow for restoring removed themes: the presets and custom themes the
1278
+ * user took out of the list, each with a button to put it back — and, for
1279
+ * custom themes, one to delete it for good. The machine leaves the flow once
1280
+ * the last one is gone.
1008
1281
  *
1009
1282
  * @internal
1010
1283
  */
1011
- function pickLuckyVariant(palette, options = {}) {
1012
- let { exclude = null, random = Math.random } = options, candidates = IMAGE_PALETTE_VARIANTS.filter((variant) => palette[variant] !== null);
1013
- if (candidates.length > 1 && (candidates = candidates.filter((variant) => variant !== exclude)), candidates.length === 0) return null;
1014
- let weights = candidates.map((variant) => interest(palette[variant])), total = weights.reduce((sum, weight) => sum + weight, 0), pick = random() * total;
1015
- for (let [index, variant] of candidates.entries()) if (pick -= weights[index], pick < 0) return variant;
1016
- return candidates.at(-1) ?? null;
1017
- }
1018
- /** A theme title for the image a palette came from, e.g. `sunset-beach.jpg` → `sunset beach` @internal */
1019
- function titleFromFileName(name) {
1020
- return name.replace(/\.[^.]+$/, "").replace(/[-_]+/g, " ").trim() || "Image theme";
1284
+ function RemovedThemes() {
1285
+ let $ = c(7), { removed, send } = useThemer(), t0;
1286
+ if ($[0] !== removed || $[1] !== send) {
1287
+ let t1;
1288
+ $[3] === send ? t1 = $[4] : (t1 = (theme) => /* @__PURE__ */ jsxs(Stack, {
1289
+ gap: 3,
1290
+ children: [
1291
+ /* @__PURE__ */ jsx(Box, {
1292
+ className: removedThumbnail,
1293
+ children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: theme.options })
1294
+ }),
1295
+ /* @__PURE__ */ jsx(Text, {
1296
+ align: "center",
1297
+ size: 1,
1298
+ textOverflow: "ellipsis",
1299
+ children: displayTitle(theme.title)
1300
+ }),
1301
+ /* @__PURE__ */ jsxs(Flex, {
1302
+ gap: 2,
1303
+ justify: "center",
1304
+ children: [/* @__PURE__ */ jsx(Button, {
1305
+ mode: "ghost",
1306
+ onClick: () => send({
1307
+ type: "theme.restore",
1308
+ slug: theme.slug
1309
+ }),
1310
+ padding: 2,
1311
+ text: "Restore"
1312
+ }), theme.source === "custom" && /* @__PURE__ */ jsx(Button, {
1313
+ mode: "ghost",
1314
+ onClick: () => send({
1315
+ type: "theme.delete",
1316
+ slug: theme.slug
1317
+ }),
1318
+ padding: 2,
1319
+ text: "Delete",
1320
+ tone: "critical"
1321
+ })]
1322
+ })
1323
+ ]
1324
+ }, theme.slug), $[3] = send, $[4] = t1), t0 = removed.map(t1), $[0] = removed, $[1] = send, $[2] = t0;
1325
+ } else t0 = $[2];
1326
+ let t1;
1327
+ return $[5] === t0 ? t1 = $[6] : (t1 = /* @__PURE__ */ jsx(ScrollArea, {
1328
+ padding: 3,
1329
+ children: /* @__PURE__ */ jsx(Stack, {
1330
+ gap: 5,
1331
+ children: t0
1332
+ })
1333
+ }), $[5] = t0, $[6] = t1), t1;
1021
1334
  }
1022
1335
  /**
1023
- * A button with an animated tooltip instead of a `title` attribute. Icon-only
1024
- * buttons take the tooltip as their accessible name.
1336
+ * A button with an animated tooltip instead of a `title` attribute, sized like
1337
+ * the Studio's own buttons (`padding` and `gap` of 2). Icon-only buttons take
1338
+ * the tooltip as their accessible name.
1025
1339
  *
1026
1340
  * @internal
1027
1341
  */
@@ -1036,6 +1350,8 @@ function TooltipButton(props) {
1036
1350
  let t1 = buttonProps.text === void 0 ? tooltip : void 0, t2;
1037
1351
  $[5] !== buttonProps || $[6] !== t1 ? (t2 = /* @__PURE__ */ jsx(Button, {
1038
1352
  "aria-label": t1,
1353
+ gap: 2,
1354
+ padding: 2,
1039
1355
  ...buttonProps
1040
1356
  }), $[5] = buttonProps, $[6] = t1, $[7] = t2) : t2 = $[7];
1041
1357
  let t3;
@@ -1135,7 +1451,7 @@ function useImagePalette(onPalette) {
1135
1451
  setBusy(!0);
1136
1452
  let isCurrent = () => mountedRef.current && latestPickRef.current === pick, extract = async () => {
1137
1453
  let palette = extractImagePalette(await readImagePixels(file));
1138
- isCurrent() && (Object.values(palette).every(_temp$3) ? toast.push({
1454
+ isCurrent() && (Object.values(palette).every(_temp$5) ? toast.push({
1139
1455
  status: "warning",
1140
1456
  title: "Found no colors in that image"
1141
1457
  }) : onPaletteRef.current(palette, file));
@@ -1155,9 +1471,10 @@ function useImagePalette(onPalette) {
1155
1471
  pickImage
1156
1472
  }, $[7] = busy, $[8] = pickImage, $[9] = t5) : t5 = $[9], t5;
1157
1473
  }
1158
- function _temp$3(swatch) {
1474
+ function _temp$5(swatch) {
1159
1475
  return swatch === null;
1160
1476
  }
1477
+ var paletteGrid = "_17s80v42", tile = "_17s80v44", variantButton = "_17s80v46", variantFrame = "_17s80v47", variantRow = "_17s80v45", variantScale = "var(--_17s80v41)", variantScaleBox = "_17s80v48", variantWidth = "var(--_17s80v40)";
1161
1478
  /** The swatches on display: the vibrant family over the muted one */
1162
1479
  const TILES = [
1163
1480
  "vibrant",
@@ -1166,28 +1483,7 @@ const TILES = [
1166
1483
  "muted",
1167
1484
  "lightMuted",
1168
1485
  "darkMuted"
1169
- ], PaletteGrid = styled.div.withConfig({
1170
- displayName: "PaletteGrid",
1171
- componentId: "sc-j2tqre-0"
1172
- })`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({
1173
- displayName: "ImageTile",
1174
- componentId: "sc-j2tqre-1"
1175
- })`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({
1176
- displayName: "Tile",
1177
- componentId: "sc-j2tqre-2"
1178
- })`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({
1179
- displayName: "VariantRow",
1180
- componentId: "sc-j2tqre-3"
1181
- })`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({
1182
- displayName: "VariantButton",
1183
- componentId: "sc-j2tqre-4"
1184
- })`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({
1185
- displayName: "VariantFrame",
1186
- componentId: "sc-j2tqre-5"
1187
- })`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({
1188
- displayName: "VariantScale",
1189
- componentId: "sc-j2tqre-6"
1190
- })`display:block;width:${144}px;transform:scale(var(--variant-scale));transform-origin:top left;`;
1486
+ ];
1191
1487
  /**
1192
1488
  * Takes the colors of a theme from an image, on device: the image is read
1193
1489
  * into a canvas and its palette extracted right there — nothing is uploaded.
@@ -1200,13 +1496,13 @@ const TILES = [
1200
1496
  function ImagePaletteSection(props) {
1201
1497
  let $ = c(38), { imageUrl, onPalette, onVariant, onLucky, options, palette } = props, { busy, pickImage } = useImagePalette(onPalette), [dragging, setDragging] = useState(!1), dragDepth = useRef(0), t0;
1202
1498
  $[0] !== options || $[1] !== palette ? (t0 = palette ? currentImageVariant(options, palette) : null, $[0] = options, $[1] = palette, $[2] = t0) : t0 = $[2];
1203
- let current = t0, rowRef = useRef(null), [variantWidth, setVariantWidth] = useState(58), t1;
1499
+ let current = t0, rowRef = useRef(null), [thumbnailWidth, setThumbnailWidth] = useState(58), t1;
1204
1500
  $[3] === Symbol.for("react.memo_cache_sentinel") ? (t1 = () => {
1205
1501
  let row = rowRef.current;
1206
1502
  if (!row) return;
1207
1503
  let measure = () => {
1208
1504
  let visible = row.clientWidth - 12 - 16;
1209
- setVariantWidth(Math.max(24, Math.floor(visible / 2.5) - 8));
1505
+ setThumbnailWidth(Math.max(24, Math.floor(visible / 2.5) - 8));
1210
1506
  };
1211
1507
  measure();
1212
1508
  let observer = new ResizeObserver(measure);
@@ -1224,6 +1520,7 @@ function ImagePaletteSection(props) {
1224
1520
  let t4;
1225
1521
  $[7] !== options || $[8] !== palette ? (t4 = IMAGE_PALETTE_VARIANTS.flatMap((variant) => palette[variant] === null ? [] : [{
1226
1522
  variant,
1523
+ title: IMAGE_PALETTE_TITLES[variant],
1227
1524
  options: applyImagePalette(options, palette, variant)
1228
1525
  }]), $[7] = options, $[8] = palette, $[9] = t4) : t4 = $[9], t3 = t4;
1229
1526
  }
@@ -1263,11 +1560,13 @@ function ImagePaletteSection(props) {
1263
1560
  children: [t8, t9]
1264
1561
  }), $[19] = t9, $[20] = t10);
1265
1562
  let t11;
1266
- $[21] !== busy || $[22] !== current || $[23] !== imageUrl || $[24] !== onLucky || $[25] !== onVariant || $[26] !== palette || $[27] !== pickImage || $[28] !== variantWidth || $[29] !== variants ? (t11 = palette ? /* @__PURE__ */ jsxs(Fragment, { children: [
1267
- /* @__PURE__ */ jsxs(PaletteGrid, {
1563
+ $[21] !== busy || $[22] !== current || $[23] !== imageUrl || $[24] !== onLucky || $[25] !== onVariant || $[26] !== palette || $[27] !== pickImage || $[28] !== thumbnailWidth || $[29] !== variants ? (t11 = palette ? /* @__PURE__ */ jsxs(Fragment, { children: [
1564
+ /* @__PURE__ */ jsxs("div", {
1565
+ className: paletteGrid,
1268
1566
  "data-with-image": imageUrl !== void 0,
1269
- children: [imageUrl && /* @__PURE__ */ jsx(ImageTile, {
1567
+ children: [imageUrl && /* @__PURE__ */ jsx("img", {
1270
1568
  alt: "",
1569
+ className: "_17s80v43",
1271
1570
  src: imageUrl
1272
1571
  }), TILES.map((key) => {
1273
1572
  let hex = palette[key], title = IMAGE_PALETTE_TITLES[key];
@@ -1279,7 +1578,8 @@ function ImagePaletteSection(props) {
1279
1578
  }),
1280
1579
  placement: "bottom",
1281
1580
  portal: !0,
1282
- children: /* @__PURE__ */ jsx(Tile, {
1581
+ children: /* @__PURE__ */ jsx("span", {
1582
+ className: tile,
1283
1583
  "data-empty": hex === null,
1284
1584
  style: { background: hex ?? void 0 }
1285
1585
  })
@@ -1292,26 +1592,37 @@ function ImagePaletteSection(props) {
1292
1592
  muted: !0,
1293
1593
  size: 0,
1294
1594
  children: "Pick the swatch to build the theme around"
1295
- }), /* @__PURE__ */ jsx(VariantRow, {
1595
+ }), /* @__PURE__ */ jsx("div", {
1596
+ className: variantRow,
1296
1597
  ref: rowRef,
1297
- style: {
1298
- "--variant-width": `${variantWidth}px`,
1299
- "--variant-scale": variantWidth / 144
1300
- },
1598
+ style: assignInlineVars({
1599
+ [variantWidth]: `${thumbnailWidth}px`,
1600
+ [variantScale]: String(thumbnailWidth / 144)
1601
+ }),
1301
1602
  children: variants.map((t12) => {
1302
- let { variant: variant_0, options: variantOptions } = t12;
1303
- return /* @__PURE__ */ jsxs(VariantButton, {
1603
+ let { variant: variant_0, title: title_0, options: variantOptions } = t12;
1604
+ return /* @__PURE__ */ jsxs("button", {
1304
1605
  "aria-pressed": variant_0 === current,
1606
+ className: variantButton,
1305
1607
  onClick: () => onVariant(variant_0),
1306
1608
  type: "button",
1307
- children: [/* @__PURE__ */ jsx(VariantFrame, { children: /* @__PURE__ */ jsx(VariantScale, { children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: variantOptions }) }) }), /* @__PURE__ */ jsx(Box, {
1609
+ children: [/* @__PURE__ */ jsx("span", {
1610
+ className: variantFrame,
1611
+ children: /* @__PURE__ */ jsx("span", {
1612
+ className: variantScaleBox,
1613
+ children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: variantOptions })
1614
+ })
1615
+ }), /* @__PURE__ */ jsx(Box, {
1616
+ as: "span",
1617
+ display: "block",
1308
1618
  paddingTop: 1,
1309
1619
  children: /* @__PURE__ */ jsx(Text, {
1310
1620
  align: "center",
1621
+ as: "span",
1311
1622
  size: 0,
1312
1623
  textOverflow: "ellipsis",
1313
1624
  weight: variant_0 === current ? "medium" : "regular",
1314
- children: IMAGE_PALETTE_TITLES[variant_0]
1625
+ children: title_0
1315
1626
  })
1316
1627
  })]
1317
1628
  }, variant_0);
@@ -1340,7 +1651,7 @@ function ImagePaletteSection(props) {
1340
1651
  tooltip: "Take the colors from an image on your device",
1341
1652
  width: "fill"
1342
1653
  })]
1343
- }), $[21] = busy, $[22] = current, $[23] = imageUrl, $[24] = onLucky, $[25] = onVariant, $[26] = palette, $[27] = pickImage, $[28] = variantWidth, $[29] = variants, $[30] = t11) : t11 = $[30];
1654
+ }), $[21] = busy, $[22] = current, $[23] = imageUrl, $[24] = onLucky, $[25] = onVariant, $[26] = palette, $[27] = pickImage, $[28] = thumbnailWidth, $[29] = variants, $[30] = t11) : t11 = $[30];
1344
1655
  let t12;
1345
1656
  $[31] !== t10 || $[32] !== t11 ? (t12 = /* @__PURE__ */ jsxs(Stack, {
1346
1657
  gap: 3,
@@ -1351,7 +1662,7 @@ function ImagePaletteSection(props) {
1351
1662
  border: !0,
1352
1663
  onDragEnter: t4,
1353
1664
  onDragLeave: t5,
1354
- onDragOver: _temp$2,
1665
+ onDragOver: _temp$4,
1355
1666
  onDrop: t6,
1356
1667
  padding: 3,
1357
1668
  radius: 3,
@@ -1359,27 +1670,33 @@ function ImagePaletteSection(props) {
1359
1670
  children: t12
1360
1671
  }), $[34] = t12, $[35] = t6, $[36] = t7, $[37] = t13) : t13 = $[37], t13;
1361
1672
  }
1362
- function _temp$2(event_0) {
1673
+ function _temp$4(event_0) {
1363
1674
  event_0.preventDefault();
1364
1675
  }
1676
+ var colorRowText = "os3rj2", range = "os3rj1", swatch = "os3rj0";
1365
1677
  const SCHEME_TITLES = {
1366
1678
  dark: "Dark mode",
1367
1679
  light: "Light mode"
1368
- }, SPLIT_HINTS = {
1369
- dark: "Shown on the right",
1370
- light: "Shown on the left"
1371
- }, SCHEME_OPTION_KEYS = [
1680
+ };
1681
+ /**
1682
+ * Where the split preview shows a scheme: the Studio's own scheme stays next
1683
+ * to the sidebar (or at the bottom, stacked on small screens), the other one
1684
+ * takes the far side
1685
+ */
1686
+ function splitHint(own, mobile) {
1687
+ return mobile ? own ? "Shown at the bottom" : "Shown at the top" : own ? "Shown on the right, next to the themer" : "Shown on the left";
1688
+ }
1689
+ const SCHEME_OPTION_KEYS = [
1372
1690
  "accent",
1373
1691
  "text",
1374
1692
  "background",
1375
1693
  "contrast"
1376
- ], Swatch = styled.input.withConfig({
1377
- displayName: "Swatch",
1378
- componentId: "sc-rahyxu-0"
1379
- })`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({
1380
- displayName: "Range",
1381
- componentId: "sc-rahyxu-1"
1382
- })`display:block;width:100%;margin:0;accent-color:var(--card-focus-ring-color);`;
1694
+ ];
1695
+ /**
1696
+ * `<input type="color">` paints the color into a shadow-DOM swatch that brings
1697
+ * its own border and padding, which then sits inside ours as a second border.
1698
+ * Stripping that chrome leaves the themed border as the only one.
1699
+ */
1383
1700
  /**
1384
1701
  * The flow for editing one of the user's own themes: its title, a live
1385
1702
  * preview, and a card per color scheme with the accent/text/background
@@ -1390,7 +1707,7 @@ const SCHEME_TITLES = {
1390
1707
  * @internal
1391
1708
  */
1392
1709
  function ThemeEditor(props) {
1393
- let $ = c(41), { focusTitle, slug } = props, { themes, images, split, send } = useThemer(), t0;
1710
+ let $ = c(42), { focusTitle, slug } = props, { themes, images, split, mobile, send } = useThemer(), t0;
1394
1711
  if ($[0] !== slug || $[1] !== themes) {
1395
1712
  let t1;
1396
1713
  $[3] === slug ? t1 = $[4] : (t1 = (candidate) => candidate.slug === slug && candidate.source === "custom", $[3] = slug, $[4] = t1), t0 = themes.find(t1), $[0] = slug, $[1] = themes, $[2] = t0;
@@ -1435,7 +1752,7 @@ function ThemeEditor(props) {
1435
1752
  title
1436
1753
  }), $[23] = send, $[24] = slug, $[25] = t7, $[26] = t8) : (t7 = $[25], t8 = $[26]);
1437
1754
  let t9;
1438
- return $[27] !== applyVariant || $[28] !== focusTitle || $[29] !== split || $[30] !== t2 || $[31] !== t3 || $[32] !== t4 || $[33] !== t5 || $[34] !== t6 || $[35] !== t7 || $[36] !== t8 || $[37] !== theme.options || $[38] !== theme.palette || $[39] !== theme.title ? (t9 = /* @__PURE__ */ jsx(ThemeEditorForm, {
1755
+ return $[27] !== applyVariant || $[28] !== focusTitle || $[29] !== mobile || $[30] !== split || $[31] !== t2 || $[32] !== t3 || $[33] !== t4 || $[34] !== t5 || $[35] !== t6 || $[36] !== t7 || $[37] !== t8 || $[38] !== theme.options || $[39] !== theme.palette || $[40] !== theme.title ? (t9 = /* @__PURE__ */ jsx(ThemeEditorForm, {
1439
1756
  focusTitle,
1440
1757
  imageUrl: t2,
1441
1758
  onDone: t3,
@@ -1448,11 +1765,12 @@ function ThemeEditor(props) {
1448
1765
  options: theme.options,
1449
1766
  palette: theme.palette,
1450
1767
  split,
1768
+ mobile,
1451
1769
  title: theme.title
1452
- }), $[27] = applyVariant, $[28] = focusTitle, $[29] = split, $[30] = t2, $[31] = t3, $[32] = t4, $[33] = t5, $[34] = t6, $[35] = t7, $[36] = t8, $[37] = theme.options, $[38] = theme.palette, $[39] = theme.title, $[40] = t9) : t9 = $[40], t9;
1770
+ }), $[27] = applyVariant, $[28] = focusTitle, $[29] = mobile, $[30] = split, $[31] = t2, $[32] = t3, $[33] = t4, $[34] = t5, $[35] = t6, $[36] = t7, $[37] = t8, $[38] = theme.options, $[39] = theme.palette, $[40] = theme.title, $[41] = t9) : t9 = $[41], t9;
1453
1771
  }
1454
1772
  function ThemeEditorForm(props) {
1455
- let $ = c(49), { focusTitle, imageUrl, onDone, onLucky, onOptionsChange, onPalette, onRemove, onTitleChange, onVariant, options, palette, split, title } = props, studioScheme = useColorSchemeValue(), t0;
1773
+ let $ = c(50), { focusTitle, imageUrl, onDone, onLucky, onOptionsChange, onPalette, onRemove, onTitleChange, onVariant, options, palette, split, mobile, title } = props, studioScheme = useColorSchemeValue(), t0;
1456
1774
  $[0] === options ? t0 = $[1] : (t0 = resolveThemeOptions(options), $[0] = options, $[1] = t0);
1457
1775
  let resolved = t0, t1;
1458
1776
  $[2] === options ? t1 = $[3] : (t1 = buildPalette(options), $[2] = options, $[3] = t1);
@@ -1476,7 +1794,7 @@ function ThemeEditorForm(props) {
1476
1794
  }), $[9] = t4) : t4 = $[9];
1477
1795
  let t5;
1478
1796
  $[10] === onTitleChange ? t5 = $[11] : (t5 = (event) => onTitleChange(event.currentTarget.value), $[10] = onTitleChange, $[11] = t5);
1479
- let t6 = focusTitle ? _temp$1 : void 0, t7;
1797
+ let t6 = focusTitle ? _temp$3 : void 0, t7;
1480
1798
  $[12] !== focusTitle || $[13] !== t5 || $[14] !== t6 || $[15] !== title ? (t7 = /* @__PURE__ */ jsxs(Stack, {
1481
1799
  gap: 2,
1482
1800
  children: [t4, /* @__PURE__ */ jsx(TextInput, {
@@ -1499,19 +1817,20 @@ function ThemeEditorForm(props) {
1499
1817
  palette
1500
1818
  }), $[17] = imageUrl, $[18] = onLucky, $[19] = onPalette, $[20] = onVariant, $[21] = options, $[22] = palette, $[23] = t8) : t8 = $[23];
1501
1819
  let t9;
1502
- $[24] !== options || $[25] !== palettes || $[26] !== patchScheme || $[27] !== resolved || $[28] !== split || $[29] !== studioScheme ? (t9 = SCHEMES.map((scheme_0) => /* @__PURE__ */ jsx(SchemeCard, {
1820
+ $[24] !== mobile || $[25] !== options || $[26] !== palettes || $[27] !== patchScheme || $[28] !== resolved || $[29] !== split || $[30] !== studioScheme ? (t9 = SCHEMES.map((scheme_0) => /* @__PURE__ */ jsx(SchemeCard, {
1503
1821
  active: split || scheme_0 === studioScheme,
1504
1822
  onChange: (changes_0) => patchScheme(scheme_0, changes_0),
1505
1823
  options: options[scheme_0] ?? {},
1506
1824
  palette: palettes[scheme_0],
1507
1825
  resolved: resolved[scheme_0],
1508
1826
  scheme: scheme_0,
1509
- split
1510
- }, scheme_0)), $[24] = options, $[25] = palettes, $[26] = patchScheme, $[27] = resolved, $[28] = split, $[29] = studioScheme, $[30] = t9) : t9 = $[30];
1827
+ split,
1828
+ splitHint: splitHint(scheme_0 === studioScheme, mobile)
1829
+ }, scheme_0)), $[24] = mobile, $[25] = options, $[26] = palettes, $[27] = patchScheme, $[28] = resolved, $[29] = split, $[30] = studioScheme, $[31] = t9) : t9 = $[31];
1511
1830
  let t10;
1512
- $[31] === t9 ? t10 = $[32] : (t10 = /* @__PURE__ */ jsx(ScrollAreaBleed, { children: t9 }), $[31] = t9, $[32] = t10);
1831
+ $[32] === t9 ? t10 = $[33] : (t10 = /* @__PURE__ */ jsx(ScrollAreaBleed, { children: t9 }), $[32] = t9, $[33] = t10);
1513
1832
  let t11;
1514
- $[33] !== t10 || $[34] !== t3 || $[35] !== t7 || $[36] !== t8 ? (t11 = /* @__PURE__ */ jsx(ScrollArea, {
1833
+ $[34] !== t10 || $[35] !== t3 || $[36] !== t7 || $[37] !== t8 ? (t11 = /* @__PURE__ */ jsx(ScrollArea, {
1515
1834
  padding: 3,
1516
1835
  children: /* @__PURE__ */ jsxs(Stack, {
1517
1836
  gap: 4,
@@ -1522,26 +1841,27 @@ function ThemeEditorForm(props) {
1522
1841
  t10
1523
1842
  ]
1524
1843
  })
1525
- }), $[33] = t10, $[34] = t3, $[35] = t7, $[36] = t8, $[37] = t11) : t11 = $[37];
1844
+ }), $[34] = t10, $[35] = t3, $[36] = t7, $[37] = t8, $[38] = t11) : t11 = $[38];
1526
1845
  let t12;
1527
- $[38] === onRemove ? t12 = $[39] : (t12 = /* @__PURE__ */ jsx(TooltipButton, {
1846
+ $[39] === onRemove ? t12 = $[40] : (t12 = /* @__PURE__ */ jsx(TooltipButton, {
1528
1847
  icon: TrashIcon,
1529
1848
  mode: "ghost",
1530
1849
  onClick: onRemove,
1531
1850
  text: "Remove",
1532
1851
  tone: "critical",
1533
1852
  tooltip: "Remove the theme — it can be restored until it is deleted"
1534
- }), $[38] = onRemove, $[39] = t12);
1853
+ }), $[39] = onRemove, $[40] = t12);
1535
1854
  let t13;
1536
- $[40] === Symbol.for("react.memo_cache_sentinel") ? (t13 = /* @__PURE__ */ jsx(Box, { flex: 1 }), $[40] = t13) : t13 = $[40];
1855
+ $[41] === Symbol.for("react.memo_cache_sentinel") ? (t13 = /* @__PURE__ */ jsx(Box, { flex: 1 }), $[41] = t13) : t13 = $[41];
1537
1856
  let t14;
1538
- $[41] === onDone ? t14 = $[42] : (t14 = /* @__PURE__ */ jsx(Button, {
1857
+ $[42] === onDone ? t14 = $[43] : (t14 = /* @__PURE__ */ jsx(Button, {
1539
1858
  mode: "ghost",
1540
1859
  onClick: onDone,
1860
+ padding: 2,
1541
1861
  text: "Done"
1542
- }), $[41] = onDone, $[42] = t14);
1862
+ }), $[42] = onDone, $[43] = t14);
1543
1863
  let t15;
1544
- $[43] !== t12 || $[44] !== t14 ? (t15 = /* @__PURE__ */ jsx(Card, {
1864
+ $[44] !== t12 || $[45] !== t14 ? (t15 = /* @__PURE__ */ jsx(Card, {
1545
1865
  borderTop: !0,
1546
1866
  padding: 3,
1547
1867
  children: /* @__PURE__ */ jsxs(Flex, {
@@ -1552,11 +1872,11 @@ function ThemeEditorForm(props) {
1552
1872
  t14
1553
1873
  ]
1554
1874
  })
1555
- }), $[43] = t12, $[44] = t14, $[45] = t15) : t15 = $[45];
1875
+ }), $[44] = t12, $[45] = t14, $[46] = t15) : t15 = $[46];
1556
1876
  let t16;
1557
- return $[46] !== t11 || $[47] !== t15 ? (t16 = /* @__PURE__ */ jsxs(Fragment, { children: [t11, t15] }), $[46] = t11, $[47] = t15, $[48] = t16) : t16 = $[48], t16;
1877
+ return $[47] !== t11 || $[48] !== t15 ? (t16 = /* @__PURE__ */ jsxs(Fragment, { children: [t11, t15] }), $[47] = t11, $[48] = t15, $[49] = t16) : t16 = $[49], t16;
1558
1878
  }
1559
- function _temp$1(event_0) {
1879
+ function _temp$3(event_0) {
1560
1880
  return event_0.currentTarget.select();
1561
1881
  }
1562
1882
  /**
@@ -1568,7 +1888,7 @@ function _temp$1(event_0) {
1568
1888
  * active, in the split preview.
1569
1889
  */
1570
1890
  function SchemeCard(props) {
1571
- let $ = c(66), { active, onChange, options, palette, resolved, scheme, split } = props, name = SCHEME_TITLES[scheme], t0;
1891
+ let $ = c(66), { active, onChange, options, palette, resolved, scheme, split, splitHint } = props, name = SCHEME_TITLES[scheme], t0;
1572
1892
  $[0] === name ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(Box, {
1573
1893
  flex: 1,
1574
1894
  children: /* @__PURE__ */ jsx(Text, {
@@ -1589,7 +1909,7 @@ function SchemeCard(props) {
1589
1909
  gap: 2,
1590
1910
  children: [t0, t1]
1591
1911
  }), $[4] = t0, $[5] = t1, $[6] = t2) : t2 = $[6];
1592
- let t3 = split ? SPLIT_HINTS[scheme] : active ? "Shown in the Studio right now" : `Shown once the Studio appearance is ${scheme}`, t4;
1912
+ let t3 = split ? splitHint : active ? "Shown in the Studio right now" : `Shown once the Studio appearance is ${scheme}`, t4;
1593
1913
  $[7] === t3 ? t4 = $[8] : (t4 = /* @__PURE__ */ jsx(Text, {
1594
1914
  muted: !0,
1595
1915
  size: 0,
@@ -1667,8 +1987,9 @@ function SchemeCard(props) {
1667
1987
  let t27 = `${name} contrast`, t28;
1668
1988
  $[50] === onChange ? t28 = $[51] : (t28 = (event) => onChange({ contrast: Number(event.currentTarget.value) }), $[50] = onChange, $[51] = t28);
1669
1989
  let t29;
1670
- $[52] !== resolved.contrast || $[53] !== t27 || $[54] !== t28 ? (t29 = /* @__PURE__ */ jsx(Range, {
1990
+ $[52] !== resolved.contrast || $[53] !== t27 || $[54] !== t28 ? (t29 = /* @__PURE__ */ jsx("input", {
1671
1991
  "aria-label": t27,
1992
+ className: range,
1672
1993
  max: 100,
1673
1994
  min: 15,
1674
1995
  onChange: t28,
@@ -1709,92 +2030,411 @@ function SchemeCard(props) {
1709
2030
  }
1710
2031
  /** One color of a scheme: a swatch with its value and an optional reset button */
1711
2032
  function ColorRow(props) {
1712
- let $ = c(24), { adjusted, auto, autoLabel: t0, label, onChange, onClear, title, value } = props, autoLabel = t0 === void 0 ? "auto" : t0, t1;
2033
+ let $ = c(23), { adjusted, auto, autoLabel: t0, label, onChange, onClear, title, value } = props, autoLabel = t0 === void 0 ? "auto" : t0, t1;
1713
2034
  $[0] === onChange ? t1 = $[1] : (t1 = (event) => onChange(event.currentTarget.value), $[0] = onChange, $[1] = t1);
1714
2035
  let t2;
1715
- $[2] !== label || $[3] !== t1 || $[4] !== value ? (t2 = /* @__PURE__ */ jsx(Swatch, {
2036
+ $[2] !== label || $[3] !== t1 || $[4] !== value ? (t2 = /* @__PURE__ */ jsx("input", {
1716
2037
  "aria-label": label,
2038
+ className: swatch,
1717
2039
  onChange: t1,
1718
2040
  type: "color",
1719
2041
  value
1720
2042
  }), $[2] = label, $[3] = t1, $[4] = value, $[5] = t2) : t2 = $[5];
1721
2043
  let t3;
1722
- $[6] === Symbol.for("react.memo_cache_sentinel") ? (t3 = { minWidth: 0 }, $[6] = t3) : t3 = $[6];
1723
- let t4;
1724
- $[7] === title ? t4 = $[8] : (t4 = /* @__PURE__ */ jsx(Text, {
2044
+ $[6] === title ? t3 = $[7] : (t3 = /* @__PURE__ */ jsx(Text, {
1725
2045
  size: 1,
1726
2046
  children: title
1727
- }), $[7] = title, $[8] = t4);
1728
- let t5 = adjusted !== void 0 && adjusted !== value ? ` → ${adjusted}` : "", t6 = auto ? ` · ${autoLabel}` : "", t7;
1729
- $[9] !== t5 || $[10] !== t6 || $[11] !== value ? (t7 = /* @__PURE__ */ jsxs(Text, {
2047
+ }), $[6] = title, $[7] = t3);
2048
+ let t4 = adjusted !== void 0 && adjusted !== value ? ` → ${adjusted}` : "", t5 = auto ? ` · ${autoLabel}` : "", t6;
2049
+ $[8] !== t4 || $[9] !== t5 || $[10] !== value ? (t6 = /* @__PURE__ */ jsxs(Text, {
1730
2050
  muted: !0,
1731
2051
  size: 0,
1732
2052
  textOverflow: "ellipsis",
1733
2053
  children: [
1734
2054
  value,
1735
- t5,
1736
- t6
2055
+ t4,
2056
+ t5
1737
2057
  ]
1738
- }), $[9] = t5, $[10] = t6, $[11] = value, $[12] = t7) : t7 = $[12];
1739
- let t8;
1740
- $[13] !== t4 || $[14] !== t7 ? (t8 = /* @__PURE__ */ jsxs(Stack, {
2058
+ }), $[8] = t4, $[9] = t5, $[10] = value, $[11] = t6) : t6 = $[11];
2059
+ let t7;
2060
+ $[12] !== t3 || $[13] !== t6 ? (t7 = /* @__PURE__ */ jsxs(Stack, {
2061
+ className: colorRowText,
1741
2062
  flex: 1,
1742
2063
  gap: 2,
1743
- style: t3,
1744
- children: [t4, t7]
1745
- }), $[13] = t4, $[14] = t7, $[15] = t8) : t8 = $[15];
1746
- let t9;
1747
- $[16] !== auto || $[17] !== autoLabel || $[18] !== onClear ? (t9 = onClear && !auto && /* @__PURE__ */ jsx(TooltipButton, {
2064
+ children: [t3, t6]
2065
+ }), $[12] = t3, $[13] = t6, $[14] = t7) : t7 = $[14];
2066
+ let t8;
2067
+ $[15] !== auto || $[16] !== autoLabel || $[17] !== onClear ? (t8 = onClear && !auto && /* @__PURE__ */ jsx(TooltipButton, {
1748
2068
  icon: ResetIcon,
1749
2069
  mode: "bleed",
1750
2070
  onClick: onClear,
1751
2071
  padding: 2,
1752
2072
  tooltip: `Reset to ${autoLabel}`
1753
- }), $[16] = auto, $[17] = autoLabel, $[18] = onClear, $[19] = t9) : t9 = $[19];
1754
- let t10;
1755
- return $[20] !== t2 || $[21] !== t8 || $[22] !== t9 ? (t10 = /* @__PURE__ */ jsxs(Flex, {
2073
+ }), $[15] = auto, $[16] = autoLabel, $[17] = onClear, $[18] = t8) : t8 = $[18];
2074
+ let t9;
2075
+ return $[19] !== t2 || $[20] !== t7 || $[21] !== t8 ? (t9 = /* @__PURE__ */ jsxs(Flex, {
1756
2076
  align: "center",
1757
2077
  gap: 2,
1758
2078
  children: [
1759
- t2,
1760
- t8,
1761
- t9
2079
+ t2,
2080
+ t7,
2081
+ t8
2082
+ ]
2083
+ }), $[19] = t2, $[20] = t7, $[21] = t8, $[22] = t9) : t9 = $[22], t9;
2084
+ }
2085
+ /**
2086
+ * The version of the code format, the first byte of every code — bumped if
2087
+ * the layout below ever changes, so older codes can still be told apart
2088
+ */
2089
+ const FIELDS = [
2090
+ "accent",
2091
+ "text",
2092
+ "background",
2093
+ "contrast"
2094
+ ];
2095
+ /**
2096
+ * Turns a theme into a short code to share in a message: `title`, and the
2097
+ * colors and contrasts of both schemes, packed into bytes — one flag byte says
2098
+ * which of the eight fields are set, each color is its three RGB bytes and a
2099
+ * contrast one byte, and the title takes up the rest — then base64url'd, so
2100
+ * it is one word without padding, safe in URLs and chat. A theme setting
2101
+ * everything comes to about 30 bytes plus its title.
2102
+ *
2103
+ * @internal
2104
+ */
2105
+ function encodeTheme(theme) {
2106
+ let bytes = [1, 0], flags = 0;
2107
+ return SCHEMES.forEach((scheme, schemeIndex) => {
2108
+ let options = theme.options[scheme] ?? {};
2109
+ FIELDS.forEach((field, fieldIndex) => {
2110
+ let value = options[field];
2111
+ if (value !== void 0) {
2112
+ if (field === "contrast") {
2113
+ if (typeof value != "number" || !Number.isFinite(value)) return;
2114
+ bytes.push(Math.round(Math.min(100, Math.max(15, value))));
2115
+ } else {
2116
+ if (typeof value != "string" || !isColor(value)) return;
2117
+ bytes.push(...rgbBytes(value));
2118
+ }
2119
+ flags |= 1 << schemeIndex * FIELDS.length + fieldIndex;
2120
+ }
2121
+ });
2122
+ }), bytes[1] = flags, bytes.push(...new TextEncoder().encode(theme.title.trim())), toBase64Url(Uint8Array.from(bytes));
2123
+ }
2124
+ /**
2125
+ * Reads a theme back out of a code — as pasted, so surrounding whitespace and
2126
+ * words around the code are fine: the first word that is a code counts. `null`
2127
+ * when nothing in the text is one.
2128
+ *
2129
+ * @internal
2130
+ */
2131
+ function decodeTheme(text) {
2132
+ for (let word of text.trim().split(/\s+/)) {
2133
+ let theme = decodeCode(word);
2134
+ if (theme) return theme;
2135
+ }
2136
+ return null;
2137
+ }
2138
+ function decodeCode(code) {
2139
+ let bytes = fromBase64Url(code);
2140
+ if (!bytes || bytes.length < 2 || bytes[0] !== 1) return null;
2141
+ let flags = bytes[1], options = {}, offset = 2;
2142
+ for (let [schemeIndex, scheme] of SCHEMES.entries()) {
2143
+ let schemeOptions = {};
2144
+ for (let [fieldIndex, field] of FIELDS.entries()) if (flags & 1 << schemeIndex * FIELDS.length + fieldIndex) {
2145
+ if (field === "contrast") {
2146
+ if (offset + 1 > bytes.length) return null;
2147
+ schemeOptions.contrast = Math.min(100, Math.max(15, bytes[offset])), offset += 1;
2148
+ } else {
2149
+ if (offset + 3 > bytes.length) return null;
2150
+ schemeOptions[field] = hexColor(bytes.subarray(offset, offset + 3)), offset += 3;
2151
+ }
2152
+ }
2153
+ Object.keys(schemeOptions).length > 0 && (options[scheme] = schemeOptions);
2154
+ }
2155
+ let title;
2156
+ try {
2157
+ title = new TextDecoder("utf-8", { fatal: !0 }).decode(bytes.subarray(offset));
2158
+ } catch {
2159
+ return null;
2160
+ }
2161
+ return {
2162
+ title: displayTitle(title),
2163
+ options
2164
+ };
2165
+ }
2166
+ /** The three RGB bytes of a `#rgb` or `#rrggbb` color */
2167
+ function rgbBytes(color) {
2168
+ let hex = color.length === 4 ? color.slice(1).replaceAll(/./g, "$&$&") : color.slice(1);
2169
+ return [
2170
+ 0,
2171
+ 2,
2172
+ 4
2173
+ ].map((start) => parseInt(hex.slice(start, start + 2), 16));
2174
+ }
2175
+ function hexColor(rgb) {
2176
+ return `#${[...rgb].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2177
+ }
2178
+ function toBase64Url(bytes) {
2179
+ let binary = "";
2180
+ for (let byte of bytes) binary += String.fromCharCode(byte);
2181
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
2182
+ }
2183
+ function fromBase64Url(code) {
2184
+ if (!/^[A-Za-z0-9_-]+$/.test(code)) return null;
2185
+ let base64 = code.replaceAll("-", "+").replaceAll("_", "/");
2186
+ try {
2187
+ return Uint8Array.from(atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "=")), (char) => char.charCodeAt(0));
2188
+ } catch {
2189
+ return null;
2190
+ }
2191
+ }
2192
+ /**
2193
+ * Copying a theme as a code to share, and adding a theme from a code someone
2194
+ * shared — with toasts telling how it went.
2195
+ *
2196
+ * @internal
2197
+ */
2198
+ function useThemeCodes() {
2199
+ let $ = c(11), { send } = useThemer(), toast = useToast(), t0;
2200
+ $[0] === toast ? t0 = $[1] : (t0 = async (theme) => {
2201
+ let title = displayTitle(theme.title);
2202
+ try {
2203
+ await navigator.clipboard.writeText(encodeTheme(theme)), toast.push({
2204
+ status: "success",
2205
+ title: `Copied ${title} as a code`,
2206
+ description: "Paste it in the themer of another Studio to add the theme there"
2207
+ });
2208
+ } catch {
2209
+ toast.push({
2210
+ status: "error",
2211
+ title: `Could not copy ${title}`
2212
+ });
2213
+ }
2214
+ }, $[0] = toast, $[1] = t0);
2215
+ let copyTheme = t0, t1;
2216
+ $[2] !== send || $[3] !== toast ? (t1 = (text) => {
2217
+ let theme_0 = decodeTheme(text);
2218
+ return theme_0 ? (send({
2219
+ type: "theme.import",
2220
+ title: theme_0.title,
2221
+ options: theme_0.options
2222
+ }), toast.push({
2223
+ status: "success",
2224
+ title: `Added ${theme_0.title}`
2225
+ }), !0) : !1;
2226
+ }, $[2] = send, $[3] = toast, $[4] = t1) : t1 = $[4];
2227
+ let addThemeFromText = t1, t2;
2228
+ $[5] === addThemeFromText ? t2 = $[6] : (t2 = async () => {
2229
+ let text_0;
2230
+ try {
2231
+ text_0 = await navigator.clipboard.readText();
2232
+ } catch {
2233
+ return "no-clipboard";
2234
+ }
2235
+ return addThemeFromText(text_0) ? "added" : "no-theme";
2236
+ }, $[5] = addThemeFromText, $[6] = t2);
2237
+ let addThemeFromClipboard = t2, t3;
2238
+ return $[7] !== addThemeFromClipboard || $[8] !== addThemeFromText || $[9] !== copyTheme ? (t3 = {
2239
+ copyTheme,
2240
+ addThemeFromText,
2241
+ addThemeFromClipboard
2242
+ }, $[7] = addThemeFromClipboard, $[8] = addThemeFromText, $[9] = copyTheme, $[10] = t3) : t3 = $[10], t3;
2243
+ }
2244
+ /**
2245
+ * Adds a theme pasted anywhere in the Studio while the themer's list is
2246
+ * showing — pastes into inputs and other editable things are theirs.
2247
+ *
2248
+ * @internal
2249
+ */
2250
+ function usePasteThemeCodes() {
2251
+ let $ = c(5), { open, send } = useThemer(), toast = useToast(), t0, t1;
2252
+ $[0] !== open || $[1] !== send || $[2] !== toast ? (t0 = () => {
2253
+ if (!open) return;
2254
+ let handlePaste = (event) => {
2255
+ if (isEditable(event.target)) return;
2256
+ let theme = decodeTheme(event.clipboardData?.getData("text/plain") ?? "");
2257
+ theme && (event.preventDefault(), send({
2258
+ type: "theme.import",
2259
+ title: theme.title,
2260
+ options: theme.options
2261
+ }), toast.push({
2262
+ status: "success",
2263
+ title: `Added ${theme.title}`
2264
+ }));
2265
+ };
2266
+ return document.addEventListener("paste", handlePaste), () => document.removeEventListener("paste", handlePaste);
2267
+ }, t1 = [
2268
+ open,
2269
+ send,
2270
+ toast
2271
+ ], $[0] = open, $[1] = send, $[2] = toast, $[3] = t0, $[4] = t1) : (t0 = $[3], t1 = $[4]), useEffect(t0, t1);
2272
+ }
2273
+ function isEditable(target) {
2274
+ return target instanceof HTMLElement && (target.isContentEditable || target.matches("input, textarea, select"));
2275
+ }
2276
+ /**
2277
+ * Where a theme code someone shared gets pasted by hand — for browsers that
2278
+ * do not hand the clipboard over, and for codes that are not on it.
2279
+ *
2280
+ * @internal
2281
+ */
2282
+ function PasteThemeDialog(props) {
2283
+ let $ = c(35), { onClose } = props, { addThemeFromText } = useThemeCodes(), [text, setText] = useState(""), [invalid, setInvalid] = useState(!1), t0;
2284
+ $[0] !== addThemeFromText || $[1] !== onClose || $[2] !== text ? (t0 = () => {
2285
+ addThemeFromText(text) ? onClose() : setInvalid(!0);
2286
+ }, $[0] = addThemeFromText, $[1] = onClose, $[2] = text, $[3] = t0) : t0 = $[3];
2287
+ let add = t0, t1;
2288
+ $[4] === onClose ? t1 = $[5] : (t1 = /* @__PURE__ */ jsx(Button, {
2289
+ mode: "ghost",
2290
+ onClick: onClose,
2291
+ padding: 2,
2292
+ text: "Cancel"
2293
+ }), $[4] = onClose, $[5] = t1);
2294
+ let t2;
2295
+ $[6] === text ? t2 = $[7] : (t2 = text.trim(), $[6] = text, $[7] = t2);
2296
+ let t3 = t2 === "", t4;
2297
+ $[8] !== add || $[9] !== t3 ? (t4 = /* @__PURE__ */ jsx(Button, {
2298
+ disabled: t3,
2299
+ onClick: add,
2300
+ padding: 2,
2301
+ text: "Add theme",
2302
+ tone: "primary"
2303
+ }), $[8] = add, $[9] = t3, $[10] = t4) : t4 = $[10];
2304
+ let t5;
2305
+ $[11] !== t1 || $[12] !== t4 ? (t5 = /* @__PURE__ */ jsxs(Flex, {
2306
+ gap: 2,
2307
+ justify: "flex-end",
2308
+ padding: 3,
2309
+ children: [t1, t4]
2310
+ }), $[11] = t1, $[12] = t4, $[13] = t5) : t5 = $[13];
2311
+ let t6;
2312
+ $[14] === add ? t6 = $[15] : (t6 = (event) => {
2313
+ event.preventDefault(), add();
2314
+ }, $[14] = add, $[15] = t6);
2315
+ let t7;
2316
+ $[16] === Symbol.for("react.memo_cache_sentinel") ? (t7 = /* @__PURE__ */ jsx(Text, {
2317
+ muted: !0,
2318
+ size: 1,
2319
+ children: "Paste the code someone copied from their themer — the theme is added to your list and applied."
2320
+ }), $[16] = t7) : t7 = $[16];
2321
+ let t8 = invalid ? "themer-paste-error" : void 0, t9 = invalid ? "That is not a theme code" : void 0, t10;
2322
+ $[17] === Symbol.for("react.memo_cache_sentinel") ? (t10 = (event_0) => {
2323
+ setText(event_0.currentTarget.value), setInvalid(!1);
2324
+ }, $[17] = t10) : t10 = $[17];
2325
+ let t11;
2326
+ $[18] !== invalid || $[19] !== t8 || $[20] !== t9 || $[21] !== text ? (t11 = /* @__PURE__ */ jsx(TextInput, {
2327
+ "aria-describedby": t8,
2328
+ "aria-invalid": invalid,
2329
+ "aria-label": "Theme code",
2330
+ autoFocus: !0,
2331
+ customValidity: t9,
2332
+ fontSize: 1,
2333
+ onChange: t10,
2334
+ placeholder: "AQ…",
2335
+ value: text
2336
+ }), $[18] = invalid, $[19] = t8, $[20] = t9, $[21] = text, $[22] = t11) : t11 = $[22];
2337
+ let t12;
2338
+ $[23] === invalid ? t12 = $[24] : (t12 = invalid && /* @__PURE__ */ jsx(Card, {
2339
+ id: "themer-paste-error",
2340
+ padding: 3,
2341
+ radius: 2,
2342
+ role: "alert",
2343
+ tone: "critical",
2344
+ children: /* @__PURE__ */ jsx(Text, {
2345
+ size: 1,
2346
+ children: "That is not a theme code — codes are one word, copied from a theme's menu."
2347
+ })
2348
+ }), $[23] = invalid, $[24] = t12);
2349
+ let t13;
2350
+ $[25] !== t11 || $[26] !== t12 ? (t13 = /* @__PURE__ */ jsxs(Stack, {
2351
+ gap: 3,
2352
+ padding: 4,
2353
+ children: [
2354
+ t7,
2355
+ t11,
2356
+ t12
1762
2357
  ]
1763
- }), $[20] = t2, $[21] = t8, $[22] = t9, $[23] = t10) : t10 = $[23], t10;
1764
- }
1765
- const Root = styled.div.withConfig({
1766
- displayName: "Root",
1767
- componentId: "sc-krwbzw-0"
1768
- })`position:relative;`, PickButton = styled.button.withConfig({
1769
- displayName: "PickButton",
1770
- componentId: "sc-krwbzw-1"
1771
- })`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$1 = styled.span.withConfig({
1772
- displayName: "Frame",
1773
- componentId: "sc-krwbzw-2"
1774
- })`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({
1775
- displayName: "MenuSlot",
1776
- componentId: "sc-krwbzw-3"
1777
- })`position:absolute;top:10px;right:10px;opacity:0;transition:opacity 100ms;${Root}:hover &,${Root}:focus-within &,&[data-visible='true']{opacity:1;}`;
2358
+ }), $[25] = t11, $[26] = t12, $[27] = t13) : t13 = $[27];
2359
+ let t14;
2360
+ $[28] !== t13 || $[29] !== t6 ? (t14 = /* @__PURE__ */ jsx("form", {
2361
+ onSubmit: t6,
2362
+ children: t13
2363
+ }), $[28] = t13, $[29] = t6, $[30] = t14) : t14 = $[30];
2364
+ let t15;
2365
+ return $[31] !== onClose || $[32] !== t14 || $[33] !== t5 ? (t15 = /* @__PURE__ */ jsx(Dialog, {
2366
+ animate: !0,
2367
+ footer: t5,
2368
+ header: "Add a shared theme",
2369
+ id: "themer-paste",
2370
+ onClickOutside: onClose,
2371
+ onClose,
2372
+ width: 0,
2373
+ children: t14
2374
+ }), $[31] = onClose, $[32] = t14, $[33] = t5, $[34] = t15) : t15 = $[34], t15;
2375
+ }
2376
+ var frame$1 = "_1wsn7882", menuCard = "_1wsn7884", menuSlot = "_1wsn7883", pickButton = "_1wsn7881", root = "_1wsn7880";
2377
+ /**
2378
+ * Scrolls the applied theme's card into view when it is not already, as a
2379
+ * ref: the list scrolls to it as it mounts — coming back from the editor or
2380
+ * the removed themes — and when another theme gets applied
2381
+ */
2382
+ function scrollRefIntoViewIfNeeded(node) {
2383
+ node && scrollIntoView(node, {
2384
+ block: "nearest",
2385
+ scrollMode: "if-needed"
2386
+ });
2387
+ }
1778
2388
  /**
1779
2389
  * One theme in the list: the floating preview with the title below it, like
1780
2390
  * an appearance option in macOS System Settings. Clicking it applies the
1781
- * theme to the whole Studio; its menu holds the edit, duplicate and remove
1782
- * flows that apply to the theme.
2391
+ * theme to the whole Studio, dragging it moves the theme to another place in
2392
+ * the list; its menu holds the move, edit, duplicate and remove flows that
2393
+ * apply to the theme.
1783
2394
  *
1784
2395
  * @internal
1785
2396
  */
1786
2397
  function ThemeCard(props) {
1787
- let $ = c(52), { active, theme } = props, { send } = useThemer(), { scheme } = useRootTheme(), [menuOpen, setMenuOpen] = useState(!1), t0;
2398
+ let $ = c(85), { active, theme } = props, { themes, send } = useThemer(), { copyTheme } = useThemeCodes(), { scheme } = useRootTheme(), [menuOpen, setMenuOpen] = useState(!1), dragControls = useDragControls(), dragged = useRef(!1), t0;
1788
2399
  $[0] === theme.title ? t0 = $[1] : (t0 = displayTitle(theme.title), $[0] = theme.title, $[1] = t0);
1789
2400
  let title = t0, t1;
1790
- $[2] !== send || $[3] !== theme.slug ? (t1 = () => send({
1791
- type: "theme.pick",
1792
- slug: theme.slug
1793
- }), $[2] = send, $[3] = theme.slug, $[4] = t1) : t1 = $[4];
1794
- let t2;
1795
- $[5] === theme.options ? t2 = $[6] : (t2 = /* @__PURE__ */ jsx(Frame$1, { children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: theme.options }) }), $[5] = theme.options, $[6] = t2);
1796
- let t3 = active ? "medium" : "regular", t4;
1797
- $[7] !== t3 || $[8] !== title ? (t4 = /* @__PURE__ */ jsx(Box, {
2401
+ $[2] === theme.slug ? t1 = $[3] : (t1 = (candidate) => candidate.slug === theme.slug, $[2] = theme.slug, $[3] = t1);
2402
+ let index = themes.findIndex(t1), t2;
2403
+ $[4] !== index || $[5] !== send || $[6] !== themes ? (t2 = (to) => {
2404
+ let order = themes.map(_temp$2);
2405
+ order.splice(to, 0, ...order.splice(index, 1)), send({
2406
+ type: "theme.reorder",
2407
+ order
2408
+ });
2409
+ }, $[4] = index, $[5] = send, $[6] = themes, $[7] = t2) : t2 = $[7];
2410
+ let moveTo = t2, t3, t4;
2411
+ $[8] === Symbol.for("react.memo_cache_sentinel") ? (t3 = () => {
2412
+ requestAnimationFrame(() => {
2413
+ dragged.current = !1;
2414
+ });
2415
+ }, t4 = () => {
2416
+ dragged.current = !0;
2417
+ }, $[8] = t3, $[9] = t4) : (t3 = $[8], t4 = $[9]);
2418
+ let t5 = active ? scrollRefIntoViewIfNeeded : void 0, t6;
2419
+ $[10] === Symbol.for("react.memo_cache_sentinel") ? (t6 = { scale: 1.04 }, $[10] = t6) : t6 = $[10];
2420
+ let t7;
2421
+ $[11] !== send || $[12] !== theme.slug ? (t7 = () => {
2422
+ dragged.current || send({
2423
+ type: "theme.pick",
2424
+ slug: theme.slug
2425
+ });
2426
+ }, $[11] = send, $[12] = theme.slug, $[13] = t7) : t7 = $[13];
2427
+ let t8;
2428
+ $[14] === dragControls ? t8 = $[15] : (t8 = (event) => {
2429
+ event.pointerType !== "touch" && dragControls.start(event);
2430
+ }, $[14] = dragControls, $[15] = t8);
2431
+ let t9;
2432
+ $[16] === theme.options ? t9 = $[17] : (t9 = /* @__PURE__ */ jsx("span", {
2433
+ className: frame$1,
2434
+ children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: theme.options })
2435
+ }), $[16] = theme.options, $[17] = t9);
2436
+ let t10 = active ? "medium" : "regular", t11;
2437
+ $[18] !== t10 || $[19] !== title ? (t11 = /* @__PURE__ */ jsx(Box, {
1798
2438
  as: "span",
1799
2439
  display: "block",
1800
2440
  paddingTop: 2,
@@ -1804,55 +2444,81 @@ function ThemeCard(props) {
1804
2444
  as: "span",
1805
2445
  size: 1,
1806
2446
  textOverflow: "ellipsis",
1807
- weight: t3,
2447
+ weight: t10,
1808
2448
  children: title
1809
2449
  })
1810
- }), $[7] = t3, $[8] = title, $[9] = t4) : t4 = $[9];
1811
- let t5;
1812
- $[10] !== active || $[11] !== t1 || $[12] !== t2 || $[13] !== t4 ? (t5 = /* @__PURE__ */ jsxs(PickButton, {
2450
+ }), $[18] = t10, $[19] = title, $[20] = t11) : t11 = $[20];
2451
+ let t12;
2452
+ $[21] !== active || $[22] !== t11 || $[23] !== t7 || $[24] !== t8 || $[25] !== t9 ? (t12 = /* @__PURE__ */ jsxs("button", {
1813
2453
  "aria-pressed": active,
1814
- onClick: t1,
2454
+ className: pickButton,
2455
+ onClick: t7,
2456
+ onPointerDown: t8,
1815
2457
  type: "button",
1816
- children: [t2, t4]
1817
- }), $[10] = active, $[11] = t1, $[12] = t2, $[13] = t4, $[14] = t5) : t5 = $[14];
1818
- let t6;
1819
- $[15] === Symbol.for("react.memo_cache_sentinel") ? (t6 = /* @__PURE__ */ jsx(Text, {
2458
+ children: [t9, t11]
2459
+ }), $[21] = active, $[22] = t11, $[23] = t7, $[24] = t8, $[25] = t9, $[26] = t12) : t12 = $[26];
2460
+ let t13;
2461
+ $[27] === Symbol.for("react.memo_cache_sentinel") ? (t13 = /* @__PURE__ */ jsx(Text, {
1820
2462
  size: 1,
1821
2463
  children: "Show more"
1822
- }), $[15] = t6) : t6 = $[15];
1823
- let t7;
1824
- $[16] === Symbol.for("react.memo_cache_sentinel") ? (t7 = { display: "inline-block" }, $[16] = t7) : t7 = $[16];
1825
- let t8 = `Show more for ${title}`, t9;
1826
- $[17] === t8 ? t9 = $[18] : (t9 = /* @__PURE__ */ jsx(Button, {
1827
- "aria-label": t8,
2464
+ }), $[27] = t13) : t13 = $[27];
2465
+ let t14 = `Show more for ${title}`, t15;
2466
+ $[28] === t14 ? t15 = $[29] : (t15 = /* @__PURE__ */ jsx(Button, {
2467
+ "aria-label": t14,
1828
2468
  fontSize: 1,
1829
2469
  icon: EllipsisHorizontalIcon,
1830
2470
  mode: "ghost",
1831
2471
  padding: 1,
1832
2472
  radius: 2
1833
- }), $[17] = t8, $[18] = t9);
1834
- let t10 = `themer-theme-${theme.slug}`, t11;
1835
- $[19] !== send || $[20] !== theme.slug || $[21] !== theme.source ? (t11 = theme.source === "custom" && /* @__PURE__ */ jsx(MenuItem, {
2473
+ }), $[28] = t14, $[29] = t15);
2474
+ let t16 = `themer-theme-${theme.slug}`, t17 = index <= 0, t18;
2475
+ $[30] !== index || $[31] !== moveTo ? (t18 = () => moveTo(index - 1), $[30] = index, $[31] = moveTo, $[32] = t18) : t18 = $[32];
2476
+ let t19;
2477
+ $[33] !== t17 || $[34] !== t18 ? (t19 = /* @__PURE__ */ jsx(MenuItem, {
2478
+ disabled: t17,
2479
+ icon: ArrowUpIcon,
2480
+ onClick: t18,
2481
+ text: "Move up"
2482
+ }), $[33] = t17, $[34] = t18, $[35] = t19) : t19 = $[35];
2483
+ let t20 = index >= themes.length - 1, t21;
2484
+ $[36] !== index || $[37] !== moveTo ? (t21 = () => moveTo(index + 1), $[36] = index, $[37] = moveTo, $[38] = t21) : t21 = $[38];
2485
+ let t22;
2486
+ $[39] !== t20 || $[40] !== t21 ? (t22 = /* @__PURE__ */ jsx(MenuItem, {
2487
+ disabled: t20,
2488
+ icon: ArrowDownIcon,
2489
+ onClick: t21,
2490
+ text: "Move down"
2491
+ }), $[39] = t20, $[40] = t21, $[41] = t22) : t22 = $[41];
2492
+ let t23;
2493
+ $[42] === Symbol.for("react.memo_cache_sentinel") ? (t23 = /* @__PURE__ */ jsx(MenuDivider, {}), $[42] = t23) : t23 = $[42];
2494
+ let t24;
2495
+ $[43] !== send || $[44] !== theme.slug || $[45] !== theme.source ? (t24 = theme.source === "custom" && /* @__PURE__ */ jsx(MenuItem, {
1836
2496
  icon: EditIcon,
1837
2497
  onClick: () => send({
1838
2498
  type: "theme.edit",
1839
2499
  slug: theme.slug
1840
2500
  }),
1841
2501
  text: "Edit"
1842
- }), $[19] = send, $[20] = theme.slug, $[21] = theme.source, $[22] = t11) : t11 = $[22];
1843
- let t12;
1844
- $[23] !== send || $[24] !== theme.slug ? (t12 = () => send({
2502
+ }), $[43] = send, $[44] = theme.slug, $[45] = theme.source, $[46] = t24) : t24 = $[46];
2503
+ let t25;
2504
+ $[47] !== send || $[48] !== theme.slug ? (t25 = () => send({
1845
2505
  type: "theme.duplicate",
1846
2506
  slug: theme.slug
1847
- }), $[23] = send, $[24] = theme.slug, $[25] = t12) : t12 = $[25];
1848
- let t13 = theme.source === "custom" ? "Duplicate" : "Duplicate to edit", t14;
1849
- $[26] !== t12 || $[27] !== t13 ? (t14 = /* @__PURE__ */ jsx(MenuItem, {
2507
+ }), $[47] = send, $[48] = theme.slug, $[49] = t25) : t25 = $[49];
2508
+ let t26 = theme.source === "custom" ? "Duplicate" : "Duplicate to edit", t27;
2509
+ $[50] !== t25 || $[51] !== t26 ? (t27 = /* @__PURE__ */ jsx(MenuItem, {
1850
2510
  icon: CopyIcon,
1851
- onClick: t12,
1852
- text: t13
1853
- }), $[26] = t12, $[27] = t13, $[28] = t14) : t14 = $[28];
1854
- let t15;
1855
- $[29] !== send || $[30] !== theme.slug || $[31] !== theme.source ? (t15 = theme.source !== "config" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuDivider, {}), /* @__PURE__ */ jsx(MenuItem, {
2511
+ onClick: t25,
2512
+ text: t26
2513
+ }), $[50] = t25, $[51] = t26, $[52] = t27) : t27 = $[52];
2514
+ let t28;
2515
+ $[53] !== copyTheme || $[54] !== theme ? (t28 = /* @__PURE__ */ jsx(MenuItem, {
2516
+ icon: ShareIcon,
2517
+ onClick: () => void copyTheme(theme),
2518
+ text: "Copy to share"
2519
+ }), $[53] = copyTheme, $[54] = theme, $[55] = t28) : t28 = $[55];
2520
+ let t29;
2521
+ $[56] !== send || $[57] !== theme.slug || $[58] !== theme.source ? (t29 = theme.source !== "config" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuDivider, {}), /* @__PURE__ */ jsx(MenuItem, {
1856
2522
  icon: TrashIcon,
1857
2523
  onClick: () => send({
1858
2524
  type: "theme.remove",
@@ -1860,59 +2526,86 @@ function ThemeCard(props) {
1860
2526
  }),
1861
2527
  text: "Remove",
1862
2528
  tone: "critical"
1863
- })] }), $[29] = send, $[30] = theme.slug, $[31] = theme.source, $[32] = t15) : t15 = $[32];
1864
- let t16;
1865
- $[33] !== t11 || $[34] !== t14 || $[35] !== t15 ? (t16 = /* @__PURE__ */ jsxs(Menu, { children: [
1866
- t11,
1867
- t14,
1868
- t15
1869
- ] }), $[33] = t11, $[34] = t14, $[35] = t15, $[36] = t16) : t16 = $[36];
1870
- let t17, t18;
1871
- $[37] === Symbol.for("react.memo_cache_sentinel") ? (t17 = () => setMenuOpen(!1), t18 = () => setMenuOpen(!0), $[37] = t17, $[38] = t18) : (t17 = $[37], t18 = $[38]);
1872
- let t19;
1873
- $[39] === scheme ? t19 = $[40] : (t19 = {
2529
+ })] }), $[56] = send, $[57] = theme.slug, $[58] = theme.source, $[59] = t29) : t29 = $[59];
2530
+ let t30;
2531
+ $[60] !== t19 || $[61] !== t22 || $[62] !== t24 || $[63] !== t27 || $[64] !== t28 || $[65] !== t29 ? (t30 = /* @__PURE__ */ jsxs(Menu, { children: [
2532
+ t19,
2533
+ t22,
2534
+ t23,
2535
+ t24,
2536
+ t27,
2537
+ t28,
2538
+ t29
2539
+ ] }), $[60] = t19, $[61] = t22, $[62] = t24, $[63] = t27, $[64] = t28, $[65] = t29, $[66] = t30) : t30 = $[66];
2540
+ let t31, t32;
2541
+ $[67] === Symbol.for("react.memo_cache_sentinel") ? (t31 = () => setMenuOpen(!1), t32 = () => setMenuOpen(!0), $[67] = t31, $[68] = t32) : (t31 = $[67], t32 = $[68]);
2542
+ let t33;
2543
+ $[69] === scheme ? t33 = $[70] : (t33 = {
1874
2544
  animate: !0,
1875
2545
  placement: "bottom-end",
1876
2546
  portal: !0,
1877
2547
  scheme
1878
- }, $[39] = scheme, $[40] = t19);
1879
- let t20;
1880
- $[41] !== t10 || $[42] !== t16 || $[43] !== t19 || $[44] !== t9 ? (t20 = /* @__PURE__ */ jsx(Tooltip, {
2548
+ }, $[69] = scheme, $[70] = t33);
2549
+ let t34;
2550
+ $[71] !== t15 || $[72] !== t16 || $[73] !== t30 || $[74] !== t33 ? (t34 = /* @__PURE__ */ jsx(Tooltip, {
1881
2551
  animate: !0,
1882
- content: t6,
2552
+ content: t13,
1883
2553
  placement: "bottom",
1884
2554
  portal: !0,
1885
2555
  children: /* @__PURE__ */ jsx(Card, {
2556
+ className: menuCard,
1886
2557
  radius: 2,
1887
2558
  scheme: "dark",
1888
- style: t7,
1889
2559
  children: /* @__PURE__ */ jsx(MenuButton, {
1890
- button: t9,
1891
- id: t10,
1892
- menu: t16,
1893
- onClose: t17,
1894
- onOpen: t18,
1895
- popover: t19
2560
+ button: t15,
2561
+ id: t16,
2562
+ menu: t30,
2563
+ onClose: t31,
2564
+ onOpen: t32,
2565
+ popover: t33
1896
2566
  })
1897
2567
  })
1898
- }), $[41] = t10, $[42] = t16, $[43] = t19, $[44] = t9, $[45] = t20) : t20 = $[45];
1899
- let t21;
1900
- $[46] !== menuOpen || $[47] !== t20 ? (t21 = /* @__PURE__ */ jsx(MenuSlot, {
2568
+ }), $[71] = t15, $[72] = t16, $[73] = t30, $[74] = t33, $[75] = t34) : t34 = $[75];
2569
+ let t35;
2570
+ $[76] !== menuOpen || $[77] !== t34 ? (t35 = /* @__PURE__ */ jsx("div", {
2571
+ className: menuSlot,
1901
2572
  "data-visible": menuOpen,
1902
- children: t20
1903
- }), $[46] = menuOpen, $[47] = t20, $[48] = t21) : t21 = $[48];
1904
- let t22;
1905
- return $[49] !== t21 || $[50] !== t5 ? (t22 = /* @__PURE__ */ jsxs(Root, { children: [t5, t21] }), $[49] = t21, $[50] = t5, $[51] = t22) : t22 = $[51], t22;
1906
- }
2573
+ children: t34
2574
+ }), $[76] = menuOpen, $[77] = t34, $[78] = t35) : t35 = $[78];
2575
+ let t36;
2576
+ return $[79] !== dragControls || $[80] !== t12 || $[81] !== t35 || $[82] !== t5 || $[83] !== theme.slug ? (t36 = /* @__PURE__ */ jsxs(Reorder.Item, {
2577
+ as: "div",
2578
+ className: root,
2579
+ dragControls,
2580
+ dragListener: !1,
2581
+ onDragEnd: t3,
2582
+ onDragStart: t4,
2583
+ ref: t5,
2584
+ value: theme.slug,
2585
+ whileDrag: t6,
2586
+ children: [t12, t35]
2587
+ }), $[79] = dragControls, $[80] = t12, $[81] = t35, $[82] = t5, $[83] = theme.slug, $[84] = t36) : t36 = $[84], t36;
2588
+ }
2589
+ function _temp$2(candidate_0) {
2590
+ return candidate_0.slug;
2591
+ }
2592
+ var cardGrid = "twjqgh0";
2593
+ /**
2594
+ * One column at the sidebar's default width; wider sidebars — and the
2595
+ * overlay on small screens — fit more cards per row
2596
+ */
1907
2597
  /**
1908
- * The flow for picking a theme: one column of theme cards — the configured
1909
- * theme, the presets and the user's own themes — with the entry points to
1910
- * the add and restore flows below.
2598
+ * The flow for picking a theme: a grid of theme cards — the configured theme,
2599
+ * the presets and the user's own themes — that drag into the order the user
2600
+ * wants, with the entry points to the add and restore flows below. A theme
2601
+ * code someone shared pastes right into the list, or through the paste button.
1911
2602
  *
1912
2603
  * @internal
1913
2604
  */
1914
2605
  function ThemeList() {
1915
- let $ = c(24), { themes, removed, active, send } = useThemer(), t0;
2606
+ let $ = c(38), { themes, removed, active, send } = useThemer(), { addThemeFromClipboard } = useThemeCodes(), [pasting, setPasting] = useState(!1);
2607
+ usePasteThemeCodes();
2608
+ let t0;
1916
2609
  $[0] === send ? t0 = $[1] : (t0 = (palette, file) => send({
1917
2610
  type: "theme.add",
1918
2611
  title: titleFromFileName(file.name),
@@ -1921,31 +2614,51 @@ function ThemeList() {
1921
2614
  imageUrl: URL.createObjectURL(file)
1922
2615
  }), $[0] = send, $[1] = t0);
1923
2616
  let { busy, pickImage } = useImagePalette(t0), t1;
1924
- if ($[2] !== active || $[3] !== themes) {
1925
- let t2;
1926
- $[5] === active ? t2 = $[6] : (t2 = (theme) => /* @__PURE__ */ jsx(ThemeCard, {
1927
- active: theme.slug === active.slug,
1928
- theme
1929
- }, theme.slug), $[5] = active, $[6] = t2), t1 = themes.map(t2), $[2] = active, $[3] = themes, $[4] = t1;
1930
- } else t1 = $[4];
2617
+ $[2] === send ? t1 = $[3] : (t1 = (order) => send({
2618
+ type: "theme.reorder",
2619
+ order
2620
+ }), $[2] = send, $[3] = t1);
1931
2621
  let t2;
1932
- $[7] !== removed.length || $[8] !== send ? (t2 = removed.length > 0 && /* @__PURE__ */ jsx(Button, {
2622
+ $[4] === themes ? t2 = $[5] : (t2 = themes.map(_temp$1), $[4] = themes, $[5] = t2);
2623
+ let t3;
2624
+ if ($[6] !== active || $[7] !== themes) {
2625
+ let t4;
2626
+ $[9] === active ? t4 = $[10] : (t4 = (theme_0) => /* @__PURE__ */ jsx(ThemeCard, {
2627
+ active: theme_0.slug === active.slug,
2628
+ theme: theme_0
2629
+ }, theme_0.slug), $[9] = active, $[10] = t4), t3 = themes.map(t4), $[6] = active, $[7] = themes, $[8] = t3;
2630
+ } else t3 = $[8];
2631
+ let t4;
2632
+ $[11] !== t1 || $[12] !== t2 || $[13] !== t3 ? (t4 = /* @__PURE__ */ jsx(MotionConfig, {
2633
+ reducedMotion: "user",
2634
+ children: /* @__PURE__ */ jsx(Reorder.Group, {
2635
+ as: "div",
2636
+ className: cardGrid,
2637
+ onReorder: t1,
2638
+ values: t2,
2639
+ children: t3
2640
+ })
2641
+ }), $[11] = t1, $[12] = t2, $[13] = t3, $[14] = t4) : t4 = $[14];
2642
+ let t5;
2643
+ $[15] !== removed.length || $[16] !== send ? (t5 = removed.length > 0 && /* @__PURE__ */ jsx(Button, {
2644
+ gap: 2,
1933
2645
  icon: RestoreIcon,
1934
2646
  mode: "bleed",
1935
2647
  onClick: () => send({ type: "flow.removed" }),
2648
+ padding: 2,
1936
2649
  text: `Show removed (${removed.length})`,
1937
2650
  width: "fill"
1938
- }), $[7] = removed.length, $[8] = send, $[9] = t2) : t2 = $[9];
1939
- let t3;
1940
- $[10] !== t1 || $[11] !== t2 ? (t3 = /* @__PURE__ */ jsx(ScrollArea, {
2651
+ }), $[15] = removed.length, $[16] = send, $[17] = t5) : t5 = $[17];
2652
+ let t6;
2653
+ $[18] !== t4 || $[19] !== t5 ? (t6 = /* @__PURE__ */ jsx(ScrollArea, {
1941
2654
  padding: 3,
1942
2655
  children: /* @__PURE__ */ jsxs(Stack, {
1943
2656
  gap: 4,
1944
- children: [t1, t2]
2657
+ children: [t4, t5]
1945
2658
  })
1946
- }), $[10] = t1, $[11] = t2, $[12] = t3) : t3 = $[12];
1947
- let t4;
1948
- $[13] === send ? t4 = $[14] : (t4 = /* @__PURE__ */ jsx(Box, {
2659
+ }), $[18] = t4, $[19] = t5, $[20] = t6) : t6 = $[20];
2660
+ let t7;
2661
+ $[21] === send ? t7 = $[22] : (t7 = /* @__PURE__ */ jsx(Box, {
1949
2662
  flex: 1,
1950
2663
  children: /* @__PURE__ */ jsx(TooltipButton, {
1951
2664
  icon: AddIcon,
@@ -1955,25 +2668,47 @@ function ThemeList() {
1955
2668
  tooltip: "Add a theme based on the applied one",
1956
2669
  width: "fill"
1957
2670
  })
1958
- }), $[13] = send, $[14] = t4);
1959
- let t5;
1960
- $[15] !== busy || $[16] !== pickImage ? (t5 = /* @__PURE__ */ jsx(ImageFileButton, {
2671
+ }), $[21] = send, $[22] = t7);
2672
+ let t8;
2673
+ $[23] !== busy || $[24] !== pickImage ? (t8 = /* @__PURE__ */ jsx(ImageFileButton, {
1961
2674
  loading: busy,
1962
2675
  mode: "ghost",
1963
2676
  onFile: pickImage,
1964
2677
  tooltip: "Add a theme from the colors of an image"
1965
- }), $[15] = busy, $[16] = pickImage, $[17] = t5) : t5 = $[17];
1966
- let t6;
1967
- $[18] !== t4 || $[19] !== t5 ? (t6 = /* @__PURE__ */ jsx(Card, {
2678
+ }), $[23] = busy, $[24] = pickImage, $[25] = t8) : t8 = $[25];
2679
+ let t9;
2680
+ $[26] === addThemeFromClipboard ? t9 = $[27] : (t9 = /* @__PURE__ */ jsx(TooltipButton, {
2681
+ icon: ClipboardIcon,
2682
+ mode: "ghost",
2683
+ onClick: async () => {
2684
+ await addThemeFromClipboard() !== "added" && setPasting(!0);
2685
+ },
2686
+ tooltip: "Add a theme from a code someone shared"
2687
+ }), $[26] = addThemeFromClipboard, $[27] = t9);
2688
+ let t10;
2689
+ $[28] !== t7 || $[29] !== t8 || $[30] !== t9 ? (t10 = /* @__PURE__ */ jsx(Card, {
1968
2690
  borderTop: !0,
1969
2691
  padding: 3,
1970
2692
  children: /* @__PURE__ */ jsxs(Flex, {
1971
2693
  gap: 2,
1972
- children: [t4, t5]
2694
+ children: [
2695
+ t7,
2696
+ t8,
2697
+ t9
2698
+ ]
1973
2699
  })
1974
- }), $[18] = t4, $[19] = t5, $[20] = t6) : t6 = $[20];
1975
- let t7;
1976
- return $[21] !== t3 || $[22] !== t6 ? (t7 = /* @__PURE__ */ jsxs(Fragment, { children: [t3, t6] }), $[21] = t3, $[22] = t6, $[23] = t7) : t7 = $[23], t7;
2700
+ }), $[28] = t7, $[29] = t8, $[30] = t9, $[31] = t10) : t10 = $[31];
2701
+ let t11;
2702
+ $[32] === pasting ? t11 = $[33] : (t11 = pasting && /* @__PURE__ */ jsx(PasteThemeDialog, { onClose: () => setPasting(!1) }), $[32] = pasting, $[33] = t11);
2703
+ let t12;
2704
+ return $[34] !== t10 || $[35] !== t11 || $[36] !== t6 ? (t12 = /* @__PURE__ */ jsxs(Fragment, { children: [
2705
+ t6,
2706
+ t10,
2707
+ t11
2708
+ ] }), $[34] = t10, $[35] = t11, $[36] = t6, $[37] = t12) : t12 = $[37], t12;
2709
+ }
2710
+ function _temp$1(theme) {
2711
+ return theme.slug;
1977
2712
  }
1978
2713
  /**
1979
2714
  * Serializes theme options into the `buildTheme` call to paste into
@@ -2032,9 +2767,11 @@ function ThemeSnippetDialog(props) {
2032
2767
  justify: "flex-end",
2033
2768
  padding: 3,
2034
2769
  children: /* @__PURE__ */ jsx(Button, {
2770
+ gap: 2,
2035
2771
  icon: ClipboardIcon,
2036
2772
  mode: "ghost",
2037
2773
  onClick: () => void handleCopy(),
2774
+ padding: 2,
2038
2775
  text: "Copy"
2039
2776
  })
2040
2777
  }), $[5] = handleCopy, $[6] = t2);
@@ -2096,6 +2833,7 @@ function ThemeSnippetDialog(props) {
2096
2833
  children: t9
2097
2834
  }), $[19] = onClose, $[20] = t2, $[21] = t9, $[22] = t10) : t10 = $[22], t10;
2098
2835
  }
2836
+ var title = "c2pqej0";
2099
2837
  const VIEW_TITLES = {
2100
2838
  list: "Themer",
2101
2839
  edit: "Edit theme",
@@ -2110,32 +2848,29 @@ const VIEW_TITLES = {
2110
2848
  * @internal
2111
2849
  */
2112
2850
  function ThemerSidebar() {
2113
- let $ = c(41), { active, split, view, send } = useThemer(), [snippetOpen, setSnippetOpen] = useState(!1), inList = view.name === "list", t0;
2114
- $[0] !== inList || $[1] !== send ? (t0 = !inList && /* @__PURE__ */ jsx(TooltipButton, {
2851
+ let $ = c(42), { active, split, view, navbarHeight, send } = useThemer(), [snippetOpen, setSnippetOpen] = useState(!1), inList = view.name === "list", t0;
2852
+ $[0] === navbarHeight ? t0 = $[1] : (t0 = navbarHeight === null ? void 0 : { height: navbarHeight }, $[0] = navbarHeight, $[1] = t0);
2853
+ let t1;
2854
+ $[2] !== inList || $[3] !== send ? (t1 = !inList && /* @__PURE__ */ jsx(TooltipButton, {
2115
2855
  icon: ArrowLeftIcon,
2116
2856
  mode: "bleed",
2117
2857
  onClick: () => send({ type: "flow.list" }),
2118
2858
  padding: 2,
2119
2859
  tooltip: "Back to the themes"
2120
- }), $[0] = inList, $[1] = send, $[2] = t0) : t0 = $[2];
2121
- let t1 = +!!inList, t2;
2122
- $[3] === Symbol.for("react.memo_cache_sentinel") ? (t2 = { minWidth: 0 }, $[3] = t2) : t2 = $[3];
2123
- let t3 = VIEW_TITLES[view.name], t4;
2124
- $[4] === t3 ? t4 = $[5] : (t4 = /* @__PURE__ */ jsx(Text, {
2125
- size: 1,
2126
- textOverflow: "ellipsis",
2127
- weight: "semibold",
2128
- children: t3
2129
- }), $[4] = t3, $[5] = t4);
2130
- let t5;
2131
- $[6] !== t1 || $[7] !== t4 ? (t5 = /* @__PURE__ */ jsx(Box, {
2860
+ }), $[2] = inList, $[3] = send, $[4] = t1) : t1 = $[4];
2861
+ let t2 = VIEW_TITLES[view.name], t3;
2862
+ $[5] === t2 ? t3 = $[6] : (t3 = /* @__PURE__ */ jsx(Box, {
2863
+ className: title,
2132
2864
  flex: 1,
2133
- paddingLeft: t1,
2134
- style: t2,
2135
- children: t4
2136
- }), $[6] = t1, $[7] = t4, $[8] = t5) : t5 = $[8];
2137
- let t6;
2138
- $[9] !== send || $[10] !== split || $[11] !== view.name ? (t6 = view.name !== "removed" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TooltipButton, {
2865
+ children: /* @__PURE__ */ jsx(Text, {
2866
+ size: 1,
2867
+ textOverflow: "ellipsis",
2868
+ weight: "semibold",
2869
+ children: t2
2870
+ })
2871
+ }), $[5] = t2, $[6] = t3);
2872
+ let t4;
2873
+ $[7] !== send || $[8] !== split || $[9] !== view.name ? (t4 = view.name !== "removed" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TooltipButton, {
2139
2874
  "aria-pressed": split,
2140
2875
  icon: SplitVerticalIcon,
2141
2876
  mode: "bleed",
@@ -2149,66 +2884,69 @@ function ThemerSidebar() {
2149
2884
  onClick: () => setSnippetOpen(!0),
2150
2885
  padding: 2,
2151
2886
  tooltip: "Show the code for the applied theme"
2152
- })] }), $[9] = send, $[10] = split, $[11] = view.name, $[12] = t6) : t6 = $[12];
2153
- let t7;
2154
- $[13] === send ? t7 = $[14] : (t7 = /* @__PURE__ */ jsx(TooltipButton, {
2887
+ })] }), $[7] = send, $[8] = split, $[9] = view.name, $[10] = t4) : t4 = $[10];
2888
+ let t5;
2889
+ $[11] === send ? t5 = $[12] : (t5 = /* @__PURE__ */ jsx(TooltipButton, {
2155
2890
  icon: CloseIcon,
2156
2891
  mode: "bleed",
2157
2892
  onClick: () => send({ type: "sidebar.close" }),
2158
2893
  padding: 2,
2159
2894
  tooltip: "Close themer"
2160
- }), $[13] = send, $[14] = t7);
2895
+ }), $[11] = send, $[12] = t5);
2896
+ let t6;
2897
+ $[13] !== t4 || $[14] !== t5 ? (t6 = /* @__PURE__ */ jsxs(Flex, { children: [t4, t5] }), $[13] = t4, $[14] = t5, $[15] = t6) : t6 = $[15];
2898
+ let t7;
2899
+ $[16] !== t1 || $[17] !== t3 || $[18] !== t6 ? (t7 = /* @__PURE__ */ jsxs(Flex, {
2900
+ align: "center",
2901
+ gap: 1,
2902
+ height: "fill",
2903
+ children: [
2904
+ t1,
2905
+ t3,
2906
+ t6
2907
+ ]
2908
+ }), $[16] = t1, $[17] = t3, $[18] = t6, $[19] = t7) : t7 = $[19];
2161
2909
  let t8;
2162
- $[15] !== t6 || $[16] !== t7 ? (t8 = /* @__PURE__ */ jsxs(Flex, { children: [t6, t7] }), $[15] = t6, $[16] = t7, $[17] = t8) : t8 = $[17];
2910
+ $[20] !== t0 || $[21] !== t7 ? (t8 = /* @__PURE__ */ jsx(Card, {
2911
+ borderBottom: !0,
2912
+ padding: 3,
2913
+ sizing: "border",
2914
+ style: t0,
2915
+ children: t7
2916
+ }), $[20] = t0, $[21] = t7, $[22] = t8) : t8 = $[22];
2163
2917
  let t9;
2164
- $[18] !== t0 || $[19] !== t5 || $[20] !== t8 ? (t9 = /* @__PURE__ */ jsx(Card, {
2165
- padding: 2,
2166
- children: /* @__PURE__ */ jsxs(Flex, {
2167
- align: "center",
2168
- gap: 1,
2169
- children: [
2170
- t0,
2171
- t5,
2172
- t8
2173
- ]
2174
- })
2175
- }), $[18] = t0, $[19] = t5, $[20] = t8, $[21] = t9) : t9 = $[21];
2918
+ $[23] === view.name ? t9 = $[24] : (t9 = view.name === "list" && /* @__PURE__ */ jsx(ThemeList, {}), $[23] = view.name, $[24] = t9);
2176
2919
  let t10;
2177
- $[22] === view.name ? t10 = $[23] : (t10 = view.name === "list" && /* @__PURE__ */ jsx(ThemeList, {}), $[22] = view.name, $[23] = t10);
2178
- let t11;
2179
- $[24] !== view.focusTitle || $[25] !== view.name || $[26] !== view.slug ? (t11 = view.name === "edit" && /* @__PURE__ */ jsx(ThemeEditor, {
2920
+ $[25] !== view.focusTitle || $[26] !== view.name || $[27] !== view.slug ? (t10 = view.name === "edit" && /* @__PURE__ */ jsx(ThemeEditor, {
2180
2921
  focusTitle: view.focusTitle,
2181
2922
  slug: view.slug
2182
- }), $[24] = view.focusTitle, $[25] = view.name, $[26] = view.slug, $[27] = t11) : t11 = $[27];
2923
+ }), $[25] = view.focusTitle, $[26] = view.name, $[27] = view.slug, $[28] = t10) : t10 = $[28];
2924
+ let t11;
2925
+ $[29] === view.name ? t11 = $[30] : (t11 = view.name === "removed" && /* @__PURE__ */ jsx(RemovedThemes, {}), $[29] = view.name, $[30] = t11);
2183
2926
  let t12;
2184
- $[28] === view.name ? t12 = $[29] : (t12 = view.name === "removed" && /* @__PURE__ */ jsx(RemovedThemes, {}), $[28] = view.name, $[29] = t12);
2185
- let t13;
2186
- $[30] !== t10 || $[31] !== t11 || $[32] !== t12 || $[33] !== t9 ? (t13 = /* @__PURE__ */ jsxs(Flex, {
2927
+ $[31] !== t10 || $[32] !== t11 || $[33] !== t8 || $[34] !== t9 ? (t12 = /* @__PURE__ */ jsxs(Flex, {
2187
2928
  direction: "column",
2188
2929
  height: "fill",
2189
2930
  children: [
2931
+ t8,
2190
2932
  t9,
2191
2933
  t10,
2192
- t11,
2193
- t12
2934
+ t11
2194
2935
  ]
2195
- }), $[30] = t10, $[31] = t11, $[32] = t12, $[33] = t9, $[34] = t13) : t13 = $[34];
2196
- let t14;
2197
- $[35] !== active || $[36] !== snippetOpen ? (t14 = snippetOpen && /* @__PURE__ */ jsx(ThemeSnippetDialog, {
2936
+ }), $[31] = t10, $[32] = t11, $[33] = t8, $[34] = t9, $[35] = t12) : t12 = $[35];
2937
+ let t13;
2938
+ $[36] !== active || $[37] !== snippetOpen ? (t13 = snippetOpen && /* @__PURE__ */ jsx(ThemeSnippetDialog, {
2198
2939
  onClose: () => setSnippetOpen(!1),
2199
2940
  theme: active
2200
- }), $[35] = active, $[36] = snippetOpen, $[37] = t14) : t14 = $[37];
2201
- let t15;
2202
- return $[38] !== t13 || $[39] !== t14 ? (t15 = /* @__PURE__ */ jsxs(Card, {
2941
+ }), $[36] = active, $[37] = snippetOpen, $[38] = t13) : t13 = $[38];
2942
+ let t14;
2943
+ return $[39] !== t12 || $[40] !== t13 ? (t14 = /* @__PURE__ */ jsxs(Card, {
2203
2944
  height: "fill",
2204
- children: [t13, t14]
2205
- }), $[38] = t13, $[39] = t14, $[40] = t15) : t15 = $[40], t15;
2945
+ children: [t12, t13]
2946
+ }), $[39] = t12, $[40] = t13, $[41] = t14) : t14 = $[41], t14;
2206
2947
  }
2207
- /**
2208
- * The narrowest the sidebar goes — and its default: narrow enough to leave the
2209
- * studio preview as much room as possible, while the themes stack in a single
2210
- * column of preview cards and the editor fits a swatch next to its label.
2211
- */
2948
+ var content = "bgn4np4", frame = "bgn4np3", sidebar = "bgn4np2";
2949
+ /** How far one arrow key press resizes the sidebar */
2212
2950
  const WIDTH_STORAGE_KEY = "sanityStudio:themer:width";
2213
2951
  function clampWidth(width) {
2214
2952
  return Math.min(400, Math.max(200, Math.round(width)));
@@ -2226,25 +2964,16 @@ function writeStoredWidth(width) {
2226
2964
  width === 200 ? localStorage.removeItem(WIDTH_STORAGE_KEY) : localStorage.setItem(WIDTH_STORAGE_KEY, String(width));
2227
2965
  } catch {}
2228
2966
  }
2229
- const Sidebar = styled(Layer).withConfig({
2230
- displayName: "Sidebar",
2231
- componentId: "sc-ryrt6y-0"
2232
- })`flex:none;`, Frame = styled(Card).withConfig({
2233
- displayName: "Frame",
2234
- componentId: "sc-ryrt6y-1"
2235
- })`position:relative;`, ResizeHandle = styled.div.withConfig({
2236
- displayName: "ResizeHandle",
2237
- componentId: "sc-ryrt6y-2"
2238
- })`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;}`;
2239
2967
  /**
2240
2968
  * The themer sidebar, in a layer along the right edge of the Studio. Its left
2241
2969
  * edge drags (or arrow-keys) it wider, up to twice its default width — the
2242
- * width sticks between sessions.
2970
+ * width sticks between sessions. As an overlay (small screens) it covers the
2971
+ * Studio edge to edge instead, with nothing to resize.
2243
2972
  *
2244
2973
  * @internal
2245
2974
  */
2246
- function ResizableSidebar() {
2247
- let $ = c(21), [width, setWidth] = useState(readStoredWidth), [dragging, setDragging] = useState(!1), drag = useRef(null), t0, t1;
2975
+ function ResizableSidebar(props) {
2976
+ let $ = c(24), { overlay } = props, [width, setWidth] = useState(readStoredWidth), [dragging, setDragging] = useState(!1), drag = useRef(null), t0, t1;
2248
2977
  $[0] === width ? (t0 = $[1], t1 = $[2]) : (t0 = () => writeStoredWidth(width), t1 = [width], $[0] = width, $[1] = t0, $[2] = t1), useEffect(t0, t1);
2249
2978
  let t2;
2250
2979
  $[3] === width ? t2 = $[4] : (t2 = (event) => {
@@ -2273,18 +3002,17 @@ function ResizableSidebar() {
2273
3002
  step !== void 0 && (event_2.preventDefault(), setWidth((current) => clampWidth(current + step)));
2274
3003
  }, $[7] = t5) : t5 = $[7];
2275
3004
  let handleKeyDown = t5, t6;
2276
- $[8] === width ? t6 = $[9] : (t6 = { width }, $[8] = width, $[9] = t6);
2277
- let t7;
2278
- $[10] === Symbol.for("react.memo_cache_sentinel") ? (t7 = () => setWidth(200), $[10] = t7) : t7 = $[10];
2279
- let t8;
2280
- $[11] !== dragging || $[12] !== handlePointerDown || $[13] !== width ? (t8 = /* @__PURE__ */ jsx(ResizeHandle, {
3005
+ $[8] !== overlay || $[9] !== width ? (t6 = overlay ? void 0 : { width }, $[8] = overlay, $[9] = width, $[10] = t6) : t6 = $[10];
3006
+ let t7 = !overlay, t8;
3007
+ $[11] !== dragging || $[12] !== handlePointerDown || $[13] !== overlay || $[14] !== width ? (t8 = !overlay && /* @__PURE__ */ jsx("div", {
2281
3008
  "aria-label": "Resize the themer",
3009
+ className: "bgn4np5",
2282
3010
  "aria-orientation": "vertical",
2283
3011
  "aria-valuemax": 400,
2284
3012
  "aria-valuemin": 200,
2285
3013
  "aria-valuenow": width,
2286
3014
  "data-dragging": dragging,
2287
- onDoubleClick: t7,
3015
+ onDoubleClick: () => setWidth(200),
2288
3016
  onKeyDown: handleKeyDown,
2289
3017
  onPointerCancel: handlePointerUp,
2290
3018
  onPointerDown: handlePointerDown,
@@ -2292,148 +3020,94 @@ function ResizableSidebar() {
2292
3020
  onPointerUp: handlePointerUp,
2293
3021
  role: "separator",
2294
3022
  tabIndex: 0
2295
- }), $[11] = dragging, $[12] = handlePointerDown, $[13] = width, $[14] = t8) : t8 = $[14];
3023
+ }), $[11] = dragging, $[12] = handlePointerDown, $[13] = overlay, $[14] = width, $[15] = t8) : t8 = $[15];
2296
3024
  let t9;
2297
- $[15] === Symbol.for("react.memo_cache_sentinel") ? (t9 = /* @__PURE__ */ jsx(Box, {
3025
+ $[16] === Symbol.for("react.memo_cache_sentinel") ? (t9 = /* @__PURE__ */ jsx(Box, {
3026
+ className: content,
2298
3027
  height: "fill",
2299
3028
  overflow: "hidden",
2300
3029
  children: /* @__PURE__ */ jsx(ThemerSidebar, {})
2301
- }), $[15] = t9) : t9 = $[15];
3030
+ }), $[16] = t9) : t9 = $[16];
2302
3031
  let t10;
2303
- $[16] === t8 ? t10 = $[17] : (t10 = /* @__PURE__ */ jsxs(Frame, {
2304
- borderLeft: !0,
3032
+ $[17] !== t7 || $[18] !== t8 ? (t10 = /* @__PURE__ */ jsxs(Card, {
3033
+ borderLeft: t7,
3034
+ className: frame,
2305
3035
  height: "fill",
2306
3036
  children: [t8, t9]
2307
- }), $[16] = t8, $[17] = t10);
3037
+ }), $[17] = t7, $[18] = t8, $[19] = t10) : t10 = $[19];
2308
3038
  let t11;
2309
- return $[18] !== t10 || $[19] !== t6 ? (t11 = /* @__PURE__ */ jsx(Sidebar, {
3039
+ return $[20] !== overlay || $[21] !== t10 || $[22] !== t6 ? (t11 = /* @__PURE__ */ jsx(Layer, {
3040
+ className: sidebar,
3041
+ "data-overlay": overlay,
2310
3042
  style: t6,
2311
3043
  zOffset: 100,
2312
3044
  children: t10
2313
- }), $[18] = t10, $[19] = t6, $[20] = t11) : t11 = $[20], t11;
3045
+ }), $[20] = overlay, $[21] = t10, $[22] = t6, $[23] = t11) : t11 = $[23], t11;
2314
3046
  }
2315
- const STORAGE_KEY = "sanityStudio:themer:state", LEGACY_STORAGE_KEY = "sanityStudio:themer:options";
2316
- function sanitizeColor(value) {
2317
- return typeof value == "string" && isColor(value) ? value.toLowerCase() : null;
3047
+ const QUERY = "(prefers-reduced-motion: reduce)";
3048
+ function subscribe(onChange) {
3049
+ let media = matchMedia(QUERY);
3050
+ return media.addEventListener("change", onChange), () => media.removeEventListener("change", onChange);
2318
3051
  }
2319
- function sanitizeContrast(value) {
2320
- return typeof value == "number" && Number.isFinite(value) ? Math.min(100, Math.max(15, value)) : null;
3052
+ function getSnapshot() {
3053
+ return matchMedia(QUERY).matches;
2321
3054
  }
2322
- function sanitizeSchemeOptions(value) {
2323
- let options = {};
2324
- if (!value || typeof value != "object") return options;
2325
- 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"));
2326
- return accent && (options.accent = accent), text && (options.text = text), background && (options.background = background), contrast !== null && (options.contrast = contrast), options;
3055
+ function getServerSnapshot() {
3056
+ return !1;
2327
3057
  }
2328
3058
  /**
2329
- * Options from before they were grouped by scheme: one accent, text color and
2330
- * contrast for both schemes, and a background per scheme. Both schemes get
2331
- * the shared colors, so the theme keeps looking the same.
3059
+ * Whether the user prefers reduced motion — live, so that turning the
3060
+ * setting on while the Studio is open takes effect right away.
3061
+ *
3062
+ * @internal
2332
3063
  */
2333
- function sanitizeFlatOptions(value) {
2334
- let accent = sanitizeColor(Reflect.get(value, "accent"));
2335
- if (!accent) return null;
2336
- let shared = { accent }, text = sanitizeColor(Reflect.get(value, "text")), contrast = sanitizeContrast(Reflect.get(value, "contrast"));
2337
- text && (shared.text = text), contrast !== null && (shared.contrast = contrast);
2338
- let backgrounds = Reflect.get(value, "background"), options = {};
2339
- for (let scheme of SCHEMES) {
2340
- let background = backgrounds && typeof backgrounds == "object" ? sanitizeColor(Reflect.get(backgrounds, scheme)) : null;
2341
- options[scheme] = background ? {
2342
- ...shared,
2343
- background
2344
- } : { ...shared };
2345
- }
2346
- return options;
2347
- }
2348
- function sanitizeOptions(value) {
2349
- if (!value || typeof value != "object") return null;
2350
- if ("accent" in value) return sanitizeFlatOptions(value);
2351
- let options = {};
2352
- for (let scheme of SCHEMES) {
2353
- let schemeOptions = sanitizeSchemeOptions(Reflect.get(value, scheme));
2354
- Object.keys(schemeOptions).length > 0 && (options[scheme] = schemeOptions);
2355
- }
2356
- return options;
2357
- }
2358
- function isReservedSlug(slug) {
2359
- return slug === "config" || presets.some((preset) => preset.slug === slug);
2360
- }
2361
- function sanitizeCustomTheme(value) {
2362
- if (!value || typeof value != "object") return null;
2363
- let slug = Reflect.get(value, "slug");
2364
- if (typeof slug != "string" || !slug || isReservedSlug(slug)) return null;
2365
- let options = sanitizeOptions(Reflect.get(value, "options"));
2366
- if (!options) return null;
2367
- let title = Reflect.get(value, "title"), palette = sanitizePalette(Reflect.get(value, "palette"));
2368
- return {
2369
- slug,
2370
- title: displayTitle(typeof title == "string" ? title : ""),
2371
- options,
2372
- ...palette ? { palette } : {}
2373
- };
2374
- }
2375
- function sanitizePalette(value) {
2376
- if (!value || typeof value != "object") return null;
2377
- let palette = {}, swatches = 0;
2378
- for (let key of IMAGE_PALETTE_KEYS) {
2379
- let swatch = sanitizeColor(Reflect.get(value, key));
2380
- palette[key] = swatch, swatch && swatches++;
2381
- }
2382
- return swatches > 0 ? palette : null;
2383
- }
2384
- function sanitizeState(value) {
2385
- if (!value || typeof value != "object") return initialThemerState;
2386
- let custom = [], rawCustom = Reflect.get(value, "custom");
2387
- if (Array.isArray(rawCustom)) for (let item of rawCustom) {
2388
- let theme = sanitizeCustomTheme(item);
2389
- theme && !custom.some((existing) => existing.slug === theme.slug) && custom.push(theme);
2390
- }
2391
- let removed = [], rawRemoved = Reflect.get(value, "removed");
2392
- 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);
2393
- let rawActive = Reflect.get(value, "active");
2394
- return {
2395
- active: typeof rawActive == "string" ? rawActive : null,
2396
- custom,
2397
- removed
2398
- };
2399
- }
2400
- function readLegacyState() {
2401
- let raw = localStorage.getItem(LEGACY_STORAGE_KEY);
2402
- if (!raw) return null;
2403
- let options = sanitizeOptions(JSON.parse(raw));
2404
- if (!options) return null;
2405
- let theme = createCustomTheme("Draft theme", options);
2406
- return {
2407
- active: theme.slug,
2408
- custom: [theme],
2409
- removed: []
2410
- };
3064
+ function usePrefersReducedMotion() {
3065
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
2411
3066
  }
2412
3067
  /**
2413
- * Restores the themer state from localStorage, so the user's themes survive
2414
- * studio reloads. Falls back to the initial state when nothing usable is
2415
- * stored.
3068
+ * The height of the Studio navbar inside the given element, in pixels — kept
3069
+ * up to date as the navbar renders, remounts and resizes, and `null` until
3070
+ * there is one. The themer's header takes this height, so that its bottom
3071
+ * border lines up with the navbar's whatever height the Studio version, its
3072
+ * breakpoint or a custom navbar gives it.
2416
3073
  *
2417
3074
  * @internal
2418
3075
  */
2419
- function readStoredState() {
2420
- try {
2421
- if (typeof localStorage > "u") return initialThemerState;
2422
- let raw = localStorage.getItem(STORAGE_KEY);
2423
- return raw ? sanitizeState(JSON.parse(raw)) : readLegacyState() ?? initialThemerState;
2424
- } catch {
2425
- return initialThemerState;
2426
- }
2427
- }
2428
- /** @internal */
2429
- function writeStoredState(state) {
2430
- try {
2431
- if (typeof localStorage > "u") return;
2432
- localStorage.setItem(STORAGE_KEY, JSON.stringify(state)), localStorage.removeItem(LEGACY_STORAGE_KEY);
2433
- } catch {}
2434
- }
3076
+ function useStudioNavbarHeight(ref) {
3077
+ let $ = c(3), [height, setHeight] = useState(null), t0, t1;
3078
+ return $[0] === ref ? (t0 = $[1], t1 = $[2]) : (t0 = () => {
3079
+ let root = ref.current;
3080
+ if (!root) return;
3081
+ let navbar = null, resizeObserver = new ResizeObserver(() => {
3082
+ navbar && setHeight(navbar.getBoundingClientRect().height);
3083
+ }), observeNavbar = () => {
3084
+ let next = root.querySelector("[data-testid=\"studio-navbar\"]");
3085
+ next !== navbar && (navbar && resizeObserver.unobserve(navbar), navbar = next, navbar ? (resizeObserver.observe(navbar), setHeight(navbar.getBoundingClientRect().height)) : setHeight(null));
3086
+ }, mutationObserver = new MutationObserver(observeNavbar);
3087
+ return mutationObserver.observe(root, {
3088
+ childList: !0,
3089
+ subtree: !0
3090
+ }), observeNavbar(), () => {
3091
+ mutationObserver.disconnect(), resizeObserver.disconnect();
3092
+ };
3093
+ }, t1 = [ref], $[0] = ref, $[1] = t0, $[2] = t1), useEffect(t0, t1), height;
3094
+ }
3095
+ var layout = "_1yq4ar78", panelTransitionClasses = {
3096
+ slideIn: "_1yq4ar76",
3097
+ slideOut: "_1yq4ar77"
3098
+ }, splitTransitionClasses = {
3099
+ resize: "_1yq4ar70",
3100
+ crossfade: "_1yq4ar71",
3101
+ slideIn: "_1yq4ar72",
3102
+ slideOut: "_1yq4ar73",
3103
+ dropIn: "_1yq4ar74",
3104
+ dropOut: "_1yq4ar75"
3105
+ }, studioScheme = {
3106
+ light: "_1yq4ar79",
3107
+ dark: "_1yq4ar7a"
3108
+ };
2435
3109
  function sameStoredState(a, b) {
2436
- return a.active === b.active && a.custom === b.custom && a.removed === b.removed;
3110
+ return a.active === b.active && a.custom === b.custom && a.removed === b.removed && a.order === b.order;
2437
3111
  }
2438
3112
  function selectView(snapshot) {
2439
3113
  let { editing } = snapshot.context;
@@ -2455,13 +3129,21 @@ function sameView(a, b) {
2455
3129
  * `layout` level rather than in `activeToolLayout` because the split preview
2456
3130
  * renders the Studio twice, and the sidebar must not come along.
2457
3131
  *
2458
- * The single preview inherits the color scheme from the Studio, so it follows
2459
- * the appearance setting (light/dark/system) like any other theme.
3132
+ * The Studio next to the sidebar always follows the appearance setting
3133
+ * (light/dark/system) and the picked theme like any other theme would. The
3134
+ * split preview adds a second copy in the opposite scheme on the far side —
3135
+ * or on top, on small screens — through React's view transitions (React
3136
+ * 19.3), styled in `ThemerLayout.css.ts`.
3137
+ *
3138
+ * The machine says what shows and when the layout is in motion (its `panel`,
3139
+ * `split` and `moving` tags); the layout defers what shows, which puts the
3140
+ * panel's and the copy's mounts in a transition — what lets React animate
3141
+ * them — and picks the view transition classes from the tags.
2460
3142
  *
2461
3143
  * @internal
2462
3144
  */
2463
3145
  function ThemerLayout(props) {
2464
- let $ = c(55), { baseOptions, ...layoutProps } = props, t0;
3146
+ let $ = c(85), { baseOptions, ...layoutProps } = props, t0;
2465
3147
  $[0] === baseOptions ? t0 = $[1] : (t0 = () => ({
2466
3148
  baseOptions,
2467
3149
  stored: readStoredState()
@@ -2469,38 +3151,34 @@ function ThemerLayout(props) {
2469
3151
  let [input] = useState(t0), t1;
2470
3152
  $[2] === input ? t1 = $[3] : (t1 = { input }, $[2] = input, $[3] = t1);
2471
3153
  let [snapshot, send, actorRef] = useActor(themerMachine, t1), stored = useSelector(actorRef, selectStoredState, sameStoredState), view = useSelector(actorRef, selectView, sameView), t2;
2472
- $[4] === snapshot ? t2 = $[5] : (t2 = snapshot.matches({ sidebar: "open" }), $[4] = snapshot, $[5] = t2);
3154
+ $[4] === snapshot ? t2 = $[5] : (t2 = snapshot.hasTag("panel"), $[4] = snapshot, $[5] = t2);
2473
3155
  let open = t2, t3;
2474
- $[6] === snapshot ? t3 = $[7] : (t3 = snapshot.matches({ preview: "split" }), $[6] = snapshot, $[7] = t3);
2475
- let split = t3, { images } = snapshot.context, t4, t5;
2476
- $[8] === stored ? (t4 = $[9], t5 = $[10]) : (t4 = () => writeStoredState(stored), t5 = [stored], $[8] = stored, $[9] = t4, $[10] = t5), useEffect(t4, t5);
2477
- let previousImages = useRef(images), t6, t7;
2478
- $[11] === images ? (t6 = $[12], t7 = $[13]) : (t6 = () => {
2479
- let current = new Set(Object.values(images));
2480
- for (let url of Object.values(previousImages.current)) current.has(url) || URL.revokeObjectURL(url);
2481
- previousImages.current = images;
2482
- }, t7 = [images], $[11] = images, $[12] = t6, $[13] = t7), useEffect(t6, t7);
2483
- let t8, t9;
2484
- $[14] === Symbol.for("react.memo_cache_sentinel") ? (t8 = () => () => {
2485
- for (let url_0 of Object.values(previousImages.current)) URL.revokeObjectURL(url_0);
2486
- }, t9 = [], $[14] = t8, $[15] = t9) : (t8 = $[14], t9 = $[15]), useEffect(t8, t9);
2487
- let t10;
2488
- $[16] !== baseOptions || $[17] !== stored ? (t10 = resolveThemes(stored, baseOptions), $[16] = baseOptions, $[17] = stored, $[18] = t10) : t10 = $[18];
2489
- let { themes, removed, active } = t10, activeOptions = active.source === "config" ? null : active.options, t11;
2490
- $[19] === activeOptions ? t11 = $[20] : (t11 = activeOptions === null ? null : buildTheme(activeOptions), $[19] = activeOptions, $[20] = t11);
2491
- let theme = t11, scheme = useColorSchemeValue(), t12;
2492
- $[21] !== scheme || $[22] !== theme?.v2?.color ? (t12 = () => {
2493
- let background = theme?.v2?.color[scheme].default.bg;
3156
+ $[6] === snapshot ? t3 = $[7] : (t3 = snapshot.hasTag("split"), $[6] = snapshot, $[7] = t3);
3157
+ let split = t3, t4;
3158
+ $[8] === snapshot ? t4 = $[9] : (t4 = snapshot.hasTag("moving"), $[8] = snapshot, $[9] = t4);
3159
+ let moving = t4, t5;
3160
+ $[10] === snapshot ? t5 = $[11] : (t5 = snapshot.hasTag("switching"), $[10] = snapshot, $[11] = t5);
3161
+ let switching = t5, { images } = snapshot.context, mobile = useMediaIndex() <= 1, studioRef = useRef(null), navbarHeight = useStudioNavbarHeight(studioRef), reduceMotion = usePrefersReducedMotion(), deferredOpen = useDeferredValue(reduceMotion ? null : open), deferredSplit = useDeferredValue(reduceMotion ? null : split), shownOpen = deferredOpen ?? open, shownSplit = deferredSplit ?? split, t6;
3162
+ $[12] !== baseOptions || $[13] !== stored ? (t6 = resolveThemes(stored, baseOptions), $[12] = baseOptions, $[13] = stored, $[14] = t6) : t6 = $[14];
3163
+ let { themes, removed, active } = t6, activeOptions = active.source === "config" ? null : active.options, t7;
3164
+ $[15] === activeOptions ? t7 = $[16] : (t7 = activeOptions === null ? null : buildTheme(activeOptions), $[15] = activeOptions, $[16] = t7);
3165
+ let theme = t7, deferredTheme = useDeferredValue(reduceMotion ? void 0 : theme), shownTheme = deferredTheme === void 0 ? theme : deferredTheme, scheme = useColorSchemeValue(), oppositeScheme = scheme === "dark" ? "light" : "dark", t8;
3166
+ $[17] !== scheme || $[18] !== shownTheme?.v2?.color ? (t8 = () => {
3167
+ let background = shownTheme?.v2?.color[scheme].default.bg;
2494
3168
  if (background === void 0) return;
2495
3169
  let { style } = document.body, previous = style.backgroundColor;
2496
3170
  return style.backgroundColor = background, () => {
2497
3171
  style.backgroundColor = previous;
2498
3172
  };
2499
- }, $[21] = scheme, $[22] = theme?.v2?.color, $[23] = t12) : t12 = $[23];
2500
- let t13;
2501
- $[24] !== scheme || $[25] !== theme ? (t13 = [scheme, theme], $[24] = scheme, $[25] = theme, $[26] = t13) : t13 = $[26], useEffect(t12, t13);
2502
- let t14;
2503
- $[27] !== active || $[28] !== baseOptions || $[29] !== images || $[30] !== open || $[31] !== removed || $[32] !== send || $[33] !== split || $[34] !== themes || $[35] !== view ? (t14 = {
3173
+ }, $[17] = scheme, $[18] = shownTheme?.v2?.color, $[19] = t8) : t8 = $[19];
3174
+ let t9;
3175
+ $[20] !== scheme || $[21] !== shownTheme ? (t9 = [scheme, shownTheme], $[20] = scheme, $[21] = shownTheme, $[22] = t9) : t9 = $[22], useEffect(t8, t9);
3176
+ let t10, t11;
3177
+ $[23] === actorRef ? (t10 = $[24], t11 = $[25]) : (t10 = () => () => {
3178
+ for (let url of Object.values(actorRef.getSnapshot().context.images)) URL.revokeObjectURL(url);
3179
+ }, t11 = [actorRef], $[23] = actorRef, $[24] = t10, $[25] = t11), useEffect(t10, t11);
3180
+ let t12;
3181
+ $[26] !== active || $[27] !== baseOptions || $[28] !== images || $[29] !== mobile || $[30] !== navbarHeight || $[31] !== open || $[32] !== removed || $[33] !== send || $[34] !== split || $[35] !== themes || $[36] !== view ? (t12 = {
2504
3182
  baseOptions,
2505
3183
  themes,
2506
3184
  removed,
@@ -2509,41 +3187,98 @@ function ThemerLayout(props) {
2509
3187
  view,
2510
3188
  open,
2511
3189
  split,
3190
+ mobile,
3191
+ navbarHeight,
2512
3192
  send
2513
- }, $[27] = active, $[28] = baseOptions, $[29] = images, $[30] = open, $[31] = removed, $[32] = send, $[33] = split, $[34] = themes, $[35] = view, $[36] = t14) : t14 = $[36];
2514
- let context = t14, studio = layoutProps.renderDefault(layoutProps), t15 = split ? "light" : void 0, t16;
2515
- $[37] !== studio || $[38] !== t15 || $[39] !== theme ? (t16 = /* @__PURE__ */ jsx(StudioPreview, {
2516
- scheme: t15,
2517
- theme,
2518
- children: studio
2519
- }), $[37] = studio, $[38] = t15, $[39] = theme, $[40] = t16) : t16 = $[40];
3193
+ }, $[26] = active, $[27] = baseOptions, $[28] = images, $[29] = mobile, $[30] = navbarHeight, $[31] = open, $[32] = removed, $[33] = send, $[34] = split, $[35] = themes, $[36] = view, $[37] = t12) : t12 = $[37];
3194
+ let context = t12, studio = layoutProps.renderDefault(layoutProps), t13;
3195
+ $[38] === send ? t13 = $[39] : (t13 = () => send({ type: "layout.transitioned" }), $[38] = send, $[39] = t13);
3196
+ let transitioned = t13, t14;
3197
+ $[40] !== mobile || $[41] !== moving || $[42] !== reduceMotion || $[43] !== switching ? (t14 = reduceMotion ? {
3198
+ copyEnter: "none",
3199
+ copyExit: "none",
3200
+ panelEnter: "none",
3201
+ panelExit: "none",
3202
+ update: "none",
3203
+ studioUpdate: "none"
3204
+ } : {
3205
+ copyEnter: mobile ? splitTransitionClasses.dropIn : splitTransitionClasses.slideIn,
3206
+ copyExit: mobile ? splitTransitionClasses.dropOut : splitTransitionClasses.slideOut,
3207
+ panelEnter: panelTransitionClasses.slideIn,
3208
+ panelExit: panelTransitionClasses.slideOut,
3209
+ update: switching ? splitTransitionClasses.crossfade : "none",
3210
+ studioUpdate: moving ? splitTransitionClasses.resize : switching ? splitTransitionClasses.crossfade : "none"
3211
+ }, $[40] = mobile, $[41] = moving, $[42] = reduceMotion, $[43] = switching, $[44] = t14) : t14 = $[44];
3212
+ let classes = t14, t15 = mobile ? "column" : "row", t16;
3213
+ $[45] !== classes.copyEnter || $[46] !== classes.copyExit || $[47] !== classes.update || $[48] !== mobile || $[49] !== oppositeScheme || $[50] !== shownOpen || $[51] !== shownSplit || $[52] !== shownTheme || $[53] !== studio || $[54] !== transitioned ? (t16 = shownOpen && /* @__PURE__ */ jsx(Activity, {
3214
+ mode: shownSplit ? "visible" : "hidden",
3215
+ children: /* @__PURE__ */ jsx(ViewTransition, {
3216
+ enter: classes.copyEnter,
3217
+ exit: classes.copyExit,
3218
+ onEnter: transitioned,
3219
+ onExit: transitioned,
3220
+ onUpdate: transitioned,
3221
+ update: classes.update,
3222
+ children: /* @__PURE__ */ jsx(StudioPreview, {
3223
+ borderBottom: mobile,
3224
+ borderRight: !mobile,
3225
+ scheme: oppositeScheme,
3226
+ theme: shownTheme,
3227
+ children: studio
3228
+ })
3229
+ }, "opposite")
3230
+ }), $[45] = classes.copyEnter, $[46] = classes.copyExit, $[47] = classes.update, $[48] = mobile, $[49] = oppositeScheme, $[50] = shownOpen, $[51] = shownSplit, $[52] = shownTheme, $[53] = studio, $[54] = transitioned, $[55] = t16) : t16 = $[55];
2520
3231
  let t17;
2521
- $[41] !== split || $[42] !== studio || $[43] !== theme ? (t17 = split && /* @__PURE__ */ jsx(StudioPreview, {
2522
- borderLeft: !0,
2523
- scheme: "dark",
2524
- theme,
3232
+ $[56] !== shownTheme || $[57] !== studio ? (t17 = /* @__PURE__ */ jsx(StudioPreview, {
3233
+ ref: studioRef,
3234
+ theme: shownTheme,
2525
3235
  children: studio
2526
- }), $[41] = split, $[42] = studio, $[43] = theme, $[44] = t17) : t17 = $[44];
3236
+ }), $[56] = shownTheme, $[57] = studio, $[58] = t17) : t17 = $[58];
2527
3237
  let t18;
2528
- $[45] !== open || $[46] !== theme ? (t18 = open && /* @__PURE__ */ jsx(ThemeProvider, {
2529
- theme: theme ?? void 0,
2530
- children: /* @__PURE__ */ jsx(ResizableSidebar, {})
2531
- }), $[45] = open, $[46] = theme, $[47] = t18) : t18 = $[47];
2532
- let t19;
2533
- $[48] !== t16 || $[49] !== t17 || $[50] !== t18 ? (t19 = /* @__PURE__ */ jsxs(Flex, {
3238
+ $[59] !== classes.studioUpdate || $[60] !== t17 || $[61] !== transitioned ? (t18 = /* @__PURE__ */ jsx(ViewTransition, {
3239
+ onUpdate: transitioned,
3240
+ update: classes.studioUpdate,
3241
+ children: t17
3242
+ }, "primary"), $[59] = classes.studioUpdate, $[60] = t17, $[61] = transitioned, $[62] = t18) : t18 = $[62];
3243
+ let t19 = shownOpen ? "visible" : "hidden", t20 = shownTheme ?? void 0, t21;
3244
+ $[63] === mobile ? t21 = $[64] : (t21 = /* @__PURE__ */ jsx(ResizableSidebar, { overlay: mobile }), $[63] = mobile, $[64] = t21);
3245
+ let t22;
3246
+ $[65] !== t20 || $[66] !== t21 ? (t22 = /* @__PURE__ */ jsx(ThemeProvider, {
3247
+ theme: t20,
3248
+ children: t21
3249
+ }), $[65] = t20, $[66] = t21, $[67] = t22) : t22 = $[67];
3250
+ let t23;
3251
+ $[68] !== classes.panelEnter || $[69] !== classes.panelExit || $[70] !== classes.update || $[71] !== t22 || $[72] !== transitioned ? (t23 = /* @__PURE__ */ jsx(ViewTransition, {
3252
+ enter: classes.panelEnter,
3253
+ exit: classes.panelExit,
3254
+ onEnter: transitioned,
3255
+ onExit: transitioned,
3256
+ onUpdate: transitioned,
3257
+ update: classes.update,
3258
+ children: t22
3259
+ }, "panel"), $[68] = classes.panelEnter, $[69] = classes.panelExit, $[70] = classes.update, $[71] = t22, $[72] = transitioned, $[73] = t23) : t23 = $[73];
3260
+ let t24;
3261
+ $[74] !== t19 || $[75] !== t23 ? (t24 = /* @__PURE__ */ jsx(Activity, {
3262
+ mode: t19,
3263
+ children: t23
3264
+ }), $[74] = t19, $[75] = t23, $[76] = t24) : t24 = $[76];
3265
+ let t25;
3266
+ $[77] !== t15 || $[78] !== t16 || $[79] !== t18 || $[80] !== t24 ? (t25 = /* @__PURE__ */ jsxs(Flex, {
3267
+ className: layout,
3268
+ direction: t15,
2534
3269
  height: "fill",
2535
3270
  sizing: "border",
2536
3271
  children: [
2537
3272
  t16,
2538
- t17,
2539
- t18
3273
+ t18,
3274
+ t24
2540
3275
  ]
2541
- }), $[48] = t16, $[49] = t17, $[50] = t18, $[51] = t19) : t19 = $[51];
2542
- let t20;
2543
- return $[52] !== context || $[53] !== t19 ? (t20 = /* @__PURE__ */ jsx(ThemerContext.Provider, {
3276
+ }), $[77] = t15, $[78] = t16, $[79] = t18, $[80] = t24, $[81] = t25) : t25 = $[81];
3277
+ let t26;
3278
+ return $[82] !== context || $[83] !== t25 ? (t26 = /* @__PURE__ */ jsx(ThemerContext.Provider, {
2544
3279
  value: context,
2545
- children: t19
2546
- }), $[52] = context, $[53] = t19, $[54] = t20) : t20 = $[54], t20;
3280
+ children: t25
3281
+ }), $[82] = context, $[83] = t25, $[84] = t26) : t26 = $[84], t26;
2547
3282
  }
2548
3283
  /**
2549
3284
  * One copy of the Studio, in the given color scheme — or, without one, in the
@@ -2557,17 +3292,17 @@ function ThemerLayout(props) {
2557
3292
  * keeps native form controls and scrollbars in step with it.
2558
3293
  */
2559
3294
  function StudioPreview(props) {
2560
- let $ = c(10), { borderLeft, children, scheme, theme } = props, t0 = theme ?? void 0, t1;
2561
- $[0] === scheme ? t1 = $[1] : (t1 = scheme ? { colorScheme: scheme } : void 0, $[0] = scheme, $[1] = t1);
2562
- let t2;
2563
- $[2] !== borderLeft || $[3] !== children || $[4] !== t1 ? (t2 = /* @__PURE__ */ jsx(Card, {
2564
- borderLeft,
3295
+ let $ = c(10), { borderBottom, borderRight, children, ref, scheme, theme } = props, t0 = theme ?? void 0, t1 = scheme ? studioScheme[scheme] : void 0, t2;
3296
+ $[0] !== borderBottom || $[1] !== borderRight || $[2] !== children || $[3] !== ref || $[4] !== t1 ? (t2 = /* @__PURE__ */ jsx(Card, {
3297
+ borderBottom,
3298
+ borderRight,
3299
+ className: t1,
2565
3300
  flex: 1,
2566
3301
  height: "fill",
2567
3302
  overflow: "hidden",
2568
- style: t1,
3303
+ ref,
2569
3304
  children
2570
- }), $[2] = borderLeft, $[3] = children, $[4] = t1, $[5] = t2) : t2 = $[5];
3305
+ }), $[0] = borderBottom, $[1] = borderRight, $[2] = children, $[3] = ref, $[4] = t1, $[5] = t2) : t2 = $[5];
2571
3306
  let t3;
2572
3307
  return $[6] !== scheme || $[7] !== t0 || $[8] !== t2 ? (t3 = /* @__PURE__ */ jsx(ThemeProvider, {
2573
3308
  scheme,