@bison-lab/payload-core 3.11.0 → 3.13.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/README.md +91 -19
- package/dist/admin.d.mts +39 -5
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +666 -10
- package/dist/admin.mjs.map +1 -1
- package/dist/{identity-DZOb3_Gk.mjs → identity-LAzSeorC.mjs} +48 -2
- package/dist/identity-LAzSeorC.mjs.map +1 -0
- package/dist/index.d.mts +469 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1200 -5
- package/dist/index.mjs.map +1 -1
- package/dist/looks-BbL309kO.mjs.map +1 -1
- package/dist/{looks-DsizRfFV.d.mts → looks-C6kTjvk3.d.mts} +23 -5
- package/dist/looks-C6kTjvk3.d.mts.map +1 -0
- package/dist/theme.d.mts +2 -2
- package/dist/theme.mjs +2 -2
- package/package.json +4 -4
- package/dist/identity-DZOb3_Gk.mjs.map +0 -1
- package/dist/looks-DsizRfFV.d.mts.map +0 -1
package/dist/admin.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { a as colorDocFromState, i as SYSTEM_COLOR_KEYS, n as pageEditorLooks, r as pageEditorTokens, s as stateFromColorDoc } from "./looks-BbL309kO.mjs";
|
|
3
|
-
import { FieldDescription, FieldError, FieldLabel, useConfig, useField, useForm, useFormFields } from "@payloadcms/ui";
|
|
3
|
+
import { ArrayField, CheckboxInput, FieldDescription, FieldError, FieldLabel, fieldBaseClass, useConfig, useField, useForm, useFormFields, useRowLabel } from "@payloadcms/ui";
|
|
4
4
|
import { GREY_SCALES, SHADE_STEPS, contrastForeground, contrastRatio, createColorScale, deriveDarkPalette, deriveLightPalette, hexToHSL, hslToString, includedShadeSteps, isThemeHex, presetHints, setColorScaleInclude } from "@bison-lab/tokens";
|
|
5
|
-
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
6
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { createPortal } from "react-dom";
|
|
8
8
|
import { findFont, fontFaceCss, isFontId } from "@bison-lab/fonts";
|
|
@@ -1010,15 +1010,19 @@ function deleteLibraryColor(doc, key, replacement) {
|
|
|
1010
1010
|
return next;
|
|
1011
1011
|
}
|
|
1012
1012
|
//#endregion
|
|
1013
|
+
//#region src/theme/color-usages.ts
|
|
1014
|
+
/** Theme-store REST path. LibraryField calls `/api/globals/theme` + this. */
|
|
1015
|
+
const COLOR_USAGES_PATH = "/color-usages";
|
|
1016
|
+
//#endregion
|
|
1013
1017
|
//#region src/admin/library-field.tsx
|
|
1014
1018
|
function slugify(value) {
|
|
1015
1019
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1016
1020
|
}
|
|
1017
1021
|
/**
|
|
1018
|
-
* Custom colors: the same scale controls as system colors.
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
+
* Custom colors: the same scale controls as system colors. Unused: the
|
|
1023
|
+
* Theme store's color-usages endpoint returns nothing and the row is
|
|
1024
|
+
* gone. In use: pick a live replacement; confirm rewrites stored tokens
|
|
1025
|
+
* then drops the row. Success and Destructive stay off that list.
|
|
1022
1026
|
*/
|
|
1023
1027
|
const LibraryField = ({ field, path, readOnly }) => {
|
|
1024
1028
|
const dataPath = typeof field.admin?.custom?.dataPath === "string" ? field.admin.custom.dataPath : path;
|
|
@@ -1027,14 +1031,21 @@ const LibraryField = ({ field, path, readOnly }) => {
|
|
|
1027
1031
|
hasRows: true
|
|
1028
1032
|
});
|
|
1029
1033
|
const { getDataByPath } = useForm();
|
|
1034
|
+
const { config } = useConfig();
|
|
1030
1035
|
const rows = asLibraryRows(value).length > 0 ? asLibraryRows(value) : asLibraryRows(getDataByPath?.(dataPath));
|
|
1031
1036
|
const [adding, setAdding] = useState(false);
|
|
1032
1037
|
const [name, setName] = useState("");
|
|
1033
1038
|
const [hex, setHex] = useState("");
|
|
1034
1039
|
const [sourceStep, setSourceStep] = useState(500);
|
|
1040
|
+
const [replacing, setReplacing] = useState(null);
|
|
1041
|
+
const [replacement, setReplacement] = useState("");
|
|
1042
|
+
const [busy, setBusy] = useState(false);
|
|
1035
1043
|
function write(next) {
|
|
1036
1044
|
setValue(next);
|
|
1037
1045
|
}
|
|
1046
|
+
function usagesUrl(key) {
|
|
1047
|
+
return `${config.serverURL ?? ""}${config.routes?.api ?? "/api"}/globals/theme${COLOR_USAGES_PATH}?key=${encodeURIComponent(key)}`;
|
|
1048
|
+
}
|
|
1038
1049
|
function add() {
|
|
1039
1050
|
const key = slugify(name);
|
|
1040
1051
|
if (!key || !isThemeHex(hex) || rows.some((row) => row.key === key)) return;
|
|
@@ -1048,9 +1059,42 @@ const LibraryField = ({ field, path, readOnly }) => {
|
|
|
1048
1059
|
setSourceStep(500);
|
|
1049
1060
|
setAdding(false);
|
|
1050
1061
|
}
|
|
1051
|
-
function remove(row) {
|
|
1052
|
-
|
|
1062
|
+
async function remove(row) {
|
|
1063
|
+
const key = row.key ?? "";
|
|
1064
|
+
const response = await fetch(usagesUrl(key), { credentials: "include" });
|
|
1065
|
+
if (!response.ok) return;
|
|
1066
|
+
const usages = (await response.json()).usages ?? [];
|
|
1067
|
+
if (usages.length === 0) {
|
|
1068
|
+
write(deleteLibraryColor({ colors: { library: rows } }, key).colors?.library ?? []);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
setReplacement("");
|
|
1072
|
+
setReplacing({
|
|
1073
|
+
row,
|
|
1074
|
+
usages
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
async function confirmReplace() {
|
|
1078
|
+
if (!replacing || !replacement || busy) return;
|
|
1079
|
+
const key = replacing.row.key ?? "";
|
|
1080
|
+
setBusy(true);
|
|
1081
|
+
try {
|
|
1082
|
+
if (!(await fetch(usagesUrl(key), {
|
|
1083
|
+
method: "POST",
|
|
1084
|
+
credentials: "include",
|
|
1085
|
+
headers: { "Content-Type": "application/json" },
|
|
1086
|
+
body: JSON.stringify({
|
|
1087
|
+
key,
|
|
1088
|
+
replacement
|
|
1089
|
+
})
|
|
1090
|
+
})).ok) return;
|
|
1091
|
+
write(deleteLibraryColor({ colors: { library: rows } }, key, replacement).colors?.library ?? []);
|
|
1092
|
+
setReplacing(null);
|
|
1093
|
+
} finally {
|
|
1094
|
+
setBusy(false);
|
|
1095
|
+
}
|
|
1053
1096
|
}
|
|
1097
|
+
const replacementOptions = replacing ? pageEditorLooks({ colors: { library: rows.filter((row) => row.key !== replacing.row.key) } }) : [];
|
|
1054
1098
|
return /* @__PURE__ */ jsxs("div", {
|
|
1055
1099
|
className: "field-type bl-library",
|
|
1056
1100
|
children: [
|
|
@@ -1136,6 +1180,69 @@ const LibraryField = ({ field, path, readOnly }) => {
|
|
|
1136
1180
|
]
|
|
1137
1181
|
})
|
|
1138
1182
|
}) }) : null,
|
|
1183
|
+
replacing ? /* @__PURE__ */ jsx(ThemePortal, { children: /* @__PURE__ */ jsx("div", {
|
|
1184
|
+
className: "bl-library__modal",
|
|
1185
|
+
role: "dialog",
|
|
1186
|
+
"aria-modal": "true",
|
|
1187
|
+
"aria-labelledby": "bl-library-replace-title",
|
|
1188
|
+
onClick: (event) => {
|
|
1189
|
+
if (event.target === event.currentTarget && !busy) setReplacing(null);
|
|
1190
|
+
},
|
|
1191
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1192
|
+
className: "bl-library__panel",
|
|
1193
|
+
children: [
|
|
1194
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1195
|
+
className: "bl-library__panel-head",
|
|
1196
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("h2", {
|
|
1197
|
+
id: "bl-library-replace-title",
|
|
1198
|
+
children: ["Replace ", replacing.row.label ?? replacing.row.key]
|
|
1199
|
+
}), /* @__PURE__ */ jsx("p", { children: replacing.usages.length === 1 ? "1 document uses this color. Pick a replacement at the same step." : `${replacing.usages.length} documents use this color. Pick a replacement at the same step.` })] }), /* @__PURE__ */ jsx("button", {
|
|
1200
|
+
type: "button",
|
|
1201
|
+
onClick: () => setReplacing(null),
|
|
1202
|
+
"aria-label": "Close",
|
|
1203
|
+
disabled: busy,
|
|
1204
|
+
children: "×"
|
|
1205
|
+
})]
|
|
1206
|
+
}),
|
|
1207
|
+
/* @__PURE__ */ jsx("ul", {
|
|
1208
|
+
className: "bl-library__usages",
|
|
1209
|
+
children: replacing.usages.map((usage, index) => /* @__PURE__ */ jsxs("li", { children: [
|
|
1210
|
+
usage.collection ?? usage.global,
|
|
1211
|
+
usage.id != null ? ` #${usage.id}` : "",
|
|
1212
|
+
" · ",
|
|
1213
|
+
usage.path,
|
|
1214
|
+
" · ",
|
|
1215
|
+
usage.token
|
|
1216
|
+
] }, `${usage.collection ?? usage.global}-${usage.id ?? ""}-${usage.path}-${index}`))
|
|
1217
|
+
}),
|
|
1218
|
+
/* @__PURE__ */ jsxs("label", { children: ["Replacement", /* @__PURE__ */ jsxs("select", {
|
|
1219
|
+
value: replacement,
|
|
1220
|
+
onChange: (event) => setReplacement(event.target.value),
|
|
1221
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
1222
|
+
value: "",
|
|
1223
|
+
children: "Select a color…"
|
|
1224
|
+
}), replacementOptions.map((option) => /* @__PURE__ */ jsx("option", {
|
|
1225
|
+
value: option.value,
|
|
1226
|
+
children: option.label
|
|
1227
|
+
}, option.value))]
|
|
1228
|
+
})] }),
|
|
1229
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1230
|
+
className: "bl-library__actions",
|
|
1231
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1232
|
+
type: "button",
|
|
1233
|
+
onClick: () => setReplacing(null),
|
|
1234
|
+
disabled: busy,
|
|
1235
|
+
children: "Cancel"
|
|
1236
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1237
|
+
type: "button",
|
|
1238
|
+
disabled: !replacement || busy,
|
|
1239
|
+
onClick: () => void confirmReplace(),
|
|
1240
|
+
children: "Replace and delete"
|
|
1241
|
+
})]
|
|
1242
|
+
})
|
|
1243
|
+
]
|
|
1244
|
+
})
|
|
1245
|
+
}) }) : null,
|
|
1139
1246
|
/* @__PURE__ */ jsx("div", {
|
|
1140
1247
|
className: "bl-library__list",
|
|
1141
1248
|
children: rows.map((row, index) => /* @__PURE__ */ jsx(ColorScaleCard, {
|
|
@@ -1152,7 +1259,7 @@ const LibraryField = ({ field, path, readOnly }) => {
|
|
|
1152
1259
|
};
|
|
1153
1260
|
write(copy);
|
|
1154
1261
|
},
|
|
1155
|
-
onDelete: readOnly ? void 0 : () => remove(row),
|
|
1262
|
+
onDelete: readOnly ? void 0 : () => void remove(row),
|
|
1156
1263
|
readOnly
|
|
1157
1264
|
}, row.key ?? index))
|
|
1158
1265
|
})
|
|
@@ -1173,6 +1280,8 @@ const CSS$3 = `
|
|
|
1173
1280
|
.bl-library__panel p{margin:.5rem 0 0;color:#475569;font-size:14px}
|
|
1174
1281
|
.bl-library__panel label{display:flex;flex-direction:column;gap:.4rem;margin-top:1rem;font-size:13px;font-weight:600}
|
|
1175
1282
|
.bl-library__panel input,.bl-library__panel select{min-height:2.25rem;border:1px solid #cbd5e1;border-radius:8px;padding:.35rem .5rem;background:#fff;color:#0f172a}
|
|
1283
|
+
.bl-library__usages{margin:1rem 0 0;padding-left:1.1rem;color:#475569;font-size:13px;font-weight:400}
|
|
1284
|
+
.bl-library__usages li{margin:.25rem 0}
|
|
1176
1285
|
.bl-library__actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:.5rem;margin-top:1rem}
|
|
1177
1286
|
.bl-library__actions button:last-child{border:0;background:#0f172a;color:#fff;font-weight:600}
|
|
1178
1287
|
.bl-library__actions button:last-child:disabled{opacity:.4;cursor:not-allowed}
|
|
@@ -2406,6 +2515,553 @@ const LookField = ({ field, path, readOnly }) => {
|
|
|
2406
2515
|
});
|
|
2407
2516
|
};
|
|
2408
2517
|
//#endregion
|
|
2409
|
-
|
|
2518
|
+
//#region src/roles/types.ts
|
|
2519
|
+
/**
|
|
2520
|
+
* The Roles Global a site's generated types will describe. Optional and
|
|
2521
|
+
* nullable, no index signature: a generated Global is assignable to this,
|
|
2522
|
+
* never the reverse.
|
|
2523
|
+
*/
|
|
2524
|
+
const ROLES = [
|
|
2525
|
+
"developer",
|
|
2526
|
+
"admin",
|
|
2527
|
+
"designer",
|
|
2528
|
+
"author"
|
|
2529
|
+
];
|
|
2530
|
+
const ROLE_LABELS = {
|
|
2531
|
+
developer: "Developer",
|
|
2532
|
+
admin: "Admin",
|
|
2533
|
+
designer: "Designer",
|
|
2534
|
+
author: "Author"
|
|
2535
|
+
};
|
|
2536
|
+
const CAPABILITIES = [
|
|
2537
|
+
"content",
|
|
2538
|
+
"brand",
|
|
2539
|
+
"publish",
|
|
2540
|
+
"users"
|
|
2541
|
+
];
|
|
2542
|
+
function isRole(value) {
|
|
2543
|
+
return typeof value === "string" && ROLES.includes(value);
|
|
2544
|
+
}
|
|
2545
|
+
/** Lowercase slug: `editor`, `site-editor`. Empty and punctuation are out. */
|
|
2546
|
+
function isRoleSlug(value) {
|
|
2547
|
+
return typeof value === "string" && /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
|
|
2548
|
+
}
|
|
2549
|
+
function roleLabel(role, label) {
|
|
2550
|
+
const trimmed = typeof label === "string" ? label.trim() : "";
|
|
2551
|
+
if (trimmed) return trimmed;
|
|
2552
|
+
return isRole(role) ? ROLE_LABELS[role] : role;
|
|
2553
|
+
}
|
|
2554
|
+
//#endregion
|
|
2555
|
+
//#region src/roles/matrix.ts
|
|
2556
|
+
const ROLES_SLUG = "roles";
|
|
2557
|
+
/**
|
|
2558
|
+
* Default rank and ticks. Brand is Designer only. The API tab is not a
|
|
2559
|
+
* column — it is locked to Developer in `isDeveloper`.
|
|
2560
|
+
*/
|
|
2561
|
+
const DEFAULT_ROLE_MATRIX = [
|
|
2562
|
+
{
|
|
2563
|
+
role: "developer",
|
|
2564
|
+
content: true,
|
|
2565
|
+
brand: true,
|
|
2566
|
+
publish: true,
|
|
2567
|
+
users: true
|
|
2568
|
+
},
|
|
2569
|
+
{
|
|
2570
|
+
role: "admin",
|
|
2571
|
+
content: true,
|
|
2572
|
+
brand: false,
|
|
2573
|
+
publish: true,
|
|
2574
|
+
users: true
|
|
2575
|
+
},
|
|
2576
|
+
{
|
|
2577
|
+
role: "designer",
|
|
2578
|
+
content: false,
|
|
2579
|
+
brand: true,
|
|
2580
|
+
publish: false,
|
|
2581
|
+
users: false
|
|
2582
|
+
},
|
|
2583
|
+
{
|
|
2584
|
+
role: "author",
|
|
2585
|
+
content: true,
|
|
2586
|
+
brand: false,
|
|
2587
|
+
publish: false,
|
|
2588
|
+
users: false
|
|
2589
|
+
}
|
|
2590
|
+
];
|
|
2591
|
+
const DEVELOPER_DESCRIPTION = "Everything Admin can do, plus the document API tab.";
|
|
2592
|
+
const MISSING_SEED_ROLES_MESSAGE = "Roles must include Developer, Admin, Designer, and Author.";
|
|
2593
|
+
const DUPLICATE_ROLE_SLUG_MESSAGE = "Each role needs a unique slug.";
|
|
2594
|
+
const CAPABILITY_LABELS = {
|
|
2595
|
+
content: "Content",
|
|
2596
|
+
brand: "Brand",
|
|
2597
|
+
publish: "Publish",
|
|
2598
|
+
users: "Users"
|
|
2599
|
+
};
|
|
2600
|
+
function seedRoleRows(extras = []) {
|
|
2601
|
+
return [...DEFAULT_ROLE_MATRIX.map((row) => ({
|
|
2602
|
+
...row,
|
|
2603
|
+
label: isRole(row.role) ? ROLE_LABELS[row.role] : row.role
|
|
2604
|
+
})), ...extras.map((extra) => ({ ...extra }))];
|
|
2605
|
+
}
|
|
2606
|
+
function defaultRolesFieldValue(extras = []) {
|
|
2607
|
+
return seedRoleRows(extras).map((row) => ({
|
|
2608
|
+
id: row.role,
|
|
2609
|
+
...row
|
|
2610
|
+
}));
|
|
2611
|
+
}
|
|
2612
|
+
function readRoleRow(item) {
|
|
2613
|
+
const role = "role" in item ? item.role : void 0;
|
|
2614
|
+
if (typeof role !== "string" || role.trim() === "") return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
2615
|
+
if (!isRoleSlug(role)) return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
2616
|
+
return {
|
|
2617
|
+
id: role,
|
|
2618
|
+
role,
|
|
2619
|
+
label: roleLabel(role, "label" in item && typeof item.label === "string" ? item.label : void 0),
|
|
2620
|
+
content: Boolean("content" in item && item.content),
|
|
2621
|
+
brand: Boolean("brand" in item && item.brand),
|
|
2622
|
+
publish: Boolean("publish" in item && item.publish),
|
|
2623
|
+
users: Boolean("users" in item && item.users)
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
function parseRolesMatrix(value) {
|
|
2627
|
+
if (!Array.isArray(value)) return {
|
|
2628
|
+
ok: false,
|
|
2629
|
+
message: MISSING_SEED_ROLES_MESSAGE
|
|
2630
|
+
};
|
|
2631
|
+
const rows = [];
|
|
2632
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2633
|
+
for (const item of value) {
|
|
2634
|
+
if (!item || typeof item !== "object") continue;
|
|
2635
|
+
const parsed = readRoleRow(item);
|
|
2636
|
+
if ("error" in parsed) return {
|
|
2637
|
+
ok: false,
|
|
2638
|
+
message: parsed.error
|
|
2639
|
+
};
|
|
2640
|
+
if (seen.has(parsed.role)) return {
|
|
2641
|
+
ok: false,
|
|
2642
|
+
message: DUPLICATE_ROLE_SLUG_MESSAGE
|
|
2643
|
+
};
|
|
2644
|
+
seen.add(parsed.role);
|
|
2645
|
+
rows.push(parsed);
|
|
2646
|
+
}
|
|
2647
|
+
if (ROLES.some((role) => !seen.has(role))) return {
|
|
2648
|
+
ok: false,
|
|
2649
|
+
message: MISSING_SEED_ROLES_MESSAGE
|
|
2650
|
+
};
|
|
2651
|
+
return {
|
|
2652
|
+
ok: true,
|
|
2653
|
+
rows
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
function roleSelectOptions(matrix = DEFAULT_ROLE_MATRIX) {
|
|
2657
|
+
return matrix.map((row) => ({
|
|
2658
|
+
label: roleLabel(row.role, row.label),
|
|
2659
|
+
value: row.role
|
|
2660
|
+
}));
|
|
2661
|
+
}
|
|
2662
|
+
/** Capability copy only. Developer names the API tab; nothing about MCP or seed. */
|
|
2663
|
+
function roleDescription(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
2664
|
+
if (role === "developer") return DEVELOPER_DESCRIPTION;
|
|
2665
|
+
const row = matrix.find((entry) => entry.role === role);
|
|
2666
|
+
if (!row) return "No capabilities";
|
|
2667
|
+
return CAPABILITIES.filter((capability) => row[capability]).map((capability) => CAPABILITY_LABELS[capability]).join(", ") || "No capabilities";
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* Seed ticks and package labels come back; extra rows stay as they are.
|
|
2671
|
+
*/
|
|
2672
|
+
function resetRolesMatrix(current) {
|
|
2673
|
+
const extras = [];
|
|
2674
|
+
if (Array.isArray(current)) for (const item of current) {
|
|
2675
|
+
if (!item || typeof item !== "object") continue;
|
|
2676
|
+
const parsed = readRoleRow(item);
|
|
2677
|
+
if ("error" in parsed || isRole(parsed.role)) continue;
|
|
2678
|
+
extras.push(parsed);
|
|
2679
|
+
}
|
|
2680
|
+
return [...defaultRolesFieldValue(), ...extras];
|
|
2681
|
+
}
|
|
2682
|
+
//#endregion
|
|
2683
|
+
//#region src/admin/roles-matrix-field.tsx
|
|
2684
|
+
const HIDE_ROW_CHROME = `.bl-roles-matrix .array-actions__add,.bl-roles-matrix .array-actions__duplicate,.bl-roles-matrix .array-actions__copy{display:none}.bl-roles-matrix__toolbar{display:flex;justify-content:flex-end;margin-block-end:.75rem}.bl-roles-matrix__reset{min-height:2.25rem;padding:.35rem .9rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px;background:transparent;color:inherit;font:inherit;cursor:pointer}body.bl-roles-matrix-seed-menu .array-actions__remove{display:none}`;
|
|
2685
|
+
function seedRoleFromTrigger(trigger) {
|
|
2686
|
+
let node = trigger.parentElement;
|
|
2687
|
+
while (node) {
|
|
2688
|
+
const slugs = node.querySelectorAll("[data-role-slug]");
|
|
2689
|
+
if (slugs.length === 1) return isRole(slugs[0]?.getAttribute("data-role-slug"));
|
|
2690
|
+
if (slugs.length > 1) return false;
|
|
2691
|
+
node = node.parentElement;
|
|
2692
|
+
}
|
|
2693
|
+
return false;
|
|
2694
|
+
}
|
|
2695
|
+
/**
|
|
2696
|
+
* Payload's array field: drag rank stays, add is for custom rows, seed
|
|
2697
|
+
* rows have no remove. Reset restores seed ticks and package labels.
|
|
2698
|
+
*/
|
|
2699
|
+
const RolesMatrixField = (props) => {
|
|
2700
|
+
const { setValue, value } = useField({ path: props.path });
|
|
2701
|
+
const rootRef = useRef(null);
|
|
2702
|
+
const seedMenu = useRef(false);
|
|
2703
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2704
|
+
className: "bl-roles-matrix",
|
|
2705
|
+
ref: rootRef,
|
|
2706
|
+
onClickCapture: useCallback((event) => {
|
|
2707
|
+
const target = event.target;
|
|
2708
|
+
if (!(target instanceof Element)) return;
|
|
2709
|
+
const trigger = target.closest(".array-actions__button");
|
|
2710
|
+
if (trigger) {
|
|
2711
|
+
const owner = trigger.closest(".array-field");
|
|
2712
|
+
seedMenu.current = owner !== null && owner === rootRef.current?.querySelector(".array-field") && seedRoleFromTrigger(trigger);
|
|
2713
|
+
document.body.classList.toggle("bl-roles-matrix-seed-menu", seedMenu.current);
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
if (!target.closest(".array-actions__remove")) return;
|
|
2717
|
+
if (!seedMenu.current) return;
|
|
2718
|
+
event.preventDefault();
|
|
2719
|
+
event.stopPropagation();
|
|
2720
|
+
}, []),
|
|
2721
|
+
children: [
|
|
2722
|
+
/* @__PURE__ */ jsx("style", {
|
|
2723
|
+
href: "bl-roles-matrix",
|
|
2724
|
+
precedence: "default",
|
|
2725
|
+
children: HIDE_ROW_CHROME
|
|
2726
|
+
}),
|
|
2727
|
+
/* @__PURE__ */ jsx("div", {
|
|
2728
|
+
className: "bl-roles-matrix__toolbar",
|
|
2729
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
2730
|
+
type: "button",
|
|
2731
|
+
className: "bl-roles-matrix__reset",
|
|
2732
|
+
onClick: () => setValue(resetRolesMatrix(value)),
|
|
2733
|
+
children: "Reset to defaults"
|
|
2734
|
+
})
|
|
2735
|
+
}),
|
|
2736
|
+
/* @__PURE__ */ jsx(ArrayField, { ...props })
|
|
2737
|
+
]
|
|
2738
|
+
});
|
|
2739
|
+
};
|
|
2740
|
+
//#endregion
|
|
2741
|
+
//#region src/admin/roles-row-label.tsx
|
|
2742
|
+
/**
|
|
2743
|
+
* Array row header. Shows the current display name, not “Role 01”.
|
|
2744
|
+
*/
|
|
2745
|
+
function RolesRowLabel() {
|
|
2746
|
+
const { data } = useRowLabel();
|
|
2747
|
+
const role = typeof data.role === "string" ? data.role : "";
|
|
2748
|
+
const label = roleLabel(role, data.label) || "Role";
|
|
2749
|
+
return /* @__PURE__ */ jsx("span", {
|
|
2750
|
+
"data-role-slug": role,
|
|
2751
|
+
children: label
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
//#endregion
|
|
2755
|
+
//#region src/admin/role-slug-field.tsx
|
|
2756
|
+
/**
|
|
2757
|
+
* Seed slugs stay read-only. A custom row's slug is typed here.
|
|
2758
|
+
*/
|
|
2759
|
+
const RoleSlugField = ({ field, path, readOnly }) => {
|
|
2760
|
+
const { value, setValue, showError, errorMessage } = useField({ path });
|
|
2761
|
+
const locked = Boolean(readOnly) || isRole(value);
|
|
2762
|
+
const required = Boolean(field.required);
|
|
2763
|
+
const id = `field-${path.replace(/\./g, "__")}`;
|
|
2764
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2765
|
+
className: [
|
|
2766
|
+
fieldBaseClass,
|
|
2767
|
+
"text",
|
|
2768
|
+
locked && "read-only"
|
|
2769
|
+
].filter(Boolean).join(" "),
|
|
2770
|
+
children: [
|
|
2771
|
+
/* @__PURE__ */ jsx(FieldLabel, {
|
|
2772
|
+
htmlFor: id,
|
|
2773
|
+
label: field.label,
|
|
2774
|
+
required
|
|
2775
|
+
}),
|
|
2776
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2777
|
+
className: `${fieldBaseClass}__wrap`,
|
|
2778
|
+
children: [/* @__PURE__ */ jsx(FieldError, {
|
|
2779
|
+
message: errorMessage,
|
|
2780
|
+
path,
|
|
2781
|
+
showError
|
|
2782
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
2783
|
+
id,
|
|
2784
|
+
"data-role-slug": value ?? "",
|
|
2785
|
+
type: "text",
|
|
2786
|
+
autoComplete: "off",
|
|
2787
|
+
spellCheck: false,
|
|
2788
|
+
readOnly: locked,
|
|
2789
|
+
disabled: locked,
|
|
2790
|
+
value: value ?? "",
|
|
2791
|
+
onChange: (event) => {
|
|
2792
|
+
if (!locked) setValue(event.target.value);
|
|
2793
|
+
}
|
|
2794
|
+
})]
|
|
2795
|
+
}),
|
|
2796
|
+
/* @__PURE__ */ jsx(FieldDescription, {
|
|
2797
|
+
description: field.admin?.description,
|
|
2798
|
+
path
|
|
2799
|
+
})
|
|
2800
|
+
]
|
|
2801
|
+
});
|
|
2802
|
+
};
|
|
2803
|
+
//#endregion
|
|
2804
|
+
//#region src/admin/roles-field.tsx
|
|
2805
|
+
const GRID_CSS$1 = `.bl-roles__grid{display:grid;gap:.75rem;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));margin-block:.35rem 0}.bl-roles__option{display:flex;flex-direction:column;gap:.25rem;padding:.65rem .75rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px}.bl-roles__option.is-checked{border-color:var(--theme-elevation-400,#94a3b8)}.bl-roles__option.is-included{opacity:.7}.bl-roles__what{font-size:.8rem;line-height:1.35;color:var(--theme-elevation-500,#64748b)}`;
|
|
2806
|
+
function optionValue(option) {
|
|
2807
|
+
return typeof option === "string" ? option : String(option.value);
|
|
2808
|
+
}
|
|
2809
|
+
function optionLabel(option) {
|
|
2810
|
+
return typeof option === "string" ? option : String(option.label);
|
|
2811
|
+
}
|
|
2812
|
+
function useOfferedRoles(fallback) {
|
|
2813
|
+
const { config } = useConfig();
|
|
2814
|
+
const [live, setLive] = useState(null);
|
|
2815
|
+
const api = config?.routes?.api;
|
|
2816
|
+
const serverURL = config?.serverURL ?? "";
|
|
2817
|
+
useEffect(() => {
|
|
2818
|
+
if (!api) return;
|
|
2819
|
+
let cancelled = false;
|
|
2820
|
+
fetch(`${serverURL}${api}/globals/${ROLES_SLUG}?depth=0`, { credentials: "include" }).then((response) => response.ok ? response.json() : null).then((doc) => {
|
|
2821
|
+
if (cancelled || !doc) return;
|
|
2822
|
+
const parsed = parseRolesMatrix(doc.roles);
|
|
2823
|
+
if (parsed.ok) setLive(roleSelectOptions(parsed.rows));
|
|
2824
|
+
}).catch(() => void 0);
|
|
2825
|
+
return () => {
|
|
2826
|
+
cancelled = true;
|
|
2827
|
+
};
|
|
2828
|
+
}, [api, serverURL]);
|
|
2829
|
+
return live ?? fallback;
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Users Roles checklist. Order and labels come from the field options (the
|
|
2833
|
+
* Roles Global, or the seed plus extras). Copy is `roleDescription`.
|
|
2834
|
+
* Developer is exclusive; a custom role does not clear Designer.
|
|
2835
|
+
*/
|
|
2836
|
+
const RolesField = ({ field, path: pathFromProps, readOnly, validate }) => {
|
|
2837
|
+
const { admin: { description } = {}, label, localized, options = [], required } = field;
|
|
2838
|
+
const offered = useOfferedRoles((options.length ? options : roleSelectOptions()).map((option) => ({
|
|
2839
|
+
value: optionValue(option),
|
|
2840
|
+
label: optionLabel(option)
|
|
2841
|
+
})));
|
|
2842
|
+
const { disabled, errorMessage, path, setValue, showError, value } = useField({
|
|
2843
|
+
potentiallyStalePath: pathFromProps,
|
|
2844
|
+
validate: useCallback((value, validationOptions) => {
|
|
2845
|
+
if (typeof validate === "function") return validate(value, {
|
|
2846
|
+
...validationOptions,
|
|
2847
|
+
hasMany: true,
|
|
2848
|
+
options,
|
|
2849
|
+
required
|
|
2850
|
+
});
|
|
2851
|
+
return true;
|
|
2852
|
+
}, [
|
|
2853
|
+
validate,
|
|
2854
|
+
options,
|
|
2855
|
+
required
|
|
2856
|
+
])
|
|
2857
|
+
});
|
|
2858
|
+
const selected = Array.isArray(value) ? value : [];
|
|
2859
|
+
const exclusive = selected.includes("developer") ? "developer" : null;
|
|
2860
|
+
const locked = Boolean(readOnly) || disabled;
|
|
2861
|
+
function toggle(role) {
|
|
2862
|
+
if (locked) return;
|
|
2863
|
+
if (role === "developer") {
|
|
2864
|
+
setValue(selected.includes(role) ? [] : ["developer"]);
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
if (exclusive) return;
|
|
2868
|
+
setValue(selected.includes(role) ? selected.filter((entry) => entry !== role) : [...selected, role]);
|
|
2869
|
+
}
|
|
2870
|
+
const groupLabel = typeof label === "string" ? label : "Roles";
|
|
2871
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2872
|
+
className: [
|
|
2873
|
+
fieldBaseClass,
|
|
2874
|
+
"select",
|
|
2875
|
+
"bl-roles",
|
|
2876
|
+
showError && "error",
|
|
2877
|
+
locked && "read-only"
|
|
2878
|
+
].filter(Boolean).join(" "),
|
|
2879
|
+
id: `field-${path.replace(/\./g, "__")}`,
|
|
2880
|
+
children: [
|
|
2881
|
+
/* @__PURE__ */ jsx("style", {
|
|
2882
|
+
href: "bl-roles",
|
|
2883
|
+
precedence: "default",
|
|
2884
|
+
children: GRID_CSS$1
|
|
2885
|
+
}),
|
|
2886
|
+
/* @__PURE__ */ jsx(FieldLabel, {
|
|
2887
|
+
as: "span",
|
|
2888
|
+
label,
|
|
2889
|
+
localized,
|
|
2890
|
+
path,
|
|
2891
|
+
required
|
|
2892
|
+
}),
|
|
2893
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2894
|
+
className: `${fieldBaseClass}__wrap`,
|
|
2895
|
+
children: [/* @__PURE__ */ jsx(FieldError, {
|
|
2896
|
+
message: errorMessage,
|
|
2897
|
+
path,
|
|
2898
|
+
showError
|
|
2899
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2900
|
+
"aria-label": groupLabel,
|
|
2901
|
+
className: "bl-roles__grid",
|
|
2902
|
+
role: "group",
|
|
2903
|
+
children: offered.map((option) => {
|
|
2904
|
+
const role = option.value;
|
|
2905
|
+
if (!role) return null;
|
|
2906
|
+
const included = exclusive !== null && role !== exclusive;
|
|
2907
|
+
const checked = selected.includes(role);
|
|
2908
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2909
|
+
className: [
|
|
2910
|
+
"bl-roles__option",
|
|
2911
|
+
checked && "is-checked",
|
|
2912
|
+
included && "is-included"
|
|
2913
|
+
].filter(Boolean).join(" "),
|
|
2914
|
+
children: [/* @__PURE__ */ jsx(CheckboxInput, {
|
|
2915
|
+
checked,
|
|
2916
|
+
id: `${path}-${role}`,
|
|
2917
|
+
label: option.label || roleLabel(role),
|
|
2918
|
+
onToggle: () => toggle(role),
|
|
2919
|
+
readOnly: locked || included
|
|
2920
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2921
|
+
className: "bl-roles__what",
|
|
2922
|
+
children: included ? "Included in Developer" : roleDescription(role)
|
|
2923
|
+
})]
|
|
2924
|
+
}, role);
|
|
2925
|
+
})
|
|
2926
|
+
})]
|
|
2927
|
+
}),
|
|
2928
|
+
/* @__PURE__ */ jsx(FieldDescription, {
|
|
2929
|
+
description,
|
|
2930
|
+
path
|
|
2931
|
+
})
|
|
2932
|
+
]
|
|
2933
|
+
});
|
|
2934
|
+
};
|
|
2935
|
+
//#endregion
|
|
2936
|
+
//#region src/features/types.ts
|
|
2937
|
+
/**
|
|
2938
|
+
* Package catalogue. Locked rows cannot be turned off. Empty Global falls
|
|
2939
|
+
* back to the named capability (Content → Pages/Media, Brand → Theme /
|
|
2940
|
+
* Brand assets, Users → Users/Roles/Features).
|
|
2941
|
+
*/
|
|
2942
|
+
const PACKAGE_FEATURES = [
|
|
2943
|
+
{
|
|
2944
|
+
slug: "pages",
|
|
2945
|
+
label: "Pages",
|
|
2946
|
+
fallback: "content",
|
|
2947
|
+
locked: false
|
|
2948
|
+
},
|
|
2949
|
+
{
|
|
2950
|
+
slug: "media",
|
|
2951
|
+
label: "Media",
|
|
2952
|
+
fallback: "content",
|
|
2953
|
+
locked: false
|
|
2954
|
+
},
|
|
2955
|
+
{
|
|
2956
|
+
slug: "theme",
|
|
2957
|
+
label: "Theme",
|
|
2958
|
+
fallback: "brand",
|
|
2959
|
+
locked: false
|
|
2960
|
+
},
|
|
2961
|
+
{
|
|
2962
|
+
slug: "brand-assets",
|
|
2963
|
+
label: "Brand assets",
|
|
2964
|
+
fallback: "brand",
|
|
2965
|
+
locked: false
|
|
2966
|
+
},
|
|
2967
|
+
{
|
|
2968
|
+
slug: "users",
|
|
2969
|
+
label: "Users",
|
|
2970
|
+
fallback: "users",
|
|
2971
|
+
locked: true
|
|
2972
|
+
},
|
|
2973
|
+
{
|
|
2974
|
+
slug: "roles",
|
|
2975
|
+
label: "Roles",
|
|
2976
|
+
fallback: "users",
|
|
2977
|
+
locked: true
|
|
2978
|
+
},
|
|
2979
|
+
{
|
|
2980
|
+
slug: "features",
|
|
2981
|
+
label: "Features",
|
|
2982
|
+
fallback: "users",
|
|
2983
|
+
locked: true
|
|
2984
|
+
}
|
|
2985
|
+
];
|
|
2986
|
+
function isLockedFeature(slug, locked) {
|
|
2987
|
+
if (locked === true) return true;
|
|
2988
|
+
return PACKAGE_FEATURES.find((feature) => feature.slug === slug)?.locked === true;
|
|
2989
|
+
}
|
|
2990
|
+
//#endregion
|
|
2991
|
+
//#region src/admin/features-matrix-field.tsx
|
|
2992
|
+
const GRID_CSS = `.bl-features{margin-block:.35rem 0;overflow:auto}.bl-features table{width:100%;border-collapse:collapse;font:inherit}.bl-features th,.bl-features td{padding:.45rem .6rem;border-bottom:1px solid var(--theme-elevation-150,#e2e8f0);text-align:center;white-space:nowrap}.bl-features th:first-child,.bl-features td:first-child{text-align:left;font-weight:600}.bl-features th{font-size:.8rem;color:var(--theme-elevation-500,#64748b)}.bl-features input{inline-size:1rem;block-size:1rem}`;
|
|
2993
|
+
function asRows(value) {
|
|
2994
|
+
return Array.isArray(value) ? value : [];
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Feature × role grid. Columns come from the Roles Global (or the seed).
|
|
2998
|
+
* Locked rows and Developer cannot be cleared.
|
|
2999
|
+
*/
|
|
3000
|
+
const FeaturesMatrixField = (props) => {
|
|
3001
|
+
const { setValue, value } = useField({ path: props.path });
|
|
3002
|
+
const { config } = useConfig();
|
|
3003
|
+
const [columns, setColumns] = useState(roleSelectOptions(seedRoleRows()));
|
|
3004
|
+
const api = config?.routes?.api;
|
|
3005
|
+
const serverURL = config?.serverURL ?? "";
|
|
3006
|
+
useEffect(() => {
|
|
3007
|
+
if (!api) return;
|
|
3008
|
+
let cancelled = false;
|
|
3009
|
+
fetch(`${serverURL}${api}/globals/${ROLES_SLUG}?depth=0`, { credentials: "include" }).then((response) => response.ok ? response.json() : null).then((doc) => {
|
|
3010
|
+
if (cancelled || !doc) return;
|
|
3011
|
+
const parsed = parseRolesMatrix(doc.roles);
|
|
3012
|
+
if (parsed.ok) setColumns(roleSelectOptions(parsed.rows));
|
|
3013
|
+
}).catch(() => void 0);
|
|
3014
|
+
return () => {
|
|
3015
|
+
cancelled = true;
|
|
3016
|
+
};
|
|
3017
|
+
}, [api, serverURL]);
|
|
3018
|
+
const rows = asRows(value);
|
|
3019
|
+
function toggle(slug, role, checked) {
|
|
3020
|
+
if (role === "developer") return;
|
|
3021
|
+
setValue(rows.map((row) => {
|
|
3022
|
+
if (row.slug !== slug || isLockedFeature(row.slug, row.locked)) return row;
|
|
3023
|
+
const roles = new Set(row.roles ?? []);
|
|
3024
|
+
if (checked) roles.add(role);
|
|
3025
|
+
else roles.delete(role);
|
|
3026
|
+
roles.add("developer");
|
|
3027
|
+
return {
|
|
3028
|
+
...row,
|
|
3029
|
+
roles: [...roles]
|
|
3030
|
+
};
|
|
3031
|
+
}));
|
|
3032
|
+
}
|
|
3033
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3034
|
+
className: "bl-features",
|
|
3035
|
+
children: [/* @__PURE__ */ jsx("style", {
|
|
3036
|
+
href: "bl-features",
|
|
3037
|
+
precedence: "default",
|
|
3038
|
+
children: GRID_CSS
|
|
3039
|
+
}), /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", {
|
|
3040
|
+
scope: "col",
|
|
3041
|
+
children: "Feature"
|
|
3042
|
+
}), columns.map((column) => /* @__PURE__ */ jsx("th", {
|
|
3043
|
+
scope: "col",
|
|
3044
|
+
children: column.label
|
|
3045
|
+
}, column.value))] }) }), /* @__PURE__ */ jsx("tbody", { children: rows.map((row) => {
|
|
3046
|
+
const locked = isLockedFeature(row.slug, row.locked);
|
|
3047
|
+
return /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", {
|
|
3048
|
+
scope: "row",
|
|
3049
|
+
children: row.label || row.slug
|
|
3050
|
+
}), columns.map((column) => {
|
|
3051
|
+
const disabled = locked || column.value === "developer";
|
|
3052
|
+
const checked = column.value === "developer" || locked || Boolean(row.roles?.includes(column.value));
|
|
3053
|
+
return /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("input", {
|
|
3054
|
+
type: "checkbox",
|
|
3055
|
+
"aria-label": `${row.label || row.slug} · ${column.label}`,
|
|
3056
|
+
checked,
|
|
3057
|
+
disabled,
|
|
3058
|
+
onChange: (event) => toggle(row.slug, column.value, event.target.checked)
|
|
3059
|
+
}) }, column.value);
|
|
3060
|
+
})] }, row.slug);
|
|
3061
|
+
}) })] })]
|
|
3062
|
+
});
|
|
3063
|
+
};
|
|
3064
|
+
//#endregion
|
|
3065
|
+
export { AppearanceField, ColorField, ColorScaleField, ContrastReport, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PairingField, PublishChild, RoleSlugField, RolesField, RolesMatrixField, RolesRowLabel, SectionHeading, ThemeDocumentControls, ThemeSaveButton };
|
|
2410
3066
|
|
|
2411
3067
|
//# sourceMappingURL=admin.mjs.map
|