@sanity/themer 0.8.1 → 0.10.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,205 +1,452 @@
1
1
  import "@sanity/themer/bundle.css";
2
2
  import { l as isColor, n as buildTheme, o as SCHEMES, t as presets } from "./presets-CqBgS3A-.js";
3
- import { a as duplicateTitle, i as displayTitle, l as IMAGE_PALETTE_KEYS, o as initialThemerState, r as createCustomTheme, s as resolveThemes, v as ThemerContext, y as useThemer } from "./ResizableSidebar.css.ts.vanilla-M9EuPu-q.js";
4
- import { blue, cyan, green, magenta, orange, purple, red, yellow } from "@sanity/color";
3
+ import { a as displayTitle, c as resolveThemes, d as viewTransitionClasses, f as NAVBAR_SELECTOR, i as createCustomTheme, l as addThemerTransitionType, n as CONFIG_TITLE, o as duplicateTitle, s as initialThemerState, u as layout } from "./themes-au1BLKmY.js";
5
4
  import { c } from "react/compiler-runtime";
6
5
  import { definePlugin, useColorSchemeValue } from "sanity";
7
- import { Button, Card, Flex, Text, ThemeProvider, useMediaIndex, usePrefersReducedMotion } from "@sanity/ui";
8
- import { useActor, useSelector } from "@xstate/react";
9
- import { Activity, ViewTransition, addTransitionType, lazy, startTransition, useDeferredValue, useEffect, useRef, useState, useSyncExternalStore } from "react";
10
- import { assign, not, or, setup, stateIn } from "xstate";
11
- import { jsx, jsxs } from "react/jsx-runtime";
12
6
  import { ColorWheelIcon } from "@sanity/icons/ColorWheel";
7
+ import { BoundaryElementProvider, Box, Button, Card, Flex, Text, ThemeProvider, useMediaIndex, usePrefersReducedMotion } from "@sanity/ui";
13
8
  import { Tooltip } from "@sanity/ui/tooltip";
14
- import { animate, motion, useMotionValue, useTransform } from "motion/react";
15
- const STORAGE_KEY = "sanityStudio:themer:state", LEGACY_STORAGE_KEY = "sanityStudio:themer:options", VISITED_STORAGE_KEY = "sanityStudio:themer:visited";
16
- function sanitizeColor(value) {
17
- return typeof value == "string" && isColor(value) ? value.toLowerCase() : null;
9
+ import { Activity, Suspense, ViewTransition, createContext, lazy, startTransition, use, useEffect, useReducer, useRef, useState, useTransition } from "react";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ import { browser } from "react-dom";
12
+ import { dequal } from "dequal/lite";
13
+ import * as v from "valibot";
14
+ import { useActorRef } from "@xstate/react";
15
+ import { assign, not, setup } from "xstate";
16
+ /** @internal */
17
+ const ToolIsOpenContext = createContext(!1), ToolDispatchContext = createContext(() => {
18
+ throw Error("ToolDispatchContext not initialized");
19
+ }), ToolSplitIsOpenContext = createContext(!1), ToolShouldDetectNavbarHeightContext = createContext(!1), PluginConfigContext = createContext(null), AnimatedColorWheelIcon = lazy(() => import("./AnimatedColorWheelIcon-CwWOVAsw.js"));
20
+ function ThemerNavbarButton() {
21
+ let $ = c(27), config = use(PluginConfigContext), dispatch = use(ToolDispatchContext), open = use(ToolIsOpenContext), shouldDetectNavbarHeight = use(ToolShouldDetectNavbarHeightContext), split = use(ToolSplitIsOpenContext);
22
+ if (!config) throw TypeError("PluginConfigContext.Provider is missing");
23
+ let [busy, startBusyTransition] = useTransition(), iconRef = useRef(null), prefersReducedMotion = usePrefersReducedMotion(), t0;
24
+ $[0] !== dispatch || $[1] !== split ? (t0 = () => startBusyTransition(() => {
25
+ split && addThemerTransitionType("split-screen:open"), addThemerTransitionType("open"), dispatch({ type: "open" });
26
+ }), $[0] = dispatch, $[1] = split, $[2] = t0) : t0 = $[2];
27
+ let handleOpen = t0, t1;
28
+ $[3] !== dispatch || $[4] !== split ? (t1 = () => startBusyTransition(() => {
29
+ split && addThemerTransitionType("split-screen:close"), addThemerTransitionType("close"), dispatch({ type: "close" });
30
+ }), $[3] = dispatch, $[4] = split, $[5] = t1) : t1 = $[5];
31
+ let handleClose = t1, t2;
32
+ $[6] !== dispatch || $[7] !== open ? (t2 = () => startTransition(() => {
33
+ dispatch({ type: "prerender" }), open || iconRef.current?.spin();
34
+ }), $[6] = dispatch, $[7] = open, $[8] = t2) : t2 = $[8];
35
+ let handlePrerender = t2, t3;
36
+ $[9] === dispatch ? t3 = $[10] : (t3 = (root) => {
37
+ let navbar = root?.closest(NAVBAR_SELECTOR);
38
+ if (!navbar) return;
39
+ let resizeObserver = new ResizeObserver((entries) => {
40
+ let height = entries[0]?.borderBoxSize[0]?.blockSize;
41
+ height && startTransition(() => dispatch({
42
+ type: "set-navbar-height",
43
+ height
44
+ }));
45
+ });
46
+ return resizeObserver.observe(navbar), () => resizeObserver.disconnect();
47
+ }, $[9] = dispatch, $[10] = t3);
48
+ let observeNavbarHeight = t3, { title } = config, t4;
49
+ $[11] === title ? t4 = $[12] : (t4 = /* @__PURE__ */ jsx(Text, {
50
+ size: 1,
51
+ children: title
52
+ }), $[11] = title, $[12] = t4);
53
+ let t5 = shouldDetectNavbarHeight ? observeNavbarHeight : void 0, t6;
54
+ $[13] !== busy || $[14] !== prefersReducedMotion ? (t6 = prefersReducedMotion ? /* @__PURE__ */ jsx(ColorWheelIcon, {}) : /* @__PURE__ */ jsx(Suspense, {
55
+ fallback: /* @__PURE__ */ jsx(ColorWheelIcon, {}),
56
+ children: /* @__PURE__ */ jsx(AnimatedColorWheelIcon, {
57
+ busy,
58
+ ref: iconRef
59
+ })
60
+ }), $[13] = busy, $[14] = prefersReducedMotion, $[15] = t6) : t6 = $[15];
61
+ let t7 = open ? handleClose : handleOpen, t8 = open || busy, t9;
62
+ $[16] !== busy || $[17] !== handlePrerender || $[18] !== t5 || $[19] !== t6 || $[20] !== t7 || $[21] !== t8 || $[22] !== title ? (t9 = /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(ViewTransition, {
63
+ default: "none",
64
+ update: viewTransitionClasses.navbarButton.update,
65
+ children: /* @__PURE__ */ jsx(Button, {
66
+ ref: t5,
67
+ "aria-busy": busy,
68
+ "aria-label": title,
69
+ icon: t6,
70
+ mode: "bleed",
71
+ onClick: t7,
72
+ onFocus: handlePrerender,
73
+ onMouseEnter: handlePrerender,
74
+ padding: 2,
75
+ selected: t8
76
+ })
77
+ }) }), $[16] = busy, $[17] = handlePrerender, $[18] = t5, $[19] = t6, $[20] = t7, $[21] = t8, $[22] = title, $[23] = t9) : t9 = $[23];
78
+ let t10;
79
+ return $[24] !== t4 || $[25] !== t9 ? (t10 = /* @__PURE__ */ jsx(ViewTransition, {
80
+ default: "none",
81
+ update: viewTransitionClasses.navbarButtonTooltip.update,
82
+ children: /* @__PURE__ */ jsx(Tooltip, {
83
+ animate: !0,
84
+ content: t4,
85
+ portal: !0,
86
+ children: t9
87
+ })
88
+ }), $[24] = t4, $[25] = t9, $[26] = t10) : t10 = $[26], t10;
18
89
  }
