@sanity/themer 0.8.1 → 0.9.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,442 @@
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-Bxgltjmb.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 } 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-DEy-cy3X.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), 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] === Symbol.for("react.memo_cache_sentinel") ? (t6 = /* @__PURE__ */ jsx(ColorWheelIcon, {}), $[13] = t6) : t6 = $[13];
55
+ let t7;
56
+ $[14] === busy ? t7 = $[15] : (t7 = /* @__PURE__ */ jsx(Suspense, {
57
+ fallback: t6,
58
+ children: /* @__PURE__ */ jsx(AnimatedColorWheelIcon, {
59
+ busy,
60
+ ref: iconRef
61
+ })
62
+ }), $[14] = busy, $[15] = t7);
63
+ let t8 = open ? handleClose : handleOpen, t9 = open || busy, t10;
64
+ $[16] !== busy || $[17] !== handlePrerender || $[18] !== t5 || $[19] !== t7 || $[20] !== t8 || $[21] !== t9 || $[22] !== title ? (t10 = /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(ViewTransition, {
65
+ default: "none",
66
+ update: viewTransitionClasses.navbarButton.update,
67
+ children: /* @__PURE__ */ jsx(Button, {
68
+ ref: t5,
69
+ "aria-busy": busy,
70
+ "aria-label": title,
71
+ icon: t7,
72
+ mode: "bleed",
73
+ onClick: t8,
74
+ onFocus: handlePrerender,
75
+ onMouseEnter: handlePrerender,
76
+ padding: 2,
77
+ selected: t9
78
+ })
79
+ }) }), $[16] = busy, $[17] = handlePrerender, $[18] = t5, $[19] = t7, $[20] = t8, $[21] = t9, $[22] = title, $[23] = t10) : t10 = $[23];
80
+ let t11;
81
+ return $[24] !== t10 || $[25] !== t4 ? (t11 = /* @__PURE__ */ jsx(ViewTransition, {
82
+ default: "none",
83
+ update: viewTransitionClasses.navbarButtonTooltip.update,
84
+ children: /* @__PURE__ */ jsx(Tooltip, {
85
+ animate: !0,
86
+ content: t4,
87
+ portal: !0,
88
+ children: t10
89
+ })
90
+ }), $[24] = t10, $[25] = t4, $[26] = t11) : t11 = $[26], t11;
18
91
  }
