@bison-lab/payload-core 3.8.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/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 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
+ 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,34 +79,129 @@ function firstImageIn(blocks, field = "image") {
79
79
  }
80
80
  }
81
81
  //#endregion
82
- //#region src/theme/preview.ts
82
+ //#region src/theme/appearance-labels.ts
83
83
  /**
84
- * Device sizes the Theme (and, later, Pages) live-preview toolbar offers.
85
- * One list so the panes match.
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 THEME_PREVIEW_BREAKPOINTS = [
88
- {
89
- label: "Mobile",
90
- name: "mobile",
91
- width: 375,
92
- height: 667
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
- label: "Tablet",
96
- name: "tablet",
97
- width: 768,
98
- height: 1024
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
- label: "Desktop",
102
- name: "desktop",
103
- width: 1440,
104
- height: 900
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 hintSelect(name, label, hints, defaultValue) {
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(hints).map(([value, { label: optionLabel, hint }]) => ({
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
- function colorField(name, label, defaultValue) {
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: "text",
306
+ type: "group",
135
307
  label,
136
- required: true,
137
- defaultValue,
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) {
@@ -164,89 +334,317 @@ function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
164
334
  }
165
335
  };
166
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
+ }
167
347
  /**
168
- * Settings → Theme: the five brand colours, grey scale, presets, default
169
- * theme, body and heading fonts, and optional logo. Access is passed in
170
- * (`canManageBrand` for update). Drafts autosave; the public site reads
171
- * the published version through `getPublishedTheme`.
172
- *
173
- * `previewPath` wires `admin.livePreview` when the site has a route.
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.
174
351
  */
175
352
  function createTheme(options) {
176
- const access = requireAccess(options);
177
- const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 4.5, previewPath, logo, onPublish } = options;
178
- const fields = [
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 = [
179
357
  {
180
- name: "brand",
181
- type: "group",
182
- label: "Brand colours",
358
+ label: "Colors",
183
359
  fields: [
184
- colorField("primary", "Primary", seed.brandPrimary),
185
- colorField("secondary", "Secondary", seed.brandSecondary),
186
- colorField("accent", "Accent", seed.brandAccent),
187
- colorField("highlight", "Highlight", seed.brandHighlight),
188
- colorField("success", "Success", seed.brandSuccess)
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
+ }
189
439
  ]
190
440
  },
191
441
  {
192
- name: "contrast",
193
- type: "ui",
194
- admin: {
195
- components: { Field: THEME_CONTRAST_REPORT },
196
- custom: { target: contrastTarget }
197
- }
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")]
198
460
  },
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
461
  {
206
- name: "fonts",
207
- type: "group",
208
- label: "Fonts",
209
- fields: [fontField("body", "Body", seed.fontBody, fonts, fontsBaseUrl, false), fontField("heading", "Heading", headingSelectValue(seed.fontHeading), fonts, fontsBaseUrl, true)]
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")]
210
476
  }
211
477
  ];
212
- if (logo) fields.push({
213
- name: "logo",
214
- type: "upload",
215
- relationTo: logo.collection,
216
- label: "Logo"
217
- }, {
218
- name: "logoMark",
219
- type: "upload",
220
- relationTo: logo.collection,
221
- label: "Logo mark"
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
+ ]
222
504
  });
505
+ const fields = [{
506
+ type: "tabs",
507
+ tabs
508
+ }, {
509
+ name: "publishChild",
510
+ type: "text",
511
+ admin: { hidden: true }
512
+ }];
223
513
  return {
224
514
  slug: THEME_SLUG,
225
515
  label: "Theme",
226
516
  admin: {
227
517
  group: "Settings",
228
518
  custom: {
229
- previewPath,
230
519
  contrastTarget,
231
- fontsBaseUrl
520
+ fontsBaseUrl,
521
+ identityFallback: identity?.fallback
232
522
  },
233
- ...previewPath ? { livePreview: {
234
- url: previewPath,
235
- breakpoints: [...THEME_PREVIEW_BREAKPOINTS]
236
- } } : {}
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."
237
611
  },
238
- versions: { drafts: { autosave: { interval: 800 } } },
612
+ upload: {
613
+ mimeTypes: [...BRAND_ASSETS_MIME_TYPES],
614
+ crop: false,
615
+ focalPoint: false
616
+ },
617
+ versions: true,
239
618
  access: {
240
619
  read: access.read,
620
+ create: access.update,
241
621
  update: access.update,
622
+ delete: access.update,
242
623
  readVersions: access.update
243
624
  },
244
- fields,
245
- hooks: { afterChange: [async ({ doc }) => {
246
- if (!onPublish) return;
247
- const themeDoc = doc;
248
- if (themeDoc._status !== "published") return;
249
- await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);
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");
250
648
  }] }
251
649
  };
252
650
  }
@@ -264,6 +662,32 @@ async function seedTheme(payload, seed) {
264
662
  });
265
663
  }
266
664
  //#endregion
267
- export { DESCRIPTION_LENGTH, SHARE_IMAGE_SIZE, THEME_COLOR_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_SLUG, createTheme, documentTitle, firstImageIn, noIndexField, seedTheme, seoPlugin, titleTemplate, truncateAtWord };
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 };
268
692
 
269
693
  //# sourceMappingURL=index.mjs.map