@liiift-studio/sanity-font-manager 2.6.0 → 2.7.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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +695 -437
  3. package/dist/index.js +1628 -17701
  4. package/dist/index.mjs +1537 -17599
  5. package/package.json +83 -83
  6. package/src/components/BatchUploadFonts.jsx +655 -653
  7. package/src/components/BulkActions.jsx +99 -99
  8. package/src/components/ExistingDocumentResolver.jsx +152 -152
  9. package/src/components/FontReviewCard.jsx +455 -455
  10. package/src/components/FontScriptUploaderComponent.jsx +463 -463
  11. package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
  12. package/src/components/KeyValueInput.jsx +95 -95
  13. package/src/components/KeyValueReferenceInput.jsx +267 -267
  14. package/src/components/NestedObjectArraySelector.jsx +146 -146
  15. package/src/components/PriceInput.jsx +26 -26
  16. package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
  17. package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
  18. package/src/components/SetOTF.jsx +87 -87
  19. package/src/components/SingleUploaderTool.jsx +674 -674
  20. package/src/components/StatusDisplay.jsx +26 -26
  21. package/src/components/StyleCountInput.jsx +16 -16
  22. package/src/components/UpdateScriptsComponent.jsx +76 -76
  23. package/src/components/UploadButton.jsx +43 -43
  24. package/src/components/UploadModal.jsx +309 -304
  25. package/src/components/UploadScriptsComponent.jsx +539 -539
  26. package/src/components/UploadStep1Settings.jsx +272 -272
  27. package/src/components/UploadStep2Review.jsx +478 -474
  28. package/src/components/UploadStep3Execute.jsx +234 -234
  29. package/src/components/UploadStep3bInstances.jsx +396 -396
  30. package/src/components/UploadSummary.jsx +196 -196
  31. package/src/components/VariableInstanceReferencesInput.jsx +190 -190
  32. package/src/hooks/useNestedObjects.js +92 -92
  33. package/src/hooks/useSanityClient.js +9 -9
  34. package/src/index.js +120 -120
  35. package/src/schema/openTypeField.js +1995 -1995
  36. package/src/schema/styleCountField.js +12 -12
  37. package/src/schema/stylesField.js +302 -302
  38. package/src/schema/stylisticSetField.js +301 -301
  39. package/src/utils/buildUploadPlan.js +326 -326
  40. package/src/utils/executeUploadPlan.js +430 -430
  41. package/src/utils/executionReducer.js +56 -56
  42. package/src/utils/fontHelpers.js +281 -267
  43. package/src/utils/generateCssFile.js +207 -207
  44. package/src/utils/generateFontData.js +98 -98
  45. package/src/utils/generateFontFile.js +38 -38
  46. package/src/utils/generateKeywords.js +185 -185
  47. package/src/utils/generateSubset.js +45 -45
  48. package/src/utils/getEmptyFontKit.js +101 -101
  49. package/src/utils/parseFont.js +56 -56
  50. package/src/utils/parseVariableFontInstances.js +301 -301
  51. package/src/utils/planReducer.js +531 -517
  52. package/src/utils/planTypes.js +183 -183
  53. package/src/utils/processFontFiles.js +530 -530
  54. package/src/utils/regenerateFontData.js +146 -146
  55. package/src/utils/resolveExistingFont.js +87 -87
  56. package/src/utils/retitleFontEntries.js +154 -0
  57. package/src/utils/sanitizeForSanityId.js +65 -65
  58. package/src/utils/setupDecompressors.js +27 -27
  59. package/src/utils/updateFontPrices.js +94 -94
  60. package/src/utils/updateTypefaceDocument.js +162 -162
  61. package/src/utils/uploadFontFiles.js +405 -405
  62. package/src/utils/utils.js +24 -24
