@bison-lab/payload-core 3.25.0 → 3.26.1

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 CHANGED
@@ -321,9 +321,16 @@ read from the live Features and Roles Globals) so Payload's
321
321
  `createPages`, `createUsers`, `createMedia`, `slugField`,
322
322
  `createRedirects`, `adminOnlyApiTab`, and `documentTitleActions` ship from the same
323
323
  React-free entry. Pages take the
324
- site's hero and layout blocks, reserved-slug predicate, and preview
325
- path. The slug follows the title until first publish, then locks. Unlock
326
- warns in plain words and the same save writes a 308. `createRedirects()`
324
+ site's hero and layout blocks, reserved-slug predicate, preview
325
+ path, and optional `folderRoots` (`patients`, `providers`, …). Parent
326
+ on the page editor is where the page lives: site root, those roots,
327
+ published CMS pages, folders created by earlier saves, or New folder….
328
+ The address is that folder plus a title leaf until first publish, then
329
+ locks. Layout carries the same standing copy as the hero — a page cannot
330
+ be published without a section — and Publish names that fix instead of
331
+ only `Content → Layout`. Title-follow does not wipe the folder prefix. A taken address is
332
+ refused — never rewritten to `slug-2`. Unlock + a parent change writes a
333
+ 308 the same way a typed slug change does. `createRedirects()`
327
334
  wraps `@payloadcms/plugin-redirects` for `pages` only (permanent 308) and
328
335
  collapses chains when a published slug changes. The plugin stores the
329
336
  rows; the site still has to serve them. Pin the plugin to the same
package/dist/admin.d.mts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { ThemeConfig } from "@bison-lab/tokens";
3
3
  import { ReactNode } from "react";
4
4
  import * as react_jsx_runtime0 from "react/jsx-runtime";
5
- import { ArrayFieldClientComponent, GroupFieldClientComponent, JSONFieldClientComponent, SelectFieldClientComponent, TextFieldClientComponent, UIFieldClientComponent } from "payload";
5
+ import { ArrayFieldClientComponent, DefaultCellComponentProps, GroupFieldClientComponent, JSONFieldClientComponent, SelectFieldClientComponent, TextFieldClientComponent, UIFieldClientComponent } from "payload";
6
6
 
7
7
  //#region src/admin/color-field.d.ts
8
8
  /**
@@ -128,11 +128,20 @@ declare const RoleSlugField: TextFieldClientComponent;
128
128
  //#endregion
129
129
  //#region src/admin/slug-field.d.ts
130
130
  /**
131
- * Slug follows the title until first publish. After that it is read-only
132
- * until Unlock, which warns that the old address will redirect.
131
+ * Parent, new-folder name, composed address, and Unlock. Slug follows the
132
+ * title leaf until first publish; the folder prefix is not wiped. After
133
+ * publish the cluster locks until Unlock, which warns that the old address
134
+ * will redirect.
133
135
  */
134
136
  declare const SlugField: TextFieldClientComponent;
135
137
  //#endregion
138
+ //#region src/admin/page-title-cell.d.ts
139
+ /**
140
+ * Indent a Pages list title by how deep its slug sits under a parent folder.
141
+ * DefaultCell still draws the document link — a custom Cell replaces that wrap.
142
+ */
143
+ declare function PageTitleCell(props: DefaultCellComponentProps): react_jsx_runtime0.JSX.Element;
144
+ //#endregion
136
145
  //#region src/admin/role-name-field.d.ts
137
146
  /**
138
147
  * Role name. Letters and spaces only, so the hidden slug stays
@@ -221,5 +230,5 @@ declare function AdminNavItemRowLabel(): react_jsx_runtime0.JSX.Element;
221
230
  */
222
231
  declare const AdminNavEntityField: TextFieldClientComponent;
223
232
  //#endregion
