@driftime/sanity-plugin-font 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +492 -2
  2. package/dist/catalog-BVw1r_SG.js +183 -0
  3. package/dist/catalog-BVw1r_SG.js.map +1 -0
  4. package/dist/catalog-KAqAIjzw.js +96 -0
  5. package/dist/catalog-KAqAIjzw.js.map +1 -0
  6. package/dist/catalog.d.ts +29 -0
  7. package/dist/catalog.d.ts.map +1 -0
  8. package/dist/catalog.js +10 -0
  9. package/dist/catalog.js.map +1 -0
  10. package/dist/family-BAGnEjZg.js +36 -0
  11. package/dist/family-BAGnEjZg.js.map +1 -0
  12. package/dist/font-styles.d.ts +21 -0
  13. package/dist/font-styles.d.ts.map +1 -0
  14. package/dist/font-styles.js +43 -0
  15. package/dist/font-styles.js.map +1 -0
  16. package/dist/index.d.ts +76 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +2364 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/names.d.ts +8 -0
  21. package/dist/names.d.ts.map +1 -0
  22. package/dist/names.js +0 -0
  23. package/dist/next-loader.d.ts +19 -0
  24. package/dist/next-loader.d.ts.map +1 -0
  25. package/dist/next-loader.js +28 -0
  26. package/dist/next-loader.js.map +1 -0
  27. package/dist/next.d.ts +42 -0
  28. package/dist/next.d.ts.map +1 -0
  29. package/dist/next.js +48 -0
  30. package/dist/next.js.map +1 -0
  31. package/dist/render.d.ts +12 -0
  32. package/dist/render.d.ts.map +1 -0
  33. package/dist/render.js +183 -0
  34. package/dist/render.js.map +1 -0
  35. package/dist/resolve-Bkp0dEmr.d.ts +81 -0
  36. package/dist/resolve-Bkp0dEmr.d.ts.map +1 -0
  37. package/dist/types-DZl90ROd.d.ts +106 -0
  38. package/dist/types-DZl90ROd.d.ts.map +1 -0
  39. package/dist/types-DheP5tax.js +27 -0
  40. package/dist/types-DheP5tax.js.map +1 -0
  41. package/dist/utils-BV38iODp.js +29 -0
  42. package/dist/utils-BV38iODp.js.map +1 -0
  43. package/dist/vite.d.ts +61 -0
  44. package/dist/vite.d.ts.map +1 -0
  45. package/dist/vite.js +53 -0
  46. package/dist/vite.js.map +1 -0
  47. package/package.json +120 -4
