@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.
- package/LICENSE.md +22 -0
- package/README.md +19 -0
- package/dist/access/authd.d.ts +19 -0
- package/dist/access/authd.d.ts.map +1 -0
- package/dist/access/authd.js +18 -0
- package/dist/access/authd.js.map +1 -0
- package/dist/collections/font.d.ts +29 -0
- package/dist/collections/font.d.ts.map +1 -0
- package/dist/collections/font.js +327 -0
- package/dist/collections/font.js.map +1 -0
- package/dist/collections/fontOptimized.d.ts +20 -0
- package/dist/collections/fontOptimized.d.ts.map +1 -0
- package/dist/collections/fontOptimized.js +111 -0
- package/dist/collections/fontOptimized.js.map +1 -0
- package/dist/collections/fontOriginal.d.ts +25 -0
- package/dist/collections/fontOriginal.d.ts.map +1 -0
- package/dist/collections/fontOriginal.js +49 -0
- package/dist/collections/fontOriginal.js.map +1 -0
- package/dist/components/DevFonts.d.ts +52 -0
- package/dist/components/DevFonts.d.ts.map +1 -0
- package/dist/components/DevFonts.js +75 -0
- package/dist/components/DevFonts.js.map +1 -0
- package/dist/components/admin/FontOriginalUpload.d.ts +15 -0
- package/dist/components/admin/FontOriginalUpload.d.ts.map +1 -0
- package/dist/components/admin/FontOriginalUpload.js +22 -0
- package/dist/components/admin/FontOriginalUpload.js.map +1 -0
- package/dist/components/admin/FontOriginalUpload.scss +9 -0
- package/dist/endpoints/exportFonts.d.ts +49 -0
- package/dist/endpoints/exportFonts.d.ts.map +1 -0
- package/dist/endpoints/exportFonts.js +162 -0
- package/dist/endpoints/exportFonts.js.map +1 -0
- package/dist/extractFonts.d.ts +20 -0
- package/dist/extractFonts.d.ts.map +1 -0
- package/dist/extractFonts.js +18 -0
- package/dist/extractFonts.js.map +1 -0
- package/dist/fields/font.d.ts +14 -0
- package/dist/fields/font.d.ts.map +1 -0
- package/dist/fields/font.js +37 -0
- package/dist/fields/font.js.map +1 -0
- package/dist/globals/fontSet.d.ts +15 -0
- package/dist/globals/fontSet.d.ts.map +1 -0
- package/dist/globals/fontSet.js +25 -0
- package/dist/globals/fontSet.js.map +1 -0
- package/dist/hooks/optimizeFont.d.ts +56 -0
- package/dist/hooks/optimizeFont.d.ts.map +1 -0
- package/dist/hooks/optimizeFont.js +185 -0
- package/dist/hooks/optimizeFont.js.map +1 -0
- package/dist/hooks/optimizeFromOriginals.d.ts +43 -0
- package/dist/hooks/optimizeFromOriginals.d.ts.map +1 -0
- package/dist/hooks/optimizeFromOriginals.js +313 -0
- package/dist/hooks/optimizeFromOriginals.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/activeFonts.d.ts +64 -0
- package/dist/lib/activeFonts.d.ts.map +1 -0
- package/dist/lib/activeFonts.js +139 -0
- package/dist/lib/activeFonts.js.map +1 -0
- package/dist/lib/families.d.ts +38 -0
- package/dist/lib/families.d.ts.map +1 -0
- package/dist/lib/families.js +44 -0
- package/dist/lib/families.js.map +1 -0
- package/dist/lib/mergeConfig.d.ts +28 -0
- package/dist/lib/mergeConfig.d.ts.map +1 -0
- package/dist/lib/mergeConfig.js +73 -0
- package/dist/lib/mergeConfig.js.map +1 -0
- package/dist/lib/refs.d.ts +8 -0
- package/dist/lib/refs.d.ts.map +1 -0
- package/dist/lib/refs.js +11 -0
- package/dist/lib/refs.js.map +1 -0
- package/dist/lib/uploadBytes.d.ts +26 -0
- package/dist/lib/uploadBytes.d.ts.map +1 -0
- package/dist/lib/uploadBytes.js +98 -0
- package/dist/lib/uploadBytes.js.map +1 -0
- package/dist/plugin.d.ts +57 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +93 -0
- package/dist/plugin.js.map +1 -0
- package/dist/scripts/cli.d.ts +3 -0
- package/dist/scripts/cli.d.ts.map +1 -0
- package/dist/scripts/cli.js +13 -0
- package/dist/scripts/cli.js.map +1 -0
- package/dist/scripts/downloadFonts.d.ts +37 -0
- package/dist/scripts/downloadFonts.d.ts.map +1 -0
- package/dist/scripts/downloadFonts.js +216 -0
- package/dist/scripts/downloadFonts.js.map +1 -0
- 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"}
|