@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
|
@@ -1,639 +1,653 @@
|
|
|
1
|
-
// Batch font uploader — drag-and-drop file list, confirm-to-upload, elapsed timer, Wake Lock, and beforeunload guard for long uploads
|
|
2
|
-
|
|
3
|
-
import React, { useCallback, useState, useMemo, useRef, useEffect } from 'react';
|
|
4
|
-
import { Card, Box, Flex, Grid, Text, Label, Switch, Button, Spinner, Tooltip, Stack } from '@sanity/ui';
|
|
5
|
-
import { ControlsIcon, InfoOutlineIcon, TrashIcon, UploadIcon, WarningOutlineIcon } from '@sanity/icons';
|
|
6
|
-
import { useFormValue } from 'sanity';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const [
|
|
35
|
-
const [
|
|
36
|
-
const [
|
|
37
|
-
const [
|
|
38
|
-
const [
|
|
39
|
-
const [
|
|
40
|
-
const [
|
|
41
|
-
const [
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
setError(
|
|
136
|
-
setStatus(
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
setStatus(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
if (!
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
<
|
|
370
|
-
<
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
<
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
{
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
}
|
|
1
|
+
// Batch font uploader — drag-and-drop file list, confirm-to-upload, elapsed timer, Wake Lock, and beforeunload guard for long uploads
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useState, useMemo, useRef, useEffect, lazy, Suspense } from 'react';
|
|
4
|
+
import { Card, Box, Flex, Grid, Text, Label, Switch, Button, Spinner, Tooltip, Stack } from '@sanity/ui';
|
|
5
|
+
import { ControlsIcon, InfoOutlineIcon, TrashIcon, UploadIcon, WarningOutlineIcon } from '@sanity/icons';
|
|
6
|
+
import { useFormValue } from 'sanity';
|
|
7
|
+
|
|
8
|
+
const UploadModal = lazy(() => import('./UploadModal'));
|
|
9
|
+
|
|
10
|
+
import { useSanityClient } from '../hooks/useSanityClient';
|
|
11
|
+
import { processFontFiles } from '../utils/processFontFiles';
|
|
12
|
+
import { uploadFontFiles } from '../utils/uploadFontFiles';
|
|
13
|
+
import { updateTypefaceDocument } from '../utils/updateTypefaceDocument';
|
|
14
|
+
import { generateStyleKeywords } from '../utils/generateKeywords';
|
|
15
|
+
import { renameFontDocuments } from '../utils/regenerateFontData';
|
|
16
|
+
import { updateFontPrices } from '../utils/updateFontPrices';
|
|
17
|
+
import generateCssFile from '../utils/generateCssFile';
|
|
18
|
+
|
|
19
|
+
import StatusDisplay from './StatusDisplay';
|
|
20
|
+
import PriceInput from './PriceInput';
|
|
21
|
+
import { RegenerateSubfamiliesComponent } from './RegenerateSubfamiliesComponent';
|
|
22
|
+
|
|
23
|
+
// Accepted font file extensions
|
|
24
|
+
const ACCEPTED_EXTENSIONS = ['ttf', 'otf', 'woff', 'woff2', 'eot', 'svg'];
|
|
25
|
+
|
|
26
|
+
/** Formats elapsed seconds as "Xm Ys" or "Ys". */
|
|
27
|
+
const formatElapsed = (s) => {
|
|
28
|
+
const m = Math.floor(s / 60);
|
|
29
|
+
const sec = s % 60;
|
|
30
|
+
return m > 0 ? `${m}m ${sec}s` : `${sec}s`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const BatchUploadFonts = () => {
|
|
34
|
+
const [status, setStatus] = useState('ready');
|
|
35
|
+
const [ready, setReady] = useState(true);
|
|
36
|
+
const [inputPrice, setInputPrice] = useState('0');
|
|
37
|
+
const [error, setError] = useState(false);
|
|
38
|
+
const [preserveShortenedNames, setPreserveShortenedNames] = useState(true);
|
|
39
|
+
const [preserveFileNames, setPreserveFileNames] = useState(false);
|
|
40
|
+
const [showUtilities, setShowUtilities] = useState(false);
|
|
41
|
+
const [pendingFiles, setPendingFiles] = useState([]);
|
|
42
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
43
|
+
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
|
44
|
+
const [showUploadModal, setShowUploadModal] = useState(false);
|
|
45
|
+
|
|
46
|
+
const fileInputRef = useRef(null);
|
|
47
|
+
const elapsedTimerRef = useRef(null);
|
|
48
|
+
const wakeLockRef = useRef(null);
|
|
49
|
+
|
|
50
|
+
const client = useSanityClient();
|
|
51
|
+
|
|
52
|
+
const doc_id = useFormValue(['_id']);
|
|
53
|
+
const title = useFormValue(['title']);
|
|
54
|
+
const preferredStyleRef = useFormValue(['preferredStyle']);
|
|
55
|
+
const slug = useFormValue(['slug']);
|
|
56
|
+
const stylesObject = useFormValue(['styles']) || { fonts: [], variableFont: [] };
|
|
57
|
+
const subfamiliesArray = stylesObject?.subfamilies || [];
|
|
58
|
+
|
|
59
|
+
const { weightKeywordList, italicKeywordList } = useMemo(() => generateStyleKeywords(), []);
|
|
60
|
+
|
|
61
|
+
// Elapsed timer — runs while upload is in progress
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (ready !== true) {
|
|
64
|
+
setElapsedSeconds(0);
|
|
65
|
+
elapsedTimerRef.current = setInterval(() => {
|
|
66
|
+
setElapsedSeconds(s => s + 1);
|
|
67
|
+
}, 1000);
|
|
68
|
+
} else {
|
|
69
|
+
clearInterval(elapsedTimerRef.current);
|
|
70
|
+
}
|
|
71
|
+
return () => clearInterval(elapsedTimerRef.current);
|
|
72
|
+
}, [ready]);
|
|
73
|
+
|
|
74
|
+
// Warn before navigating away while an upload is running
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (ready !== true) {
|
|
77
|
+
const handler = (e) => { e.preventDefault(); e.returnValue = ''; };
|
|
78
|
+
window.addEventListener('beforeunload', handler);
|
|
79
|
+
return () => window.removeEventListener('beforeunload', handler);
|
|
80
|
+
}
|
|
81
|
+
}, [ready]);
|
|
82
|
+
|
|
83
|
+
// Wake Lock — prevents the screen from sleeping during long uploads
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (ready !== true) {
|
|
86
|
+
navigator.wakeLock?.request('screen')
|
|
87
|
+
.then(lock => { wakeLockRef.current = lock; })
|
|
88
|
+
.catch(() => {});
|
|
89
|
+
} else if (wakeLockRef.current) {
|
|
90
|
+
wakeLockRef.current.release().catch(() => {});
|
|
91
|
+
wakeLockRef.current = null;
|
|
92
|
+
}
|
|
93
|
+
}, [ready]);
|
|
94
|
+
|
|
95
|
+
/** Validates that title and price are set before starting an upload. */
|
|
96
|
+
const validateInputs = (title, inputPrice) => {
|
|
97
|
+
const price = Number(inputPrice);
|
|
98
|
+
if (!title) {
|
|
99
|
+
setStatus('Typeface needs a title');
|
|
100
|
+
setError(true);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (isNaN(price) || typeof price !== 'number') {
|
|
104
|
+
setStatus('Invalid price — please refresh and try again');
|
|
105
|
+
setError(true);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Sorts font files so TTF/OTF are processed before web formats. */
|
|
112
|
+
const sortFilesByType = (files) => {
|
|
113
|
+
if (!files) return [];
|
|
114
|
+
const typeOrder = ['ttf', 'otf', 'eot', 'svg', 'woff', 'woff2'];
|
|
115
|
+
return Array.from(files).sort((a, b) => {
|
|
116
|
+
const aIndex = typeOrder.indexOf(a.name.split('.').pop().toLowerCase());
|
|
117
|
+
const bIndex = typeOrder.indexOf(b.name.split('.').pop().toLowerCase());
|
|
118
|
+
if (aIndex === bIndex) return a.name.localeCompare(b.name);
|
|
119
|
+
return aIndex - bIndex;
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Returns only files with accepted font extensions. */
|
|
124
|
+
const filterFontFiles = (files) =>
|
|
125
|
+
Array.from(files).filter(f => ACCEPTED_EXTENSIONS.includes(f.name.split('.').pop().toLowerCase()));
|
|
126
|
+
|
|
127
|
+
/** Sets final status after upload completes, reporting any failed files. */
|
|
128
|
+
const handleCompletionStatus = (failedFiles, setError, setStatus) => {
|
|
129
|
+
if (failedFiles.length > 0) {
|
|
130
|
+
console.error('Failed uploads:', {
|
|
131
|
+
files: failedFiles,
|
|
132
|
+
names: failedFiles.map(f => f.name),
|
|
133
|
+
metadata: failedFiles.map(f => f?.fk?.name?.records),
|
|
134
|
+
});
|
|
135
|
+
setError(true);
|
|
136
|
+
setStatus(`Upload completed with errors. Failed files: ${failedFiles.map(f => f.name).join(', ')}`);
|
|
137
|
+
} else {
|
|
138
|
+
setError(false);
|
|
139
|
+
setStatus('Upload completed successfully');
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** Adds files from the file picker to the pending list. */
|
|
144
|
+
const handleFileSelect = useCallback((e) => {
|
|
145
|
+
const files = filterFontFiles(e.target.files);
|
|
146
|
+
if (files.length > 0) setPendingFiles(prev => [...prev, ...files]);
|
|
147
|
+
e.target.value = '';
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
/** Removes a single file from the pending list by object reference. */
|
|
151
|
+
const handleRemoveFile = useCallback((file) => {
|
|
152
|
+
setPendingFiles(prev => prev.filter(f => f !== file));
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
const handleDragEnter = useCallback((e) => { e.preventDefault(); setIsDragging(true); }, []);
|
|
156
|
+
const handleDragOver = useCallback((e) => { e.preventDefault(); }, []);
|
|
157
|
+
const handleDragLeave = useCallback((e) => { e.preventDefault(); setIsDragging(false); }, []);
|
|
158
|
+
|
|
159
|
+
/** Adds dropped font files to the pending list. */
|
|
160
|
+
const handleDrop = useCallback((e) => {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
setIsDragging(false);
|
|
163
|
+
const files = filterFontFiles(e.dataTransfer.files);
|
|
164
|
+
if (files.length > 0) setPendingFiles(prev => [...prev, ...files]);
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
/** Processes and uploads the confirmed pending file list. */
|
|
168
|
+
const handleConfirmUpload = useCallback(async () => {
|
|
169
|
+
try {
|
|
170
|
+
setStatus('Uploading font files...');
|
|
171
|
+
setReady('upload');
|
|
172
|
+
setError(false);
|
|
173
|
+
|
|
174
|
+
if (!validateInputs(title, inputPrice)) {
|
|
175
|
+
setReady(true);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const sortedFiles = sortFilesByType(pendingFiles);
|
|
180
|
+
setPendingFiles([]);
|
|
181
|
+
|
|
182
|
+
const { fontsObjects, subfamilies, uniqueSubfamilies, newPreferredStyle, failedFiles } =
|
|
183
|
+
await processFontFiles(
|
|
184
|
+
sortedFiles,
|
|
185
|
+
title,
|
|
186
|
+
weightKeywordList,
|
|
187
|
+
italicKeywordList,
|
|
188
|
+
setStatus,
|
|
189
|
+
preserveShortenedNames,
|
|
190
|
+
preserveFileNames,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const { fontRefs, variableRefs } = await uploadFontFiles(
|
|
194
|
+
fontsObjects,
|
|
195
|
+
subfamilies,
|
|
196
|
+
client,
|
|
197
|
+
inputPrice,
|
|
198
|
+
stylesObject,
|
|
199
|
+
setStatus,
|
|
200
|
+
setError,
|
|
201
|
+
preserveFileNames,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
await updateTypefaceDocument(
|
|
205
|
+
doc_id,
|
|
206
|
+
fontRefs,
|
|
207
|
+
variableRefs,
|
|
208
|
+
subfamilies,
|
|
209
|
+
uniqueSubfamilies,
|
|
210
|
+
subfamiliesArray,
|
|
211
|
+
preferredStyleRef,
|
|
212
|
+
newPreferredStyle,
|
|
213
|
+
stylesObject,
|
|
214
|
+
client,
|
|
215
|
+
setStatus,
|
|
216
|
+
setError,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
handleCompletionStatus(failedFiles, setError, setStatus);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.error(e.message);
|
|
222
|
+
setError(true);
|
|
223
|
+
setStatus('Error uploading font');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
setReady(true);
|
|
227
|
+
setError(false);
|
|
228
|
+
}, [pendingFiles, stylesObject, title, slug, doc_id, inputPrice, weightKeywordList, italicKeywordList, client, preferredStyleRef, subfamiliesArray, preserveShortenedNames, preserveFileNames]);
|
|
229
|
+
|
|
230
|
+
/** Renames all existing font documents in this typeface by re-reading their TTF metadata. */
|
|
231
|
+
const handleRenameExistingFonts = useCallback(async () => {
|
|
232
|
+
try {
|
|
233
|
+
setStatus('Processing font documents...');
|
|
234
|
+
setReady('rename');
|
|
235
|
+
setError(false);
|
|
236
|
+
|
|
237
|
+
if (!title) {
|
|
238
|
+
setStatus('Typeface needs a title');
|
|
239
|
+
setError(true);
|
|
240
|
+
setReady(true);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const result = await renameFontDocuments({
|
|
245
|
+
client,
|
|
246
|
+
typefaceName: title,
|
|
247
|
+
slug,
|
|
248
|
+
weightKeywordList,
|
|
249
|
+
italicKeywordList,
|
|
250
|
+
preserveShortenedNames,
|
|
251
|
+
setStatus,
|
|
252
|
+
setError,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (!result.success) setError(true);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
console.error('Error renaming font documents:', err);
|
|
258
|
+
setError(true);
|
|
259
|
+
setStatus(`Error: ${err.message}`);
|
|
260
|
+
}
|
|
261
|
+
setReady(true);
|
|
262
|
+
}, [title, client, slug, weightKeywordList, italicKeywordList, preserveShortenedNames]);
|
|
263
|
+
|
|
264
|
+
/** Bulk-sets the same price on every font in this typeface. */
|
|
265
|
+
const handleChangeFontPrice = useCallback(async () => {
|
|
266
|
+
setStatus('Updating font prices...');
|
|
267
|
+
setReady('price');
|
|
268
|
+
setError(false);
|
|
269
|
+
|
|
270
|
+
await updateFontPrices({ client, title, slug, inputPrice, doc_id, setStatus, setError });
|
|
271
|
+
|
|
272
|
+
setReady(true);
|
|
273
|
+
}, [title, slug, client, doc_id, inputPrice]);
|
|
274
|
+
|
|
275
|
+
/** Regenerates the CSS @font-face file for every font in this typeface from its woff2 asset. */
|
|
276
|
+
const handleRegenerateCssFiles = useCallback(async () => {
|
|
277
|
+
try {
|
|
278
|
+
setStatus('Regenerating CSS files...');
|
|
279
|
+
setReady('css');
|
|
280
|
+
setError(false);
|
|
281
|
+
|
|
282
|
+
if (!title) { setStatus('Typeface needs a title'); setError(true); setReady(true); return false; }
|
|
283
|
+
if (!slug?.current) { setStatus('Typeface needs a slug'); setError(true); setReady(true); return false; }
|
|
284
|
+
|
|
285
|
+
const typeface = await client.fetch(
|
|
286
|
+
`*[_type == "typeface" && slug.current == $slug][0]`,
|
|
287
|
+
{ slug: slug.current }
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (!typeface) { setStatus('Typeface not found'); setError(true); setReady(true); return false; }
|
|
291
|
+
if (!typeface.styles?.fonts?.length) { setStatus('No fonts found in typeface'); setError(true); setReady(true); return false; }
|
|
292
|
+
|
|
293
|
+
const fontRefs = typeface.styles.fonts;
|
|
294
|
+
setStatus(`Regenerating CSS for ${fontRefs.length} fonts...`);
|
|
295
|
+
|
|
296
|
+
let updatedCount = 0;
|
|
297
|
+
let errorCount = 0;
|
|
298
|
+
|
|
299
|
+
for (let i = 0; i < fontRefs.length; i++) {
|
|
300
|
+
try {
|
|
301
|
+
const fontDoc = await client.fetch(`*[_id == $id][0]`, { id: fontRefs[i]._ref });
|
|
302
|
+
if (!fontDoc) { errorCount++; continue; }
|
|
303
|
+
if (!fontDoc.fileInput?.woff2?.asset) { errorCount++; continue; }
|
|
304
|
+
|
|
305
|
+
const woff2Asset = await client.fetch(`*[_id == $id][0]`, { id: fontDoc.fileInput.woff2.asset._ref });
|
|
306
|
+
if (!woff2Asset?.url) { errorCount++; continue; }
|
|
307
|
+
|
|
308
|
+
const woff2Response = await fetch(woff2Asset.url);
|
|
309
|
+
const woff2Blob = await woff2Response.blob();
|
|
310
|
+
const woff2File = new File([woff2Blob], `${fontDoc._id}.woff2`, { type: 'font/woff2' });
|
|
311
|
+
|
|
312
|
+
setStatus(`Regenerating CSS for font ${i + 1}/${fontRefs.length}: ${fontDoc.title}`);
|
|
313
|
+
|
|
314
|
+
const updatedFileInput = await generateCssFile({
|
|
315
|
+
woff2File,
|
|
316
|
+
fileInput: fontDoc.fileInput,
|
|
317
|
+
fileName: fontDoc._id,
|
|
318
|
+
fontName: fontDoc.title,
|
|
319
|
+
variableFont: fontDoc.variableFont || false,
|
|
320
|
+
weight: fontDoc.weight || 400,
|
|
321
|
+
client,
|
|
322
|
+
style: fontDoc.style || 'normal',
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await client.patch(fontRefs[i]._ref).set({ fileInput: updatedFileInput }).commit();
|
|
326
|
+
updatedCount++;
|
|
327
|
+
setStatus(`Regenerated CSS for ${updatedCount}/${fontRefs.length} fonts...`);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.error(`Error regenerating CSS for font ${fontRefs[i]._ref}:`, err);
|
|
330
|
+
errorCount++;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const successMessage = `Successfully regenerated CSS for ${updatedCount} fonts${errorCount > 0 ? ` (${errorCount} errors)` : ''}`;
|
|
335
|
+
setStatus(successMessage);
|
|
336
|
+
if (errorCount > 0) setError(true);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
console.error('Error regenerating CSS files:', err);
|
|
339
|
+
setError(true);
|
|
340
|
+
setStatus(`Error: ${err.message}`);
|
|
341
|
+
}
|
|
342
|
+
setReady(true);
|
|
343
|
+
}, [title, slug, client]);
|
|
344
|
+
|
|
345
|
+
/** Handles price field changes. */
|
|
346
|
+
const handleInputChange = (e) => {
|
|
347
|
+
setInputPrice(e.target.value);
|
|
348
|
+
setError(false);
|
|
349
|
+
setStatus('ready');
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/** Renders an info-icon tooltip trigger wrapping a label. */
|
|
353
|
+
const renderTooltipLabel = (label, description) => (
|
|
354
|
+
<Tooltip
|
|
355
|
+
content={<Box padding={2} style={{ maxWidth: 260 }}><Text size={1} style={{ lineHeight: 1.6 }}>{description}</Text></Box>}
|
|
356
|
+
placement="top"
|
|
357
|
+
portal
|
|
358
|
+
>
|
|
359
|
+
<Flex align="center" gap={1} style={{ cursor: 'default' }}>
|
|
360
|
+
<Label>{label}</Label>
|
|
361
|
+
<InfoOutlineIcon style={{ opacity: 0.5, display: 'block' }} />
|
|
362
|
+
</Flex>
|
|
363
|
+
</Tooltip>
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
/** Renders the in-progress state: spinner, live status, elapsed time, and do-not-close warning. */
|
|
367
|
+
const renderProcessing = () => (
|
|
368
|
+
<Stack space={3} paddingY={2}>
|
|
369
|
+
<Flex align="center" gap={3}>
|
|
370
|
+
<Spinner />
|
|
371
|
+
<Text size={1} muted>{status}</Text>
|
|
372
|
+
</Flex>
|
|
373
|
+
<Card tone="caution" border radius={2} padding={2}>
|
|
374
|
+
<Flex align="center" justify="space-between" gap={2}>
|
|
375
|
+
<Flex align="center" gap={2}>
|
|
376
|
+
<WarningOutlineIcon style={{ flexShrink: 0 }} />
|
|
377
|
+
<Text size={1} weight="semibold">Do not close or reload this tab</Text>
|
|
378
|
+
</Flex>
|
|
379
|
+
<Text size={1} muted style={{ flexShrink: 0 }}>{formatElapsed(elapsedSeconds)}</Text>
|
|
380
|
+
</Flex>
|
|
381
|
+
</Card>
|
|
382
|
+
</Stack>
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
/** Renders the drag-and-drop zone. */
|
|
386
|
+
const renderDropZone = () => (
|
|
387
|
+
<Box
|
|
388
|
+
onDragEnter={handleDragEnter}
|
|
389
|
+
onDragOver={handleDragOver}
|
|
390
|
+
onDragLeave={handleDragLeave}
|
|
391
|
+
onDrop={handleDrop}
|
|
392
|
+
style={{
|
|
393
|
+
border: `2px dashed ${isDragging ? 'var(--card-focus-ring-color)' : 'var(--card-border-color)'}`,
|
|
394
|
+
borderRadius: 4,
|
|
395
|
+
padding: '28px 16px',
|
|
396
|
+
textAlign: 'center',
|
|
397
|
+
background: isDragging ? 'rgba(100, 153, 255, 0.06)' : 'transparent',
|
|
398
|
+
transition: 'border-color 0.12s, background 0.12s',
|
|
399
|
+
cursor: 'default',
|
|
400
|
+
}}
|
|
401
|
+
>
|
|
402
|
+
<input
|
|
403
|
+
ref={fileInputRef}
|
|
404
|
+
type="file"
|
|
405
|
+
multiple
|
|
406
|
+
hidden
|
|
407
|
+
accept=".ttf,.otf,.woff,.woff2,.eot,.svg"
|
|
408
|
+
onChange={handleFileSelect}
|
|
409
|
+
/>
|
|
410
|
+
<Stack space={3}>
|
|
411
|
+
<Text size={1} muted>
|
|
412
|
+
{isDragging ? 'Release to add files' : 'Drop font files here'}
|
|
413
|
+
</Text>
|
|
414
|
+
<Flex justify="center">
|
|
415
|
+
<Button
|
|
416
|
+
mode="ghost"
|
|
417
|
+
tone="primary"
|
|
418
|
+
fontSize={1}
|
|
419
|
+
padding={2}
|
|
420
|
+
text="Browse files"
|
|
421
|
+
onClick={() => fileInputRef.current?.click()}
|
|
422
|
+
/>
|
|
423
|
+
</Flex>
|
|
424
|
+
</Stack>
|
|
425
|
+
</Box>
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
/** Renders the sorted pending file list with a scrollable container, file count, and upload action. */
|
|
429
|
+
const renderFileList = () => {
|
|
430
|
+
const sorted = sortFilesByType(pendingFiles);
|
|
431
|
+
return (
|
|
432
|
+
<Stack space={2}>
|
|
433
|
+
{/* Header: file count + clear */}
|
|
434
|
+
<Flex align="center" justify="space-between">
|
|
435
|
+
<Text size={1} muted>
|
|
436
|
+
{pendingFiles.length} file{pendingFiles.length === 1 ? '' : 's'} selected
|
|
437
|
+
</Text>
|
|
438
|
+
<Button
|
|
439
|
+
mode="bleed"
|
|
440
|
+
tone="default"
|
|
441
|
+
fontSize={1}
|
|
442
|
+
padding={1}
|
|
443
|
+
text="Clear all"
|
|
444
|
+
onClick={() => setPendingFiles([])}
|
|
445
|
+
/>
|
|
446
|
+
</Flex>
|
|
447
|
+
|
|
448
|
+
{/* Scrollable file list */}
|
|
449
|
+
<Box style={{ maxHeight: '260px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
450
|
+
{sorted.map((file, i) => {
|
|
451
|
+
const ext = file.name.split('.').pop().toUpperCase();
|
|
452
|
+
return (
|
|
453
|
+
<Card key={`${file.name}-${file.size}-${i}`} border radius={1} paddingX={2} paddingY={2}>
|
|
454
|
+
<Flex justify="space-between" align="center" gap={2}>
|
|
455
|
+
<Flex gap={3} align="center" style={{ flex: 1, minWidth: 0 }}>
|
|
456
|
+
<Text
|
|
457
|
+
size={0}
|
|
458
|
+
style={{ fontFamily: 'monospace', minWidth: '2.5rem', flexShrink: 0 }}
|
|
459
|
+
>
|
|
460
|
+
{ext}
|
|
461
|
+
</Text>
|
|
462
|
+
<Box style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
463
|
+
<Text size={1}>{file.name}</Text>
|
|
464
|
+
</Box>
|
|
465
|
+
</Flex>
|
|
466
|
+
<Button
|
|
467
|
+
mode="bleed"
|
|
468
|
+
tone="critical"
|
|
469
|
+
icon={TrashIcon}
|
|
470
|
+
padding={2}
|
|
471
|
+
onClick={() => handleRemoveFile(file)}
|
|
472
|
+
/>
|
|
473
|
+
</Flex>
|
|
474
|
+
</Card>
|
|
475
|
+
);
|
|
476
|
+
})}
|
|
477
|
+
</Box>
|
|
478
|
+
|
|
479
|
+
{/* Add more files zone */}
|
|
480
|
+
<Box
|
|
481
|
+
onDragEnter={handleDragEnter}
|
|
482
|
+
onDragOver={handleDragOver}
|
|
483
|
+
onDragLeave={handleDragLeave}
|
|
484
|
+
onDrop={handleDrop}
|
|
485
|
+
style={{
|
|
486
|
+
border: `2px dashed ${isDragging ? 'var(--card-focus-ring-color)' : 'var(--card-border-color)'}`,
|
|
487
|
+
borderRadius: 4,
|
|
488
|
+
padding: '10px 16px',
|
|
489
|
+
textAlign: 'center',
|
|
490
|
+
background: isDragging ? 'rgba(100, 153, 255, 0.06)' : 'transparent',
|
|
491
|
+
transition: 'border-color 0.12s, background 0.12s',
|
|
492
|
+
}}
|
|
493
|
+
>
|
|
494
|
+
<input
|
|
495
|
+
ref={fileInputRef}
|
|
496
|
+
type="file"
|
|
497
|
+
multiple
|
|
498
|
+
hidden
|
|
499
|
+
accept=".ttf,.otf,.woff,.woff2,.eot,.svg"
|
|
500
|
+
onChange={handleFileSelect}
|
|
501
|
+
/>
|
|
502
|
+
<Flex align="center" justify="center" gap={2}>
|
|
503
|
+
<Text size={1} muted>{isDragging ? 'Release to add' : 'Drop more files or'}</Text>
|
|
504
|
+
<Button
|
|
505
|
+
mode="bleed"
|
|
506
|
+
tone="primary"
|
|
507
|
+
fontSize={1}
|
|
508
|
+
padding={1}
|
|
509
|
+
text="browse"
|
|
510
|
+
onClick={() => fileInputRef.current?.click()}
|
|
511
|
+
/>
|
|
512
|
+
</Flex>
|
|
513
|
+
</Box>
|
|
514
|
+
|
|
515
|
+
{/* Upload confirm */}
|
|
516
|
+
<Button
|
|
517
|
+
mode="ghost"
|
|
518
|
+
tone="primary"
|
|
519
|
+
icon={UploadIcon}
|
|
520
|
+
text={`Upload ${pendingFiles.length} Font${pendingFiles.length === 1 ? '' : 's'}`}
|
|
521
|
+
style={{ width: '100%' }}
|
|
522
|
+
onClick={handleConfirmUpload}
|
|
523
|
+
/>
|
|
524
|
+
</Stack>
|
|
525
|
+
);
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const hasRequiredFields = title && title !== '' && slug && slug !== '';
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<>
|
|
532
|
+
{!hasRequiredFields && (
|
|
533
|
+
<Card border padding={4} radius={2} tone="caution">
|
|
534
|
+
<Flex align="center" gap={3}>
|
|
535
|
+
<Text size={2}>
|
|
536
|
+
<WarningOutlineIcon />
|
|
537
|
+
</Text>
|
|
538
|
+
<Stack space={2}>
|
|
539
|
+
<Text size={1} weight="semibold">
|
|
540
|
+
{!title || title === '' ? 'Title required to use font uploader' : 'Slug required to use font uploader'}
|
|
541
|
+
</Text>
|
|
542
|
+
<Text size={1} muted>
|
|
543
|
+
Add a {!title || title === '' ? 'title' : 'slug'} to this typeface document, then return to the Styles tab to upload fonts.
|
|
544
|
+
</Text>
|
|
545
|
+
</Stack>
|
|
546
|
+
</Flex>
|
|
547
|
+
</Card>
|
|
548
|
+
)}
|
|
549
|
+
{hasRequiredFields &&
|
|
550
|
+
<>
|
|
551
|
+
<StatusDisplay
|
|
552
|
+
status={status}
|
|
553
|
+
error={error}
|
|
554
|
+
action={
|
|
555
|
+
<Button
|
|
556
|
+
mode={showUtilities ? 'default' : 'ghost'}
|
|
557
|
+
tone="primary"
|
|
558
|
+
icon={ControlsIcon}
|
|
559
|
+
text="Utilities"
|
|
560
|
+
fontSize={1}
|
|
561
|
+
padding={2}
|
|
562
|
+
onClick={() => setShowUtilities(v => !v)}
|
|
563
|
+
/>
|
|
564
|
+
}
|
|
565
|
+
/>
|
|
566
|
+
|
|
567
|
+
<Button
|
|
568
|
+
mode="default"
|
|
569
|
+
tone="primary"
|
|
570
|
+
icon={UploadIcon}
|
|
571
|
+
text="Upload Fonts"
|
|
572
|
+
fontSize={2}
|
|
573
|
+
padding={4}
|
|
574
|
+
onClick={() => setShowUploadModal(true)}
|
|
575
|
+
style={{ width: '100%' }}
|
|
576
|
+
/>
|
|
577
|
+
|
|
578
|
+
{/* New upload modal */}
|
|
579
|
+
{showUploadModal && (
|
|
580
|
+
<Suspense fallback={<Spinner />}>
|
|
581
|
+
<UploadModal
|
|
582
|
+
open={showUploadModal}
|
|
583
|
+
onClose={() => setShowUploadModal(false)}
|
|
584
|
+
client={client}
|
|
585
|
+
docId={doc_id}
|
|
586
|
+
typefaceTitle={title}
|
|
587
|
+
stylesObject={stylesObject}
|
|
588
|
+
preferredStyleRef={preferredStyleRef}
|
|
589
|
+
slug={slug}
|
|
590
|
+
/>
|
|
591
|
+
</Suspense>
|
|
592
|
+
)}
|
|
593
|
+
|
|
594
|
+
{/* Utilities panel — toggled via the Utilities button */}
|
|
595
|
+
{showUtilities && (
|
|
596
|
+
<Card border padding={3} shadow={1} radius={2} marginTop={3}>
|
|
597
|
+
<Stack space={4}>
|
|
598
|
+
|
|
599
|
+
{/* Regenerate Subfamilies */}
|
|
600
|
+
<Stack space={2}>
|
|
601
|
+
<Text size={1} weight="semibold" style={{ lineHeight: 1.6 }}>Regenerate Subfamilies</Text>
|
|
602
|
+
<RegenerateSubfamiliesComponent />
|
|
603
|
+
</Stack>
|
|
604
|
+
|
|
605
|
+
{/* Rename Fonts */}
|
|
606
|
+
<Stack space={3}>
|
|
607
|
+
<Text size={1} weight="semibold" style={{ lineHeight: 1.6 }}>Rename Fonts (name table, Full Name)</Text>
|
|
608
|
+
<Flex align="center" gap={2}>
|
|
609
|
+
<Switch
|
|
610
|
+
checked={preserveShortenedNames}
|
|
611
|
+
onChange={(e) => setPreserveShortenedNames(e.target.checked)}
|
|
612
|
+
/>
|
|
613
|
+
{renderTooltipLabel(
|
|
614
|
+
'Preserve shortened names',
|
|
615
|
+
'Abbreviations in font names are kept as-is (e.g. "XNarrow" stays "XNarrow", "Bd" stays "Bd").'
|
|
616
|
+
)}
|
|
617
|
+
</Flex>
|
|
618
|
+
{ready === 'rename'
|
|
619
|
+
? renderProcessing()
|
|
620
|
+
: <Button mode="ghost" tone="primary" text="Rename Existing Fonts" style={{ width: '100%' }} onClick={handleRenameExistingFonts} disabled={ready !== true} />
|
|
621
|
+
}
|
|
622
|
+
</Stack>
|
|
623
|
+
|
|
624
|
+
{/* Update Font Prices */}
|
|
625
|
+
<Stack space={3}>
|
|
626
|
+
<Text size={1} weight="semibold" style={{ lineHeight: 1.6 }}>Update Font Prices</Text>
|
|
627
|
+
{ready === 'price'
|
|
628
|
+
? renderProcessing()
|
|
629
|
+
: <Stack space={2}>
|
|
630
|
+
<PriceInput inputPrice={inputPrice} handleInputChange={handleInputChange} />
|
|
631
|
+
<Button mode="ghost" tone="primary" text="Update All Font Prices" style={{ width: '100%' }} onClick={handleChangeFontPrice} disabled={ready !== true} />
|
|
632
|
+
</Stack>
|
|
633
|
+
}
|
|
634
|
+
</Stack>
|
|
635
|
+
|
|
636
|
+
{/* Regenerate CSS */}
|
|
637
|
+
<Stack space={3}>
|
|
638
|
+
<Text size={1} weight="semibold" style={{ lineHeight: 1.6 }}>Regenerate CSS</Text>
|
|
639
|
+
<Text size={1} muted style={{ lineHeight: 1.6 }}>Rebuilds the CSS @font-face files for all fonts in the typeface fonts list.</Text>
|
|
640
|
+
{ready === 'css'
|
|
641
|
+
? renderProcessing()
|
|
642
|
+
: <Button mode="ghost" tone="primary" text="Regenerate CSS Files" style={{ width: '100%' }} onClick={handleRegenerateCssFiles} disabled={ready !== true} />
|
|
643
|
+
}
|
|
644
|
+
</Stack>
|
|
645
|
+
|
|
646
|
+
</Stack>
|
|
647
|
+
</Card>
|
|
648
|
+
)}
|
|
649
|
+
</>
|
|
650
|
+
}
|
|
651
|
+
</>
|
|
652
|
+
);
|
|
653
|
+
};
|