@emdash-cms/admin 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/{LocaleDirectionProvider-CI7U6BBr.js → LocaleDirectionProvider-BGcmv_N7.js} +26 -14
  2. package/dist/LocaleDirectionProvider-BGcmv_N7.js.map +1 -0
  3. package/dist/{config-D8i8U4bV.d.ts → config-YdN-uv6S.d.ts} +1 -1
  4. package/dist/{config-D8i8U4bV.d.ts.map → config-YdN-uv6S.d.ts.map} +1 -1
  5. package/dist/index.d.ts +148 -15
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2080 -1257
  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/eu/messages.mjs +1 -1
  14. package/dist/locales/fa/messages.mjs +1 -1
  15. package/dist/locales/fr/messages.mjs +1 -1
  16. package/dist/locales/id/messages.mjs +1 -0
  17. package/dist/locales/index.d.ts +1 -1
  18. package/dist/locales/index.js +1 -1
  19. package/dist/locales/ja/messages.mjs +1 -1
  20. package/dist/locales/ko/messages.mjs +1 -1
  21. package/dist/locales/pl/messages.mjs +1 -0
  22. package/dist/locales/pseudo/messages.mjs +1 -1
  23. package/dist/locales/pt-BR/messages.mjs +1 -1
  24. package/dist/locales/zh-CN/messages.mjs +1 -1
  25. package/dist/locales/zh-TW/messages.mjs +1 -1
  26. package/dist/messages-B2njiT3N.js +6 -0
  27. package/dist/messages-B2njiT3N.js.map +1 -0
  28. package/dist/messages-BH4d_eo3.js +6 -0
  29. package/dist/messages-BH4d_eo3.js.map +1 -0
  30. package/dist/messages-BI3LQfHj.js +6 -0
  31. package/dist/messages-BI3LQfHj.js.map +1 -0
  32. package/dist/messages-BLk7kRSt.js +6 -0
  33. package/dist/messages-BLk7kRSt.js.map +1 -0
  34. package/dist/messages-C0fFDufP.js +6 -0
  35. package/dist/messages-C0fFDufP.js.map +1 -0
  36. package/dist/messages-C2W1GOne.js +6 -0
  37. package/dist/messages-C2W1GOne.js.map +1 -0
  38. package/dist/messages-C7bn0sIO.js +6 -0
  39. package/dist/messages-C7bn0sIO.js.map +1 -0
  40. package/dist/messages-CKVC1F84.js +6 -0
  41. package/dist/messages-CKVC1F84.js.map +1 -0
  42. package/dist/messages-D6pXx3hr.js +6 -0
  43. package/dist/messages-D6pXx3hr.js.map +1 -0
  44. package/dist/messages-DQUgrzIz.js +6 -0
  45. package/dist/messages-DQUgrzIz.js.map +1 -0
  46. package/dist/messages-DfGxk-9q.js +6 -0
  47. package/dist/messages-DfGxk-9q.js.map +1 -0
  48. package/dist/messages-Dkb9TndV.js +6 -0
  49. package/dist/messages-Dkb9TndV.js.map +1 -0
  50. package/dist/messages-DzS9GmJY.js +6 -0
  51. package/dist/messages-DzS9GmJY.js.map +1 -0
  52. package/dist/messages-FaHuWwRM.js +6 -0
  53. package/dist/messages-FaHuWwRM.js.map +1 -0
  54. package/dist/messages-lrnW_v3N.js +6 -0
  55. package/dist/messages-lrnW_v3N.js.map +1 -0
  56. package/dist/{plugins-XhZqfegd.js → plugins-DUJY_8HN.js} +9 -2
  57. package/dist/plugins-DUJY_8HN.js.map +1 -0
  58. package/dist/styles.css +1 -1
  59. package/package.json +2 -4
  60. package/dist/LocaleDirectionProvider-CI7U6BBr.js.map +0 -1
  61. package/dist/messages-57qx1FXa.js +0 -6
  62. package/dist/messages-57qx1FXa.js.map +0 -1
  63. package/dist/messages-8uPA5VDB.js +0 -6
  64. package/dist/messages-8uPA5VDB.js.map +0 -1
  65. package/dist/messages-Bc3iE3Xx.js +0 -6
  66. package/dist/messages-Bc3iE3Xx.js.map +0 -1
  67. package/dist/messages-BudiTpRZ.js +0 -6
  68. package/dist/messages-BudiTpRZ.js.map +0 -1
  69. package/dist/messages-CLJeZfc0.js +0 -6
  70. package/dist/messages-CLJeZfc0.js.map +0 -1
  71. package/dist/messages-CXyBDum7.js +0 -6
  72. package/dist/messages-CXyBDum7.js.map +0 -1
  73. package/dist/messages-DFXk1gAB.js +0 -6
  74. package/dist/messages-DFXk1gAB.js.map +0 -1
  75. package/dist/messages-DHg9t0ML.js +0 -6
  76. package/dist/messages-DHg9t0ML.js.map +0 -1
  77. package/dist/messages-DT-_3Gqh.js +0 -6
  78. package/dist/messages-DT-_3Gqh.js.map +0 -1
  79. package/dist/messages-DYWnQ-qc.js +0 -6
  80. package/dist/messages-DYWnQ-qc.js.map +0 -1
  81. package/dist/messages-DrHBJ5Tm.js +0 -6
  82. package/dist/messages-DrHBJ5Tm.js.map +0 -1
  83. package/dist/messages-TukFV9EF.js +0 -6
  84. package/dist/messages-TukFV9EF.js.map +0 -1
  85. package/dist/messages-cadUq4Pc.js +0 -6
  86. package/dist/messages-cadUq4Pc.js.map +0 -1
  87. package/dist/plugins-XhZqfegd.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { c as fetchManifest, i as fetchPlugins, l as parseApiResponse, n as enablePlugin, o as API_BASE, r as fetchPlugin, s as apiFetch, t as disablePlugin, u as throwResponseError } from "./plugins-XhZqfegd.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-CI7U6BBr.js";
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-DUJY_8HN.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-BGcmv_N7.js";
3
3
  import "./locales/index.js";
4
4
  import { 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";
@@ -10,7 +10,7 @@ import * as React from "react";
10
10
  import { 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, ArrowLeft, ArrowRight, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, BracketsCurly, Calendar, CalendarBlank, CaretDown, CaretLeft, CaretRight, CaretUp, ChatCircle, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, Code, CodeBlock, 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, ImageSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, MagnifyingGlass, Minus, Monitor, Moon, Palette, PaperPlaneTilt, Paragraph, Pencil, PencilSimple, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Rows, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, SlidersHorizontal, Sparkle, SquaresFour, Stack, Storefront, Sun, 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, 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, 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, ImageSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, MagnifyingGlass, Minus, Monitor, Moon, Palette, PaperPlaneTilt, Paragraph, Pencil, PencilSimple, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Rows, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, SlidersHorizontal, Sparkle, SquaresFour, Stack, Storefront, Sun, 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";
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";
@@ -100,6 +100,34 @@ function useTheme() {
100
100
  return context;
101
101
  }
102
102
 
103
+ //#endregion
104
+ //#region src/lib/auth-provider-context.tsx
105
+ const AuthProviderContext = createContext({});
106
+ /**
107
+ * Provider that makes auth provider components available to all descendants
108
+ */
109
+ function AuthProviderProvider({ children, authProviders }) {
110
+ return /* @__PURE__ */ jsx(AuthProviderContext.Provider, {
111
+ value: authProviders,
112
+ children
113
+ });
114
+ }
115
+ /**
116
+ * Get all auth provider modules
117
+ */
118
+ function useAuthProviders() {
119
+ return useContext(AuthProviderContext);
120
+ }
121
+ /**
122
+ * Get auth providers as an ordered array (buttons first, then forms)
123
+ */
124
+ function useAuthProviderList() {
125
+ const providers = useContext(AuthProviderContext);
126
+ return Object.values(providers).toSorted((a, b) => {
127
+ return (a.LoginForm ? 1 : 0) - (b.LoginForm ? 1 : 0);
128
+ });
129
+ }
130
+
103
131
  //#endregion
104
132
  //#region src/lib/plugin-context.tsx
105
133
  const PluginAdminContext = createContext({});
@@ -191,6 +219,37 @@ function slugify(text) {
191
219
  return text.toLowerCase().normalize("NFD").replace(DIACRITICS_PATTERN, "").replace(WHITESPACE_UNDERSCORE_PATTERN, "-").replace(NON_ALPHANUMERIC_HYPHEN_PATTERN, "").replace(MULTIPLE_HYPHENS_PATTERN, "-").replace(LEADING_TRAILING_HYPHEN_PATTERN, "");
192
220
  }
193
221
 
222
+ //#endregion
223
+ //#region src/components/ArrowIcons.tsx
224
+ /** Caret pointing in the "forward" direction — right in LTR, left in RTL. */
225
+ function CaretNext({ className, ...props }) {
226
+ return /* @__PURE__ */ jsx(CaretRightIcon, {
227
+ className: cn("rtl:-scale-x-100", className),
228
+ ...props
229
+ });
230
+ }
231
+ /** Caret pointing in the "backward" direction — left in LTR, right in RTL. */
232
+ function CaretPrev({ className, ...props }) {
233
+ return /* @__PURE__ */ jsx(CaretRightIcon, {
234
+ className: cn("rotate-180 rtl:rotate-0", className),
235
+ ...props
236
+ });
237
+ }
238
+ /** Arrow pointing in the "forward" direction — right in LTR, left in RTL. */
239
+ function ArrowNext({ className, ...props }) {
240
+ return /* @__PURE__ */ jsx(ArrowRightIcon, {
241
+ className: cn("rtl:-scale-x-100", className),
242
+ ...props
243
+ });
244
+ }
245
+ /** Arrow pointing in the "backward" direction — left in LTR, right in RTL. */
246
+ function ArrowPrev({ className, ...props }) {
247
+ return /* @__PURE__ */ jsx(ArrowRightIcon, {
248
+ className: cn("rotate-180 rtl:rotate-0", className),
249
+ ...props
250
+ });
251
+ }
252
+
194
253
  //#endregion
195
254
  //#region src/components/DialogError.tsx
196
255
  /**
@@ -863,7 +922,7 @@ function CommentInbox({ comments, counts, isLoading, nextCursor, collections, ac
863
922
  id: "aHKcKc",
864
923
  message: "Previous page"
865
924
  }),
866
- children: /* @__PURE__ */ jsx(CaretLeft, { className: "h-4 w-4" })
925
+ children: /* @__PURE__ */ jsx(CaretPrev, { className: "h-4 w-4" })
867
926
  }),
868
927
  /* @__PURE__ */ jsxs("span", {
869
928
  className: "text-sm",
@@ -887,7 +946,7 @@ function CommentInbox({ comments, counts, isLoading, nextCursor, collections, ac
887
946
  id: "/dqduX",
888
947
  message: "Next page"
889
948
  }),
890
- children: /* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4" })
949
+ children: /* @__PURE__ */ jsx(CaretNext, { className: "h-4 w-4" })
891
950
  })
892
951
  ]
893
952
  })]
@@ -1105,6 +1164,8 @@ async function fetchContentList(collection, options) {
1105
1164
  if (options?.limit) params.set("limit", String(options.limit));
1106
1165
  if (options?.status) params.set("status", options.status);
1107
1166
  if (options?.locale) params.set("locale", options.locale);
1167
+ if (options?.orderBy) params.set("orderBy", options.orderBy);
1168
+ if (options?.order) params.set("order", options.order);
1108
1169
  return parseApiResponse(await apiFetch(`${API_BASE}/content/${collection}${params.toString() ? `?${params}` : ""}`), "Failed to fetch content");
1109
1170
  }
1110
1171
  /**
@@ -1196,7 +1257,9 @@ async function unscheduleContent(collection, id) {
1196
1257
  * Get a preview URL for content
1197
1258
  *
1198
1259
  * Returns a signed URL that allows viewing draft content.
1199
- * Returns null if preview is not configured (missing EMDASH_PREVIEW_SECRET).
1260
+ * Returns null if the EmDash runtime isn't initialized on the server
1261
+ * (responds with NOT_CONFIGURED). The preview secret itself no longer
1262
+ * needs to be set explicitly — it auto-generates on first use.
1200
1263
  */
1201
1264
  async function getPreviewUrl(collection, id, options) {
1202
1265
  try {
@@ -2261,6 +2324,10 @@ const API_TOKEN_SCOPES = {
2261
2324
  MediaWrite: "media:write",
2262
2325
  SchemaRead: "schema:read",
2263
2326
  SchemaWrite: "schema:write",
2327
+ TaxonomiesManage: "taxonomies:manage",
2328
+ MenusManage: "menus:manage",
2329
+ SettingsRead: "settings:read",
2330
+ SettingsManage: "settings:manage",
2264
2331
  Admin: "admin"
2265
2332
  };
2266
2333
  /**
@@ -2455,22 +2522,37 @@ async function uninstallMarketplacePlugin(id, opts = {}) {
2455
2522
  async function checkPluginUpdates() {
2456
2523
  return (await parseApiResponse(await apiFetch(`${API_BASE}/admin/plugins/updates`), "Failed to check for updates")).items;
2457
2524
  }
2458
- /** Human-readable labels for plugin capabilities */
2525
+ /**
2526
+ * Human-readable labels for plugin capabilities.
2527
+ *
2528
+ * Canonical names are the keys; legacy names alias to the same labels so
2529
+ * old manifests still render meaningful copy until they're republished.
2530
+ */
2459
2531
  const CAPABILITY_LABELS = {
2532
+ "content:read": "Read your content",
2533
+ "content:write": "Create, update, and delete content",
2534
+ "media:read": "Access your media library",
2535
+ "media:write": "Upload and manage media",
2536
+ "users:read": "Read user accounts",
2537
+ "network:request": "Make network requests",
2538
+ "network:request:unrestricted": "Make network requests to any host (unrestricted)",
2460
2539
  "read:content": "Read your content",
2461
2540
  "write:content": "Create, update, and delete content",
2462
2541
  "read:media": "Access your media library",
2463
2542
  "write:media": "Upload and manage media",
2543
+ "read:users": "Read user accounts",
2464
2544
  "network:fetch": "Make network requests",
2465
2545
  "network:fetch:any": "Make network requests to any host (unrestricted)"
2466
2546
  };
2547
+ /** Capability names that grant scoped network access (legacy + canonical). */
2548
+ const NETWORK_REQUEST_CAPABILITIES = new Set(["network:request", "network:fetch"]);
2467
2549
  /**
2468
2550
  * Get a human-readable description for a capability.
2469
- * For network:fetch, appends the allowed hosts if provided.
2551
+ * For scoped network capabilities, appends the allowed hosts if provided.
2470
2552
  */
2471
2553
  function describeCapability(capability, allowedHosts) {
2472
2554
  const base = CAPABILITY_LABELS[capability] ?? capability;
2473
- if (capability === "network:fetch" && allowedHosts && allowedHosts.length > 0) return `${base} to: ${allowedHosts.join(", ")}`;
2555
+ if (NETWORK_REQUEST_CAPABILITIES.has(capability) && allowedHosts && allowedHosts.length > 0) return `${base} to: ${allowedHosts.join(", ")}`;
2474
2556
  return base;
2475
2557
  }
2476
2558
 
@@ -2606,6 +2688,33 @@ function useCurrentUser() {
2606
2688
  });
2607
2689
  }
2608
2690
 
2691
+ //#endregion
2692
+ //#region src/lib/datetime-local.ts
2693
+ /**
2694
+ * Helpers for round-tripping `datetime` field values through
2695
+ * `<input type="datetime-local">`.
2696
+ *
2697
+ * Stored field values are full ISO 8601 (`YYYY-MM-DDTHH:mm:ss.sssZ`) or
2698
+ * date-only (`YYYY-MM-DD`) per the per-field zod schema in
2699
+ * `packages/core/src/schema/zod-generator.ts`. The browser input only
2700
+ * accepts `YYYY-MM-DDTHH:mm`, so widgets must convert in both directions.
2701
+ *
2702
+ * The widget treats the value as UTC for a stable round-trip. Using
2703
+ * `new Date(...).toISOString()` (the convention in the publish-schedule
2704
+ * UI in `ContentEditor.tsx`) would shift values by the local-UTC offset on
2705
+ * every save, mutating the persisted time on each edit cycle.
2706
+ */
2707
+ /** Format a stored datetime field value for the input. */
2708
+ function toDatetimeLocalInputValue(value) {
2709
+ if (typeof value !== "string" || value === "") return "";
2710
+ if (value.length === 10) return `${value}T00:00`;
2711
+ return value.slice(0, 16);
2712
+ }
2713
+ /** Convert an input value (`YYYY-MM-DDTHH:mm`) back to the stored ISO shape. */
2714
+ function fromDatetimeLocalInputValue(value) {
2715
+ return value === "" ? "" : `${value}:00.000Z`;
2716
+ }
2717
+
2609
2718
  //#endregion
2610
2719
  //#region src/lib/url.ts
2611
2720
  /**
@@ -2659,652 +2768,101 @@ function safeIconUrl(url, width) {
2659
2768
  }
2660
2769
 
2661
2770
  //#endregion
2662
- //#region src/components/BlockKitFieldWidget.tsx
2663
- /**
2664
- * Renders Block Kit elements as a field widget for sandboxed plugins.
2665
- * Decomposes a JSON value into per-element values keyed by action_id,
2666
- * and recomposes on change.
2667
- */
2668
- function BlockKitFieldWidget({ label, elements, value, onChange }) {
2669
- const obj = value && typeof value === "object" ? value : {};
2670
- const objRef = React.useRef(obj);
2671
- objRef.current = obj;
2672
- const handleElementChange = React.useCallback((actionId, elementValue) => {
2673
- onChange({
2674
- ...objRef.current,
2675
- [actionId]: elementValue
2676
- });
2677
- }, [onChange]);
2678
- const validElements = elements.filter((el) => el.action_id);
2679
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
2680
- className: "text-sm font-medium leading-none",
2681
- children: label
2682
- }), /* @__PURE__ */ jsx("div", {
2683
- className: "mt-2 space-y-3",
2684
- children: validElements.map((el) => /* @__PURE__ */ jsx(BlockKitFieldElement, {
2685
- element: el,
2686
- value: obj[el.action_id],
2687
- onChange: handleElementChange
2688
- }, el.action_id))
2689
- })] });
2771
+ //#region src/lib/media-utils.ts
2772
+ function providerItemToMediaItem(providerId, item) {
2773
+ return {
2774
+ id: item.id,
2775
+ filename: item.filename,
2776
+ mimeType: item.mimeType,
2777
+ url: item.previewUrl || "",
2778
+ size: item.size || 0,
2779
+ width: item.width,
2780
+ height: item.height,
2781
+ alt: item.alt,
2782
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2783
+ provider: providerId,
2784
+ meta: item.meta
2785
+ };
2690
2786
  }
2691
- function BlockKitFieldElement({ element, value, onChange }) {
2692
- switch (element.type) {
2693
- case "text_input": return /* @__PURE__ */ jsx(Input, {
2694
- label: element.label,
2695
- placeholder: element.placeholder,
2696
- value: typeof value === "string" ? value : "",
2697
- onChange: (e) => onChange(element.action_id, e.target.value)
2698
- });
2699
- case "number_input": return /* @__PURE__ */ jsx(Input, {
2700
- label: element.label,
2701
- type: "number",
2702
- value: typeof value === "number" ? String(value) : "",
2703
- onChange: (e) => {
2704
- const n = Number(e.target.value);
2705
- onChange(element.action_id, e.target.value && Number.isFinite(n) ? n : void 0);
2706
- }
2707
- });
2708
- case "toggle": return /* @__PURE__ */ jsx(Switch, {
2709
- label: element.label,
2710
- checked: !!value,
2711
- onCheckedChange: (checked) => onChange(element.action_id, checked)
2712
- });
2713
- case "select": {
2714
- const options = Array.isArray(element.options) ? element.options : [];
2715
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
2716
- className: "text-sm font-medium mb-1.5 block",
2717
- children: element.label
2718
- }), /* @__PURE__ */ jsxs("select", {
2719
- className: "flex w-full rounded-md border border-kumo-line bg-transparent px-3 py-2 text-sm",
2720
- value: typeof value === "string" ? value : "",
2721
- onChange: (e) => onChange(element.action_id, e.target.value),
2722
- children: [/* @__PURE__ */ jsx("option", {
2723
- value: "",
2724
- children: "Select..."
2725
- }), options.map((opt) => /* @__PURE__ */ jsx("option", {
2726
- value: opt.value,
2727
- children: opt.label
2728
- }, opt.value))]
2729
- })] });
2730
- }
2731
- default: return /* @__PURE__ */ jsxs("div", {
2732
- className: "text-sm text-kumo-subtle",
2733
- children: ["Unsupported widget element type: ", element.type]
2734
- });
2735
- }
2787
+ function getFileIcon(mimeType) {
2788
+ if (mimeType.startsWith("video/")) return "🎬";
2789
+ if (mimeType.startsWith("audio/")) return "🎵";
2790
+ if (mimeType.includes("pdf")) return "📄";
2791
+ if (mimeType.includes("document") || mimeType.includes("word")) return "📝";
2792
+ if (mimeType.includes("spreadsheet") || mimeType.includes("excel")) return "📊";
2793
+ return "📁";
2794
+ }
2795
+ function formatFileSize(bytes) {
2796
+ if (bytes === 0) return "0 B";
2797
+ const k = 1024;
2798
+ const sizes = [
2799
+ "B",
2800
+ "KB",
2801
+ "MB",
2802
+ "GB"
2803
+ ];
2804
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
2805
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
2736
2806
  }
2737
2807
 
2738
2808
  //#endregion
2739
- //#region src/components/editor/DocumentOutline.tsx
2809
+ //#region src/components/MediaPickerModal.tsx
2740
2810
  /**
2741
- * Document Outline
2811
+ * Media Picker Modal
2742
2812
  *
2743
- * Displays a tree structure of headings from the TipTap editor.
2744
- * - Shows H1 at root, H2 indented, H3 further indented
2745
- * - Click-to-navigate to heading position
2746
- * - Highlights the current section based on cursor position
2813
+ * A modal dialog for selecting media from the library or uploading new files.
2814
+ * Supports multiple media providers with tabbed navigation.
2815
+ * Used by the rich text editor and image field components.
2747
2816
  */
2748
- function getIndentClass(level) {
2749
- switch (level) {
2750
- case 1: return "ps-0";
2751
- case 2: return "ps-4";
2752
- case 3: return "ps-8";
2753
- default: return "ps-0";
2754
- }
2755
- }
2756
- function getTextClass(level) {
2757
- switch (level) {
2758
- case 1: return "font-medium";
2759
- case 2: return "font-normal";
2760
- case 3: return "font-normal text-kumo-subtle";
2761
- default: return "font-normal";
2762
- }
2763
- }
2764
2817
  /**
2765
- * Extract headings from the TipTap editor document
2818
+ * Probe image URL to get dimensions
2766
2819
  */
