@sanity/themer 0.9.0 → 0.10.1

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,10 +1,10 @@
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 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";
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";
4
4
  import { c } from "react/compiler-runtime";
5
5
  import { definePlugin, useColorSchemeValue } from "sanity";
6
6
  import { ColorWheelIcon } from "@sanity/icons/ColorWheel";
7
- import { BoundaryElementProvider, Box, Button, Card, Flex, Text, ThemeProvider, useMediaIndex } from "@sanity/ui";
7
+ import { BoundaryElementProvider, Box, Button, Card, Flex, Text, ThemeProvider, useMediaIndex, usePrefersReducedMotion } from "@sanity/ui";
8
8
  import { Tooltip } from "@sanity/ui/tooltip";
9
9
  import { Activity, Suspense, ViewTransition, createContext, lazy, startTransition, use, useEffect, useReducer, useRef, useState, useTransition } from "react";
10
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -16,11 +16,11 @@ import { assign, not, setup } from "xstate";
16
16
  /** @internal */
17
17
  const ToolIsOpenContext = createContext(!1), ToolDispatchContext = createContext(() => {
18
18
  throw Error("ToolDispatchContext not initialized");
19
- }), ToolSplitIsOpenContext = createContext(!1), ToolShouldDetectNavbarHeightContext = createContext(!1), PluginConfigContext = createContext(null), AnimatedColorWheelIcon = lazy(() => import("./AnimatedColorWheelIcon-DEy-cy3X.js"));
19
+ }), ToolSplitIsOpenContext = createContext(!1), ToolShouldDetectNavbarHeightContext = createContext(!1), PluginConfigContext = createContext(null), AnimatedColorWheelIcon = lazy(() => import("./AnimatedColorWheelIcon-CwWOVAsw.js"));
20
20
  function ThemerNavbarButton() {
21
21
  let $ = c(27), config = use(PluginConfigContext), dispatch = use(ToolDispatchContext), open = use(ToolIsOpenContext), shouldDetectNavbarHeight = use(ToolShouldDetectNavbarHeightContext), split = use(ToolSplitIsOpenContext);
22
22
  if (!config) throw TypeError("PluginConfigContext.Provider is missing");
23
- let [busy, startBusyTransition] = useTransition(), iconRef = useRef(null), t0;
23
+ let [busy, startBusyTransition] = useTransition(), iconRef = useRef(null), prefersReducedMotion = usePrefersReducedMotion(), t0;
24
24
  $[0] !== dispatch || $[1] !== split ? (t0 = () => startBusyTransition(() => {
25
25
  split && addThemerTransitionType("split-screen:open"), addThemerTransitionType("open"), dispatch({ type: "open" });
26
26
  }), $[0] = dispatch, $[1] = split, $[2] = t0) : t0 = $[2];
@@ -51,59 +51,56 @@ function ThemerNavbarButton() {
51
51
  children: title
52
52
  }), $[11] = title, $[12] = t4);
53
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,
54
+ $[13] !== busy || $[14] !== prefersReducedMotion ? (t6 = prefersReducedMotion ? /* @__PURE__ */ jsx(ColorWheelIcon, {}) : /* @__PURE__ */ jsx(Suspense, {
55
+ fallback: /* @__PURE__ */ jsx(ColorWheelIcon, {}),
58
56
  children: /* @__PURE__ */ jsx(AnimatedColorWheelIcon, {
59
57
  busy,
60
58
  ref: iconRef
61
59
  })
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, {
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, {
65
63
  default: "none",
66
64
  update: viewTransitionClasses.navbarButton.update,
67
65
  children: /* @__PURE__ */ jsx(Button, {
68
66
  ref: t5,
69
67
  "aria-busy": busy,
70
68
  "aria-label": title,
71
- icon: t7,
69
+ icon: t6,
72
70
  mode: "bleed",
73
- onClick: t8,
71
+ onClick: t7,
74
72
  onFocus: handlePrerender,
75
73
  onMouseEnter: handlePrerender,
76
74
  padding: 2,
77
- selected: t9
75
+ selected: t8
78
76
  })
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, {
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, {
82
80
  default: "none",
83
81
  update: viewTransitionClasses.navbarButtonTooltip.update,
84
82
  children: /* @__PURE__ */ jsx(Tooltip, {
85
83
  animate: !0,
86
84
  content: t4,
87
85
  portal: !0,
88
- children: t10
86
+ children: t9
89
87
  })
90
- }), $[24] = t10, $[25] = t4, $[26] = t11) : t11 = $[26], t11;
88
+ }), $[24] = t4, $[25] = t9, $[26] = t10) : t10 = $[26], t10;
91
89
  }
