@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/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,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 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) {
93
219
  return {
94
220
  name,
95
221
  type: "select",
96
222
  label,
97
223
  required: true,
98
224
  defaultValue,
99
- options: Object.entries(hints).map(([value, { label: optionLabel, hint }]) => ({
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
- 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) {
106
304
  return {
107
305
  name,
108
- type: "text",
306
+ type: "group",
109
307
  label,
110
- required: true,
111
- defaultValue,
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: the five brand colours, grey scale, presets, default
143
- * theme, body and heading fonts, and optional logo. Access is passed in
144
- * (`canManageBrand` for update). Drafts autosave; the public site reads
145
- * the published version through `getPublishedTheme`.
146
- *
147
- * `previewPath` is stored for BIS-61, which wires the live preview pane.
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 { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 4.5, previewPath, logo, onPublish } = options;
152
- 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 = [
153
357
  {
154
- name: "brand",
155
- type: "group",
156
- label: "Brand colours",
358
+ label: "Colors",
157
359
  fields: [
158
- colorField("primary", "Primary", seed.brandPrimary),
159
- colorField("secondary", "Secondary", seed.brandSecondary),
160
- colorField("accent", "Accent", seed.brandAccent),
161
- colorField("highlight", "Highlight", seed.brandHighlight),
162
- colorField("success", "Success", seed.brandSuccess)
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
- name: "contrast",
167
- type: "ui",
168
- admin: {
169
- components: { Field: THEME_CONTRAST_REPORT },
170
- custom: { target: contrastTarget }
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
- name: "fonts",
181
- type: "group",
182
- label: "Fonts",
183
- fields: [fontField("body", "Body", seed.fontBody, fonts, fontsBaseUrl, false), fontField("heading", "Heading", headingSelectValue(seed.fontHeading), fonts, fontsBaseUrl, true)]
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) fields.push({
187
- name: "logo",
188
- type: "upload",
189
- relationTo: logo.collection,
190
- label: "Logo"
191
- }, {
192
- name: "logoMark",
193
- type: "upload",
194
- relationTo: logo.collection,
195
- label: "Logo mark"
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: { drafts: { autosave: { interval: 800 } } },
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
- hooks: { afterChange: [async ({ doc }) => {
216
- if (!onPublish) return;
217
- const themeDoc = doc;
218
- if (themeDoc._status !== "published") return;
219
- await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);
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
- export { DESCRIPTION_LENGTH, SHARE_IMAGE_SIZE, THEME_COLOR_FIELD, THEME_CONTRAST_REPORT, THEME_FONT_FIELD, THEME_SLUG, createTheme, documentTitle, firstImageIn, noIndexField, seedTheme, seoPlugin, titleTemplate, truncateAtWord };
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