@bison-lab/payload-core 3.9.0 → 3.10.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.
@@ -11,6 +11,7 @@ const THEME_CONTRAST_REPORT = "@bison-lab/payload-core/admin#ContrastReport";
11
11
  const THEME_SECTION_HEADING = "@bison-lab/payload-core/admin#SectionHeading";
12
12
  const THEME_PUBLISH_FIELD = "@bison-lab/payload-core/admin#PublishChild";
13
13
  const THEME_SAVE_BUTTON = "@bison-lab/payload-core/admin#HiddenSaveButton";
14
+ const THEME_IDENTITY_FALLBACK = "@bison-lab/payload-core/admin#IdentityFallback";
14
15
  function headingSelectValue(heading) {
15
16
  return heading ?? "";
16
17
  }
@@ -141,7 +142,7 @@ function rewriteColorToken(token, fromKey, toKey) {
141
142
  return `${toKey}-${step}`;
142
143
  }
143
144
  function themeColorKeys(doc) {
144
- const custom = (doc?.colors?.library ?? []).map((row) => row.key?.trim()).filter((key) => Boolean(key));
145
+ const custom = (Array.isArray(doc?.colors?.library) ? doc.colors.library : []).map((row) => row.key?.trim()).filter((key) => Boolean(key));
145
146
  return [...SYSTEM_COLOR_KEYS, ...custom];
146
147
  }
147
148
  /**
@@ -151,7 +152,7 @@ function themeColorKeys(doc) {
151
152
  * has `key`.
152
153
  */
153
154
  function deleteLibraryColor(doc, key, replacement) {
154
- const library = [...doc.colors?.library ?? []];
155
+ const library = [...Array.isArray(doc.colors?.library) ? doc.colors.library : []];
155
156
  const index = library.findIndex((row) => row.key === key);
156
157
  if (index === -1) throw new Error(`No custom color named ${JSON.stringify(key)}`);
157
158
  library.splice(index, 1);
@@ -193,6 +194,6 @@ function resolveThemeIdentity(doc, fallback) {
193
194
  };
194
195
  }
195
196
  //#endregion
196
- export { headingSelectValue as S, THEME_LIBRARY_FIELD as _, SYSTEM_COLOR_KEYS as a, THEME_SAVE_BUTTON as b, themeConfigFromDoc as c, THEME_APPEARANCE_FIELD as d, THEME_COLOR_FIELD as f, THEME_GREY_SCALE_FIELD as g, THEME_FONT_FIELD as h, themeColorKeys as i, validateSourceIncluded as l, THEME_CONTRAST_REPORT as m, deleteLibraryColor as n, THEME_SLUG as o, THEME_COLOR_SCALE_FIELD as p, rewriteColorToken as r, docFromConfig as s, resolveThemeIdentity as t, validateThemeHex as u, THEME_PAIRING_FIELD as v, THEME_SECTION_HEADING as x, THEME_PUBLISH_FIELD as y };
197
+ export { headingSelectValue as C, THEME_SECTION_HEADING as S, THEME_IDENTITY_FALLBACK as _, SYSTEM_COLOR_KEYS as a, THEME_PUBLISH_FIELD as b, themeConfigFromDoc as c, THEME_APPEARANCE_FIELD as d, THEME_COLOR_FIELD as f, THEME_GREY_SCALE_FIELD as g, THEME_FONT_FIELD as h, themeColorKeys as i, validateSourceIncluded as l, THEME_CONTRAST_REPORT as m, deleteLibraryColor as n, THEME_SLUG as o, THEME_COLOR_SCALE_FIELD as p, rewriteColorToken as r, docFromConfig as s, resolveThemeIdentity as t, validateThemeHex as u, THEME_LIBRARY_FIELD as v, THEME_SAVE_BUTTON as x, THEME_PAIRING_FIELD as y };
197
198
 