92
90
  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;
91
+ let $ = c(6), t0;
92
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = {
93
+ location: "topbar",
94
+ name: "themer-topbar",
95
+ render: ThemerNavbarButton
96
+ }, $[0] = t0) : t0 = $[0];
97
+ let button = t0, t1;
98
+ $[1] === props.__internal_actions ? t1 = $[2] : (t1 = props.__internal_actions ?? [], $[1] = props.__internal_actions, $[2] = t1);
99
+ let t2;
100
+ return $[3] !== props || $[4] !== t1 ? (t2 = props.renderDefault({
101
+ ...props,
102
+ __internal_actions: [...t1, button]
103
+ }), $[3] = props, $[4] = t1, $[5] = t2) : t2 = $[5], t2;
107
104
  }
108
105
  function ThemerNavbarProvider(t0) {
109
106
  let $ = c(15), { children, config, dispatch, open, shouldDetectNavbarHeight, split } = t0, t1 = shouldDetectNavbarHeight ?? !1, t2;
@@ -175,6 +172,11 @@ function toolReducer(state, action) {
175
172
  default: throw Error(`Unknown themer tool action: ${JSON.stringify(action)}`);
176
173
  }
177
174
  }
175
+ /**
176
+ * Where a tab starts: closed, nothing prerendered, no theme applied —
177
+ * `ThemerProvider` seeds only the applied theme, from the machine's restored
178
+ * snapshot
179
+ */
178
180
  const initialToolState = {
179
181
  navbarHeight: null,
180
182
  open: !1,
@@ -283,13 +285,17 @@ function parseThemerState(input) {
283
285
  let result = v.safeParse(themerStateSchema, input);
284
286
  return result.success ? result.output : null;
285
287
  }
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
- ]),
288
+ /** The flows a session restores to */
289
+ const flowSchema = v.picklist([
290
+ "list",
291
+ "edit",
292
+ "removed"
293
+ ]), snapshotValueSchema = v.object({
294
+ flow: v.union([flowSchema, v.object({ list: v.picklist([
295
+ "idle",
296
+ "pasting",
297
+ "snippet"
298
+ ]) })]),
293
299
  theme: v.picklist(["applied", "switching"])
294
300
  });
295
301
  /**
@@ -322,8 +328,9 @@ function snapshotFromState(state, editing = null, flow = "list") {
322
328
  /**
323
329
  * The machine's snapshot as the last session persisted it, made fit for this
324
330
  * 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.
331
+ * subject or falls back to the list, a dialog that was open is closed, a
332
+ * switch that was under way is over, and the image URLs of the last session
333
+ * are gone with it.
327
334
  *
328
335
  * @internal
329
336
  */
