@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.
- package/README.md +437 -437
- package/dist/UploadModal-6LIX7XOK.js +6 -0
- package/dist/UploadModal-NME2W53V.mjs +6 -0
- package/dist/chunk-646WCBRR.mjs +7276 -0
- package/dist/chunk-FH4QKHOH.js +7276 -0
- package/dist/index.js +747 -1675
- package/dist/index.mjs +400 -1237
- package/package.json +85 -85
- package/src/components/BatchUploadFonts.jsx +653 -639
- package/src/components/BulkActions.jsx +99 -0
- package/src/components/ExistingDocumentResolver.jsx +152 -0
- package/src/components/FontReviewCard.jsx +415 -0
- package/src/components/FontScriptUploaderComponent.jsx +463 -463
- package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
- package/src/components/KeyValueInput.jsx +95 -95
- package/src/components/KeyValueReferenceInput.jsx +254 -254
- package/src/components/NestedObjectArraySelector.jsx +146 -146
- package/src/components/PriceInput.jsx +26 -26
- package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
- package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
- package/src/components/SetOTF.jsx +87 -87
- package/src/components/SingleUploaderTool.jsx +672 -673
- package/src/components/StatusDisplay.jsx +26 -26
- package/src/components/StyleCountInput.jsx +16 -16
- package/src/components/UpdateScriptsComponent.jsx +76 -76
- package/src/components/UploadButton.jsx +43 -43
- package/src/components/UploadModal.jsx +268 -0
- package/src/components/UploadScriptsComponent.jsx +539 -537
- package/src/components/UploadStep1Settings.jsx +272 -0
- package/src/components/UploadStep2Review.jsx +472 -0
- package/src/components/UploadStep3Execute.jsx +234 -0
- package/src/components/UploadSummary.jsx +196 -0
- package/src/components/VariableInstanceReferencesInput.jsx +190 -190
- package/src/hooks/useNestedObjects.js +92 -92
- package/src/hooks/useSanityClient.js +9 -9
- package/src/index.js +115 -70
- package/src/schema/openTypeField.js +1945 -1945
- package/src/schema/styleCountField.js +12 -12
- package/src/schema/stylesField.js +268 -268
- package/src/schema/stylisticSetField.js +301 -301
- package/src/utils/buildUploadPlan.js +325 -0
- package/src/utils/executeUploadPlan.js +437 -0
- package/src/utils/executionReducer.js +56 -0
- package/src/utils/fontHelpers.js +267 -0
- package/src/utils/generateCssFile.js +207 -205
- package/src/utils/generateFontData.js +98 -145
- package/src/utils/generateFontFile.js +38 -38
- package/src/utils/generateKeywords.js +185 -185
- package/src/utils/generateSubset.js +45 -45
- package/src/utils/getEmptyFontKit.js +101 -99
- package/src/utils/parseFont.js +55 -0
- package/src/utils/parseVariableFontInstances.js +211 -211
- package/src/utils/planReducer.js +517 -0
- package/src/utils/planTypes.js +183 -0
- package/src/utils/processFontFiles.js +529 -477
- package/src/utils/regenerateFontData.js +146 -146
- package/src/utils/resolveExistingFont.js +87 -0
- package/src/utils/sanitizeForSanityId.js +65 -65
- package/src/utils/updateFontPrices.js +94 -94
- package/src/utils/updateTypefaceDocument.js +149 -160
- package/src/utils/uploadFontFiles.js +405 -316
- package/src/utils/utils.js +24 -24
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// Step 1 — File upload: drag-and-drop zone, file list table, type breakdown with mismatch detection
|
|
2
|
+
|
|
3
|
+
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
|
4
|
+
import { Box, Flex, Stack, Text, Button, Card, Badge } from '@sanity/ui';
|
|
5
|
+
import { UploadIcon, TrashIcon } from '@sanity/icons';
|
|
6
|
+
|
|
7
|
+
/** Accepted font file extensions */
|
|
8
|
+
const ACCEPTED_EXTENSIONS = ['ttf', 'otf', 'woff', 'woff2', 'eot', 'svg'];
|
|
9
|
+
|
|
10
|
+
/** Sort order: TTF/OTF before web formats so metadata is available for webfont fallback */
|
|
11
|
+
const TYPE_ORDER = ['ttf', 'otf', 'eot', 'svg', 'woff', 'woff2'];
|
|
12
|
+
|
|
13
|
+
/** Returns only files with accepted font extensions */
|
|
14
|
+
const filterFontFiles = (files) =>
|
|
15
|
+
Array.from(files).filter(f => ACCEPTED_EXTENSIONS.includes(f.name.split('.').pop().toLowerCase()));
|
|
16
|
+
|
|
17
|
+
/** Sorts font files so TTF/OTF are processed before web formats */
|
|
18
|
+
const sortFilesByType = (files) =>
|
|
19
|
+
Array.from(files).sort((a, b) => {
|
|
20
|
+
const aIdx = TYPE_ORDER.indexOf(a.name.split('.').pop().toLowerCase());
|
|
21
|
+
const bIdx = TYPE_ORDER.indexOf(b.name.split('.').pop().toLowerCase());
|
|
22
|
+
if (aIdx === bIdx) return a.name.localeCompare(b.name);
|
|
23
|
+
return aIdx - bIdx;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Step 1 component — file selection only. Settings are in Step 2.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} props
|
|
30
|
+
* @param {object} props.settings - Current plan settings
|
|
31
|
+
* @param {function} props.onStartProcessing - Called with (sortedFiles, settings) when user clicks "Process Files"
|
|
32
|
+
*/
|
|
33
|
+
export default function UploadStep1Settings({ settings, onStartProcessing }) {
|
|
34
|
+
const [pendingFiles, setPendingFiles] = useState([]);
|
|
35
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
36
|
+
const [filterType, setFilterType] = useState(null);
|
|
37
|
+
const fileInputRef = useRef(null);
|
|
38
|
+
|
|
39
|
+
const handleFileSelect = useCallback((e) => {
|
|
40
|
+
const files = filterFontFiles(e.target.files);
|
|
41
|
+
if (files.length > 0) setPendingFiles(prev => [...prev, ...files]);
|
|
42
|
+
e.target.value = '';
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const handleRemoveFile = useCallback((file) => {
|
|
46
|
+
setPendingFiles(prev => prev.filter(f => f !== file));
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const handleDragEnter = useCallback((e) => { e.preventDefault(); setIsDragging(true); }, []);
|
|
50
|
+
const handleDragOver = useCallback((e) => { e.preventDefault(); }, []);
|
|
51
|
+
const handleDragLeave = useCallback((e) => { e.preventDefault(); setIsDragging(false); }, []);
|
|
52
|
+
const handleDrop = useCallback((e) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
setIsDragging(false);
|
|
55
|
+
const files = filterFontFiles(e.dataTransfer.files);
|
|
56
|
+
if (files.length > 0) setPendingFiles(prev => [...prev, ...files]);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const handleProcess = useCallback(() => {
|
|
60
|
+
const sorted = sortFilesByType(pendingFiles);
|
|
61
|
+
onStartProcessing(sorted, settings);
|
|
62
|
+
}, [pendingFiles, settings, onStartProcessing]);
|
|
63
|
+
|
|
64
|
+
/** Count files by extension and detect outliers (types whose count differs from the majority) */
|
|
65
|
+
const typeBreakdown = useMemo(() => {
|
|
66
|
+
const counts = {};
|
|
67
|
+
pendingFiles.forEach(f => {
|
|
68
|
+
const ext = f.name.split('.').pop().toLowerCase();
|
|
69
|
+
counts[ext] = (counts[ext] || 0) + 1;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const values = Object.values(counts);
|
|
73
|
+
if (values.length <= 1) return { counts, modeCount: 0, outlierExts: new Set() };
|
|
74
|
+
|
|
75
|
+
// Find the mode (most frequent count) — types matching the mode are normal, others are outliers
|
|
76
|
+
const freq = {};
|
|
77
|
+
values.forEach(v => { freq[v] = (freq[v] || 0) + 1; });
|
|
78
|
+
const modeCount = Number(Object.entries(freq).sort((a, b) => b[1] - a[1])[0][0]);
|
|
79
|
+
|
|
80
|
+
const outlierExts = new Set();
|
|
81
|
+
Object.entries(counts).forEach(([ext, count]) => {
|
|
82
|
+
if (count !== modeCount) outlierExts.add(ext);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return { counts, modeCount, outlierExts };
|
|
86
|
+
}, [pendingFiles]);
|
|
87
|
+
|
|
88
|
+
/** Files filtered by the active type filter — displayed in upload order */
|
|
89
|
+
const displayedFiles = useMemo(() => {
|
|
90
|
+
if (!filterType) return pendingFiles;
|
|
91
|
+
return pendingFiles.filter(f => f.name.split('.').pop().toLowerCase() === filterType);
|
|
92
|
+
}, [pendingFiles, filterType]);
|
|
93
|
+
|
|
94
|
+
const dropZoneStyle = {
|
|
95
|
+
border: `2px dashed ${isDragging ? 'var(--card-focus-ring-color)' : 'var(--card-border-color)'}`,
|
|
96
|
+
borderRadius: 4,
|
|
97
|
+
padding: pendingFiles.length > 0 ? '10px 16px' : '28px 16px',
|
|
98
|
+
textAlign: 'center',
|
|
99
|
+
background: isDragging ? 'rgba(100, 153, 255, 0.06)' : 'transparent',
|
|
100
|
+
transition: 'border-color 0.12s, background 0.12s',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Stack space={4}>
|
|
105
|
+
{/* Drop zone */}
|
|
106
|
+
<Box
|
|
107
|
+
onDragEnter={handleDragEnter}
|
|
108
|
+
onDragOver={handleDragOver}
|
|
109
|
+
onDragLeave={handleDragLeave}
|
|
110
|
+
onDrop={handleDrop}
|
|
111
|
+
style={dropZoneStyle}
|
|
112
|
+
>
|
|
113
|
+
<input
|
|
114
|
+
ref={fileInputRef}
|
|
115
|
+
type="file"
|
|
116
|
+
multiple
|
|
117
|
+
hidden
|
|
118
|
+
accept=".ttf,.otf,.woff,.woff2,.eot,.svg"
|
|
119
|
+
onChange={handleFileSelect}
|
|
120
|
+
/>
|
|
121
|
+
{pendingFiles.length === 0 ? (
|
|
122
|
+
<Stack space={3}>
|
|
123
|
+
<Text size={1} muted>{isDragging ? 'Release to add files' : 'Drop font files here'}</Text>
|
|
124
|
+
<Flex justify="center">
|
|
125
|
+
<Button
|
|
126
|
+
mode="ghost"
|
|
127
|
+
tone="primary"
|
|
128
|
+
fontSize={1}
|
|
129
|
+
padding={2}
|
|
130
|
+
text="Browse files"
|
|
131
|
+
onClick={() => fileInputRef.current?.click()}
|
|
132
|
+
/>
|
|
133
|
+
</Flex>
|
|
134
|
+
</Stack>
|
|
135
|
+
) : (
|
|
136
|
+
<Flex align="center" justify="center" gap={2}>
|
|
137
|
+
<Text size={1} muted>{isDragging ? 'Release to add' : 'Drop more files or'}</Text>
|
|
138
|
+
<Button
|
|
139
|
+
mode="bleed"
|
|
140
|
+
tone="primary"
|
|
141
|
+
fontSize={1}
|
|
142
|
+
padding={1}
|
|
143
|
+
text="browse"
|
|
144
|
+
onClick={() => fileInputRef.current?.click()}
|
|
145
|
+
/>
|
|
146
|
+
</Flex>
|
|
147
|
+
)}
|
|
148
|
+
</Box>
|
|
149
|
+
|
|
150
|
+
{/* File breakdown + list */}
|
|
151
|
+
{pendingFiles.length > 0 && (
|
|
152
|
+
<Stack space={3}>
|
|
153
|
+
{/* Summary: total + type breakdown */}
|
|
154
|
+
<Flex align="center" justify="space-between">
|
|
155
|
+
<Flex align="center" gap={2}>
|
|
156
|
+
<Text size={1} weight="semibold">
|
|
157
|
+
{filterType
|
|
158
|
+
? `${displayedFiles.length} of ${pendingFiles.length} files (${filterType.toUpperCase()})`
|
|
159
|
+
: `${pendingFiles.length} file${pendingFiles.length === 1 ? '' : 's'}`
|
|
160
|
+
}
|
|
161
|
+
</Text>
|
|
162
|
+
<Flex gap={1}>
|
|
163
|
+
{TYPE_ORDER.filter(ext => typeBreakdown.counts[ext]).map(ext => {
|
|
164
|
+
const count = typeBreakdown.counts[ext];
|
|
165
|
+
const isOutlier = typeBreakdown.outlierExts.has(ext);
|
|
166
|
+
const isActive = filterType === ext;
|
|
167
|
+
return (
|
|
168
|
+
<Badge
|
|
169
|
+
key={ext}
|
|
170
|
+
tone={isOutlier ? 'critical' : isActive ? 'primary' : 'default'}
|
|
171
|
+
mode={isActive ? 'default' : isOutlier ? 'default' : 'outline'}
|
|
172
|
+
fontSize={0}
|
|
173
|
+
style={{ cursor: 'pointer' }}
|
|
174
|
+
onClick={() => setFilterType(isActive ? null : ext)}
|
|
175
|
+
>
|
|
176
|
+
{count} {ext.toUpperCase()}
|
|
177
|
+
</Badge>
|
|
178
|
+
);
|
|
179
|
+
})}
|
|
180
|
+
{filterType && (
|
|
181
|
+
<Badge
|
|
182
|
+
mode="outline"
|
|
183
|
+
fontSize={0}
|
|
184
|
+
style={{ cursor: 'pointer' }}
|
|
185
|
+
onClick={() => setFilterType(null)}
|
|
186
|
+
>
|
|
187
|
+
Clear filter
|
|
188
|
+
</Badge>
|
|
189
|
+
)}
|
|
190
|
+
</Flex>
|
|
191
|
+
</Flex>
|
|
192
|
+
<Button
|
|
193
|
+
mode="bleed"
|
|
194
|
+
tone="default"
|
|
195
|
+
fontSize={1}
|
|
196
|
+
padding={1}
|
|
197
|
+
text="Clear all"
|
|
198
|
+
onClick={() => setPendingFiles([])}
|
|
199
|
+
/>
|
|
200
|
+
</Flex>
|
|
201
|
+
|
|
202
|
+
{/* File table */}
|
|
203
|
+
<Box style={{ maxHeight: 350, overflowY: 'auto' }}>
|
|
204
|
+
{/* Table header */}
|
|
205
|
+
<Flex
|
|
206
|
+
align="center"
|
|
207
|
+
gap={2}
|
|
208
|
+
paddingX={2}
|
|
209
|
+
paddingY={1}
|
|
210
|
+
style={{ borderBottom: '1px solid var(--card-border-color)' }}
|
|
211
|
+
>
|
|
212
|
+
<Text size={0} weight="semibold" muted style={{ width: 56, flexShrink: 0 }}>Type</Text>
|
|
213
|
+
<Text size={0} weight="semibold" muted style={{ flex: 1 }}>File Name</Text>
|
|
214
|
+
<Box style={{ width: 32 }} />
|
|
215
|
+
</Flex>
|
|
216
|
+
<Stack space={0}>
|
|
217
|
+
{displayedFiles.map((file, i) => {
|
|
218
|
+
const ext = file.name.split('.').pop().toLowerCase();
|
|
219
|
+
return (
|
|
220
|
+
<Flex
|
|
221
|
+
key={`${file.name}-${file.size}-${i}`}
|
|
222
|
+
align="center"
|
|
223
|
+
gap={2}
|
|
224
|
+
paddingX={2}
|
|
225
|
+
paddingY={2}
|
|
226
|
+
style={{
|
|
227
|
+
borderBottom: '1px solid var(--card-border-color)',
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<Badge
|
|
231
|
+
tone="primary"
|
|
232
|
+
mode="outline"
|
|
233
|
+
fontSize={0}
|
|
234
|
+
style={{ width: 56, flexShrink: 0, textAlign: 'center' }}
|
|
235
|
+
>
|
|
236
|
+
{ext.toUpperCase()}
|
|
237
|
+
</Badge>
|
|
238
|
+
<Text size={1} style={{ flex: 1, textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
239
|
+
{file.name}
|
|
240
|
+
</Text>
|
|
241
|
+
<Button
|
|
242
|
+
mode="bleed"
|
|
243
|
+
tone="critical"
|
|
244
|
+
icon={TrashIcon}
|
|
245
|
+
padding={1}
|
|
246
|
+
onClick={() => handleRemoveFile(file)}
|
|
247
|
+
style={{ flexShrink: 0 }}
|
|
248
|
+
/>
|
|
249
|
+
</Flex>
|
|
250
|
+
);
|
|
251
|
+
})}
|
|
252
|
+
</Stack>
|
|
253
|
+
</Box>
|
|
254
|
+
</Stack>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
{/* Process button */}
|
|
258
|
+
{pendingFiles.length > 0 && (
|
|
259
|
+
<Button
|
|
260
|
+
mode="default"
|
|
261
|
+
tone="primary"
|
|
262
|
+
icon={UploadIcon}
|
|
263
|
+
text={`Process ${pendingFiles.length} File${pendingFiles.length === 1 ? '' : 's'}`}
|
|
264
|
+
style={{ width: '100%' }}
|
|
265
|
+
fontSize={2}
|
|
266
|
+
padding={4}
|
|
267
|
+
onClick={handleProcess}
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
270
|
+
</Stack>
|
|
271
|
+
);
|
|
272
|
+
}
|