@pro-laico/payload-fonts 0.0.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 (88) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +19 -0
  3. package/dist/access/authd.d.ts +19 -0
  4. package/dist/access/authd.d.ts.map +1 -0
  5. package/dist/access/authd.js +18 -0
  6. package/dist/access/authd.js.map +1 -0
  7. package/dist/collections/font.d.ts +29 -0
  8. package/dist/collections/font.d.ts.map +1 -0
  9. package/dist/collections/font.js +327 -0
  10. package/dist/collections/font.js.map +1 -0
  11. package/dist/collections/fontOptimized.d.ts +20 -0
  12. package/dist/collections/fontOptimized.d.ts.map +1 -0
  13. package/dist/collections/fontOptimized.js +111 -0
  14. package/dist/collections/fontOptimized.js.map +1 -0
  15. package/dist/collections/fontOriginal.d.ts +25 -0
  16. package/dist/collections/fontOriginal.d.ts.map +1 -0
  17. package/dist/collections/fontOriginal.js +49 -0
  18. package/dist/collections/fontOriginal.js.map +1 -0
  19. package/dist/components/DevFonts.d.ts +52 -0
  20. package/dist/components/DevFonts.d.ts.map +1 -0
  21. package/dist/components/DevFonts.js +75 -0
  22. package/dist/components/DevFonts.js.map +1 -0
  23. package/dist/components/admin/FontOriginalUpload.d.ts +15 -0
  24. package/dist/components/admin/FontOriginalUpload.d.ts.map +1 -0
  25. package/dist/components/admin/FontOriginalUpload.js +22 -0
  26. package/dist/components/admin/FontOriginalUpload.js.map +1 -0
  27. package/dist/components/admin/FontOriginalUpload.scss +9 -0
  28. package/dist/endpoints/exportFonts.d.ts +49 -0
  29. package/dist/endpoints/exportFonts.d.ts.map +1 -0
  30. package/dist/endpoints/exportFonts.js +162 -0
  31. package/dist/endpoints/exportFonts.js.map +1 -0
  32. package/dist/extractFonts.d.ts +20 -0
  33. package/dist/extractFonts.d.ts.map +1 -0
  34. package/dist/extractFonts.js +18 -0
  35. package/dist/extractFonts.js.map +1 -0
  36. package/dist/fields/font.d.ts +14 -0
  37. package/dist/fields/font.d.ts.map +1 -0
  38. package/dist/fields/font.js +37 -0
  39. package/dist/fields/font.js.map +1 -0
  40. package/dist/globals/fontSet.d.ts +15 -0
  41. package/dist/globals/fontSet.d.ts.map +1 -0
  42. package/dist/globals/fontSet.js +25 -0
  43. package/dist/globals/fontSet.js.map +1 -0
  44. package/dist/hooks/optimizeFont.d.ts +56 -0
  45. package/dist/hooks/optimizeFont.d.ts.map +1 -0
  46. package/dist/hooks/optimizeFont.js +185 -0
  47. package/dist/hooks/optimizeFont.js.map +1 -0
  48. package/dist/hooks/optimizeFromOriginals.d.ts +43 -0
  49. package/dist/hooks/optimizeFromOriginals.d.ts.map +1 -0
  50. package/dist/hooks/optimizeFromOriginals.js +313 -0
  51. package/dist/hooks/optimizeFromOriginals.js.map +1 -0
  52. package/dist/index.d.ts +8 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +14 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/lib/activeFonts.d.ts +64 -0
  57. package/dist/lib/activeFonts.d.ts.map +1 -0
  58. package/dist/lib/activeFonts.js +139 -0
  59. package/dist/lib/activeFonts.js.map +1 -0
  60. package/dist/lib/families.d.ts +38 -0
  61. package/dist/lib/families.d.ts.map +1 -0
  62. package/dist/lib/families.js +44 -0
  63. package/dist/lib/families.js.map +1 -0
  64. package/dist/lib/mergeConfig.d.ts +28 -0
  65. package/dist/lib/mergeConfig.d.ts.map +1 -0
  66. package/dist/lib/mergeConfig.js +73 -0
  67. package/dist/lib/mergeConfig.js.map +1 -0
  68. package/dist/lib/refs.d.ts +8 -0
  69. package/dist/lib/refs.d.ts.map +1 -0
  70. package/dist/lib/refs.js +11 -0
  71. package/dist/lib/refs.js.map +1 -0
  72. package/dist/lib/uploadBytes.d.ts +26 -0
  73. package/dist/lib/uploadBytes.d.ts.map +1 -0
  74. package/dist/lib/uploadBytes.js +98 -0
  75. package/dist/lib/uploadBytes.js.map +1 -0
  76. package/dist/plugin.d.ts +57 -0
  77. package/dist/plugin.d.ts.map +1 -0
  78. package/dist/plugin.js +93 -0
  79. package/dist/plugin.js.map +1 -0
  80. package/dist/scripts/cli.d.ts +3 -0
  81. package/dist/scripts/cli.d.ts.map +1 -0
  82. package/dist/scripts/cli.js +13 -0
  83. package/dist/scripts/cli.js.map +1 -0
  84. package/dist/scripts/downloadFonts.d.ts +37 -0
  85. package/dist/scripts/downloadFonts.d.ts.map +1 -0
  86. package/dist/scripts/downloadFonts.js +216 -0
  87. package/dist/scripts/downloadFonts.js.map +1 -0
  88. package/package.json +98 -0