19
- function sanitizeContrast(value) {
20
- return typeof value == "number" && Number.isFinite(value) ? Math.min(100, Math.max(15, value)) : null;
92
+ function ThemerNavbar(props) {
93
+ "use memo";
94
+ let $ = c(4), t0;
95
+ if ($[0] !== props) {
96
+ let button = {
97
+ location: "topbar",
98
+ name: "themer-topbar",
99
+ render: ThemerNavbarButton
100
+ }, t1;
101
+ $[2] === props.__internal_actions ? t1 = $[3] : (t1 = props.__internal_actions ?? [], $[2] = props.__internal_actions, $[3] = t1), t0 = props.renderDefault({
102
+ ...props,
103
+ __internal_actions: [...t1, button]
104
+ }), $[0] = props, $[1] = t0;
105
+ } else t0 = $[1];
106
+ return t0;
21
107
  }
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;
108
+ function ThemerNavbarProvider(t0) {
109
+ let $ = c(15), { children, config, dispatch, open, shouldDetectNavbarHeight, split } = t0, t1 = shouldDetectNavbarHeight ?? !1, t2;
110
+ $[0] !== children || $[1] !== t1 ? (t2 = /* @__PURE__ */ jsx(ToolShouldDetectNavbarHeightContext, {
111
+ value: t1,
112
+ children
113
+ }), $[0] = children, $[1] = t1, $[2] = t2) : t2 = $[2];
114
+ let t3;
115
+ $[3] !== split || $[4] !== t2 ? (t3 = /* @__PURE__ */ jsx(ToolSplitIsOpenContext, {
116
+ value: split,
117
+ children: t2
118
+ }), $[3] = split, $[4] = t2, $[5] = t3) : t3 = $[5];
119
+ let t4;
120
+ $[6] !== open || $[7] !== t3 ? (t4 = /* @__PURE__ */ jsx(ToolIsOpenContext, {
121
+ value: open,
122
+ children: t3
123
+ }), $[6] = open, $[7] = t3, $[8] = t4) : t4 = $[8];
124
+ let t5;
125
+ $[9] !== dispatch || $[10] !== t4 ? (t5 = /* @__PURE__ */ jsx(ToolDispatchContext, {
126
+ value: dispatch,
127
+ children: t4
128
+ }), $[9] = dispatch, $[10] = t4, $[11] = t5) : t5 = $[11];
129
+ let t6;
130
+ return $[12] !== config || $[13] !== t5 ? (t6 = /* @__PURE__ */ jsx(PluginConfigContext, {
131
+ value: config,
132
+ children: t5
133
+ }), $[12] = config, $[13] = t5, $[14] = t6) : t6 = $[14], t6;
134
+ }
135
+ function toolReducer(state, action) {
136
+ switch (action.type) {
137
+ case "set-navbar-height": return state.navbarHeight === action.height ? state : {
138
+ ...state,
139
+ navbarHeight: action.height
140
+ };
141
+ case "prerender": return state.prerender ? state : {
142
+ ...state,
143
+ prerender: !0
144
+ };
145
+ case "open": return state.open ? state : {
146
+ ...state,
147
+ open: !0,
148
+ prerender: !0
149
+ };
150
+ case "close": return state.open ? {
151
+ ...state,
152
+ open: !1
153
+ } : state;
154
+ case "split-screen:prerender": return state.prerenderSplitScreen ? state : {
155
+ ...state,
156
+ prerenderSplitScreen: !0
157
+ };
158
+ case "split-screen:open": return state.split ? state : {
159
+ ...state,
160
+ prerenderSplitScreen: !0,
161
+ split: !0
162
+ };
163
+ case "split-screen:close": return state.split ? {
164
+ ...state,
165
+ split: !1
166
+ } : state;
167
+ case "unset-theme": return state.theme === null ? state : {
168
+ ...state,
169
+ theme: null
170
+ };
171
+ case "set-theme": return dequal(state.theme, action.theme) ? state : {
172
+ ...state,
173
+ theme: action.theme
174
+ };
175
+ default: throw Error(`Unknown themer tool action: ${JSON.stringify(action)}`);
176
+ }
27
177
  }
178
+ const initialToolState = {
179
+ navbarHeight: null,
180
+ open: !1,
181
+ prerender: !1,
182
+ prerenderSplitScreen: !1,
183
+ split: !1,
184
+ theme: null
185
+ };
28
186
  /**
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.
187
+ * The options of the applied theme — `null` for the configured theme, which
188
+ * applies nothing on top of the Studio's own, and for an applied theme that
189
+ * no longer resolves. Cheaper than resolving the whole list when only the
190
+ * applied theme matters.
191
+ *
192
+ * @internal
32
193
  */
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;
194
+ function resolveActiveThemeOptions(state) {
195
+ if (state.active === null || state.removed.includes(state.active)) return null;
196
+ for (let theme of state.custom) if (theme.slug === state.active) return theme.options;
197
+ for (let preset of presets) if (preset.slug === state.active) return preset.options;
198
+ return null;
47
199
  }
48
- function sanitizeOptions(value) {
49
- if (!value || typeof value != "object") return null;
50
- if ("accent" in value) return sanitizeFlatOptions(value);
200
+ /** Drops the keys whose value is `undefined`, which a dropped option leaves behind */
201
+ function compact(value) {
202
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0));
203
+ }
204
+ /** A `#rgb` or `#rrggbb` color, lowercased */
205
+ 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({
206
+ accent: optionalColor,
207
+ text: optionalColor,
208
+ background: optionalColor,
209
+ contrast: optionalContrast
210
+ }), v.transform((options) => compact(options))), groupedOptionsSchema = v.pipe(v.looseObject({
211
+ light: v.fallback(v.optional(schemeOptionsSchema), void 0),
212
+ dark: v.fallback(v.optional(schemeOptionsSchema), void 0)
213
+ }), v.check((input) => !("accent" in input), "flat options"), v.transform(({ light, dark }) => {
51
214
  let options = {};
215
+ return light && Object.keys(light).length > 0 && (options.light = light), dark && Object.keys(dark).length > 0 && (options.dark = dark), options;
216
+ })), flatOptionsSchema = v.pipe(v.object({
217
+ accent: color,
218
+ text: optionalColor,
219
+ contrast: optionalContrast,
220
+ background: v.fallback(v.optional(v.object({
221
+ light: optionalColor,
222
+ dark: optionalColor
223
+ })), void 0)
224
+ }), v.transform(({ accent, text, contrast, background }) => {
225
+ let shared = compact({
226
+ accent,
227
+ text,
228
+ contrast
229
+ }), options = {};
52
230
  for (let scheme of SCHEMES) {
53
- let schemeOptions = sanitizeSchemeOptions(Reflect.get(value, scheme));
54
- Object.keys(schemeOptions).length > 0 && (options[scheme] = schemeOptions);
231
+ let schemeBackground = background?.[scheme];
232
+ options[scheme] = schemeBackground ? {
233
+ ...shared,
234
+ background: schemeBackground
235
+ } : { ...shared };
55
236
  }
56
237
  return options;
57
- }
238
+ })), themeOptionsSchema = v.union([flatOptionsSchema, groupedOptionsSchema]), swatch = v.fallback(v.optional(v.nullable(color), null), null), paletteSchema = v.pipe(v.object({
239
+ vibrant: swatch,
240
+ lightVibrant: swatch,
241
+ darkVibrant: swatch,
242
+ muted: swatch,
243
+ lightMuted: swatch,
244
+ darkMuted: swatch,
245
+ dominant: swatch
246
+ }), v.transform((palette) => Object.values(palette).some((item) => item !== null) ? palette : null));
58
247
  function isReservedSlug(slug) {
59
248
  return slug === "config" || presets.some((preset) => preset.slug === slug);
60
249
  }
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);
250
+ const customThemeSchema = v.pipe(v.object({
251
+ slug: v.pipe(v.string(), v.nonEmpty(), v.check((slug) => !isReservedSlug(slug), "reserved slug")),
252
+ title: v.fallback(v.optional(v.string()), void 0),
253
+ options: themeOptionsSchema,
254
+ palette: v.fallback(v.optional(paletteSchema), void 0)
255
+ }), v.transform(({ slug, title, options, palette }) => ({
256
+ slug,
257
+ title: displayTitle(title ?? ""),
258
+ options,
259
+ ...palette ? { palette } : {}
260
+ }))), lenientList = v.fallback(v.optional(v.array(v.unknown()), []), []), themerStateSchema = v.pipe(v.object({
261
+ active: v.fallback(v.nullable(v.string()), null),
262
+ custom: lenientList,
263
+ removed: lenientList,
264
+ order: lenientList
265
+ }), v.transform((state) => {
266
+ let custom = [];
267
+ for (let item of state.custom) {
268
+ let result = v.safeParse(customThemeSchema, item);
269
+ result.success && !custom.some((theme) => theme.slug === result.output.slug) && custom.push(result.output);
90
270
  }
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");
271
+ let isCustom = (slug) => custom.some((theme) => theme.slug === slug), isPreset = (slug) => presets.some((preset) => preset.slug === slug), removed = [], order = [];
272
+ for (let slug of state.removed) typeof slug != "string" || slug === "config" || removed.includes(slug) || (isPreset(slug) || isCustom(slug)) && removed.push(slug);
273
+ for (let slug of state.order) typeof slug != "string" || order.includes(slug) || (slug === "config" || isPreset(slug) || isCustom(slug)) && order.push(slug);
96
274
  return {
97
- active: typeof rawActive == "string" ? rawActive : null,
275
+ active: state.active,
98
276
  custom,
99
277
  removed,
100
278
  order
101
279
  };
280
+ }));
281
+ /** The persisted themer state, or `null` for anything else @internal */
282
+ function parseThemerState(input) {
283
+ let result = v.safeParse(themerStateSchema, input);
284
+ return result.success ? result.output : null;
102
285
  }
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);
286
+ /** The machine's state value, as persisted — only what the machine has states for restores @internal */
287
+ const snapshotValueSchema = v.object({
288
+ flow: v.picklist([
289
+ "list",
290
+ "edit",
291
+ "removed"
292
+ ]),
293
+ theme: v.picklist(["applied", "switching"])
294
+ });
295
+ /**
296
+ * A snapshot for the machine to start a session from: the given themes, with
297
+ * nothing being switched to, in the list unless a flow and its subject are
298
+ * given. It is how the themes alone — all that earlier versions persisted —
299
+ * are handed to a machine that only restores snapshots, and what a restored
300
+ * snapshot is made fit for this session as.
301
+ *
302
+ * @internal
303
+ */
304
+ function snapshotFromState(state, editing = null, flow = "list") {
109
305
  return {
110
- active: theme.slug,
111
- custom: [theme],
112
- removed: [],
113
- order: []
306
+ status: "active",
307
+ output: void 0,
308
+ error: void 0,
309
+ value: {
310
+ flow,
311
+ theme: "applied"
312
+ },
313
+ context: {
314
+ ...state,
315
+ editing,
316
+ images: {}
317
+ },
318
+ historyValue: {},
319
+ children: {}
114
320
  };
115
321
  }
