@bison-lab/payload-core 3.8.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["payloadSeoPlugin"],"sources":["../src/seo/fields.ts","../src/seo/text.ts","../src/seo/plugin.ts","../src/theme/preview.ts","../src/theme/global.ts","../src/theme/seed.ts"],"sourcesContent":["import type { CheckboxField } from \"payload\";\n\n/**\n * The index switch, last in the SEO tab. Off by default: a page is public\n * unless an editor says otherwise. `pageMetadata` turns it into\n * `noindex, nofollow`; a site's sitemap should filter on it too.\n */\nexport const noIndexField: CheckboxField = {\n name: \"noIndex\",\n type: \"checkbox\",\n label: \"Hide this page from search engines\",\n defaultValue: false,\n admin: {\n description:\n \"Search engines will not list this page and the sitemap will leave it out. Anyone with the link can still open it.\",\n },\n};\n","/** The length a search result shows of a description before it is cut. */\nexport const DESCRIPTION_LENGTH = 155;\n\n/**\n * Prose cut to `max` characters at a word, with an ellipsis, so a generated\n * description does not end mid-syllable. Text that fits is returned trimmed\n * and whole. A first word longer than `max` is cut mid-word, since there is\n * no boundary to cut at.\n */\nexport function truncateAtWord(text: string, max = DESCRIPTION_LENGTH): string {\n const trimmed = text.trim();\n if (trimmed.length <= max) return trimmed;\n const cut = trimmed.slice(0, max);\n const lastSpace = cut.lastIndexOf(\" \");\n return `${lastSpace > 0 ? cut.slice(0, lastSpace) : cut}…`;\n}\n","import { seoPlugin as payloadSeoPlugin } from \"@payloadcms/plugin-seo\";\nimport type {\n GenerateDescription,\n GenerateImage,\n GenerateTitle,\n GenerateURL,\n} from \"@payloadcms/plugin-seo/types\";\nimport type { CollectionSlug, Plugin, UploadCollectionSlug } from \"payload\";\n\nimport { noIndexField } from \"./fields\";\nimport { truncateAtWord } from \"./text\";\nimport { documentTitle } from \"./title\";\nimport type { MediaId } from \"./types\";\n\n/**\n * What the plugin reads off a document: its title, for the generated meta\n * title. The site's generated `Page` is assignable to this; the `urlFor`,\n * `describeFrom` and `imageFor` callbacks read the rest, typed as the site\n * chooses through `TDoc`.\n */\nexport interface SeoDoc {\n title?: string | null;\n}\n\nexport interface SeoPluginOptions<TDoc extends SeoDoc = SeoDoc> {\n /** Appended to every generated title: `<page title> | <siteName>`. */\n siteName: string;\n /**\n * The absolute public URL of a document from the form's data, for the\n * search-result preview and the Generate button beside it. `undefined`\n * when the document has no address yet (a slug not typed), which leaves\n * the preview empty rather than pointing at the home page.\n */\n urlFor: (doc: TDoc) => string | undefined;\n /**\n * Prose to cut a generated description from, the first plain-text field a\n * page always has near the top (a hero's body, say). Cut at a word near\n * 155 characters. Without it the Generate button fills in nothing.\n */\n describeFrom?: (doc: TDoc) => string | null | undefined;\n /**\n * An image already on the page, for the Meta Image Generate button, so an\n * editor need not upload a second copy of the hero picture. `firstImageIn`\n * walks block rows for one. The upload chooser stays, so any other media\n * row can still be picked.\n */\n imageFor?: (doc: TDoc) => MediaId | null | undefined;\n /** Collections that get the SEO tab. Default `['pages']`. */\n collections?: CollectionSlug[];\n /** The upload collection the meta image comes from. Default `'media'`. */\n uploadsCollection?: UploadCollectionSlug;\n}\n\n/**\n * `@payloadcms/plugin-seo` configured the Bison Lab way: an SEO tab beside a\n * Content tab (never below the block editor), holding the overview with its\n * character counts, meta title, description and image with Generate buttons,\n * the search-result preview, and the `noIndex` switch.\n *\n * Every string the tab generates comes from the options, so a site spells\n * its name and its URL scheme once. Pin `@payloadcms/plugin-seo` to the same\n * version as `payload` in the site; the plugin's admin components are\n * resolved from the site's import map, so run `payload generate:importmap`\n * after adding it.\n */\nexport function seoPlugin<TDoc extends SeoDoc = SeoDoc>({\n siteName,\n urlFor,\n describeFrom,\n imageFor,\n collections = [\"pages\"],\n uploadsCollection = \"media\",\n}: SeoPluginOptions<TDoc>): Plugin {\n const generateTitle: GenerateTitle<TDoc> = ({ doc }) =>\n doc?.title ? documentTitle(siteName, doc.title) : \"\";\n const generateDescription: GenerateDescription<TDoc> = ({ doc }) =>\n truncateAtWord(describeFrom?.(doc) ?? \"\");\n const generateURL: GenerateURL<TDoc> = ({ doc }) => urlFor(doc) ?? \"\";\n // Only when the site can name one: the button appears with the function.\n const generateImage: GenerateImage<TDoc> | undefined = imageFor\n ? ({ doc }) => imageFor(doc) ?? \"\"\n : undefined;\n\n return payloadSeoPlugin({\n collections,\n uploadsCollection,\n tabbedUI: true,\n generateTitle,\n generateDescription,\n generateURL,\n ...(generateImage ? { generateImage } : {}),\n fields: ({ defaultFields }) => [...defaultFields, noIndexField],\n });\n}\n\n/**\n * The first image among some block rows, as a media id, for `imageFor`. Each\n * row is checked for `field` holding either a bare id or a populated upload\n * document; rows without one are skipped. Pass the page's hero and layout\n * together (`[...doc.hero, ...doc.layout]`) to search in reading order.\n */\nexport function firstImageIn(\n blocks: unknown,\n field = \"image\",\n): MediaId | undefined {\n if (!Array.isArray(blocks)) return undefined;\n for (const block of blocks) {\n if (typeof block !== \"object\" || block === null) continue;\n const value = (block as Record<string, unknown>)[field];\n const id =\n typeof value === \"object\" && value !== null && \"id\" in value\n ? (value as { id: unknown }).id\n : value;\n if (typeof id === \"number\" || (typeof id === \"string\" && id !== \"\"))\n return id;\n }\n return undefined;\n}\n","/**\n * Device sizes the Theme (and, later, Pages) live-preview toolbar offers.\n * One list so the panes match.\n */\nexport const THEME_PREVIEW_BREAKPOINTS = [\n { label: \"Mobile\", name: \"mobile\", width: 375, height: 667 },\n { label: \"Tablet\", name: \"tablet\", width: 768, height: 1024 },\n { label: \"Desktop\", name: \"desktop\", width: 1440, height: 900 },\n] as const;\n","import { catalog, type FontEntry } from \"@bison-lab/fonts\";\nimport { presetHints } from \"@bison-lab/tokens\";\nimport type { Field, GlobalConfig, SelectField, TextField } from \"payload\";\n\nimport {\n SAME_AS_BODY,\n THEME_COLOR_FIELD,\n THEME_CONTRAST_REPORT,\n THEME_FONT_FIELD,\n headingSelectValue,\n} from \"./fields\";\nimport { themeConfigFromDoc, validateThemeHex } from \"./map\";\nimport { THEME_PREVIEW_BREAKPOINTS } from \"./preview\";\nimport { THEME_SLUG, type CreateThemeOptions, type ThemeDoc } from \"./types\";\n\nfunction requireAccess(options: CreateThemeOptions): CreateThemeOptions[\"access\"] {\n const read = options.access?.read;\n const update = options.access?.update;\n if (!read || !update) {\n throw new Error(\n \"createTheme requires access.read and access.update. Pass the site's predicates; canManageBrand is the intended update predicate until BIS-43 moves src/platform here.\",\n );\n }\n return { read, update };\n}\n\nfunction hintSelect<K extends string>(\n name: string,\n label: string,\n hints: Record<K, { label: string; hint: string }>,\n defaultValue: K,\n): SelectField {\n return {\n name,\n type: \"select\",\n label,\n required: true,\n defaultValue,\n options: (Object.entries(hints) as [K, { label: string; hint: string }][]).map(\n ([value, { label: optionLabel, hint }]) => ({\n label: `${optionLabel} — ${hint}`,\n value,\n }),\n ),\n };\n}\n\nfunction colorField(name: string, label: string, defaultValue: string): TextField {\n return {\n name,\n type: \"text\",\n label,\n required: true,\n defaultValue,\n validate: validateThemeHex,\n admin: { components: { Field: THEME_COLOR_FIELD } },\n };\n}\n\nfunction fontField(\n name: string,\n label: string,\n defaultValue: string,\n fonts: readonly FontEntry[],\n fontsBaseUrl: string,\n sameAsBody: boolean,\n): SelectField {\n const options = fonts.map((font) => ({ label: font.family, value: font.id }));\n return {\n name,\n type: \"select\",\n label,\n required: !sameAsBody,\n defaultValue,\n options: sameAsBody ? [{ label: \"Same as body\", value: SAME_AS_BODY }, ...options] : options,\n admin: {\n components: { Field: THEME_FONT_FIELD },\n custom: { fontsBaseUrl, ids: fonts.map((font) => font.id), sameAsBody },\n },\n };\n}\n\n/**\n * Settings → Theme: the five brand colours, grey scale, presets, default\n * theme, body and heading fonts, and optional logo. Access is passed in\n * (`canManageBrand` for update). Drafts autosave; the public site reads\n * the published version through `getPublishedTheme`.\n *\n * `previewPath` wires `admin.livePreview` when the site has a route.\n */\nexport function createTheme(options: CreateThemeOptions): GlobalConfig {\n const access = requireAccess(options);\n const {\n seed,\n fonts = catalog,\n fontsBaseUrl = \"/fonts\",\n contrastTarget = 4.5,\n previewPath,\n logo,\n onPublish,\n } = options;\n\n const fields: Field[] = [\n {\n name: \"brand\",\n type: \"group\",\n label: \"Brand colours\",\n fields: [\n colorField(\"primary\", \"Primary\", seed.brandPrimary),\n colorField(\"secondary\", \"Secondary\", seed.brandSecondary),\n colorField(\"accent\", \"Accent\", seed.brandAccent),\n colorField(\"highlight\", \"Highlight\", seed.brandHighlight),\n colorField(\"success\", \"Success\", seed.brandSuccess),\n ],\n },\n {\n name: \"contrast\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_CONTRAST_REPORT },\n custom: { target: contrastTarget },\n },\n },\n hintSelect(\"greyScale\", \"Grey scale\", presetHints.greyScale, seed.greyScale),\n hintSelect(\"radius\", \"Radius\", presetHints.radius, seed.radius),\n hintSelect(\"shadow\", \"Shadow\", presetHints.shadow, seed.shadow),\n hintSelect(\"motion\", \"Motion\", presetHints.motion, seed.motion),\n hintSelect(\"density\", \"Density\", presetHints.density, seed.density),\n hintSelect(\"defaultTheme\", \"Default theme\", presetHints.defaultTheme, seed.defaultTheme),\n {\n name: \"fonts\",\n type: \"group\",\n label: \"Fonts\",\n fields: [\n fontField(\"body\", \"Body\", seed.fontBody, fonts, fontsBaseUrl, false),\n fontField(\n \"heading\",\n \"Heading\",\n headingSelectValue(seed.fontHeading),\n fonts,\n fontsBaseUrl,\n true,\n ),\n ],\n },\n ];\n\n if (logo) {\n fields.push(\n {\n name: \"logo\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Logo\",\n },\n {\n name: \"logoMark\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Logo mark\",\n },\n );\n }\n\n return {\n slug: THEME_SLUG,\n label: \"Theme\",\n admin: {\n group: \"Settings\",\n custom: { previewPath, contrastTarget, fontsBaseUrl },\n ...(previewPath\n ? { livePreview: { url: previewPath, breakpoints: [...THEME_PREVIEW_BREAKPOINTS] } }\n : {}),\n },\n versions: { drafts: { autosave: { interval: 800 } } },\n access: {\n read: access.read,\n update: access.update,\n readVersions: access.update,\n },\n fields,\n hooks: {\n afterChange: [\n async ({ doc }) => {\n if (!onPublish) return;\n const themeDoc = doc as ThemeDoc;\n if (themeDoc._status !== \"published\") return;\n await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);\n },\n ],\n },\n };\n}\n","import type { ThemeConfig } from \"@bison-lab/tokens\";\n\nimport { docFromConfig } from \"./map\";\nimport { THEME_SLUG } from \"./types\";\nimport type { ThemeDoc } from \"./types\";\n\nexport interface SeedThemePayload {\n updateGlobal: (args: {\n slug: string;\n data: ThemeDoc;\n draft?: boolean;\n }) => Promise<unknown>;\n}\n\n/**\n * Writes a published Theme from the seed. For a site's migration `up()`,\n * so the row exists on deploy and renders what `bison-theme.css` rendered.\n */\nexport async function seedTheme(payload: SeedThemePayload, seed: ThemeConfig): Promise<unknown> {\n return payload.updateGlobal({\n slug: THEME_SLUG,\n data: docFromConfig(seed),\n draft: false,\n });\n}\n"],"mappings":"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"}
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":";;;;;;;;;;;AAOA,MAAa,eAA8B;CACzC,MAAM;CACN,MAAM;CACN,OAAO;CACP,cAAc;CACd,OAAO,EACL,aACE,qHACH;CACF;;;;ACfD,MAAa,qBAAqB;;;;;;;AAQlC,SAAgB,eAAe,MAAc,MAAA,KAAkC;CAC7E,MAAM,UAAU,KAAK,MAAM;AAC3B,KAAI,QAAQ,UAAU,IAAK,QAAO;CAClC,MAAM,MAAM,QAAQ,MAAM,GAAG,IAAI;CACjC,MAAM,YAAY,IAAI,YAAY,IAAI;AACtC,QAAO,GAAG,YAAY,IAAI,IAAI,MAAM,GAAG,UAAU,GAAG,IAAI;;;;;;;;;;;;;;;;ACmD1D,SAAgB,UAAwC,EACtD,UACA,QACA,cACA,UACA,cAAc,CAAC,QAAQ,EACvB,oBAAoB,WACa;CACjC,MAAM,iBAAsC,EAAE,UAC5C,KAAK,QAAQ,cAAc,UAAU,IAAI,MAAM,GAAG;CACpD,MAAM,uBAAkD,EAAE,UACxD,eAAe,eAAe,IAAI,IAAI,GAAG;CAC3C,MAAM,eAAkC,EAAE,UAAU,OAAO,IAAI,IAAI;CAEnE,MAAM,gBAAiD,YAClD,EAAE,UAAU,SAAS,IAAI,IAAI,KAC9B,KAAA;AAEJ,QAAOA,YAAiB;EACtB;EACA;EACA,UAAU;EACV;EACA;EACA;EACA,GAAI,gBAAgB,EAAE,eAAe,GAAG,EAAE;EAC1C,SAAS,EAAE,oBAAoB,CAAC,GAAG,eAAe,aAAa;EAChE,CAAC;;;;;;;;AASJ,SAAgB,aACd,QACA,QAAQ,SACa;AACrB,KAAI,CAAC,MAAM,QAAQ,OAAO,CAAE,QAAO,KAAA;AACnC,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM;EACjD,MAAM,QAAS,MAAkC;EACjD,MAAM,KACJ,OAAO,UAAU,YAAY,UAAU,QAAQ,QAAQ,QAClD,MAA0B,KAC3B;AACN,MAAI,OAAO,OAAO,YAAa,OAAO,OAAO,YAAY,OAAO,GAC9D,QAAO;;;;;;;;;;AC7Gb,MAAa,qBAAqB;CAChC,cAAc;EACZ,OAAO;GAAE,OAAO;GAAS,MAAM;GAA8B;EAC7D,MAAM;GAAE,OAAO;GAAQ,MAAM;GAA6B;EAC1D,QAAQ;GAAE,OAAO;GAAiB,MAAM;GAAwC;EACjF;CACD,QAAQ;EACN,OAAO;GAAE,OAAO;GAAS,MAAM;GAAwB;EACvD,QAAQ;GAAE,OAAO;GAAU,MAAM;GAAgC;EACjE,SAAS;GAAE,OAAO;GAAW,MAAM;GAA6B;EAChE,MAAM;GAAE,OAAO;GAAQ,MAAM;GAA+B;EAC7D;CACD,QAAQ;EACN,MAAM;GAAE,OAAO;GAAQ,MAAM;GAAwB;EACrD,QAAQ;GAAE,OAAO;GAAU,MAAM;GAAqC;EACtE,UAAU;GAAE,OAAO;GAAY,MAAM;GAAsC;EAC5E;CACD,QAAQ;EACN,QAAQ;GAAE,OAAO;GAAS,MAAM;GAAuB;EACvD,QAAQ;GAAE,OAAO;GAAU,MAAM;GAAwB;EACzD,SAAS;GAAE,OAAO;GAAU,MAAM;GAA2B;EAC9D;CACD,SAAS;EACP,SAAS;GAAE,OAAO;GAAW,MAAM;GAA+B;EAClE,SAAS;GAAE,OAAO;GAAY,MAAM;GAA8B;EAClE,UAAU;GAAE,OAAO;GAAY,MAAM;GAA2C;EACjF;CACF;;;AC9BD,MAAa,iBAAiB;CAAC;CAAU;CAAc;CAAc;CAAW;;;;;;;;AAWhF,SAAgB,kBAAkB,QAAkB,UAAoB,OAA6B;AACnG,KAAI,UAAU,SAAU,QAAO;EAAE,GAAG;EAAQ,QAAQ,SAAS;EAAQ;AACrE,KAAI,UAAU,aAAc,QAAO;EAAE,GAAG;EAAQ,YAAY,SAAS;EAAY;AACjF,KAAI,UAAU,aAAc,QAAO;EAAE,GAAG;EAAQ,YAAY,SAAS;EAAY;AACjF,QAAO;EAAE,GAAG;EAAQ,MAAM,SAAS;EAAM,SAAS,SAAS;EAAS,UAAU,SAAS;EAAU;;AAGnG,SAAgB,aAAa,OAAqC;AAChE,QAAO,OAAO,UAAU,YAAa,eAAqC,SAAS,MAAM;;;;ACG3F,SAASC,gBAAc,SAA2D;CAChF,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,SAAS,QAAQ,QAAQ;AAC/B,KAAI,CAAC,QAAQ,CAAC,OACZ,OAAM,IAAI,MACR,wKACD;AAEH,QAAO;EAAE;EAAM;EAAQ;;AAGzB,SAAS,mBAAmB,SAAqC;CAC/D,MAAM,MAAM,QAAQ,eAAe,QAAQ,KAAK;AAChD,KAAI,CAAC,IACH,OAAM,IAAI,MACR,0HACD;AAEH,QAAO;;AAGT,SAAS,YACP,MACA,OACA,SACA,cACA,OACa;AACb,QAAO;EACL;EACA,MAAM;EACN;EACA,UAAU;EACV;EACA,SAAU,OAAO,QAAQ,QAAQ,CAA4C,KAC1E,CAAC,OAAO,EAAE,OAAO,aAAa,aAAa;GAC1C,OAAO,GAAG,YAAY,KAAK;GAC3B;GACD,EACF;EACD,GAAI,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,OAAO,EAAE,EAAE,GAAG,EAAE;EAC7D;;AAGH,MAAM,eAAe,YAAY,KAAK,UAAU;CAAE,OAAO,OAAO,KAAK;CAAE,OAAO,OAAO,KAAK;CAAE,EAAE;AAE9F,SAAS,iBAAiB,YAA8B;CACtD,MAAM,SAAS,EAAE,QAAQ,MAAM;AAC/B,QAAO;EACL;GACE,MAAM;GACN,MAAM;GACN,OAAO;GACP,UAAU;GACV,GAAI,aAAa,EAAE,cAAc,YAAY,GAAG,EAAE;GAClD,UAAU;GACV,OAAO;GACR;EACD;GACE,MAAM;GACN,MAAM;GACN,OAAO;GACP,UAAU;GACV,cAAc;GACd,SAAS;GACT,OAAO;GACR;EACD;GACE,MAAM;GACN,MAAM;GACN,OAAO;GACP,OAAO;GACR;EACD;GACE,MAAM;GACN,MAAM;GACN,OAAO;GACP,cAAc;GACd,OAAO;GACR;EACD;GACE,MAAM;GACN,MAAM;GACN,OAAO;GACP,UAAU;GACV,cAAc;GACd,SAAS;IACP;KAAE,OAAO;KAAe,OAAO;KAAO;IACtC;KAAE,OAAO;KAAe,OAAO;KAAU;IACzC;KAAE,OAAO;KAAU,OAAO;KAAU;IACrC;GACD,OAAO;GACR;EACD;GACE,MAAM;GACN,MAAM;GACN,OAAO;GACP,SAAS;GACT,SAAS;GACT,UAAU;GACV,OAAO;GACR;EACF;;AAGH,SAAS,iBAAiB,MAAc,OAAe,YAA2B;AAChF,QAAO;EACL;EACA,MAAM;EACN;EACA,QAAQ,iBAAiB,WAAW;EACpC,OAAO,EAAE,YAAY,EAAE,OAAO,yBAAyB,EAAE;EAC1D;;AAGH,SAAS,UACP,MACA,OACA,cACA,OACA,cACA,YACa;CACb,MAAM,UAAU,MAAM,KAAK,UAAU;EAAE,OAAO,KAAK;EAAQ,OAAO,KAAK;EAAI,EAAE;AAC7E,QAAO;EACL;EACA,MAAM;EACN;EACA,UAAU,CAAC;EACX;EACA,SAAS,aAAa,CAAC;GAAE,OAAO;GAAgB,OAAA;GAAqB,EAAE,GAAG,QAAQ,GAAG;EACrF,OAAO;GACL,YAAY,EAAE,OAAO,kBAAkB;GACvC,QAAQ;IAAE;IAAc,KAAK,MAAM,KAAK,SAAS,KAAK,GAAG;IAAE;IAAY;GACxE;EACF;;;;;;;;AASH,SAAgB,YAAY,SAA2C;CACrE,MAAM,SAASA,gBAAc,QAAQ;CACrC,MAAM,cAAc,mBAAmB,QAAQ;CAC/C,MAAM,EACJ,MACA,QAAQ,SACR,eAAe,UACf,iBAAA,GACA,MACA,UACA,cACE;CAEJ,MAAM,OAA0B;EAC9B;GACE,OAAO;GACP,QAAQ,CACN;IACE,MAAM;IACN,MAAM;IACN,OAAO;IACP,QAAQ;KACN;MACE,MAAM;MACN,MAAM;MACN,OAAO;MACP,QAAQ;OACN;QACE,MAAM;QACN,MAAM;QACN,OAAO;SACL,YAAY,EAAE,OAAO,uBAAuB;SAC5C,QAAQ;UACN,OAAO;UACP,MAAM;UACP;SACF;QACF;OACD,iBAAiB,WAAW,WAAW,KAAK,aAAa;OACzD,iBAAiB,aAAa,aAAa,KAAK,eAAe;OAC/D,iBAAiB,UAAU,UAAU,KAAK,YAAY;OACtD,iBAAiB,aAAa,aAAa,KAAK,eAAe;OAC/D;QACE,MAAM;QACN,MAAM;QACN,OAAO;SACL,YAAY,EAAE,OAAO,uBAAuB;SAC5C,QAAQ;UACN,OAAO;UACP,MAAM;UACP;SACF;QACF;OACD,iBAAiB,WAAW,WAAW,KAAK,aAAa;OACzD,iBAAiB,eAAe,eAAe,YAAY;OAC5D;MACF;KACD,YAAY,aAAa,eAAe,YAAY,WAAW,KAAK,WAAW,uBAAuB;KACtG;MACE,MAAM;MACN,MAAM;MACN,OAAO;MACP,QAAQ;OAAE,UAAU;OAAS,QAAQ;OAAU;MAC/C,QAAQ;OACN;QAAE,MAAM;QAAO,MAAM;QAAQ,OAAO;QAAO,UAAU;QAAM,OAAO,EAAE,QAAQ,MAAM;QAAE;OACpF;QAAE,MAAM;QAAS,MAAM;QAAQ,OAAO;QAAQ,UAAU;QAAM,OAAO,EAAE,QAAQ,MAAM;QAAE;OACvF,GAAG,kBAAkB;OACtB;MACD,OAAO,EAAE,YAAY,EAAE,OAAO,qBAAqB,EAAE;MACtD;KACF;IACF,EACD;IACE,MAAM;IACN,MAAM;IACN,OAAO;KACL,YAAY,EAAE,OAAO,uBAAuB;KAC5C,QAAQ,EAAE,QAAQ,gBAAgB;KACnC;IACF,CACF;GACF;EACD;GACE,OAAO;GACP,QAAQ,CACN;IACE,MAAM;IACN,MAAM;IACN,OAAO;IACP,QAAQ;KACN,UACE,WACA,gBACA,mBAAmB,KAAK,YAAY,EACpC,OACA,cACA,KACD;KACD,UAAU,QAAQ,aAAa,KAAK,UAAU,OAAO,cAAc,MAAM;KACzE;MACE,MAAM;MACN,MAAM;MACN,OAAO;OAAE,YAAY,EAAE,OAAO,qBAAqB;OAAE,QAAQ,EAAE,cAAc;OAAE;MAChF;KACF;IACF,CACF;GACF;EACD;GACE,OAAO;GACP,QAAQ,CACN;IACE,MAAM;IACN,MAAM;IACN,OAAO;IACP,QAAQ;KACN,YAAY,gBAAgB,iBAAiB,mBAAmB,cAAc,KAAK,aAAa;KAChG,YAAY,UAAU,gBAAgB,mBAAmB,QAAQ,KAAK,OAAO;KAC7E,YAAY,UAAU,SAAS,mBAAmB,QAAQ,KAAK,OAAO;KACtE,YAAY,UAAU,UAAU,mBAAmB,QAAQ,KAAK,OAAO;KACvE,YAAY,WAAW,WAAW,mBAAmB,SAAS,KAAK,QAAQ;KAC5E;IACD,OAAO,EAAE,YAAY,EAAE,OAAO,wBAAwB,EAAE;IACzD,CACF;GACF;EACF;AAED,MAAK,KAAK;EACR,OAAO;EACP,QAAQ,OACJ;GACE;IACE,MAAM;IACN,MAAM;IACN,YAAY,KAAK;IACjB,OAAO;IACP,OAAO,EACL,aAAa,oDACd;IACF;GACD;IACE,MAAM;IACN,MAAM;IACN,YAAY,KAAK;IACjB,OAAO;IACP,OAAO,EACL,aAAa,kDACd;IACF;GACD;IACE,MAAM;IACN,MAAM;IACN,YAAY,KAAK;IACjB,OAAO;IACP,OAAO,EACL,aACE,2FACH;IACF;GACF,GACD,CACE;GACE,MAAM;GACN,MAAM;GACN,OAAO;IACL,YAAY,EAAE,OAAO,yBAAyB;IAC9C,QAAQ,EAAE,UAAU,UAAU,UAAU;IACzC;GACF,CACF;EACN,CAAC;CAEF,MAAM,SAAkB,CACtB;EAAE,MAAM;EAAQ;EAAM,EACtB;EACE,MAAM;EACN,MAAM;EACN,OAAO,EAAE,QAAQ,MAAM;EACxB,CACF;AAED,QAAO;EACL,MAAM;EACN,OAAO;EACP,OAAO;GACL,OAAO;GACP,QAAQ;IAAE;IAAgB;IAAc,kBAAkB,UAAU;IAAU;GAC9E,YAAY,EACV,UAAU,EACR,YAAY,mBACb,EACF;GACF;EACD,QAAQ;GACN,MAAM,OAAO;GACb,QAAQ,OAAO;GAChB;EACD;EACA,OAAO;GACL,cAAc,EACX,EAAE,MAAM,kBAAkB;IAEzB,MAAM,EAAE,cAAc,GAAG,SADR;AAEjB,QAAI,aAAa,aAAa,CAC5B,QAAO,kBAAmB,eAA4B,EAAE,EAAE,MAAM,aAAa;AAE/E,WAAO;KAEV;GACD,aAAa,CACX,OAAO,EAAE,UAAU;AACjB,QAAI,CAAC,UAAW;IAChB,MAAM,WAAW;AACjB,UAAM,UAAU,mBAAmB,UAAU,KAAK,EAAE,SAAS;KAEhE;GACF;EACF;;;;AClYH,MAAM,iBAAiB;CAAC;CAAU;CAAiB;CAAU;CAAS;CAAS;;;;;;;AAQ/E,SAAgB,YAAY,QAAwB;AAClD,KAAI,CAAC,aAAa,KAAK,OAAO,CAC5B,OAAM,IAAI,MAAM,aAAa;CAG/B,IAAI,MAAM,OAAO,QAAQ,kCAAkC,GAAG;AAE9D,MAAK,MAAM,OAAO,gBAAgB;AAChC,QAAM,IAAI,QAAQ,IAAI,OAAO,IAAI,IAAI,yBAAyB,IAAI,QAAQ,KAAK,EAAE,GAAG;AACpF,QAAM,IAAI,QAAQ,IAAI,OAAO,IAAI,IAAI,eAAe,KAAK,EAAE,GAAG;;AAGhE,OAAM,IAAI,QAAQ,4DAA4D,GAAG;AACjF,OAAM,IAAI,QACR,sFACA,SACD;AAED,QAAO;;AAGT,SAAgB,YAAY,MAAoE;AAC9F,KAAI,KAAK,aAAa,gBAAiB,QAAO;AAC9C,KAAI,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,aAAa,CAAC,SAAS,OAAO,CAAE,QAAO;AACtF,QAAO;;;;AC5BT,MAAa,oBAAoB;;AAGjC,MAAa,0BAA0B;CACrC;CACA;CACA;CACA;CACD;AAcD,SAAS,cAAc,SAAuE;CAC5F,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,SAAS,QAAQ,QAAQ;AAC/B,KAAI,CAAC,QAAQ,CAAC,OACZ,OAAM,IAAI,MACR,8KACD;AAEH,QAAO;EAAE;EAAM;EAAQ;;;;;;;AAQzB,SAAgB,kBAAkB,SAAqD;CACrF,MAAM,SAAS,cAAc,QAAQ;AAGrC,QAAO;EACL,MAHW,QAAQ,QAAA;EAInB,QAAQ;GAAE,UAAU;GAAe,QAAQ;GAAgB;EAC3D,OAAO;GACL,OAAO;GACP,YAAY;GACZ,aAAa;GACd;EACD,QAAQ;GACN,WAAW,CAAC,GAAG,wBAAwB;GACvC,MAAM;GACN,YAAY;GACb;EACD,UAAU;EACV,QAAQ;GACN,MAAM,OAAO;GACb,QAAQ,OAAO;GACf,QAAQ,OAAO;GACf,QAAQ,OAAO;GACf,cAAc,OAAO;GACtB;EACD,QAAQ;GACN;IAAE,MAAM;IAAS,MAAM;IAAQ,OAAO;IAAS;GAC/C;IAAE,MAAM;IAAS,MAAM;IAAY,OAAO;IAAe;GACzD;IAAE,MAAM;IAAO,MAAM;IAAQ,OAAO;IAAY,UAAU;IAAM;GACjE;EACD,OAAO,EACL,iBAAiB,EACd,EAAE,KAAK,gBAAgB;AACtB,OAAI,cAAc,YAAY,cAAc,SAAU;GACtD,MAAM,OAAO,IAAI;AACjB,OAAI,CAAC,QAAQ,CAAC,YAAY,KAAK,CAAE;AACjC,QAAK,OAAO,OAAO,KAAK,YAAY,KAAK,KAAK,SAAS,OAAO,CAAC,EAAE,OAAO;IAE3E,EACF;EACF;;;;;;;;AChEH,eAAsB,UAAU,SAA2B,MAAqC;AAC9F,QAAO,QAAQ,aAAa;EAC1B,MAAM;EACN,MAAM,cAAc,KAAK;EACzB,OAAO;EACR,CAAC;;;;;;;;ACnBJ,MAAa,4BAA4B;CACvC;EAAE,OAAO;EAAU,MAAM;EAAU,OAAO;EAAK,QAAQ;EAAK;CAC5D;EAAE,OAAO;EAAU,MAAM;EAAU,OAAO;EAAK,QAAQ;EAAM;CAC7D;EAAE,OAAO;EAAW,MAAM;EAAW,OAAO;EAAM,QAAQ;EAAK;CAChE"}
@@ -0,0 +1,189 @@
1
+ import { FontEntry } from "@bison-lab/fonts";
2
+ import { ColorScale, ColorScaleInclude, ShadeStep, ThemeConfig } from "@bison-lab/tokens";
3
+ import { Access } from "payload";
4
+
5
+ //#region src/theme/identity.d.ts
6
+ interface ThemeIdentityAsset {
7
+ url: string;
8
+ alt: string;
9
+ }
10
+ interface ThemeIdentityFallback {
11
+ lockup?: ThemeIdentityAsset;
12
+ mark?: ThemeIdentityAsset;
13
+ favicon?: ThemeIdentityAsset;
14
+ }
15
+ interface ResolvedThemeIdentity {
16
+ lockup: ThemeIdentityAsset | null;
17
+ mark: ThemeIdentityAsset | null;
18
+ favicon: ThemeIdentityAsset | null;
19
+ }
20
+ /**
21
+ * Uploaded logo, favicon, and mobile-menu mark, or the fallback the site
22
+ * passed into `createTheme`. Empty fields keep the fallback;
23
+ * `--color-primary-mark` is not a Theme field.
24
+ */
25
+ declare function resolveThemeIdentity(doc: ThemeDoc | null | undefined, fallback?: ThemeIdentityFallback): ResolvedThemeIdentity;
26
+ //#endregion
27
+ //#region src/theme/types.d.ts
28
+ /**
29
+ * The Theme document a site's generated types will describe. Optional and
30
+ * nullable, no index signature: a generated Global is assignable to this,
31
+ * never the reverse.
32
+ *
33
+ * Legacy flat `brand.*` hexes, `fonts`, and preset fields at the root stay
34
+ * readable so `getPublishedTheme` still maps a pre-BIS-87 row.
35
+ */
36
+ interface ThemeColorDoc {
37
+ hex?: string | null;
38
+ sourceStep?: ShadeStep | string | null;
39
+ scale?: ColorScale | null;
40
+ stale?: boolean | null;
41
+ include?: ColorScaleInclude | "custom" | null;
42
+ includedSteps?: ShadeStep[] | string[] | null;
43
+ }
44
+ interface ThemeLibraryColorDoc extends ThemeColorDoc {
45
+ key?: string | null;
46
+ label?: string | null;
47
+ }
48
+ interface ThemeDoc {
49
+ colors?: {
50
+ brand?: {
51
+ primary?: ThemeColorDoc | string | null;
52
+ secondary?: ThemeColorDoc | string | null;
53
+ accent?: ThemeColorDoc | string | null;
54
+ highlight?: ThemeColorDoc | string | null;
55
+ success?: ThemeColorDoc | string | null;
56
+ destructive?: ThemeColorDoc | string | null;
57
+ } | null;
58
+ library?: ThemeLibraryColorDoc[] | null;
59
+ greyScale?: ThemeConfig["greyScale"] | null;
60
+ } | null;
61
+ typography?: {
62
+ body?: string | null;
63
+ heading?: string | null;
64
+ } | null;
65
+ appearance?: {
66
+ defaultTheme?: ThemeConfig["defaultTheme"] | null;
67
+ radius?: ThemeConfig["radius"] | null;
68
+ shadow?: ThemeConfig["shadow"] | null;
69
+ motion?: ThemeConfig["motion"] | null;
70
+ density?: ThemeConfig["density"] | null;
71
+ } | null;
72
+ /** @deprecated Pre-BIS-87 flat brand hexes. */
73
+ brand?: {
74
+ primary?: ThemeColorDoc | string | null;
75
+ secondary?: ThemeColorDoc | string | null;
76
+ accent?: ThemeColorDoc | string | null;
77
+ highlight?: ThemeColorDoc | string | null;
78
+ success?: ThemeColorDoc | string | null;
79
+ destructive?: ThemeColorDoc | string | null;
80
+ } | null;
81
+ /** @deprecated Pre-BIS-87; now `colors.greyScale`. */
82
+ greyScale?: ThemeConfig["greyScale"] | null;
83
+ /** @deprecated Pre-BIS-87; now `appearance.*`. */
84
+ radius?: ThemeConfig["radius"] | null;
85
+ shadow?: ThemeConfig["shadow"] | null;
86
+ motion?: ThemeConfig["motion"] | null;
87
+ density?: ThemeConfig["density"] | null;
88
+ defaultTheme?: ThemeConfig["defaultTheme"] | null;
89
+ /** @deprecated Pre-BIS-87; now `typography`. */
90
+ fonts?: {
91
+ body?: string | null;
92
+ heading?: string | null;
93
+ } | null;
94
+ logo?: number | string | ThemeUploadDoc | null;
95
+ favicon?: number | string | ThemeUploadDoc | null;
96
+ logoMark?: number | string | ThemeUploadDoc | null;
97
+ }
98
+ interface ThemeUploadDoc {
99
+ id?: number | string | null;
100
+ url?: string | null;
101
+ alt?: string | null;
102
+ }
103
+ /**
104
+ * What an editor can change. `darkSelector` and `fontWeights` stay on the
105
+ * seed — they mean nothing in the admin — and `themeConfigFromDoc` puts
106
+ * them back.
107
+ */
108
+ type EditableTheme = Omit<ThemeConfig, "darkSelector" | "fontWeights">;
109
+ interface CreateThemeOptions {
110
+ /**
111
+ * Required, no default. Predicates live in each site's `src/platform`
112
+ * until BIS-43; pass `canManageBrand` as `update` (and typically
113
+ * `isAuthenticated` as `read`).
114
+ */
115
+ access: {
116
+ read: Access;
117
+ update: Access;
118
+ };
119
+ /** The site's `bison.config.json`. Every field's `defaultValue`, and the fallback `getPublishedTheme` returns. */
120
+ seed: ThemeConfig;
121
+ /**
122
+ * Destructive seed hex. Required unless `seed.brandDestructive` is set.
123
+ * Not a package default — the site names it.
124
+ */
125
+ destructive?: string;
126
+ /** Default: the whole catalogue. */
127
+ fonts?: readonly FontEntry[];
128
+ /** Where the site's `serveFont` route answers, for the picker's specimens. Default `"/fonts"`. */
129
+ fontsBaseUrl?: string;
130
+ /** Fill + automatic label target. Default `7`. Warnings never block save. */
131
+ contrastTarget?: number;
132
+ /**
133
+ * @deprecated Theme has no preview pane (SPI-56 canceled). Kept so a
134
+ * site that still passes it does not fail to boot.
135
+ */
136
+ previewPath?: string;
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.
141
+ */
142
+ logo?: {
143
+ collection: string;
144
+ };
145
+ /**
146
+ * Fallback art when logo, favicon, or mobile-menu mark is empty. The
147
+ * site passes today's files; `--color-primary-mark` is not a Theme field.
148
+ */
149
+ identity?: {
150
+ fallback?: ThemeIdentityFallback;
151
+ };
152
+ /** Fires from `afterChange` on every save — Theme has no draft mode. */
153
+ onPublish?: (theme: ThemeConfig, doc: ThemeDoc) => void | Promise<void>;
154
+ }
155
+ interface ThemeHeadOptions {
156
+ fontsBaseUrl?: string;
157
+ attribute?: "class" | "data-theme";
158
+ identity?: ResolvedThemeIdentity;
159
+ }
160
+ interface ThemeHead {
161
+ css: string;
162
+ preloads: {
163
+ href: string;
164
+ type: "font/woff2";
165
+ }[];
166
+ identity?: ResolvedThemeIdentity;
167
+ }
168
+ declare const THEME_SLUG = "theme";
169
+ declare const SYSTEM_COLOR_KEYS: readonly ["primary", "secondary", "accent", "highlight", "success", "destructive"];
170
+ type SystemColorKey = (typeof SYSTEM_COLOR_KEYS)[number];
171
+ //#endregion
172
+ //#region src/theme/library.d.ts
173
+ /**
174
+ * Page editors pick `coral-400`, never a raw hex. Rewrite every use of
175
+ * `fromKey` onto `toKey` at the same step: `coral-400` → `highlight-400`.
176
+ * A bare key (`coral`) becomes `toKey`. Anything else is left alone.
177
+ */
178
+ declare function rewriteColorToken(token: string, fromKey: string, toKey: string): string;
179
+ declare function themeColorKeys(doc: ThemeDoc | null | undefined): string[];
180
+ /**
181
+ * Remove a custom color. Unused: omit `replacement` and the row is gone.
182
+ * In use: pass another system or custom key; the caller rewrites page
183
+ * tokens with `rewriteColorToken`. After this, no library row still
184
+ * has `key`.
185
+ */
186
+ declare function deleteLibraryColor(doc: ThemeDoc, key: string, replacement?: string): ThemeDoc;
187
+ //#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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"library-BGq4wGif.d.mts","names":[],"sources":["../src/theme/identity.ts","../src/theme/types.ts","../src/theme/library.ts"],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ "use client";
2
+ import { SHADE_STEPS, createColorScale, generateColorScale, isThemeHex } from "@bison-lab/tokens";
3
+ //#region src/theme/map.ts
4
+ function pick(value, fallback) {
5
+ return value == null ? fallback : value;
6
+ }
7
+ function headingFromDoc(heading, seed) {
8
+ if (heading === void 0) return seed;
9
+ if (heading === null || heading === "") return null;
10
+ return heading;
11
+ }
12
+ function hexFromColor(value, fallback) {
13
+ if (typeof value === "string") return value || fallback;
14
+ return pick(value?.hex, fallback);
15
+ }
16
+ function colorDocFromHex(hex, sourceStep = 500) {
17
+ return colorDocFromState(createColorScale(hex, sourceStep));
18
+ }
19
+ function colorDocFromState(state) {
20
+ return {
21
+ hex: state.hex,
22
+ sourceStep: state.sourceStep,
23
+ scale: state.scale,
24
+ stale: state.stale,
25
+ include: Array.isArray(state.include) ? "custom" : state.include,
26
+ includedSteps: Array.isArray(state.include) ? [...state.include] : void 0
27
+ };
28
+ }
29
+ function stateFromColorDoc(doc, fallbackHex = "#000000") {
30
+ const hex = isThemeHex(doc?.hex) ? doc.hex : fallbackHex;
31
+ const sourceStep = Number(doc?.sourceStep) || 500;
32
+ const include = includeFromDoc(doc, sourceStep);
33
+ return {
34
+ hex,
35
+ sourceStep,
36
+ scale: doc?.scale && typeof doc.scale === "object" ? doc.scale : generateColorScale(hex, sourceStep),
37
+ stale: Boolean(doc?.stale),
38
+ include
39
+ };
40
+ }
41
+ function includeFromDoc(doc, sourceStep) {
42
+ if (doc?.include === "source") return "source";
43
+ if (doc?.include === "custom" && Array.isArray(doc.includedSteps)) {
44
+ const steps = doc.includedSteps.map(Number).filter((step) => SHADE_STEPS.includes(step));
45
+ if (!steps.includes(sourceStep)) steps.push(sourceStep);
46
+ return steps;
47
+ }
48
+ return "all";
49
+ }
50
+ /**
51
+ * Nested Theme document → flat `ThemeConfig`. Null or missing editor
52
+ * fields take the seed; `darkSelector` and `fontWeights` always come from
53
+ * the seed. Reads the BIS-87 groups and the pre-BIS-87 flat fields.
54
+ */
55
+ function themeConfigFromDoc(doc, seed) {
56
+ const brand = doc?.colors?.brand ?? doc?.brand;
57
+ const typography = doc?.typography ?? doc?.fonts;
58
+ const appearance = doc?.appearance;
59
+ return {
60
+ brandPrimary: hexFromColor(brand?.primary, seed.brandPrimary),
61
+ brandSecondary: hexFromColor(brand?.secondary, seed.brandSecondary),
62
+ brandAccent: hexFromColor(brand?.accent, seed.brandAccent),
63
+ brandHighlight: hexFromColor(brand?.highlight, seed.brandHighlight),
64
+ brandSuccess: hexFromColor(brand?.success, seed.brandSuccess),
65
+ brandDestructive: hexFromColor(brand?.destructive, seed.brandDestructive ?? "") || seed.brandDestructive,
66
+ greyScale: pick(doc?.colors?.greyScale ?? doc?.greyScale, seed.greyScale),
67
+ radius: pick(appearance?.radius ?? doc?.radius, seed.radius),
68
+ shadow: pick(appearance?.shadow ?? doc?.shadow, seed.shadow),
69
+ motion: pick(appearance?.motion ?? doc?.motion, seed.motion),
70
+ density: pick(appearance?.density ?? doc?.density, seed.density),
71
+ defaultTheme: pick(appearance?.defaultTheme ?? doc?.defaultTheme, seed.defaultTheme),
72
+ fontBody: pick(typography?.body, seed.fontBody),
73
+ fontHeading: headingFromDoc(typography?.heading, seed.fontHeading),
74
+ darkSelector: seed.darkSelector,
75
+ fontWeights: seed.fontWeights
76
+ };
77
+ }
78
+ /** The document `seedTheme` writes so a fresh Global matches the seed config. */
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");
82
+ return {
83
+ colors: {
84
+ brand: {
85
+ primary: colorDocFromHex(config.brandPrimary),
86
+ secondary: colorDocFromHex(config.brandSecondary),
87
+ accent: colorDocFromHex(config.brandAccent),
88
+ highlight: colorDocFromHex(config.brandHighlight),
89
+ success: colorDocFromHex(config.brandSuccess),
90
+ destructive: colorDocFromHex(destructiveHex)
91
+ },
92
+ library: [],
93
+ greyScale: config.greyScale
94
+ },
95
+ typography: {
96
+ body: config.fontBody,
97
+ heading: config.fontHeading
98
+ },
99
+ appearance: {
100
+ defaultTheme: config.defaultTheme,
101
+ radius: config.radius,
102
+ shadow: config.shadow,
103
+ motion: config.motion,
104
+ density: config.density
105
+ }
106
+ };
107
+ }
108
+ //#endregion
109
+ export { themeConfigFromDoc as i, docFromConfig as n, stateFromColorDoc as r, colorDocFromState as t };
110
+
111
+ //# sourceMappingURL=map-BUDkX8g1.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map-BUDkX8g1.mjs","names":[],"sources":["../src/theme/map.ts"],"sourcesContent":["import {\n createColorScale,\n generateColorScale,\n isThemeHex,\n SHADE_STEPS,\n type ColorScale,\n type ColorScaleInclude,\n type ColorScaleState,\n type ShadeStep,\n type ThemeConfig,\n} from \"@bison-lab/tokens\";\n\nimport { SAME_AS_BODY } from \"./fields\";\nimport type { ThemeColorDoc, ThemeDoc } from \"./types\";\n\nfunction pick<T>(value: T | null | undefined, fallback: T): T {\n return value == null ? fallback : value;\n}\n\nfunction headingFromDoc(\n heading: string | null | undefined,\n seed: ThemeConfig[\"fontHeading\"],\n): string | null {\n if (heading === undefined) return seed;\n if (heading === null || heading === SAME_AS_BODY) return null;\n return heading;\n}\n\nfunction hexFromColor(value: ThemeColorDoc | string | null | undefined, fallback: string): string {\n if (typeof value === \"string\") return value || fallback;\n return pick(value?.hex, fallback);\n}\n\nexport function colorDocFromHex(hex: string, sourceStep: ShadeStep = 500): ThemeColorDoc {\n return colorDocFromState(createColorScale(hex, sourceStep));\n}\n\nexport function colorDocFromState(state: ColorScaleState): ThemeColorDoc {\n return {\n hex: state.hex,\n sourceStep: state.sourceStep,\n scale: state.scale,\n stale: state.stale,\n include: Array.isArray(state.include) ? \"custom\" : state.include,\n includedSteps: Array.isArray(state.include) ? [...state.include] : undefined,\n };\n}\n\nexport function stateFromColorDoc(\n doc: ThemeColorDoc | null | undefined,\n fallbackHex = \"#000000\",\n): ColorScaleState {\n const hex = isThemeHex(doc?.hex) ? doc.hex : fallbackHex;\n const sourceStep = (Number(doc?.sourceStep) || 500) as ShadeStep;\n const include = includeFromDoc(doc, sourceStep);\n const scale =\n doc?.scale && typeof doc.scale === \"object\" ? (doc.scale as ColorScale) : generateColorScale(hex, sourceStep);\n return { hex, sourceStep, scale, stale: Boolean(doc?.stale), include };\n}\n\nfunction includeFromDoc(doc: ThemeColorDoc | null | undefined, sourceStep: ShadeStep): ColorScaleInclude {\n if (doc?.include === \"source\") return \"source\";\n if (doc?.include === \"custom\" && Array.isArray(doc.includedSteps)) {\n const steps = doc.includedSteps.map(Number).filter((step): step is ShadeStep => SHADE_STEPS.includes(step as ShadeStep));\n if (!steps.includes(sourceStep)) steps.push(sourceStep);\n return steps;\n }\n return \"all\";\n}\n\n/**\n * Nested Theme document → flat `ThemeConfig`. Null or missing editor\n * fields take the seed; `darkSelector` and `fontWeights` always come from\n * the seed. Reads the BIS-87 groups and the pre-BIS-87 flat fields.\n */\nexport function themeConfigFromDoc(doc: ThemeDoc | null | undefined, seed: ThemeConfig): ThemeConfig {\n const brand = doc?.colors?.brand ?? doc?.brand;\n const typography = doc?.typography ?? doc?.fonts;\n const appearance = doc?.appearance;\n return {\n brandPrimary: hexFromColor(brand?.primary, seed.brandPrimary),\n brandSecondary: hexFromColor(brand?.secondary, seed.brandSecondary),\n brandAccent: hexFromColor(brand?.accent, seed.brandAccent),\n brandHighlight: hexFromColor(brand?.highlight, seed.brandHighlight),\n brandSuccess: hexFromColor(brand?.success, seed.brandSuccess),\n brandDestructive: hexFromColor(brand?.destructive, seed.brandDestructive ?? \"\") || seed.brandDestructive,\n greyScale: pick(doc?.colors?.greyScale ?? doc?.greyScale, seed.greyScale),\n radius: pick(appearance?.radius ?? doc?.radius, seed.radius),\n shadow: pick(appearance?.shadow ?? doc?.shadow, seed.shadow),\n motion: pick(appearance?.motion ?? doc?.motion, seed.motion),\n density: pick(appearance?.density ?? doc?.density, seed.density),\n defaultTheme: pick(appearance?.defaultTheme ?? doc?.defaultTheme, seed.defaultTheme),\n fontBody: pick(typography?.body, seed.fontBody),\n fontHeading: headingFromDoc(typography?.heading, seed.fontHeading),\n darkSelector: seed.darkSelector,\n fontWeights: seed.fontWeights,\n };\n}\n\n/** The document `seedTheme` writes so a fresh Global matches the seed config. */\nexport function docFromConfig(config: ThemeConfig, destructive?: string): ThemeDoc {\n const destructiveHex = destructive ?? config.brandDestructive;\n if (!destructiveHex) {\n throw new Error(\"docFromConfig requires a destructive hex on the config or as the second argument\");\n }\n return {\n colors: {\n brand: {\n primary: colorDocFromHex(config.brandPrimary),\n secondary: colorDocFromHex(config.brandSecondary),\n accent: colorDocFromHex(config.brandAccent),\n highlight: colorDocFromHex(config.brandHighlight),\n success: colorDocFromHex(config.brandSuccess),\n destructive: colorDocFromHex(destructiveHex),\n },\n library: [],\n greyScale: config.greyScale,\n },\n typography: {\n body: config.fontBody,\n heading: config.fontHeading,\n },\n appearance: {\n defaultTheme: config.defaultTheme,\n radius: config.radius,\n shadow: config.shadow,\n motion: config.motion,\n density: config.density,\n },\n };\n}\n\nexport function validateThemeHex(value: unknown): true | string {\n if (!isThemeHex(value)) return \"Enter a six-digit hex colour like #1e3a5f\";\n return true;\n}\n\nexport function validateSourceIncluded(\n value: unknown,\n { siblingData }: { siblingData?: { sourceStep?: unknown; include?: unknown } },\n): true | string {\n const include = siblingData?.include;\n if (include === \"all\" || include === \"source\") return true;\n const source = Number(siblingData?.sourceStep);\n const steps = Array.isArray(value) ? value.map(Number) : [];\n if (!SHADE_STEPS.includes(source as ShadeStep)) return true;\n if (!steps.includes(source)) return \"The source step cannot be excluded\";\n return true;\n}\n"],"mappings":"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"}
package/dist/react.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { i as themeConfigFromDoc, n as docFromConfig } from "./map-BUDkX8g1.mjs";
2
3
  import { auditTheme, buildThemeCss, isThemeHex, themeFontIds } from "@bison-lab/tokens";
