@liiift-studio/sanity-font-manager 2.2.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.
@@ -0,0 +1,94 @@
1
+ // Bulk-updates the price field across all font documents linked to a typeface
2
+
3
+ /**
4
+ * Sets the same price on every font document referenced by a typeface.
5
+ *
6
+ * @param {Object} params
7
+ * @param {Object} params.client - Sanity client
8
+ * @param {string} params.title - Typeface title
9
+ * @param {Object} params.slug - Typeface slug object
10
+ * @param {string} params.inputPrice - New price value (will be coerced to Number)
11
+ * @param {string} params.doc_id - Document ID (used to detect draft state)
12
+ * @param {Function} params.setStatus
13
+ * @param {Function} params.setError
14
+ * @returns {Promise<Object>}
15
+ */
16
+ export const updateFontPrices = async ({
17
+ client,
18
+ title,
19
+ slug,
20
+ inputPrice,
21
+ doc_id,
22
+ setStatus,
23
+ setError,
24
+ }) => {
25
+ try {
26
+ if (!title) {
27
+ setStatus('Typeface needs a title');
28
+ setError(true);
29
+ console.error('Typeface needs title');
30
+ return { success: false, message: 'Typeface needs title' };
31
+ }
32
+
33
+ if (!slug?.current) {
34
+ setStatus('Typeface needs a slug');
35
+ setError(true);
36
+ console.error('Typeface needs slug');
37
+ return { success: false, message: 'Typeface needs slug' };
38
+ }
39
+
40
+ const price = Number(inputPrice);
41
+ if (isNaN(price)) {
42
+ setStatus('Invalid price value');
43
+ setError(true);
44
+ console.error('Invalid price value');
45
+ return { success: false, message: 'Invalid price value' };
46
+ }
47
+
48
+ setStatus('Fetching typeface document...');
49
+ const typeface = await client.fetch(
50
+ `*[_type == "typeface" && slug.current == $slug][0]`,
51
+ { slug: slug.current }
52
+ );
53
+
54
+ if (!typeface) {
55
+ setStatus('Typeface not found');
56
+ setError(true);
57
+ console.error('Typeface not found');
58
+ return { success: false, message: 'Typeface not found' };
59
+ }
60
+
61
+ if (!typeface.styles?.fonts?.length) {
62
+ setStatus('No fonts found in typeface');
63
+ setError(true);
64
+ console.error('No fonts found in typeface');
65
+ return { success: false, message: 'No fonts found in typeface' };
66
+ }
67
+
68
+ const fontRefs = typeface.styles.fonts;
69
+ setStatus(`Updating prices for ${fontRefs.length} fonts...`);
70
+
71
+ let updatedCount = 0;
72
+ for (let i = 0; i < fontRefs.length; i++) {
73
+ try {
74
+ await client.patch(fontRefs[i]._ref).set({ price, sell: price > 0 }).commit();
75
+ updatedCount++;
76
+ setStatus(`Updated ${updatedCount}/${fontRefs.length} fonts...`);
77
+ } catch (err) {
78
+ console.error(`Error updating font ${fontRefs[i]._ref}:`, err);
79
+ }
80
+ }
81
+
82
+ const successMessage = `Successfully updated prices for ${updatedCount} fonts to $${price}`;
83
+ setStatus(successMessage);
84
+ console.log(successMessage);
85
+
86
+ return { success: true, message: successMessage, updatedCount };
87
+ } catch (err) {
88
+ const errorMessage = `Error: ${err.message}`;
89
+ console.error('Error updating font prices:', err);
90
+ setError(true);
91
+ setStatus(errorMessage);
92
+ return { success: false, message: errorMessage };
93
+ }
94
+ };
@@ -0,0 +1,160 @@
1
+ // Patches the parent typeface document's styles.fonts array with newly uploaded font references
2
+
3
+ import { nanoid } from 'nanoid';
4
+
5
+ /**
6
+ * Patches a typeface document (draft and published) with the new font references,
7
+ * subfamily structure, and preferred style derived from the upload batch.
8
+ *
9
+ * @param {string} doc_id - The Sanity document ID (may be a draft)
10
+ * @param {Object[]} fontRefs - New regular font references
11
+ * @param {Object[]} variableRefs - New variable font references
12
+ * @param {Object} subfamilies - Map of font ID → subfamily name
13
+ * @param {string[]} uniqueSubfamilies
14
+ * @param {Object[]} subfamiliesArray - Existing subfamilies array from the typeface
15
+ * @param {Object} preferredStyleRef - Existing preferred style reference
16
+ * @param {Object} newPreferredStyle - Candidate preferred style from the upload
17
+ * @param {Object} stylesObject - Existing typeface styles object
18
+ * @param {Object} client - Sanity client
19
+ * @param {Function} setStatus
20
+ * @param {Function} setError
21
+ */
22
+ export const updateTypefaceDocument = async (
23
+ doc_id,
24
+ fontRefs,
25
+ variableRefs,
26
+ subfamilies,
27
+ uniqueSubfamilies,
28
+ subfamiliesArray,
29
+ preferredStyleRef,
30
+ newPreferredStyle,
31
+ stylesObject,
32
+ client,
33
+ setStatus,
34
+ setError,
35
+ ) => {
36
+ console.log('Updating typeface document with new fonts:', { fontRefs, variableRefs, subfamilies, uniqueSubfamilies });
37
+ setStatus('Updating typeface references...');
38
+
39
+ let patch = {
40
+ styles: {
41
+ fonts: stylesObject.fonts ? [...stylesObject.fonts, ...fontRefs] : [...fontRefs],
42
+ subfamilies: uniqueSubfamilies.length > 1 ? uniqueSubfamilies : [],
43
+ variableFont: stylesObject?.variableFont ? [...stylesObject.variableFont, ...variableRefs] : [...variableRefs],
44
+ },
45
+ };
46
+
47
+ setStatus('Organising font subfamilies...');
48
+ subfamiliesArray = subfamiliesArray || [];
49
+
50
+ // Create any missing subfamily groups
51
+ uniqueSubfamilies.forEach(subfamilyName => {
52
+ if (!subfamiliesArray.find(sf => sf.title === subfamilyName)) {
53
+ subfamiliesArray.push({
54
+ title: subfamilyName,
55
+ _key: nanoid(),
56
+ _type: 'object',
57
+ fonts: [],
58
+ });
59
+ }
60
+ });
61
+
62
+ // Associate fonts with their subfamily groups (skip VF fonts)
63
+ if (subfamiliesArray.length > 0) {
64
+ Object.entries(subfamilies).forEach(([id, subfamilyName]) => {
65
+ if (id.toLowerCase().includes('vf')) return;
66
+
67
+ const subfamilyIndex = subfamiliesArray.findIndex(sf => sf.title === subfamilyName);
68
+ if (subfamilyIndex !== -1) {
69
+ subfamiliesArray[subfamilyIndex].fonts.push({
70
+ _ref: id,
71
+ _key: nanoid(),
72
+ _type: 'reference',
73
+ _weak: true,
74
+ });
75
+ }
76
+ });
77
+
78
+ // Deduplicate references within each subfamily
79
+ subfamiliesArray = subfamiliesArray.map(subfamily => ({
80
+ ...subfamily,
81
+ fonts: subfamily.fonts.filter((font, index, self) =>
82
+ index === self.findIndex(f => f._ref === font._ref)
83
+ ),
84
+ }));
85
+ }
86
+
87
+ patch.styles.subfamilies = subfamiliesArray;
88
+
89
+ // Optionally update preferred style
90
+ await updatePreferredStyle(doc_id, preferredStyleRef, newPreferredStyle, patch, client);
91
+
92
+ console.log('doc_id: ', doc_id);
93
+ console.log('Typeface patch: ', patch);
94
+ console.log('New preferred style: ', newPreferredStyle);
95
+ console.log('SubfamiliesArray:', subfamiliesArray);
96
+
97
+ try {
98
+ await client.patch(doc_id).set(patch).commit();
99
+ console.log(`Updated document: ${doc_id}`);
100
+
101
+ if (doc_id.startsWith('drafts.')) {
102
+ await updatePublishedDocument(doc_id, patch, client);
103
+ }
104
+ } catch (err) {
105
+ console.error('Error updating document:', err.message);
106
+ setStatus('Error updating typeface');
107
+ setError(true);
108
+ }
109
+ };
110
+
111
+ /**
112
+ * Conditionally sets a new preferredStyle on the patch when the candidate has a higher weight.
113
+ * @param {string} doc_id
114
+ * @param {Object} preferredStyleRef
115
+ * @param {Object} newPreferredStyle
116
+ * @param {Object} patch
117
+ * @param {Object} client
118
+ */
119
+ const updatePreferredStyle = async (doc_id, preferredStyleRef, newPreferredStyle, patch, client) => {
120
+ if (
121
+ preferredStyleRef?._ref &&
122
+ preferredStyleRef._ref !== '' &&
123
+ preferredStyleRef._ref !== null &&
124
+ newPreferredStyle._ref !== preferredStyleRef._ref
125
+ ) {
126
+ // Parameterized — doc_id comes from Sanity's useFormValue but we parameterize defensively
127
+ const prefStyleResult = await client.fetch(
128
+ `*[_id == $docId]{ preferredStyle->{ weight, style, _id } }`,
129
+ { docId: doc_id }
130
+ );
131
+ const prefStyle = prefStyleResult[0]?.preferredStyle;
132
+
133
+ if (!prefStyle?.weight || prefStyle === null || prefStyle.weight < newPreferredStyle.weight) {
134
+ patch.preferredStyle = {
135
+ _type: 'reference',
136
+ _ref: newPreferredStyle._ref,
137
+ _weak: true,
138
+ };
139
+ }
140
+ }
141
+ };
142
+
143
+ /**
144
+ * Applies the same patch to the published document if it exists.
145
+ * @param {string} doc_id - Draft document ID
146
+ * @param {Object} patch
147
+ * @param {Object} client
148
+ */
149
+ const updatePublishedDocument = async (doc_id, patch, client) => {
150
+ const publishedId = doc_id.replace('drafts.', '');
151
+ // Parameterized to prevent injection from any draft ID edge cases
152
+ const publishedDoc = await client.fetch(`*[_id == $publishedId]`, { publishedId }).then(res => res[0]);
153
+
154
+ if (publishedDoc) {
155
+ await client.patch(publishedId).set(patch).commit();
156
+ console.log(`Updated published document: ${publishedId}`);
157
+ } else {
158
+ console.log(`No published document found for ${publishedId}, skipping`);
159
+ }
160
+ };
@@ -0,0 +1,316 @@
1
+ // Core batch upload orchestrator — uploads each format to Sanity, generates CSS and metadata, then creates or updates font documents
2
+
3
+ import { nanoid } from 'nanoid';
4
+ import generateCssFile from './generateCssFile';
5
+ import generateFontData from './generateFontData';
6
+ import { parseVariableFontInstances } from './parseVariableFontInstances';
7
+
8
+ /**
9
+ * Uploads all font files to Sanity and creates/updates font documents.
10
+ * @param {Object} fontsObjects
11
+ * @param {Object} subfamilies
12
+ * @param {Object} client - Sanity client
13
+ * @param {string} inputPrice
14
+ * @param {Object} stylesObject - Existing typeface styles object
15
+ * @param {Function} setStatus
16
+ * @param {Function} setError
17
+ * @returns {Promise<Object>} fontRefs, variableRefs, failedFiles
18
+ */
19
+ export const uploadFontFiles = async (
20
+ fontsObjects,
21
+ subfamilies,
22
+ client,
23
+ inputPrice,
24
+ stylesObject,
25
+ setStatus,
26
+ setError,
27
+ ) => {
28
+ let fontRefs = [];
29
+ let variableRefs = [];
30
+ let failedFiles = [];
31
+
32
+ const fontObjectKeys = Object.keys(fontsObjects);
33
+
34
+ // Upload files for each font
35
+ for (let i = 0; i < fontObjectKeys.length; i++) {
36
+ const id = fontObjectKeys[i];
37
+ const fontObject = fontsObjects[id];
38
+ const files = fontObject.files;
39
+ const fontKit = fontObject.fontKit;
40
+ let newFileInput = fontObject.fileInput;
41
+
42
+ fontObject.subfamily = subfamilies[id] ? subfamilies[id] : '';
43
+ fontObject.price = Number(inputPrice) ? Number(inputPrice) : 0;
44
+ if (fontObject.price > 0) fontObject.sell = true;
45
+
46
+ for (let j = 0; j < files.length; j++) {
47
+ const file = files[j];
48
+ const fileType = determineFileType(file);
49
+
50
+ console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Uploading font file: ${fontObject._id}.${fileType}`);
51
+ setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Uploading font file: ${fontObject._id}.${fileType}`);
52
+
53
+ try {
54
+ const baseAsset = await client.assets.upload('file', file, { filename: fontObject._id + '.' + fileType });
55
+
56
+ newFileInput[fileType] = {
57
+ _type: 'file',
58
+ asset: { _ref: baseAsset._id, _type: 'reference' },
59
+ };
60
+
61
+ if (fileType === 'woff2') {
62
+ console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating CSS for: ${fontObject.title}`);
63
+ setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating CSS for: ${fontObject.title}`);
64
+ newFileInput = await generateCssFile({
65
+ woff2File: file,
66
+ fileInput: newFileInput,
67
+ fontName: fontObject.title,
68
+ fileName: fontObject._id,
69
+ variableFont: fontObject.variableFont,
70
+ weight: fontObject.weight,
71
+ client: client,
72
+ style: fontObject.style,
73
+ });
74
+ }
75
+
76
+ if (fileType === 'ttf') {
77
+ console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating font data for: ${fontObject.title}`);
78
+ setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating font data for: ${fontObject.title}`);
79
+ const metadata = await generateFontData({
80
+ fontId: fontObject._id,
81
+ url: baseAsset.url,
82
+ fontKit: fontKit,
83
+ client: client,
84
+ commit: false,
85
+ });
86
+ Object.assign(fontObject, metadata);
87
+ }
88
+ } catch (err) {
89
+ console.error('Error uploading font: ', fontObject.title, err.message);
90
+ setStatus('Error uploading font: ' + err.message);
91
+ setError(true);
92
+ failedFiles.push({ name: file.name, fk: fontKit });
93
+ continue;
94
+ }
95
+
96
+ fontObject.fileInput = newFileInput;
97
+ fontsObjects[id] = fontObject;
98
+ }
99
+ }
100
+
101
+ // Create or update Sanity documents
102
+ console.log('Creating/updating Sanity font documents:', fontsObjects);
103
+
104
+ for (let i = 0; i < fontObjectKeys.length; i++) {
105
+ const fontId = fontObjectKeys[i];
106
+ const font = fontsObjects[fontId];
107
+
108
+ const fontRef = await createOrUpdateFontDocument(font, client, setError);
109
+
110
+ if (fontRef) {
111
+ if (!font.variableFont) {
112
+ addToFontRefs(fontRef, font, fontRefs, stylesObject);
113
+ } else {
114
+ addToVariableRefs(fontRef, font, variableRefs, stylesObject);
115
+ }
116
+ console.log(`[${i + 1}/${fontObjectKeys.length}] ${fontRef._ref} created`);
117
+ setStatus(`[${i + 1}/${fontObjectKeys.length}] ${fontRef._ref} created`);
118
+ }
119
+ }
120
+
121
+ return { fontRefs, variableRefs, failedFiles };
122
+ };
123
+
124
+ /**
125
+ * Returns the file extension for a font file.
126
+ * @param {File} file
127
+ * @returns {string}
128
+ */
129
+ const determineFileType = (file) => {
130
+ if (file.name.endsWith('.otf')) return 'otf';
131
+ if (file.name.endsWith('.ttf')) return 'ttf';
132
+ if (file.name.endsWith('.woff')) return 'woff';
133
+ if (file.name.endsWith('.woff2')) return 'woff2';
134
+ if (file.name.endsWith('.eot')) return 'eot';
135
+ if (file.name.endsWith('.svg')) return 'svg';
136
+ return '';
137
+ };
138
+
139
+ /**
140
+ * Creates a new font document or updates an existing one, returning its reference.
141
+ * @param {Object} font
142
+ * @param {Object} client
143
+ * @param {Function} setError
144
+ * @returns {Promise<Object|null>}
145
+ */
146
+ const createOrUpdateFontDocument = async (font, client, setError) => {
147
+ try {
148
+ // Parameterized query prevents GROQ injection via font._id
149
+ const existingFont = await client.fetch(
150
+ `*[_type == 'font' && _id == $fontId]{
151
+ fileInput,
152
+ description,
153
+ metaData,
154
+ metrics,
155
+ opentypeFeatures,
156
+ characterSet,
157
+ subfamily,
158
+ scriptFileInput,
159
+ variableInstanceReferences
160
+ }`,
161
+ { fontId: font._id }
162
+ ).then(res => res[0]);
163
+
164
+ const { files, fontKit } = font;
165
+ delete font.files;
166
+ delete font.fontKit;
167
+ // Remove temp field used by preserveFileNames — must not be saved to Sanity
168
+ delete font.originalFilename;
169
+
170
+ if (font.variableFont && font.variableInstances) {
171
+ const instanceMappings = await parseVariableFontInstances(font, client);
172
+ if (instanceMappings.length > 0) {
173
+ font.variableInstanceReferences = instanceMappings;
174
+ }
175
+ }
176
+
177
+ let fontResponse;
178
+ if (existingFont) {
179
+ fontResponse = await updateExistingFont(font, existingFont, client);
180
+ } else {
181
+ fontResponse = await createNewFont(font, client);
182
+ }
183
+
184
+ return {
185
+ _key: nanoid(),
186
+ _type: 'reference',
187
+ _ref: fontResponse._id,
188
+ _weak: true,
189
+ };
190
+ } catch (e) {
191
+ console.error('Error creating font: ', font.title, font.subfamily, e);
192
+ setError(true);
193
+ return null;
194
+ }
195
+ };
196
+
197
+ /**
198
+ * Patches an existing font document, merging file references and preserving stored metadata.
199
+ * @param {Object} font
200
+ * @param {Object} existingFont
201
+ * @param {Object} client
202
+ * @returns {Promise<Object>}
203
+ */
204
+ const updateExistingFont = async (font, existingFont, client) => {
205
+ if (existingFont.fileInput) {
206
+ const newFileInput = { ...font.fileInput };
207
+ Object.keys(existingFont.fileInput).forEach(key => {
208
+ if (!newFileInput[key]) newFileInput[key] = existingFont.fileInput[key];
209
+ });
210
+ font.fileInput = newFileInput;
211
+ }
212
+
213
+ font.metaData = !font?.metaData || Object.keys(font?.metaData || {}).length === 0
214
+ ? existingFont?.metaData || {}
215
+ : font.metaData;
216
+ font.metrics = !font?.metrics || Object.keys(font?.metrics || {}).length === 0
217
+ ? existingFont?.metrics || {}
218
+ : font.metrics;
219
+ font.opentypeFeatures = !font?.opentypeFeatures || Object.keys(font?.opentypeFeatures || {}).length === 0
220
+ ? existingFont?.opentypeFeatures || {}
221
+ : font.opentypeFeatures;
222
+ font.characterSet = !font?.characterSet || Object.keys(font?.characterSet || {}).length === 0
223
+ ? existingFont?.characterSet || {}
224
+ : font.characterSet;
225
+ font.scriptFileInput = existingFont?.scriptFileInput || {};
226
+
227
+ cleanMetadataValues(font);
228
+
229
+ if (font.variableFont && existingFont?.variableInstanceReferences &&
230
+ (!font.variableInstanceReferences || font.variableInstanceReferences.length === 0)) {
231
+ font.variableInstanceReferences = existingFont.variableInstanceReferences;
232
+ }
233
+
234
+ console.log('Updating existing font: ', font._id, font.title);
235
+
236
+ const patchObject = {
237
+ fileInput: font.fileInput,
238
+ subfamily: font.subfamily,
239
+ weight: font.weight,
240
+ };
241
+
242
+ if (font.variableInstanceReferences) {
243
+ patchObject.variableInstanceReferences = font.variableInstanceReferences;
244
+ }
245
+
246
+ return await client.patch(font._id).set(patchObject).commit();
247
+ };
248
+
249
+ /**
250
+ * Creates a new font document in Sanity.
251
+ * @param {Object} font
252
+ * @param {Object} client
253
+ * @returns {Promise<Object>}
254
+ */
255
+ const createNewFont = async (font, client) => {
256
+ console.log('Creating new font: ', font._id, font.title);
257
+ if (font.metaData) cleanMetadataValues(font);
258
+
259
+ const newDocument = {
260
+ _key: nanoid(),
261
+ _id: font._id,
262
+ _type: 'font',
263
+ ...font,
264
+ };
265
+
266
+ return await client.createOrReplace(newDocument);
267
+ };
268
+
269
+ /**
270
+ * Removes null metadata values and strips control characters from string values.
271
+ * @param {Object} font
272
+ */
273
+ const cleanMetadataValues = (font) => {
274
+ if (!font.metaData) return;
275
+ Object.keys(font.metaData).forEach(key => {
276
+ if (font.metaData[key] == null) {
277
+ font.metaData[key] = '';
278
+ } else {
279
+ font.metaData[key] = font.metaData[key].replace(/[-]/g, '');
280
+ }
281
+ });
282
+ };
283
+
284
+ /**
285
+ * Adds a font reference to fontRefs if it's not already in the existing styles.
286
+ * @param {Object} fontRef
287
+ * @param {Object} font
288
+ * @param {Array} fontRefs
289
+ * @param {Object} stylesObject
290
+ */
291
+ const addToFontRefs = (fontRef, font, fontRefs, stylesObject) => {
292
+ if (stylesObject.fonts && stylesObject.fonts.length > 0) {
293
+ const fontExists = stylesObject.fonts.findIndex(f => f._ref === fontRef._ref);
294
+ const inFontRefs = fontRefs.findIndex(f => f._ref === fontRef._ref);
295
+ if (fontExists === -1 && inFontRefs === -1) fontRefs.push(fontRef);
296
+ } else {
297
+ fontRefs.push(fontRef);
298
+ }
299
+ };
300
+
301
+ /**
302
+ * Adds a variable font reference to variableRefs if it's not already in the existing styles.
303
+ * @param {Object} fontRef
304
+ * @param {Object} font
305
+ * @param {Array} variableRefs
306
+ * @param {Object} stylesObject
307
+ */
308
+ const addToVariableRefs = (fontRef, font, variableRefs, stylesObject) => {
309
+ if (stylesObject?.variableFont?.length) {
310
+ const vfExists = stylesObject.variableFont.findIndex(f => f._ref === fontRef._ref);
311
+ const inVariableRefs = variableRefs.findIndex(f => f._ref === fontRef._ref);
312
+ if (vfExists === -1 && inVariableRefs === -1 && font.variableFont) variableRefs.push(fontRef);
313
+ } else {
314
+ variableRefs.push(fontRef);
315
+ }
316
+ };
@@ -0,0 +1,16 @@
1
+ // Script variant constants (SCRIPTS, SCRIPTS_OBJECT) and HtmlDescription component for the supported script list
2
+
3
+ import React from 'react'
4
+
5
+ /** Renders children as-is; used for rich-text fields that return HTML strings */
6
+ export const HtmlDescription = ({ children }) => {
7
+ return children || ''
8
+ }
9
+
10
+ /** Script variants available for this studio instance — comma-separated SANITY_STUDIO_SCRIPTS env var */
11
+ export const SCRIPTS = (process.env.SANITY_STUDIO_SCRIPTS || '').split(',').map((script) => script.trim()).filter(Boolean);
12
+
13
+ /** SCRIPTS as Sanity select option objects */
14
+ export const SCRIPTS_OBJECT = SCRIPTS.map((script) => {
15
+ return {title: script[0].toUpperCase() + script.slice(1), value: script}
16
+ });