@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/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 { 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/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) {
@@ -165,88 +335,309 @@ function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
165
335
  };
166
336
  }
167
337
  /**
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.
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 { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 4.5, previewPath, logo, onPublish } = options;
178
- const fields = [
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
- name: "brand",
181
- type: "group",
182
- label: "Brand colours",
183
- 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)
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
- name: "contrast",
193
- type: "ui",
194
- admin: {
195
- components: { Field: THEME_CONTRAST_REPORT },
196
- custom: { target: contrastTarget }
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
- 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)]
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
- 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"
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
- ...previewPath ? { livePreview: {
234
- url: previewPath,
235
- breakpoints: [...THEME_PREVIEW_BREAKPOINTS]
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
- versions: { drafts: { autosave: { interval: 800 } } },
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
- 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);
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
- 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 };
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