198
- //# sourceMappingURL=identity-D1X0Om4Z.mjs.map
199
+ //# sourceMappingURL=identity-Bg-G05mb.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-Bg-G05mb.mjs","names":[],"sources":["../src/theme/fields.ts","../src/theme/map.ts","../src/theme/types.ts","../src/theme/library.ts","../src/theme/identity.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_COLOR_SCALE_FIELD = \"@bison-lab/payload-core/admin#ColorScaleField\";\nexport const THEME_LIBRARY_FIELD = \"@bison-lab/payload-core/admin#LibraryField\";\nexport const THEME_FONT_FIELD = \"@bison-lab/payload-core/admin#FontField\";\nexport const THEME_PAIRING_FIELD = \"@bison-lab/payload-core/admin#PairingField\";\nexport const THEME_APPEARANCE_FIELD = \"@bison-lab/payload-core/admin#AppearanceField\";\nexport const THEME_GREY_SCALE_FIELD = \"@bison-lab/payload-core/admin#GreyScaleField\";\nexport const THEME_CONTRAST_REPORT = \"@bison-lab/payload-core/admin#ContrastReport\";\nexport const THEME_SECTION_HEADING = \"@bison-lab/payload-core/admin#SectionHeading\";\nexport const THEME_PUBLISH_FIELD = \"@bison-lab/payload-core/admin#PublishChild\";\nexport const THEME_SAVE_BUTTON = \"@bison-lab/payload-core/admin#HiddenSaveButton\";\nexport const THEME_IDENTITY_FALLBACK = \"@bison-lab/payload-core/admin#IdentityFallback\";\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: \"colors.brand.primary.hex\",\n brandSecondary: \"colors.brand.secondary.hex\",\n brandAccent: \"colors.brand.accent.hex\",\n brandHighlight: \"colors.brand.highlight.hex\",\n brandSuccess: \"colors.brand.success.hex\",\n brandDestructive: \"colors.brand.destructive.hex\",\n greyScale: \"colors.greyScale\",\n defaultTheme: \"appearance.defaultTheme\",\n radius: \"appearance.radius\",\n shadow: \"appearance.shadow\",\n motion: \"appearance.motion\",\n density: \"appearance.density\",\n fontBody: \"typography.body\",\n fontHeading: \"typography.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 {\n createColorScale,\n generateColorScale,\n isThemeHex,\n SHADE_STEPS,\n type ColorScale,\n type ColorScaleInclude,\n type ColorScaleState,\n type ShadeStep,\n type ThemeConfig,\n} from \"@bison-lab/tokens\";\n\nimport { SAME_AS_BODY } from \"./fields\";\nimport type { ThemeColorDoc, 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\nfunction hexFromColor(value: ThemeColorDoc | string | null | undefined, fallback: string): string {\n if (typeof value === \"string\") return value || fallback;\n return pick(value?.hex, fallback);\n}\n\nexport function colorDocFromHex(hex: string, sourceStep: ShadeStep = 500): ThemeColorDoc {\n return colorDocFromState(createColorScale(hex, sourceStep));\n}\n\nexport function colorDocFromState(state: ColorScaleState): ThemeColorDoc {\n return {\n hex: state.hex,\n sourceStep: state.sourceStep,\n scale: state.scale,\n stale: state.stale,\n include: Array.isArray(state.include) ? \"custom\" : state.include,\n includedSteps: Array.isArray(state.include) ? [...state.include] : undefined,\n };\n}\n\nexport function stateFromColorDoc(\n doc: ThemeColorDoc | null | undefined,\n fallbackHex = \"#000000\",\n): ColorScaleState {\n const hex = isThemeHex(doc?.hex) ? doc.hex : fallbackHex;\n const sourceStep = (Number(doc?.sourceStep) || 500) as ShadeStep;\n const include = includeFromDoc(doc, sourceStep);\n const scale =\n doc?.scale && typeof doc.scale === \"object\" ? (doc.scale as ColorScale) : generateColorScale(hex, sourceStep);\n return { hex, sourceStep, scale, stale: Boolean(doc?.stale), include };\n}\n\nfunction includeFromDoc(doc: ThemeColorDoc | null | undefined, sourceStep: ShadeStep): ColorScaleInclude {\n if (doc?.include === \"source\") return \"source\";\n if (doc?.include === \"custom\" && Array.isArray(doc.includedSteps)) {\n const steps = doc.includedSteps.map(Number).filter((step): step is ShadeStep => SHADE_STEPS.includes(step as ShadeStep));\n if (!steps.includes(sourceStep)) steps.push(sourceStep);\n return steps;\n }\n return \"all\";\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. Reads the BIS-87 groups and the pre-BIS-87 flat fields.\n */\nexport function themeConfigFromDoc(doc: ThemeDoc | null | undefined, seed: ThemeConfig): ThemeConfig {\n const brand = doc?.colors?.brand ?? doc?.brand;\n const typography = doc?.typography ?? doc?.fonts;\n const appearance = doc?.appearance;\n return {\n brandPrimary: hexFromColor(brand?.primary, seed.brandPrimary),\n brandSecondary: hexFromColor(brand?.secondary, seed.brandSecondary),\n brandAccent: hexFromColor(brand?.accent, seed.brandAccent),\n brandHighlight: hexFromColor(brand?.highlight, seed.brandHighlight),\n brandSuccess: hexFromColor(brand?.success, seed.brandSuccess),\n brandDestructive: hexFromColor(brand?.destructive, seed.brandDestructive ?? \"\") || seed.brandDestructive,\n greyScale: pick(doc?.colors?.greyScale ?? doc?.greyScale, seed.greyScale),\n radius: pick(appearance?.radius ?? doc?.radius, seed.radius),\n shadow: pick(appearance?.shadow ?? doc?.shadow, seed.shadow),\n motion: pick(appearance?.motion ?? doc?.motion, seed.motion),\n density: pick(appearance?.density ?? doc?.density, seed.density),\n defaultTheme: pick(appearance?.defaultTheme ?? doc?.defaultTheme, seed.defaultTheme),\n fontBody: pick(typography?.body, seed.fontBody),\n fontHeading: headingFromDoc(typography?.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, destructive?: string): ThemeDoc {\n const destructiveHex = destructive ?? config.brandDestructive;\n if (!destructiveHex) {\n throw new Error(\"docFromConfig requires a destructive hex on the config or as the second argument\");\n }\n return {\n colors: {\n brand: {\n primary: colorDocFromHex(config.brandPrimary),\n secondary: colorDocFromHex(config.brandSecondary),\n accent: colorDocFromHex(config.brandAccent),\n highlight: colorDocFromHex(config.brandHighlight),\n success: colorDocFromHex(config.brandSuccess),\n destructive: colorDocFromHex(destructiveHex),\n },\n library: [],\n greyScale: config.greyScale,\n },\n typography: {\n body: config.fontBody,\n heading: config.fontHeading,\n },\n appearance: {\n defaultTheme: config.defaultTheme,\n radius: config.radius,\n shadow: config.shadow,\n motion: config.motion,\n density: config.density,\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\nexport function validateSourceIncluded(\n value: unknown,\n { siblingData }: { siblingData?: { sourceStep?: unknown; include?: unknown } },\n): true | string {\n const include = siblingData?.include;\n if (include === \"all\" || include === \"source\") return true;\n const source = Number(siblingData?.sourceStep);\n const steps = Array.isArray(value) ? value.map(Number) : [];\n if (!SHADE_STEPS.includes(source as ShadeStep)) return true;\n if (!steps.includes(source)) return \"The source step cannot be excluded\";\n return true;\n}\n","import type { Access } from \"payload\";\nimport type { FontEntry } from \"@bison-lab/fonts\";\nimport type { ColorScale, ColorScaleInclude, ShadeStep, ThemeConfig } from \"@bison-lab/tokens\";\n\nimport type { ResolvedThemeIdentity, ThemeIdentityFallback } from \"./identity\";\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 *\n * Legacy flat `brand.*` hexes, `fonts`, and preset fields at the root stay\n * readable so `getPublishedTheme` still maps a pre-BIS-87 row.\n */\nexport interface ThemeColorDoc {\n hex?: string | null;\n sourceStep?: ShadeStep | string | null;\n scale?: ColorScale | null;\n stale?: boolean | null;\n include?: ColorScaleInclude | \"custom\" | null;\n includedSteps?: ShadeStep[] | string[] | null;\n}\n\nexport interface ThemeLibraryColorDoc extends ThemeColorDoc {\n key?: string | null;\n label?: string | null;\n}\n\nexport interface ThemeDoc {\n colors?: {\n brand?: {\n primary?: ThemeColorDoc | string | null;\n secondary?: ThemeColorDoc | string | null;\n accent?: ThemeColorDoc | string | null;\n highlight?: ThemeColorDoc | string | null;\n success?: ThemeColorDoc | string | null;\n destructive?: ThemeColorDoc | string | null;\n } | null;\n library?: ThemeLibraryColorDoc[] | null;\n greyScale?: ThemeConfig[\"greyScale\"] | null;\n } | null;\n typography?: {\n body?: string | null;\n heading?: string | null;\n } | null;\n appearance?: {\n defaultTheme?: ThemeConfig[\"defaultTheme\"] | null;\n radius?: ThemeConfig[\"radius\"] | null;\n shadow?: ThemeConfig[\"shadow\"] | null;\n motion?: ThemeConfig[\"motion\"] | null;\n density?: ThemeConfig[\"density\"] | null;\n } | null;\n /** @deprecated Pre-BIS-87 flat brand hexes. */\n brand?: {\n primary?: ThemeColorDoc | string | null;\n secondary?: ThemeColorDoc | string | null;\n accent?: ThemeColorDoc | string | null;\n highlight?: ThemeColorDoc | string | null;\n success?: ThemeColorDoc | string | null;\n destructive?: ThemeColorDoc | string | null;\n } | null;\n /** @deprecated Pre-BIS-87; now `colors.greyScale`. */\n greyScale?: ThemeConfig[\"greyScale\"] | null;\n /** @deprecated Pre-BIS-87; now `appearance.*`. */\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 /** @deprecated Pre-BIS-87; now `typography`. */\n fonts?: {\n body?: string | null;\n heading?: string | null;\n } | null;\n logo?: number | string | ThemeUploadDoc | null;\n favicon?: number | string | ThemeUploadDoc | null;\n logoMark?: number | string | ThemeUploadDoc | 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`).\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 /**\n * Destructive seed hex. Required unless `seed.brandDestructive` is set.\n * Not a package default — the site names it.\n */\n destructive?: string;\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 /** Fill + automatic label target. Default `7`. Warnings never block save. */\n contrastTarget?: number;\n /**\n * @deprecated Theme has no preview pane (SPI-56 canceled). Kept so a\n * site that still passes it does not fail to boot.\n */\n previewPath?: string;\n /**\n * Adds logo, favicon, and mobile-menu mark upload relations when\n * given. Pass `BRAND_ASSETS_SLUG` (or the slug you gave\n * `createBrandAssets`). Identity is a Theme child with its own Publish.\n */\n logo?: { collection: string };\n /**\n * Fallback art when logo, favicon, or mobile-menu mark is empty. The\n * site passes today's files; `--color-primary-mark` is not a Theme field.\n */\n identity?: { fallback?: ThemeIdentityFallback };\n /** Fires from `afterChange` on every save — Theme has no draft mode. */\n onPublish?: (theme: ThemeConfig, doc: ThemeDoc) => void | Promise<void>;\n}\n\nexport interface ThemeHeadOptions {\n fontsBaseUrl?: string;\n attribute?: \"class\" | \"data-theme\";\n identity?: ResolvedThemeIdentity;\n}\n\nexport interface ThemeHead {\n css: string;\n preloads: { href: string; type: \"font/woff2\" }[];\n identity?: ResolvedThemeIdentity;\n}\n\nexport const THEME_SLUG = \"theme\";\n\nexport const SYSTEM_COLOR_KEYS = [\n \"primary\",\n \"secondary\",\n \"accent\",\n \"highlight\",\n \"success\",\n \"destructive\",\n] as const;\n\nexport type SystemColorKey = (typeof SYSTEM_COLOR_KEYS)[number];\n","import { SHADE_STEPS, type ShadeStep } from \"@bison-lab/tokens\";\n\nimport { SYSTEM_COLOR_KEYS, type ThemeDoc } from \"./types\";\n\nconst STEP_SET = new Set<number>(SHADE_STEPS);\n\n/**\n * Page editors pick `coral-400`, never a raw hex. Rewrite every use of\n * `fromKey` onto `toKey` at the same step: `coral-400` → `highlight-400`.\n * A bare key (`coral`) becomes `toKey`. Anything else is left alone.\n */\nexport function rewriteColorToken(token: string, fromKey: string, toKey: string): string {\n if (token === fromKey) return toKey;\n const dash = token.lastIndexOf(\"-\");\n if (dash <= 0) return token;\n const key = token.slice(0, dash);\n const step = Number(token.slice(dash + 1));\n if (key !== fromKey || !STEP_SET.has(step)) return token;\n return `${toKey}-${step as ShadeStep}`;\n}\n\nexport function themeColorKeys(doc: ThemeDoc | null | undefined): string[] {\n const custom = (Array.isArray(doc?.colors?.library) ? doc.colors.library : [])\n .map((row) => row.key?.trim())\n .filter((key): key is string => Boolean(key));\n return [...SYSTEM_COLOR_KEYS, ...custom];\n}\n\n/**\n * Remove a custom color. Unused: omit `replacement` and the row is gone.\n * In use: pass another system or custom key; the caller rewrites page\n * tokens with `rewriteColorToken`. After this, no library row still\n * has `key`.\n */\nexport function deleteLibraryColor(doc: ThemeDoc, key: string, replacement?: string): ThemeDoc {\n const library = [...(Array.isArray(doc.colors?.library) ? doc.colors.library : [])];\n const index = library.findIndex((row) => row.key === key);\n if (index === -1) {\n throw new Error(`No custom color named ${JSON.stringify(key)}`);\n }\n library.splice(index, 1);\n const next: ThemeDoc = {\n ...doc,\n colors: { ...doc.colors, library },\n };\n if (replacement !== undefined) {\n if (replacement === key) {\n throw new Error(\"Replacement must be a different color\");\n }\n if (!themeColorKeys(next).includes(replacement)) {\n throw new Error(`Replacement ${JSON.stringify(replacement)} is not a system or remaining custom color`);\n }\n }\n return next;\n}\n","import type { ThemeDoc, ThemeUploadDoc } from \"./types\";\n\nexport interface ThemeIdentityAsset {\n url: string;\n alt: string;\n}\n\nexport interface ThemeIdentityFallback {\n lockup?: ThemeIdentityAsset;\n mark?: ThemeIdentityAsset;\n favicon?: ThemeIdentityAsset;\n}\n\nexport interface ResolvedThemeIdentity {\n lockup: ThemeIdentityAsset | null;\n mark: ThemeIdentityAsset | null;\n favicon: ThemeIdentityAsset | null;\n}\n\nfunction fromUpload(value: ThemeDoc[\"logo\"], fallback?: ThemeIdentityAsset): ThemeIdentityAsset | null {\n if (value && typeof value === \"object\") {\n const upload = value as ThemeUploadDoc;\n if (upload.url) {\n return { url: upload.url, alt: upload.alt ?? fallback?.alt ?? \"\" };\n }\n }\n return fallback ?? null;\n}\n\n/**\n * Uploaded logo, favicon, and mobile-menu mark, or the fallback the site\n * passed into `createTheme`. Empty fields keep the fallback;\n * `--color-primary-mark` is not a Theme field.\n */\nexport function resolveThemeIdentity(\n doc: ThemeDoc | null | undefined,\n fallback?: ThemeIdentityFallback,\n): ResolvedThemeIdentity {\n return {\n lockup: fromUpload(doc?.logo, fallback?.lockup),\n mark: fromUpload(doc?.logoMark, fallback?.mark),\n favicon: fromUpload(doc?.favicon, fallback?.favicon),\n };\n}\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -106,9 +106,10 @@ declare function truncateAtWord(text: string, max?: number): string;
106
106
  //#endregion