116
322
  /**
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.
323
+ * The machine's snapshot as the last session persisted it, made fit for this
324
+ * one: the themes are parsed like any persisted state, the flow keeps its
325
+ * subject or falls back to the list, a switch that was under way is over,
326
+ * and the image URLs of the last session are gone with it.
120
327
  *
121
328
  * @internal
122
329
  */
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 {}
330
+ const persistedSnapshotSchema = v.pipe(v.object({
331
+ status: v.literal("active"),
332
+ value: snapshotValueSchema,
333
+ context: v.pipe(v.looseObject({ editing: v.fallback(v.nullable(v.object({
334
+ slug: v.string(),
335
+ focusTitle: v.boolean()
336
+ })), null) }), v.transform(({ editing, ...state }) => ({
337
+ editing,
338
+ state: parseThemerState(state)
339
+ })), v.check(({ state }) => state !== null, "no themes"))
340
+ }), v.transform(({ value, context }) => {
341
+ let state = context.state ?? {
342
+ active: null,
343
+ custom: [],
344
+ removed: [],
345
+ order: []
346
+ }, editing = context.editing && state.custom.some((theme) => theme.slug === context.editing?.slug) && !state.removed.includes(context.editing.slug) ? context.editing : null;
347
+ return snapshotFromState(state, editing, value.flow === "edit" && editing === null || value.flow === "removed" && state.removed.length === 0 ? "list" : value.flow);
348
+ })), SNAPSHOT_STORAGE_KEY = "sanityStudio:themer:snapshot", STATE_STORAGE_KEY = "sanityStudio:themer:state", LEGACY_STORAGE_KEY = "sanityStudio:themer:options", LEGACY_DRAFT = {
349
+ slug: "custom-draft",
350
+ title: "Draft theme"
351
+ };
352
+ function readJson(key) {
353
+ let raw = localStorage.getItem(key);
354
+ return raw ? JSON.parse(raw) : void 0;
138
355
  }
139
356
  /**
140
- * Whether the themer sidebar has been opened before, in any session — the
141
- * cue for the navbar to stop introducing the tool.
357
+ * Restores the machine's snapshot from what the last session persisted, for
358
+ * `createActor` to start from, so the user's themes survive Studio reloads:
359
+ * the snapshot itself where there is one that fits, and one made from the
360
+ * themes alone where the snapshot no longer fits the machine or where
361
+ * earlier versions of the tool stored the themes without a snapshot.
362
+ * `undefined` when nothing usable is stored — or storage cannot be read at
363
+ * all — leaves the machine to start from no themes.
364
+ *
365
+ * The parsing is the schemas' (see `schemas.ts`): nothing here or in the
366
+ * machine looks at what is stored beyond handing it over.
142
367
  *
143
368
  * @internal
144
369
  */
