@liiift-studio/sanity-font-manager 2.4.0 → 2.5.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 (36) hide show
  1. package/dist/UploadModal-ADNRGQUI.mjs +6 -0
  2. package/dist/UploadModal-WPK2CXLR.js +6 -0
  3. package/dist/chunk-JCDZ7SWZ.js +7711 -0
  4. package/dist/chunk-TMDE4A54.mjs +7711 -0
  5. package/dist/index.js +666 -1647
  6. package/dist/index.mjs +319 -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 +455 -0
  12. package/src/components/SingleUploaderTool.jsx +3 -4
  13. package/src/components/UploadModal.jsx +304 -0
  14. package/src/components/UploadScriptsComponent.jsx +23 -21
  15. package/src/components/UploadStep1Settings.jsx +272 -0
  16. package/src/components/UploadStep2Review.jsx +474 -0
  17. package/src/components/UploadStep3Execute.jsx +234 -0
  18. package/src/components/UploadStep3bInstances.jsx +396 -0
  19. package/src/components/UploadSummary.jsx +196 -0
  20. package/src/index.js +46 -0
  21. package/src/utils/buildUploadPlan.js +326 -0
  22. package/src/utils/executeUploadPlan.js +430 -0
  23. package/src/utils/executionReducer.js +56 -0
  24. package/src/utils/fontHelpers.js +267 -0
  25. package/src/utils/generateCssFile.js +79 -77
  26. package/src/utils/generateFontData.js +47 -94
  27. package/src/utils/getEmptyFontKit.js +19 -17
  28. package/src/utils/parseFont.js +55 -0
  29. package/src/utils/parseVariableFontInstances.js +237 -147
  30. package/src/utils/planReducer.js +517 -0
  31. package/src/utils/planTypes.js +183 -0
  32. package/src/utils/processFontFiles.js +121 -78
  33. package/src/utils/regenerateFontData.js +2 -2
  34. package/src/utils/resolveExistingFont.js +87 -0
  35. package/src/utils/updateTypefaceDocument.js +15 -2
  36. package/src/utils/uploadFontFiles.js +405 -405
@@ -1,19 +1,20 @@
1
- // Reads font files via FileReader, parses with fontkit, and builds the fontsObjects map — exports individual weight/style extraction helpers
1
+ // Reads font files via FileReader, parses with lib-font, and builds the fontsObjects map — exports individual weight/style extraction helpers
2
2
 
3
- import * as fontkit from 'fontkit';
3
+ import { parseFont } from './parseFont';
4
+ import { getNameString, getVariationAxes, getItalicAngle, getWeightClass } from './fontHelpers';
4
5
  import { nanoid } from 'nanoid';
5
6
  import { expandAbbreviations, removeWeightNames, reverseSpellingLookup } from './generateKeywords';
6
7
  import { sanitizeForSanityId } from './sanitizeForSanityId';
7
8
 
8
9
  /**
9
- * Reads a font file and returns its content as a Uint8Array.
10
+ * Reads a font file and returns its content as an ArrayBuffer.
10
11
  * @param {File} file
11
- * @returns {Promise<Uint8Array>}
12
+ * @returns {Promise<ArrayBuffer>}
12
13
  */
