@liiift-studio/sanity-font-manager 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/UploadModal-6LIX7XOK.js +6 -0
  2. package/dist/UploadModal-NME2W53V.mjs +6 -0
  3. package/dist/chunk-646WCBRR.mjs +7276 -0
  4. package/dist/chunk-FH4QKHOH.js +7276 -0
  5. package/dist/index.js +664 -1647
  6. package/dist/index.mjs +317 -1209
  7. package/package.json +5 -5
  8. package/src/components/BatchUploadFonts.jsx +57 -44
  9. package/src/components/BulkActions.jsx +99 -0
  10. package/src/components/ExistingDocumentResolver.jsx +152 -0
  11. package/src/components/FontReviewCard.jsx +415 -0
  12. package/src/components/SingleUploaderTool.jsx +3 -4
  13. package/src/components/UploadModal.jsx +268 -0
  14. package/src/components/UploadScriptsComponent.jsx +23 -21
  15. package/src/components/UploadStep1Settings.jsx +272 -0
  16. package/src/components/UploadStep2Review.jsx +472 -0
  17. package/src/components/UploadStep3Execute.jsx +234 -0
  18. package/src/components/UploadSummary.jsx +196 -0
  19. package/src/index.js +45 -0
  20. package/src/utils/buildUploadPlan.js +325 -0
  21. package/src/utils/executeUploadPlan.js +437 -0
  22. package/src/utils/executionReducer.js +56 -0
  23. package/src/utils/fontHelpers.js +267 -0
  24. package/src/utils/generateCssFile.js +79 -77
  25. package/src/utils/generateFontData.js +47 -94
  26. package/src/utils/getEmptyFontKit.js +19 -17
  27. package/src/utils/parseFont.js +55 -0
  28. package/src/utils/planReducer.js +517 -0
  29. package/src/utils/planTypes.js +183 -0
  30. package/src/utils/processFontFiles.js +120 -78
  31. package/src/utils/regenerateFontData.js +2 -2
  32. package/src/utils/resolveExistingFont.js +87 -0
  33. package/src/utils/uploadFontFiles.js +405 -405