145
- function hasVisited() {
370
+ function readPersistedSnapshot() {
146
371
  try {
147
- return typeof localStorage < "u" && localStorage.getItem(VISITED_STORAGE_KEY) !== null;
372
+ if (typeof localStorage > "u") return;
373
+ let persisted = readJson(SNAPSHOT_STORAGE_KEY);
374
+ if (persisted !== void 0) {
375
+ let snapshot = v.safeParse(persistedSnapshotSchema, persisted);
376
+ if (snapshot.success) return snapshot.output;
377
+ let state = parseThemerState(persisted && typeof persisted == "object" ? Reflect.get(persisted, "context") : void 0);
378
+ if (state) return snapshotFromState(state);
379
+ }
380
+ let state = parseThemerState(readJson(STATE_STORAGE_KEY));
381
+ return state ? snapshotFromState(state) : readLegacyDraft();
148
382
  } catch {
149
- return !1;
383
+ return;
150
384
  }
151
385
  }
386
+ function readLegacyDraft() {
387
+ let options = v.safeParse(themeOptionsSchema, readJson(LEGACY_STORAGE_KEY));
388
+ if (!options.success) return;
389
+ let theme = {
390
+ ...LEGACY_DRAFT,
391
+ options: options.output
392
+ };
393
+ return snapshotFromState({
394
+ active: theme.slug,
395
+ custom: [theme],
396
+ removed: [],
397
+ order: []
398
+ });
399
+ }
152
400
  /**
153
- * Notes that the themer sidebar has been opened, keeping the time it first
154
- * was.
401
+ * Persists the machine's snapshot for the next session, and lets go of what
402
+ * earlier versions stored once it is written — which completes their
403
+ * migration.
155
404
  *
156
405
  * @internal
157
406
  */
158
- function markVisited() {
407
+ function writePersistedSnapshot(snapshot) {
159
408
  try {
160
- if (typeof localStorage > "u" || localStorage.getItem(VISITED_STORAGE_KEY) !== null) return;
161
- localStorage.setItem(VISITED_STORAGE_KEY, (/* @__PURE__ */ new Date()).toISOString());
409
+ if (typeof localStorage > "u") return;
410
+ localStorage.setItem(SNAPSHOT_STORAGE_KEY, JSON.stringify(snapshot)), localStorage.removeItem(STATE_STORAGE_KEY), localStorage.removeItem(LEGACY_STORAGE_KEY);
162
411
  } catch {}
163
412
  }
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
413
  function themesOf(context) {
176
- return resolveThemes(context, context.baseOptions);
414
+ return resolveThemes(context);
177
415
  }
178
416
  function revokeObjectUrl(url) {
179
417
  typeof URL < "u" && typeof URL.revokeObjectURL == "function" && URL.revokeObjectURL(url);
180
418
  }
181
419
  /**
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.
420
+ * The themes of the themer tool and the sidebar's way through them: which
421
+ * `flow` the sidebar is in — picking a theme from the `list`, `edit`ing one of
422
+ * the user's own themes, or restoring `removed` ones — and, in a parallel
423
+ * `theme` region, whether another theme was just applied. The context carries
424
+ * the persisted state (the applied theme, the user's themes and what was
425
+ * removed) alongside what the flows need. Whether the sidebar is open and
426
+ * whether the Studio shows twice is not the machine's: the tool reducer in
427
+ * `ThemerProvider` owns that, so that the navbar toggle and the sidebar can
428
+ * change it in transitions of their own. Nor is persisting: the machine
429
+ * never touches storage and takes no input — it starts from no themes, and a
430
+ * session with something to pick up from restores its persisted snapshot
431
+ * instead (see `storage.ts` and `sync.ts`), while the persisted state of
432
+ * other tabs reaches it as a `themes.sync` event like any other.
189
433
  *
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.
434
+ * The `theme` region is `switching` (its tag) right after an event that
435
+ * applies another theme — picking one, adding, duplicating, importing or
436
+ * editing one, removing or deleting the applied one — which is when the
437
+ * layout cross-fades the Studio to it. Edits to the applied theme's colors are
438
+ * not switches, they follow the pointer: the first one ends the switch, and so
439
+ * does `MOTION_DURATION` when none follows.
203
440
  *
204
441
  * Theme operations are handled in every flow, and the flows leave on their
205
442
  * own when they lose their subject: the editor when its theme is removed or
@@ -210,34 +447,30 @@ function revokeObjectUrl(url) {
210
447
  const themerMachine = setup({
211
448
  types: {
212
449
  context: {},
213
- events: {},
214
- input: {}
450
+ events: {}
215
451
  },
216
452
  guards: {
217
453
  isCustomTheme: ({ context }, params) => themesOf(context).themes.some((theme) => theme.slug === params.slug && theme.source === "custom"),
218
454
  isEditingListedTheme: ({ context }) => context.editing !== null && themesOf(context).themes.some((theme) => theme.slug === context.editing?.slug && theme.source === "custom"),
219
455
  hasRemovedThemes: ({ context }) => themesOf(context).removed.length > 0,
220
- sidebarShown: or([stateIn({ sidebar: "opening" }), stateIn({ sidebar: "open" })]),
221
456
  isAnotherTheme: ({ context }, params) => params.slug !== (context.active ?? "config"),
222
457
  isAppliedTheme: ({ context }, params) => params.slug === context.active,
458
+ appliesAnotherTheme: ({ context }, params) => params.state.active !== context.active,
223
459
  hasOptions: (_, params) => params.options !== void 0
224
460
  },
225
461
  actions: {
226
- persist: ({ context }) => {
227
- let { active, custom, removed, order } = context;
228
- writeStoredState({
229
- active,
230
- custom,
231
- removed,
232
- order
233
- });
234
- },
462
+ sync: assign((_, params) => ({
463
+ active: params.state.active,
464
+ custom: params.state.custom,
465
+ removed: params.state.removed,
466
+ order: params.state.order
467
+ })),
235
468
  revokeImage: (_, params) => {
236
469
  params.url && revokeObjectUrl(params.url);
237
470
  },
238
471
  pick: assign((_, params) => ({ active: params.slug === "config" ? null : params.slug })),
239
472
  add: assign(({ context }, params) => {
240
- let theme = createCustomTheme(params.title ?? "Untitled theme", params.options ?? themesOf(context).active.options, params.palette);
473
+ let theme = createCustomTheme(params.title ?? "Untitled theme", params.options ?? themesOf(context).active?.options ?? {}, params.palette);
241
474
  return {
242
475
  active: theme.slug,
243
476
  custom: [...context.custom, theme],
@@ -259,7 +492,11 @@ const themerMachine = setup({
259
492
  };
260
493
  }),
261
494
  duplicate: assign(({ context }, params) => {
262
- let { themes, removed } = themesOf(context), source = [...themes, ...removed].find((theme) => theme.slug === params.slug);
495
+ let { themes, removed } = themesOf(context), source = params.slug === "config" ? {
496
+ title: CONFIG_TITLE,
497
+ options: {},
498
+ palette: void 0
499
+ } : [...themes, ...removed].find((theme) => theme.slug === params.slug);
263
500
  if (!source) return {};
264
501
  let theme = createCustomTheme(duplicateTitle(source.title), source.options, source.palette);
265
502
  return {
@@ -313,83 +550,13 @@ const themerMachine = setup({
313
550
  }
314
551
  }).createMachine({
315
552
  id: "themer",
316
- context: ({ input }) => ({
317
- ...input.stored,
318
- baseOptions: input.baseOptions,
553
+ context: () => ({
554
+ ...initialThemerState,
319
555
  editing: null,
320
556
  images: {}
321
557
  }),
322
- entry: "persist",
323
558
  type: "parallel",
324
559
  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
560
  theme: {
394
561
  initial: "applied",
395
562
  states: {
@@ -436,13 +603,20 @@ const themerMachine = setup({
436
603
  },
437
604
  target: "switching"
438
605
  },
439
- "theme.import": "switching"
606
+ "theme.import": "switching",
607
+ "themes.sync": {
608
+ guard: {
609
+ type: "appliesAnotherTheme",
610
+ params: ({ event }) => ({ state: event.state })
611
+ },
612
+ target: "switching"
613
+ }
440
614
  } },
441
615
  switching: {
442
616
  tags: ["switching"],
443
- after: { [MOTION_DURATION]: "applied" },
617
+ after: { 2e3: "applied" },
444
618
  on: {
445
- "layout.transitioned": "applied",
619
+ "theme.update": "applied",
446
620
  "theme.pick": {
447
621
  guard: {
448
622
  type: "isAnotherTheme",
@@ -494,6 +668,14 @@ const themerMachine = setup({
494
668
  "theme.import": {
495
669
  target: "switching",
496
670
  reenter: !0
671
+ },
672
+ "themes.sync": {
673
+ guard: {
674
+ type: "appliesAnotherTheme",
675
+ params: ({ event }) => ({ state: event.state })
676
+ },
677
+ target: "switching",
678
+ reenter: !0
497
679
  }
498
680
  }
499
681
  }
@@ -505,7 +687,7 @@ const themerMachine = setup({
505
687
  "theme.pick": { actions: [{
506
688
  type: "pick",
507
689
  params: ({ event }) => ({ slug: event.slug })
508
- }, "persist"] },
690
+ }] },
509
691
  "theme.add": {
510
692
  target: ".edit",
511
693
  actions: [{
@@ -516,14 +698,14 @@ const themerMachine = setup({
516
698
  palette: event.palette,
517
699
  imageUrl: event.imageUrl
518
700
  })
519
- }, "persist"]
701
+ }]
520
702
  },
521
703
  "theme.duplicate": {
522
704
  target: ".edit",
523
705
  actions: [{
524
706
  type: "duplicate",
525
707
  params: ({ event }) => ({ slug: event.slug })
526
- }, "persist"]
708
+ }]
527
709
  },
528
710
  "theme.edit": {
529
711
  target: ".edit",
@@ -534,55 +716,51 @@ const themerMachine = setup({
534
716
  actions: [{
535
717
  type: "startEditing",
536
718
  params: ({ event }) => ({ slug: event.slug })
537
- }, "persist"]
719
+ }]
538
720
  },
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
- ] },
721
+ "theme.update": { actions: [{
722
+ type: "revokeImage",
723
+ params: ({ context, event }) => ({ url: event.imageUrl ? context.images[event.slug] : void 0 })
724
+ }, {
725
+ type: "update",
726
+ params: ({ event }) => ({
727
+ slug: event.slug,
728
+ title: event.title,
729
+ options: event.options,
730
+ palette: event.palette,
731
+ imageUrl: event.imageUrl
732
+ })
733
+ }] },
556
734
  "theme.remove": { actions: [{
557
735
  type: "remove",
558
736
  params: ({ event }) => ({ slug: event.slug })
559
- }, "persist"] },
737
+ }] },
560
738
  "theme.restore": { actions: [{
561
739
  type: "restore",
562
740
  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
- ] },
741
+ }] },
742
+ "theme.delete": { actions: [{
743
+ type: "revokeImage",
744
+ params: ({ context, event }) => ({ url: context.images[event.slug] })
745
+ }, {
746
+ type: "delete",
747
+ params: ({ event }) => ({ slug: event.slug })
748
+ }] },
575
749
  "theme.reorder": { actions: [{
576
750
  type: "reorder",
577
751
  params: ({ event }) => ({ order: event.order })
578
- }, "persist"] },
752
+ }] },
579
753
  "theme.import": { actions: [{
580
754
  type: "import",
581
755
  params: ({ event }) => ({
582
756
  title: event.title,
583
757
  options: event.options
584
758
  })
585
- }, "persist"] },
759
+ }] },
760
+ "themes.sync": { actions: {
761
+ type: "sync",
762
+ params: ({ event }) => ({ state: event.state })
763
+ } },
586
764
  "flow.list": ".list",
587
765
  "flow.removed": {
588
766
  target: ".removed",
@@ -606,140 +784,32 @@ const themerMachine = setup({
606
784
  }
607
785
  }
608
786
  });
609
- /** The part of the machine context that is persisted between sessions @internal */
610
- function selectStoredState(snapshot) {
611
- let { active, custom, removed, order } = snapshot.context;
612
- return {
613
- active,
614
- custom,
615
- removed,
616
- order
617
- };
618
- }
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);
623
- }
624
- function getSnapshot() {
625
- return matchMedia(QUERY).matches;
626
- }
627
- function getServerSnapshot() {
628
- return !1;
629
- }
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);
638
- }
639
- /**
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
642
- *
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.
652
- *
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()`.
681
- *
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.
691
- *
692
- * @internal
693
- */
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);
787
+ function PersistActorSnapshot(t0) {
788
+ let $ = c(3), { actorRef } = t0, t1, t2;
789
+ return $[0] === actorRef ? (t1 = $[1], t2 = $[2]) : (t1 = () => {
790
+ let cancel, cancelOrClear = function cancelOrClear() {
791
+ "cancelIdleCallback" in window ? cancelIdleCallback(cancel) : clearTimeout(cancel);
792
+ }, subscription = actorRef.subscribe(() => {
793
+ cancelOrClear();
794
+ let callback = () => {
795
+ writePersistedSnapshot(actorRef.getPersistedSnapshot());
796
+ };
797
+ cancel = "cancelIdleCallback" in window ? requestIdleCallback(callback) : setTimeout(callback, 1e3);
699
798
  });
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;
799
+ return () => {
800
+ subscription.unsubscribe(), cancelOrClear();
801
+ };
802
+ }, t2 = [actorRef], $[0] = actorRef, $[1] = t1, $[2] = t2), useEffect(t1, t2), null;
708
803
  }
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
804
  /**
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.
805
+ * The sidebar — everything in it, from the theme list to the snippet dialog —
806
+ * loads the first time it opens. There is no `Suspense` boundary around it,
807
+ * so it must only ever mount from a deferred render: that keeps the Studio as
808
+ * it was while the code loads, with the navbar toggle showing the deferred
809
+ * value as pending, where an urgent render would suspend up to the Studio's
810
+ * own boundary and swap the whole Studio for its loading screen.
727
811
  */
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;
742
- }
812
+ const ResizableSidebar = lazy(() => import("./ResizableSidebar-BpgLA4XQ.js"));
743
813
  /**
744
814
  * Wraps the whole Studio so that the theme picked in the themer sidebar
745
815
  * applies everywhere while the user browses around, runs the themer machine
@@ -768,146 +838,94 @@ function sameView(a, b) {
768
838
  *
769
839
  * @internal
770
840
  */
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,
841
+ function ThemerLayout(t0) {
842
+ let $ = c(50), { children, backgroundColor, config, dispatch, navbarHeight, open, persistedSnapshot, prerender, prerenderSplitScreen, scheme, split } = t0, t1;
843
+ $[0] === persistedSnapshot ? t1 = $[1] : (t1 = { snapshot: persistedSnapshot }, $[0] = persistedSnapshot, $[1] = t1);
844
+ let actorRef = useActorRef(themerMachine, t1), oppositeScheme = scheme === "dark" ? "light" : "dark", t2;
845
+ $[2] === backgroundColor ? t2 = $[3] : (t2 = { backgroundColor }, $[2] = backgroundColor, $[3] = t2);
846
+ let t3 = split && open ? "visible" : "hidden", t4;
847
+ $[4] !== children || $[5] !== config || $[6] !== dispatch || $[7] !== open || $[8] !== oppositeScheme || $[9] !== prerenderSplitScreen || $[10] !== split ? (t4 = prerenderSplitScreen && /* @__PURE__ */ jsx(StudioPreview, {
848
+ borderRight: !0,
849
+ config,
850
+ dispatch,
816
851
  open,
817
- loading,
852
+ scheme: oppositeScheme,
818
853
  split,
819
- mobile,
854
+ children
855
+ }), $[4] = children, $[5] = config, $[6] = dispatch, $[7] = open, $[8] = oppositeScheme, $[9] = prerenderSplitScreen, $[10] = split, $[11] = t4) : t4 = $[11];
856
+ let t5;
857
+ $[12] === t4 ? t5 = $[13] : (t5 = /* @__PURE__ */ jsx(ViewTransition, {
858
+ default: "none",
859
+ enter: viewTransitionClasses.splitscreen.enter,
860
+ update: viewTransitionClasses.splitscreen.update,
861
+ exit: viewTransitionClasses.splitscreen.exit,
862
+ children: t4
863
+ }), $[12] = t4, $[13] = t5);
864
+ let t6;
865
+ $[14] !== t3 || $[15] !== t5 ? (t6 = /* @__PURE__ */ jsx(Activity, {
866
+ mode: t3,
867
+ children: t5
868
+ }), $[14] = t3, $[15] = t5, $[16] = t6) : t6 = $[16];
869
+ let t7;
870
+ $[17] !== children || $[18] !== config || $[19] !== dispatch || $[20] !== open || $[21] !== split ? (t7 = /* @__PURE__ */ jsx(ViewTransition, {
871
+ default: "none",
872
+ update: viewTransitionClasses.studio.update,
873
+ children: /* @__PURE__ */ jsx(StudioPreview, {
874
+ config,
875
+ dispatch,
876
+ open,
877
+ shouldDetectNavbarHeight: !0,
878
+ split,
879
+ children
880
+ })
881
+ }), $[17] = children, $[18] = config, $[19] = dispatch, $[20] = open, $[21] = split, $[22] = t7) : t7 = $[22];
882
+ let t8 = open ? "visible" : "hidden", t9;
883
+ $[23] !== actorRef || $[24] !== dispatch || $[25] !== navbarHeight || $[26] !== prerender || $[27] !== split ? (t9 = prerender && /* @__PURE__ */ jsx(ResizableSidebar, {
884
+ actorRef,
885
+ dispatch,
820
886
  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, {
887
+ split
888
+ }), $[23] = actorRef, $[24] = dispatch, $[25] = navbarHeight, $[26] = prerender, $[27] = split, $[28] = t9) : t9 = $[28];
889
+ let t10;
890
+ $[29] === t9 ? t10 = $[30] : (t10 = /* @__PURE__ */ jsx(ViewTransition, {
891
+ default: "none",
892
+ enter: viewTransitionClasses.sidebar.enter,
893
+ update: viewTransitionClasses.sidebar.update,
894
+ exit: viewTransitionClasses.sidebar.exit,
895
+ children: t9
896
+ }), $[29] = t9, $[30] = t10);
897
+ let t11;
898
+ $[31] !== t10 || $[32] !== t8 ? (t11 = /* @__PURE__ */ jsx(Activity, {
899
+ mode: t8,
900
+ children: t10
901
+ }), $[31] = t10, $[32] = t8, $[33] = t11) : t11 = $[33];
902
+ let t12;
903
+ $[34] !== t11 || $[35] !== t2 || $[36] !== t6 || $[37] !== t7 ? (t12 = /* @__PURE__ */ jsxs(Flex, {
896
904
  className: layout,
897
- direction: t15,
905
+ direction: "row",
898
906
  height: "fill",
899
907
  sizing: "border",
908
+ style: t2,
900
909
  children: [
901
- t16,
902
- t18,
903
- t24
910
+ t6,
911
+ t7,
912
+ t11
904
913
  ]
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;
914
+ }), $[34] = t11, $[35] = t2, $[36] = t6, $[37] = t7, $[38] = t12) : t12 = $[38];
915
+ let t13;
916
+ $[39] !== actorRef || $[40] !== dispatch ? (t13 = /* @__PURE__ */ jsx(ThemerThemeCrossfader, {
917
+ actorRef,
918
+ dispatch
919
+ }), $[39] = actorRef, $[40] = dispatch, $[41] = t13) : t13 = $[41];
920
+ let t14, t15;
921
+ $[42] === actorRef ? (t14 = $[43], t15 = $[44]) : (t14 = /* @__PURE__ */ jsx(RevokeImageUrls, { actorRef }), t15 = /* @__PURE__ */ jsx(PersistActorSnapshot, { actorRef }), $[42] = actorRef, $[43] = t14, $[44] = t15);
922
+ let t16;
923
+ return $[45] !== t12 || $[46] !== t13 || $[47] !== t14 || $[48] !== t15 ? (t16 = /* @__PURE__ */ jsxs(Fragment, { children: [
924
+ t12,
925
+ t13,
926
+ t14,
927
+ t15
928
+ ] }), $[45] = t12, $[46] = t13, $[47] = t14, $[48] = t15, $[49] = t16) : t16 = $[49], t16;
911
929
  }
912
930
  /**
913
931
  * One copy of the Studio, in the given color scheme — or, without one, in the
@@ -920,292 +938,100 @@ function ThemerLayout(props) {
920
938
  * they stay in sync while navigating. The `color-scheme` of a forced scheme
921
939
  * keeps native form controls and scrollbars in step with it.
922
940
  */
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,
941
+ function StudioPreview(t0) {
942
+ let $ = c(17), { borderRight, children, config, dispatch, open, scheme, shouldDetectNavbarHeight, split } = t0, [boundaryElement, setBoundaryElement] = useState(null), t1;
943
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t1 = (boundaryElement_0) => (startTransition(() => setBoundaryElement(boundaryElement_0)), _temp), $[0] = t1) : t1 = $[0];
944
+ let t2;
945
+ $[1] === children ? t2 = $[2] : (t2 = /* @__PURE__ */ jsx(ViewTransition, {
946
+ update: "none",
947
+ children
948
+ }), $[1] = children, $[2] = t2);
949
+ let t3;
950
+ $[3] !== config || $[4] !== dispatch || $[5] !== open || $[6] !== shouldDetectNavbarHeight || $[7] !== split || $[8] !== t2 ? (t3 = /* @__PURE__ */ jsx(ThemerNavbarProvider, {
951
+ config,
952
+ dispatch,
953
+ open,
954
+ shouldDetectNavbarHeight,
955
+ split,
956
+ children: t2
957
+ }), $[3] = config, $[4] = dispatch, $[5] = open, $[6] = shouldDetectNavbarHeight, $[7] = split, $[8] = t2, $[9] = t3) : t3 = $[9];
958
+ let t4;
959
+ $[10] !== boundaryElement || $[11] !== t3 ? (t4 = /* @__PURE__ */ jsx(BoundaryElementProvider, {
960
+ element: boundaryElement,
961
+ children: t3
962
+ }), $[10] = boundaryElement, $[11] = t3, $[12] = t4) : t4 = $[12];
963
+ let t5;
964
+ return $[13] !== borderRight || $[14] !== scheme || $[15] !== t4 ? (t5 = /* @__PURE__ */ jsx(Card, {
927
965
  borderRight,
928
- className: t1,
929
966
  flex: 1,
930
967
  height: "fill",
931
968
  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
969
  scheme,
938
- theme: t0,
939
- children: t2
940
- }), $[6] = scheme, $[7] = t0, $[8] = t2, $[9] = t3) : t3 = $[9], t3;
941
- }
942
- /**
943
- * The colors the slices fill with, clockwise from 12 o'clock: the palette's
944
- * hues in rainbow order, like a real color swatch
945
- */
946
- const SLICE_COLORS = [
947
- red[500].hex,
948
- orange[500].hex,
949
- yellow[400].hex,
950
- green[500].hex,
951
- cyan[500].hex,
952
- blue[500].hex,
953
- purple[500].hex,
954
- magenta[500].hex
955
- ], TOTAL_ACTS = 2 + (1 - Math.cbrt(1 / 8)), ANIMATION_DURATION = .55 * TOTAL_ACTS;
956
- function actAt(progress) {
957
- return progress * TOTAL_ACTS;
958
- }
959
- /**
960
- * How far the wheel has turned at `progress` (0–1), in degrees.
961
- *
962
- * It stands still while the top half of the slices pop in. As the 6 o'clock
963
- * slice pops in it starts spinning — one full turn with a cubic ease-in, from
964
- * rest to top speed as the bottom half pop in — and it stands still again the
965
- * moment it is back upright: the clearing of {@link isSliceFilled} takes off
966
- * at the speed the spin reached, as if the wheel kept turning and eased out
967
- * over a second turn, so the two read as one movement.
968
- */
969
- function wheelRotation(progress) {
970
- let spin = actAt(progress) - 1;
971
- return spin <= 0 || spin >= 1 ? 0 : 360 * spin ** 3;
972
- }
973
- /**
974
- * Whether slice `index` (clockwise from 12 o'clock) shows its color at
975
- * `progress` (0–1). A slice pops in whole as the fill reaches its leading
976
- * edge, and pops out whole as the clearing does — stepped, never partial.
977
- *
978
- * The fill goes clockwise at a steady pace over the first two acts, half of
979
- * the slices in each. The clearing follows in the third act, the same way
980
- * round, on a cubic ease-out that starts at the speed the spin of
981
- * {@link wheelRotation} ends with: the slices at first pop out as quickly as
982
- * they were passing by, then ever more slowly.
983
- */
984
- function isSliceFilled(progress, index) {
985
- let act = actAt(progress);
986
- if (act * 8 / 2 <= index) return !1;
987
- let clearing = act - 2;
988
- return clearing <= 0 || (1 - (1 - clearing) ** 3) * 8 < index;
989
- }
990
- /**
991
- * The geometry of `ColorWheelIcon` from `@sanity/icons`: a ring between two
992
- * circles around the center of a 25×25 viewBox, cut into eight slices by the
993
- * spokes of its outline path
994
- */
995
- const CENTER = 12.5, INNER_RADIUS = 3.5;
996
- /** A point on a circle around the center, at `degrees` clockwise from 3 o'clock */
997
- function point(radius, degrees) {
998
- let radians = degrees * Math.PI / 180;
999
- return `${CENTER + radius * Math.cos(radians)} ${CENTER + radius * Math.sin(radians)}`;
970
+ ref: t1,
971
+ children: t4
972
+ }), $[13] = borderRight, $[14] = scheme, $[15] = t4, $[16] = t5) : t5 = $[16], t5;
1000
973
  }
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
- }
974
+ function _temp() {}
975
+ function ThemerThemeCrossfader(t0) {
976
+ let $ = c(4), { actorRef, dispatch } = t0, t1, t2;
977
+ return $[0] !== actorRef || $[1] !== dispatch ? (t1 = () => {
978
+ let subscription = actorRef.subscribe((snapshot) => {
979
+ let switching = snapshot.hasTag("switching"), theme = resolveActiveThemeOptions(snapshot.context);
980
+ startTransition(() => {
981
+ switching && addThemerTransitionType("crossfade"), dispatch(theme ? {
982
+ type: "set-theme",
983
+ theme
984
+ } : { type: "unset-theme" });
1122
985
  });
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;
986
+ });
987
+ return () => subscription.unsubscribe();
988
+ }, t2 = [actorRef, dispatch], $[0] = actorRef, $[1] = dispatch, $[2] = t1, $[3] = t2) : (t1 = $[2], t2 = $[3]), useEffect(t1, t2), null;
989
+ }
990
+ function RevokeImageUrls(t0) {
991
+ let $ = c(3), { actorRef } = t0, t1, t2;
992
+ return $[0] === actorRef ? (t1 = $[1], t2 = $[2]) : (t1 = () => () => {
993
+ for (let url of Object.values(actorRef.getSnapshot().context.images)) URL.revokeObjectURL(url);
994
+ }, t2 = [actorRef], $[0] = actorRef, $[1] = t1, $[2] = t2), useEffect(t1, t2), null;
1171
995
  }
