@bison-lab/payload-core 3.14.0 → 3.15.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,6 +1,5 @@
1
- import { $ as defaultFeaturesFieldValue, A as ROLES_SLUG, B as CAPABILITIES, C as DUPLICATE_ROLE_NAME_MESSAGE, D as MISSING_SEED_ROLES_MESSAGE, E as LAST_USERS_TICK_MESSAGE, F as resetRolesMatrix, G as THEME_FEATURE_SLUGS, H as DEFAULT_ROLE_GRANTS, I as roleDescription, J as isRoleSlug, K as isRole, L as roleSelectOptions, M as defaultRolesFieldValue, N as normalizeStoredRoles, O as ROLES_FIELD_DESCRIPTION, P as parseRolesMatrix, Q as MISSING_PACKAGE_FEATURES_MESSAGE, R as seedRoleRows, S as DEVELOPER_DESCRIPTION, T as INVALID_ROLE_NAME_MESSAGE, U as ROLES, V as CAPABILITY_FEATURES, W as ROLE_LABELS, X as sanitizeRoleNameInput, Y as roleLabel, Z as slugifyRoleName, _ as isDeveloper, a as hideUnlessFeature, at as FEATURES_SLUG, b as storedRoles, c as canManageContent, ct as PACKAGE_FEATURES, d as hasCapability, dt as isFeatureSlug, et as featureCatalogue, f as hasGrant, ft as isLockedFeature, g as isAuthenticated, h as isAdminOrSelf, i as hasFeature, it as validateFeaturesMatrix, j as applyRoleSlugsFromNames, k as ROLES_GLOBAL_DESCRIPTION, l as canPublish, lt as PACKAGE_FEATURE_SLUGS, m as isAdmin, n as canUseFeature, nt as parseFeaturesMatrix, o as authenticatedOrPublished, ot as FEATURE_GROUPS, p as hasRole, pt as isPackageFeatureSlug, q as isRoleName, r as getFeaturesMatrix, rt as stampFeatureCatalogue, s as canManageBrand, st as FEATURE_GROUP_LABELS, tt as featureGroupLabel, u as getRolesMatrix, ut as isFeatureGroupId, v as isDeveloperTab, w as DUPLICATE_ROLE_SLUG_MESSAGE, x as DEFAULT_ROLE_MATRIX, y as isPrivilegedRole, z as validateRolesMatrix } from "./access-B3LglN2J.mjs";
2
- import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-BlFAJLnd.mjs";
3
- import { A as THEME_FONT_FIELD, C as validateThemeHex, D as THEME_COLOR_SCALE_FIELD, E as THEME_COLOR_FIELD, F as THEME_PUBLISH_FIELD, I as THEME_SAVE_BUTTON, L as THEME_SECTION_HEADING, M as THEME_IDENTITY_FALLBACK, N as THEME_LIBRARY_FIELD, O as THEME_CONTRAST_REPORT, P as THEME_PAIRING_FIELD, R as headingSelectValue, S as validateSourceIncluded, T as THEME_APPEARANCE_FIELD, _ as THEME_SLUG, a as pageEditorLooks, c as findColorTokens, d as rewriteColorToken, f as themeColorKeys, g as THEME_IDENTITY_SLUG, h as THEME_COLORS_SLUG, i as PAGE_EDITOR_SYSTEM_KEYS, j as THEME_GREY_SCALE_FIELD, k as THEME_DOCUMENT_CONTROLS, l as rewriteColorTokens, m as THEME_APPEARANCE_SLUG, n as colorTokenField, o as pageEditorTokens, p as SYSTEM_COLOR_KEYS, r as lookField, s as themeLibraryFromDoc, t as resolveThemeIdentity, u as deleteLibraryColor, v as THEME_TYPOGRAPHY_SLUG, w as LOOK_FIELD, x as themeConfigFromDoc, y as docFromConfig } from "./identity-Dwyq8519.mjs";
1
+ import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-C4ILz4p2.mjs";
2
+ import { A as THEME_FONT_FIELD, C as validateThemeHex, D as THEME_COLOR_SCALE_FIELD, E as THEME_COLOR_FIELD, F as THEME_PUBLISH_FIELD, I as THEME_SAVE_BUTTON, L as THEME_SECTION_HEADING, M as THEME_IDENTITY_FALLBACK, N as THEME_LIBRARY_FIELD, O as THEME_CONTRAST_REPORT, P as THEME_PAIRING_FIELD, R as headingSelectValue, S as validateSourceIncluded, T as THEME_APPEARANCE_FIELD, _ as THEME_SLUG, a as pageEditorLooks, c as findColorTokens, d as rewriteColorToken, f as themeColorKeys, g as THEME_IDENTITY_SLUG, h as THEME_COLORS_SLUG, i as PAGE_EDITOR_SYSTEM_KEYS, j as THEME_GREY_SCALE_FIELD, k as THEME_DOCUMENT_CONTROLS, l as rewriteColorTokens, m as THEME_APPEARANCE_SLUG, n as colorTokenField, o as pageEditorTokens, p as SYSTEM_COLOR_KEYS, r as lookField, s as themeLibraryFromDoc, t as resolveThemeIdentity, u as deleteLibraryColor, v as THEME_TYPOGRAPHY_SLUG, w as LOOK_FIELD, x as themeConfigFromDoc, y as docFromConfig } from "./identity-LAzSeorC.mjs";
4
3
  import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
5
4
  import { catalog } from "@bison-lab/fonts";
6
5
  import { DESTRUCTIVE_SCALE_HEX, SHADE_STEPS, presetHints } from "@bison-lab/tokens";
@@ -304,75 +303,664 @@ async function readJson(req) {
304
303
  return null;
305
304
  }
306
305
  //#endregion
307
- //#region src/theme/publish.ts
306
+ //#region src/features/types.ts
307
+ const FEATURES_SLUG = "features";
308
+ const FEATURE_GROUPS = [
309
+ "pages",
310
+ "media",
311
+ "theme",
312
+ "users"
313
+ ];
314
+ const FEATURE_GROUP_LABELS = {
315
+ pages: "Pages",
316
+ media: "Media",
317
+ theme: "Theme",
318
+ users: "Users"
319
+ };
320
+ const PACKAGE_FEATURE_SLUGS = [
321
+ "content",
322
+ "publish",
323
+ "media",
324
+ "theme-colors",
325
+ "theme-typography",
326
+ "theme-appearance",
327
+ "theme-identity",
328
+ "brand-assets",
329
+ "users",
330
+ "roles"
331
+ ];
308
332
  /**
309
- * Publishing a child writes that slice only. Other responsibilities stay
310
- * as they already are on the stored document. Unsaved form state on a
311
- * different child never goes live. A first save (no stored row) still
312
- * writes only the named child; `getPublishedTheme` fills the rest from
313
- * the seed.
333
+ * Package catalogue. Features itself is not a row that screen is
334
+ * Developer-only in code. Empty Global falls back to these defaults.
314
335
  */