13
14
  export const readFontFile = (file) => {
14
15
  return new Promise((resolve, reject) => {
15
16
  const reader = new FileReader();
16
- reader.onload = (event) => { resolve(new Uint8Array(event.target.result)); };
17
+ reader.onload = (event) => { resolve(event.target.result); };
17
18
  reader.onerror = (error) => { reject(error); };
18
19
  reader.readAsArrayBuffer(file);
19
20
  });
@@ -47,13 +48,12 @@ export const processFontFiles = async (
47
48
  for (let i = 0; i < files.length; i++) {
48
49
  const file = files[i];
49
50
  const fontBuffer = await readFontFile(file);
50
- const font = fontkit.create(fontBuffer);
51
+ const font = await parseFont(fontBuffer, file.name);
51
52
 
52
- console.log('File name: ', file.name);
53
+ console.log('File name:', file.name);
53
54
 
54
- if (file.name.endsWith('.woff2') || file.name.endsWith('.woff')) {
55
- await handleWebfontMetadata(file, font, files);
56
- }
55
+ // For webfonts with missing metadata, try to extract from TTF companion
56
+ const ttfFallbackMeta = await getWebfontFallbackMetadata(file, font, files);
57
57
 
58
58
  let { weightName, subfamilyName, fontTitle, style, italicKW, variableFont } = extractFontMetadata(
59
59
  font,
@@ -61,6 +61,7 @@ export const processFontFiles = async (
61
61
  weightKeywordList,
62
62
  italicKeywordList,
63
63
  preserveShortenedNames,
64
+ ttfFallbackMeta,
64
65
  );
65
66
 
66
67
  let id;
@@ -109,72 +110,107 @@ export const processFontFiles = async (
109
110
 
110
111
  console.log('Subfamilies:', subfamilies);
111
112
  console.log('Unique subfamilies:', uniqueSubfamilies, uniqueSubfamilies.length);
112
- console.log('Font objects:', fontsObjects);
113
+ console.log('Font objects:', Object.keys(fontsObjects));
113
114
 
114
115
  return { fontsObjects, subfamilies, uniqueSubfamilies, newPreferredStyle, failedFiles };
115
116
  };
116
117
 
117
118
  /**
118
- * Patches webfont name records from a matching TTF when woff/woff2 metadata is missing.
119
+ * Gets fallback metadata from a matching TTF when woff/woff2 metadata is missing.
120
+ * Returns null if no fallback is needed or no TTF companion exists.
121
+ * Unlike the old fontkit approach, this does NOT mutate the font object.
119
122
  * @param {File} file
120
- * @param {Object} font
123
+ * @param {object} font - lib-font parsed font
121
124
  * @param {File[]} files
125
+ * @returns {Promise<{ fullName: string, familyName: string, subfamilyName: string, preferredSubfamily: string }|null>}
122
126
  */
123
- const handleWebfontMetadata = async (file, font, files) => {
124
- if (
125
- !font?.name?.records?.fullName ||
126
- font?.name?.records?.fullName === '' ||
127
- !/^[A-Z0-9]+$/.test(font?.name?.records?.fullName)
128
- ) {
129
- const ttfFile = files.find(f => f.name === file.name.replace('.woff2', '.ttf').replace('.woff', '.ttf'));
130
- if (ttfFile) {
131
- const ttfFileBuffer = await readFontFile(ttfFile);
132
- const ttfFileData = fontkit.create(ttfFileBuffer);
133
- if (ttfFileData) font.name.records = ttfFileData?.name?.records;
134
- }
127
+ const getWebfontFallbackMetadata = async (file, font, files) => {
128
+ if (!file.name.endsWith('.woff2') && !file.name.endsWith('.woff')) return null;
129
+
130
+ const fullName = getNameString(font, 4);
131
+ // Check if name table is missing or corrupt (empty, or only uppercase hex-like garbage)
132
+ if (fullName && fullName !== '' && !/^[A-Z0-9]+$/.test(fullName)) return null;
133
+
134
+ const ttfFile = files.find(f => f.name === file.name.replace('.woff2', '.ttf').replace('.woff', '.ttf'));
135
+ if (!ttfFile) return null;
136
+
137
+ try {
138
+ const ttfBuffer = await readFontFile(ttfFile);
139
+ const ttfFont = await parseFont(ttfBuffer, ttfFile.name);
140
+ return {
141
+ fullName: getNameString(ttfFont, 4),
142
+ familyName: getNameString(ttfFont, 1),
143
+ subfamilyName: getNameString(ttfFont, 2),
144
+ preferredSubfamily: getNameString(ttfFont, 17),
145
+ preferredFamily: getNameString(ttfFont, 16),
146
+ };
147
+ } catch (err) {
148
+ console.warn('Could not parse TTF companion for webfont fallback:', err.message);
149
+ return null;
135
150
  }
136
151
  };
137
152
 
138
153
  /**
139
- * Extracts and normalises metadata from a fontkit font object.
140
- * @param {Object} font
141
- * @param {string} title
154
+ * Extracts and normalises metadata from a lib-font parsed font object.
155
+ * @param {object} font - lib-font parsed font
156
+ * @param {string} title - Typeface title
142
157
  * @param {string[]} weightKeywordList
143
158
  * @param {string[]} italicKeywordList
144
159
  * @param {boolean} preserveShortenedNames
160
+ * @param {object|null} ttfFallbackMeta - Fallback metadata from TTF companion (for webfonts with missing names)
145
161
  * @returns {Object}
146
162
  */
147
- export const extractFontMetadata = (font, title, weightKeywordList, italicKeywordList, preserveShortenedNames = false) => {
148
- let weightName = extractWeightName(font, italicKeywordList);
163
+ export const extractFontMetadata = (font, title, weightKeywordList, italicKeywordList, preserveShortenedNames = false, ttfFallbackMeta = null) => {
164
+ let weightName = extractWeightName(font, italicKeywordList, ttfFallbackMeta);
149
165
  if (!preserveShortenedNames) {
150
166
  weightName = expandAbbreviations(weightName);
151
167
  }
152
168
 
153
- if ((weightName === '' || weightName.toLowerCase() === 'roman') && font?.name?.records?.fullName) {
154
- weightName = extractWeightFromFullName(font, title);
169
+ const fullName = getNameString(font, 4) || ttfFallbackMeta?.fullName || '';
170
+
171
+ const axes = getVariationAxes(font);
172
+ const variableFont = axes !== null;
173
+
174
+ // For non-VF fonts, fall back to extracting weight from fullName when weightName is empty
175
+ if (!variableFont && (weightName === '' || weightName.toLowerCase() === 'roman') && fullName) {
176
+ weightName = extractWeightFromFullName(font, title, ttfFallbackMeta);
155
177
  if (!preserveShortenedNames) {
156
178
  weightName = expandAbbreviations(weightName);
157
179
  }
158
180
  }
159
181
 
160
- const variableFont = font?.variationAxes && Object.keys(font.variationAxes).length > 0;
182
+ // Subfamily detection extract width/optical variant from name table.
183
+ // Primary: nameId4 (fullName) minus typeface title — the most complete name record,
184
+ // always contains width + weight (e.g. "Gear XXNarrow Regular" → "XXNarrow Regular").
185
+ // Fallback: nameId1 (familyName) minus typeface title — contains width but not always weight.
186
+ // processSubfamilyName then strips weight/italic keywords, leaving just the width variant.
187
+ // This matches the production logic that has been reliable across all foundry sites.
188
+ const trimmedTitle = title.trim();
161
189
 
162
- let subfamilyName = font?.name?.records?.fullName?.en?.replace(title.trim(), '').trim() ||
163
- font.subfamilyName.trim().replace(title.trim(), '').trim();
190
+ const nameId4Remainder = fullName ? fullName.replace(trimmedTitle, '').trim() : '';
191
+ const nameId1 = getNameString(font, 1) || ttfFallbackMeta?.familyName || '';
192
+ const nameId1Remainder = nameId1 ? nameId1.replace(trimmedTitle, '').trim() : '';
193
+
194
+ let subfamilyName = nameId4Remainder || nameId1Remainder;
164
195
 
165
196
  if (!preserveShortenedNames) {
166
197
  subfamilyName = expandAbbreviations(subfamilyName);
167
198
  }
168
199
 
169
- let fontTitle = font?.fullName.trim();
170
- let style = (font?.italicAngle !== 0 || font?.fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular';
200
+ let fontTitle = fullName.trim() || '';
201
+ const italicAngle = getItalicAngle(font);
202
+ let style = (italicAngle !== 0 || fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular';
171
203
 
172
204
  const italicKW = processItalicKeywords(font, fontTitle, italicKeywordList);
173
205
 
174
206
  subfamilyName = processSubfamilyName(subfamilyName, weightKeywordList, italicKW, preserveShortenedNames);
175
207
  fontTitle = formatFontTitle(fontTitle, preserveShortenedNames);
176
208
 
177
- subfamilyName = subfamilyName === '' ? 'Regular' : subfamilyName.replace(/\s+/g, ' ').trim();
209
+ // Style-only names are not subfamilies strip them
210
+ subfamilyName = subfamilyName
211
+ .replace(/\b(Italic|Slant|Slanted|Oblique|Backslant|Roman|Upright)\b/gi, '')
212
+ .replace(/\s+/g, ' ')
213
+ .trim();
178
214
 
179
215
  if (subfamilyName !== '') {
180
216
  weightName = weightName
@@ -187,6 +223,7 @@ export const extractFontMetadata = (font, title, weightKeywordList, italicKeywor
187
223
  if (!fontTitle.toLowerCase().includes('vf')) {
188
224
  fontTitle = fontTitle + ' VF';
189
225
  }
226
+ // Variable fonts are not placed in subfamilies — they go in the separate variableFont array
190
227
  subfamilyName = '';
191
228
  }
192
229
 
@@ -200,20 +237,18 @@ export const extractFontMetadata = (font, title, weightKeywordList, italicKeywor
200
237
  /**
201
238
  * Extracts the weight name from a font's preferred subfamily or subfamily record.
202
239
  * Returns "Variable" for variable fonts.
203
- * @param {Object} font
240
+ * @param {object} font - lib-font parsed font
204
241
  * @param {string[]} italicKW
242
+ * @param {object|null} ttfFallbackMeta
205
243
  * @returns {string}
206
244
  */
207
- export const extractWeightName = (font, italicKW) => {
208
- let weightName = font?.name?.records?.preferredSubfamily || font?.name?.records?.fontSubfamily;
209
-
210
- if (typeof weightName === 'object') {
211
- weightName = weightName?.en ||
212
- (weightName.constructor === Object ? weightName[Object.keys(weightName)[0]] : null);
213
- }
245
+ export const extractWeightName = (font, italicKW, ttfFallbackMeta = null) => {
246
+ let weightName = getNameString(font, 17) || getNameString(font, 2) ||
247
+ ttfFallbackMeta?.preferredSubfamily || ttfFallbackMeta?.subfamilyName || '';
214
248
 
215
- if (font?.variationAxes && Object.keys(font.variationAxes).length > 0) {
216
- return 'Variable';
249
+ const axes = getVariationAxes(font);
250
+ if (axes !== null) {
251
+ return '';
217
252
  }
218
253
 
219
254
  if (italicKW) {
@@ -236,17 +271,15 @@ export const extractWeightName = (font, italicKW) => {
236
271
 
237
272
  /**
238
273
  * Extracts a weight name from the font's full name record when subfamily is empty or "Roman".
239
- * @param {Object} font
274
+ * @param {object} font - lib-font parsed font
240
275
  * @param {string} title
276
+ * @param {object|null} ttfFallbackMeta
241
277
  * @returns {string}
242
278
  */
243
- export const extractWeightFromFullName = (font, title) => {
244
- let weightName = font?.name?.records?.fullName;
245
- weightName = weightName?.en
246
- ? weightName.en
247
- : (weightName?.constructor === Object ? weightName[Object.keys(weightName)[0]] : weightName);
248
- weightName = weightName?.replace(title + ' ', '').replace(title, '').trim();
249
- weightName = weightName?.replace('Italic', '').replace('It', '').replace('Slanted', '').replace('Slant', '').trim();
279
+ export const extractWeightFromFullName = (font, title, ttfFallbackMeta = null) => {
280
+ let weightName = getNameString(font, 4) || ttfFallbackMeta?.fullName || '';
281
+ weightName = weightName.replace(title + ' ', '').replace(title, '').trim();
282
+ weightName = weightName.replace('Italic', '').replace('It', '').replace('Slanted', '').replace('Slant', '').trim();
250
283
  return weightName;
251
284
  };
252
285
 
@@ -282,13 +315,14 @@ export const processSubfamilyName = (subfamilyName, weightKeywordList, italicKey
282
315
 
283
316
  /**
284
317
  * Collects italic keywords present in a font's full name.
285
- * @param {Object} font
318
+ * @param {object} font - lib-font parsed font
286
319
  * @param {string} fontTitle
287
320
  * @param {string[]} italicKeywordList
288
321
  * @returns {string[]}
289
322
  */
290
323
  export const processItalicKeywords = (font, fontTitle, italicKeywordList) => {
291
324
  let italicKW = [];
325
+ const fullName = getNameString(font, 4);
292
326
 
293
327
  italicKeywordList.forEach(keyword => {
294
328
  const kw = keyword.trim();
@@ -297,7 +331,7 @@ export const processItalicKeywords = (font, fontTitle, italicKeywordList) => {
297
331
  fontTitle = fontTitle.replace(kwRegex, '').trim();
298
332
  italicKW.push(kw);
299
333
  }
300
- if (font?.fullName && typeof font.fullName === 'string' && font.fullName.toLowerCase().includes(kw.toLowerCase())) {
334
+ if (fullName && fullName.toLowerCase().includes(kw.toLowerCase())) {
301
335
  if (!italicKW.includes(kw)) italicKW.push(kw);
302
336
  }
303
337
  });
@@ -327,7 +361,7 @@ export const formatFontTitle = (fontTitle, preserveShortenedNames = false) => {
327
361
 
328
362
  /**
329
363
  * Appends any italic keywords to the font title that aren't already present.
330
- * @param {Object} font
364
+ * @param {object} font - lib-font parsed font
331
365
  * @param {string} fontTitle
332
366
  * @param {string[]} italicKW
333
367
  * @param {string} style
@@ -335,8 +369,9 @@ export const formatFontTitle = (fontTitle, preserveShortenedNames = false) => {
335
369
  * @returns {string}
336
370
  */
337
371
  export const addItalicToFontTitle = (font, fontTitle, italicKW, style, preserveShortenedNames = false) => {
338
- const hasItalicAngle = font?.italicAngle !== 0;
339
- const hasItalicInName = font?.fullName.toLowerCase().includes('italic');
372
+ const hasItalicAngle = getItalicAngle(font) !== 0;
373
+ const fullName = getNameString(font, 4);
374
+ const hasItalicInName = fullName.toLowerCase().includes('italic');
340
375
 
341
376
  if (italicKW.length > 0 || hasItalicAngle || hasItalicInName) {
342
377
  italicKW = [...new Set(italicKW)];
@@ -379,7 +414,7 @@ export const addItalicToFontTitle = (font, fontTitle, italicKW, style, preserveS
379
414
  * @param {string} id
380
415
  * @param {string} fontTitle
381
416
  * @param {string} title
382
- * @param {Object} font
417
+ * @param {object} font - lib-font parsed font
383
418
  * @param {boolean} variableFont
384
419
  * @param {string} weightName
385
420
  * @param {string} subfamilyName
@@ -388,13 +423,16 @@ export const addItalicToFontTitle = (font, fontTitle, italicKW, style, preserveS
388
423
  * @returns {Object}
389
424
  */
390
425
  export const createFontObject = (id, fontTitle, title, font, variableFont, weightName, subfamilyName, file, originalFilename = null) => {
426
+ const italicAngle = getItalicAngle(font);
427
+ const fullName = getNameString(font, 4);
428
+
391
429
  const fontObject = {
392
430
  _key: nanoid(),
393
431
  _id: id,
394
432
  title: fontTitle,
395
433
  slug: { _type: 'slug', current: id },
396
434
  typefaceName: title,
397
- style: (font?.italicAngle !== 0 || font?.fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular',
435
+ style: (italicAngle !== 0 || fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular',
398
436
  variableFont: variableFont,
399
437
  weightName: weightName,
400
438
  subfamily: subfamilyName,
@@ -414,13 +452,14 @@ export const createFontObject = (id, fontTitle, title, font, variableFont, weigh
414
452
 
415
453
  /**
416
454
  * Determines a numeric CSS weight value for a font.
417
- * @param {Object} font
455
+ * @param {object} font - lib-font parsed font
418
456
  * @param {string} weightName
419
457
  * @returns {number}
420
458
  */
421
459
  export const determineWeight = (font, weightName) => {
422
- if (font['OS/2']?.usWeightClass) {
423
- return Number(font['OS/2'].usWeightClass);
460
+ const usWeightClass = getWeightClass(font);
461
+ if (usWeightClass) {
462
+ return Number(usWeightClass);
424
463
  }
425
464
 
426
465
  const wn = weightName?.toLowerCase() || '';
@@ -462,7 +501,7 @@ export const sortFontObjects = (fontsObjects) => {
462
501
  * Logs font metadata to the console for debugging.
463
502
  * @param {string} id
464
503
  * @param {string} fontTitle
465
- * @param {Object} font
504
+ * @param {object} font - lib-font parsed font
466
505
  * @param {string} fileName
467
506
  * @param {string} subfamilyName
468
507
  * @param {string} style
@@ -471,17 +510,21 @@ export const sortFontObjects = (fontsObjects) => {
471
510
  * @param {string[]} italicKW
472
511
  */
473
512
  export const logFontInfo = (id, fontTitle, font, fileName, subfamilyName, style, weightName, variableFont, italicKW) => {
513
+ const fullName = getNameString(font, 4);
514
+ const familyName = getNameString(font, 1);
515
+ const italicAngle = getItalicAngle(font);
516
+
474
517
  console.log('=== Font Info ====');
475
- console.log('Font id: ', id);
476
- console.log('Font title: ', fontTitle);
477
- console.log('Fontkit fullName: ', font.fullName);
478
- console.log('Fontkit family name: ', font.familyName);
479
- console.log('File name: ', fileName);
480
- console.log('Subfamily: ', subfamilyName);
481
- console.log('Style: ', style);
482
- console.log('Weight: ', weightName);
483
- console.log('Variable: ', variableFont);
484
- console.log('italicKW: ', italicKW);
485
- console.log('Font italic angle: ', (font?.italicAngle !== 0 || font?.fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular');
518
+ console.log('Font id:', id);
519
+ console.log('Font title:', fontTitle);
520
+ console.log('Full name:', fullName);
521
+ console.log('Family name:', familyName);
522
+ console.log('File name:', fileName);
523
+ console.log('Subfamily:', subfamilyName);
524
+ console.log('Style:', style);
525
+ console.log('Weight:', weightName);
526
+ console.log('Variable:', variableFont);
527
+ console.log('ItalicKW:', italicKW);
528
+ console.log('Italic detection:', (italicAngle !== 0 || fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular');
486
529
  console.log('=======');
487
530
  };
@@ -1,6 +1,6 @@
1
1
  // Renames font document IDs across a typeface when a typeface slug changes
2
2
 
3
- import * as fontkit from 'fontkit';
3
+ import { parseFont } from './parseFont';
4
4
  import {
5
5
  readFontFile,
6
6
  extractFontMetadata,
@@ -100,7 +100,7 @@ export const renameFontDocuments = async ({
100
100
  }
101
101
 
102
102
  const fontBuffer = await readFontFile(file);
103
- const font = fontkit.create(fontBuffer);
103
+ const font = await parseFont(fontBuffer, `${fontDoc._id}.ttf`);
104
104
 
105
105
  const { weightName, subfamilyName, fontTitle } = extractFontMetadata(
106
106
  font,
@@ -0,0 +1,87 @@
1
+ // Standalone document resolution — determines if a font already exists in Sanity
2
+
3
+ import { RECOMMENDATION } from './planTypes';
4
+
5
+ /**
6
+ * Resolves whether a font document already exists in Sanity, returning match details
7
+ * and a recommendation for how to proceed.
8
+ *
9
+ * Resolution strategies (in priority order):
10
+ * 1. Exact _id match or draft _id match or slug.current match
11
+ * 2. Content match by typefaceName + weightName + style + subfamily + variableFont
12
+ *
13
+ * @param {object} font - { _id, typefaceName, weightName, style, subfamily, variableFont, title }
14
+ * @param {object} client - Sanity client (parameterized queries only)
15
+ * @returns {Promise<{ exact: object|null, candidates: object[], recommendation: string, lookupFailed: boolean }>}
16
+ */
17
+ export const resolveExistingFont = async (font, client) => {
18
+ const result = {
19
+ exact: null,
20
+ candidates: [],
21
+ recommendation: RECOMMENDATION.CREATE,
22
+ lookupFailed: false,
23
+ };
24
+
25
+ try {
26
+ // Strategy 1: ID / slug match
27
+ const idMatches = await client.fetch(
28
+ `*[_type == 'font' && (_id == $id || _id == $draftId || slug.current == $id)]{
29
+ _id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
30
+ fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
31
+ scriptFileInput, variableInstanceReferences
32
+ }`,
33
+ { id: font._id, draftId: `drafts.${font._id}` }
34
+ );
35
+
36
+ if (idMatches.length > 0) {
37
+ result.exact = idMatches[0];
38
+ result.recommendation = RECOMMENDATION.USE_EXACT;
39
+ return result;
40
+ }
41
+
42
+ // Strategy 2: Content match (only when ID query returns nothing)
43
+ const subfamily = font.subfamily || '';
44
+ const contentMatches = await client.fetch(
45
+ `*[_type == 'font'
46
+ && lower(typefaceName) == lower($typefaceName)
47
+ && lower(weightName) == lower($weightName)
48
+ && lower(style) == lower($style)
49
+ && (variableFont == $variableFont || (!defined(variableFont) && $variableFont == false))
50
+ && (
51
+ lower(coalesce(subfamily, '')) == lower($subfamily)
52
+ || (lower(coalesce(subfamily, '')) in ['', 'regular'] && lower($subfamily) in ['', 'regular'])
53
+ )
54
+ ]{
55
+ _id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
56
+ fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
57
+ scriptFileInput, variableInstanceReferences
58
+ }`,
59
+ {
60
+ typefaceName: font.typefaceName,
61
+ weightName: font.weightName || '',
62
+ style: font.style || 'Regular',
63
+ variableFont: font.variableFont || false,
64
+ subfamily: subfamily === '' ? 'regular' : subfamily,
65
+ }
66
+ );
67
+
68
+ if (contentMatches.length === 1) {
69
+ result.candidates = contentMatches;
70
+ result.recommendation = RECOMMENDATION.USE_CANDIDATE;
71
+ return result;
72
+ }
73
+
74
+ if (contentMatches.length > 1) {
75
+ result.candidates = contentMatches;
76
+ result.recommendation = RECOMMENDATION.AMBIGUOUS;
77
+ console.warn(`Ambiguous font match for "${font.title}" — ${contentMatches.length} candidates found:`,
78
+ contentMatches.map(c => c._id));
79
+ return result;
80
+ }
81
+ } catch (err) {
82
+ console.error('Error resolving existing font:', font._id, err.message);
83
+ result.lookupFailed = true;
84
+ }
85
+
86
+ return result;
87
+ };
@@ -38,9 +38,22 @@ export const updateTypefaceDocument = async (
38
38
 
39
39
  // Use dot-path keys so .set() does not clobber sibling fields
40
40
  // (styles.collections, styles.pairs, styles.free, styles.displayStyles)
41
+ // Deduplicate by _ref to prevent duplicate entries on re-upload
42
+ const dedupeRefs = (existing, incoming) => {
43
+ const merged = [...(existing || [])];
44
+ const existingRefs = new Set(merged.map(r => r._ref).filter(Boolean));
45
+ incoming.forEach(ref => {
46
+ if (ref._ref && !existingRefs.has(ref._ref)) {
47
+ merged.push(ref);
48
+ existingRefs.add(ref._ref);
49
+ }
50
+ });
51
+ return merged;
52
+ };
53
+
41
54
  let patch = {
42
- 'styles.fonts': stylesObject.fonts ? [...stylesObject.fonts, ...fontRefs] : [...fontRefs],
43
- 'styles.variableFont': stylesObject?.variableFont ? [...stylesObject.variableFont, ...variableRefs] : [...variableRefs],
55
+ 'styles.fonts': dedupeRefs(stylesObject.fonts, fontRefs),
56
+ 'styles.variableFont': dedupeRefs(stylesObject?.variableFont, variableRefs),
44
57
  };
45
58
 
46
59
  setStatus('Organising font subfamilies...');