@bison-lab/payload-core 0.1.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,23 +1,29 @@
1
1
  # `@bison-lab/payload-core`
2
2
 
3
- Site-agnostic Payload CMS configuration for Bison Lab sites. Today: the SEO
4
- tab on a collection and the metadata reader for the pages it describes. The
5
- rest of the shared CMS layer (access matrix, collection factories, slug field)
6
- joins it here as it is extracted.
3
+ Site-agnostic Payload CMS configuration for Bison Lab sites: the SEO tab on
4
+ a collection, the Theme Global, and the readers a page route and a root
5
+ layout use. The rest of the shared CMS layer (access matrix, collection
6
+ factories, slug field) joins it here as it is extracted.
7
+
8
+ Full guide: <https://payload.bisonlab.ai/site/seo/>
7
9
 
8
10
  ```bash
9
- pnpm add @bison-lab/payload-core @payloadcms/plugin-seo
11
+ pnpm add @bison-lab/payload-core @payloadcms/plugin-seo @bison-lab/tokens @bison-lab/fonts
10
12
  ```
11
13
 
12
- Peers: `payload` and `@payloadcms/plugin-seo`. Pin the plugin to the same
14
+ Peers: `payload` and `@payloadcms/plugin-seo` are required. `@bison-lab/tokens`
15
+ and `@bison-lab/fonts` are needed to adopt the Theme Global; `@payloadcms/ui`
16
+ and `react` are needed for its admin fields. Pin the plugin to the same
13
17
  version as `payload`; Payload releases them in lockstep.
14
18
 
15
- ## Two entry points, and why
19
+ ## Four entry points, and why
16
20
 
17
- | Import | Contents | Runs where |
18
- | ---------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------- |
19
- | `@bison-lab/payload-core` | `seoPlugin`, `noIndexField`, `SHARE_IMAGE_SIZE`, title and text helpers, types | Node. What `payload.config.ts` imports; it loads the plugin. |
20
- | `@bison-lab/payload-core/metadata` | `pageMetadata`, the title helpers, the same types | Server. What a page route imports; it does not load the plugin. |
21
+ | Import | Contents | Runs where |
22
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
23
+ | `@bison-lab/payload-core` | `seoPlugin`, `createTheme`, `seedTheme`, import-map strings, title and text helpers, types | Node. What `payload.config.ts` imports; it loads the plugin. |
24
+ | `@bison-lab/payload-core/metadata` | `pageMetadata`, the title helpers, the same types | Server. What a page route imports; it does not load the plugin. |
25
+ | `@bison-lab/payload-core/theme` | `getPublishedTheme`, `themeConfigFromDoc`, `themeHead` | Server. What a root layout imports; it does not load the plugin or React. |
26
+ | `@bison-lab/payload-core/admin` | `ColorField`, `FontField`, `ContrastReport` | Admin. Referenced by import-map string; `generate:importmap` writes it. |
21
27
 
22
28
  ## What editors get
23
29
 
@@ -114,15 +120,88 @@ carry through Next's metadata merge.
114
120
 
115
121
  The sitemap is the site's: filter `meta.noIndex` out of it.
116
122
 
123
+ ## Adopting the Theme global
124
+
125
+ Settings → Theme: the five brand colours, grey scale, radius, shadow, motion,
126
+ density, default theme, body and heading fonts, and an optional logo. Whoever
127
+ holds brand rights edits a draft; publish is deliberate. Contrast is measured
128
+ as they type and shown; it is never enforced. `bison.config.json` is the seed
129
+ a site starts from, not the source of truth.
130
+
131
+ **1. Register the Global.** Access is required — the predicates live in the
132
+ site's `src/platform` until they move here. Pass `canManageBrand` as
133
+ `update`.
134
+
135
+ ```ts
136
+ // payload.config.ts
137
+ import { createTheme } from "@bison-lab/payload-core";
138
+ import bisonConfig from "../bison.config.json";
139
+ import { canManageBrand, isAuthenticated } from "@/platform/access";
140
+
141
+ export default buildConfig({
142
+ globals: [
143
+ createTheme({
144
+ access: { read: isAuthenticated, update: canManageBrand },
145
+ seed: bisonConfig,
146
+ logo: { collection: "brand-assets" },
147
+ onPublish: async () => {
148
+ revalidateTag("theme");
149
+ },
150
+ }),
151
+ ],
152
+ });
153
+ ```
154
+
155
+ Then `payload generate:importmap` (the colour, font and contrast fields
156
+ resolve from `@bison-lab/payload-core/admin`), `payload generate:types`, and
157
+ `payload migrate:create`.
158
+
159
+ **2. Seed the row on deploy.** A migration `up()` writes a published version
160
+ so the site renders what `bison-theme.css` rendered before anyone opens the
161
+ admin:
162
+
163
+ ```ts
164
+ import { seedTheme } from "@bison-lab/payload-core";
165
+ import bisonConfig from "../bison.config.json";
166
+
167
+ export async function up({ payload }) {
168
+ await seedTheme(payload, bisonConfig);
169
+ }
170
+ ```
171
+
172
+ **3. Serve the catalogue.** A Next route around `serveFont` from
173
+ `@bison-lab/fonts/server`, listed in `serverExternalPackages`, at the path
174
+ you pass as `fontsBaseUrl` (default `/fonts`).
175
+
176
+ **4. Render the published theme in the root layout.**
177
+
178
+ ```ts
179
+ // app/layout.tsx
180
+ import { getPublishedTheme, themeHead } from "@bison-lab/payload-core/theme";
181
+ import bisonConfig from "../bison.config.json";
182
+
183
+ const theme = await getPublishedTheme(payload, bisonConfig);
184
+ const { css, preloads } = themeHead(theme);
185
+
186
+ // <link rel="preload" as="font" type="font/woff2" crossOrigin="" href={href} />
187
+ // <style dangerouslySetInnerHTML={{ __html: css }} />
188
+ ```
189
+
190
+ If the admin layout renders the same head, the admin panel restyles too.
191
+
192
+ `getPublishedTheme` reads `draft: false` and falls back to the seed when
193
+ nothing has been published. A newer draft never reaches the public site.
194
+
117
195
  ## No generated types