315
- function publishThemeChild(stored, incoming, child) {
316
- if (child === "colors") return {
317
- ...stored,
318
- colors: incoming.colors
336
+ const PACKAGE_FEATURES = [
337
+ {
338
+ slug: "content",
339
+ label: "Content",
340
+ group: "pages",
341
+ defaultReleased: true
342
+ },
343
+ {
344
+ slug: "publish",
345
+ label: "Publish",
346
+ group: "pages",
347
+ defaultReleased: true
348
+ },
349
+ {
350
+ slug: "media",
351
+ label: "Media",
352
+ group: "media",
353
+ defaultReleased: true
354
+ },
355
+ {
356
+ slug: "theme-colors",
357
+ label: "Colors",
358
+ group: "theme",
359
+ defaultReleased: true
360
+ },
361
+ {
362
+ slug: "theme-typography",
363
+ label: "Typography",
364
+ group: "theme",
365
+ defaultReleased: true
366
+ },
367
+ {
368
+ slug: "theme-appearance",
369
+ label: "Appearance",
370
+ group: "theme",
371
+ defaultReleased: true
372
+ },
373
+ {
374
+ slug: "theme-identity",
375
+ label: "Identity",
376
+ group: "theme",
377
+ defaultReleased: true
378
+ },
379
+ {
380
+ slug: "brand-assets",
381
+ label: "Brand assets",
382
+ group: "theme",
383
+ defaultReleased: true
384
+ },
385
+ {
386
+ slug: "users",
387
+ label: "Users",
388
+ group: "users",
389
+ defaultReleased: true
390
+ },
391
+ {
392
+ slug: "roles",
393
+ label: "Roles",
394
+ group: "users",
395
+ defaultReleased: true
396
+ }
397
+ ];
398
+ function isFeatureGroupId(value) {
399
+ return typeof value === "string" && FEATURE_GROUPS.includes(value);
400
+ }
401
+ function isPackageFeatureSlug(value) {
402
+ return typeof value === "string" && PACKAGE_FEATURE_SLUGS.includes(value);
403
+ }
404
+ function isFeatureSlug(value) {
405
+ return typeof value === "string" && /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
406
+ }
407
+ /** @deprecated Locks are gone; the release switch is the valve. */
408
+ function isLockedFeature(_slug, _locked) {
409
+ return false;
410
+ }
411
+ //#endregion
412
+ //#region src/features/matrix.ts
413
+ const MISSING_PACKAGE_FEATURES_MESSAGE = "Features must include Content, Publish, Media, Theme screens, Brand assets, Users, and Roles.";
414
+ function featureCatalogue(extras = []) {
415
+ return [...PACKAGE_FEATURES.map((feature) => ({ ...feature })), ...extras.map((extra) => ({
416
+ slug: extra.slug,
417
+ label: extra.label,
418
+ group: extra.group ?? "users",
419
+ defaultReleased: extra.defaultReleased ?? false
420
+ }))];
421
+ }
422
+ function defaultFeaturesFieldValue(extras = []) {
423
+ return featureCatalogue(extras).map((feature) => ({
424
+ id: feature.slug,
425
+ slug: feature.slug,
426
+ label: feature.label,
427
+ group: feature.group,
428
+ released: feature.defaultReleased
429
+ }));
430
+ }
431
+ function stampFeatureCatalogue(value, extras = []) {
432
+ const bySlug = new Map(featureCatalogue(extras).map((feature) => [feature.slug, feature]));
433
+ if (!Array.isArray(value)) return defaultFeaturesFieldValue(extras);
434
+ return value.flatMap((item) => {
435
+ if (!item || typeof item !== "object") return [];
436
+ const slug = "slug" in item && typeof item.slug === "string" ? item.slug : "";
437
+ const feature = bySlug.get(slug);
438
+ if (!feature) return [];
439
+ return [{
440
+ id: feature.slug,
441
+ slug: feature.slug,
442
+ label: feature.label,
443
+ group: feature.group,
444
+ released: Boolean("released" in item && item.released)
445
+ }];
446
+ });
447
+ }
448
+ function parseFeaturesMatrix(value) {
449
+ if (!Array.isArray(value) || value.length === 0) return {
450
+ ok: false,
451
+ message: MISSING_PACKAGE_FEATURES_MESSAGE
319
452
  };
320
- if (child === "typography") return {
321
- ...stored,
322
- typography: incoming.typography
453
+ const rows = [];
454
+ const seen = /* @__PURE__ */ new Set();
455
+ for (const item of value) {
456
+ if (!item || typeof item !== "object") continue;
457
+ const slug = "slug" in item ? item.slug : void 0;
458
+ if (!isFeatureSlug(slug) || seen.has(slug)) continue;
459
+ seen.add(slug);
460
+ const pack = PACKAGE_FEATURES.find((feature) => feature.slug === slug);
461
+ const label = "label" in item && typeof item.label === "string" ? item.label : pack?.label ?? slug;
462
+ const group = "group" in item && isFeatureGroupId(item.group) ? item.group : pack?.group ?? "users";
463
+ rows.push({
464
+ id: slug,
465
+ slug,
466
+ label,
467
+ group,
468
+ released: Boolean("released" in item && item.released)
469
+ });
470
+ }
471
+ if (PACKAGE_FEATURES.some((feature) => !seen.has(feature.slug))) return {
472
+ ok: false,
473
+ message: MISSING_PACKAGE_FEATURES_MESSAGE
323
474
  };
324
- if (child === "appearance") return {
325
- ...stored,
326
- appearance: incoming.appearance
475
+ return {
476
+ ok: true,
477
+ rows
327
478
  };
479
+ }
480
+ function validateFeaturesMatrix(value, extras = []) {
481
+ const parsed = parseFeaturesMatrix(value);
482
+ if (!parsed.ok) return parsed.message;
483
+ const allowed = new Set(featureCatalogue(extras).map((feature) => feature.slug));
484
+ if (parsed.rows.some((row) => !allowed.has(row.slug))) return MISSING_PACKAGE_FEATURES_MESSAGE;
485
+ return true;
486
+ }
487
+ function featureGroupLabel(group) {
488
+ return isFeatureGroupId(group) ? FEATURE_GROUP_LABELS[group] : group;
489
+ }
490
+ function packageFeatureLabel(slug, extras = []) {
491
+ const pack = PACKAGE_FEATURES.find((feature) => feature.slug === slug);
492
+ if (pack) return pack.label;
493
+ return extras.find((feature) => feature.slug === slug)?.label ?? slug;
494
+ }
495
+ function isGroupOnlySlug(slug) {
496
+ return isFeatureGroupId(slug) && !PACKAGE_FEATURES.some((feature) => feature.slug === slug);
497
+ }
498
+ //#endregion
499
+ //#region src/roles/types.ts
500
+ /**
501
+ * The Roles Global a site's generated types will describe. Optional and
502
+ * nullable, no index signature: a generated Global is assignable to this,
503
+ * never the reverse.
504
+ */
505
+ const ROLES = [
506
+ "developer",
507
+ "admin",
508
+ "designer",
509
+ "author"
510
+ ];
511
+ const ROLE_LABELS = {
512
+ developer: "Developer",
513
+ admin: "Admin",
514
+ designer: "Designer",
515
+ author: "Author"
516
+ };
517
+ /** Capability aliases over feature grants. Brand is any Theme-group leaf. */
518
+ const CAPABILITIES = [
519
+ "content",
520
+ "brand",
521
+ "publish",
522
+ "users"
523
+ ];
524
+ const THEME_FEATURE_SLUGS = [
525
+ "theme-colors",
526
+ "theme-typography",
527
+ "theme-appearance",
528
+ "theme-identity",
529
+ "brand-assets"
530
+ ];
531
+ const CAPABILITY_FEATURES = {
532
+ content: ["content"],
533
+ publish: ["publish"],
534
+ users: ["users"],
535
+ brand: THEME_FEATURE_SLUGS
536
+ };
537
+ const DEFAULT_ROLE_GRANTS = {
538
+ developer: [],
539
+ admin: [
540
+ "content",
541
+ "publish",
542
+ "media",
543
+ "users",
544
+ "roles"
545
+ ],
546
+ designer: [
547
+ "content",
548
+ "media",
549
+ ...THEME_FEATURE_SLUGS,
550
+ "users",
551
+ "roles"
552
+ ],
553
+ author: ["content", "media"]
554
+ };
555
+ function isRole(value) {
556
+ return typeof value === "string" && ROLES.includes(value);
557
+ }
558
+ /** Lowercase slug from a letters-and-spaces name: `designer`, `the-designer`. */
559
+ function isRoleSlug(value) {
560
+ return typeof value === "string" && /^[a-z]+(-[a-z]+)*$/.test(value);
561
+ }
562
+ /** Display name: letters and single spaces only. `The Greatest Designer`. */
563
+ function isRoleName(value) {
564
+ return typeof value === "string" && /^[A-Za-z]+(?: [A-Za-z]+)*$/.test(value.trim());
565
+ }
566
+ /** Strip digits and punctuation as the name is typed. Trailing space stays. */
567
+ function sanitizeRoleNameInput(value) {
568
+ return value.replace(/[^A-Za-z ]/g, "").replace(/ {2,}/g, " ");
569
+ }
570
+ /** Name → stored key. `The Greatest Designer` → `the-greatest-designer`. */
571
+ function slugifyRoleName(value) {
572
+ if (typeof value !== "string") return "";
573
+ return value.trim().toLowerCase().replace(/[^a-z]+/g, "-").replace(/^-|-$/g, "");
574
+ }
575
+ function roleLabel(role, label) {
576
+ const trimmed = typeof label === "string" ? label.trim() : "";
577
+ if (trimmed) return trimmed;
578
+ return isRole(role) ? ROLE_LABELS[role] : role;
579
+ }
580
+ function defaultGrantsForRole(role, extraGrants) {
581
+ if (isRole(role)) return [...DEFAULT_ROLE_GRANTS[role]];
582
+ return extraGrants ? [...extraGrants] : [];
583
+ }
584
+ //#endregion
585
+ //#region src/roles/matrix.ts
586
+ const ROLES_SLUG = "roles";
587
+ const ROLES_GLOBAL_DESCRIPTION = "Who may do what. Drag to change rank.";
588
+ const ROLES_FIELD_DESCRIPTION = "Drag to change rank. Ticks are features released on Features. Developer has the whole catalogue.";
589
+ /**
590
+ * Default rank and grants. Developer is implicit (empty grants). Brand
591
+ * screens default to Designer.
592
+ */
593
+ const DEFAULT_ROLE_MATRIX = ROLES.map((role) => ({
594
+ role,
595
+ grants: defaultGrantsForRole(role)
596
+ }));
597
+ const DEVELOPER_DESCRIPTION = "Everything, including features not released for assignment.";
598
+ const LAST_USERS_TICK_MESSAGE = "Keep Users granted on at least one of Admin or Developer.";
599
+ const MISSING_SEED_ROLES_MESSAGE = "Roles must include Developer, Admin, Designer, and Author.";
600
+ const DUPLICATE_ROLE_SLUG_MESSAGE = "Each role needs a unique slug.";
601
+ const DUPLICATE_ROLE_NAME_MESSAGE = "Each role needs a unique name.";
602
+ const INVALID_ROLE_NAME_MESSAGE = "Use letters and spaces only.";
603
+ function uniqueSlugs(values) {
604
+ const slugs = [];
605
+ for (const value of values) if (typeof value === "string" && isFeatureSlug(value) && !slugs.includes(value)) slugs.push(value);
606
+ return slugs;
607
+ }
608
+ /** Read stored grants, or rebuild them from the old capability ticks. */
609
+ function grantsFromStoredRole(item) {
610
+ if ("grants" in item && Array.isArray(item.grants)) return uniqueSlugs(item.grants);
611
+ const grants = [];
612
+ if ("content" in item && item.content) grants.push("content", "media");
613
+ if ("publish" in item && item.publish) grants.push("publish");
614
+ if ("users" in item && item.users) grants.push("users", "roles");
615
+ if ("brand" in item && item.brand) grants.push(...THEME_FEATURE_SLUGS);
616
+ return uniqueSlugs(grants);
617
+ }
618
+ function seedRoleRows(extras = []) {
619
+ return [...DEFAULT_ROLE_MATRIX.map((row) => ({
620
+ ...row,
621
+ grants: [...row.grants],
622
+ label: isRole(row.role) ? ROLE_LABELS[row.role] : row.role
623
+ })), ...extras.map((extra) => ({
624
+ role: extra.role,
625
+ label: extra.label,
626
+ grants: extra.grants ? [...extra.grants] : []
627
+ }))];
628
+ }
629
+ function defaultRolesFieldValue(extras = []) {
630
+ return seedRoleRows(extras).map((row) => ({
631
+ id: row.role,
632
+ ...row
633
+ }));
634
+ }
635
+ function readRoleRow(item) {
636
+ const role = "role" in item ? item.role : void 0;
637
+ if (typeof role !== "string" || role.trim() === "") return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
638
+ if (!isRoleSlug(role)) return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
328
639
  return {
329
- ...stored,
330
- logo: incoming.logo,
331
- favicon: incoming.favicon,
332
- logoMark: incoming.logoMark
640
+ id: role,
641
+ role,
642
+ label: roleLabel(role, "label" in item && typeof item.label === "string" ? item.label : void 0),
643
+ grants: grantsFromStoredRole(item)
333
644
  };
334
645
  }
335
- function sliceFromTheme(theme, child) {
336
- if (child === "colors") return { colors: theme.colors };
337
- if (child === "typography") return { typography: theme.typography };
338
- if (child === "appearance") return { appearance: theme.appearance };
646
+ function parseRolesMatrix(value) {
647
+ if (!Array.isArray(value)) return {
648
+ ok: false,
649
+ message: MISSING_SEED_ROLES_MESSAGE
650
+ };
651
+ const rows = [];
652
+ const seen = /* @__PURE__ */ new Set();
653
+ for (const item of value) {
654
+ if (!item || typeof item !== "object") continue;
655
+ const role = "role" in item ? item.role : void 0;
656
+ if (typeof role !== "string" || role.trim() === "") continue;
657
+ const parsed = readRoleRow(item);
658
+ if ("error" in parsed) return {
659
+ ok: false,
660
+ message: parsed.error
661
+ };
662
+ if (seen.has(parsed.role)) return {
663
+ ok: false,
664
+ message: DUPLICATE_ROLE_SLUG_MESSAGE
665
+ };
666
+ seen.add(parsed.role);
667
+ rows.push(parsed);
668
+ }
669
+ if (ROLES.some((role) => !seen.has(role))) return {
670
+ ok: false,
671
+ message: MISSING_SEED_ROLES_MESSAGE
672
+ };
339
673
  return {
340
- logo: theme.logo,
341
- favicon: theme.favicon,
342
- logoMark: theme.logoMark
674
+ ok: true,
675
+ rows
343
676
  };
344
677
  }
345
- function colorHex(color) {
346
- if (typeof color === "string") return color;
347
- return color?.hex ?? void 0;
678
+ function roleHasGrant(row, slug) {
679
+ if (row.role === "developer") return true;
680
+ return row.grants.includes(slug);
681
+ }
682
+ function roleHasCapability(row, capability) {
683
+ if (row.role === "developer") return true;
684
+ return CAPABILITY_FEATURES[capability].some((slug) => row.grants.includes(slug));
348
685
  }
349
686
  /**
350
- * A Theme page is a real Global. Hydrate from the store only when this
351
- * page has never been savedotherwise afterRead would overwrite the form.
687
+ * Mint a slug from the name only when the row has none yet. An existing
688
+ * key seed or customstays put so a rename cannot orphan users.
352
689
  */
353
- function hasThemeSlice(doc, child) {
354
- if (!doc) return false;
355
- if (child === "colors") return Boolean(colorHex(doc.colors?.brand?.primary));
356
- if (child === "typography") return Boolean(doc.typography?.body);
357
- if (child === "appearance") return Boolean(doc.appearance?.radius);
358
- return Boolean(doc.logo || doc.favicon || doc.logoMark);
690
+ function applyRoleSlugsFromNames(value) {
691
+ if (!Array.isArray(value)) return value;
692
+ return value.map((item) => {
693
+ if (!item || typeof item !== "object") return item;
694
+ const row = item;
695
+ if (isRoleSlug(row.role)) return item;
696
+ const slug = slugifyRoleName(row.label);
697
+ return isRoleSlug(slug) ? {
698
+ ...row,
699
+ role: slug
700
+ } : item;
701
+ });
702
+ }
703
+ function hasDuplicateRoleNames(value) {
704
+ if (!Array.isArray(value)) return false;
705
+ const seen = /* @__PURE__ */ new Set();
706
+ for (const item of value) {
707
+ if (!item || typeof item !== "object") continue;
708
+ const role = "role" in item && typeof item.role === "string" ? item.role : "";
709
+ const name = ("label" in item && typeof item.label === "string" ? item.label : "").trim() || (isRole(role) ? ROLE_LABELS[role] : "");
710
+ if (!name) continue;
711
+ const key = name.toLowerCase();
712
+ if (seen.has(key)) return true;
713
+ seen.add(key);
714
+ }
715
+ return false;
716
+ }
717
+ function hasInvalidRoleName(value) {
718
+ if (!Array.isArray(value)) return false;
719
+ for (const item of value) {
720
+ if (!item || typeof item !== "object") continue;
721
+ const label = "label" in item && typeof item.label === "string" ? item.label.trim() : "";
722
+ if (!label) continue;
723
+ if (!isRoleName(label)) return true;
724
+ }
725
+ return false;
726
+ }
727
+ function validateRolesMatrix(value) {
728
+ if (hasDuplicateRoleNames(value)) return DUPLICATE_ROLE_NAME_MESSAGE;
729
+ if (hasInvalidRoleName(value)) return INVALID_ROLE_NAME_MESSAGE;
730
+ const parsed = parseRolesMatrix(applyRoleSlugsFromNames(value));
731
+ if (!parsed.ok) return parsed.message;
732
+ return true;
359
733
  }
360
734
  /**
361
- * Save on a Theme page writes that slice onto the hidden `theme` row.
362
- * Other children stay as stored.
735
+ * Developer is exclusive. Admin does not swallow Designer: both store.
736
+ * Unknown slugs (dropped catalogue keys such as `approver`) fall away unless
737
+ * they appear on the matrix.
363
738
  */
364
- async function persistThemeChild(payload, incoming, child) {
365
- const next = publishThemeChild(await payload.findGlobal({
366
- slug: "theme",
367
- draft: false,
368
- overrideAccess: true
369
- }) ?? {}, incoming, child);
370
- await payload.updateGlobal({
371
- slug: THEME_SLUG,
372
- data: next,
373
- draft: false
739
+ function normalizeStoredRoles(value, matrix = DEFAULT_ROLE_MATRIX) {
740
+ if (!Array.isArray(value)) return [];
741
+ const allowed = new Set(matrix.map((row) => row.role));
742
+ const roles = [...new Set(value.filter((entry) => typeof entry === "string" && allowed.has(entry)))];
743
+ if (roles.includes("developer")) return ["developer"];
744
+ return roles;
745
+ }
746
+ function roleSelectOptions(matrix = DEFAULT_ROLE_MATRIX) {
747
+ return matrix.map((row) => ({
748
+ label: roleLabel(row.role, row.label),
749
+ value: row.role
750
+ }));
751
+ }
752
+ /** Grant copy only. Developer names the unreleased catalogue; nothing about MCP or seed. */
753
+ function roleDescription(role, matrix = DEFAULT_ROLE_MATRIX) {
754
+ if (role === "developer") return DEVELOPER_DESCRIPTION;
755
+ const row = matrix.find((entry) => entry.role === role);
756
+ if (!row) return "No capabilities";
757
+ return row.grants.map((slug) => packageFeatureLabel(slug)).join(", ") || "No capabilities";
758
+ }
759
+ /**
760
+ * Seed grants and package labels come back; extra rows stay as they are.
761
+ */
762
+ function resetRolesMatrix(current) {
763
+ const extras = [];
764
+ if (Array.isArray(current)) for (const item of current) {
765
+ if (!item || typeof item !== "object") continue;
766
+ const parsed = readRoleRow(item);
767
+ if ("error" in parsed || isRole(parsed.role)) continue;
768
+ extras.push(parsed);
769
+ }
770
+ return [...defaultRolesFieldValue(), ...extras];
771
+ }
772
+ //#endregion
773
+ //#region src/roles/access.ts
774
+ function isAccessArgs$1(value) {
775
+ return typeof value === "object" && value !== null && "req" in value;
776
+ }
777
+ function storedRoles(user) {
778
+ return Array.isArray(user?.roles) ? user.roles : [];
779
+ }
780
+ function hasRole(user, role) {
781
+ return storedRoles(user).includes(role);
782
+ }
783
+ /** Not a tick. True only when `developer` is stored on the row. */
784
+ function isDeveloper(user) {
785
+ return hasRole(user, "developer");
786
+ }
787
+ function hasCapability(user, capability, matrix = DEFAULT_ROLE_MATRIX) {
788
+ if (isDeveloper(user)) return true;
789
+ return storedRoles(user).some((role) => {
790
+ const row = matrix.find((entry) => entry.role === role);
791
+ return row ? roleHasCapability(row, capability) : false;
374
792
  });
375
- return next;
793
+ }
794
+ function hasGrant(user, slug, matrix = DEFAULT_ROLE_MATRIX) {
795
+ if (isDeveloper(user)) return true;
796
+ return storedRoles(user).some((role) => {
797
+ const row = matrix.find((entry) => entry.role === role);
798
+ return row ? roleHasGrant(row, slug) : false;
799
+ });
800
+ }
801
+ /**
802
+ * Reads the saved Roles Global, falling back to the seed when the row is
803
+ * empty, missing, or unreadable. Always override-access so a Designer
804
+ * evaluating Theme does not have to read Settings → Roles.
805
+ */
806
+ async function getRolesMatrix(req = {}, extras = []) {
807
+ const fallback = seedRoleRows(extras);
808
+ const findGlobal = req.payload?.findGlobal;
809
+ if (typeof findGlobal !== "function") return fallback;
810
+ try {
811
+ const doc = await findGlobal({
812
+ slug: ROLES_SLUG,
813
+ overrideAccess: true,
814
+ req
815
+ });
816
+ const parsed = parseRolesMatrix(doc && typeof doc === "object" && "roles" in doc ? doc.roles : void 0);
817
+ return parsed.ok ? parsed.rows : fallback;
818
+ } catch {
819
+ return fallback;
820
+ }
821
+ }
822
+ function capabilityPredicate(capability) {
823
+ function predicate(userOrArgs, matrix) {
824
+ if (isAccessArgs$1(userOrArgs)) {
825
+ const user = userOrArgs.req.user;
826
+ if (matrix) return hasCapability(user, capability, matrix);
827
+ return getRolesMatrix(userOrArgs.req).then((rows) => hasCapability(user, capability, rows));
828
+ }
829
+ return hasCapability(userOrArgs, capability, matrix ?? DEFAULT_ROLE_MATRIX);
830
+ }
831
+ return predicate;
832
+ }
833
+ const canManageContent = capabilityPredicate("content");
834
+ const canManageBrand = capabilityPredicate("brand");
835
+ const canPublish = capabilityPredicate("publish");
836
+ /** Users grant on any held role (Developer is implicit). */
837
+ const isAdmin = capabilityPredicate("users");
838
+ /** This role id currently has the Users grant, or is Developer. */
839
+ function isPrivilegedRole(role, matrix = DEFAULT_ROLE_MATRIX) {
840
+ if (typeof role !== "string") return false;
841
+ if (role === "developer") return true;
842
+ const row = matrix.find((entry) => entry.role === role);
843
+ return row ? roleHasGrant(row, "users") : false;
844
+ }
845
+ function isAuthenticated(userOrArgs) {
846
+ if (isAccessArgs$1(userOrArgs)) return Boolean(userOrArgs.req.user);
847
+ return Boolean(userOrArgs);
848
+ }
849
+ const isAdminOrSelf = async ({ req }) => {
850
+ if (!req.user) return false;
851
+ if (await isAdmin({ req })) return true;
852
+ return { id: { equals: req.user.id } };
853
+ };
854
+ const authenticatedOrPublished = ({ req: { user } }) => {
855
+ if (isAuthenticated(user)) return true;
856
+ return { _status: { equals: "published" } };
857
+ };
858
+ /**
859
+ * API tab condition. Code-locked to the `developer` slug — not a Features
860
+ * row and not a Role tick a client can grant.
861
+ */
862
+ function isDeveloperTab({ req }) {
863
+ return isDeveloper(req.user);
864
+ }
865
+ /** Access for chrome globals (Features, Better Editor settings). */
866
+ function developerOnlyAccess() {
867
+ return {
868
+ read: ({ req }) => isDeveloper(req.user),
869
+ update: ({ req }) => isDeveloper(req.user)
870
+ };
871
+ }
872
+ /** `admin.hidden`: hide unless the login is Developer. */
873
+ function hideUnlessDeveloper({ user }) {
874
+ return !isDeveloper(user);
875
+ }
876
+ //#endregion
877
+ //#region src/features/access.ts
878
+ function isAccessArgs(value) {
879
+ return typeof value === "object" && value !== null && "req" in value;
880
+ }
881
+ function grantedOnReleased(user, slug, row, matrix) {
882
+ if (!row.released) return false;
883
+ return hasGrant(user, slug, matrix);
884
+ }
885
+ /**
886
+ * True when the feature is released and a held role is granted it.
887
+ * Developer is always allowed. An empty Global falls back to catalogue
888
+ * defaults. A group slug (`pages`, `theme`) is true when any child is.
889
+ */
890
+ function hasFeature(user, slug, features = null, matrix = DEFAULT_ROLE_MATRIX) {
891
+ if (isDeveloper(user)) return true;
892
+ if (!user) return false;
893
+ const rows = features && features.length > 0 ? features : defaultFeaturesFieldValue();
894
+ const leaf = rows.find((entry) => entry.slug === slug);
895
+ if (leaf) return grantedOnReleased(user, slug, leaf, matrix);
896
+ if (!isGroupOnlySlug(slug)) return false;
897
+ return rows.some((entry) => entry.group === slug && grantedOnReleased(user, entry.slug, entry, matrix));
898
+ }
899
+ /**
900
+ * Reads the saved Features Global. `null` means empty or unreadable — callers
901
+ * fall back to catalogue defaults. Always override-access.
902
+ */
903
+ async function getFeaturesMatrix(req = {}) {
904
+ const findGlobal = req.payload?.findGlobal;
905
+ if (typeof findGlobal !== "function") return null;
906
+ try {
907
+ const doc = await findGlobal({
908
+ slug: FEATURES_SLUG,
909
+ overrideAccess: true,
910
+ req
911
+ });
912
+ const parsed = parseFeaturesMatrix(doc && typeof doc === "object" && "features" in doc ? doc.features : void 0);
913
+ return parsed.ok ? parsed.rows : null;
914
+ } catch {
915
+ return null;
916
+ }
917
+ }
918
+ /**
919
+ * Access / nav helper for one catalogue slug. Sync against a passed grid;
920
+ * async when given `req` so it can read both Globals.
921
+ */
922
+ function canUseFeature(slug) {
923
+ function predicate(userOrArgs, features, roles) {
924
+ if (isAccessArgs(userOrArgs)) {
925
+ const user = userOrArgs.req.user;
926
+ if (features !== void 0) return hasFeature(user, slug, features, roles ?? DEFAULT_ROLE_MATRIX);
927
+ return Promise.all([getFeaturesMatrix(userOrArgs.req), getRolesMatrix(userOrArgs.req)]).then(([grid, matrix]) => hasFeature(user, slug, grid, matrix));
928
+ }
929
+ return hasFeature(userOrArgs, slug, features ?? null, roles ?? DEFAULT_ROLE_MATRIX);
930
+ }
931
+ return predicate;
932
+ }
933
+ /**
934
+ * Feature slugs (and group slugs) this login may use. Written onto the
935
+ * user at afterRead so `admin.hidden({ user })` can follow the saved
936
+ * Features and Roles Globals without a `req`.
937
+ */
938
+ async function allowedFeaturesForUser(user, req = {}) {
939
+ const [features, roles] = await Promise.all([getFeaturesMatrix(req), getRolesMatrix(req)]);
940
+ return [...(features && features.length > 0 ? features : defaultFeaturesFieldValue()).map((row) => row.slug), ...FEATURE_GROUPS].filter((slug) => hasFeature(user, slug, features, roles));
941
+ }
942
+ function userHasAllowedFeature(user, slug) {
943
+ if (isDeveloper(user)) return true;
944
+ const allowed = user?.allowedFeatures;
945
+ if (!Array.isArray(allowed)) return false;
946
+ return allowed.includes(slug);
947
+ }
948
+ /** `admin.hidden`: hide when the login cannot use the feature. */
949
+ function hideUnlessFeature(slug) {
950
+ return (args) => {
951
+ const user = args.user ?? null;
952
+ if (isDeveloper(user)) return false;
953
+ if (user && Array.isArray(user.allowedFeatures)) return !userHasAllowedFeature(user, slug);
954
+ if (args.req) {
955
+ const result = canUseFeature(slug)({ req: {
956
+ ...args.req,
957
+ user
958
+ } });
959
+ if (result instanceof Promise) return result.then((ok) => !ok);
960
+ return !result;
961
+ }
962
+ return !canUseFeature(slug)(user);
963
+ };
376
964
  }
377
965
  //#endregion
378
966
  //#region src/theme/global.ts
@@ -677,35 +1265,11 @@ function identityFields(collection) {
677
1265
  }
678
1266
  ];
679
1267
  }
680
- function pageHooks(child) {
681
- return {
682
- beforeChange: [async ({ data, req }) => {
683
- if (req?.payload) await persistThemeChild(req.payload, data, child);
684
- return data;
685
- }],
686
- afterRead: [async ({ doc, req }) => {
687
- if (hasThemeSlice(doc, child)) return doc;
688
- const theme = await req?.payload?.findGlobal({
689
- slug: THEME_SLUG,
690
- draft: false,
691
- depth: child === "identity" ? 1 : 0,
692
- overrideAccess: true
693
- });
694
- if (!theme) return doc;
695
- return {
696
- ...doc,
697
- ...sliceFromTheme(theme, child)
698
- };
699
- }]
700
- };
701
- }
702
1268
  /**
703
- * Standard Payload Globals in an `admin.group: "Theme"` nav section —
704
- * the same pattern as Content and Settings. Each page is its own Global
705
- * (Save, fields, access). A hidden `theme` store is the published read
706
- * model `getPublishedTheme` already knows. Locking is off: these are
707
- * settings forms, not collaborative documents. No draft mode, no preview
708
- * pane.
1269
+ * One Theme Global. Colors, Typography, Appearance, and Identity are
1270
+ * tabs on that document. Save writes the whole form Payload hydrates
1271
+ * every tab from the live row, so an untouched tab does not revert.
1272
+ * Locking is off. No draft mode, no preview pane.
709
1273
  */
710
1274
  function createTheme(options) {
711
1275
  const access = requireAccess$1(options);
@@ -720,59 +1284,49 @@ function createTheme(options) {
720
1284
  const typeFields = typographyFields(seed, fonts, fontsBaseUrl);
721
1285
  const lookFields = appearanceFields(seed);
722
1286
  const markFields = logo ? identityFields(logo.collection) : [];
723
- const store = {
1287
+ return [{
724
1288
  slug: THEME_SLUG,
725
1289
  label: "Theme",
726
1290
  lockDocuments: false,
727
1291
  admin: {
728
- hidden: true,
1292
+ group: "Theme",
1293
+ hidden: hideUnlessFeature("theme"),
729
1294
  hideAPIURL: true,
730
- custom: adminCustom
1295
+ custom: adminCustom,
1296
+ components: { elements: { beforeDocumentControls: [THEME_DOCUMENT_CONTROLS] } }
731
1297
  },
732
1298
  access: {
733
1299
  read: access.read,
734
1300
  update: access.update
735
1301
  },
736
- fields: [
737
- ...colorFields,
738
- ...typeFields,
739
- ...lookFields,
740
- ...markFields
741
- ],
1302
+ fields: [{
1303
+ type: "tabs",
1304
+ tabs: [
1305
+ {
1306
+ label: "Colors",
1307
+ fields: colorFields
1308
+ },
1309
+ {
1310
+ label: "Typography",
1311
+ fields: typeFields
1312
+ },
1313
+ {
1314
+ label: "Appearance",
1315
+ fields: lookFields
1316
+ },
1317
+ ...logo ? [{
1318
+ label: "Identity",
1319
+ fields: markFields
1320
+ }] : []
1321
+ ]
1322
+ }],
742
1323
  endpoints: colorUsageEndpoints(access, colorUsages),
743
1324
  hooks: { afterChange: [async ({ doc }) => {
744
1325
  if (!onPublish) return;
745
1326
  const themeDoc = doc;
746
1327
  await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);
747
1328
  }] }
748
- };
749
- function page(slug, label, child, fields, preview) {
750
- return {
751
- slug,
752
- label,
753
- lockDocuments: false,
754
- admin: {
755
- group: "Theme",
756
- hidden: hideUnlessFeature("theme"),
757
- hideAPIURL: true,
758
- custom: adminCustom,
759
- components: preview ? { elements: { beforeDocumentControls: [THEME_DOCUMENT_CONTROLS] } } : void 0
760
- },
761
- access: {
762
- read: access.read,
763
- update: access.update
764
- },
765
- fields,
766
- hooks: pageHooks(child)
767
- };
768
- }
769
- return [
770
- store,
771
- page(THEME_COLORS_SLUG, "Colors", "colors", colorFields, true),
772
- page(THEME_TYPOGRAPHY_SLUG, "Typography", "typography", typeFields, true),
773
- page(THEME_APPEARANCE_SLUG, "Appearance", "appearance", lookFields, true),
774
- ...logo ? [page(THEME_IDENTITY_SLUG, "Identity", "identity", markFields, false)] : []
775
- ];
1329
+ }];
776
1330
  }
777
1331
  //#endregion
778
1332
  //#region src/brand-assets/sanitize.ts
@@ -840,6 +1394,7 @@ function createBrandAssets(options) {
840
1394
  admin: {
841
1395
  group: "Settings",
842
1396
  useAsTitle: "label",
1397
+ hidden: hideUnlessFeature("brand-assets"),
843
1398
  description: "Logo, favicon, and mobile-menu mark. SVG preferred; PNG and ICO allowed."
844
1399
  },
845
1400
  upload: {
@@ -921,6 +1476,52 @@ const THEME_PREVIEW_BREAKPOINTS = [
921
1476
  }
922
1477
  ];
923
1478
  //#endregion
1479
+ //#region src/theme/publish.ts
1480
+ /**
1481
+ * Publishing a child writes that slice only. Other responsibilities stay
1482
+ * as they already are on the stored document. Unsaved form state on a
1483
+ * different child never goes live. A first save (no stored row) still
1484
+ * writes only the named child; `getPublishedTheme` fills the rest from
1485
+ * the seed.
1486
+ */
1487
+ function publishThemeChild(stored, incoming, child) {
1488
+ if (child === "colors") return {
1489
+ ...stored,
1490
+ colors: incoming.colors
1491
+ };
1492
+ if (child === "typography") return {
1493
+ ...stored,
1494
+ typography: incoming.typography
1495
+ };
1496
+ if (child === "appearance") return {
1497
+ ...stored,
1498
+ appearance: incoming.appearance
1499
+ };
1500
+ return {
1501
+ ...stored,
1502
+ logo: incoming.logo,
1503
+ favicon: incoming.favicon,
1504
+ logoMark: incoming.logoMark
1505
+ };
1506
+ }
1507
+ /**
1508
+ * Save on a Theme page writes that slice onto the hidden `theme` row.
1509
+ * Other children stay as stored.
1510
+ */
1511
+ async function persistThemeChild(payload, incoming, child) {
1512
+ const next = publishThemeChild(await payload.findGlobal({
1513
+ slug: "theme",
1514
+ draft: false,
1515
+ overrideAccess: true
1516
+ }) ?? {}, incoming, child);
1517
+ await payload.updateGlobal({
1518
+ slug: THEME_SLUG,
1519
+ data: next,
1520
+ draft: false
1521
+ });
1522
+ return next;
1523
+ }
1524
+ //#endregion
924
1525
  //#region src/roles/fields.ts
925
1526
  const ROLES_MATRIX_FIELD = "@bison-lab/payload-core/admin#RolesMatrixField";
926
1527
  const ROLES_ROW_LABEL = "@bison-lab/payload-core/admin#RolesRowLabel";
@@ -1030,69 +1631,67 @@ const FEATURES_MATRIX_FIELD = "@bison-lab/payload-core/admin#FeaturesMatrixField
1030
1631
  */
1031
1632
  function createFeatures({ extras = [] } = {}) {
1032
1633
  const catalogue = featureCatalogue(extras);
1634
+ const featuresField = {
1635
+ name: "features",
1636
+ type: "array",
1637
+ label: "Features",
1638
+ labels: {
1639
+ singular: "Feature",
1640
+ plural: "Features"
1641
+ },
1642
+ minRows: catalogue.length,
1643
+ maxRows: catalogue.length,
1644
+ required: true,
1645
+ defaultValue: defaultFeaturesFieldValue(extras),
1646
+ validate: (value) => validateFeaturesMatrix(value, extras),
1647
+ hooks: { beforeChange: [({ value }) => {
1648
+ if (!Array.isArray(value)) return value;
1649
+ return stampFeatureCatalogue(value, extras);
1650
+ }] },
1651
+ admin: {
1652
+ components: { Field: FEATURES_MATRIX_FIELD },
1653
+ description: "Release a feature so it can be assigned on Roles. Off keeps it Developer-only.",
1654
+ initCollapsed: false
1655
+ },
1656
+ fields: [
1657
+ {
1658
+ name: "slug",
1659
+ type: "text",
1660
+ label: "Slug",
1661
+ required: true,
1662
+ admin: { readOnly: true }
1663
+ },
1664
+ {
1665
+ name: "label",
1666
+ type: "text",
1667
+ label: "Name",
1668
+ required: true,
1669
+ admin: { readOnly: true }
1670
+ },
1671
+ {
1672
+ name: "group",
1673
+ type: "text",
1674
+ label: "Group",
1675
+ admin: { hidden: true }
1676
+ },
1677
+ {
1678
+ name: "released",
1679
+ type: "checkbox",
1680
+ label: "Released",
1681
+ admin: { hidden: true }
1682
+ }
1683
+ ]
1684
+ };
1033
1685
  return {
1034
1686
  slug: FEATURES_SLUG,
1035
1687
  label: "Features",
1036
1688
  admin: {
1037
1689
  group: "Settings",
1038
- hidden: ({ user }) => !isDeveloper(user),
1690
+ hidden: hideUnlessDeveloper,
1039
1691
  description: "Which features other people may be granted on Roles."
1040
1692
  },
1041
- access: {
1042
- read: ({ req }) => isDeveloper(req.user),
1043
- update: ({ req }) => isDeveloper(req.user)
1044
- },
1045
- fields: [{
1046
- name: "features",
1047
- type: "array",
1048
- label: "Features",
1049
- labels: {
1050
- singular: "Feature",
1051
- plural: "Features"
1052
- },
1053
- minRows: catalogue.length,
1054
- maxRows: catalogue.length,
1055
- required: true,
1056
- defaultValue: defaultFeaturesFieldValue(extras),
1057
- validate: (value) => validateFeaturesMatrix(value, extras),
1058
- hooks: { beforeChange: [({ value }) => {
1059
- if (!Array.isArray(value)) return value;
1060
- return stampFeatureCatalogue(value, extras);
1061
- }] },
1062
- admin: {
1063
- components: { Field: FEATURES_MATRIX_FIELD },
1064
- description: "Release a feature so it can be assigned on Roles. Off keeps it Developer-only.",
1065
- initCollapsed: false
1066
- },
1067
- fields: [
1068
- {
1069
- name: "slug",
1070
- type: "text",
1071
- label: "Slug",
1072
- required: true,
1073
- admin: { readOnly: true }
1074
- },
1075
- {
1076
- name: "label",
1077
- type: "text",
1078
- label: "Name",
1079
- required: true,
1080
- admin: { readOnly: true }
1081
- },
1082
- {
1083
- name: "group",
1084
- type: "text",
1085
- label: "Group",
1086
- admin: { hidden: true }
1087
- },
1088
- {
1089
- name: "released",
1090
- type: "checkbox",
1091
- label: "Released",
1092
- admin: { hidden: true }
1093
- }
1094
- ]
1095
- }]
1693
+ access: developerOnlyAccess(),
1694
+ fields: [featuresField]
1096
1695
  };
1097
1696
  }
1098
1697
  //#endregion
@@ -1189,6 +1788,15 @@ function slugField({ collection, isReserved }) {
1189
1788
  //#endregion
1190
1789
  //#region src/collections/pages.ts
1191
1790
  /**
1791
+ * Content to create or save a draft; Publish to publish or edit a live
1792
+ * page. Without Publish, update is constrained to `_status: draft`.
1793
+ */
1794
+ const pagesUpdate = async (args) => {
1795
+ if (!await Promise.resolve(canUseFeature("content")(args))) return false;
1796
+ if (await Promise.resolve(canPublish(args))) return true;
1797
+ return { _status: { equals: "draft" } };
1798
+ };
1799
+ /**
1192
1800
  * CMS-managed pages: one required hero, then a reorderable body of sections.
1193
1801
  * The CMS owns copy and the order of sections; what a section looks like is
1194
1802
  * code-owned, which is why the blocks are an argument.
@@ -1215,7 +1823,7 @@ function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, pr
1215
1823
  read: authenticatedOrPublished,
1216
1824
  readVersions: isAuthenticated,
1217
1825
  create: canUseFeature("content"),
1218
- update: canUseFeature("content"),
1826
+ update: pagesUpdate,
1219
1827
  delete: isAdmin
1220
1828
  },
1221
1829
  versions: {
@@ -1374,27 +1982,45 @@ function createUsers({ secureCookies, rolesField, extras = [] }) {
1374
1982
  return arg.result;
1375
1983
  }]
1376
1984
  },
1377
- fields: [{
1378
- type: "row",
1379
- fields: [nameField("firstName"), nameField("lastName")]
1380
- }, {
1381
- name: "roles",
1382
- type: "select",
1383
- hasMany: true,
1384
- required: true,
1385
- defaultValue: ["author"],
1386
- options: roleSelectOptions(fallback),
1387
- admin: {
1388
- components: { Field: rolesField ?? "@bison-lab/payload-core/admin#RolesField" },
1389
- description: "One person can hold several, e.g. Author + Designer."
1985
+ fields: [
1986
+ {
1987
+ type: "row",
1988
+ fields: [nameField("firstName"), nameField("lastName")]
1390
1989
  },
1391
- access: { update: isAdmin },
1392
- validate: rolesValidate(extras),
1393
- hooks: { beforeValidate: [async ({ value, req }) => {
1394
- if (!Array.isArray(value)) return value;
1395
- return normalizeStoredRoles(value, await getRolesMatrix(req, extras));
1396
- }] }
1397
- }]
1990
+ {
1991
+ name: "roles",
1992
+ type: "select",
1993
+ hasMany: true,
1994
+ required: true,
1995
+ defaultValue: ["author"],
1996
+ saveToJWT: true,
1997
+ options: roleSelectOptions(fallback),
1998
+ admin: {
1999
+ components: { Field: rolesField ?? "@bison-lab/payload-core/admin#RolesField" },
2000
+ description: "One person can hold several, e.g. Author + Designer."
2001
+ },
2002
+ access: { update: isAdmin },
2003
+ validate: rolesValidate(extras),
2004
+ hooks: { beforeValidate: [async ({ value, req }) => {
2005
+ if (!Array.isArray(value)) return value;
2006
+ return normalizeStoredRoles(value, await getRolesMatrix(req, extras));
2007
+ }] }
2008
+ },
2009
+ {
2010
+ name: "allowedFeatures",
2011
+ type: "json",
2012
+ admin: {
2013
+ hidden: true,
2014
+ readOnly: true
2015
+ },
2016
+ access: { update: () => false },
2017
+ saveToJWT: true,
2018
+ hooks: { afterRead: [async ({ siblingData, data, req }) => allowedFeaturesForUser({
2019
+ id: siblingData?.id ?? data?.id,
2020
+ roles: siblingData?.roles ?? data?.roles
2021
+ }, req ?? {})] }
2022
+ }
2023
+ ]
1398
2024
  };
1399
2025
  }
1400
2026
  //#endregion
@@ -1470,6 +2096,6 @@ const adminOnlyApiTab = definePlugin({
1470
2096
  })
1471
2097
  });
1472
2098
  //#endregion
1473
- export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, CAPABILITY_FEATURES, DEFAULT_ROLE_GRANTS, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, DUPLICATE_ROLE_NAME_MESSAGE, DUPLICATE_ROLE_SLUG_MESSAGE, FEATURES_MATRIX_FIELD, FEATURES_SLUG, FEATURE_GROUPS, FEATURE_GROUP_LABELS, INVALID_ROLE_NAME_MESSAGE, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_MESSAGE, LOOK_FIELD, MISSING_PACKAGE_FEATURES_MESSAGE, MISSING_SEED_ROLES_MESSAGE, PACKAGE_FEATURES, PACKAGE_FEATURE_SLUGS, PAGE_EDITOR_SYSTEM_KEYS, ROLES, ROLES_FIELD, ROLES_FIELD_DESCRIPTION, ROLES_GLOBAL_DESCRIPTION, ROLES_GRANTS_FIELD, ROLES_MATRIX_FIELD, ROLES_ROW_LABEL, ROLES_SLUG, ROLE_LABELS, ROLE_NAME_FIELD, ROLE_SLUG_FIELD, 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_FEATURE_SLUGS, 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, applyRoleSlugsFromNames, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, canUseFeature, colorTokenField, createBrandAssets, createFeatures, createMedia, createPages, createRoles, createTheme, createUsers, defaultFeaturesFieldValue, defaultRolesFieldValue, deleteLibraryColor, documentTitle, featureCatalogue, featureGroupLabel, findColorTokens, findColorUsages, firstImageIn, getFeaturesMatrix, getRolesMatrix, hasCapability, hasFeature, hasGrant, hasRole, hideUnlessFeature, isAdmin, isAdminOrSelf, isAuthenticated, isDeveloper, isDeveloperTab, isFeatureGroupId, isFeatureSlug, isLockedFeature, isPackageFeatureSlug, isPrivilegedRole, isRole, isRoleName, isRoleSlug, lookField, noIndexField, normalizeSlug, normalizeStoredRoles, pageEditorLooks, pageEditorTokens, parseFeaturesMatrix, parseRolesMatrix, persistThemeChild, publishThemeChild, resetRolesMatrix, resolveThemeIdentity, rewriteColorToken, rewriteColorTokens, rewriteColorUsages, roleDescription, roleLabel, roleSelectOptions, sanitizeRoleNameInput, sanitizeSvg, seedFeatures, seedRoleRows, seedRoles, seedTheme, seoPlugin, slugField, slugifyRoleName, stampFeatureCatalogue, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateFeaturesMatrix, validateRolesMatrix };
2099
+ export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, CAPABILITY_FEATURES, DEFAULT_ROLE_GRANTS, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, DUPLICATE_ROLE_NAME_MESSAGE, DUPLICATE_ROLE_SLUG_MESSAGE, FEATURES_MATRIX_FIELD, FEATURES_SLUG, FEATURE_GROUPS, FEATURE_GROUP_LABELS, INVALID_ROLE_NAME_MESSAGE, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_MESSAGE, LOOK_FIELD, MISSING_PACKAGE_FEATURES_MESSAGE, MISSING_SEED_ROLES_MESSAGE, PACKAGE_FEATURES, PACKAGE_FEATURE_SLUGS, PAGE_EDITOR_SYSTEM_KEYS, ROLES, ROLES_FIELD, ROLES_FIELD_DESCRIPTION, ROLES_GLOBAL_DESCRIPTION, ROLES_GRANTS_FIELD, ROLES_MATRIX_FIELD, ROLES_ROW_LABEL, ROLES_SLUG, ROLE_LABELS, ROLE_NAME_FIELD, ROLE_SLUG_FIELD, 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_FEATURE_SLUGS, 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, allowedFeaturesForUser, applyRoleSlugsFromNames, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, canUseFeature, colorTokenField, createBrandAssets, createFeatures, createMedia, createPages, createRoles, createTheme, createUsers, defaultFeaturesFieldValue, defaultRolesFieldValue, deleteLibraryColor, developerOnlyAccess, documentTitle, featureCatalogue, featureGroupLabel, findColorTokens, findColorUsages, firstImageIn, getFeaturesMatrix, getRolesMatrix, hasCapability, hasFeature, hasGrant, hasRole, hideUnlessDeveloper, hideUnlessFeature, isAdmin, isAdminOrSelf, isAuthenticated, isDeveloper, isDeveloperTab, isFeatureGroupId, isFeatureSlug, isLockedFeature, isPackageFeatureSlug, isPrivilegedRole, isRole, isRoleName, isRoleSlug, lookField, noIndexField, normalizeSlug, normalizeStoredRoles, pageEditorLooks, pageEditorTokens, parseFeaturesMatrix, parseRolesMatrix, persistThemeChild, publishThemeChild, resetRolesMatrix, resolveThemeIdentity, rewriteColorToken, rewriteColorTokens, rewriteColorUsages, roleDescription, roleLabel, roleSelectOptions, sanitizeRoleNameInput, sanitizeSvg, seedFeatures, seedRoleRows, seedRoles, seedTheme, seoPlugin, slugField, slugifyRoleName, stampFeatureCatalogue, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateFeaturesMatrix, validateRolesMatrix };
1474
2100
 
1475
2101
  //# sourceMappingURL=index.mjs.map