107
107
  //#region src/theme/global.d.ts
108
108
  /**
109
- * Settings → Theme children: Colors, Typography, Appearance, Identity.
110
- * Each child publishes itself. No draft mode: a save is live.
111
- * `getPublishedTheme` reads that row. There is no Theme preview pane.
109
+ * Settings → Theme. Field tabs: Colors, Typography, Appearance, Identity.
110
+ * Publish in the Save slot writes the open tab only. No draft mode: a
111
+ * save is live. `getPublishedTheme` reads that row. There is no Theme
112
+ * preview pane.
112
113
  */
113
114
  declare function createTheme(options: CreateThemeOptions): GlobalConfig;
114
115
  //#endregion
@@ -206,6 +207,7 @@ declare const THEME_CONTRAST_REPORT = "@bison-lab/payload-core/admin#ContrastRep
206
207
  declare const THEME_SECTION_HEADING = "@bison-lab/payload-core/admin#SectionHeading";
207
208
  declare const THEME_PUBLISH_FIELD = "@bison-lab/payload-core/admin#PublishChild";
208
209
  declare const THEME_SAVE_BUTTON = "@bison-lab/payload-core/admin#HiddenSaveButton";
210
+ declare const THEME_IDENTITY_FALLBACK = "@bison-lab/payload-core/admin#IdentityFallback";
209
211
  //#endregion
210
- export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, type CreateBrandAssetsOptions, type CreateThemeOptions, DESCRIPTION_LENGTH, type EditableTheme, type MediaId, type ResolvedThemeIdentity, SHARE_IMAGE_SIZE, SYSTEM_COLOR_KEYS, type SeoDoc, type SeoImageDoc, type SeoImageSize, type SeoImageValue, type SeoMeta, type SeoPluginOptions, type SystemColorKey, THEME_APPEARANCE_FIELD, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, type ThemeChild, type ThemeColorDoc, type ThemeDoc, type ThemeIdentityFallback, type ThemeLibraryColorDoc, type ThemeUploadDoc, createBrandAssets, createTheme, deleteLibraryColor, documentTitle, firstImageIn, noIndexField, publishThemeChild, resolveThemeIdentity, rewriteColorToken, sanitizeSvg, seedTheme, seoPlugin, themeColorKeys, titleTemplate, truncateAtWord };
212
+ export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, type CreateBrandAssetsOptions, type CreateThemeOptions, DESCRIPTION_LENGTH, type EditableTheme, type MediaId, type ResolvedThemeIdentity, SHARE_IMAGE_SIZE, SYSTEM_COLOR_KEYS, type SeoDoc, type SeoImageDoc, type SeoImageSize, type SeoImageValue, type SeoMeta, type SeoPluginOptions, type SystemColorKey, THEME_APPEARANCE_FIELD, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_IDENTITY_FALLBACK, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, type ThemeChild, type ThemeColorDoc, type ThemeDoc, type ThemeIdentityFallback, type ThemeLibraryColorDoc, type ThemeUploadDoc, createBrandAssets, createTheme, deleteLibraryColor, documentTitle, firstImageIn, noIndexField, publishThemeChild, resolveThemeIdentity, rewriteColorToken, sanitizeSvg, seedTheme, seoPlugin, themeColorKeys, titleTemplate, truncateAtWord };
211
213
  //# 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/brand-assets/collection.ts","../src/brand-assets/sanitize.ts","../src/theme/seed.ts","../src/theme/preview.ts","../src/theme/publish.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;iBI2JgB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,YAAA;;;cC3K7C,iBAAA;;cAGA,uBAAA;AAAA,UAOI,wBAAA;;ALMjB;;;;;EKCE,MAAA;IAAU,IAAA,EAAM,MAAA;IAAQ,MAAA,EAAQ,MAAA;EAAA;ELGsB;EKDtD,IAAA;AAAA;;;;;;iBAmBc,iBAAA,CAAkB,OAAA,EAAS,wBAAA,GAA2B,gBAAA;;;;;;;;;iBClCtD,WAAA,CAAY,MAAA;;;UCFX,gBAAA;EACf,YAAA,GAAe,IAAA;IACb,IAAA;IACA,IAAA,EAAM,QAAA;IACN,KAAA;EAAA,MACI,OAAA;AAAA;;;APaR;;iBONsB,SAAA,CAAU,OAAA,EAAS,gBAAA,EAAkB,IAAA,EAAM,WAAA,GAAc,OAAA;;;;;;;cCdlE,yBAAA;EAAA;;;;;;;;;;;;;;;;;cCFA,cAAA;AAAA,KAED,UAAA,WAAqB,cAAA;;;;ATgBjC;;;;iBSPgB,iBAAA,CAAkB,MAAA,EAAQ,QAAA,EAAU,QAAA,EAAU,QAAA,EAAU,KAAA,EAAO,UAAA,GAAa,QAAA;;;cCT/E,iBAAA;AAAA,cACA,uBAAA;AAAA,cACA,mBAAA;AAAA,cACA,gBAAA;AAAA,cACA,mBAAA;AAAA,cACA,sBAAA;AAAA,cACA,sBAAA;AAAA,cACA,qBAAA;AAAA,cACA,qBAAA;AAAA,cACA,mBAAA;AAAA,cACA,iBAAA"}
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/brand-assets/collection.ts","../src/brand-assets/sanitize.ts","../src/theme/seed.ts","../src/theme/preview.ts","../src/theme/publish.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;;iBIoJgB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,YAAA;;;cCpK7C,iBAAA;;cAGA,uBAAA;AAAA,UAOI,wBAAA;;ALMjB;;;;;EKCE,MAAA;IAAU,IAAA,EAAM,MAAA;IAAQ,MAAA,EAAQ,MAAA;EAAA;ELGsB;EKDtD,IAAA;AAAA;;;;;;iBAmBc,iBAAA,CAAkB,OAAA,EAAS,wBAAA,GAA2B,gBAAA;;;;;;;;;iBClCtD,WAAA,CAAY,MAAA;;;UCFX,gBAAA;EACf,YAAA,GAAe,IAAA;IACb,IAAA;IACA,IAAA,EAAM,QAAA;IACN,KAAA;EAAA,MACI,OAAA;AAAA;;;APaR;;iBONsB,SAAA,CAAU,OAAA,EAAS,gBAAA,EAAkB,IAAA,EAAM,WAAA,GAAc,OAAA;;;;;;;cCdlE,yBAAA;EAAA;;;;;;;;;;;;;;;;;cCFA,cAAA;AAAA,KAED,UAAA,WAAqB,cAAA;;;;ATgBjC;;;;iBSPgB,iBAAA,CAAkB,MAAA,EAAQ,QAAA,EAAU,QAAA,EAAU,QAAA,EAAU,KAAA,EAAO,UAAA,GAAa,QAAA;;;cCT/E,iBAAA;AAAA,cACA,uBAAA;AAAA,cACA,mBAAA;AAAA,cACA,gBAAA;AAAA,cACA,mBAAA;AAAA,cACA,sBAAA;AAAA,cACA,sBAAA;AAAA,cACA,qBAAA;AAAA,cACA,qBAAA;AAAA,cACA,mBAAA;AAAA,cACA,iBAAA;AAAA,cACA,uBAAA"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-C4ILz4p2.mjs";
2
- import { S as headingSelectValue, _ as THEME_LIBRARY_FIELD, a as SYSTEM_COLOR_KEYS, b as THEME_SAVE_BUTTON, c as themeConfigFromDoc, d as THEME_APPEARANCE_FIELD, f as THEME_COLOR_FIELD, g as THEME_GREY_SCALE_FIELD, h as THEME_FONT_FIELD, i as themeColorKeys, l as validateSourceIncluded, m as THEME_CONTRAST_REPORT, n as deleteLibraryColor, o as THEME_SLUG, p as THEME_COLOR_SCALE_FIELD, r as rewriteColorToken, s as docFromConfig, t as resolveThemeIdentity, u as validateThemeHex, v as THEME_PAIRING_FIELD, x as THEME_SECTION_HEADING, y as THEME_PUBLISH_FIELD } from "./identity-D1X0Om4Z.mjs";
2
+ import { C as headingSelectValue, S as THEME_SECTION_HEADING, _ as THEME_IDENTITY_FALLBACK, a as SYSTEM_COLOR_KEYS, b as THEME_PUBLISH_FIELD, c as themeConfigFromDoc, d as THEME_APPEARANCE_FIELD, f as THEME_COLOR_FIELD, g as THEME_GREY_SCALE_FIELD, h as THEME_FONT_FIELD, i as themeColorKeys, l as validateSourceIncluded, m as THEME_CONTRAST_REPORT, n as deleteLibraryColor, o as THEME_SLUG, p as THEME_COLOR_SCALE_FIELD, r as rewriteColorToken, s as docFromConfig, t as resolveThemeIdentity, u as validateThemeHex, v as THEME_LIBRARY_FIELD, x as THEME_SAVE_BUTTON, y as THEME_PAIRING_FIELD } from "./identity-Bg-G05mb.mjs";
3
3
  import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
4
4
  import { catalog } from "@bison-lab/fonts";
5
5
  import { SHADE_STEPS, presetHints } from "@bison-lab/tokens";
@@ -334,20 +334,11 @@ function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
334
334
  }
335
335
  };
336
336
  }
337
- function publishField(child) {
338
- return {
339
- name: `publish${child[0].toUpperCase()}${child.slice(1)}`,
340
- type: "ui",
341
- admin: {
342
- components: { Field: THEME_PUBLISH_FIELD },
343
- custom: { child }
344
- }
345
- };
346
- }
347
337
  /**
348
- * Settings → Theme children: Colors, Typography, Appearance, Identity.
349
- * Each child publishes itself. No draft mode: a save is live.
350
- * `getPublishedTheme` reads that row. There is no Theme preview pane.
338
+ * Settings → Theme. Field tabs: Colors, Typography, Appearance, Identity.
339
+ * Publish in the Save slot writes the open tab only. No draft mode: a
340
+ * save is live. `getPublishedTheme` reads that row. There is no Theme
341
+ * preview pane.
351
342
  */
