@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.
Files changed (62) hide show
  1. package/README.md +437 -437
  2. package/dist/UploadModal-6LIX7XOK.js +6 -0
  3. package/dist/UploadModal-NME2W53V.mjs +6 -0
  4. package/dist/chunk-646WCBRR.mjs +7276 -0
  5. package/dist/chunk-FH4QKHOH.js +7276 -0
  6. package/dist/index.js +747 -1675
  7. package/dist/index.mjs +400 -1237
  8. package/package.json +85 -85
  9. package/src/components/BatchUploadFonts.jsx +653 -639
  10. package/src/components/BulkActions.jsx +99 -0
  11. package/src/components/ExistingDocumentResolver.jsx +152 -0
  12. package/src/components/FontReviewCard.jsx +415 -0
  13. package/src/components/FontScriptUploaderComponent.jsx +463 -463
  14. package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
  15. package/src/components/KeyValueInput.jsx +95 -95
  16. package/src/components/KeyValueReferenceInput.jsx +254 -254
  17. package/src/components/NestedObjectArraySelector.jsx +146 -146
  18. package/src/components/PriceInput.jsx +26 -26
  19. package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
  20. package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
  21. package/src/components/SetOTF.jsx +87 -87
  22. package/src/components/SingleUploaderTool.jsx +672 -673
  23. package/src/components/StatusDisplay.jsx +26 -26
  24. package/src/components/StyleCountInput.jsx +16 -16
  25. package/src/components/UpdateScriptsComponent.jsx +76 -76
  26. package/src/components/UploadButton.jsx +43 -43
  27. package/src/components/UploadModal.jsx +268 -0
  28. package/src/components/UploadScriptsComponent.jsx +539 -537
  29. package/src/components/UploadStep1Settings.jsx +272 -0
  30. package/src/components/UploadStep2Review.jsx +472 -0
  31. package/src/components/UploadStep3Execute.jsx +234 -0
  32. package/src/components/UploadSummary.jsx +196 -0
  33. package/src/components/VariableInstanceReferencesInput.jsx +190 -190
  34. package/src/hooks/useNestedObjects.js +92 -92
  35. package/src/hooks/useSanityClient.js +9 -9
  36. package/src/index.js +115 -70
  37. package/src/schema/openTypeField.js +1945 -1945
  38. package/src/schema/styleCountField.js +12 -12
  39. package/src/schema/stylesField.js +268 -268
  40. package/src/schema/stylisticSetField.js +301 -301
  41. package/src/utils/buildUploadPlan.js +325 -0
  42. package/src/utils/executeUploadPlan.js +437 -0
  43. package/src/utils/executionReducer.js +56 -0
  44. package/src/utils/fontHelpers.js +267 -0
  45. package/src/utils/generateCssFile.js +207 -205
  46. package/src/utils/generateFontData.js +98 -145
  47. package/src/utils/generateFontFile.js +38 -38
  48. package/src/utils/generateKeywords.js +185 -185
  49. package/src/utils/generateSubset.js +45 -45
  50. package/src/utils/getEmptyFontKit.js +101 -99
  51. package/src/utils/parseFont.js +55 -0
  52. package/src/utils/parseVariableFontInstances.js +211 -211
  53. package/src/utils/planReducer.js +517 -0
  54. package/src/utils/planTypes.js +183 -0
  55. package/src/utils/processFontFiles.js +529 -477
  56. package/src/utils/regenerateFontData.js +146 -146
  57. package/src/utils/resolveExistingFont.js +87 -0
  58. package/src/utils/sanitizeForSanityId.js +65 -65
  59. package/src/utils/updateFontPrices.js +94 -94
  60. package/src/utils/updateTypefaceDocument.js +149 -160
  61. package/src/utils/uploadFontFiles.js +405 -316
  62. package/src/utils/utils.js +24 -24
