@bison-lab/payload-core 3.7.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 +38 -3
- package/dist/index.d.mts +25 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +33 -3
- package/dist/index.mjs.map +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 +1 -1
- package/dist/types-DWwL-JEr.mjs.map +1 -1
- package/dist/{types-B3FC9sdF.d.mts → types-pFpmDeSA.d.mts} +5 -3
- package/dist/{types-B3FC9sdF.d.mts.map → types-pFpmDeSA.d.mts.map} +1 -1
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -13,10 +13,12 @@ pnpm add @bison-lab/payload-core @payloadcms/plugin-seo @bison-lab/tokens @bison
|
|
|
13
13
|
|
|
14
14
|
Peers: `payload` and `@payloadcms/plugin-seo` are required. `@bison-lab/tokens`
|
|
15
15
|
and `@bison-lab/fonts` are needed to adopt the Theme Global; `@payloadcms/ui`
|
|
16
|
-
and `react` are needed for its admin fields
|
|
17
|
-
|
|
16
|
+
and `react` are needed for its admin fields; `@bison-lab/ui` and
|
|
17
|
+
`@payloadcms/live-preview-react` are needed for the live theme preview.
|
|
18
|
+
Pin the plugin to the same version as `payload`; Payload releases them in
|
|
19
|
+
lockstep.
|
|
18
20
|
|
|
19
|
-
##
|
|
21
|
+
## Five entry points, and why
|
|
20
22
|
|
|
21
23
|
| Import | Contents | Runs where |
|
|
22
24
|
| ---------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
|
|
@@ -24,6 +26,7 @@ version as `payload`; Payload releases them in lockstep.
|
|
|
24
26
|
| `@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
27
|
| `@bison-lab/payload-core/theme` | `getPublishedTheme`, `themeConfigFromDoc`, `themeHead` | Server. What a root layout imports; it does not load the plugin or React. |
|
|
26
28
|
| `@bison-lab/payload-core/admin` | `ColorField`, `FontField`, `ContrastReport` | Admin. Referenced by import-map string; `generate:importmap` writes it. |
|
|
29
|
+
| `@bison-lab/payload-core/react` | `ThemePreview` | Client. What the theme-preview route imports; it loads ui and live-preview. |
|
|
27
30
|
|
|
28
31
|
## What editors get
|
|
29
32
|
|
|
@@ -143,6 +146,7 @@ export default buildConfig({
|
|
|
143
146
|
createTheme({
|
|
144
147
|
access: { read: isAuthenticated, update: canManageBrand },
|
|
145
148
|
seed: bisonConfig,
|
|
149
|
+
previewPath: "/theme-preview",
|
|
146
150
|
logo: { collection: "brand-assets" },
|
|
147
151
|
onPublish: async () => {
|
|
148
152
|
revalidateTag("theme");
|
|
@@ -192,6 +196,37 @@ If the admin layout renders the same head, the admin panel restyles too.
|
|
|
192
196
|
`getPublishedTheme` reads `draft: false` and falls back to the seed when
|
|
193
197
|
nothing has been published. A newer draft never reaches the public site.
|
|
194
198
|
|
|
199
|
+
**5. Own the preview route.** The path is code-owned (in the site's
|
|
200
|
+
`ENDPOINTS`), never a nav link, never in the sitemap, reserved from CMS
|
|
201
|
+
slugs, and gated to an authenticated user. It renders `ThemePreview` with
|
|
202
|
+
no site chrome. `createTheme({ previewPath })` points Payload's live-preview
|
|
203
|
+
pane at that path; omit `previewPath` and the pane stays closed.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// app/theme-preview/page.tsx
|
|
207
|
+
import { ThemePreview } from "@bison-lab/payload-core/react";
|
|
208
|
+
import { getPublishedTheme } from "@bison-lab/payload-core/theme";
|
|
209
|
+
import bisonConfig from "../../../bison.config.json";
|
|
210
|
+
|
|
211
|
+
const theme = await getPublishedTheme(payload, bisonConfig);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<ThemePreview
|
|
215
|
+
theme={theme}
|
|
216
|
+
seed={bisonConfig}
|
|
217
|
+
fontsBaseUrl="/fonts"
|
|
218
|
+
serverURL={absoluteSiteUrl}
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`ThemePreview` rebuilds the stylesheet in the browser from each live-preview
|
|
224
|
+
message. A half-typed hex keeps the last complete theme so the iframe does
|
|
225
|
+
not go blank. The light / dark toggle sets `data-theme` on the preview root,
|
|
226
|
+
not the document. Pages that later grow a live-preview pane should import
|
|
227
|
+
`THEME_PREVIEW_BREAKPOINTS` from `@bison-lab/payload-core` so the toolbars
|
|
228
|
+
match.
|
|
229
|
+
|
|
195
230
|
## No generated types
|
|
196
231
|
|
|
197
232
|
The package cannot import a site's `payload-types`, so the shapes it reads
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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-
|
|
2
|
+
import { i as ThemeDoc, n as EditableTheme, r as THEME_SLUG, s as ThemeUploadDoc, t as CreateThemeOptions } from "./types-pFpmDeSA.mjs";
|
|
3
3
|
import { ThemeConfig } from "@bison-lab/tokens";
|
|
4
4
|
import { CheckboxField, CollectionSlug, GlobalConfig, Plugin, UploadCollectionSlug } from "payload";
|
|
5
5
|
|
|
@@ -111,7 +111,7 @@ declare function truncateAtWord(text: string, max?: number): string;
|
|
|
111
111
|
* (`canManageBrand` for update). Drafts autosave; the public site reads
|
|
112
112
|
* the published version through `getPublishedTheme`.
|
|
113
113
|
*
|
|
114
|
-
* `previewPath`
|
|
114
|
+
* `previewPath` wires `admin.livePreview` when the site has a route.
|
|
115
115
|
*/
|
|
116
116
|
declare function createTheme(options: CreateThemeOptions): GlobalConfig;
|
|
117
117
|
//#endregion
|
|
@@ -129,10 +129,32 @@ interface SeedThemePayload {
|
|
|
129
129
|
*/
|
|
130
130
|
declare function seedTheme(payload: SeedThemePayload, seed: ThemeConfig): Promise<unknown>;
|
|
131
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
|
|
132
154
|
//#region src/theme/fields.d.ts
|
|
133
155
|
declare const THEME_COLOR_FIELD = "@bison-lab/payload-core/admin#ColorField";
|
|
134
156
|
declare const THEME_FONT_FIELD = "@bison-lab/payload-core/admin#FontField";
|
|
135
157
|
declare const THEME_CONTRAST_REPORT = "@bison-lab/payload-core/admin#ContrastReport";
|
|
136
158
|
//#endregion
|
|
137
|
-
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_SLUG, type ThemeDoc, type ThemeUploadDoc, createTheme, documentTitle, firstImageIn, noIndexField, seedTheme, seoPlugin, titleTemplate, truncateAtWord };
|
|
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 };
|
|
138
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","../src/theme/global.ts","../src/theme/seed.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;;;;
|
|
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
|
@@ -79,6 +79,32 @@ function firstImageIn(blocks, field = "image") {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
//#endregion
|
|
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
|
|
82
108
|
//#region src/theme/global.ts
|
|
83
109
|
function requireAccess(options) {
|
|
84
110
|
const read = options.access?.read;
|
|
@@ -144,7 +170,7 @@ function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
|
|
|
144
170
|
* (`canManageBrand` for update). Drafts autosave; the public site reads
|
|
145
171
|
* the published version through `getPublishedTheme`.
|
|
146
172
|
*
|
|
147
|
-
* `previewPath`
|
|
173
|
+
* `previewPath` wires `admin.livePreview` when the site has a route.
|
|
148
174
|
*/
|
|
149
175
|
function createTheme(options) {
|
|
150
176
|
const access = requireAccess(options);
|
|
@@ -203,7 +229,11 @@ function createTheme(options) {
|
|
|
203
229
|
previewPath,
|
|
204
230
|
contrastTarget,
|
|
205
231
|
fontsBaseUrl
|
|
206
|
-
}
|
|
232
|
+
},
|
|
233
|
+
...previewPath ? { livePreview: {
|
|
234
|
+
url: previewPath,
|
|
235
|
+
breakpoints: [...THEME_PREVIEW_BREAKPOINTS]
|
|
236
|
+
} } : {}
|
|
207
237
|
},
|
|
208
238
|
versions: { drafts: { autosave: { interval: 800 } } },
|
|
209
239
|
access: {
|
|
@@ -234,6 +264,6 @@ async function seedTheme(payload, seed) {
|
|
|
234
264
|
});
|
|
235
265
|
}
|
|
236
266
|
//#endregion
|
|
237
|
-
export { DESCRIPTION_LENGTH, SHARE_IMAGE_SIZE, THEME_COLOR_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_SLUG, createTheme, documentTitle, firstImageIn, noIndexField, seedTheme, seoPlugin, titleTemplate, truncateAtWord };
|
|
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 };
|
|
238
268
|
|
|
239
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","../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","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_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` is stored for BIS-61, which wires the live preview pane.\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 },\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":";;;;;;;;;;;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;;;;;ACpGb,SAAS,cAAc,SAA2D;CAChF,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,SAAS,QAAQ,QAAQ;AAC/B,KAAI,CAAC,QAAQ,CAAC,OACZ,OAAM,IAAI,MACR,wKACD;AAEH,QAAO;EAAE;EAAM;EAAQ;;AAGzB,SAAS,WACP,MACA,OACA,OACA,cACa;AACb,QAAO;EACL;EACA,MAAM;EACN;EACA,UAAU;EACV;EACA,SAAU,OAAO,QAAQ,MAAM,CAA4C,KACxE,CAAC,OAAO,EAAE,OAAO,aAAa,aAAa;GAC1C,OAAO,GAAG,YAAY,KAAK;GAC3B;GACD,EACF;EACF;;AAGH,SAAS,WAAW,MAAc,OAAe,cAAiC;AAChF,QAAO;EACL;EACA,MAAM;EACN;EACA,UAAU;EACV;EACA,UAAU;EACV,OAAO,EAAE,YAAY,EAAE,OAAO,mBAAmB,EAAE;EACpD;;AAGH,SAAS,UACP,MACA,OACA,cACA,OACA,cACA,YACa;CACb,MAAM,UAAU,MAAM,KAAK,UAAU;EAAE,OAAO,KAAK;EAAQ,OAAO,KAAK;EAAI,EAAE;AAC7E,QAAO;EACL;EACA,MAAM;EACN;EACA,UAAU,CAAC;EACX;EACA,SAAS,aAAa,CAAC;GAAE,OAAO;GAAgB,OAAA;GAAqB,EAAE,GAAG,QAAQ,GAAG;EACrF,OAAO;GACL,YAAY,EAAE,OAAO,kBAAkB;GACvC,QAAQ;IAAE;IAAc,KAAK,MAAM,KAAK,SAAS,KAAK,GAAG;IAAE;IAAY;GACxE;EACF;;;;;;;;;;AAWH,SAAgB,YAAY,SAA2C;CACrE,MAAM,SAAS,cAAc,QAAQ;CACrC,MAAM,EACJ,MACA,QAAQ,SACR,eAAe,UACf,iBAAiB,KACjB,aACA,MACA,cACE;CAEJ,MAAM,SAAkB;EACtB;GACE,MAAM;GACN,MAAM;GACN,OAAO;GACP,QAAQ;IACN,WAAW,WAAW,WAAW,KAAK,aAAa;IACnD,WAAW,aAAa,aAAa,KAAK,eAAe;IACzD,WAAW,UAAU,UAAU,KAAK,YAAY;IAChD,WAAW,aAAa,aAAa,KAAK,eAAe;IACzD,WAAW,WAAW,WAAW,KAAK,aAAa;IACpD;GACF;EACD;GACE,MAAM;GACN,MAAM;GACN,OAAO;IACL,YAAY,EAAE,OAAO,uBAAuB;IAC5C,QAAQ,EAAE,QAAQ,gBAAgB;IACnC;GACF;EACD,WAAW,aAAa,cAAc,YAAY,WAAW,KAAK,UAAU;EAC5E,WAAW,UAAU,UAAU,YAAY,QAAQ,KAAK,OAAO;EAC/D,WAAW,UAAU,UAAU,YAAY,QAAQ,KAAK,OAAO;EAC/D,WAAW,UAAU,UAAU,YAAY,QAAQ,KAAK,OAAO;EAC/D,WAAW,WAAW,WAAW,YAAY,SAAS,KAAK,QAAQ;EACnE,WAAW,gBAAgB,iBAAiB,YAAY,cAAc,KAAK,aAAa;EACxF;GACE,MAAM;GACN,MAAM;GACN,OAAO;GACP,QAAQ,CACN,UAAU,QAAQ,QAAQ,KAAK,UAAU,OAAO,cAAc,MAAM,EACpE,UACE,WACA,WACA,mBAAmB,KAAK,YAAY,EACpC,OACA,cACA,KACD,CACF;GACF;EACF;AAED,KAAI,KACF,QAAO,KACL;EACE,MAAM;EACN,MAAM;EACN,YAAY,KAAK;EACjB,OAAO;EACR,EACD;EACE,MAAM;EACN,MAAM;EACN,YAAY,KAAK;EACjB,OAAO;EACR,CACF;AAGH,QAAO;EACL,MAAM;EACN,OAAO;EACP,OAAO;GACL,OAAO;GACP,QAAQ;IAAE;IAAa;IAAgB;IAAc;GACtD;EACD,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,KAAK,EAAE,EAAE;EACrD,QAAQ;GACN,MAAM,OAAO;GACb,QAAQ,OAAO;GACf,cAAc,OAAO;GACtB;EACD;EACA,OAAO,EACL,aAAa,CACX,OAAO,EAAE,UAAU;AACjB,OAAI,CAAC,UAAW;GAChB,MAAM,WAAW;AACjB,OAAI,SAAS,YAAY,YAAa;AACtC,SAAM,UAAU,mBAAmB,UAAU,KAAK,EAAE,SAAS;IAEhE,EACF;EACF;;;;;;;;ACzKH,eAAsB,UAAU,SAA2B,MAAqC;AAC9F,QAAO,QAAQ,aAAa;EAC1B,MAAM;EACN,MAAM,cAAc,KAAK;EACzB,OAAO;EACR,CAAC"}
|
|
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/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
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
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
2
|
import { ThemeConfig } from "@bison-lab/tokens";
|
|
3
3
|
|
|
4
4
|
//#region src/theme/published.d.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-DWwL-JEr.mjs","names":[],"sources":["../src/theme/fields.ts","../src/theme/map.ts","../src/theme/types.ts"],"sourcesContent":["import type { ThemeConfig } from \"@bison-lab/tokens\";\n\nimport type { EditableTheme } from \"./types\";\n\nexport const THEME_COLOR_FIELD = \"@bison-lab/payload-core/admin#ColorField\";\nexport const THEME_FONT_FIELD = \"@bison-lab/payload-core/admin#FontField\";\nexport const THEME_CONTRAST_REPORT = \"@bison-lab/payload-core/admin#ContrastReport\";\n\n/**\n * Document path for each editable `ThemeConfig` key. Locked with `satisfies`\n * so a new config field without a Theme field (or the reverse) fails at\n * compile time. `darkSelector` and `fontWeights` are seed-only.\n */\nexport const THEME_FIELD_PATHS = {\n brandPrimary: \"brand.primary\",\n brandSecondary: \"brand.secondary\",\n brandAccent: \"brand.accent\",\n brandHighlight: \"brand.highlight\",\n brandSuccess: \"brand.success\",\n greyScale: \"greyScale\",\n defaultTheme: \"defaultTheme\",\n radius: \"radius\",\n shadow: \"shadow\",\n motion: \"motion\",\n density: \"density\",\n fontBody: \"fonts.body\",\n fontHeading: \"fonts.heading\",\n} as const satisfies Record<keyof EditableTheme, string>;\n\nexport type ThemeFieldPath = (typeof THEME_FIELD_PATHS)[keyof typeof THEME_FIELD_PATHS];\n\n/** Empty select value for heading: same family as body. */\nexport const SAME_AS_BODY = \"\";\n\nexport function headingSelectValue(heading: ThemeConfig[\"fontHeading\"]): string {\n return heading ?? SAME_AS_BODY;\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 type { Access } from \"payload\";\nimport type { FontEntry } from \"@bison-lab/fonts\";\nimport type { ThemeConfig } from \"@bison-lab/tokens\";\n\n/**\n * The Theme document a site's generated types will describe. Optional and\n * nullable, no index signature: a generated Global is assignable to this,\n * never the reverse.\n */\nexport interface ThemeDoc {\n brand?: {\n primary?: string | null;\n secondary?: string | null;\n accent?: string | null;\n highlight?: string | null;\n success?: string | null;\n } | null;\n greyScale?: ThemeConfig[\"greyScale\"] | null;\n radius?: ThemeConfig[\"radius\"] | null;\n shadow?: ThemeConfig[\"shadow\"] | null;\n motion?: ThemeConfig[\"motion\"] | null;\n density?: ThemeConfig[\"density\"] | null;\n defaultTheme?: ThemeConfig[\"defaultTheme\"] | null;\n fonts?: {\n body?: string | null;\n heading?: string | null;\n } | null;\n logo?: number | string | ThemeUploadDoc | null;\n logoMark?: number | string | ThemeUploadDoc | null;\n _status?: \"draft\" | \"published\" | null;\n}\n\nexport interface ThemeUploadDoc {\n id?: number | string | null;\n url?: string | null;\n alt?: string | null;\n}\n\n/**\n * What an editor can change. `darkSelector` and `fontWeights` stay on the\n * seed — they mean nothing in the admin — and `themeConfigFromDoc` puts\n * them back.\n */\nexport type EditableTheme = Omit<ThemeConfig, \"darkSelector\" | \"fontWeights\">;\n\nexport interface CreateThemeOptions {\n /**\n * Required, no default. Predicates live in each site's `src/platform`\n * until BIS-43; pass `canManageBrand` as `update` (and typically\n * `isAuthenticated` as `read`). `readVersions` / draft visibility follow\n * `update`, so only a brand manager sees unpublished edits.\n */\n access: { read: Access; update: Access };\n /** The site's `bison.config.json`. Every field's `defaultValue`, and the fallback `getPublishedTheme` returns. */\n seed: ThemeConfig;\n /** Default: the whole catalogue. */\n fonts?: readonly FontEntry[];\n /** Where the site's `serveFont` route answers, for the picker's specimens. Default `\"/fonts\"`. */\n fontsBaseUrl?: string;\n /** Body-text target the contrast report judges against. Default `4.5`. */\n contrastTarget?: number;\n /**\n *
|
|
1
|
+
{"version":3,"file":"types-DWwL-JEr.mjs","names":[],"sources":["../src/theme/fields.ts","../src/theme/map.ts","../src/theme/types.ts"],"sourcesContent":["import type { ThemeConfig } from \"@bison-lab/tokens\";\n\nimport type { EditableTheme } from \"./types\";\n\nexport const THEME_COLOR_FIELD = \"@bison-lab/payload-core/admin#ColorField\";\nexport const THEME_FONT_FIELD = \"@bison-lab/payload-core/admin#FontField\";\nexport const THEME_CONTRAST_REPORT = \"@bison-lab/payload-core/admin#ContrastReport\";\n\n/**\n * Document path for each editable `ThemeConfig` key. Locked with `satisfies`\n * so a new config field without a Theme field (or the reverse) fails at\n * compile time. `darkSelector` and `fontWeights` are seed-only.\n */\nexport const THEME_FIELD_PATHS = {\n brandPrimary: \"brand.primary\",\n brandSecondary: \"brand.secondary\",\n brandAccent: \"brand.accent\",\n brandHighlight: \"brand.highlight\",\n brandSuccess: \"brand.success\",\n greyScale: \"greyScale\",\n defaultTheme: \"defaultTheme\",\n radius: \"radius\",\n shadow: \"shadow\",\n motion: \"motion\",\n density: \"density\",\n fontBody: \"fonts.body\",\n fontHeading: \"fonts.heading\",\n} as const satisfies Record<keyof EditableTheme, string>;\n\nexport type ThemeFieldPath = (typeof THEME_FIELD_PATHS)[keyof typeof THEME_FIELD_PATHS];\n\n/** Empty select value for heading: same family as body. */\nexport const SAME_AS_BODY = \"\";\n\nexport function headingSelectValue(heading: ThemeConfig[\"fontHeading\"]): string {\n return heading ?? SAME_AS_BODY;\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 type { Access } from \"payload\";\nimport type { FontEntry } from \"@bison-lab/fonts\";\nimport type { ThemeConfig } from \"@bison-lab/tokens\";\n\n/**\n * The Theme document a site's generated types will describe. Optional and\n * nullable, no index signature: a generated Global is assignable to this,\n * never the reverse.\n */\nexport interface ThemeDoc {\n brand?: {\n primary?: string | null;\n secondary?: string | null;\n accent?: string | null;\n highlight?: string | null;\n success?: string | null;\n } | null;\n greyScale?: ThemeConfig[\"greyScale\"] | null;\n radius?: ThemeConfig[\"radius\"] | null;\n shadow?: ThemeConfig[\"shadow\"] | null;\n motion?: ThemeConfig[\"motion\"] | null;\n density?: ThemeConfig[\"density\"] | null;\n defaultTheme?: ThemeConfig[\"defaultTheme\"] | null;\n fonts?: {\n body?: string | null;\n heading?: string | null;\n } | null;\n logo?: number | string | ThemeUploadDoc | null;\n logoMark?: number | string | ThemeUploadDoc | null;\n _status?: \"draft\" | \"published\" | null;\n}\n\nexport interface ThemeUploadDoc {\n id?: number | string | null;\n url?: string | null;\n alt?: string | null;\n}\n\n/**\n * What an editor can change. `darkSelector` and `fontWeights` stay on the\n * seed — they mean nothing in the admin — and `themeConfigFromDoc` puts\n * them back.\n */\nexport type EditableTheme = Omit<ThemeConfig, \"darkSelector\" | \"fontWeights\">;\n\nexport interface CreateThemeOptions {\n /**\n * Required, no default. Predicates live in each site's `src/platform`\n * until BIS-43; pass `canManageBrand` as `update` (and typically\n * `isAuthenticated` as `read`). `readVersions` / draft visibility follow\n * `update`, so only a brand manager sees unpublished edits.\n */\n access: { read: Access; update: Access };\n /** The site's `bison.config.json`. Every field's `defaultValue`, and the fallback `getPublishedTheme` returns. */\n seed: ThemeConfig;\n /** Default: the whole catalogue. */\n fonts?: readonly FontEntry[];\n /** Where the site's `serveFont` route answers, for the picker's specimens. Default `\"/fonts\"`. */\n fontsBaseUrl?: string;\n /** Body-text target the contrast report judges against. Default `4.5`. */\n contrastTarget?: number;\n /**\n * Code-owned preview route. When set, `createTheme` wires\n * `admin.livePreview.url` to it and `THEME_PREVIEW_BREAKPOINTS`.\n * Omit it and the pane stays closed — a site that has not built the\n * route does not get an empty iframe.\n */\n previewPath?: string;\n /** Adds `logo` and `logoMark` upload relations when given. */\n logo?: { collection: string };\n /** Fires from `afterChange` on a published save only, not an autosaved draft. */\n onPublish?: (theme: ThemeConfig, doc: ThemeDoc) => void | Promise<void>;\n}\n\nexport interface ThemeHeadOptions {\n fontsBaseUrl?: string;\n attribute?: \"class\" | \"data-theme\";\n}\n\nexport interface ThemeHead {\n css: string;\n preloads: { href: string; type: \"font/woff2\" }[];\n}\n\nexport const THEME_SLUG = \"theme\";\n"],"mappings":";;AAIA,MAAa,oBAAoB;AACjC,MAAa,mBAAmB;AAChC,MAAa,wBAAwB;AA4BrC,SAAgB,mBAAmB,SAA6C;AAC9E,QAAO,WAAA;;;;AC9BT,SAAS,KAAQ,OAA6B,UAAgB;AAC5D,QAAO,SAAS,OAAO,WAAW;;AAGpC,SAAS,eACP,SACA,MACe;AACf,KAAI,YAAY,KAAA,EAAW,QAAO;AAClC,KAAI,YAAY,QAAQ,YAAA,GAA0B,QAAO;AACzD,QAAO;;;;;;;AAQT,SAAgB,mBAAmB,KAAkC,MAAgC;CACnG,MAAM,QAAQ,KAAK;AACnB,QAAO;EACL,cAAc,KAAK,OAAO,SAAS,KAAK,aAAa;EACrD,gBAAgB,KAAK,OAAO,WAAW,KAAK,eAAe;EAC3D,aAAa,KAAK,OAAO,QAAQ,KAAK,YAAY;EAClD,gBAAgB,KAAK,OAAO,WAAW,KAAK,eAAe;EAC3D,cAAc,KAAK,OAAO,SAAS,KAAK,aAAa;EACrD,WAAW,KAAK,KAAK,WAAW,KAAK,UAAU;EAC/C,QAAQ,KAAK,KAAK,QAAQ,KAAK,OAAO;EACtC,QAAQ,KAAK,KAAK,QAAQ,KAAK,OAAO;EACtC,QAAQ,KAAK,KAAK,QAAQ,KAAK,OAAO;EACtC,SAAS,KAAK,KAAK,SAAS,KAAK,QAAQ;EACzC,cAAc,KAAK,KAAK,cAAc,KAAK,aAAa;EACxD,UAAU,KAAK,KAAK,OAAO,MAAM,KAAK,SAAS;EAC/C,aAAa,eAAe,KAAK,OAAO,SAAS,KAAK,YAAY;EAClE,cAAc,KAAK;EACnB,aAAa,KAAK;EACnB;;;AAIH,SAAgB,cAAc,QAA+B;AAC3D,QAAO;EACL,OAAO;GACL,SAAS,OAAO;GAChB,WAAW,OAAO;GAClB,QAAQ,OAAO;GACf,WAAW,OAAO;GAClB,SAAS,OAAO;GACjB;EACD,WAAW,OAAO;EAClB,QAAQ,OAAO;EACf,QAAQ,OAAO;EACf,QAAQ,OAAO;EACf,SAAS,OAAO;EAChB,cAAc,OAAO;EACrB,OAAO;GACL,MAAM,OAAO;GACb,SAAS,OAAO;GACjB;EACF;;AAGH,SAAgB,iBAAiB,OAA+B;AAC9D,KAAI,CAAC,WAAW,MAAM,CAAE,QAAO;AAC/B,QAAO;;;;ACeT,MAAa,aAAa"}
|
|
@@ -61,8 +61,10 @@ interface CreateThemeOptions {
|
|
|
61
61
|
/** Body-text target the contrast report judges against. Default `4.5`. */
|
|
62
62
|
contrastTarget?: number;
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* Code-owned preview route. When set, `createTheme` wires
|
|
65
|
+
* `admin.livePreview.url` to it and `THEME_PREVIEW_BREAKPOINTS`.
|
|
66
|
+
* Omit it and the pane stays closed — a site that has not built the
|
|
67
|
+
* route does not get an empty iframe.
|
|
66
68
|
*/
|
|
67
69
|
previewPath?: string;
|
|
68
70
|
/** Adds `logo` and `logoMark` upload relations when given. */
|
|
@@ -86,4 +88,4 @@ interface ThemeHead {
|
|
|
86
88
|
declare const THEME_SLUG = "theme";
|
|
87
89
|
//#endregion
|
|
88
90
|
export { ThemeHead as a, ThemeDoc as i, EditableTheme as n, ThemeHeadOptions as o, THEME_SLUG as r, ThemeUploadDoc as s, CreateThemeOptions as t };
|
|
89
|
-
//# sourceMappingURL=types-
|
|
91
|
+
//# sourceMappingURL=types-pFpmDeSA.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-
|
|
1
|
+
{"version":3,"file":"types-pFpmDeSA.d.mts","names":[],"sources":["../src/theme/types.ts"],"mappings":";;;;;;;AASA;;;UAAiB,QAAA;EACf,KAAA;IACE,OAAA;IACA,SAAA;IACA,MAAA;IACA,SAAA;IACA,OAAA;EAAA;EAEF,SAAA,GAAY,WAAA;EACZ,MAAA,GAAS,WAAA;EACT,MAAA,GAAS,WAAA;EACT,MAAA,GAAS,WAAA;EACT,OAAA,GAAU,WAAA;EACV,YAAA,GAAe,WAAA;EACf,KAAA;IACE,IAAA;IACA,OAAA;EAAA;EAEF,IAAA,qBAAyB,cAAA;EACzB,QAAA,qBAA6B,cAAA;EAC7B,OAAA;AAAA;AAAA,UAGe,cAAA;EACf,EAAA;EACA,GAAA;EACA,GAAA;AAAA;;;;;;KAQU,aAAA,GAAgB,IAAA,CAAK,WAAA;AAAA,UAEhB,kBAAA;EAjBc;;;;AAI/B;;EAoBE,MAAA;IAAU,IAAA,EAAM,MAAA;IAAQ,MAAA,EAAQ,MAAA;EAAA;EAjBhC;EAmBA,IAAA,EAAM,WAAA;EAnBH;EAqBH,KAAA,YAAiB,SAAA;EAbM;EAevB,YAAA;EAf0B;EAiB1B,cAAA;EAfe;;;;;;EAsBf,WAAA;EAIoB;EAFpB,IAAA;IAAS,UAAA;EAAA;EAEwD;EAAjE,SAAA,IAAa,KAAA,EAAO,WAAA,EAAa,GAAA,EAAK,QAAA,YAAoB,OAAA;AAAA;AAAA,UAG3C,gBAAA;EACf,YAAA;EACA,SAAA;AAAA;AAAA,UAGe,SAAA;EACf,GAAA;EACA,QAAA;IAAY,IAAA;IAAc,IAAA;EAAA;AAAA;AAAA,cAGf,UAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bison-lab/payload-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"description": "Site-agnostic Payload CMS configuration for Bison Lab sites: the SEO tab, the Theme global, and the metadata and theme readers for the pages they describe",
|
|
5
5
|
"homepage": "https://components.bisonlab.ai",
|
|
6
6
|
"repository": {
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"./admin": {
|
|
31
31
|
"import": "./dist/admin.mjs",
|
|
32
32
|
"types": "./dist/admin.d.mts"
|
|
33
|
+
},
|
|
34
|
+
"./react": {
|
|
35
|
+
"import": "./dist/react.mjs",
|
|
36
|
+
"types": "./dist/react.d.mts"
|
|
33
37
|
}
|
|
34
38
|
},
|
|
35
39
|
"files": [
|
|
@@ -38,6 +42,8 @@
|
|
|
38
42
|
"peerDependencies": {
|
|
39
43
|
"@bison-lab/fonts": "^3.0.0",
|
|
40
44
|
"@bison-lab/tokens": "^3.0.0",
|
|
45
|
+
"@bison-lab/ui": "^3.0.0",
|
|
46
|
+
"@payloadcms/live-preview-react": "^3.0.0",
|
|
41
47
|
"@payloadcms/plugin-seo": "^3.0.0",
|
|
42
48
|
"@payloadcms/ui": "^3.0.0",
|
|
43
49
|
"payload": "^3.0.0",
|
|
@@ -50,6 +56,12 @@
|
|
|
50
56
|
"@bison-lab/tokens": {
|
|
51
57
|
"optional": true
|
|
52
58
|
},
|
|
59
|
+
"@bison-lab/ui": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"@payloadcms/live-preview-react": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
53
65
|
"@payloadcms/ui": {
|
|
54
66
|
"optional": true
|
|
55
67
|
},
|
|
@@ -58,6 +70,7 @@
|
|
|
58
70
|
}
|
|
59
71
|
},
|
|
60
72
|
"devDependencies": {
|
|
73
|
+
"@payloadcms/live-preview-react": "^3.88.0",
|
|
61
74
|
"@payloadcms/plugin-seo": "^3.88.0",
|
|
62
75
|
"@payloadcms/ui": "^3.88.0",
|
|
63
76
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -70,8 +83,9 @@
|
|
|
70
83
|
"tsdown": "^0.21.0",
|
|
71
84
|
"typescript": "^5.9.3",
|
|
72
85
|
"vitest": "^4.1.2",
|
|
73
|
-
"@bison-lab/
|
|
74
|
-
"@bison-lab/
|
|
86
|
+
"@bison-lab/tokens": "3.7.0",
|
|
87
|
+
"@bison-lab/ui": "3.7.0",
|
|
88
|
+
"@bison-lab/fonts": "3.6.0"
|
|
75
89
|
},
|
|
76
90
|
"publishConfig": {
|
|
77
91
|
"access": "public"
|