@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
@@ -0,0 +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={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', 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
+ }
@@ -0,0 +1,196 @@
1
+ // Post-upload summary — results display with retry for failed fonts and typeface patch
2
+
3
+ import React, { useState, useCallback } from 'react';
4
+ import { Stack, Flex, Text, Card, Badge, Button, Box, Spinner } from '@sanity/ui';
5
+ import { CheckmarkCircleIcon, WarningOutlineIcon, ResetIcon } from '@sanity/icons';
6
+ import { updateTypefaceDocument } from '../utils/updateTypefaceDocument';
7
+
8
+ /**
9
+ * Post-upload summary — shows execution results with retry options.
10
+ */
11
+ export default function UploadSummary({
12
+ plan,
13
+ result,
14
+ onClose,
15
+ onRetry,
16
+ client,
17
+ docId,
18
+ stylesObject,
19
+ preferredStyleRef,
20
+ }) {
21
+ const [retryingPatch, setRetryingPatch] = useState(false);
22
+ const [patchRetryResult, setPatchRetryResult] = useState(null);
23
+
24
+ const hasFailedFonts = result?.failedFonts?.length > 0;
25
+ const hasTypefacePatchError = result?.typefacePatchError && !patchRetryResult?.success;
26
+ const allSuccess = result?.success && !hasTypefacePatchError;
27
+
28
+ /** Retry the typeface document patch only */
29
+ const handleRetryTypefacePatch = useCallback(async () => {
30
+ if (!result || !client || !docId) return;
31
+ setRetryingPatch(true);
32
+ setPatchRetryResult(null);
33
+
34
+ try {
35
+ const subfamilies = {};
36
+ const uniqueSubfamilies = new Set();
37
+ for (const entry of Object.values(plan.fonts)) {
38
+ if (entry.status === 'error') continue;
39
+ subfamilies[entry.documentId] = entry.subfamily;
40
+ if (entry.subfamily) uniqueSubfamilies.add(entry.subfamily);
41
+ }
42
+
43
+ await updateTypefaceDocument(
44
+ docId,
45
+ result.fontRefs || [],
46
+ result.variableRefs || [],
47
+ subfamilies,
48
+ [...uniqueSubfamilies],
49
+ stylesObject?.subfamilies || [],
50
+ preferredStyleRef || {},
51
+ { weight: -100, style: 'Italic', _ref: result.fontRefs?.[0]?._ref || '' },
52
+ stylesObject || {},
53
+ client,
54
+ () => {},
55
+ () => {},
56
+ );
57
+
58
+ setPatchRetryResult({ success: true });
59
+ } catch (err) {
60
+ console.error('Typeface patch retry failed:', err);
61
+ setPatchRetryResult({ success: false, error: err.message });
62
+ }
63
+
64
+ setRetryingPatch(false);
65
+ }, [result, client, docId, plan, stylesObject, preferredStyleRef]);
66
+
67
+ return (
68
+ <Stack space={4}>
69
+ {/* Header */}
70
+ <Flex align="center" gap={3} ref={(el) => el?.focus?.()} tabIndex={-1}>
71
+ {allSuccess ? (
72
+ <CheckmarkCircleIcon style={{ color: '#43b649', fontSize: 28 }} />
73
+ ) : (
74
+ <WarningOutlineIcon style={{ color: '#f03e2f', fontSize: 28 }} />
75
+ )}
76
+ <Text size={2} weight="semibold">
77
+ {allSuccess ? 'Upload Complete' : 'Upload Completed with Issues'}
78
+ </Text>
79
+ </Flex>
80
+
81
+ {/* Stats */}
82
+ {result && (
83
+ <Card border padding={4} radius={2}>
84
+ <Stack space={3}>
85
+ <Flex gap={2} wrap="wrap">
86
+ {result.created > 0 && (
87
+ <Badge tone="positive" fontSize={1}>
88
+ {result.created} created
89
+ </Badge>
90
+ )}
91
+ {result.updated > 0 && (
92
+ <Badge tone="caution" fontSize={1}>
93
+ {result.updated} updated
94
+ </Badge>
95
+ )}
96
+ {result.failed > 0 && (
97
+ <Badge tone="critical" fontSize={1}>
98
+ {result.failed} failed
99
+ </Badge>
100
+ )}
101
+ {result.skipped > 0 && (
102
+ <Badge mode="outline" fontSize={1}>
103
+ {result.skipped} skipped
104
+ </Badge>
105
+ )}
106
+ </Flex>
107
+ </Stack>
108
+ </Card>
109
+ )}
110
+
111
+ {/* Failed fonts */}
112
+ {hasFailedFonts && (
113
+ <Stack space={3}>
114
+ <Flex align="center" justify="space-between">
115
+ <Flex align="center" gap={2}>
116
+ <Text size={1} weight="semibold">Failed Fonts</Text>
117
+ <Badge tone="critical" fontSize={0}>{result.failedFonts.length}</Badge>
118
+ </Flex>
119
+ <Button
120
+ mode="ghost"
121
+ tone="primary"
122
+ icon={ResetIcon}
123
+ text="Retry Failed"
124
+ fontSize={1}
125
+ padding={2}
126
+ onClick={() => onRetry(result.failedFonts.map(f => f.tempId).filter(Boolean))}
127
+ />
128
+ </Flex>
129
+ <Stack space={2}>
130
+ {result.failedFonts.map((f, i) => (
131
+ <Card key={i} tone="critical" border padding={3} radius={2}>
132
+ <Stack space={2}>
133
+ <Flex align="center" gap={2}>
134
+ <Text size={1} weight="semibold">{f.title}</Text>
135
+ {f.failedAt && f.failedAt !== 'unknown' && (
136
+ <Badge tone="critical" fontSize={0} mode="outline">Failed at: {f.failedAt}</Badge>
137
+ )}
138
+ </Flex>
139
+ <Text size={1}>{f.error}</Text>
140
+ </Stack>
141
+ </Card>
142
+ ))}
143
+ </Stack>
144
+ </Stack>
145
+ )}
146
+
147
+ {/* Typeface patch error */}
148
+ {hasTypefacePatchError && (
149
+ <Card tone="caution" border padding={4} radius={2}>
150
+ <Stack space={3}>
151
+ <Text size={1} weight="semibold">Typeface Document Not Updated</Text>
152
+ <Text size={1}>
153
+ {result.created + result.updated} font document{result.created + result.updated === 1 ? '' : 's'} created/updated successfully, but the typeface document could not be patched to reference them.
154
+ </Text>
155
+ <Text size={1} muted>{result.typefacePatchError}</Text>
156
+ <Flex gap={2}>
157
+ <Button
158
+ mode="default"
159
+ tone="primary"
160
+ icon={retryingPatch ? undefined : ResetIcon}
161
+ text={retryingPatch ? 'Retrying...' : 'Retry Typeface Patch'}
162
+ disabled={retryingPatch}
163
+ onClick={handleRetryTypefacePatch}
164
+ />
165
+ {retryingPatch && <Spinner />}
166
+ </Flex>
167
+ </Stack>
168
+ </Card>
169
+ )}
170
+
171
+ {/* Successful typeface patch retry */}
172
+ {patchRetryResult?.success && (
173
+ <Card tone="positive" border padding={3} radius={2}>
174
+ <Text size={1}>Typeface document updated successfully on retry.</Text>
175
+ </Card>
176
+ )}
177
+
178
+ {/* Failed typeface patch retry */}
179
+ {patchRetryResult && !patchRetryResult.success && (
180
+ <Card tone="critical" border padding={3} radius={2}>
181
+ <Text size={1}>Retry failed: {patchRetryResult.error}</Text>
182
+ </Card>
183
+ )}
184
+
185
+ {/* Close */}
186
+ <Flex justify="flex-end">
187
+ <Button
188
+ mode="default"
189
+ tone="primary"
190
+ text="Close"
191
+ onClick={onClose}
192
+ />
193
+ </Flex>
194
+ </Stack>
195
+ );
196
+ }