package/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Pro Laico LLC <contact@prolaico.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @pro-laico/payload-fonts
2
+
3
+ Custom fonts for [Payload CMS](https://payloadcms.com/) — a Font typeface collection that subsets uploaded files to served WOFF2s, an optional active-font global, and an export endpoint for `next/font/local`.
4
+
5
+ ```bash
6
+ pnpm add @pro-laico/payload-fonts
7
+ ```
8
+
9
+ **[Documentation →](https://payload-plugins.prolaico.com/docs/plugins/payload-fonts)**
10
+
11
+ ## Troubleshooting
12
+
13
+ **Fonts upload but nothing is served ("the font subsetter failed to load")** — a bundler (Next/Turbopack) inlined the subsetter's wasm/native assets, so `subset-font` can't load at runtime. Keep them external in `next.config`:
14
+
15
+ ```ts
16
+ serverExternalPackages: ['subset-font', 'harfbuzzjs', 'fontkit']
17
+ ```
18
+
19
+ Ported from [`@pro-laico/fonts`](https://github.com/pro-laico/atomic-payload/tree/main/packages/fonts) (Atomic Payload, MIT).
@@ -0,0 +1,19 @@
1
+ import type { Access } from 'payload';
2
+ /**
3
+ * Default access for the `font` collection and `fontSet` global: any authenticated
4
+ * user. Reads are gated too because font binaries may be licensed.
5
+ *
6
+ * This is intentionally permissive — `@pro-laico/payload-fonts` ships no role model of
7
+ * its own. Projects that need stricter control (e.g. admin-only writes) should override
8
+ * `access` at registration time rather than editing this package:
9
+ *
10
+ * ```ts
11
+ * fontsPlugin({
12
+ * fontOverrides: { access: { create: adminOnly, update: adminOnly, delete: adminOnly } },
13
+ * includeFontSet: true,
14
+ * fontSetOverrides: { access: { update: adminOnly } },
15
+ * })
16
+ * ```
17
+ */
18
+ export declare const authd: Access;
19
+ //# sourceMappingURL=authd.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authd.d.ts","sourceRoot":"","sources":["../../src/access/authd.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAErC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,KAAK,EAAE,MAAuC,CAAA"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Default access for the `font` collection and `fontSet` global: any authenticated
3
+ * user. Reads are gated too because font binaries may be licensed.
4
+ *
5
+ * This is intentionally permissive — `@pro-laico/payload-fonts` ships no role model of
6
+ * its own. Projects that need stricter control (e.g. admin-only writes) should override
7
+ * `access` at registration time rather than editing this package:
8
+ *
9
+ * ```ts
10
+ * fontsPlugin({
11
+ * fontOverrides: { access: { create: adminOnly, update: adminOnly, delete: adminOnly } },
12
+ * includeFontSet: true,
13
+ * fontSetOverrides: { access: { update: adminOnly } },
14
+ * })
15
+ * ```
16
+ */ export const authd = ({ req })=>Boolean(req.user);
17
+
18
+ //# sourceMappingURL=authd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/access/authd.ts"],"sourcesContent":["import type { Access } from 'payload'\n\n/**\n * Default access for the `font` collection and `fontSet` global: any authenticated\n * user. Reads are gated too because font binaries may be licensed.\n *\n * This is intentionally permissive — `@pro-laico/payload-fonts` ships no role model of\n * its own. Projects that need stricter control (e.g. admin-only writes) should override\n * `access` at registration time rather than editing this package:\n *\n * ```ts\n * fontsPlugin({\n * fontOverrides: { access: { create: adminOnly, update: adminOnly, delete: adminOnly } },\n * includeFontSet: true,\n * fontSetOverrides: { access: { update: adminOnly } },\n * })\n * ```\n */\nexport const authd: Access = ({ req }) => Boolean(req.user)\n"],"names":["authd","req","Boolean","user"],"mappings":"AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMA,QAAgB,CAAC,EAAEC,GAAG,EAAE,GAAKC,QAAQD,IAAIE,IAAI,EAAC"}
@@ -0,0 +1,29 @@
1
+ import { type CollectionConfig } from 'payload';
2
+ import type { Charset } from '../hooks/optimizeFont';
3
+ import { type FontFamilyConfig } from '../lib/families';
4
+ /** Admin component subpath (Payload import map) for the create-only original uploader. */
5
+ export declare const FontOriginalUploadPath = "@pro-laico/payload-fonts/admin/FontOriginalUpload";
6
+ export interface CreateFontCollectionOptions {
7
+ /** Characters the subsetter keeps, or a preset ('latin' | 'latin-ext'). Default 'latin'. */
8
+ charset?: Charset;
9
+ /** Slug of the archival original upload collection. Default 'fontOriginal'. */
10
+ originalSlug?: string;
11
+ /** Slug of the optimized (served) upload collection. Default 'fontOptimized'. */
12
+ optimizedSlug?: string;
13
+ /** The options offered by the `family` field. Default sans/serif/mono/display. */
14
+ families?: FontFamilyConfig[];
15
+ }
16
+ /**
17
+ * The `Font` collection — ONE document per **typeface** (e.g. "Inter"). The four family slots
18
+ * (sans/serif/mono/display) select one of these each.
19
+ *
20
+ * It is NOT itself an upload collection. Editors drop font files into standard Payload `upload`
21
+ * fields backed by `fontOriginal` (raw archive) — either the `variable` group (a single
22
+ * variable file per upright/italic) OR the `weights` array (one file per weight/style). On save,
23
+ * {@link optimizeFromOriginalsHook} subsets each referenced original to a served
24
+ * `fontOptimized` WOFF2. The slots are Payload's own upload field, thinly wrapped to be
25
+ * create-only (no "Choose from existing"), so each original belongs to exactly one typeface —
26
+ * keeping asset cleanup safe and race-free.
27
+ */
28
+ export declare const createFontCollection: (opts?: CreateFontCollectionOptions) => CollectionConfig;
29
+ //# sourceMappingURL=font.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"font.d.ts","sourceRoot":"","sources":["../../src/collections/font.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,gBAAgB,EAGtB,MAAM,SAAS,CAAA;AAIhB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,KAAK,gBAAgB,EAAuB,MAAM,iBAAiB,CAAA;AAO5E,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,sDAAsD,CAAA;AASzF,MAAM,WAAW,2BAA2B;IAC1C,4FAA4F;IAC5F,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAC9B;AA2FD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAM,2BAAgC,KAAG,gBAgH7E,CAAA"}
@@ -0,0 +1,327 @@
1
+ import { APIError } from "payload";
2
+ import { authd } from "../access/authd.js";
3
+ import { cleanupFontAssetsHook, optimizeFromOriginalsHook, originalIdsFromDoc } from "../hooks/optimizeFromOriginals.js";
4
+ import { resolveFontFamilies } from "../lib/families.js";
5
+ import { FONT_OPTIMIZED_SLUG } from "./fontOptimized.js";
6
+ import { FONT_ORIGINAL_SLUG } from "./fontOriginal.js";
7
+ /** Standard `next/font/local` weight steps offered by each weight row. */ const WEIGHT_OPTIONS = [
8
+ '100',
9
+ '200',
10
+ '300',
11
+ '400',
12
+ '500',
13
+ '600',
14
+ '700',
15
+ '800',
16
+ '900'
17
+ ];
18
+ /** Admin component subpath (Payload import map) for the create-only original uploader. */ export const FontOriginalUploadPath = '@pro-laico/payload-fonts/admin/FontOriginalUpload';
19
+ /**
20
+ * A FRESH admin config that renders the `fontOriginal` slot create-only (no "Choose from
21
+ * existing"), so every slot uploads a fresh original — never shared between typefaces. A
22
+ * factory (not a shared object) because Payload mutates field configs in place during
23
+ * sanitization; sharing one reference across slots corrupts it.
24
+ */ const createOnlyUpload = ()=>({
25
+ components: {
26
+ Field: {
27
+ path: FontOriginalUploadPath
28
+ }
29
+ }
30
+ });
31
+ const hasVariable = (data)=>{
32
+ const v = data?.variable ?? {};
33
+ return Boolean(v.upright || v.italic);
34
+ };
35
+ const hasWeights = (data)=>Array.isArray(data?.weights) && data.weights.some((w)=>w?.file);
36
+ /**
37
+ * `afterRead`: report how many served `fontOptimized` files this typeface produced, so an editor can
38
+ * see at a glance whether optimization succeeded — `0` means nothing was served (a swap/re-save is
39
+ * due, or the upload failed to subset). Skipped on list reads (`findMany`) so it's one count query
40
+ * on the edit view, not one per row. Populates the virtual `servedFiles` sidebar field.
41
+ */ const servedFilesHook = (optimizedSlug)=>async ({ doc, findMany, req })=>{
42
+ if (findMany || !req?.payload) return doc;
43
+ const id = doc.id;
44
+ if (id == null) return doc;
45
+ try {
46
+ const { totalDocs } = await req.payload.count({
47
+ collection: optimizedSlug,
48
+ where: {
49
+ font: {
50
+ equals: id
51
+ }
52
+ },
53
+ overrideAccess: true,
54
+ req
55
+ });
56
+ doc.servedFiles = totalDocs;
57
+ } catch {
58
+ // a count failure shouldn't break the read — just leave it unset
59
+ }
60
+ return doc;
61
+ };
62
+ /**
63
+ * `beforeValidate`: a typeface needs at least one file, and can't mix a variable font with
64
+ * specific weights (you compose from one or the other). Runs on create and on any update that
65
+ * touches these fields, so an unrelated partial edit is left alone.
66
+ */ const requireFontFiles = ({ data, operation })=>{
67
+ const touches = operation === 'create' || data != null && ('variable' in data || 'weights' in data);
68
+ if (!touches) return data;
69
+ if (hasVariable(data) && hasWeights(data)) {
70
+ throw new APIError('Use either a variable font or specific weight files, not both.', 400, null, true);
71
+ }
72
+ if (!hasVariable(data) && !hasWeights(data)) {
73
+ throw new APIError('Add at least one font file before saving.', 400, null, true);
74
+ }
75
+ return data;
76
+ };
77
+ /**
78
+ * `beforeValidate`: enforce one `fontOriginal` per typeface — reject a save that references an
79
+ * original already used by ANOTHER typeface. The create-only upload slots make sharing
80
+ * impossible from the admin UI, but this is the data-layer guarantee (covers the REST API,
81
+ * imports, seeds, and a future Payload upgrade that might un-hide "Choose from existing"). It's
82
+ * what makes the direct asset cleanup in {@link cleanupFontAssetsHook} /
83
+ * {@link optimizeFromOriginalsHook} safe: a de-referenced or deleted original is never still in
84
+ * use elsewhere.
85
+ */ const makeRejectSharedOriginals = (fontSlug)=>async ({ data, originalDoc, req })=>{
86
+ if (!data || !req?.payload) return data;
87
+ const ids = originalIdsFromDoc(data);
88
+ if (ids.length === 0) return data;
89
+ const selfId = originalDoc?.id ?? data.id;
90
+ const refs = [
91
+ {
92
+ 'variable.upright': {
93
+ in: ids
94
+ }
95
+ },
96
+ {
97
+ 'variable.italic': {
98
+ in: ids
99
+ }
100
+ },
101
+ {
102
+ 'weights.file': {
103
+ in: ids
104
+ }
105
+ }
106
+ ];
107
+ const where = selfId != null ? {
108
+ and: [
109
+ {
110
+ id: {
111
+ not_equals: selfId
112
+ }
113
+ },
114
+ {
115
+ or: refs
116
+ }
117
+ ]
118
+ } : {
119
+ or: refs
120
+ };
121
+ const res = await req.payload.find({
122
+ collection: fontSlug,
123
+ where: where,
124
+ depth: 0,
125
+ limit: 1,
126
+ overrideAccess: true,
127
+ req
128
+ });
129
+ if (res.totalDocs > 0) {
130
+ const other = res.docs[0].title || 'another typeface';
131
+ throw new APIError(`That font file is already used by ${other}. Each typeface needs its own upload — add a fresh copy for this slot.`, 400, null, true);
132
+ }
133
+ return data;
134
+ };
135
+ /**
136
+ * The `Font` collection — ONE document per **typeface** (e.g. "Inter"). The four family slots
137
+ * (sans/serif/mono/display) select one of these each.
138
+ *
139
+ * It is NOT itself an upload collection. Editors drop font files into standard Payload `upload`
140
+ * fields backed by `fontOriginal` (raw archive) — either the `variable` group (a single
141
+ * variable file per upright/italic) OR the `weights` array (one file per weight/style). On save,
142
+ * {@link optimizeFromOriginalsHook} subsets each referenced original to a served
143
+ * `fontOptimized` WOFF2. The slots are Payload's own upload field, thinly wrapped to be
144
+ * create-only (no "Choose from existing"), so each original belongs to exactly one typeface —
145
+ * keeping asset cleanup safe and race-free.
146
+ */ export const createFontCollection = (opts = {})=>{
147
+ const fontSlug = 'font';
148
+ const originalSlug = opts.originalSlug || FONT_ORIGINAL_SLUG;
149
+ const optimizedSlug = opts.optimizedSlug || FONT_OPTIMIZED_SLUG;
150
+ const families = resolveFontFamilies(opts.families);
151
+ const fields = [
152
+ {
153
+ name: 'title',
154
+ type: 'text',
155
+ required: true,
156
+ label: 'Typeface name'
157
+ },
158
+ {
159
+ name: 'family',
160
+ type: 'radio',
161
+ required: true,
162
+ label: 'Preferred Family',
163
+ interfaceName: 'GenericFontFamily',
164
+ options: families.map((r)=>({
165
+ label: r.label,
166
+ value: r.key
167
+ }))
168
+ },
169
+ // Editor-facing status: how many web-ready files this typeface produced. Virtual (computed on
170
+ // read by `servedFilesHook`, never stored); shown only on an existing doc so a `0` after saving
171
+ // flags a failed/empty optimization instead of it failing silently.
172
+ {
173
+ name: 'servedFiles',
174
+ type: 'number',
175
+ virtual: true,
176
+ admin: {
177
+ readOnly: true,
178
+ position: 'sidebar',
179
+ description: 'Web-ready files generated from your uploads. 0 means nothing was served yet — re-save; if it stays 0, the upload may have failed to optimize (check server logs).',
180
+ condition: (data)=>Boolean(data?.id)
181
+ }
182
+ },
183
+ {
184
+ name: 'variable',
185
+ type: 'group',
186
+ label: 'Variable font',
187
+ admin: {
188
+ description: 'One file covering many weights. Use this OR specific weights below — not both.',
189
+ // Hidden once specific weights are added; you compose from one path or the other.
190
+ condition: (data)=>!hasWeights(data)
191
+ },
192
+ fields: [
193
+ {
194
+ type: 'row',
195
+ fields: [
196
+ {
197
+ name: 'upright',
198
+ type: 'upload',
199
+ relationTo: originalSlug,
200
+ label: 'Upright (normal)',
201
+ admin: createOnlyUpload()
202
+ },
203
+ {
204
+ name: 'italic',
205
+ type: 'upload',
206
+ relationTo: originalSlug,
207
+ label: 'Italic',
208
+ admin: createOnlyUpload()
209
+ }
210
+ ]
211
+ }
212
+ ]
213
+ },
214
+ {
215
+ name: 'weights',
216
+ type: 'array',
217
+ label: 'Weights',
218
+ labels: {
219
+ singular: 'Weight file',
220
+ plural: 'Weight files'
221
+ },
222
+ admin: {
223
+ description: 'One file per weight/style. Add only the weights you need.',
224
+ condition: (data)=>!hasVariable(data)
225
+ },
226
+ // Each weight + style must be unique so the generated next/font src array has no conflicts.
227
+ validate: (rows)=>{
228
+ const seen = new Set();
229
+ for (const r of Array.isArray(rows) ? rows : []){
230
+ const key = `${r?.weight ?? ''}|${r?.style ?? ''}`;
231
+ if (seen.has(key)) return `Two files share weight ${r?.weight ?? '?'} ${r?.style ?? 'normal'}. Each weight + style must be unique.`;
232
+ seen.add(key);
233
+ }
234
+ return true;
235
+ },
236
+ fields: [
237
+ {
238
+ type: 'row',
239
+ fields: [
240
+ {
241
+ name: 'weight',
242
+ type: 'select',
243
+ required: true,
244
+ defaultValue: '400',
245
+ options: WEIGHT_OPTIONS
246
+ },
247
+ {
248
+ name: 'style',
249
+ type: 'radio',
250
+ required: true,
251
+ defaultValue: 'normal',
252
+ options: [
253
+ 'normal',
254
+ 'italic'
255
+ ]
256
+ }
257
+ ]
258
+ },
259
+ {
260
+ name: 'file',
261
+ type: 'upload',
262
+ relationTo: originalSlug,
263
+ required: true,
264
+ admin: createOnlyUpload()
265
+ }
266
+ ]
267
+ }
268
+ ];
269
+ return {
270
+ slug: fontSlug,
271
+ access: {
272
+ create: authd,
273
+ delete: authd,
274
+ read: authd,
275
+ update: authd
276
+ },
277
+ admin: {
278
+ group: 'Assets',
279
+ useAsTitle: 'title',
280
+ enableListViewSelectAPI: true,
281
+ defaultColumns: [
282
+ 'title',
283
+ 'family'
284
+ ],
285
+ description: 'Upload typefaces here to add them to your library. Uploading alone doesn’t put a font on your site — activate it by picking it in Font Set.'
286
+ },
287
+ timestamps: true,
288
+ // When a font is populated as a relationship target (e.g. the `fontSet` global at depth),
289
+ // return only its identifying metadata and NOT the `variable` / `weights` upload slots — which
290
+ // would otherwise drag the private `fontOriginal` blobs through every populated row.
291
+ defaultPopulate: {
292
+ title: true,
293
+ family: true
294
+ },
295
+ fields,
296
+ hooks: {
297
+ // Guard the referenced originals: at least one file, and no original shared across typefaces.
298
+ beforeValidate: [
299
+ requireFontFiles,
300
+ makeRejectSharedOriginals(fontSlug)
301
+ ],
302
+ // On save, (re)build the served WOFF2 files from the referenced originals (and clean up
303
+ // any original a swapped/removed slot de-referenced).
304
+ afterChange: [
305
+ optimizeFromOriginalsHook({
306
+ charset: opts.charset,
307
+ originalSlug,
308
+ optimizedSlug
309
+ })
310
+ ],
311
+ // On read (edit view only), surface the served-file count so a failed optimize isn't silent.
312
+ afterRead: [
313
+ servedFilesHook(optimizedSlug)
314
+ ],
315
+ // Before delete, cascade to the served files + the archived originals (beforeDelete so the
316
+ // `fontOptimized.font` relationship is still intact — see the hook).
317
+ beforeDelete: [
318
+ cleanupFontAssetsHook({
319
+ originalSlug,
320
+ optimizedSlug
321
+ })
322
+ ]
323
+ }
324
+ };
325
+ };
326
+
327
+ //# sourceMappingURL=font.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/collections/font.ts"],"sourcesContent":["import {\n APIError,\n type ArrayField,\n type CollectionAfterReadHook,\n type CollectionBeforeValidateHook,\n type CollectionConfig,\n type CollectionSlug,\n type Field,\n} from 'payload'\n\nimport { authd } from '../access/authd'\nimport { cleanupFontAssetsHook, optimizeFromOriginalsHook, originalIdsFromDoc } from '../hooks/optimizeFromOriginals'\nimport type { Charset } from '../hooks/optimizeFont'\nimport { type FontFamilyConfig, resolveFontFamilies } from '../lib/families'\nimport { FONT_OPTIMIZED_SLUG } from './fontOptimized'\nimport { FONT_ORIGINAL_SLUG } from './fontOriginal'\n\n/** Standard `next/font/local` weight steps offered by each weight row. */\nconst WEIGHT_OPTIONS = ['100', '200', '300', '400', '500', '600', '700', '800', '900']\n\n/** Admin component subpath (Payload import map) for the create-only original uploader. */\nexport const FontOriginalUploadPath = '@pro-laico/payload-fonts/admin/FontOriginalUpload'\n/**\n * A FRESH admin config that renders the `fontOriginal` slot create-only (no \"Choose from\n * existing\"), so every slot uploads a fresh original — never shared between typefaces. A\n * factory (not a shared object) because Payload mutates field configs in place during\n * sanitization; sharing one reference across slots corrupts it.\n */\nconst createOnlyUpload = () => ({ components: { Field: { path: FontOriginalUploadPath } } })\n\nexport interface CreateFontCollectionOptions {\n /** Characters the subsetter keeps, or a preset ('latin' | 'latin-ext'). Default 'latin'. */\n charset?: Charset\n /** Slug of the archival original upload collection. Default 'fontOriginal'. */\n originalSlug?: string\n /** Slug of the optimized (served) upload collection. Default 'fontOptimized'. */\n optimizedSlug?: string\n /** The options offered by the `family` field. Default sans/serif/mono/display. */\n families?: FontFamilyConfig[]\n}\n\ntype VariableGroup = { upright?: unknown; italic?: unknown }\nconst hasVariable = (data: Record<string, unknown> | undefined): boolean => {\n const v = (data?.variable ?? {}) as VariableGroup\n return Boolean(v.upright || v.italic)\n}\nconst hasWeights = (data: Record<string, unknown> | undefined): boolean =>\n Array.isArray(data?.weights) && (data.weights as Array<{ file?: unknown }>).some((w) => w?.file)\n\n/**\n * `afterRead`: report how many served `fontOptimized` files this typeface produced, so an editor can\n * see at a glance whether optimization succeeded — `0` means nothing was served (a swap/re-save is\n * due, or the upload failed to subset). Skipped on list reads (`findMany`) so it's one count query\n * on the edit view, not one per row. Populates the virtual `servedFiles` sidebar field.\n */\nconst servedFilesHook =\n (optimizedSlug: string): CollectionAfterReadHook =>\n async ({ doc, findMany, req }) => {\n if (findMany || !req?.payload) return doc\n const id = (doc as { id?: string | number }).id\n if (id == null) return doc\n try {\n const { totalDocs } = await req.payload.count({\n collection: optimizedSlug as CollectionSlug,\n where: { font: { equals: id } },\n overrideAccess: true,\n req,\n })\n ;(doc as Record<string, unknown>).servedFiles = totalDocs\n } catch {\n // a count failure shouldn't break the read — just leave it unset\n }\n return doc\n }\n\n/**\n * `beforeValidate`: a typeface needs at least one file, and can't mix a variable font with\n * specific weights (you compose from one or the other). Runs on create and on any update that\n * touches these fields, so an unrelated partial edit is left alone.\n */\nconst requireFontFiles: CollectionBeforeValidateHook = ({ data, operation }) => {\n const touches = operation === 'create' || (data != null && ('variable' in data || 'weights' in data))\n if (!touches) return data\n if (hasVariable(data) && hasWeights(data)) {\n throw new APIError('Use either a variable font or specific weight files, not both.', 400, null, true)\n }\n if (!hasVariable(data) && !hasWeights(data)) {\n throw new APIError('Add at least one font file before saving.', 400, null, true)\n }\n return data\n}\n\n/**\n * `beforeValidate`: enforce one `fontOriginal` per typeface — reject a save that references an\n * original already used by ANOTHER typeface. The create-only upload slots make sharing\n * impossible from the admin UI, but this is the data-layer guarantee (covers the REST API,\n * imports, seeds, and a future Payload upgrade that might un-hide \"Choose from existing\"). It's\n * what makes the direct asset cleanup in {@link cleanupFontAssetsHook} /\n * {@link optimizeFromOriginalsHook} safe: a de-referenced or deleted original is never still in\n * use elsewhere.\n */\nconst makeRejectSharedOriginals =\n (fontSlug: string): CollectionBeforeValidateHook =>\n async ({ data, originalDoc, req }) => {\n if (!data || !req?.payload) return data\n const ids = originalIdsFromDoc(data as Record<string, unknown>)\n if (ids.length === 0) return data\n const selfId = (originalDoc as { id?: string | number } | undefined)?.id ?? (data as { id?: string | number }).id\n const refs = [{ 'variable.upright': { in: ids } }, { 'variable.italic': { in: ids } }, { 'weights.file': { in: ids } }]\n const where = selfId != null ? { and: [{ id: { not_equals: selfId } }, { or: refs }] } : { or: refs }\n const res = await req.payload.find({\n collection: fontSlug as CollectionSlug,\n where: where as never,\n depth: 0,\n limit: 1,\n overrideAccess: true,\n req,\n })\n if (res.totalDocs > 0) {\n const other = (res.docs[0] as { title?: string }).title || 'another typeface'\n throw new APIError(\n `That font file is already used by ${other}. Each typeface needs its own upload — add a fresh copy for this slot.`,\n 400,\n null,\n true,\n )\n }\n return data\n }\n\n/**\n * The `Font` collection — ONE document per **typeface** (e.g. \"Inter\"). The four family slots\n * (sans/serif/mono/display) select one of these each.\n *\n * It is NOT itself an upload collection. Editors drop font files into standard Payload `upload`\n * fields backed by `fontOriginal` (raw archive) — either the `variable` group (a single\n * variable file per upright/italic) OR the `weights` array (one file per weight/style). On save,\n * {@link optimizeFromOriginalsHook} subsets each referenced original to a served\n * `fontOptimized` WOFF2. The slots are Payload's own upload field, thinly wrapped to be\n * create-only (no \"Choose from existing\"), so each original belongs to exactly one typeface —\n * keeping asset cleanup safe and race-free.\n */\nexport const createFontCollection = (opts: CreateFontCollectionOptions = {}): CollectionConfig => {\n const fontSlug = 'font'\n const originalSlug = (opts.originalSlug || FONT_ORIGINAL_SLUG) as CollectionSlug\n const optimizedSlug = opts.optimizedSlug || FONT_OPTIMIZED_SLUG\n const families = resolveFontFamilies(opts.families)\n\n const fields: Field[] = [\n { name: 'title', type: 'text', required: true, label: 'Typeface name' },\n {\n name: 'family',\n type: 'radio',\n required: true,\n label: 'Preferred Family',\n interfaceName: 'GenericFontFamily',\n options: families.map((r) => ({ label: r.label, value: r.key })),\n },\n // Editor-facing status: how many web-ready files this typeface produced. Virtual (computed on\n // read by `servedFilesHook`, never stored); shown only on an existing doc so a `0` after saving\n // flags a failed/empty optimization instead of it failing silently.\n {\n name: 'servedFiles',\n type: 'number',\n virtual: true,\n admin: {\n readOnly: true,\n position: 'sidebar',\n description:\n 'Web-ready files generated from your uploads. 0 means nothing was served yet — re-save; if it stays 0, the upload may have failed to optimize (check server logs).',\n condition: (data) => Boolean((data as { id?: unknown })?.id),\n },\n },\n {\n name: 'variable',\n type: 'group',\n label: 'Variable font',\n admin: {\n description: 'One file covering many weights. Use this OR specific weights below — not both.',\n // Hidden once specific weights are added; you compose from one path or the other.\n condition: (data) => !hasWeights(data as Record<string, unknown>),\n },\n fields: [\n {\n type: 'row',\n fields: [\n { name: 'upright', type: 'upload', relationTo: originalSlug, label: 'Upright (normal)', admin: createOnlyUpload() },\n { name: 'italic', type: 'upload', relationTo: originalSlug, label: 'Italic', admin: createOnlyUpload() },\n ],\n },\n ],\n },\n {\n name: 'weights',\n type: 'array',\n label: 'Weights',\n labels: { singular: 'Weight file', plural: 'Weight files' },\n admin: {\n description: 'One file per weight/style. Add only the weights you need.',\n condition: (data) => !hasVariable(data as Record<string, unknown>),\n },\n // Each weight + style must be unique so the generated next/font src array has no conflicts.\n validate: ((rows: Array<{ weight?: string; style?: string }> | null | undefined) => {\n const seen = new Set<string>()\n for (const r of Array.isArray(rows) ? rows : []) {\n const key = `${r?.weight ?? ''}|${r?.style ?? ''}`\n if (seen.has(key)) return `Two files share weight ${r?.weight ?? '?'} ${r?.style ?? 'normal'}. Each weight + style must be unique.`\n seen.add(key)\n }\n return true\n }) as ArrayField['validate'],\n fields: [\n {\n type: 'row',\n fields: [\n { name: 'weight', type: 'select', required: true, defaultValue: '400', options: WEIGHT_OPTIONS },\n { name: 'style', type: 'radio', required: true, defaultValue: 'normal', options: ['normal', 'italic'] },\n ],\n },\n { name: 'file', type: 'upload', relationTo: originalSlug, required: true, admin: createOnlyUpload() },\n ],\n },\n ]\n\n return {\n slug: fontSlug,\n access: { create: authd, delete: authd, read: authd, update: authd },\n admin: {\n group: 'Assets',\n useAsTitle: 'title',\n enableListViewSelectAPI: true,\n defaultColumns: ['title', 'family'],\n description:\n 'Upload typefaces here to add them to your library. Uploading alone doesn’t put a font on your site — activate it by picking it in Font Set.',\n },\n timestamps: true,\n // When a font is populated as a relationship target (e.g. the `fontSet` global at depth),\n // return only its identifying metadata and NOT the `variable` / `weights` upload slots — which\n // would otherwise drag the private `fontOriginal` blobs through every populated row.\n defaultPopulate: { title: true, family: true },\n fields,\n hooks: {\n // Guard the referenced originals: at least one file, and no original shared across typefaces.\n beforeValidate: [requireFontFiles, makeRejectSharedOriginals(fontSlug)],\n // On save, (re)build the served WOFF2 files from the referenced originals (and clean up\n // any original a swapped/removed slot de-referenced).\n afterChange: [optimizeFromOriginalsHook({ charset: opts.charset, originalSlug, optimizedSlug })],\n // On read (edit view only), surface the served-file count so a failed optimize isn't silent.\n afterRead: [servedFilesHook(optimizedSlug)],\n // Before delete, cascade to the served files + the archived originals (beforeDelete so the\n // `fontOptimized.font` relationship is still intact — see the hook).\n beforeDelete: [cleanupFontAssetsHook({ originalSlug, optimizedSlug })],\n },\n }\n}\n"],"names":["APIError","authd","cleanupFontAssetsHook","optimizeFromOriginalsHook","originalIdsFromDoc","resolveFontFamilies","FONT_OPTIMIZED_SLUG","FONT_ORIGINAL_SLUG","WEIGHT_OPTIONS","FontOriginalUploadPath","createOnlyUpload","components","Field","path","hasVariable","data","v","variable","Boolean","upright","italic","hasWeights","Array","isArray","weights","some","w","file","servedFilesHook","optimizedSlug","doc","findMany","req","payload","id","totalDocs","count","collection","where","font","equals","overrideAccess","servedFiles","requireFontFiles","operation","touches","makeRejectSharedOriginals","fontSlug","originalDoc","ids","length","selfId","refs","in","and","not_equals","or","res","find","depth","limit","other","docs","title","createFontCollection","opts","originalSlug","families","fields","name","type","required","label","interfaceName","options","map","r","value","key","virtual","admin","readOnly","position","description","condition","relationTo","labels","singular","plural","validate","rows","seen","Set","weight","style","has","add","defaultValue","slug","access","create","delete","read","update","group","useAsTitle","enableListViewSelectAPI","defaultColumns","timestamps","defaultPopulate","family","hooks","beforeValidate","afterChange","charset","afterRead","beforeDelete"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { CollectionConfig } from 'payload';
2
+ /** Slug of the optimized (served) weight-file upload collection. */
3
+ export declare const FONT_OPTIMIZED_SLUG = "fontOptimized";
4
+ export interface CreateFontOptimizedCollectionOptions {
5
+ /** Slug of the `Font` typeface collection these belong to. Default 'font'. */
6
+ fontSlug?: string;
7
+ /** Slug of the archival original collection each is derived from. Default 'fontOriginal'. */
8
+ originalSlug?: string;
9
+ }
10
+ /**
11
+ * The optimized, subsetted WOFF2 files the site actually serves — one upload document per
12
+ * weight/style (or variable file). They are DERIVED, not uploaded by hand: the `Font`
13
+ * collection's save hook reads the referenced `fontOriginal` files, subsets each to WOFF2, and
14
+ * creates one of these linked back to its source original and its owning typeface.
15
+ *
16
+ * Hidden from admin nav (editors only ever touch `Font`), and always server-side stored — the
17
+ * save hook writes the bytes, so there's nothing to client-upload.
18
+ */
19
+ export declare const createFontOptimizedCollection: (opts?: CreateFontOptimizedCollectionOptions) => CollectionConfig;
20
+ //# sourceMappingURL=fontOptimized.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fontOptimized.d.ts","sourceRoot":"","sources":["../../src/collections/fontOptimized.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,SAAS,CAAA;AAK/D,oEAAoE;AACpE,eAAO,MAAM,mBAAmB,kBAAkB,CAAA;AAElD,MAAM,WAAW,oCAAoC;IACnD,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6FAA6F;IAC7F,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GAAI,OAAM,oCAAyC,KAAG,gBA6B/F,CAAA"}
@@ -0,0 +1,111 @@
1
+ import { authd } from "../access/authd.js";
2
+ import { FONT_ORIGINAL_SLUG } from "./fontOriginal.js";
3
+ /** Slug of the optimized (served) weight-file upload collection. */ export const FONT_OPTIMIZED_SLUG = 'fontOptimized';
4
+ /**
5
+ * The optimized, subsetted WOFF2 files the site actually serves — one upload document per
6
+ * weight/style (or variable file). They are DERIVED, not uploaded by hand: the `Font`
7
+ * collection's save hook reads the referenced `fontOriginal` files, subsets each to WOFF2, and
8
+ * creates one of these linked back to its source original and its owning typeface.
9
+ *
10
+ * Hidden from admin nav (editors only ever touch `Font`), and always server-side stored — the
11
+ * save hook writes the bytes, so there's nothing to client-upload.
12
+ */ export const createFontOptimizedCollection = (opts = {})=>{
13
+ const fontSlug = opts.fontSlug || 'font';
14
+ const originalSlug = opts.originalSlug || FONT_ORIGINAL_SLUG;
15
+ return {
16
+ slug: FONT_OPTIMIZED_SLUG,
17
+ // The optimized files ARE public web fonts — the build-time export endpoint (no user
18
+ // session) downloads them to self-host via next/font. Public read lets that read succeed
19
+ // even when they're on cloud storage served through Payload's access-controlled file route.
20
+ // Writes stay gated; the raw originals stay private.
21
+ access: {
22
+ create: authd,
23
+ delete: authd,
24
+ read: ()=>true,
25
+ update: authd
26
+ },
27
+ admin: {
28
+ group: 'Assets',
29
+ hidden: true,
30
+ useAsTitle: 'filename',
31
+ defaultColumns: [
32
+ 'filename',
33
+ 'weight',
34
+ 'style',
35
+ 'isVariable'
36
+ ]
37
+ },
38
+ upload: {
39
+ mimeTypes: [
40
+ 'font/woff2'
41
+ ]
42
+ },
43
+ fields: [
44
+ // The owning typeface — the export endpoint and the reconcile hook query by it.
45
+ {
46
+ name: 'font',
47
+ type: 'relationship',
48
+ relationTo: fontSlug,
49
+ admin: {
50
+ readOnly: true
51
+ }
52
+ },
53
+ // The source original this was subsetted from — the reconcile key (one optimized per original).
54
+ {
55
+ name: 'original',
56
+ type: 'relationship',
57
+ relationTo: originalSlug,
58
+ admin: {
59
+ readOnly: true
60
+ }
61
+ },
62
+ // CSS font-weight: a single step ('400') or a variable range ('100 900'); served verbatim to next/font.
63
+ {
64
+ name: 'weight',
65
+ type: 'text',
66
+ admin: {
67
+ readOnly: true
68
+ }
69
+ },
70
+ {
71
+ name: 'style',
72
+ type: 'radio',
73
+ options: [
74
+ 'normal',
75
+ 'italic'
76
+ ],
77
+ admin: {
78
+ readOnly: true
79
+ }
80
+ },
81
+ {
82
+ name: 'isVariable',
83
+ type: 'checkbox',
84
+ admin: {
85
+ readOnly: true
86
+ }
87
+ },
88
+ // This upright variable file ALSO carries italics via its axes (`ital`, or a negative
89
+ // `slnt`) — the serving layers emit a second, italic @font-face from the same file, unless
90
+ // the typeface has an explicit italic file.
91
+ {
92
+ name: 'italCapable',
93
+ type: 'checkbox',
94
+ admin: {
95
+ readOnly: true
96
+ }
97
+ },
98
+ // For slnt-based italics: the positive CSS `oblique` angle (deg) matching the axis extreme.
99
+ {
100
+ name: 'obliqueAngle',
101
+ type: 'number',
102
+ admin: {
103
+ readOnly: true,
104
+ condition: (data)=>Boolean(data?.italCapable)
105
+ }
106
+ }
107
+ ]
108
+ };
109
+ };
110
+
111
+ //# sourceMappingURL=fontOptimized.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/collections/fontOptimized.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug } from 'payload'\n\nimport { authd } from '../access/authd'\nimport { FONT_ORIGINAL_SLUG } from './fontOriginal'\n\n/** Slug of the optimized (served) weight-file upload collection. */\nexport const FONT_OPTIMIZED_SLUG = 'fontOptimized'\n\nexport interface CreateFontOptimizedCollectionOptions {\n /** Slug of the `Font` typeface collection these belong to. Default 'font'. */\n fontSlug?: string\n /** Slug of the archival original collection each is derived from. Default 'fontOriginal'. */\n originalSlug?: string\n}\n\n/**\n * The optimized, subsetted WOFF2 files the site actually serves — one upload document per\n * weight/style (or variable file). They are DERIVED, not uploaded by hand: the `Font`\n * collection's save hook reads the referenced `fontOriginal` files, subsets each to WOFF2, and\n * creates one of these linked back to its source original and its owning typeface.\n *\n * Hidden from admin nav (editors only ever touch `Font`), and always server-side stored — the\n * save hook writes the bytes, so there's nothing to client-upload.\n */\nexport const createFontOptimizedCollection = (opts: CreateFontOptimizedCollectionOptions = {}): CollectionConfig => {\n const fontSlug = opts.fontSlug || 'font'\n const originalSlug = opts.originalSlug || FONT_ORIGINAL_SLUG\n return {\n slug: FONT_OPTIMIZED_SLUG,\n // The optimized files ARE public web fonts — the build-time export endpoint (no user\n // session) downloads them to self-host via next/font. Public read lets that read succeed\n // even when they're on cloud storage served through Payload's access-controlled file route.\n // Writes stay gated; the raw originals stay private.\n access: { create: authd, delete: authd, read: () => true, update: authd },\n admin: { group: 'Assets', hidden: true, useAsTitle: 'filename', defaultColumns: ['filename', 'weight', 'style', 'isVariable'] },\n upload: { mimeTypes: ['font/woff2'] },\n fields: [\n // The owning typeface — the export endpoint and the reconcile hook query by it.\n { name: 'font', type: 'relationship', relationTo: fontSlug as CollectionSlug, admin: { readOnly: true } },\n // The source original this was subsetted from — the reconcile key (one optimized per original).\n { name: 'original', type: 'relationship', relationTo: originalSlug as CollectionSlug, admin: { readOnly: true } },\n // CSS font-weight: a single step ('400') or a variable range ('100 900'); served verbatim to next/font.\n { name: 'weight', type: 'text', admin: { readOnly: true } },\n { name: 'style', type: 'radio', options: ['normal', 'italic'], admin: { readOnly: true } },\n { name: 'isVariable', type: 'checkbox', admin: { readOnly: true } },\n // This upright variable file ALSO carries italics via its axes (`ital`, or a negative\n // `slnt`) — the serving layers emit a second, italic @font-face from the same file, unless\n // the typeface has an explicit italic file.\n { name: 'italCapable', type: 'checkbox', admin: { readOnly: true } },\n // For slnt-based italics: the positive CSS `oblique` angle (deg) matching the axis extreme.\n { name: 'obliqueAngle', type: 'number', admin: { readOnly: true, condition: (data) => Boolean(data?.italCapable) } },\n ],\n }\n}\n"],"names":["authd","FONT_ORIGINAL_SLUG","FONT_OPTIMIZED_SLUG","createFontOptimizedCollection","opts","fontSlug","originalSlug","slug","access","create","delete","read","update","admin","group","hidden","useAsTitle","defaultColumns","upload","mimeTypes","fields","name","type","relationTo","readOnly","options","condition","data","Boolean","italCapable"],"mappings":"AAEA,SAASA,KAAK,QAAQ,qBAAiB;AACvC,SAASC,kBAAkB,QAAQ,oBAAgB;AAEnD,kEAAkE,GAClE,OAAO,MAAMC,sBAAsB,gBAAe;AASlD;;;;;;;;CAQC,GACD,OAAO,MAAMC,gCAAgC,CAACC,OAA6C,CAAC,CAAC;IAC3F,MAAMC,WAAWD,KAAKC,QAAQ,IAAI;IAClC,MAAMC,eAAeF,KAAKE,YAAY,IAAIL;IAC1C,OAAO;QACLM,MAAML;QACN,qFAAqF;QACrF,yFAAyF;QACzF,4FAA4F;QAC5F,qDAAqD;QACrDM,QAAQ;YAAEC,QAAQT;YAAOU,QAAQV;YAAOW,MAAM,IAAM;YAAMC,QAAQZ;QAAM;QACxEa,OAAO;YAAEC,OAAO;YAAUC,QAAQ;YAAMC,YAAY;YAAYC,gBAAgB;gBAAC;gBAAY;gBAAU;gBAAS;aAAa;QAAC;QAC9HC,QAAQ;YAAEC,WAAW;gBAAC;aAAa;QAAC;QACpCC,QAAQ;YACN,gFAAgF;YAChF;gBAAEC,MAAM;gBAAQC,MAAM;gBAAgBC,YAAYlB;gBAA4BQ,OAAO;oBAAEW,UAAU;gBAAK;YAAE;YACxG,gGAAgG;YAChG;gBAAEH,MAAM;gBAAYC,MAAM;gBAAgBC,YAAYjB;gBAAgCO,OAAO;oBAAEW,UAAU;gBAAK;YAAE;YAChH,wGAAwG;YACxG;gBAAEH,MAAM;gBAAUC,MAAM;gBAAQT,OAAO;oBAAEW,UAAU;gBAAK;YAAE;YAC1D;gBAAEH,MAAM;gBAASC,MAAM;gBAASG,SAAS;oBAAC;oBAAU;iBAAS;gBAAEZ,OAAO;oBAAEW,UAAU;gBAAK;YAAE;YACzF;gBAAEH,MAAM;gBAAcC,MAAM;gBAAYT,OAAO;oBAAEW,UAAU;gBAAK;YAAE;YAClE,sFAAsF;YACtF,2FAA2F;YAC3F,4CAA4C;YAC5C;gBAAEH,MAAM;gBAAeC,MAAM;gBAAYT,OAAO;oBAAEW,UAAU;gBAAK;YAAE;YACnE,4FAA4F;YAC5F;gBAAEH,MAAM;gBAAgBC,MAAM;gBAAUT,OAAO;oBAAEW,UAAU;oBAAME,WAAW,CAACC,OAASC,QAAQD,MAAME;gBAAa;YAAE;SACpH;IACH;AACF,EAAC"}