352
343
  function createTheme(options) {
353
344
  const access = requireAccess$1(options);
@@ -356,94 +347,90 @@ function createTheme(options) {
356
347
  const tabs = [
357
348
  {
358
349
  label: "Colors",
359
- fields: [
360
- publishField("colors"),
361
- {
362
- name: "colors",
363
- type: "group",
364
- label: "Colors",
365
- fields: [
366
- {
367
- name: "brand",
368
- type: "group",
369
- label: "Colors",
370
- fields: [
371
- {
372
- name: "themeColorsHeading",
373
- type: "ui",
374
- admin: {
375
- components: { Field: THEME_SECTION_HEADING },
376
- custom: {
377
- title: "Theme colors",
378
- hint: "House semantic fills page editors can pick. Custom colors sit with these."
379
- }
350
+ fields: [{
351
+ name: "colors",
352
+ type: "group",
353
+ label: false,
354
+ fields: [
355
+ {
356
+ name: "brand",
357
+ type: "group",
358
+ label: false,
359
+ fields: [
360
+ {
361
+ name: "themeColorsHeading",
362
+ type: "ui",
363
+ admin: {
364
+ components: { Field: THEME_SECTION_HEADING },
365
+ custom: {
366
+ title: "Theme colors",
367
+ hint: "House semantic fills page editors can pick. Custom colors sit with these."
380
368
  }
381
- },
382
- systemColorGroup("primary", "Primary", seed.brandPrimary),
383
- systemColorGroup("secondary", "Secondary", seed.brandSecondary),
384
- systemColorGroup("accent", "Accent", seed.brandAccent),
385
- systemColorGroup("highlight", "Highlight", seed.brandHighlight),
386
- {
387
- name: "statusColorsHeading",
388
- type: "ui",
389
- admin: {
390
- components: { Field: THEME_SECTION_HEADING },
391
- custom: {
392
- title: "Success and Destructive",
393
- hint: "Theme chrome only. These are not offered as fills when someone is writing a page."
394
- }
369
+ }
370
+ },
371
+ systemColorGroup("primary", "Primary", seed.brandPrimary),
372
+ systemColorGroup("secondary", "Secondary", seed.brandSecondary),
373
+ systemColorGroup("accent", "Accent", seed.brandAccent),
374
+ systemColorGroup("highlight", "Highlight", seed.brandHighlight),
375
+ {
376
+ name: "statusColorsHeading",
377
+ type: "ui",
378
+ admin: {
379
+ components: { Field: THEME_SECTION_HEADING },
380
+ custom: {
381
+ title: "Success and Destructive",
382
+ hint: "Theme chrome only. These are not offered as fills when someone is writing a page."
395
383
  }
396
- },
397
- systemColorGroup("success", "Success", seed.brandSuccess),
398
- systemColorGroup("destructive", "Destructive", destructive)
399
- ]
384
+ }
385
+ },
386
+ systemColorGroup("success", "Success", seed.brandSuccess),
387
+ systemColorGroup("destructive", "Destructive", destructive)
388
+ ]
389
+ },
390
+ namedSelect("greyScale", "Gray family", presetHints.greyScale, seed.greyScale, THEME_GREY_SCALE_FIELD),
391
+ {
392
+ name: "library",
393
+ type: "array",
394
+ label: "Custom colors",
395
+ labels: {
396
+ singular: "Color",
397
+ plural: "Colors"
400
398
  },
401
- namedSelect("greyScale", "Gray family", presetHints.greyScale, seed.greyScale, THEME_GREY_SCALE_FIELD),
402
- {
403
- name: "library",
404
- type: "array",
405
- label: "Custom colors",
406
- labels: {
407
- singular: "Color",
408
- plural: "Colors"
399
+ fields: [
400
+ {
401
+ name: "key",
402
+ type: "text",
403
+ label: "Key",
404
+ required: true,
405
+ admin: { hidden: true }
409
406
  },
410
- fields: [
411
- {
412
- name: "key",
413
- type: "text",
414
- label: "Key",
415
- required: true,
416
- admin: { hidden: true }
417
- },
418
- {
419
- name: "label",
420
- type: "text",
421
- label: "Name",
422
- required: true,
423
- admin: { hidden: true }
424
- },
425
- ...colorScaleFields()
426
- ],
427
- admin: { components: { Field: THEME_LIBRARY_FIELD } }
428
- }
429
- ]
430
- },
431
- {
432
- name: "contrast",
433
- type: "ui",
434
- admin: {
435
- components: { Field: THEME_CONTRAST_REPORT },
436
- custom: { target: contrastTarget }
407
+ {
408
+ name: "label",
409
+ type: "text",
410
+ label: "Name",
411
+ required: true,
412
+ admin: { hidden: true }
413
+ },
414
+ ...colorScaleFields()
415
+ ],
416
+ admin: { components: { Field: THEME_LIBRARY_FIELD } }
437
417
  }
418
+ ]
419
+ }, {
420
+ name: "contrast",
421
+ type: "ui",
422
+ admin: {
423
+ components: { Field: THEME_CONTRAST_REPORT },
424
+ custom: { target: contrastTarget }
438
425
  }
439
- ]
426
+ }]
440
427
  },
441
428
  {
442
429
  label: "Typography",
443
430
  fields: [{
444
431
  name: "typography",
445
432
  type: "group",
446
- label: "Typography",
433
+ label: false,
447
434
  fields: [
448
435
  fontField("heading", "Heading font", headingSelectValue(seed.fontHeading), fonts, fontsBaseUrl, true),
449
436
  fontField("body", "Body font", seed.fontBody, fonts, fontsBaseUrl, false),
@@ -456,14 +443,14 @@ function createTheme(options) {
456
443
  }
457
444
  }
458
445
  ]
459
- }, publishField("typography")]
446
+ }]
460
447
  },
461
448
  {
462
449
  label: "Appearance",
463
450
  fields: [{
464
451
  name: "appearance",
465
452
  type: "group",
466
- label: "Appearance",
453
+ label: false,
467
454
  fields: [
468
455
  namedSelect("defaultTheme", "Default theme", APPEARANCE_CHOICES.defaultTheme, seed.defaultTheme),
469
456
  namedSelect("radius", "Corner style", APPEARANCE_CHOICES.radius, seed.radius),
@@ -472,12 +459,12 @@ function createTheme(options) {
472
459
  namedSelect("density", "Spacing", APPEARANCE_CHOICES.density, seed.density)
473
460
  ],
474
461
  admin: { components: { Field: THEME_APPEARANCE_FIELD } }
475
- }, publishField("appearance")]
462
+ }]
476
463
  }
477
464
  ];
478
- if (logo) tabs.push({
465
+ tabs.push({
479
466
  label: "Identity",
480
- fields: [
467
+ fields: logo ? [
481
468
  {
482
469
  name: "logo",
483
470
  type: "upload",
@@ -498,9 +485,15 @@ function createTheme(options) {
498
485
  relationTo: logo.collection,
499
486
  label: "Mobile menu logo",
500
487
  admin: { description: "Optional. Used when the full logo is too wide for the mobile menu — typically the mark." }
501
- },
502
- publishField("identity")
503
- ]
488
+ }
489
+ ] : [{
490
+ name: "identityFallback",
491
+ type: "ui",
492
+ admin: {
493
+ components: { Field: THEME_IDENTITY_FALLBACK },
494
+ custom: { fallback: identity?.fallback }
495
+ }
496
+ }]
504
497
  });
505
498
  const fields = [{
506
499
  type: "tabs",
@@ -688,6 +681,6 @@ const THEME_PREVIEW_BREAKPOINTS = [
688
681
  }
689
682
  ];
690
683
  //#endregion
691
- export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, DESCRIPTION_LENGTH, SHARE_IMAGE_SIZE, SYSTEM_COLOR_KEYS, THEME_APPEARANCE_FIELD, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, createBrandAssets, createTheme, deleteLibraryColor, documentTitle, firstImageIn, noIndexField, publishThemeChild, resolveThemeIdentity, rewriteColorToken, sanitizeSvg, seedTheme, seoPlugin, themeColorKeys, titleTemplate, truncateAtWord };
684
+ export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, DESCRIPTION_LENGTH, SHARE_IMAGE_SIZE, SYSTEM_COLOR_KEYS, THEME_APPEARANCE_FIELD, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_IDENTITY_FALLBACK, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, createBrandAssets, createTheme, deleteLibraryColor, documentTitle, firstImageIn, noIndexField, publishThemeChild, resolveThemeIdentity, rewriteColorToken, sanitizeSvg, seedTheme, seoPlugin, themeColorKeys, titleTemplate, truncateAtWord };
692
685
 
693
686
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["payloadSeoPlugin","requireAccess"],"sources":["../src/seo/fields.ts","../src/seo/text.ts","../src/seo/plugin.ts","../src/theme/appearance-labels.ts","../src/theme/publish.ts","../src/theme/global.ts","../src/brand-assets/sanitize.ts","../src/brand-assets/collection.ts","../src/theme/seed.ts","../src/theme/preview.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 * Named choices the Appearance child shows. Values stay the token presets;\n * labels match the mock workflow (Soft → pill, Gentle → minimal, Balanced\n * → default, Follow device → system). No rem, ms, or CSS variable tables.\n */\nexport const APPEARANCE_CHOICES = {\n defaultTheme: {\n light: { label: \"Light\", hint: \"Always opens in light mode\" },\n dark: { label: \"Dark\", hint: \"Always opens in dark mode\" },\n system: { label: \"Follow device\", hint: \"Matches the visitor’s system setting\" },\n },\n radius: {\n sharp: { label: \"Sharp\", hint: \"Crisp and structured\" },\n subtle: { label: \"Subtle\", hint: \"Clean with a little softness\" },\n rounded: { label: \"Rounded\", hint: \"Friendly and contemporary\" },\n pill: { label: \"Soft\", hint: \"Very rounded and expressive\" },\n },\n shadow: {\n flat: { label: \"Flat\", hint: \"Minimal visual depth\" },\n subtle: { label: \"Subtle\", hint: \"Light separation between surfaces\" },\n elevated: { label: \"Elevated\", hint: \"More noticeable layering and depth\" },\n },\n motion: {\n snappy: { label: \"Quick\", hint: \"Fast and responsive\" },\n smooth: { label: \"Smooth\", hint: \"Balanced and natural\" },\n minimal: { label: \"Gentle\", hint: \"Slower and more relaxed\" },\n },\n density: {\n compact: { label: \"Compact\", hint: \"Fits more content on screen\" },\n default: { label: \"Balanced\", hint: \"Comfortable for most sites\" },\n spacious: { label: \"Spacious\", hint: \"More breathing room and larger controls\" },\n },\n} as const;\n\nexport const APPEARANCE_FAMILY_COPY: Record<\n keyof typeof APPEARANCE_CHOICES,\n { title: string; hint: string }\n> = {\n defaultTheme: {\n title: \"Default theme\",\n hint: \"Choose how the public site opens for visitors.\",\n },\n radius: {\n title: \"Corner style\",\n hint: \"Controls the shape of buttons, inputs, cards, and panels across the site.\",\n },\n shadow: {\n title: \"Depth\",\n hint: \"Choose how much cards and floating elements stand out from the page.\",\n },\n motion: {\n title: \"Motion\",\n hint: \"Controls how quickly menus, cards, and interface transitions move.\",\n },\n density: {\n title: \"Spacing\",\n hint: \"Choose how compact or roomy forms, buttons, cards, and page sections feel.\",\n },\n};\n\nexport const FONT_ROLE_COPY = {\n heading: {\n label: \"Heading font\",\n hint: \"Used for page titles, section headings, and cards\",\n },\n body: {\n label: \"Body font\",\n hint: \"Used for paragraphs, navigation, forms, and buttons\",\n },\n} as const;\n\nexport const SYSTEM_COLOR_COPY: Record<\n \"primary\" | \"secondary\" | \"accent\" | \"highlight\" | \"success\" | \"destructive\",\n { label: string; hint: string; group: \"brand\" | \"status\" }\n> = {\n primary: { label: \"Primary\", hint: \"Primary actions and key links\", group: \"brand\" },\n secondary: { label: \"Secondary\", hint: \"Supporting accents and eyebrows\", group: \"brand\" },\n accent: { label: \"Accent\", hint: \"Dark brand surfaces\", group: \"brand\" },\n highlight: { label: \"Highlight\", hint: \"Promotional and emphasis fills\", group: \"brand\" },\n success: {\n label: \"Success\",\n hint: \"Theme chrome for confirmations. Not offered as a page-editor fill.\",\n group: \"status\",\n },\n destructive: {\n label: \"Destructive\",\n hint: \"Theme chrome for errors and dangerous actions. Not offered as a page-editor fill.\",\n group: \"status\",\n },\n};\n","import type { ThemeDoc } from \"./types\";\n\nexport const THEME_CHILDREN = [\"colors\", \"typography\", \"appearance\", \"identity\"] as const;\n\nexport type ThemeChild = (typeof THEME_CHILDREN)[number];\n\n/**\n * Publishing a child writes that slice only. Other responsibilities stay\n * as they already are on the stored document. Unsaved form state on a\n * different child never goes live. A first save (no stored row) still\n * writes only the named child; `getPublishedTheme` fills the rest from\n * the seed.\n */\nexport function publishThemeChild(stored: ThemeDoc, incoming: ThemeDoc, child: ThemeChild): ThemeDoc {\n if (child === \"colors\") return { ...stored, colors: incoming.colors };\n if (child === \"typography\") return { ...stored, typography: incoming.typography };\n if (child === \"appearance\") return { ...stored, appearance: incoming.appearance };\n return { ...stored, logo: incoming.logo, favicon: incoming.favicon, logoMark: incoming.logoMark };\n}\n\nexport function isThemeChild(value: unknown): value is ThemeChild {\n return typeof value === \"string\" && (THEME_CHILDREN as readonly string[]).includes(value);\n}\n","import { catalog, type FontEntry } from \"@bison-lab/fonts\";\nimport { presetHints, SHADE_STEPS } from \"@bison-lab/tokens\";\nimport type { Field, GlobalConfig, SelectField, TabsField } from \"payload\";\n\nimport {\n SAME_AS_BODY,\n THEME_APPEARANCE_FIELD,\n THEME_COLOR_SCALE_FIELD,\n THEME_CONTRAST_REPORT,\n THEME_FONT_FIELD,\n THEME_GREY_SCALE_FIELD,\n THEME_LIBRARY_FIELD,\n THEME_PAIRING_FIELD,\n THEME_PUBLISH_FIELD,\n THEME_SAVE_BUTTON,\n THEME_SECTION_HEADING,\n headingSelectValue,\n} from \"./fields\";\nimport { APPEARANCE_CHOICES } from \"./appearance-labels\";\nimport { themeConfigFromDoc, validateSourceIncluded, validateThemeHex } from \"./map\";\nimport { isThemeChild, publishThemeChild } from \"./publish\";\nimport { READABILITY_TARGET } from \"./readability\";\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 requireDestructive(options: CreateThemeOptions): string {\n const hex = options.destructive ?? options.seed.brandDestructive;\n if (!hex) {\n throw new Error(\n \"createTheme requires a destructive hex: pass options.destructive or seed.brandDestructive. There is no package default.\",\n );\n }\n return hex;\n}\n\nfunction namedSelect<K extends string>(\n name: string,\n label: string,\n choices: Record<K, { label: string; hint: string }>,\n defaultValue: K,\n field?: string,\n): SelectField {\n return {\n name,\n type: \"select\",\n label,\n required: true,\n defaultValue,\n options: (Object.entries(choices) as [K, { label: string; hint: string }][]).map(\n ([value, { label: optionLabel, hint }]) => ({\n label: `${optionLabel} — ${hint}`,\n value,\n }),\n ),\n ...(field ? { admin: { components: { Field: field } } } : {}),\n };\n}\n\nconst STEP_OPTIONS = SHADE_STEPS.map((step) => ({ label: String(step), value: String(step) }));\n\nfunction colorScaleFields(hexDefault?: string): Field[] {\n const hidden = { hidden: true } as const;\n return [\n {\n name: \"hex\",\n type: \"text\",\n label: \"Hex\",\n required: true,\n ...(hexDefault ? { defaultValue: hexDefault } : {}),\n validate: validateThemeHex,\n admin: hidden,\n },\n {\n name: \"sourceStep\",\n type: \"select\",\n label: \"Source step\",\n required: true,\n defaultValue: \"500\",\n options: STEP_OPTIONS,\n admin: hidden,\n },\n {\n name: \"scale\",\n type: \"json\",\n label: \"Scale\",\n admin: hidden,\n },\n {\n name: \"stale\",\n type: \"checkbox\",\n label: \"Stale\",\n defaultValue: false,\n admin: hidden,\n },\n {\n name: \"include\",\n type: \"select\",\n label: \"Include\",\n required: true,\n defaultValue: \"all\",\n options: [\n { label: \"Include all\", value: \"all\" },\n { label: \"Source only\", value: \"source\" },\n { label: \"Custom\", value: \"custom\" },\n ],\n admin: hidden,\n },\n {\n name: \"includedSteps\",\n type: \"select\",\n label: \"Included steps\",\n hasMany: true,\n options: STEP_OPTIONS,\n validate: validateSourceIncluded,\n admin: hidden,\n },\n ];\n}\n\nfunction systemColorGroup(name: string, label: string, hexDefault: string): Field {\n return {\n name,\n type: \"group\",\n label,\n fields: colorScaleFields(hexDefault),\n admin: { components: { Field: THEME_COLOR_SCALE_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\nfunction publishField(child: \"colors\" | \"typography\" | \"appearance\" | \"identity\"): Field {\n return {\n name: `publish${child[0].toUpperCase()}${child.slice(1)}`,\n type: \"ui\",\n admin: { components: { Field: THEME_PUBLISH_FIELD }, custom: { child } },\n };\n}\n\n/**\n * Settings → Theme children: Colors, Typography, Appearance, Identity.\n * Each child publishes itself. No draft mode: a save is live.\n * `getPublishedTheme` reads that row. There is no Theme preview pane.\n */\nexport function createTheme(options: CreateThemeOptions): GlobalConfig {\n const access = requireAccess(options);\n const destructive = requireDestructive(options);\n const {\n seed,\n fonts = catalog,\n fontsBaseUrl = \"/fonts\",\n contrastTarget = READABILITY_TARGET,\n logo,\n identity,\n onPublish,\n } = options;\n\n const tabs: TabsField[\"tabs\"] = [\n {\n label: \"Colors\",\n fields: [\n publishField(\"colors\"),\n {\n name: \"colors\",\n type: \"group\",\n label: \"Colors\",\n fields: [\n {\n name: \"brand\",\n type: \"group\",\n label: \"Colors\",\n fields: [\n {\n name: \"themeColorsHeading\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_SECTION_HEADING },\n custom: {\n title: \"Theme colors\",\n hint: \"House semantic fills page editors can pick. Custom colors sit with these.\",\n },\n },\n },\n systemColorGroup(\"primary\", \"Primary\", seed.brandPrimary),\n systemColorGroup(\"secondary\", \"Secondary\", seed.brandSecondary),\n systemColorGroup(\"accent\", \"Accent\", seed.brandAccent),\n systemColorGroup(\"highlight\", \"Highlight\", seed.brandHighlight),\n {\n name: \"statusColorsHeading\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_SECTION_HEADING },\n custom: {\n title: \"Success and Destructive\",\n hint: \"Theme chrome only. These are not offered as fills when someone is writing a page.\",\n },\n },\n },\n systemColorGroup(\"success\", \"Success\", seed.brandSuccess),\n systemColorGroup(\"destructive\", \"Destructive\", destructive),\n ],\n },\n namedSelect(\"greyScale\", \"Gray family\", presetHints.greyScale, seed.greyScale, THEME_GREY_SCALE_FIELD),\n {\n name: \"library\",\n type: \"array\",\n label: \"Custom colors\",\n labels: { singular: \"Color\", plural: \"Colors\" },\n fields: [\n { name: \"key\", type: \"text\", label: \"Key\", required: true, admin: { hidden: true } },\n { name: \"label\", type: \"text\", label: \"Name\", required: true, admin: { hidden: true } },\n ...colorScaleFields(),\n ],\n admin: { components: { Field: THEME_LIBRARY_FIELD } },\n },\n ],\n },\n {\n name: \"contrast\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_CONTRAST_REPORT },\n custom: { target: contrastTarget },\n },\n },\n ],\n },\n {\n label: \"Typography\",\n fields: [\n {\n name: \"typography\",\n type: \"group\",\n label: \"Typography\",\n fields: [\n fontField(\n \"heading\",\n \"Heading font\",\n headingSelectValue(seed.fontHeading),\n fonts,\n fontsBaseUrl,\n true,\n ),\n fontField(\"body\", \"Body font\", seed.fontBody, fonts, fontsBaseUrl, false),\n {\n name: \"pairing\",\n type: \"ui\",\n admin: { components: { Field: THEME_PAIRING_FIELD }, custom: { fontsBaseUrl } },\n },\n ],\n },\n publishField(\"typography\"),\n ],\n },\n {\n label: \"Appearance\",\n fields: [\n {\n name: \"appearance\",\n type: \"group\",\n label: \"Appearance\",\n fields: [\n namedSelect(\"defaultTheme\", \"Default theme\", APPEARANCE_CHOICES.defaultTheme, seed.defaultTheme),\n namedSelect(\"radius\", \"Corner style\", APPEARANCE_CHOICES.radius, seed.radius),\n namedSelect(\"shadow\", \"Depth\", APPEARANCE_CHOICES.shadow, seed.shadow),\n namedSelect(\"motion\", \"Motion\", APPEARANCE_CHOICES.motion, seed.motion),\n namedSelect(\"density\", \"Spacing\", APPEARANCE_CHOICES.density, seed.density),\n ],\n admin: { components: { Field: THEME_APPEARANCE_FIELD } },\n },\n publishField(\"appearance\"),\n ],\n },\n ];\n\n if (logo) {\n tabs.push({\n label: \"Identity\",\n fields: [\n {\n name: \"logo\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Logo\",\n admin: {\n description: \"The full wordmark used in the header and footer.\",\n },\n },\n {\n name: \"favicon\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Favicon\",\n admin: {\n description: \"The small icon in the browser tab. PNG or SVG.\",\n },\n },\n {\n name: \"logoMark\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Mobile menu logo\",\n admin: {\n description:\n \"Optional. Used when the full logo is too wide for the mobile menu — typically the mark.\",\n },\n },\n publishField(\"identity\"),\n ],\n });\n }\n\n const fields: Field[] = [\n { type: \"tabs\", tabs },\n {\n name: \"publishChild\",\n type: \"text\",\n admin: { hidden: true },\n },\n ];\n\n return {\n slug: THEME_SLUG,\n label: \"Theme\",\n admin: {\n group: \"Settings\",\n custom: { contrastTarget, fontsBaseUrl, identityFallback: identity?.fallback },\n components: {\n elements: {\n SaveButton: THEME_SAVE_BUTTON,\n },\n },\n },\n access: {\n read: access.read,\n update: access.update,\n },\n fields,\n hooks: {\n beforeChange: [\n ({ data, originalDoc }) => {\n const incoming = data as ThemeDoc & { publishChild?: string };\n const { publishChild, ...rest } = incoming;\n if (isThemeChild(publishChild)) {\n return publishThemeChild((originalDoc as ThemeDoc) ?? {}, rest, publishChild);\n }\n return rest;\n },\n ],\n afterChange: [\n async ({ doc }) => {\n if (!onPublish) return;\n const themeDoc = doc as ThemeDoc;\n await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);\n },\n ],\n },\n };\n}\n","const DANGEROUS_TAGS = [\"script\", \"foreignObject\", \"iframe\", \"embed\", \"object\"] as const;\n\n/**\n * Strips the SVG features a lockup does not need and an attacker would\n * use: script, foreignObject, iframe/embed/object, event handlers,\n * javascript: / data:text/html URLs, and xml-stylesheet. Internal `#`\n * refs and ordinary drawing elements stay.\n */\nexport function sanitizeSvg(source: string): string {\n if (!/<svg[\\s>]/i.test(source)) {\n throw new Error(\"Not an SVG\");\n }\n\n let out = source.replace(/<\\?xml-stylesheet[\\s\\S]*?\\?>/gi, \"\");\n\n for (const tag of DANGEROUS_TAGS) {\n out = out.replace(new RegExp(`<${tag}\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/${tag}\\\\s*>`, \"gi\"), \"\");\n out = out.replace(new RegExp(`<${tag}\\\\b[^>]*\\\\/>`, \"gi\"), \"\");\n }\n\n out = out.replace(/\\s+on[a-z][a-z0-9-]*\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, \"\");\n out = out.replace(\n /((?:href|src|xlink:href)\\s*=\\s*)([\"'])\\s*(?:javascript:|data:text\\/html)[^\"']*\\2/gi,\n \"$1$2$2\",\n );\n\n return out;\n}\n\nexport function isSvgUpload(file: { mimetype?: string; name?: string; data?: Buffer }): boolean {\n if (file.mimetype === \"image/svg+xml\") return true;\n if (typeof file.name === \"string\" && file.name.toLowerCase().endsWith(\".svg\")) return true;\n return false;\n}\n","import type { Access, CollectionConfig } from \"payload\";\n\nimport { isSvgUpload, sanitizeSvg } from \"./sanitize\";\n\nexport const BRAND_ASSETS_SLUG = \"brand-assets\";\n\n/** SVG preferred; PNG allowed. ICO is for the favicon slot. */\nexport const BRAND_ASSETS_MIME_TYPES = [\n \"image/svg+xml\",\n \"image/png\",\n \"image/x-icon\",\n \"image/vnd.microsoft.icon\",\n] as const;\n\nexport interface CreateBrandAssetsOptions {\n /**\n * Required, no default — same shape as `createTheme`. `read` is typically\n * public so the live site can load the lockup; `update` is `canManageBrand`\n * and covers create, update, delete, and version history. Predicates live\n * in each site's `src/platform` until BIS-43.\n */\n access: { read: Access; update: Access };\n /** Default `brand-assets`. Pass the same slug to `createTheme({ logo })`. */\n slug?: string;\n}\n\nfunction requireAccess(options: CreateBrandAssetsOptions): CreateBrandAssetsOptions[\"access\"] {\n const read = options.access?.read;\n const update = options.access?.update;\n if (!read || !update) {\n throw new Error(\n \"createBrandAssets 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\n/**\n * Locked cupboard for lockup and mark. Not ordinary Media — only a brand\n * manager writes it. Theme's `logo` / `logoMark` point here when the site\n * passes this slug as `logo.collection`.\n */\nexport function createBrandAssets(options: CreateBrandAssetsOptions): CollectionConfig {\n const access = requireAccess(options);\n const slug = options.slug ?? BRAND_ASSETS_SLUG;\n\n return {\n slug,\n labels: { singular: \"Brand asset\", plural: \"Brand assets\" },\n admin: {\n group: \"Settings\",\n useAsTitle: \"label\",\n description: \"Logo, favicon, and mobile-menu mark. SVG preferred; PNG and ICO allowed.\",\n },\n upload: {\n mimeTypes: [...BRAND_ASSETS_MIME_TYPES],\n crop: false,\n focalPoint: false,\n },\n versions: true,\n access: {\n read: access.read,\n create: access.update,\n update: access.update,\n delete: access.update,\n readVersions: access.update,\n },\n fields: [\n { name: \"label\", type: \"text\", label: \"Label\" },\n { name: \"notes\", type: \"textarea\", label: \"Usage notes\" },\n { name: \"alt\", type: \"text\", label: \"Alt text\", required: true },\n ],\n hooks: {\n beforeOperation: [\n ({ req, operation }) => {\n if (operation !== \"create\" && operation !== \"update\") return;\n const file = req.file;\n if (!file || !isSvgUpload(file)) return;\n file.data = Buffer.from(sanitizeSvg(file.data.toString(\"utf8\")), \"utf8\");\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","/**\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"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["payloadSeoPlugin","requireAccess"],"sources":["../src/seo/fields.ts","../src/seo/text.ts","../src/seo/plugin.ts","../src/theme/appearance-labels.ts","../src/theme/publish.ts","../src/theme/global.ts","../src/brand-assets/sanitize.ts","../src/brand-assets/collection.ts","../src/theme/seed.ts","../src/theme/preview.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 * Named choices the Appearance child shows. Values stay the token presets;\n * labels match the mock workflow (Soft → pill, Gentle → minimal, Balanced\n * → default, Follow device → system). No rem, ms, or CSS variable tables.\n */\nexport const APPEARANCE_CHOICES = {\n defaultTheme: {\n light: { label: \"Light\", hint: \"Always opens in light mode\" },\n dark: { label: \"Dark\", hint: \"Always opens in dark mode\" },\n system: { label: \"Follow device\", hint: \"Matches the visitor’s system setting\" },\n },\n radius: {\n sharp: { label: \"Sharp\", hint: \"Crisp and structured\" },\n subtle: { label: \"Subtle\", hint: \"Clean with a little softness\" },\n rounded: { label: \"Rounded\", hint: \"Friendly and contemporary\" },\n pill: { label: \"Soft\", hint: \"Very rounded and expressive\" },\n },\n shadow: {\n flat: { label: \"Flat\", hint: \"Minimal visual depth\" },\n subtle: { label: \"Subtle\", hint: \"Light separation between surfaces\" },\n elevated: { label: \"Elevated\", hint: \"More noticeable layering and depth\" },\n },\n motion: {\n snappy: { label: \"Quick\", hint: \"Fast and responsive\" },\n smooth: { label: \"Smooth\", hint: \"Balanced and natural\" },\n minimal: { label: \"Gentle\", hint: \"Slower and more relaxed\" },\n },\n density: {\n compact: { label: \"Compact\", hint: \"Fits more content on screen\" },\n default: { label: \"Balanced\", hint: \"Comfortable for most sites\" },\n spacious: { label: \"Spacious\", hint: \"More breathing room and larger controls\" },\n },\n} as const;\n\nexport const APPEARANCE_FAMILY_COPY: Record<\n keyof typeof APPEARANCE_CHOICES,\n { title: string; hint: string }\n> = {\n defaultTheme: {\n title: \"Default theme\",\n hint: \"Choose how the public site opens for visitors.\",\n },\n radius: {\n title: \"Corner style\",\n hint: \"Controls the shape of buttons, inputs, cards, and panels across the site.\",\n },\n shadow: {\n title: \"Depth\",\n hint: \"Choose how much cards and floating elements stand out from the page.\",\n },\n motion: {\n title: \"Motion\",\n hint: \"Controls how quickly menus, cards, and interface transitions move.\",\n },\n density: {\n title: \"Spacing\",\n hint: \"Choose how compact or roomy forms, buttons, cards, and page sections feel.\",\n },\n};\n\nexport const FONT_ROLE_COPY = {\n heading: {\n label: \"Heading font\",\n hint: \"Used for page titles, section headings, and cards\",\n },\n body: {\n label: \"Body font\",\n hint: \"Used for paragraphs, navigation, forms, and buttons\",\n },\n} as const;\n\nexport const SYSTEM_COLOR_COPY: Record<\n \"primary\" | \"secondary\" | \"accent\" | \"highlight\" | \"success\" | \"destructive\",\n { label: string; hint: string; group: \"brand\" | \"status\" }\n> = {\n primary: { label: \"Primary\", hint: \"Primary actions and key links\", group: \"brand\" },\n secondary: { label: \"Secondary\", hint: \"Supporting accents and eyebrows\", group: \"brand\" },\n accent: { label: \"Accent\", hint: \"Dark brand surfaces\", group: \"brand\" },\n highlight: { label: \"Highlight\", hint: \"Promotional and emphasis fills\", group: \"brand\" },\n success: {\n label: \"Success\",\n hint: \"Theme chrome for confirmations. Not offered as a page-editor fill.\",\n group: \"status\",\n },\n destructive: {\n label: \"Destructive\",\n hint: \"Theme chrome for errors and dangerous actions. Not offered as a page-editor fill.\",\n group: \"status\",\n },\n};\n","import type { ThemeDoc } from \"./types\";\n\nexport const THEME_CHILDREN = [\"colors\", \"typography\", \"appearance\", \"identity\"] as const;\n\nexport type ThemeChild = (typeof THEME_CHILDREN)[number];\n\n/**\n * Publishing a child writes that slice only. Other responsibilities stay\n * as they already are on the stored document. Unsaved form state on a\n * different child never goes live. A first save (no stored row) still\n * writes only the named child; `getPublishedTheme` fills the rest from\n * the seed.\n */\nexport function publishThemeChild(stored: ThemeDoc, incoming: ThemeDoc, child: ThemeChild): ThemeDoc {\n if (child === \"colors\") return { ...stored, colors: incoming.colors };\n if (child === \"typography\") return { ...stored, typography: incoming.typography };\n if (child === \"appearance\") return { ...stored, appearance: incoming.appearance };\n return { ...stored, logo: incoming.logo, favicon: incoming.favicon, logoMark: incoming.logoMark };\n}\n\nexport function isThemeChild(value: unknown): value is ThemeChild {\n return typeof value === \"string\" && (THEME_CHILDREN as readonly string[]).includes(value);\n}\n","import { catalog, type FontEntry } from \"@bison-lab/fonts\";\nimport { presetHints, SHADE_STEPS } from \"@bison-lab/tokens\";\nimport type { Field, GlobalConfig, SelectField, TabsField } from \"payload\";\n\nimport {\n SAME_AS_BODY,\n THEME_APPEARANCE_FIELD,\n THEME_COLOR_SCALE_FIELD,\n THEME_CONTRAST_REPORT,\n THEME_FONT_FIELD,\n THEME_GREY_SCALE_FIELD,\n THEME_LIBRARY_FIELD,\n THEME_PAIRING_FIELD,\n THEME_SAVE_BUTTON,\n THEME_IDENTITY_FALLBACK,\n THEME_SECTION_HEADING,\n headingSelectValue,\n} from \"./fields\";\nimport { APPEARANCE_CHOICES } from \"./appearance-labels\";\nimport { themeConfigFromDoc, validateSourceIncluded, validateThemeHex } from \"./map\";\nimport { isThemeChild, publishThemeChild } from \"./publish\";\nimport { READABILITY_TARGET } from \"./readability\";\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 requireDestructive(options: CreateThemeOptions): string {\n const hex = options.destructive ?? options.seed.brandDestructive;\n if (!hex) {\n throw new Error(\n \"createTheme requires a destructive hex: pass options.destructive or seed.brandDestructive. There is no package default.\",\n );\n }\n return hex;\n}\n\nfunction namedSelect<K extends string>(\n name: string,\n label: string,\n choices: Record<K, { label: string; hint: string }>,\n defaultValue: K,\n field?: string,\n): SelectField {\n return {\n name,\n type: \"select\",\n label,\n required: true,\n defaultValue,\n options: (Object.entries(choices) as [K, { label: string; hint: string }][]).map(\n ([value, { label: optionLabel, hint }]) => ({\n label: `${optionLabel} — ${hint}`,\n value,\n }),\n ),\n ...(field ? { admin: { components: { Field: field } } } : {}),\n };\n}\n\nconst STEP_OPTIONS = SHADE_STEPS.map((step) => ({ label: String(step), value: String(step) }));\n\nfunction colorScaleFields(hexDefault?: string): Field[] {\n const hidden = { hidden: true } as const;\n return [\n {\n name: \"hex\",\n type: \"text\",\n label: \"Hex\",\n required: true,\n ...(hexDefault ? { defaultValue: hexDefault } : {}),\n validate: validateThemeHex,\n admin: hidden,\n },\n {\n name: \"sourceStep\",\n type: \"select\",\n label: \"Source step\",\n required: true,\n defaultValue: \"500\",\n options: STEP_OPTIONS,\n admin: hidden,\n },\n {\n name: \"scale\",\n type: \"json\",\n label: \"Scale\",\n admin: hidden,\n },\n {\n name: \"stale\",\n type: \"checkbox\",\n label: \"Stale\",\n defaultValue: false,\n admin: hidden,\n },\n {\n name: \"include\",\n type: \"select\",\n label: \"Include\",\n required: true,\n defaultValue: \"all\",\n options: [\n { label: \"Include all\", value: \"all\" },\n { label: \"Source only\", value: \"source\" },\n { label: \"Custom\", value: \"custom\" },\n ],\n admin: hidden,\n },\n {\n name: \"includedSteps\",\n type: \"select\",\n label: \"Included steps\",\n hasMany: true,\n options: STEP_OPTIONS,\n validate: validateSourceIncluded,\n admin: hidden,\n },\n ];\n}\n\nfunction systemColorGroup(name: string, label: string, hexDefault: string): Field {\n return {\n name,\n type: \"group\",\n label,\n fields: colorScaleFields(hexDefault),\n admin: { components: { Field: THEME_COLOR_SCALE_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. Field tabs: Colors, Typography, Appearance, Identity.\n * Publish in the Save slot writes the open tab only. No draft mode: a\n * save is live. `getPublishedTheme` reads that row. There is no Theme\n * preview pane.\n */\nexport function createTheme(options: CreateThemeOptions): GlobalConfig {\n const access = requireAccess(options);\n const destructive = requireDestructive(options);\n const {\n seed,\n fonts = catalog,\n fontsBaseUrl = \"/fonts\",\n contrastTarget = READABILITY_TARGET,\n logo,\n identity,\n onPublish,\n } = options;\n\n const tabs: TabsField[\"tabs\"] = [\n {\n label: \"Colors\",\n fields: [\n {\n name: \"colors\",\n type: \"group\",\n label: false,\n fields: [\n {\n name: \"brand\",\n type: \"group\",\n label: false,\n fields: [\n {\n name: \"themeColorsHeading\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_SECTION_HEADING },\n custom: {\n title: \"Theme colors\",\n hint: \"House semantic fills page editors can pick. Custom colors sit with these.\",\n },\n },\n },\n systemColorGroup(\"primary\", \"Primary\", seed.brandPrimary),\n systemColorGroup(\"secondary\", \"Secondary\", seed.brandSecondary),\n systemColorGroup(\"accent\", \"Accent\", seed.brandAccent),\n systemColorGroup(\"highlight\", \"Highlight\", seed.brandHighlight),\n {\n name: \"statusColorsHeading\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_SECTION_HEADING },\n custom: {\n title: \"Success and Destructive\",\n hint: \"Theme chrome only. These are not offered as fills when someone is writing a page.\",\n },\n },\n },\n systemColorGroup(\"success\", \"Success\", seed.brandSuccess),\n systemColorGroup(\"destructive\", \"Destructive\", destructive),\n ],\n },\n namedSelect(\"greyScale\", \"Gray family\", presetHints.greyScale, seed.greyScale, THEME_GREY_SCALE_FIELD),\n {\n name: \"library\",\n type: \"array\",\n label: \"Custom colors\",\n labels: { singular: \"Color\", plural: \"Colors\" },\n fields: [\n { name: \"key\", type: \"text\", label: \"Key\", required: true, admin: { hidden: true } },\n { name: \"label\", type: \"text\", label: \"Name\", required: true, admin: { hidden: true } },\n ...colorScaleFields(),\n ],\n admin: { components: { Field: THEME_LIBRARY_FIELD } },\n },\n ],\n },\n {\n name: \"contrast\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_CONTRAST_REPORT },\n custom: { target: contrastTarget },\n },\n },\n ],\n },\n {\n label: \"Typography\",\n fields: [\n {\n name: \"typography\",\n type: \"group\",\n label: false,\n fields: [\n fontField(\n \"heading\",\n \"Heading font\",\n headingSelectValue(seed.fontHeading),\n fonts,\n fontsBaseUrl,\n true,\n ),\n fontField(\"body\", \"Body font\", seed.fontBody, fonts, fontsBaseUrl, false),\n {\n name: \"pairing\",\n type: \"ui\",\n admin: { components: { Field: THEME_PAIRING_FIELD }, custom: { fontsBaseUrl } },\n },\n ],\n },\n ],\n },\n {\n label: \"Appearance\",\n fields: [\n {\n name: \"appearance\",\n type: \"group\",\n label: false,\n fields: [\n namedSelect(\"defaultTheme\", \"Default theme\", APPEARANCE_CHOICES.defaultTheme, seed.defaultTheme),\n namedSelect(\"radius\", \"Corner style\", APPEARANCE_CHOICES.radius, seed.radius),\n namedSelect(\"shadow\", \"Depth\", APPEARANCE_CHOICES.shadow, seed.shadow),\n namedSelect(\"motion\", \"Motion\", APPEARANCE_CHOICES.motion, seed.motion),\n namedSelect(\"density\", \"Spacing\", APPEARANCE_CHOICES.density, seed.density),\n ],\n admin: { components: { Field: THEME_APPEARANCE_FIELD } },\n },\n ],\n },\n ];\n\n tabs.push({\n label: \"Identity\",\n fields: logo\n ? [\n {\n name: \"logo\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Logo\",\n admin: {\n description: \"The full wordmark used in the header and footer.\",\n },\n },\n {\n name: \"favicon\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Favicon\",\n admin: {\n description: \"The small icon in the browser tab. PNG or SVG.\",\n },\n },\n {\n name: \"logoMark\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Mobile menu logo\",\n admin: {\n description:\n \"Optional. Used when the full logo is too wide for the mobile menu — typically the mark.\",\n },\n },\n ]\n : [\n {\n name: \"identityFallback\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_IDENTITY_FALLBACK },\n custom: { fallback: identity?.fallback },\n },\n },\n ],\n });\n\n const fields: Field[] = [\n { type: \"tabs\", tabs },\n {\n name: \"publishChild\",\n type: \"text\",\n admin: { hidden: true },\n },\n ];\n\n return {\n slug: THEME_SLUG,\n label: \"Theme\",\n admin: {\n group: \"Settings\",\n custom: { contrastTarget, fontsBaseUrl, identityFallback: identity?.fallback },\n components: {\n elements: {\n SaveButton: THEME_SAVE_BUTTON,\n },\n },\n },\n access: {\n read: access.read,\n update: access.update,\n },\n fields,\n hooks: {\n beforeChange: [\n ({ data, originalDoc }) => {\n const incoming = data as ThemeDoc & { publishChild?: string };\n const { publishChild, ...rest } = incoming;\n if (isThemeChild(publishChild)) {\n return publishThemeChild((originalDoc as ThemeDoc) ?? {}, rest, publishChild);\n }\n return rest;\n },\n ],\n afterChange: [\n async ({ doc }) => {\n if (!onPublish) return;\n const themeDoc = doc as ThemeDoc;\n await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);\n },\n ],\n },\n };\n}\n","const DANGEROUS_TAGS = [\"script\", \"foreignObject\", \"iframe\", \"embed\", \"object\"] as const;\n\n/**\n * Strips the SVG features a lockup does not need and an attacker would\n * use: script, foreignObject, iframe/embed/object, event handlers,\n * javascript: / data:text/html URLs, and xml-stylesheet. Internal `#`\n * refs and ordinary drawing elements stay.\n */\nexport function sanitizeSvg(source: string): string {\n if (!/<svg[\\s>]/i.test(source)) {\n throw new Error(\"Not an SVG\");\n }\n\n let out = source.replace(/<\\?xml-stylesheet[\\s\\S]*?\\?>/gi, \"\");\n\n for (const tag of DANGEROUS_TAGS) {\n out = out.replace(new RegExp(`<${tag}\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/${tag}\\\\s*>`, \"gi\"), \"\");\n out = out.replace(new RegExp(`<${tag}\\\\b[^>]*\\\\/>`, \"gi\"), \"\");\n }\n\n out = out.replace(/\\s+on[a-z][a-z0-9-]*\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, \"\");\n out = out.replace(\n /((?:href|src|xlink:href)\\s*=\\s*)([\"'])\\s*(?:javascript:|data:text\\/html)[^\"']*\\2/gi,\n \"$1$2$2\",\n );\n\n return out;\n}\n\nexport function isSvgUpload(file: { mimetype?: string; name?: string; data?: Buffer }): boolean {\n if (file.mimetype === \"image/svg+xml\") return true;\n if (typeof file.name === \"string\" && file.name.toLowerCase().endsWith(\".svg\")) return true;\n return false;\n}\n","import type { Access, CollectionConfig } from \"payload\";\n\nimport { isSvgUpload, sanitizeSvg } from \"./sanitize\";\n\nexport const BRAND_ASSETS_SLUG = \"brand-assets\";\n\n/** SVG preferred; PNG allowed. ICO is for the favicon slot. */\nexport const BRAND_ASSETS_MIME_TYPES = [\n \"image/svg+xml\",\n \"image/png\",\n \"image/x-icon\",\n \"image/vnd.microsoft.icon\",\n] as const;\n\nexport interface CreateBrandAssetsOptions {\n /**\n * Required, no default — same shape as `createTheme`. `read` is typically\n * public so the live site can load the lockup; `update` is `canManageBrand`\n * and covers create, update, delete, and version history. Predicates live\n * in each site's `src/platform` until BIS-43.\n */\n access: { read: Access; update: Access };\n /** Default `brand-assets`. Pass the same slug to `createTheme({ logo })`. */\n slug?: string;\n}\n\nfunction requireAccess(options: CreateBrandAssetsOptions): CreateBrandAssetsOptions[\"access\"] {\n const read = options.access?.read;\n const update = options.access?.update;\n if (!read || !update) {\n throw new Error(\n \"createBrandAssets 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\n/**\n * Locked cupboard for lockup and mark. Not ordinary Media — only a brand\n * manager writes it. Theme's `logo` / `logoMark` point here when the site\n * passes this slug as `logo.collection`.\n */\nexport function createBrandAssets(options: CreateBrandAssetsOptions): CollectionConfig {\n const access = requireAccess(options);\n const slug = options.slug ?? BRAND_ASSETS_SLUG;\n\n return {\n slug,\n labels: { singular: \"Brand asset\", plural: \"Brand assets\" },\n admin: {\n group: \"Settings\",\n useAsTitle: \"label\",\n description: \"Logo, favicon, and mobile-menu mark. SVG preferred; PNG and ICO allowed.\",\n },\n upload: {\n mimeTypes: [...BRAND_ASSETS_MIME_TYPES],\n crop: false,\n focalPoint: false,\n },\n versions: true,\n access: {\n read: access.read,\n create: access.update,\n update: access.update,\n delete: access.update,\n readVersions: access.update,\n },\n fields: [\n { name: \"label\", type: \"text\", label: \"Label\" },\n { name: \"notes\", type: \"textarea\", label: \"Usage notes\" },\n { name: \"alt\", type: \"text\", label: \"Alt text\", required: true },\n ],\n hooks: {\n beforeOperation: [\n ({ req, operation }) => {\n if (operation !== \"create\" && operation !== \"update\") return;\n const file = req.file;\n if (!file || !isSvgUpload(file)) return;\n file.data = Buffer.from(sanitizeSvg(file.data.toString(\"utf8\")), \"utf8\");\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","/**\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"],"mappings":"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"}
package/dist/theme.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as SYSTEM_COLOR_KEYS, c as themeConfigFromDoc, i as themeColorKeys, n as deleteLibraryColor, o as THEME_SLUG, r as rewriteColorToken, t as resolveThemeIdentity } from "./identity-D1X0Om4Z.mjs";
1
+ import { a as SYSTEM_COLOR_KEYS, c as themeConfigFromDoc, i as themeColorKeys, n as deleteLibraryColor, o as THEME_SLUG, r as rewriteColorToken, t as resolveThemeIdentity } from "./identity-Bg-G05mb.mjs";
2
2
  import { fontFaceCss, fontPreloads } from "@bison-lab/fonts";
3
3
  import { buildThemeCss, themeFontIds } from "@bison-lab/tokens";
4
4
  //#region src/theme/published.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bison-lab/payload-core",
3
- "version": "3.9.0",
3
+ "version": "3.10.0",
4
4
  "description": "Site-agnostic Payload CMS configuration for Bison Lab sites: the SEO tab, the Theme global, the brand-assets cupboard, and the metadata and theme readers for the pages they describe",
5
5
  "homepage": "https://components.bisonlab.ai",
6
6
  "repository": {
@@ -76,6 +76,7 @@
76
76
  "@testing-library/jest-dom": "^6.9.1",
77
77
  "@testing-library/react": "^16.3.2",
78
78
  "@types/react": "^19.2.14",
79
+ "@types/react-dom": "^19.2.3",
79
80
  "jsdom": "^29.0.1",
80
81
  "payload": "^3.88.0",
81
82
  "react": "^19.2.4",
@@ -83,9 +84,9 @@
83
84
  "tsdown": "^0.21.0",
84
85
  "typescript": "^5.9.3",
85
86
  "vitest": "^4.1.2",
86
- "@bison-lab/fonts": "3.6.0",
87
87
  "@bison-lab/tokens": "3.9.0",
88
- "@bison-lab/ui": "3.9.0"
88
+ "@bison-lab/ui": "3.9.0",
89
+ "@bison-lab/fonts": "3.6.0"
89
90
  },
90
91
  "publishConfig": {
91
92
  "access": "public"