@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.
- package/README.md +49 -49
- package/dist/admin.d.mts +71 -6
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +1877 -178
- package/dist/admin.mjs.map +1 -1
- package/dist/identity-Bg-G05mb.mjs +199 -0
- package/dist/identity-Bg-G05mb.mjs.map +1 -0
- package/dist/index.d.mts +62 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +507 -90
- 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.mjs +3 -58
- package/dist/react.mjs.map +1 -1
- 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 +5 -4
- package/dist/types-DWwL-JEr.mjs +0 -76
- package/dist/types-DWwL-JEr.mjs.map +0 -1
- package/dist/types-pFpmDeSA.d.mts +0 -91
- package/dist/types-pFpmDeSA.d.mts.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 { C as headingSelectValue, S as THEME_SECTION_HEADING, _ as THEME_IDENTITY_FALLBACK, a as SYSTEM_COLOR_KEYS, b as THEME_PUBLISH_FIELD, c as themeConfigFromDoc, d as THEME_APPEARANCE_FIELD, f as THEME_COLOR_FIELD, g as THEME_GREY_SCALE_FIELD, h as THEME_FONT_FIELD, i as themeColorKeys, l as validateSourceIncluded, m as THEME_CONTRAST_REPORT, n as deleteLibraryColor, o as THEME_SLUG, p as THEME_COLOR_SCALE_FIELD, r as rewriteColorToken, s as docFromConfig, t as resolveThemeIdentity, u as validateThemeHex, v as THEME_LIBRARY_FIELD, x as THEME_SAVE_BUTTON, y as THEME_PAIRING_FIELD } from "./identity-Bg-G05mb.mjs";
|
|
3
3
|
import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
|
|
4
4
|
import { catalog } from "@bison-lab/fonts";
|
|
5
|
-
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,34 +79,129 @@ function firstImageIn(blocks, field = "image") {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
//#endregion
|
|
82
|
-
//#region src/theme/
|
|
82
|
+
//#region src/theme/appearance-labels.ts
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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.
|
|
86
87
|
*/
|
|
87
|
-
const
|
|
88
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|
|
93
102
|
},
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
}
|
|
99
120
|
},
|
|
100
|
-
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|
|
105
162
|
}
|
|
163
|
+
};
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/theme/publish.ts
|
|
166
|
+
const THEME_CHILDREN = [
|
|
167
|
+
"colors",
|
|
168
|
+
"typography",
|
|
169
|
+
"appearance",
|
|
170
|
+
"identity"
|
|
106
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
|
+
}
|
|
107
202
|
//#endregion
|
|
108
203
|
//#region src/theme/global.ts
|
|
109
|
-
function requireAccess(options) {
|
|
204
|
+
function requireAccess$1(options) {
|
|
110
205
|
const read = options.access?.read;
|
|
111
206
|
const update = options.access?.update;
|
|
112
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.");
|
|
@@ -115,28 +210,103 @@ function requireAccess(options) {
|
|
|
115
210
|
update
|
|
116
211
|
};
|
|
117
212
|
}
|
|
118
|
-
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) {
|
|
119
219
|
return {
|
|
120
220
|
name,
|
|
121
221
|
type: "select",
|
|
122
222
|
label,
|
|
123
223
|
required: true,
|
|
124
224
|
defaultValue,
|
|
125
|
-
options: Object.entries(
|
|
225
|
+
options: Object.entries(choices).map(([value, { label: optionLabel, hint }]) => ({
|
|
126
226
|
label: `${optionLabel} — ${hint}`,
|
|
127
227
|
value
|
|
128
|
-
}))
|
|
228
|
+
})),
|
|
229
|
+
...field ? { admin: { components: { Field: field } } } : {}
|
|
129
230
|
};
|
|
130
231
|
}
|
|
131
|
-
|
|
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) {
|
|
132
304
|
return {
|
|
133
305
|
name,
|
|
134
|
-
type: "
|
|
306
|
+
type: "group",
|
|
135
307
|
label,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
validate: validateThemeHex,
|
|
139
|
-
admin: { components: { Field: THEME_COLOR_FIELD } }
|
|
308
|
+
fields: colorScaleFields(hexDefault),
|
|
309
|
+
admin: { components: { Field: THEME_COLOR_SCALE_FIELD } }
|
|
140
310
|
};
|
|
141
311
|
}
|
|
142
312
|
function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
|
|
@@ -165,88 +335,309 @@ function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
|
|
|
165
335
|
};
|
|
166
336
|
}
|
|
167
337
|
/**
|
|
168
|
-
* Settings → Theme
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* `previewPath` wires `admin.livePreview` when the site has a route.
|
|
338
|
+
* Settings → Theme. Field tabs: Colors, Typography, Appearance, Identity.
|
|
339
|
+
* Publish in the Save slot writes the open tab only. No draft mode: a
|
|
340
|
+
* save is live. `getPublishedTheme` reads that row. There is no Theme
|
|
341
|
+
* preview pane.
|
|
174
342
|
*/
|
|
175
343
|
function createTheme(options) {
|
|
176
|
-
const access = requireAccess(options);
|
|
177
|
-
const
|
|
178
|
-
const
|
|
344
|
+
const access = requireAccess$1(options);
|
|
345
|
+
const destructive = requireDestructive(options);
|
|
346
|
+
const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 7, logo, identity, onPublish } = options;
|
|
347
|
+
const tabs = [
|
|
179
348
|
{
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
349
|
+
label: "Colors",
|
|
350
|
+
fields: [{
|
|
351
|
+
name: "colors",
|
|
352
|
+
type: "group",
|
|
353
|
+
label: false,
|
|
354
|
+
fields: [
|
|
355
|
+
{
|
|
356
|
+
name: "brand",
|
|
357
|
+
type: "group",
|
|
358
|
+
label: false,
|
|
359
|
+
fields: [
|
|
360
|
+
{
|
|
361
|
+
name: "themeColorsHeading",
|
|
362
|
+
type: "ui",
|
|
363
|
+
admin: {
|
|
364
|
+
components: { Field: THEME_SECTION_HEADING },
|
|
365
|
+
custom: {
|
|
366
|
+
title: "Theme colors",
|
|
367
|
+
hint: "House semantic fills page editors can pick. Custom colors sit with these."
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
systemColorGroup("primary", "Primary", seed.brandPrimary),
|
|
372
|
+
systemColorGroup("secondary", "Secondary", seed.brandSecondary),
|
|
373
|
+
systemColorGroup("accent", "Accent", seed.brandAccent),
|
|
374
|
+
systemColorGroup("highlight", "Highlight", seed.brandHighlight),
|
|
375
|
+
{
|
|
376
|
+
name: "statusColorsHeading",
|
|
377
|
+
type: "ui",
|
|
378
|
+
admin: {
|
|
379
|
+
components: { Field: THEME_SECTION_HEADING },
|
|
380
|
+
custom: {
|
|
381
|
+
title: "Success and Destructive",
|
|
382
|
+
hint: "Theme chrome only. These are not offered as fills when someone is writing a page."
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
systemColorGroup("success", "Success", seed.brandSuccess),
|
|
387
|
+
systemColorGroup("destructive", "Destructive", destructive)
|
|
388
|
+
]
|
|
389
|
+
},
|
|
390
|
+
namedSelect("greyScale", "Gray family", presetHints.greyScale, seed.greyScale, THEME_GREY_SCALE_FIELD),
|
|
391
|
+
{
|
|
392
|
+
name: "library",
|
|
393
|
+
type: "array",
|
|
394
|
+
label: "Custom colors",
|
|
395
|
+
labels: {
|
|
396
|
+
singular: "Color",
|
|
397
|
+
plural: "Colors"
|
|
398
|
+
},
|
|
399
|
+
fields: [
|
|
400
|
+
{
|
|
401
|
+
name: "key",
|
|
402
|
+
type: "text",
|
|
403
|
+
label: "Key",
|
|
404
|
+
required: true,
|
|
405
|
+
admin: { hidden: true }
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: "label",
|
|
409
|
+
type: "text",
|
|
410
|
+
label: "Name",
|
|
411
|
+
required: true,
|
|
412
|
+
admin: { hidden: true }
|
|
413
|
+
},
|
|
414
|
+
...colorScaleFields()
|
|
415
|
+
],
|
|
416
|
+
admin: { components: { Field: THEME_LIBRARY_FIELD } }
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
}, {
|
|
420
|
+
name: "contrast",
|
|
421
|
+
type: "ui",
|
|
422
|
+
admin: {
|
|
423
|
+
components: { Field: THEME_CONTRAST_REPORT },
|
|
424
|
+
custom: { target: contrastTarget }
|
|
425
|
+
}
|
|
426
|
+
}]
|
|
190
427
|
},
|
|
191
428
|
{
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
429
|
+
label: "Typography",
|
|
430
|
+
fields: [{
|
|
431
|
+
name: "typography",
|
|
432
|
+
type: "group",
|
|
433
|
+
label: false,
|
|
434
|
+
fields: [
|
|
435
|
+
fontField("heading", "Heading font", headingSelectValue(seed.fontHeading), fonts, fontsBaseUrl, true),
|
|
436
|
+
fontField("body", "Body font", seed.fontBody, fonts, fontsBaseUrl, false),
|
|
437
|
+
{
|
|
438
|
+
name: "pairing",
|
|
439
|
+
type: "ui",
|
|
440
|
+
admin: {
|
|
441
|
+
components: { Field: THEME_PAIRING_FIELD },
|
|
442
|
+
custom: { fontsBaseUrl }
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
]
|
|
446
|
+
}]
|
|
198
447
|
},
|
|
199
|
-
hintSelect("greyScale", "Grey scale", presetHints.greyScale, seed.greyScale),
|
|
200
|
-
hintSelect("radius", "Radius", presetHints.radius, seed.radius),
|
|
201
|
-
hintSelect("shadow", "Shadow", presetHints.shadow, seed.shadow),
|
|
202
|
-
hintSelect("motion", "Motion", presetHints.motion, seed.motion),
|
|
203
|
-
hintSelect("density", "Density", presetHints.density, seed.density),
|
|
204
|
-
hintSelect("defaultTheme", "Default theme", presetHints.defaultTheme, seed.defaultTheme),
|
|
205
448
|
{
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
449
|
+
label: "Appearance",
|
|
450
|
+
fields: [{
|
|
451
|
+
name: "appearance",
|
|
452
|
+
type: "group",
|
|
453
|
+
label: false,
|
|
454
|
+
fields: [
|
|
455
|
+
namedSelect("defaultTheme", "Default theme", APPEARANCE_CHOICES.defaultTheme, seed.defaultTheme),
|
|
456
|
+
namedSelect("radius", "Corner style", APPEARANCE_CHOICES.radius, seed.radius),
|
|
457
|
+
namedSelect("shadow", "Depth", APPEARANCE_CHOICES.shadow, seed.shadow),
|
|
458
|
+
namedSelect("motion", "Motion", APPEARANCE_CHOICES.motion, seed.motion),
|
|
459
|
+
namedSelect("density", "Spacing", APPEARANCE_CHOICES.density, seed.density)
|
|
460
|
+
],
|
|
461
|
+
admin: { components: { Field: THEME_APPEARANCE_FIELD } }
|
|
462
|
+
}]
|
|
210
463
|
}
|
|
211
464
|
];
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
465
|
+
tabs.push({
|
|
466
|
+
label: "Identity",
|
|
467
|
+
fields: logo ? [
|
|
468
|
+
{
|
|
469
|
+
name: "logo",
|
|
470
|
+
type: "upload",
|
|
471
|
+
relationTo: logo.collection,
|
|
472
|
+
label: "Logo",
|
|
473
|
+
admin: { description: "The full wordmark used in the header and footer." }
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "favicon",
|
|
477
|
+
type: "upload",
|
|
478
|
+
relationTo: logo.collection,
|
|
479
|
+
label: "Favicon",
|
|
480
|
+
admin: { description: "The small icon in the browser tab. PNG or SVG." }
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
name: "logoMark",
|
|
484
|
+
type: "upload",
|
|
485
|
+
relationTo: logo.collection,
|
|
486
|
+
label: "Mobile menu logo",
|
|
487
|
+
admin: { description: "Optional. Used when the full logo is too wide for the mobile menu — typically the mark." }
|
|
488
|
+
}
|
|
489
|
+
] : [{
|
|
490
|
+
name: "identityFallback",
|
|
491
|
+
type: "ui",
|
|
492
|
+
admin: {
|
|
493
|
+
components: { Field: THEME_IDENTITY_FALLBACK },
|
|
494
|
+
custom: { fallback: identity?.fallback }
|
|
495
|
+
}
|
|
496
|
+
}]
|
|
222
497
|
});
|
|
498
|
+
const fields = [{
|
|
499
|
+
type: "tabs",
|
|
500
|
+
tabs
|
|
501
|
+
}, {
|
|
502
|
+
name: "publishChild",
|
|
503
|
+
type: "text",
|
|
504
|
+
admin: { hidden: true }
|
|
505
|
+
}];
|
|
223
506
|
return {
|
|
224
507
|
slug: THEME_SLUG,
|
|
225
508
|
label: "Theme",
|
|
226
509
|
admin: {
|
|
227
510
|
group: "Settings",
|
|
228
511
|
custom: {
|
|
229
|
-
previewPath,
|
|
230
512
|
contrastTarget,
|
|
231
|
-
fontsBaseUrl
|
|
513
|
+
fontsBaseUrl,
|
|
514
|
+
identityFallback: identity?.fallback
|
|
232
515
|
},
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
516
|
+
components: { elements: { SaveButton: THEME_SAVE_BUTTON } }
|
|
517
|
+
},
|
|
518
|
+
access: {
|
|
519
|
+
read: access.read,
|
|
520
|
+
update: access.update
|
|
521
|
+
},
|
|
522
|
+
fields,
|
|
523
|
+
hooks: {
|
|
524
|
+
beforeChange: [({ data, originalDoc }) => {
|
|
525
|
+
const { publishChild, ...rest } = data;
|
|
526
|
+
if (isThemeChild(publishChild)) return publishThemeChild(originalDoc ?? {}, rest, publishChild);
|
|
527
|
+
return rest;
|
|
528
|
+
}],
|
|
529
|
+
afterChange: [async ({ doc }) => {
|
|
530
|
+
if (!onPublish) return;
|
|
531
|
+
const themeDoc = doc;
|
|
532
|
+
await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);
|
|
533
|
+
}]
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/brand-assets/sanitize.ts
|
|
539
|
+
const DANGEROUS_TAGS = [
|
|
540
|
+
"script",
|
|
541
|
+
"foreignObject",
|
|
542
|
+
"iframe",
|
|
543
|
+
"embed",
|
|
544
|
+
"object"
|
|
545
|
+
];
|
|
546
|
+
/**
|
|
547
|
+
* Strips the SVG features a lockup does not need and an attacker would
|
|
548
|
+
* use: script, foreignObject, iframe/embed/object, event handlers,
|
|
549
|
+
* javascript: / data:text/html URLs, and xml-stylesheet. Internal `#`
|
|
550
|
+
* refs and ordinary drawing elements stay.
|
|
551
|
+
*/
|
|
552
|
+
function sanitizeSvg(source) {
|
|
553
|
+
if (!/<svg[\s>]/i.test(source)) throw new Error("Not an SVG");
|
|
554
|
+
let out = source.replace(/<\?xml-stylesheet[\s\S]*?\?>/gi, "");
|
|
555
|
+
for (const tag of DANGEROUS_TAGS) {
|
|
556
|
+
out = out.replace(new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}\\s*>`, "gi"), "");
|
|
557
|
+
out = out.replace(new RegExp(`<${tag}\\b[^>]*\\/>`, "gi"), "");
|
|
558
|
+
}
|
|
559
|
+
out = out.replace(/\s+on[a-z][a-z0-9-]*\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
|
|
560
|
+
out = out.replace(/((?:href|src|xlink:href)\s*=\s*)(["'])\s*(?:javascript:|data:text\/html)[^"']*\2/gi, "$1$2$2");
|
|
561
|
+
return out;
|
|
562
|
+
}
|
|
563
|
+
function isSvgUpload(file) {
|
|
564
|
+
if (file.mimetype === "image/svg+xml") return true;
|
|
565
|
+
if (typeof file.name === "string" && file.name.toLowerCase().endsWith(".svg")) return true;
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
//#endregion
|
|
569
|
+
//#region src/brand-assets/collection.ts
|
|
570
|
+
const BRAND_ASSETS_SLUG = "brand-assets";
|
|
571
|
+
/** SVG preferred; PNG allowed. ICO is for the favicon slot. */
|
|
572
|
+
const BRAND_ASSETS_MIME_TYPES = [
|
|
573
|
+
"image/svg+xml",
|
|
574
|
+
"image/png",
|
|
575
|
+
"image/x-icon",
|
|
576
|
+
"image/vnd.microsoft.icon"
|
|
577
|
+
];
|
|
578
|
+
function requireAccess(options) {
|
|
579
|
+
const read = options.access?.read;
|
|
580
|
+
const update = options.access?.update;
|
|
581
|
+
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.");
|
|
582
|
+
return {
|
|
583
|
+
read,
|
|
584
|
+
update
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Locked cupboard for lockup and mark. Not ordinary Media — only a brand
|
|
589
|
+
* manager writes it. Theme's `logo` / `logoMark` point here when the site
|
|
590
|
+
* passes this slug as `logo.collection`.
|
|
591
|
+
*/
|
|
592
|
+
function createBrandAssets(options) {
|
|
593
|
+
const access = requireAccess(options);
|
|
594
|
+
return {
|
|
595
|
+
slug: options.slug ?? "brand-assets",
|
|
596
|
+
labels: {
|
|
597
|
+
singular: "Brand asset",
|
|
598
|
+
plural: "Brand assets"
|
|
599
|
+
},
|
|
600
|
+
admin: {
|
|
601
|
+
group: "Settings",
|
|
602
|
+
useAsTitle: "label",
|
|
603
|
+
description: "Logo, favicon, and mobile-menu mark. SVG preferred; PNG and ICO allowed."
|
|
237
604
|
},
|
|
238
|
-
|
|
605
|
+
upload: {
|
|
606
|
+
mimeTypes: [...BRAND_ASSETS_MIME_TYPES],
|
|
607
|
+
crop: false,
|
|
608
|
+
focalPoint: false
|
|
609
|
+
},
|
|
610
|
+
versions: true,
|
|
239
611
|
access: {
|
|
240
612
|
read: access.read,
|
|
613
|
+
create: access.update,
|
|
241
614
|
update: access.update,
|
|
615
|
+
delete: access.update,
|
|
242
616
|
readVersions: access.update
|
|
243
617
|
},
|
|
244
|
-
fields
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
618
|
+
fields: [
|
|
619
|
+
{
|
|
620
|
+
name: "label",
|
|
621
|
+
type: "text",
|
|
622
|
+
label: "Label"
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: "notes",
|
|
626
|
+
type: "textarea",
|
|
627
|
+
label: "Usage notes"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
name: "alt",
|
|
631
|
+
type: "text",
|
|
632
|
+
label: "Alt text",
|
|
633
|
+
required: true
|
|
634
|
+
}
|
|
635
|
+
],
|
|
636
|
+
hooks: { beforeOperation: [({ req, operation }) => {
|
|
637
|
+
if (operation !== "create" && operation !== "update") return;
|
|
638
|
+
const file = req.file;
|
|
639
|
+
if (!file || !isSvgUpload(file)) return;
|
|
640
|
+
file.data = Buffer.from(sanitizeSvg(file.data.toString("utf8")), "utf8");
|
|
250
641
|
}] }
|
|
251
642
|
};
|
|
252
643
|
}
|
|
@@ -264,6 +655,32 @@ async function seedTheme(payload, seed) {
|
|
|
264
655
|
});
|
|
265
656
|
}
|
|
266
657
|
//#endregion
|
|
267
|
-
|
|
658
|
+
//#region src/theme/preview.ts
|
|
659
|
+
/**
|
|
660
|
+
* Device sizes the Theme (and, later, Pages) live-preview toolbar offers.
|
|
661
|
+
* One list so the panes match.
|
|
662
|
+
*/
|
|
663
|
+
const THEME_PREVIEW_BREAKPOINTS = [
|
|
664
|
+
{
|
|
665
|
+
label: "Mobile",
|
|
666
|
+
name: "mobile",
|
|
667
|
+
width: 375,
|
|
668
|
+
height: 667
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
label: "Tablet",
|
|
672
|
+
name: "tablet",
|
|
673
|
+
width: 768,
|
|
674
|
+
height: 1024
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
label: "Desktop",
|
|
678
|
+
name: "desktop",
|
|
679
|
+
width: 1440,
|
|
680
|
+
height: 900
|
|
681
|
+
}
|
|
682
|
+
];
|
|
683
|
+
//#endregion
|
|
684
|
+
export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, DESCRIPTION_LENGTH, SHARE_IMAGE_SIZE, SYSTEM_COLOR_KEYS, THEME_APPEARANCE_FIELD, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_IDENTITY_FALLBACK, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, createBrandAssets, createTheme, deleteLibraryColor, documentTitle, firstImageIn, noIndexField, publishThemeChild, resolveThemeIdentity, rewriteColorToken, sanitizeSvg, seedTheme, seoPlugin, themeColorKeys, titleTemplate, truncateAtWord };
|
|
268
685
|
|
|
269
686
|
//# sourceMappingURL=index.mjs.map
|