@@ -1,234 +1,234 @@
1
- // Step 3 — Upload execution with per-font progress
2
-
3
- import React, { useEffect, useReducer, useRef, useState, useMemo } from 'react';
4
- import { Box, Stack, Flex, Text, Card, Spinner, Badge } from '@sanity/ui';
5
- import { WarningOutlineIcon, CheckmarkCircleIcon } from '@sanity/icons';
6
- import { executeUploadPlan } from '../utils/executeUploadPlan';
7
- import { executionReducer, createInitialExecutionState } from '../utils/executionReducer';
8
- import { EXECUTION_STATUS } from '../utils/planTypes';
9
-
10
- /** Formats elapsed seconds as "Xm Ys" or "Ys" */
11
- const formatElapsed = (s) => {
12
- const m = Math.floor(s / 60);
13
- const sec = s % 60;
14
- return m > 0 ? `${m}m ${sec}s` : `${sec}s`;
15
- };
16
-
17
- /**
18
- * Step 3 — executes the upload plan, showing per-font progress.
19
- * Uses a separate executionReducer to avoid re-rendering the review UI.
20
- */
21
- export default function UploadStep3Execute({
22
- plan,
23
- client,
24
- docId,
25
- stylesObject,
26
- preferredStyleRef,
27
- retryTempIds,
28
- onComplete,
29
- }) {
30
- const [execState, execDispatch] = useReducer(executionReducer, null, createInitialExecutionState);
31
- const [result, setResult] = useState(null);
32
- const [elapsedSeconds, setElapsedSeconds] = useState(0);
33
- const startedRef = useRef(false);
34
- const timerRef = useRef(null);
35
- const wakeLockRef = useRef(null);
36
-
37
- // Build the execution plan — filter to only failed fonts when retrying
38
- const executionPlan = useMemo(() => {
39
- if (!retryTempIds) return plan;
40
- return {
41
- ...plan,
42
- fonts: Object.fromEntries(
43
- Object.entries(plan.fonts).filter(([tempId]) => retryTempIds.includes(tempId))
44
- ),
45
- };
46
- }, [plan, retryTempIds]);
47
-
48
- const fontEntries = useMemo(() =>
49
- Object.values(executionPlan.fonts).filter(f => f.status !== 'error'),
50
- [executionPlan]
51
- );
52
-
53
- // Start execution once on mount
54
- useEffect(() => {
55
- if (startedRef.current) return;
56
- startedRef.current = true;
57
-
58
- // Start elapsed timer
59
- timerRef.current = setInterval(() => setElapsedSeconds(s => s + 1), 1000);
60
-
61
- // Request wake lock
62
- navigator.wakeLock?.request('screen')
63
- .then(lock => { wakeLockRef.current = lock; })
64
- .catch(() => {});
65
-
66
- execDispatch({ type: 'SET_EXECUTION_STATUS', status: 'uploading' });
67
-
68
- executeUploadPlan({
69
- plan: executionPlan,
70
- client,
71
- docId,
72
- stylesObject,
73
- preferredStyleRef,
74
- onProgress: (event) => {
75
- if (event.type === 'font-upload-start' || event.type === 'file-uploaded' ||
76
- event.type === 'css-generated' || event.type === 'metadata-generated' ||
77
- event.type === 'document-created') {
78
- if (event.fontProgress) {
79
- execDispatch({
80
- type: 'SET_FONT_EXECUTION_PROGRESS',
81
- tempId: event.tempId,
82
- progress: event.fontProgress,
83
- });
84
- }
85
- } else if (event.type === 'typeface-patching') {
86
- execDispatch({ type: 'SET_EXECUTION_STATUS', status: 'patching-typeface' });
87
- } else if (event.type === 'typeface-error') {
88
- execDispatch({ type: 'SET_EXECUTION_ERROR', error: event.error });
89
- }
90
- },
91
- }).then((executionResult) => {
92
- setResult(executionResult);
93
- clearInterval(timerRef.current);
94
- wakeLockRef.current?.release().catch(() => {});
95
- execDispatch({ type: 'SET_EXECUTION_STATUS', status: executionResult.success ? 'complete' : 'error' });
96
- onComplete(executionResult);
97
- }).catch((err) => {
98
- clearInterval(timerRef.current);
99
- wakeLockRef.current?.release().catch(() => {});
100
- execDispatch({ type: 'SET_EXECUTION_ERROR', error: err.message });
101
- const errorResult = {
102
- success: false, created: 0, updated: 0, failed: 1, skipped: 0,
103
- failedFonts: [{ title: 'Unknown', tempId: 'unknown', error: err.message, failedAt: 'unknown' }],
104
- fontRefs: [], variableRefs: [], typefacePatchError: err.message,
105
- };
106
- setResult(errorResult);
107
- onComplete(errorResult);
108
- });
109
-
110
- return () => {
111
- clearInterval(timerRef.current);
112
- wakeLockRef.current?.release().catch(() => {});
113
- };
114
- }, []);
115
-
116
- const completedCount = Object.values(execState.progress).filter(p =>
117
- p.status === EXECUTION_STATUS.COMPLETE || p.status === EXECUTION_STATUS.ERROR
118
- ).length;
119
-
120
- return (
121
- <Stack space={4}>
122
- {/* Global progress */}
123
- <Card border padding={3} radius={2}>
124
- <Stack space={3}>
125
- <Flex align="center" gap={3}>
126
- {execState.status === 'complete'
127
- ? <CheckmarkCircleIcon style={{ color: '#43b649', fontSize: 20 }} />
128
- : execState.status === 'error'
129
- ? <WarningOutlineIcon style={{ color: 'var(--card-badge-critical-bg-color)', fontSize: 20 }} />
130
- : <Spinner />
131
- }
132
- <Text size={1} weight="semibold">
133
- {execState.status === 'patching-typeface'
134
- ? 'Updating typeface document...'
135
- : execState.status === 'complete'
136
- ? 'Upload complete'
137
- : execState.status === 'error'
138
- ? 'Upload failed'
139
- : `Uploading ${completedCount} of ${fontEntries.length} fonts...`
140
- }
141
- </Text>
142
- <Text size={1} muted style={{ marginLeft: 'auto' }}>{formatElapsed(elapsedSeconds)}</Text>
143
- </Flex>
144
-
145
- {/* Progress bar */}
146
- <Box style={{ height: 4, background: 'var(--card-border-color)', borderRadius: 2, overflow: 'hidden' }}>
147
- <Box
148
- style={{
149
- height: '100%',
150
- width: '100%',
151
- transformOrigin: 'left',
152
- transform: `scaleX(${fontEntries.length > 0 ? completedCount / fontEntries.length : 0})`,
153
- background: execState.status === 'error'
154
- ? 'var(--card-badge-critical-bg-color)'
155
- : 'var(--card-badge-positive-bg-color)',
156
- transition: 'transform 0.3s ease-out',
157
- borderRadius: 2,
158
- }}
159
- />
160
- </Box>
161
- </Stack>
162
- </Card>
163
-
164
- {/* Do-not-close warning */}
165
- {execState.status !== 'complete' && execState.status !== 'error' && (
166
- <Card tone="caution" border radius={2} padding={2}>
167
- <Flex align="center" gap={2}>
168
- <WarningOutlineIcon style={{ flexShrink: 0 }} />
169
- <Text size={1} weight="semibold">Do not close or reload this tab</Text>
170
- </Flex>
171
- </Card>
172
- )}
173
-
174
- {/* Per-font progress list */}
175
- <Box style={{ maxHeight: 400, overflowY: 'auto' }}>
176
- <Stack space={1}>
177
- {fontEntries.map(entry => {
178
- const progress = execState.progress[entry.tempId];
179
- const status = progress?.status || EXECUTION_STATUS.QUEUED;
180
-
181
- return (
182
- <Card key={entry.tempId} border radius={1} padding={2}>
183
- <Flex align="center" gap={2}>
184
- <Text size={1} style={{ flex: 1 }}>
185
- {entry.title}
186
- </Text>
187
- <Box style={{ width: 120, flexShrink: 0, textAlign: 'right' }}>
188
- {status === EXECUTION_STATUS.QUEUED && (
189
- <Badge mode="outline" fontSize={0}>Queued</Badge>
190
- )}
191
- {status === EXECUTION_STATUS.UPLOADING_ASSETS && (
192
- <Flex gap={1} align="center" justify="flex-end">
193
- <Text size={0} muted>{progress?.currentFile || 'Uploading...'}</Text>
194
- <Spinner style={{ width: 12, height: 12 }} />
195
- </Flex>
196
- )}
197
- {(status === EXECUTION_STATUS.GENERATING_CSS || status === EXECUTION_STATUS.GENERATING_METADATA) && (
198
- <Flex gap={1} align="center" justify="flex-end">
199
- <Text size={0} muted>{status === EXECUTION_STATUS.GENERATING_CSS ? 'CSS' : 'Metadata'}</Text>
200
- <Spinner style={{ width: 12, height: 12 }} />
201
- </Flex>
202
- )}
203
- {status === EXECUTION_STATUS.CREATING_DOCUMENT && (
204
- <Flex gap={1} align="center" justify="flex-end">
205
- <Text size={0} muted>Creating doc</Text>
206
- <Spinner style={{ width: 12, height: 12 }} />
207
- </Flex>
208
- )}
209
- {status === EXECUTION_STATUS.COMPLETE && (
210
- <Badge tone="positive" fontSize={0}>Done</Badge>
211
- )}
212
- {status === EXECUTION_STATUS.ERROR && (
213
- <Badge tone="critical" fontSize={0}>Failed</Badge>
214
- )}
215
- </Box>
216
- </Flex>
217
- {status === EXECUTION_STATUS.ERROR && progress?.error && (
218
- <Text size={0} muted style={{ marginTop: 4 }}>{progress.error}</Text>
219
- )}
220
- </Card>
221
- );
222
- })}
223
- </Stack>
224
- </Box>
225
-
226
- {/* Error details */}
227
- {execState.error && (
228
- <Card tone="critical" border padding={3} radius={2}>
229
- <Text size={1}>{execState.error}</Text>
230
- </Card>
231
- )}
232
- </Stack>
233
- );
234
- }
1
+ // Step 3 — Upload execution with per-font progress
2
+
3
+ import React, { useEffect, useReducer, useRef, useState, useMemo } from 'react';
4
+ import { Box, Stack, Flex, Text, Card, Spinner, Badge } from '@sanity/ui';
5
+ import { WarningOutlineIcon, CheckmarkCircleIcon } from '@sanity/icons';
6
+ import { executeUploadPlan } from '../utils/executeUploadPlan';
7
+ import { executionReducer, createInitialExecutionState } from '../utils/executionReducer';
8
+ import { EXECUTION_STATUS } from '../utils/planTypes';
9
+
10
+ /** Formats elapsed seconds as "Xm Ys" or "Ys" */
11
+ const formatElapsed = (s) => {
12
+ const m = Math.floor(s / 60);
13
+ const sec = s % 60;
14
+ return m > 0 ? `${m}m ${sec}s` : `${sec}s`;
15
+ };
16
+
17
+ /**
18
+ * Step 3 — executes the upload plan, showing per-font progress.
19
+ * Uses a separate executionReducer to avoid re-rendering the review UI.
20
+ */
21
+ export default function UploadStep3Execute({
22
+ plan,
23
+ client,
24
+ docId,
25
+ stylesObject,
26
+ preferredStyleRef,
27
+ retryTempIds,
28
+ onComplete,
29
+ }) {
30
+ const [execState, execDispatch] = useReducer(executionReducer, null, createInitialExecutionState);
31
+ const [result, setResult] = useState(null);
32
+ const [elapsedSeconds, setElapsedSeconds] = useState(0);
33
+ const startedRef = useRef(false);
34
+ const timerRef = useRef(null);
35
+ const wakeLockRef = useRef(null);
36
+
37
+ // Build the execution plan — filter to only failed fonts when retrying
38
+ const executionPlan = useMemo(() => {
39
+ if (!retryTempIds) return plan;
40
+ return {
41
+ ...plan,
42
+ fonts: Object.fromEntries(
43
+ Object.entries(plan.fonts).filter(([tempId]) => retryTempIds.includes(tempId))
44
+ ),
45
+ };
46
+ }, [plan, retryTempIds]);
47
+
48
+ const fontEntries = useMemo(() =>
49
+ Object.values(executionPlan.fonts).filter(f => f.status !== 'error'),
50
+ [executionPlan]
51
+ );
52
+
53
+ // Start execution once on mount
54
+ useEffect(() => {
55
+ if (startedRef.current) return;
56
+ startedRef.current = true;
57
+
58
+ // Start elapsed timer
59
+ timerRef.current = setInterval(() => setElapsedSeconds(s => s + 1), 1000);
60
+
61
+ // Request wake lock
62
+ navigator.wakeLock?.request('screen')
63
+ .then(lock => { wakeLockRef.current = lock; })
64
+ .catch(() => {});
65
+
66
+ execDispatch({ type: 'SET_EXECUTION_STATUS', status: 'uploading' });
67
+
68
+ executeUploadPlan({
69
+ plan: executionPlan,
70
+ client,
71
+ docId,
72
+ stylesObject,
73
+ preferredStyleRef,
74
+ onProgress: (event) => {
75
+ if (event.type === 'font-upload-start' || event.type === 'file-uploaded' ||
76
+ event.type === 'css-generated' || event.type === 'metadata-generated' ||
77
+ event.type === 'document-created') {
78
+ if (event.fontProgress) {
79
+ execDispatch({
80
+ type: 'SET_FONT_EXECUTION_PROGRESS',
81
+ tempId: event.tempId,
82
+ progress: event.fontProgress,
83
+ });
84
+ }
85
+ } else if (event.type === 'typeface-patching') {
86
+ execDispatch({ type: 'SET_EXECUTION_STATUS', status: 'patching-typeface' });
87
+ } else if (event.type === 'typeface-error') {
88
+ execDispatch({ type: 'SET_EXECUTION_ERROR', error: event.error });
89
+ }
90
+ },
91
+ }).then((executionResult) => {
92
+ setResult(executionResult);
93
+ clearInterval(timerRef.current);
94
+ wakeLockRef.current?.release().catch(() => {});
95
+ execDispatch({ type: 'SET_EXECUTION_STATUS', status: executionResult.success ? 'complete' : 'error' });
96
+ onComplete(executionResult);
97
+ }).catch((err) => {
98
+ clearInterval(timerRef.current);
99
+ wakeLockRef.current?.release().catch(() => {});
100
+ execDispatch({ type: 'SET_EXECUTION_ERROR', error: err.message });
101
+ const errorResult = {
102
+ success: false, created: 0, updated: 0, failed: 1, skipped: 0,
103
+ failedFonts: [{ title: 'Unknown', tempId: 'unknown', error: err.message, failedAt: 'unknown' }],
104
+ fontRefs: [], variableRefs: [], typefacePatchError: err.message,
105
+ };
106
+ setResult(errorResult);
107
+ onComplete(errorResult);
108
+ });
109
+
110
+ return () => {
111
+ clearInterval(timerRef.current);
112
+ wakeLockRef.current?.release().catch(() => {});
113
+ };
114
+ }, []);
115
+
116
+ const completedCount = Object.values(execState.progress).filter(p =>
117
+ p.status === EXECUTION_STATUS.COMPLETE || p.status === EXECUTION_STATUS.ERROR
118
+ ).length;
119
+
120
+ return (
121
+ <Stack space={4}>
122
+ {/* Global progress */}
123
+ <Card border padding={3} radius={2}>
124
+ <Stack space={3}>
125
+ <Flex align="center" gap={3}>
126
+ {execState.status === 'complete'
127
+ ? <CheckmarkCircleIcon style={{ color: '#43b649', fontSize: 20 }} />
128
+ : execState.status === 'error'
129
+ ? <WarningOutlineIcon style={{ color: 'var(--card-badge-critical-bg-color)', fontSize: 20 }} />
130
+ : <Spinner />
131
+ }
132
+ <Text size={1} weight="semibold">
133
+ {execState.status === 'patching-typeface'
134
+ ? 'Updating typeface document...'
135
+ : execState.status === 'complete'
136
+ ? 'Upload complete'
137
+ : execState.status === 'error'
138
+ ? 'Upload failed'
139
+ : `Uploading ${completedCount} of ${fontEntries.length} fonts...`
140
+ }
141
+ </Text>
142
+ <Text size={1} muted style={{ marginLeft: 'auto' }}>{formatElapsed(elapsedSeconds)}</Text>
143
+ </Flex>
144
+
145
+ {/* Progress bar */}
146
+ <Box style={{ height: 4, background: 'var(--card-border-color)', borderRadius: 2, overflow: 'hidden' }}>
147
+ <Box
148
+ style={{
149
+ height: '100%',
150
+ width: '100%',
151
+ transformOrigin: 'left',
152
+ transform: `scaleX(${fontEntries.length > 0 ? completedCount / fontEntries.length : 0})`,
153
+ background: execState.status === 'error'
154
+ ? 'var(--card-badge-critical-bg-color)'
155
+ : 'var(--card-badge-positive-bg-color)',
156
+ transition: 'transform 0.3s ease-out',
157
+ borderRadius: 2,
158
+ }}
159
+ />
160
+ </Box>
161
+ </Stack>
162
+ </Card>
163
+
164
+ {/* Do-not-close warning */}
165
+ {execState.status !== 'complete' && execState.status !== 'error' && (
166
+ <Card tone="caution" border radius={2} padding={2}>
167
+ <Flex align="center" gap={2}>
168
+ <WarningOutlineIcon style={{ flexShrink: 0 }} />
169
+ <Text size={1} weight="semibold">Do not close or reload this tab</Text>
170
+ </Flex>
171
+ </Card>
172
+ )}
173
+
174
+ {/* Per-font progress list */}
175
+ <Box style={{ maxHeight: 400, overflowY: 'auto' }}>
176
+ <Stack space={1}>
177
+ {fontEntries.map(entry => {
178
+ const progress = execState.progress[entry.tempId];
179
+ const status = progress?.status || EXECUTION_STATUS.QUEUED;
180
+
181
+ return (
182
+ <Card key={entry.tempId} border radius={1} padding={2}>
183
+ <Flex align="center" gap={2}>
184
+ <Text size={1} style={{ flex: 1 }}>
185
+ {entry.title}
186
+ </Text>
187
+ <Box style={{ width: 120, flexShrink: 0, textAlign: 'right' }}>
188
+ {status === EXECUTION_STATUS.QUEUED && (
189
+ <Badge mode="outline" fontSize={0}>Queued</Badge>
190
+ )}
191
+ {status === EXECUTION_STATUS.UPLOADING_ASSETS && (
192
+ <Flex gap={1} align="center" justify="flex-end">
193
+ <Text size={0} muted>{progress?.currentFile || 'Uploading...'}</Text>
194
+ <Spinner style={{ width: 12, height: 12 }} />
195
+ </Flex>
196
+ )}
197
+ {(status === EXECUTION_STATUS.GENERATING_CSS || status === EXECUTION_STATUS.GENERATING_METADATA) && (
198
+ <Flex gap={1} align="center" justify="flex-end">
199
+ <Text size={0} muted>{status === EXECUTION_STATUS.GENERATING_CSS ? 'CSS' : 'Metadata'}</Text>
200
+ <Spinner style={{ width: 12, height: 12 }} />
201
+ </Flex>
202
+ )}
203
+ {status === EXECUTION_STATUS.CREATING_DOCUMENT && (
204
+ <Flex gap={1} align="center" justify="flex-end">
205
+ <Text size={0} muted>Creating doc</Text>
206
+ <Spinner style={{ width: 12, height: 12 }} />
207
+ </Flex>
208
+ )}
209
+ {status === EXECUTION_STATUS.COMPLETE && (
210
+ <Badge tone="positive" fontSize={0}>Done</Badge>
211
+ )}
212
+ {status === EXECUTION_STATUS.ERROR && (
213
+ <Badge tone="critical" fontSize={0}>Failed</Badge>
214
+ )}
215
+ </Box>
216
+ </Flex>
217
+ {status === EXECUTION_STATUS.ERROR && progress?.error && (
218
+ <Text size={0} muted style={{ marginTop: 4 }}>{progress.error}</Text>
219
+ )}
220
+ </Card>
221
+ );
222
+ })}
223
+ </Stack>
224
+ </Box>
225
+
226
+ {/* Error details */}
227
+ {execState.error && (
228
+ <Card tone="critical" border padding={3} radius={2}>
229
+ <Text size={1}>{execState.error}</Text>
230
+ </Card>
231
+ )}
232
+ </Stack>
233
+ );
234
+ }