@bison-lab/payload-core 3.10.0 → 3.12.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,10 @@
1
1
  import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-C4ILz4p2.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";
2
+ import { A as THEME_IDENTITY_FALLBACK, C as THEME_APPEARANCE_FIELD, D as THEME_DOCUMENT_CONTROLS, E as THEME_CONTRAST_REPORT, F as THEME_SECTION_HEADING, I as headingSelectValue, M as THEME_PAIRING_FIELD, N as THEME_PUBLISH_FIELD, O as THEME_FONT_FIELD, P as THEME_SAVE_BUTTON, S as LOOK_FIELD, T as THEME_COLOR_SCALE_FIELD, _ as docFromConfig, a as pageEditorLooks, b as validateSourceIncluded, c as deleteLibraryColor, d as SYSTEM_COLOR_KEYS, f as THEME_APPEARANCE_SLUG, g as THEME_TYPOGRAPHY_SLUG, h as THEME_SLUG, i as PAGE_EDITOR_SYSTEM_KEYS, j as THEME_LIBRARY_FIELD, k as THEME_GREY_SCALE_FIELD, l as rewriteColorToken, m as THEME_IDENTITY_SLUG, n as colorTokenField, o as pageEditorTokens, p as THEME_COLORS_SLUG, r as lookField, s as themeLibraryFromDoc, t as resolveThemeIdentity, u as themeColorKeys, w as THEME_COLOR_FIELD, x as validateThemeHex, y as themeConfigFromDoc } from "./identity-DZOb3_Gk.mjs";
3
3
  import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
4
4
  import { catalog } from "@bison-lab/fonts";
5
- import { SHADE_STEPS, presetHints } from "@bison-lab/tokens";
5
+ import { DESTRUCTIVE_SCALE_HEX, SHADE_STEPS, presetHints } from "@bison-lab/tokens";
6
+ import { APIError, ValidationError, definePlugin, validations } from "payload";
7
+ import { select, text } from "payload/shared";
6
8
  //#region src/seo/fields.ts
7
9
  /**
8
10
  * The index switch, last in the SEO tab. Off by default: a page is public
@@ -163,12 +165,6 @@ const APPEARANCE_CHOICES = {
163
165
  };
164
166
  //#endregion
165
167
  //#region src/theme/publish.ts
166
- const THEME_CHILDREN = [
167
- "colors",
168
- "typography",
169
- "appearance",
170
- "identity"
171
- ];
172
168
  /**
173
169
  * Publishing a child writes that slice only. Other responsibilities stay
174
170
  * as they already are on the stored document. Unsaved form state on a
@@ -196,24 +192,63 @@ function publishThemeChild(stored, incoming, child) {
196
192
  logoMark: incoming.logoMark
197
193
  };
198
194
  }
199
- function isThemeChild(value) {
200
- return typeof value === "string" && THEME_CHILDREN.includes(value);
195
+ function sliceFromTheme(theme, child) {
196
+ if (child === "colors") return { colors: theme.colors };
197
+ if (child === "typography") return { typography: theme.typography };
198
+ if (child === "appearance") return { appearance: theme.appearance };
199
+ return {
200
+ logo: theme.logo,
201
+ favicon: theme.favicon,
202
+ logoMark: theme.logoMark
203
+ };
204
+ }
205
+ function colorHex(color) {
206
+ if (typeof color === "string") return color;
207
+ return color?.hex ?? void 0;
208
+ }
209
+ /**
210
+ * A Theme page is a real Global. Hydrate from the store only when this
211
+ * page has never been saved — otherwise afterRead would overwrite the form.
212
+ */
213
+ function hasThemeSlice(doc, child) {
214
+ if (!doc) return false;
215
+ if (child === "colors") return Boolean(colorHex(doc.colors?.brand?.primary));
216
+ if (child === "typography") return Boolean(doc.typography?.body);
217
+ if (child === "appearance") return Boolean(doc.appearance?.radius);
218
+ return Boolean(doc.logo || doc.favicon || doc.logoMark);
219
+ }
220
+ /**
221
+ * Save on a Theme page writes that slice onto the hidden `theme` row.
222
+ * Other children stay as stored.
223
+ */
224
+ async function persistThemeChild(payload, incoming, child) {
225
+ const next = publishThemeChild(await payload.findGlobal({
226
+ slug: "theme",
227
+ draft: false,
228
+ overrideAccess: true
229
+ }) ?? {}, incoming, child);
230
+ await payload.updateGlobal({
231
+ slug: THEME_SLUG,
232
+ data: next,
233
+ draft: false
234
+ });
235
+ return next;
201
236
  }
202
237
  //#endregion
203
238
  //#region src/theme/global.ts
239
+ /** Same value as tokens `SUCCESS_SCALE_HEX`. Inlined so an older tokens peer still boots. */
240
+ const LIBRARY_SUCCESS_HEX = "#22c55e";
204
241
  function requireAccess$1(options) {
205
242
  const read = options.access?.read;
206
243
  const update = options.access?.update;
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.");
244
+ if (!read || !update) throw new Error("createTheme requires access.read and access.update. Pass canManageBrand from this package as update.");
208
245
  return {
209
246
  read,
210
247
  update
211
248
  };
212
249
  }
213
250
  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;
251
+ return options.destructive ?? options.seed.brandDestructive ?? DESTRUCTIVE_SCALE_HEX;
217
252
  }
