@sanity/themer 0.9.0 → 0.10.0

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