3
4
  import { useRef, useState } from "react";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -19,64 +20,8 @@ function themeHead(config, options = {}) {
19
20
  const theme = buildThemeCss(config, { attribute: options.attribute });
20
21
  return {
21
22
  css: faces ? `${faces}\n\n${theme}` : theme,
22
- preloads: fontPreloads(ids, fontsBaseUrl)
23
- };
24
- }
25
- //#endregion
26
- //#region src/theme/map.ts
27
- function pick(value, fallback) {
28
- return value == null ? fallback : value;
29
- }
30
- function headingFromDoc(heading, seed) {
31
- if (heading === void 0) return seed;
32
- if (heading === null || heading === "") return null;
33
- return heading;
34
- }
35
- /**
36
- * Nested Theme document → flat `ThemeConfig`. Null or missing editor
37
- * fields take the seed; `darkSelector` and `fontWeights` always come from
38
- * the seed.
39
- */
40
- function themeConfigFromDoc(doc, seed) {
41
- const brand = doc?.brand;
42
- return {
43
- brandPrimary: pick(brand?.primary, seed.brandPrimary),
44
- brandSecondary: pick(brand?.secondary, seed.brandSecondary),
45
- brandAccent: pick(brand?.accent, seed.brandAccent),
46
- brandHighlight: pick(brand?.highlight, seed.brandHighlight),
47
- brandSuccess: pick(brand?.success, seed.brandSuccess),
48
- greyScale: pick(doc?.greyScale, seed.greyScale),
49
- radius: pick(doc?.radius, seed.radius),
50
- shadow: pick(doc?.shadow, seed.shadow),
51
- motion: pick(doc?.motion, seed.motion),
52
- density: pick(doc?.density, seed.density),
53
- defaultTheme: pick(doc?.defaultTheme, seed.defaultTheme),
54
- fontBody: pick(doc?.fonts?.body, seed.fontBody),
55
- fontHeading: headingFromDoc(doc?.fonts?.heading, seed.fontHeading),
56
- darkSelector: seed.darkSelector,
57
- fontWeights: seed.fontWeights
58
- };
59
- }
60
- /** The document `seedTheme` writes so a fresh Global matches the seed config. */
61
- function docFromConfig(config) {
62
- return {
63
- brand: {
64
- primary: config.brandPrimary,
65
- secondary: config.brandSecondary,
66
- accent: config.brandAccent,
67
- highlight: config.brandHighlight,
68
- success: config.brandSuccess
69
- },
70
- greyScale: config.greyScale,
71
- radius: config.radius,
72
- shadow: config.shadow,
73
- motion: config.motion,
74
- density: config.density,
75
- defaultTheme: config.defaultTheme,
76
- fonts: {
77
- body: config.fontBody,
78
- heading: config.fontHeading
79
- }
23
+ preloads: fontPreloads(ids, fontsBaseUrl),
24
+ identity: options.identity
80
25
  };
81
26
  }