package/dist/index.js ADDED
@@ -0,0 +1,2364 @@
1
+ import { a as defaultStyles, c as logger, i as defaultSources, l as pluginName, n as parseWeights, o as defaultSubsets, r as apiVersion, s as defaultWeights, t as parseCatalog } from "./catalog-KAqAIjzw.js";
2
+ import { n as isRecord, t as isDefined } from "./utils-BV38iODp.js";
3
+ import { n as createStylesheetUrl, t as getFamilyName } from "./family-BAGnEjZg.js";
4
+ import { a as fontStyles, i as fontSources, o as fontTypeName, r as fontFileTypeName, s as fontWeights, t as fontCategories } from "./types-DheP5tax.js";
5
+ import { ChangeIndicator, FieldActionsProvider, FieldActionsResolver, FormField, defineArrayMember, defineField, definePlugin, defineType, getPublishedId, insert, set, setIfMissing, unset, useClient, useFormValue } from "sanity";
6
+ import { c } from "react/compiler-runtime";
7
+ import { createContext, createElement, useContext, useEffect, useRef, useState } from "react";
8
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
+ import { Badge, Box, Button, Card, Dialog, Flex, Grid, Select, Spinner, Stack, Tab, TabList, TabPanel, Text, TextInput } from "@sanity/ui";
10
+ import { randomKey } from "@sanity/util/content";
11
+ import { BinaryDocumentIcon } from "@sanity/icons/BinaryDocument";
12
+ import { UploadIcon } from "@sanity/icons/Upload";
13
+ import { TrashIcon } from "@sanity/icons/Trash";
14
+ import { CloseIcon } from "@sanity/icons/Close";
15
+ import { EarthGlobeIcon } from "@sanity/icons/EarthGlobe";
16
+ import { WarningOutlineIcon } from "@sanity/icons/WarningOutline";
17
+ import { SearchIcon } from "@sanity/icons/Search";
18
+ import { fontCatalog } from "@driftime/sanity-plugin-font/catalog";
19
+ /**
20
+ * Checks whether a form node has members, as objects and arrays of objects do.
21
+ *
22
+ * @param node - The form node to check.
23
+ * @returns True if the node has members.
24
+ */
25
+ function hasMembers(node) {
26
+ return "members" in node;
27
+ }
28
+ /**
29
+ * Collects the validation on a form node and everything inside it.
30
+ *
31
+ * @param node - The form node.
32
+ * @returns Every validation result on the node or inside it.
33
+ */
34
+ function collectNodeValidation(node) {
35
+ return [...node.validation, ...hasMembers(node) ? collectMemberValidation(node.members) : []];
36
+ }
37
+ /**
38
+ * Collects the validation anywhere inside a field's members. The form only passes a field the
39
+ * validation at its own path, so a field that hides its members would otherwise show none of theirs.
40
+ *
41
+ * @param members - The members.
42
+ * @returns Every validation result on the members or inside them.
43
+ */
44
+ function collectMemberValidation(members) {
45
+ return members.flatMap((member) => member.kind === "field" ? collectNodeValidation(member.field) : member.kind === "fieldSet" ? collectMemberValidation(member.fieldSet.members) : member.kind === "item" ? collectNodeValidation(member.item) : []);
46
+ }
47
+ function Field(t0) {
48
+ let $ = c(40), { actions, schemaType, title, description, presence, validation, level, inputId, path, changed, children, inputProps } = t0, t1;
49
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t1 = [], $[0] = t1) : t1 = $[0];
50
+ let [headerActions, setHeaderActions] = useState(t1), t2;
51
+ $[1] === Symbol.for("react.memo_cache_sentinel") ? (t2 = ["_id"], $[1] = t2) : t2 = $[1];
52
+ let documentId = useFormValue(t2), fieldValidation, focused, t3;
53
+ if ($[2] !== actions || $[3] !== documentId || $[4] !== inputProps.focused || $[5] !== inputProps.members || $[6] !== path || $[7] !== schemaType || $[8] !== validation) {
54
+ let publishedId = typeof documentId == "string" ? getPublishedId(documentId) : void 0;
55
+ focused = inputProps.focused === !0;
56
+ let t4;
57
+ $[12] !== inputProps.members || $[13] !== validation ? (t4 = [...validation, ...collectMemberValidation(inputProps.members)], $[12] = inputProps.members, $[13] = validation, $[14] = t4) : t4 = $[14], fieldValidation = t4, t3 = isDefined(publishedId) && isDefined(actions) && /* @__PURE__ */ jsx(FieldActionsResolver, {
58
+ actions,
59
+ documentId: publishedId,
60
+ documentType: schemaType.name,
61
+ onActions: setHeaderActions,
62
+ path,
63
+ schemaType
64
+ }), $[2] = actions, $[3] = documentId, $[4] = inputProps.focused, $[5] = inputProps.members, $[6] = path, $[7] = schemaType, $[8] = validation, $[9] = fieldValidation, $[10] = focused, $[11] = t3;
65
+ } else fieldValidation = $[9], focused = $[10], t3 = $[11];
66
+ let t4;
67
+ $[15] !== changed || $[16] !== children || $[17] !== focused || $[18] !== path ? (t4 = /* @__PURE__ */ jsx(ChangeIndicator, {
68
+ path,
69
+ hasFocus: focused,
70
+ isChanged: changed,
71
+ children
72
+ }), $[15] = changed, $[16] = children, $[17] = focused, $[18] = path, $[19] = t4) : t4 = $[19];
73
+ let t5;
74
+ $[20] !== description || $[21] !== fieldValidation || $[22] !== headerActions || $[23] !== inputId || $[24] !== inputProps.readOnly || $[25] !== level || $[26] !== path || $[27] !== presence || $[28] !== schemaType.deprecated || $[29] !== t4 || $[30] !== title ? (t5 = /* @__PURE__ */ jsx(FormField, {
75
+ __unstable_headerActions: headerActions,
76
+ __unstable_presence: presence,
77
+ description,
78
+ inputId,
79
+ level,
80
+ title,
81
+ validation: fieldValidation,
82
+ deprecated: schemaType.deprecated,
83
+ path,
84
+ readOnly: inputProps.readOnly,
85
+ children: t4
86
+ }), $[20] = description, $[21] = fieldValidation, $[22] = headerActions, $[23] = inputId, $[24] = inputProps.readOnly, $[25] = level, $[26] = path, $[27] = presence, $[28] = schemaType.deprecated, $[29] = t4, $[30] = title, $[31] = t5) : t5 = $[31];
87
+ let t6;
88
+ $[32] !== focused || $[33] !== headerActions || $[34] !== path || $[35] !== t5 ? (t6 = /* @__PURE__ */ jsx(FieldActionsProvider, {
89
+ actions: headerActions,
90
+ focused,
91
+ path,
92
+ children: t5
93
+ }), $[32] = focused, $[33] = headerActions, $[34] = path, $[35] = t5, $[36] = t6) : t6 = $[36];
94
+ let t7;
95
+ return $[37] !== t3 || $[38] !== t6 ? (t7 = /* @__PURE__ */ jsxs(Fragment$1, { children: [t3, t6] }), $[37] = t3, $[38] = t6, $[39] = t7) : t7 = $[39], t7;
96
+ }
97
+ function Hidden() {
98
+ return null;
99
+ }
100
+ /** Default props for the plugin's own icons. Icons from `@sanity/icons` already match the Studio. */
101
+ const defaultIconProps = {
102
+ width: "1em",
103
+ height: "1em",
104
+ strokeWidth: 1.5
105
+ };
106
+ /**
107
+ * Wraps an SVG icon component for use in the Studio.
108
+ *
109
+ * @param icon - The icon to wrap.
110
+ * @param props - Props that override the Studio icon defaults.
111
+ * @returns An icon component.
112
+ */
113
+ function createSanityIcon(icon, props) {
114
+ let Icon = icon;
115
+ function SanityIcon() {
116
+ return createElement(Icon, {
117
+ ...defaultIconProps,
118
+ ...props
119
+ });
120
+ }
121
+ return SanityIcon;
122
+ }
123
+ function DropZone(t0) {
124
+ let $ = c(28), onFiles, props, readOnly;
125
+ $[0] === t0 ? (onFiles = $[1], props = $[2], readOnly = $[3]) : ({readOnly, onFiles, ...props} = t0, $[0] = t0, $[1] = onFiles, $[2] = props, $[3] = readOnly);
126
+ let [dragging, setDragging] = useState(!1), input = useRef(null), t1;
127
+ $[4] === readOnly ? t1 = $[5] : (t1 = function handleDragOver(event) {
128
+ readOnly !== !0 && (event.preventDefault(), setDragging(!0));
129
+ }, $[4] = readOnly, $[5] = t1);
130
+ let handleDragOver = t1, t2;
131
+ $[6] !== onFiles || $[7] !== readOnly ? (t2 = function handleDrop(event_0) {
132
+ event_0.preventDefault(), setDragging(!1), readOnly !== !0 && onFiles([...event_0.dataTransfer.files]);
133
+ }, $[6] = onFiles, $[7] = readOnly, $[8] = t2) : t2 = $[8];
134
+ let handleDrop = t2, t3 = readOnly === !0 ? "transparent" : "inherit", t4;
135
+ $[9] === Symbol.for("react.memo_cache_sentinel") ? (t4 = () => {
136
+ setDragging(!1);
137
+ }, $[9] = t4) : t4 = $[9];
138
+ let t5;
139
+ $[10] === Symbol.for("react.memo_cache_sentinel") ? (t5 = /* @__PURE__ */ jsx(Text, {
140
+ size: 1,
141
+ muted: !0,
142
+ children: /* @__PURE__ */ jsx(BinaryDocumentIcon, {})
143
+ }), $[10] = t5) : t5 = $[10];
144
+ let t6 = dragging ? "Drop to upload files" : "Drag WOFF2 or WOFF files here", t7;
145
+ $[11] === t6 ? t7 = $[12] : (t7 = /* @__PURE__ */ jsxs(Flex, {
146
+ flex: 1,
147
+ align: "center",
148
+ gap: 3,
149
+ paddingLeft: 1,
150
+ children: [t5, /* @__PURE__ */ jsx(Text, {
151
+ size: 1,
152
+ muted: !0,
153
+ children: t6
154
+ })]
155
+ }), $[11] = t6, $[12] = t7);
156
+ let t8;
157
+ $[13] === Symbol.for("react.memo_cache_sentinel") ? (t8 = () => {
158
+ input.current?.click();
159
+ }, $[13] = t8) : t8 = $[13];
160
+ let t9;
161
+ $[14] === readOnly ? t9 = $[15] : (t9 = /* @__PURE__ */ jsx(Button, {
162
+ type: "button",
163
+ mode: "bleed",
164
+ icon: UploadIcon,
165
+ text: "Upload",
166
+ disabled: readOnly,
167
+ onClick: t8
168
+ }), $[14] = readOnly, $[15] = t9);
169
+ let t10;
170
+ $[16] === onFiles ? t10 = $[17] : (t10 = /* @__PURE__ */ jsx("input", {
171
+ ref: input,
172
+ type: "file",
173
+ accept: ".woff2,.woff",
174
+ multiple: !0,
175
+ hidden: !0,
176
+ onChange: (event_1) => {
177
+ onFiles([...event_1.currentTarget.files ?? []]), event_1.currentTarget.value = "";
178
+ }
179
+ }), $[16] = onFiles, $[17] = t10);
180
+ let t11;
181
+ $[18] !== t10 || $[19] !== t7 || $[20] !== t9 ? (t11 = /* @__PURE__ */ jsxs(Flex, {
182
+ align: "center",
183
+ gap: 4,
184
+ justify: "space-between",
185
+ children: [
186
+ t7,
187
+ t9,
188
+ t10
189
+ ]
190
+ }), $[18] = t10, $[19] = t7, $[20] = t9, $[21] = t11) : t11 = $[21];
191
+ let t12;
192
+ return $[22] !== handleDragOver || $[23] !== handleDrop || $[24] !== props || $[25] !== t11 || $[26] !== t3 ? (t12 = /* @__PURE__ */ jsx(Card, {
193
+ tone: t3,
194
+ border: !0,
195
+ paddingX: 3,
196
+ paddingY: 2,
197
+ radius: 2,
198
+ onDragOver: handleDragOver,
199
+ onDragLeave: t4,
200
+ onDrop: handleDrop,
201
+ ...props,
202
+ children: t11
203
+ }), $[22] = handleDragOver, $[23] = handleDrop, $[24] = props, $[25] = t11, $[26] = t3, $[27] = t12) : t12 = $[27], t12;
204
+ }
205
+ /** Reads each `@font-face` rule in a Google Fonts stylesheet. */
206
+ const fontFacePattern = /@font-face\s*\{(?<body>[^}]*)\}/gu;
207
+ /**
208
+ * Reads the files, weights, and styles out of a Google Fonts stylesheet.
209
+ *
210
+ * @param css - The stylesheet.
211
+ * @returns The files.
212
+ */
213
+ function parseStylesheet(css) {
214
+ return [...css.matchAll(fontFacePattern)].flatMap(({ groups }) => {
215
+ let body = groups?.body ?? "", url = /src:\s*url\((?<url>[^)]+)\)/u.exec(body)?.groups?.url, weight = /font-weight:\s*(?<weight>[\d ]+);/u.exec(body)?.groups?.weight, style = /font-style:\s*(?<style>\w+);/u.exec(body)?.groups?.style;
216
+ return isDefined(url) && isDefined(weight) && isDefined(style) ? [{
217
+ url,
218
+ weight,
219
+ style
220
+ }] : [];
221
+ });
222
+ }
223
+ /**
224
+ * Lists the files a sample loads.
225
+ *
226
+ * @param sample - The sample.
227
+ * @returns The files, or undefined when Google Fonts couldn't be reached.
228
+ */
229
+ async function listFaceSources(sample) {
230
+ if (sample.source === "upload") return sample.files.map(({ url, weight, maxWeight, style }) => ({
231
+ url,
232
+ weight: isDefined(maxWeight) && maxWeight > weight ? `${weight} ${maxWeight}` : `${weight}`,
233
+ style
234
+ }));
235
+ let { family, weights, styles, text } = sample, response = await fetch(createStylesheetUrl(family, weights, styles, "swap", text));
236
+ if (response.ok) return parseStylesheet(await response.text());
237
+ }
238
+ /**
239
+ * Loads a sample's files and registers them with the document under a name of their own, so a cut-down font made
240
+ * for a preview never stands in for the real one anywhere else in the Studio.
241
+ *
242
+ * @param sample - The sample.
243
+ * @param name - Name to register the files under.
244
+ * @returns The name, or undefined when the files couldn't be loaded.
245
+ */
246
+ async function loadSample(sample, name) {
247
+ try {
248
+ let sources = await listFaceSources(sample);
249
+ if (!isDefined(sources)) return;
250
+ let faces = await Promise.all(sources.map(async ({ url, weight, style }) => {
251
+ let face = new FontFace(name, `url(${url})`, {
252
+ weight,
253
+ style
254
+ });
255
+ return await face.load(), face;
256
+ }));
257
+ for (let face of faces) document.fonts.add(face);
258
+ return name;
259
+ } catch {
260
+ return;
261
+ }
262
+ }
263
+ /** Sample requests shared by every field in the session, keyed by what they load, since a font file never changes. */
264
+ const sampleRequests = /* @__PURE__ */ new Map();
265
+ /** Number of samples requested so far, which names each one uniquely even after a failed one is retried. */
266
+ let sampleCount = 0;
267
+ /** Names of the samples that have loaded, keyed like the requests, so a sample shown again appears straight away. */
268
+ const loadedSamples = /* @__PURE__ */ new Map();
269
+ /**
270
+ * Builds the key a sample is requested under.
271
+ *
272
+ * @param sample - The sample.
273
+ * @returns The key.
274
+ */
275
+ function getSampleKey(sample) {
276
+ return JSON.stringify(sample);
277
+ }
278
+ /**
279
+ * Loads a sample, reusing a request already made for the same files. A request that fails is forgotten, so a file that
280
+ * was still uploading loads the next time it's shown.
281
+ *
282
+ * @param sample - The sample.
283
+ * @returns Name the files are registered under, or undefined when they couldn't be loaded.
284
+ */
285
+ async function requestSample(sample) {
286
+ let key = getSampleKey(sample), request = sampleRequests.get(key);
287
+ isDefined(request) || (sampleCount += 1, request = loadSample(sample, `font-sample-${sampleCount}`), sampleRequests.set(key, request));
288
+ let name = await request;
289
+ return isDefined(name) ? loadedSamples.set(key, name) : sampleRequests.delete(key), name;
290
+ }
291
+ /**
292
+ * Reads a sample that has already loaded, without waiting.
293
+ *
294
+ * @param key - The sample's key.
295
+ * @returns Name the files are registered under, or undefined when the sample hasn't loaded.
296
+ */
297
+ function readLoadedSample(key) {
298
+ return loadedSamples.get(key);
299
+ }
300
+ /**
301
+ * Loads a font sample. A sample that has already loaded is returned straight away, so it never flashes in the
302
+ * Studio's own font when it's shown again.
303
+ *
304
+ * @param sample - The sample, or undefined when there's nothing to show.
305
+ * @returns Name to set as the `font-family`, or undefined until the sample has loaded, and whether it's still loading.
306
+ */
307
+ function useFontSample(sample) {
308
+ let $ = c(14), [settled, setSettled] = useState(), key, t0;
309
+ $[0] === sample ? (key = $[1], t0 = $[2]) : (key = isDefined(sample) ? getSampleKey(sample) : void 0, t0 = isDefined(key) ? readLoadedSample(key) : void 0, $[0] = sample, $[1] = key, $[2] = t0);
310
+ let cached = t0, t1, t2;
311
+ $[3] !== key || $[4] !== sample ? (t1 = () => {
312
+ if (!isDefined(sample) || !isDefined(key) || isDefined(readLoadedSample(key))) return;
313
+ let active = !0;
314
+ return (async function load(wanted, wantedKey) {
315
+ let name = await requestSample(wanted);
316
+ active && setSettled({
317
+ key: wantedKey,
318
+ name
319
+ });
320
+ })(sample, key), () => {
321
+ active = !1;
322
+ };
323
+ }, t2 = [sample, key], $[3] = key, $[4] = sample, $[5] = t1, $[6] = t2) : (t1 = $[5], t2 = $[6]), useEffect(t1, t2);
324
+ let result = settled?.key === key ? settled : void 0, name_0 = cached ?? result?.name, t3;
325
+ $[7] !== key || $[8] !== name_0 || $[9] !== result ? (t3 = isDefined(key) && !isDefined(name_0) && !isDefined(result), $[7] = key, $[8] = name_0, $[9] = result, $[10] = t3) : t3 = $[10];
326
+ let t4;
327
+ return $[11] !== name_0 || $[12] !== t3 ? (t4 = {
328
+ name: name_0,
329
+ loading: t3
330
+ }, $[11] = name_0, $[12] = t3, $[13] = t4) : t4 = $[13], t4;
331
+ }
332
+ /** Name type designers give each weight. */
333
+ const fontWeightLabels = {
334
+ 100: "Thin",
335
+ 200: "ExtraLight",
336
+ 300: "Light",
337
+ 400: "Regular",
338
+ 500: "Medium",
339
+ 600: "SemiBold",
340
+ 700: "Bold",
341
+ 800: "ExtraBold",
342
+ 900: "Black"
343
+ };
344
+ /**
345
+ * Lists every combination of the weights and styles, lightest first, with each weight's italic after its normal.
346
+ *
347
+ * @param weights - The weights.
348
+ * @param styles - The styles.
349
+ * @returns The combinations.
350
+ */
351
+ function listVariants(weights, styles) {
352
+ return weights.flatMap((weight) => styles.map((style) => ({
353
+ weight,
354
+ style
355
+ })));
356
+ }
357
+ /**
358
+ * Names a weight and style the way type designers do, such as "Bold Italic".
359
+ *
360
+ * @param variant - The weight and style.
361
+ * @returns The name.
362
+ */
363
+ function describeVariant({ weight, style }) {
364
+ let label = fontWeightLabels[weight];
365
+ return style === "normal" ? label : weight === 400 ? "Italic" : `${label} Italic`;
366
+ }
367
+ /**
368
+ * Names the weights and style a file holds, such as "Bold Italic", or "Thin to Black" for a variable font.
369
+ *
370
+ * @param file - The file's weight, heaviest weight, and style.
371
+ * @returns The name.
372
+ */
373
+ function describeFile({ weight, maxWeight, style }) {
374
+ if (!isDefined(weight)) return "Font file";
375
+ if (!isDefined(maxWeight)) return describeVariant({
376
+ weight,
377
+ style: style ?? "normal"
378
+ });
379
+ let range = `${fontWeightLabels[weight]} to ${fontWeightLabels[maxWeight]}`;
380
+ return style === "italic" ? `${range} Italic` : range;
381
+ }
382
+ /**
383
+ * Checks whether a Google family has every weight, style, and character set a field requires.
384
+ *
385
+ * @param family - The family.
386
+ * @param variants - The weights and styles required.
387
+ * @param subsets - The character sets required.
388
+ * @returns Whether the family meets them all.
389
+ */
390
+ function meetsRequirements(family, variants, subsets) {
391
+ let { weights, italics } = family;
392
+ return variants.every(({ weight, style }) => (style === "italic" ? italics : weights).includes(weight)) && subsets.every((subset) => family.subsets.includes(subset));
393
+ }
394
+ /**
395
+ * Checks whether an uploaded file holds a weight and style, counting every weight a variable font file covers.
396
+ *
397
+ * @param file - The file.
398
+ * @param variant - The weight and style.
399
+ * @returns Whether the file holds it.
400
+ */
401
+ function holdsVariant(file, { weight, style }) {
402
+ let { weight: lightest, maxWeight, style: fileStyle } = file;
403
+ return !isDefined(file.asset?._ref) || !isDefined(lightest) || (fileStyle ?? "normal") !== style ? !1 : weight === lightest || isDefined(maxWeight) && weight >= lightest && weight <= maxWeight;
404
+ }
405
+ /**
406
+ * Identifies the weights and style a file holds, so two files holding the same ones can be told apart from the rest.
407
+ *
408
+ * @param file - The file's weight, heaviest weight, and style.
409
+ * @returns The identifier.
410
+ */
411
+ function getCoverageKey({ weight, maxWeight, style }) {
412
+ return `${weight ?? ""}-${maxWeight ?? ""}-${style ?? "normal"}`;
413
+ }
414
+ /**
415
+ * Lists the uploaded files that hold the same weights and style as a file before them.
416
+ *
417
+ * @param files - The uploaded files, in order.
418
+ * @returns The repeated files.
419
+ */
420
+ function findDuplicateFiles(files) {
421
+ let seen = /* @__PURE__ */ new Set();
422
+ return files.filter((file) => {
423
+ if (!isDefined(file.weight)) return !1;
424
+ let coverage = getCoverageKey(file), repeated = seen.has(coverage);
425
+ return seen.add(coverage), repeated;
426
+ });
427
+ }
428
+ /**
429
+ * Lists the weights and styles the uploaded files actually hold, for showing them. A variable font file shows the
430
+ * required weights within its range, or its lightest and heaviest when none are.
431
+ *
432
+ * @param files - The uploaded files.
433
+ * @param required - The weights and styles the field requires.
434
+ * @returns The weights and styles, lightest first, with each weight's italic after its normal.
435
+ */
436
+ function listFileVariants(files, required) {
437
+ let variants = files.flatMap(({ asset, weight, maxWeight, style = "normal" }) => {
438
+ if (!isDefined(asset?._ref) || !isDefined(weight)) return [];
439
+ if (!isDefined(maxWeight)) return [{
440
+ weight,
441
+ style
442
+ }];
443
+ let inRange = required.filter((variant) => variant.style === style && variant.weight >= weight && variant.weight <= maxWeight);
444
+ return isDefined(inRange) ? inRange : [{
445
+ weight,
446
+ style
447
+ }, {
448
+ weight: maxWeight,
449
+ style
450
+ }];
451
+ });
452
+ return [...new Map(variants.map((variant) => [`${variant.weight}-${variant.style}`, variant])).values()].toSorted((first, second) => first.weight - second.weight || +(first.style === "italic") - (second.style === "italic"));
453
+ }
454
+ /**
455
+ * Lists the weights and styles a field requires that no uploaded file holds.
456
+ *
457
+ * @param files - The uploaded files.
458
+ * @param variants - The weights and styles required.
459
+ * @returns The missing weights and styles.
460
+ */
461
+ function listMissingVariants(files, variants) {
462
+ return variants.filter((variant) => !files.some((file) => holdsVariant(file, variant)));
463
+ }
464
+ function FileRow(t0) {
465
+ let $ = c(52), duplicate, filename, maxWeight, onRemove, props, readOnly, style, uploading, url, weight;
466
+ $[0] === t0 ? (duplicate = $[1], filename = $[2], maxWeight = $[3], onRemove = $[4], props = $[5], readOnly = $[6], style = $[7], uploading = $[8], url = $[9], weight = $[10]) : ({url, weight, maxWeight, style, filename, uploading, duplicate, readOnly, onRemove, ...props} = t0, $[0] = t0, $[1] = duplicate, $[2] = filename, $[3] = maxWeight, $[4] = onRemove, $[5] = props, $[6] = readOnly, $[7] = style, $[8] = uploading, $[9] = url, $[10] = weight);
467
+ let t1;
468
+ $[11] !== maxWeight || $[12] !== style || $[13] !== weight ? (t1 = describeFile({
469
+ weight,
470
+ maxWeight,
471
+ style
472
+ }), $[11] = maxWeight, $[12] = style, $[13] = weight, $[14] = t1) : t1 = $[14];
473
+ let label = t1, t2;
474
+ $[15] !== maxWeight || $[16] !== style || $[17] !== url || $[18] !== weight ? (t2 = isDefined(url) && isDefined(weight) ? {
475
+ source: "upload",
476
+ files: [{
477
+ url,
478
+ weight,
479
+ ...isDefined(maxWeight) && { maxWeight },
480
+ style: style ?? "normal"
481
+ }]
482
+ } : void 0, $[15] = maxWeight, $[16] = style, $[17] = url, $[18] = weight, $[19] = t2) : t2 = $[19];
483
+ let { name, loading } = useFontSample(t2), t3 = duplicate ? "caution" : "default", t4;
484
+ $[20] === Symbol.for("react.memo_cache_sentinel") ? (t4 = {
485
+ width: 40,
486
+ flexShrink: 0
487
+ }, $[20] = t4) : t4 = $[20];
488
+ let t5;
489
+ $[21] === name ? t5 = $[22] : (t5 = isDefined(name) ? `"${name}"` : void 0, $[21] = name, $[22] = t5);
490
+ let t6 = +!loading, t7;
491
+ $[23] !== style || $[24] !== t5 || $[25] !== t6 || $[26] !== weight ? (t7 = /* @__PURE__ */ jsx(Box, {
492
+ style: t4,
493
+ children: /* @__PURE__ */ jsx(Text, {
494
+ size: 3,
495
+ align: "center",
496
+ style: {
497
+ fontFamily: t5,
498
+ fontWeight: weight,
499
+ fontStyle: style,
500
+ opacity: t6,
501
+ transition: "opacity 150ms ease-out"
502
+ },
503
+ children: "Aa"
504
+ })
505
+ }), $[23] = style, $[24] = t5, $[25] = t6, $[26] = weight, $[27] = t7) : t7 = $[27];
506
+ let t8;
507
+ $[28] === Symbol.for("react.memo_cache_sentinel") ? (t8 = { minWidth: 0 }, $[28] = t8) : t8 = $[28];
508
+ let t9;
509
+ $[29] === label ? t9 = $[30] : (t9 = /* @__PURE__ */ jsx(Text, {
510
+ size: 1,
511
+ weight: "medium",
512
+ textOverflow: "ellipsis",
513
+ children: label
514
+ }), $[29] = label, $[30] = t9);
515
+ let t10;
516
+ $[31] === filename ? t10 = $[32] : (t10 = isDefined(filename) && /* @__PURE__ */ jsx(Text, {
517
+ size: 1,
518
+ muted: !0,
519
+ textOverflow: "ellipsis",
520
+ translate: "no",
521
+ children: filename
522
+ }), $[31] = filename, $[32] = t10);
523
+ let t11;
524
+ $[33] !== t10 || $[34] !== t9 ? (t11 = /* @__PURE__ */ jsxs(Stack, {
525
+ flex: 1,
526
+ gap: 2,
527
+ style: t8,
528
+ children: [t9, t10]
529
+ }), $[33] = t10, $[34] = t9, $[35] = t11) : t11 = $[35];
530
+ let t12;
531
+ $[36] === duplicate ? t12 = $[37] : (t12 = duplicate && /* @__PURE__ */ jsx(Badge, {
532
+ tone: "caution",
533
+ children: "Duplicate"
534
+ }), $[36] = duplicate, $[37] = t12);
535
+ let t13 = readOnly === !0 || uploading, t14 = uploading ? `Uploading ${label}` : `Remove ${label}`, t15;
536
+ $[38] !== onRemove || $[39] !== t13 || $[40] !== t14 || $[41] !== uploading ? (t15 = /* @__PURE__ */ jsx(Button, {
537
+ type: "button",
538
+ mode: "bleed",
539
+ tone: "critical",
540
+ icon: TrashIcon,
541
+ loading: uploading,
542
+ disabled: t13,
543
+ onClick: onRemove,
544
+ "aria-label": t14
545
+ }), $[38] = onRemove, $[39] = t13, $[40] = t14, $[41] = uploading, $[42] = t15) : t15 = $[42];
546
+ let t16;
547
+ $[43] !== t11 || $[44] !== t12 || $[45] !== t15 || $[46] !== t7 ? (t16 = /* @__PURE__ */ jsxs(Flex, {
548
+ align: "center",
549
+ gap: 3,
550
+ children: [
551
+ t7,
552
+ t11,
553
+ t12,
554
+ t15
555
+ ]
556
+ }), $[43] = t11, $[44] = t12, $[45] = t15, $[46] = t7, $[47] = t16) : t16 = $[47];
557
+ let t17;
558
+ return $[48] !== props || $[49] !== t16 || $[50] !== t3 ? (t17 = /* @__PURE__ */ jsx(Card, {
559
+ padding: 3,
560
+ radius: 2,
561
+ border: !0,
562
+ tone: t3,
563
+ ...props,
564
+ children: t16
565
+ }), $[48] = props, $[49] = t16, $[50] = t3, $[51] = t17) : t17 = $[51], t17;
566
+ }
567
+ const FilesContext = createContext(void 0);
568
+ /**
569
+ * Reads what the font field shares with its files input.
570
+ *
571
+ * @returns A function that fills in the family name and kind of typeface.
572
+ * @throws When used outside the provider.
573
+ */
574
+ function useFilesContext() {
575
+ let filesContext = useContext(FilesContext);
576
+ if (!isDefined(filesContext)) throw Error(logger.format("The files input is only available inside a font field."));
577
+ return filesContext;
578
+ }
579
+ /**
580
+ * Provides a way to fill in the font's family name and kind of typeface to its files input.
581
+ *
582
+ * @returns The provider.
583
+ */
584
+ function FilesProvider(t0) {
585
+ let $ = c(5), { fillDetails, children } = t0, t1;
586
+ $[0] === fillDetails ? t1 = $[1] : (t1 = { fillDetails }, $[0] = fillDetails, $[1] = t1);
587
+ let t2;
588
+ return $[2] !== children || $[3] !== t1 ? (t2 = /* @__PURE__ */ jsx(FilesContext, {
589
+ value: t1,
590
+ children
591
+ }), $[2] = children, $[3] = t1, $[4] = t2) : t2 = $[4], t2;
592
+ }
593
+ /**
594
+ * Looks up the original filenames of uploaded file assets, which the stored references don't carry.
595
+ *
596
+ * @param references - IDs of the assets.
597
+ * @returns Each filename, keyed by asset ID, once they've loaded.
598
+ */
599
+ function useAssetFilenames(references) {
600
+ let $ = c(8), t0;
601
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = { apiVersion }, $[0] = t0) : t0 = $[0];
602
+ let client = useClient(t0), t1;
603
+ $[1] === Symbol.for("react.memo_cache_sentinel") ? (t1 = {}, $[1] = t1) : t1 = $[1];
604
+ let [filenames, setFilenames] = useState(t1), t2;
605
+ $[2] === references ? t2 = $[3] : (t2 = references.toSorted().join(","), $[2] = references, $[3] = t2);
606
+ let key = t2, t3, t4;
607
+ return $[4] !== client || $[5] !== key ? (t3 = () => {
608
+ if (!isDefined(key)) return;
609
+ let active = !0;
610
+ return (async function load(ids) {
611
+ let assets = await client.fetch("*[_id in $ids] { _id, originalFilename }", { ids });
612
+ active && setFilenames(Object.fromEntries(assets.flatMap(_temp$2)));
613
+ })(key.split(",")), () => {
614
+ active = !1;
615
+ };
616
+ }, t4 = [client, key], $[4] = client, $[5] = key, $[6] = t3, $[7] = t4) : (t3 = $[6], t4 = $[7]), useEffect(t3, t4), filenames;
617
+ }
618
+ function _temp$2(t0) {
619
+ let { _id, originalFilename } = t0;
620
+ return isDefined(originalFilename) ? [[_id, originalFilename]] : [];
621
+ }
622
+ /**
623
+ * Builds the address Sanity serves an uploaded file from, given its asset ID.
624
+ *
625
+ * @param reference - ID of the file asset, such as `file-abc123-woff2`.
626
+ * @param projectId - ID of the Sanity project.
627
+ * @param dataset - Name of the dataset.
628
+ * @returns The address, or undefined when the ID isn't a file asset's.
629
+ */
630
+ function createFileUrl(reference, projectId, dataset) {
631
+ let { hash, extension } = reference?.match(/^file-(?<hash>[\da-f]+)-(?<extension>\w+)$/u)?.groups ?? {};
632
+ if (isDefined(hash) && isDefined(extension)) return `https://cdn.sanity.io/files/${projectId}/${dataset}/${hash}.${extension}`;
633
+ }
634
+ /** Tables the details are read from. */
635
+ const wantedTables = /* @__PURE__ */ new Set([
636
+ "OS/2",
637
+ "fvar",
638
+ "name",
639
+ "post"
640
+ ]), knownTags = /* @__PURE__ */ "cmap.head.hhea.hmtx.maxp.name.OS/2.post.cvt .fpgm.glyf.loca.prep.CFF .VORG.EBDT.EBLC.gasp.hdmx.kern.LTSH.PCLT.VDMX.vhea.vmtx.BASE.GDEF.GPOS.GSUB.EBSC.JSTF.MATH.CBDT.CBLC.COLR.CPAL.SVG .sbix.acnt.avar.bdat.bloc.bsln.cvar.fdsc.feat.fmtx.fvar.gvar.hsty.just.lcar.mort.morx.opbd.prop.trak.Zapf.Silf.Glat.Gloc.Feat.Sill".split(".");
641
+ /**
642
+ * Checks whether a value is the browser's decompression.
643
+ *
644
+ * @param value - The value to check.
645
+ * @returns True if the value can be constructed.
646
+ */
647
+ function isDecompressor(value) {
648
+ return typeof value == "function";
649
+ }
650
+ /**
651
+ * Creates the browser's decompression for a format.
652
+ *
653
+ * @param format - The format.
654
+ * @returns The decompression.
655
+ * @throws When the browser has no decompression, or doesn't support the format.
656
+ */
657
+ function createDecompression(format) {
658
+ let Decompression = globalThis.DecompressionStream;
659
+ if (!isDecompressor(Decompression)) throw Error("The browser can't decompress fonts.");
660
+ return new Decompression(format);
661
+ }
662
+ /**
663
+ * Checks whether the browser can decompress Brotli itself.
664
+ *
665
+ * @returns Whether it can.
666
+ */
667
+ function supportsBrotli() {
668
+ try {
669
+ return isDefined(createDecompression("brotli"));
670
+ } catch {
671
+ return !1;
672
+ }
673
+ }
674
+ /**
675
+ * Decompresses bytes, with the browser's own decompression where it can. Chrome and Edge can't decompress Brotli yet,
676
+ * so they load a decoder for it, and only when a WOFF2 file is read.
677
+ *
678
+ * @param data - The compressed bytes.
679
+ * @param format - The compression: zlib for WOFF, or Brotli for WOFF2.
680
+ * @returns The decompressed bytes.
681
+ */
682
+ async function decompress(data, format) {
683
+ if (format === "brotli" && !supportsBrotli()) {
684
+ let { decompress: decode } = await import("brotli/decompress");
685
+ return decode(data);
686
+ }
687
+ let stream = new Blob([new Uint8Array(data)]).stream().pipeThrough(createDecompression(format));
688
+ return new Uint8Array(await new Response(stream).arrayBuffer());
689
+ }
690
+ /**
691
+ * Reads four bytes as a table tag.
692
+ *
693
+ * @param view - The bytes.
694
+ * @param offset - Where the tag starts.
695
+ * @returns The tag.
696
+ */
697
+ function readTag(view, offset) {
698
+ return String.fromCodePoint(view.getUint8(offset), view.getUint8(offset + 1), view.getUint8(offset + 2), view.getUint8(offset + 3));
699
+ }
700
+ /**
701
+ * Finds the wanted tables in a WOFF file, each compressed on its own.
702
+ *
703
+ * @param view - The file.
704
+ * @returns The tables.
705
+ */
706
+ async function readWoffTables(view) {
707
+ let count = view.getUint16(12), entries = Array.from({ length: count }, (_, index) => {
708
+ let entry = 44 + index * 20;
709
+ return {
710
+ tag: readTag(view, entry),
711
+ offset: view.getUint32(entry + 4),
712
+ length: view.getUint32(entry + 8),
713
+ originalLength: view.getUint32(entry + 12)
714
+ };
715
+ }), tables = await Promise.all(entries.filter(({ tag }) => wantedTables.has(tag)).map(async ({ tag, offset, length, originalLength }) => {
716
+ let data = new Uint8Array(view.buffer, view.byteOffset + offset, length), table = length < originalLength ? await decompress(data, "deflate") : data;
717
+ return [tag, new DataView(table.buffer, table.byteOffset, table.byteLength)];
718
+ }));
719
+ return new Map(tables);
720
+ }
721
+ /**
722
+ * Reads a WOFF2 variable-length number, which spends seven bits of each byte on the value.
723
+ *
724
+ * @param view - The file.
725
+ * @param offset - Where the number starts.
726
+ * @returns The number, and where the next value starts.
727
+ */
728
+ function readBase128(view, offset) {
729
+ let value = 0;
730
+ for (let index = 0; index < 5; index += 1) {
731
+ let byte = view.getUint8(offset + index);
732
+ if (value = value * 128 + byte % 128, byte < 128) return {
733
+ value,
734
+ next: offset + index + 1
735
+ };
736
+ }
737
+ throw Error("The number is longer than WOFF2 allows.");
738
+ }
739
+ /**
740
+ * Finds the wanted tables in a WOFF2 file, whose tables are compressed together. The tables read are never
741
+ * transformed, so they're used exactly as they come out.
742
+ *
743
+ * @param view - The file.
744
+ * @returns The tables, or undefined for a file of several fonts.
745
+ */
746
+ async function readWoff2Tables(view) {
747
+ if (readTag(view, 4) === "ttcf") return;
748
+ let count = view.getUint16(12), compressedLength = view.getUint32(20), entries = [], offset = 48;
749
+ for (let index = 0; index < count; index += 1) {
750
+ let flags = view.getUint8(offset), known = knownTags[flags % 64], tag = known ?? readTag(view, offset + 1), version = Math.floor(flags / 64), original = readBase128(view, offset + (isDefined(known) ? 1 : 5)), stored = (tag === "glyf" || tag === "loca" ? version === 0 : version !== 0) ? readBase128(view, original.next) : void 0;
751
+ entries.push({
752
+ tag,
753
+ length: stored?.value ?? original.value
754
+ }), offset = stored?.next ?? original.next;
755
+ }
756
+ let data = await decompress(new Uint8Array(view.buffer, view.byteOffset + offset, compressedLength), "brotli"), tables = /* @__PURE__ */ new Map(), start = 0;
757
+ for (let { tag, length } of entries) wantedTables.has(tag) && tables.set(tag, new DataView(data.buffer, data.byteOffset + start, length)), start += length;
758
+ return tables;
759
+ }
760
+ /**
761
+ * Rounds a weight to the nearest one a font can be loaded in.
762
+ *
763
+ * @param value - The weight.
764
+ * @returns The nearest weight from 100 to 900.
765
+ */
766
+ function roundWeight(value) {
767
+ let rounded = Math.min(900, Math.max(100, Math.round(value / 100) * 100));
768
+ return fontWeights.find((weight) => weight === rounded) ?? 400;
769
+ }
770
+ /**
771
+ * Reads the weight range of a variable font from its axes.
772
+ *
773
+ * @param table - The `fvar` table, or undefined for a font that isn't variable.
774
+ * @returns The lightest and heaviest weights, or undefined when the font has no weight axis.
775
+ */
776
+ function readWeightAxis(table) {
777
+ if (!isDefined(table)) return;
778
+ let axesOffset = table.getUint16(4), axisCount = table.getUint16(8), axisSize = table.getUint16(10);
779
+ for (let index = 0; index < axisCount; index += 1) {
780
+ let axis = axesOffset + index * axisSize;
781
+ if (readTag(table, axis) === "wght") return {
782
+ min: table.getInt32(axis + 4) / 65536,
783
+ max: table.getInt32(axis + 12) / 65536
784
+ };
785
+ }
786
+ }
787
+ /**
788
+ * Reads the kind of typeface from the classifications a font records about itself: the fixed-pitch flag for monospace,
789
+ * then the IBM family class, then the PANOSE classification.
790
+ *
791
+ * @param metrics - The `OS/2` table.
792
+ * @param post - The `post` table, or undefined when the file has none.
793
+ * @returns The kind of typeface, or undefined when the font doesn't say.
794
+ */
795
+ function readCategory(metrics, post) {
796
+ let familyClass = metrics.getUint8(30), panoseFamily = metrics.getUint8(32), panoseSerif = metrics.getUint8(33), panoseProportion = metrics.getUint8(35);
797
+ if (isDefined(post) && post.getUint32(12) !== 0 || panoseFamily === 2 && panoseProportion === 9) return "monospace";
798
+ if (familyClass >= 1 && familyClass <= 7) return "serif";
799
+ if (familyClass === 8) return "sans-serif";
800
+ if (familyClass === 9) return "display";
801
+ if (familyClass === 10 || panoseFamily === 3) return "handwriting";
802
+ if (panoseFamily === 4) return "display";
803
+ if (panoseFamily === 2 && panoseSerif >= 2 && panoseSerif <= 10) return "serif";
804
+ if (panoseFamily === 2 && panoseSerif >= 11 && panoseSerif <= 15) return "sans-serif";
805
+ }
806
+ /**
807
+ * Reads the family name, preferring the typographic family over the legacy one, which can include the weight.
808
+ *
809
+ * @param table - The `name` table, or undefined when the file has none.
810
+ * @returns The family name, or undefined when there isn't one.
811
+ */
812
+ function readFamilyName(table) {
813
+ if (!isDefined(table)) return;
814
+ let count = table.getUint16(2), storage = table.getUint16(4), records = Array.from({ length: count }, (_, index) => {
815
+ let record = 6 + index * 12;
816
+ return {
817
+ platform: table.getUint16(record),
818
+ language: table.getUint16(record + 4),
819
+ name: table.getUint16(record + 6),
820
+ length: table.getUint16(record + 8),
821
+ offset: table.getUint16(record + 10)
822
+ };
823
+ });
824
+ for (let wanted of [16, 1]) {
825
+ let record = records.find(({ platform, language, name }) => platform === 3 && language === 1033 && name === wanted) ?? records.find(({ platform, name }) => platform === 3 && name === wanted) ?? records.find(({ platform, name }) => platform === 1 && name === wanted);
826
+ if (!isDefined(record)) continue;
827
+ let bytes = new Uint8Array(table.buffer, table.byteOffset + storage + record.offset, record.length), name = new TextDecoder(record.platform === 3 ? "utf-16be" : "macintosh").decode(bytes).trim();
828
+ if (isDefined(name)) return name;
829
+ }
830
+ }
831
+ /**
832
+ * Reads a WOFF or WOFF2 file's family name, its weight or, for a variable font, its range of weights, and its style.
833
+ *
834
+ * @param buffer - The file.
835
+ * @returns The details, or undefined when the file isn't a WOFF or WOFF2 font, holds several fonts, or can't be read.
836
+ */
837
+ async function readFontFile(buffer) {
838
+ try {
839
+ let view = new DataView(buffer), signature = readTag(view, 0), tables;
840
+ signature === "wOFF" ? tables = await readWoffTables(view) : signature === "wOF2" && (tables = await readWoff2Tables(view));
841
+ let metrics = tables?.get("OS/2");
842
+ if (!isDefined(tables) || !isDefined(metrics)) return;
843
+ let selection = metrics.getUint16(62), axis = readWeightAxis(tables.get("fvar")), family = readFamilyName(tables.get("name")), category = readCategory(metrics, tables.get("post")), weight = roundWeight(axis?.min ?? metrics.getUint16(4)), maxWeight = isDefined(axis) ? roundWeight(axis.max) : void 0;
844
+ return {
845
+ ...isDefined(family) && { family },
846
+ ...isDefined(category) && { category },
847
+ weight,
848
+ ...isDefined(maxWeight) && maxWeight > weight && { maxWeight },
849
+ style: selection % 2 == 1 || Math.floor(selection / 512) % 2 == 1 ? "italic" : "normal"
850
+ };
851
+ } catch {
852
+ return;
853
+ }
854
+ }
855
+ /**
856
+ * Checks whether an array item is an uploaded font file.
857
+ *
858
+ * @param item - The item.
859
+ * @returns True if the item is a font file.
860
+ */
861
+ function isFontFile(item) {
862
+ return isRecord(item) && item._type === "fontFile" && typeof item._key == "string";
863
+ }
864
+ /**
865
+ * Picks the family most of the files belong to, the earliest winning a tie, so one stray file doesn't decide it.
866
+ *
867
+ * @param families - Family name of each file, in order.
868
+ * @returns The family, or undefined when no file names one.
869
+ */
870
+ function findCommonFamily(families) {
871
+ let counts = /* @__PURE__ */ new Map();
872
+ for (let family of families) counts.set(family, (counts.get(family) ?? 0) + 1);
873
+ let common, highest = 0;
874
+ for (let [family, count] of counts) count > highest && (common = family, highest = count);
875
+ return common;
876
+ }
877
+ /**
878
+ * Builds a stored file from an asset and what it holds.
879
+ *
880
+ * @param key - Key to store the file under.
881
+ * @param assetId - ID of the file asset.
882
+ * @param details - What the file holds.
883
+ * @returns The stored file.
884
+ */
885
+ function createFontFile(key, assetId, { family, weight, maxWeight, style }) {
886
+ return {
887
+ _type: fontFileTypeName,
888
+ _key: key,
889
+ asset: {
890
+ _type: "reference",
891
+ _ref: assetId
892
+ },
893
+ ...isDefined(family) && { family },
894
+ weight,
895
+ ...isDefined(maxWeight) && { maxWeight },
896
+ style
897
+ };
898
+ }
899
+ function FilesInput({ value, onChange, readOnly }) {
900
+ let { fillDetails } = useFilesContext(), client = useClient({ apiVersion }), { projectId, dataset } = client.config(), [pending, setPending] = useState([]), [notices, setNotices] = useState([]), [uploadedNames, setUploadedNames] = useState({}), files = (value ?? []).filter((item) => isFontFile(item)), filenames = {
901
+ ...useAssetFilenames(files.flatMap(({ asset }) => isDefined(asset?._ref) ? [asset._ref] : [])),
902
+ ...uploadedNames
903
+ }, duplicates = new Set(findDuplicateFiles(files).map(({ _key }) => _key)), rows = [...files.map(({ _key: _key_0, asset: asset_0, weight, maxWeight, style }) => ({
904
+ key: _key_0,
905
+ url: isDefined(projectId) && isDefined(dataset) ? createFileUrl(asset_0?._ref, projectId, dataset) : void 0,
906
+ weight,
907
+ maxWeight,
908
+ style,
909
+ filename: isDefined(asset_0?._ref) ? filenames[asset_0._ref] : void 0,
910
+ uploading: !1
911
+ })), ...pending.map(({ key, filename, url, details }) => ({
912
+ key,
913
+ url,
914
+ ...details,
915
+ filename,
916
+ uploading: !0
917
+ }))].toSorted((first, second) => (first.weight ?? 0) - (second.weight ?? 0) || +(first.style === "italic") - (second.style === "italic"));
918
+ /**
919
+ * Uploads a file, trying again when the upload fails.
920
+ *
921
+ * @param file - The file.
922
+ * @param filename - Name to store the file under.
923
+ * @param retries - Times left to try again.
924
+ * @returns The uploaded asset.
925
+ * @throws When the last attempt fails.
926
+ */
927
+ async function uploadAsset(file, filename_0, retries) {
928
+ try {
929
+ return await client.assets.upload("file", file, { filename: filename_0 });
930
+ } catch (error) {
931
+ if (retries <= 0) throw error;
932
+ return uploadAsset(file, filename_0, retries - 1);
933
+ }
934
+ }
935
+ /**
936
+ * Uploads one file that has been read and adds it to the font.
937
+ *
938
+ * @param file - The file.
939
+ * @param pendingFile - The file as it's shown while uploading.
940
+ */
941
+ async function upload(file_0, { key: key_0, filename: filename_1, url: url_0, details: details_0 }) {
942
+ try {
943
+ let asset_2 = await uploadAsset(file_0, filename_1, 2);
944
+ setUploadedNames((current_0) => ({
945
+ ...current_0,
946
+ [asset_2._id]: asset_2.originalFilename ?? filename_1
947
+ })), onChange([setIfMissing([]), insert([createFontFile(key_0, asset_2._id, details_0)], "after", [-1])]);
948
+ } catch (error_0) {
949
+ logger.warn(`Couldn't upload ${filename_1}: ${error_0 instanceof Error ? error_0.message : String(error_0)}`), setNotices((current) => [...current, {
950
+ tone: "critical",
951
+ message: `${filename_1} couldn't be uploaded. Try again.`
952
+ }]);
953
+ }
954
+ setPending((current_1) => current_1.filter((entry) => entry.key !== key_0)), URL.revokeObjectURL(url_0);
955
+ }
956
+ async function handleFiles(selected) {
957
+ let read = await Promise.all(selected.map(async (file_1) => ({
958
+ file: file_1,
959
+ details: await readFontFile(await file_1.arrayBuffer())
960
+ }))), held = new Map([...files, ...pending.map(({ details: details_1 }) => details_1)].map((file_2) => [getCoverageKey(file_2), describeFile(file_2)])), family = [...files.map((file_3) => file_3.family), ...pending.map(({ details: details_3 }) => details_3.family)].find((name) => isDefined(name)) ?? findCommonFamily(read.flatMap(({ details: details_2 }) => isDefined(details_2?.family) ? [details_2.family] : [])), fresh = [], uploads = [];
961
+ for (let { file: file_4, details: details_4 } of read) {
962
+ if (!isDefined(details_4)) {
963
+ fresh.push({
964
+ tone: "critical",
965
+ message: `${file_4.name} isn't a WOFF or WOFF2 font, or couldn't be read.`
966
+ });
967
+ continue;
968
+ }
969
+ if (isDefined(family) && isDefined(details_4.family) && details_4.family !== family) {
970
+ fresh.push({
971
+ tone: "caution",
972
+ message: `${file_4.name} wasn't added. Every file in a font must come from the same family, and this one is from ${details_4.family}, not ${family}.`
973
+ });
974
+ continue;
975
+ }
976
+ let coverage = getCoverageKey(details_4), existing = held.get(coverage);
977
+ isDefined(existing) ? fresh.push({
978
+ tone: "caution",
979
+ message: `${file_4.name} wasn't added, because ${existing} is already uploaded.`
980
+ }) : (held.set(coverage, describeFile(details_4)), uploads.push({
981
+ file: file_4,
982
+ pendingFile: {
983
+ key: randomKey(12),
984
+ filename: file_4.name,
985
+ url: URL.createObjectURL(file_4),
986
+ details: details_4
987
+ }
988
+ }));
989
+ }
990
+ if (setNotices(fresh), isDefined(uploads)) {
991
+ let category = uploads.map(({ pendingFile }) => pendingFile.details.category).find((kind) => isDefined(kind));
992
+ fillDetails(family, category);
993
+ }
994
+ setPending((current_2) => [...current_2, ...uploads.map(({ pendingFile: pendingFile_0 }) => pendingFile_0)]);
995
+ let queue = [...uploads];
996
+ /** Uploads the queued files one after another. */
997
+ async function drain() {
998
+ let next = queue.shift();
999
+ isDefined(next) && (await upload(next.file, next.pendingFile), await drain());
1000
+ }
1001
+ await Promise.all(Array.from({ length: Math.min(3, queue.length) }, drain));
1002
+ }
1003
+ return /* @__PURE__ */ jsxs(Stack, {
1004
+ gap: 3,
1005
+ children: [
1006
+ isDefined(rows) && /* @__PURE__ */ jsx(Stack, {
1007
+ gap: 2,
1008
+ children: rows.map(({ key: key_1, url: url_1, weight: weight_0, maxWeight: maxWeight_0, style: style_0, filename: filename_2, uploading }) => /* @__PURE__ */ jsx(FileRow, {
1009
+ url: url_1,
1010
+ weight: weight_0,
1011
+ maxWeight: maxWeight_0,
1012
+ style: style_0,
1013
+ filename: filename_2,
1014
+ uploading,
1015
+ duplicate: duplicates.has(key_1),
1016
+ readOnly,
1017
+ onRemove: () => {
1018
+ onChange(unset([{ _key: key_1 }]));
1019
+ }
1020
+ }, key_1))
1021
+ }),
1022
+ ["critical", "caution"].map((tone) => {
1023
+ let messages = notices.filter((notice) => notice.tone === tone);
1024
+ return isDefined(messages) && /* @__PURE__ */ jsx(Card, {
1025
+ tone,
1026
+ padding: 3,
1027
+ radius: 2,
1028
+ border: !0,
1029
+ children: /* @__PURE__ */ jsx(Stack, {
1030
+ gap: 3,
1031
+ children: messages.map(({ message }) => /* @__PURE__ */ jsx(Text, {
1032
+ size: 1,
1033
+ children: message
1034
+ }, message))
1035
+ })
1036
+ }, tone);
1037
+ }),
1038
+ /* @__PURE__ */ jsx(DropZone, {
1039
+ readOnly,
1040
+ onFiles: (selected_0) => {
1041
+ handleFiles(selected_0);
1042
+ }
1043
+ })
1044
+ ]
1045
+ });
1046
+ }
1047
+ function CodeText(t0) {
1048
+ let $ = c(10), props, text;
1049
+ $[0] === t0 ? (props = $[1], text = $[2]) : ({text, ...props} = t0, $[0] = t0, $[1] = props, $[2] = text);
1050
+ let t1, t2;
1051
+ if ($[3] !== props || $[4] !== text) {
1052
+ let parts = text.split("`");
1053
+ t1 = props, t2 = parts.map((part, index) => {
1054
+ let start = parts.slice(0, index).join("`").length;
1055
+ return index % 2 == 1 ? /* @__PURE__ */ jsx("code", { children: part }, start) : part;
1056
+ }), $[3] = props, $[4] = text, $[5] = t1, $[6] = t2;
1057
+ } else t1 = $[5], t2 = $[6];
1058
+ let t3;
1059
+ return $[7] !== t1 || $[8] !== t2 ? (t3 = /* @__PURE__ */ jsx("span", {
1060
+ ...t1,
1061
+ children: t2
1062
+ }), $[7] = t1, $[8] = t2, $[9] = t3) : t3 = $[9], t3;
1063
+ }
1064
+ function Problem(t0) {
1065
+ let $ = c(14), problem, props;
1066
+ $[0] === t0 ? (problem = $[1], props = $[2]) : ({problem, ...props} = t0, $[0] = t0, $[1] = problem, $[2] = props);
1067
+ let t1;
1068
+ $[3] === Symbol.for("react.memo_cache_sentinel") ? (t1 = /* @__PURE__ */ jsx(Text, {
1069
+ size: 1,
1070
+ children: /* @__PURE__ */ jsx(WarningOutlineIcon, {})
1071
+ }), $[3] = t1) : t1 = $[3];
1072
+ let t2;
1073
+ $[4] === problem.title ? t2 = $[5] : (t2 = /* @__PURE__ */ jsx(Text, {
1074
+ size: 1,
1075
+ weight: "medium",
1076
+ children: /* @__PURE__ */ jsx(CodeText, { text: problem.title })
1077
+ }), $[4] = problem.title, $[5] = t2);
1078
+ let t3;
1079
+ $[6] === problem.description ? t3 = $[7] : (t3 = /* @__PURE__ */ jsx(Text, {
1080
+ size: 1,
1081
+ muted: !0,
1082
+ children: /* @__PURE__ */ jsx(CodeText, { text: problem.description })
1083
+ }), $[6] = problem.description, $[7] = t3);
1084
+ let t4;
1085
+ $[8] !== t2 || $[9] !== t3 ? (t4 = /* @__PURE__ */ jsxs(Flex, {
1086
+ gap: 3,
1087
+ children: [t1, /* @__PURE__ */ jsxs(Stack, {
1088
+ gap: 3,
1089
+ children: [t2, t3]
1090
+ })]
1091
+ }), $[8] = t2, $[9] = t3, $[10] = t4) : t4 = $[10];
1092
+ let t5;
1093
+ return $[11] !== props || $[12] !== t4 ? (t5 = /* @__PURE__ */ jsx(Card, {
1094
+ tone: "caution",
1095
+ padding: 4,
1096
+ radius: 2,
1097
+ border: !0,
1098
+ ...props,
1099
+ children: t4
1100
+ }), $[11] = props, $[12] = t4, $[13] = t5) : t5 = $[13], t5;
1101
+ }
1102
+ /**
1103
+ * Works out which item a key press moves to.
1104
+ *
1105
+ * @param layout - The grid's layout.
1106
+ * @param key - The key pressed.
1107
+ * @param current - Index of the current item.
1108
+ * @param last - Index of the last item.
1109
+ * @returns The new index, or undefined when the key doesn't move.
1110
+ */
1111
+ function getNextIndex({ columns }, key, current, last) {
1112
+ if (key === "ArrowRight") return current + 1;
1113
+ if (key === "ArrowLeft") return current - 1;
1114
+ if (key === "ArrowDown") return current + columns;
1115
+ if (key === "ArrowUp") return current - columns;
1116
+ if (key === "Home") return 0;
1117
+ if (key === "End") return last;
1118
+ }
1119
+ /**
1120
+ * Works out which rows to render at a scroll position, so only items near the viewport are mounted.
1121
+ * Padding replaces the other rows, so the scrollbar stays the right size.
1122
+ *
1123
+ * @param layout - The grid's layout.
1124
+ * @param scrollRow - The row scrolled to.
1125
+ * @param count - Number of items in the grid.
1126
+ * @returns The first and last rows to render, and the total number of rows.
1127
+ */
1128
+ function getVisibleRows({ columns, visibleRows, overscan }, scrollRow, count) {
1129
+ let totalRows = Math.ceil(count / columns), firstRow = Math.max(0, scrollRow - overscan);
1130
+ return {
1131
+ totalRows,
1132
+ firstRow,
1133
+ lastRow: Math.min(totalRows, firstRow + visibleRows + overscan * 2)
1134
+ };
1135
+ }
1136
+ /**
1137
+ * Works out the scroll position that centres an item, for opening on the selected one.
1138
+ *
1139
+ * @param layout - The grid's layout.
1140
+ * @param index - Index of the item.
1141
+ * @returns The scroll position.
1142
+ */
1143
+ function getCenteredScrollTop({ columns, rowHeight, height }, index) {
1144
+ return Math.max(0, Math.floor(index / columns) * rowHeight - height / 2);
1145
+ }
1146
+ /**
1147
+ * Works out the scroll position that brings an item into view, keeping the current one if it's already visible. It
1148
+ * uses the index because Home and End can move to rows that aren't mounted.
1149
+ *
1150
+ * @param layout - The grid's layout.
1151
+ * @param index - Index of the item.
1152
+ * @param scrollTop - The current scroll position.
1153
+ * @returns The scroll position.
1154
+ */
1155
+ function getRevealedScrollTop({ columns, rowHeight, height }, index, scrollTop) {
1156
+ let top = Math.floor(index / columns) * rowHeight;
1157
+ return top < scrollTop ? top : top + rowHeight > scrollTop + height ? top + rowHeight - height : scrollTop;
1158
+ }
1159
+ /** Readable name of each kind of typeface. */
1160
+ const fontCategoryLabels = {
1161
+ "sans-serif": "Sans Serif",
1162
+ serif: "Serif",
1163
+ display: "Display",
1164
+ handwriting: "Handwriting",
1165
+ monospace: "Monospace"
1166
+ }, gridLayout = {
1167
+ columns: 3,
1168
+ rowHeight: 80,
1169
+ visibleRows: 5,
1170
+ height: 392,
1171
+ overscan: 5
1172
+ };
1173
+ /**
1174
+ * Describes the sample a card shows: the family's name, in its regular weight or the nearest it has.
1175
+ *
1176
+ * @param family - The family.
1177
+ * @returns The sample.
1178
+ */
1179
+ function createCellSample({ family, weights, italics }) {
1180
+ let italic = !isDefined(weights), [weight = 400] = (italic ? italics : weights).toSorted((first, second) => Math.abs(first - 400) - Math.abs(second - 400));
1181
+ return {
1182
+ source: "google",
1183
+ family,
1184
+ weights: [weight],
1185
+ styles: [italic ? "italic" : "normal"],
1186
+ text: family
1187
+ };
1188
+ }
1189
+ function Cell(t0) {
1190
+ let $ = c(46), family, focusable, onSelect, position, props, selected, total;
1191
+ $[0] === t0 ? (family = $[1], focusable = $[2], onSelect = $[3], position = $[4], props = $[5], selected = $[6], total = $[7]) : ({family, selected, focusable, position, total, onSelect, ...props} = t0, $[0] = t0, $[1] = family, $[2] = focusable, $[3] = onSelect, $[4] = position, $[5] = props, $[6] = selected, $[7] = total);
1192
+ let t1;
1193
+ $[8] === family ? t1 = $[9] : (t1 = createCellSample(family), $[8] = family, $[9] = t1);
1194
+ let { name, loading } = useFontSample(t1), { weights, italics, variable } = family, t2;
1195
+ $[10] !== italics || $[11] !== weights ? (t2 = /* @__PURE__ */ new Set([...weights, ...italics]), $[10] = italics, $[11] = weights, $[12] = t2) : t2 = $[12];
1196
+ let count = t2.size, t3 = fontCategoryLabels[family.category], t4 = variable ? "Variable" : `${count} ${count === 1 ? "weight" : "weights"}`, t5;
1197
+ $[13] !== t3 || $[14] !== t4 ? (t5 = [t3, t4], $[13] = t3, $[14] = t4, $[15] = t5) : t5 = $[15];
1198
+ let details = t5, t6 = focusable ? 0 : -1, t7;
1199
+ $[16] === Symbol.for("react.memo_cache_sentinel") ? (t7 = {
1200
+ height: 72,
1201
+ minWidth: 0,
1202
+ boxShadow: "inset 0 0 0 1px var(--card-border-color)"
1203
+ }, $[16] = t7) : t7 = $[16];
1204
+ let t8;
1205
+ $[17] !== details || $[18] !== family.family ? (t8 = [family.family, ...details], $[17] = details, $[18] = family.family, $[19] = t8) : t8 = $[19];
1206
+ let t9 = t8.join(", "), t10;
1207
+ $[20] !== family || $[21] !== onSelect ? (t10 = () => {
1208
+ onSelect(family);
1209
+ }, $[20] = family, $[21] = onSelect, $[22] = t10) : t10 = $[22];
1210
+ let t11;
1211
+ $[23] === Symbol.for("react.memo_cache_sentinel") ? (t11 = {
1212
+ minWidth: 0,
1213
+ textAlign: "left"
1214
+ }, $[23] = t11) : t11 = $[23];
1215
+ let t12;
1216
+ $[24] === name ? t12 = $[25] : (t12 = isDefined(name) ? `"${name}"` : void 0, $[24] = name, $[25] = t12);
1217
+ let t13 = +!loading, t14;
1218
+ $[26] !== t12 || $[27] !== t13 ? (t14 = {
1219
+ fontFamily: t12,
1220
+ opacity: t13,
1221
+ transition: "opacity 150ms ease-out"
1222
+ }, $[26] = t12, $[27] = t13, $[28] = t14) : t14 = $[28];
1223
+ let t15;
1224
+ $[29] !== family.family || $[30] !== t14 ? (t15 = /* @__PURE__ */ jsx(Text, {
1225
+ size: 3,
1226
+ textOverflow: "ellipsis",
1227
+ translate: "no",
1228
+ style: t14,
1229
+ children: family.family
1230
+ }), $[29] = family.family, $[30] = t14, $[31] = t15) : t15 = $[31];
1231
+ let t16 = details.join(" · "), t17;
1232
+ $[32] === t16 ? t17 = $[33] : (t17 = /* @__PURE__ */ jsx(Text, {
1233
+ size: 0,
1234
+ muted: !0,
1235
+ textOverflow: "ellipsis",
1236
+ children: t16
1237
+ }), $[32] = t16, $[33] = t17);
1238
+ let t18;
1239
+ $[34] !== t15 || $[35] !== t17 ? (t18 = /* @__PURE__ */ jsx(Box, {
1240
+ style: t11,
1241
+ children: /* @__PURE__ */ jsxs(Stack, {
1242
+ gap: 3,
1243
+ children: [t15, t17]
1244
+ })
1245
+ }), $[34] = t15, $[35] = t17, $[36] = t18) : t18 = $[36];
1246
+ let t19;
1247
+ return $[37] !== position || $[38] !== props || $[39] !== selected || $[40] !== t10 || $[41] !== t18 || $[42] !== t6 || $[43] !== t9 || $[44] !== total ? (t19 = /* @__PURE__ */ jsx(Button, {
1248
+ type: "button",
1249
+ role: "option",
1250
+ "aria-selected": selected,
1251
+ "aria-posinset": position,
1252
+ "aria-setsize": total,
1253
+ tabIndex: t6,
1254
+ mode: "bleed",
1255
+ selected,
1256
+ padding: 3,
1257
+ style: t7,
1258
+ "aria-label": t9,
1259
+ onClick: t10,
1260
+ ...props,
1261
+ children: t18
1262
+ }), $[37] = position, $[38] = props, $[39] = selected, $[40] = t10, $[41] = t18, $[42] = t6, $[43] = t9, $[44] = total, $[45] = t19) : t19 = $[45], t19;
1263
+ }
1264
+ function Library(t0) {
1265
+ let $ = c(42), activeCell, activeIndex, families, onClearSearch, onSelect, props, rows, search, selected;
1266
+ $[0] === t0 ? (activeCell = $[1], activeIndex = $[2], families = $[3], onClearSearch = $[4], onSelect = $[5], props = $[6], rows = $[7], search = $[8], selected = $[9]) : ({families, rows, selected, activeIndex, activeCell, search, onSelect, onClearSearch, ...props} = t0, $[0] = t0, $[1] = activeCell, $[2] = activeIndex, $[3] = families, $[4] = onClearSearch, $[5] = onSelect, $[6] = props, $[7] = rows, $[8] = search, $[9] = selected);
1267
+ let { totalRows, firstRow, lastRow } = rows, T0, t1, t2, t3, t4, t5, t6;
1268
+ if ($[10] !== activeCell || $[11] !== activeIndex || $[12] !== families || $[13] !== firstRow || $[14] !== lastRow || $[15] !== onClearSearch || $[16] !== onSelect || $[17] !== props || $[18] !== search || $[19] !== selected || $[20] !== totalRows) {
1269
+ let visible = families.slice(firstRow * 3, lastRow * 3);
1270
+ T0 = Box, t1 = "listbox", t2 = "Google Fonts", $[28] === families ? t3 = $[29] : (t3 = isDefined(families) ? {
1271
+ height: 392,
1272
+ overflowY: "auto"
1273
+ } : void 0, $[28] = families, $[29] = t3), t4 = props, $[30] !== families || $[31] !== onClearSearch || $[32] !== search ? (t5 = !isDefined(families) && /* @__PURE__ */ jsxs(Flex, {
1274
+ align: "center",
1275
+ justify: "center",
1276
+ direction: "column",
1277
+ gap: 3,
1278
+ paddingTop: 5,
1279
+ paddingBottom: 6,
1280
+ children: [/* @__PURE__ */ jsx(Text, {
1281
+ size: 1,
1282
+ muted: !0,
1283
+ children: isDefined(search) ? `Nothing matches "${search}".` : "No fonts of this kind."
1284
+ }), /* @__PURE__ */ jsx(Button, {
1285
+ type: "button",
1286
+ mode: "ghost",
1287
+ text: isDefined(search) ? "Clear search" : "Show all kinds",
1288
+ onClick: onClearSearch
1289
+ })]
1290
+ }), $[30] = families, $[31] = onClearSearch, $[32] = search, $[33] = t5) : t5 = $[33], t6 = isDefined(families) && /* @__PURE__ */ jsx(Grid, {
1291
+ gridTemplateColumns: 3,
1292
+ style: {
1293
+ columnGap: 8,
1294
+ rowGap: 8,
1295
+ paddingTop: firstRow * 80,
1296
+ paddingBottom: (totalRows - lastRow) * 80
1297
+ },
1298
+ children: visible.map((family, offset) => {
1299
+ let index = firstRow * 3 + offset;
1300
+ return /* @__PURE__ */ jsx(Cell, {
1301
+ ref: index === activeIndex ? activeCell : void 0,
1302
+ family,
1303
+ selected: family.family === selected,
1304
+ focusable: index === activeIndex,
1305
+ position: index + 1,
1306
+ total: families.length,
1307
+ onSelect
1308
+ }, family.family);
1309
+ })
1310
+ }), $[10] = activeCell, $[11] = activeIndex, $[12] = families, $[13] = firstRow, $[14] = lastRow, $[15] = onClearSearch, $[16] = onSelect, $[17] = props, $[18] = search, $[19] = selected, $[20] = totalRows, $[21] = T0, $[22] = t1, $[23] = t2, $[24] = t3, $[25] = t4, $[26] = t5, $[27] = t6;
1311
+ } else T0 = $[21], t1 = $[22], t2 = $[23], t3 = $[24], t4 = $[25], t5 = $[26], t6 = $[27];
1312
+ let t7;
1313
+ return $[34] !== T0 || $[35] !== t1 || $[36] !== t2 || $[37] !== t3 || $[38] !== t4 || $[39] !== t5 || $[40] !== t6 ? (t7 = /* @__PURE__ */ jsxs(T0, {
1314
+ role: t1,
1315
+ "aria-label": t2,
1316
+ style: t3,
1317
+ ...t4,
1318
+ children: [t5, t6]
1319
+ }), $[34] = T0, $[35] = t1, $[36] = t2, $[37] = t3, $[38] = t4, $[39] = t5, $[40] = t6, $[41] = t7) : t7 = $[41], t7;
1320
+ }
1321
+ function Recent(t0) {
1322
+ let $ = c(24), families, onForget, onSelect, props, selected;
1323
+ $[0] === t0 ? (families = $[1], onForget = $[2], onSelect = $[3], props = $[4], selected = $[5]) : ({families, selected, onSelect, onForget, ...props} = t0, $[0] = t0, $[1] = families, $[2] = onForget, $[3] = onSelect, $[4] = props, $[5] = selected);
1324
+ let t1;
1325
+ $[6] === Symbol.for("react.memo_cache_sentinel") ? (t1 = /* @__PURE__ */ jsx(Text, {
1326
+ size: 1,
1327
+ weight: "medium",
1328
+ muted: !0,
1329
+ children: "Recent fonts"
1330
+ }), $[6] = t1) : t1 = $[6];
1331
+ let t2;
1332
+ $[7] === onForget ? t2 = $[8] : (t2 = /* @__PURE__ */ jsxs(Flex, {
1333
+ align: "center",
1334
+ justify: "space-between",
1335
+ gap: 2,
1336
+ children: [t1, /* @__PURE__ */ jsx(Button, {
1337
+ type: "button",
1338
+ mode: "bleed",
1339
+ fontSize: 1,
1340
+ padding: 2,
1341
+ text: "Clear",
1342
+ onClick: onForget,
1343
+ "aria-label": "Clear recent fonts"
1344
+ })]
1345
+ }), $[7] = onForget, $[8] = t2);
1346
+ let t3;
1347
+ $[9] === Symbol.for("react.memo_cache_sentinel") ? (t3 = {
1348
+ columnGap: 8,
1349
+ rowGap: 8
1350
+ }, $[9] = t3) : t3 = $[9];
1351
+ let t4;
1352
+ if ($[10] !== families || $[11] !== onSelect || $[12] !== selected) {
1353
+ let t5;
1354
+ $[14] !== families.length || $[15] !== onSelect || $[16] !== selected ? (t5 = (family, index) => /* @__PURE__ */ jsx(Cell, {
1355
+ family,
1356
+ selected: family.family === selected,
1357
+ focusable: !0,
1358
+ position: index + 1,
1359
+ total: families.length,
1360
+ onSelect
1361
+ }, family.family), $[14] = families.length, $[15] = onSelect, $[16] = selected, $[17] = t5) : t5 = $[17], t4 = families.map(t5), $[10] = families, $[11] = onSelect, $[12] = selected, $[13] = t4;
1362
+ } else t4 = $[13];
1363
+ let t5;
1364
+ $[18] === t4 ? t5 = $[19] : (t5 = /* @__PURE__ */ jsx(Grid, {
1365
+ role: "listbox",
1366
+ "aria-label": "Recent fonts",
1367
+ gridTemplateColumns: 3,
1368
+ style: t3,
1369
+ children: t4
1370
+ }), $[18] = t4, $[19] = t5);
1371
+ let t6;
1372
+ return $[20] !== props || $[21] !== t2 || $[22] !== t5 ? (t6 = /* @__PURE__ */ jsxs(Stack, {
1373
+ gap: 3,
1374
+ ...props,
1375
+ children: [t2, t5]
1376
+ }), $[20] = props, $[21] = t2, $[22] = t5, $[23] = t6) : t6 = $[23], t6;
1377
+ }
1378
+ /** Orders the picker offers, the first being the default. */
1379
+ const familySorts = [
1380
+ {
1381
+ name: "popular",
1382
+ label: "Popular",
1383
+ compare: () => 0
1384
+ },
1385
+ {
1386
+ name: "trending",
1387
+ label: "Trending",
1388
+ compare: (first, second) => first.trending - second.trending
1389
+ },
1390
+ {
1391
+ name: "newest",
1392
+ label: "Newest",
1393
+ compare: (first, second) => second.added.localeCompare(first.added)
1394
+ },
1395
+ {
1396
+ name: "name",
1397
+ label: "Name",
1398
+ compare: (first, second) => first.family.localeCompare(second.family, "en-US")
1399
+ }
1400
+ ];
1401
+ /**
1402
+ * Normalises text for search, so a family matches however its name is spaced or capitalised.
1403
+ *
1404
+ * @param value - The text.
1405
+ * @returns The normalised text.
1406
+ */
1407
+ function normalizeTerms(value) {
1408
+ return value.toLowerCase().replaceAll(/[\s-]+/gu, "");
1409
+ }
1410
+ /**
1411
+ * Loads the catalog the build step downloaded.
1412
+ *
1413
+ * @returns The families, or why they can't be shown.
1414
+ */
1415
+ async function readCatalog() {
1416
+ let { ready, load } = fontCatalog;
1417
+ if (!ready) return { problem: {
1418
+ title: "Google Fonts isn't set up",
1419
+ description: "Add `withFonts()` to the Studio's build configuration, then restart the Studio."
1420
+ } };
1421
+ if (!isDefined(load)) return { problem: {
1422
+ title: "Couldn't download Google Fonts",
1423
+ description: "The Studio's build couldn't reach Google Fonts. Check the connection, then restart the Studio."
1424
+ } };
1425
+ let module = await load(), families = parseCatalog(module.default);
1426
+ return isDefined(families) ? { families } : { problem: {
1427
+ title: "Couldn't load Google Fonts",
1428
+ description: "Restart the Studio and try again."
1429
+ } };
1430
+ }
1431
+ /** Catalog request shared by every field in the session, since the catalog never changes while the Studio runs. */
1432
+ let catalogRequest;
1433
+ /**
1434
+ * Loads the catalog, reusing a request already in progress.
1435
+ *
1436
+ * @returns The families, or why they can't be shown.
1437
+ */
1438
+ async function requestCatalog() {
1439
+ return catalogRequest ??= readCatalog(), await catalogRequest;
1440
+ }
1441
+ /**
1442
+ * Loads the Google Fonts catalog. Only an open picker calls this, so a Studio that never opens one never loads it.
1443
+ *
1444
+ * @returns The families or why they can't be shown, or undefined while loading.
1445
+ */
1446
+ function useFontCatalog() {
1447
+ let $ = c(2), [result, setResult] = useState(), t0, t1;
1448
+ return $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = () => {
1449
+ let active = !0;
1450
+ return (async function load() {
1451
+ let loaded = await requestCatalog();
1452
+ active && setResult(loaded);
1453
+ })(), () => {
1454
+ active = !1;
1455
+ };
1456
+ }, t1 = [], $[0] = t0, $[1] = t1) : (t0 = $[0], t1 = $[1]), useEffect(t0, t1), result;
1457
+ }
1458
+ /**
1459
+ * Creates a list of recent choices kept in the browser's local storage rather than the dataset, one list per scope.
1460
+ *
1461
+ * @param storageKey - Local storage key the lists are kept under, followed by each list's scope.
1462
+ * @param limit - Number of choices each list keeps.
1463
+ * @returns Functions that read, add to, and clear a list.
1464
+ */
1465
+ function createRecentStore(storageKey, limit) {
1466
+ /**
1467
+ * Builds the storage key for one list.
1468
+ *
1469
+ * @param scope - The list's scope.
1470
+ * @returns The storage key.
1471
+ */
1472
+ function getStorageKey(scope) {
1473
+ return `${storageKey}:${scope}`;
1474
+ }
1475
+ /**
1476
+ * Reads a list.
1477
+ *
1478
+ * @param scope - The list's scope.
1479
+ * @returns The choices, most recent first.
1480
+ */
1481
+ function read(scope) {
1482
+ try {
1483
+ let stored = JSON.parse(globalThis.localStorage.getItem(getStorageKey(scope)) ?? "[]");
1484
+ return Array.isArray(stored) ? stored.filter((entry) => typeof entry == "string") : [];
1485
+ } catch {
1486
+ return [];
1487
+ }
1488
+ }
1489
+ /**
1490
+ * Adds a choice to the front of a list.
1491
+ *
1492
+ * @param scope - The list's scope.
1493
+ * @param name - The choice.
1494
+ * @returns The updated choices, most recent first.
1495
+ */
1496
+ function write(scope, name) {
1497
+ let updated = [name, ...read(scope).filter((entry) => entry !== name)].slice(0, limit);
1498
+ try {
1499
+ globalThis.localStorage.setItem(getStorageKey(scope), JSON.stringify(updated));
1500
+ } catch {}
1501
+ return updated;
1502
+ }
1503
+ /**
1504
+ * Clears a list.
1505
+ *
1506
+ * @param scope - The list's scope.
1507
+ */
1508
+ function clear(scope) {
1509
+ try {
1510
+ globalThis.localStorage.removeItem(getStorageKey(scope));
1511
+ } catch {}
1512
+ }
1513
+ return {
1514
+ read,
1515
+ write,
1516
+ clear
1517
+ };
1518
+ }
1519
+ /**
1520
+ * Tracks one list of recent choices. The list is read when the component mounts, so choices made in another tab are
1521
+ * included.
1522
+ *
1523
+ * @param store - The store the list is kept in.
1524
+ * @param scope - The list's scope.
1525
+ * @returns The recent choices, most recent first, and functions to add to and clear them.
1526
+ */
1527
+ function useRecent(store, scope) {
1528
+ let $ = c(13), t0;
1529
+ $[0] !== scope || $[1] !== store ? (t0 = () => store.read(scope), $[0] = scope, $[1] = store, $[2] = t0) : t0 = $[2];
1530
+ let [recent, setRecent] = useState(t0), t1;
1531
+ $[3] !== scope || $[4] !== store ? (t1 = function remember(name) {
1532
+ setRecent(store.write(scope, name));
1533
+ }, $[3] = scope, $[4] = store, $[5] = t1) : t1 = $[5];
1534
+ let remember = t1, t2;
1535
+ $[6] !== scope || $[7] !== store ? (t2 = function forget() {
1536
+ store.clear(scope), setRecent([]);
1537
+ }, $[6] = scope, $[7] = store, $[8] = t2) : t2 = $[8];
1538
+ let forget = t2, t3;
1539
+ return $[9] !== forget || $[10] !== recent || $[11] !== remember ? (t3 = {
1540
+ recent,
1541
+ remember,
1542
+ forget
1543
+ }, $[9] = forget, $[10] = recent, $[11] = remember, $[12] = t3) : t3 = $[12], t3;
1544
+ }
1545
+ /** Recent Google fonts, kept in the browser, up to one row of the grid. */
1546
+ const recentFontStore = createRecentStore("driftime:sanity-plugin-font:recent", 3);
1547
+ /**
1548
+ * Tracks the Google fonts an author chose most recently. The list is read when the picker opens, so choices made in
1549
+ * another tab are included.
1550
+ *
1551
+ * @returns The recent family names, most recent first, and functions to add to and clear them.
1552
+ */
1553
+ function useRecentFonts() {
1554
+ return useRecent(recentFontStore, "google");
1555
+ }
1556
+ function Picker(t0) {
1557
+ let $ = c(43), categories, families, id, onSelect, props, selected, subsets, variants;
1558
+ $[0] === t0 ? (categories = $[1], families = $[2], id = $[3], onSelect = $[4], props = $[5], selected = $[6], subsets = $[7], variants = $[8]) : ({id, selected, categories, families, variants, subsets, onSelect, ...props} = t0, $[0] = t0, $[1] = categories, $[2] = families, $[3] = id, $[4] = onSelect, $[5] = props, $[6] = selected, $[7] = subsets, $[8] = variants);
1559
+ let result = useFontCatalog(), { recent, remember, forget } = useRecentFonts(), catalog = result?.families ?? [], byName = new Map(catalog.map(_temp$1)), listed = isDefined(families) ? families.flatMap((name) => byName.get(name) ?? []) : catalog, t1;
1560
+ $[9] !== categories || $[10] !== subsets || $[11] !== variants ? (t1 = (family_0) => categories.includes(family_0.category) && meetsRequirements(family_0, variants, subsets), $[9] = categories, $[10] = subsets, $[11] = variants, $[12] = t1) : t1 = $[12];
1561
+ let offered = listed.filter(t1), offeredCategories = fontCategories.filter((member) => offered.some((family_1) => family_1.category === member)), [search, setSearch] = useState(""), [category, setCategory] = useState(), [sort, setSort] = useState(familySorts[0]), [scrollRow, setScrollRow] = useState(0), [movedIndex, setMovedIndex] = useState(), scroller = useRef(null), activeCell = useRef(null), focusIndex = useRef(void 0), scrollFrame = useRef(void 0), centred = useRef(!1), query = normalizeTerms(search), filtering = isDefined(query) || isDefined(category), t2;
1562
+ $[13] !== families || $[14] !== sort?.compare ? (t2 = isDefined(families) ? void 0 : sort?.compare, $[13] = families, $[14] = sort?.compare, $[15] = t2) : t2 = $[15];
1563
+ let results = offered.filter((family_2) => (!isDefined(query) || normalizeTerms(family_2.family).includes(query)) && (!isDefined(category) || family_2.category === category)).toSorted(t2), offeredByName = new Map(offered.map(_temp2)), recentFamilies = filtering ? [] : recent.flatMap((name_0) => {
1564
+ let family_4 = offeredByName.get(name_0);
1565
+ return isDefined(family_4) ? [family_4] : [];
1566
+ }), rows = getVisibleRows(gridLayout, scrollRow, results.length), selectedIndex = filtering || !isDefined(selected) ? -1 : results.findIndex((t3) => {
1567
+ let { family: family_5 } = t3;
1568
+ return family_5 === selected;
1569
+ }), activeIndex = movedIndex ?? Math.max(0, selectedIndex);
1570
+ useEffect(() => {
1571
+ let element = scroller.current;
1572
+ isDefined(element) && selectedIndex !== -1 && !centred.current && (centred.current = !0, element.scrollTop = getCenteredScrollTop(gridLayout, selectedIndex));
1573
+ }, [scroller, selectedIndex]);
1574
+ let t4, t5;
1575
+ $[16] === Symbol.for("react.memo_cache_sentinel") ? (t4 = () => () => {
1576
+ isDefined(scrollFrame.current) && cancelAnimationFrame(scrollFrame.current);
1577
+ }, t5 = [], $[16] = t4, $[17] = t5) : (t4 = $[16], t5 = $[17]), useEffect(t4, t5);
1578
+ let t6, t7;
1579
+ $[18] === activeIndex ? (t6 = $[19], t7 = $[20]) : (t6 = () => {
1580
+ focusIndex.current === activeIndex && (focusIndex.current = void 0, isDefined(activeCell.current) && activeCell.current.focus());
1581
+ }, t7 = [activeIndex], $[18] = activeIndex, $[19] = t6, $[20] = t7), useEffect(t6, t7);
1582
+ let resetScroll = function resetScroll() {
1583
+ setMovedIndex(void 0), isDefined(scroller.current) && (scroller.current.scrollTop = 0);
1584
+ }, t8;
1585
+ $[21] === Symbol.for("react.memo_cache_sentinel") ? (t8 = function clearSearch() {
1586
+ setSearch(""), setCategory(void 0), setMovedIndex(void 0);
1587
+ }, $[21] = t8) : t8 = $[21];
1588
+ let clearSearch = t8, t9;
1589
+ $[22] === resetScroll ? t9 = $[23] : (t9 = function handleSearch(event) {
1590
+ setSearch(event.currentTarget.value), resetScroll();
1591
+ }, $[22] = resetScroll, $[23] = t9);
1592
+ let handleSearch = t9, t10;
1593
+ $[24] === resetScroll ? t10 = $[25] : (t10 = function handleCategory(event_0) {
1594
+ let { value } = event_0.currentTarget;
1595
+ setCategory(fontCategories.find((member_0) => member_0 === value)), resetScroll();
1596
+ }, $[24] = resetScroll, $[25] = t10);
1597
+ let handleCategory = t10, t11;
1598
+ $[26] === resetScroll ? t11 = $[27] : (t11 = function handleSort(event_1) {
1599
+ let { value: value_0 } = event_1.currentTarget;
1600
+ setSort(familySorts.find((t12) => {
1601
+ let { name: name_1 } = t12;
1602
+ return name_1 === value_0;
1603
+ })), resetScroll();
1604
+ }, $[26] = resetScroll, $[27] = t11);
1605
+ let handleSort = t11, t12;
1606
+ $[28] !== onSelect || $[29] !== remember ? (t12 = function handleSelect(family_6) {
1607
+ remember(family_6.family), onSelect(family_6);
1608
+ }, $[28] = onSelect, $[29] = remember, $[30] = t12) : t12 = $[30];
1609
+ let handleSelect = t12, selectActive = function selectActive() {
1610
+ let family_7 = results[activeIndex];
1611
+ isDefined(family_7) && handleSelect(family_7);
1612
+ }, handleSearchKeyDown = function handleSearchKeyDown(event_2) {
1613
+ event_2.key === "Enter" ? (event_2.preventDefault(), selectActive()) : event_2.key === "ArrowDown" && (event_2.preventDefault(), isDefined(activeCell.current) && activeCell.current.focus());
1614
+ }, handleScroll = function handleScroll() {
1615
+ isDefined(scrollFrame.current) || (scrollFrame.current = requestAnimationFrame(() => {
1616
+ scrollFrame.current = void 0;
1617
+ let element_0 = scroller.current;
1618
+ isDefined(element_0) && setScrollRow(Math.floor(element_0.scrollTop / 80));
1619
+ }));
1620
+ }, handleGridKeyDown = function handleGridKeyDown(event_3) {
1621
+ let last = results.length - 1;
1622
+ if (last < 0) return;
1623
+ if (event_3.key === "Enter" || event_3.key === " ") {
1624
+ event_3.preventDefault(), selectActive();
1625
+ return;
1626
+ }
1627
+ let next = getNextIndex(gridLayout, event_3.key, activeIndex, last);
1628
+ if (!isDefined(next)) return;
1629
+ event_3.preventDefault();
1630
+ let index = Math.min(Math.max(next, 0), last);
1631
+ focusIndex.current = index, setMovedIndex(index);
1632
+ let element_1 = scroller.current;
1633
+ isDefined(element_1) && (element_1.scrollTop = getRevealedScrollTop(gridLayout, index, element_1.scrollTop));
1634
+ }, T0 = Stack, t14 = !isDefined(result?.problem) && /* @__PURE__ */ jsxs(Flex, {
1635
+ gap: 2,
1636
+ children: [
1637
+ /* @__PURE__ */ jsx(Box, {
1638
+ flex: 1,
1639
+ children: /* @__PURE__ */ jsx(TextInput, {
1640
+ type: "search",
1641
+ icon: /* @__PURE__ */ jsx(SearchIcon, {}),
1642
+ placeholder: "Search by name",
1643
+ value: search,
1644
+ onChange: handleSearch,
1645
+ onKeyDown: handleSearchKeyDown,
1646
+ disabled: !isDefined(offered),
1647
+ "aria-label": "Search Google Fonts"
1648
+ })
1649
+ }),
1650
+ offeredCategories.length > 1 && /* @__PURE__ */ jsx(Box, {
1651
+ flex: "none",
1652
+ children: /* @__PURE__ */ jsxs(Select, {
1653
+ id: `${id}-category`,
1654
+ value: category ?? "",
1655
+ onChange: handleCategory,
1656
+ disabled: !isDefined(offered),
1657
+ "aria-label": "Kind of typeface",
1658
+ children: [/* @__PURE__ */ jsx("option", {
1659
+ value: "",
1660
+ children: "All kinds"
1661
+ }), offeredCategories.map(_temp3)]
1662
+ })
1663
+ }),
1664
+ !isDefined(families) && /* @__PURE__ */ jsx(Box, {
1665
+ flex: "none",
1666
+ children: /* @__PURE__ */ jsx(Select, {
1667
+ id: `${id}-sort`,
1668
+ value: sort?.name,
1669
+ onChange: handleSort,
1670
+ disabled: !isDefined(offered),
1671
+ "aria-label": "Sort fonts",
1672
+ children: familySorts.map(_temp4)
1673
+ })
1674
+ })
1675
+ ]
1676
+ }), t15;
1677
+ $[31] === result ? t15 = $[32] : (t15 = !isDefined(result) && /* @__PURE__ */ jsx(Flex, {
1678
+ align: "center",
1679
+ justify: "center",
1680
+ style: { height: 392 },
1681
+ children: /* @__PURE__ */ jsx(Spinner, { muted: !0 })
1682
+ }), $[31] = result, $[32] = t15);
1683
+ let t16;
1684
+ $[33] === result ? t16 = $[34] : (t16 = isDefined(result?.problem) && /* @__PURE__ */ jsx(Problem, { problem: result.problem }), $[33] = result, $[34] = t16);
1685
+ let t17 = isDefined(recentFamilies) && /* @__PURE__ */ jsx(Recent, {
1686
+ families: recentFamilies,
1687
+ selected,
1688
+ onSelect: handleSelect,
1689
+ onForget: forget
1690
+ }), t18 = isDefined(result?.families) && /* @__PURE__ */ jsxs(Stack, {
1691
+ gap: 3,
1692
+ children: [
1693
+ /* @__PURE__ */ jsx(Box, {
1694
+ paddingY: 2,
1695
+ children: /* @__PURE__ */ jsxs(Text, {
1696
+ size: 1,
1697
+ weight: "medium",
1698
+ muted: !0,
1699
+ children: [
1700
+ "All fonts (",
1701
+ results.length.toLocaleString("en-US"),
1702
+ ")"
1703
+ ]
1704
+ })
1705
+ }),
1706
+ !isDefined(offered) && /* @__PURE__ */ jsx(Flex, {
1707
+ align: "center",
1708
+ justify: "center",
1709
+ paddingTop: 5,
1710
+ paddingBottom: 6,
1711
+ children: /* @__PURE__ */ jsx(Text, {
1712
+ size: 1,
1713
+ muted: !0,
1714
+ children: "No Google font matches the options set for this field."
1715
+ })
1716
+ }),
1717
+ isDefined(offered) && /* @__PURE__ */ jsx(Library, {
1718
+ ref: scroller,
1719
+ families: results,
1720
+ rows,
1721
+ selected,
1722
+ activeIndex,
1723
+ activeCell,
1724
+ search,
1725
+ onSelect: handleSelect,
1726
+ onClearSearch: clearSearch,
1727
+ onScroll: handleScroll,
1728
+ onKeyDown: handleGridKeyDown
1729
+ })
1730
+ ]
1731
+ }), t19;
1732
+ return $[35] !== T0 || $[36] !== props || $[37] !== t14 || $[38] !== t15 || $[39] !== t16 || $[40] !== t17 || $[41] !== t18 ? (t19 = /* @__PURE__ */ jsxs(T0, {
1733
+ gap: 4,
1734
+ ...props,
1735
+ children: [
1736
+ t14,
1737
+ t15,
1738
+ t16,
1739
+ t17,
1740
+ t18
1741
+ ]
1742
+ }), $[35] = T0, $[36] = props, $[37] = t14, $[38] = t15, $[39] = t16, $[40] = t17, $[41] = t18, $[42] = t19) : t19 = $[42], t19;
1743
+ }
1744
+ function _temp4(t0) {
1745
+ let { name: name_2, label } = t0;
1746
+ return /* @__PURE__ */ jsx("option", {
1747
+ value: name_2,
1748
+ children: label
1749
+ }, name_2);
1750
+ }
1751
+ function _temp3(member_1) {
1752
+ return /* @__PURE__ */ jsx("option", {
1753
+ value: member_1,
1754
+ children: fontCategoryLabels[member_1]
1755
+ }, member_1);
1756
+ }
1757
+ function _temp2(family_3) {
1758
+ return [family_3.family, family_3];
1759
+ }
1760
+ function _temp$1(family) {
1761
+ return [family.family, family];
1762
+ }
1763
+ /**
1764
+ * Describes the sample a stored font shows in the Studio: some text from its Google family, or its uploaded files.
1765
+ *
1766
+ * @param value - The stored font.
1767
+ * @param text - Characters a Google family is loaded for.
1768
+ * @returns The sample, or undefined when nothing loadable was stored.
1769
+ */
1770
+ function useStoredSample(value, text) {
1771
+ let $ = c(16), t0;
1772
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = { apiVersion }, $[0] = t0) : t0 = $[0];
1773
+ let t1 = useClient(t0), family, styles, t2, weights;
1774
+ if ($[1] !== t1 || $[2] !== value) {
1775
+ t2 = Symbol.for("react.early_return_sentinel");
1776
+ bb0: {
1777
+ let { projectId, dataset } = t1.config(), { source, family: t3, weights: t4, styles: t5, files } = value ?? {};
1778
+ if (family = t3, weights = t4, styles = t5, source === "upload") {
1779
+ let sampleFiles = (files ?? []).flatMap((t6) => {
1780
+ let { asset, weight, maxWeight, style } = t6, url = isDefined(projectId) && isDefined(dataset) ? createFileUrl(asset?._ref, projectId, dataset) : void 0;
1781
+ return isDefined(url) && isDefined(weight) ? [{
1782
+ url,
1783
+ weight,
1784
+ ...isDefined(maxWeight) && { maxWeight },
1785
+ style: style ?? "normal"
1786
+ }] : [];
1787
+ });
1788
+ t2 = isDefined(sampleFiles) ? {
1789
+ source,
1790
+ files: sampleFiles
1791
+ } : void 0;
1792
+ break bb0;
1793
+ }
1794
+ }
1795
+ $[1] = t1, $[2] = value, $[3] = family, $[4] = styles, $[5] = t2, $[6] = weights;
1796
+ } else family = $[3], styles = $[4], t2 = $[5], weights = $[6];
1797
+ if (t2 !== Symbol.for("react.early_return_sentinel")) return t2;
1798
+ if (!isDefined(family)) return;
1799
+ let t3;
1800
+ $[7] === weights ? t3 = $[8] : (t3 = weights ?? [], $[7] = weights, $[8] = t3);
1801
+ let t4;
1802
+ $[9] === styles ? t4 = $[10] : (t4 = styles ?? [], $[9] = styles, $[10] = t4);
1803
+ let t5;
1804
+ return $[11] !== family || $[12] !== t3 || $[13] !== t4 || $[14] !== text ? (t5 = {
1805
+ source: "google",
1806
+ family,
1807
+ weights: t3,
1808
+ styles: t4,
1809
+ text
1810
+ }, $[11] = family, $[12] = t3, $[13] = t4, $[14] = text, $[15] = t5) : t5 = $[15], t5;
1811
+ }
1812
+ function Sample(t0) {
1813
+ let $ = c(17), props, text, value, variants;
1814
+ $[0] === t0 ? (props = $[1], text = $[2], value = $[3], variants = $[4]) : ({value, text, variants, ...props} = t0, $[0] = t0, $[1] = props, $[2] = text, $[3] = value, $[4] = variants);
1815
+ let t1;
1816
+ $[5] === text ? t1 = $[6] : (t1 = (variant) => ({
1817
+ ...variant,
1818
+ specimen: text ?? "Aa",
1819
+ caption: `${describeVariant(variant)} (${variant.weight})`
1820
+ }), $[5] = text, $[6] = t1);
1821
+ let items = variants.map(t1), { name, loading } = useFontSample(useStoredSample(value, items.map(_temp).join(" "))), T0 = Card, T1 = Flex, t7;
1822
+ $[7] !== loading || $[8] !== name ? (t7 = (t8) => {
1823
+ let { weight, style, specimen: specimen_0, caption } = t8;
1824
+ return /* @__PURE__ */ jsxs(Stack, {
1825
+ gap: 3,
1826
+ children: [/* @__PURE__ */ jsx(Text, {
1827
+ size: 3,
1828
+ align: "center",
1829
+ style: {
1830
+ fontFamily: isDefined(name) ? `"${name}"` : void 0,
1831
+ fontWeight: weight,
1832
+ fontStyle: style,
1833
+ opacity: +!loading,
1834
+ transition: "opacity 150ms ease-out"
1835
+ },
1836
+ children: specimen_0
1837
+ }), /* @__PURE__ */ jsx(Text, {
1838
+ size: 0,
1839
+ align: "center",
1840
+ muted: !0,
1841
+ children: caption
1842
+ })]
1843
+ }, `${weight}-${style}`);
1844
+ }, $[7] = loading, $[8] = name, $[9] = t7) : t7 = $[9];
1845
+ let t8 = items.map(t7), t9;
1846
+ $[10] !== T1 || $[11] !== t8 ? (t9 = /* @__PURE__ */ jsx(T1, {
1847
+ gap: 5,
1848
+ wrap: "wrap",
1849
+ children: t8
1850
+ }), $[10] = T1, $[11] = t8, $[12] = t9) : t9 = $[12];
1851
+ let t10;
1852
+ return $[13] !== T0 || $[14] !== props || $[15] !== t9 ? (t10 = /* @__PURE__ */ jsx(T0, {
1853
+ padding: 4,
1854
+ radius: 2,
1855
+ border: !0,
1856
+ ...props,
1857
+ children: t9
1858
+ }), $[13] = T0, $[14] = props, $[15] = t9, $[16] = t10) : t10 = $[16], t10;
1859
+ }
1860
+ function _temp(t0) {
1861
+ let { specimen } = t0;
1862
+ return specimen;
1863
+ }
1864
+ function TypeIcon(props) {
1865
+ let $ = c(5), t0, t1, t2;
1866
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = /* @__PURE__ */ jsx("path", { d: "M12 4v16" }), t1 = /* @__PURE__ */ jsx("path", { d: "M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" }), t2 = /* @__PURE__ */ jsx("path", { d: "M9 20h6" }), $[0] = t0, $[1] = t1, $[2] = t2) : (t0 = $[0], t1 = $[1], t2 = $[2]);
1867
+ let t3;
1868
+ return $[3] === props ? t3 = $[4] : (t3 = /* @__PURE__ */ jsxs("svg", {
1869
+ xmlns: "http://www.w3.org/2000/svg",
1870
+ width: 24,
1871
+ height: 24,
1872
+ viewBox: "0 0 24 24",
1873
+ fill: "none",
1874
+ stroke: "currentColor",
1875
+ strokeWidth: 2,
1876
+ strokeLinecap: "round",
1877
+ strokeLinejoin: "round",
1878
+ ...props,
1879
+ children: [
1880
+ t0,
1881
+ t1,
1882
+ t2
1883
+ ]
1884
+ }), $[3] = props, $[4] = t3), t3;
1885
+ }
1886
+ /**
1887
+ * Keeps the members of a configured list that a font can use, in their definition's order.
1888
+ *
1889
+ * @param list - Every allowed member, in order.
1890
+ * @param configured - The configured list, or undefined when none is set.
1891
+ * @returns The members, or undefined when none are usable.
1892
+ */
1893
+ function selectMembers(list, configured) {
1894
+ let selected = list.filter((member) => configured?.includes(member) === !0);
1895
+ return isDefined(selected) ? selected : void 0;
1896
+ }
1897
+ /**
1898
+ * Reads a field's options where only their shape is known, as in validation.
1899
+ *
1900
+ * @param value - The options.
1901
+ * @returns The options, or undefined when there are none.
1902
+ */
1903
+ function readFontOptions(value) {
1904
+ if (!isRecord(value)) return;
1905
+ let { sources, categories, weights, styles, subsets } = value;
1906
+ return {
1907
+ ...Array.isArray(sources) && { sources: fontSources.filter((source) => sources.includes(source)) },
1908
+ ...Array.isArray(categories) && { categories: fontCategories.filter((category) => categories.includes(category)) },
1909
+ ...Array.isArray(weights) && { weights: parseWeights(weights) },
1910
+ ...Array.isArray(styles) && { styles: fontStyles.filter((style) => styles.includes(style)) },
1911
+ ...Array.isArray(subsets) && { subsets: subsets.filter((subset) => typeof subset == "string") }
1912
+ };
1913
+ }
1914
+ /**
1915
+ * Resolves what a font field offers, requires, and previews. A field's own settings replace the plugin's, and a list
1916
+ * with nothing usable in it falls back to the default.
1917
+ *
1918
+ * @param options - The field's options.
1919
+ * @param config - The plugin configuration.
1920
+ * @returns The field's settings.
1921
+ */
1922
+ function resolveFontOptions(options, config) {
1923
+ let subsets = (options?.subsets ?? config.subsets)?.filter((subset) => isDefined(subset)), families = (options?.families ?? config.families)?.filter((family) => isDefined(family)), previewText = options?.preview?.text ?? config.preview?.text;
1924
+ return {
1925
+ sources: selectMembers(fontSources, options?.sources ?? config.sources) ?? defaultSources,
1926
+ categories: selectMembers(fontCategories, options?.categories ?? config.categories) ?? [...fontCategories],
1927
+ families: isDefined(families) ? [...new Set(families)] : void 0,
1928
+ weights: selectMembers(fontWeights, options?.weights ?? config.weights) ?? defaultWeights,
1929
+ styles: selectMembers(fontStyles, options?.styles ?? config.styles) ?? defaultStyles,
1930
+ subsets: isDefined(subsets) ? subsets : defaultSubsets,
1931
+ preview: { text: isDefined(previewText) ? previewText : void 0 }
1932
+ };
1933
+ }
1934
+ /** Fields only one source sets, which are cleared when the font switches to the other. */
1935
+ const sourceFields = {
1936
+ google: ["weights", "styles"],
1937
+ upload: ["files"]
1938
+ };
1939
+ /**
1940
+ * Lists the fields to clear when a font switches source. The family and kind of typeface are cleared too, since they
1941
+ * described the other source's font.
1942
+ *
1943
+ * @param source - The new source.
1944
+ * @returns The fields to clear.
1945
+ */
1946
+ function getStaleFields(source) {
1947
+ return [
1948
+ "family",
1949
+ "category",
1950
+ ...sourceFields[source === "google" ? "upload" : "google"]
1951
+ ];
1952
+ }
1953
+ /**
1954
+ * Checks whether a font holds uploaded files, so the upload fields are only shown and required for an upload.
1955
+ *
1956
+ * @param parent - The font.
1957
+ * @returns Whether the font is an upload.
1958
+ */
1959
+ function isUpload(parent) {
1960
+ return typeof parent == "object" && !!parent && "source" in parent && parent.source === "upload";
1961
+ }
1962
+ /** Label and icon of each source's tab. */
1963
+ const sourceTabs = {
1964
+ google: {
1965
+ label: "Google Fonts",
1966
+ icon: EarthGlobeIcon
1967
+ },
1968
+ upload: {
1969
+ label: "Upload",
1970
+ icon: UploadIcon
1971
+ }
1972
+ };
1973
+ function Sources(t0) {
1974
+ let $ = c(22), { id, offered, selected, readOnly, onSelect, children } = t0;
1975
+ if (offered.length < 2) {
1976
+ let t1;
1977
+ return $[0] === children ? t1 = $[1] : (t1 = /* @__PURE__ */ jsx(Fragment$1, { children }), $[0] = children, $[1] = t1), t1;
1978
+ }
1979
+ let t1;
1980
+ if ($[2] !== id || $[3] !== offered || $[4] !== onSelect || $[5] !== readOnly || $[6] !== selected) {
1981
+ let t2;
1982
+ $[8] !== id || $[9] !== onSelect || $[10] !== readOnly || $[11] !== selected ? (t2 = (source) => {
1983
+ let { label, icon: Icon } = sourceTabs[source];
1984
+ return /* @__PURE__ */ jsx(Tab, {
1985
+ id: `${id}-tab-${source}`,
1986
+ "aria-controls": `${id}-panel`,
1987
+ icon: /* @__PURE__ */ jsx(Icon, {}),
1988
+ label,
1989
+ selected: source === selected,
1990
+ disabled: readOnly,
1991
+ onClick: () => {
1992
+ onSelect(source);
1993
+ }
1994
+ }, source);
1995
+ }, $[8] = id, $[9] = onSelect, $[10] = readOnly, $[11] = selected, $[12] = t2) : t2 = $[12], t1 = offered.map(t2), $[2] = id, $[3] = offered, $[4] = onSelect, $[5] = readOnly, $[6] = selected, $[7] = t1;
1996
+ } else t1 = $[7];
1997
+ let t2;
1998
+ $[13] === t1 ? t2 = $[14] : (t2 = /* @__PURE__ */ jsx(TabList, {
1999
+ gap: 1,
2000
+ children: t1
2001
+ }), $[13] = t1, $[14] = t2);
2002
+ let t3 = `${id}-panel`, t4 = `${id}-tab-${selected}`, t5;
2003
+ $[15] !== children || $[16] !== t3 || $[17] !== t4 ? (t5 = /* @__PURE__ */ jsx(TabPanel, {
2004
+ id: t3,
2005
+ "aria-labelledby": t4,
2006
+ children
2007
+ }), $[15] = children, $[16] = t3, $[17] = t4, $[18] = t5) : t5 = $[18];
2008
+ let t6;
2009
+ return $[19] !== t2 || $[20] !== t5 ? (t6 = /* @__PURE__ */ jsxs(Stack, {
2010
+ gap: 4,
2011
+ children: [t2, t5]
2012
+ }), $[19] = t2, $[20] = t5, $[21] = t6) : t6 = $[21], t6;
2013
+ }
2014
+ /**
2015
+ * Builds the patches that switch a font to another source, clearing the fields the previous one set. Otherwise
2016
+ * they'd stay hidden in the document and reappear if the author switched back.
2017
+ *
2018
+ * @param typeName - The font type's schema name.
2019
+ * @param source - The new source.
2020
+ * @returns The patches.
2021
+ */
2022
+ function createSourcePatches(typeName, source) {
2023
+ return [
2024
+ setIfMissing({ _type: typeName }),
2025
+ set(source, ["source"]),
2026
+ ...getStaleFields(source).map((field) => unset([field]))
2027
+ ];
2028
+ }
2029
+ /**
2030
+ * Creates the font field input, bound to the plugin's configuration. A field's own lists replace the plugin's rather
2031
+ * than adding to them.
2032
+ *
2033
+ * @param config - The plugin configuration.
2034
+ * @returns The input component.
2035
+ */
2036
+ function createInput(config) {
2037
+ function Input(props) {
2038
+ let $ = c(38), { id, schemaType, value, onChange, readOnly, renderDefault } = props, { source: storedSource, family: storedFamily } = value ?? {}, family = getFamilyName(value), [open, setOpen] = useState(!1), [tab, setTab] = useState(), { sources, categories, families, weights, styles, subsets, preview } = resolveFontOptions(schemaType.options, config), [t0] = sources, defaultSource = t0 === void 0 ? "google" : t0, selected = tab ?? (isDefined(storedSource) && sources.includes(storedSource) ? storedSource : defaultSource), variants = listVariants(weights, styles), sampleVariants = storedSource === "upload" ? listFileVariants(value?.files ?? [], variants) : listVariants(value?.weights ?? [], value?.styles ?? []), handleSelectSource = function handleSelectSource(source) {
2039
+ setTab(source), source === "upload" && storedSource !== "upload" && onChange(createSourcePatches(schemaType.name, source));
2040
+ }, handleSelectFamily = function handleSelectFamily(selection) {
2041
+ onChange([
2042
+ ...createSourcePatches(schemaType.name, "google"),
2043
+ set(selection.family, ["family"]),
2044
+ set(selection.category, ["category"]),
2045
+ set(weights, ["weights"]),
2046
+ set(styles, ["styles"])
2047
+ ]), setOpen(!1);
2048
+ }, handleOpen = function handleOpen() {
2049
+ selected === "upload" && storedSource !== "upload" && onChange(createSourcePatches(schemaType.name, "upload")), setOpen(!0);
2050
+ }, t1;
2051
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t1 = function handleClose() {
2052
+ setTab(void 0), setOpen(!1);
2053
+ }, $[0] = t1) : t1 = $[0];
2054
+ let handleClose = t1, T0 = Stack, t3;
2055
+ $[1] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx(TypeIcon, { ...defaultIconProps }), $[1] = t3) : t3 = $[1];
2056
+ let t4 = family ?? "Select font", t5 = isDefined(family) ? `${family} currently selected` : "Select font", t6;
2057
+ $[2] !== handleOpen || $[3] !== id || $[4] !== readOnly || $[5] !== t4 || $[6] !== t5 ? (t6 = /* @__PURE__ */ jsx(Button, {
2058
+ id,
2059
+ type: "button",
2060
+ mode: "ghost",
2061
+ icon: t3,
2062
+ text: t4,
2063
+ disabled: readOnly,
2064
+ onClick: handleOpen,
2065
+ "aria-label": t5
2066
+ }), $[2] = handleOpen, $[3] = id, $[4] = readOnly, $[5] = t4, $[6] = t5, $[7] = t6) : t6 = $[7];
2067
+ let t7;
2068
+ $[8] !== onChange || $[9] !== readOnly || $[10] !== value ? (t7 = isDefined(value) && /* @__PURE__ */ jsx(Button, {
2069
+ type: "button",
2070
+ mode: "ghost",
2071
+ tone: "critical",
2072
+ icon: /* @__PURE__ */ jsx(CloseIcon, {}),
2073
+ text: "Clear",
2074
+ disabled: readOnly,
2075
+ onClick: () => {
2076
+ onChange(unset());
2077
+ },
2078
+ "aria-label": "Clear the selected font"
2079
+ }), $[8] = onChange, $[9] = readOnly, $[10] = value, $[11] = t7) : t7 = $[11];
2080
+ let t8;
2081
+ $[12] !== t6 || $[13] !== t7 ? (t8 = /* @__PURE__ */ jsxs(Flex, {
2082
+ gap: 2,
2083
+ children: [t6, t7]
2084
+ }), $[12] = t6, $[13] = t7, $[14] = t8) : t8 = $[14];
2085
+ let t9 = isDefined(family) && isDefined(sampleVariants) && /* @__PURE__ */ jsx(Sample, {
2086
+ value: value ?? {},
2087
+ text: preview.text,
2088
+ variants: sampleVariants
2089
+ }), t10;
2090
+ $[15] !== categories || $[16] !== families || $[17] !== handleClose || $[18] !== handleSelectFamily || $[19] !== handleSelectSource || $[20] !== id || $[21] !== onChange || $[22] !== open || $[23] !== props || $[24] !== readOnly || $[25] !== renderDefault || $[26] !== selected || $[27] !== sources || $[28] !== storedFamily || $[29] !== storedSource || $[30] !== subsets || $[31] !== variants ? (t10 = open && /* @__PURE__ */ jsx(Dialog, {
2091
+ id: `${id}-dialog`,
2092
+ header: "Select font",
2093
+ width: 1,
2094
+ onClose: handleClose,
2095
+ onClickOutside: handleClose,
2096
+ children: /* @__PURE__ */ jsx(Box, {
2097
+ padding: 4,
2098
+ children: /* @__PURE__ */ jsxs(Sources, {
2099
+ id,
2100
+ offered: sources,
2101
+ selected,
2102
+ readOnly,
2103
+ onSelect: handleSelectSource,
2104
+ children: [selected === "google" && /* @__PURE__ */ jsx(Picker, {
2105
+ id,
2106
+ selected: storedSource === "google" ? storedFamily : void 0,
2107
+ categories,
2108
+ families,
2109
+ variants,
2110
+ subsets,
2111
+ onSelect: handleSelectFamily
2112
+ }), selected === "upload" && /* @__PURE__ */ jsx(FilesProvider, {
2113
+ fillDetails: (name, category) => {
2114
+ onChange([...isDefined(name) ? [setIfMissing(name, ["family"])] : [], ...isDefined(category) ? [setIfMissing(category, ["category"])] : []]);
2115
+ },
2116
+ children: renderDefault(props)
2117
+ })]
2118
+ })
2119
+ })
2120
+ }), $[15] = categories, $[16] = families, $[17] = handleClose, $[18] = handleSelectFamily, $[19] = handleSelectSource, $[20] = id, $[21] = onChange, $[22] = open, $[23] = props, $[24] = readOnly, $[25] = renderDefault, $[26] = selected, $[27] = sources, $[28] = storedFamily, $[29] = storedSource, $[30] = subsets, $[31] = variants, $[32] = t10) : t10 = $[32];
2121
+ let t11;
2122
+ return $[33] !== T0 || $[34] !== t10 || $[35] !== t8 || $[36] !== t9 ? (t11 = /* @__PURE__ */ jsxs(T0, {
2123
+ gap: 3,
2124
+ children: [
2125
+ t8,
2126
+ t9,
2127
+ t10
2128
+ ]
2129
+ }), $[33] = T0, $[34] = t10, $[35] = t8, $[36] = t9, $[37] = t11) : t11 = $[37], t11;
2130
+ }
2131
+ return Input;
2132
+ }
2133
+ function Preview(t0) {
2134
+ let $ = c(12), props, style, value;
2135
+ $[0] === t0 ? (props = $[1], style = $[2], value = $[3]) : ({value, style, ...props} = t0, $[0] = t0, $[1] = props, $[2] = style, $[3] = value);
2136
+ let { name } = useFontSample(useStoredSample(value, "Aa")), t1;
2137
+ $[4] === name ? t1 = $[5] : (t1 = isDefined(name) ? `"${name}"` : void 0, $[4] = name, $[5] = t1);
2138
+ let t2;
2139
+ $[6] !== style || $[7] !== t1 ? (t2 = {
2140
+ fontFamily: t1,
2141
+ lineHeight: 1,
2142
+ ...style
2143
+ }, $[6] = style, $[7] = t1, $[8] = t2) : t2 = $[8];
2144
+ let t3;
2145
+ return $[9] !== props || $[10] !== t2 ? (t3 = /* @__PURE__ */ jsx("span", {
2146
+ style: t2,
2147
+ ...props,
2148
+ children: "Aa"
2149
+ }), $[9] = props, $[10] = t2, $[11] = t3) : t3 = $[11], t3;
2150
+ }
2151
+ /**
2152
+ * Creates preview media for a stored font, so each document shows a sample in its own font instead of an icon.
2153
+ *
2154
+ * @param value - The stored font.
2155
+ * @returns The media component, or undefined when neither a family nor a file was stored.
2156
+ * @public
2157
+ */
2158
+ function createFontPreview(value) {
2159
+ if (isDefined(value) && (isDefined(getFamilyName(value)) || isDefined(value.files))) return function FontPreview() {
2160
+ return createElement(Preview, { value });
2161
+ };
2162
+ }
2163
+ /** Weights listed in the upload fields, each with its name. */
2164
+ /**
2165
+ * Validates a font against the kinds of typeface its field allows and the weights and styles it requires.
2166
+ *
2167
+ * @param value - The font.
2168
+ * @param categories - The kinds of typeface allowed.
2169
+ * @param variants - The weights and styles required.
2170
+ * @returns True when the font is valid, or what's wrong.
2171
+ */
2172
+ function validateFont(value, categories, variants) {
2173
+ let { source, family, category, weights, styles, files } = value;
2174
+ if (source === "upload" && isDefined(category) && !categories.includes(category)) return `This field only takes ${new Intl.ListFormat("en-US", { type: "disjunction" }).format(categories.map((member) => fontCategoryLabels[member]))} fonts. Choose another kind, or upload a different font.`;
2175
+ if (source === "upload") {
2176
+ let missing = listMissingVariants(files ?? [], variants);
2177
+ return !isDefined(missing) || `Upload a file for ${new Intl.ListFormat("en-US").format(missing.map((variant) => describeVariant(variant)))}, which this field needs.`;
2178
+ }
2179
+ if (!isDefined(family)) return "Select a font.";
2180
+ let loaded = listVariants(weights ?? [], styles ?? []);
2181
+ return !variants.some((variant) => !loaded.some(({ weight, style }) => weight === variant.weight && style === variant.style)) || "Select the font again to load the weights and styles this field needs.";
2182
+ }
2183
+ /**
2184
+ * Creates the font object type, applying the plugin's settings to fields that don't set their own.
2185
+ *
2186
+ * @param config - The plugin configuration.
2187
+ * @returns The font type.
2188
+ */
2189
+ function createFontType(config) {
2190
+ return defineType({
2191
+ name: fontTypeName,
2192
+ type: "object",
2193
+ icon: createSanityIcon(TypeIcon),
2194
+ description: "Font chosen from Google Fonts or uploaded.",
2195
+ components: {
2196
+ field: Field,
2197
+ input: createInput(config)
2198
+ },
2199
+ validation: (rule) => rule.custom((value, context) => {
2200
+ if (!isDefined(value)) return !0;
2201
+ let { categories, weights, styles } = resolveFontOptions(readFontOptions(context.type?.options), config);
2202
+ return validateFont(value, categories, listVariants(weights, styles));
2203
+ }),
2204
+ preview: {
2205
+ select: {
2206
+ source: "source",
2207
+ family: "family",
2208
+ files: "files"
2209
+ },
2210
+ prepare(selection) {
2211
+ let { source, family, files } = selection;
2212
+ return {
2213
+ title: getFamilyName({
2214
+ source,
2215
+ family,
2216
+ files
2217
+ }) ?? "No font",
2218
+ subtitle: source === "upload" ? "Uploaded" : "Google Fonts",
2219
+ media: createFontPreview({
2220
+ _type: fontTypeName,
2221
+ source,
2222
+ family,
2223
+ files
2224
+ })
2225
+ };
2226
+ }
2227
+ },
2228
+ fields: [
2229
+ defineField({
2230
+ name: "source",
2231
+ type: "string",
2232
+ description: "Where the font comes from, chosen from the tabs.",
2233
+ components: { field: Hidden }
2234
+ }),
2235
+ defineField({
2236
+ name: "family",
2237
+ type: "string",
2238
+ description: "Name the site uses for the font, filled in from the font files.",
2239
+ placeholder: "Defaults to the family in the font files",
2240
+ hidden: ({ parent }) => !isUpload(parent),
2241
+ validation: (rule) => rule.custom((value, context) => {
2242
+ let files = isRecord(context.parent) ? context.parent.files : void 0;
2243
+ return isUpload(context.parent) && Array.isArray(files) && isDefined(files) && !isDefined(value) ? "Enter the name of the font family." : !0;
2244
+ })
2245
+ }),
2246
+ defineField({
2247
+ name: "category",
2248
+ type: "string",
2249
+ description: "Kind of typeface, which decides the fallback shown while the font loads.",
2250
+ options: { list: fontCategories.map((category) => ({
2251
+ title: fontCategoryLabels[category],
2252
+ value: category
2253
+ })) },
2254
+ hidden: ({ parent }) => !isUpload(parent)
2255
+ }),
2256
+ defineField({
2257
+ name: "weights",
2258
+ type: "array",
2259
+ description: "Weights to load from Google Fonts, saved when the family is chosen.",
2260
+ of: [defineArrayMember({ type: "number" })],
2261
+ hidden: !0
2262
+ }),
2263
+ defineField({
2264
+ name: "styles",
2265
+ type: "array",
2266
+ description: "Styles to load from Google Fonts, saved when the family is chosen.",
2267
+ of: [defineArrayMember({ type: "string" })],
2268
+ hidden: !0
2269
+ }),
2270
+ defineField({
2271
+ name: "files",
2272
+ type: "array",
2273
+ description: "WOFF2 or WOFF files, one for each weight and style, or a single variable font.",
2274
+ components: { input: FilesInput },
2275
+ hidden: ({ parent }) => !isUpload(parent),
2276
+ validation: (rule) => [rule.custom((value, context) => isUpload(context.parent) && !isDefined(value) ? "Upload the font's files." : !0), rule.custom((value) => {
2277
+ let duplicates = findDuplicateFiles(value ?? []);
2278
+ return !isDefined(duplicates) || `More than one file holds ${new Intl.ListFormat("en-US").format(duplicates.map((file) => describeFile(file)))}. Remove the duplicates.`;
2279
+ }).warning()],
2280
+ of: [defineArrayMember({
2281
+ name: fontFileTypeName,
2282
+ type: "file",
2283
+ options: { accept: ".woff2,.woff" },
2284
+ preview: {
2285
+ select: {
2286
+ asset: "asset",
2287
+ weight: "weight",
2288
+ maxWeight: "maxWeight",
2289
+ style: "style",
2290
+ filename: "asset.originalFilename"
2291
+ },
2292
+ prepare(selection) {
2293
+ let { asset, weight, maxWeight, style, filename } = selection;
2294
+ return {
2295
+ title: describeFile({
2296
+ weight,
2297
+ maxWeight,
2298
+ style
2299
+ }),
2300
+ subtitle: filename,
2301
+ media: isDefined(asset) ? createFontPreview({
2302
+ _type: fontTypeName,
2303
+ source: "upload",
2304
+ files: [{
2305
+ _type: fontFileTypeName,
2306
+ _key: "preview",
2307
+ asset,
2308
+ weight,
2309
+ maxWeight,
2310
+ style
2311
+ }]
2312
+ }) : void 0
2313
+ };
2314
+ }
2315
+ },
2316
+ validation: (rule) => rule.custom((value) => isDefined(value?.asset?._ref) && !isDefined(value.weight) ? "This file has no weight or style. Remove it and upload it again." : !0),
2317
+ fields: [
2318
+ defineField({
2319
+ name: "family",
2320
+ type: "string",
2321
+ description: "Name of the family the file belongs to, read from the file.",
2322
+ hidden: !0
2323
+ }),
2324
+ defineField({
2325
+ name: "weight",
2326
+ type: "number",
2327
+ description: "Weight the file holds, or the lightest a variable font file covers, read from the file.",
2328
+ hidden: !0
2329
+ }),
2330
+ defineField({
2331
+ name: "maxWeight",
2332
+ type: "number",
2333
+ description: "Heaviest weight a variable font file covers, read from the file.",
2334
+ hidden: !0
2335
+ }),
2336
+ defineField({
2337
+ name: "style",
2338
+ type: "string",
2339
+ description: "Style the file holds, read from the file.",
2340
+ hidden: !0
2341
+ })
2342
+ ]
2343
+ })]
2344
+ })
2345
+ ]
2346
+ });
2347
+ }
2348
+ const plugin = definePlugin((config) => ({
2349
+ name: pluginName,
2350
+ schema: { types: [createFontType(config)] }
2351
+ }));
2352
+ /**
2353
+ * Adds a font field type to Sanity Studio, with a searchable picker for Google Fonts and uploads for anything else.
2354
+ *
2355
+ * @param config - The plugin configuration, which every field uses unless it sets its own.
2356
+ * @returns The plugin.
2357
+ * @public
2358
+ */
2359
+ function fontPlugin(config = {}) {
2360
+ return plugin(config);
2361
+ }
2362
+ export { createFontPreview, fontPlugin };
2363
+
2364
+ //# sourceMappingURL=index.js.map