@liiift-studio/sanity-font-manager 2.3.19 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +437 -437
  2. package/dist/UploadModal-6LIX7XOK.js +6 -0
  3. package/dist/UploadModal-NME2W53V.mjs +6 -0
  4. package/dist/chunk-646WCBRR.mjs +7276 -0
  5. package/dist/chunk-FH4QKHOH.js +7276 -0
  6. package/dist/index.js +747 -1675
  7. package/dist/index.mjs +400 -1237
  8. package/package.json +85 -85
  9. package/src/components/BatchUploadFonts.jsx +653 -639
  10. package/src/components/BulkActions.jsx +99 -0
  11. package/src/components/ExistingDocumentResolver.jsx +152 -0
  12. package/src/components/FontReviewCard.jsx +415 -0
  13. package/src/components/FontScriptUploaderComponent.jsx +463 -463
  14. package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
  15. package/src/components/KeyValueInput.jsx +95 -95
  16. package/src/components/KeyValueReferenceInput.jsx +254 -254
  17. package/src/components/NestedObjectArraySelector.jsx +146 -146
  18. package/src/components/PriceInput.jsx +26 -26
  19. package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
  20. package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
  21. package/src/components/SetOTF.jsx +87 -87
  22. package/src/components/SingleUploaderTool.jsx +672 -673
  23. package/src/components/StatusDisplay.jsx +26 -26
  24. package/src/components/StyleCountInput.jsx +16 -16
  25. package/src/components/UpdateScriptsComponent.jsx +76 -76
  26. package/src/components/UploadButton.jsx +43 -43
  27. package/src/components/UploadModal.jsx +268 -0
  28. package/src/components/UploadScriptsComponent.jsx +539 -537
  29. package/src/components/UploadStep1Settings.jsx +272 -0
  30. package/src/components/UploadStep2Review.jsx +472 -0
  31. package/src/components/UploadStep3Execute.jsx +234 -0
  32. package/src/components/UploadSummary.jsx +196 -0
  33. package/src/components/VariableInstanceReferencesInput.jsx +190 -190
  34. package/src/hooks/useNestedObjects.js +92 -92
  35. package/src/hooks/useSanityClient.js +9 -9
  36. package/src/index.js +115 -70
  37. package/src/schema/openTypeField.js +1945 -1945
  38. package/src/schema/styleCountField.js +12 -12
  39. package/src/schema/stylesField.js +268 -268
  40. package/src/schema/stylisticSetField.js +301 -301
  41. package/src/utils/buildUploadPlan.js +325 -0
  42. package/src/utils/executeUploadPlan.js +437 -0
  43. package/src/utils/executionReducer.js +56 -0
  44. package/src/utils/fontHelpers.js +267 -0
  45. package/src/utils/generateCssFile.js +207 -205
  46. package/src/utils/generateFontData.js +98 -145
  47. package/src/utils/generateFontFile.js +38 -38
  48. package/src/utils/generateKeywords.js +185 -185
  49. package/src/utils/generateSubset.js +45 -45
  50. package/src/utils/getEmptyFontKit.js +101 -99
  51. package/src/utils/parseFont.js +55 -0
  52. package/src/utils/parseVariableFontInstances.js +211 -211
  53. package/src/utils/planReducer.js +517 -0
  54. package/src/utils/planTypes.js +183 -0
  55. package/src/utils/processFontFiles.js +529 -477
  56. package/src/utils/regenerateFontData.js +146 -146
  57. package/src/utils/resolveExistingFont.js +87 -0
  58. package/src/utils/sanitizeForSanityId.js +65 -65
  59. package/src/utils/updateFontPrices.js +94 -94
  60. package/src/utils/updateTypefaceDocument.js +149 -160
  61. package/src/utils/uploadFontFiles.js +405 -316
  62. package/src/utils/utils.js +24 -24