218
253
  function namedSelect(name, label, choices, defaultValue, field) {
219
254
  return {
@@ -306,7 +341,10 @@ function systemColorGroup(name, label, hexDefault) {
306
341
  type: "group",
307
342
  label,
308
343
  fields: colorScaleFields(hexDefault),
309
- admin: { components: { Field: THEME_COLOR_SCALE_FIELD } }
344
+ admin: {
345
+ components: { Field: THEME_COLOR_SCALE_FIELD },
346
+ hideGutter: true
347
+ }
310
348
  };
311
349
  }
312
350
  function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
@@ -334,205 +372,265 @@ function fontField(name, label, defaultValue, fonts, fontsBaseUrl, sameAsBody) {
334
372
  }
335
373
  };
336
374
  }
337
- /**
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.
342
- */
343
- function createTheme(options) {
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 = [
348
- {
349
- label: "Colors",
350
- fields: [{
351
- name: "colors",
375
+ function colorsFields(seed, destructive) {
376
+ return [{
377
+ name: "colors",
378
+ type: "group",
379
+ label: false,
380
+ admin: {
381
+ hideGutter: true,
382
+ width: "100%"
383
+ },
384
+ fields: [
385
+ {
386
+ name: "brand",
352
387
  type: "group",
353
388
  label: false,
389
+ admin: {
390
+ hideGutter: true,
391
+ width: "100%"
392
+ },
354
393
  fields: [
355
394
  {
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
- ]
395
+ name: "themeColorsHeading",
396
+ type: "ui",
397
+ admin: {
398
+ components: { Field: THEME_SECTION_HEADING },
399
+ custom: {
400
+ title: "Brand colors",
401
+ hint: "These colors define the visual identity of the site. Adjust them freely to match the brand."
402
+ }
403
+ }
389
404
  },
390
- namedSelect("greyScale", "Gray family", presetHints.greyScale, seed.greyScale, THEME_GREY_SCALE_FIELD),
405
+ systemColorGroup("primary", "Primary", seed.brandPrimary),
406
+ systemColorGroup("secondary", "Secondary", seed.brandSecondary),
407
+ systemColorGroup("accent", "Accent", seed.brandAccent),
408
+ systemColorGroup("highlight", "Highlight", seed.brandHighlight),
391
409
  {
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
- }]
427
- },
428
- {
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),
410
+ name: "libraryPlacement",
411
+ type: "ui",
412
+ admin: {
413
+ components: { Field: THEME_LIBRARY_FIELD },
414
+ custom: { dataPath: "colors.library" }
415
+ }
416
+ },
437
417
  {
438
- name: "pairing",
418
+ name: "greyPlacement",
439
419
  type: "ui",
440
420
  admin: {
441
- components: { Field: THEME_PAIRING_FIELD },
442
- custom: { fontsBaseUrl }
421
+ components: { Field: THEME_GREY_SCALE_FIELD },
422
+ custom: { dataPath: "colors.greyScale" }
443
423
  }
444
- }
424
+ },
425
+ {
426
+ name: "statusColorsHeading",
427
+ type: "ui",
428
+ admin: {
429
+ components: { Field: THEME_SECTION_HEADING },
430
+ custom: {
431
+ title: "Status colors",
432
+ hint: "These colors communicate meaning across the site. You can adjust them to fit your brand, but we'll help keep them recognizable as success and error states."
433
+ }
434
+ }
435
+ },
436
+ systemColorGroup("success", "Success", seed.brandSuccess || LIBRARY_SUCCESS_HEX),
437
+ systemColorGroup("destructive", "Destructive", destructive)
445
438
  ]
446
- }]
447
- },
448
- {
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
- }]
463
- }
464
- ];
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
439
  },
475
440
  {
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." }
441
+ ...namedSelect("greyScale", "Gray family", presetHints.greyScale, seed.greyScale, THEME_GREY_SCALE_FIELD),
442
+ admin: {
443
+ hidden: true,
444
+ components: { Field: THEME_GREY_SCALE_FIELD }
445
+ }
481
446
  },
482
447
  {
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." }
448
+ name: "library",
449
+ type: "array",
450
+ label: "Additional colors",
451
+ labels: {
452
+ singular: "Color",
453
+ plural: "Colors"
454
+ },
455
+ fields: [
456
+ {
457
+ name: "key",
458
+ type: "text",
459
+ label: "Key",
460
+ required: true,
461
+ admin: { hidden: true }
462
+ },
463
+ {
464
+ name: "label",
465
+ type: "text",
466
+ label: "Name",
467
+ required: true,
468
+ admin: { hidden: true }
469
+ },
470
+ ...colorScaleFields()
471
+ ],
472
+ admin: {
473
+ hidden: true,
474
+ components: { Field: THEME_LIBRARY_FIELD },
475
+ width: "100%"
476
+ }
488
477
  }
