@liiift-studio/sanity-font-manager 2.7.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 +1634 -17839
  4. package/dist/index.mjs +1551 -17745
  5. package/package.json +83 -83
  6. package/src/components/BatchUploadFonts.jsx +655 -655
  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 -309
  25. package/src/components/UploadScriptsComponent.jsx +539 -539
  26. package/src/components/UploadStep1Settings.jsx +272 -272
  27. package/src/components/UploadStep2Review.jsx +478 -478
  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 -281
  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 -531
  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 -154
  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,539 +1,539 @@
1
- // Batch uploader for script-specific font variants across multiple fonts at once
2
-
3
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
4
- import { Button, Flex, Grid, Stack, Text, TextInput, MenuButton, Menu, MenuItem, Select } from '@sanity/ui';
5
- import { parseFont } from '../utils/parseFont';
6
- import { getNameString, getVariationAxes, getItalicAngle, getWeightClass } from '../utils/fontHelpers';
7
- import slugify from 'slugify';
8
- import { useSanityClient } from '../hooks/useSanityClient';
9
- import { useFormValue } from 'sanity';
10
- import { nanoid } from 'nanoid';
11
- import generateCssFile from '../utils/generateCssFile';
12
- import { generateStyleKeywords, reverseSpellingLookup } from '../utils/generateKeywords';
13
- import { SCRIPTS } from '../utils/utils';
14
-
15
- /**
16
- * Component for uploading and managing script variants of fonts
17
- * @param {Object} props - Component props
18
- * @param {Object} props.elementProps - Element properties including ref
19
- * @returns {JSX.Element} Upload interface for script variants
20
- */
21
- export const UploadScriptsComponent = (props) => {
22
-
23
- // Props and client initialization
24
- const {elementProps: {ref}} = props;
25
- const client = useSanityClient();
26
-
27
- // Component state
28
- const [selectedScript, setSelectedScript] = useState(""); // Currently selected script
29
- const [status, setStatus] = React.useState(''); // Upload status message
30
- const [ready, setReady] = React.useState(true); // Component ready state
31
-
32
- // Form values from Sanity
33
- let doc_id = useFormValue(['_id']); // Document ID
34
- const title = useFormValue(['title']); // Typeface title
35
- const slug = useFormValue(['slug']); // URL slug
36
- const scripts = useFormValue(['scripts']) || []; // Supported scripts
37
- const stylesObject = useFormValue(['styles']); // Font styles data
38
- let subfamiliesArray = stylesObject?.subfamilies || []; // Font subfamilies
39
-
40
- // Memoized style keywords for font processing
41
- const {weightKeywordList, italicKeywordList} = useMemo(() =>
42
- generateStyleKeywords()
43
- , []);
44
-
45
- /**
46
- * Reads a font file and returns its content as a Uint8Array
47
- * @param {File} file - The font file to read
48
- * @returns {Promise<Uint8Array>} Font file content
49
- */
50
- const readFontFile = (file) => {
51
- return new Promise((resolve, reject) => {
52
- const reader = new FileReader();
53
-
54
- reader.onload = (event) => {
55
- resolve(new Uint8Array(event.target.result));
56
- };
57
-
58
- reader.onerror = (error) => { reject(error); };
59
- reader.readAsArrayBuffer(file);
60
- });
61
- };
62
-
63
-
64
- /**
65
- * Handles the upload and processing of font files for a specific script
66
- * @param {Event} event - The file input change event
67
- * @param {string} script - The selected script variant (e.g., 'cyrillic', 'greek')
68
- */
69
- const handleUpload = useCallback(async(event, script) => {
70
- setReady(false);
71
- try{
72
- let failedFiles = [];
73
-
74
- console.log('handle upload ', title, script );
75
- setStatus('uploading fonts files.. ');
76
-
77
- if(!title) {
78
- console.error('typeface needs title');
79
- return;
80
- }
81
-
82
- let fontRefs = [];
83
- let variableRefs = [];
84
- let subfamilies = {};
85
- let fontsObjects = {};
86
-
87
- // read font files ,
88
- // create if doesnt exist - create sanity fontObjects template
89
- // add font file to sanity font
90
- // create subfamily list
91
- for(var i = 0 ; i < event.target.files.length ; i++ ){
92
-
93
- const file = event.target.files[i];
94
- const fontBuffer = await readFontFile(file);
95
- const font = await parseFont(fontBuffer, file.name);
96
-
97
- const fullName = getNameString(font, 4);
98
- const familyName = getNameString(font, 1);
99
- console.log('Reading font:', fullName, file.name);
100
-
101
- let weightName = getNameString(font, 17) || getNameString(font, 2) || '';
102
- weightName = weightName.replace("Italic", "").replace("It", "").trim();
103
-
104
- if ((weightName == '' || weightName.toLowerCase() == 'roman') && fullName) {
105
- weightName = fullName.replace(title + " ", "").replace(title, "").trim();
106
- weightName = weightName.replace("Italic", "").replace("It", "").trim();
107
- }
108
-
109
- const axes = getVariationAxes(font);
110
- let variableFont = axes !== null;
111
- let subfamilyName = familyName.toLowerCase().trim().replace(title.toLowerCase().trim(),'').trim();
112
- let fontTitle = fullName;
113
- const italicAngle = getItalicAngle(font);
114
- let style = (italicAngle !== 0 || fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular';
115
-
116
- if(fontTitle.toLowerCase().trim().includes(script)){
117
- fontTitle = fontTitle.toLowerCase().trim().replace(script, '').trim();
118
- fontTitle = fontTitle.split(' ').map( word => {
119
- if( word == '') return
120
- return word;
121
- })
122
- .filter( word => word != undefined)
123
- .join(' ');
124
- }
125
-
126
- // remove weight and italic keywords from subfamily name
127
- weightKeywordList.forEach( keyword => {
128
- const kw = keyword.trim();
129
- if(subfamilyName.includes(kw)) subfamilyName = subfamilyName.replace(kw, '').trim();
130
-
131
- // if(fontTitle.includes(kw)){
132
- // fontTitle = fontTitle.replace(kw, '');
133
- // }
134
- });
135
-
136
- let italicKW = [];
137
- italicKeywordList.forEach( keyword => {
138
- const kw = keyword.toLowerCase().trim();
139
- if(subfamilyName.includes(kw)){
140
- subfamilyName = subfamilyName.replace(kw, '');
141
- }
142
-
143
- if(fontTitle.includes(kw)){
144
- fontTitle = fontTitle.replace(kw, '');
145
- italicKW.push(kw.charAt(0).toUpperCase() + kw.slice(1));
146
-
147
- }
148
- });
149
-
150
- fontTitle = fontTitle.replace(/-/g, ' ');
151
- fontTitle = fontTitle.trim().split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
152
-
153
- if(subfamilyName.trim().includes(script)){
154
- subfamilyName = subfamilyName.trim().replace(script, '').trim();
155
- }
156
-
157
- subfamilyName = subfamilyName.trim();
158
- subfamilyName = (subfamilyName == '' ) ? 'Regular' : subfamilyName.split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
159
-
160
- // remove subfamily from weight name
161
- if (subfamilyName !== '' ) {
162
- weightName = weightName
163
- .replace(`${subfamilyName} `, '')
164
- .replace(` ${subfamilyName}`, '')
165
- .trim();
166
- }
167
-
168
- if(variableFont && !fontTitle.toLowerCase().trim().endsWith(' vf')) fontTitle = fontTitle + ' VF';
169
-
170
- if(italicKW.length > 0){
171
- italicKW = italicKW.map( item => reverseSpellingLookup(item)); // replace each item in the italicKW list with the value in reverseSpellingLookup
172
- fontTitle = fontTitle + italicKW.join(' ');
173
- style = 'Italic';
174
- }
175
-
176
- let id = slugify(fontTitle.toLowerCase().trim());
177
-
178
- console.log('=== Font Info ====');
179
- console.log(' ')
180
- console.log('font id : ', id);
181
- console.log('font title : ', fontTitle);
182
- console.log('Full name:', fullName);
183
- console.log('Family name:', familyName);
184
- console.log('file name : ', file.name);
185
- console.log('subfamily : ', subfamilyName);
186
- console.log('style : ', style);
187
- console.log('weight : ', weightName);
188
- console.log('variable : ', variableFont);
189
- console.log('italicKW ', italicKW);
190
- console.log(' ')
191
- console.log('=======');
192
-
193
- subfamilies[id] = subfamilyName; // add subfamily to list
194
-
195
- if( fontsObjects[id]){
196
- fontsObjects[id].files = [...fontsObjects[id].files, file];
197
- } else {
198
- let fontObject = {
199
- _key: nanoid(),
200
- _id: id,
201
- title: fontTitle,
202
- slug: {_type:'slug', current:id},
203
- typefaceName: title, // Change to match Typeface Document
204
- style: style,
205
- variableFont: variableFont,
206
- weightName: weightName,
207
- normalWeight:true,
208
- weight: getWeightClass(font) || (
209
- /hairline|extra thin|extrathin/.test(weightName?.toLowerCase()) ? 100 :
210
- /thin|extra light|extralight/.test(weightName?.toLowerCase()) ? 200 :
211
- /light|book/.test(weightName?.toLowerCase()) ? 300 :
212
- /regular|normal/.test(weightName?.toLowerCase()) ? 400 :
213
- /medium/.test(weightName?.toLowerCase()) ? 500 :
214
- /semi bold|semibold/.test(weightName?.toLowerCase()) ? 600 :
215
- /bold/.test(weightName?.toLowerCase()) ? 700 :
216
- /extra bold|extrabold/.test(weightName?.toLowerCase()) ? 800 :
217
- /black|ultra/.test(weightName?.toLowerCase()) ? 900 :
218
- 400),
219
- files : [file],
220
- fontKit: font,
221
- scriptFileInput: {[script]:{}},
222
- };
223
- fontsObjects[id] = fontObject;
224
- }
225
- }
226
-
227
- // Extract unique subfamily names and prepare for processing
228
- let uniqueSubfamiles = [...new Set(Object.values(subfamilies))];
229
-
230
- console.log('Subfamilies : ', subfamilies, uniqueSubfamiles, uniqueSubfamiles.length);
231
- console.log('fontsObjects : ', fontsObjects);
232
-
233
- // Process each font object:
234
- // 1. Upload font files as Sanity assets
235
- // 2. Create file references linking fonts to assets
236
- // 3. Generate CSS for web fonts
237
- for(var i = 0 ; i < Object.keys(fontsObjects).length ; i++ ){
238
-
239
- let id = Object.keys(fontsObjects)[i];
240
- let fontObject = fontsObjects[id];
241
- let files = fontObject.files;
242
- let newFileInput = fontObject.scriptFileInput[script];
243
-
244
- console.log(fontObject.title , ' : subfamily : ', subfamilies[id]);
245
-
246
- // add subfamily to font object if more than one exists
247
- if(uniqueSubfamiles.length > 1) fontObject.subfamily = subfamilies[id];
248
- else fontObject.subfamily = '';
249
-
250
- // add price to font object - set sell = true if there is a price > 0
251
- fontObject.price = process.env.SANITY_STUDIO_DEFAULT_STYLE_PRICE || 40;
252
- if(fontObject.price > 0) fontObject.sell = true;
253
-
254
- // upload files
255
- for(var j = 0 ; j < files.length ; j++ ){
256
- let file = files[j];
257
- let fileType = "";
258
- if ( file.name.endsWith('.otf') ) fileType = "otf"
259
- else if ( file.name.endsWith('.ttf') ) fileType = "ttf"
260
- else if ( file.name.endsWith('.woff') ) fileType = "woff"
261
- else if ( file.name.endsWith('.woff2') ) fileType = "woff2"
262
- else if ( file.name.endsWith('.eot') ) fileType = "eot"
263
- else if ( file.name.endsWith('.svg') ) fileType = "svg"
264
-
265
- console.log('uploading font file : ', fontObject._id+'.'+fileType);
266
- const filename = fontObject._id+'-'+script;
267
- let fontTitle = fontObject.title+' '+script;
268
- fontTitle = fontTitle.split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
269
-
270
- let baseAsset = await client.assets.upload('file', file, { filename: filename+'.'+fileType })
271
- .catch( err => {
272
- console.error('error uploading font: ', fontObject.title);
273
- setStatus('error uploading font ' + err.message);
274
- });
275
-
276
- // create file ref from font
277
- newFileInput[fileType] = {
278
- _type: 'file',
279
- asset: {
280
- _ref: baseAsset._id,
281
- _type: 'reference'
282
- }
283
- }
284
-
285
- console.log('newFileInput', newFileInput);
286
-
287
- // generate css
288
- if(file.name.endsWith('.woff2')){
289
- console.log('generating css file for: ', fontObject.title);
290
- setStatus('generating css file for: ' + fontObject.title);
291
- newFileInput = await generateCssFile({
292
- woff2File: file,
293
- fileInput: newFileInput,
294
- // script: script,
295
- fontName: fontTitle,
296
- fileName: filename,
297
- variableFont: fontObject.variableFont,
298
- weight: fontObject.weight,
299
- client: client,
300
- });
301
- }
302
-
303
- fontObject.scriptFileInput[script] = newFileInput;
304
- fontsObjects[id] = fontObject;
305
-
306
- }
307
- }
308
-
309
- console.log('creating sanity fonts', fontsObjects);
310
-
311
-
312
- // create (with existing data if exists ) fonts and refs (for typeface)
313
- for(var i = 0 ; i < Object.keys(fontsObjects).length ; i++ ){
314
- let fontId = Object.keys(fontsObjects)[i];
315
- let font = fontsObjects[fontId];
316
-
317
- // add existing file refs to new file input
318
- let existingFont = await client.fetch(
319
- `*[_type == 'font' && _id == $fontId]{
320
- fileInput,
321
- description,
322
- metaData,
323
- metrics,
324
- opentypeFeatures,
325
- characterSet,
326
- subfamily,
327
- scriptFileInput,
328
- }`,
329
- { fontId: font._id }
330
- );
331
-
332
- existingFont = existingFont[0];
333
-
334
- let fontResponse;
335
- let files = font.files;
336
- let fontKit = font.fontKit;
337
- delete font.files;
338
- delete font.fontKit;
339
-
340
- console.log('creating font : ', font);
341
-
342
- try{
343
- if(existingFont && existingFont != null){
344
-
345
- if(existingFont.scriptFileInput && existingFont.scriptFileInput != null){
346
- let newFileInput = {...font.scriptFileInput};
347
-
348
- Object.keys(existingFont.scriptFileInput).forEach( key => {
349
- if(!newFileInput[key]){
350
- newFileInput[key] = existingFont.scriptFileInput[key];
351
- }
352
- });
353
- font.scriptFileInput = newFileInput;
354
- }
355
-
356
- fontResponse = await client.patch(font._id).set({ scriptFileInput: font.scriptFileInput }).commit()
357
-
358
- } else{
359
- fontResponse = await client.createOrReplace({
360
- _key: nanoid(),
361
- _id: font._id,
362
- _type: 'font',
363
- ...font,
364
- });
365
- }
366
- }
367
- catch(e){
368
- console.error('error creating font: ', font.title, font.subfamily);
369
- failedFiles = [...failedFiles, ...(files.map(file=>{return{name:file.name, fk: fontKit}}))];
370
- continue;
371
- }
372
-
373
-
374
- // Create font refs for typeface
375
- // add to fontRef array or variableRef array
376
-
377
- const fontRef = {_key: nanoid(), _type:'reference', _ref: fontResponse._id, _weak: true };
378
-
379
- console.log('font response : ', fontResponse);
380
- console.log('existing styles object : ', stylesObject);
381
-
382
- // add new font refs for typeface
383
- if(!font.variableFont){
384
- if(stylesObject.fonts && stylesObject.fonts.length > 0){
385
- let fontExists = stylesObject.fonts.findIndex( font => font._ref == fontResponse._id);
386
- let inFontRefs = fontRefs.findIndex( font => font._ref == fontResponse._id);
387
- if(fontExists == -1 && inFontRefs == -1){
388
- fontRefs.push(fontRef);
389
- }
390
- } else {
391
- fontRefs.push(fontRef);
392
- }
393
- }
394
-
395
- // add new font refs for typeface (variable)
396
- if(font.variableFont){
397
- if(stylesObject.variableFont && stylesObject.variableFont.length > 0){
398
- let vfExists = stylesObject.variableFont.findIndex( font => font._ref == fontResponse._id);
399
- let inVariableRefs = variableRefs.findIndex( font => font._ref == fontResponse._id);
400
- if( vfExists == -1 && inVariableRefs == -1 && font.variableFont){
401
- variableRefs.push(fontRef);
402
- }
403
- } else {
404
- variableRefs.push(fontRef);
405
- }
406
- }
407
-
408
- console.log(fontResponse._id, ' created!');
409
- }
410
-
411
- // Update Sanity typeface document with new font references
412
- console.log('updating styles refs (fonts, variable fonts, subfamilies) ', fontRefs, variableRefs, subfamilies, uniqueSubfamiles)
413
- setStatus('Updating font references...');
414
-
415
- let newStylesObject = stylesObject.fonts ?
416
- { ...stylesObject, fonts : [...stylesObject.fonts, ...fontRefs] }
417
- :
418
- { ...stylesObject, fonts : [...fontRefs] };
419
-
420
- if(uniqueSubfamiles.length > 1){
421
- newStylesObject.subfamilies = uniqueSubfamiles;
422
- }
423
- else{
424
- newStylesObject.subfamilies = [];
425
- }
426
-
427
- newStylesObject.variableFont = stylesObject?.variableFont ? [...stylesObject?.variableFont, ...variableRefs] : [...variableRefs];
428
-
429
- let patch = {styles:newStylesObject};
430
-
431
- subfamiliesArray = subfamiliesArray ? subfamiliesArray : [];
432
-
433
- console.log('new styles obj : ', newStylesObject);
434
- console.log('existing subfamily list : ', subfamiliesArray);
435
- console.log('unique subfamilies ', uniqueSubfamiles);
436
-
437
- subfamiliesArray = [...subfamiliesArray, ...uniqueSubfamiles].filter((sf, index, self) => {
438
- return self.indexOf(sf) === index;
439
- });
440
-
441
- patch.styles.subfamilies = subfamiliesArray;
442
-
443
- console.log('doc_id : ',doc_id);
444
- console.log('typeface patch : ',patch);
445
-
446
- let includedScripts = [ script, ...scripts].filter((lang, index, self) => {
447
- return self.indexOf(lang) === index;
448
- });
449
-
450
- patch.scripts = includedScripts;
451
-
452
- console.log('included scripts : ', includedScripts);
453
-
454
-
455
- if( doc_id.startsWith('drafts.')){
456
- await client.patch(doc_id).set(patch).commit()
457
- .catch(err => {
458
- console.error('error patching styles: ', err.message);
459
- setStatus('error patching styles '+ err.message);
460
- });
461
- doc_id = doc_id.replace('drafts.','');
462
- }
463
-
464
- await client.patch(doc_id).set(patch).commit()
465
- .catch(err => {
466
- console.error('error patching styles: ', err.message);
467
- setStatus('error patching styles');
468
- });
469
-
470
- console.log('success');
471
-
472
- if(failedFiles.length > 0){
473
- console.log('failed files : ', failedFiles);
474
- const names = failedFiles.map( file => file.name);
475
- console.log('names : ', failedFiles.map( file => file?.fk?.name?.records));
476
- setStatus('fonts uploaded with errors. Failed files : '+ names.join(', '));
477
- } else {
478
- setStatus('fonts uploaded!');
479
- }
480
- setStatus('fonts uploaded!');
481
- } catch(e){
482
- console.error(e);
483
- setStatus('error uploading font '+e.message);
484
- }
485
- setReady(true);
486
-
487
- },[title, slug, doc_id]);
488
-
489
- // Render component UI
490
- return (
491
- <Stack>
492
- {/* Display status message when processing */}
493
- {!ready &&
494
- <Text><br/>{status}<br/><br/></Text>
495
- }
496
-
497
- {/* Display upload interface when ready */}
498
- {ready &&
499
- <Stack>
500
- <Grid columns={!!(selectedScript && selectedScript !== "") ? 2 : 1} gap={2}>
501
- {/* Script selection dropdown */}
502
- <Select
503
- id="menu-button-example"
504
- onChange={(e)=>setSelectedScript(e.target.value)}
505
- >
506
- <option key={'script-none'} value={""}> </option>
507
- {SCRIPTS.map((script,i) =>
508
- <option key={'script-'+i} value={script}>
509
- {script[0]?.toUpperCase()+script.slice(1)}
510
- </option>
511
- )}
512
- </Select>
513
-
514
- {/* File upload button - only shown when script is selected */}
515
- {!!(selectedScript && selectedScript !== "") &&
516
- <>
517
- <label htmlFor="upload-scripts-file">
518
- <Button
519
- style={{pointerEvents: "none"}}
520
- text="Upload (ttf/otf/woff/woff2/etc..)"
521
- />
522
- </label>
523
- <input
524
- ref={ref}
525
- name="upload-scripts-file"
526
- id="upload-scripts-file"
527
- type="file"
528
- multiple
529
- hidden
530
- onChange={(event) => handleUpload(event, selectedScript)}
531
- />
532
- </>
533
- }
534
- </Grid>
535
- </Stack>
536
- }
537
- </Stack>
538
- )
539
- };
1
+ // Batch uploader for script-specific font variants across multiple fonts at once
2
+
3
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { Button, Flex, Grid, Stack, Text, TextInput, MenuButton, Menu, MenuItem, Select } from '@sanity/ui';
5
+ import { parseFont } from '../utils/parseFont';
6
+ import { getNameString, getVariationAxes, getItalicAngle, getWeightClass } from '../utils/fontHelpers';
7
+ import slugify from 'slugify';
8
+ import { useSanityClient } from '../hooks/useSanityClient';
9
+ import { useFormValue } from 'sanity';
10
+ import { nanoid } from 'nanoid';
11
+ import generateCssFile from '../utils/generateCssFile';
12
+ import { generateStyleKeywords, reverseSpellingLookup } from '../utils/generateKeywords';
13
+ import { SCRIPTS } from '../utils/utils';
14
+
15
+ /**
16
+ * Component for uploading and managing script variants of fonts
17
+ * @param {Object} props - Component props
18
+ * @param {Object} props.elementProps - Element properties including ref
19
+ * @returns {JSX.Element} Upload interface for script variants
20
+ */
21
+ export const UploadScriptsComponent = (props) => {
22
+
23
+ // Props and client initialization
24
+ const {elementProps: {ref}} = props;
25
+ const client = useSanityClient();
26
+
27
+ // Component state
28
+ const [selectedScript, setSelectedScript] = useState(""); // Currently selected script
29
+ const [status, setStatus] = React.useState(''); // Upload status message
30
+ const [ready, setReady] = React.useState(true); // Component ready state
31
+
32
+ // Form values from Sanity
33
+ let doc_id = useFormValue(['_id']); // Document ID
34
+ const title = useFormValue(['title']); // Typeface title
35
+ const slug = useFormValue(['slug']); // URL slug
36
+ const scripts = useFormValue(['scripts']) || []; // Supported scripts
37
+ const stylesObject = useFormValue(['styles']); // Font styles data
38
+ let subfamiliesArray = stylesObject?.subfamilies || []; // Font subfamilies
39
+
40
+ // Memoized style keywords for font processing
41
+ const {weightKeywordList, italicKeywordList} = useMemo(() =>
42
+ generateStyleKeywords()
43
+ , []);
44
+
45
+ /**
46
+ * Reads a font file and returns its content as a Uint8Array
47
+ * @param {File} file - The font file to read
48
+ * @returns {Promise<Uint8Array>} Font file content
49
+ */
50
+ const readFontFile = (file) => {
51
+ return new Promise((resolve, reject) => {
52
+ const reader = new FileReader();
53
+
54
+ reader.onload = (event) => {
55
+ resolve(new Uint8Array(event.target.result));
56
+ };
57
+
58
+ reader.onerror = (error) => { reject(error); };
59
+ reader.readAsArrayBuffer(file);
60
+ });
61
+ };
62
+
63
+
64
+ /**
65
+ * Handles the upload and processing of font files for a specific script
66
+ * @param {Event} event - The file input change event
67
+ * @param {string} script - The selected script variant (e.g., 'cyrillic', 'greek')
68
+ */
69
+ const handleUpload = useCallback(async(event, script) => {
70
+ setReady(false);
71
+ try{
72
+ let failedFiles = [];
73
+
74
+ console.log('handle upload ', title, script );
75
+ setStatus('uploading fonts files.. ');
76
+
77
+ if(!title) {
78
+ console.error('typeface needs title');
79
+ return;
80
+ }
81
+
82
+ let fontRefs = [];
83
+ let variableRefs = [];
84
+ let subfamilies = {};
85
+ let fontsObjects = {};
86
+
87
+ // read font files ,
88
+ // create if doesnt exist - create sanity fontObjects template
89
+ // add font file to sanity font
90
+ // create subfamily list
91
+ for(var i = 0 ; i < event.target.files.length ; i++ ){
92
+
93
+ const file = event.target.files[i];
94
+ const fontBuffer = await readFontFile(file);
95
+ const font = await parseFont(fontBuffer, file.name);
96
+
97
+ const fullName = getNameString(font, 4);
98
+ const familyName = getNameString(font, 1);
99
+ console.log('Reading font:', fullName, file.name);
100
+
101
+ let weightName = getNameString(font, 17) || getNameString(font, 2) || '';
102
+ weightName = weightName.replace("Italic", "").replace("It", "").trim();
103
+
104
+ if ((weightName == '' || weightName.toLowerCase() == 'roman') && fullName) {
105
+ weightName = fullName.replace(title + " ", "").replace(title, "").trim();
106
+ weightName = weightName.replace("Italic", "").replace("It", "").trim();
107
+ }
108
+
109
+ const axes = getVariationAxes(font);
110
+ let variableFont = axes !== null;
111
+ let subfamilyName = familyName.toLowerCase().trim().replace(title.toLowerCase().trim(),'').trim();
112
+ let fontTitle = fullName;
113
+ const italicAngle = getItalicAngle(font);
114
+ let style = (italicAngle !== 0 || fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular';
115
+
116
+ if(fontTitle.toLowerCase().trim().includes(script)){
117
+ fontTitle = fontTitle.toLowerCase().trim().replace(script, '').trim();
118
+ fontTitle = fontTitle.split(' ').map( word => {
119
+ if( word == '') return
120
+ return word;
121
+ })
122
+ .filter( word => word != undefined)
123
+ .join(' ');
124
+ }
125
+
126
+ // remove weight and italic keywords from subfamily name
127
+ weightKeywordList.forEach( keyword => {
128
+ const kw = keyword.trim();
129
+ if(subfamilyName.includes(kw)) subfamilyName = subfamilyName.replace(kw, '').trim();
130
+
131
+ // if(fontTitle.includes(kw)){
132
+ // fontTitle = fontTitle.replace(kw, '');
133
+ // }
134
+ });
135
+
136
+ let italicKW = [];
137
+ italicKeywordList.forEach( keyword => {
138
+ const kw = keyword.toLowerCase().trim();
139
+ if(subfamilyName.includes(kw)){
140
+ subfamilyName = subfamilyName.replace(kw, '');
141
+ }
142
+
143
+ if(fontTitle.includes(kw)){
144
+ fontTitle = fontTitle.replace(kw, '');
145
+ italicKW.push(kw.charAt(0).toUpperCase() + kw.slice(1));
146
+
147
+ }
148
+ });
149
+
150
+ fontTitle = fontTitle.replace(/-/g, ' ');
151
+ fontTitle = fontTitle.trim().split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
152
+
153
+ if(subfamilyName.trim().includes(script)){
154
+ subfamilyName = subfamilyName.trim().replace(script, '').trim();
155
+ }
156
+
157
+ subfamilyName = subfamilyName.trim();
158
+ subfamilyName = (subfamilyName == '' ) ? 'Regular' : subfamilyName.split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
159
+
160
+ // remove subfamily from weight name
161
+ if (subfamilyName !== '' ) {
162
+ weightName = weightName
163
+ .replace(`${subfamilyName} `, '')
164
+ .replace(` ${subfamilyName}`, '')
165
+ .trim();
166
+ }
167
+
168
+ if(variableFont && !fontTitle.toLowerCase().trim().endsWith(' vf')) fontTitle = fontTitle + ' VF';
169
+
170
+ if(italicKW.length > 0){
171
+ italicKW = italicKW.map( item => reverseSpellingLookup(item)); // replace each item in the italicKW list with the value in reverseSpellingLookup
172
+ fontTitle = fontTitle + italicKW.join(' ');
173
+ style = 'Italic';
174
+ }
175
+
176
+ let id = slugify(fontTitle.toLowerCase().trim());
177
+
178
+ console.log('=== Font Info ====');
179
+ console.log(' ')
180
+ console.log('font id : ', id);
181
+ console.log('font title : ', fontTitle);
182
+ console.log('Full name:', fullName);
183
+ console.log('Family name:', familyName);
184
+ console.log('file name : ', file.name);
185
+ console.log('subfamily : ', subfamilyName);
186
+ console.log('style : ', style);
187
+ console.log('weight : ', weightName);
188
+ console.log('variable : ', variableFont);
189
+ console.log('italicKW ', italicKW);
190
+ console.log(' ')
191
+ console.log('=======');
192
+
193
+ subfamilies[id] = subfamilyName; // add subfamily to list
194
+
195
+ if( fontsObjects[id]){
196
+ fontsObjects[id].files = [...fontsObjects[id].files, file];
197
+ } else {
198
+ let fontObject = {
199
+ _key: nanoid(),
200
+ _id: id,
201
+ title: fontTitle,
202
+ slug: {_type:'slug', current:id},
203
+ typefaceName: title, // Change to match Typeface Document
204
+ style: style,
205
+ variableFont: variableFont,
206
+ weightName: weightName,
207
+ normalWeight:true,
208
+ weight: getWeightClass(font) || (
209
+ /hairline|extra thin|extrathin/.test(weightName?.toLowerCase()) ? 100 :
210
+ /thin|extra light|extralight/.test(weightName?.toLowerCase()) ? 200 :
211
+ /light|book/.test(weightName?.toLowerCase()) ? 300 :
212
+ /regular|normal/.test(weightName?.toLowerCase()) ? 400 :
213
+ /medium/.test(weightName?.toLowerCase()) ? 500 :
214
+ /semi bold|semibold/.test(weightName?.toLowerCase()) ? 600 :
215
+ /bold/.test(weightName?.toLowerCase()) ? 700 :
216
+ /extra bold|extrabold/.test(weightName?.toLowerCase()) ? 800 :
217
+ /black|ultra/.test(weightName?.toLowerCase()) ? 900 :
218
+ 400),
219
+ files : [file],
220
+ fontKit: font,
221
+ scriptFileInput: {[script]:{}},
222
+ };
223
+ fontsObjects[id] = fontObject;
224
+ }
225
+ }
226
+
227
+ // Extract unique subfamily names and prepare for processing
228
+ let uniqueSubfamiles = [...new Set(Object.values(subfamilies))];
229
+
230
+ console.log('Subfamilies : ', subfamilies, uniqueSubfamiles, uniqueSubfamiles.length);
231
+ console.log('fontsObjects : ', fontsObjects);
232
+
233
+ // Process each font object:
234
+ // 1. Upload font files as Sanity assets
235
+ // 2. Create file references linking fonts to assets
236
+ // 3. Generate CSS for web fonts
237
+ for(var i = 0 ; i < Object.keys(fontsObjects).length ; i++ ){
238
+
239
+ let id = Object.keys(fontsObjects)[i];
240
+ let fontObject = fontsObjects[id];
241
+ let files = fontObject.files;
242
+ let newFileInput = fontObject.scriptFileInput[script];
243
+
244
+ console.log(fontObject.title , ' : subfamily : ', subfamilies[id]);
245
+
246
+ // add subfamily to font object if more than one exists
247
+ if(uniqueSubfamiles.length > 1) fontObject.subfamily = subfamilies[id];
248
+ else fontObject.subfamily = '';
249
+
250
+ // add price to font object - set sell = true if there is a price > 0
251
+ fontObject.price = process.env.SANITY_STUDIO_DEFAULT_STYLE_PRICE || 40;
252
+ if(fontObject.price > 0) fontObject.sell = true;
253
+
254
+ // upload files
255
+ for(var j = 0 ; j < files.length ; j++ ){
256
+ let file = files[j];
257
+ let fileType = "";
258
+ if ( file.name.endsWith('.otf') ) fileType = "otf"
259
+ else if ( file.name.endsWith('.ttf') ) fileType = "ttf"
260
+ else if ( file.name.endsWith('.woff') ) fileType = "woff"
261
+ else if ( file.name.endsWith('.woff2') ) fileType = "woff2"
262
+ else if ( file.name.endsWith('.eot') ) fileType = "eot"
263
+ else if ( file.name.endsWith('.svg') ) fileType = "svg"
264
+
265
+ console.log('uploading font file : ', fontObject._id+'.'+fileType);
266
+ const filename = fontObject._id+'-'+script;
267
+ let fontTitle = fontObject.title+' '+script;
268
+ fontTitle = fontTitle.split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
269
+
270
+ let baseAsset = await client.assets.upload('file', file, { filename: filename+'.'+fileType })
271
+ .catch( err => {
272
+ console.error('error uploading font: ', fontObject.title);
273
+ setStatus('error uploading font ' + err.message);
274
+ });
275
+
276
+ // create file ref from font
277
+ newFileInput[fileType] = {
278
+ _type: 'file',
279
+ asset: {
280
+ _ref: baseAsset._id,
281
+ _type: 'reference'
282
+ }
283
+ }
284
+
285
+ console.log('newFileInput', newFileInput);
286
+
287
+ // generate css
288
+ if(file.name.endsWith('.woff2')){
289
+ console.log('generating css file for: ', fontObject.title);
290
+ setStatus('generating css file for: ' + fontObject.title);
291
+ newFileInput = await generateCssFile({
292
+ woff2File: file,
293
+ fileInput: newFileInput,
294
+ // script: script,
295
+ fontName: fontTitle,
296
+ fileName: filename,
297
+ variableFont: fontObject.variableFont,
298
+ weight: fontObject.weight,
299
+ client: client,
300
+ });
301
+ }
302
+
303
+ fontObject.scriptFileInput[script] = newFileInput;
304
+ fontsObjects[id] = fontObject;
305
+
306
+ }
307
+ }
308
+
309
+ console.log('creating sanity fonts', fontsObjects);
310
+
311
+
312
+ // create (with existing data if exists ) fonts and refs (for typeface)
313
+ for(var i = 0 ; i < Object.keys(fontsObjects).length ; i++ ){
314
+ let fontId = Object.keys(fontsObjects)[i];
315
+ let font = fontsObjects[fontId];
316
+
317
+ // add existing file refs to new file input
318
+ let existingFont = await client.fetch(
319
+ `*[_type == 'font' && _id == $fontId]{
320
+ fileInput,
321
+ description,
322
+ metaData,
323
+ metrics,
324
+ opentypeFeatures,
325
+ characterSet,
326
+ subfamily,
327
+ scriptFileInput,
328
+ }`,
329
+ { fontId: font._id }
330
+ );
331
+
332
+ existingFont = existingFont[0];
333
+
334
+ let fontResponse;
335
+ let files = font.files;
336
+ let fontKit = font.fontKit;
337
+ delete font.files;
338
+ delete font.fontKit;
339
+
340
+ console.log('creating font : ', font);
341
+
342
+ try{
343
+ if(existingFont && existingFont != null){
344
+
345
+ if(existingFont.scriptFileInput && existingFont.scriptFileInput != null){
346
+ let newFileInput = {...font.scriptFileInput};
347
+
348
+ Object.keys(existingFont.scriptFileInput).forEach( key => {
349
+ if(!newFileInput[key]){
350
+ newFileInput[key] = existingFont.scriptFileInput[key];
351
+ }
352
+ });
353
+ font.scriptFileInput = newFileInput;
354
+ }
355
+
356
+ fontResponse = await client.patch(font._id).set({ scriptFileInput: font.scriptFileInput }).commit()
357
+
358
+ } else{
359
+ fontResponse = await client.createOrReplace({
360
+ _key: nanoid(),
361
+ _id: font._id,
362
+ _type: 'font',
363
+ ...font,
364
+ });
365
+ }
366
+ }
367
+ catch(e){
368
+ console.error('error creating font: ', font.title, font.subfamily);
369
+ failedFiles = [...failedFiles, ...(files.map(file=>{return{name:file.name, fk: fontKit}}))];
370
+ continue;
371
+ }
372
+
373
+
374
+ // Create font refs for typeface
375
+ // add to fontRef array or variableRef array
376
+
377
+ const fontRef = {_key: nanoid(), _type:'reference', _ref: fontResponse._id, _weak: true };
378
+
379
+ console.log('font response : ', fontResponse);
380
+ console.log('existing styles object : ', stylesObject);
381
+
382
+ // add new font refs for typeface
383
+ if(!font.variableFont){
384
+ if(stylesObject.fonts && stylesObject.fonts.length > 0){
385
+ let fontExists = stylesObject.fonts.findIndex( font => font._ref == fontResponse._id);
386
+ let inFontRefs = fontRefs.findIndex( font => font._ref == fontResponse._id);
387
+ if(fontExists == -1 && inFontRefs == -1){
388
+ fontRefs.push(fontRef);
389
+ }
390
+ } else {
391
+ fontRefs.push(fontRef);
392
+ }
393
+ }
394
+
395
+ // add new font refs for typeface (variable)
396
+ if(font.variableFont){
397
+ if(stylesObject.variableFont && stylesObject.variableFont.length > 0){
398
+ let vfExists = stylesObject.variableFont.findIndex( font => font._ref == fontResponse._id);
399
+ let inVariableRefs = variableRefs.findIndex( font => font._ref == fontResponse._id);
400
+ if( vfExists == -1 && inVariableRefs == -1 && font.variableFont){
401
+ variableRefs.push(fontRef);
402
+ }
403
+ } else {
404
+ variableRefs.push(fontRef);
405
+ }
406
+ }
407
+
408
+ console.log(fontResponse._id, ' created!');
409
+ }
410
+
411
+ // Update Sanity typeface document with new font references
412
+ console.log('updating styles refs (fonts, variable fonts, subfamilies) ', fontRefs, variableRefs, subfamilies, uniqueSubfamiles)
413
+ setStatus('Updating font references...');
414
+
415
+ let newStylesObject = stylesObject.fonts ?
416
+ { ...stylesObject, fonts : [...stylesObject.fonts, ...fontRefs] }
417
+ :
418
+ { ...stylesObject, fonts : [...fontRefs] };
419
+
420
+ if(uniqueSubfamiles.length > 1){
421
+ newStylesObject.subfamilies = uniqueSubfamiles;
422
+ }
423
+ else{
424
+ newStylesObject.subfamilies = [];
425
+ }
426
+
427
+ newStylesObject.variableFont = stylesObject?.variableFont ? [...stylesObject?.variableFont, ...variableRefs] : [...variableRefs];
428
+
429
+ let patch = {styles:newStylesObject};
430
+
431
+ subfamiliesArray = subfamiliesArray ? subfamiliesArray : [];
432
+
433
+ console.log('new styles obj : ', newStylesObject);
434
+ console.log('existing subfamily list : ', subfamiliesArray);
435
+ console.log('unique subfamilies ', uniqueSubfamiles);
436
+
437
+ subfamiliesArray = [...subfamiliesArray, ...uniqueSubfamiles].filter((sf, index, self) => {
438
+ return self.indexOf(sf) === index;
439
+ });
440
+
441
+ patch.styles.subfamilies = subfamiliesArray;
442
+
443
+ console.log('doc_id : ',doc_id);
444
+ console.log('typeface patch : ',patch);
445
+
446
+ let includedScripts = [ script, ...scripts].filter((lang, index, self) => {
447
+ return self.indexOf(lang) === index;
448
+ });
449
+
450
+ patch.scripts = includedScripts;
451
+
452
+ console.log('included scripts : ', includedScripts);
453
+
454
+
455
+ if( doc_id.startsWith('drafts.')){
456
+ await client.patch(doc_id).set(patch).commit()
457
+ .catch(err => {
458
+ console.error('error patching styles: ', err.message);
459
+ setStatus('error patching styles '+ err.message);
460
+ });
461
+ doc_id = doc_id.replace('drafts.','');
462
+ }
463
+
464
+ await client.patch(doc_id).set(patch).commit()
465
+ .catch(err => {
466
+ console.error('error patching styles: ', err.message);
467
+ setStatus('error patching styles');
468
+ });
469
+
470
+ console.log('success');
471
+
472
+ if(failedFiles.length > 0){
473
+ console.log('failed files : ', failedFiles);
474
+ const names = failedFiles.map( file => file.name);
475
+ console.log('names : ', failedFiles.map( file => file?.fk?.name?.records));
476
+ setStatus('fonts uploaded with errors. Failed files : '+ names.join(', '));
477
+ } else {
478
+ setStatus('fonts uploaded!');
479
+ }
480
+ setStatus('fonts uploaded!');
481
+ } catch(e){
482
+ console.error(e);
483
+ setStatus('error uploading font '+e.message);
484
+ }
485
+ setReady(true);
486
+
487
+ },[title, slug, doc_id]);
488
+
489
+ // Render component UI
490
+ return (
491
+ <Stack>
492
+ {/* Display status message when processing */}
493
+ {!ready &&
494
+ <Text><br/>{status}<br/><br/></Text>
495
+ }
496
+
497
+ {/* Display upload interface when ready */}
498
+ {ready &&
499
+ <Stack>
500
+ <Grid columns={!!(selectedScript && selectedScript !== "") ? 2 : 1} gap={2}>
501
+ {/* Script selection dropdown */}
502
+ <Select
503
+ id="menu-button-example"
504
+ onChange={(e)=>setSelectedScript(e.target.value)}
505
+ >
506
+ <option key={'script-none'} value={""}> </option>
507
+ {SCRIPTS.map((script,i) =>
508
+ <option key={'script-'+i} value={script}>
509
+ {script[0]?.toUpperCase()+script.slice(1)}
510
+ </option>
511
+ )}
512
+ </Select>
513
+
514
+ {/* File upload button - only shown when script is selected */}
515
+ {!!(selectedScript && selectedScript !== "") &&
516
+ <>
517
+ <label htmlFor="upload-scripts-file">
518
+ <Button
519
+ style={{pointerEvents: "none"}}
520
+ text="Upload (ttf/otf/woff/woff2/etc..)"
521
+ />
522
+ </label>
523
+ <input
524
+ ref={ref}
525
+ name="upload-scripts-file"
526
+ id="upload-scripts-file"
527
+ type="file"
528
+ multiple
529
+ hidden
530
+ onChange={(event) => handleUpload(event, selectedScript)}
531
+ />
532
+ </>
533
+ }
534
+ </Grid>
535
+ </Stack>
536
+ }
537
+ </Stack>
538
+ )
539
+ };