@liiift-studio/sanity-font-manager 2.7.0 → 2.7.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.
- package/LICENSE +21 -0
- package/README.md +695 -437
- package/dist/index.js +1634 -17839
- package/dist/index.mjs +1551 -17745
- package/package.json +83 -83
- package/src/components/BatchUploadFonts.jsx +655 -655
- package/src/components/BulkActions.jsx +99 -99
- package/src/components/ExistingDocumentResolver.jsx +152 -152
- package/src/components/FontReviewCard.jsx +455 -455
- 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 +267 -267
- 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 +674 -674
- 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 +309 -309
- package/src/components/UploadScriptsComponent.jsx +539 -539
- package/src/components/UploadStep1Settings.jsx +272 -272
- package/src/components/UploadStep2Review.jsx +478 -478
- package/src/components/UploadStep3Execute.jsx +234 -234
- package/src/components/UploadStep3bInstances.jsx +396 -396
- package/src/components/UploadSummary.jsx +196 -196
- 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 +120 -120
- package/src/schema/openTypeField.js +1995 -1995
- package/src/schema/styleCountField.js +12 -12
- package/src/schema/stylesField.js +302 -302
- package/src/schema/stylisticSetField.js +301 -301
- package/src/utils/buildUploadPlan.js +326 -326
- package/src/utils/executeUploadPlan.js +430 -430
- package/src/utils/executionReducer.js +56 -56
- package/src/utils/fontHelpers.js +281 -281
- package/src/utils/generateCssFile.js +207 -207
- package/src/utils/generateFontData.js +98 -98
- 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 -101
- package/src/utils/parseFont.js +56 -56
- package/src/utils/parseVariableFontInstances.js +301 -301
- package/src/utils/planReducer.js +531 -531
- package/src/utils/planTypes.js +183 -183
- package/src/utils/processFontFiles.js +530 -530
- package/src/utils/regenerateFontData.js +146 -146
- package/src/utils/resolveExistingFont.js +87 -87
- package/src/utils/retitleFontEntries.js +154 -154
- package/src/utils/sanitizeForSanityId.js +65 -65
- package/src/utils/setupDecompressors.js +27 -27
- package/src/utils/updateFontPrices.js +94 -94
- package/src/utils/updateTypefaceDocument.js +162 -162
- package/src/utils/uploadFontFiles.js +405 -405
- package/src/utils/utils.js +24 -24
package/README.md
CHANGED
|
@@ -1,437 +1,695 @@
|
|
|
1
|
-
# @liiift-studio/sanity-font-manager
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
206
|
-
{
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### `
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
| `
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
1
|
+
# @liiift-studio/sanity-font-manager
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@liiift-studio/sanity-font-manager)
|
|
4
|
+
[](#license)
|
|
5
|
+
[](https://www.sanity.io/)
|
|
6
|
+
|
|
7
|
+
Full font management suite for Sanity Studio. Handles batch upload, multi-format conversion, metadata extraction, CSS `@font-face` generation, collection and pair generation, and script variant management.
|
|
8
|
+
|
|
9
|
+
Drag a folder of font files onto a typeface document and the plugin parses each file, detects its weight and style, checks for an existing document, then — after you review and confirm — uploads every format, generates the `@font-face` CSS and metadata, creates or updates the font documents, and maps any variable-font instances.
|
|
10
|
+
|
|
11
|
+
Compatible with Sanity v3, v4, and v5.
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
The upload flow is a two-phase **plan → execute** pipeline: phase 1 parses files and resolves duplicates with **reads only**, you review and edit the result, then phase 2 performs all **writes**. Font parsing runs on [`lib-font`](https://github.com/Pomax/lib-font) (WOFF/WOFF2 decompression is bootstrapped via `pako` + a vendored `unbrotli`), so no native binaries are required.
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart TD
|
|
19
|
+
subgraph studio["Sanity Studio (typeface document)"]
|
|
20
|
+
BUF["BatchUploadFonts<br/>(schema input field)"]
|
|
21
|
+
BUF -->|opens| UM["UploadModal<br/>(lazy-loaded)"]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
subgraph plan["Phase 1 — Plan (reads only)"]
|
|
25
|
+
UM --> S1["Step 1 · Upload Files<br/>UploadStep1Settings"]
|
|
26
|
+
S1 --> BUILD["buildUploadPlan()"]
|
|
27
|
+
BUILD --> PF["parseFont() · lib-font<br/>setupDecompressors → pako + unbrotli"]
|
|
28
|
+
PF --> FH["fontHelpers<br/>weight / style / axes / metrics"]
|
|
29
|
+
FH --> RES["resolveExistingFont()<br/>exact / candidate / create"]
|
|
30
|
+
RES --> S2["Step 2 · Review<br/>FontReviewCard · BulkActions · ExistingDocumentResolver"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
subgraph execute["Phase 2 — Execute (writes)"]
|
|
34
|
+
S2 -->|user confirms| EXEC["executeUploadPlan()<br/>concurrency = 3 · 429 backoff + jitter"]
|
|
35
|
+
EXEC --> UP["Upload assets<br/>ttf / otf / woff / woff2"]
|
|
36
|
+
UP --> CSS["generateCssFile()<br/>@font-face + VF descriptors"]
|
|
37
|
+
CSS --> META["generateFontData()<br/>metadata · metrics · glyphs · features"]
|
|
38
|
+
META --> DOC["Create / update font documents"]
|
|
39
|
+
DOC --> PATCH["Patch typeface styles.fonts"]
|
|
40
|
+
PATCH --> S3b["Step 3b · Map Instances<br/>UploadStep3bInstances (variable fonts)"]
|
|
41
|
+
S3b --> SUM["Summary<br/>UploadSummary"]
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> The Mermaid source lives at [`assets/upload-pipeline.mmd`](assets/upload-pipeline.mmd) and renders inline on GitHub. **A maintainer screenshot or short GIF of the live upload modal** (drag → review table → execute → instance mapping) would make the workflow even clearer — these Studio components cannot be captured headlessly, so it is left as a follow-up. Drop the image into `assets/` and reference it with an absolute `raw.githubusercontent.com` URL.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install @liiift-studio/sanity-font-manager
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Peer dependencies
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install sanity @sanity/ui @sanity/icons react @liiift-studio/sanity-advanced-reference-array
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Peer | Required version | Notes |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `sanity` | `>=3` | |
|
|
64
|
+
| `@sanity/ui` | `>=3` | |
|
|
65
|
+
| `@sanity/icons` | `>=3` | |
|
|
66
|
+
| `react` | `>=18` | |
|
|
67
|
+
| `@liiift-studio/sanity-advanced-reference-array` | `>=1` | **Required for the [Quickstart](#quickstart) path.** `createStylesField` and the typeface reference-array fields import it at module load, so importing them without it installed will throw. You only need it if you wire fields the manual way and skip both. |
|
|
68
|
+
|
|
69
|
+
If you hit peer dependency conflicts, add `legacy-peer-deps=true` to your `.npmrc`.
|
|
70
|
+
|
|
71
|
+
### Parsing dependencies
|
|
72
|
+
|
|
73
|
+
Font parsing relies on [`lib-font`](https://github.com/Pomax/lib-font) and `pako` at runtime. If they are not already present in your studio's `node_modules`, install them alongside the plugin:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install lib-font pako
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Quickstart
|
|
82
|
+
|
|
83
|
+
Wire `BatchUploadFonts` onto the `styles` field of a typeface document and you get the full drag-and-drop upload modal. The fastest way to build the rest of the `styles` object is the `createStylesField` factory, which assembles the fonts/variable-font/collections/pairs reference arrays for you.
|
|
84
|
+
|
|
85
|
+
```jsx
|
|
86
|
+
// schemas/typeface.js
|
|
87
|
+
import {
|
|
88
|
+
BatchUploadFonts,
|
|
89
|
+
createStylesField,
|
|
90
|
+
openTypeField,
|
|
91
|
+
styleCountField,
|
|
92
|
+
} from '@liiift-studio/sanity-font-manager';
|
|
93
|
+
|
|
94
|
+
export const typeface = {
|
|
95
|
+
name: 'typeface',
|
|
96
|
+
type: 'document',
|
|
97
|
+
groups: [
|
|
98
|
+
{ name: 'styles', title: 'Styles' },
|
|
99
|
+
{ name: 'openType', title: 'Open Type' },
|
|
100
|
+
],
|
|
101
|
+
fields: [
|
|
102
|
+
{ name: 'title', type: 'string' },
|
|
103
|
+
|
|
104
|
+
// The drag-and-drop batch uploader lives on the styles object.
|
|
105
|
+
{
|
|
106
|
+
...createStylesField({ generateCollections: true, pairs: true, styleCount: true }),
|
|
107
|
+
components: { input: BatchUploadFonts },
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// Spreadable pre-built fields (optional):
|
|
111
|
+
styleCountField,
|
|
112
|
+
openTypeField,
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then set the required environment variables in your studio (`SANITY_STUDIO_SITE_URL`, `SANITY_STUDIO_PROJECT_ID`, `SANITY_STUDIO_DATASET` — see [Environment variables](#environment-variables)). Multi-format conversion and subsetting additionally need a `/api/sanity/fontWorker` endpoint on the consuming site (see [`generateFontFile`](#css-and-file-generation) / [`generateSubset`](#css-and-file-generation)); TTF/OTF/WOFF/WOFF2 upload, parsing, CSS, and metadata work without it.
|
|
118
|
+
|
|
119
|
+
Open a typeface document, drag font files onto the styles field, review the detected weights/styles, resolve any duplicates, and confirm. See [Upload workflow](#upload-workflow) for the full step-by-step.
|
|
120
|
+
|
|
121
|
+
### Prerequisites the consumer provides
|
|
122
|
+
|
|
123
|
+
This plugin supplies the upload UI, parsing, and field factories — it writes to `font` and `typeface` document types that **your studio defines**. Use the [Schema fields](#schema-fields) tables below as the contract for the document shapes the uploaders read and patch (the `font` document's `fileInput`/`metaData`/`metrics` objects, the `typeface` document's `styles.fonts`/`styles.variableFont` arrays). `createStylesField` builds the `styles` object for you; the surrounding `typeface` and `font` document types are yours to declare.
|
|
124
|
+
|
|
125
|
+
### The `fontWorker` endpoint (multi-format conversion / subsetting)
|
|
126
|
+
|
|
127
|
+
TTF/OTF/WOFF/WOFF2 upload, parsing, CSS, and metadata all work with no extra infrastructure. The additional conversions (`generateFontFile`, `generateSubset` — EOT/SVG and DS-WEB/subset WOFF2) are delegated to a `POST /api/sanity/fontWorker` route **you implement on the consuming site**, because the conversion runs server-side. The plugin sends a JSON body of:
|
|
128
|
+
|
|
129
|
+
```jsonc
|
|
130
|
+
{ "code": "generate-fonts", "srcUrl": "...", "filename": "...", "documentId": "...", "codes": ["woff", "eot", ...] }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
(plus `documentTitle`, `documentVariableFont`, `documentStyle`, `documentWeight`, `fileInput`, `language`). The endpoint URL is derived from `SANITY_STUDIO_SITE_URL`. The request is fire-and-forget (`mode: 'no-cors'`), so the worker writes the converted assets back to the Sanity document itself.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Upload workflow
|
|
138
|
+
|
|
139
|
+
`BatchUploadFonts` opens `UploadModal`, a multi-step dialog that takes you from raw font files to linked Sanity documents. The flow is split into two phases — a preview you can edit, then the actual upload — so **nothing is written until you confirm**. Each step lists the component that implements it (in parentheses) for developers; the plain-language action comes first.
|
|
140
|
+
|
|
141
|
+
1. **Drag in your fonts** — drop TTF/OTF/WOFF/WOFF2 files onto the styles field and set defaults like price and how filenames map to titles. *(`UploadStep1Settings`)*
|
|
142
|
+
2. **Review what it detected** — the modal parses every file and shows a table of detected weight, style, and subfamily, all editable per font. Any font that already exists gets an **update existing vs create new** toggle so a re-upload updates the right document instead of duplicating it. This step only **reads** from Sanity. *(`UploadStep2Review` · `FontReviewCard` · `BulkActions` · `ExistingDocumentResolver`, driven by `buildUploadPlan` + `resolveExistingFont`)*
|
|
143
|
+
3. **Confirm to upload** — on your confirmation the assets are uploaded, `@font-face` CSS and metadata are generated, font documents are created or updated, and the typeface's `styles.fonts` array is patched. Progress is shown per font. *(`UploadStep3Execute`, driven by `executeUploadPlan`)*
|
|
144
|
+
4. **Map variable-font instances** — for a variable font, its named instances (e.g. *Light*, *Bold*) are matched to the matching static font documents; an **Autofill with Matching** action does this for you, and missing instances can be created. *(`UploadStep3bInstances` — see [`VariableInstanceReferencesInput`](#variableinstancereferencesinput))*
|
|
145
|
+
5. **Review the summary** — a per-font created / updated / failed report. *(`UploadSummary`)*
|
|
146
|
+
|
|
147
|
+
### Plan / Execute API
|
|
148
|
+
|
|
149
|
+
The pipeline is also exported as plain functions and reducers, so the upload engine can be driven or tested independently of the modal UI.
|
|
150
|
+
|
|
151
|
+
| Export | Description |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `UploadModal` | The full multi-step modal dialog. Props: `open`, `onClose`, `client`, `docId`, `typefaceTitle`, `stylesObject`, `preferredStyleRef`, `slug`, `defaults`. Lazy-loaded by `BatchUploadFonts`. |
|
|
154
|
+
| `UploadStep1Settings`, `UploadStep2Review`, `UploadStep3Execute`, `UploadStep3bInstances`, `UploadSummary` | The individual step components, exported for custom modal layouts. |
|
|
155
|
+
| `FontReviewCard` | Collapsible per-font review row with editable weight/style/title/document-ID and a files indicator. |
|
|
156
|
+
| `BulkActions` | Sticky search/filter/expand-all bar for the review step, with create/update/error/conflict counts. |
|
|
157
|
+
| `ExistingDocumentResolver` | The update-existing vs create-new toggle and candidate picker for a single font. |
|
|
158
|
+
| `buildUploadPlan` | **Phase 1** — reads font files, parses with `lib-font`, resolves existing documents, and returns a complete `UploadPlan` for review. Performs Sanity reads only. |
|
|
159
|
+
| `executeUploadPlan` | **Phase 2** — uploads assets, generates CSS/metadata, creates/updates font documents, and patches the typeface. Skips fonts marked `error`; caches asset refs for idempotent retry. |
|
|
160
|
+
| `resolveExistingFont` | Resolves whether a font already exists — exact `_id`/slug match first, then content match by `typefaceName + weightName + style + subfamily + variableFont`. Returns `{ exact, candidates, recommendation, lookupFailed }`. |
|
|
161
|
+
| `planReducer` | Reducer driving the plan state machine (file processing, user edits, document resolution). |
|
|
162
|
+
| `executionReducer`, `createInitialExecutionState` | Reducer + initializer tracking per-font execution progress. |
|
|
163
|
+
| `createEmptyPlan`, `createFontDecisions` | Factories for an empty `UploadPlan` and a font's decision record. |
|
|
164
|
+
| `FONT_STATUS`, `PLAN_PHASE`, `RECOMMENDATION`, `EXECUTION_STATUS`, `PLAN_VERSION` | Enum constants for plan/execution state (single source of truth in `planTypes`). |
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Safety & reliability
|
|
169
|
+
|
|
170
|
+
The upload engine is built to be pointed at a production dataset:
|
|
171
|
+
|
|
172
|
+
- **Review before write.** Phase 1 (`buildUploadPlan`) only **reads** — it parses files and looks up existing documents. No font document is created or modified until you confirm the plan and Phase 2 (`executeUploadPlan`) runs.
|
|
173
|
+
- **Duplicate resolution.** `resolveExistingFont` matches incoming fonts to existing documents (exact ID/slug, then content match) so a re-upload updates the right document instead of creating a duplicate.
|
|
174
|
+
- **Concurrency limit.** Asset uploads run at most `CONCURRENCY_LIMIT` (3) at a time.
|
|
175
|
+
- **Rate-limit handling.** `429` responses are retried up to `MAX_RETRIES` (3) with exponential backoff and ±25% jitter (`backoffWithJitter`), so large batches don't hammer the API. The retry is rate-limit-specific — other failures (a `500`, a network drop) fail that font rather than retrying, and it surfaces in the per-font summary.
|
|
176
|
+
- **Per-font isolation.** Uploads run via `Promise.allSettled`, so one font failing does not abort the rest of the batch; each font's outcome is tracked independently.
|
|
177
|
+
- **Idempotent retry.** Execution caches asset references in progress, so a partially failed batch can be retried without re-uploading assets that already succeeded; fonts marked `error` are skipped.
|
|
178
|
+
- **Long-upload guards.** The modal holds a Wake Lock and installs a `beforeunload` guard while executing, and confirms before closing mid-flight.
|
|
179
|
+
|
|
180
|
+
## Testing
|
|
181
|
+
|
|
182
|
+
The package ships a [Vitest](https://vitest.dev/) suite under `src/tests/` covering the reducers (`planReducer`, `executionReducer`), plan types, document resolution, font parsing/metadata, CSS generation, keyword expansion, and a `lib-font` integration test with a mock font fixture.
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm test # vitest run
|
|
186
|
+
npm run test:watch
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The `build` script (`npm run build`) runs the test suite before bundling with `tsup`, so a broken test blocks publish. (The suite — like the runtime — requires `lib-font` and `pako` in `node_modules`; see [Parsing dependencies](#parsing-dependencies).)
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Components
|
|
194
|
+
|
|
195
|
+
### `BatchUploadFonts`
|
|
196
|
+
|
|
197
|
+
Drag-and-drop batch uploader for a typeface document. Accepts TTF/OTF/WOFF/WOFF2 etc., shows a reviewable file list with count, confirm button, elapsed timer, Wake Lock, and `beforeunload` guard for long uploads. Calls `uploadFontFiles` for each batch.
|
|
198
|
+
|
|
199
|
+
```jsx
|
|
200
|
+
import { BatchUploadFonts } from '@liiift-studio/sanity-font-manager';
|
|
201
|
+
|
|
202
|
+
export const typefaceSchema = {
|
|
203
|
+
name: 'typeface',
|
|
204
|
+
type: 'document',
|
|
205
|
+
fields: [
|
|
206
|
+
{
|
|
207
|
+
name: 'styles',
|
|
208
|
+
type: 'object',
|
|
209
|
+
components: { input: BatchUploadFonts },
|
|
210
|
+
fields: [ /* see Schema fields below */ ],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### `SingleUploaderTool`
|
|
217
|
+
|
|
218
|
+
Per-font file manager inside a font document. Shows TTF/OTF/WOFF/WOFF2/CSS rows always. EOT/SVG/WEB/SUBSET/DATA are hidden behind an advanced toggle (cog icon). Each row has Upload/Build/Delete controls. Handles CSS regeneration, font data extraction, and WEB+SUBSET building via fontWorker.
|
|
219
|
+
|
|
220
|
+
```jsx
|
|
221
|
+
import { SingleUploaderTool } from '@liiift-studio/sanity-font-manager';
|
|
222
|
+
|
|
223
|
+
{
|
|
224
|
+
name: 'fileInput',
|
|
225
|
+
type: 'object',
|
|
226
|
+
components: { input: SingleUploaderTool },
|
|
227
|
+
fields: [ /* format fields — see Schema fields below */ ],
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### `GenerateCollectionsPairsComponent`
|
|
232
|
+
|
|
233
|
+
One-click generator for Full Family, Uprights, Italics, and Subfamily collections, plus Regular/Italic pairs matched by weight. Has configurable price inputs for collection-per-font and pair price.
|
|
234
|
+
|
|
235
|
+
```jsx
|
|
236
|
+
import { GenerateCollectionsPairsComponent } from '@liiift-studio/sanity-font-manager';
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### `PrimaryCollectionGeneratorTypeface`
|
|
240
|
+
|
|
241
|
+
One-click generator for a single full-family collection that includes all fonts linked to the typeface. Prepends the new collection to the existing `styles.collections` array — non-destructive. Uses `SANITY_STUDIO_DEFAULT_COLLECTION_PRICE` as the default price, falling back to `100`.
|
|
242
|
+
|
|
243
|
+
Wire it up on a `string` field in the typeface schema:
|
|
244
|
+
|
|
245
|
+
```jsx
|
|
246
|
+
import { PrimaryCollectionGeneratorTypeface } from '@liiift-studio/sanity-font-manager';
|
|
247
|
+
|
|
248
|
+
{
|
|
249
|
+
name: 'generateCollectionGroup',
|
|
250
|
+
type: 'string',
|
|
251
|
+
title: 'Generate Full Family Collection',
|
|
252
|
+
description: 'Generate a collection that includes all the styles from this typeface.',
|
|
253
|
+
components: { input: PrimaryCollectionGeneratorTypeface },
|
|
254
|
+
hidden: ({ parent }) => !parent?.styles?.fonts?.length,
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### `FontScriptUploaderComponent`
|
|
259
|
+
|
|
260
|
+
Script-aware uploader for per-script font file variants (Latin, Arabic, Hebrew, etc.) stored in `scriptFileInput` on the font document.
|
|
261
|
+
|
|
262
|
+
### `UploadScriptsComponent`
|
|
263
|
+
|
|
264
|
+
Batch uploader for script-specific font variants across multiple fonts at once.
|
|
265
|
+
|
|
266
|
+
### `UpdateScriptsComponent`
|
|
267
|
+
|
|
268
|
+
Updates and re-links existing script font variant references on font documents — used to fix or reassign script variant assignments.
|
|
269
|
+
|
|
270
|
+
### `RegenerateSubfamiliesComponent`
|
|
271
|
+
|
|
272
|
+
Recalculates and patches the `subfamily` field on all fonts linked to a typeface, based on the typeface's defined subfamily groups — without re-uploading any files.
|
|
273
|
+
|
|
274
|
+
### `SetOTF`
|
|
275
|
+
|
|
276
|
+
Detects which configured OpenType feature keys are supported by the typeface's first linked font. Reads `opentypeFeatures.chars` from the font document (populated by `generateFontData`) and patches the `features` array on the field. Shows a feature count when features are detected, and clear error messages when font data is missing.
|
|
277
|
+
|
|
278
|
+
Wire it up on the `openType` object field in the typeface schema:
|
|
279
|
+
|
|
280
|
+
```jsx
|
|
281
|
+
import { SetOTF } from '@liiift-studio/sanity-font-manager';
|
|
282
|
+
|
|
283
|
+
{
|
|
284
|
+
name: 'openType',
|
|
285
|
+
type: 'object',
|
|
286
|
+
components: { input: SetOTF },
|
|
287
|
+
options: { collapsible: true },
|
|
288
|
+
fields: [ /* feature fields — each with a `feature` string e.g. 'liga', 'smcp' */ ],
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### `StyleCountInput`
|
|
293
|
+
|
|
294
|
+
Displays the total number of font styles (static + variable) linked to a typeface. Reads `styles.fonts` and `styles.variableFont` arrays from the form context. Useful as a read-only display field in the typeface schema.
|
|
295
|
+
|
|
296
|
+
```jsx
|
|
297
|
+
import { StyleCountInput } from '@liiift-studio/sanity-font-manager';
|
|
298
|
+
|
|
299
|
+
{
|
|
300
|
+
name: 'styleCount',
|
|
301
|
+
type: 'number',
|
|
302
|
+
readOnly: true,
|
|
303
|
+
components: { input: StyleCountInput },
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### `KeyValueInput`
|
|
308
|
+
|
|
309
|
+
Generic ordered key-value editor where both keys and values are plain strings. Supports add, remove, and reorder (up/down arrows). Values are stored as an array of `{ key, value }` objects.
|
|
310
|
+
|
|
311
|
+
```jsx
|
|
312
|
+
import { KeyValueInput } from '@liiift-studio/sanity-font-manager';
|
|
313
|
+
|
|
314
|
+
{
|
|
315
|
+
name: 'aliases',
|
|
316
|
+
type: 'array',
|
|
317
|
+
of: [{ type: 'object', fields: [{ name: 'key', type: 'string' }, { name: 'value', type: 'string' }] }],
|
|
318
|
+
components: { input: KeyValueInput },
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### `KeyValueReferenceInput`
|
|
323
|
+
|
|
324
|
+
Generic key-value editor where keys are plain strings and values are weak Sanity document references. Supports searching by title via a popover picker, add/remove/reorder, and an optional `topActions` slot for action buttons above the list.
|
|
325
|
+
|
|
326
|
+
| Prop | Type | Description |
|
|
327
|
+
|---|---|---|
|
|
328
|
+
| `fetchReferences` | `async (client, doc) => [{_id, title}]` | Async function that returns candidate references for the picker. Receives the Sanity client and the current document. |
|
|
329
|
+
| `topActions` | `ReactNode` | Optional content rendered above the key-value rows (e.g. autofill buttons). |
|
|
330
|
+
| `referenceType` | `string` | Document type for the created weak references (default: `'font'`). |
|
|
331
|
+
|
|
332
|
+
```jsx
|
|
333
|
+
import { KeyValueReferenceInput } from '@liiift-studio/sanity-font-manager';
|
|
334
|
+
|
|
335
|
+
{
|
|
336
|
+
name: 'instanceMap',
|
|
337
|
+
type: 'array',
|
|
338
|
+
of: [{ type: 'object', fields: [{ name: 'key', type: 'string' }, { name: 'value', type: 'reference', weak: true, to: [{ type: 'font' }] }] }],
|
|
339
|
+
components: { input: KeyValueReferenceInput },
|
|
340
|
+
// Pass props via options or a wrapper component:
|
|
341
|
+
options: {
|
|
342
|
+
fetchReferences: async (client, doc) => client.fetch('*[_type == "font"]{_id, title}'),
|
|
343
|
+
referenceType: 'font',
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### `VariableInstanceReferencesInput`
|
|
349
|
+
|
|
350
|
+
Font-specific wrapper around `KeyValueReferenceInput` for mapping variable font instance names to their matching static font documents. Provides:
|
|
351
|
+
|
|
352
|
+
- A picker filtered to fonts sharing the same `typefaceName`, excluding variable fonts
|
|
353
|
+
- **Autofill with Matching** — calls `parseVariableFontInstances` to match instance names to existing font documents by weight/style heuristics
|
|
354
|
+
- **Autofill Keys Only** — populates instance name keys from the font's `variableInstances` metadata without resolving references
|
|
355
|
+
- Autofill buttons are shown only when the document is a variable font with parsed instance data
|
|
356
|
+
- Replace/merge confirmation dialog when pairs already exist
|
|
357
|
+
|
|
358
|
+
```jsx
|
|
359
|
+
import { VariableInstanceReferencesInput } from '@liiift-studio/sanity-font-manager';
|
|
360
|
+
|
|
361
|
+
{
|
|
362
|
+
name: 'variableInstanceReferences',
|
|
363
|
+
title: 'Variable Font Instances',
|
|
364
|
+
type: 'array',
|
|
365
|
+
hidden: ({ parent }) => !parent.variableFont,
|
|
366
|
+
of: [
|
|
367
|
+
{
|
|
368
|
+
type: 'object',
|
|
369
|
+
fields: [
|
|
370
|
+
{ name: 'key', type: 'string', title: 'Instance Name' },
|
|
371
|
+
{ name: 'value', type: 'reference', weak: true, to: [{ type: 'font' }], title: 'Matching Font' },
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
components: { input: VariableInstanceReferencesInput },
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### `NestedObjectArraySelector`
|
|
380
|
+
|
|
381
|
+
Generic Sanity input that renders a searchable checkbox list of items pulled from a nested array field across documents of a given type (backed by [`useNestedObjects`](#usenestedobjects)). Configure entirely via schema `options`.
|
|
382
|
+
|
|
383
|
+
```jsx
|
|
384
|
+
import { NestedObjectArraySelector } from '@liiift-studio/sanity-font-manager';
|
|
385
|
+
|
|
386
|
+
{
|
|
387
|
+
name: 'sections',
|
|
388
|
+
type: 'array',
|
|
389
|
+
of: [{ type: 'string' }],
|
|
390
|
+
components: { input: NestedObjectArraySelector },
|
|
391
|
+
options: {
|
|
392
|
+
sourceType: 'licenseGroup', // document type to query
|
|
393
|
+
nestedField: 'sections', // array field to extract
|
|
394
|
+
titleField: 'title', // GROQ expression for display text
|
|
395
|
+
valueField: 'slug.current', // GROQ expression for stored value
|
|
396
|
+
filter: 'state == "published"',// optional GROQ filter
|
|
397
|
+
sortBy: 'title asc', // optional sort
|
|
398
|
+
emptyMessage: 'No options found',
|
|
399
|
+
searchPlaceholder: 'Search...',
|
|
400
|
+
},
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### `StatusDisplay`
|
|
405
|
+
|
|
406
|
+
Shared status bar used by all components. Shows `Status: [message]` in green on success and red on error, with an optional `action` element slot on the far right (used for the advanced toggle in `SingleUploaderTool`).
|
|
407
|
+
|
|
408
|
+
```jsx
|
|
409
|
+
import { StatusDisplay } from '@liiift-studio/sanity-font-manager';
|
|
410
|
+
|
|
411
|
+
<StatusDisplay status="ready" error={false} action={<Button ... />} />
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### `PriceInput`
|
|
415
|
+
|
|
416
|
+
Reusable `$` + number input for collection and pair price fields.
|
|
417
|
+
|
|
418
|
+
### `UploadButton`
|
|
419
|
+
|
|
420
|
+
Label-wrapped button that triggers a hidden file input.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Schema field definitions
|
|
425
|
+
|
|
426
|
+
Pre-built Sanity schema field objects that can be spread directly into a typeface schema's `fields` array. Eliminates hundreds of lines of repeated field definitions across consumer studios.
|
|
427
|
+
|
|
428
|
+
### `createStylesField`
|
|
429
|
+
|
|
430
|
+
Factory that builds the complete `styles` object field — the fonts, variable-font, collections, and pairs reference arrays plus the subfamily groups — for a typeface document. This is the field `BatchUploadFonts` reads and writes (see [Quickstart](#quickstart)). Call it with options to toggle optional sub-fields:
|
|
431
|
+
|
|
432
|
+
```js
|
|
433
|
+
import { createStylesField, BatchUploadFonts } from '@liiift-studio/sanity-font-manager';
|
|
434
|
+
|
|
435
|
+
{
|
|
436
|
+
...createStylesField({ generateCollections: true, pairs: true, styleCount: true }),
|
|
437
|
+
components: { input: BatchUploadFonts },
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
| Option | Default | Description |
|
|
442
|
+
|---|---|---|
|
|
443
|
+
| `pairs` | `true` | Include the Regular/Italic pairs reference array. |
|
|
444
|
+
| `generateCollections` | `false` | Include the collections array + `GenerateCollectionsPairsComponent`. |
|
|
445
|
+
| `generateFullFamilyCollection` | `false` | Include the full-family collection generator (`PrimaryCollectionGeneratorTypeface`). |
|
|
446
|
+
| `regenerateSubfamilies` | `false` | Include the `RegenerateSubfamiliesComponent` action. |
|
|
447
|
+
| `styleCount` | `false` | Inject the read-only style-count field (`StyleCountInput`). |
|
|
448
|
+
| `displayStyles`, `free`, `serif`, `sortHeaviestFirst`, `buySectionColumns`, `fontSizeMultiplier`, `subfamily*` | various | Storefront/display toggles — see the source for the full set. |
|
|
449
|
+
|
|
450
|
+
> Uses `@liiift-studio/sanity-advanced-reference-array` (a peer dependency — see [Peer dependencies](#peer-dependencies)) for the typeface-scoped reference pickers.
|
|
451
|
+
|
|
452
|
+
### `openTypeField`
|
|
453
|
+
|
|
454
|
+
A complete `openType` object field wired to the `openType` tab group. Includes the `features` checkbox array (all standard OpenType feature keys) plus per-feature sub-objects with `title`, `feature`, and `customText` fields. Uses `SetOTF` internally for auto-detection.
|
|
455
|
+
|
|
456
|
+
```js
|
|
457
|
+
import { openTypeField } from '@liiift-studio/sanity-font-manager';
|
|
458
|
+
|
|
459
|
+
// In your typeface schema fields array:
|
|
460
|
+
openTypeField,
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Requires the `openType` group to be declared in your schema's `groups` array:
|
|
464
|
+
```js
|
|
465
|
+
{ name: 'openType', title: 'Open Type' }
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### `createOpenTypeField`
|
|
469
|
+
|
|
470
|
+
Factory variant of `openTypeField`. Pass `{ customText: true }` to reveal a per-feature `customText` input on every feature object; pass `{ customText: true, customTextType: 'code' }` to make it a syntax-highlighted HTML `code` field (for `<span>`-wrapped sample text). Returns a plain `openTypeField` when `customText` is `false`.
|
|
471
|
+
|
|
472
|
+
```js
|
|
473
|
+
import { createOpenTypeField } from '@liiift-studio/sanity-font-manager';
|
|
474
|
+
|
|
475
|
+
// In your typeface schema fields array:
|
|
476
|
+
createOpenTypeField({ customText: true, customTextType: 'code' }),
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### `styleCountField`
|
|
480
|
+
|
|
481
|
+
A read-only `number` field in the `styles` group that displays the total count of static + variable font styles linked to the typeface. Uses `StyleCountInput` internally.
|
|
482
|
+
|
|
483
|
+
```js
|
|
484
|
+
import { styleCountField } from '@liiift-studio/sanity-font-manager';
|
|
485
|
+
|
|
486
|
+
// In your typeface schema fields array:
|
|
487
|
+
styleCountField,
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### `stylisticSetField`
|
|
491
|
+
|
|
492
|
+
A complete `stylisticSet` object field for the `stylisticSets` group. Contains two sub-arrays: `featured` (highlighted words/phrases with per-character backtick syntax, stylistic feature picker, size, and CSS overrides) and `sets` (full catalogue of feature → glyph mappings). Both include the full OpenType feature dropdown (44 named features + all 20 stylistic sets).
|
|
493
|
+
|
|
494
|
+
```js
|
|
495
|
+
import { stylisticSetField } from '@liiift-studio/sanity-font-manager';
|
|
496
|
+
|
|
497
|
+
// In your typeface schema fields array:
|
|
498
|
+
stylisticSetField,
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Requires the `stylisticSets` group to be declared in your schema's `groups` array:
|
|
502
|
+
```js
|
|
503
|
+
{ name: 'stylisticSets', title: 'Stylistic Sets' }
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Hook
|
|
509
|
+
|
|
510
|
+
### `useSanityClient`
|
|
511
|
+
|
|
512
|
+
Returns the Sanity client instance from the studio context. Used internally by all components.
|
|
513
|
+
|
|
514
|
+
```js
|
|
515
|
+
import { useSanityClient } from '@liiift-studio/sanity-font-manager';
|
|
516
|
+
|
|
517
|
+
const client = useSanityClient();
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### `useNestedObjects`
|
|
521
|
+
|
|
522
|
+
Fetches and flattens a nested array field across documents of a given type into a flat list of selectable items. Backs `NestedObjectArraySelector`.
|
|
523
|
+
|
|
524
|
+
```js
|
|
525
|
+
import { useNestedObjects } from '@liiift-studio/sanity-font-manager';
|
|
526
|
+
|
|
527
|
+
const { objects, loading, error } = useNestedObjects({
|
|
528
|
+
sourceType: 'licenseGroup', // document type to query
|
|
529
|
+
nestedField: 'sections', // array field to extract
|
|
530
|
+
titleField: 'title', // GROQ expression for display text
|
|
531
|
+
valueField: 'slug.current', // GROQ expression for stored value
|
|
532
|
+
filter: 'state == "published"', // optional GROQ filter
|
|
533
|
+
sortBy: 'title asc', // optional sort
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## Utilities
|
|
540
|
+
|
|
541
|
+
### Font parsing (lib-font)
|
|
542
|
+
|
|
543
|
+
Parsing runs on [`lib-font`](https://github.com/Pomax/lib-font). `parseFont` is the single entry point; the `fontHelpers` wrappers are the only code that touches `font.opentype.tables.*`. `setupDecompressors` registers `globalThis.pako` (WOFF) and `globalThis.unbrotli` (WOFF2) and is imported as a side effect by the package entry point and by `parseFont` — **import order matters**, so do not import `lib-font` directly before it.
|
|
544
|
+
|
|
545
|
+
| Export | Description |
|
|
546
|
+
|---|---|
|
|
547
|
+
| `parseFont` | `async (buffer, filename) => Font` — parses an `ArrayBuffer` into a `lib-font` `Font`. Enforces a 50 MB size limit and a 30 s timeout; throws on oversize/corrupt/timeout. |
|
|
548
|
+
| `getNameString` | Reads a name-table string by numeric name ID, preferring Windows/Unicode/English then Mac/Roman, with a per-font cache. |
|
|
549
|
+
| `getAllFeatureTags` | All OpenType feature tags from GSUB/GPOS (equivalent to fontkit's `availableFeatures`). |
|
|
550
|
+
| `getCharacterSet` | Array of Unicode code points covered by the font. |
|
|
551
|
+
| `getVariationAxes` | Variation-axis map for variable fonts (`min`/`default`/`max` per axis). |
|
|
552
|
+
| `getNamedInstances` | Named instances of a variable font. |
|
|
553
|
+
| `getFontMetrics` | `unitsPerEm`, ascender/descender, cap/x-height, italic angle, etc. |
|
|
554
|
+
| `getFontMetadata` | `postscriptName`, `fullName`, `familyName`, `subfamilyName`, `copyright`, `version`. |
|
|
555
|
+
| `getWeightClass` | OS/2 `usWeightClass`. |
|
|
556
|
+
| `getFsSelection`, `getMacStyle`, `getItalicAngle`, `getGlyphCount`, `getFamilyClass` | Lower-level table accessors. |
|
|
557
|
+
| `escapeCssFontName` | Escapes a font family name for safe use in CSS. |
|
|
558
|
+
|
|
559
|
+
### Font processing
|
|
560
|
+
|
|
561
|
+
| Export | Description |
|
|
562
|
+
|---|---|
|
|
563
|
+
| `processFontFiles` | Reads font files via FileReader, parses with `lib-font` (via `parseFont`), and builds the `fontsObjects` map used by `uploadFontFiles` |
|
|
564
|
+
| `extractFontMetadata` | Extracts weight name, subfamily, style, and variable font flag from a `lib-font` parsed font |
|
|
565
|
+
| `extractWeightName` | Reads the weight name from `lib-font` name records, falling back through `preferredSubfamily → fontSubfamily` |
|
|
566
|
+
| `extractWeightFromFullName` | Strips the typeface title from the font's full name to isolate the weight/style suffix |
|
|
567
|
+
| `processSubfamilyName` | Strips weight and italic keywords from a subfamily string, preserving non-style words like "Condensed" |
|
|
568
|
+
| `formatFontTitle` | Normalises a font filename into a human-readable title — expands abbreviations, title-cases, collapses spaces |
|
|
569
|
+
| `addItalicToFontTitle` | Appends the detected italic keyword to a title when the font has a non-zero italic angle |
|
|
570
|
+
| `determineWeight` | Maps a weight name to a CSS numeric weight, preferring OS/2 `usWeightClass` when available |
|
|
571
|
+
| `sortFontObjects` | Sorts a `fontsObjects` map by ascending weight, placing Regular before Italic at equal weights |
|
|
572
|
+
| `createFontObject` | Builds the full font object (id, title, weight, style, files, etc.) for a single font file |
|
|
573
|
+
| `uploadFontFiles` | Core batch upload orchestrator — uploads each format to Sanity, generates CSS and metadata, then creates or updates font documents |
|
|
574
|
+
| `updateTypefaceDocument` | Patches the parent typeface document's `styles.fonts` array with newly uploaded font references |
|
|
575
|
+
| `renameFontDocuments` | Renames font document IDs across a typeface when a typeface slug changes |
|
|
576
|
+
| `updateFontPrices` | Bulk-updates the `price` field across all font documents linked to a typeface |
|
|
577
|
+
| `sanitizeForSanityId` | Converts arbitrary strings into valid Sanity document IDs (lowercase, hyphens, no special characters) |
|
|
578
|
+
|
|
579
|
+
### CSS and file generation
|
|
580
|
+
|
|
581
|
+
| Export | Description |
|
|
582
|
+
|---|---|
|
|
583
|
+
| `generateCssFile` | Builds a `@font-face` CSS file from a WOFF2 blob — URL or base64 `src`, variable font axis descriptors, and metric-tuned fallback `@font-face` for CLS reduction |
|
|
584
|
+
| `buildVFDescriptors` | Pure function — maps variation axes (from `getVariationAxes`) to CSS descriptors (`font-weight`, `font-stretch`, `font-style`), handling degenerate axes, `slnt`/`ital` priority, and `min > max` clamping |
|
|
585
|
+
| `generateFontData` | Fetches a TTF URL, parses with `lib-font`, and patches the Sanity font document with `metaData`, `metrics`, `glyphCount`, `opentypeFeatures`, `characterSet`, and variable axes/instances |
|
|
586
|
+
| `buildFontMetadata` | Pure function — extracts `metaData` and `metrics` from a `lib-font` parsed font without any Sanity side effects |
|
|
587
|
+
| `generateFontFile` | Fires a POST to the consuming site's `/api/sanity/fontWorker` endpoint with the format codes to convert (otf, woff, woff2, eot, svg, data) |
|
|
588
|
+
| `generateSubset` | Requests DS-WEB fingerprinted WOFF2 and display subset generation from an existing WOFF2 via fontWorker |
|
|
589
|
+
| `parseVariableFontInstances` | Resolves named variable font instances into Sanity font document references, creating documents for missing instances |
|
|
590
|
+
| `getEmptyFontKit` | Returns a zeroed-out placeholder font object used when no font binary is available |
|
|
591
|
+
|
|
592
|
+
### Keyword utilities
|
|
593
|
+
|
|
594
|
+
| Export | Description |
|
|
595
|
+
|---|---|
|
|
596
|
+
| `generateStyleKeywords` | Builds weight and italic keyword lists (including abbreviation expansions like `Bd → Bold`, `Lt → Light`) for parsing font subfamily names |
|
|
597
|
+
| `reverseSpellingLookup` | Resolves a font name abbreviation to its canonical weight name |
|
|
598
|
+
| `expandAbbreviations` | Expands all known abbreviations in a string to full weight names |
|
|
599
|
+
| `removeWeightNames` | Strips weight and italic keywords from a string, leaving only non-style words |
|
|
600
|
+
|
|
601
|
+
### Constants
|
|
602
|
+
|
|
603
|
+
| Export | Description |
|
|
604
|
+
|---|---|
|
|
605
|
+
| `SCRIPTS` | Array of supported script variant names |
|
|
606
|
+
| `SCRIPTS_OBJECT` | Map of script names to their display labels |
|
|
607
|
+
| `HtmlDescription` | React component rendering the supported script list as formatted HTML |
|
|
608
|
+
| `DISCOUNT_REQUIREMENT_TYPES` | Array of supported discount-requirement type names |
|
|
609
|
+
| `DISCOUNT_REQUIREMENT_TYPES_OBJECT` | Map of discount-requirement type names to their display labels |
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## Schema fields
|
|
614
|
+
|
|
615
|
+
### Font document (`font`)
|
|
616
|
+
|
|
617
|
+
| Field | Type | Description |
|
|
618
|
+
|---|---|---|
|
|
619
|
+
| `title` | `string` | Full font name (e.g. `MyFont SemiBold Italic`) |
|
|
620
|
+
| `slug` | `slug` | Sanitized document ID as a slug (`current` = document `_id`) |
|
|
621
|
+
| `typefaceName` | `string` | Name of the parent typeface |
|
|
622
|
+
| `style` | `string` | `'Regular'` or `'Italic'` |
|
|
623
|
+
| `weight` | `number` | Numeric CSS weight (100–900) |
|
|
624
|
+
| `weightName` | `string` | Human-readable weight name (e.g. `'SemiBold'`) |
|
|
625
|
+
| `subfamily` | `string` | Subfamily name (e.g. `'Condensed'`) |
|
|
626
|
+
| `variableFont` | `boolean` | `true` for variable fonts |
|
|
627
|
+
| `normalWeight` | `boolean` | `true` when the weight is the normal/regular weight |
|
|
628
|
+
| `fileInput` | `object` | Container for all uploaded format files |
|
|
629
|
+
| `fileInput.ttf` | `file` | Uploaded TTF file (Sanity asset reference) |
|
|
630
|
+
| `fileInput.otf` | `file` | OTF file (built from TTF or uploaded directly) |
|
|
631
|
+
| `fileInput.woff2` | `file` | WOFF2 file (built from TTF or uploaded directly) |
|
|
632
|
+
| `fileInput.woff` | `file` | WOFF file |
|
|
633
|
+
| `fileInput.eot` | `file` | EOT file (legacy) |
|
|
634
|
+
| `fileInput.svg` | `file` | SVG font file (legacy) |
|
|
635
|
+
| `fileInput.css` | `file` | Generated `@font-face` CSS file |
|
|
636
|
+
| `fileInput.woff2_web` | `file` | DS-WEB fingerprinted WOFF2 for web delivery |
|
|
637
|
+
| `fileInput.woff2_subset` | `file` | Display subset WOFF2 (Latin + Latin-1, fingerprinted) |
|
|
638
|
+
| `metaData` | `object` | Font metadata — `postscriptName`, `fullName`, `familyName`, `subfamilyName`, `copyright`, `version`, `genDate` |
|
|
639
|
+
| `metrics` | `object` | Font metrics — `unitsPerEm`, `ascender`, `descender`, `lineGap`, `capHeight`, `xHeight`, `italicAngle`, etc. |
|
|
640
|
+
| `glyphCount` | `number` | Total number of glyphs |
|
|
641
|
+
| `opentypeFeatures` | `object` | Available OpenType feature tags |
|
|
642
|
+
| `characterSet` | `object` | Array of Unicode code points covered by the font |
|
|
643
|
+
| `variableInstanceReferences` | `array<object>` | Maps variable font instance names to static font document references — `[{ key: string, value: reference }]` |
|
|
644
|
+
|
|
645
|
+
### Typeface document (`typeface`)
|
|
646
|
+
|
|
647
|
+
| Field | Type | Description |
|
|
648
|
+
|---|---|---|
|
|
649
|
+
| `styles.fonts` | `array<reference>` | References to regular font documents |
|
|
650
|
+
| `styles.variableFont` | `array<reference>` | References to variable font documents |
|
|
651
|
+
| `styles.collections` | `array<reference>` | References to generated collection documents |
|
|
652
|
+
| `styles.pairs` | `array<reference>` | References to generated pair documents |
|
|
653
|
+
| `styles.subfamilies` | `array<object>` | Subfamily groups — each has `title`, `_key`, and `fonts: array<reference>` |
|
|
654
|
+
| `preferredStyle` | `reference` | Reference to the preferred regular-weight font document |
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## Environment variables
|
|
659
|
+
|
|
660
|
+
| Variable | Required | Description |
|
|
661
|
+
|---|---|---|
|
|
662
|
+
| `SANITY_STUDIO_SITE_URL` | Yes | Base URL of the consuming site. Used by `generateFontFile` and `generateSubset` to call `/api/sanity/fontWorker`. |
|
|
663
|
+
| `SANITY_STUDIO_PROJECT_ID` | Yes | Sanity project ID. Used to build CDN file URLs inside the uploaders. |
|
|
664
|
+
| `SANITY_STUDIO_DATASET` | Yes | Sanity dataset name. Used alongside `PROJECT_ID` for CDN URLs. |
|
|
665
|
+
| `SANITY_STUDIO_SCRIPTS` | No | Comma-separated script variant names (e.g. `latin,greek,arabic`). Controls which script tabs appear. |
|
|
666
|
+
| `SANITY_STUDIO_DEFAULT_COLLECTION_PRICE` | No | Default per-font price for generated collections. |
|
|
667
|
+
| `SANITY_STUDIO_DEFAULT_PAIR_PRICE` | No | Default price for generated pairs. |
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## Local development
|
|
672
|
+
|
|
673
|
+
To use the local source instead of the published npm package, symlink it into a foundry repo:
|
|
674
|
+
|
|
675
|
+
```bash
|
|
676
|
+
# From the sanity-font-manager directory:
|
|
677
|
+
npm run link:darden # symlink into Darden Studio
|
|
678
|
+
npm run link:tdf # symlink into The Designers Foundry
|
|
679
|
+
npm run link:mckl # symlink into MCKL CMS
|
|
680
|
+
npm run link:all # symlink into all three at once
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Then run the watch build so consumers pick up changes live:
|
|
684
|
+
|
|
685
|
+
```bash
|
|
686
|
+
npm run dev
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
To restore the published package in a consumer repo, run `npm install` inside that repo.
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## License
|
|
694
|
+
|
|
695
|
+
[MIT](LICENSE) — [Liiift Studio](https://github.com/Liiift-Studio)
|