82
27
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"react.mjs","names":[],"sources":["../src/theme/head.ts","../src/theme/map.ts","../src/react/theme-preview.tsx"],"sourcesContent":["import { fontFaceCss, fontPreloads } from \"@bison-lab/fonts\";\nimport { buildThemeCss, themeFontIds, type ThemeConfig } from \"@bison-lab/tokens\";\n\nimport type { ThemeHead, ThemeHeadOptions } from \"./types\";\n\n/**\n * `@font-face` plus `buildThemeCss` as one stylesheet, and the preload\n * list for the families the config names. A root layout (and the admin\n * layout, if it should restyle too) renders `css` in a `<style>` and\n * each preload as `<link rel=\"preload\" as=\"font\">`.\n */\nexport function themeHead(config: ThemeConfig, options: ThemeHeadOptions = {}): ThemeHead {\n const fontsBaseUrl = options.fontsBaseUrl ?? \"/fonts\";\n const ids = themeFontIds(config);\n const faces = fontFaceCss(ids, fontsBaseUrl);\n const theme = buildThemeCss(config, { attribute: options.attribute });\n return {\n css: faces ? `${faces}\\n\\n${theme}` : theme,\n preloads: fontPreloads(ids, fontsBaseUrl),\n };\n}\n","import { isThemeHex, type ThemeConfig } from \"@bison-lab/tokens\";\n\nimport { SAME_AS_BODY } from \"./fields\";\nimport type { ThemeDoc } from \"./types\";\n\nfunction pick<T>(value: T | null | undefined, fallback: T): T {\n return value == null ? fallback : value;\n}\n\nfunction headingFromDoc(\n heading: string | null | undefined,\n seed: ThemeConfig[\"fontHeading\"],\n): string | null {\n if (heading === undefined) return seed;\n if (heading === null || heading === SAME_AS_BODY) return null;\n return heading;\n}\n\n/**\n * Nested Theme document → flat `ThemeConfig`. Null or missing editor\n * fields take the seed; `darkSelector` and `fontWeights` always come from\n * the seed.\n */\nexport function themeConfigFromDoc(doc: ThemeDoc | null | undefined, seed: ThemeConfig): ThemeConfig {\n const brand = doc?.brand;\n return {\n brandPrimary: pick(brand?.primary, seed.brandPrimary),\n brandSecondary: pick(brand?.secondary, seed.brandSecondary),\n brandAccent: pick(brand?.accent, seed.brandAccent),\n brandHighlight: pick(brand?.highlight, seed.brandHighlight),\n brandSuccess: pick(brand?.success, seed.brandSuccess),\n greyScale: pick(doc?.greyScale, seed.greyScale),\n radius: pick(doc?.radius, seed.radius),\n shadow: pick(doc?.shadow, seed.shadow),\n motion: pick(doc?.motion, seed.motion),\n density: pick(doc?.density, seed.density),\n defaultTheme: pick(doc?.defaultTheme, seed.defaultTheme),\n fontBody: pick(doc?.fonts?.body, seed.fontBody),\n fontHeading: headingFromDoc(doc?.fonts?.heading, seed.fontHeading),\n darkSelector: seed.darkSelector,\n fontWeights: seed.fontWeights,\n };\n}\n\n/** The document `seedTheme` writes so a fresh Global matches the seed config. */\nexport function docFromConfig(config: ThemeConfig): ThemeDoc {\n return {\n brand: {\n primary: config.brandPrimary,\n secondary: config.brandSecondary,\n accent: config.brandAccent,\n highlight: config.brandHighlight,\n success: config.brandSuccess,\n },\n greyScale: config.greyScale,\n radius: config.radius,\n shadow: config.shadow,\n motion: config.motion,\n density: config.density,\n defaultTheme: config.defaultTheme,\n fonts: {\n body: config.fontBody,\n heading: config.fontHeading,\n },\n };\n}\n\nexport function validateThemeHex(value: unknown): true | string {\n if (!isThemeHex(value)) return \"Enter a six-digit hex colour like #1e3a5f\";\n return true;\n}\n","import { useLivePreview } from \"@payloadcms/live-preview-react\";\nimport { ContrastStrip, ThemeShowcase } from \"@bison-lab/ui\";\nimport { auditTheme, isThemeHex, type ThemeConfig } from \"@bison-lab/tokens\";\nimport { useRef, useState } from \"react\";\n\nimport { themeHead } from \"../theme/head\";\nimport { docFromConfig, themeConfigFromDoc } from \"../theme/map\";\nimport type { ThemeDoc } from \"../theme/types\";\n\nexport interface ThemePreviewProps {\n /** Published theme from `getPublishedTheme`. Shown until a live-preview message arrives. */\n theme: ThemeConfig;\n /** The site's `bison.config.json`. Fills fields a live message leaves empty. */\n seed: ThemeConfig;\n /** Where the site's `serveFont` route answers. */\n fontsBaseUrl: string;\n /** Absolute origin of the Payload server. Required by `useLivePreview`. */\n serverURL: string;\n /** Body-text target `ContrastStrip` judges against. Default `4.5`. */\n contrastTarget?: number;\n}\n\nfunction isRenderableTheme(config: ThemeConfig): boolean {\n return (\n isThemeHex(config.brandPrimary) &&\n isThemeHex(config.brandSecondary) &&\n isThemeHex(config.brandAccent) &&\n isThemeHex(config.brandHighlight) &&\n isThemeHex(config.brandSuccess)\n );\n}\n\n/**\n * The Theme document's preview pane, and the site's design-system page.\n * Rebuilds `themeHead` in the browser from each live-preview message.\n * A half-typed hex keeps the last complete stylesheet.\n */\nexport function ThemePreview({\n theme,\n seed,\n fontsBaseUrl,\n serverURL,\n contrastTarget = 4.5,\n}: ThemePreviewProps) {\n const { data } = useLivePreview<ThemeDoc>({\n initialData: docFromConfig(theme),\n serverURL,\n });\n const mapped = themeConfigFromDoc(data, seed);\n const last = useRef(theme);\n if (isRenderableTheme(mapped)) last.current = mapped;\n const config = last.current;\n const css = themeHead(config, { fontsBaseUrl }).css;\n const [mode, setMode] = useState<\"light\" | \"dark\">(\"light\");\n\n return (\n <div role=\"region\" aria-label=\"Theme preview\" data-theme={mode} className=\"bl-theme-preview\">\n <style>{css}</style>\n <div className=\"bl-theme-preview__bar\">\n <button type=\"button\" aria-pressed={mode === \"light\"} onClick={() => setMode(\"light\")}>\n Light\n </button>\n <button type=\"button\" aria-pressed={mode === \"dark\"} onClick={() => setMode(\"dark\")}>\n Dark\n </button>\n </div>\n <ThemeShowcase />\n <ContrastStrip checks={auditTheme(config, { target: contrastTarget })} />\n </div>\n );\n}\n"],"mappings":"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"}
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"}
package/dist/theme.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as ThemeHead, i as ThemeDoc, n as EditableTheme, o as ThemeHeadOptions, r as THEME_SLUG, s as ThemeUploadDoc, t as CreateThemeOptions } from "./types-pFpmDeSA.mjs";
2
- import { ThemeConfig } from "@bison-lab/tokens";
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";
2
+ import { ShadeStep, ThemeConfig } from "@bison-lab/tokens";
3
3
 