@@ -344,7 +351,7 @@ const persistedSnapshotSchema = v.pipe(v.object({
344
351
  removed: [],
345
352
  order: []
346
353
  }, 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);
354
+ return snapshotFromState(state, editing, typeof value.flow != "string" || value.flow === "edit" && editing === null || value.flow === "removed" && state.removed.length === 0 ? "list" : value.flow);
348
355
  })), SNAPSHOT_STORAGE_KEY = "sanityStudio:themer:snapshot", STATE_STORAGE_KEY = "sanityStudio:themer:state", LEGACY_STORAGE_KEY = "sanityStudio:themer:options", LEGACY_DRAFT = {
349
356
  slug: "custom-draft",
350
357
  title: "Draft theme"
@@ -418,18 +425,20 @@ function revokeObjectUrl(url) {
418
425
  }
419
426
  /**
420
427
  * 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.
428
+ * `flow` the sidebar is in — picking a theme from the `list`, with its
429
+ * dialogs for pasting a shared code and for the applied theme's snippet,
430
+ * `edit`ing one of the user's own themes, or restoring `removed` ones — and,
431
+ * in a parallel `theme` region, whether another theme was just applied. The
432
+ * context carries the persisted state (the applied theme, the user's themes
433
+ * and what was removed) alongside what the flows need. Whether the sidebar is
434
+ * open and whether the Studio shows twice is not the machine's: the tool
435
+ * reducer in `ThemerProvider` owns that, so that the navbar toggle and the
436
+ * sidebar can change it in transitions of their own. Nor is persisting: the
437
+ * machine never touches storage and takes no input — it starts from no
438
+ * themes, and a session with something to pick up from restores its
439
+ * persisted snapshot instead (see `storage.ts` and `sync.ts`), while the
440
+ * persisted state of other tabs reaches it as a `themes.sync` event like any
441
+ * other.
433
442
  *
434
443
  * The `theme` region is `switching` (its tag) right after an event that
435
444
  * applies another theme — picking one, adding, duplicating, importing or
@@ -453,6 +462,7 @@ const themerMachine = setup({
453
462
  isCustomTheme: ({ context }, params) => themesOf(context).themes.some((theme) => theme.slug === params.slug && theme.source === "custom"),
454
463
  isEditingListedTheme: ({ context }) => context.editing !== null && themesOf(context).themes.some((theme) => theme.slug === context.editing?.slug && theme.source === "custom"),
455
464
  hasRemovedThemes: ({ context }) => themesOf(context).removed.length > 0,
465
+ hasAppliedTheme: ({ context }) => themesOf(context).active !== void 0,
456
466
  isAnotherTheme: ({ context }, params) => params.slug !== (context.active ?? "config"),
457
467
  isAppliedTheme: ({ context }, params) => params.slug === context.active,
458
468
  appliesAnotherTheme: ({ context }, params) => params.state.active !== context.active,
@@ -768,7 +778,26 @@ const themerMachine = setup({
768
778
  }
769
779
  },
770
780
  states: {
771
- list: {},
781
+ list: {
782
+ initial: "idle",
783
+ states: {
784
+ idle: { on: {
785
+ "dialog.paste": "pasting",
786
+ "dialog.snippet": {
787
+ guard: "hasAppliedTheme",
788
+ target: "snippet"
789
+ }
790
+ } },
791
+ pasting: { on: { "dialog.close": "idle" } },
792
+ snippet: {
793
+ on: { "dialog.close": "idle" },
794
+ always: {
795
+ guard: not("hasAppliedTheme"),
796
+ target: "idle"
797
+ }
798
+ }
799
+ }
800
+ },
772
801
  edit: {
773
802
  exit: "stopEditing",
774
803
  always: {
@@ -784,57 +813,225 @@ const themerMachine = setup({
784
813
  }
785
814
  }
786
815
  });
816
+ /** The part of the machine context that is persisted between sessions @internal */
817
+ function selectStoredState(snapshot) {
818
+ let { active, custom, removed, order } = snapshot.context;
819
+ return {
820
+ active,
821
+ custom,
822
+ removed,
823
+ order
824
+ };
825
+ }
826
+ /** The channel every tab of the same origin shares — one Studio, one set of themes */
827
+ const PERSIST_TIMEOUT = 1e3;
828
+ function openBroadcastChannel() {
829
+ if (typeof BroadcastChannel != "function") return null;
830
+ let channel = new BroadcastChannel("sanity-themer");
831
+ return {
832
+ post: (message) => channel.postMessage(message),
833
+ listen: (listener) => {
834
+ let onMessage = (event) => listener(event.data);
835
+ return channel.addEventListener("message", onMessage), () => channel.removeEventListener("message", onMessage);
836
+ },
837
+ close: () => channel.close()
838
+ };
839
+ }
840
+ function browserLocks() {
841
+ return typeof navigator < "u" && "locks" in navigator ? navigator.locks : void 0;
842
+ }
843
+ function scheduleIdle(callback) {
844
+ if (typeof requestIdleCallback == "function") {
845
+ let id = requestIdleCallback(callback, { timeout: PERSIST_TIMEOUT });
846
+ return () => cancelIdleCallback(id);
847
+ }
848
+ let id = setTimeout(callback, PERSIST_TIMEOUT);
849
+ return () => clearTimeout(id);
850
+ }
851
+ function parseMessage(data) {
852
+ if (!data || typeof data != "object") return null;
853
+ let type = Reflect.get(data, "type");
854
+ if (type === "hello") {
855
+ let from = Reflect.get(data, "from");
856
+ return typeof from == "string" ? {
857
+ type,
858
+ from
859
+ } : null;
860
+ }
861
+ if (type === "state") {
862
+ let state = parseThemerState(Reflect.get(data, "state")), revision = Reflect.get(data, "revision"), from = Reflect.get(data, "from"), to = Reflect.get(data, "to");
863
+ return !state || typeof revision != "number" || !Number.isFinite(revision) || typeof from != "string" ? null : {
864
+ type,
865
+ state,
866
+ revision,
867
+ from,
868
+ ...typeof to == "string" ? { to } : {}
869
+ };
870
+ }
871
+ return null;
872
+ }
873
+ function tabId() {
874
+ return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2);
875
+ }
876
+ /**
877
+ * Keeps the themes of every tab of the Studio the same, and persists them
878
+ * from one tab only.
879
+ *
880
+ * Every tab runs its own machine, and what the machine persists — the
881
+ * applied theme, the user's themes, what was removed and reordered — goes
882
+ * around to the other tabs on a `BroadcastChannel` as it changes, where it
883
+ * reaches their machines as a `themes.sync` event: the same themes
884
+ * everywhere, with the flow each tab is in, the tool's open and split state
885
+ * (the reducer's, in `plugin.tsx`) and the images of this session staying
886
+ * the tab's own. A tab that starts asks the others for the current state,
887
+ * in case what it read from storage was not written yet.
888
+ *
889
+ * Persisting is the business of one tab at a time: the one holding the Web
890
+ * Lock, which it keeps for as long as it lives. The others queue for it, so
891
+ * the next tab takes over as the holder goes away, and a tab that takes
892
+ * over persists right away, in case the last one left with a write pending.
893
+ * Writes happen when the browser is idle — a run of edits is written once —
894
+ * and as the page hides, so that nothing pending is lost.
895
+ *
896
+ * Returns what stops the sync: it lets go of the lock, which hands the
897
+ * persisting on, and closes the channel.
898
+ *
899
+ * @internal
900
+ */
901
+ function syncThemer(actorRef, options) {
902
+ 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 = () => {
903
+ cancelPersist?.(), cancelPersist = void 0, persist(actorRef.getPersistedSnapshot());
904
+ }, persistSoon = () => {
905
+ cancelPersist?.(), cancelPersist = schedule(persistNow);
906
+ }, answer = (to) => {
907
+ channel?.post({
908
+ type: "state",
909
+ state: shared,
910
+ revision,
911
+ from: origin,
912
+ to
913
+ });
914
+ }, becomeLeader = () => {
915
+ leader = !0, persistNow();
916
+ for (let from of unanswered) answer(from);
917
+ unanswered.clear();
918
+ };
919
+ locks ? locks.request("sanity-themer:persist", { signal: abort.signal }, () => (becomeLeader(), new Promise((resolve) => {
920
+ abort.signal.addEventListener("abort", () => resolve(), { once: !0 });
921
+ }))).catch(() => {}) : becomeLeader();
922
+ let subscription = actorRef.subscribe((snapshot) => {
923
+ if (leader && persistSoon(), applying) return;
924
+ let state = selectStoredState(snapshot);
925
+ dequal(state, shared) || (shared = state, unchanged = !1, revision += 1, origin = id, channel?.post({
926
+ type: "state",
927
+ state,
928
+ revision,
929
+ from: id
930
+ }));
931
+ });
932
+ channel && (channel.listen((data) => {
933
+ let message = parseMessage(data);
934
+ if (message) {
935
+ if (message.type === "hello") {
936
+ leader ? answer(message.from) : unanswered.add(message.from);
937
+ return;
938
+ }
939
+ if (message.to !== void 0) {
940
+ if (message.to !== id) return;
941
+ if (!unchanged) {
942
+ if (origin === id) {
943
+ revision = Math.max(revision, message.revision) + 1, channel.post({
944
+ type: "state",
945
+ state: shared,
946
+ revision,
947
+ from: id
948
+ });
949
+ return;
950
+ }
951
+ if (!isNewer(message)) return;
952
+ }
953
+ } else if (!isNewer(message)) return;
954
+ if (revision = message.revision, origin = message.from, !dequal(message.state, shared)) {
955
+ shared = message.state, unchanged = !1, applying = !0;
956
+ try {
957
+ actorRef.send({
958
+ type: "themes.sync",
959
+ state: message.state
960
+ });
961
+ } finally {
962
+ applying = !1;
963
+ }
964
+ }
965
+ }
966
+ }), channel.post({
967
+ type: "hello",
968
+ from: id
969
+ }));
970
+ let flush = () => {
971
+ leader && cancelPersist && persistNow();
972
+ }, page = typeof window > "u" ? void 0 : window;
973
+ return page?.addEventListener("pagehide", flush), () => {
974
+ subscription.unsubscribe(), page?.removeEventListener("pagehide", flush), flush(), abort.abort(), channel?.close();
975
+ };
976
+ }
977
+ /**
978
+ * Persists the machine's snapshot for the next session, and keeps the themes
979
+ * in step with the other tabs of the Studio — from the one tab holding the
980
+ * Web Lock, when the browser is idle; see `sync.ts`. Rendered by the layout,
981
+ * outside the sidebar's `Activity`, so that it runs while the sidebar is
982
+ * closed too: other tabs change the themes whether this one shows them or
983
+ * not.
984
+ */
787
985
  function PersistActorSnapshot(t0) {
788
986
  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);
