@emdash-cms/admin 0.16.1 → 0.17.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.
Files changed (99) hide show
  1. package/dist/{LocaleDirectionProvider-CFKQ39Tn.js → LocaleDirectionProvider-LB7Qjnv1.js} +29 -17
  2. package/dist/LocaleDirectionProvider-LB7Qjnv1.js.map +1 -0
  3. package/dist/{config-ZP4qGbGk.d.ts → config-76UcBWeH.d.ts} +1 -1
  4. package/dist/{config-ZP4qGbGk.d.ts.map → config-76UcBWeH.d.ts.map} +1 -1
  5. package/dist/index.d.ts +142 -8
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1777 -203
  8. package/dist/index.js.map +1 -1
  9. package/dist/locales/ar/messages.mjs +1 -1
  10. package/dist/locales/de/messages.mjs +1 -1
  11. package/dist/locales/en/messages.mjs +1 -1
  12. package/dist/locales/es-419/messages.mjs +1 -1
  13. package/dist/locales/es-ES/messages.mjs +1 -1
  14. package/dist/locales/eu/messages.mjs +1 -1
  15. package/dist/locales/fa/messages.mjs +1 -1
  16. package/dist/locales/fr/messages.mjs +1 -1
  17. package/dist/locales/id/messages.mjs +1 -1
  18. package/dist/locales/index.d.ts +1 -1
  19. package/dist/locales/index.js +1 -1
  20. package/dist/locales/ja/messages.mjs +1 -1
  21. package/dist/locales/ko/messages.mjs +1 -1
  22. package/dist/locales/nb/messages.mjs +1 -0
  23. package/dist/locales/pl/messages.mjs +1 -1
  24. package/dist/locales/pseudo/messages.mjs +1 -1
  25. package/dist/locales/pt-BR/messages.mjs +1 -1
  26. package/dist/locales/th/messages.mjs +1 -0
  27. package/dist/locales/zh-CN/messages.mjs +1 -1
  28. package/dist/locales/zh-TW/messages.mjs +1 -1
  29. package/dist/messages-BKmEGNNr.js +6 -0
  30. package/dist/messages-BKmEGNNr.js.map +1 -0
  31. package/dist/messages-BLyCd3bH.js +6 -0
  32. package/dist/messages-BLyCd3bH.js.map +1 -0
  33. package/dist/messages-BPzwf13U.js +6 -0
  34. package/dist/messages-BPzwf13U.js.map +1 -0
  35. package/dist/messages-BunNlQic.js +6 -0
  36. package/dist/messages-BunNlQic.js.map +1 -0
  37. package/dist/messages-C3wS1VMq.js +6 -0
  38. package/dist/messages-C3wS1VMq.js.map +1 -0
  39. package/dist/messages-C4ONkHEI.js +6 -0
  40. package/dist/messages-C4ONkHEI.js.map +1 -0
  41. package/dist/messages-C55KydOS.js +6 -0
  42. package/dist/messages-C55KydOS.js.map +1 -0
  43. package/dist/messages-CjirHcRw.js +6 -0
  44. package/dist/messages-CjirHcRw.js.map +1 -0
  45. package/dist/messages-CkDIrR_T.js +6 -0
  46. package/dist/messages-CkDIrR_T.js.map +1 -0
  47. package/dist/messages-ClrLd7GR.js +6 -0
  48. package/dist/messages-ClrLd7GR.js.map +1 -0
  49. package/dist/messages-DM8WFh_S.js +6 -0
  50. package/dist/messages-DM8WFh_S.js.map +1 -0
  51. package/dist/messages-DW5uspYc.js +6 -0
  52. package/dist/messages-DW5uspYc.js.map +1 -0
  53. package/dist/messages-DrMj1Hby.js +6 -0
  54. package/dist/messages-DrMj1Hby.js.map +1 -0
  55. package/dist/messages-Du-_Uh8B.js +6 -0
  56. package/dist/messages-Du-_Uh8B.js.map +1 -0
  57. package/dist/messages-DzQ203Bb.js +6 -0
  58. package/dist/messages-DzQ203Bb.js.map +1 -0
  59. package/dist/messages-amIeQ8HY.js +6 -0
  60. package/dist/messages-amIeQ8HY.js.map +1 -0
  61. package/dist/messages-ngvpRuG5.js +6 -0
  62. package/dist/messages-ngvpRuG5.js.map +1 -0
  63. package/dist/messages-rxZmXkNz.js +6 -0
  64. package/dist/messages-rxZmXkNz.js.map +1 -0
  65. package/dist/styles.css +1 -1
  66. package/package.json +3 -3
  67. package/dist/LocaleDirectionProvider-CFKQ39Tn.js.map +0 -1
  68. package/dist/messages-6iBQvP4l.js +0 -6
  69. package/dist/messages-6iBQvP4l.js.map +0 -1
  70. package/dist/messages-B3qgKgae.js +0 -6
  71. package/dist/messages-B3qgKgae.js.map +0 -1
  72. package/dist/messages-B8kb5Oko.js +0 -6
  73. package/dist/messages-B8kb5Oko.js.map +0 -1
  74. package/dist/messages-BhSzAEGL.js +0 -6
  75. package/dist/messages-BhSzAEGL.js.map +0 -1
  76. package/dist/messages-C0wfBdmD.js +0 -6
  77. package/dist/messages-C0wfBdmD.js.map +0 -1
  78. package/dist/messages-CboPeBM0.js +0 -6
  79. package/dist/messages-CboPeBM0.js.map +0 -1
  80. package/dist/messages-CehVxd9q.js +0 -6
  81. package/dist/messages-CehVxd9q.js.map +0 -1
  82. package/dist/messages-D4ctBYFE.js +0 -6
  83. package/dist/messages-D4ctBYFE.js.map +0 -1
  84. package/dist/messages-DHha7f8K.js +0 -6
  85. package/dist/messages-DHha7f8K.js.map +0 -1
  86. package/dist/messages-DIehX9BP.js +0 -6
  87. package/dist/messages-DIehX9BP.js.map +0 -1
  88. package/dist/messages-DKdkwHEw.js +0 -6
  89. package/dist/messages-DKdkwHEw.js.map +0 -1
  90. package/dist/messages-DMJobUj_.js +0 -6
  91. package/dist/messages-DMJobUj_.js.map +0 -1
  92. package/dist/messages-Ddgurz-D.js +0 -6
  93. package/dist/messages-Ddgurz-D.js.map +0 -1
  94. package/dist/messages-LeijTgr3.js +0 -6
  95. package/dist/messages-LeijTgr3.js.map +0 -1
  96. package/dist/messages-S5UPBqIz.js +0 -6
  97. package/dist/messages-S5UPBqIz.js.map +0 -1
  98. package/dist/messages-vQ-B-9ct.js +0 -6
  99. package/dist/messages-vQ-B-9ct.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import { c as fetchAuthMode, d as throwResponseError, i as fetchPlugins, l as fetchManifest, n as enablePlugin, o as API_BASE, r as fetchPlugin, s as apiFetch, t as disablePlugin, u as parseApiResponse } from "./plugins-BZzztFdK.js";
2
- import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-CFKQ39Tn.js";
2
+ import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-LB7Qjnv1.js";
3
3
  import "./locales/index.js";
4
4
  import { Autocomplete, Badge, Button, Checkbox, CommandPalette, Dialog, Input, InputArea, Label, LinkButton, Loader, Popover, Select, Sidebar as KumoSidebar, Switch, Tabs, Toast, Toasty, Tooltip, buttonVariants, useSidebar } from "@cloudflare/kumo";
5
5
  import { i18n } from "@lingui/core";
6
6
  import { I18nProvider, Trans, useLingui } from "@lingui/react";
7
- import { QueryClient, QueryClientProvider, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
7
+ import { QueryClient, QueryClientProvider, keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
8
8
  import { Link, Link as Link$1, Outlet, RouterProvider, createRootRouteWithContext, createRoute, createRouter, useLocation, useNavigate, useNavigate as useNavigate$1, useParams, useParams as useParams$1, useSearch } from "@tanstack/react-router";
9
9
  import * as React$1 from "react";
10
10
  import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
11
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
12
  import { Dialog as Dialog$1 } from "@cloudflare/kumo/primitives";
13
- import { ArrowCounterClockwise, ArrowRight, ArrowRightIcon, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, BracketsCurly, Calendar, CalendarBlank, CaretDown, CaretRightIcon, CaretUp, CaretUpDown, ChatCircle, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, Code, CodeBlock, Columns, Copy, Cube, Database, DeviceMobile, DotsSixVertical, DownloadSimple, Envelope, EnvelopeSimple, Eye, EyeSlash, File, FileArrowDown, FileText, FloppyDisk, FolderOpen, Gear, GithubLogo, Globe, GlobeSimple, GridFour, HardDrive, Hash, Image as Image$1, ImageBroken, ImageSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, MagnifyingGlass, Minus, Monitor, Moon, Palette, PaperPlaneTilt, Paperclip, Paragraph, Pencil, PencilSimple, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Rows, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, SlidersHorizontal, Sparkle, SquaresFour, Stack, Storefront, Sun, Table, TextAlignCenter, TextAlignLeft, TextAlignRight, TextB, TextHOne, TextHThree, TextHTwo, TextItalic, TextStrikethrough, TextT, TextUnderline, ToggleLeft, Trash, Upload, User, UserCircle, UserPlus, Users, Warning, WarningCircle, WebhooksLogo, X, YoutubeLogo } from "@phosphor-icons/react";
13
+ import { ArrowCounterClockwise, ArrowDown, ArrowRight, ArrowRightIcon, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowUp, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, Bell, BookOpen, BracketsAngle, BracketsCurly, Calendar, CalendarBlank, CaretDown, CaretRightIcon, CaretUp, CaretUpDown, ChartBar, ChartLine, ChatCircle, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, Code, CodeBlock, Columns, Copy, Crop, Cube, Database, DeviceMobile, DotsSixVertical, DownloadSimple, Envelope, EnvelopeSimple, Eye, EyeSlash, File, FileArrowDown, FileText, FloppyDisk, Folder, FolderOpen, Gear, GithubLogo, Globe, GlobeSimple, GridFour, HardDrive, Hash, IdentificationCard, Image as Image$1, ImageBroken, ImageSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, MagnifyingGlass, Medal, Minus, Monitor, Moon, Palette, PaperPlaneTilt, Paperclip, Paragraph, Pencil, PencilSimple, Plug, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Rows, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, SlidersHorizontal, Sparkle, SquaresFour, Stack, Star, Storefront, Sun, Table, Tag, TextAlignCenter, TextAlignLeft, TextAlignRight, TextB, TextHOne, TextHThree, TextHTwo, TextItalic, TextStrikethrough, TextT, TextUnderline, ToggleLeft, Trash, Trophy, Upload, User, UserCircle, UserPlus, Users, Warning, WarningCircle, WebhooksLogo, X, YoutubeLogo } from "@phosphor-icons/react";
14
14
  import { clsx } from "clsx";
15
15
  import { twMerge } from "tailwind-merge";
16
16
  import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter, rectIntersection, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
@@ -30,7 +30,7 @@ import Typography from "@tiptap/extension-typography";
30
30
  import { EditorContent, Node as Node$2, NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes as mergeAttributes$1, useEditor, useEditorState } from "@tiptap/react";
31
31
  import { BubbleMenu } from "@tiptap/react/menus";
32
32
  import StarterKit from "@tiptap/starter-kit";
33
- import { Plugin, PluginKey } from "@tiptap/pm/state";
33
+ import { NodeSelection, Plugin, PluginKey } from "@tiptap/pm/state";
34
34
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
35
35
  import { createPortal } from "react-dom";
36
36
  import CodeBlock$1 from "@tiptap/extension-code-block";
@@ -1178,6 +1178,7 @@ async function fetchContentList(collection, options) {
1178
1178
  if (options?.locale) params.set("locale", options.locale);
1179
1179
  if (options?.orderBy) params.set("orderBy", options.orderBy);
1180
1180
  if (options?.order) params.set("order", options.order);
1181
+ if (options?.search) params.set("q", options.search);
1181
1182
  return parseApiResponse(await apiFetch(`${API_BASE}/content/${collection}${params.toString() ? `?${params}` : ""}`), "Failed to fetch content");
1182
1183
  }
1183
1184
  /**
@@ -1372,6 +1373,15 @@ async function restoreRevision(revisionId) {
1372
1373
  * Media upload, list, delete, and provider APIs
1373
1374
  */
1374
1375
  /**
1376
+ * Maximum length of the media filename search term. Mirrors the server-side
1377
+ * zod schema (`q: z.string().trim().min(1).max(200)`); keep in sync.
1378
+ */
1379
+ const MEDIA_SEARCH_MAX_LENGTH = 200;
1380
+ /** Trim and clamp a search term to the server-accepted range. */
1381
+ function normalizeMediaSearch(value) {
1382
+ return (value ?? "").trim().slice(0, MEDIA_SEARCH_MAX_LENGTH);
1383
+ }
1384
+ /**
1375
1385
  * Fetch media list
1376
1386
  */
1377
1387
  async function fetchMediaList(options) {
@@ -1382,6 +1392,10 @@ async function fetchMediaList(options) {
1382
1392
  const value = Array.isArray(options.mimeType) ? options.mimeType.join(",") : options.mimeType;
1383
1393
  if (value) params.set("mimeType", value);
1384
1394
  }
1395
+ if (options?.search) {
1396
+ const q = normalizeMediaSearch(options.search);
1397
+ if (q) params.set("q", q);
1398
+ }
1385
1399
  return parseApiResponse(await apiFetch(`${API_BASE}/media${params.toString() ? `?${params}` : ""}`), "Failed to fetch media");
1386
1400
  }
1387
1401
  /**
@@ -2008,6 +2022,70 @@ async function createBylineTranslation(id, input) {
2008
2022
  }), "Failed to create byline translation");
2009
2023
  }
2010
2024
 
2025
+ //#endregion
2026
+ //#region src/lib/api/byline-fields.ts
2027
+ /**
2028
+ * Byline custom-field schema management API (Discussion #1174, Phase 4).
2029
+ *
2030
+ * Mirrors the server-side admin endpoints at
2031
+ * `/_emdash/api/admin/byline-fields/*`. Mutation responses use the
2032
+ * shared `ApiResult<T>` envelope; this client unwraps it via
2033
+ * `parseApiResponse` and surfaces typed errors through
2034
+ * `throwResponseError` (so the admin client sees the registry's
2035
+ * `FIELD_EXISTS` / `TRANSLATABLE_LOCKED` / `REORDER_MISMATCH` messages
2036
+ * verbatim).
2037
+ */
2038
+ const BASE = `${API_BASE}/admin/byline-fields`;
2039
+ async function listBylineFields() {
2040
+ return parseApiResponse(await apiFetch(BASE), i18n._({
2041
+ id: "rLmrr+",
2042
+ message: "Failed to list byline fields"
2043
+ }));
2044
+ }
2045
+ async function getBylineFieldUsage(slug) {
2046
+ return parseApiResponse(await apiFetch(`${BASE}/${encodeURIComponent(slug)}/usage`), i18n._({
2047
+ id: "DwFS6c",
2048
+ message: "Failed to read byline field usage"
2049
+ }));
2050
+ }
2051
+ async function createBylineField(input) {
2052
+ return parseApiResponse(await apiFetch(BASE, {
2053
+ method: "POST",
2054
+ headers: { "Content-Type": "application/json" },
2055
+ body: JSON.stringify(input)
2056
+ }), i18n._({
2057
+ id: "cPkE9I",
2058
+ message: "Failed to create byline field"
2059
+ }));
2060
+ }
2061
+ async function updateBylineField(slug, input) {
2062
+ return parseApiResponse(await apiFetch(`${BASE}/${encodeURIComponent(slug)}`, {
2063
+ method: "PATCH",
2064
+ headers: { "Content-Type": "application/json" },
2065
+ body: JSON.stringify(input)
2066
+ }), i18n._({
2067
+ id: "1dl1C5",
2068
+ message: "Failed to update byline field"
2069
+ }));
2070
+ }
2071
+ async function deleteBylineField(slug) {
2072
+ const response = await apiFetch(`${BASE}/${encodeURIComponent(slug)}`, { method: "DELETE" });
2073
+ if (!response.ok) await throwResponseError(response, i18n._({
2074
+ id: "MxBmlg",
2075
+ message: "Failed to delete byline field"
2076
+ }));
2077
+ }
2078
+ async function reorderBylineFields(slugs) {
2079
+ return parseApiResponse(await apiFetch(`${BASE}/reorder`, {
2080
+ method: "POST",
2081
+ headers: { "Content-Type": "application/json" },
2082
+ body: JSON.stringify({ slugs })
2083
+ }), i18n._({
2084
+ id: "J/UlAe",
2085
+ message: "Failed to reorder byline fields"
2086
+ }));
2087
+ }
2088
+
2011
2089
  //#endregion
2012
2090
  //#region src/lib/api/menus.ts
2013
2091
  /**
@@ -2961,6 +3039,35 @@ function fromDatetimeLocalInputValue(value) {
2961
3039
  return value === "" ? "" : `${value}:00.000Z`;
2962
3040
  }
2963
3041
 
3042
+ //#endregion
3043
+ //#region src/lib/hooks.ts
3044
+ /**
3045
+ * Returns a stable function reference that always calls the latest version
3046
+ * of the provided callback. Useful for event listeners in effects where
3047
+ * you don't want the listener to be torn down and re-added when the
3048
+ * callback identity changes.
3049
+ */
3050
+ function useStableCallback(callback) {
3051
+ const ref = React$1.useRef(callback);
3052
+ React$1.useLayoutEffect(() => {
3053
+ ref.current = callback;
3054
+ });
3055
+ return React$1.useCallback((...args) => ref.current(...args), []);
3056
+ }
3057
+ /**
3058
+ * Returns a debounced version of a value that only updates after the
3059
+ * specified delay has elapsed since the last change. Useful for search
3060
+ * inputs that trigger API calls.
3061
+ */
3062
+ function useDebouncedValue$1(value, delay) {
3063
+ const [debouncedValue, setDebouncedValue] = React$1.useState(value);
3064
+ React$1.useEffect(() => {
3065
+ const timer = setTimeout(setDebouncedValue, delay, value);
3066
+ return () => clearTimeout(timer);
3067
+ }, [value, delay]);
3068
+ return debouncedValue;
3069
+ }
3070
+
2964
3071
  //#endregion
2965
3072
  //#region src/lib/media-utils.ts
2966
3073
  function providerItemToMediaItem(providerId, item) {
@@ -3178,6 +3285,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3178
3285
  const [selectedItem, setSelectedItem] = React$1.useState(null);
3179
3286
  const [activeProvider, setActiveProvider] = React$1.useState("local");
3180
3287
  const [searchQuery, setSearchQuery] = React$1.useState("");
3288
+ const debouncedSearch = useDebouncedValue$1(searchQuery, 300);
3181
3289
  const fileInputRef = React$1.useRef(null);
3182
3290
  const [imageUrl, setImageUrl] = React$1.useState("");
3183
3291
  const [isProbing, setIsProbing] = React$1.useState(false);
@@ -3214,12 +3322,18 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3214
3322
  };
3215
3323
  return providers?.find((p) => p.id === activeProvider);
3216
3324
  }, [activeProvider, providers]);