4
4
  //#region src/theme/published.d.ts
5
5
  interface ThemePayload {
@@ -11,17 +11,22 @@ interface ThemePayload {
11
11
  }) => Promise<ThemeDoc | null | undefined>;
12
12
  }
13
13
  /**
14
- * The published Theme, or `seed` when nothing has been published. A newer
15
- * draft never reaches the public site (`draft: false`). Access is
16
- * overridden so a logged-out request still gets the published colours.
14
+ * The Theme row, or `seed` when the Global is empty. Theme has no draft
15
+ * mode: `draft: false` is the live document. Access is overridden so a
16
+ * logged-out request still gets the colours.
17
17
  */
18
18
  declare function getPublishedTheme(payload: ThemePayload, seed: ThemeConfig): Promise<ThemeConfig>;
19
+ /**
20
+ * Uploaded logo, favicon, and mobile-menu mark, or the fallback the site
21
+ * passed into `createTheme`. Depth 1 so the upload url is populated.
22
+ */
23
+ declare function getPublishedIdentity(payload: ThemePayload, fallback?: ThemeIdentityFallback): Promise<ResolvedThemeIdentity>;
19
24
  //#endregion
20
25
  //#region src/theme/map.d.ts
21
26
  /**
22
27
  * Nested Theme document → flat `ThemeConfig`. Null or missing editor
23
28
  * fields take the seed; `darkSelector` and `fontWeights` always come from
24
- * the seed.
29
+ * the seed. Reads the BIS-87 groups and the pre-BIS-87 flat fields.
25
30
  */
