@bison-lab/payload-core 0.1.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -17
- package/dist/admin.d.mts +28 -0
- package/dist/admin.d.mts.map +1 -0
- package/dist/admin.mjs +433 -0
- package/dist/admin.mjs.map +1 -0
- package/dist/index.d.mts +35 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +159 -1
- package/dist/index.mjs.map +1 -1
- package/dist/metadata.d.mts +1 -1
- package/dist/theme.d.mts +38 -0
- package/dist/theme.d.mts.map +1 -0
- package/dist/theme.mjs +39 -0
- package/dist/theme.mjs.map +1 -0
- package/dist/{title-Wut0nzJQ.d.mts → title-B2-gWQZd.d.mts} +1 -1
- package/dist/{title-Wut0nzJQ.d.mts.map → title-B2-gWQZd.d.mts.map} +1 -1
- package/dist/types-B3FC9sdF.d.mts +89 -0
- package/dist/types-B3FC9sdF.d.mts.map +1 -0
- package/dist/types-DWwL-JEr.mjs +76 -0
- package/dist/types-DWwL-JEr.mjs.map +1 -0
- package/package.json +39 -4
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { a as SeoImageSize, i as SeoImageDoc, n as titleTemplate, o as SeoImageValue, r as MediaId, s as SeoMeta, t as documentTitle } from "./title-
|
|
2
|
-
import {
|
|
1
|
+
import { a as SeoImageSize, i as SeoImageDoc, n as titleTemplate, o as SeoImageValue, r as MediaId, s as SeoMeta, t as documentTitle } from "./title-B2-gWQZd.mjs";
|
|
2
|
+
import { i as ThemeDoc, n as EditableTheme, r as THEME_SLUG, s as ThemeUploadDoc, t as CreateThemeOptions } from "./types-B3FC9sdF.mjs";
|
|
3
|
+
import { ThemeConfig } from "@bison-lab/tokens";
|
|
4
|
+
import { CheckboxField, CollectionSlug, GlobalConfig, Plugin, UploadCollectionSlug } from "payload";
|
|
3
5
|
|
|
4
6
|
//#region src/seo/plugin.d.ts
|
|
5
7
|
/**
|
|
@@ -102,5 +104,35 @@ declare const DESCRIPTION_LENGTH = 155;
|
|
|
102
104
|
*/
|
|
103
105
|
declare function truncateAtWord(text: string, max?: number): string;
|
|
104
106
|
//#endregion
|
|
105
|
-
|
|
107
|
+
//#region src/theme/global.d.ts
|
|
108
|
+
/**
|
|
109
|
+
* Settings → Theme: the five brand colours, grey scale, presets, default
|
|
110
|
+
* theme, body and heading fonts, and optional logo. Access is passed in
|
|
111
|
+
* (`canManageBrand` for update). Drafts autosave; the public site reads
|
|
112
|
+
* the published version through `getPublishedTheme`.
|
|
113
|
+
*
|
|
114
|
+
* `previewPath` is stored for BIS-61, which wires the live preview pane.
|
|
115
|
+
*/
|
|
116
|
+
declare function createTheme(options: CreateThemeOptions): GlobalConfig;
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/theme/seed.d.ts
|
|
119
|
+
interface SeedThemePayload {
|
|
120
|
+
updateGlobal: (args: {
|
|
121
|
+
slug: string;
|
|
122
|
+
data: ThemeDoc;
|
|
123
|
+
draft?: boolean;
|
|
124
|
+
}) => Promise<unknown>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Writes a published Theme from the seed. For a site's migration `up()`,
|
|
128
|
+
* so the row exists on deploy and renders what `bison-theme.css` rendered.
|
|
129
|
+
*/
|
|
130
|
+
declare function seedTheme(payload: SeedThemePayload, seed: ThemeConfig): Promise<unknown>;
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/theme/fields.d.ts
|
|
133
|
+
declare const THEME_COLOR_FIELD = "@bison-lab/payload-core/admin#ColorField";
|
|
134
|
+
declare const THEME_FONT_FIELD = "@bison-lab/payload-core/admin#FontField";
|
|
135
|
+
declare const THEME_CONTRAST_REPORT = "@bison-lab/payload-core/admin#ContrastReport";
|
|
136
|
+
//#endregion
|
|
137
|
+
export { type CreateThemeOptions, DESCRIPTION_LENGTH, type EditableTheme, type MediaId, SHARE_IMAGE_SIZE, type SeoDoc, type SeoImageDoc, type SeoImageSize, type SeoImageValue, type SeoMeta, type SeoPluginOptions, THEME_COLOR_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_SLUG, type ThemeDoc, type ThemeUploadDoc, createTheme, documentTitle, firstImageIn, noIndexField, seedTheme, seoPlugin, titleTemplate, truncateAtWord };
|
|
106
138
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/seo/plugin.ts","../src/seo/fields.ts","../src/seo/share-image.ts","../src/seo/text.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/seo/plugin.ts","../src/seo/fields.ts","../src/seo/share-image.ts","../src/seo/text.ts","../src/theme/global.ts","../src/theme/seed.ts","../src/theme/fields.ts"],"mappings":";;;;;;;;;;AAoBA;;UAAiB,MAAA;EACf,KAAA;AAAA;AAAA,UAGe,gBAAA,cAA8B,MAAA,GAAS,MAAA;EAAvB;EAE/B,QAAA;EAF6C;;;;;;EAS7C,MAAA,GAAS,GAAA,EAAK,IAAA;EAiBM;;;;;EAXpB,YAAA,IAAgB,GAAA,EAAK,IAAA;EAbrB;;;;;;EAoBA,QAAA,IAAY,GAAA,EAAK,IAAA,KAAS,OAAA;EAA1B;EAEA,WAAA,GAAc,cAAA;EAFF;EAIZ,iBAAA,GAAoB,oBAAA;AAAA;;;;;;AAetB;;;;;;;iBAAgB,SAAA,cAAuB,MAAA,GAAS,MAAA,CAAA,CAAA;EAC9C,QAAA;EACA,MAAA;EACA,YAAA;EACA,QAAA;EACA,WAAA;EACA;AAAA,GACC,gBAAA,CAAiB,IAAA,IAAQ,MAAA;;;;;;;iBA6BZ,YAAA,CACd,MAAA,WACA,KAAA,YACC,OAAA;;;;;;;;cCjGU,YAAA,EAAc,aAAA;;;;;;;;;ADa3B;;cEVa,gBAAA;;;;;;;;;cCTA,kBAAA;;;;;AHmBb;;iBGXgB,cAAA,CAAe,IAAA,UAAc,GAAA;;;;;;;AHW7C;;;;iBIqEgB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,YAAA;;;UCnFzC,gBAAA;EACf,YAAA,GAAe,IAAA;IACb,IAAA;IACA,IAAA,EAAM,QAAA;IACN,KAAA;EAAA,MACI,OAAA;AAAA;;;ALaR;;iBKNsB,SAAA,CAAU,OAAA,EAAS,gBAAA,EAAkB,IAAA,EAAM,WAAA,GAAc,OAAA;;;cCdlE,iBAAA;AAAA,cACA,gBAAA;AAAA,cACA,qBAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-C4ILz4p2.mjs";
|
|
2
|
+
import { a as THEME_COLOR_FIELD, c as headingSelectValue, i as validateThemeHex, n as docFromConfig, o as THEME_CONTRAST_REPORT, r as themeConfigFromDoc, s as THEME_FONT_FIELD, t as THEME_SLUG } from "./types-DWwL-JEr.mjs";
|
|
2
3
|
import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
|
|
4
|
+
import { catalog } from "@bison-lab/fonts";
|
|
5
|
+
import { presetHints } from "@bison-lab/tokens";
|
|
3
6
|
//#region src/seo/fields.ts
|
|
4
7
|
/**
|
|
5
8
|
* The index switch, last in the SEO tab. Off by default: a page is public
|
|
@@ -76,6 +79,161 @@ function firstImageIn(blocks, field = "image") {
|
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
//#endregion
|
|
79
|
-
|
|
82
|
+
//#region src/theme/global.ts
|
|
83
|
+
function requireAccess(options) {
|
|
84
|
+
const read = options.access?.read;
|
|
85
|
+
const update = options.access?.update;
|
|
86
|
+
if (!read || !update) throw new Error("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.");
|
|
87
|
+
return {
|
|
88
|
+
read,
|
|
89
|
+
update
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function hintSelect(name, label, hints, defaultValue) {
|
|
93
|
+
return {
|
|
94
|
+
name,
|
|
95
|
+
type: "select",
|
|
96
|
+
label,
|
|
97
|
+
required: true,
|
|
98
|
+
defaultValue,
|
|
99
|
+
options: Object.entries(hints).map(([value, { label: optionLabel, hint }]) => ({
|
|
100
|
+
label: `${optionLabel} — ${hint}`,
|
|
101
|
+
value
|
|
102
|
+
}))
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function colorField(name, label, defaultValue) {
|
|
106
|
+
return {
|
|
107
|
+
name,
|
|
108
|
+
type: "text",
|
|
109
|
+
label,
|
|
110
|
+
required: true,
|
|
111
|
+
defaultValue,
|
|
112
|
+
validate: validateThemeHex,
|
|
113
|
+
admin: { components: { Field: THEME_COLOR_FIELD } }
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
|
|
117
|
+
const options = fonts.map((font) => ({
|
|
118
|
+
label: font.family,
|
|
119
|
+
value: font.id
|
|
120
|
+
}));
|
|
121
|
+
return {
|
|
122
|
+
name,
|
|
123
|
+
type: "select",
|
|
124
|
+
label,
|
|
125
|
+
required: !sameAsBody,
|
|
126
|
+
defaultValue,
|
|
127
|
+
options: sameAsBody ? [{
|
|
128
|
+
label: "Same as body",
|
|
129
|
+
value: ""
|
|
130
|
+
}, ...options] : options,
|
|
131
|
+
admin: {
|
|
132
|
+
components: { Field: THEME_FONT_FIELD },
|
|
133
|
+
custom: {
|
|
134
|
+
fontsBaseUrl,
|
|
135
|
+
ids: fonts.map((font) => font.id),
|
|
136
|
+
sameAsBody
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Settings → Theme: the five brand colours, grey scale, presets, default
|
|
143
|
+
* theme, body and heading fonts, and optional logo. Access is passed in
|
|
144
|
+
* (`canManageBrand` for update). Drafts autosave; the public site reads
|
|
145
|
+
* the published version through `getPublishedTheme`.
|
|
146
|
+
*
|
|
147
|
+
* `previewPath` is stored for BIS-61, which wires the live preview pane.
|
|
148
|
+
*/
|
|
149
|
+
function createTheme(options) {
|
|
150
|
+
const access = requireAccess(options);
|
|
151
|
+
const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 4.5, previewPath, logo, onPublish } = options;
|
|
152
|
+
const fields = [
|
|
153
|
+
{
|
|
154
|
+
name: "brand",
|
|
155
|
+
type: "group",
|
|
156
|
+
label: "Brand colours",
|
|
157
|
+
fields: [
|
|
158
|
+
colorField("primary", "Primary", seed.brandPrimary),
|
|
159
|
+
colorField("secondary", "Secondary", seed.brandSecondary),
|
|
160
|
+
colorField("accent", "Accent", seed.brandAccent),
|
|
161
|
+
colorField("highlight", "Highlight", seed.brandHighlight),
|
|
162
|
+
colorField("success", "Success", seed.brandSuccess)
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "contrast",
|
|
167
|
+
type: "ui",
|
|
168
|
+
admin: {
|
|
169
|
+
components: { Field: THEME_CONTRAST_REPORT },
|
|
170
|
+
custom: { target: contrastTarget }
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
hintSelect("greyScale", "Grey scale", presetHints.greyScale, seed.greyScale),
|
|
174
|
+
hintSelect("radius", "Radius", presetHints.radius, seed.radius),
|
|
175
|
+
hintSelect("shadow", "Shadow", presetHints.shadow, seed.shadow),
|
|
176
|
+
hintSelect("motion", "Motion", presetHints.motion, seed.motion),
|
|
177
|
+
hintSelect("density", "Density", presetHints.density, seed.density),
|
|
178
|
+
hintSelect("defaultTheme", "Default theme", presetHints.defaultTheme, seed.defaultTheme),
|
|
179
|
+
{
|
|
180
|
+
name: "fonts",
|
|
181
|
+
type: "group",
|
|
182
|
+
label: "Fonts",
|
|
183
|
+
fields: [fontField("body", "Body", seed.fontBody, fonts, fontsBaseUrl, false), fontField("heading", "Heading", headingSelectValue(seed.fontHeading), fonts, fontsBaseUrl, true)]
|
|
184
|
+
}
|
|
185
|
+
];
|
|
186
|
+
if (logo) fields.push({
|
|
187
|
+
name: "logo",
|
|
188
|
+
type: "upload",
|
|
189
|
+
relationTo: logo.collection,
|
|
190
|
+
label: "Logo"
|
|
191
|
+
}, {
|
|
192
|
+
name: "logoMark",
|
|
193
|
+
type: "upload",
|
|
194
|
+
relationTo: logo.collection,
|
|
195
|
+
label: "Logo mark"
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
slug: THEME_SLUG,
|
|
199
|
+
label: "Theme",
|
|
200
|
+
admin: {
|
|
201
|
+
group: "Settings",
|
|
202
|
+
custom: {
|
|
203
|
+
previewPath,
|
|
204
|
+
contrastTarget,
|
|
205
|
+
fontsBaseUrl
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
versions: { drafts: { autosave: { interval: 800 } } },
|
|
209
|
+
access: {
|
|
210
|
+
read: access.read,
|
|
211
|
+
update: access.update,
|
|
212
|
+
readVersions: access.update
|
|
213
|
+
},
|
|
214
|
+
fields,
|
|
215
|
+
hooks: { afterChange: [async ({ doc }) => {
|
|
216
|
+
if (!onPublish) return;
|
|
217
|
+
const themeDoc = doc;
|
|
218
|
+
if (themeDoc._status !== "published") return;
|
|
219
|
+
await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);
|
|
220
|
+
}] }
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/theme/seed.ts
|
|
225
|
+
/**
|
|
226
|
+
* Writes a published Theme from the seed. For a site's migration `up()`,
|
|
227
|
+
* so the row exists on deploy and renders what `bison-theme.css` rendered.
|
|
228
|
+
*/
|
|
229
|
+
async function seedTheme(payload, seed) {
|
|
230
|
+
return payload.updateGlobal({
|
|
231
|
+
slug: THEME_SLUG,
|
|
232
|
+
data: docFromConfig(seed),
|
|
233
|
+
draft: false
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
export { DESCRIPTION_LENGTH, SHARE_IMAGE_SIZE, THEME_COLOR_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_SLUG, createTheme, documentTitle, firstImageIn, noIndexField, seedTheme, seoPlugin, titleTemplate, truncateAtWord };
|
|
80
238
|
|
|
81
239
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["payloadSeoPlugin"],"sources":["../src/seo/fields.ts","../src/seo/text.ts","../src/seo/plugin.ts"],"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"],"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["payloadSeoPlugin"],"sources":["../src/seo/fields.ts","../src/seo/text.ts","../src/seo/plugin.ts","../src/theme/global.ts","../src/theme/seed.ts"],"sourcesContent":["import type { CheckboxField } from \"payload\";\n\n/**\n * The index switch, last in the SEO tab. Off by default: a page is public\n * unless an editor says otherwise. `pageMetadata` turns it into\n * `noindex, nofollow`; a site's sitemap should filter on it too.\n */\nexport const noIndexField: CheckboxField = {\n name: \"noIndex\",\n type: \"checkbox\",\n label: \"Hide this page from search engines\",\n defaultValue: false,\n admin: {\n description:\n \"Search engines will not list this page and the sitemap will leave it out. Anyone with the link can still open it.\",\n },\n};\n","/** The length a search result shows of a description before it is cut. */\nexport const DESCRIPTION_LENGTH = 155;\n\n/**\n * Prose cut to `max` characters at a word, with an ellipsis, so a generated\n * description does not end mid-syllable. Text that fits is returned trimmed\n * and whole. A first word longer than `max` is cut mid-word, since there is\n * no boundary to cut at.\n */\nexport function truncateAtWord(text: string, max = DESCRIPTION_LENGTH): string {\n const trimmed = text.trim();\n if (trimmed.length <= max) return trimmed;\n const cut = trimmed.slice(0, max);\n const lastSpace = cut.lastIndexOf(\" \");\n return `${lastSpace > 0 ? cut.slice(0, lastSpace) : cut}…`;\n}\n","import { seoPlugin as payloadSeoPlugin } from \"@payloadcms/plugin-seo\";\nimport type {\n GenerateDescription,\n GenerateImage,\n GenerateTitle,\n GenerateURL,\n} from \"@payloadcms/plugin-seo/types\";\nimport type { CollectionSlug, Plugin, UploadCollectionSlug } from \"payload\";\n\nimport { noIndexField } from \"./fields\";\nimport { truncateAtWord } from \"./text\";\nimport { documentTitle } from \"./title\";\nimport type { MediaId } from \"./types\";\n\n/**\n * What the plugin reads off a document: its title, for the generated meta\n * title. The site's generated `Page` is assignable to this; the `urlFor`,\n * `describeFrom` and `imageFor` callbacks read the rest, typed as the site\n * chooses through `TDoc`.\n */\nexport interface SeoDoc {\n title?: string | null;\n}\n\nexport interface SeoPluginOptions<TDoc extends SeoDoc = SeoDoc> {\n /** Appended to every generated title: `<page title> | <siteName>`. */\n siteName: string;\n /**\n * The absolute public URL of a document from the form's data, for the\n * search-result preview and the Generate button beside it. `undefined`\n * when the document has no address yet (a slug not typed), which leaves\n * the preview empty rather than pointing at the home page.\n */\n urlFor: (doc: TDoc) => string | undefined;\n /**\n * Prose to cut a generated description from, the first plain-text field a\n * page always has near the top (a hero's body, say). Cut at a word near\n * 155 characters. Without it the Generate button fills in nothing.\n */\n describeFrom?: (doc: TDoc) => string | null | undefined;\n /**\n * An image already on the page, for the Meta Image Generate button, so an\n * editor need not upload a second copy of the hero picture. `firstImageIn`\n * walks block rows for one. The upload chooser stays, so any other media\n * row can still be picked.\n */\n imageFor?: (doc: TDoc) => MediaId | null | undefined;\n /** Collections that get the SEO tab. Default `['pages']`. */\n collections?: CollectionSlug[];\n /** The upload collection the meta image comes from. Default `'media'`. */\n uploadsCollection?: UploadCollectionSlug;\n}\n\n/**\n * `@payloadcms/plugin-seo` configured the Bison Lab way: an SEO tab beside a\n * Content tab (never below the block editor), holding the overview with its\n * character counts, meta title, description and image with Generate buttons,\n * the search-result preview, and the `noIndex` switch.\n *\n * Every string the tab generates comes from the options, so a site spells\n * its name and its URL scheme once. Pin `@payloadcms/plugin-seo` to the same\n * version as `payload` in the site; the plugin's admin components are\n * resolved from the site's import map, so run `payload generate:importmap`\n * after adding it.\n */\nexport function seoPlugin<TDoc extends SeoDoc = SeoDoc>({\n siteName,\n urlFor,\n describeFrom,\n imageFor,\n collections = [\"pages\"],\n uploadsCollection = \"media\",\n}: SeoPluginOptions<TDoc>): Plugin {\n const generateTitle: GenerateTitle<TDoc> = ({ doc }) =>\n doc?.title ? documentTitle(siteName, doc.title) : \"\";\n const generateDescription: GenerateDescription<TDoc> = ({ doc }) =>\n truncateAtWord(describeFrom?.(doc) ?? \"\");\n const generateURL: GenerateURL<TDoc> = ({ doc }) => urlFor(doc) ?? \"\";\n // Only when the site can name one: the button appears with the function.\n const generateImage: GenerateImage<TDoc> | undefined = imageFor\n ? ({ doc }) => imageFor(doc) ?? \"\"\n : undefined;\n\n return payloadSeoPlugin({\n collections,\n uploadsCollection,\n tabbedUI: true,\n generateTitle,\n generateDescription,\n generateURL,\n ...(generateImage ? { generateImage } : {}),\n fields: ({ defaultFields }) => [...defaultFields, noIndexField],\n });\n}\n\n/**\n * The first image among some block rows, as a media id, for `imageFor`. Each\n * row is checked for `field` holding either a bare id or a populated upload\n * document; rows without one are skipped. Pass the page's hero and layout\n * together (`[...doc.hero, ...doc.layout]`) to search in reading order.\n */\nexport function firstImageIn(\n blocks: unknown,\n field = \"image\",\n): MediaId | undefined {\n if (!Array.isArray(blocks)) return undefined;\n for (const block of blocks) {\n if (typeof block !== \"object\" || block === null) continue;\n const value = (block as Record<string, unknown>)[field];\n const id =\n typeof value === \"object\" && value !== null && \"id\" in value\n ? (value as { id: unknown }).id\n : value;\n if (typeof id === \"number\" || (typeof id === \"string\" && id !== \"\"))\n return id;\n }\n return undefined;\n}\n","import { catalog, type FontEntry } from \"@bison-lab/fonts\";\nimport { presetHints } from \"@bison-lab/tokens\";\nimport type { Field, GlobalConfig, SelectField, TextField } from \"payload\";\n\nimport {\n SAME_AS_BODY,\n THEME_COLOR_FIELD,\n THEME_CONTRAST_REPORT,\n THEME_FONT_FIELD,\n headingSelectValue,\n} from \"./fields\";\nimport { themeConfigFromDoc, validateThemeHex } from \"./map\";\nimport { THEME_SLUG, type CreateThemeOptions, type ThemeDoc } from \"./types\";\n\nfunction requireAccess(options: CreateThemeOptions): CreateThemeOptions[\"access\"] {\n const read = options.access?.read;\n const update = options.access?.update;\n if (!read || !update) {\n throw new Error(\n \"createTheme requires access.read and access.update. Pass the site's predicates; canManageBrand is the intended update predicate until BIS-43 moves src/platform here.\",\n );\n }\n return { read, update };\n}\n\nfunction hintSelect<K extends string>(\n name: string,\n label: string,\n hints: Record<K, { label: string; hint: string }>,\n defaultValue: K,\n): SelectField {\n return {\n name,\n type: \"select\",\n label,\n required: true,\n defaultValue,\n options: (Object.entries(hints) as [K, { label: string; hint: string }][]).map(\n ([value, { label: optionLabel, hint }]) => ({\n label: `${optionLabel} — ${hint}`,\n value,\n }),\n ),\n };\n}\n\nfunction colorField(name: string, label: string, defaultValue: string): TextField {\n return {\n name,\n type: \"text\",\n label,\n required: true,\n defaultValue,\n validate: validateThemeHex,\n admin: { components: { Field: THEME_COLOR_FIELD } },\n };\n}\n\nfunction fontField(\n name: string,\n label: string,\n defaultValue: string,\n fonts: readonly FontEntry[],\n fontsBaseUrl: string,\n sameAsBody: boolean,\n): SelectField {\n const options = fonts.map((font) => ({ label: font.family, value: font.id }));\n return {\n name,\n type: \"select\",\n label,\n required: !sameAsBody,\n defaultValue,\n options: sameAsBody ? [{ label: \"Same as body\", value: SAME_AS_BODY }, ...options] : options,\n admin: {\n components: { Field: THEME_FONT_FIELD },\n custom: { fontsBaseUrl, ids: fonts.map((font) => font.id), sameAsBody },\n },\n };\n}\n\n/**\n * Settings → Theme: the five brand colours, grey scale, presets, default\n * theme, body and heading fonts, and optional logo. Access is passed in\n * (`canManageBrand` for update). Drafts autosave; the public site reads\n * the published version through `getPublishedTheme`.\n *\n * `previewPath` is stored for BIS-61, which wires the live preview pane.\n */\nexport function createTheme(options: CreateThemeOptions): GlobalConfig {\n const access = requireAccess(options);\n const {\n seed,\n fonts = catalog,\n fontsBaseUrl = \"/fonts\",\n contrastTarget = 4.5,\n previewPath,\n logo,\n onPublish,\n } = options;\n\n const fields: Field[] = [\n {\n name: \"brand\",\n type: \"group\",\n label: \"Brand colours\",\n fields: [\n colorField(\"primary\", \"Primary\", seed.brandPrimary),\n colorField(\"secondary\", \"Secondary\", seed.brandSecondary),\n colorField(\"accent\", \"Accent\", seed.brandAccent),\n colorField(\"highlight\", \"Highlight\", seed.brandHighlight),\n colorField(\"success\", \"Success\", seed.brandSuccess),\n ],\n },\n {\n name: \"contrast\",\n type: \"ui\",\n admin: {\n components: { Field: THEME_CONTRAST_REPORT },\n custom: { target: contrastTarget },\n },\n },\n hintSelect(\"greyScale\", \"Grey scale\", presetHints.greyScale, seed.greyScale),\n hintSelect(\"radius\", \"Radius\", presetHints.radius, seed.radius),\n hintSelect(\"shadow\", \"Shadow\", presetHints.shadow, seed.shadow),\n hintSelect(\"motion\", \"Motion\", presetHints.motion, seed.motion),\n hintSelect(\"density\", \"Density\", presetHints.density, seed.density),\n hintSelect(\"defaultTheme\", \"Default theme\", presetHints.defaultTheme, seed.defaultTheme),\n {\n name: \"fonts\",\n type: \"group\",\n label: \"Fonts\",\n fields: [\n fontField(\"body\", \"Body\", seed.fontBody, fonts, fontsBaseUrl, false),\n fontField(\n \"heading\",\n \"Heading\",\n headingSelectValue(seed.fontHeading),\n fonts,\n fontsBaseUrl,\n true,\n ),\n ],\n },\n ];\n\n if (logo) {\n fields.push(\n {\n name: \"logo\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Logo\",\n },\n {\n name: \"logoMark\",\n type: \"upload\",\n relationTo: logo.collection,\n label: \"Logo mark\",\n },\n );\n }\n\n return {\n slug: THEME_SLUG,\n label: \"Theme\",\n admin: {\n group: \"Settings\",\n custom: { previewPath, contrastTarget, fontsBaseUrl },\n },\n versions: { drafts: { autosave: { interval: 800 } } },\n access: {\n read: access.read,\n update: access.update,\n readVersions: access.update,\n },\n fields,\n hooks: {\n afterChange: [\n async ({ doc }) => {\n if (!onPublish) return;\n const themeDoc = doc as ThemeDoc;\n if (themeDoc._status !== \"published\") return;\n await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);\n },\n ],\n },\n };\n}\n","import type { ThemeConfig } from \"@bison-lab/tokens\";\n\nimport { docFromConfig } from \"./map\";\nimport { THEME_SLUG } from \"./types\";\nimport type { ThemeDoc } from \"./types\";\n\nexport interface SeedThemePayload {\n updateGlobal: (args: {\n slug: string;\n data: ThemeDoc;\n draft?: boolean;\n }) => Promise<unknown>;\n}\n\n/**\n * Writes a published Theme from the seed. For a site's migration `up()`,\n * so the row exists on deploy and renders what `bison-theme.css` rendered.\n */\nexport async function seedTheme(payload: SeedThemePayload, seed: ThemeConfig): Promise<unknown> {\n return payload.updateGlobal({\n slug: THEME_SLUG,\n data: docFromConfig(seed),\n draft: false,\n });\n}\n"],"mappings":"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"}
|
package/dist/metadata.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as SeoImageSize, i as SeoImageDoc, n as titleTemplate, o as SeoImageValue, r as MediaId, s as SeoMeta, t as documentTitle } from "./title-
|
|
1
|
+
import { a as SeoImageSize, i as SeoImageDoc, n as titleTemplate, o as SeoImageValue, r as MediaId, s as SeoMeta, t as documentTitle } from "./title-B2-gWQZd.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/seo/metadata.d.ts
|
|
4
4
|
/** A page as the reader needs it; a site's generated `Page` is assignable. */
|
package/dist/theme.d.mts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { a as ThemeHead, i as ThemeDoc, n as EditableTheme, o as ThemeHeadOptions, r as THEME_SLUG, s as ThemeUploadDoc, t as CreateThemeOptions } from "./types-B3FC9sdF.mjs";
|
|
2
|
+
import { ThemeConfig } from "@bison-lab/tokens";
|
|
3
|
+
|
|
4
|
+
//#region src/theme/published.d.ts
|
|
5
|
+
interface ThemePayload {
|
|
6
|
+
findGlobal: (args: {
|
|
7
|
+
slug: string;
|
|
8
|
+
draft?: boolean;
|
|
9
|
+
depth?: number;
|
|
10
|
+
overrideAccess?: boolean;
|
|
11
|
+
}) => Promise<ThemeDoc | null | undefined>;
|
|
12
|
+
}
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
declare function getPublishedTheme(payload: ThemePayload, seed: ThemeConfig): Promise<ThemeConfig>;
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/theme/map.d.ts
|
|
21
|
+
/**
|
|
22
|
+
* Nested Theme document → flat `ThemeConfig`. Null or missing editor
|
|
23
|
+
* fields take the seed; `darkSelector` and `fontWeights` always come from
|
|
24
|
+
* the seed.
|
|
25
|
+
*/
|
|
26
|
+
declare function themeConfigFromDoc(doc: ThemeDoc | null | undefined, seed: ThemeConfig): ThemeConfig;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/theme/head.d.ts
|
|
29
|
+
/**
|
|
30
|
+
* `@font-face` plus `buildThemeCss` as one stylesheet, and the preload
|
|
31
|
+
* list for the families the config names. A root layout (and the admin
|
|
32
|
+
* layout, if it should restyle too) renders `css` in a `<style>` and
|
|
33
|
+
* each preload as `<link rel="preload" as="font">`.
|
|
34
|
+
*/
|
|
35
|
+
declare function themeHead(config: ThemeConfig, options?: ThemeHeadOptions): ThemeHead;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { type CreateThemeOptions, type EditableTheme, THEME_SLUG, type ThemeDoc, type ThemeHead, type ThemeHeadOptions, type ThemeUploadDoc, getPublishedTheme, themeConfigFromDoc, themeHead };
|
|
38
|
+
//# sourceMappingURL=theme.d.mts.map
|
|
@@ -0,0 +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"}
|
package/dist/theme.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { r as themeConfigFromDoc, t as THEME_SLUG } from "./types-DWwL-JEr.mjs";
|
|
2
|
+
import { fontFaceCss, fontPreloads } from "@bison-lab/fonts";
|
|
3
|
+
import { buildThemeCss, themeFontIds } from "@bison-lab/tokens";
|
|
4
|
+
//#region src/theme/published.ts
|
|
5
|
+
/**
|
|
6
|
+
* The published Theme, or `seed` when nothing has been published. A newer
|
|
7
|
+
* draft never reaches the public site (`draft: false`). Access is
|
|
8
|
+
* overridden so a logged-out request still gets the published colours.
|
|
9
|
+
*/
|
|
10
|
+
async function getPublishedTheme(payload, seed) {
|
|
11
|
+
return themeConfigFromDoc(await payload.findGlobal({
|
|
12
|
+
slug: "theme",
|
|
13
|
+
draft: false,
|
|
14
|
+
depth: 0,
|
|
15
|
+
overrideAccess: true
|
|
16
|
+
}) ?? {}, seed);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/theme/head.ts
|
|
20
|
+
/**
|
|
21
|
+
* `@font-face` plus `buildThemeCss` as one stylesheet, and the preload
|
|
22
|
+
* list for the families the config names. A root layout (and the admin
|
|
23
|
+
* layout, if it should restyle too) renders `css` in a `<style>` and
|
|
24
|
+
* each preload as `<link rel="preload" as="font">`.
|
|
25
|
+
*/
|
|
26
|
+
function themeHead(config, options = {}) {
|
|
27
|
+
const fontsBaseUrl = options.fontsBaseUrl ?? "/fonts";
|
|
28
|
+
const ids = themeFontIds(config);
|
|
29
|
+
const faces = fontFaceCss(ids, fontsBaseUrl);
|
|
30
|
+
const theme = buildThemeCss(config, { attribute: options.attribute });
|
|
31
|
+
return {
|
|
32
|
+
css: faces ? `${faces}\n\n${theme}` : theme,
|
|
33
|
+
preloads: fontPreloads(ids, fontsBaseUrl)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { THEME_SLUG, getPublishedTheme, themeConfigFromDoc, themeHead };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=theme.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.mjs","names":[],"sources":["../src/theme/published.ts","../src/theme/head.ts"],"sourcesContent":["import type { ThemeConfig } from \"@bison-lab/tokens\";\n\nimport { themeConfigFromDoc } from \"./map\";\nimport { THEME_SLUG } from \"./types\";\nimport type { ThemeDoc } from \"./types\";\n\nexport interface ThemePayload {\n findGlobal: (args: {\n slug: string;\n draft?: boolean;\n depth?: number;\n overrideAccess?: boolean;\n }) => Promise<ThemeDoc | null | undefined>;\n}\n\n/**\n * The published Theme, or `seed` when nothing has been published. A newer\n * draft never reaches the public site (`draft: false`). Access is\n * overridden so a logged-out request still gets the published colours.\n */\nexport async function getPublishedTheme(payload: ThemePayload, seed: ThemeConfig): Promise<ThemeConfig> {\n const doc = await payload.findGlobal({\n slug: THEME_SLUG,\n draft: false,\n depth: 0,\n overrideAccess: true,\n });\n return themeConfigFromDoc(doc ?? {}, seed);\n}\n","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"],"mappings":";;;;;;;;;AAoBA,eAAsB,kBAAkB,SAAuB,MAAyC;AAOtG,QAAO,mBANK,MAAM,QAAQ,WAAW;EACnC,MAAA;EACA,OAAO;EACP,OAAO;EACP,gBAAgB;EACjB,CAAC,IAC+B,EAAE,EAAE,KAAK;;;;;;;;;;AChB5C,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;EAC1C"}
|
|
@@ -52,4 +52,4 @@ declare function documentTitle(siteName: string, pageTitle: string): string;
|
|
|
52
52
|
declare function titleTemplate(siteName: string): string;
|
|
53
53
|
//#endregion
|
|
54
54
|
export { SeoImageSize as a, SeoImageDoc as i, titleTemplate as n, SeoImageValue as o, MediaId as r, SeoMeta as s, documentTitle as t };
|
|
55
|
-
//# sourceMappingURL=title-
|
|
55
|
+
//# sourceMappingURL=title-B2-gWQZd.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"title-
|
|
1
|
+
{"version":3,"file":"title-B2-gWQZd.d.mts","names":[],"sources":["../src/seo/types.ts","../src/seo/title.ts"],"mappings":";;KACY,OAAA;;UAGK,YAAA;EACf,GAAA;EACA,KAAA;EACA,MAAA;AAAA;;;;;;;;AAWF;UAAiB,WAAA;EACf,GAAA;EACA,GAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;IAAA,CAAW,IAAA,WAAe,YAAA;EAAA;AAAA;;KAIhB,aAAA,GAAgB,OAAA,GAAU,WAAA;;AAAtC;;;UAMiB,OAAA;EACf,KAAA;EACA,WAAA;EACA,KAAA,GAAQ,aAAA;EACR,OAAA;AAAA;;;;AApCF;;;;;iBCKgB,aAAA,CAAc,QAAA,UAAkB,SAAA;;;;;iBAQhC,aAAA,CAAc,QAAA"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { FontEntry } from "@bison-lab/fonts";
|
|
2
|
+
import { ThemeConfig } from "@bison-lab/tokens";
|
|
3
|
+
import { Access } from "payload";
|
|
4
|
+
|
|
5
|
+
//#region src/theme/types.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The Theme document a site's generated types will describe. Optional and
|
|
8
|
+
* nullable, no index signature: a generated Global is assignable to this,
|
|
9
|
+
* never the reverse.
|
|
10
|
+
*/
|
|
11
|
+
interface ThemeDoc {
|
|
12
|
+
brand?: {
|
|
13
|
+
primary?: string | null;
|
|
14
|
+
secondary?: string | null;
|
|
15
|
+
accent?: string | null;
|
|
16
|
+
highlight?: string | null;
|
|
17
|
+
success?: string | null;
|
|
18
|
+
} | null;
|
|
19
|
+
greyScale?: ThemeConfig["greyScale"] | null;
|
|
20
|
+
radius?: ThemeConfig["radius"] | null;
|
|
21
|
+
shadow?: ThemeConfig["shadow"] | null;
|
|
22
|
+
motion?: ThemeConfig["motion"] | null;
|
|
23
|
+
density?: ThemeConfig["density"] | null;
|
|
24
|
+
defaultTheme?: ThemeConfig["defaultTheme"] | null;
|
|
25
|
+
fonts?: {
|
|
26
|
+
body?: string | null;
|
|
27
|
+
heading?: string | null;
|
|
28
|
+
} | null;
|
|
29
|
+
logo?: number | string | ThemeUploadDoc | null;
|
|
30
|
+
logoMark?: number | string | ThemeUploadDoc | null;
|
|
31
|
+
_status?: "draft" | "published" | null;
|
|
32
|
+
}
|
|
33
|
+
interface ThemeUploadDoc {
|
|
34
|
+
id?: number | string | null;
|
|
35
|
+
url?: string | null;
|
|
36
|
+
alt?: string | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What an editor can change. `darkSelector` and `fontWeights` stay on the
|
|
40
|
+
* seed — they mean nothing in the admin — and `themeConfigFromDoc` puts
|
|
41
|
+
* them back.
|
|
42
|
+
*/
|
|
43
|
+
type EditableTheme = Omit<ThemeConfig, "darkSelector" | "fontWeights">;
|
|
44
|
+
interface CreateThemeOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Required, no default. Predicates live in each site's `src/platform`
|
|
47
|
+
* until BIS-43; pass `canManageBrand` as `update` (and typically
|
|
48
|
+
* `isAuthenticated` as `read`). `readVersions` / draft visibility follow
|
|
49
|
+
* `update`, so only a brand manager sees unpublished edits.
|
|
50
|
+
*/
|
|
51
|
+
access: {
|
|
52
|
+
read: Access;
|
|
53
|
+
update: Access;
|
|
54
|
+
};
|
|
55
|
+
/** The site's `bison.config.json`. Every field's `defaultValue`, and the fallback `getPublishedTheme` returns. */
|
|
56
|
+
seed: ThemeConfig;
|
|
57
|
+
/** Default: the whole catalogue. */
|
|
58
|
+
fonts?: readonly FontEntry[];
|
|
59
|
+
/** Where the site's `serveFont` route answers, for the picker's specimens. Default `"/fonts"`. */
|
|
60
|
+
fontsBaseUrl?: string;
|
|
61
|
+
/** Body-text target the contrast report judges against. Default `4.5`. */
|
|
62
|
+
contrastTarget?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Stored on `admin.custom` for BIS-61, which wires `admin.livePreview`.
|
|
65
|
+
* This slice does not open a preview pane.
|
|
66
|
+
*/
|
|
67
|
+
previewPath?: string;
|
|
68
|
+
/** Adds `logo` and `logoMark` upload relations when given. */
|
|
69
|
+
logo?: {
|
|
70
|
+
collection: string;
|
|
71
|
+
};
|
|
72
|
+
/** Fires from `afterChange` on a published save only, not an autosaved draft. */
|
|
73
|
+
onPublish?: (theme: ThemeConfig, doc: ThemeDoc) => void | Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
interface ThemeHeadOptions {
|
|
76
|
+
fontsBaseUrl?: string;
|
|
77
|
+
attribute?: "class" | "data-theme";
|
|
78
|
+
}
|
|
79
|
+
interface ThemeHead {
|
|
80
|
+
css: string;
|
|
81
|
+
preloads: {
|
|
82
|
+
href: string;
|
|
83
|
+
type: "font/woff2";
|
|
84
|
+
}[];
|
|
85
|
+
}
|
|
86
|
+
declare const THEME_SLUG = "theme";
|
|
87
|
+
//#endregion
|
|
88
|
+
export { ThemeHead as a, ThemeDoc as i, EditableTheme as n, ThemeHeadOptions as o, THEME_SLUG as r, ThemeUploadDoc as s, CreateThemeOptions as t };
|
|
89
|
+
//# sourceMappingURL=types-B3FC9sdF.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-B3FC9sdF.d.mts","names":[],"sources":["../src/theme/types.ts"],"mappings":";;;;;;;AASA;;;UAAiB,QAAA;EACf,KAAA;IACE,OAAA;IACA,SAAA;IACA,MAAA;IACA,SAAA;IACA,OAAA;EAAA;EAEF,SAAA,GAAY,WAAA;EACZ,MAAA,GAAS,WAAA;EACT,MAAA,GAAS,WAAA;EACT,MAAA,GAAS,WAAA;EACT,OAAA,GAAU,WAAA;EACV,YAAA,GAAe,WAAA;EACf,KAAA;IACE,IAAA;IACA,OAAA;EAAA;EAEF,IAAA,qBAAyB,cAAA;EACzB,QAAA,qBAA6B,cAAA;EAC7B,OAAA;AAAA;AAAA,UAGe,cAAA;EACf,EAAA;EACA,GAAA;EACA,GAAA;AAAA;;;;;;KAQU,aAAA,GAAgB,IAAA,CAAK,WAAA;AAAA,UAEhB,kBAAA;EAjBc;;;;AAI/B;;EAoBE,MAAA;IAAU,IAAA,EAAM,MAAA;IAAQ,MAAA,EAAQ,MAAA;EAAA;EAjBhC;EAmBA,IAAA,EAAM,WAAA;EAnBH;EAqBH,KAAA,YAAiB,SAAA;EAbM;EAevB,YAAA;EAf0B;EAiB1B,cAAA;EAfe;;;;EAoBf,WAAA;EAXM;EAaN,IAAA;IAAS,UAAA;EAAA;EAEiD;EAA1D,SAAA,IAAa,KAAA,EAAO,WAAA,EAAa,GAAA,EAAK,QAAA,YAAoB,OAAA;AAAA;AAAA,UAG3C,gBAAA;EACf,YAAA;EACA,SAAA;AAAA;AAAA,UAGe,SAAA;EACf,GAAA;EACA,QAAA;IAAY,IAAA;IAAc,IAAA;EAAA;AAAA;AAAA,cAGf,UAAA"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isThemeHex } from "@bison-lab/tokens";
|
|
2
|
+
//#region src/theme/fields.ts
|
|
3
|
+
const THEME_COLOR_FIELD = "@bison-lab/payload-core/admin#ColorField";
|
|
4
|
+
const THEME_FONT_FIELD = "@bison-lab/payload-core/admin#FontField";
|
|
5
|
+
const THEME_CONTRAST_REPORT = "@bison-lab/payload-core/admin#ContrastReport";
|
|
6
|
+
function headingSelectValue(heading) {
|
|
7
|
+
return heading ?? "";
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/theme/map.ts
|
|
11
|
+
function pick(value, fallback) {
|
|
12
|
+
return value == null ? fallback : value;
|
|
13
|
+
}
|
|
14
|
+
function headingFromDoc(heading, seed) {
|
|
15
|
+
if (heading === void 0) return seed;
|
|
16
|
+
if (heading === null || heading === "") return null;
|
|
17
|
+
return heading;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Nested Theme document → flat `ThemeConfig`. Null or missing editor
|
|
21
|
+
* fields take the seed; `darkSelector` and `fontWeights` always come from
|
|
22
|
+
* the seed.
|
|
23
|
+
*/
|
|
24
|
+
function themeConfigFromDoc(doc, seed) {
|
|
25
|
+
const brand = doc?.brand;
|
|
26
|
+
return {
|
|
27
|
+
brandPrimary: pick(brand?.primary, seed.brandPrimary),
|
|
28
|
+
brandSecondary: pick(brand?.secondary, seed.brandSecondary),
|
|
29
|
+
brandAccent: pick(brand?.accent, seed.brandAccent),
|
|
30
|
+
brandHighlight: pick(brand?.highlight, seed.brandHighlight),
|
|
31
|
+
brandSuccess: pick(brand?.success, seed.brandSuccess),
|
|
32
|
+
greyScale: pick(doc?.greyScale, seed.greyScale),
|
|
33
|
+
radius: pick(doc?.radius, seed.radius),
|
|
34
|
+
shadow: pick(doc?.shadow, seed.shadow),
|
|
35
|
+
motion: pick(doc?.motion, seed.motion),
|
|
36
|
+
density: pick(doc?.density, seed.density),
|
|
37
|
+
defaultTheme: pick(doc?.defaultTheme, seed.defaultTheme),
|
|
38
|
+
fontBody: pick(doc?.fonts?.body, seed.fontBody),
|
|
39
|
+
fontHeading: headingFromDoc(doc?.fonts?.heading, seed.fontHeading),
|
|
40
|
+
darkSelector: seed.darkSelector,
|
|
41
|
+
fontWeights: seed.fontWeights
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** The document `seedTheme` writes so a fresh Global matches the seed config. */
|
|
45
|
+
function docFromConfig(config) {
|
|
46
|
+
return {
|
|
47
|
+
brand: {
|
|
48
|
+
primary: config.brandPrimary,
|
|
49
|
+
secondary: config.brandSecondary,
|
|
50
|
+
accent: config.brandAccent,
|
|
51
|
+
highlight: config.brandHighlight,
|
|
52
|
+
success: config.brandSuccess
|
|
53
|
+
},
|
|
54
|
+
greyScale: config.greyScale,
|
|
55
|
+
radius: config.radius,
|
|
56
|
+
shadow: config.shadow,
|
|
57
|
+
motion: config.motion,
|
|
58
|
+
density: config.density,
|
|
59
|
+
defaultTheme: config.defaultTheme,
|
|
60
|
+
fonts: {
|
|
61
|
+
body: config.fontBody,
|
|
62
|
+
heading: config.fontHeading
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function validateThemeHex(value) {
|
|
67
|
+
if (!isThemeHex(value)) return "Enter a six-digit hex colour like #1e3a5f";
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/theme/types.ts
|
|
72
|
+
const THEME_SLUG = "theme";
|
|
73
|
+
//#endregion
|
|
74
|
+
export { THEME_COLOR_FIELD as a, headingSelectValue as c, validateThemeHex as i, docFromConfig as n, THEME_CONTRAST_REPORT as o, themeConfigFromDoc as r, THEME_FONT_FIELD as s, THEME_SLUG as t };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=types-DWwL-JEr.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-DWwL-JEr.mjs","names":[],"sources":["../src/theme/fields.ts","../src/theme/map.ts","../src/theme/types.ts"],"sourcesContent":["import type { ThemeConfig } from \"@bison-lab/tokens\";\n\nimport type { EditableTheme } from \"./types\";\n\nexport const THEME_COLOR_FIELD = \"@bison-lab/payload-core/admin#ColorField\";\nexport const THEME_FONT_FIELD = \"@bison-lab/payload-core/admin#FontField\";\nexport const THEME_CONTRAST_REPORT = \"@bison-lab/payload-core/admin#ContrastReport\";\n\n/**\n * Document path for each editable `ThemeConfig` key. Locked with `satisfies`\n * so a new config field without a Theme field (or the reverse) fails at\n * compile time. `darkSelector` and `fontWeights` are seed-only.\n */\nexport const THEME_FIELD_PATHS = {\n brandPrimary: \"brand.primary\",\n brandSecondary: \"brand.secondary\",\n brandAccent: \"brand.accent\",\n brandHighlight: \"brand.highlight\",\n brandSuccess: \"brand.success\",\n greyScale: \"greyScale\",\n defaultTheme: \"defaultTheme\",\n radius: \"radius\",\n shadow: \"shadow\",\n motion: \"motion\",\n density: \"density\",\n fontBody: \"fonts.body\",\n fontHeading: \"fonts.heading\",\n} as const satisfies Record<keyof EditableTheme, string>;\n\nexport type ThemeFieldPath = (typeof THEME_FIELD_PATHS)[keyof typeof THEME_FIELD_PATHS];\n\n/** Empty select value for heading: same family as body. */\nexport const SAME_AS_BODY = \"\";\n\nexport function headingSelectValue(heading: ThemeConfig[\"fontHeading\"]): string {\n return heading ?? SAME_AS_BODY;\n}\n","import { isThemeHex, type ThemeConfig } from \"@bison-lab/tokens\";\n\nimport { SAME_AS_BODY } from \"./fields\";\nimport type { ThemeDoc } from \"./types\";\n\nfunction pick<T>(value: T | null | undefined, fallback: T): T {\n return value == null ? fallback : value;\n}\n\nfunction headingFromDoc(\n heading: string | null | undefined,\n seed: ThemeConfig[\"fontHeading\"],\n): string | null {\n if (heading === undefined) return seed;\n if (heading === null || heading === SAME_AS_BODY) return null;\n return heading;\n}\n\n/**\n * Nested Theme document → flat `ThemeConfig`. Null or missing editor\n * fields take the seed; `darkSelector` and `fontWeights` always come from\n * the seed.\n */\nexport function themeConfigFromDoc(doc: ThemeDoc | null | undefined, seed: ThemeConfig): ThemeConfig {\n const brand = doc?.brand;\n return {\n brandPrimary: pick(brand?.primary, seed.brandPrimary),\n brandSecondary: pick(brand?.secondary, seed.brandSecondary),\n brandAccent: pick(brand?.accent, seed.brandAccent),\n brandHighlight: pick(brand?.highlight, seed.brandHighlight),\n brandSuccess: pick(brand?.success, seed.brandSuccess),\n greyScale: pick(doc?.greyScale, seed.greyScale),\n radius: pick(doc?.radius, seed.radius),\n shadow: pick(doc?.shadow, seed.shadow),\n motion: pick(doc?.motion, seed.motion),\n density: pick(doc?.density, seed.density),\n defaultTheme: pick(doc?.defaultTheme, seed.defaultTheme),\n fontBody: pick(doc?.fonts?.body, seed.fontBody),\n fontHeading: headingFromDoc(doc?.fonts?.heading, seed.fontHeading),\n darkSelector: seed.darkSelector,\n fontWeights: seed.fontWeights,\n };\n}\n\n/** The document `seedTheme` writes so a fresh Global matches the seed config. */\nexport function docFromConfig(config: ThemeConfig): ThemeDoc {\n return {\n brand: {\n primary: config.brandPrimary,\n secondary: config.brandSecondary,\n accent: config.brandAccent,\n highlight: config.brandHighlight,\n success: config.brandSuccess,\n },\n greyScale: config.greyScale,\n radius: config.radius,\n shadow: config.shadow,\n motion: config.motion,\n density: config.density,\n defaultTheme: config.defaultTheme,\n fonts: {\n body: config.fontBody,\n heading: config.fontHeading,\n },\n };\n}\n\nexport function validateThemeHex(value: unknown): true | string {\n if (!isThemeHex(value)) return \"Enter a six-digit hex colour like #1e3a5f\";\n return true;\n}\n","import type { Access } from \"payload\";\nimport type { FontEntry } from \"@bison-lab/fonts\";\nimport type { ThemeConfig } from \"@bison-lab/tokens\";\n\n/**\n * The Theme document a site's generated types will describe. Optional and\n * nullable, no index signature: a generated Global is assignable to this,\n * never the reverse.\n */\nexport interface ThemeDoc {\n brand?: {\n primary?: string | null;\n secondary?: string | null;\n accent?: string | null;\n highlight?: string | null;\n success?: string | null;\n } | null;\n greyScale?: ThemeConfig[\"greyScale\"] | null;\n radius?: ThemeConfig[\"radius\"] | null;\n shadow?: ThemeConfig[\"shadow\"] | null;\n motion?: ThemeConfig[\"motion\"] | null;\n density?: ThemeConfig[\"density\"] | null;\n defaultTheme?: ThemeConfig[\"defaultTheme\"] | null;\n fonts?: {\n body?: string | null;\n heading?: string | null;\n } | null;\n logo?: number | string | ThemeUploadDoc | null;\n logoMark?: number | string | ThemeUploadDoc | null;\n _status?: \"draft\" | \"published\" | null;\n}\n\nexport interface ThemeUploadDoc {\n id?: number | string | null;\n url?: string | null;\n alt?: string | null;\n}\n\n/**\n * What an editor can change. `darkSelector` and `fontWeights` stay on the\n * seed — they mean nothing in the admin — and `themeConfigFromDoc` puts\n * them back.\n */\nexport type EditableTheme = Omit<ThemeConfig, \"darkSelector\" | \"fontWeights\">;\n\nexport interface CreateThemeOptions {\n /**\n * Required, no default. Predicates live in each site's `src/platform`\n * until BIS-43; pass `canManageBrand` as `update` (and typically\n * `isAuthenticated` as `read`). `readVersions` / draft visibility follow\n * `update`, so only a brand manager sees unpublished edits.\n */\n access: { read: Access; update: Access };\n /** The site's `bison.config.json`. Every field's `defaultValue`, and the fallback `getPublishedTheme` returns. */\n seed: ThemeConfig;\n /** Default: the whole catalogue. */\n fonts?: readonly FontEntry[];\n /** Where the site's `serveFont` route answers, for the picker's specimens. Default `\"/fonts\"`. */\n fontsBaseUrl?: string;\n /** Body-text target the contrast report judges against. Default `4.5`. */\n contrastTarget?: number;\n /**\n * Stored on `admin.custom` for BIS-61, which wires `admin.livePreview`.\n * This slice does not open a preview pane.\n */\n previewPath?: string;\n /** Adds `logo` and `logoMark` upload relations when given. */\n logo?: { collection: string };\n /** Fires from `afterChange` on a published save only, not an autosaved draft. */\n onPublish?: (theme: ThemeConfig, doc: ThemeDoc) => void | Promise<void>;\n}\n\nexport interface ThemeHeadOptions {\n fontsBaseUrl?: string;\n attribute?: \"class\" | \"data-theme\";\n}\n\nexport interface ThemeHead {\n css: string;\n preloads: { href: string; type: \"font/woff2\" }[];\n}\n\nexport const THEME_SLUG = \"theme\";\n"],"mappings":";;AAIA,MAAa,oBAAoB;AACjC,MAAa,mBAAmB;AAChC,MAAa,wBAAwB;AA4BrC,SAAgB,mBAAmB,SAA6C;AAC9E,QAAO,WAAA;;;;AC9BT,SAAS,KAAQ,OAA6B,UAAgB;AAC5D,QAAO,SAAS,OAAO,WAAW;;AAGpC,SAAS,eACP,SACA,MACe;AACf,KAAI,YAAY,KAAA,EAAW,QAAO;AAClC,KAAI,YAAY,QAAQ,YAAA,GAA0B,QAAO;AACzD,QAAO;;;;;;;AAQT,SAAgB,mBAAmB,KAAkC,MAAgC;CACnG,MAAM,QAAQ,KAAK;AACnB,QAAO;EACL,cAAc,KAAK,OAAO,SAAS,KAAK,aAAa;EACrD,gBAAgB,KAAK,OAAO,WAAW,KAAK,eAAe;EAC3D,aAAa,KAAK,OAAO,QAAQ,KAAK,YAAY;EAClD,gBAAgB,KAAK,OAAO,WAAW,KAAK,eAAe;EAC3D,cAAc,KAAK,OAAO,SAAS,KAAK,aAAa;EACrD,WAAW,KAAK,KAAK,WAAW,KAAK,UAAU;EAC/C,QAAQ,KAAK,KAAK,QAAQ,KAAK,OAAO;EACtC,QAAQ,KAAK,KAAK,QAAQ,KAAK,OAAO;EACtC,QAAQ,KAAK,KAAK,QAAQ,KAAK,OAAO;EACtC,SAAS,KAAK,KAAK,SAAS,KAAK,QAAQ;EACzC,cAAc,KAAK,KAAK,cAAc,KAAK,aAAa;EACxD,UAAU,KAAK,KAAK,OAAO,MAAM,KAAK,SAAS;EAC/C,aAAa,eAAe,KAAK,OAAO,SAAS,KAAK,YAAY;EAClE,cAAc,KAAK;EACnB,aAAa,KAAK;EACnB;;;AAIH,SAAgB,cAAc,QAA+B;AAC3D,QAAO;EACL,OAAO;GACL,SAAS,OAAO;GAChB,WAAW,OAAO;GAClB,QAAQ,OAAO;GACf,WAAW,OAAO;GAClB,SAAS,OAAO;GACjB;EACD,WAAW,OAAO;EAClB,QAAQ,OAAO;EACf,QAAQ,OAAO;EACf,QAAQ,OAAO;EACf,SAAS,OAAO;EAChB,cAAc,OAAO;EACrB,OAAO;GACL,MAAM,OAAO;GACb,SAAS,OAAO;GACjB;EACF;;AAGH,SAAgB,iBAAiB,OAA+B;AAC9D,KAAI,CAAC,WAAW,MAAM,CAAE,QAAO;AAC/B,QAAO;;;;ACaT,MAAa,aAAa"}
|