@bison-lab/payload-core 3.24.0 → 3.26.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 CHANGED
@@ -8,10 +8,10 @@ are extracted.
8
8
  Full guide: <https://payload.bisonlab.ai/site/seo/>
9
9
 
10
10
  ```bash
11
- pnpm add @bison-lab/payload-core @payloadcms/plugin-seo @bison-lab/tokens @bison-lab/fonts
11
+ pnpm add @bison-lab/payload-core @payloadcms/plugin-seo @payloadcms/plugin-redirects @bison-lab/tokens @bison-lab/fonts
12
12
  ```
13
13
 
14
- Peers: `payload` and `@payloadcms/plugin-seo` are required. `@bison-lab/tokens`
14
+ Peers: `payload`, `@payloadcms/plugin-seo`, and `@payloadcms/plugin-redirects` are required. `@bison-lab/tokens`
15
15
  and `@bison-lab/fonts` are needed to adopt the Theme Global; `@payloadcms/ui`
16
16
  and `react` are needed for its admin fields; `@bison-lab/ui` and
17
17
  `@payloadcms/live-preview-react` are needed for the live theme preview.
@@ -22,7 +22,7 @@ lockstep.
22
22
 
23
23
  | Import | Contents | Runs where |
24
24
  | ---------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
25
- | `@bison-lab/payload-core` | `seoPlugin`, `createTheme`, `createBrandAssets`, `createRoles`, `createFeatures`, `createPages`, `createUsers`, `createMedia`, `createAdminNav`, `adminNav`, `createNavigation`, `adminOnlyApiTab`, `documentTitleActions`, `seedTheme`, `seedRoles`, `seedFeatures`, predicates, `lookField`, `colorTokenField`, import-map strings, title and text helpers, types | Node. What `payload.config.ts` imports; it loads the plugin. |
25
+ | `@bison-lab/payload-core` | `seoPlugin`, `createTheme`, `createBrandAssets`, `createRoles`, `createFeatures`, `createPages`, `createUsers`, `createMedia`, `createRedirects`, `createAdminNav`, `adminNav`, `createNavigation`, `adminOnlyApiTab`, `documentTitleActions`, `seedTheme`, `seedRoles`, `seedFeatures`, predicates, `lookField`, `colorTokenField`, import-map strings, title and text helpers, types | Node. What `payload.config.ts` imports; it loads the plugin. |
26
26
  | `@bison-lab/payload-core/metadata` | `pageMetadata`, the title helpers, the same types | Server. What a page route imports; it does not load the plugin. |
27
27
  | `@bison-lab/payload-core/theme` | `getPublishedTheme`, `getPublishedIdentity`, `themeConfigFromDoc`, `themeHead`, `themeHeadFromDoc` | Server. What a root layout imports; it does not load the plugin or React. |
28
28
  | `@bison-lab/payload-core/navigation` | `getNavigation`, the Header and Footer slugs | Server. What a site header imports; it does not load the plugin or React. |
@@ -45,11 +45,12 @@ the site spells its name and its URL scheme once.
45
45
 
46
46
  ```ts
47
47
  // payload.config.ts
48
- import { firstImageIn, seoPlugin } from "@bison-lab/payload-core";
48
+ import { createRedirects, firstImageIn, seoPlugin } from "@bison-lab/payload-core";
49
49
  import type { Page } from "@/payload-types";
50
50
 
51
51
  export default buildConfig({
52
52
  plugins: [
53
+ createRedirects(),
53
54
  seoPlugin<Page>({
54
55
  siteName: "Acme Clinics",
55
56
  urlFor: (doc) => (doc.slug ? absoluteUrl(pagePath(doc.slug)) : undefined),
@@ -318,10 +319,23 @@ read from the live Features and Roles Globals) so Payload's
318
319
  ## Pages, users, and media
319
320
 
320
321
  `createPages`, `createUsers`, `createMedia`, `slugField`,
321
- `adminOnlyApiTab`, and `documentTitleActions` ship from the same
322
+ `createRedirects`, `adminOnlyApiTab`, and `documentTitleActions` ship from the same
322
323
  React-free entry. Pages take the
323
- site's hero and layout blocks, reserved-slug predicate, and preview
324
- path. Create follows the Content tick. Update without Publish is
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()`
334
+ wraps `@payloadcms/plugin-redirects` for `pages` only (permanent 308) and
335
+ collapses chains when a published slug changes. The plugin stores the
336
+ rows; the site still has to serve them. Pin the plugin to the same
337
+ version as `payload`, then `payload generate:importmap`,
338
+ `payload generate:types`, and `payload migrate:create`. Create follows the Content tick. Update without Publish is
325
339
  constrained to `_status: draft` — an Author can save a draft and cannot
326
340
  publish or edit a live page. Users sit under Settings next to Roles,
