@liiift-studio/sanity-font-manager 2.3.19 → 2.4.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 (42) hide show
  1. package/README.md +437 -437
  2. package/dist/index.js +103 -48
  3. package/dist/index.mjs +103 -48
  4. package/package.json +85 -85
  5. package/src/components/BatchUploadFonts.jsx +640 -639
  6. package/src/components/FontScriptUploaderComponent.jsx +463 -463
  7. package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
  8. package/src/components/KeyValueInput.jsx +95 -95
  9. package/src/components/KeyValueReferenceInput.jsx +254 -254
  10. package/src/components/NestedObjectArraySelector.jsx +146 -146
  11. package/src/components/PriceInput.jsx +26 -26
  12. package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
  13. package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
  14. package/src/components/SetOTF.jsx +87 -87
  15. package/src/components/SingleUploaderTool.jsx +673 -673
  16. package/src/components/StatusDisplay.jsx +26 -26
  17. package/src/components/StyleCountInput.jsx +16 -16
  18. package/src/components/UpdateScriptsComponent.jsx +76 -76
  19. package/src/components/UploadButton.jsx +43 -43
  20. package/src/components/UploadScriptsComponent.jsx +537 -537
  21. package/src/components/VariableInstanceReferencesInput.jsx +190 -190
  22. package/src/hooks/useNestedObjects.js +92 -92
  23. package/src/hooks/useSanityClient.js +9 -9
  24. package/src/index.js +70 -70
  25. package/src/schema/openTypeField.js +1945 -1945
  26. package/src/schema/styleCountField.js +12 -12
  27. package/src/schema/stylesField.js +268 -268
  28. package/src/schema/stylisticSetField.js +301 -301
  29. package/src/utils/generateCssFile.js +205 -205
  30. package/src/utils/generateFontData.js +145 -145
  31. package/src/utils/generateFontFile.js +38 -38
  32. package/src/utils/generateKeywords.js +185 -185
  33. package/src/utils/generateSubset.js +45 -45
  34. package/src/utils/getEmptyFontKit.js +99 -99
  35. package/src/utils/parseVariableFontInstances.js +211 -211
  36. package/src/utils/processFontFiles.js +487 -477
  37. package/src/utils/regenerateFontData.js +146 -146
  38. package/src/utils/sanitizeForSanityId.js +65 -65
  39. package/src/utils/updateFontPrices.js +94 -94
  40. package/src/utils/updateTypefaceDocument.js +149 -160
  41. package/src/utils/uploadFontFiles.js +115 -26
  42. package/src/utils/utils.js +24 -24