2767
- function extractHeadings(editor) {
2768
- if (!editor) return [];
2769
- const headings = [];
2770
- const doc = editor.state.doc;
2771
- let key = 0;
2772
- doc.descendants((node, pos) => {
2773
- if (node.type.name === "heading") {
2774
- const rawLevel = node.attrs.level;
2775
- const level = typeof rawLevel === "number" ? rawLevel : 1;
2776
- const text = node.textContent || "";
2777
- if (text.trim()) headings.push({
2778
- level,
2779
- text,
2780
- pos,
2781
- key: `heading-${key++}`
2820
+ function probeImageDimensions(url) {
2821
+ return new Promise((resolve, reject) => {
2822
+ const img = new window.Image();
2823
+ img.onload = () => {
2824
+ resolve({
2825
+ width: img.naturalWidth,
2826
+ height: img.naturalHeight
2782
2827
  });
2783
- }
2784
- });
2785
- return headings;
2786
- }
2787
- /**
2788
- * Find the current heading based on cursor position
2789
- */
2790
- function findCurrentHeading(headings, cursorPos) {
2791
- if (headings.length === 0) return null;
2792
- let current = null;
2793
- for (const heading of headings) if (heading.pos <= cursorPos) current = heading;
2794
- else break;
2795
- return current;
2796
- }
2797
- /**
2798
- * Document outline component showing heading tree structure
2799
- */
2800
- function DocumentOutline({ editor, className }) {
2801
- const { _: _t } = useLingui();
2802
- const [isExpanded, setIsExpanded] = React.useState(true);
2803
- const [headings, setHeadings] = React.useState([]);
2804
- const [currentPos, setCurrentPos] = React.useState(0);
2805
- React.useEffect(() => {
2806
- if (!editor) return;
2807
- const updateHeadings = () => {
2808
- setHeadings(extractHeadings(editor));
2809
- };
2810
- updateHeadings();
2811
- editor.on("update", updateHeadings);
2812
- return () => {
2813
- editor.off("update", updateHeadings);
2814
- };
2815
- }, [editor]);
2816
- React.useEffect(() => {
2817
- if (!editor) return;
2818
- const updatePosition = () => {
2819
- const { from } = editor.state.selection;
2820
- setCurrentPos(from);
2821
- };
2822
- updatePosition();
2823
- editor.on("selectionUpdate", updatePosition);
2824
- return () => {
2825
- editor.off("selectionUpdate", updatePosition);
2826
- };
2827
- }, [editor]);
2828
- const currentHeading = findCurrentHeading(headings, currentPos);
2829
- const handleHeadingClick = (heading) => {
2830
- if (!editor) return;
2831
- editor.chain().focus().setTextSelection(heading.pos).scrollIntoView().run();
2832
- };
2833
- return /* @__PURE__ */ jsxs("div", {
2834
- className: cn("space-y-2", className),
2835
- children: [/* @__PURE__ */ jsxs(Button, {
2836
- variant: "ghost",
2837
- size: "sm",
2838
- className: "w-full justify-between px-2 h-8",
2839
- onClick: () => setIsExpanded(!isExpanded),
2840
- children: [/* @__PURE__ */ jsxs("span", {
2841
- className: "flex items-center gap-2",
2842
- children: [/* @__PURE__ */ jsx(List, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", {
2843
- className: "font-semibold",
2844
- children: _t({
2845
- id: "dD7NPy",
2846
- message: "Outline"
2847
- })
2848
- })]
2849
- }), isExpanded ? /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4" })]
2850
- }), isExpanded && /* @__PURE__ */ jsx("div", {
2851
- className: "space-y-0.5",
2852
- children: headings.length === 0 ? /* @__PURE__ */ jsx("p", {
2853
- className: "text-sm text-kumo-subtle px-2 py-1",
2854
- children: _t({
2855
- id: "MMd//L",
2856
- message: "No headings in document"
2857
- })
2858
- }) : headings.map((heading) => {
2859
- const isCurrent = currentHeading?.key === heading.key;
2860
- return /* @__PURE__ */ jsx("button", {
2861
- type: "button",
2862
- onClick: () => handleHeadingClick(heading),
2863
- className: cn("w-full text-start px-2 py-1 text-sm rounded transition-colors", "hover:bg-kumo-tint/50 cursor-pointer", "truncate", getIndentClass(heading.level), getTextClass(heading.level), isCurrent && "bg-kumo-tint text-kumo-default"),
2864
- title: heading.text,
2865
- children: heading.text
2866
- }, heading.key);
2867
- })
2868
- })]
2869
- });
2870
- }
2871
-
2872
- //#endregion
2873
- //#region src/components/PluginFieldErrorBoundary.tsx
2874
- /**
2875
- * Error boundary that wraps trusted plugin field widgets.
2876
- * On render error, shows a warning with a retry button instead of crashing the editor.
2877
- */
2878
- var PluginFieldErrorBoundary = class extends React.Component {
2879
- constructor(props) {
2880
- super(props);
2881
- this.state = { hasError: false };
2882
- }
2883
- static getDerivedStateFromError(error) {
2884
- return {
2885
- hasError: true,
2886
- error
2887
2828
  };
2888
- }
2889
- render() {
2890
- if (this.state.hasError) return /* @__PURE__ */ jsxs("div", {
2891
- className: "rounded-md border border-kumo-danger/50 bg-kumo-danger/5 p-3",
2892
- children: [
2893
- /* @__PURE__ */ jsx("p", {
2894
- className: "text-sm font-medium text-kumo-danger",
2895
- children: "Plugin widget error"
2896
- }),
2897
- /* @__PURE__ */ jsx("p", {
2898
- className: "mt-1 text-xs text-kumo-subtle",
2899
- children: this.state.error?.message || "The plugin field widget failed to render."
2900
- }),
2901
- /* @__PURE__ */ jsx("button", {
2902
- type: "button",
2903
- className: "mt-2 text-xs font-medium text-kumo-brand underline",
2904
- onClick: () => this.setState({
2905
- hasError: false,
2906
- error: void 0
2907
- }),
2908
- children: "Retry"
2909
- })
2910
- ]
2911
- });
2912
- return this.props.children;
2913
- }
2914
- };
2915
-
2916
- //#endregion
2917
- //#region src/components/RepeaterField.tsx
2918
- /**
2919
- * RepeaterField — renders a list of repeating sub-field groups in the content editor.
2920
- *
2921
- * Each item is a collapsible card containing the defined sub-fields.
2922
- * Items can be added, removed, and reordered via drag-and-drop.
2923
- */
2924
- function ensureKeys(items) {
2925
- return items.map((item, i) => {
2926
- const obj = typeof item === "object" && item !== null ? item : {};
2927
- return {
2928
- ...obj,
2929
- _key: obj._key || `item-${i}-${Date.now()}`
2829
+ img.onerror = () => {
2830
+ reject(/* @__PURE__ */ new Error("Failed to load image"));
2930
2831
  };
2832
+ img.src = url;
2931
2833
  });
2932
2834
  }
2933
- function stripKeys(items) {
2934
- return items.map(({ _key, ...rest }) => rest);
2935
- }
2936
- function RepeaterField({ label, id, value, onChange, subFields, minItems = 0, maxItems }) {
2835
+ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "image/", title: providedTitle }) {
2937
2836
  const { _: _t } = useLingui();
2938
- const rawItems = Array.isArray(value) ? value : [];
2939
- const [items, setItems] = React.useState(() => ensureKeys(rawItems));
2940
- const [collapsedItems, setCollapsedItems] = React.useState(/* @__PURE__ */ new Set());
2941
- React.useEffect(() => {
2942
- const incoming = Array.isArray(value) ? value : [];
2943
- setItems((prev) => incoming.map((item, i) => {
2944
- const obj = typeof item === "object" && item !== null ? item : {};
2945
- const existingKey = obj._key || prev[i]?._key;
2946
- return {
2947
- ...obj,
2948
- _key: existingKey || `item-${i}-${Date.now()}`
2949
- };
2950
- }));
2951
- }, [value]);
2952
- const emitChange = (updated) => {
2953
- setItems(updated);
2954
- onChange(stripKeys(updated));
2955
- };
2956
- const handleAdd = () => {
2957
- if (maxItems && items.length >= maxItems) return;
2958
- const newItem = { _key: `item-${Date.now()}` };
2959
- for (const sf of subFields) newItem[sf.slug] = sf.type === "boolean" ? false : sf.type === "number" || sf.type === "integer" ? null : "";
2960
- emitChange([...items, newItem]);
2961
- };
2962
- const handleRemove = (key) => {
2963
- if (items.length <= minItems) return;
2964
- emitChange(items.filter((item) => item._key !== key));
2965
- };
2966
- const handleItemChange = (key, fieldSlug, fieldValue) => {
2967
- emitChange(items.map((item) => item._key === key ? {
2968
- ...item,
2969
- [fieldSlug]: fieldValue
2970
- } : item));
2971
- };
2972
- const handleDragEnd = (event) => {
2973
- const { active, over } = event;
2974
- if (!over || active.id === over.id) return;
2975
- const oldIndex = items.findIndex((item) => item._key === active.id);
2976
- const newIndex = items.findIndex((item) => item._key === over.id);
2977
- if (oldIndex === -1 || newIndex === -1) return;
2978
- emitChange(arrayMove(items, oldIndex, newIndex));
2979
- };
2980
- const toggleCollapse = (key) => {
2981
- setCollapsedItems((prev) => {
2982
- const next = new Set(prev);
2983
- if (next.has(key)) next.delete(key);
2984
- else next.add(key);
2985
- return next;
2986
- });
2987
- };
2988
- const canAdd = !maxItems || items.length < maxItems;
2989
- const canRemove = items.length > minItems;
2990
- return /* @__PURE__ */ jsxs("div", {
2991
- className: "space-y-2",
2992
- children: [/* @__PURE__ */ jsxs("div", {
2993
- className: "flex items-center justify-between",
2994
- children: [/* @__PURE__ */ jsxs("label", {
2995
- htmlFor: id,
2996
- className: "text-sm font-medium",
2997
- children: [label, items.length > 0 && /* @__PURE__ */ jsx("span", {
2998
- className: "ms-2 text-kumo-subtle font-normal",
2999
- children: i18n._({
3000
- id: "JcH6Wc",
3001
- message: "{0, plural, one {(# item)} other {(# items)}}",
3002
- values: { 0: items.length }
3003
- })
3004
- })]
3005
- }), canAdd && /* @__PURE__ */ jsx(Button, {
3006
- variant: "outline",
3007
- size: "sm",
3008
- icon: /* @__PURE__ */ jsx(Plus, {}),
3009
- onClick: handleAdd,
3010
- children: _t({
3011
- id: "qgF8ew",
3012
- message: "Add Item"
3013
- })
3014
- })]
3015
- }), items.length === 0 ? /* @__PURE__ */ jsxs("div", {
3016
- className: "border-2 border-dashed rounded-lg p-6 text-center text-kumo-subtle",
3017
- children: [/* @__PURE__ */ jsx("p", {
3018
- className: "text-sm",
3019
- children: _t({
3020
- id: "bWmJ0A",
3021
- message: "No items yet"
3022
- })
3023
- }), canAdd && /* @__PURE__ */ jsx(Button, {
3024
- variant: "outline",
3025
- size: "sm",
3026
- className: "mt-2",
3027
- icon: /* @__PURE__ */ jsx(Plus, {}),
3028
- onClick: handleAdd,
3029
- children: _t({
3030
- id: "924qJl",
3031
- message: "Add First Item"
3032
- })
3033
- })]
3034
- }) : /* @__PURE__ */ jsx(DndContext, {
3035
- collisionDetection: closestCenter,
3036
- onDragEnd: handleDragEnd,
3037
- children: /* @__PURE__ */ jsx(SortableContext, {
3038
- items: items.map((item) => item._key),
3039
- strategy: verticalListSortingStrategy,
3040
- children: /* @__PURE__ */ jsx("div", {
3041
- className: "space-y-2",
3042
- children: items.map((item, index) => /* @__PURE__ */ jsx(SortableRepeaterItem, {
3043
- item,
3044
- index,
3045
- subFields,
3046
- isCollapsed: collapsedItems.has(item._key),
3047
- onToggleCollapse: () => toggleCollapse(item._key),
3048
- onRemove: canRemove ? () => handleRemove(item._key) : void 0,
3049
- onChange: (fieldSlug, fieldValue) => handleItemChange(item._key, fieldSlug, fieldValue)
3050
- }, item._key))
3051
- })
3052
- })
3053
- })]
2837
+ const title = providedTitle ?? _t({
2838
+ id: "bAiEJP",
2839
+ message: "Select Image"
3054
2840
  });
3055
- }
3056
- function SortableRepeaterItem({ item, index, subFields, isCollapsed, onToggleCollapse, onRemove, onChange }) {
3057
- const { _: _t2 } = useLingui();
3058
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item._key });
3059
- const style = {
3060
- transform: CSS.Transform.toString(transform),
3061
- transition
3062
- };
3063
- const summaryField = subFields.find((sf) => sf.type === "string" || sf.type === "text");
3064
- const summaryLabel = (summaryField ? item[summaryField.slug] || "" : "") || _t2({
3065
- id: "y/5CE8",
3066
- message: "Item {0}",
3067
- values: { 0: index + 1 }
3068
- });
3069
- return /* @__PURE__ */ jsxs("div", {
3070
- ref: setNodeRef,
3071
- style,
3072
- className: cn("border rounded-lg bg-kumo-base", isDragging && "opacity-50 ring-2 ring-kumo-brand"),
3073
- children: [/* @__PURE__ */ jsxs("div", {
3074
- className: "flex items-center gap-2 px-3 py-2 border-b cursor-pointer",
3075
- onClick: onToggleCollapse,
3076
- children: [
3077
- /* @__PURE__ */ jsx(DotsSixVertical, {
3078
- className: "h-4 w-4 text-kumo-subtle cursor-grab shrink-0",
3079
- ...attributes,
3080
- ...listeners,
3081
- onClick: (e) => e.stopPropagation()
3082
- }),
3083
- isCollapsed ? /* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4 text-kumo-subtle shrink-0" }) : /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4 text-kumo-subtle shrink-0" }),
3084
- /* @__PURE__ */ jsx("span", {
3085
- className: "text-sm font-medium flex-1 truncate",
3086
- children: summaryLabel
3087
- }),
3088
- onRemove && /* @__PURE__ */ jsx(Button, {
3089
- variant: "ghost",
3090
- shape: "square",
3091
- onClick: (e) => {
3092
- e.stopPropagation();
3093
- onRemove();
3094
- },
3095
- "aria-label": _t2({
3096
- id: "mU3j1b",
3097
- message: "Remove item {0}",
3098
- values: { 0: index + 1 }
3099
- }),
3100
- children: /* @__PURE__ */ jsx(Trash, { className: "h-3.5 w-3.5 text-kumo-danger" })
3101
- })
3102
- ]
3103
- }), !isCollapsed && /* @__PURE__ */ jsx("div", {
3104
- className: "p-3 space-y-3",
3105
- children: subFields.map((sf) => /* @__PURE__ */ jsx(SubFieldInput, {
3106
- subField: sf,
3107
- value: item[sf.slug],
3108
- onChange: (v) => onChange(sf.slug, v)
3109
- }, sf.slug))
3110
- })]
3111
- });
3112
- }
3113
- function SubFieldInput({ subField, value, onChange }) {
3114
- const { _: _t3 } = useLingui();
3115
- switch (subField.type) {
3116
- case "string": return /* @__PURE__ */ jsx(Input, {
3117
- label: subField.label,
3118
- value: typeof value === "string" ? value : "",
3119
- onChange: (e) => onChange(e.target.value),
3120
- required: subField.required
3121
- });
3122
- case "text": return /* @__PURE__ */ jsx(InputArea, {
3123
- label: subField.label,
3124
- value: typeof value === "string" ? value : "",
3125
- onChange: (e) => onChange(e.target.value),
3126
- required: subField.required,
3127
- rows: 3
3128
- });
3129
- case "number":
3130
- case "integer": return /* @__PURE__ */ jsx(Input, {
3131
- label: subField.label,
3132
- type: "number",
3133
- value: typeof value === "number" ? String(value) : "",
3134
- onChange: (e) => onChange(e.target.value ? Number(e.target.value) : null),
3135
- required: subField.required,
3136
- step: subField.type === "integer" ? "1" : "any"
3137
- });
3138
- case "boolean": return /* @__PURE__ */ jsxs("label", {
3139
- className: "flex items-center gap-2",
3140
- children: [/* @__PURE__ */ jsx("input", {
3141
- type: "checkbox",
3142
- checked: Boolean(value),
3143
- onChange: (e) => onChange(e.target.checked)
3144
- }), /* @__PURE__ */ jsx("span", {
3145
- className: "text-sm",
3146
- children: subField.label
3147
- })]
3148
- });
3149
- case "datetime": return /* @__PURE__ */ jsx(Input, {
3150
- label: subField.label,
3151
- type: "datetime-local",
3152
- value: typeof value === "string" ? value : "",
3153
- onChange: (e) => onChange(e.target.value),
3154
- required: subField.required
3155
- });
3156
- case "select": return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
3157
- className: "text-sm font-medium",
3158
- children: subField.label
3159
- }), /* @__PURE__ */ jsxs("select", {
3160
- className: "w-full mt-1 rounded-md border px-3 py-2 text-sm",
3161
- value: typeof value === "string" ? value : "",
3162
- onChange: (e) => onChange(e.target.value),
3163
- required: subField.required,
3164
- children: [/* @__PURE__ */ jsx("option", {
3165
- value: "",
3166
- children: _t3({
3167
- id: "O/7I0o",
3168
- message: "Select..."
3169
- })
3170
- }), subField.options?.map((opt) => /* @__PURE__ */ jsx("option", {
3171
- value: opt,
3172
- children: opt
3173
- }, opt))]
3174
- })] });
3175
- default: return /* @__PURE__ */ jsx(Input, {
3176
- label: subField.label,
3177
- value: typeof value === "string" ? value : "",
3178
- onChange: (e) => onChange(e.target.value)
3179
- });
3180
- }
3181
- }
3182
-
3183
- //#endregion
3184
- //#region src/lib/hooks.ts
3185
- /**
3186
- * Returns a stable function reference that always calls the latest version
3187
- * of the provided callback. Useful for event listeners in effects where
3188
- * you don't want the listener to be torn down and re-added when the
3189
- * callback identity changes.
3190
- */
3191
- function useStableCallback(callback) {
3192
- const ref = React.useRef(callback);
3193
- React.useLayoutEffect(() => {
3194
- ref.current = callback;
3195
- });
3196
- return React.useCallback((...args) => ref.current(...args), []);
3197
- }
3198
- /**
3199
- * Returns a debounced version of a value that only updates after the
3200
- * specified delay has elapsed since the last change. Useful for search
3201
- * inputs that trigger API calls.
3202
- */
3203
- function useDebouncedValue$1(value, delay) {
3204
- const [debouncedValue, setDebouncedValue] = React.useState(value);
3205
- React.useEffect(() => {
3206
- const timer = setTimeout(setDebouncedValue, delay, value);
3207
- return () => clearTimeout(timer);
3208
- }, [value, delay]);
3209
- return debouncedValue;
3210
- }
3211
-
3212
- //#endregion
3213
- //#region src/lib/media-utils.ts
3214
- function providerItemToMediaItem(providerId, item) {
3215
- return {
3216
- id: item.id,
3217
- filename: item.filename,
3218
- mimeType: item.mimeType,
3219
- url: item.previewUrl || "",
3220
- size: item.size || 0,
3221
- width: item.width,
3222
- height: item.height,
3223
- alt: item.alt,
3224
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3225
- provider: providerId,
3226
- meta: item.meta
3227
- };
3228
- }
3229
- function getFileIcon(mimeType) {
3230
- if (mimeType.startsWith("video/")) return "🎬";
3231
- if (mimeType.startsWith("audio/")) return "🎵";
3232
- if (mimeType.includes("pdf")) return "📄";
3233
- if (mimeType.includes("document") || mimeType.includes("word")) return "📝";
3234
- if (mimeType.includes("spreadsheet") || mimeType.includes("excel")) return "📊";
3235
- return "📁";
3236
- }
3237
- function formatFileSize(bytes) {
3238
- if (bytes === 0) return "0 B";
3239
- const k = 1024;
3240
- const sizes = [
3241
- "B",
3242
- "KB",
3243
- "MB",
3244
- "GB"
3245
- ];
3246
- const i = Math.floor(Math.log(bytes) / Math.log(k));
3247
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
3248
- }
3249
-
3250
- //#endregion
3251
- //#region src/components/MediaPickerModal.tsx
3252
- /**
3253
- * Media Picker Modal
3254
- *
3255
- * A modal dialog for selecting media from the library or uploading new files.
3256
- * Supports multiple media providers with tabbed navigation.
3257
- * Used by the rich text editor and image field components.
3258
- */
3259
- /**
3260
- * Probe image URL to get dimensions
3261
- */
3262
- function probeImageDimensions(url) {
3263
- return new Promise((resolve, reject) => {
3264
- const img = new window.Image();
3265
- img.onload = () => {
3266
- resolve({
3267
- width: img.naturalWidth,
3268
- height: img.naturalHeight
3269
- });
3270
- };
3271
- img.onerror = () => {
3272
- reject(/* @__PURE__ */ new Error("Failed to load image"));
3273
- };
3274
- img.src = url;
3275
- });
3276
- }
3277
- function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "image/", title: providedTitle }) {
3278
- const { _: _t } = useLingui();
3279
- const title = providedTitle ?? _t({
3280
- id: "bAiEJP",
3281
- message: "Select Image"
3282
- });
3283
- const queryClient = useQueryClient();
3284
- const [selectedItem, setSelectedItem] = React.useState(null);
3285
- const [activeProvider, setActiveProvider] = React.useState("local");
3286
- const [searchQuery, setSearchQuery] = React.useState("");
3287
- const fileInputRef = React.useRef(null);
3288
- const [imageUrl, setImageUrl] = React.useState("");
3289
- const [isProbing, setIsProbing] = React.useState(false);
3290
- const [urlError, setUrlError] = React.useState(null);
3291
- const [providerDimensions, setProviderDimensions] = React.useState({});
3292
- React.useEffect(() => {
3293
- if (open) {
3294
- setSelectedItem(null);
3295
- setActiveProvider("local");
3296
- setSearchQuery("");
3297
- setImageUrl("");
3298
- setUrlError(null);
3299
- setUploadError(null);
3300
- setProviderDimensions({});
3301
- }
3302
- }, [open]);
3303
- const { data: providers } = useQuery({
3304
- queryKey: ["media-providers"],
3305
- queryFn: fetchMediaProviders,
3306
- enabled: open,
3307
- placeholderData: []
2841
+ const queryClient = useQueryClient();
2842
+ const [selectedItem, setSelectedItem] = React.useState(null);
2843
+ const [activeProvider, setActiveProvider] = React.useState("local");
2844
+ const [searchQuery, setSearchQuery] = React.useState("");
2845
+ const fileInputRef = React.useRef(null);
2846
+ const [imageUrl, setImageUrl] = React.useState("");
2847
+ const [isProbing, setIsProbing] = React.useState(false);
2848
+ const [urlError, setUrlError] = React.useState(null);
2849
+ const [providerDimensions, setProviderDimensions] = React.useState({});
2850
+ React.useEffect(() => {
2851
+ if (open) {
2852
+ setSelectedItem(null);
2853
+ setActiveProvider("local");
2854
+ setSearchQuery("");
2855
+ setImageUrl("");
2856
+ setUrlError(null);
2857
+ setUploadError(null);
2858
+ setProviderDimensions({});
2859
+ }
2860
+ }, [open]);
2861
+ const { data: providers } = useQuery({
2862
+ queryKey: ["media-providers"],
2863
+ queryFn: fetchMediaProviders,
2864
+ enabled: open,
2865
+ placeholderData: []
3308
2866
  });
3309
2867
  const activeProviderInfo = React.useMemo(() => {
3310
2868
  if (activeProvider === "local") return {
@@ -3818,139 +3376,805 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3818
3376
  })
3819
3377
  ]
3820
3378
  })
3821
- ]
3822
- })
3379
+ ]
3380
+ })
3381
+ });
3382
+ }
3383
+ function MediaPickerItem({ item, selected, onClick, onDoubleClick, onDimensionsDetected }) {
3384
+ const { _: _t2 } = useLingui();
3385
+ const isImage = item.mimeType.startsWith("image/");
3386
+ const needsDimensions = isImage && (!item.width || !item.height);
3387
+ const handleImageLoad = React.useCallback((e) => {
3388
+ if (needsDimensions && onDimensionsDetected) {
3389
+ const img = e.currentTarget;
3390
+ if (img.naturalWidth && img.naturalHeight) onDimensionsDetected(item.id, img.naturalWidth, img.naturalHeight);
3391
+ }
3392
+ }, [
3393
+ needsDimensions,
3394
+ onDimensionsDetected,
3395
+ item.id
3396
+ ]);
3397
+ return /* @__PURE__ */ jsx("li", {
3398
+ role: "option",
3399
+ "aria-selected": selected,
3400
+ children: /* @__PURE__ */ jsxs("button", {
3401
+ type: "button",
3402
+ className: cn("relative aspect-square w-full rounded-lg border-2 overflow-hidden transition-all", "hover:border-kumo-brand/50 focus:outline-none focus:ring-2 focus:ring-kumo-ring", selected ? "border-kumo-brand ring-2 ring-kumo-brand/20" : "border-transparent"),
3403
+ onClick,
3404
+ onDoubleClick,
3405
+ "aria-label": _t2({
3406
+ id: "qSiUsc",
3407
+ message: "{0}{1}",
3408
+ values: {
3409
+ 0: item.filename,
3410
+ 1: selected ? _t2({
3411
+ id: "QbxreS",
3412
+ message: " (selected)"
3413
+ }) : ""
3414
+ }
3415
+ }),
3416
+ children: [
3417
+ isImage ? /* @__PURE__ */ jsx("img", {
3418
+ src: item.url,
3419
+ alt: "",
3420
+ className: "h-full w-full object-cover",
3421
+ onLoad: handleImageLoad
3422
+ }) : /* @__PURE__ */ jsx("div", {
3423
+ className: "flex h-full w-full items-center justify-center bg-kumo-tint",
3424
+ children: /* @__PURE__ */ jsx("span", {
3425
+ className: "text-3xl",
3426
+ "aria-hidden": "true",
3427
+ children: getFileIcon(item.mimeType)
3428
+ })
3429
+ }),
3430
+ selected && /* @__PURE__ */ jsx("div", {
3431
+ className: "absolute inset-0 bg-kumo-brand/20 flex items-center justify-center",
3432
+ "aria-hidden": "true",
3433
+ children: /* @__PURE__ */ jsx("div", {
3434
+ className: "bg-kumo-brand text-white rounded-full p-1",
3435
+ children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
3436
+ })
3437
+ }),
3438
+ /* @__PURE__ */ jsx("div", {
3439
+ className: "absolute bottom-0 start-0 end-0 bg-gradient-to-t from-black/60 to-transparent p-2",
3440
+ "aria-hidden": "true",
3441
+ children: /* @__PURE__ */ jsx("p", {
3442
+ className: "text-xs text-white truncate",
3443
+ children: item.filename
3444
+ })
3445
+ })
3446
+ ]
3447
+ })
3448
+ });
3449
+ }
3450
+ function ProviderMediaItem({ item, selected, onClick, onDoubleClick, onDimensionsLoaded }) {
3451
+ const { _: _t3 } = useLingui();
3452
+ const isImage = item.mimeType.startsWith("image/");
3453
+ const needsDimensions = isImage && (!item.width || !item.height);
3454
+ const handleImageLoad = React.useCallback((e) => {
3455
+ if (needsDimensions && onDimensionsLoaded) {
3456
+ const img = e.currentTarget;
3457
+ if (img.naturalWidth && img.naturalHeight) onDimensionsLoaded(img.naturalWidth, img.naturalHeight);
3458
+ }
3459
+ }, [needsDimensions, onDimensionsLoaded]);
3460
+ return /* @__PURE__ */ jsx("li", {
3461
+ role: "option",
3462
+ "aria-selected": selected,
3463
+ children: /* @__PURE__ */ jsxs("button", {
3464
+ type: "button",
3465
+ className: cn("relative aspect-square w-full rounded-lg border-2 overflow-hidden transition-all", "hover:border-kumo-brand/50 focus:outline-none focus:ring-2 focus:ring-kumo-ring", selected ? "border-kumo-brand ring-2 ring-kumo-brand/20" : "border-transparent"),
3466
+ onClick,
3467
+ onDoubleClick,
3468
+ "aria-label": _t3({
3469
+ id: "qSiUsc",
3470
+ message: "{0}{1}",
3471
+ values: {
3472
+ 0: item.filename,
3473
+ 1: selected ? _t3({
3474
+ id: "QbxreS",
3475
+ message: " (selected)"
3476
+ }) : ""
3477
+ }
3478
+ }),
3479
+ children: [
3480
+ isImage && item.previewUrl ? /* @__PURE__ */ jsx("img", {
3481
+ src: item.previewUrl,
3482
+ alt: "",
3483
+ className: "h-full w-full object-cover",
3484
+ onLoad: handleImageLoad
3485
+ }) : /* @__PURE__ */ jsx("div", {
3486
+ className: "flex h-full w-full items-center justify-center bg-kumo-tint",
3487
+ children: /* @__PURE__ */ jsx("span", {
3488
+ className: "text-3xl",
3489
+ "aria-hidden": "true",
3490
+ children: getFileIcon(item.mimeType)
3491
+ })
3492
+ }),
3493
+ selected && /* @__PURE__ */ jsx("div", {
3494
+ className: "absolute inset-0 bg-kumo-brand/20 flex items-center justify-center",
3495
+ "aria-hidden": "true",
3496
+ children: /* @__PURE__ */ jsx("div", {
3497
+ className: "bg-kumo-brand text-white rounded-full p-1",
3498
+ children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
3499
+ })
3500
+ }),
3501
+ /* @__PURE__ */ jsx("div", {
3502
+ className: "absolute bottom-0 start-0 end-0 bg-gradient-to-t from-black/60 to-transparent p-2",
3503
+ "aria-hidden": "true",
3504
+ children: /* @__PURE__ */ jsx("p", {
3505
+ className: "text-xs text-white truncate",
3506
+ children: item.filename
3507
+ })
3508
+ })
3509
+ ]
3510
+ })
3511
+ });
3512
+ }
3513
+
3514
+ //#endregion
3515
+ //#region src/components/BlockKitMediaPickerField.tsx
3516
+ /**
3517
+ * Shared media_picker BlockKit element renderer used by `BlockKitFieldWidget`
3518
+ * (sandboxed plugin field widgets) and the `BlockKitField` switch inside
3519
+ * `PortableTextEditor` (plugin block forms).
3520
+ *
3521
+ * The stored value is the asset URL string, so values are interchangeable
3522
+ * with `text_input`. Existing arbitrary URLs are tolerated but only previewed
3523
+ * when they pass scheme/path safety checks.
3524
+ */
3525
+ function BlockKitMediaPickerField({ actionId, label, placeholder, mimeTypeFilter, value, onChange }) {
3526
+ const { _: _t } = useLingui();
3527
+ const [pickerOpen, setPickerOpen] = React.useState(false);
3528
+ const url = typeof value === "string" && value.length > 0 ? value : "";
3529
+ const filter = mimeTypeFilter ?? "image/";
3530
+ const canPreview = isSafePreviewUrl(url);
3531
+ const handleSelect = (item) => {
3532
+ const isLocalMedia = item.provider === "local" || !!item.storageKey;
3533
+ const localKey = item.storageKey || item.id;
3534
+ const nextUrl = isLocalMedia && localKey ? `/_emdash/api/media/file/${localKey}` : item.url;
3535
+ if (!nextUrl) return;
3536
+ onChange(actionId, nextUrl);
3537
+ };
3538
+ return /* @__PURE__ */ jsxs("div", { children: [
3539
+ /* @__PURE__ */ jsx("label", {
3540
+ className: "text-sm font-medium mb-1.5 block",
3541
+ children: label
3542
+ }),
3543
+ canPreview ? /* @__PURE__ */ jsxs("div", {
3544
+ className: "relative group",
3545
+ children: [/* @__PURE__ */ jsx("img", {
3546
+ src: url,
3547
+ alt: "",
3548
+ className: "max-h-40 w-full rounded-md border border-kumo-line object-contain bg-kumo-muted",
3549
+ referrerPolicy: "no-referrer",
3550
+ loading: "lazy"
3551
+ }), /* @__PURE__ */ jsxs("div", {
3552
+ className: "absolute top-2 end-2 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto transition-opacity flex gap-1",
3553
+ children: [/* @__PURE__ */ jsx(Button, {
3554
+ type: "button",
3555
+ size: "sm",
3556
+ variant: "secondary",
3557
+ onClick: () => setPickerOpen(true),
3558
+ children: _t({
3559
+ id: "o+XJ9D",
3560
+ message: "Change"
3561
+ })
3562
+ }), /* @__PURE__ */ jsx(Button, {
3563
+ type: "button",
3564
+ shape: "square",
3565
+ variant: "destructive",
3566
+ className: "h-8 w-8",
3567
+ onClick: () => onChange(actionId, ""),
3568
+ "aria-label": _t({
3569
+ id: "t/YqKh",
3570
+ message: "Remove"
3571
+ }),
3572
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
3573
+ })]
3574
+ })]
3575
+ }) : /* @__PURE__ */ jsx(Button, {
3576
+ type: "button",
3577
+ variant: "outline",
3578
+ className: "w-full h-24 border-dashed",
3579
+ onClick: () => setPickerOpen(true),
3580
+ children: /* @__PURE__ */ jsxs("div", {
3581
+ className: "flex flex-col items-center gap-1.5 text-kumo-subtle",
3582
+ children: [/* @__PURE__ */ jsx(Image$1, { className: "h-6 w-6" }), /* @__PURE__ */ jsx("span", {
3583
+ className: "text-sm",
3584
+ children: placeholder ?? _t({
3585
+ id: "1Tq98c",
3586
+ message: "Select media"
3587
+ })
3588
+ })]
3589
+ })
3590
+ }),
3591
+ /* @__PURE__ */ jsx(MediaPickerModal, {
3592
+ open: pickerOpen,
3593
+ onOpenChange: setPickerOpen,
3594
+ onSelect: handleSelect,
3595
+ mimeTypeFilter: filter,
3596
+ title: _t({
3597
+ id: "pov/PW",
3598
+ message: "Select {label}",
3599
+ values: { label }
3600
+ })
3601
+ })
3602
+ ] });
3603
+ }
3604
+ const HAS_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
3605
+ /**
3606
+ * Returns true when `url` is safe to preview via `<img src={url}>`:
3607
+ * - Same-origin relative path starting with `/` (but not `//`)
3608
+ * - External `http://` or `https://` URL
3609
+ *
3610
+ * Rejects `javascript:`, `data:`, protocol-relative `//host`, and other
3611
+ * schemes whose preview could leak credentials or trigger surprises.
3612
+ */
3613
+ function isSafePreviewUrl(url) {
3614
+ if (!url) return false;
3615
+ if (HAS_SCHEME_RE.test(url)) return isSafeUrl$1(url);
3616
+ return url.startsWith("/") && !url.startsWith("//");
3617
+ }
3618
+
3619
+ //#endregion
3620
+ //#region src/components/BlockKitFieldWidget.tsx
3621
+ /**
3622
+ * Renders Block Kit elements as a field widget for sandboxed plugins.
3623
+ * Decomposes a JSON value into per-element values keyed by action_id,
3624
+ * and recomposes on change.
3625
+ */
3626
+ function BlockKitFieldWidget({ label, elements, value, onChange }) {
3627
+ const obj = value && typeof value === "object" ? value : {};
3628
+ const objRef = React.useRef(obj);
3629
+ objRef.current = obj;
3630
+ const handleElementChange = React.useCallback((actionId, elementValue) => {
3631
+ onChange({
3632
+ ...objRef.current,
3633
+ [actionId]: elementValue
3634
+ });
3635
+ }, [onChange]);
3636
+ const validElements = elements.filter((el) => el.action_id);
3637
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
3638
+ className: "text-sm font-medium leading-none",
3639
+ children: label
3640
+ }), /* @__PURE__ */ jsx("div", {
3641
+ className: "mt-2 space-y-3",
3642
+ children: validElements.map((el) => /* @__PURE__ */ jsx(BlockKitFieldElement, {
3643
+ element: el,
3644
+ value: obj[el.action_id],
3645
+ onChange: handleElementChange
3646
+ }, el.action_id))
3647
+ })] });
3648
+ }
3649
+ function BlockKitFieldElement({ element, value, onChange }) {
3650
+ switch (element.type) {
3651
+ case "text_input": return /* @__PURE__ */ jsx(Input, {
3652
+ label: element.label,
3653
+ placeholder: element.placeholder,
3654
+ value: typeof value === "string" ? value : "",
3655
+ onChange: (e) => onChange(element.action_id, e.target.value)
3656
+ });
3657
+ case "number_input": return /* @__PURE__ */ jsx(Input, {
3658
+ label: element.label,
3659
+ type: "number",
3660
+ value: typeof value === "number" ? String(value) : "",
3661
+ onChange: (e) => {
3662
+ const n = Number(e.target.value);
3663
+ onChange(element.action_id, e.target.value && Number.isFinite(n) ? n : void 0);
3664
+ }
3665
+ });
3666
+ case "toggle": return /* @__PURE__ */ jsx(Switch, {
3667
+ label: element.label,
3668
+ checked: !!value,
3669
+ onCheckedChange: (checked) => onChange(element.action_id, checked)
3670
+ });
3671
+ case "select": {
3672
+ const options = Array.isArray(element.options) ? element.options : [];
3673
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
3674
+ className: "text-sm font-medium mb-1.5 block",
3675
+ children: element.label
3676
+ }), /* @__PURE__ */ jsxs("select", {
3677
+ className: "flex w-full rounded-md border border-kumo-line bg-transparent px-3 py-2 text-sm",
3678
+ value: typeof value === "string" ? value : "",
3679
+ onChange: (e) => onChange(element.action_id, e.target.value),
3680
+ children: [/* @__PURE__ */ jsx("option", {
3681
+ value: "",
3682
+ children: "Select..."
3683
+ }), options.map((opt) => /* @__PURE__ */ jsx("option", {
3684
+ value: opt.value,
3685
+ children: opt.label
3686
+ }, opt.value))]
3687
+ })] });
3688
+ }
3689
+ case "media_picker": return /* @__PURE__ */ jsx(BlockKitMediaPickerField, {
3690
+ actionId: element.action_id,
3691
+ label: element.label,
3692
+ placeholder: element.placeholder,
3693
+ mimeTypeFilter: element.mime_type_filter,
3694
+ value,
3695
+ onChange
3696
+ });
3697
+ default: return /* @__PURE__ */ jsxs("div", {
3698
+ className: "text-sm text-kumo-subtle",
3699
+ children: ["Unsupported widget element type: ", element.type]
3700
+ });
3701
+ }
3702
+ }
3703
+
3704
+ //#endregion
3705
+ //#region src/components/editor/DocumentOutline.tsx
3706
+ /**
3707
+ * Document Outline
3708
+ *
3709
+ * Displays a tree structure of headings from the TipTap editor.
3710
+ * - Shows H1 at root, H2 indented, H3 further indented
3711
+ * - Click-to-navigate to heading position
3712
+ * - Highlights the current section based on cursor position
3713
+ */
3714
+ function getIndentClass(level) {
3715
+ switch (level) {
3716
+ case 1: return "ps-0";
3717
+ case 2: return "ps-4";
3718
+ case 3: return "ps-8";
3719
+ default: return "ps-0";
3720
+ }
3721
+ }
3722
+ function getTextClass(level) {
3723
+ switch (level) {
3724
+ case 1: return "font-medium";
3725
+ case 2: return "font-normal";
3726
+ case 3: return "font-normal text-kumo-subtle";
3727
+ default: return "font-normal";
3728
+ }
3729
+ }
3730
+ /**
3731
+ * Extract headings from the TipTap editor document
3732
+ */
3733
+ function extractHeadings(editor) {
3734
+ if (!editor) return [];
3735
+ const headings = [];
3736
+ const doc = editor.state.doc;
3737
+ let key = 0;
3738
+ doc.descendants((node, pos) => {
3739
+ if (node.type.name === "heading") {
3740
+ const rawLevel = node.attrs.level;
3741
+ const level = typeof rawLevel === "number" ? rawLevel : 1;
3742
+ const text = node.textContent || "";
3743
+ if (text.trim()) headings.push({
3744
+ level,
3745
+ text,
3746
+ pos,
3747
+ key: `heading-${key++}`
3748
+ });
3749
+ }
3750
+ });
3751
+ return headings;
3752
+ }
3753
+ /**
3754
+ * Find the current heading based on cursor position
3755
+ */
3756
+ function findCurrentHeading(headings, cursorPos) {
3757
+ if (headings.length === 0) return null;
3758
+ let current = null;
3759
+ for (const heading of headings) if (heading.pos <= cursorPos) current = heading;
3760
+ else break;
3761
+ return current;
3762
+ }
3763
+ /**
3764
+ * Document outline component showing heading tree structure
3765
+ */
3766
+ function DocumentOutline({ editor, className }) {
3767
+ const { _: _t } = useLingui();
3768
+ const [isExpanded, setIsExpanded] = React.useState(true);
3769
+ const [headings, setHeadings] = React.useState([]);
3770
+ const [currentPos, setCurrentPos] = React.useState(0);
3771
+ React.useEffect(() => {
3772
+ if (!editor) return;
3773
+ const updateHeadings = () => {
3774
+ setHeadings(extractHeadings(editor));
3775
+ };
3776
+ updateHeadings();
3777
+ editor.on("update", updateHeadings);
3778
+ return () => {
3779
+ editor.off("update", updateHeadings);
3780
+ };
3781
+ }, [editor]);
3782
+ React.useEffect(() => {
3783
+ if (!editor) return;
3784
+ const updatePosition = () => {
3785
+ const { from } = editor.state.selection;
3786
+ setCurrentPos(from);
3787
+ };
3788
+ updatePosition();
3789
+ editor.on("selectionUpdate", updatePosition);
3790
+ return () => {
3791
+ editor.off("selectionUpdate", updatePosition);
3792
+ };
3793
+ }, [editor]);
3794
+ const currentHeading = findCurrentHeading(headings, currentPos);
3795
+ const handleHeadingClick = (heading) => {
3796
+ if (!editor) return;
3797
+ editor.chain().focus().setTextSelection(heading.pos).scrollIntoView().run();
3798
+ };
3799
+ return /* @__PURE__ */ jsxs("div", {
3800
+ className: cn("space-y-2", className),
3801
+ children: [/* @__PURE__ */ jsxs(Button, {
3802
+ variant: "ghost",
3803
+ size: "sm",
3804
+ className: "w-full justify-between px-2 h-8",
3805
+ onClick: () => setIsExpanded(!isExpanded),
3806
+ children: [/* @__PURE__ */ jsxs("span", {
3807
+ className: "flex items-center gap-2",
3808
+ children: [/* @__PURE__ */ jsx(List, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", {
3809
+ className: "font-semibold",
3810
+ children: _t({
3811
+ id: "dD7NPy",
3812
+ message: "Outline"
3813
+ })
3814
+ })]
3815
+ }), isExpanded ? /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(CaretNext, { className: "h-4 w-4" })]
3816
+ }), isExpanded && /* @__PURE__ */ jsx("div", {
3817
+ className: "space-y-0.5",
3818
+ children: headings.length === 0 ? /* @__PURE__ */ jsx("p", {
3819
+ className: "text-sm text-kumo-subtle px-2 py-1",
3820
+ children: _t({
3821
+ id: "MMd//L",
3822
+ message: "No headings in document"
3823
+ })
3824
+ }) : headings.map((heading) => {
3825
+ const isCurrent = currentHeading?.key === heading.key;
3826
+ return /* @__PURE__ */ jsx("button", {
3827
+ type: "button",
3828
+ onClick: () => handleHeadingClick(heading),
3829
+ className: cn("w-full text-start px-2 py-1 text-sm rounded transition-colors", "hover:bg-kumo-tint/50 cursor-pointer", "truncate", getIndentClass(heading.level), getTextClass(heading.level), isCurrent && "bg-kumo-tint text-kumo-default"),
3830
+ title: heading.text,
3831
+ children: heading.text
3832
+ }, heading.key);
3833
+ })
3834
+ })]
3835
+ });
3836
+ }
3837
+
3838
+ //#endregion
3839
+ //#region src/components/PluginFieldErrorBoundary.tsx
3840
+ /**
3841
+ * Error boundary that wraps trusted plugin field widgets.
3842
+ * On render error, shows a warning with a retry button instead of crashing the editor.
3843
+ */
3844
+ var PluginFieldErrorBoundary = class extends React.Component {
3845
+ constructor(props) {
3846
+ super(props);
3847
+ this.state = { hasError: false };
3848
+ }
3849
+ static getDerivedStateFromError(error) {
3850
+ return {
3851
+ hasError: true,
3852
+ error
3853
+ };
3854
+ }
3855
+ render() {
3856
+ if (this.state.hasError) return /* @__PURE__ */ jsxs("div", {
3857
+ className: "rounded-md border border-kumo-danger/50 bg-kumo-danger/5 p-3",
3858
+ children: [
3859
+ /* @__PURE__ */ jsx("p", {
3860
+ className: "text-sm font-medium text-kumo-danger",
3861
+ children: "Plugin widget error"
3862
+ }),
3863
+ /* @__PURE__ */ jsx("p", {
3864
+ className: "mt-1 text-xs text-kumo-subtle",
3865
+ children: this.state.error?.message || "The plugin field widget failed to render."
3866
+ }),
3867
+ /* @__PURE__ */ jsx("button", {
3868
+ type: "button",
3869
+ className: "mt-2 text-xs font-medium text-kumo-brand underline",
3870
+ onClick: () => this.setState({
3871
+ hasError: false,
3872
+ error: void 0
3873
+ }),
3874
+ children: "Retry"
3875
+ })
3876
+ ]
3877
+ });
3878
+ return this.props.children;
3879
+ }
3880
+ };
3881
+
3882
+ //#endregion
3883
+ //#region src/components/RepeaterField.tsx
3884
+ /**
3885
+ * RepeaterField — renders a list of repeating sub-field groups in the content editor.
3886
+ *
3887
+ * Each item is a collapsible card containing the defined sub-fields.
3888
+ * Items can be added, removed, and reordered via drag-and-drop.
3889
+ */
3890
+ function ensureKeys$1(items) {
3891
+ return items.map((item, i) => {
3892
+ const obj = typeof item === "object" && item !== null ? item : {};
3893
+ return {
3894
+ ...obj,
3895
+ _key: obj._key || `item-${i}-${Date.now()}`
3896
+ };
3897
+ });
3898
+ }
3899
+ function stripKeys$1(items) {
3900
+ return items.map(({ _key, ...rest }) => rest);
3901
+ }
3902
+ function RepeaterField({ label, id, value, onChange, subFields, minItems = 0, maxItems }) {
3903
+ const { _: _t } = useLingui();
3904
+ const rawItems = Array.isArray(value) ? value : [];
3905
+ const [items, setItems] = React.useState(() => ensureKeys$1(rawItems));
3906
+ const [collapsedItems, setCollapsedItems] = React.useState(/* @__PURE__ */ new Set());
3907
+ React.useEffect(() => {
3908
+ const incoming = Array.isArray(value) ? value : [];
3909
+ setItems((prev) => incoming.map((item, i) => {
3910
+ const obj = typeof item === "object" && item !== null ? item : {};
3911
+ const existingKey = obj._key || prev[i]?._key;
3912
+ return {
3913
+ ...obj,
3914
+ _key: existingKey || `item-${i}-${Date.now()}`
3915
+ };
3916
+ }));
3917
+ }, [value]);
3918
+ const emitChange = (updated) => {
3919
+ setItems(updated);
3920
+ onChange(stripKeys$1(updated));
3921
+ };
3922
+ const handleAdd = () => {
3923
+ if (maxItems && items.length >= maxItems) return;
3924
+ const newItem = { _key: `item-${Date.now()}` };
3925
+ for (const sf of subFields) newItem[sf.slug] = sf.type === "boolean" ? false : sf.type === "number" || sf.type === "integer" ? null : "";
3926
+ emitChange([...items, newItem]);
3927
+ };
3928
+ const handleRemove = (key) => {
3929
+ if (items.length <= minItems) return;
3930
+ emitChange(items.filter((item) => item._key !== key));
3931
+ };
3932
+ const handleItemChange = (key, fieldSlug, fieldValue) => {
3933
+ emitChange(items.map((item) => item._key === key ? {
3934
+ ...item,
3935
+ [fieldSlug]: fieldValue
3936
+ } : item));
3937
+ };
3938
+ const handleDragEnd = (event) => {
3939
+ const { active, over } = event;
3940
+ if (!over || active.id === over.id) return;
3941
+ const oldIndex = items.findIndex((item) => item._key === active.id);
3942
+ const newIndex = items.findIndex((item) => item._key === over.id);
3943
+ if (oldIndex === -1 || newIndex === -1) return;
3944
+ emitChange(arrayMove(items, oldIndex, newIndex));
3945
+ };
3946
+ const toggleCollapse = (key) => {
3947
+ setCollapsedItems((prev) => {
3948
+ const next = new Set(prev);
3949
+ if (next.has(key)) next.delete(key);
3950
+ else next.add(key);
3951
+ return next;
3952
+ });
3953
+ };
3954
+ const canAdd = !maxItems || items.length < maxItems;
3955
+ const canRemove = items.length > minItems;
3956
+ return /* @__PURE__ */ jsxs("div", {
3957
+ className: "space-y-2",
3958
+ children: [/* @__PURE__ */ jsxs("div", {
3959
+ className: "flex items-center justify-between",
3960
+ children: [/* @__PURE__ */ jsxs("label", {
3961
+ htmlFor: id,
3962
+ className: "text-sm font-medium",
3963
+ children: [label, items.length > 0 && /* @__PURE__ */ jsx("span", {
3964
+ className: "ms-2 text-kumo-subtle font-normal",
3965
+ children: i18n._({
3966
+ id: "JcH6Wc",
3967
+ message: "{0, plural, one {(# item)} other {(# items)}}",
3968
+ values: { 0: items.length }
3969
+ })
3970
+ })]
3971
+ }), canAdd && /* @__PURE__ */ jsx(Button, {
3972
+ variant: "outline",
3973
+ size: "sm",
3974
+ icon: /* @__PURE__ */ jsx(Plus, {}),
3975
+ onClick: handleAdd,
3976
+ children: _t({
3977
+ id: "qgF8ew",
3978
+ message: "Add Item"
3979
+ })
3980
+ })]
3981
+ }), items.length === 0 ? /* @__PURE__ */ jsxs("div", {
3982
+ className: "border-2 border-dashed rounded-lg p-6 text-center text-kumo-subtle",
3983
+ children: [/* @__PURE__ */ jsx("p", {
3984
+ className: "text-sm",
3985
+ children: _t({
3986
+ id: "bWmJ0A",
3987
+ message: "No items yet"
3988
+ })
3989
+ }), canAdd && /* @__PURE__ */ jsx(Button, {
3990
+ variant: "outline",
3991
+ size: "sm",
3992
+ className: "mt-2",
3993
+ icon: /* @__PURE__ */ jsx(Plus, {}),
3994
+ onClick: handleAdd,
3995
+ children: _t({
3996
+ id: "924qJl",
3997
+ message: "Add First Item"
3998
+ })
3999
+ })]
4000
+ }) : /* @__PURE__ */ jsx(DndContext, {
4001
+ collisionDetection: closestCenter,
4002
+ onDragEnd: handleDragEnd,
4003
+ children: /* @__PURE__ */ jsx(SortableContext, {
4004
+ items: items.map((item) => item._key),
4005
+ strategy: verticalListSortingStrategy,
4006
+ children: /* @__PURE__ */ jsx("div", {
4007
+ className: "space-y-2",
4008
+ children: items.map((item, index) => /* @__PURE__ */ jsx(SortableRepeaterItem, {
4009
+ item,
4010
+ index,
4011
+ subFields,
4012
+ isCollapsed: collapsedItems.has(item._key),
4013
+ onToggleCollapse: () => toggleCollapse(item._key),
4014
+ onRemove: canRemove ? () => handleRemove(item._key) : void 0,
4015
+ onChange: (fieldSlug, fieldValue) => handleItemChange(item._key, fieldSlug, fieldValue)
4016
+ }, item._key))
4017
+ })
4018
+ })
4019
+ })]
3823
4020
  });
3824
4021
  }