798
- });
799
- return () => {
800
- subscription.unsubscribe(), cancelOrClear();
801
- };
802
- }, t2 = [actorRef], $[0] = actorRef, $[1] = t1, $[2] = t2), useEffect(t1, t2), null;
987
+ return $[0] === actorRef ? (t1 = $[1], t2 = $[2]) : (t1 = () => syncThemer(actorRef, { persist: writePersistedSnapshot }), t2 = [actorRef], $[0] = actorRef, $[1] = t1, $[2] = t2), useEffect(t1, t2), null;
803
988
  }
804
989
  /**
805
990
  * The sidebar — everything in it, from the theme list to the snippet dialog —
806
991
  * 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
992
+ * so it must only ever mount from a transition: the navbar toggle prerenders
993
+ * it in one as the pointer comes near, which keeps the Studio as it was while
994
+ * the code loads, where an urgent render would suspend up to the Studio's
810
995
  * own boundary and swap the whole Studio for its loading screen.
811
996
  */
812
- const ResizableSidebar = lazy(() => import("./ResizableSidebar-BpgLA4XQ.js"));
997
+ const ResizableSidebar = lazy(() => import("./ResizableSidebar-C1nr3Ls2.js"));
813
998
  /**
814
- * Wraps the whole Studio so that the theme picked in the themer sidebar
815
- * applies everywhere while the user browses around, runs the themer machine
816
- * that the navbar toggle and the sidebar share — the user's themes, which one
817
- * is applied, which flow the sidebar is in and how the Studio is previewed —
818
- * and renders the sidebar next to the Studio. The sidebar sits at the
819
- * `layout` level rather than in `activeToolLayout` because the split preview
820
- * renders the Studio twice, and the sidebar must not come along.
999
+ * Wraps the whole Studio to run the themer machine that the sidebar's flows
1000
+ * share — the user's themes, which one is applied and which flow the sidebar
1001
+ * is in — and renders the sidebar next to the Studio. The sidebar sits at
1002
+ * the `layout` level rather than in `activeToolLayout` because the split
1003
+ * preview renders the Studio twice, and the sidebar must not come along.
821
1004
  *
822
- * The Studio next to the sidebar always follows the appearance setting
823
- * (light/dark/system) and the picked theme like any other theme would. The
824
- * split preview adds a second copy in the opposite scheme on the far side —
825
- * or on top, on small screens — through React's view transitions (React
826
- * 19.3), styled in `ThemerLayout.css.ts`.
1005
+ * Whether the sidebar is open and whether the Studio shows twice comes in as
1006
+ * props from the tool reducer in `ThemerProvider`, and goes on to the sidebar
1007
+ * as props along with the machine's actor, which the sidebar selects its
1008
+ * themes and flow from (see `ThemerProps`): the navbar toggle and the sidebar
1009
+ * dispatch to the reducer inside transitions tagged with a transition type
1010
+ * (see `addThemerTransitionType`), and the `ViewTransition` boundaries below
1011
+ * pick their classes by that type, so React animates the sidebar sliding in
1012
+ * and out, the split copy coming and going and a picked theme cross-fading
1013
+ * in (React 19.3), styled in `ViewTransitions.css.ts`. The sidebar and the
1014
+ * split copy stay mounted while hidden, in `Activity`, so that opening them
1015
+ * again is instant and they can prerender ahead of the transition. The
1016
+ * navbar toggle, which the Studio renders somewhere in `children`, reads the
1017
+ * same values and the plugin's options from the contexts
1018
+ * `ThemerNavbarProvider` provides in each copy of the Studio, and measures
1019
+ * the Studio navbar for the sidebar's header — in the Studio proper, not in
1020
+ * the split copy, which `ToolShouldDetectNavbarHeightContext` tells apart.
827
1021
  *
828
- * The machine says what shows and when the layout is in motion (its `panel`,
829
- * `split` and `moving` tags); the layout defers what shows, which puts the
830
- * panel's and the copy's mounts in a transition — what lets React animate
831
- * them — and picks the view transition classes from the tags.
1022
+ * The applied theme is the reducer's, provided above this layout in
1023
+ * `ThemerProvider`: the machine publishes it synchronously, which React does
1024
+ * not animate, so `ThemerThemeCrossfader` hands it to the reducer in a
1025
+ * transition of its own. The Studio next to the sidebar always follows the
1026
+ * appearance setting (light/dark/system) and the picked theme like any other
1027
+ * theme would; the split preview adds a second copy in the opposite scheme
1028
+ * on the far side.
832
1029
  *
833
1030
  * Whatever else on the page has a `view-transition-name` — an avatar the
834
1031
  * Studio names so it moves as one piece — comes along in step: every
835
1032
  * transition the layout starts carries `layoutTransitionType`, which the
836
1033
  * stylesheet keys on to move every group in the layout's time (see
837
- * `ThemerLayout.css.ts`).
1034
+ * `ViewTransitions.css.ts`).
838
1035
  *
839
1036
  * @internal
840
1037
  */