1172
996
  /**
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
997
+ * Keep this component extremely lightweight, and only include state that are needed by both
998
+ * `studio.components.layout` and `studio.components.navbar`, where it's not possible to prop-drill into `studio.components.navbar`,
999
+ * and that need to survive open/close of the tool
1000
+ * @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
1001
  */
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;
1002
+ function ThemerProvider(t0) {
1003
+ let $ = c(18), { children, config } = t0;
1004
+ use(browser("The current theme is stored in localStorage."));
1005
+ let [persistedSnapshot] = useState(readPersistedSnapshot), t1;
1006
+ $[0] === persistedSnapshot ? t1 = $[1] : (t1 = (initialState) => {
1007
+ if (!persistedSnapshot) return initialState;
1008
+ let theme = resolveActiveThemeOptions(persistedSnapshot.context);
1009
+ return theme ? {
1010
+ ...initialState,
1011
+ theme
1012
+ } : initialState;
1013
+ }, $[0] = persistedSnapshot, $[1] = t1);
1014
+ 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;
1015
+ $[2] === state.theme ? t2 = $[3] : (t2 = (state.theme === null ? null : buildTheme(state.theme)) ?? void 0, $[2] = state.theme, $[3] = t2);
1016
+ let t3 = state.theme?.[scheme]?.background, t4;
1017
+ $[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, {
1018
+ config,
1019
+ backgroundColor: t3,
1020
+ dispatch,
1021
+ navbarHeight: state.navbarHeight,
1022
+ open,
1023
+ persistedSnapshot,
1024
+ prerender: state.prerender,
1025
+ prerenderSplitScreen: state.prerenderSplitScreen,
1026
+ scheme,
1027
+ split,
1028
+ children
1029
+ }), $[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];
1030
+ let t5;
1031
+ return $[15] !== t2 || $[16] !== t4 ? (t5 = /* @__PURE__ */ jsx(ThemeProvider, {
1032
+ theme: t2,
1033
+ children: t4
1034
+ }), $[15] = t2, $[16] = t4, $[17] = t5) : t5 = $[17], t5;
1209
1035
  }
