@sanity/themer 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/ResizableSidebar-DD787kQr.js +2050 -0
- package/dist/ResizableSidebar-DD787kQr.js.map +1 -0
- package/dist/ResizableSidebar.css.ts.vanilla-M9EuPu-q.js +422 -0
- package/dist/ResizableSidebar.css.ts.vanilla-M9EuPu-q.js.map +1 -0
- package/dist/bundle.css +555 -555
- package/dist/tool.js +368 -2576
- package/dist/tool.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { a as DEFAULT_BACKGROUND, c as resolveThemeOptions, d as hslToHex, f as relativeLuminance, i as DEFAULT_ACCENT, o as SCHEMES, s as deriveTextColor, t as presets, u as hexToHsl } from "./presets-CqBgS3A-.js";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
/** @internal */
|
|
4
|
+
const ThemerContext = createContext(null);
|
|
5
|
+
/** @internal */
|
|
6
|
+
function useThemer() {
|
|
7
|
+
let context = useContext(ThemerContext);
|
|
8
|
+
if (!context) throw Error("useThemer must be used within the `themerTool` plugin");
|
|
9
|
+
return context;
|
|
10
|
+
}
|
|
11
|
+
/** @internal */
|
|
12
|
+
const IMAGE_PALETTE_KEYS = [
|
|
13
|
+
"vibrant",
|
|
14
|
+
"lightVibrant",
|
|
15
|
+
"darkVibrant",
|
|
16
|
+
"muted",
|
|
17
|
+
"lightMuted",
|
|
18
|
+
"darkMuted",
|
|
19
|
+
"dominant"
|
|
20
|
+
], IMAGE_PALETTE_VARIANTS = [
|
|
21
|
+
"muted",
|
|
22
|
+
"vibrant",
|
|
23
|
+
"lightMuted",
|
|
24
|
+
"lightVibrant",
|
|
25
|
+
"darkMuted",
|
|
26
|
+
"darkVibrant"
|
|
27
|
+
], IMAGE_PALETTE_TITLES = {
|
|
28
|
+
dominant: "Dominant",
|
|
29
|
+
vibrant: "Vibrant",
|
|
30
|
+
lightVibrant: "Light vibrant",
|
|
31
|
+
darkVibrant: "Dark vibrant",
|
|
32
|
+
muted: "Muted",
|
|
33
|
+
lightMuted: "Light muted",
|
|
34
|
+
darkMuted: "Dark muted"
|
|
35
|
+
}, TARGETS = [
|
|
36
|
+
["vibrant", {
|
|
37
|
+
lightness: [
|
|
38
|
+
.5,
|
|
39
|
+
.3,
|
|
40
|
+
.7
|
|
41
|
+
],
|
|
42
|
+
saturation: [
|
|
43
|
+
1,
|
|
44
|
+
.35,
|
|
45
|
+
1
|
|
46
|
+
]
|
|
47
|
+
}],
|
|
48
|
+
["lightVibrant", {
|
|
49
|
+
lightness: [
|
|
50
|
+
.74,
|
|
51
|
+
.55,
|
|
52
|
+
1
|
|
53
|
+
],
|
|
54
|
+
saturation: [
|
|
55
|
+
1,
|
|
56
|
+
.35,
|
|
57
|
+
1
|
|
58
|
+
]
|
|
59
|
+
}],
|
|
60
|
+
["darkVibrant", {
|
|
61
|
+
lightness: [
|
|
62
|
+
.26,
|
|
63
|
+
0,
|
|
64
|
+
.45
|
|
65
|
+
],
|
|
66
|
+
saturation: [
|
|
67
|
+
1,
|
|
68
|
+
.35,
|
|
69
|
+
1
|
|
70
|
+
]
|
|
71
|
+
}],
|
|
72
|
+
["muted", {
|
|
73
|
+
lightness: [
|
|
74
|
+
.5,
|
|
75
|
+
.3,
|
|
76
|
+
.7
|
|
77
|
+
],
|
|
78
|
+
saturation: [
|
|
79
|
+
.3,
|
|
80
|
+
0,
|
|
81
|
+
.4
|
|
82
|
+
]
|
|
83
|
+
}],
|
|
84
|
+
["lightMuted", {
|
|
85
|
+
lightness: [
|
|
86
|
+
.74,
|
|
87
|
+
.55,
|
|
88
|
+
1
|
|
89
|
+
],
|
|
90
|
+
saturation: [
|
|
91
|
+
.3,
|
|
92
|
+
0,
|
|
93
|
+
.4
|
|
94
|
+
]
|
|
95
|
+
}],
|
|
96
|
+
["darkMuted", {
|
|
97
|
+
lightness: [
|
|
98
|
+
.26,
|
|
99
|
+
0,
|
|
100
|
+
.45
|
|
101
|
+
],
|
|
102
|
+
saturation: [
|
|
103
|
+
.3,
|
|
104
|
+
0,
|
|
105
|
+
.4
|
|
106
|
+
]
|
|
107
|
+
}]
|
|
108
|
+
];
|
|
109
|
+
/**
|
|
110
|
+
* Extracts the color palette of an image from its pixels, the way the Vibrant
|
|
111
|
+
* algorithm does: the colors are quantized into a coarse histogram, and each
|
|
112
|
+
* swatch claims the color that comes closest to its target lightness and
|
|
113
|
+
* saturation — weighted by how much of the image it covers — that no other
|
|
114
|
+
* swatch has claimed yet. The dominant swatch is simply the most common
|
|
115
|
+
* color. Runs anywhere pixels come from, so it can run on device.
|
|
116
|
+
*
|
|
117
|
+
* @internal
|
|
118
|
+
*/
|
|
119
|
+
function extractImagePalette(pixels) {
|
|
120
|
+
let candidates = quantize(pixels), palette = {
|
|
121
|
+
dominant: null,
|
|
122
|
+
vibrant: null,
|
|
123
|
+
lightVibrant: null,
|
|
124
|
+
darkVibrant: null,
|
|
125
|
+
muted: null,
|
|
126
|
+
lightMuted: null,
|
|
127
|
+
darkMuted: null
|
|
128
|
+
};
|
|
129
|
+
if (candidates.length === 0) return palette;
|
|
130
|
+
let maximumPopulation = Math.max(...candidates.map((candidate) => candidate.population)), claimed = /* @__PURE__ */ new Set();
|
|
131
|
+
for (let [key, target] of TARGETS) {
|
|
132
|
+
let best = null, bestScore = -Infinity;
|
|
133
|
+
for (let candidate of candidates) {
|
|
134
|
+
if (claimed.has(candidate.hex) || !matchesTarget(candidate, target)) continue;
|
|
135
|
+
let score = scoreCandidate(candidate, target, maximumPopulation);
|
|
136
|
+
score > bestScore && (best = candidate, bestScore = score);
|
|
137
|
+
}
|
|
138
|
+
best && (palette[key] = best.hex, claimed.add(best.hex));
|
|
139
|
+
}
|
|
140
|
+
return palette.dominant = candidates.reduce((most, candidate) => candidate.population > most.population ? candidate : most).hex, palette;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Turns the pixels into a histogram of quantized colors, each averaged over
|
|
144
|
+
* the pixels it stands for, leaving out transparent pixels and the near-black
|
|
145
|
+
* and near-white ones that are backdrop rather than color.
|
|
146
|
+
*/
|
|
147
|
+
function quantize(pixels) {
|
|
148
|
+
let { data } = pixels, buckets = /* @__PURE__ */ new Map(), length = Math.min(data.length, pixels.width * pixels.height * 4);
|
|
149
|
+
for (let offset = 0; offset + 3 < length; offset += 4) {
|
|
150
|
+
let r = data[offset], g = data[offset + 1], b = data[offset + 2];
|
|
151
|
+
if (data[offset + 3] < 125) continue;
|
|
152
|
+
let lightness = (Math.max(r, g, b) + Math.min(r, g, b)) / 510;
|
|
153
|
+
if (lightness < .05 || lightness > .95) continue;
|
|
154
|
+
let key = r >> 3 << 10 | g >> 3 << 5 | b >> 3, bucket = buckets.get(key);
|
|
155
|
+
bucket ? (bucket.count++, bucket.r += r, bucket.g += g, bucket.b += b) : buckets.set(key, {
|
|
156
|
+
count: 1,
|
|
157
|
+
r,
|
|
158
|
+
g,
|
|
159
|
+
b
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
let candidates = [];
|
|
163
|
+
for (let bucket of buckets.values()) {
|
|
164
|
+
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);
|
|
165
|
+
candidates.push({
|
|
166
|
+
hex: rgbToHex(r, g, b),
|
|
167
|
+
population: bucket.count,
|
|
168
|
+
saturation,
|
|
169
|
+
lightness
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return candidates;
|
|
173
|
+
}
|
|
174
|
+
function matchesTarget(candidate, target) {
|
|
175
|
+
return candidate.lightness >= target.lightness[1] && candidate.lightness <= target.lightness[2] && candidate.saturation >= target.saturation[1] && candidate.saturation <= target.saturation[2];
|
|
176
|
+
}
|
|
177
|
+
function scoreCandidate(candidate, target, maximumPopulation) {
|
|
178
|
+
return 3 * (1 - Math.abs(candidate.saturation - target.saturation[0])) + 6.5 * (1 - Math.abs(candidate.lightness - target.lightness[0])) + .5 * (candidate.population / maximumPopulation);
|
|
179
|
+
}
|
|
180
|
+
function saturationAndLightness(r, g, b) {
|
|
181
|
+
let max = Math.max(r, g, b) / 255, min = Math.min(r, g, b) / 255, lightness = (max + min) / 2, delta = max - min;
|
|
182
|
+
return {
|
|
183
|
+
saturation: delta === 0 ? 0 : lightness > .5 ? delta / (2 - max - min) : delta / (max + min),
|
|
184
|
+
lightness
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function rgbToHex(r, g, b) {
|
|
188
|
+
return `#${[
|
|
189
|
+
r,
|
|
190
|
+
g,
|
|
191
|
+
b
|
|
192
|
+
].map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
|
|
193
|
+
}
|
|
194
|
+
/** Reads `#rgb` and `#rrggbb` alike — stored palettes may hold either */
|
|
195
|
+
function hexToRgb(hex) {
|
|
196
|
+
let expanded = hex.length === 4 ? `${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex.slice(1);
|
|
197
|
+
return [
|
|
198
|
+
parseInt(expanded.slice(0, 2), 16),
|
|
199
|
+
parseInt(expanded.slice(2, 4), 16),
|
|
200
|
+
parseInt(expanded.slice(4, 6), 16)
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
/** Mixes `amount` of `color` into `base` */
|
|
204
|
+
function tint(base, color, amount) {
|
|
205
|
+
let [r1, g1, b1] = hexToRgb(base), [r2, g2, b2] = hexToRgb(color);
|
|
206
|
+
return rgbToHex(Math.round(r1 + (r2 - r1) * amount), Math.round(g1 + (g2 - g1) * amount), Math.round(b1 + (b2 - b1) * amount));
|
|
207
|
+
}
|
|
208
|
+
/** How much of the palette tints each scheme's stock background */
|
|
209
|
+
const BACKGROUND_TINT = {
|
|
210
|
+
light: .08,
|
|
211
|
+
dark: .3
|
|
212
|
+
}, BUTTON_LABEL = {
|
|
213
|
+
light: "#ffffff",
|
|
214
|
+
dark: "#000000"
|
|
215
|
+
}, ACCENT_LIGHTNESS_STEP = .01;
|
|
216
|
+
function contrastRatio(a, b) {
|
|
217
|
+
let [lighter, darker] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
|
|
218
|
+
return (lighter + .05) / (darker + .05);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Makes an accent readable as a button in the given scheme: a light scheme
|
|
222
|
+
* accent is darkened until white text on it reaches AA contrast, a dark
|
|
223
|
+
* scheme accent lightened until black text does — as far as the accent's
|
|
224
|
+
* hue and saturation allow. Accents that already pass come back untouched.
|
|
225
|
+
*
|
|
226
|
+
* @internal
|
|
227
|
+
*/
|
|
228
|
+
function readableAccent(hex, scheme) {
|
|
229
|
+
let label = BUTTON_LABEL[scheme], { h, s } = hexToHsl(hex), { l } = hexToHsl(hex), current = hex;
|
|
230
|
+
for (; contrastRatio(current, label) < 4.5;) {
|
|
231
|
+
let next = scheme === "light" ? l - ACCENT_LIGHTNESS_STEP : l + ACCENT_LIGHTNESS_STEP;
|
|
232
|
+
if (next < 0 || next > 1) break;
|
|
233
|
+
l = next, current = hslToHex({
|
|
234
|
+
h,
|
|
235
|
+
s,
|
|
236
|
+
l
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return current;
|
|
240
|
+
}
|
|
241
|
+
/** The accent a variant gives a scheme, made readable on a button */
|
|
242
|
+
function variantAccent(palette, variant, scheme) {
|
|
243
|
+
let swatch = (variant ? palette[variant] : null) ?? palette.vibrant ?? (scheme === "light" ? palette.darkVibrant : palette.lightVibrant) ?? palette.dominant;
|
|
244
|
+
return swatch === null ? null : readableAccent(swatch, scheme);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Derives theme options from an image palette: the accent of both schemes is
|
|
248
|
+
* the swatch of the chosen variant — or, without one, the vibrant swatch,
|
|
249
|
+
* each scheme falling back to the vibrant swatch of its own end of the scale —
|
|
250
|
+
* adjusted until it is readable as a button. The muted swatch becomes the text color, and the light and dark muted
|
|
251
|
+
* swatches tint the stock backgrounds of their scheme — enough to carry the
|
|
252
|
+
* image's tone without giving up a usable canvas. Whatever the palette lacks
|
|
253
|
+
* stays at the theme defaults.
|
|
254
|
+
*
|
|
255
|
+
* @internal
|
|
256
|
+
*/
|
|
257
|
+
function optionsFromImagePalette(palette, variant) {
|
|
258
|
+
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");
|
|
259
|
+
return light && (options.light = light), dark && (options.dark = dark), options;
|
|
260
|
+
}
|
|
261
|
+
function schemeFromPalette(accent, text, backgroundTint, scheme) {
|
|
262
|
+
let options = {};
|
|
263
|
+
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;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Applies an image palette to existing theme options: the colors the palette
|
|
267
|
+
* provides replace the current ones scheme by scheme, and whatever it does
|
|
268
|
+
* not touch — like the contrast — stays as it is.
|
|
269
|
+
*
|
|
270
|
+
* @internal
|
|
271
|
+
*/
|
|
272
|
+
function applyImagePalette(options, palette, variant) {
|
|
273
|
+
let derived = optionsFromImagePalette(palette, variant), next = { ...options };
|
|
274
|
+
for (let scheme of SCHEMES) derived[scheme] && (next[scheme] = {
|
|
275
|
+
...options[scheme],
|
|
276
|
+
...derived[scheme]
|
|
277
|
+
});
|
|
278
|
+
return next;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The variant a theme is built around: the one whose swatch is the theme's
|
|
282
|
+
* accent, or `null` when the accent comes from somewhere else.
|
|
283
|
+
*
|
|
284
|
+
* @internal
|
|
285
|
+
*/
|
|
286
|
+
function currentImageVariant(options, palette) {
|
|
287
|
+
let accent = options.light?.accent;
|
|
288
|
+
return accent ? IMAGE_PALETTE_VARIANTS.find((variant) => palette[variant] !== null && variantAccent(palette, variant, "light") === accent) ?? null : null;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* How much a swatch has going for it as an accent: saturation first, then
|
|
292
|
+
* closeness to mid lightness — the range where an accent reads best.
|
|
293
|
+
*/
|
|
294
|
+
function interest(hex) {
|
|
295
|
+
let { s, l } = hexToHsl(hex);
|
|
296
|
+
return .1 + .55 * s + .35 * (1 - Math.abs(l - .5) * 2);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Picks a variant for the "I'm feeling lucky" button: a random one, weighted
|
|
300
|
+
* by how interesting its swatch is as an accent, and never the one the theme
|
|
301
|
+
* already uses while there are others to pick from.
|
|
302
|
+
*
|
|
303
|
+
* @internal
|
|
304
|
+
*/
|
|
305
|
+
function pickLuckyVariant(palette, options = {}) {
|
|
306
|
+
let { exclude = null, random = Math.random } = options, candidates = IMAGE_PALETTE_VARIANTS.filter((variant) => palette[variant] !== null);
|
|
307
|
+
if (candidates.length > 1 && (candidates = candidates.filter((variant) => variant !== exclude)), candidates.length === 0) return null;
|
|
308
|
+
let weights = candidates.map((variant) => interest(palette[variant])), total = weights.reduce((sum, weight) => sum + weight, 0), pick = random() * total;
|
|
309
|
+
for (let [index, variant] of candidates.entries()) if (pick -= weights[index], pick < 0) return variant;
|
|
310
|
+
return candidates.at(-1) ?? null;
|
|
311
|
+
}
|
|
312
|
+
/** A theme title for the image a palette came from, e.g. `sunset-beach.jpg` → `sunset beach` @internal */
|
|
313
|
+
function titleFromFileName(name) {
|
|
314
|
+
return name.replace(/\.[^.]+$/, "").replace(/[-_]+/g, " ").trim() || "Image theme";
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Reduces theme options to the minimal object that recreates them through
|
|
318
|
+
* `buildTheme`: in each scheme the accent is dropped when it is the stock
|
|
319
|
+
* one, the text color when it matches the one derived from the accent, and
|
|
320
|
+
* the background and contrast when they match the defaults — and a scheme
|
|
321
|
+
* with nothing left is dropped altogether. Options that boil down to the
|
|
322
|
+
* stock Studio theme reduce to `null` — they need nothing from this package.
|
|
323
|
+
*
|
|
324
|
+
* @internal
|
|
325
|
+
*/
|
|
326
|
+
function minimizeOptions(options) {
|
|
327
|
+
let resolved = resolveThemeOptions(options), minimized = {};
|
|
328
|
+
for (let scheme of SCHEMES) {
|
|
329
|
+
let values = resolved[scheme], kept = {};
|
|
330
|
+
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);
|
|
331
|
+
}
|
|
332
|
+
return Object.keys(minimized).length === 0 ? null : minimized;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Whether two sets of theme options generate the same theme, comparing their
|
|
336
|
+
* resolved forms so that derived and explicit values (and hex casing) don't
|
|
337
|
+
* matter.
|
|
338
|
+
*
|
|
339
|
+
* @internal
|
|
340
|
+
*/
|
|
341
|
+
function sameOptions(a, b) {
|
|
342
|
+
let left = resolveThemeOptions(a), right = resolveThemeOptions(b);
|
|
343
|
+
return SCHEMES.every((scheme) => sameScheme(left[scheme], right[scheme]));
|
|
344
|
+
}
|
|
345
|
+
function sameScheme(a, b) {
|
|
346
|
+
return a.accent === b.accent && a.text === b.text && a.background === b.background && a.contrast === b.contrast;
|
|
347
|
+
}
|
|
348
|
+
/** The slug of the theme the Studio config was generated from @internal */
|
|
349
|
+
const CONFIG_SLUG = "config", UNTITLED_THEME = "Untitled theme", initialThemerState = {
|
|
350
|
+
active: null,
|
|
351
|
+
custom: [],
|
|
352
|
+
removed: [],
|
|
353
|
+
order: []
|
|
354
|
+
};
|
|
355
|
+
/**
|
|
356
|
+
* Resolves the list of themes: the configured theme first, then the presets
|
|
357
|
+
* (minus the ones that would only repeat the configured theme), then the
|
|
358
|
+
* user's own themes — rearranged into the order the user dragged them into,
|
|
359
|
+
* with themes that order does not know about kept in that default order after
|
|
360
|
+
* the ones it does. Removed themes are set aside so they can be restored, and
|
|
361
|
+
* the active theme falls back to the configured one when its slug no longer
|
|
362
|
+
* resolves.
|
|
363
|
+
*
|
|
364
|
+
* @internal
|
|
365
|
+
*/
|
|
366
|
+
function resolveThemes(state, baseOptions) {
|
|
367
|
+
let config = {
|
|
368
|
+
slug: CONFIG_SLUG,
|
|
369
|
+
title: "Studio config",
|
|
370
|
+
options: baseOptions,
|
|
371
|
+
source: "config"
|
|
372
|
+
}, all = [config];
|
|
373
|
+
for (let preset of presets) sameOptions(preset.options, baseOptions) || all.push({
|
|
374
|
+
slug: preset.slug,
|
|
375
|
+
title: preset.title,
|
|
376
|
+
options: preset.options,
|
|
377
|
+
source: "preset"
|
|
378
|
+
});
|
|
379
|
+
for (let theme of state.custom) all.push({
|
|
380
|
+
slug: theme.slug,
|
|
381
|
+
title: theme.title,
|
|
382
|
+
options: theme.options,
|
|
383
|
+
source: "custom",
|
|
384
|
+
...theme.palette ? { palette: theme.palette } : {}
|
|
385
|
+
});
|
|
386
|
+
let position = new Map(state.order.map((slug, index) => [slug, index])), rank = (theme) => position.get(theme.slug) ?? state.order.length;
|
|
387
|
+
all.sort((a, b) => rank(a) - rank(b));
|
|
388
|
+
let removedSlugs = new Set(state.removed), themes = all.filter((theme) => !removedSlugs.has(theme.slug));
|
|
389
|
+
return {
|
|
390
|
+
themes,
|
|
391
|
+
removed: all.filter((theme) => removedSlugs.has(theme.slug)),
|
|
392
|
+
active: themes.find((theme) => theme.slug === state.active) ?? config
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Creates a custom theme with a slug that cannot collide with the presets or
|
|
397
|
+
* the configured theme.
|
|
398
|
+
*
|
|
399
|
+
* @internal
|
|
400
|
+
*/
|
|
401
|
+
function createCustomTheme(title, options, palette) {
|
|
402
|
+
return {
|
|
403
|
+
slug: `custom-${randomId()}`,
|
|
404
|
+
title,
|
|
405
|
+
options,
|
|
406
|
+
...palette ? { palette } : {}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/** The title of a duplicated theme @internal */
|
|
410
|
+
function duplicateTitle(title) {
|
|
411
|
+
return `${displayTitle(title)} copy`;
|
|
412
|
+
}
|
|
413
|
+
/** The title to show for a theme, falling back when the user cleared it @internal */
|
|
414
|
+
function displayTitle(title) {
|
|
415
|
+
return title.trim() || "Untitled theme";
|
|
416
|
+
}
|
|
417
|
+
function randomId() {
|
|
418
|
+
return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
419
|
+
}
|
|
420
|
+
export { titleFromFileName as _, duplicateTitle as a, minimizeOptions as c, IMAGE_PALETTE_VARIANTS as d, applyImagePalette as f, pickLuckyVariant as g, optionsFromImagePalette as h, displayTitle as i, IMAGE_PALETTE_KEYS as l, extractImagePalette as m, UNTITLED_THEME as n, initialThemerState as o, currentImageVariant as p, createCustomTheme as r, resolveThemes as s, CONFIG_SLUG as t, IMAGE_PALETTE_TITLES as u, ThemerContext as v, useThemer as y };
|
|
421
|
+
|
|
422
|
+
//# sourceMappingURL=ResizableSidebar.css.ts.vanilla-M9EuPu-q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResizableSidebar.css.ts.vanilla-M9EuPu-q.js","names":[],"sources":["../src/tool/context.ts","../src/tool/imagePalette.ts","../src/tool/options.ts","../src/tool/themes.ts"],"sourcesContent":["import {createContext, useContext} from 'react'\n\nimport {BuildThemeOptions} from '../theme/options'\nimport {ThemerEvent} from './machine'\nimport {ThemerTheme} from './themes'\n\n/**\n * The flow the sidebar is in: picking a theme from the list, editing one of\n * the user's own themes, or restoring removed ones.\n *\n * @internal\n */\nexport type ThemerView =\n | {name: 'list'}\n | {\n name: 'edit'\n slug: string\n /** Whether the title input should take focus, for themes that were just created */\n focusTitle: boolean\n }\n | {name: 'removed'}\n\n/** @internal */\nexport interface ThemerContextValue {\n /** The theme options the Studio's configured theme was generated from */\n baseOptions: BuildThemeOptions\n /** The themes to pick from, in list order */\n themes: ThemerTheme[]\n /** The removed themes, which can be restored */\n removed: ThemerTheme[]\n /** The applied theme */\n active: ThemerTheme\n /** Object URLs of the images themes took their palette from this session, by slug */\n images: Record<string, string>\n view: ThemerView\n /** Whether the themer sidebar is open */\n open: boolean\n /** Whether the sidebar is on its way to opening for the first time, pending while its code loads */\n loading: boolean\n /** Whether the Studio shows twice side by side, in light and dark */\n split: boolean\n /** Whether the Studio is on a small screen, where the sidebar covers it and the split preview stacks */\n mobile: boolean\n /** The height of the Studio navbar, which the sidebar's header matches — `null` until it has rendered */\n navbarHeight: number | null\n /** Sends an event to the themer machine */\n send: (event: ThemerEvent) => void\n}\n\n/** @internal */\nexport const ThemerContext = createContext<ThemerContextValue | null>(null)\n\n/** @internal */\nexport function useThemer(): ThemerContextValue {\n const context = useContext(ThemerContext)\n\n if (!context) {\n throw new Error('useThemer must be used within the `themerTool` plugin')\n }\n\n return context\n}\n","import {type ThemeColorSchemeKey} from '@sanity/ui/theme'\n\nimport {hexToHsl, hslToHex, relativeLuminance} from '../theme/hsl'\nimport {BuildThemeOptions, DEFAULT_BACKGROUND, SCHEMES, SchemeThemeOptions} from '../theme/options'\n\n/**\n * The swatches of an image palette — the same set the Sanity image pipeline\n * derives for uploaded assets (`metadata.palette`), extracted on device here.\n *\n * @internal\n */\nexport type ImagePaletteKey =\n | 'dominant'\n | 'vibrant'\n | 'lightVibrant'\n | 'darkVibrant'\n | 'muted'\n | 'lightMuted'\n | 'darkMuted'\n\n/**\n * An image palette: a hex color per swatch, or `null` when the image has no\n * color that qualifies for it.\n *\n * @internal\n */\nexport type ImagePalette = Record<ImagePaletteKey, string | null>\n\n/** @internal */\nexport const IMAGE_PALETTE_KEYS: ImagePaletteKey[] = [\n 'vibrant',\n 'lightVibrant',\n 'darkVibrant',\n 'muted',\n 'lightMuted',\n 'darkMuted',\n 'dominant',\n]\n\n/**\n * The swatches a theme can be built around — every swatch but the dominant\n * one, which only repeats one of them.\n *\n * @internal\n */\nexport type ImagePaletteVariant = Exclude<ImagePaletteKey, 'dominant'>\n\n/** The variants in the order the tool offers them @internal */\nexport const IMAGE_PALETTE_VARIANTS: ImagePaletteVariant[] = [\n 'muted',\n 'vibrant',\n 'lightMuted',\n 'lightVibrant',\n 'darkMuted',\n 'darkVibrant',\n]\n\n/** @internal */\nexport const IMAGE_PALETTE_TITLES: Record<ImagePaletteKey, string> = {\n dominant: 'Dominant',\n vibrant: 'Vibrant',\n lightVibrant: 'Light vibrant',\n darkVibrant: 'Dark vibrant',\n muted: 'Muted',\n lightMuted: 'Light muted',\n darkMuted: 'Dark muted',\n}\n\n/** Pixels the way a canvas hands them out: rows of RGBA bytes @internal */\nexport interface PixelData {\n data: ArrayLike<number>\n width: number\n height: number\n}\n\ninterface Bucket {\n count: number\n r: number\n g: number\n b: number\n}\n\ninterface Candidate {\n hex: string\n population: number\n saturation: number\n lightness: number\n}\n\n/** What a swatch looks for: a lightness and saturation to aim at, within bounds */\ninterface Target {\n lightness: [target: number, min: number, max: number]\n saturation: [target: number, min: number, max: number]\n}\n\n/**\n * The swatch targets of the Vibrant algorithm (the one behind Sanity's image\n * palettes), in the order they get to claim colors.\n */\nconst TARGETS: Array<[key: Exclude<ImagePaletteKey, 'dominant'>, target: Target]> = [\n ['vibrant', {lightness: [0.5, 0.3, 0.7], saturation: [1, 0.35, 1]}],\n ['lightVibrant', {lightness: [0.74, 0.55, 1], saturation: [1, 0.35, 1]}],\n ['darkVibrant', {lightness: [0.26, 0, 0.45], saturation: [1, 0.35, 1]}],\n ['muted', {lightness: [0.5, 0.3, 0.7], saturation: [0.3, 0, 0.4]}],\n ['lightMuted', {lightness: [0.74, 0.55, 1], saturation: [0.3, 0, 0.4]}],\n ['darkMuted', {lightness: [0.26, 0, 0.45], saturation: [0.3, 0, 0.4]}],\n]\n\nconst SATURATION_WEIGHT = 3\nconst LIGHTNESS_WEIGHT = 6.5\nconst POPULATION_WEIGHT = 0.5\n\n/** Pixels this transparent are not part of the picture */\nconst MINIMUM_ALPHA = 125\n\n/** Near-black and near-white pixels are backdrop rather than color */\nconst MINIMUM_LIGHTNESS = 0.05\nconst MAXIMUM_LIGHTNESS = 0.95\n\n/** How many of the 8 bits per channel survive the quantization */\nconst QUANTIZE_BITS = 5\n\n/**\n * Extracts the color palette of an image from its pixels, the way the Vibrant\n * algorithm does: the colors are quantized into a coarse histogram, and each\n * swatch claims the color that comes closest to its target lightness and\n * saturation — weighted by how much of the image it covers — that no other\n * swatch has claimed yet. The dominant swatch is simply the most common\n * color. Runs anywhere pixels come from, so it can run on device.\n *\n * @internal\n */\nexport function extractImagePalette(pixels: PixelData): ImagePalette {\n const candidates = quantize(pixels)\n const palette: ImagePalette = {\n dominant: null,\n vibrant: null,\n lightVibrant: null,\n darkVibrant: null,\n muted: null,\n lightMuted: null,\n darkMuted: null,\n }\n\n if (candidates.length === 0) return palette\n\n const maximumPopulation = Math.max(...candidates.map((candidate) => candidate.population))\n const claimed = new Set<string>()\n\n for (const [key, target] of TARGETS) {\n let best: Candidate | null = null\n let bestScore = -Infinity\n\n for (const candidate of candidates) {\n if (claimed.has(candidate.hex) || !matchesTarget(candidate, target)) continue\n\n const score = scoreCandidate(candidate, target, maximumPopulation)\n\n if (score > bestScore) {\n best = candidate\n bestScore = score\n }\n }\n\n if (best) {\n palette[key] = best.hex\n claimed.add(best.hex)\n }\n }\n\n palette.dominant = candidates.reduce((most, candidate) =>\n candidate.population > most.population ? candidate : most,\n ).hex\n\n return palette\n}\n\n/**\n * Turns the pixels into a histogram of quantized colors, each averaged over\n * the pixels it stands for, leaving out transparent pixels and the near-black\n * and near-white ones that are backdrop rather than color.\n */\nfunction quantize(pixels: PixelData): Candidate[] {\n const {data} = pixels\n const buckets = new Map<number, Bucket>()\n const shift = 8 - QUANTIZE_BITS\n const length = Math.min(data.length, pixels.width * pixels.height * 4)\n\n for (let offset = 0; offset + 3 < length; offset += 4) {\n const r = data[offset]\n const g = data[offset + 1]\n const b = data[offset + 2]\n const a = data[offset + 3]\n\n if (a < MINIMUM_ALPHA) continue\n\n const lightness = (Math.max(r, g, b) + Math.min(r, g, b)) / 510\n\n if (lightness < MINIMUM_LIGHTNESS || lightness > MAXIMUM_LIGHTNESS) continue\n\n const key =\n ((r >> shift) << (QUANTIZE_BITS * 2)) | ((g >> shift) << QUANTIZE_BITS) | (b >> shift)\n const bucket = buckets.get(key)\n\n if (bucket) {\n bucket.count++\n bucket.r += r\n bucket.g += g\n bucket.b += b\n } else {\n buckets.set(key, {count: 1, r, g, b})\n }\n }\n\n const candidates: Candidate[] = []\n\n for (const bucket of buckets.values()) {\n const r = Math.round(bucket.r / bucket.count)\n const g = Math.round(bucket.g / bucket.count)\n const b = Math.round(bucket.b / bucket.count)\n const {saturation, lightness} = saturationAndLightness(r, g, b)\n\n candidates.push({hex: rgbToHex(r, g, b), population: bucket.count, saturation, lightness})\n }\n\n return candidates\n}\n\nfunction matchesTarget(candidate: Candidate, target: Target): boolean {\n return (\n candidate.lightness >= target.lightness[1] &&\n candidate.lightness <= target.lightness[2] &&\n candidate.saturation >= target.saturation[1] &&\n candidate.saturation <= target.saturation[2]\n )\n}\n\nfunction scoreCandidate(candidate: Candidate, target: Target, maximumPopulation: number): number {\n return (\n SATURATION_WEIGHT * (1 - Math.abs(candidate.saturation - target.saturation[0])) +\n LIGHTNESS_WEIGHT * (1 - Math.abs(candidate.lightness - target.lightness[0])) +\n POPULATION_WEIGHT * (candidate.population / maximumPopulation)\n )\n}\n\nfunction saturationAndLightness(r: number, g: number, b: number) {\n const max = Math.max(r, g, b) / 255\n const min = Math.min(r, g, b) / 255\n const lightness = (max + min) / 2\n const delta = max - min\n const saturation =\n delta === 0 ? 0 : lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min)\n\n return {saturation, lightness}\n}\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n return `#${[r, g, b].map((channel) => channel.toString(16).padStart(2, '0')).join('')}`\n}\n\n/** Reads `#rgb` and `#rrggbb` alike — stored palettes may hold either */\nfunction hexToRgb(hex: string): [number, number, number] {\n const expanded =\n hex.length === 4 ? `${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex.slice(1)\n\n return [\n parseInt(expanded.slice(0, 2), 16),\n parseInt(expanded.slice(2, 4), 16),\n parseInt(expanded.slice(4, 6), 16),\n ]\n}\n\n/** Mixes `amount` of `color` into `base` */\nfunction tint(base: string, color: string, amount: number): string {\n const [r1, g1, b1] = hexToRgb(base)\n const [r2, g2, b2] = hexToRgb(color)\n\n return rgbToHex(\n Math.round(r1 + (r2 - r1) * amount),\n Math.round(g1 + (g2 - g1) * amount),\n Math.round(b1 + (b2 - b1) * amount),\n )\n}\n\n/** How much of the palette tints each scheme's stock background */\nconst BACKGROUND_TINT = {light: 0.08, dark: 0.3}\n\n/**\n * The WCAG AA contrast for normal text — what a button label needs against\n * the button. Sanity UI labels solid buttons in white on the accent's 500\n * tint in the light scheme and in black on its 400 tint in the dark scheme\n * (lighter than the accent, so an accent that passes against black passes\n * there too).\n */\nconst MINIMUM_ACCENT_CONTRAST = 4.5\nconst BUTTON_LABEL: Record<ThemeColorSchemeKey, string> = {light: '#ffffff', dark: '#000000'}\nconst ACCENT_LIGHTNESS_STEP = 0.01\n\nfunction contrastRatio(a: string, b: string): number {\n const [lighter, darker] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x)\n\n return (lighter + 0.05) / (darker + 0.05)\n}\n\n/**\n * Makes an accent readable as a button in the given scheme: a light scheme\n * accent is darkened until white text on it reaches AA contrast, a dark\n * scheme accent lightened until black text does — as far as the accent's\n * hue and saturation allow. Accents that already pass come back untouched.\n *\n * @internal\n */\nexport function readableAccent(hex: string, scheme: ThemeColorSchemeKey): string {\n const label = BUTTON_LABEL[scheme]\n const {h, s} = hexToHsl(hex)\n let {l} = hexToHsl(hex)\n let current = hex\n\n while (contrastRatio(current, label) < MINIMUM_ACCENT_CONTRAST) {\n const next = scheme === 'light' ? l - ACCENT_LIGHTNESS_STEP : l + ACCENT_LIGHTNESS_STEP\n\n if (next < 0 || next > 1) break\n\n l = next\n current = hslToHex({h, s, l})\n }\n\n return current\n}\n\n/** The accent a variant gives a scheme, made readable on a button */\nfunction variantAccent(\n palette: ImagePalette,\n variant: ImagePaletteVariant | null,\n scheme: ThemeColorSchemeKey,\n): string | null {\n const swatch =\n (variant ? palette[variant] : null) ??\n palette.vibrant ??\n (scheme === 'light' ? palette.darkVibrant : palette.lightVibrant) ??\n palette.dominant\n\n return swatch === null ? null : readableAccent(swatch, scheme)\n}\n\n/**\n * Derives theme options from an image palette: the accent of both schemes is\n * the swatch of the chosen variant — or, without one, the vibrant swatch,\n * each scheme falling back to the vibrant swatch of its own end of the scale —\n * adjusted until it is readable as a button. The muted swatch becomes the text color, and the light and dark muted\n * swatches tint the stock backgrounds of their scheme — enough to carry the\n * image's tone without giving up a usable canvas. Whatever the palette lacks\n * stays at the theme defaults.\n *\n * @internal\n */\nexport function optionsFromImagePalette(\n palette: ImagePalette,\n variant?: ImagePaletteVariant,\n): BuildThemeOptions {\n const options: BuildThemeOptions = {}\n\n const light = schemeFromPalette(\n variantAccent(palette, variant ?? null, 'light'),\n palette.muted ?? palette.darkMuted,\n palette.lightMuted ?? palette.muted ?? palette.lightVibrant,\n 'light',\n )\n const dark = schemeFromPalette(\n variantAccent(palette, variant ?? null, 'dark'),\n palette.muted ?? palette.lightMuted,\n palette.darkMuted ?? palette.muted ?? palette.darkVibrant,\n 'dark',\n )\n\n if (light) options.light = light\n if (dark) options.dark = dark\n\n return options\n}\n\nfunction schemeFromPalette(\n accent: string | null,\n text: string | null,\n backgroundTint: string | null,\n scheme: 'light' | 'dark',\n): SchemeThemeOptions | null {\n const options: SchemeThemeOptions = {}\n\n if (accent) options.accent = accent\n if (text) options.text = text\n if (backgroundTint) {\n options.background = tint(DEFAULT_BACKGROUND[scheme], backgroundTint, BACKGROUND_TINT[scheme])\n }\n\n return Object.keys(options).length > 0 ? options : null\n}\n\n/**\n * Applies an image palette to existing theme options: the colors the palette\n * provides replace the current ones scheme by scheme, and whatever it does\n * not touch — like the contrast — stays as it is.\n *\n * @internal\n */\nexport function applyImagePalette(\n options: BuildThemeOptions,\n palette: ImagePalette,\n variant?: ImagePaletteVariant,\n): BuildThemeOptions {\n const derived = optionsFromImagePalette(palette, variant)\n const next: BuildThemeOptions = {...options}\n\n for (const scheme of SCHEMES) {\n if (derived[scheme]) next[scheme] = {...options[scheme], ...derived[scheme]}\n }\n\n return next\n}\n\n/**\n * The variant a theme is built around: the one whose swatch is the theme's\n * accent, or `null` when the accent comes from somewhere else.\n *\n * @internal\n */\nexport function currentImageVariant(\n options: BuildThemeOptions,\n palette: ImagePalette,\n): ImagePaletteVariant | null {\n const accent = options.light?.accent\n\n if (!accent) return null\n\n return (\n IMAGE_PALETTE_VARIANTS.find(\n (variant) => palette[variant] !== null && variantAccent(palette, variant, 'light') === accent,\n ) ?? null\n )\n}\n\n/**\n * How much a swatch has going for it as an accent: saturation first, then\n * closeness to mid lightness — the range where an accent reads best.\n */\nfunction interest(hex: string): number {\n const {s, l} = hexToHsl(hex)\n\n return 0.1 + 0.55 * s + 0.35 * (1 - Math.abs(l - 0.5) * 2)\n}\n\n/**\n * Picks a variant for the \"I'm feeling lucky\" button: a random one, weighted\n * by how interesting its swatch is as an accent, and never the one the theme\n * already uses while there are others to pick from.\n *\n * @internal\n */\nexport function pickLuckyVariant(\n palette: ImagePalette,\n options: {exclude?: ImagePaletteVariant | null; random?: () => number} = {},\n): ImagePaletteVariant | null {\n const {exclude = null, random = Math.random} = options\n let candidates = IMAGE_PALETTE_VARIANTS.filter((variant) => palette[variant] !== null)\n\n if (candidates.length > 1) {\n candidates = candidates.filter((variant) => variant !== exclude)\n }\n\n if (candidates.length === 0) return null\n\n const weights = candidates.map((variant) => interest(palette[variant]!))\n const total = weights.reduce((sum, weight) => sum + weight, 0)\n let pick = random() * total\n\n for (const [index, variant] of candidates.entries()) {\n pick -= weights[index]\n\n if (pick < 0) return variant\n }\n\n return candidates.at(-1) ?? null\n}\n\n/** A theme title for the image a palette came from, e.g. `sunset-beach.jpg` → `sunset beach` @internal */\nexport function titleFromFileName(name: string): string {\n return (\n name\n .replace(/\\.[^.]+$/, '')\n .replace(/[-_]+/g, ' ')\n .trim() || 'Image theme'\n )\n}\n","import {\n BuildThemeOptions,\n DEFAULT_ACCENT,\n DEFAULT_BACKGROUND,\n DEFAULT_CONTRAST,\n deriveTextColor,\n ResolvedSchemeOptions,\n resolveThemeOptions,\n SCHEMES,\n SchemeThemeOptions,\n} from '../theme/options'\n\n/**\n * Reduces theme options to the minimal object that recreates them through\n * `buildTheme`: in each scheme the accent is dropped when it is the stock\n * one, the text color when it matches the one derived from the accent, and\n * the background and contrast when they match the defaults — and a scheme\n * with nothing left is dropped altogether. Options that boil down to the\n * stock Studio theme reduce to `null` — they need nothing from this package.\n *\n * @internal\n */\nexport function minimizeOptions(options: BuildThemeOptions): BuildThemeOptions | null {\n const resolved = resolveThemeOptions(options)\n const minimized: BuildThemeOptions = {}\n\n for (const scheme of SCHEMES) {\n const values = resolved[scheme]\n const kept: SchemeThemeOptions = {}\n\n if (values.accent !== DEFAULT_ACCENT) kept.accent = values.accent\n if (values.text !== deriveTextColor(values.accent)) kept.text = values.text\n if (values.background !== DEFAULT_BACKGROUND[scheme]) kept.background = values.background\n if (values.contrast !== DEFAULT_CONTRAST) kept.contrast = values.contrast\n\n if (Object.keys(kept).length > 0) minimized[scheme] = kept\n }\n\n return Object.keys(minimized).length === 0 ? null : minimized\n}\n\n/**\n * Whether two sets of theme options generate the same theme, comparing their\n * resolved forms so that derived and explicit values (and hex casing) don't\n * matter.\n *\n * @internal\n */\nexport function sameOptions(a: BuildThemeOptions, b: BuildThemeOptions): boolean {\n const left = resolveThemeOptions(a)\n const right = resolveThemeOptions(b)\n\n return SCHEMES.every((scheme) => sameScheme(left[scheme], right[scheme]))\n}\n\nfunction sameScheme(a: ResolvedSchemeOptions, b: ResolvedSchemeOptions): boolean {\n return (\n a.accent === b.accent &&\n a.text === b.text &&\n a.background === b.background &&\n a.contrast === b.contrast\n )\n}\n","import {BuildThemeOptions} from '../theme/options'\nimport {presets} from '../theme/presets'\nimport {ImagePalette} from './imagePalette'\nimport {sameOptions} from './options'\n\n/**\n * Where a theme in the themer list comes from: the theme the Studio config\n * was generated from, one of the presets defined in code, or one the user\n * added in the tool. Only custom themes are editable — the others are\n * duplicated into a custom theme first.\n */\ntype ThemerThemeSource = 'config' | 'preset' | 'custom'\n\n/** A theme the themer tool lists @internal */\nexport interface ThemerTheme {\n slug: string\n title: string\n options: BuildThemeOptions\n source: ThemerThemeSource\n /** The palette of the image the theme's colors were taken from, for custom themes */\n palette?: ImagePalette\n}\n\n/** A theme the user added in the tool, as persisted @internal */\nexport interface CustomTheme {\n slug: string\n title: string\n options: BuildThemeOptions\n /** The palette of the image the theme's colors were taken from */\n palette?: ImagePalette\n}\n\n/** The persisted themer state @internal */\nexport interface ThemerState {\n /**\n * The slug of the applied theme — `null` applies nothing on top of the\n * Studio's configured theme\n */\n active: string | null\n /** The themes the user added */\n custom: CustomTheme[]\n /** The slugs of the themes the user removed from the list */\n removed: string[]\n /**\n * The slugs of the themes in the order the user arranged them — themes not\n * in it (new presets, themes added since) follow in their default order\n */\n order: string[]\n}\n\n/** The slug of the theme the Studio config was generated from @internal */\nexport const CONFIG_SLUG = 'config'\n\n/** The title of the theme the Studio config was generated from */\nconst CONFIG_TITLE = 'Studio config'\n\n/** The title new themes start out with @internal */\nexport const UNTITLED_THEME = 'Untitled theme'\n\n/** @internal */\nexport const initialThemerState: ThemerState = {active: null, custom: [], removed: [], order: []}\n\n/** The themes the tool works with, derived from the persisted state @internal */\nexport interface ResolvedThemes {\n /** The themes to pick from, in list order */\n themes: ThemerTheme[]\n /** The removed themes, which can be restored */\n removed: ThemerTheme[]\n /** The applied theme */\n active: ThemerTheme\n}\n\n/**\n * Resolves the list of themes: the configured theme first, then the presets\n * (minus the ones that would only repeat the configured theme), then the\n * user's own themes — rearranged into the order the user dragged them into,\n * with themes that order does not know about kept in that default order after\n * the ones it does. Removed themes are set aside so they can be restored, and\n * the active theme falls back to the configured one when its slug no longer\n * resolves.\n *\n * @internal\n */\nexport function resolveThemes(state: ThemerState, baseOptions: BuildThemeOptions): ResolvedThemes {\n const config: ThemerTheme = {\n slug: CONFIG_SLUG,\n title: CONFIG_TITLE,\n options: baseOptions,\n source: 'config',\n }\n\n const all: ThemerTheme[] = [config]\n\n for (const preset of presets) {\n if (!sameOptions(preset.options, baseOptions)) {\n all.push({slug: preset.slug, title: preset.title, options: preset.options, source: 'preset'})\n }\n }\n\n for (const theme of state.custom) {\n all.push({\n slug: theme.slug,\n title: theme.title,\n options: theme.options,\n source: 'custom',\n ...(theme.palette ? {palette: theme.palette} : {}),\n })\n }\n\n const position = new Map(state.order.map((slug, index) => [slug, index]))\n const rank = (theme: ThemerTheme) => position.get(theme.slug) ?? state.order.length\n\n // A stable sort: the themes the order does not know about keep their default order\n all.sort((a, b) => rank(a) - rank(b))\n\n const removedSlugs = new Set(state.removed)\n const themes = all.filter((theme) => !removedSlugs.has(theme.slug))\n const removed = all.filter((theme) => removedSlugs.has(theme.slug))\n const active = themes.find((theme) => theme.slug === state.active) ?? config\n\n return {themes, removed, active}\n}\n\n/**\n * Creates a custom theme with a slug that cannot collide with the presets or\n * the configured theme.\n *\n * @internal\n */\nexport function createCustomTheme(\n title: string,\n options: BuildThemeOptions,\n palette?: ImagePalette,\n): CustomTheme {\n return {slug: `custom-${randomId()}`, title, options, ...(palette ? {palette} : {})}\n}\n\n/** The title of a duplicated theme @internal */\nexport function duplicateTitle(title: string): string {\n return `${displayTitle(title)} copy`\n}\n\n/** The title to show for a theme, falling back when the user cleared it @internal */\nexport function displayTitle(title: string): string {\n return title.trim() || UNTITLED_THEME\n}\n\nfunction randomId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID()\n }\n\n return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`\n}\n"],"mappings":"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"}
|