@liiift-studio/sanity-font-manager 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +695 -437
- package/dist/index.js +1628 -17701
- package/dist/index.mjs +1537 -17599
- package/package.json +83 -83
- package/src/components/BatchUploadFonts.jsx +655 -653
- package/src/components/BulkActions.jsx +99 -99
- package/src/components/ExistingDocumentResolver.jsx +152 -152
- package/src/components/FontReviewCard.jsx +455 -455
- 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 +267 -267
- 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 +674 -674
- 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 +309 -304
- package/src/components/UploadScriptsComponent.jsx +539 -539
- package/src/components/UploadStep1Settings.jsx +272 -272
- package/src/components/UploadStep2Review.jsx +478 -474
- package/src/components/UploadStep3Execute.jsx +234 -234
- package/src/components/UploadStep3bInstances.jsx +396 -396
- package/src/components/UploadSummary.jsx +196 -196
- 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 +120 -120
- package/src/schema/openTypeField.js +1995 -1995
- package/src/schema/styleCountField.js +12 -12
- package/src/schema/stylesField.js +302 -302
- package/src/schema/stylisticSetField.js +301 -301
- package/src/utils/buildUploadPlan.js +326 -326
- package/src/utils/executeUploadPlan.js +430 -430
- package/src/utils/executionReducer.js +56 -56
- package/src/utils/fontHelpers.js +281 -267
- package/src/utils/generateCssFile.js +207 -207
- package/src/utils/generateFontData.js +98 -98
- 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 -101
- package/src/utils/parseFont.js +56 -56
- package/src/utils/parseVariableFontInstances.js +301 -301
- package/src/utils/planReducer.js +531 -517
- package/src/utils/planTypes.js +183 -183
- package/src/utils/processFontFiles.js +530 -530
- package/src/utils/regenerateFontData.js +146 -146
- package/src/utils/resolveExistingFont.js +87 -87
- package/src/utils/retitleFontEntries.js +154 -0
- package/src/utils/sanitizeForSanityId.js +65 -65
- package/src/utils/setupDecompressors.js +27 -27
- package/src/utils/updateFontPrices.js +94 -94
- package/src/utils/updateTypefaceDocument.js +162 -162
- package/src/utils/uploadFontFiles.js +405 -405
- package/src/utils/utils.js +24 -24
package/src/utils/planReducer.js
CHANGED
|
@@ -1,517 +1,531 @@
|
|
|
1
|
-
// Plan reducer — manages UploadPlan state for the upload modal review UI
|
|
2
|
-
|
|
3
|
-
import { PLAN_PHASE, FONT_STATUS, PROCESSING_OWNED_FIELDS } from './planTypes';
|
|
4
|
-
import { sanitizeForSanityId } from './sanitizeForSanityId';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[PLAN_PHASE.
|
|
10
|
-
[PLAN_PHASE.
|
|
11
|
-
[PLAN_PHASE.
|
|
12
|
-
[PLAN_PHASE.
|
|
13
|
-
[PLAN_PHASE.
|
|
14
|
-
[PLAN_PHASE.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @param {object}
|
|
24
|
-
* @
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
return {
|
|
121
|
-
...state,
|
|
122
|
-
fonts
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
const
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
const
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (
|
|
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
|
-
case '
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const {
|
|
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
|
-
for (const
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
1
|
+
// Plan reducer — manages UploadPlan state for the upload modal review UI
|
|
2
|
+
|
|
3
|
+
import { PLAN_PHASE, FONT_STATUS, PROCESSING_OWNED_FIELDS } from './planTypes';
|
|
4
|
+
import { sanitizeForSanityId } from './sanitizeForSanityId';
|
|
5
|
+
import { retitleAllFonts } from './retitleFontEntries';
|
|
6
|
+
|
|
7
|
+
/** Valid phase transitions — any phase can transition to 'idle' (reset) */
|
|
8
|
+
const VALID_TRANSITIONS = {
|
|
9
|
+
[PLAN_PHASE.IDLE]: [PLAN_PHASE.PROCESSING],
|
|
10
|
+
[PLAN_PHASE.PROCESSING]: [PLAN_PHASE.REVIEWING],
|
|
11
|
+
[PLAN_PHASE.REVIEWING]: [PLAN_PHASE.READY, PLAN_PHASE.EXECUTING],
|
|
12
|
+
[PLAN_PHASE.READY]: [PLAN_PHASE.EXECUTING],
|
|
13
|
+
[PLAN_PHASE.EXECUTING]: [PLAN_PHASE.COMPLETE, PLAN_PHASE.ERROR],
|
|
14
|
+
[PLAN_PHASE.COMPLETE]: [PLAN_PHASE.EXECUTING],
|
|
15
|
+
[PLAN_PHASE.ERROR]: [PLAN_PHASE.EXECUTING],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Plan reducer for useReducer. Manages the UploadPlan state.
|
|
20
|
+
* Write domain isolation: processing actions only write processing-owned fields;
|
|
21
|
+
* user edit actions only write decisions.*.userOverride.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} state - Current UploadPlan
|
|
24
|
+
* @param {object} action - Dispatched action
|
|
25
|
+
* @returns {object} New UploadPlan
|
|
26
|
+
*/
|
|
27
|
+
export function planReducer(state, action) {
|
|
28
|
+
switch (action.type) {
|
|
29
|
+
// ---------------------------------------------------------------
|
|
30
|
+
// Phase / Settings
|
|
31
|
+
// ---------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
case 'SET_PHASE': {
|
|
34
|
+
if (action.phase === PLAN_PHASE.IDLE) {
|
|
35
|
+
return { ...state, phase: PLAN_PHASE.IDLE };
|
|
36
|
+
}
|
|
37
|
+
const validNext = VALID_TRANSITIONS[state.phase] || [];
|
|
38
|
+
if (!validNext.includes(action.phase)) {
|
|
39
|
+
console.warn(`Invalid phase transition: ${state.phase} → ${action.phase}`);
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
42
|
+
const nextState = { ...state, phase: action.phase };
|
|
43
|
+
// Allow setting total file count when entering PROCESSING phase
|
|
44
|
+
if (typeof action.totalFiles === 'number') {
|
|
45
|
+
nextState.processingProgress = {
|
|
46
|
+
...state.processingProgress,
|
|
47
|
+
total: action.totalFiles,
|
|
48
|
+
completed: 0,
|
|
49
|
+
failed: 0,
|
|
50
|
+
currentFile: null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return nextState;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case 'SET_SETTINGS': {
|
|
57
|
+
if (state.phase !== PLAN_PHASE.IDLE && state.phase !== PLAN_PHASE.REVIEWING && state.phase !== PLAN_PHASE.READY) {
|
|
58
|
+
console.warn('SET_SETTINGS blocked — settings locked during processing/execution');
|
|
59
|
+
return state;
|
|
60
|
+
}
|
|
61
|
+
const newSettings = { ...state.settings, ...action.settings };
|
|
62
|
+
let newState = { ...state, settings: newSettings };
|
|
63
|
+
|
|
64
|
+
// Retitle fonts when preserveShortenedNames changes during review
|
|
65
|
+
const shortenedChanged = 'preserveShortenedNames' in action.settings
|
|
66
|
+
&& action.settings.preserveShortenedNames !== state.settings.preserveShortenedNames;
|
|
67
|
+
if (shortenedChanged && Object.keys(state.fonts).length > 0) {
|
|
68
|
+
const typefaceTitle = action.typefaceTitle || state.settings.typefaceTitle || '';
|
|
69
|
+
const retitled = retitleAllFonts(state.fonts, newSettings.preserveShortenedNames, typefaceTitle);
|
|
70
|
+
const subfamilyGroups = rebuildSubfamilyGroups(retitled);
|
|
71
|
+
newState = { ...newState, fonts: retitled, subfamilyGroups };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return newState;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------
|
|
78
|
+
// Processing (Phase 1) — dispatched by buildUploadPlan callbacks
|
|
79
|
+
// ---------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
case 'UPDATE_PROCESSING_PROGRESS': {
|
|
82
|
+
return {
|
|
83
|
+
...state,
|
|
84
|
+
processingProgress: { ...state.processingProgress, ...action.progress },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case 'ADD_PROCESSED_FONT': {
|
|
89
|
+
const { tempId, fontEntry } = action;
|
|
90
|
+
const fonts = { ...state.fonts };
|
|
91
|
+
|
|
92
|
+
if (fonts[tempId]) {
|
|
93
|
+
// Font already exists (user may have edited) — merge processing-owned fields only
|
|
94
|
+
const existing = fonts[tempId];
|
|
95
|
+
const merged = { ...existing };
|
|
96
|
+
for (const field of PROCESSING_OWNED_FIELDS) {
|
|
97
|
+
merged[field] = fontEntry[field];
|
|
98
|
+
}
|
|
99
|
+
fonts[tempId] = merged;
|
|
100
|
+
} else {
|
|
101
|
+
fonts[tempId] = fontEntry;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Update subfamily groups
|
|
105
|
+
const subfamilyGroups = { ...state.subfamilyGroups };
|
|
106
|
+
const sfName = fontEntry.subfamily || 'default';
|
|
107
|
+
if (!fontEntry.variableFont || fontEntry.subfamily) {
|
|
108
|
+
if (!subfamilyGroups[sfName]) {
|
|
109
|
+
subfamilyGroups[sfName] = { title: sfName, fontIds: [] };
|
|
110
|
+
}
|
|
111
|
+
// Guard against duplicate tempId in fontIds
|
|
112
|
+
if (!subfamilyGroups[sfName].fontIds.includes(tempId)) {
|
|
113
|
+
subfamilyGroups[sfName] = {
|
|
114
|
+
...subfamilyGroups[sfName],
|
|
115
|
+
fontIds: [...subfamilyGroups[sfName].fontIds, tempId],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
...state,
|
|
122
|
+
fonts,
|
|
123
|
+
subfamilyGroups,
|
|
124
|
+
processingProgress: {
|
|
125
|
+
...state.processingProgress,
|
|
126
|
+
completed: state.processingProgress.completed + 1,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'SET_PROCESSING_ERROR': {
|
|
132
|
+
const { tempId, error } = action;
|
|
133
|
+
if (!state.fonts[tempId]) return state;
|
|
134
|
+
return {
|
|
135
|
+
...state,
|
|
136
|
+
fonts: {
|
|
137
|
+
...state.fonts,
|
|
138
|
+
[tempId]: { ...state.fonts[tempId], status: FONT_STATUS.ERROR, error },
|
|
139
|
+
},
|
|
140
|
+
processingProgress: {
|
|
141
|
+
...state.processingProgress,
|
|
142
|
+
failed: state.processingProgress.failed + 1,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------
|
|
148
|
+
// User Edits (Review Step)
|
|
149
|
+
// ---------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
case 'SET_FONT_TITLE': {
|
|
152
|
+
const { tempId, title, source: titleSource } = action;
|
|
153
|
+
const font = state.fonts[tempId];
|
|
154
|
+
if (!font) return state;
|
|
155
|
+
|
|
156
|
+
// When source is provided (e.g. from clicking a name table suggestion), preserve it.
|
|
157
|
+
// The display layer appends "(user override)" — don't bake it into the source string.
|
|
158
|
+
const newSource = titleSource || 'user-override';
|
|
159
|
+
|
|
160
|
+
const updated = {
|
|
161
|
+
...font,
|
|
162
|
+
title,
|
|
163
|
+
decisions: {
|
|
164
|
+
...font.decisions,
|
|
165
|
+
title: { ...font.decisions.title, userOverride: title, source: newSource },
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Auto-derive documentId unless it has its own override
|
|
170
|
+
if (!font.decisions.documentId.userOverride) {
|
|
171
|
+
const newDocId = sanitizeForSanityId(title);
|
|
172
|
+
updated.documentId = newDocId;
|
|
173
|
+
updated.decisions = {
|
|
174
|
+
...updated.decisions,
|
|
175
|
+
documentId: { ...updated.decisions.documentId, generated: newDocId },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return updateFontAndCheckConflicts(state, tempId, updated);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'SET_FONT_DOCUMENT_ID': {
|
|
183
|
+
const { tempId, documentId } = action;
|
|
184
|
+
const font = state.fonts[tempId];
|
|
185
|
+
if (!font) return state;
|
|
186
|
+
|
|
187
|
+
const sanitized = sanitizeForSanityId(documentId);
|
|
188
|
+
const updated = {
|
|
189
|
+
...font,
|
|
190
|
+
documentId: sanitized,
|
|
191
|
+
decisions: {
|
|
192
|
+
...font.decisions,
|
|
193
|
+
documentId: { ...font.decisions.documentId, userOverride: documentId },
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return updateFontAndCheckConflicts(state, tempId, updated);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case 'SET_FONT_WEIGHT': {
|
|
201
|
+
const { tempId, weight } = action;
|
|
202
|
+
const font = state.fonts[tempId];
|
|
203
|
+
if (!font) return state;
|
|
204
|
+
|
|
205
|
+
const clamped = Math.max(1, Math.min(1000, weight));
|
|
206
|
+
return {
|
|
207
|
+
...state,
|
|
208
|
+
fonts: {
|
|
209
|
+
...state.fonts,
|
|
210
|
+
[tempId]: {
|
|
211
|
+
...font,
|
|
212
|
+
weight: clamped,
|
|
213
|
+
decisions: {
|
|
214
|
+
...font.decisions,
|
|
215
|
+
weight: { ...font.decisions.weight, userOverride: clamped },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case 'SET_FONT_WEIGHT_NAME': {
|
|
223
|
+
const { tempId, weightName } = action;
|
|
224
|
+
const font = state.fonts[tempId];
|
|
225
|
+
if (!font) return state;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
...state,
|
|
229
|
+
fonts: {
|
|
230
|
+
...state.fonts,
|
|
231
|
+
[tempId]: {
|
|
232
|
+
...font,
|
|
233
|
+
weightName,
|
|
234
|
+
decisions: {
|
|
235
|
+
...font.decisions,
|
|
236
|
+
weightName: { ...font.decisions.weightName, userOverride: weightName },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
case 'SET_FONT_STYLE': {
|
|
244
|
+
const { tempId, style } = action;
|
|
245
|
+
const font = state.fonts[tempId];
|
|
246
|
+
if (!font) return state;
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...state,
|
|
250
|
+
fonts: {
|
|
251
|
+
...state.fonts,
|
|
252
|
+
[tempId]: {
|
|
253
|
+
...font,
|
|
254
|
+
style,
|
|
255
|
+
decisions: {
|
|
256
|
+
...font.decisions,
|
|
257
|
+
style: { ...font.decisions.style, userOverride: style },
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case 'SET_FONT_SUBFAMILY': {
|
|
265
|
+
const { tempId, subfamily } = action;
|
|
266
|
+
const font = state.fonts[tempId];
|
|
267
|
+
if (!font) return state;
|
|
268
|
+
|
|
269
|
+
const oldSubfamily = font.subfamily || 'default';
|
|
270
|
+
const newSubfamily = subfamily || 'default';
|
|
271
|
+
|
|
272
|
+
const updated = {
|
|
273
|
+
...font,
|
|
274
|
+
subfamily,
|
|
275
|
+
decisions: {
|
|
276
|
+
...font.decisions,
|
|
277
|
+
subfamily: { ...font.decisions.subfamily, userOverride: subfamily },
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
let newState = { ...state, fonts: { ...state.fonts, [tempId]: updated } };
|
|
282
|
+
|
|
283
|
+
// Move between subfamily groups
|
|
284
|
+
if (oldSubfamily !== newSubfamily) {
|
|
285
|
+
newState = moveFontBetweenGroups(newState, tempId, oldSubfamily, newSubfamily);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return newState;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
case 'SET_FONT_ACTION': {
|
|
292
|
+
const { tempId, decision } = action;
|
|
293
|
+
const font = state.fonts[tempId];
|
|
294
|
+
if (!font) return state;
|
|
295
|
+
|
|
296
|
+
const existingDoc = { ...font.decisions.existingDocument, userChoice: decision };
|
|
297
|
+
if (decision === 'create') {
|
|
298
|
+
existingDoc.selectedCandidate = null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
...state,
|
|
303
|
+
fonts: {
|
|
304
|
+
...state.fonts,
|
|
305
|
+
[tempId]: {
|
|
306
|
+
...font,
|
|
307
|
+
decisions: { ...font.decisions, existingDocument: existingDoc },
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
case 'SET_FONT_CANDIDATE': {
|
|
314
|
+
const { tempId, candidate } = action;
|
|
315
|
+
const font = state.fonts[tempId];
|
|
316
|
+
if (!font) return state;
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
...state,
|
|
320
|
+
fonts: {
|
|
321
|
+
...state.fonts,
|
|
322
|
+
[tempId]: {
|
|
323
|
+
...font,
|
|
324
|
+
documentId: candidate._id,
|
|
325
|
+
decisions: {
|
|
326
|
+
...font.decisions,
|
|
327
|
+
existingDocument: {
|
|
328
|
+
...font.decisions.existingDocument,
|
|
329
|
+
selectedCandidate: candidate,
|
|
330
|
+
userChoice: 'update',
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------
|
|
339
|
+
// Subfamily Organization
|
|
340
|
+
// ---------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
case 'MOVE_FONT_TO_SUBFAMILY': {
|
|
343
|
+
const { tempId, fromSubfamily, toSubfamily } = action;
|
|
344
|
+
const font = state.fonts[tempId];
|
|
345
|
+
if (!font) return state;
|
|
346
|
+
|
|
347
|
+
let newState = {
|
|
348
|
+
...state,
|
|
349
|
+
fonts: {
|
|
350
|
+
...state.fonts,
|
|
351
|
+
[tempId]: { ...font, subfamily: toSubfamily },
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
return moveFontBetweenGroups(newState, tempId, fromSubfamily, toSubfamily);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
case 'CREATE_SUBFAMILY_GROUP': {
|
|
358
|
+
const { title } = action;
|
|
359
|
+
if (state.subfamilyGroups[title]) return state;
|
|
360
|
+
return {
|
|
361
|
+
...state,
|
|
362
|
+
subfamilyGroups: {
|
|
363
|
+
...state.subfamilyGroups,
|
|
364
|
+
[title]: { title, fontIds: [] },
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case 'REMOVE_SUBFAMILY_GROUP': {
|
|
370
|
+
const { title } = action;
|
|
371
|
+
const group = state.subfamilyGroups[title];
|
|
372
|
+
if (!group) return state;
|
|
373
|
+
if (group.fontIds.length > 0) {
|
|
374
|
+
console.warn('Cannot remove subfamily group with fonts — reassign fonts first');
|
|
375
|
+
return state;
|
|
376
|
+
}
|
|
377
|
+
const { [title]: _, ...remaining } = state.subfamilyGroups;
|
|
378
|
+
return { ...state, subfamilyGroups: remaining };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------
|
|
382
|
+
// Bulk Actions
|
|
383
|
+
// ---------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
case 'ACCEPT_ALL_SUGGESTIONS': {
|
|
386
|
+
const scope = action.scope || Object.keys(state.fonts);
|
|
387
|
+
const fonts = { ...state.fonts };
|
|
388
|
+
|
|
389
|
+
for (const tempId of scope) {
|
|
390
|
+
if (!fonts[tempId]) continue;
|
|
391
|
+
fonts[tempId] = resetFontToSuggestions(fonts[tempId]);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Rebuild subfamily groups after resetting
|
|
395
|
+
const subfamilyGroups = rebuildSubfamilyGroups(fonts);
|
|
396
|
+
return { ...state, fonts, subfamilyGroups };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
case 'RESET_FONT_TO_SUGGESTIONS': {
|
|
400
|
+
const { tempId } = action;
|
|
401
|
+
const font = state.fonts[tempId];
|
|
402
|
+
if (!font) return state;
|
|
403
|
+
|
|
404
|
+
const reset = resetFontToSuggestions(font);
|
|
405
|
+
const fonts = { ...state.fonts, [tempId]: reset };
|
|
406
|
+
const subfamilyGroups = rebuildSubfamilyGroups(fonts);
|
|
407
|
+
return { ...state, fonts, subfamilyGroups };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case 'REMOVE_FONT': {
|
|
411
|
+
const { tempId } = action;
|
|
412
|
+
if (!state.fonts[tempId]) return state;
|
|
413
|
+
|
|
414
|
+
const { [tempId]: removed, ...remainingFonts } = state.fonts;
|
|
415
|
+
|
|
416
|
+
// Remove from subfamily groups
|
|
417
|
+
const subfamilyGroups = {};
|
|
418
|
+
for (const [key, group] of Object.entries(state.subfamilyGroups)) {
|
|
419
|
+
const filtered = group.fontIds.filter(id => id !== tempId);
|
|
420
|
+
if (filtered.length > 0) {
|
|
421
|
+
subfamilyGroups[key] = { ...group, fontIds: filtered };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return { ...state, fonts: remainingFonts, subfamilyGroups };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
default:
|
|
429
|
+
return state;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// -----------------------------------------------------------------------
|
|
434
|
+
// Helpers
|
|
435
|
+
// -----------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
/** Resets a font entry's overrides back to system-detected values */
|
|
438
|
+
function resetFontToSuggestions(font) {
|
|
439
|
+
const d = font.decisions;
|
|
440
|
+
return {
|
|
441
|
+
...font,
|
|
442
|
+
title: d.title.processed,
|
|
443
|
+
documentId: d.documentId.generated,
|
|
444
|
+
weight: d.weight.detected,
|
|
445
|
+
weightName: d.weightName.detected,
|
|
446
|
+
style: d.style.detected,
|
|
447
|
+
subfamily: d.subfamily.detected,
|
|
448
|
+
_idConflict: false,
|
|
449
|
+
decisions: {
|
|
450
|
+
...d,
|
|
451
|
+
title: { ...d.title, userOverride: null, source: d.title.original ? d.title.source : d.title.source },
|
|
452
|
+
documentId: { ...d.documentId, userOverride: null },
|
|
453
|
+
weight: { ...d.weight, userOverride: null },
|
|
454
|
+
weightName: { ...d.weightName, userOverride: null },
|
|
455
|
+
style: { ...d.style, userOverride: null },
|
|
456
|
+
subfamily: { ...d.subfamily, userOverride: null },
|
|
457
|
+
existingDocument: { ...d.existingDocument, userChoice: null, selectedCandidate: null },
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Moves a font between subfamily groups, creating/removing groups as needed */
|
|
463
|
+
function moveFontBetweenGroups(state, tempId, fromKey, toKey) {
|
|
464
|
+
const groups = { ...state.subfamilyGroups };
|
|
465
|
+
|
|
466
|
+
// Remove from old group
|
|
467
|
+
if (groups[fromKey]) {
|
|
468
|
+
const filtered = groups[fromKey].fontIds.filter(id => id !== tempId);
|
|
469
|
+
if (filtered.length === 0) {
|
|
470
|
+
delete groups[fromKey];
|
|
471
|
+
} else {
|
|
472
|
+
groups[fromKey] = { ...groups[fromKey], fontIds: filtered };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Add to new group (guard against duplicates)
|
|
477
|
+
if (!groups[toKey]) {
|
|
478
|
+
groups[toKey] = { title: toKey, fontIds: [] };
|
|
479
|
+
}
|
|
480
|
+
if (!groups[toKey].fontIds.includes(tempId)) {
|
|
481
|
+
groups[toKey] = { ...groups[toKey], fontIds: [...groups[toKey].fontIds, tempId] };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return { ...state, subfamilyGroups: groups };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Updates a font and checks for documentId collisions across all fonts */
|
|
488
|
+
function updateFontAndCheckConflicts(state, tempId, updatedFont) {
|
|
489
|
+
const fonts = { ...state.fonts, [tempId]: updatedFont };
|
|
490
|
+
|
|
491
|
+
// Clear old conflicts and detect new ones
|
|
492
|
+
const idMap = {};
|
|
493
|
+
for (const [id, font] of Object.entries(fonts)) {
|
|
494
|
+
fonts[id] = { ...font, _idConflict: false };
|
|
495
|
+
const docId = font.documentId;
|
|
496
|
+
if (!idMap[docId]) {
|
|
497
|
+
idMap[docId] = [id];
|
|
498
|
+
} else {
|
|
499
|
+
idMap[docId].push(id);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Mark conflicts
|
|
504
|
+
for (const ids of Object.values(idMap)) {
|
|
505
|
+
if (ids.length > 1) {
|
|
506
|
+
for (const id of ids) {
|
|
507
|
+
fonts[id] = { ...fonts[id], _idConflict: true };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return { ...state, fonts };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Rebuilds subfamily groups from the fonts map */
|
|
516
|
+
function rebuildSubfamilyGroups(fonts) {
|
|
517
|
+
const groups = {};
|
|
518
|
+
for (const [tempId, font] of Object.entries(fonts)) {
|
|
519
|
+
if (font.status === FONT_STATUS.ERROR) continue;
|
|
520
|
+
const sfName = font.subfamily || 'default';
|
|
521
|
+
if (!font.variableFont || font.subfamily) {
|
|
522
|
+
if (!groups[sfName]) {
|
|
523
|
+
groups[sfName] = { title: sfName, fontIds: [] };
|
|
524
|
+
}
|
|
525
|
+
if (!groups[sfName].fontIds.includes(tempId)) {
|
|
526
|
+
groups[sfName].fontIds.push(tempId);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return groups;
|
|
531
|
+
}
|