3325
+ const mediaQueryKey = [
3326
+ "media",
3327
+ filters?.join(",") ?? "",
3328
+ debouncedSearch.trim()
3329
+ ];
3217
3330
  const { data: localData, isLoading: localLoading, fetchNextPage: fetchNextLocalPage, hasNextPage: hasNextLocalPage, isFetchingNextPage: isFetchingNextLocalPage } = useInfiniteQuery({
3218
- queryKey: ["media", filters?.join(",") ?? ""],
3331
+ queryKey: mediaQueryKey,
3219
3332
  queryFn: ({ pageParam }) => fetchMediaList({
3220
3333
  mimeType: filters,
3221
3334
  cursor: pageParam,
3222
- limit: 100
3335
+ limit: 100,
3336
+ search: debouncedSearch.trim() || void 0
3223
3337
  }),
3224
3338
  initialPageParam: void 0,
3225
3339
  getNextPageParam: (lastPage) => lastPage.nextCursor,
@@ -3277,7 +3391,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3277
3391
  height
3278
3392
  }),
3279
3393
  onSuccess: (_updated, { id, width, height }) => {
3280
- queryClient.setQueryData(["media", filters?.join(",") ?? ""], (old) => {
3394
+ queryClient.setQueryData(mediaQueryKey, (old) => {
3281
3395
  if (!old) return old;
3282
3396
  return {
3283
3397
  ...old,
@@ -3555,11 +3669,14 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3555
3669
  }),
3556
3670
  /* @__PURE__ */ jsxs("div", {
3557
3671
  className: "flex items-center justify-between pb-3 gap-4",
3558
- children: [canSearch ? /* @__PURE__ */ jsxs("div", {
3672
+ children: [canSearch || activeProvider === "local" ? /* @__PURE__ */ jsxs("div", {
3559
3673
  className: "relative flex-1 max-w-xs",
3560
3674
  children: [/* @__PURE__ */ jsx(MagnifyingGlass, { className: "absolute start-3 top-1/2 -translate-y-1/2 h-4 w-4 text-kumo-subtle" }), /* @__PURE__ */ jsx(Input, {
3561
3675
  type: "search",
3562
- placeholder: _t({
3676
+ placeholder: activeProvider === "local" ? _t({
3677
+ id: "Fwo2VZ",
3678
+ message: "Search by filename..."
3679
+ }) : _t({
3563
3680
  id: "YIix5Y",
3564
3681
  message: "Search..."
3565
3682
  }),
@@ -3569,6 +3686,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3569
3686
  }),
3570
3687
  value: searchQuery,
3571
3688
  onChange: (e) => setSearchQuery(e.target.value),
3689
+ maxLength: MEDIA_SEARCH_MAX_LENGTH,
3572
3690
  className: "ps-9"
3573
3691
  })]
3574
3692
  }) : /* @__PURE__ */ jsx("p", {
@@ -4596,35 +4714,6 @@ function renderIcon(icon) {
4596
4714
  return /* @__PURE__ */ jsx(icon, {});
4597
4715
  }
4598
4716
 
4599
- //#endregion
4600
- //#region src/lib/hooks.ts
4601
- /**
4602
- * Returns a stable function reference that always calls the latest version
4603
- * of the provided callback. Useful for event listeners in effects where
4604
- * you don't want the listener to be torn down and re-added when the
4605
- * callback identity changes.
4606
- */
4607
- function useStableCallback(callback) {
4608
- const ref = React$1.useRef(callback);
4609
- React$1.useLayoutEffect(() => {
4610
- ref.current = callback;
4611
- });
4612
- return React$1.useCallback((...args) => ref.current(...args), []);
4613
- }
4614
- /**
4615
- * Returns a debounced version of a value that only updates after the
4616
- * specified delay has elapsed since the last change. Useful for search
4617
- * inputs that trigger API calls.
4618
- */
4619
- function useDebouncedValue$1(value, delay) {
4620
- const [debouncedValue, setDebouncedValue] = React$1.useState(value);
4621
- React$1.useEffect(() => {
4622
- const timer = setTimeout(setDebouncedValue, delay, value);
4623
- return () => clearTimeout(timer);
4624
- }, [value, delay]);
4625
- return debouncedValue;
4626
- }
4627
-
4628
4717
  //#endregion
4629
4718
  //#region src/components/editor/ImageDetailPanel.tsx
4630
4719
  /**
@@ -6086,19 +6175,25 @@ function BlockMenu({ editor, anchorElement, isOpen, onClose }) {
6086
6175
  if (!isOpen) setShowTransforms(false);
6087
6176
  }, [isOpen]);
6088
6177
  const handleDuplicate = () => {
6089
- const { selection } = editor.state;
6090
- const { $from, $to } = selection;
6091
- const blockStart = $from.start($from.depth);
6092
- const blockEnd = $to.end($to.depth);
6093
- const slice = editor.state.doc.slice(blockStart, blockEnd);
6178
+ if (!(editor.state.selection instanceof NodeSelection)) {
6179
+ onClose();
6180
+ return;
6181
+ }
6182
+ const { selection, doc } = editor.state;
6183
+ const { from, to } = selection;
6184
+ const slice = doc.slice(from, to);
6094
6185
  editor.chain().focus().command(({ tr }) => {
6095
- tr.insert(blockEnd + 1, slice.content);
6186
+ tr.insert(to, slice.content);
6096
6187
  return true;
6097
6188
  }).run();
6098
6189
  onClose();
6099
6190
  };
6100
6191
  const handleDelete = () => {
6101
- editor.chain().focus().deleteNode(editor.state.selection.$from.parent.type.name).run();
6192
+ if (!(editor.state.selection instanceof NodeSelection)) {
6193
+ onClose();
6194
+ return;
6195
+ }
6196
+ editor.chain().focus().deleteSelection().run();
6102
6197
  onClose();
6103
6198
  };
6104
6199
  const handleTransform = (transform) => {
@@ -6239,6 +6334,173 @@ function DragHandleWrapper({ editor }) {
6239
6334
  })] });
6240
6335
  }
6241
6336
 
6337
+ //#endregion
6338
+ //#region src/components/editor/HtmlBlockNode.tsx
6339
+ /**
6340
+ * HTML block node for the admin editor.
6341
+ *
6342
+ * Renders a first-class `htmlBlock` in the Portable Text editor with:
6343
+ * - A textarea for editing raw HTML source
6344
+ * - Selection ring, drag handle, and delete action
6345
+ *
6346
+ * Modeled on `PluginBlockNode.tsx` (atom node with React node view) and
6347
+ * the existing `{ _type: "htmlBlock", _key, html }` Portable Text shape
6348
+ * used by the WordPress and Contentful importers.
6349
+ */
6350
+ /**
6351
+ * True when focus is inside a text input or textarea (e.g. the HTML block's
6352
+ * source <textarea>). Editor-level keyboard shortcuts must defer to native
6353
+ * field editing in that case.
6354
+ */
6355
+ function isEditingFormField() {
6356
+ if (typeof document === "undefined") return false;
6357
+ const active = document.activeElement;
6358
+ if (!active) return false;
6359
+ const tag = active.tagName;
6360
+ return tag === "TEXTAREA" || tag === "INPUT";
6361
+ }
6362
+ function HtmlBlockNodeView({ node, updateAttributes, selected, deleteNode }) {
6363
+ const { _: _t } = useLingui();
6364
+ const html = typeof node.attrs.html === "string" ? node.attrs.html : "";
6365
+ const [draft, setDraft] = React$1.useState(html);
6366
+ const textareaRef = React$1.useRef(null);
6367
+ React$1.useEffect(() => {
6368
+ setDraft(html);
6369
+ }, [html]);
6370
+ React$1.useEffect(() => {
6371
+ const el = textareaRef.current;
6372
+ if (el) {
6373
+ el.style.height = "auto";
6374
+ el.style.height = `${el.scrollHeight}px`;
6375
+ }
6376
+ }, [draft]);
6377
+ const commitHtml = React$1.useCallback((value) => {
6378
+ updateAttributes({ html: value });
6379
+ }, [updateAttributes]);
6380
+ const handleChange = React$1.useCallback((e) => {
6381
+ setDraft(e.target.value);
6382
+ commitHtml(e.target.value);
6383
+ }, [commitHtml]);
6384
+ return /* @__PURE__ */ jsx(NodeViewWrapper, {
6385
+ className: cn("html-block relative my-3", selected && "ring-2 ring-kumo-brand ring-offset-2 rounded-lg"),
6386
+ contentEditable: false,
6387
+ "data-drag-handle": true,
6388
+ children: /* @__PURE__ */ jsxs("div", {
6389
+ className: "relative group",
6390
+ children: [/* @__PURE__ */ jsx("div", {
6391
+ className: cn("absolute -start-8 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity cursor-grab active:cursor-grabbing", selected && "opacity-100"),
6392
+ "data-drag-handle": true,
6393
+ children: /* @__PURE__ */ jsx(DotsSixVertical, { className: "h-5 w-5 text-kumo-subtle/50" })
6394
+ }), /* @__PURE__ */ jsxs("div", {
6395
+ className: cn("rounded-lg border bg-kumo-base transition-colors overflow-hidden", selected ? "border-kumo-brand/50 bg-kumo-tint/30" : "hover:border-kumo-line"),
6396
+ children: [/* @__PURE__ */ jsxs("div", {
6397
+ className: "flex items-center gap-3 px-4 py-3",
6398
+ children: [
6399
+ /* @__PURE__ */ jsx("div", {
6400
+ className: "flex-shrink-0 w-10 h-10 rounded-lg bg-kumo-tint flex items-center justify-center text-kumo-subtle",
6401
+ children: /* @__PURE__ */ jsx(BracketsAngle, { className: "h-5 w-5" })
6402
+ }),
6403
+ /* @__PURE__ */ jsx("div", {
6404
+ className: "flex-1 min-w-0",
6405
+ children: /* @__PURE__ */ jsx("div", {
6406
+ className: "text-sm font-medium",
6407
+ children: _t({
6408
+ id: "FBknns",
6409
+ message: "HTML"
6410
+ })
6411
+ })
6412
+ }),
6413
+ /* @__PURE__ */ jsx("div", {
6414
+ className: cn("flex items-center gap-1 transition-opacity", selected ? "opacity-100" : "opacity-0 group-hover:opacity-100"),
6415
+ children: /* @__PURE__ */ jsx(Button, {
6416
+ type: "button",
6417
+ variant: "ghost",
6418
+ shape: "square",
6419
+ className: "h-8 w-8 text-kumo-danger hover:text-kumo-danger hover:bg-kumo-danger/10",
6420
+ onClick: () => deleteNode(),
6421
+ title: _t({
6422
+ id: "cnGeoo",
6423
+ message: "Delete"
6424
+ }),
6425
+ "aria-label": _t({
6426
+ id: "p8SVsk",
6427
+ message: "Delete HTML block"
6428
+ }),
6429
+ children: /* @__PURE__ */ jsx(Trash, { className: "h-4 w-4" })
6430
+ })
6431
+ })
6432
+ ]
6433
+ }), /* @__PURE__ */ jsx("div", {
6434
+ className: "px-4 pb-4",
6435
+ children: /* @__PURE__ */ jsx("textarea", {
6436
+ ref: textareaRef,
6437
+ value: draft,
6438
+ onChange: handleChange,
6439
+ placeholder: _t({
6440
+ id: "cquA1F",
6441
+ message: "Enter HTML..."
6442
+ }),
6443
+ className: "w-full min-h-[100px] resize-y rounded-md border bg-kumo-overlay p-3 font-mono text-sm text-kumo-strong placeholder:text-kumo-subtle focus:outline-none focus:ring-2 focus:ring-kumo-brand",
6444
+ spellCheck: false,
6445
+ "aria-label": _t({
6446
+ id: "bbJ01O",
6447
+ message: "HTML source"
6448
+ })
6449
+ })
6450
+ })]
6451
+ })]
6452
+ })
6453
+ });
6454
+ }
6455
+ /**
6456
+ * TipTap extension: first-class HTML block.
6457
+ *
6458
+ * An atom node that stores raw HTML in a `html` attribute. Round-trips
6459
+ * through Portable Text as `{ _type: "htmlBlock", _key, html }`.
6460
+ */
6461
+ const HtmlBlockExtension = Node$1.create({
6462
+ name: "htmlBlock",
6463
+ group: "block",
6464
+ atom: true,
6465
+ draggable: true,
6466
+ selectable: true,
6467
+ addAttributes() {
6468
+ return { html: {
6469
+ default: "",
6470
+ parseHTML: (element) => element.getAttribute("data-html-content") ?? "",
6471
+ renderHTML: (attributes) => {
6472
+ const html = typeof attributes.html === "string" ? attributes.html : "";
6473
+ if (!html) return {};
6474
+ return { "data-html-content": html };
6475
+ }
6476
+ } };
6477
+ },
6478
+ parseHTML() {
6479
+ return [{ tag: "div[data-html-block]" }];
6480
+ },
6481
+ renderHTML({ HTMLAttributes }) {
6482
+ return ["div", mergeAttributes(HTMLAttributes, { "data-html-block": "" })];
6483
+ },
6484
+ addNodeView() {
6485
+ return ReactNodeViewRenderer(HtmlBlockNodeView);
6486
+ },
6487
+ addKeyboardShortcuts() {
6488
+ const deleteHtmlBlock = () => {
6489
+ if (isEditingFormField()) return false;
6490
+ const { selection } = this.editor.state;
6491
+ if (this.editor.state.doc.nodeAt(selection.from)?.type.name === "htmlBlock") {
6492
+ this.editor.commands.deleteSelection();
6493
+ return true;
6494
+ }
6495
+ return false;
6496
+ };
6497
+ return {
6498
+ Backspace: deleteHtmlBlock,
6499
+ Delete: deleteHtmlBlock
6500
+ };
6501
+ }
6502
+ });
6503
+
6242
6504
  //#endregion
6243
6505
  //#region src/components/editor/ImageNode.tsx
6244
6506
  /**
@@ -7191,6 +7453,14 @@ function convertPMNode(node) {
7191
7453
  language: typeof rawLanguage === "string" ? rawLanguage : void 0
7192
7454
  };
7193
7455
  }
7456
+ case "htmlBlock": {
7457
+ const rawHtml = node.attrs?.html;
7458
+ return {
7459
+ _type: "htmlBlock",
7460
+ _key: generateKey(),
7461
+ html: typeof rawHtml === "string" ? rawHtml : ""
7462
+ };
7463
+ }
7194
7464
  case "image": {
7195
7465
  const attrs = node.attrs ?? {};
7196
7466
  const provider = attrStr(attrs.provider);
@@ -7449,6 +7719,10 @@ function convertPTBlock(block) {
7449
7719
  };
7450
7720
  }
7451
7721
  case "break": return { type: "horizontalRule" };
7722
+ case "htmlBlock": return {
7723
+ type: "htmlBlock",
7724
+ attrs: { html: block.html || "" }
7725
+ };
7452
7726
  case "table": {
7453
7727
  const tableBlock = block;
7454
7728
  const tableMarkDefs = tableBlock.markDefs || [];
@@ -7688,6 +7962,29 @@ const defaultSlashCommands = [
7688
7962
  editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
7689
7963
  }
7690
7964
  },
7965
+ {
7966
+ id: "htmlBlock",
7967
+ title: {
7968
+ id: "FBknns",
7969
+ message: "HTML"
7970
+ },
7971
+ description: {
7972
+ id: "iHrC09",
7973
+ message: "Insert raw HTML"
7974
+ },
7975
+ icon: BracketsAngle,
7976
+ aliases: [
7977
+ "html",
7978
+ "raw",
7979
+ "markup"
7980
+ ],
7981
+ command: ({ editor, range }) => {
7982
+ editor.chain().focus().deleteRange(range).insertContent({
7983
+ type: "htmlBlock",
7984
+ attrs: { html: "" }
7985
+ }).run();
7986
+ }
7987
+ },
7691
7988
  {
7692
7989
  id: "divider",
7693
7990
  title: {
@@ -8569,6 +8866,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8569
8866
  underline: {}
8570
8867
  }),
8571
8868
  CodeBlockExtension,
8869
+ HtmlBlockExtension,
8572
8870
  ImageExtension,
8573
8871
  MarkdownLinkExtension,
8574
8872
  PluginBlockExtension,
@@ -8723,7 +9021,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8723
9021
  })
8724
9022
  });
8725
9023
  return /* @__PURE__ */ jsxs("div", {
8726
- className: cn("border rounded-lg overflow-hidden", minimal && "border-0 rounded-none -mx-4", focusMode === "spotlight" && "spotlight-mode", className),
9024
+ className: cn("border rounded-lg overflow-clip", minimal && "border-0 rounded-none -mx-4", focusMode === "spotlight" && "spotlight-mode", className),
8727
9025
  "aria-labelledby": ariaLabelledby,
8728
9026
  children: [
8729
9027
  !minimal && /* @__PURE__ */ jsx(EditorToolbar, {
@@ -9136,7 +9434,7 @@ function EditorToolbar({ editor, focusMode, onFocusModeChange }) {
9136
9434
  id: "maNyuq",
9137
9435
  message: "Text formatting"
9138
9436
  }),
9139
- className: "border-b bg-kumo-tint/50 p-1 flex flex-wrap gap-0.5",
9437
+ className: "sticky -top-6 z-10 border-b bg-kumo-tint p-1 flex flex-wrap gap-0.5",
9140
9438
  onKeyDown: handleKeyDown,
9141
9439
  children: [
9142
9440
  /* @__PURE__ */ jsxs(ToolbarGroup, { children: [
@@ -10326,10 +10624,11 @@ async function fetchTaxonomyDefs$1() {
10326
10624
  return (await parseApiResponse(await apiFetch(`/_emdash/api/taxonomies`), "Failed to fetch taxonomies")).taxonomies;
10327
10625
  }
10328
10626
  /**
10329
- * Fetch terms for a taxonomy
10627
+ * Fetch terms for a taxonomy, scoped to the entry's locale so only the matching
10628
+ * translation variants are offered.
10330
10629
  */
10331
- async function fetchTerms$1(taxonomyName) {
10332
- return (await parseApiResponse(await apiFetch(`/_emdash/api/taxonomies/${taxonomyName}/terms`), i18n._({
10630
+ async function fetchTerms$1(taxonomyName, locale) {
10631
+ return (await parseApiResponse(await apiFetch(withLocale(`/_emdash/api/taxonomies/${taxonomyName}/terms`, locale)), i18n._({
10333
10632
  id: "uvkjA+",
10334
10633
  message: "Failed to fetch terms"
10335
10634
  }))).terms;
@@ -10337,8 +10636,8 @@ async function fetchTerms$1(taxonomyName) {
10337
10636
  /**
10338
10637
  * Fetch entry terms
10339
10638
  */
10340
- async function fetchEntryTerms(collection, entryId, taxonomy) {
10341
- return (await parseApiResponse(await apiFetch(`/_emdash/api/content/${collection}/${entryId}/terms/${taxonomy}`), i18n._({
10639
+ async function fetchEntryTerms(collection, entryId, taxonomy, locale) {
10640
+ return (await parseApiResponse(await apiFetch(withLocale(`/_emdash/api/content/${collection}/${entryId}/terms/${taxonomy}`, locale)), i18n._({
10342
10641
  id: "pbqrlw",
10343
10642
  message: "Failed to fetch entry terms"
10344
10643
  }))).terms;
@@ -10346,8 +10645,8 @@ async function fetchEntryTerms(collection, entryId, taxonomy) {
10346
10645
  /**
10347
10646
  * Set entry terms
10348
10647
  */
10349
- async function setEntryTerms(collection, entryId, taxonomy, termIds) {
10350
- const res = await apiFetch(`/_emdash/api/content/${collection}/${entryId}/terms/${taxonomy}`, {
10648
+ async function setEntryTerms(collection, entryId, taxonomy, termIds, locale) {
10649
+ const res = await apiFetch(withLocale(`/_emdash/api/content/${collection}/${entryId}/terms/${taxonomy}`, locale), {
10351
10650
  method: "POST",
10352
10651
  headers: { "Content-Type": "application/json" },
10353
10652
  body: JSON.stringify({ termIds })
@@ -10495,40 +10794,46 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
10495
10794
  /**
10496
10795
  * Single taxonomy section
10497
10796
  */
10498
- function TaxonomySection({ taxonomy, collection, entryId, onChange }) {
10797
+ function TaxonomySection({ taxonomy, collection, entryId, entryLocale, onChange }) {
10499
10798
  const { _: _t2 } = useLingui();
10500
10799
  const queryClient = useQueryClient();
10501
10800
  const toastManager = Toast.useToastManager();
10502
10801
  const [newCategoryLabel, setNewCategoryLabel] = React$1.useState("");
10503
10802
  const [showCategoryInput, setShowCategoryInput] = React$1.useState(false);
10504
10803
  const { data: terms = EMPTY_TERMS } = useQuery({
10505
- queryKey: ["taxonomy-terms", taxonomy.name],
10506
- queryFn: () => fetchTerms$1(taxonomy.name)
10804
+ queryKey: [
10805
+ "taxonomy-terms",
10806
+ taxonomy.name,
10807
+ entryLocale
10808
+ ],
10809
+ queryFn: () => fetchTerms$1(taxonomy.name, entryLocale)
10507
10810
  });
10508
10811
  const { data: entryTerms = EMPTY_TERMS } = useQuery({
10509
10812
  queryKey: [
10510
10813
  "entry-terms",
10511
10814
  collection,
10512
10815
  entryId,
10513
- taxonomy.name
10816
+ taxonomy.name,
10817
+ entryLocale
10514
10818
  ],
10515
10819
  queryFn: () => {
10516
10820
  if (!entryId) return [];
10517
- return fetchEntryTerms(collection, entryId, taxonomy.name);
10821
+ return fetchEntryTerms(collection, entryId, taxonomy.name, entryLocale);
10518
10822
  },
10519
10823
  enabled: !!entryId
10520
10824
  });
10521
10825
  const saveMutation = useMutation({
10522
10826
  mutationFn: (termIds) => {
10523
10827
  if (!entryId) throw new Error("No entry ID");
10524
- return setEntryTerms(collection, entryId, taxonomy.name, termIds);
10828
+ return setEntryTerms(collection, entryId, taxonomy.name, termIds, entryLocale);
10525
10829
  },
10526
10830
  onSuccess: () => {
10527
10831
  queryClient.invalidateQueries({ queryKey: [
10528
10832
  "entry-terms",
10529
10833
  collection,
10530
10834
  entryId,
10531
- taxonomy.name
10835
+ taxonomy.name,
10836
+ entryLocale
10532
10837
  ] });
10533
10838
  toastManager.add({ title: _t2({
10534
10839
  id: "KDffey",
@@ -10554,10 +10859,15 @@ function TaxonomySection({ taxonomy, collection, entryId, onChange }) {
10554
10859
  const createTermMutation = useMutation({
10555
10860
  mutationFn: (label) => createTerm(taxonomy.name, {
10556
10861
  slug: slugify(label),
10557
- label
10862
+ label,
10863
+ ...entryLocale ? { locale: entryLocale } : {}
10558
10864
  }),
10559
10865
  onSuccess: (newTerm) => {
10560
- queryClient.invalidateQueries({ queryKey: ["taxonomy-terms", taxonomy.name] });
10866
+ queryClient.invalidateQueries({ queryKey: [
10867
+ "taxonomy-terms",
10868
+ taxonomy.name,
10869
+ entryLocale
10870
+ ] });
10561
10871
  const newSelected = new Set(selectedIds);
10562
10872
  newSelected.add(newTerm.id);
10563
10873
  setSelectedIds(newSelected);
@@ -10677,7 +10987,7 @@ function TaxonomySection({ taxonomy, collection, entryId, onChange }) {
10677
10987
  /**
10678
10988
  * Main TaxonomySidebar component
10679
10989
  */
10680
- function TaxonomySidebar({ collection, entryId, onChange }) {
10990
+ function TaxonomySidebar({ collection, entryId, entryLocale, onChange }) {
10681
10991
  const { _: _t3 } = useLingui();
10682
10992
  const { data: taxonomies = [] } = useQuery({
10683
10993
  queryKey: ["taxonomy-defs"],
@@ -10699,6 +11009,7 @@ function TaxonomySidebar({ collection, entryId, onChange }) {
10699
11009
  taxonomy,
10700
11010
  collection,
10701
11011
  entryId,
11012
+ entryLocale,
10702
11013
  onChange: (termIds) => onChange?.(taxonomy.name, termIds)
10703
11014
  }, taxonomy.name))
10704
11015
  })] })
@@ -11499,6 +11810,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, entry
11499
11810
  }), /* @__PURE__ */ jsx(BylineCreditsEditor, {
11500
11811
  credits: activeBylines,
11501
11812
  bylines: availableBylines ?? [],
11813
+ selectedBylineDetails: item?.bylines?.map((entry) => entry.byline),
11502
11814
  bylinesLoaded: availableBylinesLoaded,
11503
11815
  onChange: handleBylinesChange,
11504
11816
  onQuickCreate: onQuickCreateByline,
@@ -11528,7 +11840,8 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, entry
11528
11840
  className: "p-4 border-t",
11529
11841
  children: /* @__PURE__ */ jsx(TaxonomySidebar, {
11530
11842
  collection,
11531
- entryId: item.id
11843
+ entryId: item.id,
11844
+ entryLocale: item.locale ?? entryLocale
11532
11845
  })
11533
11846
  }),
11534
11847
  hasSeo && !isNew && onSeoChange && /* @__PURE__ */ jsxs("div", {
@@ -12161,9 +12474,10 @@ function FileFieldRenderer({ id, label, value, onChange, required, allowedMimeTy
12161
12474
  ]
12162
12475
  });
12163
12476
  }
12164
- function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuickEdit, entryLocale, i18n, bylinesLoaded = true }) {
12477
+ function BylineCreditsEditor({ credits, bylines, selectedBylineDetails, onChange, onQuickCreate, onQuickEdit, entryLocale, i18n, bylinesLoaded = true }) {
12165
12478
  const { _: _t7 } = useLingui();
12166
- const [selectedBylineId, setSelectedBylineId] = React$1.useState("");
12479
+ const [search, setSearch] = React$1.useState("");
12480
+ const debouncedSearch = useDebouncedValue$1(search, 300);
12167
12481
  const [quickName, setQuickName] = React$1.useState("");
12168
12482
  const [quickSlug, setQuickSlug] = React$1.useState("");
12169
12483
  const [quickError, setQuickError] = React$1.useState(null);
@@ -12173,8 +12487,44 @@ function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuic
12173
12487
  const [editSlug, setEditSlug] = React$1.useState("");
12174
12488
  const [editError, setEditError] = React$1.useState(null);
12175
12489
  const [isEditing, setIsEditing] = React$1.useState(false);
12176
- const bylineMap = React$1.useMemo(() => new Map(bylines.map((b) => [b.id, b])), [bylines]);
12177
- const availableToAdd = bylines.filter((b) => !credits.some((c) => c.bylineId === b.id));
12490
+ const trimmedSearch = debouncedSearch.trim();
12491
+ const searchEnabled = trimmedSearch.length > 0;
12492
+ const searchResults = useQuery({
12493
+ queryKey: [
12494
+ "bylines",
12495
+ "credit-picker",
12496
+ entryLocale ?? null,
12497
+ trimmedSearch
12498
+ ],
12499
+ queryFn: () => fetchBylines({
12500
+ search: trimmedSearch,
12501
+ locale: entryLocale ?? void 0,
12502
+ limit: 20
12503
+ }),
12504
+ enabled: searchEnabled,
12505
+ placeholderData: keepPreviousData
12506
+ });
12507
+ const resultPool = searchEnabled ? searchResults.data?.items ?? [] : bylines;
12508
+ const hasMoreResults = searchEnabled ? !!searchResults.data?.nextCursor : bylines.length >= 100;
12509
+ const bylineMap = React$1.useMemo(() => {
12510
+ const map = /* @__PURE__ */ new Map();
12511
+ for (const b of selectedBylineDetails ?? []) map.set(b.id, b);
12512
+ for (const b of bylines) map.set(b.id, b);
12513
+ for (const b of searchResults.data?.items ?? []) map.set(b.id, b);
12514
+ return map;
12515
+ }, [
12516
+ selectedBylineDetails,
12517
+ bylines,
12518
+ searchResults.data?.items
12519
+ ]);
12520
+ const availableToAdd = resultPool.filter((b) => !credits.some((c) => c.bylineId === b.id));
12521
+ const addByline = (bylineId) => {
12522
+ if (credits.some((c) => c.bylineId === bylineId)) return;
12523
+ onChange([...credits, {
12524
+ bylineId,
12525
+ roleLabel: null
12526
+ }]);
12527
+ };
12178
12528
  const move = (index, direction) => {
12179
12529
  const target = index + direction;
12180
12530
  if (target < 0 || target >= credits.length) return;
@@ -12226,39 +12576,70 @@ function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuic
12226
12576
  })]
12227
12577
  }),
12228
12578
  /* @__PURE__ */ jsxs("div", {
12229
- className: "flex gap-2",
12230
- children: [/* @__PURE__ */ jsx(Select, {
12231
- value: selectedBylineId,
12232
- onValueChange: (v) => setSelectedBylineId(v ?? ""),
12233
- items: {
12234
- "": _t7({
12235
- id: "r5zcIE",
12236
- message: "Select byline..."
12579
+ className: "space-y-2",
12580
+ children: [
12581
+ /* @__PURE__ */ jsx(Input, {
12582
+ value: search,
12583
+ onChange: (e) => setSearch(e.target.value),
12584
+ placeholder: _t7({
12585
+ id: "96Um6i",
12586
+ message: "Search bylines to add..."
12237
12587
  }),
12238
- ...Object.fromEntries(availableToAdd.map((b) => [b.id, b.displayName]))
12239
- },
12240
- "aria-label": _t7({
12241
- id: "r4Xk7n",
12242
- message: "Select byline"
12588
+ "aria-label": _t7({
12589
+ id: "4L2LjM",
12590
+ message: "Search bylines"
12591
+ })
12243
12592
  }),
12244
- className: "w-full"
12245
- }), /* @__PURE__ */ jsx(Button, {
12246
- type: "button",
12247
- variant: "secondary",
12248
- onClick: () => {
12249
- if (!selectedBylineId) return;
12250
- onChange([...credits, {
12251
- bylineId: selectedBylineId,
12252
- roleLabel: null
12253
- }]);
12254
- setSelectedBylineId("");
12255
- },
12256
- disabled: !selectedBylineId,
12257
- children: _t7({
12258
- id: "m16xKo",
12259
- message: "Add"
12593
+ searchEnabled && searchResults.isLoading ? /* @__PURE__ */ jsx("p", {
12594
+ className: "text-sm text-kumo-subtle",
12595
+ children: _t7({
12596
+ id: "Du+zn+",
12597
+ message: "Searching..."
12598
+ })
12599
+ }) : availableToAdd.length > 0 ? /* @__PURE__ */ jsx("ul", {
12600
+ className: "max-h-48 divide-y overflow-y-auto rounded border",
12601
+ children: availableToAdd.map((b) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
12602
+ type: "button",
12603
+ className: "flex w-full items-center justify-between gap-2 p-2 text-start hover:bg-kumo-tint",
12604
+ onClick: () => addByline(b.id),
12605
+ children: [/* @__PURE__ */ jsxs("span", {
12606
+ className: "min-w-0",
12607
+ children: [/* @__PURE__ */ jsx("span", {
12608
+ className: "block truncate text-sm font-medium",
12609
+ children: b.displayName
12610
+ }), /* @__PURE__ */ jsx("span", {
12611
+ className: "block truncate text-xs text-kumo-subtle",
12612
+ children: b.slug
12613
+ })]
12614
+ }), /* @__PURE__ */ jsx("span", {
12615
+ className: "text-xs text-kumo-subtle",
12616
+ children: _t7({
12617
+ id: "m16xKo",
12618
+ message: "Add"
12619
+ })
12620
+ })]
12621
+ }) }, b.id))
12622
+ }) : searchEnabled && searchResults.isError ? /* @__PURE__ */ jsx("p", {
12623
+ className: "text-sm text-kumo-danger",
12624
+ children: _t7({
12625
+ id: "617r+T",
12626
+ message: "Couldn't search bylines. Please try again."
12627
+ })
12628
+ }) : searchEnabled ? /* @__PURE__ */ jsx("p", {
12629
+ className: "text-sm text-kumo-subtle",
12630
+ children: _t7({
12631
+ id: "ncgkI9",
12632
+ message: "No matching bylines."
12633
+ })
12634
+ }) : null,
12635
+ hasMoreResults && /* @__PURE__ */ jsx("p", {
12636
+ className: "text-xs text-kumo-subtle",
12637
+ children: _t7({
12638
+ id: "7Jpj2/",
12639
+ message: "Keep typing to narrow down more bylines."
12640
+ })
12260
12641
  })
12261
- })]
12642
+ ]
12262
12643
  }),
12263
12644
  credits.length > 0 ? /* @__PURE__ */ jsx("div", {
12264
12645
  className: "space-y-2",
@@ -12637,33 +13018,44 @@ function getItemTitle$1(item) {
12637
13018
  /**
12638
13019
  * Content list view with table display and trash tab
12639
13020
  */
12640
- function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n, activeLocale, onLocaleChange, urlPattern, sort, onSortChange, total }) {
13021
+ function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n, activeLocale, onLocaleChange, urlPattern, sort, onSortChange, total, onSearchChange }) {
12641
13022
  const { _: _t } = useLingui();
12642
13023
  const [activeTab, setActiveTab] = React$1.useState("all");
12643
13024
  const [searchQuery, setSearchQuery] = React$1.useState("");
12644
13025
  const [page, setPage] = React$1.useState(0);
13026
+ const serverSearch = !!onSearchChange;
13027
+ const debouncedSearch = useDebouncedValue$1(searchQuery, 300);
13028
+ React$1.useEffect(() => {
13029
+ if (onSearchChange) onSearchChange(debouncedSearch.trim());
13030
+ }, [debouncedSearch, onSearchChange]);
12645
13031
  const handleSearchChange = (e) => {
12646
13032
  setSearchQuery(e.target.value);
12647
13033
  setPage(0);
12648
13034
  };
12649
13035
  const filteredItems = React$1.useMemo(() => {
12650
- if (!searchQuery) return items;
13036
+ if (serverSearch || !searchQuery) return items;
12651
13037
  const query = searchQuery.toLowerCase();
12652
13038
  return items.filter((item) => getItemTitle$1(item).toLowerCase().includes(query));
12653
- }, [items, searchQuery]);
12654
- const effectiveTotal = typeof total === "number" && !searchQuery ? total : filteredItems.length;
13039
+ }, [
13040
+ items,
13041
+ searchQuery,
13042
+ serverSearch
13043
+ ]);
13044
+ const activeSearch = serverSearch ? debouncedSearch.trim() : searchQuery;
13045
+ const effectiveTotal = typeof total === "number" && (serverSearch || !searchQuery) ? total : filteredItems.length;
12655
13046
  const totalPages = Math.max(1, Math.ceil(effectiveTotal / PAGE_SIZE));
12656
13047
  const clampedPage = Math.min(page, totalPages - 1);
12657
13048
  const paginatedItems = filteredItems.slice(clampedPage * PAGE_SIZE, (clampedPage + 1) * PAGE_SIZE);
12658
13049
  React$1.useEffect(() => {
12659
- if (!hasMore || !onLoadMore || searchQuery) return;
13050
+ if (!hasMore || !onLoadMore || !serverSearch && searchQuery) return;
12660
13051
  if (clampedPage >= Math.ceil(filteredItems.length / PAGE_SIZE) - 1) onLoadMore();
12661
13052
  }, [
12662
13053
  clampedPage,
12663
13054
  filteredItems.length,
12664
13055
  hasMore,
12665
13056
  onLoadMore,
12666
- searchQuery
13057
+ searchQuery,
13058
+ serverSearch
12667
13059
  ]);
12668
13060
  return /* @__PURE__ */ jsxs("div", {
12669
13061
  className: "space-y-4",
@@ -12693,7 +13085,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12693
13085
  })
12694
13086
  })]
12695
13087
  }),
12696
- items.length > 0 && /* @__PURE__ */ jsxs("div", {
13088
+ (serverSearch || items.length > 0) && /* @__PURE__ */ jsxs("div", {
12697
13089
  className: "relative max-w-sm",
12698
13090
  children: [/* @__PURE__ */ jsx(MagnifyingGlass, { className: "absolute start-3 top-1/2 -translate-y-1/2 h-4 w-4 text-kumo-subtle" }), /* @__PURE__ */ jsx(Input, {
12699
13091
  type: "search",
@@ -12810,10 +13202,14 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12810
13202
  message: "Loading..."
12811
13203
  })]
12812
13204
  })
12813
- }) }) : items.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
13205
+ }) }) : items.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
12814
13206
  colSpan: i18n ? 5 : 4,
12815
13207
  className: "px-4 py-8 text-center text-kumo-subtle",
12816
- children: [
13208
+ children: activeSearch ? _t({
13209
+ id: "QpOp1w",
13210
+ message: "No results for \"{activeSearch}\"",
13211
+ values: { activeSearch }
13212
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
12817
13213
  _t({
12818
13214
  id: "yHHMoK",
12819
13215
  message: "No {0} yet.",
@@ -12830,14 +13226,14 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12830
13226
  message: "Create your first one"
12831
13227
  })
12832
13228
  })
12833
- ]
13229
+ ] })
12834
13230
  }) }) : paginatedItems.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
12835
13231
  colSpan: i18n ? 5 : 4,
12836
13232
  className: "px-4 py-8 text-center text-kumo-subtle",
12837
13233
  children: _t({
12838
- id: "huTRDC",
12839
- message: "No results for \"{searchQuery}\"",
12840
- values: { searchQuery }
13234
+ id: "QpOp1w",
13235
+ message: "No results for \"{activeSearch}\"",
13236
+ values: { activeSearch }
12841
13237
  })
12842
13238
  }) }) : paginatedItems.map((item) => /* @__PURE__ */ jsx(ContentListItem, {
12843
13239
  item,
@@ -12855,10 +13251,11 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12855
13251
  children: [/* @__PURE__ */ jsx("span", {
12856
13252
  className: "text-sm text-kumo-subtle",
12857
13253
  children: renderItemCount({
12858
- searchQuery,
13254
+ searchQuery: activeSearch,
12859
13255
  filteredCount: filteredItems.length,
12860
13256
  total,
12861
- hasMore
13257
+ hasMore,
13258
+ serverSearch
12862
13259
  })
12863
13260
  }), /* @__PURE__ */ jsxs("div", {
12864
13261
  className: "flex items-center gap-2",
@@ -13041,21 +13438,26 @@ function SortableTh({ field, sort, onSortChange, label }) {
13041
13438
  }
13042
13439
  /**
13043
13440
  * Render the row-count line above pagination. The rules are:
13044
- * - A search query always wins — say how many matches there are.
13441
+ * - A search query always wins — say how many matches there are. In
13442
+ * server-search mode the server reports the full match count via `total`;
13443
+ * `filteredCount` is only the loaded page, so it would undercount.
13045
13444
  * - When the server reported a total, use it (no `+` suffix needed —
13046
13445
  * we know the count).
13047
13446
  * - Otherwise fall back to the pre-refactor behavior: loaded count,
13048
13447
  * with `+` when there are more pages the user hasn't fetched yet.
13049
13448
  */
13050
- function renderItemCount({ searchQuery, filteredCount, total, hasMore }) {
13051
- if (searchQuery) return i18n._({
13052
- id: "WqhW35",
13053
- message: "{filteredCount, plural, one {# item matching \"{searchQuery}\"} other {# items matching \"{searchQuery}\"}}",
13054
- values: {
13055
- filteredCount,
13056
- searchQuery
13057
- }
13058
- });
13449
+ function renderItemCount({ searchQuery, filteredCount, total, hasMore, serverSearch }) {
13450
+ if (searchQuery) {
13451
+ const matchCount = serverSearch && typeof total === "number" ? total : filteredCount;
13452
+ return i18n._({
13453
+ id: "UFFm2d",
13454
+ message: "{matchCount, plural, one {# item matching \"{searchQuery}\"} other {# items matching \"{searchQuery}\"}}",
13455
+ values: {
13456
+ matchCount,
13457
+ searchQuery
13458
+ }
13459
+ });
13460
+ }
13059
13461
  if (typeof total === "number") return i18n._({
13060
13462
  id: "Q/6//K",
13061
13463
  message: "{total, plural, one {# item} other {# items}}",
@@ -13593,8 +13995,8 @@ function AllowedTypesEditor({ value, onChange }) {
13593
13995
 
13594
13996
  //#endregion
13595
13997
  //#region src/components/FieldEditor.tsx
13596
- const SLUG_INVALID_CHARS_REGEX = /[^a-z0-9]+/g;
13597
- const SLUG_LEADING_TRAILING_REGEX = /^_|_$/g;
13998
+ const SLUG_INVALID_CHARS_REGEX$1 = /[^a-z0-9]+/g;
13999
+ const SLUG_LEADING_TRAILING_REGEX$1 = /^_|_$/g;
13598
14000
  function getInitialFormState(field) {
13599
14001
  if (field) return {
13600
14002
  step: "config",
@@ -13846,7 +14248,7 @@ function FieldEditor({ open, onOpenChange, field, onSave, isSaving }) {
13846
14248
  ];
13847
14249
  const handleLabelChange = (value) => {
13848
14250
  setField("label", value);
13849
- if (!field) setField("slug", value.toLowerCase().replace(SLUG_INVALID_CHARS_REGEX, "_").replace(SLUG_LEADING_TRAILING_REGEX, ""));
14251
+ if (!field) setField("slug", value.toLowerCase().replace(SLUG_INVALID_CHARS_REGEX$1, "_").replace(SLUG_LEADING_TRAILING_REGEX$1, ""));
13850
14252
  };
13851
14253
  const handleTypeSelect = (type) => {
13852
14254
  setFormState((prev) => ({
@@ -14205,7 +14607,7 @@ function FieldEditor({ open, onOpenChange, field, onSave, isSaving }) {
14205
14607
  updated[i] = {
14206
14608
  ...sf,
14207
14609
  label: e.target.value,
14208
- slug: e.target.value.toLowerCase().replace(SLUG_INVALID_CHARS_REGEX, "_").replace(SLUG_LEADING_TRAILING_REGEX, "")
14610
+ slug: e.target.value.toLowerCase().replace(SLUG_INVALID_CHARS_REGEX$1, "_").replace(SLUG_LEADING_TRAILING_REGEX$1, "")
14209
14611
  };
14210
14612
  setFormState((prev) => ({
14211
14613
  ...prev,
@@ -14992,7 +15394,7 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
14992
15394
  strategy: verticalListSortingStrategy,
14993
15395
  children: /* @__PURE__ */ jsx("div", {
14994
15396
  className: "divide-y",
14995
- children: fields.map((field) => /* @__PURE__ */ jsx(FieldRow, {
15397
+ children: fields.map((field) => /* @__PURE__ */ jsx(FieldRow$1, {
14996
15398
  field,
14997
15399
  isFromCode,
14998
15400
  onEdit: () => handleEditField(field),
@@ -15044,7 +15446,7 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
15044
15446
  ]
15045
15447
  });
15046
15448
  }
15047
- function FieldRow({ field, isFromCode, onEdit, onDelete }) {
15449
+ function FieldRow$1({ field, isFromCode, onEdit, onDelete }) {
15048
15450
  const { _: _t2 } = useLingui();
15049
15451
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
15050
15452
  id: field.id,
@@ -20221,15 +20623,34 @@ function formatDate$1(isoString) {
20221
20623
 
20222
20624
  //#endregion
20223
20625
  //#region src/components/MediaLibrary.tsx
20626
+ /** Maps a coarse type-filter choice to the media list's `mimeType` filter. */
20627
+ function mimeForTypeFilter(value) {
20628
+ switch (value) {
20629
+ case "image": return "image/";
20630
+ case "video": return "video/";
20631
+ case "audio": return "audio/";
20632
+ case "document": return ["application/", "text/"];
20633
+ default: return;
20634
+ }
20635
+ }
20224
20636
  /**
20225
20637
  * Media library component with upload, provider tabs, and grid view
20226
20638
  */
20227
- function MediaLibrary({ items = [], isLoading, onUpload, onDelete, onItemUpdated, hasMore, onLoadMore }) {
20639
+ function MediaLibrary({ items = [], isLoading, onUpload, onDelete, onItemUpdated, hasMore, onLoadMore, onLocalSearchChange, onLocalMimeFilterChange }) {
20228
20640
  const { _: _t } = useLingui();
20229
20641
  const [viewMode, setViewMode] = React$1.useState("grid");
20230
20642
  const [selectedItem, setSelectedItem] = React$1.useState(null);
20231
20643
  const [activeProvider, setActiveProvider] = React$1.useState("local");
20232
20644
  const [searchQuery, setSearchQuery] = React$1.useState("");
20645
+ const [localTypeFilter, setLocalTypeFilter] = React$1.useState("all");
20646
+ const debouncedSearch = useDebouncedValue$1(searchQuery, 300);
20647
+ React$1.useEffect(() => {
20648
+ if (activeProvider === "local" && onLocalSearchChange) onLocalSearchChange(debouncedSearch.trim());
20649
+ }, [
20650
+ debouncedSearch,
20651
+ activeProvider,
20652
+ onLocalSearchChange
20653
+ ]);
20233
20654
  const [uploadState, setUploadState] = React$1.useState({ status: "idle" });
20234
20655
  const fileInputRef = React$1.useRef(null);
20235
20656
  const [loadedDimensions, setLoadedDimensions] = React$1.useState({});
@@ -20549,17 +20970,61 @@ function MediaLibrary({ items = [], isLoading, onUpload, onDelete, onItemUpdated
20549
20970
  ]
20550
20971
  })]
20551
20972
  }),
20552
- canSearch && /* @__PURE__ */ jsxs("div", {
20553
- className: "relative max-w-sm",
20554
- children: [/* @__PURE__ */ jsx(MagnifyingGlass, { className: "absolute start-3 top-1/2 -translate-y-1/2 h-4 w-4 text-kumo-subtle" }), /* @__PURE__ */ jsx(Input, {
20555
- type: "search",
20556
- placeholder: _t({
20557
- id: "YIix5Y",
20558
- message: "Search..."
20559
- }),
20560
- value: searchQuery,
20561
- onChange: (e) => setSearchQuery(e.target.value),
20562
- className: "ps-9"
20973
+ (canSearch || activeProvider === "local") && /* @__PURE__ */ jsxs("div", {
20974
+ className: "flex flex-wrap items-center gap-3",
20975
+ children: [/* @__PURE__ */ jsxs("div", {
20976
+ className: "relative max-w-sm flex-1",
20977
+ children: [/* @__PURE__ */ jsx(MagnifyingGlass, { className: "absolute start-3 top-1/2 -translate-y-1/2 h-4 w-4 text-kumo-subtle" }), /* @__PURE__ */ jsx(Input, {
20978
+ type: "search",
20979
+ placeholder: activeProvider === "local" ? _t({
20980
+ id: "Fwo2VZ",
20981
+ message: "Search by filename..."
20982
+ }) : _t({
20983
+ id: "YIix5Y",
20984
+ message: "Search..."
20985
+ }),
20986
+ "aria-label": _t({
20987
+ id: "GzM+mT",
20988
+ message: "Search media"
20989
+ }),
20990
+ value: searchQuery,
20991
+ onChange: (e) => setSearchQuery(e.target.value),
20992
+ maxLength: MEDIA_SEARCH_MAX_LENGTH,
20993
+ className: "ps-9"
20994
+ })]
20995
+ }), activeProvider === "local" && /* @__PURE__ */ jsx(Select, {
20996
+ value: localTypeFilter,
20997
+ onValueChange: (v) => {
20998
+ const next = v ?? "all";
20999
+ setLocalTypeFilter(next);
21000
+ onLocalMimeFilterChange?.(mimeForTypeFilter(next));
21001
+ },
21002
+ items: {
21003
+ all: _t({
21004
+ id: "h6zvpR",
21005
+ message: "All types"
21006
+ }),
21007
+ image: _t({
21008
+ id: "an5hVd",
21009
+ message: "Images"
21010
+ }),
21011
+ video: _t({
21012
+ id: "vSJd18",
21013
+ message: "Video"
21014
+ }),
21015
+ audio: _t({
21016
+ id: "/rTz0M",
21017
+ message: "Audio"
21018
+ }),
21019
+ document: _t({
21020
+ id: "E/muDO",
21021
+ message: "Documents"
21022
+ })
21023
+ },
21024
+ "aria-label": _t({
21025
+ id: "mP+cvB",
21026
+ message: "Filter by type"
21027
+ })
20563
21028
  })]
20564
21029
  }),
20565
21030
  currentLoading && currentItems.length === 0 && currentProviderItems.length === 0 ? /* @__PURE__ */ jsx("div", {
@@ -21371,8 +21836,9 @@ function MenuEditor() {
21371
21836
  });
21372
21837
  };
21373
21838
  const handleAddContent = (item) => {
21839
+ const type = item.collection === "pages" ? "page" : item.collection === "posts" ? "post" : "collection";
21374
21840
  createMutation.mutate({
21375
- type: item.collection,
21841
+ type,
21376
21842
  label: item.title,
21377
21843
  referenceCollection: item.collection,
21378
21844
  referenceId: item.id
@@ -28589,7 +29055,7 @@ function SetupWizard() {
28589
29055
  * Quick navigation and search across the admin interface.
28590
29056
  * Opens with Cmd+K (Mac) or Ctrl+K (Windows/Linux).
28591
29057
  */
28592
- const ROLE_ADMIN$2 = 50;
29058
+ const ROLE_ADMIN$3 = 50;
28593
29059
  const ROLE_EDITOR$2 = 40;
28594
29060
  const ROUTE_PARAM_REGEX = /\$(\w+)/g;
28595
29061
  const SEARCH_DEBOUNCE_MS = 300;
@@ -28691,7 +29157,7 @@ function buildNavItems(manifest, userRole) {
28691
29157
  },
28692
29158
  to: "/content-types",
28693
29159
  icon: Database,
28694
- minRole: ROLE_ADMIN$2,
29160
+ minRole: ROLE_ADMIN$3,
28695
29161
  keywords: ["schema", "collections"]
28696
29162
  }, {
28697
29163
  id: "categories",
@@ -28723,7 +29189,7 @@ function buildNavItems(manifest, userRole) {
28723
29189
  },
28724
29190
  to: "/users",
28725
29191
  icon: Users,
28726
- minRole: ROLE_ADMIN$2,
29192
+ minRole: ROLE_ADMIN$3,
28727
29193
  keywords: ["accounts", "team"]
28728
29194
  }, {
28729
29195
  id: "plugins",
@@ -28733,7 +29199,7 @@ function buildNavItems(manifest, userRole) {
28733
29199
  },
28734
29200
  to: "/plugins-manager",
28735
29201
  icon: PuzzlePiece,
28736
- minRole: ROLE_ADMIN$2,
29202
+ minRole: ROLE_ADMIN$3,
28737
29203
  keywords: ["extensions", "add-ons"]
28738
29204
  }, {
28739
29205
  id: "import",
@@ -28743,7 +29209,7 @@ function buildNavItems(manifest, userRole) {
28743
29209
  },
28744
29210
  to: "/import/wordpress",
28745
29211
  icon: Upload,
28746
- minRole: ROLE_ADMIN$2,
29212
+ minRole: ROLE_ADMIN$3,
28747
29213
  keywords: ["wordpress", "migrate"]
28748
29214
  }, {
28749
29215
  id: "settings",
@@ -28753,7 +29219,7 @@ function buildNavItems(manifest, userRole) {
28753
29219
  },
28754
29220
  to: "/settings",
28755
29221
  icon: Gear,
28756
- minRole: ROLE_ADMIN$2,
29222
+ minRole: ROLE_ADMIN$3,
28757
29223
  keywords: ["configuration", "preferences"]
28758
29224
  }, {
28759
29225
  id: "security",
@@ -28763,7 +29229,7 @@ function buildNavItems(manifest, userRole) {
28763
29229
  },
28764
29230
  to: "/settings/security",
28765
29231
  icon: Gear,
28766
- minRole: ROLE_ADMIN$2,
29232
+ minRole: ROLE_ADMIN$3,
28767
29233
  keywords: ["passkeys", "authentication"]
28768
29234
  });
28769
29235
  for (const [pluginId, config] of Object.entries(manifest.plugins)) {
@@ -28953,9 +29419,124 @@ function AdminCommandPalette({ manifest }) {
28953
29419
 
28954
29420
  //#endregion
28955
29421
  //#region src/components/Sidebar.tsx
28956
- const ROLE_ADMIN$1 = 50;
29422
+ const ROLE_ADMIN$2 = 50;
28957
29423
  const ROLE_EDITOR$1 = 40;
28958
29424
  /**
29425
+ * Static invariants for nav entries that have AC-level visibility
29426
+ * requirements (Phase 5 of Discussion #1174: "Admin sees the 'Byline
29427
+ * Schema' entry; Editor does not").
29428
+ *
29429
+ * Exported as plain data so a unit test can assert the route + role
29430
+ * pairing without mounting Kumo's Sidebar primitive — which portals
29431
+ * its rendered content to `document.body` and applies collapse-state
29432
+ * CSS (`display:none` on labels at narrow viewports), making
29433
+ * full-DOM tests of role filtering brittle. The runtime `adminItems`
29434
+ * array below references these constants directly so the test
29435
+ * effectively guards the production list.
29436
+ */
29437
+ const BYLINE_SCHEMA_NAV_ITEM = {
29438
+ to: "/byline-schema",
29439
+ minRole: ROLE_ADMIN$2
29440
+ };
29441
+ /**
29442
+ * Filter a nav-items list by user role. Pure function — exported so
29443
+ * tests can verify the role gate without rendering the sidebar. An
29444
+ * item passes when it has no `minRole` (public) or the user is at
29445
+ * least the required level.
29446
+ */
29447
+ function filterNavItemsByRole(items, userRole) {
29448
+ return items.filter((item) => !item.minRole || userRole >= item.minRole);
29449
+ }
29450
+ /**
29451
+ * Static map of common plugin admin-page icon names to Phosphor components.
29452
+ *
29453
+ * Plugins declare `adminPages: [{ path, label, icon }]`, where `icon` is a
29454
+ * lower/kebab name. This table covers the names used across the EmDash
29455
+ * docs/templates (including lucide-style names like `settings`/`chart` that
29456
+ * don't match Phosphor's own naming) plus common nav glyphs. These are
29457
+ * statically imported, so the everyday case resolves *synchronously* and the
29458
+ * handful of components ship in the main bundle — the full Phosphor set is
29459
+ * never pulled in for them. Any name not listed here is resolved lazily
29460
+ * (see `resolveNavIcon`), so there is no hard ceiling.
29461
+ */
29462
+ const NAV_ICON_MAP = {
29463
+ settings: Gear,
29464
+ gear: Gear,
29465
+ chart: ChartBar,
29466
+ "chart-line": ChartLine,
29467
+ dashboard: SquaresFour,
29468
+ history: ClockCounterClockwise,
29469
+ image: Image$1,
29470
+ award: Medal,
29471
+ trophy: Trophy,
29472
+ grid: GridFour,
29473
+ crop: Crop,
29474
+ book: BookOpen,
29475
+ plug: Plug,
29476
+ code: Code,
29477
+ file: FileText,
29478
+ document: FileText,
29479
+ users: Users,
29480
+ database: Database,
29481
+ list: List,
29482
+ calendar: CalendarBlank,
29483
+ bell: Bell,
29484
+ folder: Folder,
29485
+ star: Star,
29486
+ tag: Tag,
29487
+ link: LinkSimple,
29488
+ search: MagnifyingGlass,
29489
+ palette: Palette,
29490
+ upload: Upload
29491
+ };
29492
+ /** Word separators in icon names: kebab, snake, or whitespace. */
29493
+ const ICON_NAME_SEPARATOR = /[-_\s]+/;
29494
+ /**
29495
+ * Convert a kebab/snake/space icon name to Phosphor's PascalCase component
29496
+ * name (`chart-bar` → `ChartBar`). Exported for unit testing the pure mapping.
29497
+ */
29498
+ function toPhosphorIconName(name) {
29499
+ return name.split(ICON_NAME_SEPARATOR).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
29500
+ }
29501
+ /**
29502
+ * Cache of lazily-loaded icon components, keyed by Phosphor component name.
29503
+ * `React.lazy` must return a stable identity across renders (a fresh lazy
29504
+ * component on every render would remount and re-suspend), so memoize here.
29505
+ */
29506
+ const lazyIconCache = /* @__PURE__ */ new Map();
29507
+ /**
29508
+ * Resolve a plugin page's `icon` name to a component.
29509
+ *
29510
+ * Resolution order:
29511
+ * 1. No icon → `PuzzlePiece` (the common icon-less page never suspends).
29512
+ * 2. A name in `NAV_ICON_MAP` → its statically-imported component (sync,
29513
+ * already in the main bundle — no extra chunk for everyday icons).
29514
+ * 3. Anything else → the matching `@phosphor-icons/react` component, loaded
29515
+ * lazily from a code-split chunk the first time it's used. This gives
29516
+ * access to the entire Phosphor set without pulling it into the main
29517
+ * bundle, and only loads when a plugin uses an icon outside the map.
29518
+ * Names that don't exist in Phosphor fall back to `PuzzlePiece`.
29519
+ *
29520
+ * Case 3 returns a `React.lazy` component, so call sites must render the
29521
+ * result inside a `<React.Suspense>` boundary (see `NavMenuLink`). Exported
29522
+ * so a unit test can assert resolution without mounting the portal-heavy
29523
+ * Kumo Sidebar.
29524
+ */
29525
+ function resolveNavIcon(name) {
29526
+ if (!name) return PuzzlePiece;
29527
+ const mapped = NAV_ICON_MAP[name];
29528
+ if (mapped) return mapped;
29529
+ const componentName = toPhosphorIconName(name);
29530
+ let icon = lazyIconCache.get(componentName);
29531
+ if (!icon) {
29532
+ icon = React$1.lazy(async () => {
29533
+ return { default: (await import("@phosphor-icons/react"))[componentName] ?? PuzzlePiece };
29534
+ });
29535
+ lazyIconCache.set(componentName, icon);
29536
+ }
29537
+ return icon;
29538
+ }
29539
+ /**
28959
29540
  * Navigation item rendered as a TanStack Router <Link> inside kumo's
28960
29541
  * Sidebar.MenuItem. Styled to match kumo MenuButton appearance.
28961
29542
  * This approach guarantees client-side navigation works correctly.
@@ -28963,6 +29544,7 @@ const ROLE_EDITOR$1 = 40;
28963
29544
  function NavMenuLink({ item, isActive }) {
28964
29545
  const { state } = useSidebar();
28965
29546
  const Icon = item.icon;
29547
+ const iconClassName = cn("emdash-nav-icon size-[18px] shrink-0 transition-colors duration-200", isActive ? "text-white" : "text-white/60 group-hover/menu-button:text-white/90");
28966
29548
  const link = /* @__PURE__ */ jsxs(Link$1, {
28967
29549
  to: item.to,
28968
29550
  params: item.params,
@@ -28970,9 +29552,15 @@ function NavMenuLink({ item, isActive }) {
28970
29552
  "data-active": isActive || void 0,
28971
29553
  "data-sidebar": "menu-button",
28972
29554
  className: cn("emdash-nav-link group/menu-button flex w-full min-w-0 items-center gap-2.5 rounded-md no-underline outline-none cursor-pointer", "min-h-[36px] px-3 py-1.5 text-[13px]", "transition-all duration-200 ease-out", isActive ? "bg-kumo-brand text-white" : "text-white/70 hover:text-white hover:bg-white/8", "focus-visible:ring-2 focus-visible:ring-kumo-brand/50"),
28973
- children: [/* @__PURE__ */ jsx(Icon, {
28974
- className: cn("emdash-nav-icon size-[18px] shrink-0 transition-colors duration-200", isActive ? "text-white" : "text-white/60 group-hover/menu-button:text-white/90"),
28975
- "aria-hidden": "true"
29555
+ children: [/* @__PURE__ */ jsx(React$1.Suspense, {
29556
+ fallback: /* @__PURE__ */ jsx(PuzzlePiece, {
29557
+ className: iconClassName,
29558
+ "aria-hidden": "true"
29559
+ }),
29560
+ children: /* @__PURE__ */ jsx(Icon, {
29561
+ className: iconClassName,
29562
+ "aria-hidden": "true"
29563
+ })
28976
29564
  }), /* @__PURE__ */ jsxs("span", {
28977
29565
  className: "emdash-nav-label flex flex-1 items-center min-w-0 text-start overflow-hidden",
28978
29566
  children: [item.label, item.badge != null && item.badge > 0 && /* @__PURE__ */ jsx(KumoSidebar.MenuBadge, { children: item.badge })]
@@ -29060,7 +29648,7 @@ function SidebarNav({ manifest }) {
29060
29648
  message: "Redirects"
29061
29649
  }),
29062
29650
  icon: ArrowsLeftRight,
29063
- minRole: ROLE_ADMIN$1
29651
+ minRole: ROLE_ADMIN$2
29064
29652
  },
29065
29653
  {
29066
29654
  to: "/widgets",
@@ -29105,7 +29693,7 @@ function SidebarNav({ manifest }) {
29105
29693
  message: "Content Types"
29106
29694
  }),
29107
29695
  icon: Database,
29108
- minRole: ROLE_ADMIN$1
29696
+ minRole: ROLE_ADMIN$2
29109
29697
  },
29110
29698
  {
29111
29699
  to: "/users",
@@ -29114,7 +29702,7 @@ function SidebarNav({ manifest }) {
29114
29702
  message: "Users"
29115
29703
  }),
29116
29704
  icon: Users,
29117
- minRole: ROLE_ADMIN$1
29705
+ minRole: ROLE_ADMIN$2
29118
29706
  },
29119
29707
  {
29120
29708
  to: "/plugins-manager",
@@ -29123,7 +29711,7 @@ function SidebarNav({ manifest }) {
29123
29711
  message: "Plugins"
29124
29712
  }),
29125
29713
  icon: PuzzlePiece,
29126
- minRole: ROLE_ADMIN$1
29714
+ minRole: ROLE_ADMIN$2
29127
29715
  }
29128
29716
  ];
29129
29717
  if (manifest.registry) adminItems.push({
@@ -29133,7 +29721,7 @@ function SidebarNav({ manifest }) {
29133
29721
  message: "Registry"
29134
29722
  }),
29135
29723
  icon: Storefront,
29136
- minRole: ROLE_ADMIN$1
29724
+ minRole: ROLE_ADMIN$2
29137
29725
  });
29138
29726
  else if (manifest.marketplace) adminItems.push({
29139
29727
  to: "/plugins/marketplace",
@@ -29142,7 +29730,7 @@ function SidebarNav({ manifest }) {
29142
29730
  message: "Marketplace"
29143
29731
  }),
29144
29732
  icon: Storefront,
29145
- minRole: ROLE_ADMIN$1
29733
+ minRole: ROLE_ADMIN$2
29146
29734
  });
29147
29735
  if (manifest.marketplace) adminItems.push({
29148
29736
  to: "/themes/marketplace",
@@ -29151,7 +29739,7 @@ function SidebarNav({ manifest }) {
29151
29739
  message: "Themes"
29152
29740
  }),
29153
29741
  icon: Palette,
29154
- minRole: ROLE_ADMIN$1
29742
+ minRole: ROLE_ADMIN$2
29155
29743
  });
29156
29744
  adminItems.push({
29157
29745
  to: "/import/wordpress",
@@ -29160,7 +29748,7 @@ function SidebarNav({ manifest }) {
29160
29748
  message: "Import"
29161
29749
  }),
29162
29750
  icon: Upload,
29163
- minRole: ROLE_ADMIN$1
29751
+ minRole: ROLE_ADMIN$2
29164
29752
  }, {
29165
29753
  to: "/settings",
29166
29754
  label: _t({
@@ -29168,7 +29756,7 @@ function SidebarNav({ manifest }) {
29168
29756
  message: "Settings"
29169
29757
  }),
29170
29758
  icon: Gear,
29171
- minRole: ROLE_ADMIN$1
29759
+ minRole: ROLE_ADMIN$2
29172
29760
  });
29173
29761
  const pluginItems = [];
29174
29762
  for (const [pluginId, config] of Object.entries(manifest.plugins)) {
@@ -29182,16 +29770,15 @@ function SidebarNav({ manifest }) {
29182
29770
  pluginItems.push({
29183
29771
  to: `/plugins/${pluginId}${page.path}`,
29184
29772
  label,
29185
- icon: PuzzlePiece
29773
+ icon: resolveNavIcon(page.icon)
29186
29774
  });
29187
29775
  }
29188
29776
  }
29189
29777
  }
29190
- const filterByRole = (items) => items.filter((item) => !item.minRole || userRole >= item.minRole);
29191
- const visibleContent = filterByRole(contentItems);
29192
- const visibleManage = filterByRole(manageItems);
29193
- const visibleAdmin = filterByRole(adminItems);
29194
- const visiblePlugins = filterByRole(pluginItems);
29778
+ const visibleContent = filterNavItemsByRole(contentItems, userRole);
29779
+ const visibleManage = filterNavItemsByRole(manageItems, userRole);
29780
+ const visibleAdmin = filterNavItemsByRole(adminItems, userRole);
29781
+ const visiblePlugins = filterNavItemsByRole(pluginItems, userRole);
29195
29782
  function renderNavItems(items) {
29196
29783
  return items.map((item, index) => {
29197
29784
  return /* @__PURE__ */ jsx(NavMenuLink, {
@@ -35288,6 +35875,858 @@ function AuthorMappingStep({ authorMappings, emdashUsers, onMappingChange, onCon
35288
35875
  });
35289
35876
  }
35290
35877
 
35878
+ //#endregion
35879
+ //#region src/components/BylineFieldEditor.tsx
35880
+ /**
35881
+ * BylineFieldEditor (Phase 5 of Discussion #1174).
35882
+ *
35883
+ * Purpose-built create/edit dialog for byline custom-field definitions.
35884
+ * Constrains the type select to the five v1 types declared by the
35885
+ * `BylineFieldType` union — narrower than the content-field universe —
35886
+ * and exposes the `translatable` toggle (the storage-split flag) that
35887
+ * the content `FieldEditor` doesn't have.
35888
+ *
35889
+ * Single-step form (not the type-picker → config flow used by
35890
+ * `FieldEditor`): with only five types a `<Select>` is faster than a
35891
+ * grid of cards and the dialog stays under ~200 LOC.
35892
+ *
35893
+ * The `translatable` toggle is locked when editing a field that already
35894
+ * has stored values — the server returns `TRANSLATABLE_LOCKED` (409) for
35895
+ * the flip, but disabling the input + showing a help message saves a
35896
+ * round-trip and is friendlier than a toast. Usage is passed in by the
35897
+ * parent (which fetches `getBylineFieldUsage` on edit-open) so this
35898
+ * component stays a pure controlled form.
35899
+ */
35900
+ const SLUG_INVALID_CHARS_REGEX = /[^a-z0-9]+/g;
35901
+ const SLUG_LEADING_TRAILING_REGEX = /^_|_$/g;
35902
+ /**
35903
+ * The five v1 byline field types. Kept in lock-step with the server-side
35904
+ * `BYLINE_FIELD_TYPES` constant (`packages/core/src/schema/types.ts`).
35905
+ *
35906
+ * `MessageDescriptor`s are resolved with `t(descriptor)` inside the
35907
+ * component so the strings extract correctly to the Lingui catalog.
35908
+ */
35909
+ const TYPE_OPTIONS = [
35910
+ {
35911
+ value: "string",
35912
+ label: {
35913
+ id: "q13dKg",
35914
+ message: "Short text"
35915
+ }
35916
+ },
35917
+ {
35918
+ value: "text",
35919
+ label: {
35920
+ id: "g+zzQs",
35921
+ message: "Long text"
35922
+ }
35923
+ },
35924
+ {
35925
+ value: "url",
35926
+ label: {
35927
+ id: "IagCbF",
35928
+ message: "URL"
35929
+ }
35930
+ },
35931
+ {
35932
+ value: "boolean",
35933
+ label: {
35934
+ id: "bOwPjT",
35935
+ message: "Boolean"
35936
+ }
35937
+ },
35938
+ {
35939
+ value: "select",
35940
+ label: {
35941
+ id: "rG3WVm",
35942
+ message: "Select"
35943
+ }
35944
+ }
35945
+ ];
35946
+ function initialFormState(field) {
35947
+ if (!field) return {
35948
+ slug: "",
35949
+ label: "",
35950
+ type: "string",
35951
+ required: false,
35952
+ translatable: true,
35953
+ options: ""
35954
+ };
35955
+ return {
35956
+ slug: field.slug,
35957
+ label: field.label,
35958
+ type: field.type,
35959
+ required: field.required,
35960
+ translatable: field.translatable,
35961
+ options: field.validation?.options?.join("\n") ?? ""
35962
+ };
35963
+ }
35964
+ function BylineFieldEditor({ open, onOpenChange, field, usageTotal = 0, onSave, isSaving, error }) {
35965
+ const { _: _t } = useLingui();
35966
+ const isEdit = !!field;
35967
+ const [state, setState] = React$1.useState(() => initialFormState(field));
35968
+ React$1.useEffect(() => {
35969
+ if (open) setState(initialFormState(field));
35970
+ }, [open, field]);
35971
+ const setField = (key, value) => setState((prev) => ({
35972
+ ...prev,
35973
+ [key]: value
35974
+ }));
35975
+ const handleLabelChange = (value) => {
35976
+ setField("label", value);
35977
+ if (!isEdit) setField("slug", value.toLowerCase().replace(SLUG_INVALID_CHARS_REGEX, "_").replace(SLUG_LEADING_TRAILING_REGEX, ""));
35978
+ };
35979
+ const parsedSelectOptions = state.type === "select" ? state.options.split("\n").map((o) => o.trim()).filter(Boolean) : [];
35980
+ const handleSave = () => {
35981
+ if (!state.label.trim()) return;
35982
+ if (!isEdit && !state.slug.trim()) return;
35983
+ if (state.type === "select" && parsedSelectOptions.length === 0) return;
35984
+ const validation = state.type === "select" ? { options: parsedSelectOptions } : null;
35985
+ if (isEdit) onSave({
35986
+ label: state.label,
35987
+ required: state.required,
35988
+ translatable: field && state.translatable !== field.translatable ? state.translatable : void 0,
35989
+ validation
35990
+ });
35991
+ else onSave({
35992
+ slug: state.slug,
35993
+ label: state.label,
35994
+ type: state.type,
35995
+ required: state.required,
35996
+ translatable: state.translatable,
35997
+ validation
35998
+ });
35999
+ };
36000
+ const translatableLocked = isEdit && usageTotal > 0;
36001
+ const canSave = state.label.trim().length > 0 && (isEdit || state.slug.trim().length > 0) && (state.type !== "select" || parsedSelectOptions.length > 0);
36002
+ return /* @__PURE__ */ jsx(Dialog.Root, {
36003
+ open,
36004
+ onOpenChange,
36005
+ children: /* @__PURE__ */ jsxs(Dialog, {
36006
+ className: "p-6 max-w-xl",
36007
+ size: "lg",
36008
+ children: [
36009
+ /* @__PURE__ */ jsxs("div", {
36010
+ className: "flex items-start justify-between gap-4 mb-4",
36011
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
36012
+ className: "text-lg font-semibold leading-none tracking-tight",
36013
+ children: isEdit ? _t({
36014
+ id: "hcfOYc",
36015
+ message: "Edit byline field"
36016
+ }) : _t({
36017
+ id: "hT2KP5",
36018
+ message: "New byline field"
36019
+ })
36020
+ }), /* @__PURE__ */ jsx(Dialog.Close, {
36021
+ "aria-label": _t({
36022
+ id: "yz7wBu",
36023
+ message: "Close"
36024
+ }),
36025
+ render: (props) => /* @__PURE__ */ jsxs(Button, {
36026
+ ...props,
36027
+ variant: "ghost",
36028
+ shape: "square",
36029
+ "aria-label": _t({
36030
+ id: "yz7wBu",
36031
+ message: "Close"
36032
+ }),
36033
+ className: "absolute end-4 top-4",
36034
+ children: [/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", {
36035
+ className: "sr-only",
36036
+ children: _t({
36037
+ id: "yz7wBu",
36038
+ message: "Close"
36039
+ })
36040
+ })]
36041
+ })
36042
+ })]
36043
+ }),
36044
+ /* @__PURE__ */ jsxs("div", {
36045
+ className: "space-y-5",
36046
+ children: [
36047
+ /* @__PURE__ */ jsxs("div", {
36048
+ className: "grid grid-cols-2 gap-4",
36049
+ children: [/* @__PURE__ */ jsx(Input, {
36050
+ label: _t({
36051
+ id: "87a/t/",
36052
+ message: "Label"
36053
+ }),
36054
+ value: state.label,
36055
+ onChange: (e) => handleLabelChange(e.target.value),
36056
+ placeholder: _t({
36057
+ id: "qMgKul",
36058
+ message: "Job title"
36059
+ })
36060
+ }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Input, {
36061
+ label: _t({
36062
+ id: "L85WcV",
36063
+ message: "Slug"
36064
+ }),
36065
+ value: state.slug,
36066
+ onChange: (e) => setField("slug", e.target.value),
36067
+ placeholder: _t({
36068
+ id: "gddbae",
36069
+ message: "job_title"
36070
+ }),
36071
+ disabled: isEdit
36072
+ }), isEdit && /* @__PURE__ */ jsx("p", {
36073
+ className: "text-xs text-kumo-subtle mt-2",
36074
+ children: _t({
36075
+ id: "5OXS9U",
36076
+ message: "Slugs cannot be changed after the field is created."
36077
+ })
36078
+ })] })]
36079
+ }),
36080
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Select, {
36081
+ label: _t({
36082
+ id: "+zy2Nq",
36083
+ message: "Type"
36084
+ }),
36085
+ value: state.type,
36086
+ onValueChange: (v) => setField("type", v ?? "string"),
36087
+ items: Object.fromEntries(TYPE_OPTIONS.map(({ value, label }) => [value, _t(label)])),
36088
+ disabled: isEdit
36089
+ }), isEdit && /* @__PURE__ */ jsx("p", {
36090
+ className: "text-xs text-kumo-subtle mt-2",
36091
+ children: _t({
36092
+ id: "NF9uuv",
36093
+ message: "Field type cannot be changed after creation."
36094
+ })
36095
+ })] }),
36096
+ /* @__PURE__ */ jsxs("div", {
36097
+ className: "flex flex-wrap items-center gap-6",
36098
+ children: [/* @__PURE__ */ jsx(Switch, {
36099
+ checked: state.required,
36100
+ onCheckedChange: (checked) => setField("required", checked),
36101
+ label: /* @__PURE__ */ jsx("span", {
36102
+ className: "text-sm",
36103
+ children: _t({
36104
+ id: "TMLAx2",
36105
+ message: "Required"
36106
+ })
36107
+ })
36108
+ }), /* @__PURE__ */ jsxs("div", {
36109
+ className: "flex flex-col gap-1",
36110
+ children: [/* @__PURE__ */ jsx(Switch, {
36111
+ checked: state.translatable,
36112
+ onCheckedChange: (checked) => setField("translatable", checked),
36113
+ disabled: translatableLocked,
36114
+ label: /* @__PURE__ */ jsx("span", {
36115
+ className: "text-sm",
36116
+ children: _t({
36117
+ id: "O/SdND",
36118
+ message: "Translatable"
36119
+ })
36120
+ })
36121
+ }), /* @__PURE__ */ jsx("p", {
36122
+ className: "text-xs text-kumo-subtle max-w-xs",
36123
+ children: translatableLocked ? _t({
36124
+ id: "3GPtwD",
36125
+ message: "Locked because this field has stored values. Delete the values (or the field) to change this."
36126
+ }) : state.translatable ? _t({
36127
+ id: "ONAVuM",
36128
+ message: "Stored per locale — each translation of a byline gets its own value."
36129
+ }) : _t({
36130
+ id: "msPOkF",
36131
+ message: "Shared across all translations of the same byline."
36132
+ })
36133
+ })]
36134
+ })]
36135
+ }),
36136
+ state.type === "select" && /* @__PURE__ */ jsx(InputArea, {
36137
+ label: _t({
36138
+ id: "okW6+j",
36139
+ message: "Options (one per line)"
36140
+ }),
36141
+ value: state.options,
36142
+ onChange: (e) => setField("options", e.target.value),
36143
+ placeholder: _t({
36144
+ id: "Y9cJqL",
36145
+ message: "Editor\nReporter\nPhotographer"
36146
+ }),
36147
+ rows: 5
36148
+ }),
36149
+ /* @__PURE__ */ jsx(DialogError, {
36150
+ message: getMutationError(error),
36151
+ className: "mt-1"
36152
+ })
36153
+ ]
36154
+ }),
36155
+ /* @__PURE__ */ jsxs("div", {
36156
+ className: "mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
36157
+ children: [/* @__PURE__ */ jsx(Button, {
36158
+ variant: "outline",
36159
+ onClick: () => onOpenChange(false),
36160
+ disabled: isSaving,
36161
+ children: _t({
36162
+ id: "dEgA5A",
36163
+ message: "Cancel"
36164
+ })
36165
+ }), /* @__PURE__ */ jsx(Button, {
36166
+ onClick: handleSave,
36167
+ disabled: !canSave || isSaving,
36168
+ children: isSaving ? _t({
36169
+ id: "K/F6pa",
36170
+ message: "Saving…"
36171
+ }) : isEdit ? _t({
36172
+ id: "y3aU20",
36173
+ message: "Save changes"
36174
+ }) : _t({
36175
+ id: "A3eIjV",
36176
+ message: "Create field"
36177
+ })
36178
+ })]
36179
+ })
36180
+ ]
36181
+ })
36182
+ });
36183
+ }
36184
+
36185
+ //#endregion
36186
+ //#region src/routes/byline-schema.tsx
36187
+ /**
36188
+ * Byline custom-field schema management (Phase 5 of Discussion #1174).
36189
+ *
36190
+ * Admin can create / edit / delete / reorder byline custom-field
36191
+ * definitions. Modelled on the content-type management UX but uses the
36192
+ * purpose-built `BylineFieldEditor` (5-type subset + `translatable`
36193
+ * toggle).
36194
+ *
36195
+ * Reorder uses ↑/↓ buttons per row rather than drag-and-drop — the v1
36196
+ * registry caps the field set at a small number (admins typically
36197
+ * register 2–5 custom fields per site), accessibility is better without
36198
+ * pulling in a DnD dependency, and screen-reader users get usable
36199
+ * keyboard semantics for free. The reorder API takes the full slug list
36200
+ * in the desired order; clicking ↑ swaps the row with its neighbour and
36201
+ * fires the mutation.
36202
+ *
36203
+ * Permission gate: `ROLE_ADMIN`. The route is also gated at the sidebar
36204
+ * level (`minRole: ROLE_ADMIN`), but a manually-typed URL still hits
36205
+ * this page — so the in-component check is the source of truth.
36206
+ */
36207
+ const ROLE_ADMIN$1 = 50;
36208
+ const QUERY_KEY = ["byline-fields"];
36209
+ function BylineSchemaPage() {
36210
+ const { _: _t } = useLingui();
36211
+ const queryClient = useQueryClient();
36212
+ const toastManager = Toast.useToastManager();
36213
+ const { data: currentUser, isLoading: userLoading } = useCurrentUser();
36214
+ const [editingField, setEditingField] = React$1.useState(null);
36215
+ const [creating, setCreating] = React$1.useState(false);
36216
+ const [deletingField, setDeletingField] = React$1.useState(null);
36217
+ const fieldsQuery = useQuery({
36218
+ queryKey: QUERY_KEY,
36219
+ queryFn: listBylineFields,
36220
+ enabled: !!currentUser && currentUser.role >= ROLE_ADMIN$1
36221
+ });
36222
+ const usageQuery = useQuery({
36223
+ queryKey: [
36224
+ "byline-schema",
36225
+ "usage",
36226
+ editingField?.slug ?? null
36227
+ ],
36228
+ queryFn: () => editingField ? getBylineFieldUsage(editingField.slug) : Promise.resolve(null),
36229
+ enabled: !!editingField
36230
+ });
36231
+ const deleteUsageQuery = useQuery({
36232
+ queryKey: [
36233
+ "byline-schema",
36234
+ "usage",
36235
+ deletingField?.slug ?? null
36236
+ ],
36237
+ queryFn: () => deletingField ? getBylineFieldUsage(deletingField.slug) : Promise.resolve(null),
36238
+ enabled: !!deletingField
36239
+ });
36240
+ const createMutation = useMutation({
36241
+ mutationFn: (input) => createBylineField(input),
36242
+ onSuccess: (field) => {
36243
+ queryClient.invalidateQueries({ queryKey: QUERY_KEY });
36244
+ toastManager.add({
36245
+ title: _t({
36246
+ id: "5fyK5P",
36247
+ message: "Field created"
36248
+ }),
36249
+ description: _t({
36250
+ id: "CmHSIV",
36251
+ message: "Created \"{0}\".",
36252
+ values: { 0: field.label }
36253
+ })
36254
+ });
36255
+ setCreating(false);
36256
+ },
36257
+ onError: (mutationError) => {
36258
+ toastManager.add({
36259
+ title: _t({
36260
+ id: "WWQD//",
36261
+ message: "Failed to create field"
36262
+ }),
36263
+ description: mutationError instanceof Error ? mutationError.message : _t({
36264
+ id: "Vw8l6h",
36265
+ message: "An error occurred"
36266
+ }),
36267
+ type: "error"
36268
+ });
36269
+ }
36270
+ });
36271
+ const updateMutation = useMutation({
36272
+ mutationFn: (vars) => updateBylineField(vars.slug, vars.input),
36273
+ onSuccess: (field) => {
36274
+ queryClient.invalidateQueries({ queryKey: QUERY_KEY });
36275
+ toastManager.add({
36276
+ title: _t({
36277
+ id: "oaEgX0",
36278
+ message: "Field updated"
36279
+ }),
36280
+ description: _t({
36281
+ id: "m64wqx",
36282
+ message: "Saved \"{0}\".",
36283
+ values: { 0: field.label }
36284
+ })
36285
+ });
36286
+ setEditingField(null);
36287
+ },
36288
+ onError: (mutationError) => {
36289
+ toastManager.add({
36290
+ title: _t({
36291
+ id: "nfiPO9",
36292
+ message: "Failed to save field"
36293
+ }),
36294
+ description: mutationError instanceof Error ? mutationError.message : _t({
36295
+ id: "Vw8l6h",
36296
+ message: "An error occurred"
36297
+ }),
36298
+ type: "error"
36299
+ });
36300
+ }
36301
+ });
36302
+ const deleteMutation = useMutation({
36303
+ mutationFn: (slug) => deleteBylineField(slug),
36304
+ onSuccess: () => {
36305
+ queryClient.invalidateQueries({ queryKey: QUERY_KEY });
36306
+ toastManager.add({ title: _t({
36307
+ id: "LX8Pu1",
36308
+ message: "Field deleted"
36309
+ }) });
36310
+ setDeletingField(null);
36311
+ },
36312
+ onError: (mutationError) => {
36313
+ toastManager.add({
36314
+ title: _t({
36315
+ id: "e0xtVp",
36316
+ message: "Failed to delete field"
36317
+ }),
36318
+ description: mutationError instanceof Error ? mutationError.message : _t({
36319
+ id: "Vw8l6h",
36320
+ message: "An error occurred"
36321
+ }),
36322
+ type: "error"
36323
+ });
36324
+ }
36325
+ });
36326
+ const reorderMutation = useMutation({
36327
+ mutationFn: (slugs) => reorderBylineFields(slugs),
36328
+ onSuccess: () => {
36329
+ queryClient.invalidateQueries({ queryKey: QUERY_KEY });
36330
+ },
36331
+ onError: (mutationError) => {
36332
+ toastManager.add({
36333
+ title: _t({
36334
+ id: "eGNX2z",
36335
+ message: "Failed to reorder fields"
36336
+ }),
36337
+ description: mutationError instanceof Error ? mutationError.message : _t({
36338
+ id: "Vw8l6h",
36339
+ message: "An error occurred"
36340
+ }),
36341
+ type: "error"
36342
+ });
36343
+ }
36344
+ });
36345
+ if (userLoading) return /* @__PURE__ */ jsx("div", {
36346
+ className: "flex items-center justify-center min-h-[50vh]",
36347
+ children: /* @__PURE__ */ jsx(Loader, {})
36348
+ });
36349
+ if (!currentUser || currentUser.role < ROLE_ADMIN$1) return /* @__PURE__ */ jsx("div", {
36350
+ className: "flex items-center justify-center min-h-[50vh]",
36351
+ children: /* @__PURE__ */ jsxs("div", {
36352
+ className: "text-center",
36353
+ children: [/* @__PURE__ */ jsx("h1", {
36354
+ className: "text-2xl font-bold",
36355
+ children: _t({
36356
+ id: "S2Rr8h",
36357
+ message: "Access denied"
36358
+ })
36359
+ }), /* @__PURE__ */ jsx("p", {
36360
+ className: "mt-2 text-kumo-subtle",
36361
+ children: _t({
36362
+ id: "MKw3nY",
36363
+ message: "You need admin permissions to manage byline schema."
36364
+ })
36365
+ })]
36366
+ })
36367
+ });
36368
+ const fields = fieldsQuery.data?.items ?? [];
36369
+ const handleMove = (index, delta) => {
36370
+ const targetIndex = index + delta;
36371
+ if (targetIndex < 0 || targetIndex >= fields.length) return;
36372
+ const next = [...fields];
36373
+ const a = next[index];
36374
+ const b = next[targetIndex];
36375
+ if (!a || !b) return;
36376
+ next[index] = b;
36377
+ next[targetIndex] = a;
36378
+ reorderMutation.mutate(next.map((f) => f.slug));
36379
+ };
36380
+ const handleCreate = (input) => {
36381
+ if ("slug" in input && "type" in input) createMutation.mutate(input);
36382
+ };
36383
+ const handleEdit = (input) => {
36384
+ if (!editingField) return;
36385
+ if (!("slug" in input) && !("type" in input)) updateMutation.mutate({
36386
+ slug: editingField.slug,
36387
+ input
36388
+ });
36389
+ };
36390
+ return /* @__PURE__ */ jsxs("div", {
36391
+ className: "space-y-4",
36392
+ children: [
36393
+ /* @__PURE__ */ jsxs("div", {
36394
+ className: "flex items-center justify-between",
36395
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
36396
+ className: "text-2xl font-bold",
36397
+ children: _t({
36398
+ id: "5YHQju",
36399
+ message: "Byline schema"
36400
+ })
36401
+ }), /* @__PURE__ */ jsx("p", {
36402
+ className: "text-kumo-subtle text-sm",
36403
+ children: _t({
36404
+ id: "yYyG5z",
36405
+ message: "Define custom fields stored on every byline — job title, pronouns, social handles, and more."
36406
+ })
36407
+ })] }), /* @__PURE__ */ jsx(Button, {
36408
+ icon: /* @__PURE__ */ jsx(Plus, {}),
36409
+ onClick: () => setCreating(true),
36410
+ children: _t({
36411
+ id: "kV1LSq",
36412
+ message: "New field"
36413
+ })
36414
+ })]
36415
+ }),
36416
+ /* @__PURE__ */ jsx("div", {
36417
+ className: "rounded-md border bg-kumo-base overflow-x-auto",
36418
+ children: /* @__PURE__ */ jsxs("table", {
36419
+ className: "w-full",
36420
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
36421
+ className: "border-b bg-kumo-tint/50",
36422
+ children: [
36423
+ /* @__PURE__ */ jsx("th", {
36424
+ scope: "col",
36425
+ className: "px-4 py-3 text-start text-sm font-medium",
36426
+ children: _t({
36427
+ id: "87a/t/",
36428
+ message: "Label"
36429
+ })
36430
+ }),
36431
+ /* @__PURE__ */ jsx("th", {
36432
+ scope: "col",
36433
+ className: "px-4 py-3 text-start text-sm font-medium",
36434
+ children: _t({
36435
+ id: "L85WcV",
36436
+ message: "Slug"
36437
+ })
36438
+ }),
36439
+ /* @__PURE__ */ jsx("th", {
36440
+ scope: "col",
36441
+ className: "px-4 py-3 text-start text-sm font-medium",
36442
+ children: _t({
36443
+ id: "+zy2Nq",
36444
+ message: "Type"
36445
+ })
36446
+ }),
36447
+ /* @__PURE__ */ jsx("th", {
36448
+ scope: "col",
36449
+ className: "px-4 py-3 text-start text-sm font-medium",
36450
+ children: _t({
36451
+ id: "O/SdND",
36452
+ message: "Translatable"
36453
+ })
36454
+ }),
36455
+ /* @__PURE__ */ jsx("th", {
36456
+ scope: "col",
36457
+ className: "px-4 py-3 text-start text-sm font-medium",
36458
+ children: _t({
36459
+ id: "TMLAx2",
36460
+ message: "Required"
36461
+ })
36462
+ }),
36463
+ /* @__PURE__ */ jsx("th", {
36464
+ scope: "col",
36465
+ className: "px-4 py-3 text-end text-sm font-medium",
36466
+ children: _t({
36467
+ id: "7L01XJ",
36468
+ message: "Actions"
36469
+ })
36470
+ })
36471
+ ]
36472
+ }) }), /* @__PURE__ */ jsx("tbody", {
36473
+ className: "divide-y divide-kumo-line",
36474
+ children: fieldsQuery.isLoading ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
36475
+ colSpan: 6,
36476
+ className: "px-4 py-8 text-center text-kumo-subtle",
36477
+ children: _t({
36478
+ id: "IMm99K",
36479
+ message: "Loading byline fields…"
36480
+ })
36481
+ }) }) : fieldsQuery.isError ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
36482
+ colSpan: 6,
36483
+ className: "px-4 py-8 text-center",
36484
+ children: [
36485
+ /* @__PURE__ */ jsx("p", {
36486
+ className: "text-kumo-danger font-medium",
36487
+ children: _t({
36488
+ id: "ORkiLh",
36489
+ message: "Couldn't load byline fields."
36490
+ })
36491
+ }),
36492
+ /* @__PURE__ */ jsx("p", {
36493
+ className: "text-sm text-kumo-subtle mt-1",
36494
+ children: fieldsQuery.error instanceof Error ? fieldsQuery.error.message : _t({
36495
+ id: "W5A0Ly",
36496
+ message: "An unexpected error occurred."
36497
+ })
36498
+ }),
36499
+ /* @__PURE__ */ jsx(Button, {
36500
+ variant: "outline",
36501
+ size: "sm",
36502
+ className: "mt-3",
36503
+ onClick: () => void fieldsQuery.refetch(),
36504
+ children: _t({
36505
+ id: "6gRgw8",
36506
+ message: "Retry"
36507
+ })
36508
+ })
36509
+ ]
36510
+ }) }) : fields.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
36511
+ colSpan: 6,
36512
+ className: "px-4 py-12 text-center text-kumo-subtle",
36513
+ children: [
36514
+ /* @__PURE__ */ jsx(IdentificationCard, {
36515
+ className: "mx-auto h-8 w-8 mb-3 text-kumo-subtle",
36516
+ "aria-hidden": "true"
36517
+ }),
36518
+ /* @__PURE__ */ jsx("p", { children: _t({
36519
+ id: "7XUltI",
36520
+ message: "No byline fields yet."
36521
+ }) }),
36522
+ /* @__PURE__ */ jsx("p", {
36523
+ className: "text-xs mt-1",
36524
+ children: _t({
36525
+ id: "USA0K0",
36526
+ message: "Add fields like \"Job title\" or \"Pronouns\" to enrich every byline."
36527
+ })
36528
+ })
36529
+ ]
36530
+ }) }) : fields.map((field, index) => /* @__PURE__ */ jsx(FieldRow, {
36531
+ field,
36532
+ isFirst: index === 0,
36533
+ isLast: index === fields.length - 1,
36534
+ onMoveUp: () => handleMove(index, -1),
36535
+ onMoveDown: () => handleMove(index, 1),
36536
+ onEdit: () => setEditingField(field),
36537
+ onDelete: () => setDeletingField(field),
36538
+ reorderPending: reorderMutation.isPending
36539
+ }, field.id))
36540
+ })]
36541
+ })
36542
+ }),
36543
+ /* @__PURE__ */ jsx(BylineFieldEditor, {
36544
+ open: creating,
36545
+ onOpenChange: (open) => {
36546
+ if (!open) {
36547
+ setCreating(false);
36548
+ createMutation.reset();
36549
+ }
36550
+ },
36551
+ onSave: handleCreate,
36552
+ isSaving: createMutation.isPending,
36553
+ error: createMutation.error
36554
+ }),
36555
+ editingField && /* @__PURE__ */ jsx(BylineFieldEditor, {
36556
+ open: !!editingField,
36557
+ onOpenChange: (open) => {
36558
+ if (!open) {
36559
+ setEditingField(null);
36560
+ updateMutation.reset();
36561
+ }
36562
+ },
36563
+ field: editingField,
36564
+ usageTotal: usageQuery.data?.totalAffectedRows ?? 0,
36565
+ onSave: handleEdit,
36566
+ isSaving: updateMutation.isPending,
36567
+ error: updateMutation.error
36568
+ }, editingField.id),
36569
+ /* @__PURE__ */ jsx(ConfirmDialog, {
36570
+ open: !!deletingField,
36571
+ onClose: () => {
36572
+ setDeletingField(null);
36573
+ deleteMutation.reset();
36574
+ },
36575
+ title: _t({
36576
+ id: "S353b7",
36577
+ message: "Delete byline field?"
36578
+ }),
36579
+ description: deletingField ? deleteUsageQuery.isLoading ? _t({
36580
+ id: "WeKBmZ",
36581
+ message: "Checking how many stored values reference \"{0}\"…",
36582
+ values: { 0: deletingField.label }
36583
+ }) : deleteUsageQuery.isError ? _t({
36584
+ id: "AsyqqW",
36585
+ message: "Couldn't verify how many values reference \"{0}\". Deleting will still remove every stored value for this field — but the count above could not be checked.",
36586
+ values: { 0: deletingField.label }
36587
+ }) : deleteUsageQuery.data && deleteUsageQuery.data.totalAffectedRows > 0 ? _t({
36588
+ id: "YjS/z6",
36589
+ message: "Deleting \"{0}\" will also remove {1, plural, one {# stored value} other {# stored values}} across all bylines. This cannot be undone.",
36590
+ values: {
36591
+ 0: deletingField.label,
36592
+ 1: deleteUsageQuery.data.totalAffectedRows
36593
+ }
36594
+ }) : _t({
36595
+ id: "27Gnm/",
36596
+ message: "Are you sure you want to delete \"{0}\"? No stored values reference this field.",
36597
+ values: { 0: deletingField.label }
36598
+ }) : "",
36599
+ confirmLabel: _t({
36600
+ id: "cnGeoo",
36601
+ message: "Delete"
36602
+ }),
36603
+ pendingLabel: _t({
36604
+ id: "gXQX4x",
36605
+ message: "Deleting…"
36606
+ }),
36607
+ isPending: deleteMutation.isPending,
36608
+ error: deleteMutation.error,
36609
+ onConfirm: () => {
36610
+ if (deletingField) deleteMutation.mutate(deletingField.slug);
36611
+ }
36612
+ })
36613
+ ]
36614
+ });
36615
+ }
36616
+ function FieldRow({ field, isFirst, isLast, onMoveUp, onMoveDown, onEdit, onDelete, reorderPending }) {
36617
+ const { _: _t2 } = useLingui();
36618
+ return /* @__PURE__ */ jsxs("tr", {
36619
+ className: "hover:bg-kumo-tint/25",
36620
+ children: [
36621
+ /* @__PURE__ */ jsx("td", {
36622
+ className: "px-4 py-3 font-medium",
36623
+ children: field.label
36624
+ }),
36625
+ /* @__PURE__ */ jsx("td", {
36626
+ className: "px-4 py-3",
36627
+ children: /* @__PURE__ */ jsx("code", {
36628
+ className: "text-sm bg-kumo-tint px-1.5 py-0.5 rounded",
36629
+ children: field.slug
36630
+ })
36631
+ }),
36632
+ /* @__PURE__ */ jsx("td", {
36633
+ className: "px-4 py-3 text-sm",
36634
+ children: field.type
36635
+ }),
36636
+ /* @__PURE__ */ jsx("td", {
36637
+ className: "px-4 py-3 text-sm",
36638
+ children: field.translatable ? _t2({
36639
+ id: "l75CjT",
36640
+ message: "Yes"
36641
+ }) : _t2({
36642
+ id: "Vqmggp",
36643
+ message: "No (shared across translations)"
36644
+ })
36645
+ }),
36646
+ /* @__PURE__ */ jsx("td", {
36647
+ className: "px-4 py-3 text-sm",
36648
+ children: field.required ? _t2({
36649
+ id: "l75CjT",
36650
+ message: "Yes"
36651
+ }) : _t2({
36652
+ id: "1UzENP",
36653
+ message: "No"
36654
+ })
36655
+ }),
36656
+ /* @__PURE__ */ jsx("td", {
36657
+ className: "px-4 py-3 text-end",
36658
+ children: /* @__PURE__ */ jsxs("div", {
36659
+ className: "flex items-center justify-end gap-1",
36660
+ children: [
36661
+ /* @__PURE__ */ jsx(Button, {
36662
+ variant: "ghost",
36663
+ shape: "square",
36664
+ size: "sm",
36665
+ "aria-label": _t2({
36666
+ id: "ko+NNU",
36667
+ message: "Move \"{0}\" up",
36668
+ values: { 0: field.label }
36669
+ }),
36670
+ disabled: isFirst || reorderPending,
36671
+ onClick: onMoveUp,
36672
+ children: /* @__PURE__ */ jsx(ArrowUp, {
36673
+ className: "h-4 w-4",
36674
+ "aria-hidden": "true"
36675
+ })
36676
+ }),
36677
+ /* @__PURE__ */ jsx(Button, {
36678
+ variant: "ghost",
36679
+ shape: "square",
36680
+ size: "sm",
36681
+ "aria-label": _t2({
36682
+ id: "Q23Rqr",
36683
+ message: "Move \"{0}\" down",
36684
+ values: { 0: field.label }
36685
+ }),
36686
+ disabled: isLast || reorderPending,
36687
+ onClick: onMoveDown,
36688
+ children: /* @__PURE__ */ jsx(ArrowDown, {
36689
+ className: "h-4 w-4",
36690
+ "aria-hidden": "true"
36691
+ })
36692
+ }),
36693
+ /* @__PURE__ */ jsx(Button, {
36694
+ variant: "ghost",
36695
+ shape: "square",
36696
+ size: "sm",
36697
+ "aria-label": _t2({
36698
+ id: "N6j2JH",
36699
+ message: "Edit {0}",
36700
+ values: { 0: field.label }
36701
+ }),
36702
+ onClick: onEdit,
36703
+ children: /* @__PURE__ */ jsx(Pencil, {
36704
+ className: "h-4 w-4",
36705
+ "aria-hidden": "true"
36706
+ })
36707
+ }),
36708
+ /* @__PURE__ */ jsx(Button, {
36709
+ variant: "ghost",
36710
+ shape: "square",
36711
+ size: "sm",
36712
+ "aria-label": _t2({
36713
+ id: "Y2tU6I",
36714
+ message: "Delete {0}",
36715
+ values: { 0: field.label }
36716
+ }),
36717
+ onClick: onDelete,
36718
+ children: /* @__PURE__ */ jsx(Trash, {
36719
+ className: "h-4 w-4 text-kumo-danger",
36720
+ "aria-hidden": "true"
36721
+ })
36722
+ })
36723
+ ]
36724
+ })
36725
+ })
36726
+ ]
36727
+ });
36728
+ }
36729
+
35291
36730
  //#endregion
35292
36731
  //#region src/routes/bylines.tsx
35293
36732
  /**
@@ -35304,7 +36743,8 @@ function toFormState(byline) {
35304
36743
  bio: "",
35305
36744
  websiteUrl: "",
35306
36745
  userId: null,
35307
- isGuest: false
36746
+ isGuest: false,
36747
+ customFields: {}
35308
36748
  };
35309
36749
  return {
35310
36750
  slug: byline.slug,
@@ -35312,7 +36752,8 @@ function toFormState(byline) {
35312
36752
  bio: byline.bio ?? "",
35313
36753
  websiteUrl: byline.websiteUrl ?? "",
35314
36754
  userId: byline.userId,
35315
- isGuest: byline.isGuest
36755
+ isGuest: byline.isGuest,
36756
+ customFields: byline.customFields ?? {}
35316
36757
  };
35317
36758
  }
35318
36759
  function getUserLabel(user) {
@@ -35325,6 +36766,7 @@ function BylinesPage() {
35325
36766
  const navigate = useNavigate$1();
35326
36767
  const { locale: routeLocale } = useSearch({ from: "/_admin/bylines" });
35327
36768
  const [search, setSearch] = React$1.useState("");
36769
+ const debouncedSearch = useDebouncedValue$1(search, 300);
35328
36770
  const [guestFilter, setGuestFilter] = React$1.useState("all");
35329
36771
  const [selectedId, setSelectedId] = React$1.useState(null);
35330
36772
  const [showDeleteConfirm, setShowDeleteConfirm] = React$1.useState(false);
@@ -35336,6 +36778,8 @@ function BylinesPage() {
35336
36778
  });
35337
36779
  const i18n = useI18nConfig(manifest);
35338
36780
  const isMultiLocale = !!i18n && i18n.locales.length > 1;
36781
+ const { data: currentUser } = useCurrentUser();
36782
+ const canManageBylineSchema = (currentUser?.role ?? 0) >= BYLINE_SCHEMA_NAV_ITEM.minRole;
35339
36783
  const activeLocale = routeLocale ?? i18n?.defaultLocale ?? void 0;
35340
36784
  const handleLocaleChange = (locale) => {
35341
36785
  navigate({
@@ -35347,12 +36791,12 @@ function BylinesPage() {
35347
36791
  const { data, isLoading, error } = useQuery({
35348
36792
  queryKey: [
35349
36793
  "bylines",
35350
- search,
36794
+ debouncedSearch,
35351
36795
  guestFilter,
35352
36796
  activeLocale ?? null
35353
36797
  ],
35354
36798
  queryFn: () => fetchBylines({
35355
- search: search || void 0,
36799
+ search: debouncedSearch || void 0,
35356
36800
  isGuest: guestFilter === "all" ? void 0 : guestFilter === "guest",
35357
36801
  locale: activeLocale,
35358
36802
  limit: 50
@@ -35369,6 +36813,12 @@ function BylinesPage() {
35369
36813
  queryFn: () => fetchUsers({ limit: 100 })
35370
36814
  });
35371
36815
  const users = usersData?.items ?? [];
36816
+ const { data: customFieldsList, error: customFieldsError } = useQuery({
36817
+ queryKey: ["byline-fields"],
36818
+ queryFn: listBylineFields,
36819
+ staleTime: 60 * 1e3
36820
+ });
36821
+ const customFieldDefs = customFieldsList?.items ?? [];
35372
36822
  const loadMoreMutation = useMutation({
35373
36823
  mutationFn: async (snapshot) => {
35374
36824
  return {
@@ -35384,7 +36834,7 @@ function BylinesPage() {
35384
36834
  },
35385
36835
  onSuccess: ({ result, snapshot }) => {
35386
36836
  if (!loadMoreSnapshotMatches(snapshot, {
35387
- search,
36837
+ search: debouncedSearch,
35388
36838
  guestFilter,
35389
36839
  locale: activeLocale
35390
36840
  })) return;
@@ -35409,31 +36859,38 @@ function BylinesPage() {
35409
36859
  enabled: !!selectedId && isMultiLocale
35410
36860
  });
35411
36861
  const createMutation = useMutation({
35412
- mutationFn: () => createByline({
35413
- slug: form.slug,
35414
- displayName: form.displayName,
35415
- bio: form.bio || null,
35416
- websiteUrl: form.websiteUrl || null,
35417
- userId: form.userId,
35418
- isGuest: form.isGuest,
35419
- locale: activeLocale
35420
- }),
36862
+ mutationFn: () => {
36863
+ const body = {
36864
+ slug: form.slug,
36865
+ displayName: form.displayName,
36866
+ bio: form.bio || null,
36867
+ websiteUrl: form.websiteUrl || null,
36868
+ userId: form.userId,
36869
+ isGuest: form.isGuest,
36870
+ locale: activeLocale
36871
+ };
36872
+ if (!customFieldsError && Object.keys(form.customFields).length > 0) body.customFields = form.customFields;
36873
+ return createByline(body);
36874
+ },
35421
36875
  onSuccess: (created) => {
35422
36876
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
36877
+ queryClient.invalidateQueries({ queryKey: ["byline", created.id] });
35423
36878
  setSelectedId(created.id);
35424
36879
  }
35425
36880
  });
35426
36881
  const updateMutation = useMutation({
35427
36882
  mutationFn: () => {
35428
36883
  if (!selectedId) throw new Error("No byline selected");
35429
- return updateByline(selectedId, {
36884
+ const body = {
35430
36885
  slug: form.slug,
35431
36886
  displayName: form.displayName,
35432
36887
  bio: form.bio || null,
35433
36888
  websiteUrl: form.websiteUrl || null,
35434
36889
  userId: form.userId,
35435
36890
  isGuest: form.isGuest
35436
- });
36891
+ };
36892
+ if (!customFieldsError) body.customFields = form.customFields;
36893
+ return updateByline(selectedId, body);
35437
36894
  },
35438
36895
  onSuccess: () => {
35439
36896
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
@@ -35471,7 +36928,7 @@ function BylinesPage() {
35471
36928
  setSelectedId(created.id);
35472
36929
  }
35473
36930
  });
35474
- if (isLoading) return /* @__PURE__ */ jsx("div", {
36931
+ if (isLoading && !data) return /* @__PURE__ */ jsx("div", {
35475
36932
  className: "flex items-center justify-center min-h-[30vh]",
35476
36933
  children: /* @__PURE__ */ jsx(Loader, {})
35477
36934
  });
@@ -35488,21 +36945,32 @@ function BylinesPage() {
35488
36945
  return /* @__PURE__ */ jsxs("div", {
35489
36946
  className: "space-y-4",
35490
36947
  children: [
35491
- isMultiLocale && i18n && activeLocale ? /* @__PURE__ */ jsxs("div", {
35492
- className: "flex items-center justify-between",
35493
- children: [/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("h1", {
36948
+ /* @__PURE__ */ jsxs("div", {
36949
+ className: "flex items-center justify-between gap-3",
36950
+ children: [/* @__PURE__ */ jsx("h1", {
35494
36951
  className: "text-2xl font-semibold",
35495
36952
  children: _t({
35496
36953
  id: "xLGa7J",
35497
36954
  message: "Bylines"
35498
36955
  })
35499
- }) }), /* @__PURE__ */ jsx(LocaleSwitcher, {
35500
- locales: i18n.locales,
35501
- defaultLocale: i18n.defaultLocale,
35502
- value: activeLocale,
35503
- onChange: handleLocaleChange
36956
+ }), /* @__PURE__ */ jsxs("div", {
36957
+ className: "flex items-center gap-2",
36958
+ children: [canManageBylineSchema && /* @__PURE__ */ jsx(RouterLinkButton, {
36959
+ to: BYLINE_SCHEMA_NAV_ITEM.to,
36960
+ variant: "secondary",
36961
+ icon: /* @__PURE__ */ jsx(IdentificationCard, {}),
36962
+ children: _t({
36963
+ id: "5YHQju",
36964
+ message: "Byline schema"
36965
+ })
36966
+ }), isMultiLocale && i18n && activeLocale && /* @__PURE__ */ jsx(LocaleSwitcher, {
36967
+ locales: i18n.locales,
36968
+ defaultLocale: i18n.defaultLocale,
36969
+ value: activeLocale,
36970
+ onChange: handleLocaleChange
36971
+ })]
35504
36972
  })]
35505
- }) : null,
36973
+ }),
35506
36974
  /* @__PURE__ */ jsxs("div", {
35507
36975
  className: "grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]",
35508
36976
  children: [/* @__PURE__ */ jsxs("div", {
@@ -35589,7 +37057,7 @@ function BylinesPage() {
35589
37057
  variant: "secondary",
35590
37058
  className: "w-full mt-2",
35591
37059
  onClick: () => loadMoreMutation.mutate({
35592
- search,
37060
+ search: debouncedSearch,
35593
37061
  guestFilter,
35594
37062
  locale: activeLocale,
35595
37063
  cursor: nextCursor
@@ -35690,6 +37158,33 @@ function BylinesPage() {
35690
37158
  },
35691
37159
  className: "w-full"
35692
37160
  }),
37161
+ customFieldDefs.length > 0 && customFieldDefs.map((field) => /* @__PURE__ */ jsx(CustomFieldInput, {
37162
+ field,
37163
+ value: form.customFields[field.slug],
37164
+ onChange: (next) => setForm((prev) => ({
37165
+ ...prev,
37166
+ customFields: {
37167
+ ...prev.customFields,
37168
+ [field.slug]: next
37169
+ }
37170
+ }))
37171
+ }, field.id)),
37172
+ customFieldsError && /* @__PURE__ */ jsxs("div", {
37173
+ className: "rounded-md border border-kumo-danger/40 bg-kumo-danger/5 p-3 text-sm",
37174
+ children: [/* @__PURE__ */ jsx("p", {
37175
+ className: "font-medium text-kumo-danger",
37176
+ children: _t({
37177
+ id: "Jci99j",
37178
+ message: "Couldn't load custom fields."
37179
+ })
37180
+ }), /* @__PURE__ */ jsx("p", {
37181
+ className: "text-xs text-kumo-subtle mt-1",
37182
+ children: _t({
37183
+ id: "X+FaLl",
37184
+ message: "You can still edit the fixed fields above. Saving will not touch any stored custom-field values."
37185
+ })
37186
+ })]
37187
+ }),
35693
37188
  /* @__PURE__ */ jsx(Switch, {
35694
37189
  label: _t({
35695
37190
  id: "6CRaxp",
@@ -35783,6 +37278,66 @@ function BylinesPage() {
35783
37278
  ]
35784
37279
  });
35785
37280
  }
37281
+ /**
37282
+ * Renders a single registered byline custom field as the appropriate
37283
+ * Kumo input for its type (Phase 6 of #1174).
37284
+ *
37285
+ * Five v1 type cases mirror `BylineFieldType` and the inputs that
37286
+ * `BylineFieldEditor` allows admins to register. Empty string inputs
37287
+ * coerce to `null` on save so the repo's "null clears the row"
37288
+ * storage semantic engages — server-side `BylineRepository.update`
37289
+ * deletes the value row rather than storing an empty-string JSON.
37290
+ *
37291
+ * `field.required` adds a `*` after the label as a visual hint; the
37292
+ * server is authoritative on validation (Phase 6 ACs don't include a
37293
+ * client-side required check — the registry's `required` flag is
37294
+ * descriptive rather than enforced in the write path today).
37295
+ */
37296
+ function CustomFieldInput({ field, value, onChange }) {
37297
+ const { _: _t2 } = useLingui();
37298
+ const label = field.required ? `${field.label} *` : field.label;
37299
+ const stringValue = typeof value === "string" ? value : "";
37300
+ switch (field.type) {
37301
+ case "string": return /* @__PURE__ */ jsx(Input, {
37302
+ label,
37303
+ value: stringValue,
37304
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
37305
+ });
37306
+ case "text": return /* @__PURE__ */ jsx(InputArea, {
37307
+ label,
37308
+ value: stringValue,
37309
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value),
37310
+ rows: 3
37311
+ });
37312
+ case "url": return /* @__PURE__ */ jsx(Input, {
37313
+ type: "url",
37314
+ label,
37315
+ value: stringValue,
37316
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
37317
+ });
37318
+ case "boolean": return /* @__PURE__ */ jsx(Switch, {
37319
+ label,
37320
+ checked: value === true,
37321
+ onCheckedChange: (checked) => onChange(checked)
37322
+ });
37323
+ case "select": {
37324
+ const options = field.validation?.options ?? [];
37325
+ const items = Object.create(null);
37326
+ items[""] = _t2({
37327
+ id: "9npDF6",
37328
+ message: "-- Select --"
37329
+ });
37330
+ for (const opt of options) items[opt] = opt;
37331
+ return /* @__PURE__ */ jsx(Select, {
37332
+ label,
37333
+ value: stringValue,
37334
+ onValueChange: (v) => onChange(!v ? null : v),
37335
+ items,
37336
+ className: "w-full"
37337
+ });
37338
+ }
37339
+ }
37340
+ }
35786
37341
 
35787
37342
  //#endregion
35788
37343
  //#region src/components/users/RoleBadge.tsx
@@ -37146,13 +38701,15 @@ function ContentListPage() {
37146
38701
  field: "updatedAt",
37147
38702
  direction: "desc"
37148
38703
  });
38704
+ const [searchTerm, setSearchTerm] = React$1.useState("");
37149
38705
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error } = useInfiniteQuery({
37150
38706
  queryKey: [
37151
38707
  "content",
37152
38708
  collection,
37153
38709
  {
37154
38710
  locale: activeLocale,
37155
- sort
38711
+ sort,
38712
+ search: searchTerm
37156
38713
  }
37157
38714
  ],
37158
38715
  queryFn: ({ pageParam }) => fetchContentList(collection, {
@@ -37160,7 +38717,8 @@ function ContentListPage() {
37160
38717
  cursor: pageParam,
37161
38718
  limit: 100,
37162
38719
  orderBy: sort.field,
37163
- order: sort.direction
38720
+ order: sort.direction,
38721
+ search: searchTerm || void 0
37164
38722
  }),
37165
38723
  initialPageParam: void 0,
37166
38724
  getNextPageParam: (lastPage) => lastPage.nextCursor,
@@ -37299,7 +38857,8 @@ function ContentListPage() {
37299
38857
  urlPattern: collectionConfig.urlPattern,
37300
38858
  sort,
37301
38859
  onSortChange: setSort,
37302
- total
38860
+ total,
38861
+ onSearchChange: setSearchTerm
37303
38862
  });
37304
38863
  }
37305
38864
  const contentNewRoute = createRoute({
@@ -37914,11 +39473,18 @@ const mediaRoute = createRoute({
37914
39473
  });
37915
39474
  function MediaPage() {
37916
39475
  const queryClient = useQueryClient();
39476
+ const [search, setSearch] = React$1.useState("");
39477
+ const [mimeFilter, setMimeFilter] = React$1.useState(void 0);
37917
39478
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error } = useInfiniteQuery({
37918
- queryKey: ["media"],
39479
+ queryKey: ["media", {
39480
+ search,
39481
+ mime: Array.isArray(mimeFilter) ? mimeFilter.join(",") : mimeFilter ?? ""
39482
+ }],
37919
39483
  queryFn: ({ pageParam }) => fetchMediaList({
37920
39484
  cursor: pageParam,
37921
- limit: 100
39485
+ limit: 100,
39486
+ search: search || void 0,
39487
+ mimeType: mimeFilter
37922
39488
  }),
37923
39489
  initialPageParam: void 0,
37924
39490
  getNextPageParam: (lastPage) => lastPage.nextCursor
@@ -37945,7 +39511,9 @@ function MediaPage() {
37945
39511
  hasMore: !!hasNextPage,
37946
39512
  onLoadMore: () => void fetchNextPage(),
37947
39513
  onUpload: (file) => uploadMutation.mutate(file),
37948
- onDelete: (id) => deleteMutation.mutate(id)
39514
+ onDelete: (id) => deleteMutation.mutate(id),
39515
+ onLocalSearchChange: setSearch,
39516
+ onLocalMimeFilterChange: setMimeFilter
37949
39517
  });
37950
39518
  }
37951
39519
  const commentsRoute = createRoute({
@@ -38312,6 +39880,11 @@ const bylinesRoute = createRoute({
38312
39880
  component: BylinesPage,
38313
39881
  validateSearch: parseBylinesLocaleSearch
38314
39882
  });
39883
+ const bylineSchemaRoute = createRoute({
39884
+ getParentRoute: () => adminLayoutRoute,
39885
+ path: "/byline-schema",
39886
+ component: BylineSchemaPage
39887
+ });
38315
39888
  const contentTypesListRoute = createRoute({
38316
39889
  getParentRoute: () => adminLayoutRoute,
38317
39890
  path: "/content-types",
@@ -38538,6 +40111,7 @@ const adminRoutes = adminLayoutRoute.addChildren([
38538
40111
  taxonomyRoute,
38539
40112
  usersRoute,
38540
40113
  bylinesRoute,
40114
+ bylineSchemaRoute,
38541
40115
  widgetsRoute,
38542
40116
  settingsRoute,
38543
40117
  generalSettingsRoute,
@@ -38695,5 +40269,5 @@ function AdminApp({ pluginAdmins = EMPTY_PLUGINS, authProviders = EMPTY_AUTH_PRO
38695
40269
  }
38696
40270
 
38697
40271
  //#endregion
38698
- export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, AuthProviderProvider, CAPABILITY_LABELS, ContentEditor, ContentList, DEFAULT_LOCALE, Dashboard, EditorHeader, Header, Link, LoginPage, MediaLibrary, MediaPickerModal, PasskeyLogin, PasskeyRegistration, PluginAdminProvider, PortableTextEditor, SUPPORTED_LOCALES, SUPPORTED_LOCALE_CODES, SaveButton, Settings, SetupWizard, Shell, KumoSidebar as Sidebar, SidebarNav, analyzeWpPluginSite, analyzeWxr, apiFetch, bulkCommentAction, checkPluginUpdates, cn, compareRevisions, completeSignup, createAdminRouter, createAllowedDomain, createApiToken, createByline, createBylineTranslation, createCollection, createContent, createField, createMenu, createMenuItem, createMenuTranslation, createRedirect, createSection, createTaxonomy, createTerm, createTermTranslation, createWidget, createWidgetArea, deleteAllowedDomain, deleteByline, deleteCollection, deleteComment, deleteContent, deleteField, deleteFromProvider, deleteMedia, deleteMenu, deleteMenuItem, deletePasskey, deleteRedirect, deleteSection, deleteTerm, deleteWidget, deleteWidgetArea, describeCapability, disablePlugin, disableUser, discardDraft, duplicateContent, enablePlugin, enableUser, executeWpPluginImport, executeWxrImport, fetch404Summary, fetchAllowedDomains, fetchApiTokens, fetchAuthMode, fetchByline, fetchBylineTranslations, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenuTranslations, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, fetchTermTranslations, fetchTerms, fetchTheme, fetchTranslations, fetchTrashedContent, fetchUser, fetchUsers, fetchWidgetArea, fetchWidgetAreas, fetchWidgetComponents, generatePreviewUrl, getDraftStatus, getPreviewUrl, hasAllowedDomains, importWxrMedia, installMarketplacePlugin, inviteUser, parseApiResponse, permanentDeleteContent, prepareWxrImport, probeImportUrl, publishContent, registerOrphanedTable, renamePasskey, reorderFields, reorderMenuItems, reorderWidgets, requestSignup, resolveLocale, restoreContent, restoreRevision, revokeApiToken, rewriteContentUrls, scheduleContent, searchMarketplace, searchThemes, sendRecoveryLink, sendTestEmail, setSearchEnabled, throwResponseError, uninstallMarketplacePlugin, unpublishContent, unscheduleContent, updateAllowedDomain, updateByline, updateCollection, updateCommentStatus, updateContent, updateField, updateMarketplacePlugin, updateMedia, updateMenu, updateMenuItem, updateRedirect, updateSection, updateSettings, updateTerm, updateUser, updateWidget, uploadMedia, uploadToProvider, useAuthProviderList, useAuthProviders, useCurrentUser, useLocale, useNavigate, useParams, usePluginAdmins, usePluginField, usePluginHasPages, usePluginHasWidgets, usePluginPage, usePluginWidget, validateInviteToken, verifySignupToken };
40272
+ export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, AuthProviderProvider, CAPABILITY_LABELS, ContentEditor, ContentList, DEFAULT_LOCALE, Dashboard, EditorHeader, Header, Link, LoginPage, MEDIA_SEARCH_MAX_LENGTH, MediaLibrary, MediaPickerModal, PasskeyLogin, PasskeyRegistration, PluginAdminProvider, PortableTextEditor, SUPPORTED_LOCALES, SUPPORTED_LOCALE_CODES, SaveButton, Settings, SetupWizard, Shell, KumoSidebar as Sidebar, SidebarNav, analyzeWpPluginSite, analyzeWxr, apiFetch, bulkCommentAction, checkPluginUpdates, cn, compareRevisions, completeSignup, createAdminRouter, createAllowedDomain, createApiToken, createByline, createBylineField, createBylineTranslation, createCollection, createContent, createField, createMenu, createMenuItem, createMenuTranslation, createRedirect, createSection, createTaxonomy, createTerm, createTermTranslation, createWidget, createWidgetArea, deleteAllowedDomain, deleteByline, deleteBylineField, deleteCollection, deleteComment, deleteContent, deleteField, deleteFromProvider, deleteMedia, deleteMenu, deleteMenuItem, deletePasskey, deleteRedirect, deleteSection, deleteTerm, deleteWidget, deleteWidgetArea, describeCapability, disablePlugin, disableUser, discardDraft, duplicateContent, enablePlugin, enableUser, executeWpPluginImport, executeWxrImport, fetch404Summary, fetchAllowedDomains, fetchApiTokens, fetchAuthMode, fetchByline, fetchBylineTranslations, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenuTranslations, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, fetchTermTranslations, fetchTerms, fetchTheme, fetchTranslations, fetchTrashedContent, fetchUser, fetchUsers, fetchWidgetArea, fetchWidgetAreas, fetchWidgetComponents, generatePreviewUrl, getBylineFieldUsage, getDraftStatus, getPreviewUrl, hasAllowedDomains, importWxrMedia, installMarketplacePlugin, inviteUser, listBylineFields, parseApiResponse, permanentDeleteContent, prepareWxrImport, probeImportUrl, publishContent, registerOrphanedTable, renamePasskey, reorderBylineFields, reorderFields, reorderMenuItems, reorderWidgets, requestSignup, resolveLocale, restoreContent, restoreRevision, revokeApiToken, rewriteContentUrls, scheduleContent, searchMarketplace, searchThemes, sendRecoveryLink, sendTestEmail, setSearchEnabled, throwResponseError, uninstallMarketplacePlugin, unpublishContent, unscheduleContent, updateAllowedDomain, updateByline, updateBylineField, updateCollection, updateCommentStatus, updateContent, updateField, updateMarketplacePlugin, updateMedia, updateMenu, updateMenuItem, updateRedirect, updateSection, updateSettings, updateTerm, updateUser, updateWidget, uploadMedia, uploadToProvider, useAuthProviderList, useAuthProviders, useCurrentUser, useLocale, useNavigate, useParams, usePluginAdmins, usePluginField, usePluginHasPages, usePluginHasWidgets, usePluginPage, usePluginWidget, validateInviteToken, verifySignupToken };
38699
40273
  //# sourceMappingURL=index.js.map