224
- export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, SlugField, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
233
+ export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PageTitleCell, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, SlugField, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
225
234
  //# sourceMappingURL=admin.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/color-field.tsx","../src/admin/color-scale-field.tsx","../src/admin/library-field.tsx","../src/admin/font-field.tsx","../src/admin/pairing-field.tsx","../src/admin/appearance-field.tsx","../src/admin/grey-scale-field.tsx","../src/admin/contrast-report.tsx","../src/admin/section-heading.tsx","../src/admin/publish-child.tsx","../src/admin/save-button.tsx","../src/admin/preview-mode.tsx","../src/admin/identity-fallback.tsx","../src/admin/look-field.tsx","../src/admin/roles-matrix-field.tsx","../src/admin/roles-row-label.tsx","../src/admin/role-slug-field.tsx","../src/admin/slug-field.tsx","../src/admin/role-name-field.tsx","../src/admin/roles-field.tsx","../src/admin/roles-grants-field.tsx","../src/admin/features-matrix-field.tsx","../src/admin/document-title-actions.tsx","../src/admin/document-create-new.tsx","../src/admin-nav/types.ts","../src/admin/admin-nav.tsx","../src/admin/admin-nav-row-label.tsx","../src/admin/admin-nav-item-row-label.tsx","../src/admin/admin-nav-entity-field.tsx"],"mappings":";;;;;;;;;;;cAWa,UAAA,EAAY,wBAAA;;;;;;;cCsBZ,eAAA,EAAiB,yBAAA;;;;;;;;;cCOjB,YAAA,EAAc,yBAAA;;;;;;;;;cCtBd,SAAA,EAAW,0BAAA;;;;;;;cCNX,YAAA,EAAc,sBAAA;;;;;;;cCCd,eAAA,EAAiB,yBAAA;;;ALF9B;;;;AAAA,cM4Da,cAAA,EAAgB,0BAAA;;;;;;;cCnDhB,cAAA,EAAgB,sBAAA;;;;;;cCbhB,cAAA,EAAgB,sBAAA;;;cC0IhB,YAAA,EAAc,sBAAA;;;;;;;;iBCtIX,qBAAA,CAAA;;AVAhB;;;iBUQgB,eAAA,CAAA,GAAe,kBAAA,CAAA,GAAA,CAAA,OAAA;;iBA0Bf,gBAAA,CAAA,GAAgB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;KCrCpB,gBAAA;AAAA,iBAkCI,mBAAA,CAAoB,IAAA,EAAM,gBAAA;;;;;;;cClC7B,gBAAA,EAAkB,sBAAA;;;;;;;cCIlB,SAAA,EAAW,wBAAA;;;;;;;;cCqBX,gBAAA,EAAkB,yBAAA;;;;;;iBC1Bf,aAAA,CAAA,GAAa,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;cCChB,aAAA,EAAe,wBAAA;;;;;;;cCGf,SAAA,EAAW,wBAAA;;;;;;;cCFX,aAAA,EAAe,wBAAA;;;;;;;;cC8Cf,UAAA,EAAY,0BAAA;;;;;;;;cChCZ,gBAAA,EAAkB,wBAAA;;;;;;;;;ApBZ/B;;cqB0Ca,mBAAA,EAAqB,yBAAA;;;iBCflB,oBAAA,CAAA;EAAuB;AAAA;EAAc,QAAA,GAAW,SAAA;AAAA,IAAW,kBAAA,CAAA,GAAA,CAAA,OAAA;;;iBCxB3D,iBAAA,CAAA;EAAoB;AAAA;EAAU,IAAA;AAAA,IAAc,kBAAA,CAAA,GAAA,CAAA,OAAA;;;UCmB3C,uBAAA;EACf,WAAA;EACA,OAAA;AAAA;;;UC5Be,aAAA;EACf,eAAA,GAAkB,uBAAA;AAAA;;;AzBGpB;;;iByB6CgB,QAAA,CAAA;EAAW;AAAA,GAAmB,aAAA,GAAa,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;iBCnD3C,gBAAA,CAAA,GAAgB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;iBCGhB,oBAAA,CAAA,GAAoB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;cCEvB,mBAAA,EAAqB,wBAAA"}