@@ -1,259 +1,259 @@
1
- // Generates Full Family, Uprights, Italics, and Subfamily collections plus Regular/Italic weight pairs from a typeface's linked fonts
2
-
3
- import React, { useCallback, useState } from 'react';
4
- import { Stack, Grid, Flex, Text, Button, Card, Spinner } from '@sanity/ui';
5
- import { useFormValue } from 'sanity';
6
- import slugify from 'slugify';
7
- import { nanoid } from 'nanoid';
8
-
9
- import { useSanityClient } from '../hooks/useSanityClient';
10
- import StatusDisplay from './StatusDisplay';
11
-
12
- /**
13
- * Generates Full Family, Uprights, Italics, Subfamily collections, and Regular/Italic pairs
14
- * from a typeface's linked fonts. Replaces existing collections and pairs respectively.
15
- */
16
- export const GenerateCollectionsPairsComponent = () => {
17
- const [status, setStatus] = useState('ready');
18
- const [ready, setReady] = useState(true);
19
- const [collectionPrice, setCollectionPrice] = useState(
20
- process.env.SANITY_STUDIO_DEFAULT_COLLECTION_PRICE || 20
21
- );
22
- const [pairPrice, setPairPrice] = useState(
23
- process.env.SANITY_STUDIO_DEFAULT_PAIR_PRICE || 75
24
- );
25
-
26
- const client = useSanityClient();
27
- const doc_id = useFormValue(['_id']);
28
- const title = useFormValue(['title']);
29
- const slug = useFormValue(['slug']);
30
- const stylesObject = useFormValue(['styles']);
31
-
32
- /** Creates or replaces a collection document and returns a weak reference to it. */
33
- const createSanityCollection = useCallback(async (fontsList, collectionSlug, newTitle) => {
34
- const newSlug = collectionSlug.toLowerCase().trim();
35
-
36
- const fontRefs = fontsList.map(font => ({
37
- _key: nanoid(),
38
- _type: 'reference',
39
- _ref: font._id ?? font._ref,
40
- _weak: true,
41
- }));
42
-
43
- let preferredStyle = { weight: fontsList[0].weight, style: fontsList[0].style, _ref: fontsList[0]._ref };
44
- fontsList.forEach(font => {
45
- if (Number(font.weight) < Number(preferredStyle.weight)) return;
46
- if (Number(font.weight) === Number(preferredStyle.weight) && preferredStyle.style === 'Italic' && font.style === 'Regular') {
47
- preferredStyle = { weight: font.weight, style: font.style, _ref: font._id };
48
- } else if (Number(font.weight) > Number(preferredStyle.weight)) {
49
- preferredStyle = { weight: font.weight, style: font.style, _ref: font._id };
50
- }
51
- });
52
-
53
- const price = (collectionPrice ? Number(collectionPrice) : 0) * fontRefs.length;
54
-
55
- await client.createOrReplace({
56
- _key: nanoid(),
57
- _id: newSlug,
58
- _type: 'collection',
59
- state: 'active',
60
- type: 'collection',
61
- preferredStyle: { _type: 'reference', _ref: preferredStyle._ref, _weak: true },
62
- title: newTitle,
63
- slug: { _type: 'slug', current: newSlug },
64
- fonts: fontRefs,
65
- price,
66
- }).catch(err => { console.error('Error creating collection:', err.message); });
67
-
68
- return { _ref: newSlug, _type: 'reference', _weak: true, _key: nanoid() };
69
- }, [collectionPrice, client]);
70
-
71
- /** Creates or replaces a pair document and returns a weak reference to it. */
72
- const createSanityPair = useCallback(async (pair, pairSlug, newTitle) => {
73
- const newSlug = pairSlug.toLowerCase().trim();
74
-
75
- const fontRefs = pair.map(font => ({
76
- _key: nanoid(),
77
- _type: 'reference',
78
- _ref: font._id,
79
- _weak: true,
80
- }));
81
-
82
- await client.createOrReplace({
83
- _key: nanoid(),
84
- _id: newSlug,
85
- _type: 'pair',
86
- preferredStyle: { _type: 'reference', _ref: fontRefs[0]._ref, _weak: true },
87
- title: newTitle,
88
- slug: { _type: 'slug', current: newSlug },
89
- fonts: fontRefs,
90
- price: pairPrice ? Number(pairPrice) : 0,
91
- }).catch(err => { console.error('Error creating pair:', err.message); });
92
-
93
- return { _ref: newSlug, _type: 'reference', _weak: true, _key: nanoid() };
94
- }, [pairPrice, client]);
95
-
96
- /** Generates Full Family, Uprights, Italics, and Subfamily collections. */
97
- const handleGenerateCollections = useCallback(async () => {
98
- setStatus('Generating collections...');
99
- setReady(false);
100
- try {
101
- const result = await client.fetch(
102
- `*[_type == "typeface" && _id == $id]{ "fonts": styles.fonts[] -> }[0]`,
103
- { id: doc_id }
104
- );
105
- const sanityFonts = result?.fonts ?? [];
106
-
107
- const subfamilies = stylesObject?.subfamilies ?? [];
108
- const totalCollections = subfamilies.length + 3;
109
-
110
- const fullFamily = [], uprights = [], italics = [];
111
- for (const font of sanityFonts) {
112
- fullFamily.push(font);
113
- if (font.style === 'Regular') uprights.push(font);
114
- else italics.push(font);
115
- }
116
-
117
- const typefacePatch = [];
118
-
119
- if (fullFamily.length > 1) {
120
- setStatus(`[1/${totalCollections}] Generating full family collection`);
121
- typefacePatch.push(await createSanityCollection(fullFamily, `${slug.current}-full-family`, `${title} Full Family`));
122
- }
123
- if (uprights.length > 1) {
124
- setStatus(`[2/${totalCollections}] Generating uprights collection`);
125
- const ref = await createSanityCollection(uprights, `${slug.current}-uprights`, `${title} Uprights`);
126
- if (ref) typefacePatch.push(ref);
127
- }
128
- if (italics.length > 1) {
129
- setStatus(`[3/${totalCollections}] Generating italics collection`);
130
- const ref = await createSanityCollection(italics, `${slug.current}-italics`, `${title} Italics`);
131
- if (ref) typefacePatch.push(ref);
132
- }
133
- for (let i = 0; i < subfamilies.length; i++) {
134
- setStatus(`[${i + 4}/${totalCollections}] Generating ${subfamilies[i].title} collection`);
135
- const ref = await createSanityCollection(
136
- subfamilies[i].fonts,
137
- `${slug.current}-${slugify(subfamilies[i].title)}-family`,
138
- `${title} ${subfamilies[i].title} Family`
139
- );
140
- if (ref) typefacePatch.push(ref);
141
- }
142
-
143
- await client.patch(doc_id).set({ styles: { ...stylesObject, collections: typefacePatch } }).commit();
144
- setStatus('Collections generated');
145
- } catch (err) {
146
- console.error('Error generating collections:', err);
147
- setStatus('Error generating collections');
148
- }
149
- setReady(true);
150
- }, [doc_id, title, slug, stylesObject, collectionPrice, client, createSanityCollection]);
151
-
152
- /** Generates Regular/Italic pairs matched by subfamily and weight. */
153
- const handleGeneratePairs = useCallback(async () => {
154
- setStatus('Generating pairs...');
155
- setReady(false);
156
- try {
157
- const result = await client.fetch(
158
- `*[_type == "typeface" && _id == $id]{ "fonts": styles.fonts[] -> }[0]`,
159
- { id: doc_id }
160
- );
161
- const sanityFonts = result?.fonts ?? [];
162
-
163
- const regular = [], italic = [];
164
- for (const font of sanityFonts) {
165
- if (font.style === 'Regular') regular.push(font);
166
- else italic.push(font);
167
- }
168
-
169
- const pairs = [];
170
- for (const reg of regular) {
171
- for (const ita of italic) {
172
- if (ita.subfamily === reg.subfamily && ita.weight === reg.weight && ita.weightName === reg.weightName) {
173
- pairs.push([reg, ita]);
174
- }
175
- }
176
- }
177
-
178
- const typefacePatch = [];
179
- for (let i = 0; i < pairs.length; i++) {
180
- const [reg] = pairs[i];
181
- let pairSlug, pairTitle;
182
- if (reg.subfamily && reg.subfamily !== '') {
183
- if (reg.subfamily === 'Regular') {
184
- pairSlug = `${slug.current}-${slugify(reg.weightName)}s`;
185
- pairTitle = `${title} ${reg.weightName}s`;
186
- } else {
187
- pairSlug = `${slug.current}-${slugify(reg.subfamily)}-${slugify(reg.weightName)}s`;
188
- pairTitle = `${title} ${reg.subfamily} ${reg.weightName}s`;
189
- }
190
- } else {
191
- pairSlug = `${slug.current}-${slugify(reg.weightName)}s`;
192
- pairTitle = `${title} ${reg.weightName}s`;
193
- }
194
- setStatus(`[${i + 1}/${pairs.length}] Generating ${pairTitle}`);
195
- const ref = await createSanityPair(pairs[i], pairSlug, pairTitle);
196
- if (ref) typefacePatch.push(ref);
197
- }
198
-
199
- const preferredStyle = regular[0]?._id ?? '';
200
- await client.patch(doc_id).set({
201
- preferredStyle: { _ref: preferredStyle, _type: 'reference', _weak: true },
202
- styles: { ...stylesObject, pairs: typefacePatch },
203
- }).commit();
204
-
205
- setStatus('Pairs generated');
206
- } catch (err) {
207
- console.error('Error generating pairs:', err);
208
- setStatus('Error generating pairs');
209
- }
210
- setReady(true);
211
- }, [doc_id, title, slug, stylesObject, pairPrice, client, createSanityPair]);
212
-
213
- if (!title || !slug) return null;
214
-
215
- return (
216
- <Stack space={2}>
217
- <StatusDisplay status={status} error={false} />
218
- <Card border padding={2} shadow={1} radius={2}>
219
- {ready ? (
220
- <Stack space={3}>
221
- <Grid columns={[2]} gap={4} marginTop={1} marginBottom={1}>
222
- <Stack space={2}>
223
- <Text size={1} muted>Collection price / font</Text>
224
- <Flex align="center" gap={2}>
225
- <Text size={1} muted>$</Text>
226
- <input
227
- value={collectionPrice}
228
- onChange={(e) => setCollectionPrice(e.target.value)}
229
- type="number"
230
- style={{ textAlign: 'end', padding: '5px', maxWidth: '75px' }}
231
- />
232
- </Flex>
233
- </Stack>
234
- <Stack space={2}>
235
- <Text size={1} muted>Pair price</Text>
236
- <Flex align="center" gap={2}>
237
- <Text size={1} muted>$</Text>
238
- <input
239
- value={pairPrice}
240
- onChange={(e) => setPairPrice(e.target.value)}
241
- type="number"
242
- style={{ textAlign: 'end', padding: '5px', maxWidth: '75px' }}
243
- />
244
- </Flex>
245
- </Stack>
246
- </Grid>
247
- <Button mode="ghost" tone="primary" text="Generate Collections" style={{ width: '100%' }} onClick={handleGenerateCollections} />
248
- <Button mode="ghost" tone="primary" text="Generate Pairs" style={{ width: '100%' }} onClick={handleGeneratePairs} />
249
- </Stack>
250
- ) : (
251
- <Flex align="center" justify="center" gap={3} padding={4}>
252
- <Spinner />
253
- <Text muted size={1}>{status}</Text>
254
- </Flex>
255
- )}
256
- </Card>
257
- </Stack>
258
- );
259
- };
1
+ // Generates Full Family, Uprights, Italics, and Subfamily collections plus Regular/Italic weight pairs from a typeface's linked fonts
2
+
3
+ import React, { useCallback, useState } from 'react';
4
+ import { Stack, Grid, Flex, Text, Button, Card, Spinner } from '@sanity/ui';
5
+ import { useFormValue } from 'sanity';
6
+ import slugify from 'slugify';
7
+ import { nanoid } from 'nanoid';
8
+
9
+ import { useSanityClient } from '../hooks/useSanityClient';
10
+ import StatusDisplay from './StatusDisplay';
11
+
12
+ /**
13
+ * Generates Full Family, Uprights, Italics, Subfamily collections, and Regular/Italic pairs
14
+ * from a typeface's linked fonts. Replaces existing collections and pairs respectively.
15
+ */
16
+ export const GenerateCollectionsPairsComponent = () => {
17
+ const [status, setStatus] = useState('ready');
18
+ const [ready, setReady] = useState(true);
19
+ const [collectionPrice, setCollectionPrice] = useState(
20
+ process.env.SANITY_STUDIO_DEFAULT_COLLECTION_PRICE || 20
21
+ );
22
+ const [pairPrice, setPairPrice] = useState(
23
+ process.env.SANITY_STUDIO_DEFAULT_PAIR_PRICE || 75
24
+ );
25
+
26
+ const client = useSanityClient();
27
+ const doc_id = useFormValue(['_id']);
28
+ const title = useFormValue(['title']);
29
+ const slug = useFormValue(['slug']);
30
+ const stylesObject = useFormValue(['styles']);
31
+
32
+ /** Creates or replaces a collection document and returns a weak reference to it. */
33
+ const createSanityCollection = useCallback(async (fontsList, collectionSlug, newTitle) => {
34
+ const newSlug = collectionSlug.toLowerCase().trim();
35
+
36
+ const fontRefs = fontsList.map(font => ({
37
+ _key: nanoid(),
38
+ _type: 'reference',
39
+ _ref: font._id ?? font._ref,
40
+ _weak: true,
41
+ }));
42
+
43
+ let preferredStyle = { weight: fontsList[0].weight, style: fontsList[0].style, _ref: fontsList[0]._ref };
44
+ fontsList.forEach(font => {
45
+ if (Number(font.weight) < Number(preferredStyle.weight)) return;
46
+ if (Number(font.weight) === Number(preferredStyle.weight) && preferredStyle.style === 'Italic' && font.style === 'Regular') {
47
+ preferredStyle = { weight: font.weight, style: font.style, _ref: font._id };
48
+ } else if (Number(font.weight) > Number(preferredStyle.weight)) {
49
+ preferredStyle = { weight: font.weight, style: font.style, _ref: font._id };
50
+ }
51
+ });
52
+
53
+ const price = (collectionPrice ? Number(collectionPrice) : 0) * fontRefs.length;
54
+
55
+ await client.createOrReplace({
56
+ _key: nanoid(),
57
+ _id: newSlug,
58
+ _type: 'collection',
59
+ state: 'active',
60
+ type: 'collection',
61
+ preferredStyle: { _type: 'reference', _ref: preferredStyle._ref, _weak: true },
62
+ title: newTitle,
63
+ slug: { _type: 'slug', current: newSlug },
64
+ fonts: fontRefs,
65
+ price,
66
+ }).catch(err => { console.error('Error creating collection:', err.message); });
67
+
68
+ return { _ref: newSlug, _type: 'reference', _weak: true, _key: nanoid() };
69
+ }, [collectionPrice, client]);
70
+
71
+ /** Creates or replaces a pair document and returns a weak reference to it. */
72
+ const createSanityPair = useCallback(async (pair, pairSlug, newTitle) => {
73
+ const newSlug = pairSlug.toLowerCase().trim();
74
+
75
+ const fontRefs = pair.map(font => ({
76
+ _key: nanoid(),
77
+ _type: 'reference',
78
+ _ref: font._id,
79
+ _weak: true,
80
+ }));
81
+
82
+ await client.createOrReplace({
83
+ _key: nanoid(),
84
+ _id: newSlug,
85
+ _type: 'pair',
86
+ preferredStyle: { _type: 'reference', _ref: fontRefs[0]._ref, _weak: true },
87
+ title: newTitle,
88
+ slug: { _type: 'slug', current: newSlug },
89
+ fonts: fontRefs,
90
+ price: pairPrice ? Number(pairPrice) : 0,
91
+ }).catch(err => { console.error('Error creating pair:', err.message); });
92
+
93
+ return { _ref: newSlug, _type: 'reference', _weak: true, _key: nanoid() };
94
+ }, [pairPrice, client]);
95
+
96
+ /** Generates Full Family, Uprights, Italics, and Subfamily collections. */
97
+ const handleGenerateCollections = useCallback(async () => {
98
+ setStatus('Generating collections...');
99
+ setReady(false);
100
+ try {
101
+ const result = await client.fetch(
102
+ `*[_type == "typeface" && _id == $id]{ "fonts": styles.fonts[] -> }[0]`,
103
+ { id: doc_id }
104
+ );
105
+ const sanityFonts = result?.fonts ?? [];
106
+
107
+ const subfamilies = stylesObject?.subfamilies ?? [];
108
+ const totalCollections = subfamilies.length + 3;
109
+
110
+ const fullFamily = [], uprights = [], italics = [];
111
+ for (const font of sanityFonts) {
112
+ fullFamily.push(font);
113
+ if (font.style === 'Regular') uprights.push(font);
114
+ else italics.push(font);
115
+ }
116
+
117
+ const typefacePatch = [];
118
+
119
+ if (fullFamily.length > 1) {
120
+ setStatus(`[1/${totalCollections}] Generating full family collection`);
121
+ typefacePatch.push(await createSanityCollection(fullFamily, `${slug.current}-full-family`, `${title} Full Family`));
122
+ }
123
+ if (uprights.length > 1) {
124
+ setStatus(`[2/${totalCollections}] Generating uprights collection`);
125
+ const ref = await createSanityCollection(uprights, `${slug.current}-uprights`, `${title} Uprights`);
126
+ if (ref) typefacePatch.push(ref);
127
+ }
128
+ if (italics.length > 1) {
129
+ setStatus(`[3/${totalCollections}] Generating italics collection`);
130
+ const ref = await createSanityCollection(italics, `${slug.current}-italics`, `${title} Italics`);
131
+ if (ref) typefacePatch.push(ref);
132
+ }
133
+ for (let i = 0; i < subfamilies.length; i++) {
134
+ setStatus(`[${i + 4}/${totalCollections}] Generating ${subfamilies[i].title} collection`);
135
+ const ref = await createSanityCollection(
136
+ subfamilies[i].fonts,
137
+ `${slug.current}-${slugify(subfamilies[i].title)}-family`,
138
+ `${title} ${subfamilies[i].title} Family`
139
+ );
140
+ if (ref) typefacePatch.push(ref);
141
+ }
142
+
143
+ await client.patch(doc_id).set({ styles: { ...stylesObject, collections: typefacePatch } }).commit();
144
+ setStatus('Collections generated');
145
+ } catch (err) {
146
+ console.error('Error generating collections:', err);
147
+ setStatus('Error generating collections');
148
+ }
149
+ setReady(true);
150
+ }, [doc_id, title, slug, stylesObject, collectionPrice, client, createSanityCollection]);
151
+
152
+ /** Generates Regular/Italic pairs matched by subfamily and weight. */
153
+ const handleGeneratePairs = useCallback(async () => {
154
+ setStatus('Generating pairs...');
155
+ setReady(false);
156
+ try {
157
+ const result = await client.fetch(
158
+ `*[_type == "typeface" && _id == $id]{ "fonts": styles.fonts[] -> }[0]`,
159
+ { id: doc_id }
160
+ );
161
+ const sanityFonts = result?.fonts ?? [];
162
+
163
+ const regular = [], italic = [];
164
+ for (const font of sanityFonts) {
165
+ if (font.style === 'Regular') regular.push(font);
166
+ else italic.push(font);
167
+ }
168
+
169
+ const pairs = [];
170
+ for (const reg of regular) {
171
+ for (const ita of italic) {
172
+ if (ita.subfamily === reg.subfamily && ita.weight === reg.weight && ita.weightName === reg.weightName) {
173
+ pairs.push([reg, ita]);
174
+ }
175
+ }
176
+ }
177
+
178
+ const typefacePatch = [];
179
+ for (let i = 0; i < pairs.length; i++) {
180
+ const [reg] = pairs[i];
181
+ let pairSlug, pairTitle;
182
+ if (reg.subfamily && reg.subfamily !== '') {
183
+ if (reg.subfamily === 'Regular') {
184
+ pairSlug = `${slug.current}-${slugify(reg.weightName)}s`;
185
+ pairTitle = `${title} ${reg.weightName}s`;
186
+ } else {
187
+ pairSlug = `${slug.current}-${slugify(reg.subfamily)}-${slugify(reg.weightName)}s`;
188
+ pairTitle = `${title} ${reg.subfamily} ${reg.weightName}s`;
189
+ }
190
+ } else {
191
+ pairSlug = `${slug.current}-${slugify(reg.weightName)}s`;
192
+ pairTitle = `${title} ${reg.weightName}s`;
193
+ }
194
+ setStatus(`[${i + 1}/${pairs.length}] Generating ${pairTitle}`);
195
+ const ref = await createSanityPair(pairs[i], pairSlug, pairTitle);
196
+ if (ref) typefacePatch.push(ref);
197
+ }
198
+
199
+ const preferredStyle = regular[0]?._id ?? '';
200
+ await client.patch(doc_id).set({
201
+ preferredStyle: { _ref: preferredStyle, _type: 'reference', _weak: true },
202
+ styles: { ...stylesObject, pairs: typefacePatch },
203
+ }).commit();
204
+
205
+ setStatus('Pairs generated');
206
+ } catch (err) {
207
+ console.error('Error generating pairs:', err);
208
+ setStatus('Error generating pairs');
209
+ }
210
+ setReady(true);
211
+ }, [doc_id, title, slug, stylesObject, pairPrice, client, createSanityPair]);
212
+
213
+ if (!title || !slug) return null;
214
+
215
+ return (
216
+ <Stack space={2}>
217
+ <StatusDisplay status={status} error={false} />
218
+ <Card border padding={2} shadow={1} radius={2}>
219
+ {ready ? (
220
+ <Stack space={3}>
221
+ <Grid columns={[2]} gap={4} marginTop={1} marginBottom={1}>
222
+ <Stack space={2}>
223
+ <Text size={1} muted>Collection price / font</Text>
224
+ <Flex align="center" gap={2}>
225
+ <Text size={1} muted>$</Text>
226
+ <input
227
+ value={collectionPrice}
228
+ onChange={(e) => setCollectionPrice(e.target.value)}
229
+ type="number"
230
+ style={{ textAlign: 'end', padding: '5px', maxWidth: '75px' }}
231
+ />
232
+ </Flex>
233
+ </Stack>
234
+ <Stack space={2}>
235
+ <Text size={1} muted>Pair price</Text>
236
+ <Flex align="center" gap={2}>
237
+ <Text size={1} muted>$</Text>
238
+ <input
239
+ value={pairPrice}
240
+ onChange={(e) => setPairPrice(e.target.value)}
241
+ type="number"
242
+ style={{ textAlign: 'end', padding: '5px', maxWidth: '75px' }}
243
+ />
244
+ </Flex>
245
+ </Stack>
246
+ </Grid>
247
+ <Button mode="ghost" tone="primary" text="Generate Collections" style={{ width: '100%' }} onClick={handleGenerateCollections} />
248
+ <Button mode="ghost" tone="primary" text="Generate Pairs" style={{ width: '100%' }} onClick={handleGeneratePairs} />
249
+ </Stack>
250
+ ) : (
251
+ <Flex align="center" justify="center" gap={3} padding={4}>
252
+ <Spinner />
253
+ <Text muted size={1}>{status}</Text>
254
+ </Flex>
255
+ )}
256
+ </Card>
257
+ </Stack>
258
+ );
259
+ };