@bison-lab/payload-core 0.1.0 → 3.8.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 +132 -18
- package/dist/admin.d.mts +28 -0
- package/dist/admin.d.mts.map +1 -0
- package/dist/admin.mjs +433 -0
- package/dist/admin.mjs.map +1 -0
- package/dist/index.d.mts +57 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +189 -1
- package/dist/index.mjs.map +1 -1
- package/dist/metadata.d.mts +1 -1
- package/dist/react.d.mts +32 -0
- package/dist/react.d.mts.map +1 -0
- package/dist/react.mjs +132 -0
- package/dist/react.mjs.map +1 -0
- package/dist/theme.d.mts +38 -0
- package/dist/theme.d.mts.map +1 -0
- package/dist/theme.mjs +39 -0
- package/dist/theme.mjs.map +1 -0
- package/dist/{title-Wut0nzJQ.d.mts → title-B2-gWQZd.d.mts} +1 -1
- package/dist/{title-Wut0nzJQ.d.mts.map → title-B2-gWQZd.d.mts.map} +1 -1
- package/dist/types-DWwL-JEr.mjs +76 -0
- package/dist/types-DWwL-JEr.mjs.map +1 -0
- package/dist/types-pFpmDeSA.d.mts +91 -0
- package/dist/types-pFpmDeSA.d.mts.map +1 -0
- package/package.json +53 -4
|
@@ -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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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { a as SeoImageSize, i as SeoImageDoc, n as titleTemplate, o as SeoImageValue, r as MediaId, s as SeoMeta, t as documentTitle } from "./title-
|
|
2
|
-
import {
|
|
1
|
+
import { a as SeoImageSize, i as SeoImageDoc, n as titleTemplate, o as SeoImageValue, r as MediaId, s as SeoMeta, t as documentTitle } from "./title-B2-gWQZd.mjs";
|
|
2
|
+
import { i as ThemeDoc, n as EditableTheme, r as THEME_SLUG, s as ThemeUploadDoc, t as CreateThemeOptions } from "./types-pFpmDeSA.mjs";
|
|
3
|
+
import { ThemeConfig } from "@bison-lab/tokens";
|
|
4
|
+
import { CheckboxField, CollectionSlug, GlobalConfig, Plugin, UploadCollectionSlug } from "payload";
|
|
3
5
|
|
|
4
6
|
//#region src/seo/plugin.d.ts
|
|
5
7
|
/**
|
|
@@ -102,5 +104,57 @@ declare const DESCRIPTION_LENGTH = 155;
|
|
|
102
104
|
*/
|
|
103
105
|
declare function truncateAtWord(text: string, max?: number): string;
|
|
104
106
|
//#endregion
|
|
105
|
-
|
|
107
|
+
//#region src/theme/global.d.ts
|
|
108
|
+
/**
|
|
109
|
+
* Settings → Theme: the five brand colours, grey scale, presets, default
|
|
110
|
+
* theme, body and heading fonts, and optional logo. Access is passed in
|
|
111
|
+
* (`canManageBrand` for update). Drafts autosave; the public site reads
|
|
112
|
+
* the published version through `getPublishedTheme`.
|
|
113
|
+
*
|
|
114
|
+
* `previewPath` wires `admin.livePreview` when the site has a route.
|
|
115
|
+
*/
|
|
116
|
+
declare function createTheme(options: CreateThemeOptions): GlobalConfig;
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/theme/seed.d.ts
|
|
119
|
+
interface SeedThemePayload {
|
|
120
|
+
updateGlobal: (args: {
|
|
121
|
+
slug: string;
|
|
122
|
+
data: ThemeDoc;
|
|
123
|
+
draft?: boolean;
|
|
124
|
+
}) => Promise<unknown>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Writes a published Theme from the seed. For a site's migration `up()`,
|
|
128
|
+
* so the row exists on deploy and renders what `bison-theme.css` rendered.
|
|
129
|
+
*/
|
|
130
|
+
declare function seedTheme(payload: SeedThemePayload, seed: ThemeConfig): Promise<unknown>;
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/theme/preview.d.ts
|
|
133
|
+
/**
|
|
134
|
+
* Device sizes the Theme (and, later, Pages) live-preview toolbar offers.
|
|
135
|
+
* One list so the panes match.
|
|
136
|
+
*/
|
|
137
|
+
declare const THEME_PREVIEW_BREAKPOINTS: readonly [{
|
|
138
|
+
readonly label: "Mobile";
|
|
139
|
+
readonly name: "mobile";
|
|
140
|
+
readonly width: 375;
|
|
141
|
+
readonly height: 667;
|
|
142
|
+
}, {
|
|
143
|
+
readonly label: "Tablet";
|
|
144
|
+
readonly name: "tablet";
|
|
145
|
+
readonly width: 768;
|
|
146
|
+
readonly height: 1024;
|
|
147
|
+
}, {
|
|
148
|
+
readonly label: "Desktop";
|
|
149
|
+
readonly name: "desktop";
|
|
150
|
+
readonly width: 1440;
|
|
151
|
+
readonly height: 900;
|
|
152
|
+
}];
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/theme/fields.d.ts
|
|
155
|
+
declare const THEME_COLOR_FIELD = "@bison-lab/payload-core/admin#ColorField";
|
|
156
|
+
declare const THEME_FONT_FIELD = "@bison-lab/payload-core/admin#FontField";
|
|
157
|
+
declare const THEME_CONTRAST_REPORT = "@bison-lab/payload-core/admin#ContrastReport";
|
|
158
|
+
//#endregion
|
|
159
|
+
export { type CreateThemeOptions, DESCRIPTION_LENGTH, type EditableTheme, type MediaId, SHARE_IMAGE_SIZE, type SeoDoc, type SeoImageDoc, type SeoImageSize, type SeoImageValue, type SeoMeta, type SeoPluginOptions, THEME_COLOR_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_SLUG, type ThemeDoc, type ThemeUploadDoc, createTheme, documentTitle, firstImageIn, noIndexField, seedTheme, seoPlugin, titleTemplate, truncateAtWord };
|
|
106
160
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/seo/plugin.ts","../src/seo/fields.ts","../src/seo/share-image.ts","../src/seo/text.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/seo/plugin.ts","../src/seo/fields.ts","../src/seo/share-image.ts","../src/seo/text.ts","../src/theme/global.ts","../src/theme/seed.ts","../src/theme/preview.ts","../src/theme/fields.ts"],"mappings":";;;;;;;;;;AAoBA;;UAAiB,MAAA;EACf,KAAA;AAAA;AAAA,UAGe,gBAAA,cAA8B,MAAA,GAAS,MAAA;EAAvB;EAE/B,QAAA;EAF6C;;;;;;EAS7C,MAAA,GAAS,GAAA,EAAK,IAAA;EAiBM;;;;;EAXpB,YAAA,IAAgB,GAAA,EAAK,IAAA;EAbrB;;;;;;EAoBA,QAAA,IAAY,GAAA,EAAK,IAAA,KAAS,OAAA;EAA1B;EAEA,WAAA,GAAc,cAAA;EAFF;EAIZ,iBAAA,GAAoB,oBAAA;AAAA;;;;;;AAetB;;;;;;;iBAAgB,SAAA,cAAuB,MAAA,GAAS,MAAA,CAAA,CAAA;EAC9C,QAAA;EACA,MAAA;EACA,YAAA;EACA,QAAA;EACA,WAAA;EACA;AAAA,GACC,gBAAA,CAAiB,IAAA,IAAQ,MAAA;;;;;;;iBA6BZ,YAAA,CACd,MAAA,WACA,KAAA,YACC,OAAA;;;;;;;;cCjGU,YAAA,EAAc,aAAA;;;;;;;;;ADa3B;;cEVa,gBAAA;;;;;;;;;cCTA,kBAAA;;;;;AHmBb;;iBGXgB,cAAA,CAAe,IAAA,UAAc,GAAA;;;;;;;AHW7C;;;;iBIsEgB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,YAAA;;;UCpFzC,gBAAA;EACf,YAAA,GAAe,IAAA;IACb,IAAA;IACA,IAAA,EAAM,QAAA;IACN,KAAA;EAAA,MACI,OAAA;AAAA;;;ALaR;;iBKNsB,SAAA,CAAU,OAAA,EAAS,gBAAA,EAAkB,IAAA,EAAM,WAAA,GAAc,OAAA;;;;;;;cCdlE,yBAAA;EAAA;;;;;;;;;;;;;;;;;cCAA,iBAAA;AAAA,cACA,gBAAA;AAAA,cACA,qBAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-C4ILz4p2.mjs";
|
|
2
|
+
import { a as THEME_COLOR_FIELD, c as headingSelectValue, i as validateThemeHex, n as docFromConfig, o as THEME_CONTRAST_REPORT, r as themeConfigFromDoc, s as THEME_FONT_FIELD, t as THEME_SLUG } from "./types-DWwL-JEr.mjs";
|
|
2
3
|
import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
|
|
4
|
+
import { catalog } from "@bison-lab/fonts";
|
|
5
|
+
import { presetHints } from "@bison-lab/tokens";
|
|
3
6
|
//#region src/seo/fields.ts
|
|
4
7
|
/**
|
|
5
8
|
* The index switch, last in the SEO tab. Off by default: a page is public
|
|
@@ -76,6 +79,191 @@ function firstImageIn(blocks, field = "image") {
|
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
//#endregion
|
|
79
|
-
|
|
82
|
+
//#region src/theme/preview.ts
|
|
83
|
+
/**
|
|
84
|
+
* Device sizes the Theme (and, later, Pages) live-preview toolbar offers.
|
|
85
|
+
* One list so the panes match.
|
|
86
|
+
*/
|
|
87
|
+
const THEME_PREVIEW_BREAKPOINTS = [
|
|
88
|
+
{
|
|
89
|
+
label: "Mobile",
|
|
90
|
+
name: "mobile",
|
|
91
|
+
width: 375,
|
|
92
|
+
height: 667
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: "Tablet",
|
|
96
|
+
name: "tablet",
|
|
97
|
+
width: 768,
|
|
98
|
+
height: 1024
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: "Desktop",
|
|
102
|
+
name: "desktop",
|
|
103
|
+
width: 1440,
|
|
104
|
+
height: 900
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/theme/global.ts
|
|
109
|
+
function requireAccess(options) {
|
|
110
|
+
const read = options.access?.read;
|
|
111
|
+
const update = options.access?.update;
|
|
112
|
+
if (!read || !update) throw new Error("createTheme requires access.read and access.update. Pass the site's predicates; canManageBrand is the intended update predicate until BIS-43 moves src/platform here.");
|
|
113
|
+
return {
|
|
114
|
+
read,
|
|
115
|
+
update
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function hintSelect(name, label, hints, defaultValue) {
|
|
119
|
+
return {
|
|
120
|
+
name,
|
|
121
|
+
type: "select",
|
|
122
|
+
label,
|
|
123
|
+
required: true,
|
|
124
|
+
defaultValue,
|
|
125
|
+
options: Object.entries(hints).map(([value, { label: optionLabel, hint }]) => ({
|
|
126
|
+
label: `${optionLabel} — ${hint}`,
|
|
127
|
+
value
|
|
128
|
+
}))
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function colorField(name, label, defaultValue) {
|
|
132
|
+
return {
|
|
133
|
+
name,
|
|
134
|
+
type: "text",
|
|
135
|
+
label,
|
|
136
|
+
required: true,
|
|
137
|
+
defaultValue,
|
|
138
|
+
validate: validateThemeHex,
|
|
139
|
+
admin: { components: { Field: THEME_COLOR_FIELD } }
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
|
|
143
|
+
const options = fonts.map((font) => ({
|
|
144
|
+
label: font.family,
|
|
145
|
+
value: font.id
|
|
146
|
+
}));
|
|
147
|
+
return {
|
|
148
|
+
name,
|
|
149
|
+
type: "select",
|
|
150
|
+
label,
|
|
151
|
+
required: !sameAsBody,
|
|
152
|
+
defaultValue,
|
|
153
|
+
options: sameAsBody ? [{
|
|
154
|
+
label: "Same as body",
|
|
155
|
+
value: ""
|
|
156
|
+
}, ...options] : options,
|
|
157
|
+
admin: {
|
|
158
|
+
components: { Field: THEME_FONT_FIELD },
|
|
159
|
+
custom: {
|
|
160
|
+
fontsBaseUrl,
|
|
161
|
+
ids: fonts.map((font) => font.id),
|
|
162
|
+
sameAsBody
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Settings → Theme: the five brand colours, grey scale, presets, default
|
|
169
|
+
* theme, body and heading fonts, and optional logo. Access is passed in
|
|
170
|
+
* (`canManageBrand` for update). Drafts autosave; the public site reads
|
|
171
|
+
* the published version through `getPublishedTheme`.
|
|
172
|
+
*
|
|
173
|
+
* `previewPath` wires `admin.livePreview` when the site has a route.
|
|
174
|
+
*/
|
|
175
|
+
function createTheme(options) {
|
|
176
|
+
const access = requireAccess(options);
|
|
177
|
+
const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 4.5, previewPath, logo, onPublish } = options;
|
|
178
|
+
const fields = [
|
|
179
|
+
{
|
|
180
|
+
name: "brand",
|
|
181
|
+
type: "group",
|
|
182
|
+
label: "Brand colours",
|
|
183
|
+
fields: [
|
|
184
|
+
colorField("primary", "Primary", seed.brandPrimary),
|
|
185
|
+
colorField("secondary", "Secondary", seed.brandSecondary),
|
|
186
|
+
colorField("accent", "Accent", seed.brandAccent),
|
|
187
|
+
colorField("highlight", "Highlight", seed.brandHighlight),
|
|
188
|
+
colorField("success", "Success", seed.brandSuccess)
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "contrast",
|
|
193
|
+
type: "ui",
|
|
194
|
+
admin: {
|
|
195
|
+
components: { Field: THEME_CONTRAST_REPORT },
|
|
196
|
+
custom: { target: contrastTarget }
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
hintSelect("greyScale", "Grey scale", presetHints.greyScale, seed.greyScale),
|
|
200
|
+
hintSelect("radius", "Radius", presetHints.radius, seed.radius),
|
|
201
|
+
hintSelect("shadow", "Shadow", presetHints.shadow, seed.shadow),
|
|
202
|
+
hintSelect("motion", "Motion", presetHints.motion, seed.motion),
|
|
203
|
+
hintSelect("density", "Density", presetHints.density, seed.density),
|
|
204
|
+
hintSelect("defaultTheme", "Default theme", presetHints.defaultTheme, seed.defaultTheme),
|
|
205
|
+
{
|
|
206
|
+
name: "fonts",
|
|
207
|
+
type: "group",
|
|
208
|
+
label: "Fonts",
|
|
209
|
+
fields: [fontField("body", "Body", seed.fontBody, fonts, fontsBaseUrl, false), fontField("heading", "Heading", headingSelectValue(seed.fontHeading), fonts, fontsBaseUrl, true)]
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
if (logo) fields.push({
|
|
213
|
+
name: "logo",
|
|
214
|
+
type: "upload",
|
|
215
|
+
relationTo: logo.collection,
|
|
216
|
+
label: "Logo"
|
|
217
|
+
}, {
|
|
218
|
+
name: "logoMark",
|
|
219
|
+
type: "upload",
|
|
220
|
+
relationTo: logo.collection,
|
|
221
|
+
label: "Logo mark"
|
|
222
|
+
});
|
|
223
|
+
return {
|
|
224
|
+
slug: THEME_SLUG,
|
|
225
|
+
label: "Theme",
|
|
226
|
+
admin: {
|
|
227
|
+
group: "Settings",
|
|
228
|
+
custom: {
|
|
229
|
+
previewPath,
|
|
230
|
+
contrastTarget,
|
|
231
|
+
fontsBaseUrl
|
|
232
|
+
},
|
|
233
|
+
...previewPath ? { livePreview: {
|
|
234
|
+
url: previewPath,
|
|
235
|
+
breakpoints: [...THEME_PREVIEW_BREAKPOINTS]
|
|
236
|
+
} } : {}
|
|
237
|
+
},
|
|
238
|
+
versions: { drafts: { autosave: { interval: 800 } } },
|
|
239
|
+
access: {
|
|
240
|
+
read: access.read,
|
|
241
|
+
update: access.update,
|
|
242
|
+
readVersions: access.update
|
|
243
|
+
},
|
|
244
|
+
fields,
|
|
245
|
+
hooks: { afterChange: [async ({ doc }) => {
|
|
246
|
+
if (!onPublish) return;
|
|
247
|
+
const themeDoc = doc;
|
|
248
|
+
if (themeDoc._status !== "published") return;
|
|
249
|
+
await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);
|
|
250
|
+
}] }
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/theme/seed.ts
|
|
255
|
+
/**
|
|
256
|
+
* Writes a published Theme from the seed. For a site's migration `up()`,
|
|
257
|
+
* so the row exists on deploy and renders what `bison-theme.css` rendered.
|
|
258
|
+
*/
|
|
259
|
+
async function seedTheme(payload, seed) {
|
|
260
|
+
return payload.updateGlobal({
|
|
261
|
+
slug: THEME_SLUG,
|
|
262
|
+
data: docFromConfig(seed),
|
|
263
|
+
draft: false
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
export { DESCRIPTION_LENGTH, SHARE_IMAGE_SIZE, THEME_COLOR_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_SLUG, createTheme, documentTitle, firstImageIn, noIndexField, seedTheme, seoPlugin, titleTemplate, truncateAtWord };
|
|
80
268
|
|
|
81
269
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["payloadSeoPlugin"],"sources":["../src/seo/fields.ts","../src/seo/text.ts","../src/seo/plugin.ts"],"sourcesContent":["import type { CheckboxField } from \"payload\";\n\n/**\n * The index switch, last in the SEO tab. Off by default: a page is public\n * unless an editor says otherwise. `pageMetadata` turns it into\n * `noindex, nofollow`; a site's sitemap should filter on it too.\n */\nexport const noIndexField: CheckboxField = {\n name: \"noIndex\",\n type: \"checkbox\",\n label: \"Hide this page from search engines\",\n defaultValue: false,\n admin: {\n description:\n \"Search engines will not list this page and the sitemap will leave it out. Anyone with the link can still open it.\",\n },\n};\n","/** The length a search result shows of a description before it is cut. */\nexport const DESCRIPTION_LENGTH = 155;\n\n/**\n * Prose cut to `max` characters at a word, with an ellipsis, so a generated\n * description does not end mid-syllable. Text that fits is returned trimmed\n * and whole. A first word longer than `max` is cut mid-word, since there is\n * no boundary to cut at.\n */\nexport function truncateAtWord(text: string, max = DESCRIPTION_LENGTH): string {\n const trimmed = text.trim();\n if (trimmed.length <= max) return trimmed;\n const cut = trimmed.slice(0, max);\n const lastSpace = cut.lastIndexOf(\" \");\n return `${lastSpace > 0 ? cut.slice(0, lastSpace) : cut}…`;\n}\n","import { seoPlugin as payloadSeoPlugin } from \"@payloadcms/plugin-seo\";\nimport type {\n GenerateDescription,\n GenerateImage,\n GenerateTitle,\n GenerateURL,\n} from \"@payloadcms/plugin-seo/types\";\nimport type { CollectionSlug, Plugin, UploadCollectionSlug } from \"payload\";\n\nimport { noIndexField } from \"./fields\";\nimport { truncateAtWord } from \"./text\";\nimport { documentTitle } from \"./title\";\nimport type { MediaId } from \"./types\";\n\n/**\n * What the plugin reads off a document: its title, for the generated meta\n * title. The site's generated `Page` is assignable to this; the `urlFor`,\n * `describeFrom` and `imageFor` callbacks read the rest, typed as the site\n * chooses through `TDoc`.\n */\nexport interface SeoDoc {\n title?: string | null;\n}\n\nexport interface SeoPluginOptions<TDoc extends SeoDoc = SeoDoc> {\n /** Appended to every generated title: `<page title> | <siteName>`. */\n siteName: string;\n /**\n * The absolute public URL of a document from the form's data, for the\n * search-result preview and the Generate button beside it. `undefined`\n * when the document has no address yet (a slug not typed), which leaves\n * the preview empty rather than pointing at the home page.\n */\n urlFor: (doc: TDoc) => string | undefined;\n /**\n * Prose to cut a generated description from, the first plain-text field a\n * page always has near the top (a hero's body, say). Cut at a word near\n * 155 characters. Without it the Generate button fills in nothing.\n */\n describeFrom?: (doc: TDoc) => string | null | undefined;\n /**\n * An image already on the page, for the Meta Image Generate button, so an\n * editor need not upload a second copy of the hero picture. `firstImageIn`\n * walks block rows for one. The upload chooser stays, so any other media\n * row can still be picked.\n */\n imageFor?: (doc: TDoc) => MediaId | null | undefined;\n /** Collections that get the SEO tab. Default `['pages']`. */\n collections?: CollectionSlug[];\n /** The upload collection the meta image comes from. Default `'media'`. */\n uploadsCollection?: UploadCollectionSlug;\n}\n\n/**\n * `@payloadcms/plugin-seo` configured the Bison Lab way: an SEO tab beside a\n * Content tab (never below the block editor), holding the overview with its\n * character counts, meta title, description and image with Generate buttons,\n * the search-result preview, and the `noIndex` switch.\n *\n * Every string the tab generates comes from the options, so a site spells\n * its name and its URL scheme once. Pin `@payloadcms/plugin-seo` to the same\n * version as `payload` in the site; the plugin's admin components are\n * resolved from the site's import map, so run `payload generate:importmap`\n * after adding it.\n */\nexport function seoPlugin<TDoc extends SeoDoc = SeoDoc>({\n siteName,\n urlFor,\n describeFrom,\n imageFor,\n collections = [\"pages\"],\n uploadsCollection = \"media\",\n}: SeoPluginOptions<TDoc>): Plugin {\n const generateTitle: GenerateTitle<TDoc> = ({ doc }) =>\n doc?.title ? documentTitle(siteName, doc.title) : \"\";\n const generateDescription: GenerateDescription<TDoc> = ({ doc }) =>\n truncateAtWord(describeFrom?.(doc) ?? \"\");\n const generateURL: GenerateURL<TDoc> = ({ doc }) => urlFor(doc) ?? \"\";\n // Only when the site can name one: the button appears with the function.\n const generateImage: GenerateImage<TDoc> | undefined = imageFor\n ? ({ doc }) => imageFor(doc) ?? \"\"\n : undefined;\n\n return payloadSeoPlugin({\n collections,\n uploadsCollection,\n tabbedUI: true,\n generateTitle,\n generateDescription,\n generateURL,\n ...(generateImage ? { generateImage } : {}),\n fields: ({ defaultFields }) => [...defaultFields, noIndexField],\n });\n}\n\n/**\n * The first image among some block rows, as a media id, for `imageFor`. Each\n * row is checked for `field` holding either a bare id or a populated upload\n * document; rows without one are skipped. Pass the page's hero and layout\n * together (`[...doc.hero, ...doc.layout]`) to search in reading order.\n */\nexport function firstImageIn(\n blocks: unknown,\n field = \"image\",\n): MediaId | undefined {\n if (!Array.isArray(blocks)) return undefined;\n for (const block of blocks) {\n if (typeof block !== \"object\" || block === null) continue;\n const value = (block as Record<string, unknown>)[field];\n const id =\n typeof value === \"object\" && value !== null && \"id\" in value\n ? (value as { id: unknown }).id\n : value;\n if (typeof id === \"number\" || (typeof id === \"string\" && id !== \"\"))\n return id;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;AAOA,MAAa,eAA8B;CACzC,MAAM;CACN,MAAM;CACN,OAAO;CACP,cAAc;CACd,OAAO,EACL,aACE,qHACH;CACF;;;;ACfD,MAAa,qBAAqB;;;;;;;AAQlC,SAAgB,eAAe,MAAc,MAAA,KAAkC;CAC7E,MAAM,UAAU,KAAK,MAAM;AAC3B,KAAI,QAAQ,UAAU,IAAK,QAAO;CAClC,MAAM,MAAM,QAAQ,MAAM,GAAG,IAAI;CACjC,MAAM,YAAY,IAAI,YAAY,IAAI;AACtC,QAAO,GAAG,YAAY,IAAI,IAAI,MAAM,GAAG,UAAU,GAAG,IAAI;;;;;;;;;;;;;;;;ACmD1D,SAAgB,UAAwC,EACtD,UACA,QACA,cACA,UACA,cAAc,CAAC,QAAQ,EACvB,oBAAoB,WACa;CACjC,MAAM,iBAAsC,EAAE,UAC5C,KAAK,QAAQ,cAAc,UAAU,IAAI,MAAM,GAAG;CACpD,MAAM,uBAAkD,EAAE,UACxD,eAAe,eAAe,IAAI,IAAI,GAAG;CAC3C,MAAM,eAAkC,EAAE,UAAU,OAAO,IAAI,IAAI;CAEnE,MAAM,gBAAiD,YAClD,EAAE,UAAU,SAAS,IAAI,IAAI,KAC9B,KAAA;AAEJ,QAAOA,YAAiB;EACtB;EACA;EACA,UAAU;EACV;EACA;EACA;EACA,GAAI,gBAAgB,EAAE,eAAe,GAAG,EAAE;EAC1C,SAAS,EAAE,oBAAoB,CAAC,GAAG,eAAe,aAAa;EAChE,CAAC;;;;;;;;AASJ,SAAgB,aACd,QACA,QAAQ,SACa;AACrB,KAAI,CAAC,MAAM,QAAQ,OAAO,CAAE,QAAO,KAAA;AACnC,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM;EACjD,MAAM,QAAS,MAAkC;EACjD,MAAM,KACJ,OAAO,UAAU,YAAY,UAAU,QAAQ,QAAQ,QAClD,MAA0B,KAC3B;AACN,MAAI,OAAO,OAAO,YAAa,OAAO,OAAO,YAAY,OAAO,GAC9D,QAAO"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["payloadSeoPlugin"],"sources":["../src/seo/fields.ts","../src/seo/text.ts","../src/seo/plugin.ts","../src/theme/preview.ts","../src/theme/global.ts","../src/theme/seed.ts"],"sourcesContent":["import type { CheckboxField } from \"payload\";\n\n/**\n * The index switch, last in the SEO tab. Off by default: a page is public\n * unless an editor says otherwise. `pageMetadata` turns it into\n * `noindex, nofollow`; a site's sitemap should filter on it too.\n */\nexport const noIndexField: CheckboxField = {\n name: \"noIndex\",\n type: \"checkbox\",\n label: \"Hide this page from search engines\",\n defaultValue: false,\n admin: {\n description:\n \"Search engines will not list this page and the sitemap will leave it out. Anyone with the link can still open it.\",\n },\n};\n","/** The length a search result shows of a description before it is cut. */\nexport const DESCRIPTION_LENGTH = 155;\n\n/**\n * Prose cut to `max` characters at a word, with an ellipsis, so a generated\n * description does not end mid-syllable. Text that fits is returned trimmed\n * and whole. A first word longer than `max` is cut mid-word, since there is\n * no boundary to cut at.\n */\nexport function truncateAtWord(text: string, max = DESCRIPTION_LENGTH): string {\n const trimmed = text.trim();\n if (trimmed.length <= max) return trimmed;\n const cut = trimmed.slice(0, max);\n const lastSpace = cut.lastIndexOf(\" \");\n return `${lastSpace > 0 ? cut.slice(0, lastSpace) : cut}…`;\n}\n","import { seoPlugin as payloadSeoPlugin } from \"@payloadcms/plugin-seo\";\nimport type {\n GenerateDescription,\n GenerateImage,\n GenerateTitle,\n GenerateURL,\n} from \"@payloadcms/plugin-seo/types\";\nimport type { CollectionSlug, Plugin, UploadCollectionSlug } from \"payload\";\n\nimport { noIndexField } from \"./fields\";\nimport { truncateAtWord } from \"./text\";\nimport { documentTitle } from \"./title\";\nimport type { MediaId } from \"./types\";\n\n/**\n * What the plugin reads off a document: its title, for the generated meta\n * title. The site's generated `Page` is assignable to this; the `urlFor`,\n * `describeFrom` and `imageFor` callbacks read the rest, typed as the site\n * chooses through `TDoc`.\n */\nexport interface SeoDoc {\n title?: string | null;\n}\n\nexport interface SeoPluginOptions<TDoc extends SeoDoc = SeoDoc> {\n /** Appended to every generated title: `<page title> | <siteName>`. */\n siteName: string;\n /**\n * The absolute public URL of a document from the form's data, for the\n * search-result preview and the Generate button beside it. `undefined`\n * when the document has no address yet (a slug not typed), which leaves\n * the preview empty rather than pointing at the home page.\n */\n urlFor: (doc: TDoc) => string | undefined;\n /**\n * Prose to cut a generated description from, the first plain-text field a\n * page always has near the top (a hero's body, say). Cut at a word near\n * 155 characters. Without it the Generate button fills in nothing.\n */\n describeFrom?: (doc: TDoc) => string | null | undefined;\n /**\n * An image already on the page, for the Meta Image Generate button, so an\n * editor need not upload a second copy of the hero picture. `firstImageIn`\n * walks block rows for one. The upload chooser stays, so any other media\n * row can still be picked.\n */\n imageFor?: (doc: TDoc) => MediaId | null | undefined;\n /** Collections that get the SEO tab. Default `['pages']`. */\n collections?: CollectionSlug[];\n /** The upload collection the meta image comes from. Default `'media'`. */\n uploadsCollection?: UploadCollectionSlug;\n}\n\n/**\n * `@payloadcms/plugin-seo` configured the Bison Lab way: an SEO tab beside a\n * Content tab (never below the block editor), holding the overview with its\n * character counts, meta title, description and image with Generate buttons,\n * the search-result preview, and the `noIndex` switch.\n *\n * Every string the tab generates comes from the options, so a site spells\n * its name and its URL scheme once. Pin `@payloadcms/plugin-seo` to the same\n * version as `payload` in the site; the plugin's admin components are\n * resolved from the site's import map, so run `payload generate:importmap`\n * after adding it.\n */\nexport function seoPlugin<TDoc extends SeoDoc = SeoDoc>({\n siteName,\n urlFor,\n describeFrom,\n imageFor,\n collections = [\"pages\"],\n uploadsCollection = \"media\",\n}: SeoPluginOptions<TDoc>): Plugin {\n const generateTitle: GenerateTitle<TDoc> = ({ doc }) =>\n doc?.title ? documentTitle(siteName, doc.title) : \"\";\n const generateDescription: GenerateDescription<TDoc> = ({ doc }) =>\n truncateAtWord(describeFrom?.(doc) ?? \"\");\n const generateURL: GenerateURL<TDoc> = ({ doc }) => urlFor(doc) ?? \"\";\n // Only when the site can name one: the button appears with the function.\n const generateImage: GenerateImage<TDoc> | undefined = imageFor\n ? ({ doc }) => imageFor(doc) ?? \"\"\n : undefined;\n\n return payloadSeoPlugin({\n collections,\n uploadsCollection,\n tabbedUI: true,\n generateTitle,\n generateDescription,\n generateURL,\n ...(generateImage ? { generateImage } : {}),\n fields: ({ defaultFields }) => [...defaultFields, noIndexField],\n });\n}\n\n/**\n * The first image among some block rows, as a media id, for `imageFor`. Each\n * row is checked for `field` holding either a bare id or a populated upload\n * document; rows without one are skipped. Pass the page's hero and layout\n * together (`[...doc.hero, ...doc.layout]`) to search in reading order.\n */\nexport function firstImageIn(\n blocks: unknown,\n field = \"image\",\n): MediaId | undefined {\n if (!Array.isArray(blocks)) return undefined;\n for (const block of blocks) {\n if (typeof block !== \"object\" || block === null) continue;\n const value = (block as Record<string, unknown>)[field];\n const id =\n typeof value === \"object\" && value !== null && \"id\" in value\n ? (value as { id: unknown }).id\n : value;\n if (typeof id === \"number\" || (typeof id === \"string\" && id !== \"\"))\n return id;\n }\n return undefined;\n}\n","/**\n * Device sizes the Theme (and, later, Pages) live-preview toolbar offers.\n * One list so the panes match.\n */\nexport const THEME_PREVIEW_BREAKPOINTS = [\n { label: \"Mobile\", name: \"mobile\", width: 375, height: 667 },\n { label: \"Tablet\", name: \"tablet\", width: 768, height: 1024 },\n { label: \"Desktop\", name: \"desktop\", width: 1440, height: 900 },\n] as const;\n","import { catalog, type FontEntry } from \"@bison-lab/fonts\";\nimport { presetHints } from \"@bison-lab/tokens\";\nimport type { Field, GlobalConfig, SelectField, TextField } from \"payload\";\n\nimport {\n SAME_AS_BODY,\n THEME_COLOR_FIELD,\n THEME_CONTRAST_REPORT,\n THEME_FONT_FIELD,\n headingSelectValue,\n} from \"./fields\";\nimport { themeConfigFromDoc, validateThemeHex } from \"./map\";\nimport { THEME_PREVIEW_BREAKPOINTS } from \"./preview\";\nimport { THEME_SLUG, type CreateThemeOptions, type ThemeDoc } from \"./types\";\n\nfunction requireAccess(options: CreateThemeOptions): CreateThemeOptions[\"access\"] {\n const read = options.access?.read;\n const update = options.access?.update;\n if (!read || !update) {\n throw new Error(\n \"createTheme requires access.read and access.update. Pass the site's predicates; canManageBrand is the intended update predicate until BIS-43 moves src/platform here.\",\n );\n }\n return { read, update };\n}\n\nfunction hintSelect<K extends string>(\n name: string,\n label: string,\n hints: Record<K, { label: string; hint: string }>,\n defaultValue: K,\n): SelectField {\n return {\n name,\n type: \"select\",\n label,\n required: true,\n defaultValue,\n options: (Object.entries(hints) as [K, { label: string; hint: string }][]).map(\n ([value, { label: optionLabel, hint }]) => ({\n label: `${optionLabel} — ${hint}`,\n value,\n }),\n ),\n };\n}\n\nfunction colorField(name: string, label: string, defaultValue: string): TextField {\n return {\n name,\n type: \"text\",\n label,\n required: true,\n defaultValue,\n validate: validateThemeHex,\n admin: { components: { Field: THEME_COLOR_FIELD } },\n };\n}\n\nfunction fontField(\n name: string,\n label: string,\n defaultValue: string,\n fonts: readonly FontEntry[],\n fontsBaseUrl: string,\n sameAsBody: boolean,\n): SelectField {\n const options = fonts.map((font) => ({ label: font.family, value: font.id }));\n return {\n name,\n type: \"select\",\n label,\n required: !sameAsBody,\n defaultValue,\n options: sameAsBody ? [{ label: \"Same as body\", value: SAME_AS_BODY }, ...options] : options,\n admin: {\n components: { Field: THEME_FONT_FIELD },\n custom: { fontsBaseUrl, ids: fonts.map((font) => font.id), sameAsBody },\n },\n };\n}\n\n/**\n * Settings → Theme: the five brand colours, grey scale, presets, default\n * theme, body and heading fonts, and optional logo. Access is passed in\n * (`canManageBrand` for update). Drafts autosave; the public site reads\n * the published version through `getPublishedTheme`.\n *\n * `previewPath` wires `admin.livePreview` when the site has a route.\n */\nexport function createTheme(options: CreateThemeOptions): GlobalConfig {\n const access = requireAccess(options);\n const {\n seed,\n fonts = catalog,\n fontsBaseUrl = \"/fonts\",\n contrastTarget = 4.5,\n previewPath,\n logo,\n onPublish,\n } = options;\n\n const fields: Field[] = [\n {\n name: \"brand\",\n type: \"group\",\n label: \"Brand colours\",\n fields: [\n colorField(\"primary\", \"Primary\", seed.brandPrimary),\n colorField(\"secondary\", \"Secondary\", seed.brandSecondary),\n colorField(\"accent\", \"Accent\", seed.brandAccent),\n colorField(\"highlight\", \"Highlight\", seed.brandHighlight),\n colorField(\"success\", \"Success\", seed.brandSuccess),\n ],\n },\n {\n name: \"contrast\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_CONTRAST_REPORT },\n custom: { target: contrastTarget },\n },\n },\n hintSelect(\"greyScale\", \"Grey scale\", presetHints.greyScale, seed.greyScale),\n hintSelect(\"radius\", \"Radius\", presetHints.radius, seed.radius),\n hintSelect(\"shadow\", \"Shadow\", presetHints.shadow, seed.shadow),\n hintSelect(\"motion\", \"Motion\", presetHints.motion, seed.motion),\n hintSelect(\"density\", \"Density\", presetHints.density, seed.density),\n hintSelect(\"defaultTheme\", \"Default theme\", presetHints.defaultTheme, seed.defaultTheme),\n {\n name: \"fonts\",\n type: \"group\",\n label: \"Fonts\",\n fields: [\n fontField(\"body\", \"Body\", seed.fontBody, fonts, fontsBaseUrl, false),\n fontField(\n \"heading\",\n \"Heading\",\n headingSelectValue(seed.fontHeading),\n fonts,\n fontsBaseUrl,\n true,\n ),\n ],\n },\n ];\n\n if (logo) {\n fields.push(\n {\n name: \"logo\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Logo\",\n },\n {\n name: \"logoMark\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Logo mark\",\n },\n );\n }\n\n return {\n slug: THEME_SLUG,\n label: \"Theme\",\n admin: {\n group: \"Settings\",\n custom: { previewPath, contrastTarget, fontsBaseUrl },\n ...(previewPath\n ? { livePreview: { url: previewPath, breakpoints: [...THEME_PREVIEW_BREAKPOINTS] } }\n : {}),\n },\n versions: { drafts: { autosave: { interval: 800 } } },\n access: {\n read: access.read,\n update: access.update,\n readVersions: access.update,\n },\n fields,\n hooks: {\n afterChange: [\n async ({ doc }) => {\n if (!onPublish) return;\n const themeDoc = doc as ThemeDoc;\n if (themeDoc._status !== \"published\") return;\n await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);\n },\n ],\n },\n };\n}\n","import type { ThemeConfig } from \"@bison-lab/tokens\";\n\nimport { docFromConfig } from \"./map\";\nimport { THEME_SLUG } from \"./types\";\nimport type { ThemeDoc } from \"./types\";\n\nexport interface SeedThemePayload {\n updateGlobal: (args: {\n slug: string;\n data: ThemeDoc;\n draft?: boolean;\n }) => Promise<unknown>;\n}\n\n/**\n * Writes a published Theme from the seed. For a site's migration `up()`,\n * so the row exists on deploy and renders what `bison-theme.css` rendered.\n */\nexport async function seedTheme(payload: SeedThemePayload, seed: ThemeConfig): Promise<unknown> {\n return payload.updateGlobal({\n slug: THEME_SLUG,\n data: docFromConfig(seed),\n draft: false,\n });\n}\n"],"mappings":"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"}
|
package/dist/metadata.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as SeoImageSize, i as SeoImageDoc, n as titleTemplate, o as SeoImageValue, r as MediaId, s as SeoMeta, t as documentTitle } from "./title-
|
|
1
|
+
import { a as SeoImageSize, i as SeoImageDoc, n as titleTemplate, o as SeoImageValue, r as MediaId, s as SeoMeta, t as documentTitle } from "./title-B2-gWQZd.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/seo/metadata.d.ts
|
|
4
4
|
/** A page as the reader needs it; a site's generated `Page` is assignable. */
|
package/dist/react.d.mts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
import { ThemeConfig } from "@bison-lab/tokens";
|
|
3
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/react/theme-preview.d.ts
|
|
6
|
+
interface ThemePreviewProps {
|
|
7
|
+
/** Published theme from `getPublishedTheme`. Shown until a live-preview message arrives. */
|
|
8
|
+
theme: ThemeConfig;
|
|
9
|
+
/** The site's `bison.config.json`. Fills fields a live message leaves empty. */
|
|
10
|
+
seed: ThemeConfig;
|
|
11
|
+
/** Where the site's `serveFont` route answers. */
|
|
12
|
+
fontsBaseUrl: string;
|
|
13
|
+
/** Absolute origin of the Payload server. Required by `useLivePreview`. */
|
|
14
|
+
serverURL: string;
|
|
15
|
+
/** Body-text target `ContrastStrip` judges against. Default `4.5`. */
|
|
16
|
+
contrastTarget?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The Theme document's preview pane, and the site's design-system page.
|
|
20
|
+
* Rebuilds `themeHead` in the browser from each live-preview message.
|
|
21
|
+
* A half-typed hex keeps the last complete stylesheet.
|
|
22
|
+
*/
|
|
23
|
+
declare function ThemePreview({
|
|
24
|
+
theme,
|
|
25
|
+
seed,
|
|
26
|
+
fontsBaseUrl,
|
|
27
|
+
serverURL,
|
|
28
|
+
contrastTarget
|
|
29
|
+
}: ThemePreviewProps): react_jsx_runtime0.JSX.Element;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ThemePreview, type ThemePreviewProps };
|
|
32
|
+
//# sourceMappingURL=react.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.mts","names":[],"sources":["../src/react/theme-preview.tsx"],"mappings":";;;;;UASiB,iBAAA;;EAEf,KAAA,EAAO,WAAA;EAFQ;EAIf,IAAA,EAAM,WAAA;;EAEN,YAAA;EAJA;EAMA,SAAA;EAJA;EAMA,cAAA;AAAA;;;;;AAkBF;iBAAgB,YAAA,CAAA;EACd,KAAA;EACA,IAAA;EACA,YAAA;EACA,SAAA;EACA;AAAA,GACC,iBAAA,GAAiB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { auditTheme, buildThemeCss, isThemeHex, themeFontIds } from "@bison-lab/tokens";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { fontFaceCss, fontPreloads } from "@bison-lab/fonts";
|
|
6
|
+
import { useLivePreview } from "@payloadcms/live-preview-react";
|
|
7
|
+
import { ContrastStrip, ThemeShowcase } from "@bison-lab/ui";
|
|
8
|
+
//#region src/theme/head.ts
|
|
9
|
+
/**
|
|
10
|
+
* `@font-face` plus `buildThemeCss` as one stylesheet, and the preload
|
|
11
|
+
* list for the families the config names. A root layout (and the admin
|
|
12
|
+
* layout, if it should restyle too) renders `css` in a `<style>` and
|
|
13
|
+
* each preload as `<link rel="preload" as="font">`.
|
|
14
|
+
*/
|
|
15
|
+
function themeHead(config, options = {}) {
|
|
16
|
+
const fontsBaseUrl = options.fontsBaseUrl ?? "/fonts";
|
|
17
|
+
const ids = themeFontIds(config);
|
|
18
|
+
const faces = fontFaceCss(ids, fontsBaseUrl);
|
|
19
|
+
const theme = buildThemeCss(config, { attribute: options.attribute });
|
|
20
|
+
return {
|
|
21
|
+
css: faces ? `${faces}\n\n${theme}` : theme,
|
|
22
|
+
preloads: fontPreloads(ids, fontsBaseUrl)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/theme/map.ts
|
|
27
|
+
function pick(value, fallback) {
|
|
28
|
+
return value == null ? fallback : value;
|
|
29
|
+
}
|
|
30
|
+
function headingFromDoc(heading, seed) {
|
|
31
|
+
if (heading === void 0) return seed;
|
|
32
|
+
if (heading === null || heading === "") return null;
|
|
33
|
+
return heading;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Nested Theme document → flat `ThemeConfig`. Null or missing editor
|
|
37
|
+
* fields take the seed; `darkSelector` and `fontWeights` always come from
|
|
38
|
+
* the seed.
|
|
39
|
+
*/
|
|
40
|
+
function themeConfigFromDoc(doc, seed) {
|
|
41
|
+
const brand = doc?.brand;
|
|
42
|
+
return {
|
|
43
|
+
brandPrimary: pick(brand?.primary, seed.brandPrimary),
|
|
44
|
+
brandSecondary: pick(brand?.secondary, seed.brandSecondary),
|
|
45
|
+
brandAccent: pick(brand?.accent, seed.brandAccent),
|
|
46
|
+
brandHighlight: pick(brand?.highlight, seed.brandHighlight),
|
|
47
|
+
brandSuccess: pick(brand?.success, seed.brandSuccess),
|
|
48
|
+
greyScale: pick(doc?.greyScale, seed.greyScale),
|
|
49
|
+
radius: pick(doc?.radius, seed.radius),
|
|
50
|
+
shadow: pick(doc?.shadow, seed.shadow),
|
|
51
|
+
motion: pick(doc?.motion, seed.motion),
|
|
52
|
+
density: pick(doc?.density, seed.density),
|
|
53
|
+
defaultTheme: pick(doc?.defaultTheme, seed.defaultTheme),
|
|
54
|
+
fontBody: pick(doc?.fonts?.body, seed.fontBody),
|
|
55
|
+
fontHeading: headingFromDoc(doc?.fonts?.heading, seed.fontHeading),
|
|
56
|
+
darkSelector: seed.darkSelector,
|
|
57
|
+
fontWeights: seed.fontWeights
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** The document `seedTheme` writes so a fresh Global matches the seed config. */
|
|
61
|
+
function docFromConfig(config) {
|
|
62
|
+
return {
|
|
63
|
+
brand: {
|
|
64
|
+
primary: config.brandPrimary,
|
|
65
|
+
secondary: config.brandSecondary,
|
|
66
|
+
accent: config.brandAccent,
|
|
67
|
+
highlight: config.brandHighlight,
|
|
68
|
+
success: config.brandSuccess
|
|
69
|
+
},
|
|
70
|
+
greyScale: config.greyScale,
|
|
71
|
+
radius: config.radius,
|
|
72
|
+
shadow: config.shadow,
|
|
73
|
+
motion: config.motion,
|
|
74
|
+
density: config.density,
|
|
75
|
+
defaultTheme: config.defaultTheme,
|
|
76
|
+
fonts: {
|
|
77
|
+
body: config.fontBody,
|
|
78
|
+
heading: config.fontHeading
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/react/theme-preview.tsx
|
|
84
|
+
function isRenderableTheme(config) {
|
|
85
|
+
return isThemeHex(config.brandPrimary) && isThemeHex(config.brandSecondary) && isThemeHex(config.brandAccent) && isThemeHex(config.brandHighlight) && isThemeHex(config.brandSuccess);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The Theme document's preview pane, and the site's design-system page.
|
|
89
|
+
* Rebuilds `themeHead` in the browser from each live-preview message.
|
|
90
|
+
* A half-typed hex keeps the last complete stylesheet.
|
|
91
|
+
*/
|
|
92
|
+
function ThemePreview({ theme, seed, fontsBaseUrl, serverURL, contrastTarget = 4.5 }) {
|
|
93
|
+
const { data } = useLivePreview({
|
|
94
|
+
initialData: docFromConfig(theme),
|
|
95
|
+
serverURL
|
|
96
|
+
});
|
|
97
|
+
const mapped = themeConfigFromDoc(data, seed);
|
|
98
|
+
const last = useRef(theme);
|
|
99
|
+
if (isRenderableTheme(mapped)) last.current = mapped;
|
|
100
|
+
const config = last.current;
|
|
101
|
+
const css = themeHead(config, { fontsBaseUrl }).css;
|
|
102
|
+
const [mode, setMode] = useState("light");
|
|
103
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
104
|
+
role: "region",
|
|
105
|
+
"aria-label": "Theme preview",
|
|
106
|
+
"data-theme": mode,
|
|
107
|
+
className: "bl-theme-preview",
|
|
108
|
+
children: [
|
|
109
|
+
/* @__PURE__ */ jsx("style", { children: css }),
|
|
110
|
+
/* @__PURE__ */ jsxs("div", {
|
|
111
|
+
className: "bl-theme-preview__bar",
|
|
112
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
113
|
+
type: "button",
|
|
114
|
+
"aria-pressed": mode === "light",
|
|
115
|
+
onClick: () => setMode("light"),
|
|
116
|
+
children: "Light"
|
|
117
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
118
|
+
type: "button",
|
|
119
|
+
"aria-pressed": mode === "dark",
|
|
120
|
+
onClick: () => setMode("dark"),
|
|
121
|
+
children: "Dark"
|
|
122
|
+
})]
|
|
123
|
+
}),
|
|
124
|
+
/* @__PURE__ */ jsx(ThemeShowcase, {}),
|
|
125
|
+
/* @__PURE__ */ jsx(ContrastStrip, { checks: auditTheme(config, { target: contrastTarget }) })
|
|
126
|
+
]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
export { ThemePreview };
|
|
131
|
+
|
|
132
|
+
//# sourceMappingURL=react.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.mjs","names":[],"sources":["../src/theme/head.ts","../src/theme/map.ts","../src/react/theme-preview.tsx"],"sourcesContent":["import { fontFaceCss, fontPreloads } from \"@bison-lab/fonts\";\nimport { buildThemeCss, themeFontIds, type ThemeConfig } from \"@bison-lab/tokens\";\n\nimport type { ThemeHead, ThemeHeadOptions } from \"./types\";\n\n/**\n * `@font-face` plus `buildThemeCss` as one stylesheet, and the preload\n * list for the families the config names. A root layout (and the admin\n * layout, if it should restyle too) renders `css` in a `<style>` and\n * each preload as `<link rel=\"preload\" as=\"font\">`.\n */\nexport function themeHead(config: ThemeConfig, options: ThemeHeadOptions = {}): ThemeHead {\n const fontsBaseUrl = options.fontsBaseUrl ?? \"/fonts\";\n const ids = themeFontIds(config);\n const faces = fontFaceCss(ids, fontsBaseUrl);\n const theme = buildThemeCss(config, { attribute: options.attribute });\n return {\n css: faces ? `${faces}\\n\\n${theme}` : theme,\n preloads: fontPreloads(ids, fontsBaseUrl),\n };\n}\n","import { isThemeHex, type ThemeConfig } from \"@bison-lab/tokens\";\n\nimport { SAME_AS_BODY } from \"./fields\";\nimport type { ThemeDoc } from \"./types\";\n\nfunction pick<T>(value: T | null | undefined, fallback: T): T {\n return value == null ? fallback : value;\n}\n\nfunction headingFromDoc(\n heading: string | null | undefined,\n seed: ThemeConfig[\"fontHeading\"],\n): string | null {\n if (heading === undefined) return seed;\n if (heading === null || heading === SAME_AS_BODY) return null;\n return heading;\n}\n\n/**\n * Nested Theme document → flat `ThemeConfig`. Null or missing editor\n * fields take the seed; `darkSelector` and `fontWeights` always come from\n * the seed.\n */\nexport function themeConfigFromDoc(doc: ThemeDoc | null | undefined, seed: ThemeConfig): ThemeConfig {\n const brand = doc?.brand;\n return {\n brandPrimary: pick(brand?.primary, seed.brandPrimary),\n brandSecondary: pick(brand?.secondary, seed.brandSecondary),\n brandAccent: pick(brand?.accent, seed.brandAccent),\n brandHighlight: pick(brand?.highlight, seed.brandHighlight),\n brandSuccess: pick(brand?.success, seed.brandSuccess),\n greyScale: pick(doc?.greyScale, seed.greyScale),\n radius: pick(doc?.radius, seed.radius),\n shadow: pick(doc?.shadow, seed.shadow),\n motion: pick(doc?.motion, seed.motion),\n density: pick(doc?.density, seed.density),\n defaultTheme: pick(doc?.defaultTheme, seed.defaultTheme),\n fontBody: pick(doc?.fonts?.body, seed.fontBody),\n fontHeading: headingFromDoc(doc?.fonts?.heading, seed.fontHeading),\n darkSelector: seed.darkSelector,\n fontWeights: seed.fontWeights,\n };\n}\n\n/** The document `seedTheme` writes so a fresh Global matches the seed config. */\nexport function docFromConfig(config: ThemeConfig): ThemeDoc {\n return {\n brand: {\n primary: config.brandPrimary,\n secondary: config.brandSecondary,\n accent: config.brandAccent,\n highlight: config.brandHighlight,\n success: config.brandSuccess,\n },\n greyScale: config.greyScale,\n radius: config.radius,\n shadow: config.shadow,\n motion: config.motion,\n density: config.density,\n defaultTheme: config.defaultTheme,\n fonts: {\n body: config.fontBody,\n heading: config.fontHeading,\n },\n };\n}\n\nexport function validateThemeHex(value: unknown): true | string {\n if (!isThemeHex(value)) return \"Enter a six-digit hex colour like #1e3a5f\";\n return true;\n}\n","import { useLivePreview } from \"@payloadcms/live-preview-react\";\nimport { ContrastStrip, ThemeShowcase } from \"@bison-lab/ui\";\nimport { auditTheme, isThemeHex, type ThemeConfig } from \"@bison-lab/tokens\";\nimport { useRef, useState } from \"react\";\n\nimport { themeHead } from \"../theme/head\";\nimport { docFromConfig, themeConfigFromDoc } from \"../theme/map\";\nimport type { ThemeDoc } from \"../theme/types\";\n\nexport interface ThemePreviewProps {\n /** Published theme from `getPublishedTheme`. Shown until a live-preview message arrives. */\n theme: ThemeConfig;\n /** The site's `bison.config.json`. Fills fields a live message leaves empty. */\n seed: ThemeConfig;\n /** Where the site's `serveFont` route answers. */\n fontsBaseUrl: string;\n /** Absolute origin of the Payload server. Required by `useLivePreview`. */\n serverURL: string;\n /** Body-text target `ContrastStrip` judges against. Default `4.5`. */\n contrastTarget?: number;\n}\n\nfunction isRenderableTheme(config: ThemeConfig): boolean {\n return (\n isThemeHex(config.brandPrimary) &&\n isThemeHex(config.brandSecondary) &&\n isThemeHex(config.brandAccent) &&\n isThemeHex(config.brandHighlight) &&\n isThemeHex(config.brandSuccess)\n );\n}\n\n/**\n * The Theme document's preview pane, and the site's design-system page.\n * Rebuilds `themeHead` in the browser from each live-preview message.\n * A half-typed hex keeps the last complete stylesheet.\n */\nexport function ThemePreview({\n theme,\n seed,\n fontsBaseUrl,\n serverURL,\n contrastTarget = 4.5,\n}: ThemePreviewProps) {\n const { data } = useLivePreview<ThemeDoc>({\n initialData: docFromConfig(theme),\n serverURL,\n });\n const mapped = themeConfigFromDoc(data, seed);\n const last = useRef(theme);\n if (isRenderableTheme(mapped)) last.current = mapped;\n const config = last.current;\n const css = themeHead(config, { fontsBaseUrl }).css;\n const [mode, setMode] = useState<\"light\" | \"dark\">(\"light\");\n\n return (\n <div role=\"region\" aria-label=\"Theme preview\" data-theme={mode} className=\"bl-theme-preview\">\n <style>{css}</style>\n <div className=\"bl-theme-preview__bar\">\n <button type=\"button\" aria-pressed={mode === \"light\"} onClick={() => setMode(\"light\")}>\n Light\n </button>\n <button type=\"button\" aria-pressed={mode === \"dark\"} onClick={() => setMode(\"dark\")}>\n Dark\n </button>\n </div>\n <ThemeShowcase />\n <ContrastStrip checks={auditTheme(config, { target: contrastTarget })} />\n </div>\n );\n}\n"],"mappings":"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"}
|
package/dist/theme.d.mts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { a as ThemeHead, i as ThemeDoc, n as EditableTheme, o as ThemeHeadOptions, r as THEME_SLUG, s as ThemeUploadDoc, t as CreateThemeOptions } from "./types-pFpmDeSA.mjs";
|
|
2
|
+
import { ThemeConfig } from "@bison-lab/tokens";
|
|
3
|
+
|
|
4
|
+
//#region src/theme/published.d.ts
|
|
5
|
+
interface ThemePayload {
|
|
6
|
+
findGlobal: (args: {
|
|
7
|
+
slug: string;
|
|
8
|
+
draft?: boolean;
|
|
9
|
+
depth?: number;
|
|
10
|
+
overrideAccess?: boolean;
|
|
11
|
+
}) => Promise<ThemeDoc | null | undefined>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The published Theme, or `seed` when nothing has been published. A newer
|
|
15
|
+
* draft never reaches the public site (`draft: false`). Access is
|
|
16
|
+
* overridden so a logged-out request still gets the published colours.
|
|
17
|
+
*/
|
|
18
|
+
declare function getPublishedTheme(payload: ThemePayload, seed: ThemeConfig): Promise<ThemeConfig>;
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/theme/map.d.ts
|
|
21
|
+
/**
|
|
22
|
+
* Nested Theme document → flat `ThemeConfig`. Null or missing editor
|
|
23
|
+
* fields take the seed; `darkSelector` and `fontWeights` always come from
|
|
24
|
+
* the seed.
|
|
25
|
+
*/
|
|
26
|
+
declare function themeConfigFromDoc(doc: ThemeDoc | null | undefined, seed: ThemeConfig): ThemeConfig;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/theme/head.d.ts
|
|
29
|
+
/**
|
|
30
|
+
* `@font-face` plus `buildThemeCss` as one stylesheet, and the preload
|
|
31
|
+
* list for the families the config names. A root layout (and the admin
|
|
32
|
+
* layout, if it should restyle too) renders `css` in a `<style>` and
|
|
33
|
+
* each preload as `<link rel="preload" as="font">`.
|
|
34
|
+
*/
|
|
35
|
+
declare function themeHead(config: ThemeConfig, options?: ThemeHeadOptions): ThemeHead;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { type CreateThemeOptions, type EditableTheme, THEME_SLUG, type ThemeDoc, type ThemeHead, type ThemeHeadOptions, type ThemeUploadDoc, getPublishedTheme, themeConfigFromDoc, themeHead };
|
|
38
|
+
//# sourceMappingURL=theme.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.mts","names":[],"sources":["../src/theme/published.ts","../src/theme/map.ts","../src/theme/head.ts"],"mappings":";;;;UAMiB,YAAA;EACf,UAAA,GAAa,IAAA;IACX,IAAA;IACA,KAAA;IACA,KAAA;IACA,cAAA;EAAA,MACI,OAAA,CAAQ,QAAA;AAAA;;;;;;iBAQM,iBAAA,CAAkB,OAAA,EAAS,YAAA,EAAc,IAAA,EAAM,WAAA,GAAc,OAAA,CAAQ,WAAA;;;;;AAd3F;;;iBCiBgB,kBAAA,CAAmB,GAAA,EAAK,QAAA,qBAA6B,IAAA,EAAM,WAAA,GAAc,WAAA;;;;;ADjBzF;;;;iBEKgB,SAAA,CAAU,MAAA,EAAQ,WAAA,EAAa,OAAA,GAAS,gBAAA,GAAwB,SAAA"}
|