@bison-lab/payload-core 3.10.0 → 3.11.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.
@@ -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_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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"}
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: \"Success messages and confirmations\",\n group: \"status\",\n },\n destructive: {\n label: \"Destructive\",\n hint: \"Errors, dangerous actions, and destructive confirmations\",\n group: \"status\",\n },\n};\n","import { THEME_SLUG, type ThemeColorDoc, 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\nexport function sliceFromTheme(theme: ThemeDoc, child: ThemeChild): ThemeDoc {\n if (child === \"colors\") return { colors: theme.colors };\n if (child === \"typography\") return { typography: theme.typography };\n if (child === \"appearance\") return { appearance: theme.appearance };\n return { logo: theme.logo, favicon: theme.favicon, logoMark: theme.logoMark };\n}\n\nfunction colorHex(color: ThemeColorDoc | string | null | undefined): string | undefined {\n if (typeof color === \"string\") return color;\n return color?.hex ?? undefined;\n}\n\n/**\n * A Theme page is a real Global. Hydrate from the store only when this\n * page has never been saved — otherwise afterRead would overwrite the form.\n */\nexport function hasThemeSlice(doc: ThemeDoc | null | undefined, child: ThemeChild): boolean {\n if (!doc) return false;\n if (child === \"colors\") return Boolean(colorHex(doc.colors?.brand?.primary));\n if (child === \"typography\") return Boolean(doc.typography?.body);\n if (child === \"appearance\") return Boolean(doc.appearance?.radius);\n return Boolean(doc.logo || doc.favicon || doc.logoMark);\n}\n\nexport interface ThemeStorePayload {\n findGlobal: (args: {\n slug: string;\n draft?: boolean;\n depth?: number;\n overrideAccess?: boolean;\n }) => Promise<ThemeDoc | null | undefined>;\n updateGlobal: (args: { slug: string; data: ThemeDoc; draft?: boolean }) => Promise<unknown>;\n}\n\n/**\n * Save on a Theme page writes that slice onto the hidden `theme` row.\n * Other children stay as stored.\n */\nexport async function persistThemeChild(\n payload: ThemeStorePayload,\n incoming: ThemeDoc,\n child: ThemeChild,\n): Promise<ThemeDoc> {\n const stored =\n (await payload.findGlobal({\n slug: THEME_SLUG,\n draft: false,\n overrideAccess: true,\n })) ?? {};\n const next = publishThemeChild(stored, incoming, child);\n await payload.updateGlobal({ slug: THEME_SLUG, data: next, draft: false });\n return next;\n}\n","import { catalog, type FontEntry } from \"@bison-lab/fonts\";\nimport { DESTRUCTIVE_SCALE_HEX, presetHints, SHADE_STEPS } from \"@bison-lab/tokens\";\n\n/** Same value as tokens `SUCCESS_SCALE_HEX`. Inlined so an older tokens peer still boots. */\nconst LIBRARY_SUCCESS_HEX = \"#22c55e\";\nimport type { Field, GlobalConfig, SelectField } from \"payload\";\n\nimport {\n SAME_AS_BODY,\n THEME_APPEARANCE_FIELD,\n THEME_COLOR_SCALE_FIELD,\n THEME_FONT_FIELD,\n THEME_GREY_SCALE_FIELD,\n THEME_LIBRARY_FIELD,\n THEME_PAIRING_FIELD,\n THEME_DOCUMENT_CONTROLS,\n THEME_SECTION_HEADING,\n headingSelectValue,\n} from \"./fields\";\nimport { APPEARANCE_CHOICES } from \"./appearance-labels\";\nimport { themeConfigFromDoc, validateSourceIncluded, validateThemeHex } from \"./map\";\nimport { hasThemeSlice, persistThemeChild, sliceFromTheme, type ThemeChild } from \"./publish\";\nimport { READABILITY_TARGET } from \"./readability\";\nimport {\n THEME_APPEARANCE_SLUG,\n THEME_COLORS_SLUG,\n THEME_IDENTITY_SLUG,\n THEME_SLUG,\n THEME_TYPOGRAPHY_SLUG,\n type CreateThemeOptions,\n type ThemeDoc,\n} 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 return options.destructive ?? options.seed.brandDestructive ?? DESTRUCTIVE_SCALE_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 }, hideGutter: true },\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 colorsFields(seed: CreateThemeOptions[\"seed\"], destructive: string): Field[] {\n return [\n {\n name: \"colors\",\n type: \"group\",\n label: false,\n admin: { hideGutter: true, width: \"100%\" },\n fields: [\n {\n name: \"brand\",\n type: \"group\",\n label: false,\n admin: { hideGutter: true, width: \"100%\" },\n fields: [\n {\n name: \"themeColorsHeading\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_SECTION_HEADING },\n custom: {\n title: \"Brand colors\",\n hint: \"These colors define the visual identity of the site. Adjust them freely to match the brand.\",\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: \"libraryPlacement\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_LIBRARY_FIELD },\n custom: { dataPath: \"colors.library\" },\n },\n },\n {\n name: \"greyPlacement\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_GREY_SCALE_FIELD },\n custom: { dataPath: \"colors.greyScale\" },\n },\n },\n {\n name: \"statusColorsHeading\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_SECTION_HEADING },\n custom: {\n title: \"Status colors\",\n hint: \"These colors communicate meaning across the site. You can adjust them to fit your brand, but we'll help keep them recognizable as success and error states.\",\n },\n },\n },\n systemColorGroup(\"success\", \"Success\", seed.brandSuccess || LIBRARY_SUCCESS_HEX),\n systemColorGroup(\"destructive\", \"Destructive\", destructive),\n ],\n },\n {\n ...namedSelect(\"greyScale\", \"Gray family\", presetHints.greyScale, seed.greyScale, THEME_GREY_SCALE_FIELD),\n admin: { hidden: true, components: { Field: THEME_GREY_SCALE_FIELD } },\n },\n {\n name: \"library\",\n type: \"array\",\n label: \"Additional 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: { hidden: true, components: { Field: THEME_LIBRARY_FIELD }, width: \"100%\" },\n },\n ],\n },\n ];\n}\n\nfunction typographyFields(\n seed: CreateThemeOptions[\"seed\"],\n fonts: readonly FontEntry[],\n fontsBaseUrl: string,\n): Field[] {\n return [\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 ];\n}\n\nfunction appearanceFields(seed: CreateThemeOptions[\"seed\"]): Field[] {\n return [\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 ];\n}\n\nfunction identityFields(collection: string): Field[] {\n return [\n {\n name: \"logo\",\n type: \"upload\",\n relationTo: 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: 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: 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\nfunction pageHooks(child: ThemeChild) {\n return {\n beforeChange: [\n async ({ data, req }: { data: ThemeDoc; req?: { payload?: Parameters<typeof persistThemeChild>[0] } }) => {\n if (req?.payload) await persistThemeChild(req.payload, data, child);\n return data;\n },\n ],\n afterRead: [\n async ({\n doc,\n req,\n }: {\n doc: ThemeDoc;\n req?: {\n payload?: {\n findGlobal: (args: {\n slug: string;\n draft?: boolean;\n depth?: number;\n overrideAccess?: boolean;\n }) => Promise<ThemeDoc | null | undefined>;\n };\n };\n }) => {\n if (hasThemeSlice(doc, child)) return doc;\n const theme = await req?.payload?.findGlobal({\n slug: THEME_SLUG,\n draft: false,\n depth: child === \"identity\" ? 1 : 0,\n overrideAccess: true,\n });\n if (!theme) return doc;\n return { ...doc, ...sliceFromTheme(theme, child) };\n },\n ],\n };\n}\n\n/**\n * Standard Payload Globals in an `admin.group: \"Theme\"` nav section —\n * the same pattern as Content and Settings. Each page is its own Global\n * (Save, fields, access). A hidden `theme` store is the published read\n * model `getPublishedTheme` already knows. Locking is off: these are\n * settings forms, not collaborative documents. No draft mode, no preview\n * 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 adminCustom = { contrastTarget, fontsBaseUrl, identityFallback: identity?.fallback };\n const colorFields = colorsFields(seed, destructive);\n const typeFields = typographyFields(seed, fonts, fontsBaseUrl);\n const lookFields = appearanceFields(seed);\n const markFields = logo ? identityFields(logo.collection) : [];\n\n const store: GlobalConfig = {\n slug: THEME_SLUG,\n label: \"Theme\",\n lockDocuments: false,\n admin: {\n hidden: true,\n hideAPIURL: true,\n custom: adminCustom,\n },\n access: {\n read: access.read,\n update: access.update,\n },\n fields: [...colorFields, ...typeFields, ...lookFields, ...markFields],\n hooks: {\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 function page(\n slug: string,\n label: string,\n child: ThemeChild,\n fields: Field[],\n preview: boolean,\n ): GlobalConfig {\n return {\n slug,\n label,\n lockDocuments: false,\n admin: {\n group: \"Theme\",\n hideAPIURL: true,\n custom: adminCustom,\n components: preview\n ? { elements: { beforeDocumentControls: [THEME_DOCUMENT_CONTROLS] } }\n : undefined,\n },\n access: {\n read: access.read,\n update: access.update,\n },\n fields,\n hooks: pageHooks(child),\n };\n }\n\n return [\n store,\n page(THEME_COLORS_SLUG, \"Colors\", \"colors\", colorFields, true),\n page(THEME_TYPOGRAPHY_SLUG, \"Typography\", \"typography\", typeFields, true),\n page(THEME_APPEARANCE_SLUG, \"Appearance\", \"appearance\", lookFields, true),\n ...(logo ? [page(THEME_IDENTITY_SLUG, \"Identity\", \"identity\", markFields, false)] : []),\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,5 +1,5 @@
1
1
  "use client";