3825
- function MediaPickerItem({ item, selected, onClick, onDoubleClick, onDimensionsDetected }) {
4022
+ function SortableRepeaterItem({ item, index, subFields, isCollapsed, onToggleCollapse, onRemove, onChange }) {
3826
4023
  const { _: _t2 } = useLingui();
3827
- const isImage = item.mimeType.startsWith("image/");
3828
- const needsDimensions = isImage && (!item.width || !item.height);
3829
- const handleImageLoad = React.useCallback((e) => {
3830
- if (needsDimensions && onDimensionsDetected) {
3831
- const img = e.currentTarget;
3832
- if (img.naturalWidth && img.naturalHeight) onDimensionsDetected(item.id, img.naturalWidth, img.naturalHeight);
3833
- }
3834
- }, [
3835
- needsDimensions,
3836
- onDimensionsDetected,
3837
- item.id
3838
- ]);
3839
- return /* @__PURE__ */ jsx("li", {
3840
- role: "option",
3841
- "aria-selected": selected,
3842
- children: /* @__PURE__ */ jsxs("button", {
3843
- type: "button",
3844
- className: cn("relative aspect-square w-full rounded-lg border-2 overflow-hidden transition-all", "hover:border-kumo-brand/50 focus:outline-none focus:ring-2 focus:ring-kumo-ring", selected ? "border-kumo-brand ring-2 ring-kumo-brand/20" : "border-transparent"),
3845
- onClick,
3846
- onDoubleClick,
3847
- "aria-label": _t2({
3848
- id: "qSiUsc",
3849
- message: "{0}{1}",
3850
- values: {
3851
- 0: item.filename,
3852
- 1: selected ? _t2({
3853
- id: "QbxreS",
3854
- message: " (selected)"
3855
- }) : ""
3856
- }
3857
- }),
4024
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item._key });
4025
+ const style = {
4026
+ transform: CSS.Transform.toString(transform),
4027
+ transition
4028
+ };
4029
+ const summaryField = subFields.find((sf) => sf.type === "string" || sf.type === "text");
4030
+ const summaryLabel = (summaryField ? item[summaryField.slug] || "" : "") || _t2({
4031
+ id: "y/5CE8",
4032
+ message: "Item {0}",
4033
+ values: { 0: index + 1 }
4034
+ });
4035
+ return /* @__PURE__ */ jsxs("div", {
4036
+ ref: setNodeRef,
4037
+ style,
4038
+ className: cn("border rounded-lg bg-kumo-base", isDragging && "opacity-50 ring-2 ring-kumo-brand"),
4039
+ children: [/* @__PURE__ */ jsxs("div", {
4040
+ className: "flex items-center gap-2 px-3 py-2 border-b cursor-pointer",
4041
+ onClick: onToggleCollapse,
3858
4042
  children: [
3859
- isImage ? /* @__PURE__ */ jsx("img", {
3860
- src: item.url,
3861
- alt: "",
3862
- className: "h-full w-full object-cover",
3863
- onLoad: handleImageLoad
3864
- }) : /* @__PURE__ */ jsx("div", {
3865
- className: "flex h-full w-full items-center justify-center bg-kumo-tint",
3866
- children: /* @__PURE__ */ jsx("span", {
3867
- className: "text-3xl",
3868
- "aria-hidden": "true",
3869
- children: getFileIcon(item.mimeType)
3870
- })
4043
+ /* @__PURE__ */ jsx(DotsSixVertical, {
4044
+ className: "h-4 w-4 text-kumo-subtle cursor-grab shrink-0",
4045
+ ...attributes,
4046
+ ...listeners,
4047
+ onClick: (e) => e.stopPropagation()
3871
4048
  }),
3872
- selected && /* @__PURE__ */ jsx("div", {
3873
- className: "absolute inset-0 bg-kumo-brand/20 flex items-center justify-center",
3874
- "aria-hidden": "true",
3875
- children: /* @__PURE__ */ jsx("div", {
3876
- className: "bg-kumo-brand text-white rounded-full p-1",
3877
- children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
3878
- })
4049
+ isCollapsed ? /* @__PURE__ */ jsx(CaretNext, { className: "h-4 w-4 text-kumo-subtle shrink-0" }) : /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4 text-kumo-subtle shrink-0" }),
4050
+ /* @__PURE__ */ jsx("span", {
4051
+ className: "text-sm font-medium flex-1 truncate",
4052
+ children: summaryLabel
3879
4053
  }),
3880
- /* @__PURE__ */ jsx("div", {
3881
- className: "absolute bottom-0 start-0 end-0 bg-gradient-to-t from-black/60 to-transparent p-2",
3882
- "aria-hidden": "true",
3883
- children: /* @__PURE__ */ jsx("p", {
3884
- className: "text-xs text-white truncate",
3885
- children: item.filename
3886
- })
4054
+ onRemove && /* @__PURE__ */ jsx(Button, {
4055
+ variant: "ghost",
4056
+ shape: "square",
4057
+ onClick: (e) => {
4058
+ e.stopPropagation();
4059
+ onRemove();
4060
+ },
4061
+ "aria-label": _t2({
4062
+ id: "mU3j1b",
4063
+ message: "Remove item {0}",
4064
+ values: { 0: index + 1 }
4065
+ }),
4066
+ children: /* @__PURE__ */ jsx(Trash, { className: "h-3.5 w-3.5 text-kumo-danger" })
3887
4067
  })
3888
4068
  ]
3889
- })
4069
+ }), !isCollapsed && /* @__PURE__ */ jsx("div", {
4070
+ className: "p-3 space-y-3",
4071
+ children: subFields.map((sf) => /* @__PURE__ */ jsx(SubFieldInput, {
4072
+ subField: sf,
4073
+ value: item[sf.slug],
4074
+ onChange: (v) => onChange(sf.slug, v)
4075
+ }, sf.slug))
4076
+ })]
3890
4077
  });
3891
4078
  }
3892
- function ProviderMediaItem({ item, selected, onClick, onDoubleClick, onDimensionsLoaded }) {
4079
+ function SubFieldInput({ subField, value, onChange }) {
3893
4080
  const { _: _t3 } = useLingui();
3894
- const isImage = item.mimeType.startsWith("image/");
3895
- const needsDimensions = isImage && (!item.width || !item.height);
3896
- const handleImageLoad = React.useCallback((e) => {
3897
- if (needsDimensions && onDimensionsLoaded) {
3898
- const img = e.currentTarget;
3899
- if (img.naturalWidth && img.naturalHeight) onDimensionsLoaded(img.naturalWidth, img.naturalHeight);
3900
- }
3901
- }, [needsDimensions, onDimensionsLoaded]);
3902
- return /* @__PURE__ */ jsx("li", {
3903
- role: "option",
3904
- "aria-selected": selected,
3905
- children: /* @__PURE__ */ jsxs("button", {
3906
- type: "button",
3907
- className: cn("relative aspect-square w-full rounded-lg border-2 overflow-hidden transition-all", "hover:border-kumo-brand/50 focus:outline-none focus:ring-2 focus:ring-kumo-ring", selected ? "border-kumo-brand ring-2 ring-kumo-brand/20" : "border-transparent"),
3908
- onClick,
3909
- onDoubleClick,
3910
- "aria-label": _t3({
3911
- id: "qSiUsc",
3912
- message: "{0}{1}",
3913
- values: {
3914
- 0: item.filename,
3915
- 1: selected ? _t3({
3916
- id: "QbxreS",
3917
- message: " (selected)"
3918
- }) : ""
3919
- }
3920
- }),
3921
- children: [
3922
- isImage && item.previewUrl ? /* @__PURE__ */ jsx("img", {
3923
- src: item.previewUrl,
3924
- alt: "",
3925
- className: "h-full w-full object-cover",
3926
- onLoad: handleImageLoad
3927
- }) : /* @__PURE__ */ jsx("div", {
3928
- className: "flex h-full w-full items-center justify-center bg-kumo-tint",
3929
- children: /* @__PURE__ */ jsx("span", {
3930
- className: "text-3xl",
3931
- "aria-hidden": "true",
3932
- children: getFileIcon(item.mimeType)
3933
- })
3934
- }),
3935
- selected && /* @__PURE__ */ jsx("div", {
3936
- className: "absolute inset-0 bg-kumo-brand/20 flex items-center justify-center",
3937
- "aria-hidden": "true",
3938
- children: /* @__PURE__ */ jsx("div", {
3939
- className: "bg-kumo-brand text-white rounded-full p-1",
3940
- children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
3941
- })
3942
- }),
3943
- /* @__PURE__ */ jsx("div", {
3944
- className: "absolute bottom-0 start-0 end-0 bg-gradient-to-t from-black/60 to-transparent p-2",
3945
- "aria-hidden": "true",
3946
- children: /* @__PURE__ */ jsx("p", {
3947
- className: "text-xs text-white truncate",
3948
- children: item.filename
3949
- })
4081
+ switch (subField.type) {
4082
+ case "string": return /* @__PURE__ */ jsx(Input, {
4083
+ label: subField.label,
4084
+ value: typeof value === "string" ? value : "",
4085
+ onChange: (e) => onChange(e.target.value),
4086
+ required: subField.required,
4087
+ dir: "auto"
4088
+ });
4089
+ case "text": return /* @__PURE__ */ jsx(InputArea, {
4090
+ label: subField.label,
4091
+ value: typeof value === "string" ? value : "",
4092
+ onChange: (e) => onChange(e.target.value),
4093
+ required: subField.required,
4094
+ rows: 3,
4095
+ dir: "auto"
4096
+ });
4097
+ case "number":
4098
+ case "integer": return /* @__PURE__ */ jsx(Input, {
4099
+ label: subField.label,
4100
+ type: "number",
4101
+ value: typeof value === "number" ? String(value) : "",
4102
+ onChange: (e) => onChange(e.target.value ? Number(e.target.value) : null),
4103
+ required: subField.required,
4104
+ step: subField.type === "integer" ? "1" : "any"
4105
+ });
4106
+ case "boolean": return /* @__PURE__ */ jsxs("label", {
4107
+ className: "flex items-center gap-2",
4108
+ children: [/* @__PURE__ */ jsx("input", {
4109
+ type: "checkbox",
4110
+ checked: Boolean(value),
4111
+ onChange: (e) => onChange(e.target.checked)
4112
+ }), /* @__PURE__ */ jsx("span", {
4113
+ className: "text-sm",
4114
+ children: subField.label
4115
+ })]
4116
+ });
4117
+ case "datetime": return /* @__PURE__ */ jsx(Input, {
4118
+ label: subField.label,
4119
+ type: "datetime-local",
4120
+ value: toDatetimeLocalInputValue(value),
4121
+ onChange: (e) => onChange(fromDatetimeLocalInputValue(e.target.value)),
4122
+ required: subField.required
4123
+ });
4124
+ case "select": return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
4125
+ className: "text-sm font-medium",
4126
+ children: subField.label
4127
+ }), /* @__PURE__ */ jsxs("select", {
4128
+ className: "w-full mt-1 rounded-md border px-3 py-2 text-sm",
4129
+ value: typeof value === "string" ? value : "",
4130
+ onChange: (e) => onChange(e.target.value),
4131
+ required: subField.required,
4132
+ children: [/* @__PURE__ */ jsx("option", {
4133
+ value: "",
4134
+ children: _t3({
4135
+ id: "O/7I0o",
4136
+ message: "Select..."
3950
4137
  })
3951
- ]
3952
- })
4138
+ }), subField.options?.map((opt) => /* @__PURE__ */ jsx("option", {
4139
+ value: opt,
4140
+ children: opt
4141
+ }, opt))]
4142
+ })] });
4143
+ default: return /* @__PURE__ */ jsx(Input, {
4144
+ label: subField.label,
4145
+ value: typeof value === "string" ? value : "",
4146
+ onChange: (e) => onChange(e.target.value)
4147
+ });
4148
+ }
4149
+ }
4150
+
4151
+ //#endregion
4152
+ //#region src/lib/hooks.ts
4153
+ /**
4154
+ * Returns a stable function reference that always calls the latest version
4155
+ * of the provided callback. Useful for event listeners in effects where
4156
+ * you don't want the listener to be torn down and re-added when the
4157
+ * callback identity changes.
4158
+ */
4159
+ function useStableCallback(callback) {
4160
+ const ref = React.useRef(callback);
4161
+ React.useLayoutEffect(() => {
4162
+ ref.current = callback;
3953
4163
  });
4164
+ return React.useCallback((...args) => ref.current(...args), []);
4165
+ }
4166
+ /**
4167
+ * Returns a debounced version of a value that only updates after the
4168
+ * specified delay has elapsed since the last change. Useful for search
4169
+ * inputs that trigger API calls.
4170
+ */
4171
+ function useDebouncedValue$1(value, delay) {
4172
+ const [debouncedValue, setDebouncedValue] = React.useState(value);
4173
+ React.useEffect(() => {
4174
+ const timer = setTimeout(setDebouncedValue, delay, value);
4175
+ return () => clearTimeout(timer);
4176
+ }, [value, delay]);
4177
+ return debouncedValue;
3954
4178
  }
3955
4179
 
3956
4180
  //#endregion
@@ -5062,7 +5286,7 @@ function BlockMenu({ editor, anchorElement, isOpen, onClose }) {
5062
5286
  type: "button",
5063
5287
  className: "flex items-center gap-2 w-full px-3 py-2 text-sm hover:bg-kumo-tint text-start",
5064
5288
  onClick: () => setShowTransforms(false),
5065
- children: [/* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4 rotate-180" }), /* @__PURE__ */ jsx("span", { children: "Back" })]
5289
+ children: [/* @__PURE__ */ jsx(CaretPrev, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", { children: "Back" })]
5066
5290
  }),
5067
5291
  /* @__PURE__ */ jsx("div", { className: "h-px bg-kumo-line my-1" }),
5068
5292
  blockTransforms.map((transform) => /* @__PURE__ */ jsxs("button", {
@@ -5082,7 +5306,7 @@ function BlockMenu({ editor, anchorElement, isOpen, onClose }) {
5082
5306
  children: [/* @__PURE__ */ jsxs("span", {
5083
5307
  className: "flex items-center gap-2",
5084
5308
  children: [/* @__PURE__ */ jsx(Paragraph, { className: "h-4 w-4 text-kumo-subtle" }), /* @__PURE__ */ jsx("span", { children: "Turn into" })]
5085
- }), /* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4 text-kumo-subtle" })]
5309
+ }), /* @__PURE__ */ jsx(CaretNext, { className: "h-4 w-4 text-kumo-subtle" })]
5086
5310
  }),
5087
5311
  /* @__PURE__ */ jsxs("button", {
5088
5312
  type: "button",
@@ -6638,6 +6862,24 @@ function SlashCommandMenu({ state, onCommand, onClose: _onClose, setSelectedInde
6638
6862
  }, item.id))
6639
6863
  }), document.body);
6640
6864
  }
6865
+ function getPluginBlockDefaultValues(fields) {
6866
+ const defaults = {};
6867
+ for (const field of fields ?? []) {
6868
+ const initialValue = "initial_value" in field ? field.initial_value : void 0;
6869
+ if (initialValue !== void 0) defaults[field.action_id] = initialValue;
6870
+ }
6871
+ return defaults;
6872
+ }
6873
+ function buildPluginBlockFormValues(block, initialValues) {
6874
+ const defaults = getPluginBlockDefaultValues(block?.fields);
6875
+ return initialValues ? {
6876
+ ...defaults,
6877
+ ...initialValues
6878
+ } : defaults;
6879
+ }
6880
+ function hasPluginBlockFormData(values) {
6881
+ return Object.values(values).some((value) => value !== void 0 && value !== null && value !== "");
6882
+ }
6641
6883
  /**
6642
6884
  * Plugin block insertion/editing modal.
6643
6885
  * When the block has `fields`, renders Block Kit elements.
@@ -6648,8 +6890,7 @@ function PluginBlockModal({ block, initialValues, onClose, onInsert }) {
6648
6890
  const inputRef = React.useRef(null);
6649
6891
  React.useEffect(() => {
6650
6892
  if (block) {
6651
- if (initialValues) setFormValues({ ...initialValues });
6652
- else setFormValues({});
6893
+ setFormValues(buildPluginBlockFormValues(block, initialValues));
6653
6894
  if (!block.fields || block.fields.length === 0) setTimeout(() => inputRef.current?.focus(), 0);
6654
6895
  }
6655
6896
  }, [block, initialValues]);
@@ -6669,13 +6910,20 @@ function PluginBlockModal({ block, initialValues, onClose, onInsert }) {
6669
6910
  };
6670
6911
  const isEditing = !!initialValues;
6671
6912
  const hasFields = block?.fields && block.fields.length > 0;
6672
- const canSubmit = hasFields ? Object.values(formValues).some((v) => v !== void 0 && v !== null && v !== "") : typeof formValues.id === "string" && formValues.id.trim().length > 0;
6913
+ const canSubmit = hasFields ? hasPluginBlockFormData(formValues) : typeof formValues.id === "string" && formValues.id.trim().length > 0;
6914
+ const dialogSize = (() => {
6915
+ if (!hasFields) return "sm";
6916
+ const fields = block?.fields ?? [];
6917
+ if (fields.some((f) => f.type === "repeater")) return "xl";
6918
+ if (fields.length > 3) return "lg";
6919
+ return "base";
6920
+ })();
6673
6921
  return /* @__PURE__ */ jsx(Dialog.Root, {
6674
6922
  open: !!block,
6675
6923
  onOpenChange: (open) => !open && onClose(),
6676
6924
  children: /* @__PURE__ */ jsxs(Dialog, {
6677
6925
  className: "p-6",
6678
- size: "sm",
6926
+ size: dialogSize,
6679
6927
  children: [/* @__PURE__ */ jsxs("div", {
6680
6928
  className: "flex items-start justify-between gap-4 mb-4",
6681
6929
  children: [/* @__PURE__ */ jsxs(Dialog.Title, {
@@ -6702,7 +6950,7 @@ function PluginBlockModal({ block, initialValues, onClose, onInsert }) {
6702
6950
  }), /* @__PURE__ */ jsxs("form", {
6703
6951
  onSubmit: handleSubmit,
6704
6952
  children: [/* @__PURE__ */ jsx("div", {
6705
- className: "py-4 space-y-4",
6953
+ className: "py-4 space-y-4 max-h-[70vh] overflow-y-auto -mx-1 px-1",
6706
6954
  children: hasFields ? block.fields.map((field) => /* @__PURE__ */ jsx(BlockKitField, {
6707
6955
  field,
6708
6956
  pluginId: block.pluginId,
@@ -6711,6 +6959,7 @@ function PluginBlockModal({ block, initialValues, onClose, onInsert }) {
6711
6959
  }, field.action_id)) : /* @__PURE__ */ jsx(Input, {
6712
6960
  ref: inputRef,
6713
6961
  type: "url",
6962
+ className: "w-full",
6714
6963
  placeholder: block?.placeholder || "Enter URL...",
6715
6964
  value: typeof formValues.id === "string" ? formValues.id : "",
6716
6965
  onChange: (e) => handleFieldChange("id", e.target.value)
@@ -6752,6 +7001,7 @@ function BlockKitField({ field, pluginId, value, onChange }) {
6752
7001
  onChange: (e) => onChange(field.action_id, e.target.value)
6753
7002
  }) : /* @__PURE__ */ jsx(Input, {
6754
7003
  type: "text",
7004
+ className: "w-full",
6755
7005
  placeholder,
6756
7006
  value: typeof value === "string" ? value : "",
6757
7007
  onChange: (e) => onChange(field.action_id, e.target.value)
@@ -6765,6 +7015,7 @@ function BlockKitField({ field, pluginId, value, onChange }) {
6765
7015
  children: field.label
6766
7016
  }), /* @__PURE__ */ jsx(Input, {
6767
7017
  type: "number",
7018
+ className: "w-full",
6768
7019
  min,
6769
7020
  max,
6770
7021
  value: typeof value === "number" ? String(value) : "",
@@ -6789,12 +7040,252 @@ function BlockKitField({ field, pluginId, value, onChange }) {
6789
7040
  children: field.label
6790
7041
  })]
6791
7042
  });
7043
+ case "repeater": return /* @__PURE__ */ jsx(BlockKitRepeater, {
7044
+ field,
7045
+ pluginId,
7046
+ value,
7047
+ onChange
7048
+ });
7049
+ case "media_picker": return /* @__PURE__ */ jsx(BlockKitMediaPickerField, {
7050
+ actionId: field.action_id,
7051
+ label: field.label,
7052
+ placeholder: field.placeholder,
7053
+ mimeTypeFilter: field.mime_type_filter,
7054
+ value,
7055
+ onChange
7056
+ });
6792
7057
  default: return /* @__PURE__ */ jsxs("div", {
6793
7058
  className: "text-sm text-kumo-subtle",
6794
7059
  children: ["Unknown field type: ", field.type]
6795
7060
  });
6796
7061
  }
6797
7062
  }
7063
+ function ensureKeys(items) {
7064
+ return items.map((item, i) => {
7065
+ const obj = typeof item === "object" && item !== null ? item : {};
7066
+ return {
7067
+ ...obj,
7068
+ _key: obj._key || `item-${i}-${Date.now()}`
7069
+ };
7070
+ });
7071
+ }
7072
+ function stripKeys(items) {
7073
+ return items.map(({ _key, ...rest }) => rest);
7074
+ }
7075
+ function BlockKitRepeater({ field, pluginId, value, onChange }) {
7076
+ const { _: _t2 } = useLingui();
7077
+ const rawItems = React.useMemo(() => Array.isArray(value) ? value : [], [value]);
7078
+ const [items, setItems] = React.useState(() => ensureKeys(rawItems));
7079
+ const [expanded, setExpanded] = React.useState(/* @__PURE__ */ new Set());
7080
+ const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
7081
+ const lastEmittedRef = React.useRef(null);
7082
+ React.useEffect(() => {
7083
+ if (lastEmittedRef.current === rawItems) return;
7084
+ setItems((prev) => rawItems.map((item, i) => {
7085
+ const obj = typeof item === "object" && item !== null ? item : {};
7086
+ const existingKey = obj._key || prev[i]?._key;
7087
+ return {
7088
+ ...obj,
7089
+ _key: existingKey || `item-${i}-${Date.now()}`
7090
+ };
7091
+ }));
7092
+ }, [rawItems]);
7093
+ const minItems = field.min_items ?? 0;
7094
+ const maxItems = field.max_items;
7095
+ const canAdd = maxItems === void 0 || items.length < maxItems;
7096
+ const canRemove = items.length > minItems;
7097
+ const addButtonLabel = field.item_label ? _t2({
7098
+ id: "3nLdaX",
7099
+ message: "Add {0}",
7100
+ values: { 0: field.item_label }
7101
+ }) : _t2({
7102
+ id: "NQ8llq",
7103
+ message: "Add item"
7104
+ });
7105
+ const emit = (next) => {
7106
+ setItems(next);
7107
+ const stripped = stripKeys(next);
7108
+ lastEmittedRef.current = stripped;
7109
+ onChange(field.action_id, stripped);
7110
+ };
7111
+ const handleAdd = () => {
7112
+ if (!canAdd) return;
7113
+ const newItem = { _key: `item-${Date.now()}-${Math.random().toString(36).slice(2, 7)}` };
7114
+ for (const sf of field.fields) switch (sf.type) {
7115
+ case "toggle":
7116
+ newItem[sf.action_id] = false;
7117
+ break;
7118
+ case "number_input":
7119
+ newItem[sf.action_id] = void 0;
7120
+ break;
7121
+ default: newItem[sf.action_id] = "";
7122
+ }
7123
+ setExpanded((prev) => {
7124
+ const next = new Set(prev);
7125
+ next.add(newItem._key);
7126
+ return next;
7127
+ });
7128
+ emit([...items, newItem]);
7129
+ };
7130
+ const handleRemove = (key) => {
7131
+ if (!canRemove) return;
7132
+ setExpanded((prev) => {
7133
+ if (!prev.has(key)) return prev;
7134
+ const next = new Set(prev);
7135
+ next.delete(key);
7136
+ return next;
7137
+ });
7138
+ emit(items.filter((it) => it._key !== key));
7139
+ };
7140
+ const handleItemChange = (key, subActionId, subValue) => {
7141
+ emit(items.map((it) => it._key === key ? {
7142
+ ...it,
7143
+ [subActionId]: subValue
7144
+ } : it));
7145
+ };
7146
+ const handleDragEnd = (event) => {
7147
+ const { active, over } = event;
7148
+ if (!over || active.id === over.id) return;
7149
+ const oldIndex = items.findIndex((it) => it._key === active.id);
7150
+ const newIndex = items.findIndex((it) => it._key === over.id);
7151
+ if (oldIndex === -1 || newIndex === -1) return;
7152
+ emit(arrayMove(items, oldIndex, newIndex));
7153
+ };
7154
+ const toggleExpanded = (key) => {
7155
+ setExpanded((prev) => {
7156
+ const next = new Set(prev);
7157
+ if (next.has(key)) next.delete(key);
7158
+ else next.add(key);
7159
+ return next;
7160
+ });
7161
+ };
7162
+ return /* @__PURE__ */ jsxs("div", {
7163
+ className: "space-y-2",
7164
+ children: [/* @__PURE__ */ jsxs("div", {
7165
+ className: "flex items-center justify-between",
7166
+ children: [/* @__PURE__ */ jsxs("label", {
7167
+ className: "text-sm font-medium",
7168
+ children: [field.label, items.length > 0 && /* @__PURE__ */ jsxs("span", {
7169
+ className: "ms-2 text-kumo-subtle font-normal",
7170
+ children: [
7171
+ "(",
7172
+ items.length,
7173
+ ")"
7174
+ ]
7175
+ })]
7176
+ }), canAdd && /* @__PURE__ */ jsx(Button, {
7177
+ variant: "outline",
7178
+ size: "sm",
7179
+ icon: /* @__PURE__ */ jsx(Plus, {}),
7180
+ onClick: handleAdd,
7181
+ type: "button",
7182
+ children: addButtonLabel
7183
+ })]
7184
+ }), items.length === 0 ? /* @__PURE__ */ jsxs("div", {
7185
+ className: "border-2 border-dashed rounded-lg p-6 text-center text-kumo-subtle",
7186
+ children: [/* @__PURE__ */ jsx("p", {
7187
+ className: "text-sm",
7188
+ children: _t2({
7189
+ id: "bWmJ0A",
7190
+ message: "No items yet"
7191
+ })
7192
+ }), canAdd && /* @__PURE__ */ jsx(Button, {
7193
+ variant: "outline",
7194
+ size: "sm",
7195
+ className: "mt-2",
7196
+ icon: /* @__PURE__ */ jsx(Plus, {}),
7197
+ onClick: handleAdd,
7198
+ type: "button",
7199
+ children: addButtonLabel
7200
+ })]
7201
+ }) : /* @__PURE__ */ jsx(DndContext, {
7202
+ sensors,
7203
+ collisionDetection: closestCenter,
7204
+ onDragEnd: handleDragEnd,
7205
+ children: /* @__PURE__ */ jsx(SortableContext, {
7206
+ items: items.map((it) => it._key),
7207
+ strategy: verticalListSortingStrategy,
7208
+ children: /* @__PURE__ */ jsx("div", {
7209
+ className: "space-y-2",
7210
+ children: items.map((item, index) => /* @__PURE__ */ jsx(BlockKitRepeaterItem, {
7211
+ item,
7212
+ index,
7213
+ fields: field.fields,
7214
+ pluginId,
7215
+ isCollapsed: !expanded.has(item._key),
7216
+ onToggleCollapse: () => toggleExpanded(item._key),
7217
+ onRemove: canRemove ? () => handleRemove(item._key) : void 0,
7218
+ onChange: (subActionId, v) => handleItemChange(item._key, subActionId, v)
7219
+ }, item._key))
7220
+ })
7221
+ })
7222
+ })]
7223
+ });
7224
+ }
7225
+ function BlockKitRepeaterItem({ item, index, fields, pluginId, isCollapsed, onToggleCollapse, onRemove, onChange }) {
7226
+ const { _: _t3 } = useLingui();
7227
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item._key });
7228
+ const style = {
7229
+ transform: CSS.Transform.toString(transform),
7230
+ transition
7231
+ };
7232
+ const summaryField = fields.find((f) => f.type === "text_input");
7233
+ const summaryLabel = (summaryField && typeof item[summaryField.action_id] === "string" ? item[summaryField.action_id] : "").trim() || _t3({
7234
+ id: "y/5CE8",
7235
+ message: "Item {0}",
7236
+ values: { 0: index + 1 }
7237
+ });
7238
+ return /* @__PURE__ */ jsxs("div", {
7239
+ ref: setNodeRef,
7240
+ style,
7241
+ className: cn("border border-kumo-line rounded-lg bg-kumo-base", isDragging && "opacity-50 ring-2 ring-kumo-brand"),
7242
+ children: [/* @__PURE__ */ jsxs("div", {
7243
+ className: "flex items-center gap-2 px-3 py-2 border-b border-kumo-line",
7244
+ children: [
7245
+ /* @__PURE__ */ jsx("span", {
7246
+ className: "inline-flex h-4 w-4 text-kumo-subtle cursor-grab shrink-0",
7247
+ "aria-label": _t3({
7248
+ id: "KGi3u9",
7249
+ message: "Drag to reorder"
7250
+ }),
7251
+ ...attributes,
7252
+ ...listeners,
7253
+ children: /* @__PURE__ */ jsx(DotsSixVertical, { className: "h-4 w-4" })
7254
+ }),
7255
+ /* @__PURE__ */ jsxs("button", {
7256
+ type: "button",
7257
+ className: "flex items-center gap-2 flex-1 min-w-0 text-start cursor-pointer",
7258
+ onClick: onToggleCollapse,
7259
+ "aria-expanded": !isCollapsed,
7260
+ children: [isCollapsed ? /* @__PURE__ */ jsx(CaretNext, { className: "h-4 w-4 text-kumo-subtle shrink-0" }) : /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4 text-kumo-subtle shrink-0" }), /* @__PURE__ */ jsx("span", {
7261
+ className: "text-sm font-medium flex-1 truncate",
7262
+ children: summaryLabel
7263
+ })]
7264
+ }),
7265
+ onRemove && /* @__PURE__ */ jsx(Button, {
7266
+ variant: "ghost",
7267
+ shape: "square",
7268
+ type: "button",
7269
+ onClick: onRemove,
7270
+ "aria-label": _t3({
7271
+ id: "mU3j1b",
7272
+ message: "Remove item {0}",
7273
+ values: { 0: index + 1 }
7274
+ }),
7275
+ children: /* @__PURE__ */ jsx(Trash, { className: "h-3.5 w-3.5 text-kumo-danger" })
7276
+ })
7277
+ ]
7278
+ }), !isCollapsed && /* @__PURE__ */ jsx("div", {
7279
+ className: "p-3 space-y-3",
7280
+ children: fields.map((sf) => /* @__PURE__ */ jsx(BlockKitField, {
7281
+ field: sf,
7282
+ pluginId,
7283
+ value: item[sf.action_id],
7284
+ onChange: (actionId, v) => onChange(actionId, v)
7285
+ }, sf.action_id))
7286
+ })]
7287
+ });
7288
+ }
6798
7289
  /**
6799
7290
  * Select field that supports loading options dynamically via `optionsRoute`.
6800
7291
  * When `optionsRoute` is set, fetches `{ items: [{ id, name }] }` from the plugin route.
@@ -6893,7 +7384,7 @@ function EditorFooter({ editor }) {
6893
7384
  * Portable Text Editor Component
6894
7385
  */
6895
7386
  function PortableTextEditor({ value, onChange, placeholder = "Start writing...", className, editable = true, "aria-labelledby": ariaLabelledby, pluginBlocks = [], focusMode: controlledFocusMode, onFocusModeChange, onEditorReady, minimal = false, onBlockSidebarOpen, onBlockSidebarClose }) {
6896
- const { _: _t2 } = useLingui();
7387
+ const { _: _t4 } = useLingui();
6897
7388
  const onChangeRef = React.useRef(onChange);
6898
7389
  React.useEffect(() => {
6899
7390
  onChangeRef.current = onChange;
@@ -6977,14 +7468,14 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
6977
7468
  for (const block of pluginBlocks) cmds.push({
6978
7469
  id: `plugin-${block.pluginId}-${block.type}`,
6979
7470
  title: block.label,
6980
- description: block.description ?? _t2({
7471
+ description: block.description ?? _t4({
6981
7472
  id: "g0zqOw",
6982
7473
  message: "Embed a {0}",
6983
7474
  values: { 0: block.label }
6984
7475
  }),
6985
7476
  icon: resolveIcon(block.icon),
6986
7477
  aliases: [block.type],
6987
- category: {
7478
+ category: block.category ?? {
6988
7479
  id: "aTofd0",
6989
7480
  message: "Embeds"
6990
7481
  },
@@ -6994,7 +7485,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
6994
7485
  }
6995
7486
  });
6996
7487
  return cmds;
6997
- }, [pluginBlocks, _t2]);
7488
+ }, [pluginBlocks, _t4]);
6998
7489
  const filterCommandsRef = React.useRef((_q) => []);
6999
7490
  filterCommandsRef.current = (query) => {
7000
7491
  if (!query) return slashCommands;
@@ -7002,8 +7493,8 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
7002
7493
  const titleMatches = [];
7003
7494
  const otherMatches = [];
7004
7495
  for (const item of slashCommands) {
7005
- const titleStr = typeof item.title === "string" ? item.title : _t2(item.title);
7006
- const descStr = typeof item.description === "string" ? item.description : _t2(item.description);
7496
+ const titleStr = typeof item.title === "string" ? item.title : _t4(item.title);
7497
+ const descStr = typeof item.description === "string" ? item.description : _t4(item.description);
7007
7498
  if (titleStr.toLowerCase().includes(searchText)) titleMatches.push(item);
7008
7499
  else if (descStr.toLowerCase().includes(searchText) || item.aliases?.some((alias) => alias.toLowerCase().includes(searchText))) otherMatches.push(item);
7009
7500
  }
@@ -7055,14 +7546,22 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
7055
7546
  content: initialContent,
7056
7547
  editable,
7057
7548
  immediatelyRender: true,
7058
- editorProps: React.useMemo(() => ({ attributes: { class: "prose prose-sm sm:prose-base dark:prose-invert max-w-none focus:outline-none min-h-[200px] p-4" } }), []),
7549
+ editorProps: React.useMemo(() => ({ attributes: {
7550
+ class: "prose prose-sm sm:prose-base dark:prose-invert max-w-none focus:outline-none min-h-[200px] p-4",
7551
+ dir: "auto"
7552
+ } }), []),
7059
7553
  onUpdate: ({ editor: updatedEditor }) => {
7060
7554
  const cb = onChangeRef.current;
7061
7555
  if (cb) cb(prosemirrorToPortableText(updatedEditor.getJSON()));
7062
7556
  }
7063
7557
  });
7064
7558
  React.useEffect(() => {
7065
- if (editor && onEditorReady) onEditorReady(editor);
7559
+ if (editor && onEditorReady) {
7560
+ onEditorReady(editor);
7561
+ return () => {
7562
+ onEditorReady(null);
7563
+ };
7564
+ }
7066
7565
  }, [editor, onEditorReady]);
7067
7566
  React.useEffect(() => {
7068
7567
  if (editor) registerPluginBlocks(editor, pluginBlocks);
@@ -8406,7 +8905,8 @@ function SeoPanel({ contentKey, seo, onChange }) {
8406
8905
  value: draft.title,
8407
8906
  onChange: (e) => {
8408
8907
  updateDraft({ title: e.target.value });
8409
- }
8908
+ },
8909
+ dir: "auto"
8410
8910
  }),
8411
8911
  /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(InputArea, {
8412
8912
  label: _t({
@@ -8425,7 +8925,8 @@ function SeoPanel({ contentKey, seo, onChange }) {
8425
8925
  onChange: (e) => {
8426
8926
  updateDraft({ description: e.target.value });
8427
8927
  },
8428
- rows: 3
8928
+ rows: 3,
8929
+ dir: "auto"
8429
8930
  }) }),
8430
8931
  /* @__PURE__ */ jsx(Input, {
8431
8932
  label: _t({
@@ -8567,7 +9068,7 @@ function CategoryCheckboxTree({ term, level = 0, selectedIds, onToggle }) {
8567
9068
  const isChecked = selectedIds.has(term.id);
8568
9069
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("label", {
8569
9070
  className: "flex items-center py-1 cursor-pointer hover:bg-kumo-tint/50 rounded px-2",
8570
- style: { marginLeft: `${level}rem` },
9071
+ style: { marginInlineStart: `${level}rem` },
8571
9072
  children: [/* @__PURE__ */ jsx("input", {
8572
9073
  type: "checkbox",
8573
9074
  checked: isChecked,
@@ -8666,7 +9167,7 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
8666
9167
  type: "button",
8667
9168
  onClick: handleCreate,
8668
9169
  disabled: isCreating,
8669
- className: "w-full text-left px-3 py-2 text-sm hover:bg-kumo-tint text-kumo-accent flex items-center gap-1 border-t",
9170
+ className: "w-full text-start px-3 py-2 text-sm hover:bg-kumo-tint text-kumo-accent flex items-center gap-1 border-t",
8670
9171
  children: [/* @__PURE__ */ jsx(Plus, { className: "w-3 h-3" }), isCreating ? "Creating..." : `Create "${trimmedInput}"`]
8671
9172
  })]
8672
9173
  })]
@@ -8917,13 +9418,6 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
8917
9418
  roleLabel: entry.roleLabel
8918
9419
  })) ?? []);
8919
9420
  const [portableTextEditor, setPortableTextEditor] = React.useState(null);
8920
- const ptEditorClaimedRef = React.useRef(false);
8921
- React.useCallback((editor) => {
8922
- if (!ptEditorClaimedRef.current) {
8923
- ptEditorClaimedRef.current = true;
8924
- setPortableTextEditor(editor);
8925
- }
8926
- }, []);
8927
9421
  const [blockSidebarPanel, setBlockSidebarPanel] = React.useState(null);
8928
9422
  const handleBlockSidebarOpen = React.useCallback((panel) => {
8929
9423
  setBlockSidebarPanel(panel);
@@ -8946,6 +9440,25 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
8946
9440
  })) ?? []
8947
9441
  }));
8948
9442
  const pendingAutosaveStateRef = React.useRef(null);
9443
+ const [previousItemId, setPreviousItemId] = React.useState(item?.id ?? null);
9444
+ if (item && item.id !== previousItemId) {
9445
+ setPreviousItemId(item.id);
9446
+ setFormData(item.data);
9447
+ setSlug(item.slug || "");
9448
+ setSlugTouched(!!item.slug);
9449
+ setStatus(item.status);
9450
+ const nextBylines = item.bylines?.map((entry) => ({
9451
+ bylineId: entry.byline.id,
9452
+ roleLabel: entry.roleLabel
9453
+ })) ?? [];
9454
+ setInternalBylines(nextBylines);
9455
+ setLastSavedData(serializeEditorState({
9456
+ data: item.data,
9457
+ slug: item.slug || "",
9458
+ bylines: nextBylines
9459
+ }));
9460
+ pendingAutosaveStateRef.current = null;
9461
+ }
8949
9462
  const itemDataString = React.useMemo(() => item ? JSON.stringify(item.data) : "", [item?.data]);
8950
9463
  React.useEffect(() => {
8951
9464
  if (item) {
@@ -9112,7 +9625,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
9112
9625
  onSubmit: handleSubmit,
9113
9626
  className: cn("space-y-6 transition-all duration-300", isDistractionFree && "fixed inset-0 z-50 bg-kumo-base p-8 overflow-auto"),
9114
9627
  children: [/* @__PURE__ */ jsxs("div", {
9115
- className: cn("flex flex-wrap items-center justify-between gap-y-2", isDistractionFree && "opacity-0 hover:opacity-100 transition-opacity duration-200 fixed top-0 start-0 end-0 bg-kumo-base/95 backdrop-blur p-4 z-10"),
9628
+ className: cn("flex flex-wrap items-center justify-between gap-y-2", !isDistractionFree && "sticky top-0 z-30 -mx-6 -mt-6 px-6 pt-6 pb-3 mb-3 border-b border-kumo-line bg-kumo-base/95 supports-[backdrop-filter]:bg-kumo-base/80 backdrop-blur", isDistractionFree && "opacity-0 hover:opacity-100 transition-opacity duration-200 fixed top-0 start-0 end-0 bg-kumo-base/95 backdrop-blur p-4 z-10"),
9116
9629
  children: [/* @__PURE__ */ jsxs("div", {
9117
9630
  className: "flex items-center space-x-4",
9118
9631
  children: [
@@ -9129,7 +9642,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
9129
9642
  variant: "ghost",
9130
9643
  shape: "square"
9131
9644
  }),
9132
- children: /* @__PURE__ */ jsx(ArrowLeft, {
9645
+ children: /* @__PURE__ */ jsx(ArrowPrev, {
9133
9646
  className: "h-5 w-5",
9134
9647
  "aria-hidden": "true"
9135
9648
  })
@@ -9324,6 +9837,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
9324
9837
  children: /* @__PURE__ */ jsx("div", {
9325
9838
  className: "space-y-4",
9326
9839
  children: Object.entries(fields).map(([name, field]) => {
9840
+ const fieldKey = `${name}:${item?.id ?? "new"}`;
9327
9841
  const fieldEl = /* @__PURE__ */ jsx(FieldRenderer, {
9328
9842
  name,
9329
9843
  field,
@@ -9335,14 +9849,14 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
9335
9849
  onBlockSidebarOpen: field.kind === "portableText" ? handleBlockSidebarOpen : void 0,
9336
9850
  onBlockSidebarClose: field.kind === "portableText" ? handleBlockSidebarClose : void 0,
9337
9851
  manifest
9338
- }, name);
9852
+ }, fieldKey);
9339
9853
  if (name === "featured_image" && field.kind === "image" && hasSeo && !isNew && onSeoChange) return /* @__PURE__ */ jsxs("div", {
9340
9854
  className: "grid grid-cols-1 gap-6 md:grid-cols-2",
9341
9855
  children: [/* @__PURE__ */ jsx("div", { children: fieldEl }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(SeoImageField, {
9342
9856
  seo: item?.seo,
9343
9857
  onChange: onSeoChange
9344
9858
  }) })]
9345
- }, `${name}-with-seo`);
9859
+ }, `${fieldKey}-with-seo`);
9346
9860
  return fieldEl;
9347
9861
  })
9348
9862
  })
@@ -9751,6 +10265,7 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
9751
10265
  value: typeof value === "string" ? value : "",
9752
10266
  onChange: (e) => handleChange(e.target.value),
9753
10267
  required: field.required,
10268
+ dir: "auto",
9754
10269
  className: minimal ? "border-0 bg-transparent px-0 text-lg font-medium focus-visible:ring-0 focus-visible:ring-offset-0" : void 0
9755
10270
  });
9756
10271
  case "number": return /* @__PURE__ */ jsx(Input, {
@@ -9801,34 +10316,37 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
9801
10316
  value: typeof value === "string" ? value : "",
9802
10317
  onChange: (e) => handleChange(e.target.value),
9803
10318
  rows: 10,
10319
+ dir: "auto",
9804
10320
  placeholder: _t2({
9805
10321
  id: "dPFFy+",
9806
10322
  message: "Enter markdown content..."
9807
10323
  })
9808
10324
  });
9809
10325
  case "select": {
10326
+ const selectOptions = Array.isArray(field.options) ? field.options : [];
9810
10327
  const selectItems = {};
9811
- for (const opt of field.options ?? []) selectItems[opt.value] = opt.label;
10328
+ for (const opt of selectOptions) selectItems[opt.value] = opt.label;
9812
10329
  return /* @__PURE__ */ jsx(Select, {
9813
10330
  id,
9814
10331
  label,
9815
10332
  value: typeof value === "string" ? value : "",
9816
10333
  onValueChange: (v) => handleChange(v ?? ""),
9817
10334
  items: selectItems,
9818
- children: field.options?.map((opt) => /* @__PURE__ */ jsx(Select.Option, {
10335
+ children: selectOptions.map((opt) => /* @__PURE__ */ jsx(Select.Option, {
9819
10336
  value: opt.value,
9820
10337
  children: opt.label
9821
10338
  }, opt.value))
9822
10339
  });
9823
10340
  }
9824
10341
  case "multiSelect": {
10342
+ const multiSelectOptions = Array.isArray(field.options) ? field.options : [];
9825
10343
  const selected = Array.isArray(value) ? value : [];
9826
10344
  return /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx(Label, {
9827
10345
  className: labelClass,
9828
10346
  children: label
9829
10347
  }), /* @__PURE__ */ jsx("div", {
9830
10348
  className: "mt-2 flex flex-wrap gap-x-4 gap-y-2",
9831
- children: field.options?.map((opt) => {
10349
+ children: multiSelectOptions.map((opt) => {
9832
10350
  const isChecked = selected.includes(opt.value);
9833
10351
  return /* @__PURE__ */ jsx(Checkbox, {
9834
10352
  label: opt.label,
@@ -9844,8 +10362,8 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
9844
10362
  label,
9845
10363
  id,
9846
10364
  type: "datetime-local",
9847
- value: typeof value === "string" ? value : "",
9848
- onChange: (e) => handleChange(e.target.value),
10365
+ value: toDatetimeLocalInputValue(value),
10366
+ onChange: (e) => handleChange(fromDatetimeLocalInputValue(e.target.value)),
9849
10367
  required: field.required
9850
10368
  });
9851
10369
  case "image": {
@@ -9897,7 +10415,8 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
9897
10415
  id,
9898
10416
  value: typeof value === "string" ? value : "",
9899
10417
  onChange: (e) => handleChange(e.target.value),
9900
- required: field.required
10418
+ required: field.required,
10419
+ dir: "auto"
9901
10420
  });
9902
10421
  }
9903
10422
  }
@@ -10537,7 +11056,7 @@ function getItemTitle$1(item) {
10537
11056
  /**
10538
11057
  * Content list view with table display and trash tab
10539
11058
  */
10540
- function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n: i18n$1, activeLocale, onLocaleChange, urlPattern }) {
11059
+ function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n: i18n$1, activeLocale, onLocaleChange, urlPattern, sort, onSortChange }) {
10541
11060
  const { _: _t } = useLingui();
10542
11061
  const [activeTab, setActiveTab] = React.useState("all");
10543
11062
  const [searchQuery, setSearchQuery] = React.useState("");
@@ -10653,34 +11172,38 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
10653
11172
  children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
10654
11173
  className: "border-b bg-kumo-tint/50",
10655
11174
  children: [
10656
- /* @__PURE__ */ jsx("th", {
10657
- scope: "col",
10658
- className: "px-4 py-3 text-start text-sm font-medium",
10659
- children: _t({
11175
+ /* @__PURE__ */ jsx(SortableTh, {
11176
+ field: "title",
11177
+ sort,
11178
+ onSortChange,
11179
+ label: _t({
10660
11180
  id: "MHrjPM",
10661
11181
  message: "Title"
10662
11182
  })
10663
11183
  }),
10664
- /* @__PURE__ */ jsx("th", {
10665
- scope: "col",
10666
- className: "px-4 py-3 text-start text-sm font-medium",
10667
- children: _t({
11184
+ /* @__PURE__ */ jsx(SortableTh, {
11185
+ field: "status",
11186
+ sort,
11187
+ onSortChange,
11188
+ label: _t({
10668
11189
  id: "uAQUqI",
10669
11190
  message: "Status"
10670
11191
  })
10671
11192
  }),
10672
- i18n$1 && /* @__PURE__ */ jsx("th", {
10673
- scope: "col",
10674
- className: "px-4 py-3 text-start text-sm font-medium",
10675
- children: _t({
11193
+ i18n$1 && /* @__PURE__ */ jsx(SortableTh, {
11194
+ field: "locale",
11195
+ sort,
11196
+ onSortChange,
11197
+ label: _t({
10676
11198
  id: "8NbHF7",
10677
11199
  message: "Locale"
10678
11200
  })
10679
11201
  }),
10680
- /* @__PURE__ */ jsx("th", {
10681
- scope: "col",
10682
- className: "px-4 py-3 text-start text-sm font-medium",
10683
- children: _t({
11202
+ /* @__PURE__ */ jsx(SortableTh, {
11203
+ field: "updatedAt",
11204
+ sort,
11205
+ onSortChange,
11206
+ label: _t({
10684
11207
  id: "mYGY3B",
10685
11208
  message: "Date"
10686
11209
  })
@@ -10694,7 +11217,17 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
10694
11217
  })
10695
11218
  })
10696
11219
  ]
10697
- }) }), /* @__PURE__ */ jsx("tbody", { children: items.length === 0 && !isLoading ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
11220
+ }) }), /* @__PURE__ */ jsx("tbody", { children: isLoading && items.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
11221
+ colSpan: i18n$1 ? 5 : 4,
11222
+ className: "px-4 py-8 text-center text-kumo-subtle",
11223
+ children: /* @__PURE__ */ jsxs("span", {
11224
+ className: "inline-flex items-center gap-2",
11225
+ children: [/* @__PURE__ */ jsx(Loader, { size: "sm" }), _t({
11226
+ id: "Z3FXyt",
11227
+ message: "Loading..."
11228
+ })]
11229
+ })
11230
+ }) }) : items.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
10698
11231
  colSpan: i18n$1 ? 5 : 4,
10699
11232
  className: "px-4 py-8 text-center text-kumo-subtle",
10700
11233
  children: [
@@ -10765,7 +11298,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
10765
11298
  id: "aHKcKc",
10766
11299
  message: "Previous page"
10767
11300
  }),
10768
- children: /* @__PURE__ */ jsx(CaretLeft, {
11301
+ children: /* @__PURE__ */ jsx(CaretPrev, {
10769
11302
  className: "h-4 w-4",
10770
11303
  "aria-hidden": "true"
10771
11304
  })
@@ -10787,7 +11320,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
10787
11320
  id: "/dqduX",
10788
11321
  message: "Next page"
10789
11322
  }),
10790
- children: /* @__PURE__ */ jsx(CaretRight, {
11323
+ children: /* @__PURE__ */ jsx(CaretNext, {
10791
11324
  className: "h-4 w-4",
10792
11325
  "aria-hidden": "true"
10793
11326
  })
@@ -10842,7 +11375,17 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
10842
11375
  })
10843
11376
  })
10844
11377
  ]
10845
- }) }), /* @__PURE__ */ jsx("tbody", { children: trashedItems.length === 0 && !isTrashedLoading ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
11378
+ }) }), /* @__PURE__ */ jsx("tbody", { children: isTrashedLoading && trashedItems.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
11379
+ colSpan: 3,
11380
+ className: "px-4 py-8 text-center text-kumo-subtle",
11381
+ children: /* @__PURE__ */ jsxs("span", {
11382
+ className: "inline-flex items-center gap-2",
11383
+ children: [/* @__PURE__ */ jsx(Loader, { size: "sm" }), _t({
11384
+ id: "Z3FXyt",
11385
+ message: "Loading..."
11386
+ })]
11387
+ })
11388
+ }) }) : trashedItems.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
10846
11389
  colSpan: 3,
10847
11390
  className: "px-4 py-8 text-center text-kumo-subtle",
10848
11391
  children: _t({
@@ -10873,6 +11416,52 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
10873
11416
  ]
10874
11417
  });
10875
11418
  }
11419
+ /**
11420
+ * Table header that doubles as a sort control when the parent opted in by
11421
+ * passing `onSortChange`. When no callback is provided we fall back to a
11422
+ * plain `<th>` so legacy callers (and screen readers) see exactly the same
11423
+ * markup as before this change.
11424
+ *
11425
+ * The button's accessible name is just the column label — the sort state
11426
+ * is conveyed via `aria-sort` on the <th>, which screen readers announce
11427
+ * automatically. Adding a verbose aria-label would make each header re-read
11428
+ * the sort instruction on every focus, which is noisy.
11429
+ */
11430
+ function SortableTh({ field, sort, onSortChange, label }) {
11431
+ const isActive = sort?.field === field;
11432
+ const direction = isActive ? sort?.direction : void 0;
11433
+ if (!onSortChange) return /* @__PURE__ */ jsx("th", {
11434
+ scope: "col",
11435
+ className: "px-4 py-3 text-start text-sm font-medium",
11436
+ children: label
11437
+ });
11438
+ const ariaSort = isActive ? direction === "asc" ? "ascending" : "descending" : "none";
11439
+ const handleClick = () => {
11440
+ if (isActive) onSortChange({
11441
+ field,
11442
+ direction: direction === "asc" ? "desc" : "asc"
11443
+ });
11444
+ else onSortChange({
11445
+ field,
11446
+ direction: "desc"
11447
+ });
11448
+ };
11449
+ const Icon = isActive ? direction === "asc" ? CaretUp : CaretDown : CaretUpDown;
11450
+ return /* @__PURE__ */ jsx("th", {
11451
+ scope: "col",
11452
+ "aria-sort": ariaSort,
11453
+ className: "px-4 py-3 text-start text-sm font-medium",
11454
+ children: /* @__PURE__ */ jsxs("button", {
11455
+ type: "button",
11456
+ onClick: handleClick,
11457
+ className: cn("inline-flex items-center gap-1 rounded hover:text-kumo-brand", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-kumo-brand", isActive ? "text-kumo-fg" : "text-kumo-subtle"),
11458
+ children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx(Icon, {
11459
+ className: "h-3 w-3",
11460
+ "aria-hidden": "true"
11461
+ })]
11462
+ })
11463
+ });
11464
+ }
10876
11465
  function ContentListItem({ item, collection, onDelete, onDuplicate, showLocale, urlPattern }) {
10877
11466
  const { _: _t2 } = useLingui();
10878
11467
  const title = getItemTitle$1(item);
@@ -11155,6 +11744,37 @@ function StatusBadge({ status, hasPendingChanges }) {
11155
11744
  });
11156
11745
  }
11157
11746
 
11747
+ //#endregion
11748
+ //#region src/components/EditorHeader.tsx
11749
+ /**
11750
+ * Sticky editor header with consistent placement of save / primary actions.
11751
+ *
11752
+ * Usage:
11753
+ *
11754
+ * <EditorHeader
11755
+ * leading={<BackLink />}
11756
+ * actions={<SaveButton ... />}
11757
+ * >
11758
+ * <h1 className="text-2xl font-bold">{title}</h1>
11759
+ * </EditorHeader>
11760
+ */
11761
+ function EditorHeader({ leading, children, actions, sticky = true, className }) {
11762
+ return /* @__PURE__ */ jsxs("div", {
11763
+ "data-editor-header": true,
11764
+ className: cn(sticky && "sticky top-0 z-30 -mx-6 -mt-6 px-6 pt-6", sticky && "bg-kumo-base/95 supports-[backdrop-filter]:bg-kumo-base/80 backdrop-blur", sticky && "pb-3 mb-3 border-b border-kumo-line", "flex flex-wrap items-center justify-between gap-y-2 gap-x-4", className),
11765
+ children: [/* @__PURE__ */ jsxs("div", {
11766
+ className: "flex items-center gap-4 min-w-0",
11767
+ children: [leading, /* @__PURE__ */ jsx("div", {
11768
+ className: "min-w-0",
11769
+ children
11770
+ })]
11771
+ }), actions && /* @__PURE__ */ jsx("div", {
11772
+ className: "flex items-center gap-2 flex-wrap",
11773
+ children: actions
11774
+ })]
11775
+ });
11776
+ }
11777
+
11158
11778
  //#endregion
11159
11779
  //#region src/components/FieldEditor.tsx
11160
11780
  const SLUG_INVALID_CHARS_REGEX = /[^a-z0-9]+/g;
@@ -12094,7 +12714,7 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
12094
12714
  const [labelSingular, setLabelSingular] = React.useState(collection?.labelSingular ?? "");
12095
12715
  const [description, setDescription] = React.useState(collection?.description ?? "");
12096
12716
  const [urlPattern, setUrlPattern] = React.useState(collection?.urlPattern ?? "");
12097
- const [supports, setSupports] = React.useState((collection?.supports ?? ["drafts"]).filter((s) => s !== "seo"));
12717
+ const [supports, setSupports] = React.useState((collection?.supports ?? ["drafts", "revisions"]).filter((s) => s !== "seo"));
12098
12718
  const [hasSeo, setHasSeo] = React.useState(collection?.hasSeo ?? false);
12099
12719
  const [commentsEnabled, setCommentsEnabled] = React.useState(collection?.commentsEnabled ?? false);
12100
12720
  const [commentsModeration, setCommentsModeration] = React.useState(collection?.commentsModeration ?? "first_time");
@@ -12193,30 +12813,43 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
12193
12813
  return /* @__PURE__ */ jsxs("div", {
12194
12814
  className: "space-y-6",
12195
12815
  children: [
12196
- /* @__PURE__ */ jsxs("div", {
12197
- className: "flex items-center space-x-4",
12198
- children: [/* @__PURE__ */ jsx(Link$1, {
12816
+ /* @__PURE__ */ jsxs(EditorHeader, {
12817
+ leading: /* @__PURE__ */ jsx(Link$1, {
12199
12818
  to: "/content-types",
12200
- "aria-label": "Back to Content Types",
12819
+ "aria-label": _t({
12820
+ id: "oqi/nm",
12821
+ message: "Back to Content Types"
12822
+ }),
12201
12823
  className: buttonVariants({
12202
12824
  variant: "ghost",
12203
12825
  shape: "square"
12204
12826
  }),
12205
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-5 w-5" })
12206
- }), /* @__PURE__ */ jsxs("div", {
12207
- className: "flex-1",
12208
- children: [/* @__PURE__ */ jsx("h1", {
12209
- className: "text-2xl font-bold",
12210
- children: isNew ? "New Content Type" : collection?.label
12211
- }), !isNew && /* @__PURE__ */ jsxs("p", {
12212
- className: "text-kumo-subtle text-sm",
12213
- children: [/* @__PURE__ */ jsx("code", {
12214
- className: "bg-kumo-tint px-1.5 py-0.5 rounded",
12215
- children: collection?.slug
12216
- }), isFromCode && /* @__PURE__ */ jsx("span", {
12217
- className: "ms-2 text-purple-600 dark:text-purple-400",
12218
- children: "Defined in code"
12219
- })]
12827
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-5 w-5" })
12828
+ }),
12829
+ actions: !isFromCode && !isNew ? /* @__PURE__ */ jsx(SaveButton, {
12830
+ type: "submit",
12831
+ form: "content-type-editor-form",
12832
+ isDirty: !!hasChanges,
12833
+ isSaving: !!isSaving,
12834
+ disabled: !urlPatternValid
12835
+ }) : null,
12836
+ children: [/* @__PURE__ */ jsx("h1", {
12837
+ className: "text-2xl font-bold truncate",
12838
+ children: isNew ? _t({
12839
+ id: "NFFxa9",
12840
+ message: "New Content Type"
12841
+ }) : collection?.label
12842
+ }), !isNew && /* @__PURE__ */ jsxs("p", {
12843
+ className: "text-kumo-subtle text-sm",
12844
+ children: [/* @__PURE__ */ jsx("code", {
12845
+ className: "bg-kumo-tint px-1.5 py-0.5 rounded",
12846
+ children: collection?.slug
12847
+ }), isFromCode && /* @__PURE__ */ jsx("span", {
12848
+ className: "ms-2 text-purple-600 dark:text-purple-400",
12849
+ children: _t({
12850
+ id: "7tGkih",
12851
+ message: "Defined in code"
12852
+ })
12220
12853
  })]
12221
12854
  })]
12222
12855
  }),
@@ -12235,6 +12868,7 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
12235
12868
  children: [/* @__PURE__ */ jsx("div", {
12236
12869
  className: "lg:col-span-1",
12237
12870
  children: /* @__PURE__ */ jsxs("form", {
12871
+ id: "content-type-editor-form",
12238
12872
  onSubmit: handleSubmit,
12239
12873
  className: "space-y-4",
12240
12874
  children: [
@@ -14742,54 +15376,13 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
14742
15376
  * It's a standalone page for authentication.
14743
15377
  *
14744
15378
  * Supports:
14745
- * - Passkey authentication (primary)
14746
- * - OAuth (GitHub, Google) when configured
15379
+ * - Passkey authentication (always available)
15380
+ * - Pluggable auth providers (AT Protocol, GitHub, Google, etc.) when configured
14747
15381
  * - Magic link (email) when configured
14748
15382
  *
14749
15383
  * When external auth (e.g., Cloudflare Access) is configured, this page
14750
15384
  * redirects to the admin dashboard since authentication is handled externally.
14751
15385
  */
14752
- function GitHubIcon({ className }) {
14753
- return /* @__PURE__ */ jsx("svg", {
14754
- className,
14755
- viewBox: "0 0 24 24",
14756
- fill: "currentColor",
14757
- children: /* @__PURE__ */ jsx("path", { d: "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" })
14758
- });
14759
- }
14760
- function GoogleIcon({ className }) {
14761
- return /* @__PURE__ */ jsxs("svg", {
14762
- className,
14763
- viewBox: "0 0 24 24",
14764
- children: [
14765
- /* @__PURE__ */ jsx("path", {
14766
- fill: "#4285F4",
14767
- d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
14768
- }),
14769
- /* @__PURE__ */ jsx("path", {
14770
- fill: "#34A853",
14771
- d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
14772
- }),
14773
- /* @__PURE__ */ jsx("path", {
14774
- fill: "#FBBC05",
14775
- d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
14776
- }),
14777
- /* @__PURE__ */ jsx("path", {
14778
- fill: "#EA4335",
14779
- d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
14780
- })
14781
- ]
14782
- });
14783
- }
14784
- const OAUTH_PROVIDERS = [{
14785
- id: "github",
14786
- name: "GitHub",
14787
- icon: /* @__PURE__ */ jsx(GitHubIcon, { className: "h-5 w-5" })
14788
- }, {
14789
- id: "google",
14790
- name: "Google",
14791
- icon: /* @__PURE__ */ jsx(GoogleIcon, { className: "h-5 w-5" })
14792
- }];
14793
15386
  function MagicLinkForm({ onBack }) {
14794
15387
  const { _: _t } = useLingui();
14795
15388
  const [email, setEmail] = React.useState("");
@@ -14930,22 +15523,21 @@ function MagicLinkForm({ onBack }) {
14930
15523
  ]
14931
15524
  });
14932
15525
  }
14933
- function handleOAuthClick(providerId) {
14934
- window.location.href = `/_emdash/api/auth/oauth/${providerId}`;
14935
- }
14936
15526
  function LoginPage({ redirectUrl = "/_emdash/admin" }) {
14937
15527
  const safeRedirectUrl = sanitizeRedirectUrl(redirectUrl);
14938
15528
  const { _: _t2 } = useLingui();
14939
15529
  const { locale, setLocale } = useLocale();
14940
15530
  const [method, setMethod] = React.useState("passkey");
14941
15531
  const [urlError, setUrlError] = React.useState(null);
14942
- const { data: manifest, isLoading: manifestLoading } = useQuery({
14943
- queryKey: ["manifest"],
14944
- queryFn: fetchManifest
15532
+ const [activeProvider, setActiveProvider] = React.useState(null);
15533
+ const authProviderList = useAuthProviderList();
15534
+ const { data: authInfo, isLoading: authModeLoading } = useQuery({
15535
+ queryKey: ["authMode"],
15536
+ queryFn: fetchAuthMode
14945
15537
  });
14946
15538
  React.useEffect(() => {
14947
- if (manifest?.authMode && manifest.authMode !== "passkey") window.location.href = safeRedirectUrl;
14948
- }, [manifest, safeRedirectUrl]);
15539
+ if (authInfo?.authMode && authInfo.authMode !== "passkey") window.location.href = safeRedirectUrl;
15540
+ }, [authInfo, safeRedirectUrl]);
14949
15541
  React.useEffect(() => {
14950
15542
  const params = new URLSearchParams(window.location.search);
14951
15543
  const error = params.get("error");
@@ -14962,15 +15554,12 @@ function LoginPage({ redirectUrl = "/_emdash/admin" }) {
14962
15554
  const handleSuccess = () => {
14963
15555
  window.location.href = safeRedirectUrl;
14964
15556
  };
14965
- if (manifestLoading || manifest?.authMode && manifest.authMode !== "passkey") return /* @__PURE__ */ jsx("div", {
15557
+ const buttonProviders = authProviderList.filter((p) => p.LoginButton);
15558
+ if (authModeLoading || authInfo?.authMode && authInfo.authMode !== "passkey") return /* @__PURE__ */ jsx("div", {
14966
15559
  className: "min-h-screen flex items-center justify-center bg-kumo-base p-4",
14967
15560
  children: /* @__PURE__ */ jsxs("div", {
14968
15561
  className: "flex flex-col items-center",
14969
- children: [/* @__PURE__ */ jsx(BrandLogo, {
14970
- logoUrl: manifest?.admin?.logo,
14971
- siteName: manifest?.admin?.siteName,
14972
- className: "h-10 mb-4"
14973
- }), /* @__PURE__ */ jsx(Loader, {})]
15562
+ children: [/* @__PURE__ */ jsx(BrandLogo, { className: "h-10 mb-4" }), /* @__PURE__ */ jsx(Loader, {})]
14974
15563
  })
14975
15564
  });
14976
15565
  return /* @__PURE__ */ jsx("div", {
@@ -14980,19 +15569,19 @@ function LoginPage({ redirectUrl = "/_emdash/admin" }) {
14980
15569
  children: [
14981
15570
  /* @__PURE__ */ jsxs("div", {
14982
15571
  className: "text-center mb-8",
14983
- children: [/* @__PURE__ */ jsx(BrandLogo, {
14984
- logoUrl: manifest?.admin?.logo,
14985
- siteName: manifest?.admin?.siteName,
14986
- className: "h-10 mx-auto mb-2"
14987
- }), /* @__PURE__ */ jsxs("h1", {
15572
+ children: [/* @__PURE__ */ jsx(BrandLogo, { className: "h-10 mx-auto mb-2" }), /* @__PURE__ */ jsx("h1", {
14988
15573
  className: "text-2xl font-semibold text-kumo-default",
14989
- children: [method === "passkey" && _t2({
14990
- id: "lE0wHD",
14991
- message: "Sign in to your site"
14992
- }), method === "magic-link" && _t2({
15574
+ children: method === "magic-link" ? _t2({
14993
15575
  id: "me9L29",
14994
15576
  message: "Sign in with email"
14995
- })]
15577
+ }) : activeProvider ? _t2({
15578
+ id: "e/ToF5",
15579
+ message: "Sign in with {0}",
15580
+ values: { 0: authProviderList.find((p) => p.id === activeProvider)?.label ?? activeProvider }
15581
+ }) : _t2({
15582
+ id: "lE0wHD",
15583
+ message: "Sign in to your site"
15584
+ })
14996
15585
  })]
14997
15586
  }),
14998
15587
  urlError && /* @__PURE__ */ jsx("div", {
@@ -15001,71 +15590,94 @@ function LoginPage({ redirectUrl = "/_emdash/admin" }) {
15001
15590
  }),
15002
15591
  /* @__PURE__ */ jsxs("div", {
15003
15592
  className: "bg-kumo-base border rounded-lg shadow-sm p-6",
15004
- children: [method === "passkey" && /* @__PURE__ */ jsxs("div", {
15005
- className: "space-y-6",
15006
- children: [
15007
- /* @__PURE__ */ jsx(PasskeyLogin, {
15008
- optionsEndpoint: "/_emdash/api/auth/passkey/options",
15009
- verifyEndpoint: "/_emdash/api/auth/passkey/verify",
15010
- onSuccess: handleSuccess,
15011
- buttonText: _t2({
15012
- id: "QjsOMP",
15013
- message: "Sign in with Passkey"
15014
- })
15015
- }),
15016
- /* @__PURE__ */ jsxs("div", {
15017
- className: "relative",
15018
- children: [/* @__PURE__ */ jsx("div", {
15019
- className: "absolute inset-0 flex items-center",
15020
- children: /* @__PURE__ */ jsx("span", { className: "w-full border-t" })
15021
- }), /* @__PURE__ */ jsx("div", {
15022
- className: "relative flex justify-center text-xs uppercase",
15023
- children: /* @__PURE__ */ jsx("span", {
15024
- className: "bg-kumo-base px-2 text-kumo-subtle",
15025
- children: _t2({
15026
- id: "zW+FpA",
15027
- message: "Or continue with"
15593
+ children: [
15594
+ method === "passkey" && !activeProvider && /* @__PURE__ */ jsxs("div", {
15595
+ className: "space-y-6",
15596
+ children: [
15597
+ /* @__PURE__ */ jsx(PasskeyLogin, {
15598
+ optionsEndpoint: "/_emdash/api/auth/passkey/options",
15599
+ verifyEndpoint: "/_emdash/api/auth/passkey/verify",
15600
+ onSuccess: handleSuccess,
15601
+ buttonText: _t2({
15602
+ id: "QjsOMP",
15603
+ message: "Sign in with Passkey"
15604
+ })
15605
+ }),
15606
+ /* @__PURE__ */ jsxs("div", {
15607
+ className: "relative",
15608
+ children: [/* @__PURE__ */ jsx("div", {
15609
+ className: "absolute inset-0 flex items-center",
15610
+ children: /* @__PURE__ */ jsx("span", { className: "w-full border-t" })
15611
+ }), /* @__PURE__ */ jsx("div", {
15612
+ className: "relative flex justify-center text-xs uppercase",
15613
+ children: /* @__PURE__ */ jsx("span", {
15614
+ className: "bg-kumo-base px-2 text-kumo-subtle",
15615
+ children: _t2({
15616
+ id: "zW+FpA",
15617
+ message: "Or continue with"
15618
+ })
15028
15619
  })
15620
+ })]
15621
+ }),
15622
+ buttonProviders.length > 0 && /* @__PURE__ */ jsx("div", {
15623
+ className: `grid gap-3 ${buttonProviders.length === 1 ? "grid-cols-1" : "grid-cols-2"}`,
15624
+ children: buttonProviders.map((provider) => {
15625
+ const Btn = provider.LoginButton;
15626
+ const hasForm = !!provider.LoginForm;
15627
+ const selectProvider = () => setActiveProvider(provider.id);
15628
+ return /* @__PURE__ */ jsx("div", {
15629
+ onClick: hasForm ? selectProvider : void 0,
15630
+ children: /* @__PURE__ */ jsx(Btn, {})
15631
+ }, provider.id);
15029
15632
  })
15030
- })]
15031
- }),
15032
- /* @__PURE__ */ jsx("div", {
15033
- className: "grid grid-cols-2 gap-3",
15034
- children: OAUTH_PROVIDERS.map((provider) => /* @__PURE__ */ jsxs(Button, {
15035
- variant: "outline",
15036
- type: "button",
15037
- onClick: () => handleOAuthClick(provider.id),
15633
+ }),
15634
+ /* @__PURE__ */ jsx(Button, {
15635
+ variant: "ghost",
15038
15636
  className: "w-full justify-center",
15039
- children: [provider.icon, /* @__PURE__ */ jsx("span", {
15040
- className: "ms-2",
15041
- children: provider.name
15042
- })]
15043
- }, provider.id))
15044
- }),
15045
- /* @__PURE__ */ jsx(Button, {
15046
- variant: "ghost",
15047
- className: "w-full justify-center",
15048
- type: "button",
15049
- onClick: () => setMethod("magic-link"),
15050
- children: _t2({
15051
- id: "RxerEl",
15052
- message: "Sign in with email link"
15637
+ type: "button",
15638
+ onClick: () => setMethod("magic-link"),
15639
+ children: _t2({
15640
+ id: "RxerEl",
15641
+ message: "Sign in with email link"
15642
+ })
15053
15643
  })
15054
- })
15055
- ]
15056
- }), method === "magic-link" && /* @__PURE__ */ jsx(MagicLinkForm, { onBack: () => setMethod("passkey") })]
15644
+ ]
15645
+ }),
15646
+ method === "passkey" && activeProvider && (() => {
15647
+ const provider = authProviderList.find((p) => p.id === activeProvider);
15648
+ if (!provider?.LoginForm) return null;
15649
+ const Form = provider.LoginForm;
15650
+ return /* @__PURE__ */ jsxs("div", {
15651
+ className: "space-y-4",
15652
+ children: [/* @__PURE__ */ jsx(Form, {}), /* @__PURE__ */ jsx(Button, {
15653
+ type: "button",
15654
+ variant: "ghost",
15655
+ className: "w-full justify-center",
15656
+ onClick: () => setActiveProvider(null),
15657
+ children: _t2({
15658
+ id: "VCoEm+",
15659
+ message: "Back to login"
15660
+ })
15661
+ })]
15662
+ });
15663
+ })(),
15664
+ method === "magic-link" && /* @__PURE__ */ jsx(MagicLinkForm, { onBack: () => setMethod("passkey") })
15665
+ ]
15057
15666
  }),
15058
15667
  /* @__PURE__ */ jsx("p", {
15059
15668
  className: "text-center mt-6 text-sm text-kumo-subtle",
15060
- children: method === "passkey" ? _t2({
15061
- id: "+ET/av",
15062
- message: "Use your registered passkey to sign in securely."
15063
- }) : _t2({
15669
+ children: method === "magic-link" ? _t2({
15064
15670
  id: "pv+wH3",
15065
15671
  message: "We'll send you a link to sign in without a password."
15672
+ }) : activeProvider ? _t2({
15673
+ id: "TZGT4o",
15674
+ message: "Enter your handle to sign in."
15675
+ }) : _t2({
15676
+ id: "+ET/av",
15677
+ message: "Use your registered passkey to sign in securely."
15066
15678
  })
15067
15679
  }),
15068
- manifest?.signupEnabled && /* @__PURE__ */ jsx("p", {
15680
+ authInfo?.signupEnabled && /* @__PURE__ */ jsx("p", {
15069
15681
  className: "text-center mt-4 text-sm text-kumo-subtle",
15070
15682
  children: /* @__PURE__ */ jsx(Trans, {
15071
15683
  id: "352VU2",
@@ -15964,7 +16576,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
15964
16576
  to: "/plugins/$pluginId/$",
15965
16577
  params: {
15966
16578
  pluginId: plugin.id,
15967
- _splat: "settings"
16579
+ _splat: ""
15968
16580
  },
15969
16581
  children: /* @__PURE__ */ jsxs(Button, {
15970
16582
  variant: "ghost",
@@ -16006,7 +16618,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
16006
16618
  }),
16007
16619
  onClick: () => setExpanded(!expanded),
16008
16620
  "aria-expanded": expanded,
16009
- children: [expanded ? /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4" }), /* @__PURE__ */ jsxs("span", {
16621
+ children: [expanded ? /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(CaretNext, { className: "h-4 w-4" }), /* @__PURE__ */ jsxs("span", {
16010
16622
  className: "sr-only",
16011
16623
  children: [
16012
16624
  expanded ? _t2({
@@ -16609,7 +17221,7 @@ function BackLink() {
16609
17221
  return /* @__PURE__ */ jsxs(Link$1, {
16610
17222
  to: "/plugins/marketplace",
16611
17223
  className: "inline-flex items-center gap-1 text-sm text-kumo-subtle hover:text-kumo-default",
16612
- children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" }), _t2({
17224
+ children: [/* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" }), _t2({
16613
17225
  id: "x5HVV+",
16614
17226
  message: "Back to marketplace"
16615
17227
  })]
@@ -16656,7 +17268,7 @@ function ScreenshotLightbox({ screenshots, index, isBlurred = false, onClose, on
16656
17268
  id: "k7xxm/",
16657
17269
  message: "Previous screenshot"
16658
17270
  }),
16659
- children: /* @__PURE__ */ jsx(CaretLeft, { className: "h-5 w-5" })
17271
+ children: /* @__PURE__ */ jsx(CaretPrev, { className: "h-5 w-5" })
16660
17272
  }),
16661
17273
  /* @__PURE__ */ jsx("img", {
16662
17274
  src: screenshots[index],
@@ -16677,7 +17289,7 @@ function ScreenshotLightbox({ screenshots, index, isBlurred = false, onClose, on
16677
17289
  id: "tgnDjk",
16678
17290
  message: "Next screenshot"
16679
17291
  }),
16680
- children: /* @__PURE__ */ jsx(CaretRight, { className: "h-5 w-5" })
17292
+ children: /* @__PURE__ */ jsx(CaretNext, { className: "h-5 w-5" })
16681
17293
  }),
16682
17294
  /* @__PURE__ */ jsxs("div", {
16683
17295
  className: "absolute bottom-4 start-1/2 -translate-x-1/2 rounded-full bg-black/50 px-3 py-1 text-sm text-white",
@@ -18219,7 +18831,7 @@ function MenuEditor() {
18219
18831
  message: "Back"
18220
18832
  }),
18221
18833
  onClick: () => navigate({ to: "/menus" }),
18222
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
18834
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
18223
18835
  }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
18224
18836
  className: "text-3xl font-bold",
18225
18837
  children: menu.label
@@ -18783,7 +19395,7 @@ function MenuList() {
18783
19395
  name: "name",
18784
19396
  required: true,
18785
19397
  placeholder: "primary",
18786
- pattern: "[a-z0-9-]+",
19398
+ pattern: "[a-z0-9\\-]+",
18787
19399
  title: _t({
18788
19400
  id: "6ZQzu/",
18789
19401
  message: "Only lowercase letters, numbers, and hyphens"
@@ -19523,7 +20135,7 @@ function Redirects() {
19523
20135
  }),
19524
20136
  /* @__PURE__ */ jsx("div", {
19525
20137
  className: "w-8 text-center text-kumo-subtle",
19526
- children: /* @__PURE__ */ jsx(ArrowRight, { size: 14 })
20138
+ children: /* @__PURE__ */ jsx(ArrowNext, { size: 14 })
19527
20139
  }),
19528
20140
  /* @__PURE__ */ jsx("div", {
19529
20141
  className: "flex-1 font-mono text-xs truncate",
@@ -19803,7 +20415,7 @@ function SectionEditor() {
19803
20415
  id: "t4JBeQ",
19804
20416
  message: "Back to sections"
19805
20417
  }),
19806
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-5 w-5" })
20418
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-5 w-5" })
19807
20419
  })
19808
20420
  }), /* @__PURE__ */ jsx("h1", {
19809
20421
  className: "text-2xl font-bold",
@@ -19865,6 +20477,16 @@ function SectionEditorForm({ section, isSaving, onSave }) {
19865
20477
  keywords,
19866
20478
  content
19867
20479
  ]) !== lastSavedData;
20480
+ const [blockSidebarPanel, setBlockSidebarPanel] = React.useState(null);
20481
+ const handleBlockSidebarOpen = React.useCallback((panel) => {
20482
+ setBlockSidebarPanel(panel);
20483
+ }, []);
20484
+ const handleBlockSidebarClose = React.useCallback(() => {
20485
+ setBlockSidebarPanel((prev) => {
20486
+ prev?.onClose();
20487
+ return null;
20488
+ });
20489
+ }, []);
19868
20490
  const handleSave = () => {
19869
20491
  const keywordsArray = keywords.split(",").map((k) => k.trim()).filter(Boolean);
19870
20492
  onSave({
@@ -19877,43 +20499,41 @@ function SectionEditorForm({ section, isSaving, onSave }) {
19877
20499
  };
19878
20500
  return /* @__PURE__ */ jsxs("div", {
19879
20501
  className: "space-y-6",
19880
- children: [/* @__PURE__ */ jsxs("div", {
19881
- className: "flex items-center justify-between",
19882
- children: [/* @__PURE__ */ jsxs("div", {
19883
- className: "flex items-center gap-4",
19884
- children: [/* @__PURE__ */ jsx(Link$1, {
19885
- to: "/sections",
19886
- children: /* @__PURE__ */ jsx(Button, {
19887
- variant: "ghost",
19888
- shape: "square",
19889
- "aria-label": _t2({
19890
- id: "t4JBeQ",
19891
- message: "Back to sections"
19892
- }),
19893
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-5 w-5" })
19894
- })
19895
- }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
19896
- className: "text-2xl font-bold",
19897
- children: section.title
19898
- }), /* @__PURE__ */ jsxs("p", {
19899
- className: "text-sm text-kumo-subtle",
19900
- children: [
19901
- section.source === "theme" ? _t2({
19902
- id: "wg2j4A",
19903
- message: "Theme Section"
19904
- }) : _t2({
19905
- id: "dqjp2n",
19906
- message: "Custom Section"
19907
- }),
19908
- " ·",
19909
- " ",
19910
- section.slug
19911
- ]
19912
- })] })]
19913
- }), /* @__PURE__ */ jsx(SaveButton, {
20502
+ children: [/* @__PURE__ */ jsxs(EditorHeader, {
20503
+ leading: /* @__PURE__ */ jsx(Link$1, {
20504
+ to: "/sections",
20505
+ children: /* @__PURE__ */ jsx(Button, {
20506
+ variant: "ghost",
20507
+ shape: "square",
20508
+ "aria-label": _t2({
20509
+ id: "t4JBeQ",
20510
+ message: "Back to sections"
20511
+ }),
20512
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-5 w-5" })
20513
+ })
20514
+ }),
20515
+ actions: /* @__PURE__ */ jsx(SaveButton, {
19914
20516
  isSaving,
19915
20517
  isDirty,
19916
20518
  onClick: handleSave
20519
+ }),
20520
+ children: [/* @__PURE__ */ jsx("h1", {
20521
+ className: "text-2xl font-bold truncate",
20522
+ children: section.title
20523
+ }), /* @__PURE__ */ jsxs("p", {
20524
+ className: "text-sm text-kumo-subtle",
20525
+ children: [
20526
+ section.source === "theme" ? _t2({
20527
+ id: "wg2j4A",
20528
+ message: "Theme Section"
20529
+ }) : _t2({
20530
+ id: "dqjp2n",
20531
+ message: "Custom Section"
20532
+ }),
20533
+ " ·",
20534
+ " ",
20535
+ section.slug
20536
+ ]
19917
20537
  })]
19918
20538
  }), /* @__PURE__ */ jsxs("div", {
19919
20539
  className: "grid grid-cols-12 gap-6",
@@ -19929,12 +20549,24 @@ function SectionEditorForm({ section, isSaving, onSave }) {
19929
20549
  })
19930
20550
  }), /* @__PURE__ */ jsx(PortableTextEditor, {
19931
20551
  value: content,
19932
- onChange: (value) => setContent(value)
20552
+ onChange: (value) => setContent(value),
20553
+ onBlockSidebarOpen: handleBlockSidebarOpen,
20554
+ onBlockSidebarClose: handleBlockSidebarClose
19933
20555
  })]
19934
20556
  })
19935
- }), /* @__PURE__ */ jsxs("div", {
20557
+ }), /* @__PURE__ */ jsx("div", {
19936
20558
  className: "col-span-4 space-y-6",
19937
- children: [/* @__PURE__ */ jsxs("div", {
20559
+ children: blockSidebarPanel?.type === "image" ? /* @__PURE__ */ jsx(ImageDetailPanel, {
20560
+ attributes: blockSidebarPanel.attrs,
20561
+ onUpdate: (attrs) => blockSidebarPanel.onUpdate(attrs),
20562
+ onReplace: (attrs) => blockSidebarPanel.onReplace(attrs),
20563
+ onDelete: () => {
20564
+ blockSidebarPanel.onDelete();
20565
+ setBlockSidebarPanel(null);
20566
+ },
20567
+ onClose: handleBlockSidebarClose,
20568
+ inline: true
20569
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
19938
20570
  className: "rounded-lg border bg-kumo-base p-6 space-y-4",
19939
20571
  children: [
19940
20572
  /* @__PURE__ */ jsx("h2", {
@@ -19967,7 +20599,7 @@ function SectionEditorForm({ section, isSaving, onSave }) {
19967
20599
  setSlugTouched(true);
19968
20600
  },
19969
20601
  placeholder: "section-slug",
19970
- pattern: "[a-z0-9-]+"
20602
+ pattern: "[a-z0-9\\-]+"
19971
20603
  }), /* @__PURE__ */ jsx("p", {
19972
20604
  className: "text-xs text-kumo-subtle mt-1",
19973
20605
  children: _t2({
@@ -20040,7 +20672,7 @@ function SectionEditorForm({ section, isSaving, onSave }) {
20040
20672
  })
20041
20673
  })
20042
20674
  ]
20043
- })]
20675
+ })] })
20044
20676
  })]
20045
20677
  })]
20046
20678
  });
@@ -20235,7 +20867,7 @@ function Sections() {
20235
20867
  },
20236
20868
  required: true,
20237
20869
  placeholder: "hero-banner",
20238
- pattern: "[a-z0-9-]+",
20870
+ pattern: "[a-z0-9\\-]+",
20239
20871
  title: _t({
20240
20872
  id: "BjF0Jv",
20241
20873
  message: "Lowercase letters, numbers, and hyphens only"
@@ -20564,7 +21196,7 @@ function SettingsLink({ to, icon, title, description }) {
20564
21196
  className: "text-sm text-kumo-subtle",
20565
21197
  children: description
20566
21198
  })] })]
20567
- }), /* @__PURE__ */ jsx(CaretRight, { className: "h-5 w-5 text-kumo-subtle" })]
21199
+ }), /* @__PURE__ */ jsx(CaretNext, { className: "h-5 w-5 text-kumo-subtle" })]
20568
21200
  });
20569
21201
  }
20570
21202
  /**
@@ -21002,7 +21634,7 @@ function AllowedDomainsSettings() {
21002
21634
  id: "9aZHfH",
21003
21635
  message: "Back to settings"
21004
21636
  }),
21005
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
21637
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
21006
21638
  })
21007
21639
  }), /* @__PURE__ */ jsx("h1", {
21008
21640
  className: "text-2xl font-bold",
@@ -21459,6 +22091,50 @@ const API_TOKEN_SCOPE_VALUES = [
21459
22091
  message: "Modify collection schemas"
21460
22092
  }
21461
22093
  },
22094
+ {
22095
+ scope: API_TOKEN_SCOPES.TaxonomiesManage,
22096
+ label: {
22097
+ id: "cfmuSI",
22098
+ message: "Taxonomies Manage"
22099
+ },
22100
+ description: {
22101
+ id: "mmBufY",
22102
+ message: "Create, update, and delete taxonomy terms"
22103
+ }
22104
+ },
22105
+ {
22106
+ scope: API_TOKEN_SCOPES.MenusManage,
22107
+ label: {
22108
+ id: "Wosws9",
22109
+ message: "Menus Manage"
22110
+ },
22111
+ description: {
22112
+ id: "DGhfmD",
22113
+ message: "Create, update, and delete navigation menus"
22114
+ }
22115
+ },
22116
+ {
22117
+ scope: API_TOKEN_SCOPES.SettingsRead,
22118
+ label: {
22119
+ id: "tYQSLw",
22120
+ message: "Settings Read"
22121
+ },
22122
+ description: {
22123
+ id: "fHKrk0",
22124
+ message: "Read site settings"
22125
+ }
22126
+ },
22127
+ {
22128
+ scope: API_TOKEN_SCOPES.SettingsManage,
22129
+ label: {
22130
+ id: "+K5A49",
22131
+ message: "Settings Manage"
22132
+ },
22133
+ description: {
22134
+ id: "RAXQwS",
22135
+ message: "Update site settings"
22136
+ }
22137
+ },
21462
22138
  {
21463
22139
  scope: API_TOKEN_SCOPES.Admin,
21464
22140
  label: {
@@ -21539,7 +22215,7 @@ function ApiTokenSettings() {
21539
22215
  id: "9aZHfH",
21540
22216
  message: "Back to settings"
21541
22217
  }),
21542
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
22218
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
21543
22219
  })
21544
22220
  }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
21545
22221
  className: "text-2xl font-bold",
@@ -21948,7 +22624,7 @@ function EmailSettings() {
21948
22624
  id: "9aZHfH",
21949
22625
  message: "Back to settings"
21950
22626
  }),
21951
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
22627
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
21952
22628
  })
21953
22629
  }), /* @__PURE__ */ jsx("h1", {
21954
22630
  className: "text-2xl font-bold",
@@ -21979,7 +22655,7 @@ function EmailSettings() {
21979
22655
  id: "9aZHfH",
21980
22656
  message: "Back to settings"
21981
22657
  }),
21982
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
22658
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
21983
22659
  })
21984
22660
  }), /* @__PURE__ */ jsx("h1", {
21985
22661
  className: "text-2xl font-bold",
@@ -22281,7 +22957,7 @@ function GeneralSettings() {
22281
22957
  id: "9aZHfH",
22282
22958
  message: "Back to settings"
22283
22959
  }),
22284
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
22960
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
22285
22961
  })
22286
22962
  }), /* @__PURE__ */ jsx("h1", {
22287
22963
  className: "text-2xl font-bold",
@@ -22304,9 +22980,8 @@ function GeneralSettings() {
22304
22980
  return /* @__PURE__ */ jsxs("div", {
22305
22981
  className: "space-y-6",
22306
22982
  children: [
22307
- /* @__PURE__ */ jsxs("div", {
22308
- className: "flex items-center gap-3",
22309
- children: [/* @__PURE__ */ jsx(Link$1, {
22983
+ /* @__PURE__ */ jsx(EditorHeader, {
22984
+ leading: /* @__PURE__ */ jsx(Link$1, {
22310
22985
  to: "/settings",
22311
22986
  children: /* @__PURE__ */ jsx(Button, {
22312
22987
  variant: "ghost",
@@ -22315,21 +22990,36 @@ function GeneralSettings() {
22315
22990
  id: "9aZHfH",
22316
22991
  message: "Back to settings"
22317
22992
  }),
22318
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
22993
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
22319
22994
  })
22320
- }), /* @__PURE__ */ jsx("h1", {
22321
- className: "text-2xl font-bold",
22995
+ }),
22996
+ actions: /* @__PURE__ */ jsx(Button, {
22997
+ type: "submit",
22998
+ form: "general-settings-form",
22999
+ disabled: saveMutation.isPending,
23000
+ icon: /* @__PURE__ */ jsx(FloppyDisk, {}),
23001
+ children: saveMutation.isPending ? _t({
23002
+ id: "XvjC4F",
23003
+ message: "Saving..."
23004
+ }) : _t({
23005
+ id: "UGT5vp",
23006
+ message: "Save Settings"
23007
+ })
23008
+ }),
23009
+ children: /* @__PURE__ */ jsx("h1", {
23010
+ className: "text-2xl font-bold truncate",
22322
23011
  children: _t({
22323
23012
  id: "Cb/Jr5",
22324
23013
  message: "General Settings"
22325
23014
  })
22326
- })]
23015
+ })
22327
23016
  }),
22328
23017
  saveStatus && /* @__PURE__ */ jsxs("div", {
22329
23018
  className: `flex items-center gap-2 rounded-lg border p-3 text-sm ${saveStatus.type === "success" ? "border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-200" : "border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950/30 dark:text-red-200"}`,
22330
23019
  children: [saveStatus.type === "success" ? /* @__PURE__ */ jsx(CheckCircle, { className: "h-4 w-4 flex-shrink-0" }) : /* @__PURE__ */ jsx(WarningCircle, { className: "h-4 w-4 flex-shrink-0" }), saveStatus.message]
22331
23020
  }),
22332
23021
  /* @__PURE__ */ jsxs("form", {
23022
+ id: "general-settings-form",
22333
23023
  onSubmit: handleSubmit,
22334
23024
  className: "space-y-6",
22335
23025
  children: [
@@ -22912,7 +23602,7 @@ function SecuritySettings() {
22912
23602
  id: "9aZHfH",
22913
23603
  message: "Back to settings"
22914
23604
  }),
22915
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
23605
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
22916
23606
  })
22917
23607
  }), /* @__PURE__ */ jsx("h1", {
22918
23608
  className: "text-2xl font-bold",
@@ -23133,7 +23823,7 @@ function SeoSettings() {
23133
23823
  id: "9aZHfH",
23134
23824
  message: "Back to settings"
23135
23825
  }),
23136
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
23826
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
23137
23827
  })
23138
23828
  }), /* @__PURE__ */ jsx("h1", {
23139
23829
  className: "text-2xl font-bold",
@@ -23156,9 +23846,8 @@ function SeoSettings() {
23156
23846
  return /* @__PURE__ */ jsxs("div", {
23157
23847
  className: "space-y-6",
23158
23848
  children: [
23159
- /* @__PURE__ */ jsxs("div", {
23160
- className: "flex items-center gap-3",
23161
- children: [/* @__PURE__ */ jsx(Link$1, {
23849
+ /* @__PURE__ */ jsx(EditorHeader, {
23850
+ leading: /* @__PURE__ */ jsx(Link$1, {
23162
23851
  to: "/settings",
23163
23852
  children: /* @__PURE__ */ jsx(Button, {
23164
23853
  variant: "ghost",
@@ -23167,21 +23856,36 @@ function SeoSettings() {
23167
23856
  id: "9aZHfH",
23168
23857
  message: "Back to settings"
23169
23858
  }),
23170
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
23859
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
23171
23860
  })
23172
- }), /* @__PURE__ */ jsx("h1", {
23173
- className: "text-2xl font-bold",
23861
+ }),
23862
+ actions: /* @__PURE__ */ jsx(Button, {
23863
+ type: "submit",
23864
+ form: "seo-settings-form",
23865
+ disabled: saveMutation.isPending,
23866
+ icon: /* @__PURE__ */ jsx(FloppyDisk, {}),
23867
+ children: saveMutation.isPending ? _t({
23868
+ id: "XvjC4F",
23869
+ message: "Saving..."
23870
+ }) : _t({
23871
+ id: "TJnLTa",
23872
+ message: "Save SEO Settings"
23873
+ })
23874
+ }),
23875
+ children: /* @__PURE__ */ jsx("h1", {
23876
+ className: "text-2xl font-bold truncate",
23174
23877
  children: _t({
23175
23878
  id: "GfWoKv",
23176
23879
  message: "SEO Settings"
23177
23880
  })
23178
- })]
23881
+ })
23179
23882
  }),
23180
23883
  saveStatus && /* @__PURE__ */ jsxs("div", {
23181
23884
  className: `flex items-center gap-2 rounded-lg border p-3 text-sm ${saveStatus.type === "success" ? "border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-200" : "border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950/30 dark:text-red-200"}`,
23182
23885
  children: [saveStatus.type === "success" ? /* @__PURE__ */ jsx(CheckCircle, { className: "h-4 w-4 flex-shrink-0" }) : /* @__PURE__ */ jsx(WarningCircle, { className: "h-4 w-4 flex-shrink-0" }), saveStatus.message]
23183
23886
  }),
23184
23887
  /* @__PURE__ */ jsxs("form", {
23888
+ id: "seo-settings-form",
23185
23889
  onSubmit: handleSubmit,
23186
23890
  className: "space-y-6",
23187
23891
  children: [/* @__PURE__ */ jsxs("div", {
@@ -23340,7 +24044,7 @@ function SocialSettings() {
23340
24044
  id: "9aZHfH",
23341
24045
  message: "Back to settings"
23342
24046
  }),
23343
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
24047
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
23344
24048
  })
23345
24049
  }), /* @__PURE__ */ jsx("h1", {
23346
24050
  className: "text-2xl font-bold",
@@ -23363,9 +24067,8 @@ function SocialSettings() {
23363
24067
  return /* @__PURE__ */ jsxs("div", {
23364
24068
  className: "space-y-6",
23365
24069
  children: [
23366
- /* @__PURE__ */ jsxs("div", {
23367
- className: "flex items-center gap-3",
23368
- children: [/* @__PURE__ */ jsx(Link$1, {
24070
+ /* @__PURE__ */ jsx(EditorHeader, {
24071
+ leading: /* @__PURE__ */ jsx(Link$1, {
23369
24072
  to: "/settings",
23370
24073
  children: /* @__PURE__ */ jsx(Button, {
23371
24074
  variant: "ghost",
@@ -23374,21 +24077,36 @@ function SocialSettings() {
23374
24077
  id: "9aZHfH",
23375
24078
  message: "Back to settings"
23376
24079
  }),
23377
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
24080
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
23378
24081
  })
23379
- }), /* @__PURE__ */ jsx("h1", {
23380
- className: "text-2xl font-bold",
24082
+ }),
24083
+ actions: /* @__PURE__ */ jsx(Button, {
24084
+ type: "submit",
24085
+ form: "social-settings-form",
24086
+ disabled: saveMutation.isPending,
24087
+ icon: /* @__PURE__ */ jsx(FloppyDisk, {}),
24088
+ children: saveMutation.isPending ? _t({
24089
+ id: "XvjC4F",
24090
+ message: "Saving..."
24091
+ }) : _t({
24092
+ id: "KZrfYJ",
24093
+ message: "Save Social Links"
24094
+ })
24095
+ }),
24096
+ children: /* @__PURE__ */ jsx("h1", {
24097
+ className: "text-2xl font-bold truncate",
23381
24098
  children: _t({
23382
24099
  id: "d0rUsW",
23383
24100
  message: "Social Links"
23384
24101
  })
23385
- })]
24102
+ })
23386
24103
  }),
23387
24104
  saveStatus && /* @__PURE__ */ jsxs("div", {
23388
24105
  className: `flex items-center gap-2 rounded-lg border p-3 text-sm ${saveStatus.type === "success" ? "border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-200" : "border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950/30 dark:text-red-200"}`,
23389
24106
  children: [saveStatus.type === "success" ? /* @__PURE__ */ jsx(CheckCircle, { className: "h-4 w-4 flex-shrink-0" }) : /* @__PURE__ */ jsx(WarningCircle, { className: "h-4 w-4 flex-shrink-0" }), saveStatus.message]
23390
24107
  }),
23391
24108
  /* @__PURE__ */ jsxs("form", {
24109
+ id: "social-settings-form",
23392
24110
  onSubmit: handleSubmit,
23393
24111
  className: "space-y-6",
23394
24112
  children: [/* @__PURE__ */ jsxs("div", {
@@ -23516,8 +24234,9 @@ function SocialSettings() {
23516
24234
  *
23517
24235
  * Steps:
23518
24236
  * 1. Site Configuration (title, tagline, sample content)
23519
- * 2. Admin Account (email, name)
23520
- * 3. Passkey Registration
24237
+ * 2. Create admin account — user picks any available auth method:
24238
+ * - Passkey (always available)
24239
+ * - Any configured auth provider (AT Protocol, GitHub, Google, etc.)
23521
24240
  */
23522
24241
  async function fetchSetupStatus() {
23523
24242
  return parseApiResponse(await apiFetch("/_emdash/api/setup/status"), "Failed to fetch setup status");
@@ -23538,8 +24257,8 @@ async function executeAdminSetup(data) {
23538
24257
  }
23539
24258
  function SiteStep({ seedInfo, onNext, isLoading, error }) {
23540
24259
  const { _: _t } = useLingui();
23541
- const [title, setTitle] = React.useState("");
23542
- const [tagline, setTagline] = React.useState("");
24260
+ const [title, setTitle] = React.useState(seedInfo?.title ?? "");
24261
+ const [tagline, setTagline] = React.useState(seedInfo?.tagline ?? "");
23543
24262
  const [includeContent, setIncludeContent] = React.useState(true);
23544
24263
  const [errors, setErrors] = React.useState({});
23545
24264
  const validate = () => {
@@ -23751,58 +24470,97 @@ function AdminStep({ onNext, onBack, isLoading, error }) {
23751
24470
  ]
23752
24471
  });
23753
24472
  }
23754
- function handlePasskeySuccess() {
24473
+ function handleSetupSuccess() {
23755
24474
  window.location.href = "/_emdash/admin";
23756
24475
  }
23757
- function PasskeyStep({ adminData, onBack }) {
24476
+ function AuthMethodStep({ adminData, providers, onBack }) {
23758
24477
  const { _: _t3 } = useLingui();
24478
+ const [activeProvider, setActiveProvider] = React.useState(null);
24479
+ const buttonProviders = providers.filter((p) => p.LoginButton);
24480
+ const hasProviders = buttonProviders.length > 0;
24481
+ if (activeProvider) {
24482
+ const provider = providers.find((p) => p.id === activeProvider);
24483
+ if (provider && (provider.SetupStep || provider.LoginForm)) return /* @__PURE__ */ jsxs("div", {
24484
+ className: "space-y-4",
24485
+ children: [
24486
+ /* @__PURE__ */ jsx("div", {
24487
+ className: "text-center mb-2",
24488
+ children: /* @__PURE__ */ jsx("h3", {
24489
+ className: "text-lg font-medium",
24490
+ children: _t3({
24491
+ id: "e/ToF5",
24492
+ message: "Sign in with {0}",
24493
+ values: { 0: provider.label }
24494
+ })
24495
+ })
24496
+ }),
24497
+ provider.SetupStep ? /* @__PURE__ */ jsx(provider.SetupStep, { onComplete: handleSetupSuccess }) : provider.LoginForm ? /* @__PURE__ */ jsx(provider.LoginForm, {}) : null,
24498
+ /* @__PURE__ */ jsx(Button, {
24499
+ type: "button",
24500
+ variant: "ghost",
24501
+ className: "w-full justify-center",
24502
+ onClick: () => setActiveProvider(null),
24503
+ children: _t3({
24504
+ id: "2ygf/L",
24505
+ message: "← Back"
24506
+ })
24507
+ })
24508
+ ]
24509
+ });
24510
+ }
23759
24511
  return /* @__PURE__ */ jsxs("div", {
23760
24512
  className: "space-y-6",
23761
24513
  children: [
23762
24514
  /* @__PURE__ */ jsxs("div", {
23763
24515
  className: "text-center",
23764
- children: [
23765
- /* @__PURE__ */ jsx("div", {
23766
- className: "inline-flex items-center justify-center w-16 h-16 rounded-full bg-kumo-brand/10 mb-4",
23767
- children: /* @__PURE__ */ jsx("svg", {
23768
- className: "w-8 h-8 text-kumo-brand",
23769
- fill: "none",
23770
- stroke: "currentColor",
23771
- viewBox: "0 0 24 24",
23772
- children: /* @__PURE__ */ jsx("path", {
23773
- strokeLinecap: "round",
23774
- strokeLinejoin: "round",
23775
- strokeWidth: 2,
23776
- d: "M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 6.844A21.88 21.88 0 0015.171 17m3.839 1.132c.645-2.266.99-4.659.99-7.132A8 8 0 008 4.07M3 15.364c.64-1.319 1-2.8 1-4.364 0-1.457.39-2.823 1.07-4"
23777
- })
23778
- })
23779
- }),
23780
- /* @__PURE__ */ jsx("h3", {
23781
- className: "text-lg font-medium",
23782
- children: _t3({
23783
- id: "fifQ8k",
23784
- message: "Set up your passkey"
23785
- })
23786
- }),
23787
- /* @__PURE__ */ jsx("p", {
23788
- className: "text-sm text-kumo-subtle mt-1",
23789
- children: _t3({
23790
- id: "YdZ7D8",
23791
- message: "Passkeys are more secure than passwords. You'll use your device's biometrics, PIN, or security key to sign in."
23792
- })
24516
+ children: [/* @__PURE__ */ jsx("h3", {
24517
+ className: "text-lg font-medium",
24518
+ children: _t3({
24519
+ id: "HAV363",
24520
+ message: "Choose how to sign in"
23793
24521
  })
23794
- ]
24522
+ }), /* @__PURE__ */ jsx("p", {
24523
+ className: "text-sm text-kumo-subtle mt-1",
24524
+ children: _t3({
24525
+ id: "NH33Oi",
24526
+ message: "Pick any method to create your admin account."
24527
+ })
24528
+ })]
23795
24529
  }),
23796
24530
  /* @__PURE__ */ jsx(PasskeyRegistration, {
23797
24531
  optionsEndpoint: "/_emdash/api/setup/admin",
23798
24532
  verifyEndpoint: "/_emdash/api/setup/admin/verify",
23799
- onSuccess: handlePasskeySuccess,
24533
+ onSuccess: handleSetupSuccess,
23800
24534
  buttonText: _t3({
23801
24535
  id: "AKqzR1",
23802
24536
  message: "Create Passkey"
23803
24537
  }),
23804
24538
  additionalData: { ...adminData }
23805
24539
  }),
24540
+ hasProviders && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
24541
+ className: "relative",
24542
+ children: [/* @__PURE__ */ jsx("div", {
24543
+ className: "absolute inset-0 flex items-center",
24544
+ children: /* @__PURE__ */ jsx("span", { className: "w-full border-t" })
24545
+ }), /* @__PURE__ */ jsx("div", {
24546
+ className: "relative flex justify-center text-xs uppercase",
24547
+ children: /* @__PURE__ */ jsx("span", {
24548
+ className: "bg-kumo-base px-2 text-kumo-subtle",
24549
+ children: "Or continue with"
24550
+ })
24551
+ })]
24552
+ }), /* @__PURE__ */ jsx("div", {
24553
+ className: `grid gap-3 ${buttonProviders.length === 1 ? "grid-cols-1" : "grid-cols-2"}`,
24554
+ children: buttonProviders.map((provider) => {
24555
+ const Btn = provider.LoginButton;
24556
+ const hasForm = !!provider.LoginForm || !!provider.SetupStep;
24557
+ const selectProvider = () => setActiveProvider(provider.id);
24558
+ return /* @__PURE__ */ jsx("div", {
24559
+ onClick: hasForm ? selectProvider : void 0,
24560
+ children: /* @__PURE__ */ jsx(Btn, {})
24561
+ }, provider.id);
24562
+ })
24563
+ })] }),
23806
24564
  /* @__PURE__ */ jsx(Button, {
23807
24565
  type: "button",
23808
24566
  variant: "ghost",
@@ -23842,8 +24600,8 @@ function StepIndicator$1({ currentStep, useAccessAuth }) {
23842
24600
  {
23843
24601
  key: "passkey",
23844
24602
  label: _t4({
23845
- id: "FGSN4s",
23846
- message: "Passkey"
24603
+ id: "n1ekoW",
24604
+ message: "Sign In"
23847
24605
  })
23848
24606
  }
23849
24607
  ];
@@ -23881,6 +24639,17 @@ function SetupWizard() {
23881
24639
  const [_siteData, setSiteData] = React.useState(null);
23882
24640
  const [adminData, setAdminData] = React.useState(null);
23883
24641
  const [error, setError] = React.useState();
24642
+ const [urlError, setUrlError] = React.useState(null);
24643
+ const authProviderList = useAuthProviderList();
24644
+ React.useEffect(() => {
24645
+ const params = new URLSearchParams(window.location.search);
24646
+ const errorParam = params.get("error");
24647
+ const message = params.get("message");
24648
+ if (errorParam) {
24649
+ setUrlError(message || `Authentication error: ${errorParam}`);
24650
+ window.history.replaceState({}, "", window.location.pathname);
24651
+ }
24652
+ }, []);
23884
24653
  const { data: status, isLoading: statusLoading, error: statusError } = useQuery({
23885
24654
  queryKey: ["setup", "status"],
23886
24655
  queryFn: fetchSetupStatus,
@@ -23999,6 +24768,10 @@ function SetupWizard() {
23999
24768
  })
24000
24769
  ]
24001
24770
  }),
24771
+ urlError && /* @__PURE__ */ jsx("div", {
24772
+ className: "mb-6 rounded-lg bg-kumo-danger/10 border border-kumo-danger/20 p-4 text-sm text-kumo-danger",
24773
+ children: urlError
24774
+ }),
24002
24775
  /* @__PURE__ */ jsx(StepIndicator$1, {
24003
24776
  currentStep,
24004
24777
  useAccessAuth
@@ -24021,8 +24794,9 @@ function SetupWizard() {
24021
24794
  isLoading: adminMutation.isPending,
24022
24795
  error
24023
24796
  }),
24024
- currentStep === "passkey" && adminData && /* @__PURE__ */ jsx(PasskeyStep, {
24797
+ currentStep === "passkey" && adminData && /* @__PURE__ */ jsx(AuthMethodStep, {
24025
24798
  adminData,
24799
+ providers: authProviderList,
24026
24800
  onBack: () => {
24027
24801
  setError(void 0);
24028
24802
  setCurrentStep("admin");
@@ -24783,7 +25557,7 @@ function SidebarNav({ manifest }) {
24783
25557
  collapsible: true,
24784
25558
  defaultOpen: true,
24785
25559
  children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
24786
- className: "[&>span]:text-start",
25560
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
24787
25561
  children: _t({
24788
25562
  id: "4b3oEV",
24789
25563
  message: "Content"
@@ -24795,7 +25569,7 @@ function SidebarNav({ manifest }) {
24795
25569
  collapsible: true,
24796
25570
  defaultOpen: true,
24797
25571
  children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
24798
- className: "[&>span]:text-start",
25572
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
24799
25573
  children: _t({
24800
25574
  id: "wckWOP",
24801
25575
  message: "Manage"
@@ -24807,7 +25581,7 @@ function SidebarNav({ manifest }) {
24807
25581
  collapsible: true,
24808
25582
  defaultOpen: true,
24809
25583
  children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
24810
- className: "[&>span]:text-start",
25584
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
24811
25585
  children: _t({
24812
25586
  id: "U3pytU",
24813
25587
  message: "Admin"
@@ -24818,7 +25592,7 @@ function SidebarNav({ manifest }) {
24818
25592
  collapsible: true,
24819
25593
  defaultOpen: true,
24820
25594
  children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
24821
- className: "[&>span]:text-start",
25595
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
24822
25596
  children: _t({
24823
25597
  id: "ohUJJM",
24824
25598
  message: "Plugins"
@@ -25788,7 +26562,7 @@ function TermRow({ term, level = 0, onEdit, onDelete }) {
25788
26562
  className: "flex items-center gap-4 py-2 px-4 border-b hover:bg-kumo-tint/50",
25789
26563
  children: [
25790
26564
  /* @__PURE__ */ jsxs("div", {
25791
- style: { marginLeft: `${level * 1.5}rem` },
26565
+ style: { marginInlineStart: `${level * 1.5}rem` },
25792
26566
  className: "flex-1",
25793
26567
  children: [/* @__PURE__ */ jsx("span", {
25794
26568
  className: "font-medium",
@@ -26826,7 +27600,7 @@ function ThemeMarketplaceDetail({ themeId }) {
26826
27600
  children: [/* @__PURE__ */ jsxs(Link$1, {
26827
27601
  to: "/themes/marketplace",
26828
27602
  className: "inline-flex items-center gap-1 text-sm text-kumo-subtle hover:text-kumo-default",
26829
- children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" }), _t({
27603
+ children: [/* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" }), _t({
26830
27604
  id: "6wOzc6",
26831
27605
  message: "Back to Themes"
26832
27606
  })]
@@ -26854,7 +27628,7 @@ function ThemeMarketplaceDetail({ themeId }) {
26854
27628
  /* @__PURE__ */ jsxs(Link$1, {
26855
27629
  to: "/themes/marketplace",
26856
27630
  className: "inline-flex items-center gap-1 text-sm text-kumo-subtle hover:text-kumo-default",
26857
- children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" }), _t({
27631
+ children: [/* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" }), _t({
26858
27632
  id: "6wOzc6",
26859
27633
  message: "Back to Themes"
26860
27634
  })]
@@ -27052,7 +27826,7 @@ function Lightbox({ urls, index, onClose, onPrev, onNext }) {
27052
27826
  id: "DHhJ7s",
27053
27827
  message: "Previous"
27054
27828
  }),
27055
- children: /* @__PURE__ */ jsx(CaretLeft, { className: "h-5 w-5" })
27829
+ children: /* @__PURE__ */ jsx(CaretPrev, { className: "h-5 w-5" })
27056
27830
  }), /* @__PURE__ */ jsx("button", {
27057
27831
  onClick: onNext,
27058
27832
  className: "absolute end-2 top-1/2 -translate-y-1/2 rounded-full bg-kumo-base/80 p-2 shadow hover:bg-kumo-base",
@@ -27060,7 +27834,7 @@ function Lightbox({ urls, index, onClose, onPrev, onNext }) {
27060
27834
  id: "hXzOVo",
27061
27835
  message: "Next"
27062
27836
  }),
27063
- children: /* @__PURE__ */ jsx(CaretRight, { className: "h-5 w-5" })
27837
+ children: /* @__PURE__ */ jsx(CaretNext, { className: "h-5 w-5" })
27064
27838
  })] }),
27065
27839
  /* @__PURE__ */ jsxs("div", {
27066
27840
  className: "absolute bottom-2 start-1/2 -translate-x-1/2 rounded-full bg-kumo-base/80 px-3 py-1 text-xs",
@@ -27132,7 +27906,20 @@ function Widgets() {
27132
27906
  const [activeId, setActiveId] = React.useState(null);
27133
27907
  const [activeDragData, setActiveDragData] = React.useState(null);
27134
27908
  const [expandedWidgets, setExpandedWidgets] = React.useState(/* @__PURE__ */ new Set());
27909
+ const [blockSidebarPanel, setBlockSidebarPanel] = React.useState(null);
27135
27910
  const draggingFromPaletteRef = React.useRef(false);
27911
+ const handleBlockSidebarOpen = React.useCallback((panel) => {
27912
+ setBlockSidebarPanel((prev) => {
27913
+ prev?.onClose();
27914
+ return panel;
27915
+ });
27916
+ }, []);
27917
+ const handleBlockSidebarClose = React.useCallback(() => {
27918
+ setBlockSidebarPanel((prev) => {
27919
+ prev?.onClose();
27920
+ return null;
27921
+ });
27922
+ }, []);
27136
27923
  const { data: areas = [], isLoading } = useQuery({
27137
27924
  queryKey: ["widget-areas"],
27138
27925
  queryFn: fetchWidgetAreas
@@ -27270,174 +28057,189 @@ function Widgets() {
27270
28057
  collisionDetection,
27271
28058
  onDragStart: handleDragStart,
27272
28059
  onDragEnd: handleDragEnd,
27273
- children: [/* @__PURE__ */ jsxs("div", {
27274
- className: "space-y-6",
27275
- children: [/* @__PURE__ */ jsxs("div", {
27276
- className: "flex items-center justify-between",
27277
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
27278
- className: "text-3xl font-bold",
27279
- children: "Widgets"
27280
- }), /* @__PURE__ */ jsx("p", {
27281
- className: "text-kumo-subtle",
27282
- children: "Manage content widgets in your widget areas"
27283
- })] }), /* @__PURE__ */ jsxs(Dialog.Root, {
27284
- open: isCreateAreaOpen,
27285
- onOpenChange: (open) => {
27286
- setIsCreateAreaOpen(open);
27287
- if (!open) setCreateAreaError(null);
27288
- },
27289
- children: [/* @__PURE__ */ jsx(Dialog.Trigger, { render: (props) => /* @__PURE__ */ jsx(Button, {
27290
- ...props,
27291
- icon: /* @__PURE__ */ jsx(Plus, {}),
27292
- children: "Add Widget Area"
27293
- }) }), /* @__PURE__ */ jsxs(Dialog, {
27294
- className: "p-6",
27295
- size: "lg",
27296
- children: [/* @__PURE__ */ jsxs("div", {
27297
- className: "flex items-start justify-between gap-4 mb-4",
27298
- children: [/* @__PURE__ */ jsx(Dialog.Title, {
27299
- className: "text-lg font-semibold leading-none tracking-tight",
27300
- children: "Create Widget Area"
27301
- }), /* @__PURE__ */ jsx(Dialog.Close, {
27302
- "aria-label": "Close",
27303
- render: (props) => /* @__PURE__ */ jsxs(Button, {
27304
- ...props,
27305
- variant: "ghost",
27306
- shape: "square",
28060
+ children: [
28061
+ /* @__PURE__ */ jsxs("div", {
28062
+ className: "space-y-6",
28063
+ children: [/* @__PURE__ */ jsxs("div", {
28064
+ className: "flex items-center justify-between",
28065
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
28066
+ className: "text-3xl font-bold",
28067
+ children: "Widgets"
28068
+ }), /* @__PURE__ */ jsx("p", {
28069
+ className: "text-kumo-subtle",
28070
+ children: "Manage content widgets in your widget areas"
28071
+ })] }), /* @__PURE__ */ jsxs(Dialog.Root, {
28072
+ open: isCreateAreaOpen,
28073
+ onOpenChange: (open) => {
28074
+ setIsCreateAreaOpen(open);
28075
+ if (!open) setCreateAreaError(null);
28076
+ },
28077
+ children: [/* @__PURE__ */ jsx(Dialog.Trigger, { render: (props) => /* @__PURE__ */ jsx(Button, {
28078
+ ...props,
28079
+ icon: /* @__PURE__ */ jsx(Plus, {}),
28080
+ children: "Add Widget Area"
28081
+ }) }), /* @__PURE__ */ jsxs(Dialog, {
28082
+ className: "p-6",
28083
+ size: "lg",
28084
+ children: [/* @__PURE__ */ jsxs("div", {
28085
+ className: "flex items-start justify-between gap-4 mb-4",
28086
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
28087
+ className: "text-lg font-semibold leading-none tracking-tight",
28088
+ children: "Create Widget Area"
28089
+ }), /* @__PURE__ */ jsx(Dialog.Close, {
27307
28090
  "aria-label": "Close",
27308
- className: "absolute end-4 top-4",
27309
- children: [/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", {
27310
- className: "sr-only",
27311
- children: "Close"
27312
- })]
27313
- })
28091
+ render: (props) => /* @__PURE__ */ jsxs(Button, {
28092
+ ...props,
28093
+ variant: "ghost",
28094
+ shape: "square",
28095
+ "aria-label": "Close",
28096
+ className: "absolute end-4 top-4",
28097
+ children: [/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", {
28098
+ className: "sr-only",
28099
+ children: "Close"
28100
+ })]
28101
+ })
28102
+ })]
28103
+ }), /* @__PURE__ */ jsxs("form", {
28104
+ onSubmit: handleCreateArea,
28105
+ className: "space-y-4",
28106
+ children: [
28107
+ /* @__PURE__ */ jsx(Input, {
28108
+ label: "Name",
28109
+ name: "name",
28110
+ required: true,
28111
+ placeholder: "sidebar",
28112
+ pattern: "[a-z0-9\\-]+"
28113
+ }),
28114
+ /* @__PURE__ */ jsx(Input, {
28115
+ label: "Label",
28116
+ name: "label",
28117
+ required: true,
28118
+ placeholder: "Main Sidebar"
28119
+ }),
28120
+ /* @__PURE__ */ jsx(Input, {
28121
+ label: "Description",
28122
+ name: "description",
28123
+ placeholder: "Appears on posts and pages"
28124
+ }),
28125
+ /* @__PURE__ */ jsx(DialogError, { message: createAreaError || getMutationError(createAreaMutation.error) }),
28126
+ /* @__PURE__ */ jsxs("div", {
28127
+ className: "flex justify-end gap-2",
28128
+ children: [/* @__PURE__ */ jsx(Button, {
28129
+ type: "button",
28130
+ variant: "outline",
28131
+ onClick: () => setIsCreateAreaOpen(false),
28132
+ children: "Cancel"
28133
+ }), /* @__PURE__ */ jsx(Button, {
28134
+ type: "submit",
28135
+ disabled: createAreaMutation.isPending,
28136
+ children: "Create"
28137
+ })]
28138
+ })
28139
+ ]
27314
28140
  })]
27315
- }), /* @__PURE__ */ jsxs("form", {
27316
- onSubmit: handleCreateArea,
27317
- className: "space-y-4",
28141
+ })]
28142
+ })]
28143
+ }), /* @__PURE__ */ jsxs("div", {
28144
+ className: "grid grid-cols-12 gap-6",
28145
+ children: [/* @__PURE__ */ jsx("div", {
28146
+ className: "col-span-4",
28147
+ children: /* @__PURE__ */ jsxs("div", {
28148
+ className: "rounded-lg border bg-kumo-base p-6 space-y-4",
27318
28149
  children: [
27319
- /* @__PURE__ */ jsx(Input, {
27320
- label: "Name",
27321
- name: "name",
27322
- required: true,
27323
- placeholder: "sidebar",
27324
- pattern: "[a-z0-9-]+"
28150
+ /* @__PURE__ */ jsx("h2", {
28151
+ className: "text-xl font-semibold",
28152
+ children: "Available Widgets"
27325
28153
  }),
27326
- /* @__PURE__ */ jsx(Input, {
27327
- label: "Label",
27328
- name: "label",
27329
- required: true,
27330
- placeholder: "Main Sidebar"
27331
- }),
27332
- /* @__PURE__ */ jsx(Input, {
27333
- label: "Description",
27334
- name: "description",
27335
- placeholder: "Appears on posts and pages"
28154
+ /* @__PURE__ */ jsx("p", {
28155
+ className: "text-sm text-kumo-subtle",
28156
+ children: "Drag widgets into an area to add them"
27336
28157
  }),
27337
- /* @__PURE__ */ jsx(DialogError, { message: createAreaError || getMutationError(createAreaMutation.error) }),
27338
28158
  /* @__PURE__ */ jsxs("div", {
27339
- className: "flex justify-end gap-2",
27340
- children: [/* @__PURE__ */ jsx(Button, {
27341
- type: "button",
27342
- variant: "outline",
27343
- onClick: () => setIsCreateAreaOpen(false),
27344
- children: "Cancel"
27345
- }), /* @__PURE__ */ jsx(Button, {
27346
- type: "submit",
27347
- disabled: createAreaMutation.isPending,
27348
- children: "Create"
27349
- })]
28159
+ className: "space-y-2",
28160
+ children: [BUILTIN_WIDGETS.map((item) => /* @__PURE__ */ jsx(DraggablePaletteItem, {
28161
+ id: item.id,
28162
+ label: _t(item.label),
28163
+ description: _t(item.description),
28164
+ widgetInput: {
28165
+ ...item.input,
28166
+ title: _t(item.label)
28167
+ }
28168
+ }, item.id)), components.map((comp) => /* @__PURE__ */ jsx(DraggablePaletteItem, {
28169
+ id: `palette-comp-${comp.id}`,
28170
+ label: comp.label,
28171
+ description: comp.description,
28172
+ widgetInput: {
28173
+ type: "component",
28174
+ title: comp.label,
28175
+ componentId: comp.id
28176
+ }
28177
+ }, `palette-comp-${comp.id}`))]
27350
28178
  })
27351
28179
  ]
27352
- })]
28180
+ })
28181
+ }), /* @__PURE__ */ jsx("div", {
28182
+ className: "col-span-8 space-y-4",
28183
+ children: areas.length === 0 ? /* @__PURE__ */ jsx("div", {
28184
+ className: "rounded-lg border bg-kumo-base p-12 text-center",
28185
+ children: /* @__PURE__ */ jsx("p", {
28186
+ className: "text-kumo-subtle",
28187
+ children: "No widget areas yet. Create one to get started."
28188
+ })
28189
+ }) : areas.map((area) => /* @__PURE__ */ jsx(WidgetAreaPanel, {
28190
+ area,
28191
+ expandedWidgets,
28192
+ onToggleWidget: toggleWidget,
28193
+ isDraggingPalette: activeDragData !== null && isPaletteItem(activeDragData),
28194
+ components,
28195
+ pluginBlocks,
28196
+ onBlockSidebarOpen: handleBlockSidebarOpen,
28197
+ onBlockSidebarClose: handleBlockSidebarClose
28198
+ }, area.id))
27353
28199
  })]
27354
28200
  })]
27355
- }), /* @__PURE__ */ jsxs("div", {
27356
- className: "grid grid-cols-12 gap-6",
27357
- children: [/* @__PURE__ */ jsx("div", {
27358
- className: "col-span-4",
28201
+ }),
28202
+ /* @__PURE__ */ jsx(DragOverlay, {
28203
+ dropAnimation: draggingFromPaletteRef.current ? null : void 0,
28204
+ children: activePaletteLabel ? /* @__PURE__ */ jsx("div", {
28205
+ className: "rounded border bg-kumo-base p-3 shadow-lg opacity-90",
28206
+ children: /* @__PURE__ */ jsx("div", {
28207
+ className: "font-medium",
28208
+ children: activePaletteLabel
28209
+ })
28210
+ }) : activeWidget ? /* @__PURE__ */ jsx("div", {
28211
+ className: "rounded border bg-kumo-base p-3 shadow-lg opacity-90",
27359
28212
  children: /* @__PURE__ */ jsxs("div", {
27360
- className: "rounded-lg border bg-kumo-base p-6 space-y-4",
28213
+ className: "flex items-center gap-2",
27361
28214
  children: [
27362
- /* @__PURE__ */ jsx("h2", {
27363
- className: "text-xl font-semibold",
27364
- children: "Available Widgets"
27365
- }),
27366
- /* @__PURE__ */ jsx("p", {
27367
- className: "text-sm text-kumo-subtle",
27368
- children: "Drag widgets into an area to add them"
28215
+ /* @__PURE__ */ jsx(DotsSixVertical, { className: "h-4 w-4 text-kumo-subtle" }),
28216
+ /* @__PURE__ */ jsx("span", {
28217
+ className: "font-medium",
28218
+ children: activeWidget.title || "Untitled Widget"
27369
28219
  }),
27370
- /* @__PURE__ */ jsxs("div", {
27371
- className: "space-y-2",
27372
- children: [BUILTIN_WIDGETS.map((item) => /* @__PURE__ */ jsx(DraggablePaletteItem, {
27373
- id: item.id,
27374
- label: _t(item.label),
27375
- description: _t(item.description),
27376
- widgetInput: {
27377
- ...item.input,
27378
- title: _t(item.label)
27379
- }
27380
- }, item.id)), components.map((comp) => /* @__PURE__ */ jsx(DraggablePaletteItem, {
27381
- id: `palette-comp-${comp.id}`,
27382
- label: comp.label,
27383
- description: comp.description,
27384
- widgetInput: {
27385
- type: "component",
27386
- title: comp.label,
27387
- componentId: comp.id
27388
- }
27389
- }, `palette-comp-${comp.id}`))]
28220
+ /* @__PURE__ */ jsxs("span", {
28221
+ className: "text-xs text-kumo-subtle",
28222
+ children: [
28223
+ "(",
28224
+ activeWidget.type,
28225
+ ")"
28226
+ ]
27390
28227
  })
27391
28228
  ]
27392
28229
  })
27393
- }), /* @__PURE__ */ jsx("div", {
27394
- className: "col-span-8 space-y-4",
27395
- children: areas.length === 0 ? /* @__PURE__ */ jsx("div", {
27396
- className: "rounded-lg border bg-kumo-base p-12 text-center",
27397
- children: /* @__PURE__ */ jsx("p", {
27398
- className: "text-kumo-subtle",
27399
- children: "No widget areas yet. Create one to get started."
27400
- })
27401
- }) : areas.map((area) => /* @__PURE__ */ jsx(WidgetAreaPanel, {
27402
- area,
27403
- expandedWidgets,
27404
- onToggleWidget: toggleWidget,
27405
- isDraggingPalette: activeDragData !== null && isPaletteItem(activeDragData),
27406
- components,
27407
- pluginBlocks
27408
- }, area.id))
27409
- })]
27410
- })]
27411
- }), /* @__PURE__ */ jsx(DragOverlay, {
27412
- dropAnimation: draggingFromPaletteRef.current ? null : void 0,
27413
- children: activePaletteLabel ? /* @__PURE__ */ jsx("div", {
27414
- className: "rounded border bg-kumo-base p-3 shadow-lg opacity-90",
27415
- children: /* @__PURE__ */ jsx("div", {
27416
- className: "font-medium",
27417
- children: activePaletteLabel
27418
- })
27419
- }) : activeWidget ? /* @__PURE__ */ jsx("div", {
27420
- className: "rounded border bg-kumo-base p-3 shadow-lg opacity-90",
27421
- children: /* @__PURE__ */ jsxs("div", {
27422
- className: "flex items-center gap-2",
27423
- children: [
27424
- /* @__PURE__ */ jsx(DotsSixVertical, { className: "h-4 w-4 text-kumo-subtle" }),
27425
- /* @__PURE__ */ jsx("span", {
27426
- className: "font-medium",
27427
- children: activeWidget.title || "Untitled Widget"
27428
- }),
27429
- /* @__PURE__ */ jsxs("span", {
27430
- className: "text-xs text-kumo-subtle",
27431
- children: [
27432
- "(",
27433
- activeWidget.type,
27434
- ")"
27435
- ]
27436
- })
27437
- ]
27438
- })
27439
- }) : null
27440
- })]
28230
+ }) : null
28231
+ }),
28232
+ blockSidebarPanel?.type === "image" && /* @__PURE__ */ jsx(ImageDetailPanel, {
28233
+ attributes: blockSidebarPanel.attrs,
28234
+ onUpdate: (attrs) => blockSidebarPanel.onUpdate(attrs),
28235
+ onReplace: (attrs) => blockSidebarPanel.onReplace(attrs),
28236
+ onDelete: () => {
28237
+ blockSidebarPanel.onDelete();
28238
+ setBlockSidebarPanel(null);
28239
+ },
28240
+ onClose: handleBlockSidebarClose
28241
+ })
28242
+ ]
27441
28243
  });
27442
28244
  }
27443
28245
  /** A draggable item in the available widgets palette */
@@ -27464,7 +28266,7 @@ function DraggablePaletteItem({ id, label, description, widgetInput }) {
27464
28266
  })]
27465
28267
  });
27466
28268
  }
27467
- function WidgetAreaPanel({ area, expandedWidgets, onToggleWidget, isDraggingPalette, components, pluginBlocks }) {
28269
+ function WidgetAreaPanel({ area, expandedWidgets, onToggleWidget, isDraggingPalette, components, pluginBlocks, onBlockSidebarOpen, onBlockSidebarClose }) {
27468
28270
  const queryClient = useQueryClient();
27469
28271
  const toastManager = Toast.useToastManager();
27470
28272
  const [deleteAreaName, setDeleteAreaName] = React.useState(null);
@@ -27510,7 +28312,9 @@ function WidgetAreaPanel({ area, expandedWidgets, onToggleWidget, isDraggingPale
27510
28312
  isExpanded: expandedWidgets.has(widget.id),
27511
28313
  onToggle: () => onToggleWidget(widget.id),
27512
28314
  components,
27513
- pluginBlocks
28315
+ pluginBlocks,
28316
+ onBlockSidebarOpen,
28317
+ onBlockSidebarClose
27514
28318
  }, widget.id))
27515
28319
  }) : null,
27516
28320
  isDraggingPalette && /* @__PURE__ */ jsx("div", {
@@ -27540,7 +28344,7 @@ function WidgetAreaPanel({ area, expandedWidgets, onToggleWidget, isDraggingPale
27540
28344
  ]
27541
28345
  });
27542
28346
  }
27543
- function WidgetItem({ widget, areaName, isExpanded, onToggle, components, pluginBlocks }) {
28347
+ function WidgetItem({ widget, areaName, isExpanded, onToggle, components, pluginBlocks, onBlockSidebarOpen, onBlockSidebarClose }) {
27544
28348
  const queryClient = useQueryClient();
27545
28349
  const toastManager = Toast.useToastManager();
27546
28350
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
@@ -27603,7 +28407,7 @@ function WidgetItem({ widget, areaName, isExpanded, onToggle, components, plugin
27603
28407
  children: /* @__PURE__ */ jsxs("div", {
27604
28408
  className: "flex items-center gap-2",
27605
28409
  children: [
27606
- isExpanded ? /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4" }),
28410
+ isExpanded ? /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(CaretNext, { className: "h-4 w-4" }),
27607
28411
  /* @__PURE__ */ jsx("span", {
27608
28412
  className: "font-medium",
27609
28413
  children: widget.title || "Untitled Widget"
@@ -27632,12 +28436,14 @@ function WidgetItem({ widget, areaName, isExpanded, onToggle, components, plugin
27632
28436
  components,
27633
28437
  pluginBlocks,
27634
28438
  onSave: (input) => updateMutation.mutate(input),
27635
- isSaving: updateMutation.isPending
28439
+ isSaving: updateMutation.isPending,
28440
+ onBlockSidebarOpen,
28441
+ onBlockSidebarClose
27636
28442
  })]
27637
28443
  });
27638
28444
  }
27639
28445
  /** Inline editor form for a widget, rendered when the widget is expanded */
27640
- function WidgetEditor({ widget, components, pluginBlocks, onSave, isSaving }) {
28446
+ function WidgetEditor({ widget, components, pluginBlocks, onSave, isSaving, onBlockSidebarOpen, onBlockSidebarClose }) {
27641
28447
  const [title, setTitle] = React.useState(widget.title ?? "");
27642
28448
  const [content, setContent] = React.useState(Array.isArray(widget.content) ? widget.content : []);
27643
28449
  const [menuName, setMenuName] = React.useState(widget.menuName ?? "");
@@ -27676,7 +28482,9 @@ function WidgetEditor({ widget, components, pluginBlocks, onSave, isSaving }) {
27676
28482
  onChange: (value) => setContent(value),
27677
28483
  minimal: true,
27678
28484
  placeholder: "Write widget content...",
27679
- pluginBlocks
28485
+ pluginBlocks,
28486
+ onBlockSidebarOpen,
28487
+ onBlockSidebarClose
27680
28488
  })] }),
27681
28489
  widget.type === "menu" && /* @__PURE__ */ jsxs(Select, {
27682
28490
  label: "Menu",
@@ -29616,7 +30424,7 @@ function PostTypeRow({ postType, selection, expanded, onToggleExpand, onToggleEn
29616
30424
  onClick: onToggleExpand,
29617
30425
  className: "flex items-center gap-1 text-start",
29618
30426
  "aria-expanded": expanded,
29619
- children: [expanded ? /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4 text-kumo-subtle" }) : /* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4 text-kumo-subtle" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
30427
+ children: [expanded ? /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4 text-kumo-subtle" }) : /* @__PURE__ */ jsx(CaretNext, { className: "h-4 w-4 text-kumo-subtle" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
29620
30428
  className: "font-medium",
29621
30429
  children: postType.name
29622
30430
  }), /* @__PURE__ */ jsx("p", {
@@ -31894,16 +32702,25 @@ function ContentListPage() {
31894
32702
  });
31895
32703
  const i18n = manifest?.i18n;
31896
32704
  const activeLocale = i18n ? localeParam ?? i18n.defaultLocale : void 0;
32705
+ const [sort, setSort] = React.useState({
32706
+ field: "updatedAt",
32707
+ direction: "desc"
32708
+ });
31897
32709
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error } = useInfiniteQuery({
31898
32710
  queryKey: [
31899
32711
  "content",
31900
32712
  collection,
31901
- { locale: activeLocale }
32713
+ {
32714
+ locale: activeLocale,
32715
+ sort
32716
+ }
31902
32717
  ],
31903
32718
  queryFn: ({ pageParam }) => fetchContentList(collection, {
31904
32719
  locale: activeLocale,
31905
32720
  cursor: pageParam,
31906
- limit: 100
32721
+ limit: 100,
32722
+ orderBy: sort.field,
32723
+ order: sort.direction
31907
32724
  }),
31908
32725
  initialPageParam: void 0,
31909
32726
  getNextPageParam: (lastPage) => lastPage.nextCursor,
@@ -32014,7 +32831,9 @@ function ContentListPage() {
32014
32831
  i18n,
32015
32832
  activeLocale,
32016
32833
  onLocaleChange: handleLocaleChange,
32017
- urlPattern: collectionConfig.urlPattern
32834
+ urlPattern: collectionConfig.urlPattern,
32835
+ sort,
32836
+ onSortChange: setSort
32018
32837
  });
32019
32838
  }
32020
32839
  const contentNewRoute = createRoute({
@@ -32734,7 +33553,7 @@ function MarketplaceBrowsePage() {
32734
33553
  const { data: plugins } = useQuery({
32735
33554
  queryKey: ["plugins"],
32736
33555
  queryFn: async () => {
32737
- const { fetchPlugins } = await import("./plugins-XhZqfegd.js").then((n) => n.a);
33556
+ const { fetchPlugins } = await import("./plugins-DUJY_8HN.js").then((n) => n.a);
32738
33557
  return fetchPlugins();
32739
33558
  }
32740
33559
  });
@@ -32753,7 +33572,7 @@ function MarketplaceDetailPage() {
32753
33572
  const { data: plugins } = useQuery({
32754
33573
  queryKey: ["plugins"],
32755
33574
  queryFn: async () => {
32756
- const { fetchPlugins } = await import("./plugins-XhZqfegd.js").then((n) => n.a);
33575
+ const { fetchPlugins } = await import("./plugins-DUJY_8HN.js").then((n) => n.a);
32757
33576
  return fetchPlugins();
32758
33577
  }
32759
33578
  });
@@ -33166,7 +33985,8 @@ const router = createAdminRouter(queryClient);
33166
33985
  * Main Admin Application
33167
33986
  */
33168
33987
  const EMPTY_PLUGINS = {};
33169
- function AdminApp({ pluginAdmins = EMPTY_PLUGINS, locale = "en", messages = {} }) {
33988
+ const EMPTY_AUTH_PROVIDERS = {};
33989
+ function AdminApp({ pluginAdmins = EMPTY_PLUGINS, authProviders = EMPTY_AUTH_PROVIDERS, locale = "en", messages = {} }) {
33170
33990
  React.useEffect(() => {
33171
33991
  document.getElementById("emdash-boot-loader")?.remove();
33172
33992
  }, []);
@@ -33180,16 +34000,19 @@ function AdminApp({ pluginAdmins = EMPTY_PLUGINS, locale = "en", messages = {} }
33180
34000
  }
33181
34001
  return /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(I18nProvider, {
33182
34002
  i18n,
33183
- children: /* @__PURE__ */ jsx(LocaleDirectionProvider, { children: /* @__PURE__ */ jsx(Toasty, { children: /* @__PURE__ */ jsx(PluginAdminProvider, {
33184
- pluginAdmins,
33185
- children: /* @__PURE__ */ jsx(QueryClientProvider, {
33186
- client: queryClient,
33187
- children: /* @__PURE__ */ jsx(RouterProvider, { router })
34003
+ children: /* @__PURE__ */ jsx(LocaleDirectionProvider, { children: /* @__PURE__ */ jsx(Toasty, { children: /* @__PURE__ */ jsx(AuthProviderProvider, {
34004
+ authProviders,
34005
+ children: /* @__PURE__ */ jsx(PluginAdminProvider, {
34006
+ pluginAdmins,
34007
+ children: /* @__PURE__ */ jsx(QueryClientProvider, {
34008
+ client: queryClient,
34009
+ children: /* @__PURE__ */ jsx(RouterProvider, { router })
34010
+ })
33188
34011
  })
33189
34012
  }) }) })
33190
34013
  }) });
33191
34014
  }
33192
34015
 
33193
34016
  //#endregion
33194
- export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, CAPABILITY_LABELS, ContentEditor, ContentList, DEFAULT_LOCALE, Dashboard, 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, createCollection, createContent, createField, createMenu, createMenuItem, createRedirect, createSection, createTaxonomy, createTerm, 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, fetchByline, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, 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, useCurrentUser, useLocale, useNavigate, useParams, usePluginAdmins, usePluginField, usePluginHasPages, usePluginHasWidgets, usePluginPage, usePluginWidget, validateInviteToken, verifySignupToken };
34017
+ 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, createCollection, createContent, createField, createMenu, createMenuItem, createRedirect, createSection, createTaxonomy, createTerm, 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, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, 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 };
33195
34018
  //# sourceMappingURL=index.js.map