@sanity/themer
0.8.10.9.0
dist/ResizableSidebar-BpgLA4XQ.js+
dist/ResizableSidebar-BpgLA4XQ.jsNew file+2437
Index: package/dist/ResizableSidebar-BpgLA4XQ.js
===================================================================
--- package/dist/ResizableSidebar-BpgLA4XQ.js
+++ package/dist/ResizableSidebar-BpgLA4XQ.js
@@ -0,0 +1,2437 @@
+import { a as DEFAULT_BACKGROUND, c as resolveThemeOptions, d as hslToHex, f as relativeLuminance, i as DEFAULT_ACCENT, l as isColor, n as buildTheme, o as SCHEMES, r as buildPalette, s as deriveTextColor, u as hexToHsl } from "./presets-CqBgS3A-.js";
+import { a as displayTitle, c as resolveThemes, l as addThemerTransitionType, n as CONFIG_TITLE, t as CONFIG_SLUG } from "./themes-Bxgltjmb.js";
+import { c } from "react/compiler-runtime";
+import { useColorSchemeValue } from "sanity";
+import { Badge, BoundaryElementProvider, Box, Button, Card, Dialog, Flex, Layer, Stack, Text, TextInput, ThemeProvider, useMediaIndex, useRootTheme, useTheme_v2 } from "@sanity/ui";
+import { Tooltip } from "@sanity/ui/tooltip";
+import { Activity, createContext, startTransition, useContext, useEffect, useRef, useState } from "react";
+import { Fragment, jsx, jsxs } from "react/jsx-runtime";
+import { dequal } from "dequal/lite";
+import { useSelector } from "@xstate/react";
+import { MotionConfig, Reorder, useDragControls } from "motion/react";
+import { ArrowLeftIcon } from "@sanity/icons/ArrowLeft";
+import { CloseIcon } from "@sanity/icons/Close";
+import { CodeBlockIcon } from "@sanity/icons/CodeBlock";
+import { SplitVerticalIcon } from "@sanity/icons/SplitVertical";
+import { assignInlineVars } from "@vanilla-extract/dynamic";
+import { ResetIcon } from "@sanity/icons/Reset";
+import { TrashIcon } from "@sanity/icons/Trash";
+import { SparklesIcon } from "@sanity/icons/Sparkles";
+import { ImageIcon } from "@sanity/icons/Image";
+import { useToast } from "@sanity/ui/toast";
+import { AddIcon } from "@sanity/icons/Add";
+import { ClipboardIcon } from "@sanity/icons/Clipboard";
+import { RestoreIcon } from "@sanity/icons/Restore";
+import { ArrowDownIcon } from "@sanity/icons/ArrowDown";
+import { ArrowUpIcon } from "@sanity/icons/ArrowUp";
+import { CopyIcon } from "@sanity/icons/Copy";
+import { EditIcon } from "@sanity/icons/Edit";
+import { EllipsisHorizontalIcon } from "@sanity/icons/EllipsisHorizontal";
+import { ShareIcon } from "@sanity/icons/Share";
+import { Menu, MenuButton, MenuDivider, MenuItem } from "@sanity/ui/menu";
+import scrollIntoView from "scroll-into-view-if-needed";
+import { Code } from "@sanity/ui/code";
+import { registerLanguage } from "react-refractor";
+import typescript from "refractor/typescript";
+var root$2 = "_1o50s7t0";
+const ScrollAreaInsetContext = createContext({
+ left: 0,
+ right: 0,
+ bottom: 0
+});
+/**
+* The gutter the last scroll area measured. It is the same for every scroll
+* area (the reserved gutter does not depend on the content), so the next one
+* starts out with the right padding instead of shifting after its first
+* measurement — which would also throw off anything scrolling into view as
+* the content mounts.
+*/
+let lastGutter = 0;
+/**
+* The scrolling body of a sidebar flow. It always reserves the scrollbar's
+* space, so the content does not shift when the list grows or shrinks past
+* the point where it needs to scroll — and it takes that space out of its
+* right padding, so the content stays centered rather than hugging the left.
+* With overlay scrollbars there is no gutter and the padding stays even.
+*
+* @internal
+*/
+function ScrollArea(props) {
+ let $ = c(15), { children, padding } = props, { space } = useTheme_v2(), ref = useRef(null), [gutter, setGutter] = useState(_temp$8), t0, t1;
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = () => {
+ let element = ref.current;
+ if (!element) return;
+ let measure = () => {
+ lastGutter = element.offsetWidth - element.clientWidth, setGutter(lastGutter);
+ };
+ measure();
+ let observer = new ResizeObserver(measure);
+ return observer.observe(element), () => observer.disconnect();
+ }, t1 = [], $[0] = t0, $[1] = t1) : (t0 = $[0], t1 = $[1]), useEffect(t0, t1);
+ let t2 = space[padding], t3 = Math.max(0, space[padding] - gutter), t4 = space[padding], t5;
+ $[2] !== t2 || $[3] !== t3 || $[4] !== t4 ? (t5 = {
+ left: t2,
+ right: t3,
+ bottom: t4
+ }, $[2] = t2, $[3] = t3, $[4] = t4, $[5] = t5) : t5 = $[5];
+ let inset = t5, t6;
+ $[6] === inset.right ? t6 = $[7] : (t6 = { paddingRight: inset.right }, $[6] = inset.right, $[7] = t6);
+ let t7;
+ $[8] !== children || $[9] !== inset ? (t7 = /* @__PURE__ */ jsx(ScrollAreaInsetContext, {
+ value: inset,
+ children
+ }), $[8] = children, $[9] = inset, $[10] = t7) : t7 = $[10];
+ let t8;
+ return $[11] !== padding || $[12] !== t6 || $[13] !== t7 ? (t8 = /* @__PURE__ */ jsx(Box, {
+ className: root$2,
+ flex: 1,
+ overflow: "auto",
+ padding,
+ ref,
+ style: t6,
+ children: t7
+ }), $[11] = padding, $[12] = t6, $[13] = t7, $[14] = t8) : t8 = $[14], t8;
+}
+function _temp$8() {
+ return lastGutter;
+}
+/**
+* Content that runs edge to edge in a scroll area — up to the scrollbar —
+* instead of sitting inside its padding. It goes last: it runs down to the
+* bottom edge too.
+*
+* @internal
+*/
+function ScrollAreaBleed(props) {
+ let $ = c(7), { left, right, bottom } = useContext(ScrollAreaInsetContext), t0 = -left, t1 = -right, t2 = -bottom, t3;
+ $[0] !== t0 || $[1] !== t1 || $[2] !== t2 ? (t3 = {
+ marginLeft: t0,
+ marginRight: t1,
+ marginBottom: t2
+ }, $[0] = t0, $[1] = t1, $[2] = t2, $[3] = t3) : t3 = $[3];
+ let t4;
+ return $[4] !== props.children || $[5] !== t3 ? (t4 = /* @__PURE__ */ jsx("div", {
+ style: t3,
+ children: props.children
+ }), $[4] = props.children, $[5] = t3, $[6] = t4) : t4 = $[6], t4;
+}
+var avatar = "_1aa9huuh", body = "_1aa9huui", button = "_1aa9huuq", colors = {
+ bg: "var(--_1aa9huu0)",
+ fg: "var(--_1aa9huu1)",
+ mutedFg: "var(--_1aa9huu2)",
+ border: "var(--_1aa9huu3)",
+ accent: "var(--_1aa9huu4)",
+ avatar: "var(--_1aa9huu5)",
+ selectedBg: "var(--_1aa9huu6)",
+ selectedFg: "var(--_1aa9huu7)",
+ inputBg: "var(--_1aa9huu8)",
+ inputBorder: "var(--_1aa9huu9)"
+}, field = "_1aa9huun", footer = "_1aa9huup", form = "_1aa9huum", input = "_1aa9huuo", line = {
+ fg: "_1aa9huus _1aa9huur",
+ muted: "_1aa9huut _1aa9huur",
+ selected: "_1aa9huuu _1aa9huur"
+}, list = "_1aa9huuj", listRow = "_1aa9huuk", logo = "_1aa9huuf", navbar = "_1aa9huue", pane = "_1aa9huul", root$1 = "_1aa9huua", spacer = "_1aa9huug", studio = {
+ light: "_1aa9huub",
+ dark: "_1aa9huud _1aa9huub"
+};
+/** The document rows in the list pane — the widths of their title lines */
+const LIST_ROWS = [
+ "62%",
+ "46%",
+ "70%",
+ "52%"
+], FIELDS$1 = ["32%", "24%"];
+/**
+* A tiny, lo-fi mockup of a Studio in the given theme — the navbar over the
+* structure tool with a list of documents on the left and a document form on
+* the right — in both color schemes at once: the light scheme on the left and
+* the dark scheme on the right, split diagonally. It only paints colors the
+* theme options change (backgrounds, text, borders, the accent) so that a
+* glance shows what the theme does to the Studio — with few and bold enough
+* elements to still read when scaled down to a thumbnail.
+*
+* @internal
+*/
+function ThemePreview(props) {
+ let $ = c(11), { options } = props, t0;
+ $[0] === options ? t0 = $[1] : (t0 = buildTheme(options), $[0] = options, $[1] = t0);
+ let theme = t0, t1;
+ $[2] === Symbol.for("react.memo_cache_sentinel") ? (t1 = /* @__PURE__ */ jsx(MockStudio, { scheme: "light" }), $[2] = t1) : t1 = $[2];
+ let t2;
+ $[3] === theme ? t2 = $[4] : (t2 = /* @__PURE__ */ jsx(ThemeProvider, {
+ scheme: "light",
+ theme,
+ tone: "default",
+ children: t1
+ }), $[3] = theme, $[4] = t2);
+ let t3;
+ $[5] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx(MockStudio, { scheme: "dark" }), $[5] = t3) : t3 = $[5];
+ let t4;
+ $[6] === theme ? t4 = $[7] : (t4 = /* @__PURE__ */ jsx(ThemeProvider, {
+ scheme: "dark",
+ theme,
+ tone: "default",
+ children: t3
+ }), $[6] = theme, $[7] = t4);
+ let t5;
+ return $[8] !== t2 || $[9] !== t4 ? (t5 = /* @__PURE__ */ jsxs("div", {
+ "aria-hidden": !0,
+ className: root$1,
+ children: [t2, t4]
+ }), $[8] = t2, $[9] = t4, $[10] = t5) : t5 = $[10], t5;
+}
+function MockStudio(props) {
+ let $ = c(19), { scheme } = props, { color } = useTheme_v2(), selected = color.selectable.default.selected, inputColor = color.input.default.enabled, t0 = studio[scheme], t1;
+ $[0] !== color.avatar.blue.bg || $[1] !== color.bg || $[2] !== color.border || $[3] !== color.button.default.primary.enabled.bg || $[4] !== color.fg || $[5] !== color.muted.fg || $[6] !== inputColor.bg || $[7] !== inputColor.border || $[8] !== selected.bg || $[9] !== selected.fg ? (t1 = assignInlineVars({
+ [colors.bg]: color.bg,
+ [colors.fg]: color.fg,
+ [colors.mutedFg]: color.muted.fg,
+ [colors.border]: color.border,
+ [colors.accent]: color.button.default.primary.enabled.bg,
+ [colors.avatar]: color.avatar.blue.bg,
+ [colors.selectedBg]: selected.bg,
+ [colors.selectedFg]: selected.fg,
+ [colors.inputBg]: inputColor.bg,
+ [colors.inputBorder]: inputColor.border
+ }), $[0] = color.avatar.blue.bg, $[1] = color.bg, $[2] = color.border, $[3] = color.button.default.primary.enabled.bg, $[4] = color.fg, $[5] = color.muted.fg, $[6] = inputColor.bg, $[7] = inputColor.border, $[8] = selected.bg, $[9] = selected.fg, $[10] = t1) : t1 = $[10];
+ let t2;
+ $[11] === Symbol.for("react.memo_cache_sentinel") ? (t2 = /* @__PURE__ */ jsx("div", { className: logo }), $[11] = t2) : t2 = $[11];
+ let t3;
+ $[12] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsxs("div", {
+ className: navbar,
+ children: [
+ t2,
+ /* @__PURE__ */ jsx("div", {
+ className: line.fg,
+ style: { width: "22%" }
+ }),
+ /* @__PURE__ */ jsx("div", { className: spacer }),
+ /* @__PURE__ */ jsx("div", { className: avatar })
+ ]
+ }), $[12] = t3) : t3 = $[12];
+ let t4;
+ $[13] === Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx("div", {
+ className: list,
+ children: LIST_ROWS.map(_temp$7)
+ }), $[13] = t4) : t4 = $[13];
+ let t5;
+ $[14] === Symbol.for("react.memo_cache_sentinel") ? (t5 = /* @__PURE__ */ jsxs("div", {
+ className: form,
+ children: [/* @__PURE__ */ jsx("div", {
+ className: line.fg,
+ style: { width: "45%" }
+ }), FIELDS$1.map(_temp2$1)]
+ }), $[14] = t5) : t5 = $[14];
+ let t6;
+ $[15] === Symbol.for("react.memo_cache_sentinel") ? (t6 = /* @__PURE__ */ jsxs("div", {
+ className: body,
+ children: [t4, /* @__PURE__ */ jsxs("div", {
+ className: pane,
+ children: [t5, /* @__PURE__ */ jsx("div", {
+ className: footer,
+ children: /* @__PURE__ */ jsx("div", { className: button })
+ })]
+ })]
+ }), $[15] = t6) : t6 = $[15];
+ let t7;
+ return $[16] !== t0 || $[17] !== t1 ? (t7 = /* @__PURE__ */ jsxs("div", {
+ className: t0,
+ style: t1,
+ children: [t3, t6]
+ }), $[16] = t0, $[17] = t1, $[18] = t7) : t7 = $[18], t7;
+}
+function _temp2$1(width_0) {
+ return /* @__PURE__ */ jsxs("div", {
+ className: field,
+ children: [/* @__PURE__ */ jsx("div", {
+ className: line.muted,
+ style: { width: width_0 }
+ }), /* @__PURE__ */ jsx("div", { className: input })]
+ }, width_0);
+}
+function _temp$7(width, index) {
+ let isSelected = index === 1;
+ return /* @__PURE__ */ jsx("div", {
+ className: listRow,
+ "data-selected": isSelected,
+ children: /* @__PURE__ */ jsx("div", {
+ className: isSelected ? line.selected : line.muted,
+ style: { width }
+ })
+ }, width);
+}
+/**
+* The floating preview card of a theme: the lo-fi studio mockup on a raised,
+* rounded card, like the appearance previews in macOS System Settings.
+*
+* @internal
+*/
+function ThemeThumbnail(props) {
+ let $ = c(2), t0;
+ return $[0] === props.options ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(Card, {
+ overflow: "hidden",
+ radius: 3,
+ shadow: 1,
+ children: /* @__PURE__ */ jsx(ThemePreview, { options: props.options })
+ }), $[0] = props.options, $[1] = t0), t0;
+}
+var removedThumbnail = "_1vdkp0v0";
+/**
+* The flow for restoring removed themes: the presets and custom themes the
+* user took out of the list, each with a button to put it back — and, for
+* custom themes, one to delete it for good. The machine leaves the flow once
+* the last one is gone.
+*
+* @internal
+*/
+function RemovedThemes(t0) {
+ let $ = c(7), { actorRef, removed } = t0, t1;
+ if ($[0] !== actorRef || $[1] !== removed) {
+ let t2;
+ $[3] === actorRef ? t2 = $[4] : (t2 = (theme) => /* @__PURE__ */ jsxs(Stack, {
+ gap: 3,
+ children: [
+ /* @__PURE__ */ jsx(Box, {
+ className: removedThumbnail,
+ children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: theme.options })
+ }),
+ /* @__PURE__ */ jsx(Text, {
+ align: "center",
+ size: 1,
+ textOverflow: "ellipsis",
+ children: displayTitle(theme.title)
+ }),
+ /* @__PURE__ */ jsxs(Flex, {
+ gap: 2,
+ justify: "center",
+ children: [/* @__PURE__ */ jsx(Button, {
+ mode: "ghost",
+ onClick: () => actorRef.send({
+ type: "theme.restore",
+ slug: theme.slug
+ }),
+ padding: 2,
+ text: "Restore"
+ }), theme.source === "custom" && /* @__PURE__ */ jsx(Button, {
+ mode: "ghost",
+ onClick: () => actorRef.send({
+ type: "theme.delete",
+ slug: theme.slug
+ }),
+ padding: 2,
+ text: "Delete",
+ tone: "critical"
+ })]
+ })
+ ]
+ }, theme.slug), $[3] = actorRef, $[4] = t2), t1 = removed.map(t2), $[0] = actorRef, $[1] = removed, $[2] = t1;
+ } else t1 = $[2];
+ let t2;
+ return $[5] === t1 ? t2 = $[6] : (t2 = /* @__PURE__ */ jsx(ScrollArea, {
+ padding: 3,
+ children: /* @__PURE__ */ jsx(Stack, {
+ gap: 5,
+ children: t1
+ })
+ }), $[5] = t1, $[6] = t2), t2;
+}
+/** The variants in the order the tool offers them @internal */
+const IMAGE_PALETTE_VARIANTS = [
+ "muted",
+ "vibrant",
+ "lightMuted",
+ "lightVibrant",
+ "darkMuted",
+ "darkVibrant"
+], IMAGE_PALETTE_TITLES = {
+ dominant: "Dominant",
+ vibrant: "Vibrant",
+ lightVibrant: "Light vibrant",
+ darkVibrant: "Dark vibrant",
+ muted: "Muted",
+ lightMuted: "Light muted",
+ darkMuted: "Dark muted"
+}, TARGETS = [
+ ["vibrant", {
+ lightness: [
+ .5,
+ .3,
+ .7
+ ],
+ saturation: [
+ 1,
+ .35,
+ 1
+ ]
+ }],
+ ["lightVibrant", {
+ lightness: [
+ .74,
+ .55,
+ 1
+ ],
+ saturation: [
+ 1,
+ .35,
+ 1
+ ]
+ }],
+ ["darkVibrant", {
+ lightness: [
+ .26,
+ 0,
+ .45
+ ],
+ saturation: [
+ 1,
+ .35,
+ 1
+ ]
+ }],
+ ["muted", {
+ lightness: [
+ .5,
+ .3,
+ .7
+ ],
+ saturation: [
+ .3,
+ 0,
+ .4
+ ]
+ }],
+ ["lightMuted", {
+ lightness: [
+ .74,
+ .55,
+ 1
+ ],
+ saturation: [
+ .3,
+ 0,
+ .4
+ ]
+ }],
+ ["darkMuted", {
+ lightness: [
+ .26,
+ 0,
+ .45
+ ],
+ saturation: [
+ .3,
+ 0,
+ .4
+ ]
+ }]
+];
+/**
+* Extracts the color palette of an image from its pixels, the way the Vibrant
+* algorithm does: the colors are quantized into a coarse histogram, and each
+* swatch claims the color that comes closest to its target lightness and
+* saturation — weighted by how much of the image it covers — that no other
+* swatch has claimed yet. The dominant swatch is simply the most common
+* color. Runs anywhere pixels come from, so it can run on device.
+*
+* @internal
+*/
+function extractImagePalette(pixels) {
+ let candidates = quantize(pixels), palette = {
+ dominant: null,
+ vibrant: null,
+ lightVibrant: null,
+ darkVibrant: null,
+ muted: null,
+ lightMuted: null,
+ darkMuted: null
+ };
+ if (candidates.length === 0) return palette;
+ let maximumPopulation = Math.max(...candidates.map((candidate) => candidate.population)), claimed = /* @__PURE__ */ new Set();
+ for (let [key, target] of TARGETS) {
+ let best = null, bestScore = -Infinity;
+ for (let candidate of candidates) {
+ if (claimed.has(candidate.hex) || !matchesTarget(candidate, target)) continue;
+ let score = scoreCandidate(candidate, target, maximumPopulation);
+ score > bestScore && (best = candidate, bestScore = score);
+ }
+ best && (palette[key] = best.hex, claimed.add(best.hex));
+ }
+ return palette.dominant = candidates.reduce((most, candidate) => candidate.population > most.population ? candidate : most).hex, palette;
+}
+/**
+* Turns the pixels into a histogram of quantized colors, each averaged over
+* the pixels it stands for, leaving out transparent pixels and the near-black
+* and near-white ones that are backdrop rather than color.
+*/
+function quantize(pixels) {
+ let { data } = pixels, buckets = /* @__PURE__ */ new Map(), length = Math.min(data.length, pixels.width * pixels.height * 4);
+ for (let offset = 0; offset + 3 < length; offset += 4) {
+ let r = data[offset], g = data[offset + 1], b = data[offset + 2];
+ if (data[offset + 3] < 125) continue;
+ let lightness = (Math.max(r, g, b) + Math.min(r, g, b)) / 510;
+ if (lightness < .05 || lightness > .95) continue;
+ let key = r >> 3 << 10 | g >> 3 << 5 | b >> 3, bucket = buckets.get(key);
+ bucket ? (bucket.count++, bucket.r += r, bucket.g += g, bucket.b += b) : buckets.set(key, {
+ count: 1,
+ r,
+ g,
+ b
+ });
+ }
+ let candidates = [];
+ for (let bucket of buckets.values()) {
+ let r = Math.round(bucket.r / bucket.count), g = Math.round(bucket.g / bucket.count), b = Math.round(bucket.b / bucket.count), { saturation, lightness } = saturationAndLightness(r, g, b);
+ candidates.push({
+ hex: rgbToHex(r, g, b),
+ population: bucket.count,
+ saturation,
+ lightness
+ });
+ }
+ return candidates;
+}
+function matchesTarget(candidate, target) {
+ return candidate.lightness >= target.lightness[1] && candidate.lightness <= target.lightness[2] && candidate.saturation >= target.saturation[1] && candidate.saturation <= target.saturation[2];
+}
+function scoreCandidate(candidate, target, maximumPopulation) {
+ return 3 * (1 - Math.abs(candidate.saturation - target.saturation[0])) + 6.5 * (1 - Math.abs(candidate.lightness - target.lightness[0])) + .5 * (candidate.population / maximumPopulation);
+}
+function saturationAndLightness(r, g, b) {
+ let max = Math.max(r, g, b) / 255, min = Math.min(r, g, b) / 255, lightness = (max + min) / 2, delta = max - min;
+ return {
+ saturation: delta === 0 ? 0 : lightness > .5 ? delta / (2 - max - min) : delta / (max + min),
+ lightness
+ };
+}
+function rgbToHex(r, g, b) {
+ return `#${[
+ r,
+ g,
+ b
+ ].map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
+}
+/** Reads `#rgb` and `#rrggbb` alike — stored palettes may hold either */
+function hexToRgb(hex) {
+ let expanded = hex.length === 4 ? `${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex.slice(1);
+ return [
+ parseInt(expanded.slice(0, 2), 16),
+ parseInt(expanded.slice(2, 4), 16),
+ parseInt(expanded.slice(4, 6), 16)
+ ];
+}
+/** Mixes `amount` of `color` into `base` */
+function tint(base, color, amount) {
+ let [r1, g1, b1] = hexToRgb(base), [r2, g2, b2] = hexToRgb(color);
+ return rgbToHex(Math.round(r1 + (r2 - r1) * amount), Math.round(g1 + (g2 - g1) * amount), Math.round(b1 + (b2 - b1) * amount));
+}
+/** How much of the palette tints each scheme's stock background */
+const BACKGROUND_TINT = {
+ light: .08,
+ dark: .3
+}, BUTTON_LABEL = {
+ light: "#ffffff",
+ dark: "#000000"
+}, ACCENT_LIGHTNESS_STEP = .01;
+function contrastRatio(a, b) {
+ let [lighter, darker] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
+ return (lighter + .05) / (darker + .05);
+}
+/**
+* Makes an accent readable as a button in the given scheme: a light scheme
+* accent is darkened until white text on it reaches AA contrast, a dark
+* scheme accent lightened until black text does — as far as the accent's
+* hue and saturation allow. Accents that already pass come back untouched.
+*
+* @internal
+*/
+function readableAccent(hex, scheme) {
+ let label = BUTTON_LABEL[scheme], { h, s } = hexToHsl(hex), { l } = hexToHsl(hex), current = hex;
+ for (; contrastRatio(current, label) < 4.5;) {
+ let next = scheme === "light" ? l - ACCENT_LIGHTNESS_STEP : l + ACCENT_LIGHTNESS_STEP;
+ if (next < 0 || next > 1) break;
+ l = next, current = hslToHex({
+ h,
+ s,
+ l
+ });
+ }
+ return current;
+}
+/** The accent a variant gives a scheme, made readable on a button */
+function variantAccent(palette, variant, scheme) {
+ let swatch = (variant ? palette[variant] : null) ?? palette.vibrant ?? (scheme === "light" ? palette.darkVibrant : palette.lightVibrant) ?? palette.dominant;
+ return swatch === null ? null : readableAccent(swatch, scheme);
+}
+/**
+* Derives theme options from an image palette: the accent of both schemes is
+* the swatch of the chosen variant — or, without one, the vibrant swatch,
+* each scheme falling back to the vibrant swatch of its own end of the scale —
+* adjusted until it is readable as a button. The muted swatch becomes the text color, and the light and dark muted
+* swatches tint the stock backgrounds of their scheme — enough to carry the
+* image's tone without giving up a usable canvas. Whatever the palette lacks
+* stays at the theme defaults.
+*
+* @internal
+*/
+function optionsFromImagePalette(palette, variant) {
+ let options = {}, light = schemeFromPalette(variantAccent(palette, variant ?? null, "light"), palette.muted ?? palette.darkMuted, palette.lightMuted ?? palette.muted ?? palette.lightVibrant, "light"), dark = schemeFromPalette(variantAccent(palette, variant ?? null, "dark"), palette.muted ?? palette.lightMuted, palette.darkMuted ?? palette.muted ?? palette.darkVibrant, "dark");
+ return light && (options.light = light), dark && (options.dark = dark), options;
+}
+function schemeFromPalette(accent, text, backgroundTint, scheme) {
+ let options = {};
+ return accent && (options.accent = accent), text && (options.text = text), backgroundTint && (options.background = tint(DEFAULT_BACKGROUND[scheme], backgroundTint, BACKGROUND_TINT[scheme])), Object.keys(options).length > 0 ? options : null;
+}
+/**
+* Applies an image palette to existing theme options: the colors the palette
+* provides replace the current ones scheme by scheme, and whatever it does
+* not touch — like the contrast — stays as it is.
+*
+* @internal
+*/
+function applyImagePalette(options, palette, variant) {
+ let derived = optionsFromImagePalette(palette, variant), next = { ...options };
+ for (let scheme of SCHEMES) derived[scheme] && (next[scheme] = {
+ ...options[scheme],
+ ...derived[scheme]
+ });
+ return next;
+}
+/**
+* The variant a theme is built around: the one whose swatch is the theme's
+* accent, or `null` when the accent comes from somewhere else.
+*
+* @internal
+*/
+function currentImageVariant(options, palette) {
+ let accent = options.light?.accent;
+ return accent ? IMAGE_PALETTE_VARIANTS.find((variant) => palette[variant] !== null && variantAccent(palette, variant, "light") === accent) ?? null : null;
+}
+/**
+* How much a swatch has going for it as an accent: saturation first, then
+* closeness to mid lightness — the range where an accent reads best.
+*/
+function interest(hex) {
+ let { s, l } = hexToHsl(hex);
+ return .1 + .55 * s + .35 * (1 - Math.abs(l - .5) * 2);
+}
+/**
+* Picks a variant for the "I'm feeling lucky" button: a random one, weighted
+* by how interesting its swatch is as an accent, and never the one the theme
+* already uses while there are others to pick from.
+*
+* @internal
+*/
+function pickLuckyVariant(palette, options = {}) {
+ let { exclude = null, random = Math.random } = options, candidates = IMAGE_PALETTE_VARIANTS.filter((variant) => palette[variant] !== null);
+ if (candidates.length > 1 && (candidates = candidates.filter((variant) => variant !== exclude)), candidates.length === 0) return null;
+ let weights = candidates.map((variant) => interest(palette[variant])), total = weights.reduce((sum, weight) => sum + weight, 0), pick = random() * total;
+ for (let [index, variant] of candidates.entries()) if (pick -= weights[index], pick < 0) return variant;
+ return candidates.at(-1) ?? null;
+}
+/** A theme title for the image a palette came from, e.g. `sunset-beach.jpg` → `sunset beach` @internal */
+function titleFromFileName(name) {
+ return name.replace(/\.[^.]+$/, "").replace(/[-_]+/g, " ").trim() || "Image theme";
+}
+/**
+* A button with an animated tooltip instead of a `title` attribute, sized like
+* the Studio's own buttons (`padding` and `gap` of 2). Icon-only buttons take
+* the tooltip as their accessible name.
+*
+* @internal
+*/
+function TooltipButton(props) {
+ let $ = c(11), buttonProps, tooltip;
+ $[0] === props ? (buttonProps = $[1], tooltip = $[2]) : ({tooltip, ...buttonProps} = props, $[0] = props, $[1] = buttonProps, $[2] = tooltip);
+ let t0;
+ $[3] === tooltip ? t0 = $[4] : (t0 = /* @__PURE__ */ jsx(Text, {
+ size: 1,
+ children: tooltip
+ }), $[3] = tooltip, $[4] = t0);
+ let t1 = buttonProps.text === void 0 ? tooltip : void 0, t2;
+ $[5] !== buttonProps || $[6] !== t1 ? (t2 = /* @__PURE__ */ jsx(Button, {
+ "aria-label": t1,
+ gap: 2,
+ padding: 2,
+ ...buttonProps
+ }), $[5] = buttonProps, $[6] = t1, $[7] = t2) : t2 = $[7];
+ let t3;
+ return $[8] !== t0 || $[9] !== t2 ? (t3 = /* @__PURE__ */ jsx(Tooltip, {
+ animate: !0,
+ content: t0,
+ placement: "bottom",
+ portal: !0,
+ children: t2
+ }), $[8] = t0, $[9] = t2, $[10] = t3) : t3 = $[10], t3;
+}
+/**
+* A button that opens the file picker for an image, and hands the chosen file
+* over — the picker is a hidden file input, so nothing is uploaded anywhere.
+*
+* @internal
+*/
+function ImageFileButton(props) {
+ let $ = c(11), buttonProps, onFile;
+ $[0] === props ? (buttonProps = $[1], onFile = $[2]) : ({onFile, ...buttonProps} = props, $[0] = props, $[1] = buttonProps, $[2] = onFile);
+ let inputRef = useRef(null), t0;
+ $[3] === Symbol.for("react.memo_cache_sentinel") ? (t0 = () => inputRef.current?.click(), $[3] = t0) : t0 = $[3];
+ let t1;
+ $[4] === buttonProps ? t1 = $[5] : (t1 = /* @__PURE__ */ jsx(TooltipButton, {
+ ...buttonProps,
+ icon: ImageIcon,
+ onClick: t0
+ }), $[4] = buttonProps, $[5] = t1);
+ let t2;
+ $[6] === onFile ? t2 = $[7] : (t2 = /* @__PURE__ */ jsx("input", {
+ accept: "image/*",
+ hidden: !0,
+ onChange: (event) => {
+ let file = event.currentTarget.files?.[0];
+ event.currentTarget.value = "", file && onFile(file);
+ },
+ ref: inputRef,
+ type: "file"
+ }), $[6] = onFile, $[7] = t2);
+ let t3;
+ return $[8] !== t1 || $[9] !== t2 ? (t3 = /* @__PURE__ */ jsxs(Fragment, { children: [t1, t2] }), $[8] = t1, $[9] = t2, $[10] = t3) : t3 = $[10], t3;
+}
+/**
+* Reads the pixels of an image file on device: the image is decoded, drawn
+* onto a small canvas and read back, so nothing leaves the browser.
+*
+* @internal
+*/
+async function readImagePixels(file) {
+ let image = await decodeImage(file);
+ try {
+ let scale = Math.min(1, 96 / Math.max(image.width, image.height)), width = Math.max(1, Math.round(image.width * scale)), height = Math.max(1, Math.round(image.height * scale)), canvas = document.createElement("canvas");
+ canvas.width = width, canvas.height = height;
+ let context = canvas.getContext("2d", { willReadFrequently: !0 });
+ if (!context) throw Error("Could not draw the image");
+ return context.drawImage(image, 0, 0, width, height), {
+ data: context.getImageData(0, 0, width, height).data,
+ width,
+ height
+ };
+ } finally {
+ typeof ImageBitmap < "u" && image instanceof ImageBitmap && image.close();
+ }
+}
+async function decodeImage(file) {
+ if (typeof createImageBitmap == "function") try {
+ return await createImageBitmap(file);
+ } catch {}
+ let url = URL.createObjectURL(file);
+ try {
+ return await new Promise((resolve, reject) => {
+ let image = new Image();
+ image.addEventListener("load", () => resolve(image), { once: !0 }), image.addEventListener("error", () => reject(/* @__PURE__ */ Error("Could not decode the image")), { once: !0 }), image.src = url;
+ });
+ } finally {
+ URL.revokeObjectURL(url);
+ }
+}
+/**
+* Extracts the palette of image files on device and hands it to `onPalette`,
+* along with the file it came from. Failures end up as a toast.
+*
+* @internal
+*/
+function useImagePalette(onPalette) {
+ let $ = c(10), toast = useToast(), [busy, setBusy] = useState(!1), onPaletteRef = useRef(onPalette), mountedRef = useRef(!0), latestPickRef = useRef(0), t0, t1;
+ $[0] === onPalette ? (t0 = $[1], t1 = $[2]) : (t0 = () => {
+ onPaletteRef.current = onPalette;
+ }, t1 = [onPalette], $[0] = onPalette, $[1] = t0, $[2] = t1), useEffect(t0, t1);
+ let t2, t3;
+ $[3] === Symbol.for("react.memo_cache_sentinel") ? (t2 = () => (mountedRef.current = !0, () => {
+ mountedRef.current = !1;
+ }), t3 = [], $[3] = t2, $[4] = t3) : (t2 = $[3], t3 = $[4]), useEffect(t2, t3);
+ let t4;
+ $[5] === toast ? t4 = $[6] : (t4 = (file) => {
+ let pick = latestPickRef.current += 1;
+ setBusy(!0);
+ let isCurrent = () => mountedRef.current && latestPickRef.current === pick, extract = async () => {
+ let palette = extractImagePalette(await readImagePixels(file));
+ isCurrent() && (Object.values(palette).every(_temp$6) ? toast.push({
+ status: "warning",
+ title: "Found no colors in that image"
+ }) : onPaletteRef.current(palette, file));
+ };
+ extract().catch(() => {
+ isCurrent() && toast.push({
+ status: "error",
+ title: "Could not read that image"
+ });
+ }).finally(() => {
+ isCurrent() && setBusy(!1);
+ });
+ }, $[5] = toast, $[6] = t4);
+ let pickImage = t4, t5;
+ return $[7] !== busy || $[8] !== pickImage ? (t5 = {
+ busy,
+ pickImage
+ }, $[7] = busy, $[8] = pickImage, $[9] = t5) : t5 = $[9], t5;
+}
+function _temp$6(swatch) {
+ return swatch === null;
+}
+var paletteGrid = "_17s80v42", tile = "_17s80v44", variantButton = "_17s80v46", variantFrame = "_17s80v47", variantRow = "_17s80v45", variantScale = "var(--_17s80v41)", variantScaleBox = "_17s80v48", variantWidth = "var(--_17s80v40)";
+/** The swatches on display: the vibrant family over the muted one */
+const TILES = [
+ "vibrant",
+ "lightVibrant",
+ "darkVibrant",
+ "muted",
+ "lightMuted",
+ "darkMuted"
+];
+/**
+* Takes the colors of a theme from an image, on device: the image is read
+* into a canvas and its palette extracted right there — nothing is uploaded.
+* Shows the image (while the session lasts) next to its palette, offers a
+* theme variant built around each swatch as a small preview, and a lucky
+* button that picks an interesting one.
+*
+* @internal
+*/
+function ImagePaletteSection(props) {
+ let $ = c(38), { imageUrl, onPalette, onVariant, onLucky, options, palette } = props, { busy, pickImage } = useImagePalette(onPalette), [dragging, setDragging] = useState(!1), dragDepth = useRef(0), t0;
+ $[0] !== options || $[1] !== palette ? (t0 = palette ? currentImageVariant(options, palette) : null, $[0] = options, $[1] = palette, $[2] = t0) : t0 = $[2];
+ let current = t0, rowRef = useRef(null), [thumbnailWidth, setThumbnailWidth] = useState(58), t1;
+ $[3] === Symbol.for("react.memo_cache_sentinel") ? (t1 = () => {
+ let row = rowRef.current;
+ if (!row) return;
+ let measure = () => {
+ let visible = row.clientWidth - 12 - 16;
+ setThumbnailWidth(Math.max(24, Math.floor(visible / 2.5) - 8));
+ };
+ measure();
+ let observer = new ResizeObserver(measure);
+ return observer.observe(row), () => observer.disconnect();
+ }, $[3] = t1) : t1 = $[3];
+ let t2;
+ $[4] === palette ? t2 = $[5] : (t2 = [palette], $[4] = palette, $[5] = t2), useEffect(t1, t2);
+ let t3;
+ bb0: {
+ if (!palette) {
+ let t4;
+ $[6] === Symbol.for("react.memo_cache_sentinel") ? (t4 = [], $[6] = t4) : t4 = $[6], t3 = t4;
+ break bb0;
+ }
+ let t4;
+ $[7] !== options || $[8] !== palette ? (t4 = IMAGE_PALETTE_VARIANTS.flatMap((variant) => palette[variant] === null ? [] : [{
+ variant,
+ title: IMAGE_PALETTE_TITLES[variant],
+ options: applyImagePalette(options, palette, variant)
+ }]), $[7] = options, $[8] = palette, $[9] = t4) : t4 = $[9], t3 = t4;
+ }
+ let variants = t3, t4, t5;
+ $[10] === Symbol.for("react.memo_cache_sentinel") ? (t4 = (event) => {
+ event.preventDefault(), dragDepth.current += 1, setDragging(!0);
+ }, t5 = () => {
+ dragDepth.current = Math.max(0, dragDepth.current - 1), dragDepth.current === 0 && setDragging(!1);
+ }, $[10] = t4, $[11] = t5) : (t4 = $[10], t5 = $[11]);
+ let t6;
+ $[12] === pickImage ? t6 = $[13] : (t6 = (event_1) => {
+ event_1.preventDefault(), dragDepth.current = 0, setDragging(!1);
+ let file = event_1.dataTransfer.files[0];
+ file && pickImage(file);
+ }, $[12] = pickImage, $[13] = t6);
+ let t7 = dragging ? "primary" : "default", t8;
+ $[14] === Symbol.for("react.memo_cache_sentinel") ? (t8 = /* @__PURE__ */ jsx(Box, {
+ flex: 1,
+ children: /* @__PURE__ */ jsx(Text, {
+ size: 1,
+ weight: "medium",
+ children: "Image palette"
+ })
+ }), $[14] = t8) : t8 = $[14];
+ let t9;
+ $[15] !== busy || $[16] !== palette || $[17] !== pickImage ? (t9 = palette && /* @__PURE__ */ jsx(ImageFileButton, {
+ loading: busy,
+ mode: "bleed",
+ onFile: pickImage,
+ padding: 2,
+ tooltip: "Pick another image"
+ }), $[15] = busy, $[16] = palette, $[17] = pickImage, $[18] = t9) : t9 = $[18];
+ let t10;
+ $[19] === t9 ? t10 = $[20] : (t10 = /* @__PURE__ */ jsxs(Flex, {
+ align: "center",
+ gap: 2,
+ children: [t8, t9]
+ }), $[19] = t9, $[20] = t10);
+ let t11;
+ $[21] !== busy || $[22] !== current || $[23] !== imageUrl || $[24] !== onLucky || $[25] !== onVariant || $[26] !== palette || $[27] !== pickImage || $[28] !== thumbnailWidth || $[29] !== variants ? (t11 = palette ? /* @__PURE__ */ jsxs(Fragment, { children: [
+ /* @__PURE__ */ jsxs("div", {
+ className: paletteGrid,
+ "data-with-image": imageUrl !== void 0,
+ children: [imageUrl && /* @__PURE__ */ jsx("img", {
+ alt: "",
+ className: "_17s80v43",
+ src: imageUrl
+ }), TILES.map((key) => {
+ let hex = palette[key], title = IMAGE_PALETTE_TITLES[key];
+ return /* @__PURE__ */ jsx(Tooltip, {
+ animate: !0,
+ content: /* @__PURE__ */ jsx(Text, {
+ size: 1,
+ children: hex ? `${title} · ${hex}` : `No ${title.toLowerCase()} color`
+ }),
+ placement: "bottom",
+ portal: !0,
+ children: /* @__PURE__ */ jsx("span", {
+ className: tile,
+ "data-empty": hex === null,
+ style: { background: hex ?? void 0 }
+ })
+ }, key);
+ })]
+ }),
+ /* @__PURE__ */ jsxs(Stack, {
+ gap: 2,
+ children: [/* @__PURE__ */ jsx(Text, {
+ muted: !0,
+ size: 0,
+ children: "Pick the swatch to build the theme around"
+ }), /* @__PURE__ */ jsx("div", {
+ className: variantRow,
+ ref: rowRef,
+ style: assignInlineVars({
+ [variantWidth]: `${thumbnailWidth}px`,
+ [variantScale]: String(thumbnailWidth / 144)
+ }),
+ children: variants.map((t12) => {
+ let { variant: variant_0, title: title_0, options: variantOptions } = t12;
+ return /* @__PURE__ */ jsxs("button", {
+ "aria-pressed": variant_0 === current,
+ className: variantButton,
+ onClick: () => onVariant(variant_0),
+ type: "button",
+ children: [/* @__PURE__ */ jsx("span", {
+ className: variantFrame,
+ children: /* @__PURE__ */ jsx("span", {
+ className: variantScaleBox,
+ children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: variantOptions })
+ })
+ }), /* @__PURE__ */ jsx(Box, {
+ as: "span",
+ display: "block",
+ paddingTop: 1,
+ children: /* @__PURE__ */ jsx(Text, {
+ align: "center",
+ as: "span",
+ size: 0,
+ textOverflow: "ellipsis",
+ weight: variant_0 === current ? "medium" : "regular",
+ children: title_0
+ })
+ })]
+ }, variant_0);
+ })
+ })]
+ }),
+ /* @__PURE__ */ jsx(TooltipButton, {
+ icon: SparklesIcon,
+ mode: "ghost",
+ onClick: onLucky,
+ text: "I'm feeling lucky",
+ tooltip: "Build the theme around a swatch that looks interesting",
+ width: "fill"
+ })
+ ] }) : /* @__PURE__ */ jsxs(Stack, {
+ gap: 3,
+ children: [/* @__PURE__ */ jsx(Text, {
+ muted: !0,
+ size: 0,
+ children: "Take the accent, text and background colors from an image — read on device, nothing is uploaded."
+ }), /* @__PURE__ */ jsx(ImageFileButton, {
+ loading: busy,
+ mode: "ghost",
+ onFile: pickImage,
+ text: "Pick an image",
+ tooltip: "Take the colors from an image on your device",
+ width: "fill"
+ })]
+ }), $[21] = busy, $[22] = current, $[23] = imageUrl, $[24] = onLucky, $[25] = onVariant, $[26] = palette, $[27] = pickImage, $[28] = thumbnailWidth, $[29] = variants, $[30] = t11) : t11 = $[30];
+ let t12;
+ $[31] !== t10 || $[32] !== t11 ? (t12 = /* @__PURE__ */ jsxs(Stack, {
+ gap: 3,
+ children: [t10, t11]
+ }), $[31] = t10, $[32] = t11, $[33] = t12) : t12 = $[33];
+ let t13;
+ return $[34] !== t12 || $[35] !== t6 || $[36] !== t7 ? (t13 = /* @__PURE__ */ jsx(Card, {
+ border: !0,
+ onDragEnter: t4,
+ onDragLeave: t5,
+ onDragOver: _temp$5,
+ onDrop: t6,
+ padding: 3,
+ radius: 3,
+ tone: t7,
+ children: t12
+ }), $[34] = t12, $[35] = t6, $[36] = t7, $[37] = t13) : t13 = $[37], t13;
+}
+function _temp$5(event_0) {
+ event_0.preventDefault();
+}
+var colorRowText = "os3rj2", range = "os3rj1", swatch = "os3rj0";
+const SCHEME_TITLES = {
+ dark: "Dark mode",
+ light: "Light mode"
+};
+/**
+* Where the split preview shows a scheme: the Studio's own scheme stays next
+* to the sidebar (or at the bottom, stacked on small screens), the other one
+* takes the far side
+*/
+function splitHint(own) {
+ return own ? "Shown on the right, next to the themer" : "Shown on the left";
+}
+const SCHEME_OPTION_KEYS = [
+ "accent",
+ "text",
+ "background",
+ "contrast"
+];
+/**
+* `<input type="color">` paints the color into a shadow-DOM swatch that brings
+* its own border and padding, which then sits inside ours as a second border.
+* Stripping that chrome leaves the themed border as the only one.
+*/
+/**
+* The flow for editing one of the user's own themes: its title, a live
+* preview, and a card per color scheme with the accent/text/background
+* pickers and the contrast slider of that scheme. Every change applies to the
+* whole Studio right away — in the scheme the Studio is showing, or in both
+* side by side in the split preview.
+*
+* @internal
+*/
+function ThemeEditor(t0) {
+ let $ = c(41), { actorRef, focusTitle, slug, split, themes } = t0, images = useSelector(actorRef, _temp$4), t1;
+ if ($[0] !== slug || $[1] !== themes) {
+ let t2;
+ $[3] === slug ? t2 = $[4] : (t2 = (candidate) => candidate.slug === slug && candidate.source === "custom", $[3] = slug, $[4] = t2), t1 = themes.find(t2), $[0] = slug, $[1] = themes, $[2] = t1;
+ } else t1 = $[2];
+ let theme = t1;
+ if (!theme) return null;
+ let t2;
+ $[5] !== actorRef || $[6] !== slug || $[7] !== theme.options || $[8] !== theme.palette ? (t2 = (variant) => {
+ theme.palette && variant !== null && actorRef.send({
+ type: "theme.update",
+ slug,
+ options: applyImagePalette(theme.options, theme.palette, variant)
+ });
+ }, $[5] = actorRef, $[6] = slug, $[7] = theme.options, $[8] = theme.palette, $[9] = t2) : t2 = $[9];
+ let applyVariant = t2, t3 = images[slug], t4;
+ $[10] === actorRef ? t4 = $[11] : (t4 = () => actorRef.send({ type: "flow.list" }), $[10] = actorRef, $[11] = t4);
+ let t5;
+ $[12] !== applyVariant || $[13] !== theme.options || $[14] !== theme.palette ? (t5 = () => {
+ theme.palette && applyVariant(pickLuckyVariant(theme.palette, { exclude: currentImageVariant(theme.options, theme.palette) }));
+ }, $[12] = applyVariant, $[13] = theme.options, $[14] = theme.palette, $[15] = t5) : t5 = $[15];
+ let t6;
+ $[16] !== actorRef || $[17] !== slug ? (t6 = (options) => actorRef.send({
+ type: "theme.update",
+ slug,
+ options
+ }), $[16] = actorRef, $[17] = slug, $[18] = t6) : t6 = $[18];
+ let t7;
+ $[19] !== actorRef || $[20] !== slug || $[21] !== theme.options ? (t7 = (palette, file) => actorRef.send({
+ type: "theme.update",
+ slug,
+ options: applyImagePalette(theme.options, palette),
+ palette,
+ imageUrl: URL.createObjectURL(file)
+ }), $[19] = actorRef, $[20] = slug, $[21] = theme.options, $[22] = t7) : t7 = $[22];
+ let t8, t9;
+ $[23] !== actorRef || $[24] !== slug ? (t8 = () => actorRef.send({
+ type: "theme.remove",
+ slug
+ }), t9 = (title) => actorRef.send({
+ type: "theme.update",
+ slug,
+ title
+ }), $[23] = actorRef, $[24] = slug, $[25] = t8, $[26] = t9) : (t8 = $[25], t9 = $[26]);
+ let t10;
+ return $[27] !== applyVariant || $[28] !== focusTitle || $[29] !== split || $[30] !== t3 || $[31] !== t4 || $[32] !== t5 || $[33] !== t6 || $[34] !== t7 || $[35] !== t8 || $[36] !== t9 || $[37] !== theme.options || $[38] !== theme.palette || $[39] !== theme.title ? (t10 = /* @__PURE__ */ jsx(ThemeEditorForm, {
+ focusTitle,
+ imageUrl: t3,
+ onDone: t4,
+ onLucky: t5,
+ onOptionsChange: t6,
+ onPalette: t7,
+ onRemove: t8,
+ onTitleChange: t9,
+ onVariant: applyVariant,
+ options: theme.options,
+ palette: theme.palette,
+ split,
+ title: theme.title
+ }), $[27] = applyVariant, $[28] = focusTitle, $[29] = split, $[30] = t3, $[31] = t4, $[32] = t5, $[33] = t6, $[34] = t7, $[35] = t8, $[36] = t9, $[37] = theme.options, $[38] = theme.palette, $[39] = theme.title, $[40] = t10) : t10 = $[40], t10;
+}
+function _temp$4(snapshot) {
+ return snapshot.context.images;
+}
+function ThemeEditorForm(props) {
+ let $ = c(49), { focusTitle, imageUrl, onDone, onLucky, onOptionsChange, onPalette, onRemove, onTitleChange, onVariant, options, palette, split, title } = props, studioScheme = useColorSchemeValue(), t0;
+ $[0] === options ? t0 = $[1] : (t0 = resolveThemeOptions(options), $[0] = options, $[1] = t0);
+ let resolved = t0, t1;
+ $[2] === options ? t1 = $[3] : (t1 = buildPalette(options), $[2] = options, $[3] = t1);
+ let palettes = t1, t2;
+ $[4] !== onOptionsChange || $[5] !== options ? (t2 = (scheme, changes) => {
+ let values = {
+ ...options[scheme],
+ ...changes
+ };
+ for (let key of SCHEME_OPTION_KEYS) values[key] === void 0 && delete values[key];
+ let next = { ...options };
+ Object.keys(values).length === 0 ? delete next[scheme] : next[scheme] = values, onOptionsChange(next);
+ }, $[4] = onOptionsChange, $[5] = options, $[6] = t2) : t2 = $[6];
+ let patchScheme = t2, t3;
+ $[7] === options ? t3 = $[8] : (t3 = /* @__PURE__ */ jsx(ThemeThumbnail, { options }), $[7] = options, $[8] = t3);
+ let t4;
+ $[9] === Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx(Text, {
+ size: 1,
+ weight: "medium",
+ children: "Title"
+ }), $[9] = t4) : t4 = $[9];
+ let t5;
+ $[10] === onTitleChange ? t5 = $[11] : (t5 = (event) => onTitleChange(event.currentTarget.value), $[10] = onTitleChange, $[11] = t5);
+ let t6 = focusTitle ? _temp2 : void 0, t7;
+ $[12] !== focusTitle || $[13] !== t5 || $[14] !== t6 || $[15] !== title ? (t7 = /* @__PURE__ */ jsxs(Stack, {
+ gap: 2,
+ children: [t4, /* @__PURE__ */ jsx(TextInput, {
+ autoFocus: focusTitle,
+ fontSize: 1,
+ onChange: t5,
+ onFocus: t6,
+ padding: 2,
+ placeholder: "Untitled theme",
+ value: title
+ })]
+ }), $[12] = focusTitle, $[13] = t5, $[14] = t6, $[15] = title, $[16] = t7) : t7 = $[16];
+ let t8;
+ $[17] !== imageUrl || $[18] !== onLucky || $[19] !== onPalette || $[20] !== onVariant || $[21] !== options || $[22] !== palette ? (t8 = /* @__PURE__ */ jsx(ImagePaletteSection, {
+ imageUrl,
+ onLucky,
+ onPalette,
+ onVariant,
+ options,
+ palette
+ }), $[17] = imageUrl, $[18] = onLucky, $[19] = onPalette, $[20] = onVariant, $[21] = options, $[22] = palette, $[23] = t8) : t8 = $[23];
+ let t9;
+ $[24] !== options || $[25] !== palettes || $[26] !== patchScheme || $[27] !== resolved || $[28] !== split || $[29] !== studioScheme ? (t9 = SCHEMES.map((scheme_0) => /* @__PURE__ */ jsx(SchemeCard, {
+ active: split || scheme_0 === studioScheme,
+ onChange: (changes_0) => patchScheme(scheme_0, changes_0),
+ options: options[scheme_0] ?? {},
+ palette: palettes[scheme_0],
+ resolved: resolved[scheme_0],
+ scheme: scheme_0,
+ split,
+ splitHint: splitHint(scheme_0 === studioScheme)
+ }, scheme_0)), $[24] = options, $[25] = palettes, $[26] = patchScheme, $[27] = resolved, $[28] = split, $[29] = studioScheme, $[30] = t9) : t9 = $[30];
+ let t10;
+ $[31] === t9 ? t10 = $[32] : (t10 = /* @__PURE__ */ jsx(ScrollAreaBleed, { children: t9 }), $[31] = t9, $[32] = t10);
+ let t11;
+ $[33] !== t10 || $[34] !== t3 || $[35] !== t7 || $[36] !== t8 ? (t11 = /* @__PURE__ */ jsx(ScrollArea, {
+ padding: 3,
+ children: /* @__PURE__ */ jsxs(Stack, {
+ gap: 4,
+ children: [
+ t3,
+ t7,
+ t8,
+ t10
+ ]
+ })
+ }), $[33] = t10, $[34] = t3, $[35] = t7, $[36] = t8, $[37] = t11) : t11 = $[37];
+ let t12;
+ $[38] === onRemove ? t12 = $[39] : (t12 = /* @__PURE__ */ jsx(TooltipButton, {
+ icon: TrashIcon,
+ mode: "ghost",
+ onClick: onRemove,
+ text: "Remove",
+ tone: "critical",
+ tooltip: "Remove the theme — it can be restored until it is deleted"
+ }), $[38] = onRemove, $[39] = t12);
+ let t13;
+ $[40] === Symbol.for("react.memo_cache_sentinel") ? (t13 = /* @__PURE__ */ jsx(Box, { flex: 1 }), $[40] = t13) : t13 = $[40];
+ let t14;
+ $[41] === onDone ? t14 = $[42] : (t14 = /* @__PURE__ */ jsx(Button, {
+ mode: "ghost",
+ onClick: onDone,
+ padding: 2,
+ text: "Done"
+ }), $[41] = onDone, $[42] = t14);
+ let t15;
+ $[43] !== t12 || $[44] !== t14 ? (t15 = /* @__PURE__ */ jsx(Card, {
+ borderTop: !0,
+ padding: 3,
+ children: /* @__PURE__ */ jsxs(Flex, {
+ gap: 2,
+ children: [
+ t12,
+ t13,
+ t14
+ ]
+ })
+ }), $[43] = t12, $[44] = t14, $[45] = t15) : t15 = $[45];
+ let t16;
+ return $[46] !== t11 || $[47] !== t15 ? (t16 = /* @__PURE__ */ jsxs(Fragment, { children: [t11, t15] }), $[46] = t11, $[47] = t15, $[48] = t16) : t16 = $[48], t16;
+}
+function _temp2(event_0) {
+ return event_0.currentTarget.select();
+}
+/**
+* The colors of one scheme. The cards span the sidebar edge to edge, one
+* after the other with a border between them, in the sidebar's own scheme —
+* the swatches show the colors they edit. The scheme the Studio is showing is
+* marked as active: its changes show up in the Studio right away, the other
+* scheme's only once the appearance setting switches to it — or both are
+* active, in the split preview.
+*/
+function SchemeCard(props) {
+ let $ = c(66), { active, onChange, options, palette, resolved, scheme, split, splitHint } = props, name = SCHEME_TITLES[scheme], t0;
+ $[0] === name ? t0 = $[1] : (t0 = /* @__PURE__ */ jsx(Box, {
+ flex: 1,
+ children: /* @__PURE__ */ jsx(Text, {
+ size: 1,
+ weight: "medium",
+ children: name
+ })
+ }), $[0] = name, $[1] = t0);
+ let t1;
+ $[2] === active ? t1 = $[3] : (t1 = active && /* @__PURE__ */ jsx(Badge, {
+ fontSize: 0,
+ tone: "primary",
+ children: "Active"
+ }), $[2] = active, $[3] = t1);
+ let t2;
+ $[4] !== t0 || $[5] !== t1 ? (t2 = /* @__PURE__ */ jsxs(Flex, {
+ align: "center",
+ gap: 2,
+ children: [t0, t1]
+ }), $[4] = t0, $[5] = t1, $[6] = t2) : t2 = $[6];
+ let t3 = split ? splitHint : active ? "Shown in the Studio right now" : `Shown once the Studio appearance is ${scheme}`, t4;
+ $[7] === t3 ? t4 = $[8] : (t4 = /* @__PURE__ */ jsx(Text, {
+ muted: !0,
+ size: 0,
+ children: t3
+ }), $[7] = t3, $[8] = t4);
+ let t5;
+ $[9] !== t2 || $[10] !== t4 ? (t5 = /* @__PURE__ */ jsxs(Stack, {
+ gap: 2,
+ children: [t2, t4]
+ }), $[9] = t2, $[10] = t4, $[11] = t5) : t5 = $[11];
+ let t6 = options.accent === void 0, t7 = `${name} accent`, t8, t9;
+ $[12] === onChange ? (t8 = $[13], t9 = $[14]) : (t8 = (accent) => onChange({ accent }), t9 = () => onChange({ accent: void 0 }), $[12] = onChange, $[13] = t8, $[14] = t9);
+ let t10;
+ $[15] !== resolved.accent || $[16] !== t6 || $[17] !== t7 || $[18] !== t8 || $[19] !== t9 ? (t10 = /* @__PURE__ */ jsx(ColorRow, {
+ auto: t6,
+ autoLabel: "default",
+ label: t7,
+ onChange: t8,
+ onClear: t9,
+ title: "Accent",
+ value: resolved.accent
+ }), $[15] = resolved.accent, $[16] = t6, $[17] = t7, $[18] = t8, $[19] = t9, $[20] = t10) : t10 = $[20];
+ let t11 = options.text === void 0, t12 = `${name} text`, t13, t14;
+ $[21] === onChange ? (t13 = $[22], t14 = $[23]) : (t13 = (text) => onChange({ text }), t14 = () => onChange({ text: void 0 }), $[21] = onChange, $[22] = t13, $[23] = t14);
+ let t15;
+ $[24] !== resolved.text || $[25] !== t11 || $[26] !== t12 || $[27] !== t13 || $[28] !== t14 ? (t15 = /* @__PURE__ */ jsx(ColorRow, {
+ auto: t11,
+ label: t12,
+ onChange: t13,
+ onClear: t14,
+ title: "Text",
+ value: resolved.text
+ }), $[24] = resolved.text, $[25] = t11, $[26] = t12, $[27] = t13, $[28] = t14, $[29] = t15) : t15 = $[29];
+ let t16 = scheme === "dark" ? palette.black : palette.white, t17 = options.background === void 0, t18 = `${name} background`, t19, t20;
+ $[30] === onChange ? (t19 = $[31], t20 = $[32]) : (t19 = (background) => onChange({ background }), t20 = () => onChange({ background: void 0 }), $[30] = onChange, $[31] = t19, $[32] = t20);
+ let t21;
+ $[33] !== resolved.background || $[34] !== t16 || $[35] !== t17 || $[36] !== t18 || $[37] !== t19 || $[38] !== t20 ? (t21 = /* @__PURE__ */ jsx(ColorRow, {
+ adjusted: t16,
+ auto: t17,
+ label: t18,
+ onChange: t19,
+ onClear: t20,
+ title: "Background",
+ value: resolved.background
+ }), $[33] = resolved.background, $[34] = t16, $[35] = t17, $[36] = t18, $[37] = t19, $[38] = t20, $[39] = t21) : t21 = $[39];
+ let t22;
+ $[40] === Symbol.for("react.memo_cache_sentinel") ? (t22 = /* @__PURE__ */ jsx(Text, {
+ size: 1,
+ children: "Contrast"
+ }), $[40] = t22) : t22 = $[40];
+ let t23 = options.contrast === void 0 ? " · auto" : "", t24;
+ $[41] !== resolved.contrast || $[42] !== t23 ? (t24 = /* @__PURE__ */ jsxs(Stack, {
+ flex: 1,
+ gap: 2,
+ children: [t22, /* @__PURE__ */ jsxs(Text, {
+ muted: !0,
+ size: 0,
+ children: [resolved.contrast, t23]
+ })]
+ }), $[41] = resolved.contrast, $[42] = t23, $[43] = t24) : t24 = $[43];
+ let t25;
+ $[44] !== onChange || $[45] !== options.contrast ? (t25 = options.contrast !== void 0 && /* @__PURE__ */ jsx(TooltipButton, {
+ icon: ResetIcon,
+ mode: "bleed",
+ onClick: () => onChange({ contrast: void 0 }),
+ padding: 2,
+ tooltip: "Reset to auto"
+ }), $[44] = onChange, $[45] = options.contrast, $[46] = t25) : t25 = $[46];
+ let t26;
+ $[47] !== t24 || $[48] !== t25 ? (t26 = /* @__PURE__ */ jsxs(Flex, {
+ align: "center",
+ gap: 2,
+ children: [t24, t25]
+ }), $[47] = t24, $[48] = t25, $[49] = t26) : t26 = $[49];
+ let t27 = `${name} contrast`, t28;
+ $[50] === onChange ? t28 = $[51] : (t28 = (event) => onChange({ contrast: Number(event.currentTarget.value) }), $[50] = onChange, $[51] = t28);
+ let t29;
+ $[52] !== resolved.contrast || $[53] !== t27 || $[54] !== t28 ? (t29 = /* @__PURE__ */ jsx("input", {
+ "aria-label": t27,
+ className: range,
+ max: 100,
+ min: 15,
+ onChange: t28,
+ step: 1,
+ type: "range",
+ value: resolved.contrast
+ }), $[52] = resolved.contrast, $[53] = t27, $[54] = t28, $[55] = t29) : t29 = $[55];
+ let t30;
+ $[56] === Symbol.for("react.memo_cache_sentinel") ? (t30 = /* @__PURE__ */ jsx(Text, {
+ muted: !0,
+ size: 0,
+ children: "100 keeps text and borders neutral — lower values blend in the accent"
+ }), $[56] = t30) : t30 = $[56];
+ let t31;
+ $[57] !== t26 || $[58] !== t29 ? (t31 = /* @__PURE__ */ jsxs(Stack, {
+ gap: 3,
+ children: [
+ t26,
+ t29,
+ t30
+ ]
+ }), $[57] = t26, $[58] = t29, $[59] = t31) : t31 = $[59];
+ let t32;
+ return $[60] !== t10 || $[61] !== t15 || $[62] !== t21 || $[63] !== t31 || $[64] !== t5 ? (t32 = /* @__PURE__ */ jsx(Card, {
+ borderTop: !0,
+ padding: 3,
+ children: /* @__PURE__ */ jsxs(Stack, {
+ gap: 4,
+ children: [
+ t5,
+ t10,
+ t15,
+ t21,
+ t31
+ ]
+ })
+ }), $[60] = t10, $[61] = t15, $[62] = t21, $[63] = t31, $[64] = t5, $[65] = t32) : t32 = $[65], t32;
+}
+/** One color of a scheme: a swatch with its value and an optional reset button */
+function ColorRow(props) {
+ let $ = c(23), { adjusted, auto, autoLabel: t0, label, onChange, onClear, title, value } = props, autoLabel = t0 === void 0 ? "auto" : t0, t1;
+ $[0] === onChange ? t1 = $[1] : (t1 = (event) => onChange(event.currentTarget.value), $[0] = onChange, $[1] = t1);
+ let t2;
+ $[2] !== label || $[3] !== t1 || $[4] !== value ? (t2 = /* @__PURE__ */ jsx("input", {
+ "aria-label": label,
+ className: swatch,
+ onChange: t1,
+ type: "color",
+ value
+ }), $[2] = label, $[3] = t1, $[4] = value, $[5] = t2) : t2 = $[5];
+ let t3;
+ $[6] === title ? t3 = $[7] : (t3 = /* @__PURE__ */ jsx(Text, {
+ size: 1,
+ children: title
+ }), $[6] = title, $[7] = t3);
+ let t4 = adjusted !== void 0 && adjusted !== value ? ` → ${adjusted}` : "", t5 = auto ? ` · ${autoLabel}` : "", t6;
+ $[8] !== t4 || $[9] !== t5 || $[10] !== value ? (t6 = /* @__PURE__ */ jsxs(Text, {
+ muted: !0,
+ size: 0,
+ textOverflow: "ellipsis",
+ children: [
+ value,
+ t4,
+ t5
+ ]
+ }), $[8] = t4, $[9] = t5, $[10] = value, $[11] = t6) : t6 = $[11];
+ let t7;
+ $[12] !== t3 || $[13] !== t6 ? (t7 = /* @__PURE__ */ jsxs(Stack, {
+ className: colorRowText,
+ flex: 1,
+ gap: 2,
+ children: [t3, t6]
+ }), $[12] = t3, $[13] = t6, $[14] = t7) : t7 = $[14];
+ let t8;
+ $[15] !== auto || $[16] !== autoLabel || $[17] !== onClear ? (t8 = onClear && !auto && /* @__PURE__ */ jsx(TooltipButton, {
+ icon: ResetIcon,
+ mode: "bleed",
+ onClick: onClear,
+ padding: 2,
+ tooltip: `Reset to ${autoLabel}`
+ }), $[15] = auto, $[16] = autoLabel, $[17] = onClear, $[18] = t8) : t8 = $[18];
+ let t9;
+ return $[19] !== t2 || $[20] !== t7 || $[21] !== t8 ? (t9 = /* @__PURE__ */ jsxs(Flex, {
+ align: "center",
+ gap: 2,
+ children: [
+ t2,
+ t7,
+ t8
+ ]
+ }), $[19] = t2, $[20] = t7, $[21] = t8, $[22] = t9) : t9 = $[22], t9;
+}
+/**
+* The version of the code format, the first byte of every code — bumped if
+* the layout below ever changes, so older codes can still be told apart
+*/
+const FIELDS = [
+ "accent",
+ "text",
+ "background",
+ "contrast"
+];
+/**
+* Turns a theme into a short code to share in a message: `title`, and the
+* colors and contrasts of both schemes, packed into bytes — one flag byte says
+* which of the eight fields are set, each color is its three RGB bytes and a
+* contrast one byte, and the title takes up the rest — then base64url'd, so
+* it is one word without padding, safe in URLs and chat. A theme setting
+* everything comes to about 30 bytes plus its title.
+*
+* @internal
+*/
+function encodeTheme(theme) {
+ let bytes = [1, 0], flags = 0;
+ return SCHEMES.forEach((scheme, schemeIndex) => {
+ let options = theme.options[scheme] ?? {};
+ FIELDS.forEach((field, fieldIndex) => {
+ let value = options[field];
+ if (value !== void 0) {
+ if (field === "contrast") {
+ if (typeof value != "number" || !Number.isFinite(value)) return;
+ bytes.push(Math.round(Math.min(100, Math.max(15, value))));
+ } else {
+ if (typeof value != "string" || !isColor(value)) return;
+ bytes.push(...rgbBytes(value));
+ }
+ flags |= 1 << schemeIndex * FIELDS.length + fieldIndex;
+ }
+ });
+ }), bytes[1] = flags, bytes.push(...new TextEncoder().encode(theme.title.trim())), toBase64Url(Uint8Array.from(bytes));
+}
+/**
+* Reads a theme back out of a code — as pasted, so surrounding whitespace and
+* words around the code are fine: the first word that is a code counts. `null`
+* when nothing in the text is one.
+*
+* @internal
+*/
+function decodeTheme(text) {
+ for (let word of text.trim().split(/\s+/)) {
+ let theme = decodeCode(word);
+ if (theme) return theme;
+ }
+ return null;
+}
+function decodeCode(code) {
+ let bytes = fromBase64Url(code);
+ if (!bytes || bytes.length < 2 || bytes[0] !== 1) return null;
+ let flags = bytes[1], options = {}, offset = 2;
+ for (let [schemeIndex, scheme] of SCHEMES.entries()) {
+ let schemeOptions = {};
+ for (let [fieldIndex, field] of FIELDS.entries()) if (flags & 1 << schemeIndex * FIELDS.length + fieldIndex) {
+ if (field === "contrast") {
+ if (offset + 1 > bytes.length) return null;
+ schemeOptions.contrast = Math.min(100, Math.max(15, bytes[offset])), offset += 1;
+ } else {
+ if (offset + 3 > bytes.length) return null;
+ schemeOptions[field] = hexColor(bytes.subarray(offset, offset + 3)), offset += 3;
+ }
+ }
+ Object.keys(schemeOptions).length > 0 && (options[scheme] = schemeOptions);
+ }
+ let title;
+ try {
+ title = new TextDecoder("utf-8", { fatal: !0 }).decode(bytes.subarray(offset));
+ } catch {
+ return null;
+ }
+ return {
+ title: displayTitle(title),
+ options
+ };
+}
+/** The three RGB bytes of a `#rgb` or `#rrggbb` color */
+function rgbBytes(color) {
+ let hex = color.length === 4 ? color.slice(1).replaceAll(/./g, "$&$&") : color.slice(1);
+ return [
+ 0,
+ 2,
+ 4
+ ].map((start) => parseInt(hex.slice(start, start + 2), 16));
+}
+function hexColor(rgb) {
+ return `#${[...rgb].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
+}
+function toBase64Url(bytes) {
+ let binary = "";
+ for (let byte of bytes) binary += String.fromCharCode(byte);
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
+}
+function fromBase64Url(code) {
+ if (!/^[A-Za-z0-9_-]+$/.test(code)) return null;
+ let base64 = code.replaceAll("-", "+").replaceAll("_", "/");
+ try {
+ return Uint8Array.from(atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "=")), (char) => char.charCodeAt(0));
+ } catch {
+ return null;
+ }
+}
+/**
+* Copying a theme as a code to share, and adding a theme from a code someone
+* shared — with toasts telling how it went.
+*
+* @internal
+*/
+function useThemeCodes(t0) {
+ let $ = c(11), { actorRef } = t0, toast = useToast(), t1;
+ $[0] === toast ? t1 = $[1] : (t1 = async (theme) => {
+ let title = displayTitle(theme.title);
+ try {
+ await navigator.clipboard.writeText(encodeTheme(theme)), toast.push({
+ status: "success",
+ title: `Copied ${title} as a code`,
+ description: "Paste it in the themer of another Studio to add the theme there"
+ });
+ } catch {
+ toast.push({
+ status: "error",
+ title: `Could not copy ${title}`
+ });
+ }
+ }, $[0] = toast, $[1] = t1);
+ let copyTheme = t1, t2;
+ $[2] !== actorRef || $[3] !== toast ? (t2 = (text) => {
+ let theme_0 = decodeTheme(text);
+ return theme_0 ? (actorRef.send({
+ type: "theme.import",
+ title: theme_0.title,
+ options: theme_0.options
+ }), toast.push({
+ status: "success",
+ title: `Added ${theme_0.title}`
+ }), !0) : !1;
+ }, $[2] = actorRef, $[3] = toast, $[4] = t2) : t2 = $[4];
+ let addThemeFromText = t2, t3;
+ $[5] === addThemeFromText ? t3 = $[6] : (t3 = async () => {
+ let text_0;
+ try {
+ text_0 = await navigator.clipboard.readText();
+ } catch {
+ return "no-clipboard";
+ }
+ return addThemeFromText(text_0) ? "added" : "no-theme";
+ }, $[5] = addThemeFromText, $[6] = t3);
+ let addThemeFromClipboard = t3, t4;
+ return $[7] !== addThemeFromClipboard || $[8] !== addThemeFromText || $[9] !== copyTheme ? (t4 = {
+ copyTheme,
+ addThemeFromText,
+ addThemeFromClipboard
+ }, $[7] = addThemeFromClipboard, $[8] = addThemeFromText, $[9] = copyTheme, $[10] = t4) : t4 = $[10], t4;
+}
+/**
+* Adds a theme pasted anywhere in the Studio while the themer's list is
+* showing — pastes into inputs and other editable things are theirs.
+*
+* @internal
+*/
+function usePasteThemeCodes(t0) {
+ let $ = c(4), { actorRef } = t0, toast = useToast(), t1, t2;
+ $[0] !== actorRef || $[1] !== toast ? (t1 = () => {
+ let handlePaste = (event) => {
+ if (isEditable(event.target)) return;
+ let theme = decodeTheme(event.clipboardData?.getData("text/plain") ?? "");
+ theme && (event.preventDefault(), actorRef.send({
+ type: "theme.import",
+ title: theme.title,
+ options: theme.options
+ }), toast.push({
+ status: "success",
+ title: `Added ${theme.title}`
+ }));
+ };
+ return document.addEventListener("paste", handlePaste), () => document.removeEventListener("paste", handlePaste);
+ }, t2 = [actorRef, toast], $[0] = actorRef, $[1] = toast, $[2] = t1, $[3] = t2) : (t1 = $[2], t2 = $[3]), useEffect(t1, t2);
+}
+function isEditable(target) {
+ return target instanceof HTMLElement && (target.isContentEditable || target.matches("input, textarea, select"));
+}
+/**
+* Where a theme code someone shared gets pasted by hand — for browsers that
+* do not hand the clipboard over, and for codes that are not on it.
+*
+* @internal
+*/
+function PasteThemeDialog(props) {
+ let $ = c(37), { actorRef, onClose } = props, t0;
+ $[0] === actorRef ? t0 = $[1] : (t0 = { actorRef }, $[0] = actorRef, $[1] = t0);
+ let { addThemeFromText } = useThemeCodes(t0), [text, setText] = useState(""), [invalid, setInvalid] = useState(!1), t1;
+ $[2] !== addThemeFromText || $[3] !== onClose || $[4] !== text ? (t1 = () => {
+ addThemeFromText(text) ? onClose() : setInvalid(!0);
+ }, $[2] = addThemeFromText, $[3] = onClose, $[4] = text, $[5] = t1) : t1 = $[5];
+ let add = t1, t2;
+ $[6] === onClose ? t2 = $[7] : (t2 = /* @__PURE__ */ jsx(Button, {
+ mode: "ghost",
+ onClick: onClose,
+ padding: 2,
+ text: "Cancel"
+ }), $[6] = onClose, $[7] = t2);
+ let t3;
+ $[8] === text ? t3 = $[9] : (t3 = text.trim(), $[8] = text, $[9] = t3);
+ let t4 = t3 === "", t5;
+ $[10] !== add || $[11] !== t4 ? (t5 = /* @__PURE__ */ jsx(Button, {
+ disabled: t4,
+ onClick: add,
+ padding: 2,
+ text: "Add theme",
+ tone: "primary"
+ }), $[10] = add, $[11] = t4, $[12] = t5) : t5 = $[12];
+ let t6;
+ $[13] !== t2 || $[14] !== t5 ? (t6 = /* @__PURE__ */ jsxs(Flex, {
+ gap: 2,
+ justify: "flex-end",
+ padding: 3,
+ children: [t2, t5]
+ }), $[13] = t2, $[14] = t5, $[15] = t6) : t6 = $[15];
+ let t7;
+ $[16] === add ? t7 = $[17] : (t7 = (event) => {
+ event.preventDefault(), add();
+ }, $[16] = add, $[17] = t7);
+ let t8;
+ $[18] === Symbol.for("react.memo_cache_sentinel") ? (t8 = /* @__PURE__ */ jsx(Text, {
+ muted: !0,
+ size: 1,
+ children: "Paste the code someone copied from their themer — the theme is added to your list and applied."
+ }), $[18] = t8) : t8 = $[18];
+ let t9 = invalid ? "themer-paste-error" : void 0, t10 = invalid ? "That is not a theme code" : void 0, t11;
+ $[19] === Symbol.for("react.memo_cache_sentinel") ? (t11 = (event_0) => {
+ setText(event_0.currentTarget.value), setInvalid(!1);
+ }, $[19] = t11) : t11 = $[19];
+ let t12;
+ $[20] !== invalid || $[21] !== t10 || $[22] !== t9 || $[23] !== text ? (t12 = /* @__PURE__ */ jsx(TextInput, {
+ "aria-describedby": t9,
+ "aria-invalid": invalid,
+ "aria-label": "Theme code",
+ autoFocus: !0,
+ customValidity: t10,
+ fontSize: 1,
+ onChange: t11,
+ placeholder: "AQ…",
+ value: text
+ }), $[20] = invalid, $[21] = t10, $[22] = t9, $[23] = text, $[24] = t12) : t12 = $[24];
+ let t13;
+ $[25] === invalid ? t13 = $[26] : (t13 = invalid && /* @__PURE__ */ jsx(Card, {
+ id: "themer-paste-error",
+ padding: 3,
+ radius: 2,
+ role: "alert",
+ tone: "critical",
+ children: /* @__PURE__ */ jsx(Text, {
+ size: 1,
+ children: "That is not a theme code — codes are one word, copied from a theme's menu."
+ })
+ }), $[25] = invalid, $[26] = t13);
+ let t14;
+ $[27] !== t12 || $[28] !== t13 ? (t14 = /* @__PURE__ */ jsxs(Stack, {
+ gap: 3,
+ padding: 4,
+ children: [
+ t8,
+ t12,
+ t13
+ ]
+ }), $[27] = t12, $[28] = t13, $[29] = t14) : t14 = $[29];
+ let t15;
+ $[30] !== t14 || $[31] !== t7 ? (t15 = /* @__PURE__ */ jsx("form", {
+ onSubmit: t7,
+ children: t14
+ }), $[30] = t14, $[31] = t7, $[32] = t15) : t15 = $[32];
+ let t16;
+ return $[33] !== onClose || $[34] !== t15 || $[35] !== t6 ? (t16 = /* @__PURE__ */ jsx(Dialog, {
+ animate: !0,
+ footer: t6,
+ header: "Add a shared theme",
+ id: "themer-paste",
+ onClickOutside: onClose,
+ onClose,
+ width: 0,
+ children: t15
+ }), $[33] = onClose, $[34] = t15, $[35] = t6, $[36] = t16) : t16 = $[36], t16;
+}
+var frame$1 = "_1wsn7882", menuCard = "_1wsn7884", menuSlot = "_1wsn7883", pickButton = "_1wsn7881", root = "_1wsn7880";
+/**
+* Scrolls the applied theme's card into view when it is not already, as a
+* ref: the list scrolls to it as it mounts — coming back from the editor or
+* the removed themes — and when another theme gets applied
+*/
+function scrollRefIntoViewIfNeeded(node) {
+ node && scrollIntoView(node, {
+ block: "nearest",
+ scrollMode: "if-needed"
+ });
+}
+/**
+* One theme in the list: the floating preview with the title below it, like
+* an appearance option in macOS System Settings. Clicking it applies the
+* theme to the whole Studio, dragging it moves the theme to another place in
+* the list; its menu holds the move, edit, duplicate and remove flows that
+* apply to the theme.
+*
+* @internal
+*/
+function ThemeCard(t0) {
+ let $ = c(79), { actorRef, active, theme, themes } = t0, t1;
+ $[0] === actorRef ? t1 = $[1] : (t1 = { actorRef }, $[0] = actorRef, $[1] = t1);
+ let { copyTheme } = useThemeCodes(t1), { scheme } = useRootTheme(), [menuOpen, setMenuOpen] = useState(!1), dragControls = useDragControls(), dragged = useRef(!1), t2;
+ $[2] === theme.title ? t2 = $[3] : (t2 = displayTitle(theme.title), $[2] = theme.title, $[3] = t2);
+ let title = t2, t3;
+ $[4] === theme.slug ? t3 = $[5] : (t3 = (candidate) => candidate.slug === theme.slug, $[4] = theme.slug, $[5] = t3);
+ let index = themes.findIndex(t3), listed = index !== -1, t4;
+ $[6] !== actorRef || $[7] !== index || $[8] !== themes ? (t4 = (to) => {
+ let order = themes.map(_temp$3);
+ order.splice(to, 0, ...order.splice(index, 1)), actorRef.send({
+ type: "theme.reorder",
+ order
+ });
+ }, $[6] = actorRef, $[7] = index, $[8] = themes, $[9] = t4) : t4 = $[9];
+ let moveTo = t4, t5, t6;
+ $[10] === Symbol.for("react.memo_cache_sentinel") ? (t5 = () => {
+ requestAnimationFrame(() => {
+ dragged.current = !1;
+ });
+ }, t6 = () => {
+ dragged.current = !0;
+ }, $[10] = t5, $[11] = t6) : (t5 = $[10], t6 = $[11]);
+ let t7 = active ? scrollRefIntoViewIfNeeded : void 0, t8;
+ $[12] === Symbol.for("react.memo_cache_sentinel") ? (t8 = { scale: 1.04 }, $[12] = t8) : t8 = $[12];
+ let t9;
+ $[13] !== actorRef || $[14] !== theme.slug ? (t9 = () => {
+ dragged.current || actorRef.send({
+ type: "theme.pick",
+ slug: theme.slug
+ });
+ }, $[13] = actorRef, $[14] = theme.slug, $[15] = t9) : t9 = $[15];
+ let t10;
+ $[16] !== dragControls || $[17] !== listed ? (t10 = (event) => {
+ listed && event.pointerType !== "touch" && dragControls.start(event);
+ }, $[16] = dragControls, $[17] = listed, $[18] = t10) : t10 = $[18];
+ let t11;
+ $[19] === theme.options ? t11 = $[20] : (t11 = /* @__PURE__ */ jsx("span", {
+ className: frame$1,
+ children: /* @__PURE__ */ jsx(ThemeThumbnail, { options: theme.options })
+ }), $[19] = theme.options, $[20] = t11);
+ let t12 = active ? "medium" : "regular", t13;
+ $[21] !== t12 || $[22] !== title ? (t13 = /* @__PURE__ */ jsx(Box, {
+ as: "span",
+ display: "block",
+ paddingTop: 2,
+ paddingX: 2,
+ children: /* @__PURE__ */ jsx(Text, {
+ align: "center",
+ as: "span",
+ size: 1,
+ textOverflow: "ellipsis",
+ weight: t12,
+ children: title
+ })
+ }), $[21] = t12, $[22] = title, $[23] = t13) : t13 = $[23];
+ let t14;
+ $[24] !== active || $[25] !== t10 || $[26] !== t11 || $[27] !== t13 || $[28] !== t9 ? (t14 = /* @__PURE__ */ jsxs("button", {
+ "aria-pressed": active,
+ className: pickButton,
+ onClick: t9,
+ onPointerDown: t10,
+ type: "button",
+ children: [t11, t13]
+ }), $[24] = active, $[25] = t10, $[26] = t11, $[27] = t13, $[28] = t9, $[29] = t14) : t14 = $[29];
+ let t15;
+ $[30] === Symbol.for("react.memo_cache_sentinel") ? (t15 = /* @__PURE__ */ jsx(Text, {
+ size: 1,
+ children: "Show more"
+ }), $[30] = t15) : t15 = $[30];
+ let t16 = `Show more for ${title}`, t17;
+ $[31] === t16 ? t17 = $[32] : (t17 = /* @__PURE__ */ jsx(Button, {
+ "aria-label": t16,
+ fontSize: 1,
+ icon: EllipsisHorizontalIcon,
+ mode: "ghost",
+ padding: 1,
+ radius: 2
+ }), $[31] = t16, $[32] = t17);
+ let t18 = `themer-theme-${theme.slug}`, t19;
+ $[33] !== index || $[34] !== listed || $[35] !== moveTo || $[36] !== themes.length ? (t19 = listed && /* @__PURE__ */ jsxs(Fragment, { children: [
+ /* @__PURE__ */ jsx(MenuItem, {
+ disabled: index <= 0,
+ icon: ArrowUpIcon,
+ onClick: () => moveTo(index - 1),
+ text: "Move up"
+ }),
+ /* @__PURE__ */ jsx(MenuItem, {
+ disabled: index >= themes.length - 1,
+ icon: ArrowDownIcon,
+ onClick: () => moveTo(index + 1),
+ text: "Move down"
+ }),
+ /* @__PURE__ */ jsx(MenuDivider, {})
+ ] }), $[33] = index, $[34] = listed, $[35] = moveTo, $[36] = themes.length, $[37] = t19) : t19 = $[37];
+ let t20;
+ $[38] !== actorRef || $[39] !== theme.slug || $[40] !== theme.source ? (t20 = theme.source === "custom" && /* @__PURE__ */ jsx(MenuItem, {
+ icon: EditIcon,
+ onClick: () => actorRef.send({
+ type: "theme.edit",
+ slug: theme.slug
+ }),
+ text: "Edit"
+ }), $[38] = actorRef, $[39] = theme.slug, $[40] = theme.source, $[41] = t20) : t20 = $[41];
+ let t21;
+ $[42] !== actorRef || $[43] !== theme.slug ? (t21 = () => actorRef.send({
+ type: "theme.duplicate",
+ slug: theme.slug
+ }), $[42] = actorRef, $[43] = theme.slug, $[44] = t21) : t21 = $[44];
+ let t22 = theme.source === "custom" ? "Duplicate" : "Duplicate to edit", t23;
+ $[45] !== t21 || $[46] !== t22 ? (t23 = /* @__PURE__ */ jsx(MenuItem, {
+ icon: CopyIcon,
+ onClick: t21,
+ text: t22
+ }), $[45] = t21, $[46] = t22, $[47] = t23) : t23 = $[47];
+ let t24;
+ $[48] !== copyTheme || $[49] !== theme ? (t24 = /* @__PURE__ */ jsx(MenuItem, {
+ icon: ShareIcon,
+ onClick: () => void copyTheme(theme),
+ text: "Copy to share"
+ }), $[48] = copyTheme, $[49] = theme, $[50] = t24) : t24 = $[50];
+ let t25;
+ $[51] !== actorRef || $[52] !== theme.slug || $[53] !== theme.source ? (t25 = theme.source !== "config" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuDivider, {}), /* @__PURE__ */ jsx(MenuItem, {
+ icon: TrashIcon,
+ onClick: () => actorRef.send({
+ type: "theme.remove",
+ slug: theme.slug
+ }),
+ text: "Remove",
+ tone: "critical"
+ })] }), $[51] = actorRef, $[52] = theme.slug, $[53] = theme.source, $[54] = t25) : t25 = $[54];
+ let t26;
+ $[55] !== t19 || $[56] !== t20 || $[57] !== t23 || $[58] !== t24 || $[59] !== t25 ? (t26 = /* @__PURE__ */ jsxs(Menu, { children: [
+ t19,
+ t20,
+ t23,
+ t24,
+ t25
+ ] }), $[55] = t19, $[56] = t20, $[57] = t23, $[58] = t24, $[59] = t25, $[60] = t26) : t26 = $[60];
+ let t27, t28;
+ $[61] === Symbol.for("react.memo_cache_sentinel") ? (t27 = () => setMenuOpen(!1), t28 = () => setMenuOpen(!0), $[61] = t27, $[62] = t28) : (t27 = $[61], t28 = $[62]);
+ let t29;
+ $[63] === scheme ? t29 = $[64] : (t29 = {
+ animate: !0,
+ placement: "bottom-end",
+ portal: !0,
+ scheme
+ }, $[63] = scheme, $[64] = t29);
+ let t30;
+ $[65] !== t17 || $[66] !== t18 || $[67] !== t26 || $[68] !== t29 ? (t30 = /* @__PURE__ */ jsx(Tooltip, {
+ animate: !0,
+ content: t15,
+ placement: "bottom",
+ portal: !0,
+ children: /* @__PURE__ */ jsx(Card, {
+ className: menuCard,
+ radius: 2,
+ scheme: "dark",
+ children: /* @__PURE__ */ jsx(MenuButton, {
+ button: t17,
+ id: t18,
+ menu: t26,
+ onClose: t27,
+ onOpen: t28,
+ popover: t29
+ })
+ })
+ }), $[65] = t17, $[66] = t18, $[67] = t26, $[68] = t29, $[69] = t30) : t30 = $[69];
+ let t31;
+ $[70] !== menuOpen || $[71] !== t30 ? (t31 = /* @__PURE__ */ jsx("div", {
+ className: menuSlot,
+ "data-visible": menuOpen,
+ children: t30
+ }), $[70] = menuOpen, $[71] = t30, $[72] = t31) : t31 = $[72];
+ let t32;
+ return $[73] !== dragControls || $[74] !== t14 || $[75] !== t31 || $[76] !== t7 || $[77] !== theme.slug ? (t32 = /* @__PURE__ */ jsxs(Reorder.Item, {
+ as: "div",
+ className: root,
+ dragControls,
+ dragListener: !1,
+ onDragEnd: t5,
+ onDragStart: t6,
+ ref: t7,
+ value: theme.slug,
+ whileDrag: t8,
+ children: [t14, t31]
+ }), $[73] = dragControls, $[74] = t14, $[75] = t31, $[76] = t7, $[77] = theme.slug, $[78] = t32) : t32 = $[78], t32;
+}
+function _temp$3(candidate_0) {
+ return candidate_0.slug;
+}
+var cardGrid = "twjqgh0";
+/**
+* The flow for picking a theme: a grid of theme cards — the configured theme,
+* the presets and the user's own themes — that drag into the order the user
+* wants, with the entry points to the add and restore flows below. A theme
+* code someone shared pastes right into the list, or through the paste button.
+*
+* @internal
+*/
+function ThemeList(t0) {
+ let $ = c(48), { actorRef, themes, removed, active } = t0, t1;
+ $[0] === actorRef ? t1 = $[1] : (t1 = { actorRef }, $[0] = actorRef, $[1] = t1);
+ let { addThemeFromClipboard } = useThemeCodes(t1), [pasting, setPasting] = useState(!1), t2;
+ $[2] === actorRef ? t2 = $[3] : (t2 = { actorRef }, $[2] = actorRef, $[3] = t2), usePasteThemeCodes(t2);
+ let t3;
+ $[4] === actorRef ? t3 = $[5] : (t3 = (palette, file) => actorRef.send({
+ type: "theme.add",
+ title: titleFromFileName(file.name),
+ options: optionsFromImagePalette(palette),
+ palette,
+ imageUrl: URL.createObjectURL(file)
+ }), $[4] = actorRef, $[5] = t3);
+ let { busy, pickImage } = useImagePalette(t3), t4;
+ $[6] === actorRef ? t4 = $[7] : (t4 = (order) => actorRef.send({
+ type: "theme.reorder",
+ order
+ }), $[6] = actorRef, $[7] = t4);
+ let t5;
+ $[8] === themes ? t5 = $[9] : (t5 = themes.map(_temp$2), $[8] = themes, $[9] = t5);
+ let t6 = !active, t7;
+ $[10] === Symbol.for("react.memo_cache_sentinel") ? (t7 = {
+ slug: CONFIG_SLUG,
+ title: CONFIG_TITLE,
+ options: {},
+ source: "config"
+ }, $[10] = t7) : t7 = $[10];
+ let t8;
+ $[11] !== actorRef || $[12] !== t6 || $[13] !== themes ? (t8 = /* @__PURE__ */ jsx(ThemeCard, {
+ actorRef,
+ active: t6,
+ theme: t7,
+ themes
+ }), $[11] = actorRef, $[12] = t6, $[13] = themes, $[14] = t8) : t8 = $[14];
+ let t9;
+ $[15] !== active?.slug || $[16] !== actorRef || $[17] !== themes ? (t9 = themes.map((theme_0) => /* @__PURE__ */ jsx(ThemeCard, {
+ actorRef,
+ active: theme_0.slug === active?.slug,
+ theme: theme_0,
+ themes
+ }, theme_0.slug)), $[15] = active?.slug, $[16] = actorRef, $[17] = themes, $[18] = t9) : t9 = $[18];
+ let t10;
+ $[19] !== t4 || $[20] !== t5 || $[21] !== t8 || $[22] !== t9 ? (t10 = /* @__PURE__ */ jsx(MotionConfig, {
+ reducedMotion: "user",
+ children: /* @__PURE__ */ jsxs(Reorder.Group, {
+ as: "div",
+ className: cardGrid,
+ onReorder: t4,
+ values: t5,
+ children: [t8, t9]
+ })
+ }), $[19] = t4, $[20] = t5, $[21] = t8, $[22] = t9, $[23] = t10) : t10 = $[23];
+ let t11;
+ $[24] !== actorRef || $[25] !== removed.length ? (t11 = removed.length > 0 && /* @__PURE__ */ jsx(Button, {
+ gap: 2,
+ icon: RestoreIcon,
+ mode: "bleed",
+ onClick: () => actorRef.send({ type: "flow.removed" }),
+ padding: 2,
+ text: `Show removed (${removed.length})`,
+ width: "fill"
+ }), $[24] = actorRef, $[25] = removed.length, $[26] = t11) : t11 = $[26];
+ let t12;
+ $[27] !== t10 || $[28] !== t11 ? (t12 = /* @__PURE__ */ jsx(ScrollArea, {
+ padding: 3,
+ children: /* @__PURE__ */ jsxs(Stack, {
+ gap: 4,
+ children: [t10, t11]
+ })
+ }), $[27] = t10, $[28] = t11, $[29] = t12) : t12 = $[29];
+ let t13;
+ $[30] === actorRef ? t13 = $[31] : (t13 = /* @__PURE__ */ jsx(Box, {
+ flex: 1,
+ children: /* @__PURE__ */ jsx(TooltipButton, {
+ icon: AddIcon,
+ mode: "ghost",
+ onClick: () => actorRef.send({ type: "theme.add" }),
+ text: "Add theme",
+ tooltip: "Add a theme based on the applied one",
+ width: "fill"
+ })
+ }), $[30] = actorRef, $[31] = t13);
+ let t14;
+ $[32] !== busy || $[33] !== pickImage ? (t14 = /* @__PURE__ */ jsx(ImageFileButton, {
+ loading: busy,
+ mode: "ghost",
+ onFile: pickImage,
+ tooltip: "Add a theme from the colors of an image"
+ }), $[32] = busy, $[33] = pickImage, $[34] = t14) : t14 = $[34];
+ let t15;
+ $[35] === addThemeFromClipboard ? t15 = $[36] : (t15 = /* @__PURE__ */ jsx(TooltipButton, {
+ icon: ClipboardIcon,
+ mode: "ghost",
+ onClick: async () => {
+ await addThemeFromClipboard() !== "added" && setPasting(!0);
+ },
+ tooltip: "Add a theme from a code someone shared"
+ }), $[35] = addThemeFromClipboard, $[36] = t15);
+ let t16;
+ $[37] !== t13 || $[38] !== t14 || $[39] !== t15 ? (t16 = /* @__PURE__ */ jsx(Card, {
+ borderTop: !0,
+ padding: 3,
+ children: /* @__PURE__ */ jsxs(Flex, {
+ gap: 2,
+ children: [
+ t13,
+ t14,
+ t15
+ ]
+ })
+ }), $[37] = t13, $[38] = t14, $[39] = t15, $[40] = t16) : t16 = $[40];
+ let t17;
+ $[41] !== actorRef || $[42] !== pasting ? (t17 = pasting && /* @__PURE__ */ jsx(PasteThemeDialog, {
+ actorRef,
+ onClose: () => setPasting(!1)
+ }), $[41] = actorRef, $[42] = pasting, $[43] = t17) : t17 = $[43];
+ let t18;
+ return $[44] !== t12 || $[45] !== t16 || $[46] !== t17 ? (t18 = /* @__PURE__ */ jsxs(Fragment, { children: [
+ t12,
+ t16,
+ t17
+ ] }), $[44] = t12, $[45] = t16, $[46] = t17, $[47] = t18) : t18 = $[47], t18;
+}
+function _temp$2(theme) {
+ return theme.slug;
+}
+/**
+* Reduces theme options to the minimal object that recreates them through
+* `buildTheme`: in each scheme the accent is dropped when it is the stock
+* one, the text color when it matches the one derived from the accent, and
+* the background and contrast when they match the defaults — and a scheme
+* with nothing left is dropped altogether. Options that boil down to the
+* stock Studio theme reduce to `null` — they need nothing from this package.
+*
+* @internal
+*/
+function minimizeOptions(options) {
+ let resolved = resolveThemeOptions(options), minimized = {};
+ for (let scheme of SCHEMES) {
+ let values = resolved[scheme], kept = {};
+ values.accent !== DEFAULT_ACCENT && (kept.accent = values.accent), values.text !== deriveTextColor(values.accent) && (kept.text = values.text), values.background !== DEFAULT_BACKGROUND[scheme] && (kept.background = values.background), values.contrast !== 85 && (kept.contrast = values.contrast), Object.keys(kept).length > 0 && (minimized[scheme] = kept);
+ }
+ return Object.keys(minimized).length === 0 ? null : minimized;
+}
+/**
+* Serializes theme options into the `buildTheme` call to paste into
+* `sanity.config.ts`, keeping only what differs from the derived defaults in
+* each scheme.
+*
+* Options that boil down to the stock Studio theme need nothing from this
+* package — so they serialize to a bare `buildTheme()` from `@sanity/ui/theme`
+* instead.
+*
+* @internal
+*/
+function createThemeSnippet(options) {
+ let minimized = minimizeOptions(options);
+ if (minimized === null) return "import {buildTheme} from '@sanity/ui/theme'\n\nexport const theme = buildTheme()\n";
+ let lines = [];
+ for (let scheme of SCHEMES) {
+ let values = minimized[scheme];
+ values && lines.push(serializeScheme(scheme, values));
+ }
+ return `import {buildTheme} from '@sanity/themer'\n\nexport const theme = buildTheme({\n${lines.join("\n")}\n})\n`;
+}
+/** A scheme with up to two options fits on one line, more spread out */
+function serializeScheme(scheme, values) {
+ let entries = [];
+ return values.accent !== void 0 && entries.push(`accent: '${values.accent}'`), values.text !== void 0 && entries.push(`text: '${values.text}'`), values.background !== void 0 && entries.push(`background: '${values.background}'`), values.contrast !== void 0 && entries.push(`contrast: ${values.contrast}`), entries.length <= 2 ? ` ${scheme}: {${entries.join(", ")}},` : ` ${scheme}: {\n${entries.map((entry) => ` ${entry},`).join("\n")}\n },`;
+}
+registerLanguage(typescript);
+/**
+* Shows the `buildTheme` snippet that makes a theme permanent, in a dialog so
+* that it does not take up sidebar space or make the sidebar scroll as the
+* snippet changes shape with the theme.
+*
+* @internal
+*/
+function ThemeSnippetDialog(props) {
+ let $ = c(23), { onClose, theme } = props, toast = useToast(), t0;
+ $[0] === theme.options ? t0 = $[1] : (t0 = createThemeSnippet(theme.options), $[0] = theme.options, $[1] = t0);
+ let snippet = t0, t1;
+ $[2] !== snippet || $[3] !== toast ? (t1 = async () => {
+ try {
+ await navigator.clipboard.writeText(snippet), toast.push({
+ status: "success",
+ title: "Copied the theme to the clipboard"
+ });
+ } catch {
+ toast.push({
+ status: "error",
+ title: "Could not copy the theme"
+ });
+ }
+ }, $[2] = snippet, $[3] = toast, $[4] = t1) : t1 = $[4];
+ let handleCopy = t1, t2;
+ $[5] === handleCopy ? t2 = $[6] : (t2 = /* @__PURE__ */ jsx(Flex, {
+ gap: 2,
+ justify: "flex-end",
+ padding: 3,
+ children: /* @__PURE__ */ jsx(Button, {
+ gap: 2,
+ icon: ClipboardIcon,
+ mode: "ghost",
+ onClick: () => void handleCopy(),
+ padding: 2,
+ text: "Copy"
+ })
+ }), $[5] = handleCopy, $[6] = t2);
+ let t3;
+ $[7] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx("code", { children: "sanity.config.ts" }), $[7] = t3) : t3 = $[7];
+ let t4;
+ $[8] === Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx("code", { children: "theme" }), $[8] = t4) : t4 = $[8];
+ let t5;
+ $[9] === Symbol.for("react.memo_cache_sentinel") ? (t5 = /* @__PURE__ */ jsx("code", { children: "defineConfig" }), $[9] = t5) : t5 = $[9];
+ let t6;
+ $[10] === theme.title ? t6 = $[11] : (t6 = displayTitle(theme.title), $[10] = theme.title, $[11] = t6);
+ let t7;
+ $[12] === t6 ? t7 = $[13] : (t7 = /* @__PURE__ */ jsxs(Text, {
+ muted: !0,
+ size: 1,
+ children: [
+ "Export this from ",
+ t3,
+ " and pass it as the ",
+ t4,
+ " of",
+ " ",
+ t5,
+ " to make “",
+ t6,
+ "” the Studio theme without this tool."
+ ]
+ }), $[12] = t6, $[13] = t7);
+ let t8;
+ $[14] === snippet ? t8 = $[15] : (t8 = /* @__PURE__ */ jsx(Card, {
+ border: !0,
+ overflow: "auto",
+ padding: 3,
+ radius: 2,
+ tone: "transparent",
+ children: /* @__PURE__ */ jsx(Code, {
+ language: "ts",
+ size: 1,
+ children: snippet
+ })
+ }), $[14] = snippet, $[15] = t8);
+ let t9;
+ $[16] !== t7 || $[17] !== t8 ? (t9 = /* @__PURE__ */ jsx(Box, {
+ padding: 4,
+ children: /* @__PURE__ */ jsxs(Stack, {
+ gap: 4,
+ children: [t7, t8]
+ })
+ }), $[16] = t7, $[17] = t8, $[18] = t9) : t9 = $[18];
+ let t10;
+ return $[19] !== onClose || $[20] !== t2 || $[21] !== t9 ? (t10 = /* @__PURE__ */ jsx(Dialog, {
+ animate: !0,
+ footer: t2,
+ header: "Add to your config",
+ id: "themer-snippet",
+ onClickOutside: onClose,
+ onClose,
+ width: 1,
+ children: t9
+ }), $[19] = onClose, $[20] = t2, $[21] = t9, $[22] = t10) : t10 = $[22], t10;
+}
+function useIsTooSmallForSplitScreen() {
+ return useMediaIndex() <= 3;
+}
+var title = "c2pqej0";
+const VIEW_TITLES = {
+ list: "Themer",
+ edit: "Edit theme",
+ removed: "Removed themes"
+};
+/**
+* The themer sidebar: a header that navigates between the flows and toggles
+* the split preview, the flow itself — picking a theme, editing one, or
+* restoring removed ones — and the dialog with the `buildTheme` snippet of
+* the applied theme.
+*
+* @internal
+*/
+function ThemerSidebar(t0) {
+ let $ = c(58), { actorRef, dispatch, navbarHeight, split } = t0, [snippetOpen, setSnippetOpen] = useState(!1), view = useSelector(actorRef, selectView, sameView), inList = view.name === "list", isTooSmallForSplitScreen = useIsTooSmallForSplitScreen(), { themes, removed, active } = useSelector(actorRef, _temp$1, dequal), t1;
+ $[0] === navbarHeight ? t1 = $[1] : (t1 = navbarHeight === null ? void 0 : { height: navbarHeight }, $[0] = navbarHeight, $[1] = t1);
+ let t2;
+ $[2] !== actorRef || $[3] !== inList ? (t2 = !inList && /* @__PURE__ */ jsx(TooltipButton, {
+ icon: ArrowLeftIcon,
+ mode: "bleed",
+ onClick: () => actorRef.send({ type: "flow.list" }),
+ padding: 2,
+ tooltip: "Back to the themes"
+ }), $[2] = actorRef, $[3] = inList, $[4] = t2) : t2 = $[4];
+ let t3 = VIEW_TITLES[view.name], t4;
+ $[5] === t3 ? t4 = $[6] : (t4 = /* @__PURE__ */ jsx(Box, {
+ className: title,
+ flex: 1,
+ children: /* @__PURE__ */ jsx(Text, {
+ size: 1,
+ textOverflow: "ellipsis",
+ weight: "semibold",
+ children: t3
+ })
+ }), $[5] = t3, $[6] = t4);
+ let t5;
+ $[7] !== active || $[8] !== dispatch || $[9] !== isTooSmallForSplitScreen || $[10] !== split || $[11] !== view.name ? (t5 = view.name !== "removed" && /* @__PURE__ */ jsxs(Fragment, { children: [!isTooSmallForSplitScreen && /* @__PURE__ */ jsx(TooltipButton, {
+ "aria-pressed": split,
+ icon: SplitVerticalIcon,
+ mode: "bleed",
+ onClick: () => startTransition(() => {
+ let type = split ? "split-screen:close" : "split-screen:open";
+ addThemerTransitionType(type), dispatch({ type });
+ }),
+ onMouseEnter: () => startTransition(() => {
+ dispatch({ type: "split-screen:prerender" });
+ }),
+ padding: 2,
+ selected: split,
+ tooltip: "Show light and dark side by side"
+ }), active && /* @__PURE__ */ jsx(TooltipButton, {
+ icon: CodeBlockIcon,
+ mode: "bleed",
+ onClick: () => setSnippetOpen(!0),
+ padding: 2,
+ tooltip: "Show the code for the applied theme"
+ })] }), $[7] = active, $[8] = dispatch, $[9] = isTooSmallForSplitScreen, $[10] = split, $[11] = view.name, $[12] = t5) : t5 = $[12];
+ let t6;
+ $[13] !== dispatch || $[14] !== split ? (t6 = /* @__PURE__ */ jsx(TooltipButton, {
+ icon: CloseIcon,
+ mode: "bleed",
+ onClick: () => startTransition(() => {
+ split && addThemerTransitionType("split-screen:close"), addThemerTransitionType("close"), dispatch({ type: "close" });
+ }),
+ padding: 2,
+ tooltip: "Close themer"
+ }), $[13] = dispatch, $[14] = split, $[15] = t6) : t6 = $[15];
+ let t7;
+ $[16] !== t5 || $[17] !== t6 ? (t7 = /* @__PURE__ */ jsxs(Flex, { children: [t5, t6] }), $[16] = t5, $[17] = t6, $[18] = t7) : t7 = $[18];
+ let t8;
+ $[19] !== t2 || $[20] !== t4 || $[21] !== t7 ? (t8 = /* @__PURE__ */ jsxs(Flex, {
+ align: "center",
+ gap: 1,
+ height: "fill",
+ children: [
+ t2,
+ t4,
+ t7
+ ]
+ }), $[19] = t2, $[20] = t4, $[21] = t7, $[22] = t8) : t8 = $[22];
+ let t9;
+ $[23] !== t1 || $[24] !== t8 ? (t9 = /* @__PURE__ */ jsx(Card, {
+ borderBottom: !0,
+ padding: 3,
+ sizing: "border",
+ style: t1,
+ children: t8
+ }), $[23] = t1, $[24] = t8, $[25] = t9) : t9 = $[25];
+ let t10 = view.name === "list" ? "visible" : "hidden", t11;
+ $[26] !== active || $[27] !== actorRef || $[28] !== removed || $[29] !== themes ? (t11 = /* @__PURE__ */ jsx(ThemeList, {
+ actorRef,
+ active,
+ removed,
+ themes
+ }), $[26] = active, $[27] = actorRef, $[28] = removed, $[29] = themes, $[30] = t11) : t11 = $[30];
+ let t12;
+ $[31] !== t10 || $[32] !== t11 ? (t12 = /* @__PURE__ */ jsx(Activity, {
+ mode: t10,
+ children: t11
+ }, "list"), $[31] = t10, $[32] = t11, $[33] = t12) : t12 = $[33];
+ let t13;
+ $[34] !== actorRef || $[35] !== split || $[36] !== themes || $[37] !== view.focusTitle || $[38] !== view.name || $[39] !== view.slug ? (t13 = view.name === "edit" && /* @__PURE__ */ jsx(ThemeEditor, {
+ actorRef,
+ focusTitle: view.focusTitle,
+ slug: view.slug,
+ split,
+ themes
+ }), $[34] = actorRef, $[35] = split, $[36] = themes, $[37] = view.focusTitle, $[38] = view.name, $[39] = view.slug, $[40] = t13) : t13 = $[40];
+ let t14 = view.name === "removed" ? "visible" : "hidden", t15;
+ $[41] !== actorRef || $[42] !== removed ? (t15 = /* @__PURE__ */ jsx(RemovedThemes, {
+ actorRef,
+ removed
+ }), $[41] = actorRef, $[42] = removed, $[43] = t15) : t15 = $[43];
+ let t16;
+ $[44] !== t14 || $[45] !== t15 ? (t16 = /* @__PURE__ */ jsx(Activity, {
+ mode: t14,
+ children: t15
+ }, "removed"), $[44] = t14, $[45] = t15, $[46] = t16) : t16 = $[46];
+ let t17;
+ $[47] !== t12 || $[48] !== t13 || $[49] !== t16 || $[50] !== t9 ? (t17 = /* @__PURE__ */ jsxs(Flex, {
+ direction: "column",
+ height: "fill",
+ children: [
+ t9,
+ t12,
+ t13,
+ t16
+ ]
+ }), $[47] = t12, $[48] = t13, $[49] = t16, $[50] = t9, $[51] = t17) : t17 = $[51];
+ let t18;
+ $[52] !== active || $[53] !== snippetOpen ? (t18 = active && snippetOpen && /* @__PURE__ */ jsx(ThemeSnippetDialog, {
+ onClose: () => setSnippetOpen(!1),
+ theme: active
+ }), $[52] = active, $[53] = snippetOpen, $[54] = t18) : t18 = $[54];
+ let t19;
+ return $[55] !== t17 || $[56] !== t18 ? (t19 = /* @__PURE__ */ jsxs(Card, {
+ height: "fill",
+ children: [t17, t18]
+ }), $[55] = t17, $[56] = t18, $[57] = t19) : t19 = $[57], t19;
+}
+function _temp$1(snapshot) {
+ return resolveThemes(snapshot.context);
+}
+function selectView(snapshot) {
+ let { editing } = snapshot.context;
+ return snapshot.matches({ flow: "edit" }) && editing ? {
+ name: "edit",
+ slug: editing.slug,
+ focusTitle: editing.focusTitle
+ } : snapshot.matches({ flow: "removed" }) ? { name: "removed" } : { name: "list" };
+}
+function sameView(a, b) {
+ return a.name === b.name ? a.name !== "edit" || b.name !== "edit" || a.slug === b.slug && a.focusTitle === b.focusTitle : !1;
+}
+var content = "bgn4np3", frame = "bgn4np2", resizeHandle = "bgn4np4", sidebar = "bgn4np1";
+/** How far one arrow key press resizes the sidebar */
+const WIDTH_STORAGE_KEY = "sanityStudio:themer:width";
+function clampWidth(width) {
+ return Math.min(400, Math.max(200, Math.round(width)));
+}
+function readStoredWidth() {
+ try {
+ let stored = Number(localStorage.getItem(WIDTH_STORAGE_KEY));
+ return Number.isFinite(stored) && stored > 0 ? clampWidth(stored) : 200;
+ } catch {
+ return 200;
+ }
+}
+function writeStoredWidth(width) {
+ try {
+ width === 200 ? localStorage.removeItem(WIDTH_STORAGE_KEY) : localStorage.setItem(WIDTH_STORAGE_KEY, String(width));
+ } catch {}
+}
+/**
+* The themer sidebar, in a layer along the right edge of the Studio. Its left
+* edge drags (or arrow-keys) it wider, up to twice its default width — the
+* width sticks between sessions. As an overlay (small screens) it covers the
+* Studio edge to edge instead, with nothing to resize.
+*
+* @internal
+*/
+function ResizableSidebar(t0) {
+ let $ = c(30), { actorRef, dispatch, navbarHeight, split } = t0, [width, setWidth] = useState(readStoredWidth), [dragging, setDragging] = useState(!1), drag = useRef(null), t1, t2;
+ $[0] === width ? (t1 = $[1], t2 = $[2]) : (t1 = () => writeStoredWidth(width), t2 = [width], $[0] = width, $[1] = t1, $[2] = t2), useEffect(t1, t2);
+ let t3;
+ $[3] === width ? t3 = $[4] : (t3 = (event) => {
+ event.button === 0 && (event.preventDefault(), event.currentTarget.setPointerCapture(event.pointerId), drag.current = {
+ pointerId: event.pointerId,
+ startX: event.clientX,
+ startWidth: width
+ }, setDragging(!0));
+ }, $[3] = width, $[4] = t3);
+ let handlePointerDown = t3, t4;
+ $[5] === Symbol.for("react.memo_cache_sentinel") ? (t4 = (event_0) => {
+ drag.current && drag.current.pointerId === event_0.pointerId && setWidth(clampWidth(drag.current.startWidth - (event_0.clientX - drag.current.startX)));
+ }, $[5] = t4) : t4 = $[5];
+ let handlePointerMove = t4, t5;
+ $[6] === Symbol.for("react.memo_cache_sentinel") ? (t5 = (event_1) => {
+ drag.current && drag.current.pointerId === event_1.pointerId && (drag.current = null, setDragging(!1));
+ }, $[6] = t5) : t5 = $[6];
+ let handlePointerUp = t5, t6;
+ $[7] === Symbol.for("react.memo_cache_sentinel") ? (t6 = (event_2) => {
+ let step = {
+ ArrowLeft: 16,
+ ArrowRight: -16,
+ Home: 400,
+ End: -400
+ }[event_2.key];
+ step !== void 0 && (event_2.preventDefault(), setWidth((current) => clampWidth(current + step)));
+ }, $[7] = t6) : t6 = $[7];
+ let handleKeyDown = t6, [boundaryElement, setBoundaryElement] = useState(null), t7;
+ $[8] === width ? t7 = $[9] : (t7 = { width }, $[8] = width, $[9] = t7);
+ let t8;
+ $[10] === Symbol.for("react.memo_cache_sentinel") ? (t8 = (boundaryElement_0) => (startTransition(() => setBoundaryElement(boundaryElement_0)), _temp), $[10] = t8) : t8 = $[10];
+ let t9;
+ $[11] === Symbol.for("react.memo_cache_sentinel") ? (t9 = () => setWidth(200), $[11] = t9) : t9 = $[11];
+ let t10;
+ $[12] !== dragging || $[13] !== handlePointerDown || $[14] !== width ? (t10 = /* @__PURE__ */ jsx("div", {
+ "aria-label": "Resize the themer",
+ className: resizeHandle,
+ "aria-orientation": "vertical",
+ "aria-valuemax": 400,
+ "aria-valuemin": 200,
+ "aria-valuenow": width,
+ "data-dragging": dragging,
+ onDoubleClick: t9,
+ onKeyDown: handleKeyDown,
+ onPointerCancel: handlePointerUp,
+ onPointerDown: handlePointerDown,
+ onPointerMove: handlePointerMove,
+ onPointerUp: handlePointerUp,
+ role: "separator",
+ tabIndex: 0
+ }), $[12] = dragging, $[13] = handlePointerDown, $[14] = width, $[15] = t10) : t10 = $[15];
+ let t11;
+ $[16] !== actorRef || $[17] !== dispatch || $[18] !== navbarHeight || $[19] !== split ? (t11 = /* @__PURE__ */ jsx(ThemerSidebar, {
+ split,
+ navbarHeight,
+ actorRef,
+ dispatch
+ }), $[16] = actorRef, $[17] = dispatch, $[18] = navbarHeight, $[19] = split, $[20] = t11) : t11 = $[20];
+ let t12;
+ $[21] !== boundaryElement || $[22] !== t11 ? (t12 = /* @__PURE__ */ jsx(Box, {
+ className: content,
+ height: "fill",
+ overflow: "hidden",
+ children: /* @__PURE__ */ jsx(BoundaryElementProvider, {
+ element: boundaryElement,
+ children: t11
+ })
+ }), $[21] = boundaryElement, $[22] = t11, $[23] = t12) : t12 = $[23];
+ let t13;
+ $[24] !== t10 || $[25] !== t12 ? (t13 = /* @__PURE__ */ jsxs(Card, {
+ borderLeft: !0,
+ className: frame,
+ height: "fill",
+ ref: t8,
+ children: [t10, t12]
+ }), $[24] = t10, $[25] = t12, $[26] = t13) : t13 = $[26];
+ let t14;
+ return $[27] !== t13 || $[28] !== t7 ? (t14 = /* @__PURE__ */ jsx(Layer, {
+ className: sidebar,
+ style: t7,
+ zOffset: 100,
+ children: t13
+ }), $[27] = t13, $[28] = t7, $[29] = t14) : t14 = $[29], t14;
+}
+function _temp() {}
+export { ResizableSidebar as default };
+
+//# sourceMappingURL=ResizableSidebar-BpgLA4XQ.js.map
\ No newline at end of file