327
341
  take `secureCookies`, and offer whatever rows the Global (or the seed
@@ -418,6 +432,7 @@ to a type that has one.
418
432
 
419
433
  `noIndexField`, the plugin's own fields, the Theme Global fields, the
420
434
  Roles Global fields, the Admin nav fields, the Header and Footer fields,
421
- and the brand-assets collection fields are columns
435
+ the Pages slug lock fields, the Redirects collection, and the
436
+ brand-assets collection fields are columns
422
437
  in every consuming site. A change to them is a schema change: say "run
423
438
  `payload migrate:create`" in the changeset.
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
  /**
@@ -126,6 +126,22 @@ declare function RolesRowLabel(): react_jsx_runtime0.JSX.Element;
126
126
  */
127
127
  declare const RoleSlugField: TextFieldClientComponent;
128
128
  //#endregion
129
+ //#region src/admin/slug-field.d.ts
130
+ /**
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.
135
+ */
136
+ declare const SlugField: TextFieldClientComponent;
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
129
145
  //#region src/admin/role-name-field.d.ts
130
146
  /**
131
147
  * Role name. Letters and spaces only, so the hidden slug stays
@@ -214,5 +230,5 @@ declare function AdminNavItemRowLabel(): react_jsx_runtime0.JSX.Element;
214
230
  */
215
231
  declare const AdminNavEntityField: TextFieldClientComponent;
216
232
  //#endregion
217
- 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, 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 };
218
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/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;;;;;;;cCCf,aAAA,EAAe,wBAAA;;;;;;;;cC8Cf,UAAA,EAAY,0BAAA;;;;;;;;cChCZ,gBAAA,EAAkB,wBAAA;;;;;;;;;AnBZ/B;;coB0Ca,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;;;AxBGpB;;;iBwB6CgB,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,13 +1,14 @@
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";
7
- import { Badge, Dialog, Field, Input, KitProvider, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@bison-lab/admin-ui";
7
+ import { Badge, Button as Button$1, Dialog, Field, Input, KitProvider, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@bison-lab/admin-ui";
8
8
  import { HexColorPicker } from "react-colorful";
9
9
  import { createPortal } from "react-dom";
10
10
  import { findFont, fontFaceCss, isFontId } from "@bison-lab/fonts";
11
+ import "payload";
11
12
  //#region src/admin/color-field.tsx
12
13
  const HEX_ERROR = "Enter a six-digit hex colour like #1e3a5f";
13
14
  /**
@@ -3004,6 +3005,324 @@ const RoleSlugField = ({ field, path, readOnly }) => {
3004
3005
  });
3005
3006
  };
3006
3007
  //#endregion
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__";
3011
+ const SLUG_UNLOCK_WARNING = "This page is live. Changing the address will send visitors from the old URL to the new one.";
3012
+ function slugifySegment(segment) {
3013
+ return segment.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3014
+ }
3015
+ /**
3016
+ * A stored slug from whatever was typed: NFKD, hyphenated, slash segments
3017
+ * kept, so `Café / About Us` becomes `cafe/about-us`.
3018
+ */
3019
+ function normalizeSlug(value) {
3020
+ return value.split("/").map(slugifySegment).filter(Boolean).join("/");
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
+ }
3051
+ //#endregion
3052
+ //#region src/admin/slug-field.tsx
3053
+ const SEARCH_DELAY_MS = 200;
3054
+ const SEARCH_LIMIT = 8;
3055
+ /**
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.
3060
+ */
3061
+ const SlugField = ({ field, path, readOnly }) => {
3062
+ const slug = useField({ path });
3063
+ const parent = useField({ path: "parent" });
3064
+ const newFolder = useField({ path: "newFolder" });
3065
+ const unlock = useField({ path: "slugUnlock" });
3066
+ const title = useFormFields(([fields]) => fields.title?.value);
3067
+ const slugLocked = Boolean(useFormFields(([fields]) => fields.slugLocked?.value));
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 ?? "";
3089
+ useEffect(() => {
3090
+ if (slugLocked || typeof title !== "string") return;
3091
+ const next = composeSlug(parent.value, title, newFolder.value);
3092
+ if (next !== slugValue) setSlug(next);
3093
+ }, [
3094
+ newFolder.value,
3095
+ parent.value,
3096
+ setSlug,
3097
+ slugLocked,
3098
+ slugValue,
3099
+ title
3100
+ ]);
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",
3229
+ children: [
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,
3292
+ disabled: locked,
3293
+ value: address,
3294
+ autoComplete: "off",
3295
+ spellCheck: false
3296
+ }), slugLocked && !unlock.value ? /* @__PURE__ */ jsx(Button$1, {
3297
+ variant: "outline",
3298
+ onClick: () => unlock.setValue(true),
3299
+ children: "Unlock"
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
+ ] })
3309
+ ]
3310
+ }) });
3311
+ };
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
3007
3326
  //#region src/admin/role-name-field.tsx
3008
3327
  /**
3009
3328
  * Role name. Letters and spaces only, so the hidden slug stays
@@ -3743,6 +4062,6 @@ const AdminNavEntityField = ({ field, path, permissions, readOnly, schemaPath })
3743
4062
  });
3744
4063
  };
3745
4064
  //#endregion
3746
- 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, 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 };
3747
4066
 
3748
4067
  //# sourceMappingURL=admin.mjs.map