489
- ] : [{
490
- name: "identityFallback",
491
- type: "ui",
492
- admin: {
493
- components: { Field: THEME_IDENTITY_FALLBACK },
494
- custom: { fallback: identity?.fallback }
478
+ ]
479
+ }];
480
+ }
481
+ function typographyFields(seed, fonts, fontsBaseUrl) {
482
+ return [{
483
+ name: "typography",
484
+ type: "group",
485
+ label: "Typography",
486
+ fields: [
487
+ fontField("heading", "Heading font", headingSelectValue(seed.fontHeading), fonts, fontsBaseUrl, true),
488
+ fontField("body", "Body font", seed.fontBody, fonts, fontsBaseUrl, false),
489
+ {
490
+ name: "pairing",
491
+ type: "ui",
492
+ admin: {
493
+ components: { Field: THEME_PAIRING_FIELD },
494
+ custom: { fontsBaseUrl }
495
+ }
495
496
  }
496
- }]
497
- });
498
- const fields = [{
499
- type: "tabs",
500
- tabs
501
- }, {
502
- name: "publishChild",
503
- type: "text",
504
- admin: { hidden: true }
497
+ ]
498
+ }];
499
+ }
500
+ function appearanceFields(seed) {
501
+ return [{
502
+ name: "appearance",
503
+ type: "group",
504
+ label: "Appearance",
505
+ fields: [
506
+ namedSelect("defaultTheme", "Default theme", APPEARANCE_CHOICES.defaultTheme, seed.defaultTheme),
507
+ namedSelect("radius", "Corner style", APPEARANCE_CHOICES.radius, seed.radius),
508
+ namedSelect("shadow", "Depth", APPEARANCE_CHOICES.shadow, seed.shadow),
509
+ namedSelect("motion", "Motion", APPEARANCE_CHOICES.motion, seed.motion),
510
+ namedSelect("density", "Spacing", APPEARANCE_CHOICES.density, seed.density)
511
+ ],
512
+ admin: { components: { Field: THEME_APPEARANCE_FIELD } }
505
513
  }];
514
+ }
515
+ function identityFields(collection) {
516
+ return [
517
+ {
518
+ name: "logo",
519
+ type: "upload",
520
+ relationTo: collection,
521
+ label: "Logo",
522
+ admin: { description: "The full wordmark used in the header and footer." }
523
+ },
524
+ {
525
+ name: "favicon",
526
+ type: "upload",
527
+ relationTo: collection,
528
+ label: "Favicon",
529
+ admin: { description: "The small icon in the browser tab. PNG or SVG." }
530
+ },
531
+ {
532
+ name: "logoMark",
533
+ type: "upload",
534
+ relationTo: collection,
535
+ label: "Mobile menu logo",
536
+ admin: { description: "Optional. Used when the full logo is too wide for the mobile menu — typically the mark." }
537
+ }
538
+ ];
539
+ }
540
+ function pageHooks(child) {
506
541
  return {
542
+ beforeChange: [async ({ data, req }) => {
543
+ if (req?.payload) await persistThemeChild(req.payload, data, child);
544
+ return data;
545
+ }],
546
+ afterRead: [async ({ doc, req }) => {
547
+ if (hasThemeSlice(doc, child)) return doc;
548
+ const theme = await req?.payload?.findGlobal({
549
+ slug: THEME_SLUG,
550
+ draft: false,
551
+ depth: child === "identity" ? 1 : 0,
552
+ overrideAccess: true
553
+ });
554
+ if (!theme) return doc;
555
+ return {
556
+ ...doc,
557
+ ...sliceFromTheme(theme, child)
558
+ };
559
+ }]
560
+ };
561
+ }
562
+ /**
563
+ * Standard Payload Globals in an `admin.group: "Theme"` nav section —
564
+ * the same pattern as Content and Settings. Each page is its own Global
565
+ * (Save, fields, access). A hidden `theme` store is the published read
566
+ * model `getPublishedTheme` already knows. Locking is off: these are
567
+ * settings forms, not collaborative documents. No draft mode, no preview
568
+ * pane.
569
+ */
570
+ function createTheme(options) {
571
+ const access = requireAccess$1(options);
572
+ const destructive = requireDestructive(options);
573
+ const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 7, logo, identity, onPublish } = options;
574
+ const adminCustom = {
575
+ contrastTarget,
576
+ fontsBaseUrl,
577
+ identityFallback: identity?.fallback
578
+ };
579
+ const colorFields = colorsFields(seed, destructive);
580
+ const typeFields = typographyFields(seed, fonts, fontsBaseUrl);
581
+ const lookFields = appearanceFields(seed);
582
+ const markFields = logo ? identityFields(logo.collection) : [];
583
+ const store = {
507
584
  slug: THEME_SLUG,
508
585
  label: "Theme",
586
+ lockDocuments: false,
509
587
  admin: {
510
- group: "Settings",
511
- custom: {
512
- contrastTarget,
513
- fontsBaseUrl,
514
- identityFallback: identity?.fallback
515
- },
516
- components: { elements: { SaveButton: THEME_SAVE_BUTTON } }
588
+ hidden: true,
589
+ hideAPIURL: true,
590
+ custom: adminCustom
517
591
  },
518
592
  access: {
519
593
  read: access.read,
520
594
  update: access.update
521
595
  },
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
- }
596
+ fields: [
597
+ ...colorFields,
598
+ ...typeFields,
599
+ ...lookFields,
600
+ ...markFields
601
+ ],
602
+ hooks: { afterChange: [async ({ doc }) => {
603
+ if (!onPublish) return;
604
+ const themeDoc = doc;
605
+ await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);
606
+ }] }
535
607
  };
608
+ function page(slug, label, child, fields, preview) {
609
+ return {
610
+ slug,
611
+ label,
612
+ lockDocuments: false,
613
+ admin: {
614
+ group: "Theme",
615
+ hideAPIURL: true,
616
+ custom: adminCustom,
617
+ components: preview ? { elements: { beforeDocumentControls: [THEME_DOCUMENT_CONTROLS] } } : void 0
618
+ },
619
+ access: {
620
+ read: access.read,
621
+ update: access.update
622
+ },
623
+ fields,
624
+ hooks: pageHooks(child)
625
+ };
626
+ }
627
+ return [
628
+ store,
629
+ page(THEME_COLORS_SLUG, "Colors", "colors", colorFields, true),
630
+ page(THEME_TYPOGRAPHY_SLUG, "Typography", "typography", typeFields, true),
631
+ page(THEME_APPEARANCE_SLUG, "Appearance", "appearance", lookFields, true),
632
+ ...logo ? [page(THEME_IDENTITY_SLUG, "Identity", "identity", markFields, false)] : []
633
+ ];
536
634
  }
537
635
  //#endregion