@@ -1,205 +1,207 @@
1
- // Builds a @font-face CSS file from a WOFF2 blob — URL or base64 src, variable font axis descriptors, metric-tuned fallback @font-face for CLS reduction
2
- import base64 from 'base-64';
3
- import { Buffer } from 'buffer';
4
- import * as fontkit from 'fontkit';
5
-
6
- function _arrayBufferToBase64(buffer) {
7
- var binary = '';
8
- var bytes = new Uint8Array(buffer);
9
- var len = bytes.byteLength;
10
- for (var i = 0; i < len; i++) {
11
- binary += String.fromCharCode(bytes[i]);
12
- }
13
- return base64.encode(binary);
14
- }
15
-
16
- /**
17
- * Reads variable axes from a fontkit font object and returns:
18
- * { descriptors, skipped }
19
- * where `descriptors` is the CSS string for the @font-face block and
20
- * `skipped` lists axis tags that have no CSS descriptor (opsz, custom axes, etc.)
21
- * so callers can surface them in comments.
22
- *
23
- * Edge cases handled:
24
- * - Degenerate axes (min === max): skipped — no actual variation range
25
- * - ital with max === 0: skipped — axis exists but font has no italic
26
- * - slnt ordering: sorted ascending as CSS spec requires
27
- * - ital + slnt coexistence: slnt takes priority (more expressive)
28
- * - min > max from corrupt font data: clamped with Math.min/Math.max
29
- * @param {Object} font - fontkit font instance
30
- * @returns {{ descriptors: string, skipped: string[] }}
31
- */
32
- export function buildVFDescriptors(font) {
33
- const cssAxes = {}
34
- const skipped = []
35
- try {
36
- const va = font.variationAxes
37
- if (!va) return { descriptors: '', skipped: [] }
38
-
39
- for (const [tag, axis] of Object.entries(va)) {
40
- const lo = Math.min(axis.min, axis.max)
41
- const hi = Math.max(axis.min, axis.max)
42
-
43
- // Skip degenerate axes no actual range
44
- if (lo === hi) { skipped.push(tag); continue }
45
-
46
- if (tag === 'wght') {
47
- cssAxes['font-weight'] = `${lo} ${hi}`
48
- } else if (tag === 'wdth') {
49
- cssAxes['font-stretch'] = `${lo}% ${hi}%`
50
- } else if (tag === 'slnt') {
51
- // slnt: ascending order required by CSS Fonts Level 4
52
- cssAxes['font-style'] = `oblique ${lo}deg ${hi}deg`
53
- } else if (tag === 'ital' && !cssAxes['font-style']) {
54
- // ital: only emit if font actually has italic range; slnt takes priority
55
- if (hi > 0) cssAxes['font-style'] = 'italic'
56
- else skipped.push(tag)
57
- } else {
58
- // opsz, GRAD, XTRA, XHGT and all other custom axes have no CSS @font-face descriptor
59
- skipped.push(tag)
60
- }
61
- }
62
- } catch (_) {
63
- // axes unreadable — no descriptors
64
- }
65
- const descriptors = Object.entries(cssAxes).map(([k, v]) => `${k}:${v}`).join(';') + (Object.keys(cssAxes).length ? ';' : '')
66
- return { descriptors, skipped }
67
- }
68
-
69
- // Cross-platform fallback font stacks by category.
70
- // Multiple local() sources ensure the first available system font is used.
71
- // Liberation Sans covers Linux; Roboto covers Android; Georgia is universal for serifs.
72
- const FALLBACK_STACKS = {
73
- 'sans-serif': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
74
- 'serif': "local('Georgia'), local('Times New Roman'), local('Times')",
75
- 'monospace': "local('Courier New'), local('Courier'), local('Menlo'), local('Monaco')",
76
- // Display and script fonts have no universally suitable system fallback; default to sans-serif
77
- 'default': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
78
- };
79
-
80
- // OS/2 sFamilyClass high byte FALLBACK_STACKS key.
81
- // Classes 1–5,7 = serif variants; 8 = sans-serif; 9 = ornamental; 10 = script; 12 = symbolic.
82
- const FAMILY_CLASS_MAP = {
83
- 1: 'serif', 2: 'serif', 3: 'serif', 4: 'serif', 5: 'serif', 7: 'serif',
84
- 8: 'sans-serif',
85
- };
86
-
87
- // Darden Studio fonts have sFamilyClass: 0 (No Classification) in their OS/2 table,
88
- // so name-based matching is used as the primary signal.
89
- const SERIF_NAMES = /jubilat|corundum|dapifer|birra|daith/i;
90
- const SANS_NAMES = /halyard|gamay|omnes|kit/i;
91
-
92
- /** Detects font category from the font name first, then OS/2 sFamilyClass as fallback */
93
- function detectFontCategory(font, fontName) {
94
- if (fontName && SERIF_NAMES.test(fontName)) return 'serif';
95
- if (fontName && SANS_NAMES.test(fontName)) return 'sans-serif';
96
- try {
97
- // fontkit v2: font['OS/2'] exposes the parsed OS/2 table directly
98
- const familyClass = font['OS/2']?.sFamilyClass ?? 0;
99
- const highByte = (familyClass >> 8) & 0xFF;
100
- return FAMILY_CLASS_MAP[highByte] ?? 'default';
101
- } catch {
102
- return 'default';
103
- }
104
- }
105
-
106
- /** Extracts metric override percentages and detects the category fallback stack from a font ArrayBuffer */
107
- function calcFallbackData(arrayBuffer, fontName) {
108
- try {
109
- let font = fontkit.create(Buffer.from(arrayBuffer));
110
- let upm = font.unitsPerEm;
111
- let category = detectFontCategory(font, fontName);
112
- return {
113
- fallbackSrc: FALLBACK_STACKS[category],
114
- ascentOverride: `${(font.ascent / upm * 100).toFixed(2)}%`,
115
- descentOverride: `${(Math.abs(font.descent) / upm * 100).toFixed(2)}%`,
116
- lineGapOverride: `${(font.lineGap / upm * 100).toFixed(2)}%`,
117
- };
118
- } catch (err) {
119
- console.error('Failed to extract fallback font data:', err);
120
- return {
121
- fallbackSrc: FALLBACK_STACKS['default'],
122
- ascentOverride: '100%',
123
- descentOverride: '0%',
124
- lineGapOverride: '0%',
125
- };
126
- }
127
- }
128
-
129
- export default async function generateCssFile({
130
- woff2File,
131
- fileInput,
132
- language = null,
133
- fileName,
134
- fontName,
135
- variableFont,
136
- weight,
137
- style = 'Normal',
138
- client,
139
- }) {
140
- try {
141
- // Read the file once; reuse the same buffer for base64 encoding and fontkit analysis
142
- let arrayBuffer = await woff2File.arrayBuffer();
143
- let b64 = _arrayBufferToBase64(arrayBuffer);
144
- let fontkitFont = fontkit.create(Buffer.from(arrayBuffer));
145
- let { fallbackSrc, ascentOverride, descentOverride, lineGapOverride } = calcFallbackData(arrayBuffer, fontName);
146
-
147
- let cssString;
148
- if (variableFont) {
149
- let { descriptors, skipped } = buildVFDescriptors(fontkitFont);
150
- // Axes with no CSS @font-face descriptor (opsz needs font-optical-sizing:auto on elements;
151
- // custom axes like GRAD require CSS @font-face syntax not yet standardised).
152
- let skipComment = skipped.length
153
- ? `/* axes present but have no @font-face descriptor: ${skipped.join(', ')}` +
154
- (skipped.includes('opsz') ? ' — add font-optical-sizing:auto to your element CSS' : '') +
155
- ' */'
156
- : ''
157
- cssString = `${skipComment}@font-face{font-family:'${fontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2-variations');${descriptors}font-display:swap;}`;
158
- } else {
159
- let fontStyle = style === 'Italic' ? 'italic' : 'normal';
160
- cssString = `@font-face{font-family:'${fontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2');font-weight:${weight};font-style:${fontStyle};font-display:swap;}`;
161
- }
162
-
163
- // Fallback @font-face: tunes a category-appropriate system font to match the custom font's
164
- // line metrics (ascent/descent/lineGap), reducing layout shift while the custom font loads.
165
- // Customers who reference fontSrc in their own projects get this fallback automatically.
166
- let fallbackCssString = `@font-face{font-family:'${fontName} Fallback';src:${fallbackSrc};ascent-override:${ascentOverride};descent-override:${descentOverride};line-gap-override:${lineGapOverride};}`;
167
-
168
- let uploadBuffer = Buffer.from(cssString + fallbackCssString, 'utf-8');
169
- let doc = await client.assets.upload('file', uploadBuffer, { filename: fileName + '.css' });
170
-
171
- let newFileInput = language == null ?
172
- {
173
- ...fileInput,
174
- css: {
175
- _type: 'file',
176
- asset: {
177
- _type: 'reference',
178
- _ref: doc._id
179
- }
180
- }
181
- }
182
- :
183
- {
184
- ...fileInput,
185
- [language]: {
186
- ...fileInput[language],
187
- css: {
188
- _type: 'file',
189
- asset: {
190
- _type: 'reference',
191
- _ref: doc._id
192
- }
193
- }
194
- }
195
- }
196
-
197
- return newFileInput;
198
-
199
- }
200
- catch (err) {
201
- console.error(err);
202
- throw err;
203
- }
204
-
205
- }
1
+ // Builds a @font-face CSS file from a WOFF2 blob — URL or base64 src, variable font axis descriptors, metric-tuned fallback @font-face for CLS reduction
2
+
3
+ import base64 from 'base-64';
4
+ import { parseFont } from './parseFont';
5
+ import { getVariationAxes, getFontMetrics, getFamilyClass, escapeCssFontName } from './fontHelpers';
6
+
7
+ function _arrayBufferToBase64(buffer) {
8
+ var binary = '';
9
+ var bytes = new Uint8Array(buffer);
10
+ var len = bytes.byteLength;
11
+ for (var i = 0; i < len; i++) {
12
+ binary += String.fromCharCode(bytes[i]);
13
+ }
14
+ return base64.encode(binary);
15
+ }
16
+
17
+ /**
18
+ * Builds CSS @font-face axis descriptors from a variation axis map.
19
+ * Accepts a pre-built axis map (from getVariationAxes) rather than a raw font object,
20
+ * so callers can share a single parse result.
21
+ *
22
+ * @param {object|null} axisMap - Keyed axis map { wght: { min, max, default }, ... } or null
23
+ * @returns {{ descriptors: string, skipped: string[] }}
24
+ */
25
+ export function buildVFDescriptors(axisMap) {
26
+ const cssAxes = {};
27
+ const skipped = [];
28
+
29
+ if (!axisMap) return { descriptors: '', skipped: [] };
30
+
31
+ try {
32
+ for (const [tag, axis] of Object.entries(axisMap)) {
33
+ const lo = Math.min(axis.min, axis.max);
34
+ const hi = Math.max(axis.min, axis.max);
35
+
36
+ // Skip degenerate axes — no actual range
37
+ if (lo === hi) { skipped.push(tag); continue; }
38
+
39
+ if (tag === 'wght') {
40
+ cssAxes['font-weight'] = `${lo} ${hi}`;
41
+ } else if (tag === 'wdth') {
42
+ // Clamp to CSS font-stretch range (50-200%)
43
+ cssAxes['font-stretch'] = `${Math.max(50, lo)}% ${Math.min(200, hi)}%`;
44
+ } else if (tag === 'slnt') {
45
+ // OpenType slnt is counter-clockwise positive; CSS oblique is clockwise positive
46
+ // Negate values and ensure ascending order
47
+ cssAxes['font-style'] = `oblique ${-hi}deg ${-lo}deg`;
48
+ } else if (tag === 'ital' && !cssAxes['font-style']) {
49
+ // ital: only note if font has italic range; slnt takes priority
50
+ if (hi > 0) cssAxes['font-style'] = 'italic';
51
+ else skipped.push(tag);
52
+ } else {
53
+ // opsz, GRAD, XTRA, XHGT and all other custom axes have no CSS @font-face descriptor
54
+ skipped.push(tag);
55
+ }
56
+ }
57
+ } catch (_) {
58
+ // axes unreadable no descriptors
59
+ }
60
+
61
+ const descriptors = Object.entries(cssAxes).map(([k, v]) => `${k}:${v}`).join(';') + (Object.keys(cssAxes).length ? ';' : '');
62
+ return { descriptors, skipped };
63
+ }
64
+
65
+ // Cross-platform fallback font stacks by category.
66
+ const FALLBACK_STACKS = {
67
+ 'sans-serif': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
68
+ 'serif': "local('Georgia'), local('Times New Roman'), local('Times')",
69
+ 'monospace': "local('Courier New'), local('Courier'), local('Menlo'), local('Monaco')",
70
+ 'default': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
71
+ };
72
+
73
+ // OS/2 sFamilyClass high byte FALLBACK_STACKS key.
74
+ const FAMILY_CLASS_MAP = {
75
+ 1: 'serif', 2: 'serif', 3: 'serif', 4: 'serif', 5: 'serif', 7: 'serif',
76
+ 8: 'sans-serif',
77
+ };
78
+
79
+ // Darden Studio fonts have sFamilyClass: 0 in their OS/2 table,
80
+ // so name-based matching is used as the primary signal.
81
+ const SERIF_NAMES = /jubilat|corundum|dapifer|birra|daith/i;
82
+ const SANS_NAMES = /halyard|gamay|omnes|kit/i;
83
+
84
+ /**
85
+ * Detects font category from the font name first, then OS/2 sFamilyClass as fallback.
86
+ * @param {object} font - lib-font Font instance
87
+ * @param {string} fontName
88
+ * @returns {string}
89
+ */
90
+ function detectFontCategory(font, fontName) {
91
+ if (fontName && SERIF_NAMES.test(fontName)) return 'serif';
92
+ if (fontName && SANS_NAMES.test(fontName)) return 'sans-serif';
93
+ try {
94
+ const familyClass = getFamilyClass(font);
95
+ const highByte = (familyClass >> 8) & 0xFF;
96
+ return FAMILY_CLASS_MAP[highByte] ?? 'default';
97
+ } catch {
98
+ return 'default';
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Extracts metric override percentages and detects the category fallback stack.
104
+ * Accepts a pre-parsed font to avoid double-parsing.
105
+ * @param {object} font - lib-font Font instance
106
+ * @param {string} fontName
107
+ * @returns {{ fallbackSrc: string, ascentOverride: string, descentOverride: string, lineGapOverride: string }}
108
+ */
109
+ function calcFallbackData(font, fontName) {
110
+ try {
111
+ const metrics = getFontMetrics(font);
112
+ const upm = metrics.unitsPerEm;
113
+ const category = detectFontCategory(font, fontName);
114
+ return {
115
+ fallbackSrc: FALLBACK_STACKS[category],
116
+ ascentOverride: `${(metrics.ascender / upm * 100).toFixed(2)}%`,
117
+ descentOverride: `${(Math.abs(metrics.descender) / upm * 100).toFixed(2)}%`,
118
+ lineGapOverride: `${(metrics.lineGap / upm * 100).toFixed(2)}%`,
119
+ };
120
+ } catch (err) {
121
+ console.error('Failed to extract fallback font data:', err);
122
+ return {
123
+ fallbackSrc: FALLBACK_STACKS['default'],
124
+ ascentOverride: '100%',
125
+ descentOverride: '0%',
126
+ lineGapOverride: '0%',
127
+ };
128
+ }
129
+ }
130
+
131
+ export default async function generateCssFile({
132
+ woff2File,
133
+ fileInput,
134
+ language = null,
135
+ fileName,
136
+ fontName,
137
+ variableFont,
138
+ weight,
139
+ style = 'Normal',
140
+ client,
141
+ }) {
142
+ try {
143
+ // Read the file once; reuse the same buffer for base64 and font analysis
144
+ let arrayBuffer = await woff2File.arrayBuffer();
145
+ let b64 = _arrayBufferToBase64(arrayBuffer);
146
+
147
+ // Parse once — share result between axis descriptors and fallback metrics
148
+ let font = await parseFont(arrayBuffer, fileName + '.woff2');
149
+ let { fallbackSrc, ascentOverride, descentOverride, lineGapOverride } = calcFallbackData(font, fontName);
150
+
151
+ // Escape font name for CSS injection prevention
152
+ const safeFontName = escapeCssFontName(fontName);
153
+
154
+ let cssString;
155
+ if (variableFont) {
156
+ const axisMap = getVariationAxes(font);
157
+ let { descriptors, skipped } = buildVFDescriptors(axisMap);
158
+ let skipComment = skipped.length
159
+ ? `/* axes present but have no @font-face descriptor: ${skipped.join(', ')}` +
160
+ (skipped.includes('opsz') ? ' — add font-optical-sizing:auto to your element CSS' : '') +
161
+ ' */'
162
+ : '';
163
+ cssString = `${skipComment}@font-face{font-family:'${safeFontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2');${descriptors}font-display:swap;}`;
164
+ } else {
165
+ let fontStyle = style === 'Italic' ? 'italic' : 'normal';
166
+ cssString = `@font-face{font-family:'${safeFontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2');font-weight:${weight};font-style:${fontStyle};font-display:swap;}`;
167
+ }
168
+
169
+ // Fallback @font-face: tunes a system font to match the custom font's metrics for CLS reduction
170
+ let fallbackCssString = `@font-face{font-family:'${safeFontName} Fallback';src:${fallbackSrc};ascent-override:${ascentOverride};descent-override:${descentOverride};line-gap-override:${lineGapOverride};}`;
171
+
172
+ // Upload as a text buffer (no Buffer polyfill needed — TextEncoder is native)
173
+ const cssBytes = new TextEncoder().encode(cssString + fallbackCssString);
174
+ let doc = await client.assets.upload('file', new Blob([cssBytes]), { filename: fileName + '.css' });
175
+
176
+ let newFileInput = language == null ?
177
+ {
178
+ ...fileInput,
179
+ css: {
180
+ _type: 'file',
181
+ asset: {
182
+ _type: 'reference',
183
+ _ref: doc._id,
184
+ },
185
+ },
186
+ }
187
+ :
188
+ {
189
+ ...fileInput,
190
+ [language]: {
191
+ ...fileInput[language],
192
+ css: {
193
+ _type: 'file',
194
+ asset: {
195
+ _type: 'reference',
196
+ _ref: doc._id,
197
+ },
198
+ },
199
+ },
200
+ };
201
+
202
+ return newFileInput;
203
+ } catch (err) {
204
+ console.error(err);
205
+ throw err;
206
+ }
207
+ }