1
+ {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/color-field.tsx","../src/admin/color-scale-field.tsx","../src/admin/library-field.tsx","../src/admin/font-field.tsx","../src/admin/pairing-field.tsx","../src/admin/appearance-field.tsx","../src/admin/grey-scale-field.tsx","../src/admin/contrast-report.tsx","../src/admin/section-heading.tsx","../src/admin/publish-child.tsx","../src/admin/save-button.tsx","../src/admin/preview-mode.tsx","../src/admin/identity-fallback.tsx","../src/admin/look-field.tsx","../src/admin/roles-matrix-field.tsx","../src/admin/roles-row-label.tsx","../src/admin/role-slug-field.tsx","../src/admin/slug-field.tsx","../src/admin/page-title-cell.tsx","../src/admin/role-name-field.tsx","../src/admin/roles-field.tsx","../src/admin/roles-grants-field.tsx","../src/admin/features-matrix-field.tsx","../src/admin/document-title-actions.tsx","../src/admin/document-create-new.tsx","../src/admin-nav/types.ts","../src/admin/admin-nav.tsx","../src/admin/admin-nav-row-label.tsx","../src/admin/admin-nav-item-row-label.tsx","../src/admin/admin-nav-entity-field.tsx"],"mappings":";;;;;;;;;;;cAWa,UAAA,EAAY,wBAAA;;;;;;;cCsBZ,eAAA,EAAiB,yBAAA;;;;;;;;;cCOjB,YAAA,EAAc,yBAAA;;;;;;;;;cCtBd,SAAA,EAAW,0BAAA;;;;;;;cCNX,YAAA,EAAc,sBAAA;;;;;;;cCCd,eAAA,EAAiB,yBAAA;;;ALF9B;;;;AAAA,cM4Da,cAAA,EAAgB,0BAAA;;;;;;;cCnDhB,cAAA,EAAgB,sBAAA;;;;;;cCbhB,cAAA,EAAgB,sBAAA;;;cC0IhB,YAAA,EAAc,sBAAA;;;;;;;;iBCtIX,qBAAA,CAAA;;AVAhB;;;iBUQgB,eAAA,CAAA,GAAe,kBAAA,CAAA,GAAA,CAAA,OAAA;;iBA0Bf,gBAAA,CAAA,GAAgB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;KCrCpB,gBAAA;AAAA,iBAkCI,mBAAA,CAAoB,IAAA,EAAM,gBAAA;;;;;;;cClC7B,gBAAA,EAAkB,sBAAA;;;;;;;cCIlB,SAAA,EAAW,wBAAA;;;;;;;;cCqBX,gBAAA,EAAkB,yBAAA;;;;;;iBC1Bf,aAAA,CAAA,GAAa,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;cCChB,aAAA,EAAe,wBAAA;;;;;;;;;cC2Bf,SAAA,EAAW,wBAAA;;;;;;;iBC1BR,aAAA,CAAc,KAAA,EAAO,yBAAA,GAAyB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;cCAjD,aAAA,EAAe,wBAAA;;;;;;;;cC8Cf,UAAA,EAAY,0BAAA;;;;;;;;cChCZ,gBAAA,EAAkB,wBAAA;;;;;;;;;ArBZ/B;;csB0Ca,mBAAA,EAAqB,yBAAA;;;iBCflB,oBAAA,CAAA;EAAuB;AAAA;EAAc,QAAA,GAAW,SAAA;AAAA,IAAW,kBAAA,CAAA,GAAA,CAAA,OAAA;;;iBCxB3D,iBAAA,CAAA;EAAoB;AAAA;EAAU,IAAA;AAAA,IAAc,kBAAA,CAAA,GAAA,CAAA,OAAA;;;UCmB3C,uBAAA;EACf,WAAA;EACA,OAAA;AAAA;;;UC5Be,aAAA;EACf,eAAA,GAAkB,uBAAA;AAAA;;;A1BGpB;;;iB0B6CgB,QAAA,CAAA;EAAW;AAAA,GAAmB,aAAA,GAAa,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;iBCnD3C,gBAAA,CAAA,GAAgB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;iBCGhB,oBAAA,CAAA,GAAoB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;cCEvB,mBAAA,EAAqB,wBAAA"}
package/dist/admin.mjs CHANGED
@@ -1,6 +1,6 @@
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 { ArrayField, Button, CheckboxInput, FieldDescription, FieldError, FieldLabel, Link, Logout, NavGroup, NavToggler, SelectField, fieldBaseClass, useAuth, useConfig, useField, useForm, useFormFields, useNav, useRowLabel } from "@payloadcms/ui";
3
+ import { ArrayField, Button, CheckboxInput, DefaultCell, FieldDescription, FieldError, FieldLabel, Link, Logout, NavGroup, NavToggler, SelectField, fieldBaseClass, useAuth, useConfig, useField, useForm, useFormFields, useNav, 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
5
  import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -3006,6 +3006,8 @@ const RoleSlugField = ({ field, path, readOnly }) => {
3006
3006
  };
3007
3007
  //#endregion
3008
3008
  //#region src/fields/slug.ts
3009
+ /** Sentinel the Parent control writes while the editor is naming a new folder. */
3010
+ const NEW_FOLDER_PARENT = "__new__";
3009
3011
  const SLUG_UNLOCK_WARNING = "This page is live. Changing the address will send visitors from the old URL to the new one.";
3010
3012
  function slugifySegment(segment) {
3011
3013
  return segment.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -3017,78 +3019,310 @@ function slugifySegment(segment) {
3017
3019
  function normalizeSlug(value) {
3018
3020
  return value.split("/").map(slugifySegment).filter(Boolean).join("/");
3019
3021
  }
3022
+ /** The folder prefix Parent stores. `__new__` resolves through `newFolder`. */
3023
+ function parentPrefix(parent, newFolder) {
3024
+ if (parent === "__new__") return normalizeSlug(newFolder ?? "");
3025
+ return typeof parent === "string" ? normalizeSlug(parent) : "";
3026
+ }
3027
+ /**
3028
+ * Address from a parent prefix and a title (or an existing leaf). Title-follow
3029
+ * uses this so a folder is not wiped when the title changes.
3030
+ */
3031
+ function composeSlug(parent, title, newFolder) {
3032
+ const prefix = parentPrefix(parent, newFolder);
3033
+ const leaf = slugifySegment(title);
3034
+ return prefix ? `${prefix}/${leaf}` : leaf;
3035
+ }
3036
+ /** Prefixes that exist only because a child slug lives under them. */
3037
+ function inferredFolders(slugs) {
3038
+ const folders = /* @__PURE__ */ new Set();
3039
+ for (const slug of slugs) {
3040
+ const parts = normalizeSlug(slug).split("/");
3041
+ for (let i = 1; i < parts.length; i += 1) folders.add(parts.slice(0, i).join("/"));
3042
+ }
3043
+ return [...folders].sort();
3044
+ }
3045
+ /** Slash count after the first segment — how far to indent a list row. */
3046
+ function slugDepth(slug) {
3047
+ const normalized = normalizeSlug(slug);
3048
+ if (!normalized) return 0;
3049
+ return normalized.split("/").length - 1;
3050
+ }
3020
3051
  //#endregion
3021
3052
  //#region src/admin/slug-field.tsx
3053
+ const SEARCH_DELAY_MS = 200;
3054
+ const SEARCH_LIMIT = 8;
3022
3055
  /**
3023
- * Slug follows the title until first publish. After that it is read-only
3024
- * until Unlock, which warns that the old address will redirect.
3056
+ * Parent, new-folder name, composed address, and Unlock. Slug follows the
3057
+ * title leaf until first publish; the folder prefix is not wiped. After
3058
+ * publish the cluster locks until Unlock, which warns that the old address
3059
+ * will redirect.
3025
3060
  */
3026
3061
  const SlugField = ({ field, path, readOnly }) => {
3027
- const { value, setValue, showError, errorMessage } = useField({ path });
3028
- const { value: unlock, setValue: setUnlock } = useField({ path: "slugUnlock" });
3062
+ const slug = useField({ path });
3063
+ const parent = useField({ path: "parent" });
3064
+ const newFolder = useField({ path: "newFolder" });
3065
+ const unlock = useField({ path: "slugUnlock" });
3029
3066
  const title = useFormFields(([fields]) => fields.title?.value);
3030
3067
  const slugLocked = Boolean(useFormFields(([fields]) => fields.slugLocked?.value));
3031
- const locked = Boolean(readOnly) || slugLocked && !unlock;
3032
- const required = Boolean(field.required);
3033
- const id = `field-${path.replace(/\./g, "__")}`;
3068
+ const locked = Boolean(readOnly) || slugLocked && !unlock.value;
3069
+ const folderRoots = useMemo(() => {
3070
+ return ((field.admin?.custom ?? {}).folderRoots ?? []).map(normalizeSlug).filter(Boolean);
3071
+ }, [field.admin?.custom]);
3072
+ const { config } = useConfig();
3073
+ const collection = "pages";
3074
+ const titleKey = config.collections.find((entry) => entry.slug === collection)?.admin?.useAsTitle ?? "title";
3075
+ const endpoint = `${config.serverURL}${config.routes.api}/${collection}`;
3076
+ const id = useId();
3077
+ const parentInputId = `${id}-parent`;
3078
+ const folderInputId = `${id}-folder`;
3079
+ const addressId = `${id}-address`;
3080
+ const listId = `${id}-list`;
3081
+ const [query, setQuery] = useState("");
3082
+ const [open, setOpen] = useState(false);
3083
+ const [results, setResults] = useState([]);
3084
+ const [allSlugs, setAllSlugs] = useState([]);
3085
+ const [active, setActive] = useState(-1);
3086
+ const creatingFolder = parent.value === NEW_FOLDER_PARENT;
3087
+ const setSlug = slug.setValue;
3088
+ const slugValue = slug.value ?? "";
3034
3089
  useEffect(() => {
3035
3090
  if (slugLocked || typeof title !== "string") return;
3036
- const next = normalizeSlug(title);
3037
- if (next !== (value ?? "")) setValue(next);
3091
+ const next = composeSlug(parent.value, title, newFolder.value);
3092
+ if (next !== slugValue) setSlug(next);
3038
3093
  }, [
3039
- setValue,
3094
+ newFolder.value,
3095
+ parent.value,
3096
+ setSlug,
3040
3097
  slugLocked,
3041
- title,
3042
- value
3098
+ slugValue,
3099
+ title
3043
3100
  ]);
3044
- return /* @__PURE__ */ jsxs("div", {
3045
- className: [
3046
- fieldBaseClass,
3047
- "text",
3048
- locked && "read-only"
3049
- ].filter(Boolean).join(" "),
3101
+ useEffect(() => {
3102
+ if (!open) return;
3103
+ const seq = { cancelled: false };
3104
+ const timer = setTimeout(() => {
3105
+ const params = new URLSearchParams({
3106
+ depth: "0",
3107
+ limit: String(SEARCH_LIMIT),
3108
+ sort: titleKey,
3109
+ "where[_status][equals]": "published"
3110
+ });
3111
+ const trimmed = query.trim();
3112
+ if (trimmed) params.set(`where[${titleKey}][like]`, trimmed);
3113
+ fetch(`${endpoint}?${params.toString()}`, { credentials: "include" }).then((response) => response.ok ? response.json() : { docs: [] }).then((body) => {
3114
+ if (seq.cancelled) return;
3115
+ setResults(body.docs ?? []);
3116
+ }).catch(() => {
3117
+ if (!seq.cancelled) setResults([]);
3118
+ });
3119
+ }, SEARCH_DELAY_MS);
3120
+ return () => {
3121
+ seq.cancelled = true;
3122
+ clearTimeout(timer);
3123
+ };
3124
+ }, [
3125
+ endpoint,
3126
+ open,
3127
+ query,
3128
+ titleKey
3129
+ ]);
3130
+ useEffect(() => {
3131
+ let cancelled = false;
3132
+ fetch(`${endpoint}?depth=0&limit=100&sort=slug`, { credentials: "include" }).then((response) => response.ok ? response.json() : { docs: [] }).then((body) => {
3133
+ if (cancelled) return;
3134
+ setAllSlugs((body.docs ?? []).flatMap((doc) => doc.slug ? [doc.slug] : []));
3135
+ }).catch(() => {
3136
+ if (!cancelled) setAllSlugs([]);
3137
+ });
3138
+ return () => {
3139
+ cancelled = true;
3140
+ };
3141
+ }, [endpoint]);
3142
+ const folders = useMemo(() => {
3143
+ const inferred = inferredFolders(allSlugs);
3144
+ return [...new Set([...folderRoots, ...inferred])].sort();
3145
+ }, [allSlugs, folderRoots]);
3146
+ const options = useMemo(() => {
3147
+ const trimmed = query.trim().toLowerCase();
3148
+ const matches = (label, path) => !trimmed || label.toLowerCase().includes(trimmed) || (path ?? "").toLowerCase().includes(trimmed);
3149
+ const list = [{
3150
+ kind: "root",
3151
+ id: "root",
3152
+ label: "Site root"
3153
+ }];
3154
+ for (const folder of folders) if (matches(folder, folder)) list.push({
3155
+ kind: "folder",
3156
+ id: folder,
3157
+ label: folder,
3158
+ path: `/${folder}`
3159
+ });
3160
+ for (const doc of results) {
3161
+ const label = String(doc[titleKey] ?? doc.id);
3162
+ const path = doc.slug ? `/${doc.slug}` : "";
3163
+ if (matches(label, path)) list.push({
3164
+ kind: "page",
3165
+ id: `page:${doc.id}`,
3166
+ label,
3167
+ path,
3168
+ page: doc
3169
+ });
3170
+ }
3171
+ list.push({
3172
+ kind: "new",
3173
+ id: NEW_FOLDER_PARENT,
3174
+ label: "New folder…"
3175
+ });
3176
+ return list;
3177
+ }, [
3178
+ folders,
3179
+ query,
3180
+ results,
3181
+ titleKey
3182
+ ]);
3183
+ function choose(option) {
3184
+ if (option.kind === "root") {
3185
+ parent.setValue("");
3186
+ newFolder.setValue("");
3187
+ } else if (option.kind === "folder") {
3188
+ parent.setValue(option.id);
3189
+ newFolder.setValue("");
3190
+ } else if (option.kind === "page") {
3191
+ parent.setValue(option.page.slug ?? "");
3192
+ newFolder.setValue("");
3193
+ } else parent.setValue(NEW_FOLDER_PARENT);
3194
+ setQuery("");
3195
+ setOpen(false);
3196
+ setActive(-1);
3197
+ }
3198
+ function onKeyDown(event) {
3199
+ if (event.key === "ArrowDown") {
3200
+ event.preventDefault();
3201
+ if (!open) {
3202
+ setOpen(true);
3203
+ return;
3204
+ }
3205
+ setActive((index) => options.length ? (index + 1) % options.length : -1);
3206
+ } else if (event.key === "ArrowUp") {
3207
+ event.preventDefault();
3208
+ setActive((index) => options.length ? index <= 0 ? options.length - 1 : index - 1 : -1);
3209
+ } else if (event.key === "Enter") {
3210
+ if (!open) return;
3211
+ event.preventDefault();
3212
+ const pick = options[active] ?? options[0];
3213
+ if (pick) choose(pick);
3214
+ } else if (event.key === "Escape") {
3215
+ if (!open) return;
3216
+ event.preventDefault();
3217
+ setOpen(false);
3218
+ setActive(-1);
3219
+ }
3220
+ }
3221
+ const selectedPage = options.find((option) => option.kind === "page" && option.page.slug === parent.value);
3222
+ const selectedFolder = parent.value && parent.value !== "__new__" ? parent.value : "";
3223
+ const parentChip = creatingFolder ? "New folder" : selectedPage ? selectedPage.label : selectedFolder ? selectedFolder : "Site root";
3224
+ const parentKind = creatingFolder ? "Folder" : selectedPage ? "Page" : selectedFolder ? "Folder" : "Root";
3225
+ const address = slug.value ? `/${slug.value}` : "/";
3226
+ const activeId = active >= 0 && open ? `${id}-option-${active}` : void 0;
3227
+ return /* @__PURE__ */ jsx(KitProvider, { children: /* @__PURE__ */ jsxs("div", {
3228
+ className: "bl-page-address",
3050
3229
  children: [
3051
- /* @__PURE__ */ jsx(FieldLabel, {
3052
- htmlFor: id,
3053
- label: field.label,
3054
- required
3055
- }),
3056
- /* @__PURE__ */ jsxs("div", {
3057
- className: `${fieldBaseClass}__wrap`,
3058
- children: [
3059
- /* @__PURE__ */ jsx(FieldError, {
3060
- message: errorMessage,
3061
- path,
3062
- showError
3063
- }),
3064
- /* @__PURE__ */ jsx("input", {
3065
- id,
3066
- type: "text",
3067
- autoComplete: "off",
3068
- spellCheck: false,
3069
- readOnly: locked,
3230
+ /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(Label, { children: "Parent" }), /* @__PURE__ */ jsx("div", {
3231
+ className: "bl-nav-kit__picker",
3232
+ children: locked ? /* @__PURE__ */ jsxs("div", {
3233
+ className: "bl-nav-kit__picker-control",
3234
+ children: [/* @__PURE__ */ jsx(Badge, {
3235
+ variant: "neutral-subtle",
3236
+ children: parentKind
3237
+ }), /* @__PURE__ */ jsx("span", { children: parentChip })]
3238
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Input, {
3239
+ id: parentInputId,
3240
+ role: "combobox",
3241
+ "aria-label": "Parent",
3242
+ placeholder: "Site root, a folder, or a published page",
3243
+ autoComplete: "off",
3244
+ value: open ? query : parentChip,
3245
+ "aria-expanded": open,
3246
+ "aria-controls": listId,
3247
+ "aria-autocomplete": "list",
3248
+ "aria-activedescendant": activeId,
3249
+ onChange: (event) => {
3250
+ setQuery(event.target.value);
3251
+ setActive(-1);
3252
+ setOpen(true);
3253
+ },
3254
+ onFocus: () => setOpen(true),
3255
+ onBlur: () => setOpen(false),
3256
+ onKeyDown
3257
+ }), open ? /* @__PURE__ */ jsx("ul", {
3258
+ id: listId,
3259
+ role: "listbox",
3260
+ className: "bl-nav-kit__picker-menu",
3261
+ children: options.map((option, index) => /* @__PURE__ */ jsxs("li", {
3262
+ id: `${id}-option-${index}`,
3263
+ role: "option",
3264
+ "aria-selected": index === active,
3265
+ className: "bl-nav-kit__picker-option",
3266
+ onMouseDown: (event) => {
3267
+ event.preventDefault();
3268
+ choose(option);
3269
+ },
3270
+ onMouseEnter: () => setActive(index),
3271
+ children: [/* @__PURE__ */ jsx("span", { children: option.label }), "path" in option && option.path ? /* @__PURE__ */ jsx("span", {
3272
+ className: "bl-nav-kit__picker-path",
3273
+ children: option.path
3274
+ }) : null]
3275
+ }, option.id))
3276
+ }) : null] })
3277
+ })] }),
3278
+ creatingFolder && !locked ? /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(Label, { children: "New folder name" }), /* @__PURE__ */ jsx(Input, {
3279
+ id: folderInputId,
3280
+ "aria-label": "New folder name",
3281
+ value: newFolder.value ?? "",
3282
+ autoComplete: "off",
3283
+ onChange: (event) => newFolder.setValue(event.target.value)
3284
+ })] }) : null,
3285
+ /* @__PURE__ */ jsxs(Field, { children: [
3286
+ /* @__PURE__ */ jsx(Label, { children: "Address" }),
3287
+ /* @__PURE__ */ jsxs("div", {
3288
+ className: "bl-nav-kit__picker-address",
3289
+ children: [/* @__PURE__ */ jsx(Input, {
3290
+ id: addressId,
3291
+ readOnly: true,
3070
3292
  disabled: locked,
3071
- value: value ?? "",
3072
- onChange: (event) => {
3073
- if (!locked) setValue(event.target.value);
3074
- }
3075
- }),
3076
- slugLocked && !unlock ? /* @__PURE__ */ jsx(Button$1, {
3293
+ value: address,
3294
+ autoComplete: "off",
3295
+ spellCheck: false
3296
+ }), slugLocked && !unlock.value ? /* @__PURE__ */ jsx(Button$1, {
3077
3297
  variant: "outline",
3078
- onClick: () => setUnlock(true),
3298
+ onClick: () => unlock.setValue(true),
3079
3299
  children: "Unlock"
3080
- }) : null
3081
- ]
3082
- }),
3083
- unlock ? /* @__PURE__ */ jsx("p", { children: SLUG_UNLOCK_WARNING }) : null,
3084
- /* @__PURE__ */ jsx(FieldDescription, {
3085
- description: field.admin?.description,
3086
- path
3087
- })
3300
+ }) : null]
3301
+ }),
3302
+ unlock.value ? /* @__PURE__ */ jsx("p", { children: SLUG_UNLOCK_WARNING }) : null,
3303
+ /* @__PURE__ */ jsx(FieldError, {
3304
+ message: slug.errorMessage,
3305
+ path,
3306
+ showError: slug.showError
3307
+ })
3308
+ ] })
3088
3309
  ]
3089
- });
3310
+ }) });
3090
3311
  };
