@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 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. Pin the plugin to the same
17
- version as `payload`; Payload releases them in lockstep.
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
- ## Four entry points, and why
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-B3FC9sdF.mjs";
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` is stored for BIS-61, which wires the live preview pane.
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
@@ -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;;;;iBIqEgB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,YAAA;;;UCnFzC,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,iBAAA;AAAA,cACA,gBAAA;AAAA,cACA,qBAAA"}
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` is stored for BIS-61, which wires the live preview pane.
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
@@ -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":"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"}
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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"}
@@ -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-B3FC9sdF.mjs";
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 * Stored on `admin.custom` for BIS-61, which wires `admin.livePreview`.\n * This slice does not open a preview pane.\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;;;;ACaT,MAAa,aAAa"}
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
- * Stored on `admin.custom` for BIS-61, which wires `admin.livePreview`.
65
- * This slice does not open a preview pane.
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-B3FC9sdF.d.mts.map
91
+ //# sourceMappingURL=types-pFpmDeSA.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-B3FC9sdF.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;;;;EAoBf,WAAA;EAXM;EAaN,IAAA;IAAS,UAAA;EAAA;EAEiD;EAA1D,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"}
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.7.0",
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/fonts": "3.6.0",
74
- "@bison-lab/tokens": "3.7.0"
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"