2
- import { SHADE_STEPS, createColorScale, generateColorScale, isThemeHex } from "@bison-lab/tokens";
2
+ import { DESTRUCTIVE_SCALE_HEX, SHADE_STEPS, createColorScale, generateColorScale, includedShadeSteps, isThemeHex } from "@bison-lab/tokens";
3
3
  //#region src/theme/map.ts
4
4
  function pick(value, fallback) {
5
5
  return value == null ? fallback : value;
@@ -62,7 +62,7 @@ function themeConfigFromDoc(doc, seed) {
62
62
  brandAccent: hexFromColor(brand?.accent, seed.brandAccent),
63
63
  brandHighlight: hexFromColor(brand?.highlight, seed.brandHighlight),
64
64
  brandSuccess: hexFromColor(brand?.success, seed.brandSuccess),
65
- brandDestructive: hexFromColor(brand?.destructive, seed.brandDestructive ?? "") || seed.brandDestructive,
65
+ brandDestructive: hexFromColor(brand?.destructive, seed.brandDestructive ?? DESTRUCTIVE_SCALE_HEX) || seed.brandDestructive || DESTRUCTIVE_SCALE_HEX,
66
66
  greyScale: pick(doc?.colors?.greyScale ?? doc?.greyScale, seed.greyScale),
67
67
  radius: pick(appearance?.radius ?? doc?.radius, seed.radius),
68
68
  shadow: pick(appearance?.shadow ?? doc?.shadow, seed.shadow),
@@ -77,8 +77,7 @@ function themeConfigFromDoc(doc, seed) {
77
77
  }
78
78
  /** The document `seedTheme` writes so a fresh Global matches the seed config. */
79
79
  function docFromConfig(config, destructive) {
80
- const destructiveHex = destructive ?? config.brandDestructive;
81
- if (!destructiveHex) throw new Error("docFromConfig requires a destructive hex on the config or as the second argument");
80
+ const destructiveHex = destructive ?? config.brandDestructive ?? DESTRUCTIVE_SCALE_HEX;
82
81
  return {
83
82
  colors: {
84
83
  brand: {
@@ -106,6 +105,95 @@ function docFromConfig(config, destructive) {
106
105
  };
107
106
  }
108
107
  //#endregion
109
- export { themeConfigFromDoc as i, docFromConfig as n, stateFromColorDoc as r, colorDocFromState as t };
108
+ //#region src/theme/types.ts
109
+ const SYSTEM_COLOR_KEYS = [
110
+ "primary",
111
+ "secondary",
112
+ "accent",
113
+ "highlight",
114
+ "success",
115
+ "destructive"
116
+ ];
117
+ //#endregion
118
+ //#region src/theme/looks.ts
119
+ /**
120
+ * System colors Color Settings offers as page-editor fills. Success and
121
+ * Destructive stay off this list — they are theme chrome only.
122
+ */
123
+ const PAGE_EDITOR_SYSTEM_KEYS = [
124
+ "primary",
125
+ "secondary",
126
+ "accent",
127
+ "highlight"
128
+ ];
129
+ const SYSTEM_LABELS = {
130
+ primary: "Primary",
131
+ secondary: "Secondary",
132
+ accent: "Accent",
133
+ highlight: "Highlight"
134
+ };
135
+ function brandColor(doc, key) {
136
+ const value = (doc?.colors?.brand ?? doc?.brand)?.[key];
137
+ if (typeof value === "string" || value == null) return value == null ? null : { hex: value };
138
+ return value;
139
+ }
140
+ function themeLibraryFromDoc(doc) {
141
+ return Array.isArray(doc?.colors?.library) ? doc.colors.library : [];
142
+ }
143
+ function libraryRows(doc) {
144
+ return themeLibraryFromDoc(doc);
145
+ }
146
+ function titleCase(key) {
147
+ return key.charAt(0).toUpperCase() + key.slice(1);
148
+ }
149
+ function stepsFor(color, fallbackHex) {
150
+ return includedShadeSteps(stateFromColorDoc(color, fallbackHex));
151
+ }
152
+ /**
153
+ * Family keys a `lookField()` may offer: the page-editor system scales
154
+ * plus every custom color on Colors. Adding Coral on Theme makes `coral`
155
+ * appear here with no `@bison-lab/*` release.
156
+ */
157
+ function pageEditorLooks(doc) {
158
+ const system = PAGE_EDITOR_SYSTEM_KEYS.map((value) => ({
159
+ label: SYSTEM_LABELS[value],
160
+ value
161
+ }));
162
+ const custom = libraryRows(doc).map((row) => {
163
+ const value = row.key?.trim();
164
+ if (!value) return null;
165
+ return {
166
+ label: row.label?.trim() || titleCase(value),
167
+ value
168
+ };
169
+ }).filter((option) => option !== null);
170
+ return [...system, ...custom];
171
+ }
172
+ /**
173
+ * Included steps as `coral-400`. Excluded steps do not appear. System
174
+ * scales with no stored include list offer the full 50–950 ramp.
175
+ */
176
+ function pageEditorTokens(doc) {
177
+ const tokens = [];
178
+ for (const key of PAGE_EDITOR_SYSTEM_KEYS) {
179
+ const label = SYSTEM_LABELS[key];
180
+ for (const step of stepsFor(brandColor(doc, key), "#000000")) tokens.push({
181
+ label: `${label} ${step}`,
182
+ value: `${key}-${step}`
183
+ });
184
+ }
185
+ for (const row of libraryRows(doc)) {
186
+ const key = row.key?.trim();
187
+ if (!key) continue;
188
+ const label = row.label?.trim() || titleCase(key);
189
+ for (const step of stepsFor(row, row.hex ?? "#000000")) tokens.push({
190
+ label: `${label} ${step}`,
191
+ value: `${key}-${step}`
192
+ });
193
+ }
194
+ return tokens;
195
+ }
196
+ //#endregion
197
+ export { colorDocFromState as a, themeConfigFromDoc as c, SYSTEM_COLOR_KEYS as i, pageEditorLooks as n, docFromConfig as o, pageEditorTokens as r, stateFromColorDoc as s, PAGE_EDITOR_SYSTEM_KEYS as t };
110
198
 
111
- //# sourceMappingURL=map-BUDkX8g1.mjs.map
199
+ //# sourceMappingURL=looks-BbL309kO.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"looks-BbL309kO.mjs","names":[],"sources":["../src/theme/map.ts","../src/theme/types.ts","../src/theme/looks.ts"],"sourcesContent":["import {\n createColorScale,\n DESTRUCTIVE_SCALE_HEX,\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:\n hexFromColor(brand?.destructive, seed.brandDestructive ?? DESTRUCTIVE_SCALE_HEX) ||\n seed.brandDestructive ||\n DESTRUCTIVE_SCALE_HEX,\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 ?? DESTRUCTIVE_SCALE_HEX;\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. Falls back to `seed.brandDestructive`, then\n * the library's `DESTRUCTIVE_SCALE_HEX` (`#ef4444`).\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 the Identity page (logo, favicon, mobile-menu mark) when given.\n * Pass `BRAND_ASSETS_SLUG` (or the slug you gave `createBrandAssets`).\n * Save on that page writes only those uploads onto the stored Theme row.\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 * Custom Colors rows. `themeHead` emits their 50–950 variables and\n * `[data-look]` bindings so a picker key paints without a package bump.\n */\n library?: ThemeLibraryColorDoc[] | null;\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\";\nexport const THEME_COLORS_SLUG = \"theme-colors\";\nexport const THEME_TYPOGRAPHY_SLUG = \"theme-typography\";\nexport const THEME_APPEARANCE_SLUG = \"theme-appearance\";\nexport const THEME_IDENTITY_SLUG = \"theme-identity\";\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 { includedShadeSteps, type ShadeStep } from \"@bison-lab/tokens\";\n\nimport { stateFromColorDoc } from \"./map\";\nimport { THEME_SLUG, type ThemeColorDoc, type ThemeDoc, type ThemeLibraryColorDoc } from \"./types\";\n\n/**\n * System colors Color Settings offers as page-editor fills. Success and\n * Destructive stay off this list — they are theme chrome only.\n */\nexport const PAGE_EDITOR_SYSTEM_KEYS = [\"primary\", \"secondary\", \"accent\", \"highlight\"] as const;\n\nexport type PageEditorSystemKey = (typeof PAGE_EDITOR_SYSTEM_KEYS)[number];\n\nexport interface LookOption {\n label: string;\n value: string;\n}\n\nconst SYSTEM_LABELS: Record<PageEditorSystemKey, string> = {\n primary: \"Primary\",\n secondary: \"Secondary\",\n accent: \"Accent\",\n highlight: \"Highlight\",\n};\n\nfunction brandColor(doc: ThemeDoc | null | undefined, key: PageEditorSystemKey): ThemeColorDoc | null {\n const brand = doc?.colors?.brand ?? doc?.brand;\n const value = brand?.[key];\n if (typeof value === \"string\" || value == null) return value == null ? null : { hex: value };\n return value;\n}\n\nexport function themeLibraryFromDoc(doc: ThemeDoc | null | undefined): ThemeLibraryColorDoc[] {\n return Array.isArray(doc?.colors?.library) ? doc.colors.library : [];\n}\n\nfunction libraryRows(doc: ThemeDoc | null | undefined): ThemeLibraryColorDoc[] {\n return themeLibraryFromDoc(doc);\n}\n\nfunction titleCase(key: string): string {\n return key.charAt(0).toUpperCase() + key.slice(1);\n}\n\nfunction stepsFor(color: ThemeColorDoc | null | undefined, fallbackHex: string): ShadeStep[] {\n return includedShadeSteps(stateFromColorDoc(color, fallbackHex));\n}\n\n/**\n * Family keys a `lookField()` may offer: the page-editor system scales\n * plus every custom color on Colors. Adding Coral on Theme makes `coral`\n * appear here with no `@bison-lab/*` release.\n */\nexport function pageEditorLooks(doc: ThemeDoc | null | undefined): LookOption[] {\n const system = PAGE_EDITOR_SYSTEM_KEYS.map((value) => ({\n label: SYSTEM_LABELS[value],\n value,\n }));\n const custom = libraryRows(doc)\n .map((row) => {\n const value = row.key?.trim();\n if (!value) return null;\n return { label: row.label?.trim() || titleCase(value), value };\n })\n .filter((option): option is LookOption => option !== null);\n return [...system, ...custom];\n}\n\n/**\n * Included steps as `coral-400`. Excluded steps do not appear. System\n * scales with no stored include list offer the full 50–950 ramp.\n */\nexport function pageEditorTokens(doc: ThemeDoc | null | undefined): LookOption[] {\n const tokens: LookOption[] = [];\n for (const key of PAGE_EDITOR_SYSTEM_KEYS) {\n const label = SYSTEM_LABELS[key];\n for (const step of stepsFor(brandColor(doc, key), \"#000000\")) {\n tokens.push({ label: `${label} ${step}`, value: `${key}-${step}` });\n }\n }\n for (const row of libraryRows(doc)) {\n const key = row.key?.trim();\n if (!key) continue;\n const label = row.label?.trim() || titleCase(key);\n for (const step of stepsFor(row, row.hex ?? \"#000000\")) {\n tokens.push({ label: `${label} ${step}`, value: `${key}-${step}` });\n }\n }\n return tokens;\n}\n\nexport async function themeDocFromRequest(req: {\n payload?: {\n findGlobal?: (args: {\n slug: string;\n depth?: number;\n overrideAccess?: boolean;\n }) => Promise<ThemeDoc | null | undefined>;\n };\n}): Promise<ThemeDoc | null> {\n try {\n const doc = await req.payload?.findGlobal?.({\n slug: THEME_SLUG,\n depth: 0,\n overrideAccess: true,\n });\n return doc ?? null;\n } catch {\n return null;\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { FontEntry } from "@bison-lab/fonts";
2
2
  import { ColorScale, ColorScaleInclude, ShadeStep, ThemeConfig } from "@bison-lab/tokens";
3
- import { Access } from "payload";
3
+ import { Access, StaticLabel, TextField, TextFieldSingleValidation } from "payload";
4
4
 
5
5
  //#region src/theme/identity.d.ts
6
6
  interface ThemeIdentityAsset {
@@ -119,8 +119,8 @@ interface CreateThemeOptions {
119
119
  /** The site's `bison.config.json`. Every field's `defaultValue`, and the fallback `getPublishedTheme` returns. */
120
120
  seed: ThemeConfig;
121
121
  /**
122
- * Destructive seed hex. Required unless `seed.brandDestructive` is set.
123
- * Not a package default — the site names it.
122
+ * Destructive seed hex. Falls back to `seed.brandDestructive`, then
123
+ * the library's `DESTRUCTIVE_SCALE_HEX` (`#ef4444`).
124
124
  */
125
125
  destructive?: string;
126
126
  /** Default: the whole catalogue. */
@@ -135,9 +135,9 @@ interface CreateThemeOptions {
135
135
  */
136
136
  previewPath?: string;
137
137
  /**
138
- * Adds logo, favicon, and mobile-menu mark upload relations when
139
- * given. Pass `BRAND_ASSETS_SLUG` (or the slug you gave
140
- * `createBrandAssets`). Identity is a Theme child with its own Publish.
138
+ * Adds the Identity page (logo, favicon, mobile-menu mark) when given.
139
+ * Pass `BRAND_ASSETS_SLUG` (or the slug you gave `createBrandAssets`).
140
+ * Save on that page writes only those uploads onto the stored Theme row.
141
141
  */
142
142
  logo?: {
143
143
  collection: string;
@@ -156,6 +156,11 @@ interface ThemeHeadOptions {
156
156
  fontsBaseUrl?: string;
157
157
  attribute?: "class" | "data-theme";
158
158
  identity?: ResolvedThemeIdentity;
159
+ /**
160
+ * Custom Colors rows. `themeHead` emits their 50–950 variables and
161
+ * `[data-look]` bindings so a picker key paints without a package bump.
162
+ */
163
+ library?: ThemeLibraryColorDoc[] | null;
159
164
  }
160
165
  interface ThemeHead {
161
166
  css: string;
@@ -166,6 +171,10 @@ interface ThemeHead {
166
171
  identity?: ResolvedThemeIdentity;
167
172
  }
168
173
  declare const THEME_SLUG = "theme";
174
+ declare const THEME_COLORS_SLUG = "theme-colors";
175
+ declare const THEME_TYPOGRAPHY_SLUG = "theme-typography";
176
+ declare const THEME_APPEARANCE_SLUG = "theme-appearance";
177
+ declare const THEME_IDENTITY_SLUG = "theme-identity";
169
178
  declare const SYSTEM_COLOR_KEYS: readonly ["primary", "secondary", "accent", "highlight", "success", "destructive"];
170
179
  type SystemColorKey = (typeof SYSTEM_COLOR_KEYS)[number];
171
180
  //#endregion
@@ -185,5 +194,50 @@ declare function themeColorKeys(doc: ThemeDoc | null | undefined): string[];
185
194
  */
186
195
  declare function deleteLibraryColor(doc: ThemeDoc, key: string, replacement?: string): ThemeDoc;
187
196
  //#endregion
188
- export { resolveThemeIdentity as _, EditableTheme as a, THEME_SLUG as c, ThemeHead as d, ThemeHeadOptions as f, ThemeIdentityFallback as g, ResolvedThemeIdentity as h, CreateThemeOptions as i, ThemeColorDoc as l, ThemeUploadDoc as m, rewriteColorToken as n, SYSTEM_COLOR_KEYS as o, ThemeLibraryColorDoc as p, themeColorKeys as r, SystemColorKey as s, deleteLibraryColor as t, ThemeDoc as u };
189
- //# sourceMappingURL=library-BGq4wGif.d.mts.map
197
+ //#region src/theme/look-field.d.ts
198
+ interface LookFieldOptions {
199
+ name?: string;
200
+ label?: StaticLabel;
201
+ required?: boolean;
202
+ admin?: TextField["admin"];
203
+ validate?: TextFieldSingleValidation;
204
+ }
205
+ type LookFieldMode = "look" | "token";
206
+ /**
207
+ * A picker any collection or global can offer. Options come from the
208
+ * published Theme at request time: page-editor system scales plus custom
209
+ * Colors. Payload select options are a static list, so this is a text
210
+ * field with an admin picker — adding Coral does not need a schema change.
211
+ */
212
+ declare function lookField(overrides?: LookFieldOptions): TextField;
213
+ /**
214
+ * Included steps as `coral-400`. Excluded steps do not appear.
215
+ */
216
+ declare function colorTokenField(overrides?: LookFieldOptions): TextField;
217
+ //#endregion
218
+ //#region src/theme/looks.d.ts
219
+ /**
220
+ * System colors Color Settings offers as page-editor fills. Success and
221
+ * Destructive stay off this list — they are theme chrome only.
222
+ */
223
+ declare const PAGE_EDITOR_SYSTEM_KEYS: readonly ["primary", "secondary", "accent", "highlight"];
224
+ type PageEditorSystemKey = (typeof PAGE_EDITOR_SYSTEM_KEYS)[number];
225
+ interface LookOption {
226
+ label: string;
227
+ value: string;
228
+ }
229
+ declare function themeLibraryFromDoc(doc: ThemeDoc | null | undefined): ThemeLibraryColorDoc[];
230
+ /**
231
+ * Family keys a `lookField()` may offer: the page-editor system scales
232
+ * plus every custom color on Colors. Adding Coral on Theme makes `coral`
233
+ * appear here with no `@bison-lab/*` release.
234
+ */
235
+ declare function pageEditorLooks(doc: ThemeDoc | null | undefined): LookOption[];
236
+ /**
237
+ * Included steps as `coral-400`. Excluded steps do not appear. System
238
+ * scales with no stored include list offer the full 50–950 ramp.
239
+ */
240
+ declare function pageEditorTokens(doc: ThemeDoc | null | undefined): LookOption[];
241
+ //#endregion
242
+ export { ThemeIdentityFallback as A, ThemeColorDoc as C, ThemeLibraryColorDoc as D, ThemeHeadOptions as E, ThemeUploadDoc as O, THEME_TYPOGRAPHY_SLUG as S, ThemeHead as T, SystemColorKey as _, pageEditorTokens as a, THEME_IDENTITY_SLUG as b, LookFieldOptions as c, deleteLibraryColor as d, rewriteColorToken as f, SYSTEM_COLOR_KEYS as g, EditableTheme as h, pageEditorLooks as i, resolveThemeIdentity as j, ResolvedThemeIdentity as k, colorTokenField as l, CreateThemeOptions as m, PAGE_EDITOR_SYSTEM_KEYS as n, themeLibraryFromDoc as o, themeColorKeys as p, PageEditorSystemKey as r, LookFieldMode as s, LookOption as t, lookField as u, THEME_APPEARANCE_SLUG as v, ThemeDoc as w, THEME_SLUG as x, THEME_COLORS_SLUG as y };
243
+ //# sourceMappingURL=looks-DsizRfFV.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"looks-DsizRfFV.d.mts","names":[],"sources":["../src/theme/identity.ts","../src/theme/types.ts","../src/theme/library.ts","../src/theme/look-field.ts","../src/theme/looks.ts"],"mappings":"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"}
package/dist/react.mjs CHANGED
@@ -1,25 +1,50 @@
1
1
  "use client";
2
- import { i as themeConfigFromDoc, n as docFromConfig } from "./map-BUDkX8g1.mjs";
3
- import { auditTheme, buildThemeCss, isThemeHex, themeFontIds } from "@bison-lab/tokens";
2
+ import { c as themeConfigFromDoc, o as docFromConfig, s as stateFromColorDoc, t as PAGE_EDITOR_SYSTEM_KEYS } from "./looks-BbL309kO.mjs";
3
+ import { auditTheme, buildThemeCss, generateNamedScaleCSS, isThemeHex, lookBindingCss, themeFontIds } from "@bison-lab/tokens";
4
4
  import { useRef, useState } from "react";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  import { fontFaceCss, fontPreloads } from "@bison-lab/fonts";
7
7
  import { useLivePreview } from "@payloadcms/live-preview-react";
8
8
  import { ContrastStrip, ThemeShowcase } from "@bison-lab/ui";
9
+ //#region src/theme/look-css.ts
10
+ /**
11
+ * Custom `--coral-*` variables plus `[data-look]` bindings for every
12
+ * page-editor family. System bindings always ship so a look key paints
13
+ * before anyone adds a custom scale.
14
+ */
15
+ function themeLookCss(library) {
16
+ const rows = Array.isArray(library) ? library : [];
17
+ const customKeys = [];
18
+ const scaleLines = [];
19
+ for (const row of rows) {
20
+ const key = row.key?.trim();
21
+ if (!key) continue;
22
+ customKeys.push(key);
23
+ const state = stateFromColorDoc(row, row.hex ?? "#000000");
24
+ scaleLines.push(generateNamedScaleCSS(key, state.scale));
25
+ }
26
+ return [scaleLines.length ? `:root {\n${scaleLines.join("\n")}\n}` : "", lookBindingCss([...PAGE_EDITOR_SYSTEM_KEYS, ...customKeys])].filter(Boolean).join("\n\n");
27
+ }
28
+ //#endregion
9
29
  //#region src/theme/head.ts
10
30
  /**
11
31
  * `@font-face` plus `buildThemeCss` as one stylesheet, and the preload
12
32
  * list for the families the config names. A root layout (and the admin
13
33
  * layout, if it should restyle too) renders `css` in a `<style>` and
14
34
  * each preload as `<link rel="preload" as="font">`.
35
+ *
36
+ * `library` adds custom `--coral-*` variables and `[data-look]` bindings
37
+ * so a Color Settings scale paints the moment it is published.
15
38
  */
16
39
  function themeHead(config, options = {}) {
17
40
  const fontsBaseUrl = options.fontsBaseUrl ?? "/fonts";
18
41
  const ids = themeFontIds(config);
19
42
  const faces = fontFaceCss(ids, fontsBaseUrl);
20
43
  const theme = buildThemeCss(config, { attribute: options.attribute });
44
+ const looks = themeLookCss(options.library);
45
+ const body = looks ? `${theme}\n\n${looks}` : theme;
21
46
  return {
22
- css: faces ? `${faces}\n\n${theme}` : theme,
47
+ css: faces ? `${faces}\n\n${body}` : body,
23
48
  preloads: fontPreloads(ids, fontsBaseUrl),
24
49
  identity: options.identity
25
50
  };
@@ -1 +1 @@
1
- {"version":3,"file":"react.mjs","names":[],"sources":["../src/theme/head.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 identity: options.identity,\n };\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":";;;;;;;;;;;;;;;AAWA,SAAgB,UAAU,QAAqB,UAA4B,EAAE,EAAa;CACxF,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,MAAM,aAAa,OAAO;CAChC,MAAM,QAAQ,YAAY,KAAK,aAAa;CAC5C,MAAM,QAAQ,cAAc,QAAQ,EAAE,WAAW,QAAQ,WAAW,CAAC;AACrE,QAAO;EACL,KAAK,QAAQ,GAAG,MAAM,MAAM,UAAU;EACtC,UAAU,aAAa,KAAK,aAAa;EACzC,UAAU,QAAQ;EACnB;;;;ACEH,SAAS,kBAAkB,QAA8B;AACvD,QACE,WAAW,OAAO,aAAa,IAC/B,WAAW,OAAO,eAAe,IACjC,WAAW,OAAO,YAAY,IAC9B,WAAW,OAAO,eAAe,IACjC,WAAW,OAAO,aAAa;;;;;;;AASnC,SAAgB,aAAa,EAC3B,OACA,MACA,cACA,WACA,iBAAiB,OACG;CACpB,MAAM,EAAE,SAAS,eAAyB;EACxC,aAAa,cAAc,MAAM;EACjC;EACD,CAAC;CACF,MAAM,SAAS,mBAAmB,MAAM,KAAK;CAC7C,MAAM,OAAO,OAAO,MAAM;AAC1B,KAAI,kBAAkB,OAAO,CAAE,MAAK,UAAU;CAC9C,MAAM,SAAS,KAAK;CACpB,MAAM,MAAM,UAAU,QAAQ,EAAE,cAAc,CAAC,CAAC;CAChD,MAAM,CAAC,MAAM,WAAW,SAA2B,QAAQ;AAE3D,QACE,qBAAC,OAAD;EAAK,MAAK;EAAS,cAAW;EAAgB,cAAY;EAAM,WAAU;YAA1E;GACE,oBAAC,SAAD,EAAA,UAAQ,KAAY,CAAA;GACpB,qBAAC,OAAD;IAAK,WAAU;cAAf,CACE,oBAAC,UAAD;KAAQ,MAAK;KAAS,gBAAc,SAAS;KAAS,eAAe,QAAQ,QAAQ;eAAE;KAE9E,CAAA,EACT,oBAAC,UAAD;KAAQ,MAAK;KAAS,gBAAc,SAAS;KAAQ,eAAe,QAAQ,OAAO;eAAE;KAE5E,CAAA,CACL;;GACN,oBAAC,eAAD,EAAiB,CAAA;GACjB,oBAAC,eAAD,EAAe,QAAQ,WAAW,QAAQ,EAAE,QAAQ,gBAAgB,CAAC,EAAI,CAAA;GACrE"}
1
+ {"version":3,"file":"react.mjs","names":[],"sources":["../src/theme/look-css.ts","../src/theme/head.ts","../src/react/theme-preview.tsx"],"sourcesContent":["import { generateNamedScaleCSS, lookBindingCss } from \"@bison-lab/tokens\";\n\nimport { PAGE_EDITOR_SYSTEM_KEYS } from \"./looks\";\nimport { stateFromColorDoc } from \"./map\";\nimport type { ThemeLibraryColorDoc } from \"./types\";\n\n/**\n * Custom `--coral-*` variables plus `[data-look]` bindings for every\n * page-editor family. System bindings always ship so a look key paints\n * before anyone adds a custom scale.\n */\nexport function themeLookCss(library?: ThemeLibraryColorDoc[] | null): string {\n const rows = Array.isArray(library) ? library : [];\n const customKeys: string[] = [];\n const scaleLines: string[] = [];\n\n for (const row of rows) {\n const key = row.key?.trim();\n if (!key) continue;\n customKeys.push(key);\n const state = stateFromColorDoc(row, row.hex ?? \"#000000\");\n scaleLines.push(generateNamedScaleCSS(key, state.scale));\n }\n\n const scales = scaleLines.length ? `:root {\\n${scaleLines.join(\"\\n\")}\\n}` : \"\";\n const bindings = lookBindingCss([...PAGE_EDITOR_SYSTEM_KEYS, ...customKeys]);\n return [scales, bindings].filter(Boolean).join(\"\\n\\n\");\n}\n","import { fontFaceCss, fontPreloads } from \"@bison-lab/fonts\";\nimport { buildThemeCss, themeFontIds, type ThemeConfig } from \"@bison-lab/tokens\";\n\nimport { themeLookCss } from \"./look-css\";\nimport { themeLibraryFromDoc } from \"./looks\";\nimport { themeConfigFromDoc } from \"./map\";\nimport type { ThemeDoc, 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 *\n * `library` adds custom `--coral-*` variables and `[data-look]` bindings\n * so a Color Settings scale paints the moment it is published.\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 const looks = themeLookCss(options.library);\n const body = looks ? `${theme}\\n\\n${looks}` : theme;\n return {\n css: faces ? `${faces}\\n\\n${body}` : body,\n preloads: fontPreloads(ids, fontsBaseUrl),\n identity: options.identity,\n };\n}\n\n/**\n * `themeHead` plus the custom scales on the Theme document. Sites that\n * still call `themeHead(getPublishedTheme())` keep working; they miss\n * `--coral-*` until they pass `library` or switch here.\n */\nexport function themeHeadFromDoc(\n doc: ThemeDoc | null | undefined,\n seed: ThemeConfig,\n options: Omit<ThemeHeadOptions, \"library\"> = {},\n): ThemeHead {\n return themeHead(themeConfigFromDoc(doc, seed), {\n ...options,\n library: themeLibraryFromDoc(doc),\n });\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":";;;;;;;;;;;;;;AAWA,SAAgB,aAAa,SAAiD;CAC5E,MAAM,OAAO,MAAM,QAAQ,QAAQ,GAAG,UAAU,EAAE;CAClD,MAAM,aAAuB,EAAE;CAC/B,MAAM,aAAuB,EAAE;AAE/B,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,MAAM,IAAI,KAAK,MAAM;AAC3B,MAAI,CAAC,IAAK;AACV,aAAW,KAAK,IAAI;EACpB,MAAM,QAAQ,kBAAkB,KAAK,IAAI,OAAO,UAAU;AAC1D,aAAW,KAAK,sBAAsB,KAAK,MAAM,MAAM,CAAC;;AAK1D,QAAO,CAFQ,WAAW,SAAS,YAAY,WAAW,KAAK,KAAK,CAAC,OAAO,IAC3D,eAAe,CAAC,GAAG,yBAAyB,GAAG,WAAW,CAAC,CACnD,CAAC,OAAO,QAAQ,CAAC,KAAK,OAAO;;;;;;;;;;;;;ACTxD,SAAgB,UAAU,QAAqB,UAA4B,EAAE,EAAa;CACxF,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,MAAM,aAAa,OAAO;CAChC,MAAM,QAAQ,YAAY,KAAK,aAAa;CAC5C,MAAM,QAAQ,cAAc,QAAQ,EAAE,WAAW,QAAQ,WAAW,CAAC;CACrE,MAAM,QAAQ,aAAa,QAAQ,QAAQ;CAC3C,MAAM,OAAO,QAAQ,GAAG,MAAM,MAAM,UAAU;AAC9C,QAAO;EACL,KAAK,QAAQ,GAAG,MAAM,MAAM,SAAS;EACrC,UAAU,aAAa,KAAK,aAAa;EACzC,UAAU,QAAQ;EACnB;;;;ACNH,SAAS,kBAAkB,QAA8B;AACvD,QACE,WAAW,OAAO,aAAa,IAC/B,WAAW,OAAO,eAAe,IACjC,WAAW,OAAO,YAAY,IAC9B,WAAW,OAAO,eAAe,IACjC,WAAW,OAAO,aAAa;;;;;;;AASnC,SAAgB,aAAa,EAC3B,OACA,MACA,cACA,WACA,iBAAiB,OACG;CACpB,MAAM,EAAE,SAAS,eAAyB;EACxC,aAAa,cAAc,MAAM;EACjC;EACD,CAAC;CACF,MAAM,SAAS,mBAAmB,MAAM,KAAK;CAC7C,MAAM,OAAO,OAAO,MAAM;AAC1B,KAAI,kBAAkB,OAAO,CAAE,MAAK,UAAU;CAC9C,MAAM,SAAS,KAAK;CACpB,MAAM,MAAM,UAAU,QAAQ,EAAE,cAAc,CAAC,CAAC;CAChD,MAAM,CAAC,MAAM,WAAW,SAA2B,QAAQ;AAE3D,QACE,qBAAC,OAAD;EAAK,MAAK;EAAS,cAAW;EAAgB,cAAY;EAAM,WAAU;YAA1E;GACE,oBAAC,SAAD,EAAA,UAAQ,KAAY,CAAA;GACpB,qBAAC,OAAD;IAAK,WAAU;cAAf,CACE,oBAAC,UAAD;KAAQ,MAAK;KAAS,gBAAc,SAAS;KAAS,eAAe,QAAQ,QAAQ;eAAE;KAE9E,CAAA,EACT,oBAAC,UAAD;KAAQ,MAAK;KAAS,gBAAc,SAAS;KAAQ,eAAe,QAAQ,OAAO;eAAE;KAE5E,CAAA,CACL;;GACN,oBAAC,eAAD,EAAiB,CAAA;GACjB,oBAAC,eAAD,EAAe,QAAQ,WAAW,QAAQ,EAAE,QAAQ,gBAAgB,CAAC,EAAI,CAAA;GACrE"}
package/dist/theme.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as resolveThemeIdentity, a as EditableTheme, c as THEME_SLUG, d as ThemeHead, f as ThemeHeadOptions, g as ThemeIdentityFallback, h as ResolvedThemeIdentity, i as CreateThemeOptions, l as ThemeColorDoc, m as ThemeUploadDoc, n as rewriteColorToken, o as SYSTEM_COLOR_KEYS, p as ThemeLibraryColorDoc, r as themeColorKeys, s as SystemColorKey, t as deleteLibraryColor, u as ThemeDoc } from "./library-BGq4wGif.mjs";
1
+ import { A as ThemeIdentityFallback, C as ThemeColorDoc, D as ThemeLibraryColorDoc, E as ThemeHeadOptions, O as ThemeUploadDoc, T as ThemeHead, _ as SystemColorKey, a as pageEditorTokens, d as deleteLibraryColor, f as rewriteColorToken, g as SYSTEM_COLOR_KEYS, h as EditableTheme, i as pageEditorLooks, j as resolveThemeIdentity, k as ResolvedThemeIdentity, l as colorTokenField, m as CreateThemeOptions, n as PAGE_EDITOR_SYSTEM_KEYS, o as themeLibraryFromDoc, p as themeColorKeys, r as PageEditorSystemKey, t as LookOption, u as lookField, w as ThemeDoc, x as THEME_SLUG } from "./looks-DsizRfFV.mjs";
2
2
  import { ShadeStep, ThemeConfig } from "@bison-lab/tokens";
3
3
 
4
4
  //#region src/theme/published.d.ts
@@ -36,8 +36,17 @@ declare function themeConfigFromDoc(doc: ThemeDoc | null | undefined, seed: Them
36
36
  * list for the families the config names. A root layout (and the admin
37
37
  * layout, if it should restyle too) renders `css` in a `<style>` and
38
38
  * each preload as `<link rel="preload" as="font">`.
39
+ *
40
+ * `library` adds custom `--coral-*` variables and `[data-look]` bindings
41
+ * so a Color Settings scale paints the moment it is published.
39
42
  */
40
43
  declare function themeHead(config: ThemeConfig, options?: ThemeHeadOptions): ThemeHead;
44
+ /**
45
+ * `themeHead` plus the custom scales on the Theme document. Sites that
46
+ * still call `themeHead(getPublishedTheme())` keep working; they miss
47
+ * `--coral-*` until they pass `library` or switch here.
48
+ */
49
+ declare function themeHeadFromDoc(doc: ThemeDoc | null | undefined, seed: ThemeConfig, options?: Omit<ThemeHeadOptions, "library">): ThemeHead;
41
50
  //#endregion
42
- export { type CreateThemeOptions, type EditableTheme, type ResolvedThemeIdentity, SYSTEM_COLOR_KEYS, type SystemColorKey, THEME_SLUG, type ThemeColorDoc, type ThemeDoc, type ThemeHead, type ThemeHeadOptions, type ThemeIdentityFallback, type ThemeLibraryColorDoc, type ThemeUploadDoc, deleteLibraryColor, getPublishedIdentity, getPublishedTheme, resolveThemeIdentity, rewriteColorToken, themeColorKeys, themeConfigFromDoc, themeHead };
51
+ export { type CreateThemeOptions, type EditableTheme, type LookOption, PAGE_EDITOR_SYSTEM_KEYS, type PageEditorSystemKey, type ResolvedThemeIdentity, SYSTEM_COLOR_KEYS, type SystemColorKey, THEME_SLUG, type ThemeColorDoc, type ThemeDoc, type ThemeHead, type ThemeHeadOptions, type ThemeIdentityFallback, type ThemeLibraryColorDoc, type ThemeUploadDoc, colorTokenField, deleteLibraryColor, getPublishedIdentity, getPublishedTheme, lookField, pageEditorLooks, pageEditorTokens, resolveThemeIdentity, rewriteColorToken, themeColorKeys, themeConfigFromDoc, themeHead, themeHeadFromDoc, themeLibraryFromDoc };
43
52
  //# sourceMappingURL=theme.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.mts","names":[],"sources":["../src/theme/published.ts","../src/theme/map.ts","../src/theme/head.ts"],"mappings":";;;;UAOiB,YAAA;EACf,UAAA,GAAa,IAAA;IACX,IAAA;IACA,KAAA;IACA,KAAA;IACA,cAAA;EAAA,MACI,OAAA,CAAQ,QAAA;AAAA;;;;;;iBAQM,iBAAA,CAAkB,OAAA,EAAS,YAAA,EAAc,IAAA,EAAM,WAAA,GAAc,OAAA,CAAQ,WAAA;;AAA3F;;;iBAcsB,oBAAA,CACpB,OAAA,EAAS,YAAA,EACT,QAAA,GAAW,qBAAA,GACV,OAAA,CAAQ,qBAAA;;;;;;;;iBCqCK,kBAAA,CAAmB,GAAA,EAAK,QAAA,qBAA6B,IAAA,EAAM,WAAA,GAAc,WAAA;;;;;ADpEzF;;;;iBEIgB,SAAA,CAAU,MAAA,EAAQ,WAAA,EAAa,OAAA,GAAS,gBAAA,GAAwB,SAAA"}
1
+ {"version":3,"file":"theme.d.mts","names":[],"sources":["../src/theme/published.ts","../src/theme/map.ts","../src/theme/head.ts"],"mappings":";;;;UAOiB,YAAA;EACf,UAAA,GAAa,IAAA;IACX,IAAA;IACA,KAAA;IACA,KAAA;IACA,cAAA;EAAA,MACI,OAAA,CAAQ,QAAA;AAAA;;;;;;iBAQM,iBAAA,CAAkB,OAAA,EAAS,YAAA,EAAc,IAAA,EAAM,WAAA,GAAc,OAAA,CAAQ,WAAA;;AAA3F;;;iBAcsB,oBAAA,CACpB,OAAA,EAAS,YAAA,EACT,QAAA,GAAW,qBAAA,GACV,OAAA,CAAQ,qBAAA;;;;;;;;iBCsCK,kBAAA,CAAmB,GAAA,EAAK,QAAA,qBAA6B,IAAA,EAAM,WAAA,GAAc,WAAA;;;;;ADrEzF;;;;;;;iBEUgB,SAAA,CAAU,MAAA,EAAQ,WAAA,EAAa,OAAA,GAAS,gBAAA,GAAwB,SAAA;;;;;;iBAmBhE,gBAAA,CACd,GAAA,EAAK,QAAA,qBACL,IAAA,EAAM,WAAA,EACN,OAAA,GAAS,IAAA,CAAK,gBAAA,eACb,SAAA"}