@@ -1,211 +1,211 @@
1
- // Resolves named variable font instances into Sanity font document references, creating documents for missing instances
2
-
3
- import { nanoid } from 'nanoid';
4
- import { expandAbbreviations } from './generateKeywords';
5
-
6
- /**
7
- * Parses a variable font's named instances and maps each to its corresponding static font document.
8
- * Uses 6 matching strategies in priority order:
9
- * 1. Exact title match
10
- * 2. Title normalisation (strips VF/var/variable prefixes, handles Regular/Italic suffixes)
11
- * 3. Abbreviation expansion
12
- * 4. Weight + style matching
13
- * 5. weightName comparison
14
- * 6. metaData.fullName fallback
15
- *
16
- * @param {Object} font - The variable font object (must have typefaceName and variableInstances)
17
- * @param {Object} client - Sanity client (parameterized queries only)
18
- * @returns {Promise<Array>} Array of { key, value, _key } instance mappings
19
- */
20
- export const parseVariableFontInstances = async (font, client) => {
21
- if (!font.variableFont || !font.variableInstances) return [];
22
-
23
- let variableInstances;
24
- try {
25
- variableInstances = JSON.parse(font.variableInstances);
26
- } catch (err) {
27
- console.error('Error parsing variable instances:', err);
28
- variableInstances = {};
29
- }
30
-
31
- if (Object.keys(variableInstances).length === 0) return [];
32
-
33
- // Fetch the typeface's curated font list (parameterized — no injection risk)
34
- let staticFonts;
35
- const typeface = await client.fetch(
36
- `*[_type == 'typeface' && title == $typefaceName][0]{
37
- 'fonts': styles.fonts[]-> {
38
- _id,
39
- title,
40
- subfamily,
41
- style,
42
- weight,
43
- weightName,
44
- metaData,
45
- variableFont
46
- }
47
- }`,
48
- { typefaceName: font.typefaceName }
49
- );
50
-
51
- if (typeface?.fonts && typeface.fonts.length > 0) {
52
- staticFonts = typeface.fonts.filter(f => !f.variableFont);
53
- console.log('Using curated typeface fonts list:', staticFonts.length, 'fonts');
54
- } else {
55
- console.warn('Typeface not found or no fonts in curated list, falling back to all fonts query');
56
- staticFonts = await client.fetch(
57
- `*[_type == 'font' && typefaceName == $typefaceName && variableFont != true]{
58
- _id,
59
- title,
60
- subfamily,
61
- style,
62
- weight,
63
- weightName,
64
- metaData
65
- }`,
66
- { typefaceName: font.typefaceName }
67
- );
68
- }
69
-
70
- console.log('Variable font instances:', Object.keys(variableInstances));
71
- console.log('Available static fonts:', staticFonts.map(sf => sf.title));
72
-
73
- const instanceMappings = [];
74
-
75
- Object.keys(variableInstances).forEach(instanceName => {
76
- let matchingFont = null;
77
-
78
- // Strategy 1: Exact title match
79
- matchingFont = staticFonts.find(sf => sf.title === instanceName);
80
-
81
- // Strategy 2: Title normalisation — strip VF/var/variable prefix words, handle Regular/Italic
82
- if (!matchingFont && staticFonts.some(sf => sf.metaData?.fullName)) {
83
- matchingFont = staticFonts.find(sf => {
84
- if (!sf.metaData?.fullName) return false;
85
-
86
- let fullName = sf.metaData.fullName;
87
-
88
- const WORDS_TO_REMOVE = ['VF', 'var', 'variable', 'VAR', 'vf'];
89
- const variableName = font.metaData?.familyName
90
- ?.replace(new RegExp(`\\b(${WORDS_TO_REMOVE.join('|')})\\b`, 'gi'), '')
91
- .replace(/\s{2,}/g, ' ')
92
- .trim();
93
-
94
- if (variableName && fullName.startsWith(variableName)) {
95
- fullName = fullName.substring(variableName.length).trim();
96
- }
97
-
98
- if (variableName) {
99
- const words = variableName.split(/\s+/).map(w => w.trim()).filter(Boolean);
100
- if (words.length > 0) {
101
- const regex = new RegExp(`\\b(${words.join('|')})\\b`, 'gi');
102
- const stripped = fullName.replace(regex, '').replace(/\s{2,}/g, ' ').trim();
103
- if (stripped !== '') fullName = stripped;
104
- }
105
- }
106
-
107
- if (fullName.startsWith(font.typefaceName)) {
108
- fullName = fullName.substring(font.typefaceName.length).trim();
109
- }
110
-
111
- if (sf.style?.toLowerCase() === 'italic' &&
112
- !fullName.toLowerCase().endsWith('italic') &&
113
- !fullName.toLowerCase().endsWith('slanted')) {
114
- fullName = fullName + ' Italic';
115
- }
116
-
117
- if (fullName.trim().toLowerCase().endsWith('regular')) {
118
- if (instanceName.trim().toLowerCase() + ' regular' === fullName.trim().toLowerCase()) return true;
119
- }
120
- if (fullName.trim().toLowerCase().startsWith('regular')) {
121
- if ('regular ' + instanceName.trim().toLowerCase() === fullName.trim().toLowerCase()) return true;
122
- }
123
- if (fullName.trim().toLowerCase().endsWith('italic')) {
124
- if (instanceName.trim().toLowerCase().endsWith('italic')) {
125
- const k = instanceName.trim().toLowerCase().slice(0, -6).trim() + ' regular italic';
126
- if (k === fullName.trim().toLowerCase()) return true;
127
- }
128
- }
129
-
130
- return fullName.trim().toLowerCase() === instanceName.trim().toLowerCase();
131
- });
132
- }
133
-
134
- // Strategy 3: Abbreviation expansion
135
- if (!matchingFont) {
136
- const expandedName = instanceName.split(' ').map(word => expandAbbreviations(word)).join(' ');
137
- matchingFont = staticFonts.find(sf => {
138
- const nameWithoutTypeface = sf.title.replace(font.typefaceName, '').trim();
139
- return nameWithoutTypeface === expandedName;
140
- });
141
- }
142
-
143
- // Strategy 4: Weight + style matching
144
- if (!matchingFont) {
145
- const isItalic = instanceName.toLowerCase().includes('italic');
146
- const weightTerms = [
147
- { term: 'thin', weight: '100' },
148
- { term: 'extralight', weight: '200' },
149
- { term: 'extra light', weight: '200' },
150
- { term: 'light', weight: '300' },
151
- { term: 'regular', weight: '400' },
152
- { term: 'normal', weight: '400' },
153
- { term: 'medium', weight: '500' },
154
- { term: 'semibold', weight: '600' },
155
- { term: 'semi bold', weight: '600' },
156
- { term: 'bold', weight: '700' },
157
- { term: 'extrabold', weight: '800' },
158
- { term: 'extra bold', weight: '800' },
159
- { term: 'black', weight: '900' },
160
- { term: 'heavy', weight: '900' },
161
- ];
162
-
163
- let instanceWeight = '400';
164
- for (const { term, weight } of weightTerms) {
165
- if (instanceName.toLowerCase().includes(term)) {
166
- instanceWeight = weight;
167
- break;
168
- }
169
- }
170
-
171
- matchingFont = staticFonts.find(sf =>
172
- sf.weight === instanceWeight &&
173
- ((isItalic && sf.style === 'Italic') || (!isItalic && sf.style === 'Regular'))
174
- );
175
- }
176
-
177
- // Strategy 5: weightName comparison
178
- if (!matchingFont) {
179
- matchingFont = staticFonts.find(sf => {
180
- if (!sf.weightName) return false;
181
- const cleanInstance = instanceName.toLowerCase().replace(/italic/i, '').trim();
182
- const cleanWeight = sf.weightName.toLowerCase().replace(/italic/i, '').trim();
183
- return cleanInstance === cleanWeight;
184
- });
185
- }
186
-
187
- // Strategy 6: Legacy metaData.fullName fallback
188
- if (!matchingFont && staticFonts.some(sf => sf.metaData?.fullName)) {
189
- matchingFont = staticFonts.find(sf => {
190
- if (!sf.metaData?.fullName) return false;
191
- const typefacePattern = new RegExp(`^${font.typefaceName}\\s+`, 'i');
192
- const stylePart = sf.metaData.fullName.replace(typefacePattern, '').trim();
193
- return instanceName.toLowerCase() === stylePart.toLowerCase();
194
- });
195
- }
196
-
197
- console.log(`Instance "${instanceName}" matched with:`, matchingFont ? matchingFont.title : 'No match found');
198
-
199
- instanceMappings.push({
200
- key: instanceName,
201
- value: matchingFont
202
- ? { _type: 'reference', _ref: matchingFont._id, _weak: true }
203
- : null,
204
- _key: nanoid(),
205
- });
206
- });
207
-
208
- return instanceMappings;
209
- };
210
-
211
- export default parseVariableFontInstances;
1
+ // Resolves named variable font instances into Sanity font document references, creating documents for missing instances
2
+
3
+ import { nanoid } from 'nanoid';
4
+ import { expandAbbreviations } from './generateKeywords';
5
+
6
+ /**
7
+ * Parses a variable font's named instances and maps each to its corresponding static font document.
8
+ * Uses 6 matching strategies in priority order:
9
+ * 1. Exact title match
10
+ * 2. Title normalisation (strips VF/var/variable prefixes, handles Regular/Italic suffixes)
11
+ * 3. Abbreviation expansion
12
+ * 4. Weight + style matching
13
+ * 5. weightName comparison
14
+ * 6. metaData.fullName fallback
15
+ *
16
+ * @param {Object} font - The variable font object (must have typefaceName and variableInstances)
17
+ * @param {Object} client - Sanity client (parameterized queries only)
18
+ * @returns {Promise<Array>} Array of { key, value, _key } instance mappings
19
+ */
20
+ export const parseVariableFontInstances = async (font, client) => {
21
+ if (!font.variableFont || !font.variableInstances) return [];
22
+
23
+ let variableInstances;
24
+ try {
25
+ variableInstances = JSON.parse(font.variableInstances);
26
+ } catch (err) {
27
+ console.error('Error parsing variable instances:', err);
28
+ variableInstances = {};
29
+ }
30
+
31
+ if (Object.keys(variableInstances).length === 0) return [];
32
+
33
+ // Fetch the typeface's curated font list (parameterized — no injection risk)
34
+ let staticFonts;
35
+ const typeface = await client.fetch(
36
+ `*[_type == 'typeface' && title == $typefaceName][0]{
37
+ 'fonts': styles.fonts[]-> {
38
+ _id,
39
+ title,
40
+ subfamily,
41
+ style,
42
+ weight,
43
+ weightName,
44
+ metaData,
45
+ variableFont
46
+ }
47
+ }`,
48
+ { typefaceName: font.typefaceName }
49
+ );
50
+
51
+ if (typeface?.fonts && typeface.fonts.length > 0) {
52
+ staticFonts = typeface.fonts.filter(f => !f.variableFont);
53
+ console.log('Using curated typeface fonts list:', staticFonts.length, 'fonts');
54
+ } else {
55
+ console.warn('Typeface not found or no fonts in curated list, falling back to all fonts query');
56
+ staticFonts = await client.fetch(
57
+ `*[_type == 'font' && typefaceName == $typefaceName && variableFont != true]{
58
+ _id,
59
+ title,
60
+ subfamily,
61
+ style,
62
+ weight,
63
+ weightName,
64
+ metaData
65
+ }`,
66
+ { typefaceName: font.typefaceName }
67
+ );
68
+ }
69
+
70
+ console.log('Variable font instances:', Object.keys(variableInstances));
71
+ console.log('Available static fonts:', staticFonts.map(sf => sf.title));
72
+
73
+ const instanceMappings = [];
74
+
75
+ Object.keys(variableInstances).forEach(instanceName => {
76
+ let matchingFont = null;
77
+
78
+ // Strategy 1: Exact title match
79
+ matchingFont = staticFonts.find(sf => sf.title === instanceName);
80
+
81
+ // Strategy 2: Title normalisation — strip VF/var/variable prefix words, handle Regular/Italic
82
+ if (!matchingFont && staticFonts.some(sf => sf.metaData?.fullName)) {
83
+ matchingFont = staticFonts.find(sf => {
84
+ if (!sf.metaData?.fullName) return false;
85
+
86
+ let fullName = sf.metaData.fullName;
87
+
88
+ const WORDS_TO_REMOVE = ['VF', 'var', 'variable', 'VAR', 'vf'];
89
+ const variableName = font.metaData?.familyName
90
+ ?.replace(new RegExp(`\\b(${WORDS_TO_REMOVE.join('|')})\\b`, 'gi'), '')
91
+ .replace(/\s{2,}/g, ' ')
92
+ .trim();
93
+
94
+ if (variableName && fullName.startsWith(variableName)) {
95
+ fullName = fullName.substring(variableName.length).trim();
96
+ }
97
+
98
+ if (variableName) {
99
+ const words = variableName.split(/\s+/).map(w => w.trim()).filter(Boolean);
100
+ if (words.length > 0) {
101
+ const regex = new RegExp(`\\b(${words.join('|')})\\b`, 'gi');
102
+ const stripped = fullName.replace(regex, '').replace(/\s{2,}/g, ' ').trim();
103
+ if (stripped !== '') fullName = stripped;
104
+ }
105
+ }
106
+
107
+ if (fullName.startsWith(font.typefaceName)) {
108
+ fullName = fullName.substring(font.typefaceName.length).trim();
109
+ }
110
+
111
+ if (sf.style?.toLowerCase() === 'italic' &&
112
+ !fullName.toLowerCase().endsWith('italic') &&
113
+ !fullName.toLowerCase().endsWith('slanted')) {
114
+ fullName = fullName + ' Italic';
115
+ }
116
+
117
+ if (fullName.trim().toLowerCase().endsWith('regular')) {
118
+ if (instanceName.trim().toLowerCase() + ' regular' === fullName.trim().toLowerCase()) return true;
119
+ }
120
+ if (fullName.trim().toLowerCase().startsWith('regular')) {
121
+ if ('regular ' + instanceName.trim().toLowerCase() === fullName.trim().toLowerCase()) return true;
122
+ }
123
+ if (fullName.trim().toLowerCase().endsWith('italic')) {
124
+ if (instanceName.trim().toLowerCase().endsWith('italic')) {
125
+ const k = instanceName.trim().toLowerCase().slice(0, -6).trim() + ' regular italic';
126
+ if (k === fullName.trim().toLowerCase()) return true;
127
+ }
128
+ }
129
+
130
+ return fullName.trim().toLowerCase() === instanceName.trim().toLowerCase();
131
+ });
132
+ }
133
+
134
+ // Strategy 3: Abbreviation expansion
135
+ if (!matchingFont) {
136
+ const expandedName = instanceName.split(' ').map(word => expandAbbreviations(word)).join(' ');
137
+ matchingFont = staticFonts.find(sf => {
138
+ const nameWithoutTypeface = sf.title.replace(font.typefaceName, '').trim();
139
+ return nameWithoutTypeface === expandedName;
140
+ });
141
+ }
142
+
143
+ // Strategy 4: Weight + style matching
144
+ if (!matchingFont) {
145
+ const isItalic = instanceName.toLowerCase().includes('italic');
146
+ const weightTerms = [
147
+ { term: 'thin', weight: '100' },
148
+ { term: 'extralight', weight: '200' },
149
+ { term: 'extra light', weight: '200' },
150
+ { term: 'light', weight: '300' },
151
+ { term: 'regular', weight: '400' },
152
+ { term: 'normal', weight: '400' },
153
+ { term: 'medium', weight: '500' },
154
+ { term: 'semibold', weight: '600' },
155
+ { term: 'semi bold', weight: '600' },
156
+ { term: 'bold', weight: '700' },
157
+ { term: 'extrabold', weight: '800' },
158
+ { term: 'extra bold', weight: '800' },
159
+ { term: 'black', weight: '900' },
160
+ { term: 'heavy', weight: '900' },
161
+ ];
162
+
163
+ let instanceWeight = '400';
164
+ for (const { term, weight } of weightTerms) {
165
+ if (instanceName.toLowerCase().includes(term)) {
166
+ instanceWeight = weight;
167
+ break;
168
+ }
169
+ }
170
+
171
+ matchingFont = staticFonts.find(sf =>
172
+ sf.weight === instanceWeight &&
173
+ ((isItalic && sf.style === 'Italic') || (!isItalic && sf.style === 'Regular'))
174
+ );
175
+ }
176
+
177
+ // Strategy 5: weightName comparison
178
+ if (!matchingFont) {
179
+ matchingFont = staticFonts.find(sf => {
180
+ if (!sf.weightName) return false;
181
+ const cleanInstance = instanceName.toLowerCase().replace(/italic/i, '').trim();
182
+ const cleanWeight = sf.weightName.toLowerCase().replace(/italic/i, '').trim();
183
+ return cleanInstance === cleanWeight;
184
+ });
185
+ }
186
+
187
+ // Strategy 6: Legacy metaData.fullName fallback
188
+ if (!matchingFont && staticFonts.some(sf => sf.metaData?.fullName)) {
189
+ matchingFont = staticFonts.find(sf => {
190
+ if (!sf.metaData?.fullName) return false;
191
+ const typefacePattern = new RegExp(`^${font.typefaceName}\\s+`, 'i');
192
+ const stylePart = sf.metaData.fullName.replace(typefacePattern, '').trim();
193
+ return instanceName.toLowerCase() === stylePart.toLowerCase();
194
+ });
195
+ }
196
+
197
+ console.log(`Instance "${instanceName}" matched with:`, matchingFont ? matchingFont.title : 'No match found');
198
+
199
+ instanceMappings.push({
200
+ key: instanceName,
201
+ value: matchingFont
202
+ ? { _type: 'reference', _ref: matchingFont._id, _weak: true }
203
+ : null,
204
+ _key: nanoid(),
205
+ });
206
+ });
207
+
208
+ return instanceMappings;
209
+ };
210
+
211
+ export default parseVariableFontInstances;