@@ -0,0 +1,325 @@
1
+ // Phase 1 orchestrator — parses font files, resolves existing documents, builds a complete upload plan for user review
2
+
3
+ import { nanoid } from 'nanoid';
4
+ import { parseFont } from './parseFont';
5
+ import { readFontFile, extractFontMetadata, determineWeight } from './processFontFiles';
6
+ import {
7
+ getNameString,
8
+ getFontMetadata,
9
+ getFontMetrics,
10
+ getVariationAxes,
11
+ getNamedInstances,
12
+ getAllFeatureTags,
13
+ getGlyphCount,
14
+ getCharacterSet,
15
+ getItalicAngle,
16
+ getWeightClass,
17
+ } from './fontHelpers';
18
+ import { resolveExistingFont } from './resolveExistingFont';
19
+ import { sanitizeForSanityId } from './sanitizeForSanityId';
20
+ import {
21
+ FONT_STATUS,
22
+ PLAN_PHASE,
23
+ RECOMMENDATION,
24
+ PLAN_VERSION,
25
+ createFontDecisions,
26
+ createEmptyPlan,
27
+ } from './planTypes';
28
+
29
+ /**
30
+ * Phase 1: Reads font files, parses metadata, resolves existing documents,
31
+ * returns a complete upload plan for user review.
32
+ *
33
+ * NOTE: This function performs Sanity READ operations (existing document lookups)
34
+ * but NO writes. The client parameter is used for reads only.
35
+ *
36
+ * @param {object} params
37
+ * @param {File[]} params.files - Font files to process
38
+ * @param {string} params.typefaceTitle - Parent typeface title
39
+ * @param {string} params.docId - Typeface document _id (for scoping resolution queries)
40
+ * @param {object} params.settings - { price, preserveShortenedNames, preserveFileNames }
41
+ * @param {object} params.client - Sanity client (read-only usage)
42
+ * @param {object} params.stylesObject - Existing typeface styles
43
+ * @param {string[]} params.weightKeywordList - Weight keywords for extraction
44
+ * @param {string[]} params.italicKeywordList - Italic keywords for extraction
45
+ * @param {function} params.onProgress - Called after each font is processed
46
+ * @returns {Promise<object>} UploadPlan object
47
+ */
48
+ export async function buildUploadPlan({
49
+ files,
50
+ typefaceTitle,
51
+ docId,
52
+ settings = {},
53
+ client,
54
+ stylesObject = {},
55
+ weightKeywordList = [],
56
+ italicKeywordList = [],
57
+ onProgress,
58
+ }) {
59
+ const plan = createEmptyPlan(settings);
60
+ plan.phase = PLAN_PHASE.PROCESSING;
61
+ plan.processingProgress.total = files.length;
62
+
63
+ for (let i = 0; i < files.length; i++) {
64
+ const file = files[i];
65
+ plan.processingProgress.currentFile = file.name;
66
+
67
+ try {
68
+ const fontBuffer = await readFontFile(file);
69
+ const font = await parseFont(fontBuffer, file.name);
70
+
71
+ const entry = await buildFontPlanEntry({
72
+ file,
73
+ font,
74
+ typefaceTitle,
75
+ settings: plan.settings,
76
+ weightKeywordList,
77
+ italicKeywordList,
78
+ client,
79
+ });
80
+
81
+ // Merge into plan — if same documentId, merge files
82
+ const existingKey = Object.keys(plan.fonts).find(k => plan.fonts[k].documentId === entry.documentId);
83
+ if (existingKey) {
84
+ plan.fonts[existingKey].files = [...plan.fonts[existingKey].files, ...entry.files];
85
+ } else {
86
+ plan.fonts[entry.tempId] = entry;
87
+
88
+ // Add to subfamily group
89
+ if (!entry.variableFont || entry.subfamily) {
90
+ const sfName = entry.subfamily;
91
+ if (!plan.subfamilyGroups[sfName]) {
92
+ plan.subfamilyGroups[sfName] = { title: sfName, fontIds: [] };
93
+ }
94
+ plan.subfamilyGroups[sfName].fontIds.push(entry.tempId);
95
+ }
96
+ }
97
+
98
+ plan.processingProgress.completed++;
99
+
100
+ if (onProgress) {
101
+ onProgress({
102
+ type: 'font-processed',
103
+ tempId: entry.tempId,
104
+ fontEntry: entry,
105
+ progress: { ...plan.processingProgress },
106
+ });
107
+ }
108
+ } catch (err) {
109
+ console.error(`Error processing ${file.name}:`, err.message);
110
+ plan.processingProgress.failed++;
111
+
112
+ const errorTempId = sanitizeForSanityId(file.name) + '-' + nanoid(6);
113
+ plan.fonts[errorTempId] = {
114
+ tempId: errorTempId,
115
+ files: [file],
116
+ sourceFileName: file.name,
117
+ title: file.name,
118
+ documentId: '',
119
+ weight: 400,
120
+ weightName: '',
121
+ style: 'Regular',
122
+ subfamily: '',
123
+ variableFont: false,
124
+ originalFilename: null,
125
+ decisions: createFontDecisions({}),
126
+ status: FONT_STATUS.ERROR,
127
+ error: err.message,
128
+ parsedMetadata: null,
129
+ glyphCount: 0,
130
+ opentypeFeatures: [],
131
+ variationAxes: null,
132
+ };
133
+
134
+ if (onProgress) {
135
+ onProgress({
136
+ type: 'font-error',
137
+ tempId: errorTempId,
138
+ error: err.message,
139
+ progress: { ...plan.processingProgress },
140
+ });
141
+ }
142
+ }
143
+ }
144
+
145
+ plan.processingProgress.currentFile = null;
146
+ plan.phase = PLAN_PHASE.REVIEWING;
147
+
148
+ if (onProgress) {
149
+ onProgress({
150
+ type: 'processing-complete',
151
+ progress: { ...plan.processingProgress },
152
+ });
153
+ }
154
+
155
+ return plan;
156
+ }
157
+
158
+ /**
159
+ * Builds a single FontPlanEntry from a parsed font file.
160
+ * @param {object} params
161
+ * @returns {Promise<object>} FontPlanEntry
162
+ */
163
+ async function buildFontPlanEntry({
164
+ file,
165
+ font,
166
+ typefaceTitle,
167
+ settings,
168
+ weightKeywordList,
169
+ italicKeywordList,
170
+ client,
171
+ }) {
172
+ // Extract metadata using existing processFontFiles helpers
173
+ const { weightName, subfamilyName, fontTitle, style, italicKW, variableFont } = extractFontMetadata(
174
+ font,
175
+ typefaceTitle,
176
+ weightKeywordList,
177
+ italicKeywordList,
178
+ settings.preserveShortenedNames,
179
+ );
180
+
181
+ // Determine title and ID based on preserveFileNames setting
182
+ let finalTitle = fontTitle;
183
+ let originalFilename = null;
184
+
185
+ if (settings.preserveFileNames) {
186
+ originalFilename = file.name.replace(/\.(ttf|otf|woff2?|eot|svg)$/i, '');
187
+ const normalizedName = originalFilename
188
+ .replace(/-/g, ' ')
189
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
190
+ .replace(/\s+/g, ' ')
191
+ .trim();
192
+ finalTitle = normalizedName;
193
+ }
194
+
195
+ const documentId = sanitizeForSanityId(finalTitle);
196
+ const tempId = documentId + '-' + nanoid(6);
197
+ const weight = Number(determineWeight(font, weightName));
198
+
199
+ // Build title alternatives from all name sources
200
+ const titleAlternatives = [
201
+ { value: getNameString(font, 1), source: 'nameId1-familyName' },
202
+ { value: getNameString(font, 4), source: 'nameId4-fullName' },
203
+ { value: getNameString(font, 6), source: 'nameId6-postscriptName' },
204
+ { value: getNameString(font, 16), source: 'nameId16-preferredFamily' },
205
+ { value: getNameString(font, 17), source: 'nameId17-preferredSubfamily' },
206
+ { value: file.name.replace(/\.(ttf|otf|woff2?|eot|svg)$/i, ''), source: 'filename' },
207
+ ].filter(alt => alt.value);
208
+
209
+ // Determine title source
210
+ const titleSource = settings.preserveFileNames ? 'filename' : 'fontkit-fullName';
211
+
212
+ // Determine weight source
213
+ const usWeightClass = getWeightClass(font);
214
+ const weightSource = usWeightClass ? 'os2-usWeightClass' : 'keyword-match';
215
+ const matchedKeyword = usWeightClass ? null : weightName;
216
+
217
+ // Determine weight name source — nameId17 (preferredSubfamily) or nameId2 (fontSubfamily)
218
+ const nameId17 = getNameString(font, 17);
219
+ const weightNameSource = variableFont
220
+ ? 'variable-font-empty'
221
+ : nameId17
222
+ ? 'nameId17-preferredSubfamily'
223
+ : 'nameId2-fontSubfamily';
224
+
225
+ // Determine style source
226
+ const italicAngle = getItalicAngle(font);
227
+ const fullName = getNameString(font, 4);
228
+ let styleSource = 'default-regular';
229
+ let styleReason = '';
230
+ if (style === 'Italic') {
231
+ if (italicAngle !== 0) {
232
+ styleSource = 'italic-angle';
233
+ styleReason = `italicAngle = ${italicAngle}`;
234
+ } else if (fullName.toLowerCase().includes('italic')) {
235
+ styleSource = 'name-contains-italic';
236
+ styleReason = 'fullName contains "italic"';
237
+ }
238
+ }
239
+
240
+ // Determine subfamily source — mirrors the logic in extractFontMetadata
241
+ const familyNameRaw = getNameString(font, 1);
242
+ const nameId4Remainder = fullName ? fullName.replace(typefaceTitle.trim(), '').trim() : '';
243
+ const nameId1Remainder = familyNameRaw ? familyNameRaw.replace(typefaceTitle.trim(), '').trim() : '';
244
+ let subfamilySource = 'default-empty';
245
+ if (nameId4Remainder && subfamilyName) {
246
+ subfamilySource = 'nameId4-subtraction';
247
+ } else if (nameId1Remainder && subfamilyName) {
248
+ subfamilySource = 'nameId1-subtraction';
249
+ }
250
+
251
+ // Build decisions audit trail
252
+ const decisions = createFontDecisions({
253
+ titleSource,
254
+ title: finalTitle,
255
+ titleOriginal: getNameString(font, 4),
256
+ documentId,
257
+ weight,
258
+ weightSource,
259
+ matchedKeyword,
260
+ weightName,
261
+ weightNameSource,
262
+ style,
263
+ styleSource,
264
+ styleReason,
265
+ subfamily: subfamilyName,
266
+ subfamilySource,
267
+ titleAlternatives,
268
+ });
269
+
270
+ // Resolve existing document
271
+ const fontForResolution = {
272
+ _id: documentId,
273
+ typefaceName: typefaceTitle,
274
+ weightName,
275
+ style,
276
+ subfamily: subfamilyName,
277
+ variableFont,
278
+ title: finalTitle,
279
+ };
280
+
281
+ try {
282
+ const resolution = await resolveExistingFont(fontForResolution, client);
283
+ decisions.existingDocument = {
284
+ recommendation: resolution.recommendation,
285
+ exact: resolution.exact,
286
+ candidates: resolution.candidates,
287
+ userChoice: null,
288
+ selectedCandidate: null,
289
+ lookupFailed: resolution.lookupFailed || false,
290
+ };
291
+ } catch (err) {
292
+ console.warn('Document resolution failed for', documentId, err.message);
293
+ decisions.existingDocument.lookupFailed = true;
294
+ }
295
+
296
+ // Extract parsed metadata snapshot (no raw fontkit/lib-font object retained)
297
+ const metadata = getFontMetadata(font);
298
+ const metrics = getFontMetrics(font);
299
+ const axes = getVariationAxes(font);
300
+
301
+ // Default empty subfamily to "Regular" — matches Regenerate Subfamilies behavior
302
+ const finalSubfamily = variableFont ? '' : (subfamilyName || 'Regular');
303
+ console.log(`[buildFontPlanEntry] "${finalTitle}" → subfamily: "${subfamilyName}" → final: "${finalSubfamily}" (variableFont: ${variableFont})`);
304
+
305
+ return {
306
+ tempId,
307
+ files: [file],
308
+ sourceFileName: file.name,
309
+ title: finalTitle,
310
+ documentId,
311
+ weight,
312
+ weightName,
313
+ style,
314
+ subfamily: finalSubfamily,
315
+ variableFont,
316
+ originalFilename,
317
+ decisions,
318
+ status: FONT_STATUS.PROCESSED,
319
+ error: null,
320
+ parsedMetadata: { ...metadata, ...metrics },
321
+ glyphCount: getGlyphCount(font),
322
+ opentypeFeatures: getAllFeatureTags(font),
323
+ variationAxes: axes,
324
+ };
325
+ }