@bison-lab/payload-core 3.7.0 → 3.9.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 +57 -22
- package/dist/admin.d.mts +62 -6
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +1795 -172
- package/dist/admin.mjs.map +1 -1
- package/dist/identity-D1X0Om4Z.mjs +198 -0
- package/dist/identity-D1X0Om4Z.mjs.map +1 -0
- package/dist/index.d.mts +82 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +520 -66
- package/dist/index.mjs.map +1 -1
- package/dist/library-BGq4wGif.d.mts +189 -0
- package/dist/library-BGq4wGif.d.mts.map +1 -0
- package/dist/map-BUDkX8g1.mjs +111 -0
- package/dist/map-BUDkX8g1.mjs.map +1 -0
- package/dist/react.d.mts +32 -0
- package/dist/react.d.mts.map +1 -0
- package/dist/react.mjs +77 -0
- package/dist/react.mjs.map +1 -0
- package/dist/theme.d.mts +12 -7
- package/dist/theme.d.mts.map +1 -1
- package/dist/theme.mjs +19 -6
- package/dist/theme.mjs.map +1 -1
- package/package.json +17 -3
- package/dist/types-B3FC9sdF.d.mts +0 -89
- package/dist/types-B3FC9sdF.d.mts.map +0 -1
- package/dist/types-DWwL-JEr.mjs +0 -76
- package/dist/types-DWwL-JEr.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +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
|
|
2
|
+
import { S as headingSelectValue, _ as THEME_LIBRARY_FIELD, a as SYSTEM_COLOR_KEYS, b as THEME_SAVE_BUTTON, c as themeConfigFromDoc, d as THEME_APPEARANCE_FIELD, f as THEME_COLOR_FIELD, g as THEME_GREY_SCALE_FIELD, h as THEME_FONT_FIELD, i as themeColorKeys, l as validateSourceIncluded, m as THEME_CONTRAST_REPORT, n as deleteLibraryColor, o as THEME_SLUG, p as THEME_COLOR_SCALE_FIELD, r as rewriteColorToken, s as docFromConfig, t as resolveThemeIdentity, u as validateThemeHex, v as THEME_PAIRING_FIELD, x as THEME_SECTION_HEADING, y as THEME_PUBLISH_FIELD } from "./identity-D1X0Om4Z.mjs";
|
|
3
3
|
import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
|
|
4
4
|
import { catalog } from "@bison-lab/fonts";
|
|
5
|
-
import { presetHints } from "@bison-lab/tokens";
|
|
5
|
+
import { SHADE_STEPS, presetHints } from "@bison-lab/tokens";
|
|
6
6
|
//#region src/seo/fields.ts
|
|
7
7
|
/**
|
|
8
8
|
* The index switch, last in the SEO tab. Off by default: a page is public
|
|
@@ -79,8 +79,129 @@ function firstImageIn(blocks, field = "image") {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
//#endregion
|
|
82
|
+
//#region src/theme/appearance-labels.ts
|
|
83
|
+
/**
|
|
84
|
+
* Named choices the Appearance child shows. Values stay the token presets;
|
|
85
|
+
* labels match the mock workflow (Soft → pill, Gentle → minimal, Balanced
|
|
86
|
+
* → default, Follow device → system). No rem, ms, or CSS variable tables.
|
|
87
|
+
*/
|
|
88
|
+
const APPEARANCE_CHOICES = {
|
|
89
|
+
defaultTheme: {
|
|
90
|
+
light: {
|
|
91
|
+
label: "Light",
|
|
92
|
+
hint: "Always opens in light mode"
|
|
93
|
+
},
|
|
94
|
+
dark: {
|
|
95
|
+
label: "Dark",
|
|
96
|
+
hint: "Always opens in dark mode"
|
|
97
|
+
},
|
|
98
|
+
system: {
|
|
99
|
+
label: "Follow device",
|
|
100
|
+
hint: "Matches the visitor’s system setting"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
radius: {
|
|
104
|
+
sharp: {
|
|
105
|
+
label: "Sharp",
|
|
106
|
+
hint: "Crisp and structured"
|
|
107
|
+
},
|
|
108
|
+
subtle: {
|
|
109
|
+
label: "Subtle",
|
|
110
|
+
hint: "Clean with a little softness"
|
|
111
|
+
},
|
|
112
|
+
rounded: {
|
|
113
|
+
label: "Rounded",
|
|
114
|
+
hint: "Friendly and contemporary"
|
|
115
|
+
},
|
|
116
|
+
pill: {
|
|
117
|
+
label: "Soft",
|
|
118
|
+
hint: "Very rounded and expressive"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
shadow: {
|
|
122
|
+
flat: {
|
|
123
|
+
label: "Flat",
|
|
124
|
+
hint: "Minimal visual depth"
|
|
125
|
+
},
|
|
126
|
+
subtle: {
|
|
127
|
+
label: "Subtle",
|
|
128
|
+
hint: "Light separation between surfaces"
|
|
129
|
+
},
|
|
130
|
+
elevated: {
|
|
131
|
+
label: "Elevated",
|
|
132
|
+
hint: "More noticeable layering and depth"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
motion: {
|
|
136
|
+
snappy: {
|
|
137
|
+
label: "Quick",
|
|
138
|
+
hint: "Fast and responsive"
|
|
139
|
+
},
|
|
140
|
+
smooth: {
|
|
141
|
+
label: "Smooth",
|
|
142
|
+
hint: "Balanced and natural"
|
|
143
|
+
},
|
|
144
|
+
minimal: {
|
|
145
|
+
label: "Gentle",
|
|
146
|
+
hint: "Slower and more relaxed"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
density: {
|
|
150
|
+
compact: {
|
|
151
|
+
label: "Compact",
|
|
152
|
+
hint: "Fits more content on screen"
|
|
153
|
+
},
|
|
154
|
+
default: {
|
|
155
|
+
label: "Balanced",
|
|
156
|
+
hint: "Comfortable for most sites"
|
|
157
|
+
},
|
|
158
|
+
spacious: {
|
|
159
|
+
label: "Spacious",
|
|
160
|
+
hint: "More breathing room and larger controls"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/theme/publish.ts
|
|
166
|
+
const THEME_CHILDREN = [
|
|
167
|
+
"colors",
|
|
168
|
+
"typography",
|
|
169
|
+
"appearance",
|
|
170
|
+
"identity"
|
|
171
|
+
];
|
|
172
|
+
/**
|
|
173
|
+
* Publishing a child writes that slice only. Other responsibilities stay
|
|
174
|
+
* as they already are on the stored document. Unsaved form state on a
|
|
175
|
+
* different child never goes live. A first save (no stored row) still
|
|
176
|
+
* writes only the named child; `getPublishedTheme` fills the rest from
|
|
177
|
+
* the seed.
|
|
178
|
+
*/
|
|
179
|
+
function publishThemeChild(stored, incoming, child) {
|
|
180
|
+
if (child === "colors") return {
|
|
181
|
+
...stored,
|
|
182
|
+
colors: incoming.colors
|
|
183
|
+
};
|
|
184
|
+
if (child === "typography") return {
|
|
185
|
+
...stored,
|
|
186
|
+
typography: incoming.typography
|
|
187
|
+
};
|
|
188
|
+
if (child === "appearance") return {
|
|
189
|
+
...stored,
|
|
190
|
+
appearance: incoming.appearance
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
...stored,
|
|
194
|
+
logo: incoming.logo,
|
|
195
|
+
favicon: incoming.favicon,
|
|
196
|
+
logoMark: incoming.logoMark
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function isThemeChild(value) {
|
|
200
|
+
return typeof value === "string" && THEME_CHILDREN.includes(value);
|
|
201
|
+
}
|
|
202
|
+
//#endregion
|
|
82
203
|
//#region src/theme/global.ts
|
|
83
|
-
function requireAccess(options) {
|
|
204
|
+
function requireAccess$1(options) {
|
|
84
205
|
const read = options.access?.read;
|
|
85
206
|
const update = options.access?.update;
|
|
86
207
|
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.");
|
|
@@ -89,28 +210,103 @@ function requireAccess(options) {
|
|
|
89
210
|
update
|
|
90
211
|
};
|
|
91
212
|
}
|
|
92
|
-
function
|
|
213
|
+
function requireDestructive(options) {
|
|
214
|
+
const hex = options.destructive ?? options.seed.brandDestructive;
|
|
215
|
+
if (!hex) throw new Error("createTheme requires a destructive hex: pass options.destructive or seed.brandDestructive. There is no package default.");
|
|
216
|
+
return hex;
|
|
217
|
+
}
|
|
218
|
+
function namedSelect(name, label, choices, defaultValue, field) {
|
|
93
219
|
return {
|
|
94
220
|
name,
|
|
95
221
|
type: "select",
|
|
96
222
|
label,
|
|
97
223
|
required: true,
|
|
98
224
|
defaultValue,
|
|
99
|
-
options: Object.entries(
|
|
225
|
+
options: Object.entries(choices).map(([value, { label: optionLabel, hint }]) => ({
|
|
100
226
|
label: `${optionLabel} — ${hint}`,
|
|
101
227
|
value
|
|
102
|
-
}))
|
|
228
|
+
})),
|
|
229
|
+
...field ? { admin: { components: { Field: field } } } : {}
|
|
103
230
|
};
|
|
104
231
|
}
|
|
105
|
-
|
|
232
|
+
const STEP_OPTIONS = SHADE_STEPS.map((step) => ({
|
|
233
|
+
label: String(step),
|
|
234
|
+
value: String(step)
|
|
235
|
+
}));
|
|
236
|
+
function colorScaleFields(hexDefault) {
|
|
237
|
+
const hidden = { hidden: true };
|
|
238
|
+
return [
|
|
239
|
+
{
|
|
240
|
+
name: "hex",
|
|
241
|
+
type: "text",
|
|
242
|
+
label: "Hex",
|
|
243
|
+
required: true,
|
|
244
|
+
...hexDefault ? { defaultValue: hexDefault } : {},
|
|
245
|
+
validate: validateThemeHex,
|
|
246
|
+
admin: hidden
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "sourceStep",
|
|
250
|
+
type: "select",
|
|
251
|
+
label: "Source step",
|
|
252
|
+
required: true,
|
|
253
|
+
defaultValue: "500",
|
|
254
|
+
options: STEP_OPTIONS,
|
|
255
|
+
admin: hidden
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "scale",
|
|
259
|
+
type: "json",
|
|
260
|
+
label: "Scale",
|
|
261
|
+
admin: hidden
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "stale",
|
|
265
|
+
type: "checkbox",
|
|
266
|
+
label: "Stale",
|
|
267
|
+
defaultValue: false,
|
|
268
|
+
admin: hidden
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: "include",
|
|
272
|
+
type: "select",
|
|
273
|
+
label: "Include",
|
|
274
|
+
required: true,
|
|
275
|
+
defaultValue: "all",
|
|
276
|
+
options: [
|
|
277
|
+
{
|
|
278
|
+
label: "Include all",
|
|
279
|
+
value: "all"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
label: "Source only",
|
|
283
|
+
value: "source"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
label: "Custom",
|
|
287
|
+
value: "custom"
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
admin: hidden
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: "includedSteps",
|
|
294
|
+
type: "select",
|
|
295
|
+
label: "Included steps",
|
|
296
|
+
hasMany: true,
|
|
297
|
+
options: STEP_OPTIONS,
|
|
298
|
+
validate: validateSourceIncluded,
|
|
299
|
+
admin: hidden
|
|
300
|
+
}
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
function systemColorGroup(name, label, hexDefault) {
|
|
106
304
|
return {
|
|
107
305
|
name,
|
|
108
|
-
type: "
|
|
306
|
+
type: "group",
|
|
109
307
|
label,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
validate: validateThemeHex,
|
|
113
|
-
admin: { components: { Field: THEME_COLOR_FIELD } }
|
|
308
|
+
fields: colorScaleFields(hexDefault),
|
|
309
|
+
admin: { components: { Field: THEME_COLOR_SCALE_FIELD } }
|
|
114
310
|
};
|
|
115
311
|
}
|
|
116
312
|
function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
|
|
@@ -138,85 +334,317 @@ function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
|
|
|
138
334
|
}
|
|
139
335
|
};
|
|
140
336
|
}
|
|
337
|
+
function publishField(child) {
|
|
338
|
+
return {
|
|
339
|
+
name: `publish${child[0].toUpperCase()}${child.slice(1)}`,
|
|
340
|
+
type: "ui",
|
|
341
|
+
admin: {
|
|
342
|
+
components: { Field: THEME_PUBLISH_FIELD },
|
|
343
|
+
custom: { child }
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
141
347
|
/**
|
|
142
|
-
* Settings → Theme:
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* the published version through `getPublishedTheme`.
|
|
146
|
-
*
|
|
147
|
-
* `previewPath` is stored for BIS-61, which wires the live preview pane.
|
|
348
|
+
* Settings → Theme children: Colors, Typography, Appearance, Identity.
|
|
349
|
+
* Each child publishes itself. No draft mode: a save is live.
|
|
350
|
+
* `getPublishedTheme` reads that row. There is no Theme preview pane.
|
|
148
351
|
*/
|
|
149
352
|
function createTheme(options) {
|
|
150
|
-
const access = requireAccess(options);
|
|
151
|
-
const
|
|
152
|
-
const
|
|
353
|
+
const access = requireAccess$1(options);
|
|
354
|
+
const destructive = requireDestructive(options);
|
|
355
|
+
const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 7, logo, identity, onPublish } = options;
|
|
356
|
+
const tabs = [
|
|
153
357
|
{
|
|
154
|
-
|
|
155
|
-
type: "group",
|
|
156
|
-
label: "Brand colours",
|
|
358
|
+
label: "Colors",
|
|
157
359
|
fields: [
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
360
|
+
publishField("colors"),
|
|
361
|
+
{
|
|
362
|
+
name: "colors",
|
|
363
|
+
type: "group",
|
|
364
|
+
label: "Colors",
|
|
365
|
+
fields: [
|
|
366
|
+
{
|
|
367
|
+
name: "brand",
|
|
368
|
+
type: "group",
|
|
369
|
+
label: "Colors",
|
|
370
|
+
fields: [
|
|
371
|
+
{
|
|
372
|
+
name: "themeColorsHeading",
|
|
373
|
+
type: "ui",
|
|
374
|
+
admin: {
|
|
375
|
+
components: { Field: THEME_SECTION_HEADING },
|
|
376
|
+
custom: {
|
|
377
|
+
title: "Theme colors",
|
|
378
|
+
hint: "House semantic fills page editors can pick. Custom colors sit with these."
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
systemColorGroup("primary", "Primary", seed.brandPrimary),
|
|
383
|
+
systemColorGroup("secondary", "Secondary", seed.brandSecondary),
|
|
384
|
+
systemColorGroup("accent", "Accent", seed.brandAccent),
|
|
385
|
+
systemColorGroup("highlight", "Highlight", seed.brandHighlight),
|
|
386
|
+
{
|
|
387
|
+
name: "statusColorsHeading",
|
|
388
|
+
type: "ui",
|
|
389
|
+
admin: {
|
|
390
|
+
components: { Field: THEME_SECTION_HEADING },
|
|
391
|
+
custom: {
|
|
392
|
+
title: "Success and Destructive",
|
|
393
|
+
hint: "Theme chrome only. These are not offered as fills when someone is writing a page."
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
systemColorGroup("success", "Success", seed.brandSuccess),
|
|
398
|
+
systemColorGroup("destructive", "Destructive", destructive)
|
|
399
|
+
]
|
|
400
|
+
},
|
|
401
|
+
namedSelect("greyScale", "Gray family", presetHints.greyScale, seed.greyScale, THEME_GREY_SCALE_FIELD),
|
|
402
|
+
{
|
|
403
|
+
name: "library",
|
|
404
|
+
type: "array",
|
|
405
|
+
label: "Custom colors",
|
|
406
|
+
labels: {
|
|
407
|
+
singular: "Color",
|
|
408
|
+
plural: "Colors"
|
|
409
|
+
},
|
|
410
|
+
fields: [
|
|
411
|
+
{
|
|
412
|
+
name: "key",
|
|
413
|
+
type: "text",
|
|
414
|
+
label: "Key",
|
|
415
|
+
required: true,
|
|
416
|
+
admin: { hidden: true }
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "label",
|
|
420
|
+
type: "text",
|
|
421
|
+
label: "Name",
|
|
422
|
+
required: true,
|
|
423
|
+
admin: { hidden: true }
|
|
424
|
+
},
|
|
425
|
+
...colorScaleFields()
|
|
426
|
+
],
|
|
427
|
+
admin: { components: { Field: THEME_LIBRARY_FIELD } }
|
|
428
|
+
}
|
|
429
|
+
]
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "contrast",
|
|
433
|
+
type: "ui",
|
|
434
|
+
admin: {
|
|
435
|
+
components: { Field: THEME_CONTRAST_REPORT },
|
|
436
|
+
custom: { target: contrastTarget }
|
|
437
|
+
}
|
|
438
|
+
}
|
|
163
439
|
]
|
|
164
440
|
},
|
|
165
441
|
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
442
|
+
label: "Typography",
|
|
443
|
+
fields: [{
|
|
444
|
+
name: "typography",
|
|
445
|
+
type: "group",
|
|
446
|
+
label: "Typography",
|
|
447
|
+
fields: [
|
|
448
|
+
fontField("heading", "Heading font", headingSelectValue(seed.fontHeading), fonts, fontsBaseUrl, true),
|
|
449
|
+
fontField("body", "Body font", seed.fontBody, fonts, fontsBaseUrl, false),
|
|
450
|
+
{
|
|
451
|
+
name: "pairing",
|
|
452
|
+
type: "ui",
|
|
453
|
+
admin: {
|
|
454
|
+
components: { Field: THEME_PAIRING_FIELD },
|
|
455
|
+
custom: { fontsBaseUrl }
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
]
|
|
459
|
+
}, publishField("typography")]
|
|
172
460
|
},
|
|
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
461
|
{
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
462
|
+
label: "Appearance",
|
|
463
|
+
fields: [{
|
|
464
|
+
name: "appearance",
|
|
465
|
+
type: "group",
|
|
466
|
+
label: "Appearance",
|
|
467
|
+
fields: [
|
|
468
|
+
namedSelect("defaultTheme", "Default theme", APPEARANCE_CHOICES.defaultTheme, seed.defaultTheme),
|
|
469
|
+
namedSelect("radius", "Corner style", APPEARANCE_CHOICES.radius, seed.radius),
|
|
470
|
+
namedSelect("shadow", "Depth", APPEARANCE_CHOICES.shadow, seed.shadow),
|
|
471
|
+
namedSelect("motion", "Motion", APPEARANCE_CHOICES.motion, seed.motion),
|
|
472
|
+
namedSelect("density", "Spacing", APPEARANCE_CHOICES.density, seed.density)
|
|
473
|
+
],
|
|
474
|
+
admin: { components: { Field: THEME_APPEARANCE_FIELD } }
|
|
475
|
+
}, publishField("appearance")]
|
|
184
476
|
}
|
|
185
477
|
];
|
|
186
|
-
if (logo)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
478
|
+
if (logo) tabs.push({
|
|
479
|
+
label: "Identity",
|
|
480
|
+
fields: [
|
|
481
|
+
{
|
|
482
|
+
name: "logo",
|
|
483
|
+
type: "upload",
|
|
484
|
+
relationTo: logo.collection,
|
|
485
|
+
label: "Logo",
|
|
486
|
+
admin: { description: "The full wordmark used in the header and footer." }
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: "favicon",
|
|
490
|
+
type: "upload",
|
|
491
|
+
relationTo: logo.collection,
|
|
492
|
+
label: "Favicon",
|
|
493
|
+
admin: { description: "The small icon in the browser tab. PNG or SVG." }
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: "logoMark",
|
|
497
|
+
type: "upload",
|
|
498
|
+
relationTo: logo.collection,
|
|
499
|
+
label: "Mobile menu logo",
|
|
500
|
+
admin: { description: "Optional. Used when the full logo is too wide for the mobile menu — typically the mark." }
|
|
501
|
+
},
|
|
502
|
+
publishField("identity")
|
|
503
|
+
]
|
|
196
504
|
});
|
|
505
|
+
const fields = [{
|
|
506
|
+
type: "tabs",
|
|
507
|
+
tabs
|
|
508
|
+
}, {
|
|
509
|
+
name: "publishChild",
|
|
510
|
+
type: "text",
|
|
511
|
+
admin: { hidden: true }
|
|
512
|
+
}];
|
|
197
513
|
return {
|
|
198
514
|
slug: THEME_SLUG,
|
|
199
515
|
label: "Theme",
|
|
200
516
|
admin: {
|
|
201
517
|
group: "Settings",
|
|
202
518
|
custom: {
|
|
203
|
-
previewPath,
|
|
204
519
|
contrastTarget,
|
|
205
|
-
fontsBaseUrl
|
|
206
|
-
|
|
520
|
+
fontsBaseUrl,
|
|
521
|
+
identityFallback: identity?.fallback
|
|
522
|
+
},
|
|
523
|
+
components: { elements: { SaveButton: THEME_SAVE_BUTTON } }
|
|
524
|
+
},
|
|
525
|
+
access: {
|
|
526
|
+
read: access.read,
|
|
527
|
+
update: access.update
|
|
528
|
+
},
|
|
529
|
+
fields,
|
|
530
|
+
hooks: {
|
|
531
|
+
beforeChange: [({ data, originalDoc }) => {
|
|
532
|
+
const { publishChild, ...rest } = data;
|
|
533
|
+
if (isThemeChild(publishChild)) return publishThemeChild(originalDoc ?? {}, rest, publishChild);
|
|
534
|
+
return rest;
|
|
535
|
+
}],
|
|
536
|
+
afterChange: [async ({ doc }) => {
|
|
537
|
+
if (!onPublish) return;
|
|
538
|
+
const themeDoc = doc;
|
|
539
|
+
await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);
|
|
540
|
+
}]
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
//#endregion
|
|
545
|
+
//#region src/brand-assets/sanitize.ts
|
|
546
|
+
const DANGEROUS_TAGS = [
|
|
547
|
+
"script",
|
|
548
|
+
"foreignObject",
|
|
549
|
+
"iframe",
|
|
550
|
+
"embed",
|
|
551
|
+
"object"
|
|
552
|
+
];
|
|
553
|
+
/**
|
|
554
|
+
* Strips the SVG features a lockup does not need and an attacker would
|
|
555
|
+
* use: script, foreignObject, iframe/embed/object, event handlers,
|
|
556
|
+
* javascript: / data:text/html URLs, and xml-stylesheet. Internal `#`
|
|
557
|
+
* refs and ordinary drawing elements stay.
|
|
558
|
+
*/
|
|
559
|
+
function sanitizeSvg(source) {
|
|
560
|
+
if (!/<svg[\s>]/i.test(source)) throw new Error("Not an SVG");
|
|
561
|
+
let out = source.replace(/<\?xml-stylesheet[\s\S]*?\?>/gi, "");
|
|
562
|
+
for (const tag of DANGEROUS_TAGS) {
|
|
563
|
+
out = out.replace(new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}\\s*>`, "gi"), "");
|
|
564
|
+
out = out.replace(new RegExp(`<${tag}\\b[^>]*\\/>`, "gi"), "");
|
|
565
|
+
}
|
|
566
|
+
out = out.replace(/\s+on[a-z][a-z0-9-]*\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
|
|
567
|
+
out = out.replace(/((?:href|src|xlink:href)\s*=\s*)(["'])\s*(?:javascript:|data:text\/html)[^"']*\2/gi, "$1$2$2");
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
function isSvgUpload(file) {
|
|
571
|
+
if (file.mimetype === "image/svg+xml") return true;
|
|
572
|
+
if (typeof file.name === "string" && file.name.toLowerCase().endsWith(".svg")) return true;
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/brand-assets/collection.ts
|
|
577
|
+
const BRAND_ASSETS_SLUG = "brand-assets";
|
|
578
|
+
/** SVG preferred; PNG allowed. ICO is for the favicon slot. */
|
|
579
|
+
const BRAND_ASSETS_MIME_TYPES = [
|
|
580
|
+
"image/svg+xml",
|
|
581
|
+
"image/png",
|
|
582
|
+
"image/x-icon",
|
|
583
|
+
"image/vnd.microsoft.icon"
|
|
584
|
+
];
|
|
585
|
+
function requireAccess(options) {
|
|
586
|
+
const read = options.access?.read;
|
|
587
|
+
const update = options.access?.update;
|
|
588
|
+
if (!read || !update) throw new Error("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.");
|
|
589
|
+
return {
|
|
590
|
+
read,
|
|
591
|
+
update
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Locked cupboard for lockup and mark. Not ordinary Media — only a brand
|
|
596
|
+
* manager writes it. Theme's `logo` / `logoMark` point here when the site
|
|
597
|
+
* passes this slug as `logo.collection`.
|
|
598
|
+
*/
|
|
599
|
+
function createBrandAssets(options) {
|
|
600
|
+
const access = requireAccess(options);
|
|
601
|
+
return {
|
|
602
|
+
slug: options.slug ?? "brand-assets",
|
|
603
|
+
labels: {
|
|
604
|
+
singular: "Brand asset",
|
|
605
|
+
plural: "Brand assets"
|
|
606
|
+
},
|
|
607
|
+
admin: {
|
|
608
|
+
group: "Settings",
|
|
609
|
+
useAsTitle: "label",
|
|
610
|
+
description: "Logo, favicon, and mobile-menu mark. SVG preferred; PNG and ICO allowed."
|
|
611
|
+
},
|
|
612
|
+
upload: {
|
|
613
|
+
mimeTypes: [...BRAND_ASSETS_MIME_TYPES],
|
|
614
|
+
crop: false,
|
|
615
|
+
focalPoint: false
|
|
207
616
|
},
|
|
208
|
-
versions:
|
|
617
|
+
versions: true,
|
|
209
618
|
access: {
|
|
210
619
|
read: access.read,
|
|
620
|
+
create: access.update,
|
|
211
621
|
update: access.update,
|
|
622
|
+
delete: access.update,
|
|
212
623
|
readVersions: access.update
|
|
213
624
|
},
|
|
214
|
-
fields
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
625
|
+
fields: [
|
|
626
|
+
{
|
|
627
|
+
name: "label",
|
|
628
|
+
type: "text",
|
|
629
|
+
label: "Label"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
name: "notes",
|
|
633
|
+
type: "textarea",
|
|
634
|
+
label: "Usage notes"
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: "alt",
|
|
638
|
+
type: "text",
|
|
639
|
+
label: "Alt text",
|
|
640
|
+
required: true
|
|
641
|
+
}
|
|
642
|
+
],
|
|
643
|
+
hooks: { beforeOperation: [({ req, operation }) => {
|
|
644
|
+
if (operation !== "create" && operation !== "update") return;
|
|
645
|
+
const file = req.file;
|
|
646
|
+
if (!file || !isSvgUpload(file)) return;
|
|
647
|
+
file.data = Buffer.from(sanitizeSvg(file.data.toString("utf8")), "utf8");
|
|
220
648
|
}] }
|
|
221
649
|
};
|
|
222
650
|
}
|
|
@@ -234,6 +662,32 @@ async function seedTheme(payload, seed) {
|
|
|
234
662
|
});
|
|
235
663
|
}
|
|
236
664
|
//#endregion
|
|
237
|
-
|
|
665
|
+
//#region src/theme/preview.ts
|
|
666
|
+
/**
|
|
667
|
+
* Device sizes the Theme (and, later, Pages) live-preview toolbar offers.
|
|
668
|
+
* One list so the panes match.
|
|
669
|
+
*/
|
|
670
|
+
const THEME_PREVIEW_BREAKPOINTS = [
|
|
671
|
+
{
|
|
672
|
+
label: "Mobile",
|
|
673
|
+
name: "mobile",
|
|
674
|
+
width: 375,
|
|
675
|
+
height: 667
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
label: "Tablet",
|
|
679
|
+
name: "tablet",
|
|
680
|
+
width: 768,
|
|
681
|
+
height: 1024
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
label: "Desktop",
|
|
685
|
+
name: "desktop",
|
|
686
|
+
width: 1440,
|
|
687
|
+
height: 900
|
|
688
|
+
}
|
|
689
|
+
];
|
|
690
|
+
//#endregion
|
|
691
|
+
export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, DESCRIPTION_LENGTH, SHARE_IMAGE_SIZE, SYSTEM_COLOR_KEYS, THEME_APPEARANCE_FIELD, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, createBrandAssets, createTheme, deleteLibraryColor, documentTitle, firstImageIn, noIndexField, publishThemeChild, resolveThemeIdentity, rewriteColorToken, sanitizeSvg, seedTheme, seoPlugin, themeColorKeys, titleTemplate, truncateAtWord };
|
|
238
692
|
|
|
239
693
|
//# sourceMappingURL=index.mjs.map
|