118
196
 
119
197
  The package cannot import a site's `payload-types`, so the shapes it reads
120
- (`SeoMeta`, `SeoImageDoc`, `SeoPage`) are hand-written and structural. A
121
- generated `Page` and `Media` are assignable to them; nothing carries an index
122
- signature, since an interface will not assign to a type that has one.
198
+ (`SeoMeta`, `SeoImageDoc`, `SeoPage`, `ThemeDoc`) are hand-written and
199
+ structural. A generated `Page`, `Media` or Theme Global is assignable to
200
+ them; nothing carries an index signature, since an interface will not assign
201
+ to a type that has one.
123
202
 
124
203
  ## Changing a field
125
204
 
126
- `noIndexField` and the plugin's own fields are columns in every consuming
127
- site. A change to them is a schema change: say "run `payload migrate:create`"
128
- in the changeset.
205
+ `noIndexField`, the plugin's own fields, and the Theme Global fields are
206
+ columns in every consuming site. A change to them is a schema change: say
207
+ "run `payload migrate:create`" in the changeset.
@@ -0,0 +1,28 @@
1
+
2
+ import { SelectFieldClientComponent, TextFieldClientComponent, UIFieldClientComponent } from "payload";
3
+
4
+ //#region src/admin/color-field.d.ts
5
+ /**
6
+ * Swatch, native colour input, and hex text, kept in step. `#rrggbb` on
7
+ * blur; the field's `validate` refuses anything else on save.
8
+ */
9
+ declare const ColorField: TextFieldClientComponent;
10
+ //#endregion
11
+ //#region src/admin/font-field.d.ts
12
+ /**
13
+ * Listbox over the catalogue ids in `admin.custom.ids`. Each option is the
14
+ * family name in that face, with the catalogue hint beneath. Faces load
15
+ * from the site's font route the first time the picker opens.
16
+ */
17
+ declare const FontField: SelectFieldClientComponent;
18
+ //#endregion
19
+ //#region src/admin/contrast-report.d.ts
20
+ /**
21
+ * Reads the five brand colours and grey scale from the form, runs
22
+ * `auditTheme` client-side, and paints its own rows. Warn only: nothing
23
+ * here touches validity.
24
+ */
25
+ declare const ContrastReport: UIFieldClientComponent;
26
+ //#endregion
27
+ export { ColorField, ContrastReport, FontField };
28
+ //# sourceMappingURL=admin.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/color-field.tsx","../src/admin/font-field.tsx","../src/admin/contrast-report.tsx"],"mappings":";;;;;;AAWA;;cAAa,UAAA,EAAY,wBAAA;;;;;AAAzB;;;cCGa,SAAA,EAAW,0BAAA;;;;;ADHxB;;;cEqEa,cAAA,EAAgB,sBAAA"}
package/dist/admin.mjs ADDED
@@ -0,0 +1,433 @@
1
+ "use client";
2
+ import { FieldDescription, FieldError, FieldLabel, useField, useFormFields } from "@payloadcms/ui";
3
+ import { auditTheme, contrastLevelLabel, contrastVerdict, isThemeHex } from "@bison-lab/tokens";
4
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { findFont, fontFaceCss } from "@bison-lab/fonts";
7
+ //#region src/admin/color-field.tsx
8
+ const HEX_ERROR = "Enter a six-digit hex colour like #1e3a5f";
9
+ /**
10
+ * Swatch, native colour input, and hex text, kept in step. `#rrggbb` on
11
+ * blur; the field's `validate` refuses anything else on save.
12
+ */
13
+ const ColorField = ({ field, path, readOnly }) => {
14
+ const { value, setValue, showError, errorMessage } = useField({ path });
15
+ const [text, setText] = useState(value ?? "");
16
+ const [localError, setLocalError] = useState(null);
17
+ const id = useId();
18
+ const hexId = `${id}-hex`;
19
+ const pickerId = `${id}-picker`;
20
+ const required = Boolean(field.required);
21
+ const valid = isThemeHex(value);
22
+ const message = localError ?? errorMessage;
23
+ const show = Boolean(localError) || showError;
24
+ useEffect(() => {
25
+ setText(value ?? "");
26
+ }, [value]);
27
+ function commit(next) {
28
+ const trimmed = next.trim();
29
+ if (isThemeHex(trimmed)) {
30
+ setValue(trimmed);
31
+ setLocalError(null);
32
+ return;
33
+ }
34
+ setLocalError(HEX_ERROR);
35
+ }
36
+ return /* @__PURE__ */ jsxs("div", {
37
+ className: "field-type bl-color-field",
38
+ children: [
39
+ /* @__PURE__ */ jsx("style", {
40
+ href: "bl-color-field",
41
+ precedence: "default",
42
+ children: CSS$2
43
+ }),
44
+ /* @__PURE__ */ jsx(FieldLabel, {
45
+ htmlFor: hexId,
46
+ label: field.label,
47
+ required
48
+ }),
49
+ /* @__PURE__ */ jsxs("div", {
50
+ className: "bl-color-field__control",
51
+ children: [
52
+ /* @__PURE__ */ jsx("span", {
53
+ className: "bl-color-field__swatch",
54
+ "aria-hidden": "true",
55
+ style: { background: valid ? value : "transparent" }
56
+ }),
57
+ /* @__PURE__ */ jsx("input", {
58
+ id: pickerId,
59
+ type: "color",
60
+ className: "bl-color-field__picker",
61
+ disabled: readOnly,
62
+ value: valid ? value : "#000000",
63
+ "aria-label": `${typeof field.label === "string" ? field.label : "Colour"} picker`,
64
+ onChange: (event) => {
65
+ setValue(event.target.value);
66
+ setText(event.target.value);
67
+ setLocalError(null);
68
+ }
69
+ }),
70
+ /* @__PURE__ */ jsx("input", {
71
+ id: hexId,
72
+ type: "text",
73
+ className: "bl-color-field__hex",
74
+ spellCheck: false,
75
+ autoComplete: "off",
76
+ disabled: readOnly,
77
+ value: text,
78
+ onChange: (event) => {
79
+ setText(event.target.value);
80
+ if (isThemeHex(event.target.value.trim())) {
81
+ setValue(event.target.value.trim());
82
+ setLocalError(null);
83
+ }
84
+ },
85
+ onBlur: () => commit(text)
86
+ })
87
+ ]
88
+ }),
89
+ /* @__PURE__ */ jsx(FieldDescription, {
90
+ description: field.admin?.description,
91
+ path
92
+ }),
93
+ /* @__PURE__ */ jsx(FieldError, {
94
+ path,
95
+ showError: show,
96
+ message
97
+ })
98
+ ]
99
+ });
100
+ };
101
+ const CSS$2 = `
102
+ .bl-color-field__control{display:flex;align-items:center;gap:calc(var(--base) * .4)}
103
+ .bl-color-field__swatch{flex:none;width:calc(var(--base) * 1.6);height:calc(var(--base) * 1.6);border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-elevation-50)}
104
+ .bl-color-field__picker{flex:none;width:calc(var(--base) * 2);height:calc(var(--base) * 2);padding:0;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:transparent;cursor:pointer}
105
+ .bl-color-field__picker:disabled{cursor:not-allowed;opacity:.6}
106
+ .bl-color-field__hex{flex:1;min-width:0;min-height:calc(var(--base) * 2);padding:calc(var(--base) * .25) calc(var(--base) * .6);border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-input-bg);color:var(--theme-elevation-800);font-family:var(--font-mono);font-size:13px}
107
+ .bl-color-field__hex:focus{border-color:var(--theme-elevation-400);outline:none;box-shadow:0 0 0 1px var(--theme-elevation-400)}
108
+ .bl-color-field__hex:disabled{color:var(--theme-elevation-400)}
109
+ `;
110
+ //#endregion
111
+ //#region src/admin/font-field.tsx
112
+ /**
113
+ * Listbox over the catalogue ids in `admin.custom.ids`. Each option is the
114
+ * family name in that face, with the catalogue hint beneath. Faces load
115
+ * from the site's font route the first time the picker opens.
116
+ */
117
+ const FontField = ({ field, path, readOnly }) => {
118
+ const { value, setValue, showError, errorMessage } = useField({ path });
119
+ const custom = field.admin?.custom ?? {};
120
+ const ids = custom.ids ?? [];
121
+ const fontsBaseUrl = custom.fontsBaseUrl ?? "/fonts";
122
+ const sameAsBody = Boolean(custom.sameAsBody);
123
+ const options = useMemo(() => {
124
+ const fromCatalogue = ids.flatMap((id) => {
125
+ const font = findFont(id);
126
+ if (!font) return [];
127
+ return [{
128
+ value: font.id,
129
+ family: font.family,
130
+ hint: font.hint,
131
+ specimen: true
132
+ }];
133
+ });
134
+ if (!sameAsBody) return fromCatalogue;
135
+ return [{
136
+ value: "",
137
+ family: "Same as body",
138
+ hint: "Use the body family for headings",
139
+ specimen: false
140
+ }, ...fromCatalogue];
141
+ }, [ids, sameAsBody]);
142
+ const [open, setOpen] = useState(false);
143
+ const [faces, setFaces] = useState(false);
144
+ const [active, setActive] = useState(-1);
145
+ const typed = useRef("");
146
+ const typedAt = useRef(0);
147
+ const id = useId();
148
+ const buttonId = `${id}-button`;
149
+ const listId = `${id}-list`;
150
+ const required = Boolean(field.required);
151
+ useEffect(() => {
152
+ if (open) setFaces(true);
153
+ }, [open]);
154
+ const selected = options.find((option) => option.value === (value ?? "")) ?? options[0];
155
+ function choose(option) {
156
+ setValue(option.value);
157
+ setOpen(false);
158
+ setActive(-1);
159
+ }
160
+ function onKeyDown(event) {
161
+ if (event.key === "ArrowDown") {
162
+ event.preventDefault();
163
+ if (!open) {
164
+ setOpen(true);
165
+ setActive(0);
166
+ return;
167
+ }
168
+ setActive((i) => options.length ? (i + 1) % options.length : -1);
169
+ } else if (event.key === "ArrowUp") {
170
+ event.preventDefault();
171
+ if (!open) {
172
+ setOpen(true);
173
+ setActive(options.length - 1);
174
+ return;
175
+ }
176
+ setActive((i) => options.length ? i <= 0 ? options.length - 1 : i - 1 : -1);
177
+ } else if (event.key === "Enter") {
178
+ if (!open) return;
179
+ event.preventDefault();
180
+ const pick = options[active] ?? options[0];
181
+ if (pick) choose(pick);
182
+ } else if (event.key === "Escape") {
183
+ if (!open) return;
184
+ event.preventDefault();
185
+ event.stopPropagation();
186
+ setOpen(false);
187
+ setActive(-1);
188
+ } else if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
189
+ const now = Date.now();
190
+ typed.current = now - typedAt.current > 600 ? event.key : typed.current + event.key;
191
+ typedAt.current = now;
192
+ const needle = typed.current.toLowerCase();
193
+ const index = options.findIndex((option) => option.family.toLowerCase().startsWith(needle));
194
+ if (index >= 0) {
195
+ if (!open) setOpen(true);
196
+ setActive(index);
197
+ }
198
+ }
199
+ }
200
+ const activeId = active >= 0 && open ? `${id}-option-${active}` : void 0;
201
+ return /* @__PURE__ */ jsxs("div", {
202
+ className: "field-type bl-font-field",
203
+ children: [
204
+ /* @__PURE__ */ jsx("style", {
205
+ href: "bl-font-field",
206
+ precedence: "default",
207
+ children: CSS$1
208
+ }),
209
+ faces ? /* @__PURE__ */ jsx("style", {
210
+ "data-bl-font-faces": "",
211
+ children: fontFaceCss(ids, fontsBaseUrl)
212
+ }) : null,
213
+ /* @__PURE__ */ jsx(FieldLabel, {
214
+ htmlFor: buttonId,
215
+ label: field.label,
216
+ required
217
+ }),
218
+ /* @__PURE__ */ jsxs("div", {
219
+ className: "bl-font-field__control",
220
+ children: [/* @__PURE__ */ jsx("button", {
221
+ id: buttonId,
222
+ type: "button",
223
+ className: "bl-font-field__trigger",
224
+ disabled: readOnly,
225
+ "aria-haspopup": "listbox",
226
+ "aria-expanded": open,
227
+ "aria-controls": listId,
228
+ "aria-activedescendant": activeId,
229
+ onClick: () => setOpen((was) => !was),
230
+ onBlur: () => setOpen(false),
231
+ onKeyDown,
232
+ children: selected?.family ?? "Choose a font"
233
+ }), open ? /* @__PURE__ */ jsx("ul", {
234
+ id: listId,
235
+ role: "listbox",
236
+ className: "bl-font-field__menu",
237
+ children: options.map((option, i) => /* @__PURE__ */ jsxs("li", {
238
+ id: `${id}-option-${i}`,
239
+ role: "option",
240
+ "aria-selected": option.value === (value ?? ""),
241
+ className: "bl-font-field__option",
242
+ onMouseDown: (event) => {
243
+ event.preventDefault();
244
+ choose(option);
245
+ },
246
+ onMouseEnter: () => setActive(i),
247
+ children: [/* @__PURE__ */ jsx("span", {
248
+ className: "bl-font-field__family",
249
+ style: option.specimen ? { fontFamily: `"${option.family}"` } : void 0,
250
+ children: option.family
251
+ }), /* @__PURE__ */ jsx("span", {
252
+ className: "bl-font-field__hint",
253
+ children: option.hint
254
+ })]
255
+ }, option.value || "same"))
256
+ }) : null]
257
+ }),
258
+ /* @__PURE__ */ jsx(FieldDescription, {
259
+ description: field.admin?.description,
260
+ path
261
+ }),
262
+ /* @__PURE__ */ jsx(FieldError, {
263
+ path,
264
+ showError,
265
+ message: errorMessage
266
+ })
267
+ ]
268
+ });
269
+ };
270
+ const CSS$1 = `
271
+ .bl-font-field__control{position:relative}
272
+ .bl-font-field__trigger{display:flex;align-items:center;width:100%;min-height:calc(var(--base) * 2);padding:calc(var(--base) * .25) calc(var(--base) * .6);border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-input-bg);color:var(--theme-elevation-800);font:inherit;text-align:left;cursor:pointer}
273
+ .bl-font-field__trigger:focus{border-color:var(--theme-elevation-400);outline:none;box-shadow:0 0 0 1px var(--theme-elevation-400)}
274
+ .bl-font-field__trigger:disabled{color:var(--theme-elevation-400);cursor:not-allowed}
275
+ .bl-font-field__menu{position:absolute;left:0;right:0;top:calc(100% + 4px);z-index:10;margin:0;padding:calc(var(--base) * .2) 0;list-style:none;max-height:18rem;overflow:auto;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-elevation-0);box-shadow:0 8px 24px var(--theme-overlay)}
276
+ .bl-font-field__option{display:flex;flex-direction:column;gap:2px;padding:calc(var(--base) * .35) calc(var(--base) * .6);cursor:pointer}
277
+ .bl-font-field__option[aria-selected="true"],.bl-font-field__option:hover{background:var(--theme-elevation-100)}
278
+ .bl-font-field__family{font-weight:500}
279
+ .bl-font-field__hint{color:var(--theme-elevation-500);font-size:12px}
280
+ `;
281
+ //#endregion
282
+ //#region src/admin/contrast-report.tsx
283
+ function fieldValue(fields, path) {
284
+ return fields[path]?.value;
285
+ }
286
+ function isCompleteBrand(values) {
287
+ return isThemeHex(values.primary) && isThemeHex(values.secondary) && isThemeHex(values.accent) && isThemeHex(values.highlight) && isThemeHex(values.success) && typeof values.greyScale === "string";
288
+ }
289
+ function LevelIcon({ level }) {
290
+ if (level === "fail") return /* @__PURE__ */ jsx("svg", {
291
+ viewBox: "0 0 16 16",
292
+ width: "14",
293
+ height: "14",
294
+ "aria-hidden": "true",
295
+ children: /* @__PURE__ */ jsx("path", {
296
+ fill: "currentColor",
297
+ d: "M8 1.5 1.5 14h13L8 1.5zm0 4.2c.4 0 .7.3.7.8v3.2c0 .4-.3.8-.7.8s-.8-.4-.8-.8V6.5c0-.5.4-.8.8-.8zm0 6.3a.8.8 0 1 1 0 1.6.8.8 0 0 1 0-1.6z"
298
+ })
299
+ });
300
+ if (level === "aa-large") return /* @__PURE__ */ jsx("svg", {
301
+ viewBox: "0 0 16 16",
302
+ width: "14",
303
+ height: "14",
304
+ "aria-hidden": "true",
305
+ children: /* @__PURE__ */ jsx("path", {
306
+ fill: "currentColor",
307
+ d: "M3 8h10v1.5H3z"
308
+ })
309
+ });
310
+ return /* @__PURE__ */ jsx("svg", {
311
+ viewBox: "0 0 16 16",
312
+ width: "14",
313
+ height: "14",
314
+ "aria-hidden": "true",
315
+ children: /* @__PURE__ */ jsx("path", {
316
+ fill: "currentColor",
317
+ d: "M8 1.2a6.8 6.8 0 1 1 0 13.6A6.8 6.8 0 0 1 8 1.2zm3.1 4.3L7.2 9.4 5 7.2l-.9.9 3.1 3.1 4.8-4.8-.9-.9z"
318
+ })
319
+ });
320
+ }
321
+ /**
322
+ * Reads the five brand colours and grey scale from the form, runs
323
+ * `auditTheme` client-side, and paints its own rows. Warn only: nothing
324
+ * here touches validity.
325
+ */
326
+ const ContrastReport = ({ field }) => {
327
+ const target = Number((field.admin?.custom)?.target) || 4.5;
328
+ const values = useFormFields(([fields]) => {
329
+ const record = fields;
330
+ return {
331
+ primary: fieldValue(record, "brand.primary"),
332
+ secondary: fieldValue(record, "brand.secondary"),
333
+ accent: fieldValue(record, "brand.accent"),
334
+ highlight: fieldValue(record, "brand.highlight"),
335
+ success: fieldValue(record, "brand.success"),
336
+ greyScale: fieldValue(record, "greyScale")
337
+ };
338
+ });
339
+ const last = useRef(null);
340
+ const complete = isCompleteBrand(values);
341
+ if (complete) last.current = auditTheme({
342
+ brandPrimary: values.primary,
343
+ brandSecondary: values.secondary,
344
+ brandAccent: values.accent,
345
+ brandHighlight: values.highlight,
346
+ brandSuccess: values.success,
347
+ greyScale: values.greyScale
348
+ }, { target });
349
+ const checks = last.current;
350
+ const stale = Boolean(checks) && !complete;
351
+ const failing = checks?.filter((check) => !check.meetsTarget).length ?? 0;
352
+ const summary = !checks ? "Enter five hex colours to measure contrast." : failing === 0 ? `Every pairing meets ${target}:1` : `${failing} of ${checks.length} pairings below ${target}:1`;
353
+ return /* @__PURE__ */ jsxs("div", {
354
+ className: "field-type bl-contrast-report",
355
+ children: [
356
+ /* @__PURE__ */ jsx("style", {
357
+ href: "bl-contrast-report",
358
+ precedence: "default",
359
+ children: CSS
360
+ }),
361
+ /* @__PURE__ */ jsx("p", {
362
+ className: "bl-contrast-report__summary",
363
+ children: summary
364
+ }),
365
+ stale ? /* @__PURE__ */ jsx("p", {
366
+ className: "bl-contrast-report__stale",
367
+ children: "Showing the last complete audit while a colour is being typed."
368
+ }) : null,
369
+ checks ? /* @__PURE__ */ jsx("ul", {
370
+ className: "bl-contrast-report__list",
371
+ children: checks.flatMap((check) => ["light", "dark"].map((mode) => {
372
+ const sample = check[mode];
373
+ const failingRow = sample.ratio < check.target;
374
+ const verdict = contrastVerdict(sample, check.target);
375
+ return /* @__PURE__ */ jsxs("li", {
376
+ className: "bl-contrast-report__row",
377
+ "data-failing": failingRow ? "" : void 0,
378
+ children: [
379
+ /* @__PURE__ */ jsx("span", {
380
+ "aria-hidden": "true",
381
+ className: "bl-contrast-report__swatch",
382
+ style: {
383
+ color: `hsl(${sample.foreground})`,
384
+ backgroundColor: `hsl(${sample.background})`
385
+ },
386
+ children: "Aa"
387
+ }),
388
+ /* @__PURE__ */ jsxs("span", {
389
+ className: "bl-contrast-report__ratio",
390
+ children: [sample.ratio.toFixed(2), ":1"]
391
+ }),
392
+ /* @__PURE__ */ jsxs("span", {
393
+ className: "bl-contrast-report__level",
394
+ children: [/* @__PURE__ */ jsx(LevelIcon, { level: sample.level }), contrastLevelLabel(sample.level)]
395
+ }),
396
+ /* @__PURE__ */ jsx("span", {
397
+ className: "bl-contrast-report__verdict",
398
+ children: verdict.text
399
+ }),
400
+ /* @__PURE__ */ jsxs("span", {
401
+ className: "bl-contrast-report__label",
402
+ children: [
403
+ check.label,
404
+ " (",
405
+ mode === "light" ? "Light" : "Dark",
406
+ ")"
407
+ ]
408
+ })
409
+ ]
410
+ }, `${check.id}-${mode}`);
411
+ }))
412
+ }) : null
413
+ ]
414
+ });
415
+ };
416
+ const CSS = `
417
+ .bl-contrast-report{margin:calc(var(--base) * .6) 0}
418
+ .bl-contrast-report__summary{margin:0 0 calc(var(--base) * .3);font-weight:600}
419
+ .bl-contrast-report__stale{margin:0 0 calc(var(--base) * .3);color:var(--theme-elevation-500);font-size:12px}
420
+ .bl-contrast-report__list{margin:0;padding:0;list-style:none;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-elevation-0)}
421
+ .bl-contrast-report__row{display:flex;flex-wrap:wrap;align-items:center;gap:calc(var(--base) * .4);padding:calc(var(--base) * .35) calc(var(--base) * .6);border-top:1px solid var(--theme-elevation-100)}
422
+ .bl-contrast-report__row:first-child{border-top:0}
423
+ .bl-contrast-report__row[data-failing]{background:var(--theme-error-50)}
424
+ .bl-contrast-report__swatch{flex:none;display:inline-flex;align-items:center;justify-content:center;width:2.5rem;height:1.75rem;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);font-weight:600}
425
+ .bl-contrast-report__ratio{width:4.5rem;font-variant-numeric:tabular-nums}
426
+ .bl-contrast-report__level{display:inline-flex;align-items:center;gap:4px;width:5.5rem}
427
+ .bl-contrast-report__verdict{width:6rem;font-weight:600}
428
+ .bl-contrast-report__label{flex:1;min-width:12rem;color:var(--theme-elevation-500)}
429
+ `;
430
+ //#endregion
431
+ export { ColorField, ContrastReport, FontField };
432
+
433
+ //# sourceMappingURL=admin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.mjs","names":["CSS","CSS"],"sources":["../src/admin/color-field.tsx","../src/admin/font-field.tsx","../src/admin/contrast-report.tsx"],"sourcesContent":["import { FieldDescription, FieldError, FieldLabel, useField } from \"@payloadcms/ui\";\nimport { isThemeHex } from \"@bison-lab/tokens\";\nimport type { TextFieldClientComponent } from \"payload\";\nimport { useEffect, useId, useState } from \"react\";\n\nconst HEX_ERROR = \"Enter a six-digit hex colour like #1e3a5f\";\n\n/**\n * Swatch, native colour input, and hex text, kept in step. `#rrggbb` on\n * blur; the field's `validate` refuses anything else on save.\n */\nexport const ColorField: TextFieldClientComponent = ({ field, path, readOnly }) => {\n const { value, setValue, showError, errorMessage } = useField<string>({ path });\n const [text, setText] = useState(value ?? \"\");\n const [localError, setLocalError] = useState<string | null>(null);\n const id = useId();\n const hexId = `${id}-hex`;\n const pickerId = `${id}-picker`;\n const required = Boolean(field.required);\n const valid = isThemeHex(value);\n const message = localError ?? errorMessage;\n const show = Boolean(localError) || showError;\n\n useEffect(() => {\n setText(value ?? \"\");\n }, [value]);\n\n function commit(next: string) {\n const trimmed = next.trim();\n if (isThemeHex(trimmed)) {\n setValue(trimmed);\n setLocalError(null);\n return;\n }\n setLocalError(HEX_ERROR);\n }\n\n return (\n <div className=\"field-type bl-color-field\">\n <style href=\"bl-color-field\" precedence=\"default\">\n {CSS}\n </style>\n <FieldLabel htmlFor={hexId} label={field.label} required={required} />\n <div className=\"bl-color-field__control\">\n <span\n className=\"bl-color-field__swatch\"\n aria-hidden=\"true\"\n style={{ background: valid ? value : \"transparent\" }}\n />\n <input\n id={pickerId}\n type=\"color\"\n className=\"bl-color-field__picker\"\n disabled={readOnly}\n value={valid ? value : \"#000000\"}\n aria-label={`${typeof field.label === \"string\" ? field.label : \"Colour\"} picker`}\n onChange={(event) => {\n setValue(event.target.value);\n setText(event.target.value);\n setLocalError(null);\n }}\n />\n <input\n id={hexId}\n type=\"text\"\n className=\"bl-color-field__hex\"\n spellCheck={false}\n autoComplete=\"off\"\n disabled={readOnly}\n value={text}\n onChange={(event) => {\n setText(event.target.value);\n if (isThemeHex(event.target.value.trim())) {\n setValue(event.target.value.trim());\n setLocalError(null);\n }\n }}\n onBlur={() => commit(text)}\n />\n </div>\n <FieldDescription description={field.admin?.description} path={path} />\n <FieldError path={path} showError={show} message={message} />\n </div>\n );\n};\n\nconst CSS = `\n.bl-color-field__control{display:flex;align-items:center;gap:calc(var(--base) * .4)}\n.bl-color-field__swatch{flex:none;width:calc(var(--base) * 1.6);height:calc(var(--base) * 1.6);border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-elevation-50)}\n.bl-color-field__picker{flex:none;width:calc(var(--base) * 2);height:calc(var(--base) * 2);padding:0;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:transparent;cursor:pointer}\n.bl-color-field__picker:disabled{cursor:not-allowed;opacity:.6}\n.bl-color-field__hex{flex:1;min-width:0;min-height:calc(var(--base) * 2);padding:calc(var(--base) * .25) calc(var(--base) * .6);border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-input-bg);color:var(--theme-elevation-800);font-family:var(--font-mono);font-size:13px}\n.bl-color-field__hex:focus{border-color:var(--theme-elevation-400);outline:none;box-shadow:0 0 0 1px var(--theme-elevation-400)}\n.bl-color-field__hex:disabled{color:var(--theme-elevation-400)}\n`;\n","import { FieldDescription, FieldError, FieldLabel, useField } from \"@payloadcms/ui\";\nimport { findFont, fontFaceCss, type FontId } from \"@bison-lab/fonts\";\nimport type { SelectFieldClientComponent } from \"payload\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\n\nimport { SAME_AS_BODY } from \"../theme/fields\";\n\ntype Option = { value: string; family: string; hint: string; specimen: boolean };\n\n/**\n * Listbox over the catalogue ids in `admin.custom.ids`. Each option is the\n * family name in that face, with the catalogue hint beneath. Faces load\n * from the site's font route the first time the picker opens.\n */\nexport const FontField: SelectFieldClientComponent = ({ field, path, readOnly }) => {\n const { value, setValue, showError, errorMessage } = useField<string>({ path });\n const custom = (field.admin?.custom ?? {}) as {\n fontsBaseUrl?: string;\n ids?: string[];\n sameAsBody?: boolean;\n };\n const ids = custom.ids ?? [];\n const fontsBaseUrl = custom.fontsBaseUrl ?? \"/fonts\";\n const sameAsBody = Boolean(custom.sameAsBody);\n\n const options = useMemo<Option[]>(() => {\n const fromCatalogue: Option[] = ids.flatMap((id) => {\n const font = findFont(id);\n if (!font) return [];\n return [{ value: font.id, family: font.family, hint: font.hint, specimen: true }];\n });\n if (!sameAsBody) return fromCatalogue;\n return [{ value: SAME_AS_BODY, family: \"Same as body\", hint: \"Use the body family for headings\", specimen: false }, ...fromCatalogue];\n }, [ids, sameAsBody]);\n\n const [open, setOpen] = useState(false);\n const [faces, setFaces] = useState(false);\n const [active, setActive] = useState(-1);\n const typed = useRef(\"\");\n const typedAt = useRef(0);\n const id = useId();\n const buttonId = `${id}-button`;\n const listId = `${id}-list`;\n const required = Boolean(field.required);\n\n useEffect(() => {\n if (open) setFaces(true);\n }, [open]);\n\n const selected = options.find((option) => option.value === (value ?? SAME_AS_BODY)) ?? options[0];\n\n function choose(option: Option) {\n setValue(option.value);\n setOpen(false);\n setActive(-1);\n }\n\n function onKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n if (!open) {\n setOpen(true);\n setActive(0);\n return;\n }\n setActive((i) => (options.length ? (i + 1) % options.length : -1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n if (!open) {\n setOpen(true);\n setActive(options.length - 1);\n return;\n }\n setActive((i) => (options.length ? (i <= 0 ? options.length - 1 : i - 1) : -1));\n } else if (event.key === \"Enter\") {\n if (!open) return;\n event.preventDefault();\n const pick = options[active] ?? options[0];\n if (pick) choose(pick);\n } else if (event.key === \"Escape\") {\n if (!open) return;\n event.preventDefault();\n event.stopPropagation();\n setOpen(false);\n setActive(-1);\n } else if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {\n const now = Date.now();\n typed.current = now - typedAt.current > 600 ? event.key : typed.current + event.key;\n typedAt.current = now;\n const needle = typed.current.toLowerCase();\n const index = options.findIndex((option) => option.family.toLowerCase().startsWith(needle));\n if (index >= 0) {\n if (!open) setOpen(true);\n setActive(index);\n }\n }\n }\n\n const activeId = active >= 0 && open ? `${id}-option-${active}` : undefined;\n\n return (\n <div className=\"field-type bl-font-field\">\n <style href=\"bl-font-field\" precedence=\"default\">\n {CSS}\n </style>\n {faces ? <style data-bl-font-faces=\"\">{fontFaceCss(ids as FontId[], fontsBaseUrl)}</style> : null}\n <FieldLabel htmlFor={buttonId} label={field.label} required={required} />\n <div className=\"bl-font-field__control\">\n <button\n id={buttonId}\n type=\"button\"\n className=\"bl-font-field__trigger\"\n disabled={readOnly}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={listId}\n aria-activedescendant={activeId}\n onClick={() => setOpen((was) => !was)}\n onBlur={() => setOpen(false)}\n onKeyDown={onKeyDown}\n >\n {selected?.family ?? \"Choose a font\"}\n </button>\n {open ? (\n <ul id={listId} role=\"listbox\" className=\"bl-font-field__menu\">\n {options.map((option, i) => (\n <li\n key={option.value || \"same\"}\n id={`${id}-option-${i}`}\n role=\"option\"\n aria-selected={option.value === (value ?? SAME_AS_BODY)}\n className=\"bl-font-field__option\"\n onMouseDown={(event) => {\n event.preventDefault();\n choose(option);\n }}\n onMouseEnter={() => setActive(i)}\n >\n <span\n className=\"bl-font-field__family\"\n style={option.specimen ? { fontFamily: `\"${option.family}\"` } : undefined}\n >\n {option.family}\n </span>\n <span className=\"bl-font-field__hint\">{option.hint}</span>\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n <FieldDescription description={field.admin?.description} path={path} />\n <FieldError path={path} showError={showError} message={errorMessage} />\n </div>\n );\n};\n\nconst CSS = `\n.bl-font-field__control{position:relative}\n.bl-font-field__trigger{display:flex;align-items:center;width:100%;min-height:calc(var(--base) * 2);padding:calc(var(--base) * .25) calc(var(--base) * .6);border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-input-bg);color:var(--theme-elevation-800);font:inherit;text-align:left;cursor:pointer}\n.bl-font-field__trigger:focus{border-color:var(--theme-elevation-400);outline:none;box-shadow:0 0 0 1px var(--theme-elevation-400)}\n.bl-font-field__trigger:disabled{color:var(--theme-elevation-400);cursor:not-allowed}\n.bl-font-field__menu{position:absolute;left:0;right:0;top:calc(100% + 4px);z-index:10;margin:0;padding:calc(var(--base) * .2) 0;list-style:none;max-height:18rem;overflow:auto;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-elevation-0);box-shadow:0 8px 24px var(--theme-overlay)}\n.bl-font-field__option{display:flex;flex-direction:column;gap:2px;padding:calc(var(--base) * .35) calc(var(--base) * .6);cursor:pointer}\n.bl-font-field__option[aria-selected=\"true\"],.bl-font-field__option:hover{background:var(--theme-elevation-100)}\n.bl-font-field__family{font-weight:500}\n.bl-font-field__hint{color:var(--theme-elevation-500);font-size:12px}\n`;\n","import { useFormFields } from \"@payloadcms/ui\";\nimport {\n auditTheme,\n contrastLevelLabel,\n contrastVerdict,\n isThemeHex,\n type ContrastCheck,\n type ContrastLevel,\n type GreyScale,\n} from \"@bison-lab/tokens\";\nimport type { UIFieldClientComponent } from \"payload\";\nimport { useRef } from \"react\";\n\nfunction fieldValue(\n fields: Record<string, { value?: unknown } | undefined>,\n path: string,\n): unknown {\n return fields[path]?.value;\n}\n\ntype BrandFormValues = {\n primary: unknown;\n secondary: unknown;\n accent: unknown;\n highlight: unknown;\n success: unknown;\n greyScale: unknown;\n};\n\nfunction isCompleteBrand(values: BrandFormValues): values is BrandFormValues & {\n primary: string;\n secondary: string;\n accent: string;\n highlight: string;\n success: string;\n greyScale: GreyScale;\n} {\n return (\n isThemeHex(values.primary) &&\n isThemeHex(values.secondary) &&\n isThemeHex(values.accent) &&\n isThemeHex(values.highlight) &&\n isThemeHex(values.success) &&\n typeof values.greyScale === \"string\"\n );\n}\n\nfunction LevelIcon({ level }: { level: ContrastLevel }) {\n if (level === \"fail\") {\n return (\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" aria-hidden=\"true\">\n <path\n fill=\"currentColor\"\n d=\"M8 1.5 1.5 14h13L8 1.5zm0 4.2c.4 0 .7.3.7.8v3.2c0 .4-.3.8-.7.8s-.8-.4-.8-.8V6.5c0-.5.4-.8.8-.8zm0 6.3a.8.8 0 1 1 0 1.6.8.8 0 0 1 0-1.6z\"\n />\n </svg>\n );\n }\n if (level === \"aa-large\") {\n return (\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" aria-hidden=\"true\">\n <path fill=\"currentColor\" d=\"M3 8h10v1.5H3z\" />\n </svg>\n );\n }\n return (\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" aria-hidden=\"true\">\n <path\n fill=\"currentColor\"\n d=\"M8 1.2a6.8 6.8 0 1 1 0 13.6A6.8 6.8 0 0 1 8 1.2zm3.1 4.3L7.2 9.4 5 7.2l-.9.9 3.1 3.1 4.8-4.8-.9-.9z\"\n />\n </svg>\n );\n}\n\n/**\n * Reads the five brand colours and grey scale from the form, runs\n * `auditTheme` client-side, and paints its own rows. Warn only: nothing\n * here touches validity.\n */\nexport const ContrastReport: UIFieldClientComponent = ({ field }) => {\n const target = Number((field.admin?.custom as { target?: number } | undefined)?.target) || 4.5;\n const values = useFormFields(([fields]) => {\n const record = fields as Record<string, { value?: unknown } | undefined>;\n return {\n primary: fieldValue(record, \"brand.primary\"),\n secondary: fieldValue(record, \"brand.secondary\"),\n accent: fieldValue(record, \"brand.accent\"),\n highlight: fieldValue(record, \"brand.highlight\"),\n success: fieldValue(record, \"brand.success\"),\n greyScale: fieldValue(record, \"greyScale\"),\n };\n });\n\n const last = useRef<ContrastCheck[] | null>(null);\n const complete = isCompleteBrand(values);\n if (complete) {\n last.current = auditTheme(\n {\n brandPrimary: values.primary,\n brandSecondary: values.secondary,\n brandAccent: values.accent,\n brandHighlight: values.highlight,\n brandSuccess: values.success,\n greyScale: values.greyScale,\n },\n { target },\n );\n }\n\n const checks = last.current;\n const stale = Boolean(checks) && !complete;\n const failing = checks?.filter((check) => !check.meetsTarget).length ?? 0;\n const summary = !checks\n ? \"Enter five hex colours to measure contrast.\"\n : failing === 0\n ? `Every pairing meets ${target}:1`\n : `${failing} of ${checks.length} pairings below ${target}:1`;\n\n return (\n <div className=\"field-type bl-contrast-report\">\n <style href=\"bl-contrast-report\" precedence=\"default\">\n {CSS}\n </style>\n <p className=\"bl-contrast-report__summary\">{summary}</p>\n {stale ? (\n <p className=\"bl-contrast-report__stale\">Showing the last complete audit while a colour is being typed.</p>\n ) : null}\n {checks ? (\n <ul className=\"bl-contrast-report__list\">\n {checks.flatMap((check) =>\n ([\"light\", \"dark\"] as const).map((mode) => {\n const sample = check[mode];\n const failingRow = sample.ratio < check.target;\n const verdict = contrastVerdict(sample, check.target);\n return (\n <li\n key={`${check.id}-${mode}`}\n className=\"bl-contrast-report__row\"\n data-failing={failingRow ? \"\" : undefined}\n >\n <span\n aria-hidden=\"true\"\n className=\"bl-contrast-report__swatch\"\n style={{\n color: `hsl(${sample.foreground})`,\n backgroundColor: `hsl(${sample.background})`,\n }}\n >\n Aa\n </span>\n <span className=\"bl-contrast-report__ratio\">{sample.ratio.toFixed(2)}:1</span>\n <span className=\"bl-contrast-report__level\">\n <LevelIcon level={sample.level} />\n {contrastLevelLabel(sample.level)}\n </span>\n <span className=\"bl-contrast-report__verdict\">{verdict.text}</span>\n <span className=\"bl-contrast-report__label\">\n {check.label} ({mode === \"light\" ? \"Light\" : \"Dark\"})\n </span>\n </li>\n );\n }),\n )}\n </ul>\n ) : null}\n </div>\n );\n};\n\nconst CSS = `\n.bl-contrast-report{margin:calc(var(--base) * .6) 0}\n.bl-contrast-report__summary{margin:0 0 calc(var(--base) * .3);font-weight:600}\n.bl-contrast-report__stale{margin:0 0 calc(var(--base) * .3);color:var(--theme-elevation-500);font-size:12px}\n.bl-contrast-report__list{margin:0;padding:0;list-style:none;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-elevation-0)}\n.bl-contrast-report__row{display:flex;flex-wrap:wrap;align-items:center;gap:calc(var(--base) * .4);padding:calc(var(--base) * .35) calc(var(--base) * .6);border-top:1px solid var(--theme-elevation-100)}\n.bl-contrast-report__row:first-child{border-top:0}\n.bl-contrast-report__row[data-failing]{background:var(--theme-error-50)}\n.bl-contrast-report__swatch{flex:none;display:inline-flex;align-items:center;justify-content:center;width:2.5rem;height:1.75rem;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);font-weight:600}\n.bl-contrast-report__ratio{width:4.5rem;font-variant-numeric:tabular-nums}\n.bl-contrast-report__level{display:inline-flex;align-items:center;gap:4px;width:5.5rem}\n.bl-contrast-report__verdict{width:6rem;font-weight:600}\n.bl-contrast-report__label{flex:1;min-width:12rem;color:var(--theme-elevation-500)}\n`;\n"],"mappings":"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"}