19
- function sanitizeContrast(value) {
20
- return typeof value == "number" && Number.isFinite(value) ? Math.min(100, Math.max(15, value)) : null;
90
+ function ThemerNavbar(props) {
91
+ "use memo";
92
+ let $ = c(4), t0;
93
+ if ($[0] !== props) {
94
+ let button = {
95
+ location: "topbar",
96
+ name: "themer-topbar",
97
+ render: ThemerNavbarButton
98
+ }, t1;
99
+ $[2] === props.__internal_actions ? t1 = $[3] : (t1 = props.__internal_actions ?? [], $[2] = props.__internal_actions, $[3] = t1), t0 = props.renderDefault({
100
+ ...props,
101
+ __internal_actions: [...t1, button]
102
+ }), $[0] = props, $[1] = t0;
103
+ } else t0 = $[1];
104
+ return t0;
21
105
  }
22
- function sanitizeSchemeOptions(value) {
23
- let options = {};
24
- if (!value || typeof value != "object") return options;
25
- 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"));
26
- return accent && (options.accent = accent), text && (options.text = text), background && (options.background = background), contrast !== null && (options.contrast = contrast), options;
106
+ function ThemerNavbarProvider(t0) {
107
+ let $ = c(15), { children, config, dispatch, open, shouldDetectNavbarHeight, split } = t0, t1 = shouldDetectNavbarHeight ?? !1, t2;
108
+ $[0] !== children || $[1] !== t1 ? (t2 = /* @__PURE__ */ jsx(ToolShouldDetectNavbarHeightContext, {
109
+ value: t1,
110
+ children
111
+ }), $[0] = children, $[1] = t1, $[2] = t2) : t2 = $[2];
112
+ let t3;
113
+ $[3] !== split || $[4] !== t2 ? (t3 = /* @__PURE__ */ jsx(ToolSplitIsOpenContext, {
114
+ value: split,
115
+ children: t2
116
+ }), $[3] = split, $[4] = t2, $[5] = t3) : t3 = $[5];
117
+ let t4;
118
+ $[6] !== open || $[7] !== t3 ? (t4 = /* @__PURE__ */ jsx(ToolIsOpenContext, {
119
+ value: open,
120
+ children: t3
121
+ }), $[6] = open, $[7] = t3, $[8] = t4) : t4 = $[8];
122
+ let t5;
123
+ $[9] !== dispatch || $[10] !== t4 ? (t5 = /* @__PURE__ */ jsx(ToolDispatchContext, {
124
+ value: dispatch,
125
+ children: t4
126
+ }), $[9] = dispatch, $[10] = t4, $[11] = t5) : t5 = $[11];
127
+ let t6;
128
+ return $[12] !== config || $[13] !== t5 ? (t6 = /* @__PURE__ */ jsx(PluginConfigContext, {
129
+ value: config,
130
+ children: t5
131
+ }), $[12] = config, $[13] = t5, $[14] = t6) : t6 = $[14], t6;
132
+ }
133
+ function toolReducer(state, action) {
134
+ switch (action.type) {
135
+ case "set-navbar-height": return state.navbarHeight === action.height ? state : {
136
+ ...state,
137
+ navbarHeight: action.height
138
+ };
139
+ case "prerender": return state.prerender ? state : {
140
+ ...state,
141
+ prerender: !0
142
+ };
143
+ case "open": return state.open ? state : {
144
+ ...state,
145
+ open: !0,
146
+ prerender: !0
147
+ };
148
+ case "close": return state.open ? {
149
+ ...state,
150
+ open: !1
151
+ } : state;
152
+ case "split-screen:prerender": return state.prerenderSplitScreen ? state : {
153
+ ...state,
154
+ prerenderSplitScreen: !0
155
+ };
156
+ case "split-screen:open": return state.split ? state : {
157
+ ...state,
158
+ prerenderSplitScreen: !0,
159
+ split: !0
160
+ };
161
+ case "split-screen:close": return state.split ? {
162
+ ...state,
163
+ split: !1
164
+ } : state;
165
+ case "unset-theme": return state.theme === null ? state : {
166
+ ...state,
167
+ theme: null
168
+ };
169
+ case "set-theme": return dequal(state.theme, action.theme) ? state : {
170
+ ...state,
171
+ theme: action.theme
172
+ };
173
+ default: throw Error(`Unknown themer tool action: ${JSON.stringify(action)}`);
174
+ }
27
175
  }
28
176
  /**
29
- * Options from before they were grouped by scheme: one accent, text color and
30
- * contrast for both schemes, and a background per scheme. Both schemes get
31
- * the shared colors, so the theme keeps looking the same.
177
+ * Where a tab starts: closed, nothing prerendered, no theme applied —
178
+ * `ThemerProvider` seeds only the applied theme, from the machine's restored
179
+ * snapshot
32
180
  */
33
- function sanitizeFlatOptions(value) {
34
- let accent = sanitizeColor(Reflect.get(value, "accent"));
35
- if (!accent) return null;
36
- let shared = { accent }, text = sanitizeColor(Reflect.get(value, "text")), contrast = sanitizeContrast(Reflect.get(value, "contrast"));
37
- text && (shared.text = text), contrast !== null && (shared.contrast = contrast);
38
- let backgrounds = Reflect.get(value, "background"), options = {};
39
- for (let scheme of SCHEMES) {
40
- let background = backgrounds && typeof backgrounds == "object" ? sanitizeColor(Reflect.get(backgrounds, scheme)) : null;
41
- options[scheme] = background ? {
42
- ...shared,
43
- background
44
- } : { ...shared };
45
- }
46
- return options;
181
+ const initialToolState = {
182
+ navbarHeight: null,
183
+ open: !1,
184
+ prerender: !1,
185
+ prerenderSplitScreen: !1,
186
+ split: !1,
187
+ theme: null
188
+ };
189
+ /**
190
+ * The options of the applied theme — `null` for the configured theme, which
191
+ * applies nothing on top of the Studio's own, and for an applied theme that
192
+ * no longer resolves. Cheaper than resolving the whole list when only the
193
+ * applied theme matters.
194
+ *
195
+ * @internal
196
+ */
197
+ function resolveActiveThemeOptions(state) {
198
+ if (state.active === null || state.removed.includes(state.active)) return null;
199
+ for (let theme of state.custom) if (theme.slug === state.active) return theme.options;
200
+ for (let preset of presets) if (preset.slug === state.active) return preset.options;
201
+ return null;
47
202
  }
48
- function sanitizeOptions(value) {
49
- if (!value || typeof value != "object") return null;
50
- if ("accent" in value) return sanitizeFlatOptions(value);
203
+ /** Drops the keys whose value is `undefined`, which a dropped option leaves behind */
204
+ function compact(value) {
205
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0));
206
+ }
207
+ /** A `#rgb` or `#rrggbb` color, lowercased */
208
+ const color = v.pipe(v.string(), v.check(isColor), v.toLowerCase()), optionalColor = v.fallback(v.optional(color), void 0), optionalContrast = v.fallback(v.optional(v.pipe(v.number(), v.finite(), v.toMinValue(15), v.toMaxValue(100))), void 0), schemeOptionsSchema = v.pipe(v.object({
209
+ accent: optionalColor,
210
+ text: optionalColor,
211
+ background: optionalColor,
212
+ contrast: optionalContrast
213
+ }), v.transform((options) => compact(options))), groupedOptionsSchema = v.pipe(v.looseObject({
214
+ light: v.fallback(v.optional(schemeOptionsSchema), void 0),
215
+ dark: v.fallback(v.optional(schemeOptionsSchema), void 0)
216
+ }), v.check((input) => !("accent" in input), "flat options"), v.transform(({ light, dark }) => {
51
217
  let options = {};
218
+ return light && Object.keys(light).length > 0 && (options.light = light), dark && Object.keys(dark).length > 0 && (options.dark = dark), options;
219
+ })), flatOptionsSchema = v.pipe(v.object({
220
+ accent: color,
221
+ text: optionalColor,
222
+ contrast: optionalContrast,
223
+ background: v.fallback(v.optional(v.object({
224
+ light: optionalColor,
225
+ dark: optionalColor
226
+ })), void 0)
227
+ }), v.transform(({ accent, text, contrast, background }) => {
228
+ let shared = compact({
229
+ accent,
230
+ text,
231
+ contrast
232
+ }), options = {};
52
233
  for (let scheme of SCHEMES) {
53
- let schemeOptions = sanitizeSchemeOptions(Reflect.get(value, scheme));
54
- Object.keys(schemeOptions).length > 0 && (options[scheme] = schemeOptions);
234
+ let schemeBackground = background?.[scheme];
235
+ options[scheme] = schemeBackground ? {
236
+ ...shared,
237
+ background: schemeBackground
238
+ } : { ...shared };
55
239
  }
56
240
  return options;
57
- }
241
+ })), themeOptionsSchema = v.union([flatOptionsSchema, groupedOptionsSchema]), swatch = v.fallback(v.optional(v.nullable(color), null), null), paletteSchema = v.pipe(v.object({
242
+ vibrant: swatch,
243
+ lightVibrant: swatch,
244
+ darkVibrant: swatch,
245
+ muted: swatch,
246
+ lightMuted: swatch,
247
+ darkMuted: swatch,
248
+ dominant: swatch
249
+ }), v.transform((palette) => Object.values(palette).some((item) => item !== null) ? palette : null));
58
250
  function isReservedSlug(slug) {
59
251
  return slug === "config" || presets.some((preset) => preset.slug === slug);
60
252
  }
61
- function sanitizeCustomTheme(value) {
62
- if (!value || typeof value != "object") return null;
63
- let slug = Reflect.get(value, "slug");
64
- if (typeof slug != "string" || !slug || isReservedSlug(slug)) return null;
65
- let options = sanitizeOptions(Reflect.get(value, "options"));
66
- if (!options) return null;
67
- let title = Reflect.get(value, "title"), palette = sanitizePalette(Reflect.get(value, "palette"));
68
- return {
69
- slug,
70
- title: displayTitle(typeof title == "string" ? title : ""),
71
- options,
72
- ...palette ? { palette } : {}
73
- };
74
- }
75
- function sanitizePalette(value) {
76
- if (!value || typeof value != "object") return null;
77
- let palette = {}, swatches = 0;
78
- for (let key of IMAGE_PALETTE_KEYS) {
79
- let swatch = sanitizeColor(Reflect.get(value, key));
80
- palette[key] = swatch, swatch && swatches++;
81
- }
82
- return swatches > 0 ? palette : null;
83
- }
84
- function sanitizeState(value) {
85
- if (!value || typeof value != "object") return initialThemerState;
86
- let custom = [], rawCustom = Reflect.get(value, "custom");
87
- if (Array.isArray(rawCustom)) for (let item of rawCustom) {
88
- let theme = sanitizeCustomTheme(item);
89
- theme && !custom.some((existing) => existing.slug === theme.slug) && custom.push(theme);
253
+ const customThemeSchema = v.pipe(v.object({
254
+ slug: v.pipe(v.string(), v.nonEmpty(), v.check((slug) => !isReservedSlug(slug), "reserved slug")),
255
+ title: v.fallback(v.optional(v.string()), void 0),
256
+ options: themeOptionsSchema,
257
+ palette: v.fallback(v.optional(paletteSchema), void 0)
258
+ }), v.transform(({ slug, title, options, palette }) => ({
259
+ slug,
260
+ title: displayTitle(title ?? ""),
261
+ options,
262
+ ...palette ? { palette } : {}
263
+ }))), lenientList = v.fallback(v.optional(v.array(v.unknown()), []), []), themerStateSchema = v.pipe(v.object({
264
+ active: v.fallback(v.nullable(v.string()), null),
265
+ custom: lenientList,
266
+ removed: lenientList,
267
+ order: lenientList
268
+ }), v.transform((state) => {
269
+ let custom = [];
270
+ for (let item of state.custom) {
271
+ let result = v.safeParse(customThemeSchema, item);
272
+ result.success && !custom.some((theme) => theme.slug === result.output.slug) && custom.push(result.output);
90
273
  }
91
- let removed = [], rawRemoved = Reflect.get(value, "removed");
92
- 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);
93
- let order = [], rawOrder = Reflect.get(value, "order");
94
- 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);
95
- let rawActive = Reflect.get(value, "active");
274
+ let isCustom = (slug) => custom.some((theme) => theme.slug === slug), isPreset = (slug) => presets.some((preset) => preset.slug === slug), removed = [], order = [];
275
+ for (let slug of state.removed) typeof slug != "string" || slug === "config" || removed.includes(slug) || (isPreset(slug) || isCustom(slug)) && removed.push(slug);
276
+ for (let slug of state.order) typeof slug != "string" || order.includes(slug) || (slug === "config" || isPreset(slug) || isCustom(slug)) && order.push(slug);
96
277
  return {
97
- active: typeof rawActive == "string" ? rawActive : null,
278
+ active: state.active,
98
279
  custom,
99
280
  removed,
100
281
  order
101
282
  };
283
+ }));
284
+ /** The persisted themer state, or `null` for anything else @internal */
285
+ function parseThemerState(input) {
286
+ let result = v.safeParse(themerStateSchema, input);
287
+ return result.success ? result.output : null;
102
288
  }
103
- function readLegacyState() {
104
- let raw = localStorage.getItem(LEGACY_STORAGE_KEY);
105
- if (!raw) return null;
106
- let options = sanitizeOptions(JSON.parse(raw));
107
- if (!options) return null;
108
- let theme = createCustomTheme("Draft theme", options);
289
+ /** The flows a session restores to */
290
+ const flowSchema = v.picklist([
291
+ "list",
292
+ "edit",
293
+ "removed"
294
+ ]), snapshotValueSchema = v.object({
295
+ flow: v.union([flowSchema, v.object({ list: v.picklist([
296
+ "idle",
297
+ "pasting",
298
+ "snippet"
299
+ ]) })]),
300
+ theme: v.picklist(["applied", "switching"])
301
+ });
302
+ /**
303
+ * A snapshot for the machine to start a session from: the given themes, with
304
+ * nothing being switched to, in the list unless a flow and its subject are
305
+ * given. It is how the themes alone — all that earlier versions persisted —
306
+ * are handed to a machine that only restores snapshots, and what a restored
307
+ * snapshot is made fit for this session as.
308
+ *
309
+ * @internal
310
+ */
311
+ function snapshotFromState(state, editing = null, flow = "list") {
109
312
  return {
110
- active: theme.slug,
111
- custom: [theme],
112
- removed: [],
113
- order: []
313
+ status: "active",
314
+ output: void 0,
315
+ error: void 0,
316
+ value: {
317
+ flow,
318
+ theme: "applied"
319
+ },
320
+ context: {
321
+ ...state,
322
+ editing,
323
+ images: {}
324
+ },
325
+ historyValue: {},
326
+ children: {}
114
327
  };
115
328
  }
116
329
  /**
117
- * Restores the themer state from localStorage, so the user's themes survive
118
- * studio reloads. Falls back to the initial state when nothing usable is
119
- * stored.
330
+ * The machine's snapshot as the last session persisted it, made fit for this
331
+ * one: the themes are parsed like any persisted state, the flow keeps its
332
+ * subject or falls back to the list, a dialog that was open is closed, a
333
+ * switch that was under way is over, and the image URLs of the last session
334
+ * are gone with it.
120
335
  *
121
336
  * @internal
122
337
  */
123
- function readStoredState() {
124
- try {
125
- if (typeof localStorage > "u") return initialThemerState;
126
- let raw = localStorage.getItem(STORAGE_KEY);
127
- return raw ? sanitizeState(JSON.parse(raw)) : readLegacyState() ?? initialThemerState;
128
- } catch {
129
- return initialThemerState;
130
- }
131
- }
132
- /** @internal */
133
- function writeStoredState(state) {
134
- try {
135
- if (typeof localStorage > "u") return;
136
- localStorage.setItem(STORAGE_KEY, JSON.stringify(state)), localStorage.removeItem(LEGACY_STORAGE_KEY);
137
- } catch {}
338
+ const persistedSnapshotSchema = v.pipe(v.object({
339
+ status: v.literal("active"),
340
+ value: snapshotValueSchema,
341
+ context: v.pipe(v.looseObject({ editing: v.fallback(v.nullable(v.object({
342
+ slug: v.string(),
343
+ focusTitle: v.boolean()
344
+ })), null) }), v.transform(({ editing, ...state }) => ({
345
+ editing,
346
+ state: parseThemerState(state)
347
+ })), v.check(({ state }) => state !== null, "no themes"))
348
+ }), v.transform(({ value, context }) => {
349
+ let state = context.state ?? {
350
+ active: null,
351
+ custom: [],
352
+ removed: [],
353
+ order: []
354
+ }, editing = context.editing && state.custom.some((theme) => theme.slug === context.editing?.slug) && !state.removed.includes(context.editing.slug) ? context.editing : null;
355
+ return snapshotFromState(state, editing, typeof value.flow != "string" || value.flow === "edit" && editing === null || value.flow === "removed" && state.removed.length === 0 ? "list" : value.flow);
356
+ })), SNAPSHOT_STORAGE_KEY = "sanityStudio:themer:snapshot", STATE_STORAGE_KEY = "sanityStudio:themer:state", LEGACY_STORAGE_KEY = "sanityStudio:themer:options", LEGACY_DRAFT = {
357
+ slug: "custom-draft",
358
+ title: "Draft theme"
359
+ };
360
+ function readJson(key) {
361
+ let raw = localStorage.getItem(key);
362
+ return raw ? JSON.parse(raw) : void 0;
138
363
  }
139
364
  /**
140
- * Whether the themer sidebar has been opened before, in any session — the
141
- * cue for the navbar to stop introducing the tool.
365
+ * Restores the machine's snapshot from what the last session persisted, for
366
+ * `createActor` to start from, so the user's themes survive Studio reloads:
367
+ * the snapshot itself where there is one that fits, and one made from the
368
+ * themes alone where the snapshot no longer fits the machine or where
369
+ * earlier versions of the tool stored the themes without a snapshot.
370
+ * `undefined` when nothing usable is stored — or storage cannot be read at
371
+ * all — leaves the machine to start from no themes.
372
+ *
373
+ * The parsing is the schemas' (see `schemas.ts`): nothing here or in the
374
+ * machine looks at what is stored beyond handing it over.
142
375
  *
143
376
  * @internal
144
377
  */
145
- function hasVisited() {
378
+ function readPersistedSnapshot() {
146
379
  try {
147
- return typeof localStorage < "u" && localStorage.getItem(VISITED_STORAGE_KEY) !== null;
380
+ if (typeof localStorage > "u") return;
381
+ let persisted = readJson(SNAPSHOT_STORAGE_KEY);
382
+ if (persisted !== void 0) {
383
+ let snapshot = v.safeParse(persistedSnapshotSchema, persisted);
384
+ if (snapshot.success) return snapshot.output;
385
+ let state = parseThemerState(persisted && typeof persisted == "object" ? Reflect.get(persisted, "context") : void 0);
386
+ if (state) return snapshotFromState(state);
387
+ }
388
+ let state = parseThemerState(readJson(STATE_STORAGE_KEY));
389
+ return state ? snapshotFromState(state) : readLegacyDraft();
148
390
  } catch {
149
- return !1;
391
+ return;
150
392
  }
151
393
  }
394
+ function readLegacyDraft() {
395
+ let options = v.safeParse(themeOptionsSchema, readJson(LEGACY_STORAGE_KEY));
396
+ if (!options.success) return;
397
+ let theme = {
398
+ ...LEGACY_DRAFT,
399
+ options: options.output
400
+ };
401
+ return snapshotFromState({
402
+ active: theme.slug,
403
+ custom: [theme],
404
+ removed: [],
405
+ order: []
406
+ });
407
+ }
152
408
  /**
153
- * Notes that the themer sidebar has been opened, keeping the time it first
154
- * was.
409
+ * Persists the machine's snapshot for the next session, and lets go of what
410
+ * earlier versions stored once it is written — which completes their
411
+ * migration.
155
412
  *
156
413
  * @internal
157
414
  */
158
- function markVisited() {
415
+ function writePersistedSnapshot(snapshot) {
159
416
  try {
160
- if (typeof localStorage > "u" || localStorage.getItem(VISITED_STORAGE_KEY) !== null) return;
161
- localStorage.setItem(VISITED_STORAGE_KEY, (/* @__PURE__ */ new Date()).toISOString());
417
+ if (typeof localStorage > "u") return;
418
+ localStorage.setItem(SNAPSHOT_STORAGE_KEY, JSON.stringify(snapshot)), localStorage.removeItem(STATE_STORAGE_KEY), localStorage.removeItem(LEGACY_STORAGE_KEY);
162
419
  } catch {}
163
420
  }
164
- /**
165
- * How long the layout's motions — the panel's and the split preview's, and
166
- * the cross-fade to another theme — are taken to last when the layout never
167
- * says: it reports every view transition it starts, so this only ends a
168
- * motion that has none (reduced motion, a browser without view transitions),
169
- * where the `moving` and `switching` tags have nothing to affect. Generous,
170
- * so that it never runs out before a slow deferred commit starts the real one.
171
- *
172
- * @internal
173
- */
174
- const MOTION_DURATION = 2e3;
175
421
  function themesOf(context) {
176
- return resolveThemes(context, context.baseOptions);
422
+ return resolveThemes(context);
177
423
  }
178
424
  function revokeObjectUrl(url) {
179
425
  typeof URL < "u" && typeof URL.revokeObjectURL == "function" && URL.revokeObjectURL(url);
180
426
  }
181
427
  /**
182
- * The state of the themer tool: three parallel regions, whether the
183
- * `sidebar` is open, which `flow` it is in — picking a theme from the `list`,
184
- * `edit`ing one of the user's own themes, or restoring `removed` ones — and
185
- * whether the `preview` shows the Studio once (`single`) or in light and dark
186
- * side by side (`split`). The context carries the persisted state (the
187
- * applied theme, the user's themes and what was removed) alongside what the
188
- * flows need.
428
+ * The themes of the themer tool and the sidebar's way through them: which
429
+ * `flow` the sidebar is in — picking a theme from the `list`, with its
430
+ * dialogs for pasting a shared code and for the applied theme's snippet,
431
+ * `edit`ing one of the user's own themes, or restoring `removed` ones — and,
432
+ * in a parallel `theme` region, whether another theme was just applied. The
433
+ * context carries the persisted state (the applied theme, the user's themes
434
+ * and what was removed) alongside what the flows need. Whether the sidebar is
435
+ * open and whether the Studio shows twice is not the machine's: the tool
436
+ * reducer in `ThemerProvider` owns that, so that the navbar toggle and the
437
+ * sidebar can change it in transitions of their own. Nor is persisting: the
438
+ * machine never touches storage and takes no input — it starts from no
439
+ * themes, and a session with something to pick up from restores its
440
+ * persisted snapshot instead (see `storage.ts` and `sync.ts`), while the
441
+ * persisted state of other tabs reaches it as a `themes.sync` event like any
442
+ * other.
189
443
  *
190
- * The sidebar and the preview pass through `opening`/`closing` and
191
- * `splitting`/`unsplitting` on their way, states tagged for the UI: `panel`
192
- * and `split` say what shows (the panel from `opening` on, the split copy from
193
- * `splitting` on), `moving` that the Studio is about to give way or take room
194
- * — which is when, and only when, the layout lets a view transition animate
195
- * it. A fourth region, `theme`, is `switching` (its tag) right after an event
196
- * that applies another theme — picking one, adding, duplicating, importing or
197
- * editing one, removing or deleting the applied one — when the layout
198
- * cross-fades to it; edits to the applied theme's colors are not switches,
199
- * they follow the pointer. These states last until the layout reports its
200
- * transition under way — every boundary reports, the panel's and the copy's
201
- * entering and leaving as much as the Studio's morphing — or for
202
- * `MOTION_DURATION` when nothing animates.
444
+ * The `theme` region is `switching` (its tag) right after an event that
445
+ * applies another theme — picking one, adding, duplicating, importing or
446
+ * editing one, removing or deleting the applied one — which is when the
447
+ * layout cross-fades the Studio to it. Edits to the applied theme's colors are
448
+ * not switches, they follow the pointer: the first one ends the switch, and so
449
+ * does `MOTION_DURATION` when none follows.
203
450
  *
204
451
  * Theme operations are handled in every flow, and the flows leave on their
205
452
  * own when they lose their subject: the editor when its theme is removed or
@@ -210,34 +457,31 @@ function revokeObjectUrl(url) {
210
457
  const themerMachine = setup({
211
458
  types: {
212
459
  context: {},
213
- events: {},
214
- input: {}
460
+ events: {}
215
461
  },
216
462
  guards: {
217
463
  isCustomTheme: ({ context }, params) => themesOf(context).themes.some((theme) => theme.slug === params.slug && theme.source === "custom"),
218
464
  isEditingListedTheme: ({ context }) => context.editing !== null && themesOf(context).themes.some((theme) => theme.slug === context.editing?.slug && theme.source === "custom"),
219
465
  hasRemovedThemes: ({ context }) => themesOf(context).removed.length > 0,
220
- sidebarShown: or([stateIn({ sidebar: "opening" }), stateIn({ sidebar: "open" })]),
466
+ hasAppliedTheme: ({ context }) => themesOf(context).active !== void 0,
221
467
  isAnotherTheme: ({ context }, params) => params.slug !== (context.active ?? "config"),
222
468
  isAppliedTheme: ({ context }, params) => params.slug === context.active,
469
+ appliesAnotherTheme: ({ context }, params) => params.state.active !== context.active,
223
470
  hasOptions: (_, params) => params.options !== void 0
224
471
  },
225
472
  actions: {
226
- persist: ({ context }) => {
227
- let { active, custom, removed, order } = context;
228
- writeStoredState({
229
- active,
230
- custom,
231
- removed,
232
- order
233
- });
234
- },
473
+ sync: assign((_, params) => ({
474
+ active: params.state.active,
475
+ custom: params.state.custom,
476
+ removed: params.state.removed,
477
+ order: params.state.order
478
+ })),
235
479
  revokeImage: (_, params) => {
236
480
  params.url && revokeObjectUrl(params.url);
237
481
  },
238
482
  pick: assign((_, params) => ({ active: params.slug === "config" ? null : params.slug })),
239
483
  add: assign(({ context }, params) => {
240
- let theme = createCustomTheme(params.title ?? "Untitled theme", params.options ?? themesOf(context).active.options, params.palette);
484
+ let theme = createCustomTheme(params.title ?? "Untitled theme", params.options ?? themesOf(context).active?.options ?? {}, params.palette);
241
485
  return {
242
486
  active: theme.slug,
243
487
  custom: [...context.custom, theme],
@@ -259,7 +503,11 @@ const themerMachine = setup({
259
503
  };
260
504
  }),
261
505
  duplicate: assign(({ context }, params) => {
262
- let { themes, removed } = themesOf(context), source = [...themes, ...removed].find((theme) => theme.slug === params.slug);
506
+ let { themes, removed } = themesOf(context), source = params.slug === "config" ? {
507
+ title: CONFIG_TITLE,
508
+ options: {},
509
+ palette: void 0
510
+ } : [...themes, ...removed].find((theme) => theme.slug === params.slug);
263
511
  if (!source) return {};
264
512
  let theme = createCustomTheme(duplicateTitle(source.title), source.options, source.palette);
265
513
  return {
@@ -313,83 +561,13 @@ const themerMachine = setup({
313
561
  }
314
562
  }).createMachine({
315
563
  id: "themer",
316
- context: ({ input }) => ({
317
- ...input.stored,
318
- baseOptions: input.baseOptions,
564
+ context: () => ({
565
+ ...initialThemerState,
319
566
  editing: null,
320
567
  images: {}
321
568
  }),
322
- entry: "persist",
323
569
  type: "parallel",
324
570
  states: {
325
- sidebar: {
326
- initial: "closed",
327
- states: {
328
- closed: { on: { "sidebar.toggle": "opening" } },
329
- opening: {
330
- tags: ["panel", "moving"],
331
- after: { [MOTION_DURATION]: "open" },
332
- on: {
333
- "layout.transitioned": "open",
334
- "sidebar.toggle": "closing",
335
- "sidebar.close": "closing"
336
- }
337
- },
338
- open: {
339
- tags: ["panel"],
340
- on: {
341
- "sidebar.toggle": "closing",
342
- "sidebar.close": "closing"
343
- }
344
- },
345
- closing: {
346
- tags: ["moving"],
347
- after: { [MOTION_DURATION]: "closed" },
348
- on: {
349
- "layout.transitioned": "closed",
350
- "sidebar.toggle": "opening"
351
- }
352
- }
353
- }
354
- },
355
- preview: {
356
- initial: "single",
357
- states: {
358
- single: { on: { "preview.toggle": "splitting" } },
359
- splitting: {
360
- tags: ["split", "moving"],
361
- after: { [MOTION_DURATION]: "split" },
362
- on: {
363
- "layout.transitioned": "split",
364
- "preview.toggle": "unsplitting",
365
- "sidebar.close": "unsplitting",
366
- "sidebar.toggle": {
367
- guard: "sidebarShown",
368
- target: "unsplitting"
369
- }
370
- }
371
- },
372
- split: {
373
- tags: ["split"],
374
- on: {
375
- "preview.toggle": "unsplitting",
376
- "sidebar.close": "unsplitting",
377
- "sidebar.toggle": {
378
- guard: "sidebarShown",
379
- target: "unsplitting"
380
- }
381
- }
382
- },
383
- unsplitting: {
384
- tags: ["moving"],
385
- after: { [MOTION_DURATION]: "single" },
386
- on: {
387
- "layout.transitioned": "single",
388
- "preview.toggle": "splitting"
389
- }
390
- }
391
- }
392
- },
393
571
  theme: {
394
572
  initial: "applied",
395
573
  states: {
@@ -436,13 +614,20 @@ const themerMachine = setup({
436
614
  },
437
615
  target: "switching"
438
616
  },
439
- "theme.import": "switching"
617
+ "theme.import": "switching",
618
+ "themes.sync": {
619
+ guard: {
620
+ type: "appliesAnotherTheme",
621
+ params: ({ event }) => ({ state: event.state })
622
+ },
623
+ target: "switching"
624
+ }
440
625
  } },
441
626
  switching: {
442
627
  tags: ["switching"],
443
- after: { [MOTION_DURATION]: "applied" },
628
+ after: { 2e3: "applied" },
444
629
  on: {
445
- "layout.transitioned": "applied",
630
+ "theme.update": "applied",
446
631
  "theme.pick": {
447
632
  guard: {
448
633
  type: "isAnotherTheme",
@@ -494,6 +679,14 @@ const themerMachine = setup({
494
679
  "theme.import": {
495
680
  target: "switching",
496
681
  reenter: !0
682
+ },
683
+ "themes.sync": {
684
+ guard: {
685
+ type: "appliesAnotherTheme",
686
+ params: ({ event }) => ({ state: event.state })
687
+ },
688
+ target: "switching",
689
+ reenter: !0
497
690
  }
498
691
  }
499
692
  }
@@ -505,7 +698,7 @@ const themerMachine = setup({
505
698
  "theme.pick": { actions: [{
506
699
  type: "pick",
507
700
  params: ({ event }) => ({ slug: event.slug })
508
- }, "persist"] },
701
+ }] },
509
702
  "theme.add": {
510
703
  target: ".edit",
511
704
  actions: [{
@@ -516,14 +709,14 @@ const themerMachine = setup({
516
709
  palette: event.palette,
517
710
  imageUrl: event.imageUrl
518
711
  })
519
- }, "persist"]
712
+ }]
520
713
  },
521
714
  "theme.duplicate": {
522
715
  target: ".edit",
523
716
  actions: [{
524
717
  type: "duplicate",
525
718
  params: ({ event }) => ({ slug: event.slug })
526
- }, "persist"]
719
+ }]
527
720
  },
528
721
  "theme.edit": {
529
722
  target: ".edit",
@@ -534,55 +727,51 @@ const themerMachine = setup({
534
727
  actions: [{
535
728
  type: "startEditing",
536
729
  params: ({ event }) => ({ slug: event.slug })
537
- }, "persist"]
730
+ }]
538
731
  },
539
- "theme.update": { actions: [
540
- {
541
- type: "revokeImage",
542
- params: ({ context, event }) => ({ url: event.imageUrl ? context.images[event.slug] : void 0 })
543
- },
544
- {
545
- type: "update",
546
- params: ({ event }) => ({
547
- slug: event.slug,
548
- title: event.title,
549
- options: event.options,
550
- palette: event.palette,
551
- imageUrl: event.imageUrl
552
- })
553
- },
554
- "persist"
555
- ] },
732
+ "theme.update": { actions: [{
733
+ type: "revokeImage",
734
+ params: ({ context, event }) => ({ url: event.imageUrl ? context.images[event.slug] : void 0 })
735
+ }, {
736
+ type: "update",
737
+ params: ({ event }) => ({
738
+ slug: event.slug,
739
+ title: event.title,
740
+ options: event.options,
741
+ palette: event.palette,
742
+ imageUrl: event.imageUrl
743
+ })
744
+ }] },
556
745
  "theme.remove": { actions: [{
557
746
  type: "remove",
558
747
  params: ({ event }) => ({ slug: event.slug })
559
- }, "persist"] },
748
+ }] },
560
749
  "theme.restore": { actions: [{
561
750
  type: "restore",
562
751
  params: ({ event }) => ({ slug: event.slug })
563
- }, "persist"] },
564
- "theme.delete": { actions: [
565
- {
566
- type: "revokeImage",
567
- params: ({ context, event }) => ({ url: context.images[event.slug] })
568
- },
569
- {
570
- type: "delete",
571
- params: ({ event }) => ({ slug: event.slug })
572
- },
573
- "persist"
574
- ] },
752
+ }] },
753
+ "theme.delete": { actions: [{
754
+ type: "revokeImage",
755
+ params: ({ context, event }) => ({ url: context.images[event.slug] })
756
+ }, {
757
+ type: "delete",
758
+ params: ({ event }) => ({ slug: event.slug })
759
+ }] },
575
760
  "theme.reorder": { actions: [{
576
761
  type: "reorder",
577
762
  params: ({ event }) => ({ order: event.order })
578
- }, "persist"] },
763
+ }] },
579
764
  "theme.import": { actions: [{
580
765
  type: "import",
581
766
  params: ({ event }) => ({
582
767
  title: event.title,
583
768
  options: event.options
584
769
  })
585
- }, "persist"] },
770
+ }] },
771
+ "themes.sync": { actions: {
772
+ type: "sync",
773
+ params: ({ event }) => ({ state: event.state })
774
+ } },
586
775
  "flow.list": ".list",
587
776
  "flow.removed": {
588
777
  target: ".removed",
@@ -590,7 +779,26 @@ const themerMachine = setup({
590
779
  }
591
780
  },
592
781
  states: {
593
- list: {},
782
+ list: {
783
+ initial: "idle",
784
+ states: {
785
+ idle: { on: {
786
+ "dialog.paste": "pasting",
787
+ "dialog.snippet": {
788
+ guard: "hasAppliedTheme",
789
+ target: "snippet"
790
+ }
791
+ } },
792
+ pasting: { on: { "dialog.close": "idle" } },
793
+ snippet: {
794
+ on: { "dialog.close": "idle" },
795
+ always: {
796
+ guard: not("hasAppliedTheme"),
797
+ target: "idle"
798
+ }
799
+ }
800
+ }
801
+ },
594
802
  edit: {
595
803
  exit: "stopEditing",
596
804
  always: {
@@ -616,596 +824,416 @@ function selectStoredState(snapshot) {
616
824
  order
617
825
  };
618
826
  }
619
- const QUERY = "(prefers-reduced-motion: reduce)";
620
- function subscribe(onChange) {
621
- let media = matchMedia(QUERY);
622
- return media.addEventListener("change", onChange), () => media.removeEventListener("change", onChange);
827
+ /** The channel every tab of the same origin shares — one Studio, one set of themes */
828
+ const PERSIST_TIMEOUT = 1e3;
829
+ function openBroadcastChannel() {
830
+ if (typeof BroadcastChannel != "function") return null;
831
+ let channel = new BroadcastChannel("sanity-themer");
832
+ return {
833
+ post: (message) => channel.postMessage(message),
834
+ listen: (listener) => {
835
+ let onMessage = (event) => listener(event.data);
836
+ return channel.addEventListener("message", onMessage), () => channel.removeEventListener("message", onMessage);
837
+ },
838
+ close: () => channel.close()
839
+ };
623
840
  }
624
- function getSnapshot() {
625
- return matchMedia(QUERY).matches;
841
+ function browserLocks() {
842
+ return typeof navigator < "u" && "locks" in navigator ? navigator.locks : void 0;
626
843
  }
627
- function getServerSnapshot() {
628
- return !1;
844
+ function scheduleIdle(callback) {
845
+ if (typeof requestIdleCallback == "function") {
846
+ let id = requestIdleCallback(callback, { timeout: PERSIST_TIMEOUT });
847
+ return () => cancelIdleCallback(id);
848
+ }
849
+ let id = setTimeout(callback, PERSIST_TIMEOUT);
850
+ return () => clearTimeout(id);
629
851
  }
630
- /**
631
- * Whether the user prefers reduced motion — live, so that turning the
632
- * setting on while the Studio is open takes effect right away.
633
- *
634
- * @internal
635
- */
636
- function usePrefersReducedMotion$1() {
637
- return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
852
+ function parseMessage(data) {
853
+ if (!data || typeof data != "object") return null;
854
+ let type = Reflect.get(data, "type");
855
+ if (type === "hello") {
856
+ let from = Reflect.get(data, "from");
857
+ return typeof from == "string" ? {
858
+ type,
859
+ from
860
+ } : null;
861
+ }
862
+ if (type === "state") {
863
+ let state = parseThemerState(Reflect.get(data, "state")), revision = Reflect.get(data, "revision"), from = Reflect.get(data, "from"), to = Reflect.get(data, "to");
864
+ return !state || typeof revision != "number" || !Number.isFinite(revision) || typeof from != "string" ? null : {
865
+ type,
866
+ state,
867
+ revision,
868
+ from,
869
+ ...typeof to == "string" ? { to } : {}
870
+ };
871
+ }
872
+ return null;
873
+ }
874
+ function tabId() {
875
+ return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2);
638
876
  }
639
877
  /**
640
- * The Studio navbar's root, the `Card` that draws its bottom border — its
641
- * test id is stable and unique to the navbar, unlike its `data-ui` name
878
+ * Keeps the themes of every tab of the Studio the same, and persists them
879
+ * from one tab only.
642
880
  *
643
- * @internal
644
- */
645
- const NAVBAR_SELECTOR = "[data-testid=\"studio-navbar\"]";
646
- /**
647
- * The height of the Studio navbar inside the given element, in pixels — kept
648
- * up to date as the navbar renders, remounts and resizes, and `null` until
649
- * there is one. The themer's header takes this height, so that its bottom
650
- * border lines up with the navbar's whatever height the Studio version, its
651
- * breakpoint or a custom navbar gives it.
881
+ * Every tab runs its own machine, and what the machine persists — the
882
+ * applied theme, the user's themes, what was removed and reordered — goes
883
+ * around to the other tabs on a `BroadcastChannel` as it changes, where it
884
+ * reaches their machines as a `themes.sync` event: the same themes
885
+ * everywhere, with the flow each tab is in, the tool's open and split state
886
+ * (the reducer's, in `plugin.tsx`) and the images of this session staying
887
+ * the tab's own. A tab that starts asks the others for the current state,
888
+ * in case what it read from storage was not written yet.
652
889
  *
653
- * @internal
654
- */
655
- function useStudioNavbarHeight(ref) {
656
- let $ = c(3), [height, setHeight] = useState(null), t0, t1;
657
- return $[0] === ref ? (t0 = $[1], t1 = $[2]) : (t0 = () => {
658
- let root = ref.current;
659
- if (!root) return;
660
- let navbar = null, resizeObserver = new ResizeObserver(() => {
661
- navbar && setHeight(navbar.getBoundingClientRect().height);
662
- }), observeNavbar = () => {
663
- let next = root.querySelector(NAVBAR_SELECTOR);
664
- next !== navbar && (navbar && resizeObserver.unobserve(navbar), navbar = next, navbar ? (resizeObserver.observe(navbar), setHeight(navbar.getBoundingClientRect().height)) : setHeight(null));
665
- }, mutationObserver = new MutationObserver(observeNavbar);
666
- return mutationObserver.observe(root, {
667
- childList: !0,
668
- subtree: !0
669
- }), observeNavbar(), () => {
670
- mutationObserver.disconnect(), resizeObserver.disconnect();
671
- };
672
- }, t1 = [ref], $[0] = ref, $[1] = t0, $[2] = t1), useEffect(t0, t1), height;
673
- }
674
- /**
675
- * `useDeferredValue` with a transition type: the returned value lags behind
676
- * `value` like a deferred one — rendered in a transition, which React
677
- * animates — and the view transition React starts for it carries the given
678
- * type (`document.startViewTransition({types})`), so a stylesheet tells it
679
- * from any other transition on the page through
680
- * `:active-view-transition-type()`.
890
+ * Persisting is the business of one tab at a time: the one holding the Web
891
+ * Lock, which it keeps for as long as it lives. The others queue for it, so
892
+ * the next tab takes over as the holder goes away, and a tab that takes
893
+ * over persists right away, in case the last one left with a write pending.
894
+ * Writes happen when the browser is idle — a run of edits is written once —
895
+ * and as the page hides, so that nothing pending is lost.
681
896
  *
682
- * React only takes types from `startTransition`, and a deferred render is
683
- * not one. So while the deferred value lags, a transition of the type joins
684
- * it, with a state update of its own for the type to belong to: React renders
685
- * every pending transition in one go, and the commit — the view transition
686
- * — carries the types of all of them. The value itself stays with
687
- * `useDeferredValue`, which reads the latest `value` as the deferred render
688
- * runs: should `value` change back before that render commits, nothing
689
- * changes and nothing animates — a queued state update would commit the
690
- * stale value and animate it, only to animate back.
897
+ * Returns what stops the sync: it lets go of the lock, which hands the
898
+ * persisting on, and closes the channel.
691
899
  *
692
900
  * @internal
693
901
  */
694
- function useTypedDeferredValue(value, type) {
695
- let $ = c(5), deferred = useDeferredValue(value), [, setTransitions] = useState(0), t0, t1;
696
- return $[0] !== deferred || $[1] !== type || $[2] !== value ? (t0 = () => {
697
- Object.is(deferred, value) || startTransition(() => {
698
- addTransitionType(type), setTransitions(_temp);
902
+ function syncThemer(actorRef, options) {
903
+ let { persist, openChannel = openBroadcastChannel, schedule = scheduleIdle } = options, channel = openChannel(), locks = channel ? "locks" in options ? options.locks : browserLocks() : void 0, id = tabId(), abort = new AbortController(), leader = !1, cancelPersist, applying = !1, shared = selectStoredState(actorRef.getSnapshot()), revision = 0, origin = id, unchanged = !0, unanswered = /* @__PURE__ */ new Set(), isNewer = (message) => message.revision > revision || message.revision === revision && message.from > origin, persistNow = () => {
904
+ cancelPersist?.(), cancelPersist = void 0, persist(actorRef.getPersistedSnapshot());
905
+ }, persistSoon = () => {
906
+ cancelPersist?.(), cancelPersist = schedule(persistNow);
907
+ }, answer = (to) => {
908
+ channel?.post({
909
+ type: "state",
910
+ state: shared,
911
+ revision,
912
+ from: origin,
913
+ to
699
914
  });
700
- }, t1 = [
701
- deferred,
702
- type,
703
- value
704
- ], $[0] = deferred, $[1] = type, $[2] = value, $[3] = t0, $[4] = t1) : (t0 = $[3], t1 = $[4]), useEffect(t0, t1), deferred;
705
- }
706
- function _temp(count) {
707
- return count + 1;
915
+ }, becomeLeader = () => {
916
+ leader = !0, persistNow();
917
+ for (let from of unanswered) answer(from);
918
+ unanswered.clear();
919
+ };
920
+ locks ? locks.request("sanity-themer:persist", { signal: abort.signal }, () => (becomeLeader(), new Promise((resolve) => {
921
+ abort.signal.addEventListener("abort", () => resolve(), { once: !0 });
922
+ }))).catch(() => {}) : becomeLeader();
923
+ let subscription = actorRef.subscribe((snapshot) => {
924
+ if (leader && persistSoon(), applying) return;
925
+ let state = selectStoredState(snapshot);
926
+ dequal(state, shared) || (shared = state, unchanged = !1, revision += 1, origin = id, channel?.post({
927
+ type: "state",
928
+ state,
929
+ revision,
930
+ from: id
931
+ }));
932
+ });
933
+ channel && (channel.listen((data) => {
934
+ let message = parseMessage(data);
935
+ if (message) {
936
+ if (message.type === "hello") {
937
+ leader ? answer(message.from) : unanswered.add(message.from);
938
+ return;
939
+ }
940
+ if (message.to !== void 0) {
941
+ if (message.to !== id) return;
942
+ if (!unchanged) {
943
+ if (origin === id) {
944
+ revision = Math.max(revision, message.revision) + 1, channel.post({
945
+ type: "state",
946
+ state: shared,
947
+ revision,
948
+ from: id
949
+ });
950
+ return;
951
+ }
952
+ if (!isNewer(message)) return;
953
+ }
954
+ } else if (!isNewer(message)) return;
955
+ if (revision = message.revision, origin = message.from, !dequal(message.state, shared)) {
956
+ shared = message.state, unchanged = !1, applying = !0;
957
+ try {
958
+ actorRef.send({
959
+ type: "themes.sync",
960
+ state: message.state
961
+ });
962
+ } finally {
963
+ applying = !1;
964
+ }
965
+ }
966
+ }
967
+ }), channel.post({
968
+ type: "hello",
969
+ from: id
970
+ }));
971
+ let flush = () => {
972
+ leader && cancelPersist && persistNow();
973
+ }, page = typeof window > "u" ? void 0 : window;
974
+ return page?.addEventListener("pagehide", flush), () => {
975
+ subscription.unsubscribe(), page?.removeEventListener("pagehide", flush), flush(), abort.abort(), channel?.close();
976
+ };
708
977
  }
709
- var layout = "_1yq4ar78", layoutTransitionType = "sanity-themer", panelTransitionClasses = {
710
- slideIn: "_1yq4ar76",
711
- slideOut: "_1yq4ar77"
712
- }, splitTransitionClasses = {
713
- resize: "_1yq4ar70",
714
- crossfade: "_1yq4ar71",
715
- slideIn: "_1yq4ar72",
716
- slideOut: "_1yq4ar73",
717
- dropIn: "_1yq4ar74",
718
- dropOut: "_1yq4ar75"
719
- }, studioScheme = {
720
- light: "_1yq4ar79",
721
- dark: "_1yq4ar7a"
722
- };
723
978
  /**
724
- * Below this media index the Studio collapses its navbar into a drawer, and
725
- * the themer follows suit: the sidebar covers the Studio instead of standing
726
- * next to it, and the split preview stacks instead of sitting side by side.
979
+ * Persists the machine's snapshot for the next session, and keeps the themes
980
+ * in step with the other tabs of the Studio — from the one tab holding the
981
+ * Web Lock, when the browser is idle; see `sync.ts`. Rendered by the layout,
982
+ * outside the sidebar's `Activity`, so that it runs while the sidebar is
983
+ * closed too: other tabs change the themes whether this one shows them or
984
+ * not.
727
985
  */
728
- const ResizableSidebar = lazy(() => import("./ResizableSidebar-DD787kQr.js"));
729
- function sameStoredState(a, b) {
730
- return a.active === b.active && a.custom === b.custom && a.removed === b.removed && a.order === b.order;
731
- }
732
- function selectView(snapshot) {
733
- let { editing } = snapshot.context;
734
- return snapshot.matches({ flow: "edit" }) && editing ? {
735
- name: "edit",
736
- slug: editing.slug,
737
- focusTitle: editing.focusTitle
738
- } : snapshot.matches({ flow: "removed" }) ? { name: "removed" } : { name: "list" };
739
- }
740
- function sameView(a, b) {
741
- return a.name === b.name ? a.name !== "edit" || b.name !== "edit" || a.slug === b.slug && a.focusTitle === b.focusTitle : !1;
986
+ function PersistActorSnapshot(t0) {
987
+ let $ = c(3), { actorRef } = t0, t1, t2;
988
+ return $[0] === actorRef ? (t1 = $[1], t2 = $[2]) : (t1 = () => syncThemer(actorRef, { persist: writePersistedSnapshot }), t2 = [actorRef], $[0] = actorRef, $[1] = t1, $[2] = t2), useEffect(t1, t2), null;
742
989
  }
743
990
  /**
744
- * Wraps the whole Studio so that the theme picked in the themer sidebar
745
- * applies everywhere while the user browses around, runs the themer machine
746
- * that the navbar toggle and the sidebar share — the user's themes, which one
747
- * is applied, which flow the sidebar is in and how the Studio is previewed —
748
- * and renders the sidebar next to the Studio. The sidebar sits at the
749
- * `layout` level rather than in `activeToolLayout` because the split preview
750
- * renders the Studio twice, and the sidebar must not come along.
991
+ * The sidebar — everything in it, from the theme list to the snippet dialog —
992
+ * loads the first time it opens. There is no `Suspense` boundary around it,
993
+ * so it must only ever mount from a transition: the navbar toggle prerenders
994
+ * it in one as the pointer comes near, which keeps the Studio as it was while
995
+ * the code loads, where an urgent render would suspend up to the Studio's
996
+ * own boundary and swap the whole Studio for its loading screen.
997
+ */
998
+ const ResizableSidebar = lazy(() => import("./ResizableSidebar-C1nr3Ls2.js"));
999
+ /**
1000
+ * Wraps the whole Studio to run the themer machine that the sidebar's flows
1001
+ * share — the user's themes, which one is applied and which flow the sidebar
1002
+ * is in — and renders the sidebar next to the Studio. The sidebar sits at
1003
+ * the `layout` level rather than in `activeToolLayout` because the split
1004
+ * preview renders the Studio twice, and the sidebar must not come along.
751
1005
  *
752
- * The Studio next to the sidebar always follows the appearance setting
753
- * (light/dark/system) and the picked theme like any other theme would. The
754
- * split preview adds a second copy in the opposite scheme on the far side —
755
- * or on top, on small screens — through React's view transitions (React
756
- * 19.3), styled in `ThemerLayout.css.ts`.
1006
+ * Whether the sidebar is open and whether the Studio shows twice comes in as
1007
+ * props from the tool reducer in `ThemerProvider`, and goes on to the sidebar
1008
+ * as props along with the machine's actor, which the sidebar selects its
1009
+ * themes and flow from (see `ThemerProps`): the navbar toggle and the sidebar
1010
+ * dispatch to the reducer inside transitions tagged with a transition type
1011
+ * (see `addThemerTransitionType`), and the `ViewTransition` boundaries below
1012
+ * pick their classes by that type, so React animates the sidebar sliding in
1013
+ * and out, the split copy coming and going and a picked theme cross-fading
1014
+ * in (React 19.3), styled in `ViewTransitions.css.ts`. The sidebar and the
1015
+ * split copy stay mounted while hidden, in `Activity`, so that opening them
1016
+ * again is instant and they can prerender ahead of the transition. The
1017
+ * navbar toggle, which the Studio renders somewhere in `children`, reads the
1018
+ * same values and the plugin's options from the contexts
1019
+ * `ThemerNavbarProvider` provides in each copy of the Studio, and measures
1020
+ * the Studio navbar for the sidebar's header — in the Studio proper, not in
1021
+ * the split copy, which `ToolShouldDetectNavbarHeightContext` tells apart.
757
1022
  *
758
- * The machine says what shows and when the layout is in motion (its `panel`,
759
- * `split` and `moving` tags); the layout defers what shows, which puts the
760
- * panel's and the copy's mounts in a transition — what lets React animate
761
- * them — and picks the view transition classes from the tags.
1023
+ * The applied theme is the reducer's, provided above this layout in
1024
+ * `ThemerProvider`: the machine publishes it synchronously, which React does
1025
+ * not animate, so `ThemerThemeCrossfader` hands it to the reducer in a
1026
+ * transition of its own. The Studio next to the sidebar always follows the
1027
+ * appearance setting (light/dark/system) and the picked theme like any other
1028
+ * theme would; the split preview adds a second copy in the opposite scheme
1029
+ * on the far side.
762
1030
  *
763
1031
  * Whatever else on the page has a `view-transition-name` — an avatar the
764
1032
  * Studio names so it moves as one piece — comes along in step: every
765
1033
  * transition the layout starts carries `layoutTransitionType`, which the
766
1034
  * stylesheet keys on to move every group in the layout's time (see
767
- * `ThemerLayout.css.ts`).
1035
+ * `ViewTransitions.css.ts`).
768
1036
  *
769
1037
  * @internal
770
1038
  */
771
- function ThemerLayout(props) {
772
- let $ = c(87), { baseOptions, ...layoutProps } = props, t0;
773
- $[0] === baseOptions ? t0 = $[1] : (t0 = () => ({
774
- baseOptions,
775
- stored: readStoredState()
776
- }), $[0] = baseOptions, $[1] = t0);
777
- let [input] = useState(t0), t1;
778
- $[2] === input ? t1 = $[3] : (t1 = { input }, $[2] = input, $[3] = t1);
779
- let [snapshot, send, actorRef] = useActor(themerMachine, t1), stored = useSelector(actorRef, selectStoredState, sameStoredState), view = useSelector(actorRef, selectView, sameView), t2;
780
- $[4] === snapshot ? t2 = $[5] : (t2 = snapshot.hasTag("panel"), $[4] = snapshot, $[5] = t2);
781
- let open = t2, t3;
782
- $[6] === snapshot ? t3 = $[7] : (t3 = snapshot.hasTag("split"), $[6] = snapshot, $[7] = t3);
783
- let split = t3, t4;
784
- $[8] === snapshot ? t4 = $[9] : (t4 = snapshot.hasTag("moving"), $[8] = snapshot, $[9] = t4);
785
- let moving = t4, t5;
786
- $[10] === snapshot ? t5 = $[11] : (t5 = snapshot.hasTag("switching"), $[10] = snapshot, $[11] = t5);
787
- let switching = t5, { images } = snapshot.context, mobile = useMediaIndex() <= 1, studioRef = useRef(null), navbarHeight = useStudioNavbarHeight(studioRef), reduceMotion = usePrefersReducedMotion$1(), deferredOpen = useTypedDeferredValue(reduceMotion ? null : open, layoutTransitionType), deferredSplit = useTypedDeferredValue(reduceMotion ? null : split, layoutTransitionType), shownOpen = deferredOpen ?? open, shownSplit = deferredSplit ?? split, [panelShown, setPanelShown] = useState(!1), panelWanted = open || panelShown, panelMounted = useTypedDeferredValue(panelWanted, layoutTransitionType);
788
- panelMounted && !panelShown && setPanelShown(!0);
789
- let loading = panelWanted !== panelMounted, t6;
790
- $[12] !== baseOptions || $[13] !== stored ? (t6 = resolveThemes(stored, baseOptions), $[12] = baseOptions, $[13] = stored, $[14] = t6) : t6 = $[14];
791
- let { themes, removed, active } = t6, activeOptions = active.source === "config" ? null : active.options, t7;
792
- $[15] === activeOptions ? t7 = $[16] : (t7 = activeOptions === null ? null : buildTheme(activeOptions), $[15] = activeOptions, $[16] = t7);
793
- let theme = t7, deferredTheme = useTypedDeferredValue(reduceMotion ? void 0 : theme, layoutTransitionType), shownTheme = deferredTheme === void 0 ? theme : deferredTheme, scheme = useColorSchemeValue(), oppositeScheme = scheme === "dark" ? "light" : "dark", t8;
794
- $[17] !== scheme || $[18] !== shownTheme?.v2?.color ? (t8 = () => {
795
- let background = shownTheme?.v2?.color[scheme].default.bg;
796
- if (background === void 0) return;
797
- let { style } = document.body, previous = style.backgroundColor;
798
- return style.backgroundColor = background, () => {
799
- style.backgroundColor = previous;
800
- };
801
- }, $[17] = scheme, $[18] = shownTheme?.v2?.color, $[19] = t8) : t8 = $[19];
802
- let t9;
803
- $[20] !== scheme || $[21] !== shownTheme ? (t9 = [scheme, shownTheme], $[20] = scheme, $[21] = shownTheme, $[22] = t9) : t9 = $[22], useEffect(t8, t9);
804
- let t10, t11;
805
- $[23] === actorRef ? (t10 = $[24], t11 = $[25]) : (t10 = () => () => {
806
- for (let url of Object.values(actorRef.getSnapshot().context.images)) URL.revokeObjectURL(url);
807
- }, t11 = [actorRef], $[23] = actorRef, $[24] = t10, $[25] = t11), useEffect(t10, t11);
808
- let t12;
809
- $[26] !== active || $[27] !== baseOptions || $[28] !== images || $[29] !== loading || $[30] !== mobile || $[31] !== navbarHeight || $[32] !== open || $[33] !== removed || $[34] !== send || $[35] !== split || $[36] !== themes || $[37] !== view ? (t12 = {
810
- baseOptions,
811
- themes,
812
- removed,
813
- active,
814
- images,
815
- view,
1039
+ function ThemerLayout(t0) {
1040
+ let $ = c(50), { children, backgroundColor, config, dispatch, navbarHeight, open, persistedSnapshot, prerender, prerenderSplitScreen, scheme, split } = t0, t1;
1041
+ $[0] === persistedSnapshot ? t1 = $[1] : (t1 = { snapshot: persistedSnapshot }, $[0] = persistedSnapshot, $[1] = t1);
1042
+ let actorRef = useActorRef(themerMachine, t1), oppositeScheme = scheme === "dark" ? "light" : "dark", t2;
1043
+ $[2] === backgroundColor ? t2 = $[3] : (t2 = { backgroundColor }, $[2] = backgroundColor, $[3] = t2);
1044
+ let t3 = split && open ? "visible" : "hidden", t4;
1045
+ $[4] !== children || $[5] !== config || $[6] !== dispatch || $[7] !== open || $[8] !== oppositeScheme || $[9] !== prerenderSplitScreen || $[10] !== split ? (t4 = prerenderSplitScreen && /* @__PURE__ */ jsx(StudioPreview, {
1046
+ borderRight: !0,
1047
+ config,
1048
+ dispatch,
816
1049
  open,
817
- loading,
1050
+ scheme: oppositeScheme,
818
1051
  split,
819
- mobile,
1052
+ children
1053
+ }), $[4] = children, $[5] = config, $[6] = dispatch, $[7] = open, $[8] = oppositeScheme, $[9] = prerenderSplitScreen, $[10] = split, $[11] = t4) : t4 = $[11];
1054
+ let t5;
1055
+ $[12] === t4 ? t5 = $[13] : (t5 = /* @__PURE__ */ jsx(ViewTransition, {
1056
+ default: "none",
1057
+ enter: viewTransitionClasses.splitscreen.enter,
1058
+ update: viewTransitionClasses.splitscreen.update,
1059
+ exit: viewTransitionClasses.splitscreen.exit,
1060
+ children: t4
1061
+ }), $[12] = t4, $[13] = t5);
1062
+ let t6;
1063
+ $[14] !== t3 || $[15] !== t5 ? (t6 = /* @__PURE__ */ jsx(Activity, {
1064
+ mode: t3,
1065
+ children: t5
1066
+ }), $[14] = t3, $[15] = t5, $[16] = t6) : t6 = $[16];
1067
+ let t7;
1068
+ $[17] !== children || $[18] !== config || $[19] !== dispatch || $[20] !== open || $[21] !== split ? (t7 = /* @__PURE__ */ jsx(ViewTransition, {
1069
+ default: "none",
1070
+ update: viewTransitionClasses.studio.update,
1071
+ children: /* @__PURE__ */ jsx(StudioPreview, {
1072
+ config,
1073
+ dispatch,
1074
+ open,
1075
+ shouldDetectNavbarHeight: !0,
1076
+ split,
1077
+ children
1078
+ })
1079
+ }), $[17] = children, $[18] = config, $[19] = dispatch, $[20] = open, $[21] = split, $[22] = t7) : t7 = $[22];
1080
+ let t8 = open ? "visible" : "hidden", t9;
1081
+ $[23] !== actorRef || $[24] !== dispatch || $[25] !== navbarHeight || $[26] !== prerender || $[27] !== split ? (t9 = prerender && /* @__PURE__ */ jsx(ResizableSidebar, {
1082
+ actorRef,
1083
+ dispatch,
820
1084
  navbarHeight,
821
- send
822
- }, $[26] = active, $[27] = baseOptions, $[28] = images, $[29] = loading, $[30] = mobile, $[31] = navbarHeight, $[32] = open, $[33] = removed, $[34] = send, $[35] = split, $[36] = themes, $[37] = view, $[38] = t12) : t12 = $[38];
823
- let context = t12, studio = layoutProps.renderDefault(layoutProps), t13;
824
- $[39] === send ? t13 = $[40] : (t13 = () => send({ type: "layout.transitioned" }), $[39] = send, $[40] = t13);
825
- let transitioned = t13, t14;
826
- $[41] !== mobile || $[42] !== moving || $[43] !== reduceMotion || $[44] !== switching ? (t14 = reduceMotion ? {
827
- copyEnter: "none",
828
- copyExit: "none",
829
- panelEnter: "none",
830
- panelExit: "none",
831
- update: "none",
832
- studioUpdate: "none"
833
- } : {
834
- copyEnter: mobile ? splitTransitionClasses.dropIn : splitTransitionClasses.slideIn,
835
- copyExit: mobile ? splitTransitionClasses.dropOut : splitTransitionClasses.slideOut,
836
- panelEnter: panelTransitionClasses.slideIn,
837
- panelExit: panelTransitionClasses.slideOut,
838
- update: switching ? splitTransitionClasses.crossfade : "none",
839
- studioUpdate: moving ? splitTransitionClasses.resize : switching ? splitTransitionClasses.crossfade : "none"
840
- }, $[41] = mobile, $[42] = moving, $[43] = reduceMotion, $[44] = switching, $[45] = t14) : t14 = $[45];
841
- let classes = t14, t15 = mobile ? "column" : "row", t16;
842
- $[46] !== classes.copyEnter || $[47] !== classes.copyExit || $[48] !== classes.update || $[49] !== mobile || $[50] !== oppositeScheme || $[51] !== shownOpen || $[52] !== shownSplit || $[53] !== shownTheme || $[54] !== studio || $[55] !== transitioned ? (t16 = shownOpen && /* @__PURE__ */ jsx(Activity, {
843
- mode: shownSplit ? "visible" : "hidden",
844
- children: /* @__PURE__ */ jsx(ViewTransition, {
845
- enter: classes.copyEnter,
846
- exit: classes.copyExit,
847
- onEnter: transitioned,
848
- onExit: transitioned,
849
- onUpdate: transitioned,
850
- update: classes.update,
851
- children: /* @__PURE__ */ jsx(StudioPreview, {
852
- borderBottom: mobile,
853
- borderRight: !mobile,
854
- scheme: oppositeScheme,
855
- theme: shownTheme,
856
- children: studio
857
- })
858
- }, "opposite")
859
- }), $[46] = classes.copyEnter, $[47] = classes.copyExit, $[48] = classes.update, $[49] = mobile, $[50] = oppositeScheme, $[51] = shownOpen, $[52] = shownSplit, $[53] = shownTheme, $[54] = studio, $[55] = transitioned, $[56] = t16) : t16 = $[56];
860
- let t17;
861
- $[57] !== shownTheme || $[58] !== studio ? (t17 = /* @__PURE__ */ jsx(StudioPreview, {
862
- ref: studioRef,
863
- theme: shownTheme,
864
- children: studio
865
- }), $[57] = shownTheme, $[58] = studio, $[59] = t17) : t17 = $[59];
866
- let t18;
867
- $[60] !== classes.studioUpdate || $[61] !== t17 || $[62] !== transitioned ? (t18 = /* @__PURE__ */ jsx(ViewTransition, {
868
- onUpdate: transitioned,
869
- update: classes.studioUpdate,
870
- children: t17
871
- }, "primary"), $[60] = classes.studioUpdate, $[61] = t17, $[62] = transitioned, $[63] = t18) : t18 = $[63];
872
- let t19 = shownOpen ? "visible" : "hidden", t20 = shownTheme ?? void 0, t21;
873
- $[64] !== mobile || $[65] !== panelMounted ? (t21 = panelMounted && /* @__PURE__ */ jsx(ResizableSidebar, { overlay: mobile }), $[64] = mobile, $[65] = panelMounted, $[66] = t21) : t21 = $[66];
874
- let t22;
875
- $[67] !== t20 || $[68] !== t21 ? (t22 = /* @__PURE__ */ jsx(ThemeProvider, {
876
- theme: t20,
877
- children: t21
878
- }), $[67] = t20, $[68] = t21, $[69] = t22) : t22 = $[69];
879
- let t23;
880
- $[70] !== classes.panelEnter || $[71] !== classes.panelExit || $[72] !== classes.update || $[73] !== t22 || $[74] !== transitioned ? (t23 = /* @__PURE__ */ jsx(ViewTransition, {
881
- enter: classes.panelEnter,
882
- exit: classes.panelExit,
883
- onEnter: transitioned,
884
- onExit: transitioned,
885
- onUpdate: transitioned,
886
- update: classes.update,
887
- children: t22
888
- }, "panel"), $[70] = classes.panelEnter, $[71] = classes.panelExit, $[72] = classes.update, $[73] = t22, $[74] = transitioned, $[75] = t23) : t23 = $[75];
889
- let t24;
890
- $[76] !== t19 || $[77] !== t23 ? (t24 = /* @__PURE__ */ jsx(Activity, {
891
- mode: t19,
892
- children: t23
893
- }), $[76] = t19, $[77] = t23, $[78] = t24) : t24 = $[78];
894
- let t25;
895
- $[79] !== t15 || $[80] !== t16 || $[81] !== t18 || $[82] !== t24 ? (t25 = /* @__PURE__ */ jsxs(Flex, {
1085
+ split
1086
+ }), $[23] = actorRef, $[24] = dispatch, $[25] = navbarHeight, $[26] = prerender, $[27] = split, $[28] = t9) : t9 = $[28];
1087
+ let t10;
1088
+ $[29] === t9 ? t10 = $[30] : (t10 = /* @__PURE__ */ jsx(ViewTransition, {
1089
+ default: "none",
1090
+ enter: viewTransitionClasses.sidebar.enter,
1091
+ update: viewTransitionClasses.sidebar.update,
1092
+ exit: viewTransitionClasses.sidebar.exit,
1093
+ children: t9
1094
+ }), $[29] = t9, $[30] = t10);
1095
+ let t11;
1096
+ $[31] !== t10 || $[32] !== t8 ? (t11 = /* @__PURE__ */ jsx(Activity, {
1097
+ mode: t8,
1098
+ children: t10
1099
+ }), $[31] = t10, $[32] = t8, $[33] = t11) : t11 = $[33];
1100
+ let t12;
1101
+ $[34] !== t11 || $[35] !== t2 || $[36] !== t6 || $[37] !== t7 ? (t12 = /* @__PURE__ */ jsxs(Flex, {
896
1102
  className: layout,
897
- direction: t15,
1103
+ direction: "row",
898
1104
  height: "fill",
899
1105
  sizing: "border",
1106
+ style: t2,
900
1107
  children: [
901
- t16,
902
- t18,
903
- t24
1108
+ t6,
1109
+ t7,
1110
+ t11
904
1111
  ]
905
- }), $[79] = t15, $[80] = t16, $[81] = t18, $[82] = t24, $[83] = t25) : t25 = $[83];
906
- let t26;
907
- return $[84] !== context || $[85] !== t25 ? (t26 = /* @__PURE__ */ jsx(ThemerContext.Provider, {
908
- value: context,
909
- children: t25
910
- }), $[84] = context, $[85] = t25, $[86] = t26) : t26 = $[86], t26;
1112
+ }), $[34] = t11, $[35] = t2, $[36] = t6, $[37] = t7, $[38] = t12) : t12 = $[38];
1113
+ let t13;
1114
+ $[39] !== actorRef || $[40] !== dispatch ? (t13 = /* @__PURE__ */ jsx(ThemerThemeCrossfader, {
1115
+ actorRef,
1116
+ dispatch
1117
+ }), $[39] = actorRef, $[40] = dispatch, $[41] = t13) : t13 = $[41];
1118
+ let t14, t15;
1119
+ $[42] === actorRef ? (t14 = $[43], t15 = $[44]) : (t14 = /* @__PURE__ */ jsx(RevokeImageUrls, { actorRef }), t15 = /* @__PURE__ */ jsx(PersistActorSnapshot, { actorRef }), $[42] = actorRef, $[43] = t14, $[44] = t15);
1120
+ let t16;
1121
+ return $[45] !== t12 || $[46] !== t13 || $[47] !== t14 || $[48] !== t15 ? (t16 = /* @__PURE__ */ jsxs(Fragment, { children: [
1122
+ t12,
1123
+ t13,
1124
+ t14,
1125
+ t15
1126
+ ] }), $[45] = t12, $[46] = t13, $[47] = t14, $[48] = t15, $[49] = t16) : t16 = $[49], t16;
911
1127
  }
912
1128
  /**
913
1129
  * One copy of the Studio, in the given color scheme — or, without one, in the
914
- * scheme the Studio is showing. The configured theme (`null`) goes through
915
- * the provider too, inheriting the Studio's, so picking a theme swaps it
916
- * instead of remounting the Studio under a new provider.
917
- *
918
- * The two copies of the split preview share the router, the document store
919
- * and every other provider above the layout — only the scheme differs — so
920
- * they stay in sync while navigating. The `color-scheme` of a forced scheme
921
- * keeps native form controls and scrollbars in step with it.
1130
+ * scheme the Studio is showing. The two copies of the split preview share the
1131
+ * router, the document store and every other provider above the layout —
1132
+ * only the scheme differs — so they stay in sync while navigating. The
1133
+ * `color-scheme` of a forced scheme keeps native form controls and scrollbars
1134
+ * in step with it. Each copy provides the navbar toggle it renders with the
1135
+ * contexts it reads (see `ThemerNavbarProvider`).
922
1136
  */
923
- function StudioPreview(props) {
924
- let $ = c(10), { borderBottom, borderRight, children, ref, scheme, theme } = props, t0 = theme ?? void 0, t1 = scheme ? studioScheme[scheme] : void 0, t2;
925
- $[0] !== borderBottom || $[1] !== borderRight || $[2] !== children || $[3] !== ref || $[4] !== t1 ? (t2 = /* @__PURE__ */ jsx(Card, {
926
- borderBottom,
1137
+ function StudioPreview(t0) {
1138
+ let $ = c(17), { borderRight, children, config, dispatch, open, scheme, shouldDetectNavbarHeight, split } = t0, [boundaryElement, setBoundaryElement] = useState(null), t1;
1139
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t1 = (boundaryElement_0) => (startTransition(() => setBoundaryElement(boundaryElement_0)), _temp), $[0] = t1) : t1 = $[0];
1140
+ let t2;
1141
+ $[1] === children ? t2 = $[2] : (t2 = /* @__PURE__ */ jsx(ViewTransition, {
1142
+ update: "none",
1143
+ children
1144
+ }), $[1] = children, $[2] = t2);
1145
+ let t3;
1146
+ $[3] !== config || $[4] !== dispatch || $[5] !== open || $[6] !== shouldDetectNavbarHeight || $[7] !== split || $[8] !== t2 ? (t3 = /* @__PURE__ */ jsx(ThemerNavbarProvider, {
1147
+ config,
1148
+ dispatch,
1149
+ open,
1150
+ shouldDetectNavbarHeight,
1151
+ split,
1152
+ children: t2
1153
+ }), $[3] = config, $[4] = dispatch, $[5] = open, $[6] = shouldDetectNavbarHeight, $[7] = split, $[8] = t2, $[9] = t3) : t3 = $[9];
1154
+ let t4;
1155
+ $[10] !== boundaryElement || $[11] !== t3 ? (t4 = /* @__PURE__ */ jsx(BoundaryElementProvider, {
1156
+ element: boundaryElement,
1157
+ children: t3
1158
+ }), $[10] = boundaryElement, $[11] = t3, $[12] = t4) : t4 = $[12];
1159
+ let t5;
1160
+ return $[13] !== borderRight || $[14] !== scheme || $[15] !== t4 ? (t5 = /* @__PURE__ */ jsx(Card, {
927
1161
  borderRight,
928
- className: t1,
929
1162
  flex: 1,
930
1163
  height: "fill",
931
1164
  overflow: "hidden",
932
- ref,
933
- children
934
- }), $[0] = borderBottom, $[1] = borderRight, $[2] = children, $[3] = ref, $[4] = t1, $[5] = t2) : t2 = $[5];
935
- let t3;
936
- return $[6] !== scheme || $[7] !== t0 || $[8] !== t2 ? (t3 = /* @__PURE__ */ jsx(ThemeProvider, {
937
1165
  scheme,
938
- theme: t0,
939
- children: t2
940
- }), $[6] = scheme, $[7] = t0, $[8] = t2, $[9] = t3) : t3 = $[9], t3;
1166
+ ref: t1,
1167
+ children: t4
1168
+ }), $[13] = borderRight, $[14] = scheme, $[15] = t4, $[16] = t5) : t5 = $[16], t5;
941
1169
  }
1170
+ function _temp() {}
942
1171
  /**
943
- * The colors the slices fill with, clockwise from 12 o'clock: the palette's
944
- * hues in rainbow order, like a real color swatch
1172
+ * Hands the applied theme to the tool reducer as the machine publishes it —
1173
+ * in a transition tagged to cross-fade while the machine says a theme is
1174
+ * being switched to, so that editing the applied theme's colors applies live
1175
+ * while picking another theme fades the Studio over to it
945
1176
  */
946
- const SLICE_COLORS = [
947
- red[500].hex,
948
- orange[500].hex,
949
- yellow[400].hex,
950
- green[500].hex,
951
- cyan[500].hex,
952
- blue[500].hex,
953
- purple[500].hex,
954
- magenta[500].hex
955
- ], TOTAL_ACTS = 2 + (1 - Math.cbrt(1 / 8)), ANIMATION_DURATION = .55 * TOTAL_ACTS;
956
- function actAt(progress) {
957
- return progress * TOTAL_ACTS;
958
- }
959
- /**
960
- * How far the wheel has turned at `progress` (0–1), in degrees.
961
- *
962
- * It stands still while the top half of the slices pop in. As the 6 o'clock
963
- * slice pops in it starts spinning — one full turn with a cubic ease-in, from
964
- * rest to top speed as the bottom half pop in — and it stands still again the
965
- * moment it is back upright: the clearing of {@link isSliceFilled} takes off
966
- * at the speed the spin reached, as if the wheel kept turning and eased out
967
- * over a second turn, so the two read as one movement.
968
- */
969
- function wheelRotation(progress) {
970
- let spin = actAt(progress) - 1;
971
- return spin <= 0 || spin >= 1 ? 0 : 360 * spin ** 3;
972
- }
973
- /**
974
- * Whether slice `index` (clockwise from 12 o'clock) shows its color at
975
- * `progress` (0–1). A slice pops in whole as the fill reaches its leading
976
- * edge, and pops out whole as the clearing does — stepped, never partial.
977
- *
978
- * The fill goes clockwise at a steady pace over the first two acts, half of
979
- * the slices in each. The clearing follows in the third act, the same way
980
- * round, on a cubic ease-out that starts at the speed the spin of
981
- * {@link wheelRotation} ends with: the slices at first pop out as quickly as
982
- * they were passing by, then ever more slowly.
983
- */
984
- function isSliceFilled(progress, index) {
985
- let act = actAt(progress);
986
- if (act * 8 / 2 <= index) return !1;
987
- let clearing = act - 2;
988
- return clearing <= 0 || (1 - (1 - clearing) ** 3) * 8 < index;
989
- }
990
- /**
991
- * The geometry of `ColorWheelIcon` from `@sanity/icons`: a ring between two
992
- * circles around the center of a 25×25 viewBox, cut into eight slices by the
993
- * spokes of its outline path
994
- */
995
- const CENTER = 12.5, INNER_RADIUS = 3.5;
996
- /** A point on a circle around the center, at `degrees` clockwise from 3 o'clock */
997
- function point(radius, degrees) {
998
- let radians = degrees * Math.PI / 180;
999
- return `${CENTER + radius * Math.cos(radians)} ${CENTER + radius * Math.sin(radians)}`;
1000
- }
1001
- /**
1002
- * The part of the ring a slice colors in: between its two spokes, from the
1003
- * inner circle out to the outer. The 12 o'clock slice is centered on 12, so
1004
- * it starts half a slice before it.
1005
- */
1006
- function slicePath(index) {
1007
- let start = index * 45 - 45 / 2 - 90, end = start + 45;
1008
- return [
1009
- `M${point(8, start)}`,
1010
- `A8 8 0 0 1 ${point(8, end)}`,
1011
- `L${point(INNER_RADIUS, end)}`,
1012
- `A${INNER_RADIUS} ${INNER_RADIUS} 0 0 0 ${point(INNER_RADIUS, start)}`,
1013
- "Z"
1014
- ].join("");
1015
- }
1016
- const SLICE_PATHS = SLICE_COLORS.map((_, index) => slicePath(index));
1017
- /**
1018
- * A filled shape, not a stroked one: the icon styles of `@sanity/ui` force
1019
- * `vector-effect: non-scaling-stroke` on every path in an icon, which fills
1020
- * are none the wiser about
1021
- */
1022
- function Slice(props) {
1023
- let $ = c(8), { index, progress } = props, t0;
1024
- $[0] === index ? t0 = $[1] : (t0 = (value) => +!!isSliceFilled(value, index), $[0] = index, $[1] = t0);
1025
- let opacity = useTransform(progress, t0), t1 = SLICE_PATHS[index], t2 = SLICE_COLORS[index], t3;
1026
- $[2] === opacity ? t3 = $[3] : (t3 = { opacity }, $[2] = opacity, $[3] = t3);
1027
- let t4;
1028
- return $[4] !== t1 || $[5] !== t2 || $[6] !== t3 ? (t4 = /* @__PURE__ */ jsx(motion.path, {
1029
- d: t1,
1030
- fill: t2,
1031
- style: t3
1032
- }), $[4] = t1, $[5] = t2, $[6] = t3, $[7] = t4) : t4 = $[7], t4;
1033
- }
1034
- /**
1035
- * The `ColorWheelIcon` of `@sanity/icons`, with an animation driven by
1036
- * `progress` (0–1): the slices pop in with color one at a time, clockwise
1037
- * from 12 o'clock like the segments of a classic spinner, the wheel spins once
1038
- * as the 6 o'clock slice pops in, and the colors pop out the same way round,
1039
- * carrying the spin's speed to a rest (see `colorWheel.ts`). At rest — at
1040
- * either end — it looks exactly like the icon it stands in for. With
1041
- * `prefers-reduced-motion: reduce` the wheel keeps still; only the colors come
1042
- * and go.
1043
- *
1044
- * @internal
1045
- */
1046
- function AnimatedColorWheelIcon(props) {
1047
- let $ = c(10), { progress } = props, prefersReducedMotion = usePrefersReducedMotion(), t0;
1048
- $[0] === prefersReducedMotion ? t0 = $[1] : (t0 = (value) => prefersReducedMotion ? 0 : wheelRotation(value), $[0] = prefersReducedMotion, $[1] = t0);
1049
- let rotate = useTransform(progress, t0), t1;
1050
- $[2] === rotate ? t1 = $[3] : (t1 = { rotate }, $[2] = rotate, $[3] = t1);
1051
- let t2;
1052
- $[4] === progress ? t2 = $[5] : (t2 = SLICE_COLORS.map((color, index) => /* @__PURE__ */ jsx(Slice, {
1053
- index,
1054
- progress
1055
- }, color)), $[4] = progress, $[5] = t2);
1056
- let t3;
1057
- $[6] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx("path", {
1058
- d: "M9.43853 5.10896L11.1606 9.26642M13.8394 15.7336L15.5615 19.891M15.7336 11.1606L19.891 9.43853M9.26642 13.8394L5.10896 15.5615M5.3139 9.52342L9.23359 11.147M15.7664 13.853L19.6861 15.4766M13.853 9.23359L15.4766 5.3139M9.52342 19.6861L11.147 15.7664M20.5 12.5C20.5 16.9183 16.9183 20.5 12.5 20.5C8.08172 20.5 4.5 16.9183 4.5 12.5C4.5 8.08172 8.08172 4.5 12.5 4.5C16.9183 4.5 20.5 8.08172 20.5 12.5ZM16 12.5C16 14.433 14.433 16 12.5 16C10.567 16 9 14.433 9 12.5C9 10.567 10.567 9 12.5 9C14.433 9 16 10.567 16 12.5Z",
1059
- stroke: "currentColor",
1060
- strokeLinejoin: "round",
1061
- strokeWidth: 1.2
1062
- }), $[6] = t3) : t3 = $[6];
1063
- let t4;
1064
- return $[7] !== t1 || $[8] !== t2 ? (t4 = /* @__PURE__ */ jsxs(motion.svg, {
1065
- "data-sanity-icon": "color-wheel",
1066
- fill: "none",
1067
- height: "1em",
1068
- style: t1,
1069
- viewBox: "0 0 25 25",
1070
- width: "1em",
1071
- xmlns: "http://www.w3.org/2000/svg",
1072
- children: [t2, t3]
1073
- }), $[7] = t1, $[8] = t2, $[9] = t4) : t4 = $[9], t4;
1074
- }
1075
- /**
1076
- * Starts loading the sidebar's code as the toggle is about to be pressed —
1077
- * the same import `React.lazy` makes, which then waits for this one
1078
- */
1079
- function preloadSidebar() {
1080
- import("./ResizableSidebar-DD787kQr.js").catch(() => {});
1081
- }
1082
- /**
1083
- * Opens or closes the sidebar — and notes the visit, which is what stops the
1084
- * navbar from introducing the tool
1085
- */
1086
- function toggleSidebar(send) {
1087
- markVisited(), send({ type: "sidebar.toggle" });
1088
- }
1089
- /**
1090
- * Whether the navbar has had its one chance per page load to introduce the
1091
- * tool. Module state rather than component state on purpose: the button
1092
- * remounts with the navbar (switching workspaces, crossing the narrow-screen
1093
- * breakpoint) and renders once per Studio copy in the split preview, and none
1094
- * of those should earn another introduction — only loading the page again does.
1095
- */
1096
- let introduced = !1;
1097
- /**
1098
- * The topbar toggle, with an icon that plays its color wheel animation: on
1099
- * hovering the button, every time; on hovering the navbar it sits in, once —
1100
- * a "hey, look, new tool!" for anyone who has never opened the sidebar, which
1101
- * the visit noted by {@link toggleSidebar} ends for good; and lap after lap
1102
- * while the sidebar's code loads, the way a spinner would.
1103
- */
1104
- function ThemerNavbarButton() {
1105
- let $ = c(26), { open, loading, send } = useThemer(), progress = useMotionValue(0), [button, setButton] = useState(null), t0;
1106
- $[0] === progress ? t0 = $[1] : (t0 = () => {
1107
- progress.isAnimating() || animate(progress, [0, 1], {
1108
- duration: ANIMATION_DURATION,
1109
- ease: "linear"
1110
- });
1111
- }, $[0] = progress, $[1] = t0);
1112
- let play = t0, t1, t2;
1113
- $[2] !== loading || $[3] !== progress ? (t1 = () => {
1114
- if (!loading) return;
1115
- let looping = !0, lap = (from) => {
1116
- animate(progress, [from, 1], {
1117
- duration: (1 - from) * ANIMATION_DURATION,
1118
- ease: "linear",
1119
- onComplete: () => {
1120
- looping && lap(0);
1121
- }
1177
+ function ThemerThemeCrossfader(t0) {
1178
+ let $ = c(4), { actorRef, dispatch } = t0, t1, t2;
1179
+ return $[0] !== actorRef || $[1] !== dispatch ? (t1 = () => {
1180
+ let subscription = actorRef.subscribe((snapshot) => {
1181
+ let switching = snapshot.hasTag("switching"), theme = resolveActiveThemeOptions(snapshot.context);
1182
+ startTransition(() => {
1183
+ switching && addThemerTransitionType("crossfade"), dispatch(theme ? {
1184
+ type: "set-theme",
1185
+ theme
1186
+ } : { type: "unset-theme" });
1122
1187
  });
1123
- };
1124
- return lap(progress.get()), () => {
1125
- looping = !1;
1126
- };
1127
- }, t2 = [loading, progress], $[2] = loading, $[3] = progress, $[4] = t1, $[5] = t2) : (t1 = $[4], t2 = $[5]), useEffect(t1, t2);
1128
- let t3, t4;
1129
- $[6] === progress ? (t3 = $[7], t4 = $[8]) : (t3 = () => () => progress.stop(), t4 = [progress], $[6] = progress, $[7] = t3, $[8] = t4), useEffect(t3, t4);
1130
- let t5, t6;
1131
- $[9] !== button || $[10] !== play ? (t5 = () => {
1132
- let navbar = button?.closest(NAVBAR_SELECTOR);
1133
- if (!navbar || introduced) return;
1134
- let introduce = () => {
1135
- introduced || (introduced = !0, hasVisited() || play());
1136
- };
1137
- return navbar.addEventListener("mouseenter", introduce, { once: !0 }), () => navbar.removeEventListener("mouseenter", introduce);
1138
- }, t6 = [button, play], $[9] = button, $[10] = play, $[11] = t5, $[12] = t6) : (t5 = $[11], t6 = $[12]), useEffect(t5, t6);
1139
- let t7;
1140
- $[13] === Symbol.for("react.memo_cache_sentinel") ? (t7 = /* @__PURE__ */ jsx(Text, {
1141
- size: 1,
1142
- children: "Themer"
1143
- }), $[13] = t7) : t7 = $[13];
1144
- let t8;
1145
- $[14] === progress ? t8 = $[15] : (t8 = /* @__PURE__ */ jsx(AnimatedColorWheelIcon, { progress }), $[14] = progress, $[15] = t8);
1146
- let t9;
1147
- $[16] === send ? t9 = $[17] : (t9 = () => toggleSidebar(send), $[16] = send, $[17] = t9);
1148
- let t10;
1149
- $[18] === play ? t10 = $[19] : (t10 = () => {
1150
- preloadSidebar(), play();
1151
- }, $[18] = play, $[19] = t10);
1152
- let t11;
1153
- return $[20] !== loading || $[21] !== open || $[22] !== t10 || $[23] !== t8 || $[24] !== t9 ? (t11 = /* @__PURE__ */ jsx(Tooltip, {
1154
- animate: !0,
1155
- content: t7,
1156
- portal: !0,
1157
- children: /* @__PURE__ */ jsx(Button, {
1158
- "aria-busy": loading,
1159
- "aria-label": "Themer",
1160
- disabled: loading,
1161
- icon: t8,
1162
- mode: "bleed",
1163
- onClick: t9,
1164
- onFocus: preloadSidebar,
1165
- onMouseEnter: t10,
1166
- padding: 2,
1167
- ref: setButton,
1168
- selected: open
1169
- })
1170
- }), $[20] = loading, $[21] = open, $[22] = t10, $[23] = t8, $[24] = t9, $[25] = t11) : t11 = $[25], t11;
1188
+ });
1189
+ return () => subscription.unsubscribe();
1190
+ }, t2 = [actorRef, dispatch], $[0] = actorRef, $[1] = dispatch, $[2] = t1, $[3] = t2) : (t1 = $[2], t2 = $[3]), useEffect(t1, t2), null;
1191
+ }
1192
+ function RevokeImageUrls(t0) {
1193
+ let $ = c(3), { actorRef } = t0, t1, t2;
1194
+ return $[0] === actorRef ? (t1 = $[1], t2 = $[2]) : (t1 = () => () => {
1195
+ for (let url of Object.values(actorRef.getSnapshot().context.images)) URL.revokeObjectURL(url);
1196
+ }, t2 = [actorRef], $[0] = actorRef, $[1] = t1, $[2] = t2), useEffect(t1, t2), null;
1171
1197
  }
1172
1198
  /**
1173
- * Adds the toggle that opens and closes the themer sidebar to the Studio
1174
- * navbar — an icon button with a tooltip in the top bar (like the Tasks
1175
- * toggle), and a regular titled action in the narrow-screen sidebar menu.
1176
- *
1177
- * In the split preview every Studio copy renders its own navbar, so the
1178
- * toggle shows up in both — they drive the same sidebar.
1179
- *
1180
- * @internal
1199
+ * Keep this component extremely lightweight, and only include state that are needed by both
1200
+ * `studio.components.layout` and `studio.components.navbar`, where it's not possible to prop-drill into `studio.components.navbar`,
1201
+ * and that need to survive open/close of the tool
1202
+ * @TODO will mount on the future `studio.components.provider` entrypoint to ensure other plugins using `studio.components.layout` is correctly wrapped in the top-level theme regardless of plugin ordering
1181
1203
  */
1182
- function ThemerNavbar(props) {
1183
- let $ = c(9), { open, send } = useThemer(), t0;
1184
- $[0] === props.__internal_actions ? t0 = $[1] : (t0 = props.__internal_actions ?? [], $[0] = props.__internal_actions, $[1] = t0);
1185
- let t1;
1186
- if ($[2] !== open || $[3] !== props || $[4] !== send || $[5] !== t0) {
1187
- let t2;
1188
- $[7] === send ? t2 = $[8] : (t2 = () => toggleSidebar(send), $[7] = send, $[8] = t2), t1 = props.renderDefault({
1189
- ...props,
1190
- __internal_actions: [
1191
- ...t0,
1192
- {
1193
- location: "topbar",
1194
- name: "themer-topbar",
1195
- render: ThemerNavbarButton
1196
- },
1197
- {
1198
- icon: ColorWheelIcon,
1199
- location: "sidebar",
1200
- name: "themer-sidebar",
1201
- onAction: t2,
1202
- selected: open,
1203
- title: "Themer"
1204
- }
1205
- ]
1206
- }), $[2] = open, $[3] = props, $[4] = send, $[5] = t0, $[6] = t1;
1207
- } else t1 = $[6];
1208
- return t1;
1204
+ function ThemerProvider(t0) {
1205
+ let $ = c(18), { children, config } = t0;
1206
+ use(browser("The current theme is stored in localStorage."));
1207
+ let [persistedSnapshot] = useState(readPersistedSnapshot), t1;
1208
+ $[0] === persistedSnapshot ? t1 = $[1] : (t1 = (initialState) => {
1209
+ if (!persistedSnapshot) return initialState;
1210
+ let theme = resolveActiveThemeOptions(persistedSnapshot.context);
1211
+ return theme ? {
1212
+ ...initialState,
1213
+ theme
1214
+ } : initialState;
1215
+ }, $[0] = persistedSnapshot, $[1] = t1);
1216
+ let [state, dispatch] = useReducer(toolReducer, initialToolState, t1), scheme = useColorSchemeValue(), index = useMediaIndex(), isMobile = index <= 2, isTooSmallForSplitScreen = index <= 3, open = state.open && !isMobile, split = state.split && !isTooSmallForSplitScreen, t2;
1217
+ $[2] === state.theme ? t2 = $[3] : (t2 = (state.theme === null ? null : buildTheme(state.theme)) ?? void 0, $[2] = state.theme, $[3] = t2);
1218
+ let t3 = state.theme?.[scheme]?.background, t4;
1219
+ $[4] !== children || $[5] !== config || $[6] !== open || $[7] !== persistedSnapshot || $[8] !== scheme || $[9] !== split || $[10] !== state.navbarHeight || $[11] !== state.prerender || $[12] !== state.prerenderSplitScreen || $[13] !== t3 ? (t4 = /* @__PURE__ */ jsx(ThemerLayout, {
1220
+ config,
1221
+ backgroundColor: t3,
1222
+ dispatch,
1223
+ navbarHeight: state.navbarHeight,
1224
+ open,
1225
+ persistedSnapshot,
1226
+ prerender: state.prerender,
1227
+ prerenderSplitScreen: state.prerenderSplitScreen,
1228
+ scheme,
1229
+ split,
1230
+ children
1231
+ }), $[4] = children, $[5] = config, $[6] = open, $[7] = persistedSnapshot, $[8] = scheme, $[9] = split, $[10] = state.navbarHeight, $[11] = state.prerender, $[12] = state.prerenderSplitScreen, $[13] = t3, $[14] = t4) : t4 = $[14];
1232
+ let t5;
1233
+ return $[15] !== t2 || $[16] !== t4 ? (t5 = /* @__PURE__ */ jsx(ThemeProvider, {
1234
+ theme: t2,
1235
+ children: t4
1236
+ }), $[15] = t2, $[16] = t4, $[17] = t5) : t5 = $[17], t5;
1209
1237
  }
1210
1238
  /**
1211
1239
  * A Studio plugin that adds a themer sidebar for `buildTheme` themes: a
@@ -1236,22 +1264,27 @@ function ThemerNavbar(props) {
1236
1264
  * @alpha
1237
1265
  */
1238
1266
  const themerTool = definePlugin((options) => {
1239
- let baseOptions = options?.config ?? {};
1240
- function ThemerLayoutWithOptions(props) {
1241
- let $ = c(2), t0;
1242
- return $[0] === props ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(ThemerLayout, {
1243
- ...props,
1244
- baseOptions
1245
- }), $[0] = props, $[1] = t0), t0;
1246
- }
1267
+ let { title = "Themer" } = options ?? {};
1247
1268
  return {
1248
1269
  name: "@sanity/themer/tool",
1249
1270
  studio: { components: {
1250
- layout: ThemerLayoutWithOptions,
1271
+ layout: defineThemerLayout({ title }),
1251
1272
  navbar: ThemerNavbar
1252
1273
  } }
1253
1274
  };
1254
1275
  });
1276
+ function defineThemerLayout(config) {
1277
+ return function DefinedThemerLayoutProvider(props) {
1278
+ "use memo";
1279
+ let $ = c(4), t0;
1280
+ $[0] === props ? t0 = $[1] : (t0 = props.renderDefault(props), $[0] = props, $[1] = t0);
1281
+ let t1;
1282
+ return $[2] === t0 ? t1 = $[3] : (t1 = /* @__PURE__ */ jsx(ThemerProvider, {
1283
+ config,
1284
+ children: t0
1285
+ }), $[2] = t0, $[3] = t1), t1;
1286
+ };
1287
+ }
1255
1288
  export { themerTool };
1256
1289
 
1257
1290
  //# sourceMappingURL=tool.js.map