@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.
- package/README.md +437 -437
- package/dist/UploadModal-6LIX7XOK.js +6 -0
- package/dist/UploadModal-NME2W53V.mjs +6 -0
- package/dist/chunk-646WCBRR.mjs +7276 -0
- package/dist/chunk-FH4QKHOH.js +7276 -0
- package/dist/index.js +747 -1675
- package/dist/index.mjs +400 -1237
- package/package.json +85 -85
- package/src/components/BatchUploadFonts.jsx +653 -639
- package/src/components/BulkActions.jsx +99 -0
- package/src/components/ExistingDocumentResolver.jsx +152 -0
- package/src/components/FontReviewCard.jsx +415 -0
- 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 +254 -254
- 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 +672 -673
- 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 +268 -0
- package/src/components/UploadScriptsComponent.jsx +539 -537
- package/src/components/UploadStep1Settings.jsx +272 -0
- package/src/components/UploadStep2Review.jsx +472 -0
- package/src/components/UploadStep3Execute.jsx +234 -0
- package/src/components/UploadSummary.jsx +196 -0
- 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 +115 -70
- package/src/schema/openTypeField.js +1945 -1945
- package/src/schema/styleCountField.js +12 -12
- package/src/schema/stylesField.js +268 -268
- package/src/schema/stylisticSetField.js +301 -301
- package/src/utils/buildUploadPlan.js +325 -0
- package/src/utils/executeUploadPlan.js +437 -0
- package/src/utils/executionReducer.js +56 -0
- package/src/utils/fontHelpers.js +267 -0
- package/src/utils/generateCssFile.js +207 -205
- package/src/utils/generateFontData.js +98 -145
- 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 -99
- package/src/utils/parseFont.js +55 -0
- package/src/utils/parseVariableFontInstances.js +211 -211
- package/src/utils/planReducer.js +517 -0
- package/src/utils/planTypes.js +183 -0
- package/src/utils/processFontFiles.js +529 -477
- package/src/utils/regenerateFontData.js +146 -146
- package/src/utils/resolveExistingFont.js +87 -0
- package/src/utils/sanitizeForSanityId.js +65 -65
- package/src/utils/updateFontPrices.js +94 -94
- package/src/utils/updateTypefaceDocument.js +149 -160
- package/src/utils/uploadFontFiles.js +405 -316
- 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
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
cssAxes['font-
|
|
48
|
-
} else if (tag === '
|
|
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
|
-
const
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
let
|
|
145
|
-
let
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
}
|