@@ -929,14 +1126,12 @@ function ThemerLayout(t0) {
929
1126
  }
930
1127
  /**
931
1128
  * One copy of the Studio, in the given color scheme — or, without one, in the
932
- * scheme the Studio is showing. The configured theme (`null`) goes through
933
- * the provider too, inheriting the Studio's, so picking a theme swaps it
934
- * instead of remounting the Studio under a new provider.
935
- *
936
- * The two copies of the split preview share the router, the document store
937
- * and every other provider above the layout — only the scheme differs — so
938
- * they stay in sync while navigating. The `color-scheme` of a forced scheme
939
- * keeps native form controls and scrollbars in step with it.
1129
+ * scheme the Studio is showing. The two copies of the split preview share the
1130
+ * router, the document store and every other provider above the layout —
1131
+ * only the scheme differs — so they stay in sync while navigating. The
1132
+ * `color-scheme` of a forced scheme keeps native form controls and scrollbars
1133
+ * in step with it. Each copy provides the navbar toggle it renders with the
1134
+ * contexts it reads (see `ThemerNavbarProvider`).
940
1135
  */
941
1136
  function StudioPreview(t0) {
942
1137
  let $ = c(17), { borderRight, children, config, dispatch, open, scheme, shouldDetectNavbarHeight, split } = t0, [boundaryElement, setBoundaryElement] = useState(null), t1;
@@ -972,6 +1167,12 @@ function StudioPreview(t0) {
972
1167
  }), $[13] = borderRight, $[14] = scheme, $[15] = t4, $[16] = t5) : t5 = $[16], t5;
973
1168
  }
974
1169
  function _temp() {}
1170
+ /**
1171
+ * Hands the applied theme to the tool reducer as the machine publishes it —
1172
+ * in a transition tagged to cross-fade while the machine says a theme is
1173
+ * being switched to, so that editing the applied theme's colors applies live
1174
+ * while picking another theme fades the Studio over to it
1175
+ */
975
1176
  function ThemerThemeCrossfader(t0) {
976
1177
  let $ = c(4), { actorRef, dispatch } = t0, t1, t2;
977
1178
  return $[0] !== actorRef || $[1] !== dispatch ? (t1 = () => {