538
636
  //#region src/brand-assets/sanitize.ts
@@ -578,7 +676,7 @@ const BRAND_ASSETS_MIME_TYPES = [
578
676
  function requireAccess(options) {
579
677
  const read = options.access?.read;
580
678
  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.");
679
+ if (!read || !update) throw new Error("createBrandAssets requires access.read and access.update. Pass canManageBrand from this package as update.");
582
680
  return {
583
681
  read,
584
682
  update
@@ -681,6 +779,659 @@ const THEME_PREVIEW_BREAKPOINTS = [
681
779
  }
682
780
  ];
683
781
  //#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 };
782
+ //#region src/roles/types.ts
783
+ /**
784
+ * The Roles Global a site's generated types will describe. Optional and
785
+ * nullable, no index signature: a generated Global is assignable to this,
786
+ * never the reverse.
787
+ */
788
+ const ROLES = [
789
+ "developer",
790
+ "admin",
791
+ "designer",
792
+ "author"
793
+ ];
794
+ const ROLE_LABELS = {
795
+ developer: "Developer",
796
+ admin: "Admin",
797
+ designer: "Designer",
798
+ author: "Author"
799
+ };
800
+ const CAPABILITIES = [
801
+ "content",
802
+ "brand",
803
+ "publish",
804
+ "users"
805
+ ];
806
+ function isRole(value) {
807
+ return typeof value === "string" && ROLES.includes(value);
808
+ }
809
+ //#endregion
810
+ //#region src/roles/matrix.ts
811
+ const ROLES_SLUG = "roles";
812
+ /**
813
+ * Default rank and ticks. Brand is Designer only. The API tab is not a
814
+ * column — it is locked to Developer in `isDeveloper`.
815
+ */
816
+ const DEFAULT_ROLE_MATRIX = [
817
+ {
818
+ role: "developer",
819
+ content: true,
820
+ brand: true,
821
+ publish: true,
822
+ users: true
823
+ },
824
+ {
825
+ role: "admin",
826
+ content: true,
827
+ brand: false,
828
+ publish: true,
829
+ users: true
830
+ },
831
+ {
832
+ role: "designer",
833
+ content: false,
834
+ brand: true,
835
+ publish: false,
836
+ users: false
837
+ },
838
+ {
839
+ role: "author",
840
+ content: true,
841
+ brand: false,
842
+ publish: false,
843
+ users: false
844
+ }
845
+ ];
846
+ const DEVELOPER_DESCRIPTION = "Everything Admin can do, plus the document API tab.";
847
+ const LAST_USERS_TICK_MESSAGE = "Keep Users ticked on at least one of Admin or Developer.";
848
+ const CAPABILITY_LABELS = {
849
+ content: "Content",
850
+ brand: "Brand",
851
+ publish: "Publish",
852
+ users: "Users"
853
+ };
854
+ function defaultRolesFieldValue() {
855
+ return DEFAULT_ROLE_MATRIX.map((row) => ({
856
+ id: row.role,
857
+ ...row
858
+ }));
859
+ }
860
+ function parseRolesMatrix(value) {
861
+ if (!Array.isArray(value)) return {
862
+ ok: false,
863
+ message: "Roles must be Developer, Admin, Designer, and Author."
864
+ };
865
+ const rows = [];
866
+ const seen = /* @__PURE__ */ new Set();
867
+ for (const item of value) {
868
+ if (!item || typeof item !== "object") continue;
869
+ const role = "role" in item ? item.role : void 0;
870
+ if (!isRole(role) || seen.has(role)) continue;
871
+ seen.add(role);
872
+ rows.push({
873
+ id: role,
874
+ role,
875
+ content: Boolean("content" in item && item.content),
876
+ brand: Boolean("brand" in item && item.brand),
877
+ publish: Boolean("publish" in item && item.publish),
878
+ users: Boolean("users" in item && item.users)
879
+ });
880
+ }
881
+ if (rows.length !== ROLES.length || ROLES.some((role) => !seen.has(role))) return {
882
+ ok: false,
883
+ message: "Roles must be Developer, Admin, Designer, and Author."
884
+ };
885
+ return {
886
+ ok: true,
887
+ rows
888
+ };
889
+ }
890
+ function hasPrivilegedUsersTick(rows) {
891
+ return rows.some((row) => (row.role === "admin" || row.role === "developer") && row.users);
892
+ }
893
+ function validateRolesMatrix(value) {
894
+ const parsed = parseRolesMatrix(value);
895
+ if (!parsed.ok) return parsed.message;
896
+ if (!hasPrivilegedUsersTick(parsed.rows)) return LAST_USERS_TICK_MESSAGE;
897
+ return true;
898
+ }
899
+ /**
900
+ * Developer is exclusive. Admin does not swallow Designer: both store.
901
+ */
902
+ function normalizeStoredRoles(value) {
903
+ if (!Array.isArray(value)) return [];
904
+ const roles = [...new Set(value.filter(isRole))];
905
+ if (roles.includes("developer")) return ["developer"];
906
+ return roles;
907
+ }
908
+ function roleSelectOptions(matrix = DEFAULT_ROLE_MATRIX) {
909
+ return matrix.map((row) => ({
910
+ label: ROLE_LABELS[row.role],
911
+ value: row.role
912
+ }));
913
+ }
914
+ /** Capability copy only. Developer names the API tab; nothing about MCP or seed. */
915
+ function roleDescription(role, matrix = DEFAULT_ROLE_MATRIX) {
916
+ if (role === "developer") return DEVELOPER_DESCRIPTION;
917
+ const row = matrix.find((entry) => entry.role === role);
918
+ if (!row) return "No capabilities";
919
+ return CAPABILITIES.filter((capability) => row[capability]).map((capability) => CAPABILITY_LABELS[capability]).join(", ") || "No capabilities";
920
+ }
921
+ //#endregion
922
+ //#region src/roles/access.ts
923
+ function isAccessArgs(value) {
924
+ return typeof value === "object" && value !== null && "req" in value;
925
+ }
926
+ function storedRoles(user) {
927
+ return Array.isArray(user?.roles) ? user.roles : [];
928
+ }
929
+ function hasRole(user, role) {
930
+ return storedRoles(user).includes(role);
931
+ }
932
+ /** Not a tick. True only when `developer` is stored on the row. */
933
+ function isDeveloper(user) {
934
+ return hasRole(user, "developer");
935
+ }
936
+ function hasCapability(user, capability, matrix = DEFAULT_ROLE_MATRIX) {
937
+ return storedRoles(user).some((role) => {
938
+ return matrix.find((entry) => entry.role === role)?.[capability] === true;
939
+ });
940
+ }
941
+ /**
942
+ * Reads the saved Roles Global, falling back to the seed when the row is
943
+ * empty, missing, or unreadable. Always override-access so a Designer
944
+ * evaluating Theme does not have to read Settings → Roles.
945
+ */
946
+ async function getRolesMatrix(req = {}) {
947
+ const findGlobal = req.payload?.findGlobal;
948
+ if (typeof findGlobal !== "function") return [...DEFAULT_ROLE_MATRIX];
949
+ try {
950
+ const doc = await findGlobal({
951
+ slug: ROLES_SLUG,
952
+ overrideAccess: true,
953
+ req
954
+ });
955
+ const parsed = parseRolesMatrix(doc && typeof doc === "object" && "roles" in doc ? doc.roles : void 0);
956
+ return parsed.ok ? parsed.rows : [...DEFAULT_ROLE_MATRIX];
957
+ } catch {
958
+ return [...DEFAULT_ROLE_MATRIX];
959
+ }
960
+ }
961
+ function capabilityPredicate(capability) {
962
+ function predicate(userOrArgs, matrix) {
963
+ if (isAccessArgs(userOrArgs)) {
964
+ const user = userOrArgs.req.user;
965
+ if (matrix) return hasCapability(user, capability, matrix);
966
+ return getRolesMatrix(userOrArgs.req).then((rows) => hasCapability(user, capability, rows));
967
+ }
968
+ return hasCapability(userOrArgs, capability, matrix ?? DEFAULT_ROLE_MATRIX);
969
+ }
970
+ return predicate;
971
+ }
972
+ const canManageContent = capabilityPredicate("content");
973
+ const canManageBrand = capabilityPredicate("brand");
974
+ const canPublish = capabilityPredicate("publish");
975
+ /** Users tick on any held role. */
976
+ const isAdmin = capabilityPredicate("users");
977
+ /** This role id currently has the Users tick. */
978
+ function isPrivilegedRole(role, matrix = DEFAULT_ROLE_MATRIX) {
979
+ return isRole(role) && matrix.some((row) => row.role === role && row.users);
980
+ }
981
+ function isAuthenticated(userOrArgs) {
982
+ if (isAccessArgs(userOrArgs)) return Boolean(userOrArgs.req.user);
983
+ return Boolean(userOrArgs);
984
+ }
985
+ const isAdminOrSelf = async ({ req }) => {
986
+ if (!req.user) return false;
987
+ if (await isAdmin({ req })) return true;
988
+ return { id: { equals: req.user.id } };
989
+ };
990
+ const authenticatedOrPublished = ({ req: { user } }) => {
991
+ if (isAuthenticated(user)) return true;
992
+ return { _status: { equals: "published" } };
993
+ };
994
+ /** API tab condition: Developer only, not the Users tick. */
995
+ function isDeveloperTab({ req }) {
996
+ return isDeveloper(req.user);
997
+ }
998
+ //#endregion
999
+ //#region src/roles/fields.ts
1000
+ const ROLES_MATRIX_FIELD = "@bison-lab/payload-core/admin#RolesMatrixField";
1001
+ const ROLES_FIELD = "@bison-lab/payload-core/admin#RolesField";
1002
+ //#endregion
1003
+ //#region src/roles/global.ts
1004
+ /**
1005
+ * Settings → Roles. Rank is the array order (Payload's drag handle). Ticks
1006
+ * are Content, Brand, Publish, Users. The API tab is not a column.
1007
+ */
1008
+ function createRoles() {
1009
+ return {
1010
+ slug: ROLES_SLUG,
1011
+ label: "Roles",
1012
+ admin: {
1013
+ group: "Settings",
1014
+ hidden: ({ user }) => !isAdmin(user),
1015
+ description: "Who may do what. Drag to change rank. The API tab is locked to Developer."
1016
+ },
1017
+ access: {
1018
+ read: (args) => isAdmin(args),
1019
+ update: (args) => isAdmin(args)
1020
+ },
1021
+ fields: [{
1022
+ name: "roles",
1023
+ type: "array",
1024
+ label: "Roles",
1025
+ labels: {
1026
+ singular: "Role",
1027
+ plural: "Roles"
1028
+ },
1029
+ minRows: 4,
1030
+ maxRows: 4,
1031
+ required: true,
1032
+ defaultValue: defaultRolesFieldValue(),
1033
+ validate: validateRolesMatrix,
1034
+ admin: {
1035
+ components: { Field: ROLES_MATRIX_FIELD },
1036
+ description: "Drag to change rank. Ticks are Content, Brand, Publish, and Users. The API tab is locked to Developer.",
1037
+ initCollapsed: false
1038
+ },
1039
+ fields: [
1040
+ {
1041
+ name: "role",
1042
+ type: "select",
1043
+ label: "Role",
1044
+ required: true,
1045
+ options: ROLES.map((value) => ({
1046
+ label: ROLE_LABELS[value],
1047
+ value
1048
+ })),
1049
+ admin: { readOnly: true }
1050
+ },
1051
+ {
1052
+ name: "content",
1053
+ type: "checkbox",
1054
+ label: "Content"
1055
+ },
1056
+ {
1057
+ name: "brand",
1058
+ type: "checkbox",
1059
+ label: "Brand"
1060
+ },
1061
+ {
1062
+ name: "publish",
1063
+ type: "checkbox",
1064
+ label: "Publish"
1065
+ },
1066
+ {
1067
+ name: "users",
1068
+ type: "checkbox",
1069
+ label: "Users"
1070
+ }
1071
+ ]
1072
+ }]
1073
+ };
1074
+ }
1075
+ //#endregion
1076
+ //#region src/roles/seed.ts
1077
+ /**
1078
+ * Writes the default matrix. For a site's migration `up()`. Never writes a
1079
+ * user row — seeding `developer` on a person stays a site concern.
1080
+ */
1081
+ async function seedRoles(payload) {
1082
+ return payload.updateGlobal({
1083
+ slug: ROLES_SLUG,
1084
+ data: { roles: defaultRolesFieldValue() }
1085
+ });
1086
+ }
1087
+ //#endregion
1088
+ //#region src/fields/slug.ts
1089
+ /**
1090
+ * A stored slug from whatever was typed: lowercased, with any run of
1091
+ * whitespace and slashes stripped from either end in one pass, so `/ about-us /`
1092
+ * does not keep the inner spaces a trim-then-strip would leave.
1093
+ */
1094
+ function normalizeSlug(value) {
1095
+ return value.replace(/^[\s/]+|[\s/]+$/g, "").toLowerCase();
1096
+ }
1097
+ async function slugProblem(slug, { collection, id, isReserved, req }) {
1098
+ if (!slug) return void 0;
1099
+ if (isReserved(slug)) return `"/${slug}" is a built-in page on this site, so a page here cannot use it. Please choose a different address.`;
1100
+ if (typeof req?.payload?.find !== "function") return void 0;
1101
+ const where = { slug: { equals: slug } };
1102
+ if (id !== void 0) where.id = { not_equals: id };
1103
+ const query = {
1104
+ collection,
1105
+ depth: 0,
1106
+ limit: 1,
1107
+ overrideAccess: true,
1108
+ pagination: false,
1109
+ req,
1110
+ where
1111
+ };
1112
+ return (await req.payload.find(query)).docs.length > 0 || (await req.payload.find({
1113
+ ...query,
1114
+ draft: true
1115
+ })).docs.length > 0 ? `Another page is already using "/${slug}". Please choose a different address.` : void 0;
1116
+ }
1117
+ /**
1118
+ * The path a document is published at, normalised on the way in. Unique
1119
+ * across the collection. `validate` is the message under the field;
1120
+ * `beforeChange` is the enforcement on a draft save, where Payload skips
1121
+ * field validation.
1122
+ */
1123
+ function slugField({ collection, isReserved }) {
1124
+ const validate = async (value, options) => {
1125
+ try {
1126
+ const builtIn = await validations.text(value, options);
1127
+ if (builtIn !== true) return builtIn;
1128
+ } catch {}
1129
+ return await slugProblem(typeof value === "string" ? value : "", {
1130
+ collection,
1131
+ id: options.id,
1132
+ isReserved,
1133
+ req: options.req
1134
+ }) ?? true;
1135
+ };
1136
+ return {
1137
+ name: "slug",
1138
+ type: "text",
1139
+ required: true,
1140
+ unique: true,
1141
+ admin: { description: "Path under the site root, no leading slash: \"about-us\" or \"patients/stories\"." },
1142
+ hooks: {
1143
+ beforeValidate: [({ value }) => typeof value === "string" ? normalizeSlug(value) : value],
1144
+ beforeChange: [async ({ data, originalDoc, req, value }) => {
1145
+ if (typeof value !== "string") return value;
1146
+ const problem = await slugProblem(value, {
1147
+ collection,
1148
+ id: originalDoc?.id ?? data?.id,
1149
+ isReserved,
1150
+ req
1151
+ });
1152
+ if (problem) throw new ValidationError({
1153
+ collection,
1154
+ errors: [{
1155
+ message: problem,
1156
+ path: "slug"
1157
+ }],
1158
+ req
1159
+ }, req?.t);
1160
+ return value;
1161
+ }]
1162
+ },
1163
+ validate
1164
+ };
1165
+ }
1166
+ //#endregion
1167
+ //#region src/collections/pages.ts
1168
+ /**
1169
+ * CMS-managed pages: one required hero, then a reorderable body of sections.
1170
+ * The CMS owns copy and the order of sections; what a section looks like is
1171
+ * code-owned, which is why the blocks are an argument.
1172
+ */
1173
+ function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, previewButton }) {
1174
+ const [defaultHero] = heroBlocks;
1175
+ if (!defaultHero) throw new Error("createPages needs at least one hero block");
1176
+ return {
1177
+ slug: "pages",
1178
+ admin: {
1179
+ useAsTitle: "title",
1180
+ group: "Content",
1181
+ defaultColumns: [
1182
+ "title",
1183
+ "slug",
1184
+ "_status",
1185
+ "updatedAt"
1186
+ ],
1187
+ preview: (doc) => previewPath(typeof doc.slug === "string" ? doc.slug : ""),
1188
+ ...previewButton ? { components: { edit: { PreviewButton: previewButton } } } : {}
1189
+ },
1190
+ access: {
1191
+ read: authenticatedOrPublished,
1192
+ readVersions: isAuthenticated,
1193
+ create: canManageContent,
1194
+ update: canManageContent,
1195
+ delete: isAdmin
1196
+ },
1197
+ versions: {
1198
+ drafts: { autosave: { interval: 375 } },
1199
+ maxPerDoc: 50
1200
+ },
1201
+ fields: [
1202
+ {
1203
+ name: "title",
1204
+ type: "text",
1205
+ required: true
1206
+ },
1207
+ slugField({
1208
+ collection: "pages",
1209
+ isReserved: isReservedSlug
1210
+ }),
1211
+ {
1212
+ name: "hero",
1213
+ type: "blocks",
1214
+ required: true,
1215
+ minRows: 1,
1216
+ maxRows: 1,
1217
+ blocks: heroBlocks,
1218
+ defaultValue: [{ blockType: defaultHero.slug }],
1219
+ admin: { description: "Every page opens with one hero. A page cannot be published without one." }
1220
+ },
1221
+ {
1222
+ name: "layout",
1223
+ type: "blocks",
1224
+ required: true,
1225
+ minRows: 1,
1226
+ blocks: layoutBlocks,
1227
+ labels: {
1228
+ singular: "Section",
1229
+ plural: "Sections"
1230
+ }
1231
+ }
1232
+ ]
1233
+ };
1234
+ }
1235
+ //#endregion
1236
+ //#region src/collections/users.ts
1237
+ const nameValidate = (value, options) => {
1238
+ if (options.operation === "update" && value === "" && options.previousValue === "") return true;
1239
+ return text(value, options);
1240
+ };
1241
+ function nameField(name) {
1242
+ return {
1243
+ name,
1244
+ type: "text",
1245
+ required: true,
1246
+ validate: nameValidate
1247
+ };
1248
+ }
1249
+ /** Refuses the save that would remove Users from the only privileged user. */
1250
+ const LAST_ADMIN_DEMOTE_MESSAGE = "Make another user an admin before removing it from this one.";
1251
+ /** Refuses the delete that would remove the only privileged user. */
1252
+ const LAST_ADMIN_DELETE_MESSAGE = "Make another user an admin before deleting this one.";
1253
+ const LAST_ADMIN_LOCK = "select pg_advisory_xact_lock(hashtext('users:last-admin'))";
1254
+ async function lockLastAdminDecision(req) {
1255
+ const { execute, sessions } = req.payload.db;
1256
+ const id = req.transactionID;
1257
+ const session = typeof id === "string" || typeof id === "number" ? sessions?.[id] : void 0;
1258
+ if (!session || typeof execute !== "function") return;
1259
+ await execute({
1260
+ db: session.db,
1261
+ raw: LAST_ADMIN_LOCK
1262
+ });
1263
+ }
1264
+ function privilegedRoles(matrix) {
1265
+ return matrix.filter((row) => row.users).map((row) => row.role);
1266
+ }
1267
+ function holdsPrivilegedRole(roles, matrix) {
1268
+ return Array.isArray(roles) && roles.some((role) => isPrivilegedRole(role, matrix));
1269
+ }
1270
+ function privilegedWhere(matrix) {
1271
+ const roles = privilegedRoles(matrix);
1272
+ if (roles.length === 0) return { id: { equals: "__none__" } };
1273
+ return { or: roles.map((role) => ({ roles: { contains: role } })) };
1274
+ }
1275
+ async function adminCount(req, excluding) {
1276
+ await lockLastAdminDecision(req);
1277
+ const holdsPrivileged = privilegedWhere(await getRolesMatrix(req));
1278
+ const { totalDocs } = await req.payload.count({
1279
+ collection: "users",
1280
+ overrideAccess: true,
1281
+ req,
1282
+ where: excluding === void 0 ? holdsPrivileged : { and: [holdsPrivileged, { id: { not_equals: excluding } }] }
1283
+ });
1284
+ return totalDocs;
1285
+ }
1286
+ const rolesValidate = async (value, options) => {
1287
+ const builtIn = select(value, options);
1288
+ if (builtIn !== true) return builtIn;
1289
+ if (options.operation !== "update" || options.id === void 0) return true;
1290
+ const matrix = await getRolesMatrix(options.req);
1291
+ if (holdsPrivilegedRole(value, matrix) || !holdsPrivilegedRole(options.previousValue, matrix)) return true;
1292
+ return await adminCount(options.req, options.id) > 0 ? true : LAST_ADMIN_DEMOTE_MESSAGE;
1293
+ };
1294
+ function createUsers({ secureCookies, rolesField }) {
1295
+ return {
1296
+ slug: "users",
1297
+ auth: {
1298
+ maxLoginAttempts: 5,
1299
+ lockTime: 600 * 1e3,
1300
+ tokenExpiration: 7200,
1301
+ cookies: {
1302
+ sameSite: "Lax",
1303
+ secure: secureCookies
1304
+ }
1305
+ },
1306
+ admin: {
1307
+ useAsTitle: "email",
1308
+ defaultColumns: [
1309
+ "email",
1310
+ "firstName",
1311
+ "lastName",
1312
+ "roles"
1313
+ ],
1314
+ hidden: ({ user }) => !isAdmin(user)
1315
+ },
1316
+ access: {
1317
+ create: isAdmin,
1318
+ delete: isAdmin,
1319
+ unlock: isAdmin,
1320
+ read: isAdminOrSelf,
1321
+ update: isAdminOrSelf
1322
+ },
1323
+ hooks: {
1324
+ beforeDelete: [async ({ id, req }) => {
1325
+ if (!isAdmin(await req.payload.findByID({
1326
+ collection: "users",
1327
+ id,
1328
+ depth: 0,
1329
+ disableErrors: true,
1330
+ overrideAccess: true,
1331
+ req
1332
+ })) || await adminCount(req, id) > 0) return;
1333
+ throw new APIError(LAST_ADMIN_DELETE_MESSAGE, 400);
1334
+ }],
1335
+ afterOperation: [async (arg) => {
1336
+ const { operation, req } = arg;
1337
+ const touchesRoles = (operation === "update" || operation === "updateByID") && arg.args.data?.roles !== void 0;
1338
+ const deletes = operation === "delete" || operation === "deleteByID";
1339
+ if ((touchesRoles || deletes) && await adminCount(req) === 0) throw new APIError(deletes ? LAST_ADMIN_DELETE_MESSAGE : LAST_ADMIN_DEMOTE_MESSAGE, 400);
1340
+ return arg.result;
1341
+ }]
1342
+ },
1343
+ fields: [{
1344
+ type: "row",
1345
+ fields: [nameField("firstName"), nameField("lastName")]
1346
+ }, {
1347
+ name: "roles",
1348
+ type: "select",
1349
+ hasMany: true,
1350
+ required: true,
1351
+ defaultValue: ["author"],
1352
+ options: roleSelectOptions(),
1353
+ admin: {
1354
+ components: { Field: rolesField ?? "@bison-lab/payload-core/admin#RolesField" },
1355
+ description: "One person can hold several, e.g. Author + Designer."
1356
+ },
1357
+ access: { update: isAdmin },
1358
+ validate: rolesValidate,
1359
+ hooks: { beforeValidate: [({ value }) => Array.isArray(value) ? normalizeStoredRoles(value) : value] }
1360
+ }]
1361
+ };
1362
+ }
1363
+ //#endregion
1364
+ //#region src/collections/media.ts
1365
+ /**
1366
+ * Uploads, readable by anyone: the public site serves these files. Writing is
1367
+ * an editorial action, deleting is not — removing an asset a live page
1368
+ * references breaks that page.
1369
+ */
1370
+ function createMedia({ staticDir = "media", mimeTypes = ["image/*"], imageSizes } = {}) {
1371
+ return {
1372
+ slug: "media",
1373
+ access: {
1374
+ read: () => true,
1375
+ create: canManageContent,
1376
+ update: canManageContent,
1377
+ delete: isAdmin
1378
+ },
1379
+ upload: {
1380
+ staticDir,
1381
+ mimeTypes,
1382
+ imageSizes
1383
+ },
1384
+ fields: [{
1385
+ name: "alt",
1386
+ type: "text",
1387
+ required: true
1388
+ }]
1389
+ };
1390
+ }
1391
+ //#endregion
1392
+ //#region src/plugins/admin-only-api-tab.ts
1393
+ function gateApiTab(entity) {
1394
+ const components = entity.admin?.components;
1395
+ const edit = components?.views?.edit;
1396
+ const api = edit && "api" in edit ? edit.api : void 0;
1397
+ return {
1398
+ ...entity,
1399
+ admin: {
1400
+ ...entity.admin,
1401
+ components: {
1402
+ ...components,
1403
+ views: {
1404
+ ...components?.views,
1405
+ edit: {
1406
+ ...edit,
1407
+ api: {
1408
+ ...api,
1409
+ tab: {
1410
+ ...api?.tab,
1411
+ condition: isDeveloperTab
1412
+ }
1413
+ }
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ };
1419
+ }
1420
+ /**
1421
+ * Gates the document API tab to Developer, config-wide. After plugins that
1422
+ * use `definePlugin` order (MCP is 10), so a collection those plugins
1423
+ * register is still covered.
1424
+ */
1425
+ const adminOnlyApiTab = definePlugin({
1426
+ slug: "admin-only-api-tab",
1427
+ order: 1e3,
1428
+ plugin: ({ config }) => ({
1429
+ ...config,
1430
+ collections: config.collections?.map(gateApiTab),
1431
+ globals: config.globals?.map(gateApiTab)
1432
+ })
1433
+ });
1434
+ //#endregion
1435
+ export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_MESSAGE, LOOK_FIELD, PAGE_EDITOR_SYSTEM_KEYS, ROLES, ROLES_FIELD, ROLES_MATRIX_FIELD, ROLES_SLUG, ROLE_LABELS, SHARE_IMAGE_SIZE, SYSTEM_COLOR_KEYS, THEME_APPEARANCE_FIELD, THEME_APPEARANCE_SLUG, THEME_COLORS_SLUG, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_DOCUMENT_CONTROLS, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_IDENTITY_FALLBACK, THEME_IDENTITY_SLUG, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, THEME_TYPOGRAPHY_SLUG, adminOnlyApiTab, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, colorTokenField, createBrandAssets, createMedia, createPages, createRoles, createTheme, createUsers, defaultRolesFieldValue, deleteLibraryColor, documentTitle, firstImageIn, getRolesMatrix, hasCapability, hasRole, isAdmin, isAdminOrSelf, isAuthenticated, isDeveloper, isDeveloperTab, isPrivilegedRole, isRole, lookField, noIndexField, normalizeSlug, normalizeStoredRoles, pageEditorLooks, pageEditorTokens, parseRolesMatrix, persistThemeChild, publishThemeChild, resolveThemeIdentity, rewriteColorToken, roleDescription, roleSelectOptions, sanitizeSvg, seedRoles, seedTheme, seoPlugin, slugField, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateRolesMatrix };
685
1436
 
686
1437
  //# sourceMappingURL=index.mjs.map