3091
3312
  //#endregion
3313
+ //#region src/admin/page-title-cell.tsx
3314
+ /**
3315
+ * Indent a Pages list title by how deep its slug sits under a parent folder.
3316
+ * DefaultCell still draws the document link — a custom Cell replaces that wrap.
3317
+ */
3318
+ function PageTitleCell(props) {
3319
+ return /* @__PURE__ */ jsx("span", {
3320
+ className: "bl-page-title-cell",
3321
+ style: { paddingInlineStart: `${slugDepth(typeof props.rowData?.slug === "string" ? props.rowData.slug : "") * 1.25}rem` },
3322
+ children: /* @__PURE__ */ jsx(DefaultCell, { ...props })
3323
+ });
3324
+ }
3325
+ //#endregion
3092
3326
  //#region src/admin/role-name-field.tsx
3093
3327
  /**
3094
3328
  * Role name. Letters and spaces only, so the hidden slug stays
@@ -3828,6 +4062,6 @@ const AdminNavEntityField = ({ field, path, permissions, readOnly, schemaPath })
3828
4062
  });
3829
4063
  };
3830
4064
  //#endregion
3831
- export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, SlugField, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
4065
+ export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PageTitleCell, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, SlugField, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
3832
4066
 
3833
4067
  //# sourceMappingURL=admin.mjs.map