26
31
  declare function themeConfigFromDoc(doc: ThemeDoc | null | undefined, seed: ThemeConfig): ThemeConfig;
27
32
  //#endregion
@@ -34,5 +39,5 @@ declare function themeConfigFromDoc(doc: ThemeDoc | null | undefined, seed: Them
34
39
  */
35
40
  declare function themeHead(config: ThemeConfig, options?: ThemeHeadOptions): ThemeHead;
36
41
  //#endregion
37
- export { type CreateThemeOptions, type EditableTheme, THEME_SLUG, type ThemeDoc, type ThemeHead, type ThemeHeadOptions, type ThemeUploadDoc, getPublishedTheme, themeConfigFromDoc, themeHead };
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 };
38
43
  //# 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":";;;;UAMiB,YAAA;EACf,UAAA,GAAa,IAAA;IACX,IAAA;IACA,KAAA;IACA,KAAA;IACA,cAAA;EAAA,MACI,OAAA,CAAQ,QAAA;AAAA;;;;;;iBAQM,iBAAA,CAAkB,OAAA,EAAS,YAAA,EAAc,IAAA,EAAM,WAAA,GAAc,OAAA,CAAQ,WAAA;;;;;AAd3F;;;iBCiBgB,kBAAA,CAAmB,GAAA,EAAK,QAAA,qBAA6B,IAAA,EAAM,WAAA,GAAc,WAAA;;;;;ADjBzF;;;;iBEKgB,SAAA,CAAU,MAAA,EAAQ,WAAA,EAAa,OAAA,GAAS,gBAAA,GAAwB,SAAA"}
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"}