1210
1036
  /**
1211
1037
  * A Studio plugin that adds a themer sidebar for `buildTheme` themes: a
@@ -1236,22 +1062,27 @@ function ThemerNavbar(props) {
1236
1062
  * @alpha
1237
1063
  */
1238
1064
  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
- }
1065
+ let { title = "Themer" } = options ?? {};
1247
1066
  return {
1248
1067
  name: "@sanity/themer/tool",
1249
1068
  studio: { components: {
1250
- layout: ThemerLayoutWithOptions,
1069
+ layout: defineThemerLayout({ title }),
1251
1070
  navbar: ThemerNavbar
1252
1071
  } }
1253
1072
  };
1254
1073
  });
1074
+ function defineThemerLayout(config) {
1075
+ return function DefinedThemerLayoutProvider(props) {
1076
+ "use memo";
1077
+ let $ = c(4), t0;
1078
+ $[0] === props ? t0 = $[1] : (t0 = props.renderDefault(props), $[0] = props, $[1] = t0);
1079
+ let t1;
1080
+ return $[2] === t0 ? t1 = $[3] : (t1 = /* @__PURE__ */ jsx(ThemerProvider, {
1081
+ config,
1082
+ children: t0
1083
+ }), $[2] = t0, $[3] = t1), t1;
1084
+ };
1085
+ }
1255
1086
  export { themerTool };
1256
1087
 
1257
1088
  //# sourceMappingURL=tool.js.map