@emdash-cms/admin 0.8.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-B43-InGI.js → LocaleDirectionProvider-BGcmv_N7.js} +21 -15
  2. package/dist/LocaleDirectionProvider-BGcmv_N7.js.map +1 -0
  3. package/dist/{config-h7tWQRka.d.ts → config-YdN-uv6S.d.ts} +1 -1
  4. package/dist/{config-h7tWQRka.d.ts.map → config-YdN-uv6S.d.ts.map} +1 -1
  5. package/dist/index.d.ts +68 -9
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1312 -952
  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 -1
  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-DUJY_8HN.js.map +1 -1
  57. package/dist/styles.css +1 -1
  58. package/package.json +2 -4
  59. package/dist/LocaleDirectionProvider-B43-InGI.js.map +0 -1
  60. package/dist/messages-B6ZguVfS.js +0 -6
  61. package/dist/messages-B6ZguVfS.js.map +0 -1
  62. package/dist/messages-BOnMi1Vb.js +0 -6
  63. package/dist/messages-BOnMi1Vb.js.map +0 -1
  64. package/dist/messages-BZuRVH-D.js +0 -6
  65. package/dist/messages-BZuRVH-D.js.map +0 -1
  66. package/dist/messages-Bc_PDiKw.js +0 -6
  67. package/dist/messages-Bc_PDiKw.js.map +0 -1
  68. package/dist/messages-Bddg68eH.js +0 -6
  69. package/dist/messages-Bddg68eH.js.map +0 -1
  70. package/dist/messages-BoP-RHEG.js +0 -6
  71. package/dist/messages-BoP-RHEG.js.map +0 -1
  72. package/dist/messages-C4hNtyU5.js +0 -6
  73. package/dist/messages-C4hNtyU5.js.map +0 -1
  74. package/dist/messages-CMwt5Gv-.js +0 -6
  75. package/dist/messages-CMwt5Gv-.js.map +0 -1
  76. package/dist/messages-CnliZpid.js +0 -6
  77. package/dist/messages-CnliZpid.js.map +0 -1
  78. package/dist/messages-CseAZVO2.js +0 -6
  79. package/dist/messages-CseAZVO2.js.map +0 -1
  80. package/dist/messages-DRyNt1qC.js +0 -6
  81. package/dist/messages-DRyNt1qC.js.map +0 -1
  82. package/dist/messages-DTehFUkD.js +0 -6
  83. package/dist/messages-DTehFUkD.js.map +0 -1
  84. package/dist/messages-DeQMOYvT.js +0 -6
  85. package/dist/messages-DeQMOYvT.js.map +0 -1
  86. package/dist/messages-Do3BokvI.js +0 -6
  87. package/dist/messages-Do3BokvI.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { c as fetchAuthMode, d as throwResponseError, i as fetchPlugins, l as fetchManifest, n as enablePlugin, o as API_BASE, r as fetchPlugin, s as apiFetch, t as disablePlugin, u as parseApiResponse } from "./plugins-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-B43-InGI.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, 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";
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";
@@ -219,6 +219,37 @@ function slugify(text) {
219
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, "");
220
220
  }
221
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
+
222
253
  //#endregion
223
254
  //#region src/components/DialogError.tsx
224
255
  /**
@@ -891,7 +922,7 @@ function CommentInbox({ comments, counts, isLoading, nextCursor, collections, ac
891
922
  id: "aHKcKc",
892
923
  message: "Previous page"
893
924
  }),
894
- children: /* @__PURE__ */ jsx(CaretLeft, { className: "h-4 w-4" })
925
+ children: /* @__PURE__ */ jsx(CaretPrev, { className: "h-4 w-4" })
895
926
  }),
896
927
  /* @__PURE__ */ jsxs("span", {
897
928
  className: "text-sm",
@@ -915,7 +946,7 @@ function CommentInbox({ comments, counts, isLoading, nextCursor, collections, ac
915
946
  id: "/dqduX",
916
947
  message: "Next page"
917
948
  }),
918
- children: /* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4" })
949
+ children: /* @__PURE__ */ jsx(CaretNext, { className: "h-4 w-4" })
919
950
  })
920
951
  ]
921
952
  })]
@@ -1226,7 +1257,9 @@ async function unscheduleContent(collection, id) {
1226
1257
  * Get a preview URL for content
1227
1258
  *
1228
1259
  * Returns a signed URL that allows viewing draft content.
1229
- * 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.
1230
1263
  */
1231
1264
  async function getPreviewUrl(collection, id, options) {
1232
1265
  try {
@@ -2489,22 +2522,37 @@ async function uninstallMarketplacePlugin(id, opts = {}) {
2489
2522
  async function checkPluginUpdates() {
2490
2523
  return (await parseApiResponse(await apiFetch(`${API_BASE}/admin/plugins/updates`), "Failed to check for updates")).items;
2491
2524
  }
2492
- /** 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
+ */
2493
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)",
2494
2539
  "read:content": "Read your content",
2495
2540
  "write:content": "Create, update, and delete content",
2496
2541
  "read:media": "Access your media library",
2497
2542
  "write:media": "Upload and manage media",
2543
+ "read:users": "Read user accounts",
2498
2544
  "network:fetch": "Make network requests",
2499
2545
  "network:fetch:any": "Make network requests to any host (unrestricted)"
2500
2546
  };
2547
+ /** Capability names that grant scoped network access (legacy + canonical). */
2548
+ const NETWORK_REQUEST_CAPABILITIES = new Set(["network:request", "network:fetch"]);
2501
2549
  /**
2502
2550
  * Get a human-readable description for a capability.
2503
- * For network:fetch, appends the allowed hosts if provided.
2551
+ * For scoped network capabilities, appends the allowed hosts if provided.
2504
2552
  */
2505
2553
  function describeCapability(capability, allowedHosts) {
2506
2554
  const base = CAPABILITY_LABELS[capability] ?? capability;
2507
- 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(", ")}`;
2508
2556
  return base;
2509
2557
  }
2510
2558
 
@@ -2640,6 +2688,33 @@ function useCurrentUser() {
2640
2688
  });
2641
2689
  }
2642
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
+
2643
2718
  //#endregion
2644
2719
  //#region src/lib/url.ts
2645
2720
  /**
@@ -2693,676 +2768,123 @@ function safeIconUrl(url, width) {
2693
2768
  }
2694
2769
 
2695
2770
  //#endregion
2696
- //#region src/components/BlockKitFieldWidget.tsx
2697
- /**
2698
- * Renders Block Kit elements as a field widget for sandboxed plugins.
2699
- * Decomposes a JSON value into per-element values keyed by action_id,
2700
- * and recomposes on change.
2701
- */
2702
- function BlockKitFieldWidget({ label, elements, value, onChange }) {
2703
- const obj = value && typeof value === "object" ? value : {};
2704
- const objRef = React.useRef(obj);
2705
- objRef.current = obj;
2706
- const handleElementChange = React.useCallback((actionId, elementValue) => {
2707
- onChange({
2708
- ...objRef.current,
2709
- [actionId]: elementValue
2710
- });
2711
- }, [onChange]);
2712
- const validElements = elements.filter((el) => el.action_id);
2713
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
2714
- className: "text-sm font-medium leading-none",
2715
- children: label
2716
- }), /* @__PURE__ */ jsx("div", {
2717
- className: "mt-2 space-y-3",
2718
- children: validElements.map((el) => /* @__PURE__ */ jsx(BlockKitFieldElement, {
2719
- element: el,
2720
- value: obj[el.action_id],
2721
- onChange: handleElementChange
2722
- }, el.action_id))
2723
- })] });
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
+ };
2724
2786
  }
2725
- function BlockKitFieldElement({ element, value, onChange }) {
2726
- switch (element.type) {
2727
- case "text_input": return /* @__PURE__ */ jsx(Input, {
2728
- label: element.label,
2729
- placeholder: element.placeholder,
2730
- value: typeof value === "string" ? value : "",
2731
- onChange: (e) => onChange(element.action_id, e.target.value)
2732
- });
2733
- case "number_input": return /* @__PURE__ */ jsx(Input, {
2734
- label: element.label,
2735
- type: "number",
2736
- value: typeof value === "number" ? String(value) : "",
2737
- onChange: (e) => {
2738
- const n = Number(e.target.value);
2739
- onChange(element.action_id, e.target.value && Number.isFinite(n) ? n : void 0);
2740
- }
2741
- });
2742
- case "toggle": return /* @__PURE__ */ jsx(Switch, {
2743
- label: element.label,
2744
- checked: !!value,
2745
- onCheckedChange: (checked) => onChange(element.action_id, checked)
2746
- });
2747
- case "select": {
2748
- const options = Array.isArray(element.options) ? element.options : [];
2749
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
2750
- className: "text-sm font-medium mb-1.5 block",
2751
- children: element.label
2752
- }), /* @__PURE__ */ jsxs("select", {
2753
- className: "flex w-full rounded-md border border-kumo-line bg-transparent px-3 py-2 text-sm",
2754
- value: typeof value === "string" ? value : "",
2755
- onChange: (e) => onChange(element.action_id, e.target.value),
2756
- children: [/* @__PURE__ */ jsx("option", {
2757
- value: "",
2758
- children: "Select..."
2759
- }), options.map((opt) => /* @__PURE__ */ jsx("option", {
2760
- value: opt.value,
2761
- children: opt.label
2762
- }, opt.value))]
2763
- })] });
2764
- }
2765
- default: return /* @__PURE__ */ jsxs("div", {
2766
- className: "text-sm text-kumo-subtle",
2767
- children: ["Unsupported widget element type: ", element.type]
2768
- });
2769
- }
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];
2770
2806
  }
2771
2807
 
2772
2808
  //#endregion
2773
- //#region src/components/editor/DocumentOutline.tsx
2809
+ //#region src/components/MediaPickerModal.tsx
2774
2810
  /**
2775
- * Document Outline
2811
+ * Media Picker Modal
2776
2812
  *
2777
- * Displays a tree structure of headings from the TipTap editor.
2778
- * - Shows H1 at root, H2 indented, H3 further indented
2779
- * - Click-to-navigate to heading position
2780
- * - 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.
2781
2816
  */
2782
- function getIndentClass(level) {
2783
- switch (level) {
2784
- case 1: return "ps-0";
2785
- case 2: return "ps-4";
2786
- case 3: return "ps-8";
2787
- default: return "ps-0";
2788
- }
2789
- }
2790
- function getTextClass(level) {
2791
- switch (level) {
2792
- case 1: return "font-medium";
2793
- case 2: return "font-normal";
2794
- case 3: return "font-normal text-kumo-subtle";
2795
- default: return "font-normal";
2796
- }
2797
- }
2798
2817
  /**
2799
- * Extract headings from the TipTap editor document
2818
+ * Probe image URL to get dimensions
2800
2819
  */
2801
- function extractHeadings(editor) {
2802
- if (!editor) return [];
2803
- const headings = [];
2804
- const doc = editor.state.doc;
2805
- let key = 0;
2806
- doc.descendants((node, pos) => {
2807
- if (node.type.name === "heading") {
2808
- const rawLevel = node.attrs.level;
2809
- const level = typeof rawLevel === "number" ? rawLevel : 1;
2810
- const text = node.textContent || "";
2811
- if (text.trim()) headings.push({
2812
- level,
2813
- text,
2814
- pos,
2815
- 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
2816
2827
  });
2817
- }
2818
- });
2819
- return headings;
2820
- }
2821
- /**
2822
- * Find the current heading based on cursor position
2823
- */
2824
- function findCurrentHeading(headings, cursorPos) {
2825
- if (headings.length === 0) return null;
2826
- let current = null;
2827
- for (const heading of headings) if (heading.pos <= cursorPos) current = heading;
2828
- else break;
2829
- return current;
2830
- }
2831
- /**
2832
- * Document outline component showing heading tree structure
2833
- */
2834
- function DocumentOutline({ editor, className }) {
2835
- const { _: _t } = useLingui();
2836
- const [isExpanded, setIsExpanded] = React.useState(true);
2837
- const [headings, setHeadings] = React.useState([]);
2838
- const [currentPos, setCurrentPos] = React.useState(0);
2839
- React.useEffect(() => {
2840
- if (!editor) return;
2841
- const updateHeadings = () => {
2842
- setHeadings(extractHeadings(editor));
2843
- };
2844
- updateHeadings();
2845
- editor.on("update", updateHeadings);
2846
- return () => {
2847
- editor.off("update", updateHeadings);
2848
- };
2849
- }, [editor]);
2850
- React.useEffect(() => {
2851
- if (!editor) return;
2852
- const updatePosition = () => {
2853
- const { from } = editor.state.selection;
2854
- setCurrentPos(from);
2855
- };
2856
- updatePosition();
2857
- editor.on("selectionUpdate", updatePosition);
2858
- return () => {
2859
- editor.off("selectionUpdate", updatePosition);
2860
- };
2861
- }, [editor]);
2862
- const currentHeading = findCurrentHeading(headings, currentPos);
2863
- const handleHeadingClick = (heading) => {
2864
- if (!editor) return;
2865
- editor.chain().focus().setTextSelection(heading.pos).scrollIntoView().run();
2866
- };
2867
- return /* @__PURE__ */ jsxs("div", {
2868
- className: cn("space-y-2", className),
2869
- children: [/* @__PURE__ */ jsxs(Button, {
2870
- variant: "ghost",
2871
- size: "sm",
2872
- className: "w-full justify-between px-2 h-8",
2873
- onClick: () => setIsExpanded(!isExpanded),
2874
- children: [/* @__PURE__ */ jsxs("span", {
2875
- className: "flex items-center gap-2",
2876
- children: [/* @__PURE__ */ jsx(List, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", {
2877
- className: "font-semibold",
2878
- children: _t({
2879
- id: "dD7NPy",
2880
- message: "Outline"
2881
- })
2882
- })]
2883
- }), isExpanded ? /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4" })]
2884
- }), isExpanded && /* @__PURE__ */ jsx("div", {
2885
- className: "space-y-0.5",
2886
- children: headings.length === 0 ? /* @__PURE__ */ jsx("p", {
2887
- className: "text-sm text-kumo-subtle px-2 py-1",
2888
- children: _t({
2889
- id: "MMd//L",
2890
- message: "No headings in document"
2891
- })
2892
- }) : headings.map((heading) => {
2893
- const isCurrent = currentHeading?.key === heading.key;
2894
- return /* @__PURE__ */ jsx("button", {
2895
- type: "button",
2896
- onClick: () => handleHeadingClick(heading),
2897
- 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"),
2898
- title: heading.text,
2899
- children: heading.text
2900
- }, heading.key);
2901
- })
2902
- })]
2903
- });
2904
- }
2905
-
2906
- //#endregion
2907
- //#region src/components/PluginFieldErrorBoundary.tsx
2908
- /**
2909
- * Error boundary that wraps trusted plugin field widgets.
2910
- * On render error, shows a warning with a retry button instead of crashing the editor.
2911
- */
2912
- var PluginFieldErrorBoundary = class extends React.Component {
2913
- constructor(props) {
2914
- super(props);
2915
- this.state = { hasError: false };
2916
- }
2917
- static getDerivedStateFromError(error) {
2918
- return {
2919
- hasError: true,
2920
- error
2921
2828
  };
2922
- }
2923
- render() {
2924
- if (this.state.hasError) return /* @__PURE__ */ jsxs("div", {
2925
- className: "rounded-md border border-kumo-danger/50 bg-kumo-danger/5 p-3",
2926
- children: [
2927
- /* @__PURE__ */ jsx("p", {
2928
- className: "text-sm font-medium text-kumo-danger",
2929
- children: "Plugin widget error"
2930
- }),
2931
- /* @__PURE__ */ jsx("p", {
2932
- className: "mt-1 text-xs text-kumo-subtle",
2933
- children: this.state.error?.message || "The plugin field widget failed to render."
2934
- }),
2935
- /* @__PURE__ */ jsx("button", {
2936
- type: "button",
2937
- className: "mt-2 text-xs font-medium text-kumo-brand underline",
2938
- onClick: () => this.setState({
2939
- hasError: false,
2940
- error: void 0
2941
- }),
2942
- children: "Retry"
2943
- })
2944
- ]
2945
- });
2946
- return this.props.children;
2947
- }
2948
- };
2949
-
2950
- //#endregion
2951
- //#region src/components/RepeaterField.tsx
2952
- /**
2953
- * RepeaterField — renders a list of repeating sub-field groups in the content editor.
2954
- *
2955
- * Each item is a collapsible card containing the defined sub-fields.
2956
- * Items can be added, removed, and reordered via drag-and-drop.
2957
- */
2958
- function ensureKeys$1(items) {
2959
- return items.map((item, i) => {
2960
- const obj = typeof item === "object" && item !== null ? item : {};
2961
- return {
2962
- ...obj,
2963
- _key: obj._key || `item-${i}-${Date.now()}`
2829
+ img.onerror = () => {
2830
+ reject(/* @__PURE__ */ new Error("Failed to load image"));
2964
2831
  };
2832
+ img.src = url;
2965
2833
  });
2966
2834
  }
2967
- function stripKeys$1(items) {
2968
- return items.map(({ _key, ...rest }) => rest);
2969
- }
2970
- function RepeaterField({ label, id, value, onChange, subFields, minItems = 0, maxItems }) {
2835
+ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "image/", title: providedTitle }) {
2971
2836
  const { _: _t } = useLingui();
2972
- const rawItems = Array.isArray(value) ? value : [];
2973
- const [items, setItems] = React.useState(() => ensureKeys$1(rawItems));
2974
- const [collapsedItems, setCollapsedItems] = React.useState(/* @__PURE__ */ new Set());
2837
+ const title = providedTitle ?? _t({
2838
+ id: "bAiEJP",
2839
+ message: "Select Image"
2840
+ });
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({});
2975
2850
  React.useEffect(() => {
2976
- const incoming = Array.isArray(value) ? value : [];
2977
- setItems((prev) => incoming.map((item, i) => {
2978
- const obj = typeof item === "object" && item !== null ? item : {};
2979
- const existingKey = obj._key || prev[i]?._key;
2980
- return {
2981
- ...obj,
2982
- _key: existingKey || `item-${i}-${Date.now()}`
2983
- };
2984
- }));
2985
- }, [value]);
2986
- const emitChange = (updated) => {
2987
- setItems(updated);
2988
- onChange(stripKeys$1(updated));
2989
- };
2990
- const handleAdd = () => {
2991
- if (maxItems && items.length >= maxItems) return;
2992
- const newItem = { _key: `item-${Date.now()}` };
2993
- for (const sf of subFields) newItem[sf.slug] = sf.type === "boolean" ? false : sf.type === "number" || sf.type === "integer" ? null : "";
2994
- emitChange([...items, newItem]);
2995
- };
2996
- const handleRemove = (key) => {
2997
- if (items.length <= minItems) return;
2998
- emitChange(items.filter((item) => item._key !== key));
2999
- };
3000
- const handleItemChange = (key, fieldSlug, fieldValue) => {
3001
- emitChange(items.map((item) => item._key === key ? {
3002
- ...item,
3003
- [fieldSlug]: fieldValue
3004
- } : item));
3005
- };
3006
- const handleDragEnd = (event) => {
3007
- const { active, over } = event;
3008
- if (!over || active.id === over.id) return;
3009
- const oldIndex = items.findIndex((item) => item._key === active.id);
3010
- const newIndex = items.findIndex((item) => item._key === over.id);
3011
- if (oldIndex === -1 || newIndex === -1) return;
3012
- emitChange(arrayMove(items, oldIndex, newIndex));
3013
- };
3014
- const toggleCollapse = (key) => {
3015
- setCollapsedItems((prev) => {
3016
- const next = new Set(prev);
3017
- if (next.has(key)) next.delete(key);
3018
- else next.add(key);
3019
- return next;
3020
- });
3021
- };
3022
- const canAdd = !maxItems || items.length < maxItems;
3023
- const canRemove = items.length > minItems;
3024
- return /* @__PURE__ */ jsxs("div", {
3025
- className: "space-y-2",
3026
- children: [/* @__PURE__ */ jsxs("div", {
3027
- className: "flex items-center justify-between",
3028
- children: [/* @__PURE__ */ jsxs("label", {
3029
- htmlFor: id,
3030
- className: "text-sm font-medium",
3031
- children: [label, items.length > 0 && /* @__PURE__ */ jsx("span", {
3032
- className: "ms-2 text-kumo-subtle font-normal",
3033
- children: i18n._({
3034
- id: "JcH6Wc",
3035
- message: "{0, plural, one {(# item)} other {(# items)}}",
3036
- values: { 0: items.length }
3037
- })
3038
- })]
3039
- }), canAdd && /* @__PURE__ */ jsx(Button, {
3040
- variant: "outline",
3041
- size: "sm",
3042
- icon: /* @__PURE__ */ jsx(Plus, {}),
3043
- onClick: handleAdd,
3044
- children: _t({
3045
- id: "qgF8ew",
3046
- message: "Add Item"
3047
- })
3048
- })]
3049
- }), items.length === 0 ? /* @__PURE__ */ jsxs("div", {
3050
- className: "border-2 border-dashed rounded-lg p-6 text-center text-kumo-subtle",
3051
- children: [/* @__PURE__ */ jsx("p", {
3052
- className: "text-sm",
3053
- children: _t({
3054
- id: "bWmJ0A",
3055
- message: "No items yet"
3056
- })
3057
- }), canAdd && /* @__PURE__ */ jsx(Button, {
3058
- variant: "outline",
3059
- size: "sm",
3060
- className: "mt-2",
3061
- icon: /* @__PURE__ */ jsx(Plus, {}),
3062
- onClick: handleAdd,
3063
- children: _t({
3064
- id: "924qJl",
3065
- message: "Add First Item"
3066
- })
3067
- })]
3068
- }) : /* @__PURE__ */ jsx(DndContext, {
3069
- collisionDetection: closestCenter,
3070
- onDragEnd: handleDragEnd,
3071
- children: /* @__PURE__ */ jsx(SortableContext, {
3072
- items: items.map((item) => item._key),
3073
- strategy: verticalListSortingStrategy,
3074
- children: /* @__PURE__ */ jsx("div", {
3075
- className: "space-y-2",
3076
- children: items.map((item, index) => /* @__PURE__ */ jsx(SortableRepeaterItem, {
3077
- item,
3078
- index,
3079
- subFields,
3080
- isCollapsed: collapsedItems.has(item._key),
3081
- onToggleCollapse: () => toggleCollapse(item._key),
3082
- onRemove: canRemove ? () => handleRemove(item._key) : void 0,
3083
- onChange: (fieldSlug, fieldValue) => handleItemChange(item._key, fieldSlug, fieldValue)
3084
- }, item._key))
3085
- })
3086
- })
3087
- })]
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: []
3088
2866
  });
3089
- }
3090
- function SortableRepeaterItem({ item, index, subFields, isCollapsed, onToggleCollapse, onRemove, onChange }) {
3091
- const { _: _t2 } = useLingui();
3092
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item._key });
3093
- const style = {
3094
- transform: CSS.Transform.toString(transform),
3095
- transition
3096
- };
3097
- const summaryField = subFields.find((sf) => sf.type === "string" || sf.type === "text");
3098
- const summaryLabel = (summaryField ? item[summaryField.slug] || "" : "") || _t2({
3099
- id: "y/5CE8",
3100
- message: "Item {0}",
3101
- values: { 0: index + 1 }
3102
- });
3103
- return /* @__PURE__ */ jsxs("div", {
3104
- ref: setNodeRef,
3105
- style,
3106
- className: cn("border rounded-lg bg-kumo-base", isDragging && "opacity-50 ring-2 ring-kumo-brand"),
3107
- children: [/* @__PURE__ */ jsxs("div", {
3108
- className: "flex items-center gap-2 px-3 py-2 border-b cursor-pointer",
3109
- onClick: onToggleCollapse,
3110
- children: [
3111
- /* @__PURE__ */ jsx(DotsSixVertical, {
3112
- className: "h-4 w-4 text-kumo-subtle cursor-grab shrink-0",
3113
- ...attributes,
3114
- ...listeners,
3115
- onClick: (e) => e.stopPropagation()
3116
- }),
3117
- 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" }),
3118
- /* @__PURE__ */ jsx("span", {
3119
- className: "text-sm font-medium flex-1 truncate",
3120
- children: summaryLabel
3121
- }),
3122
- onRemove && /* @__PURE__ */ jsx(Button, {
3123
- variant: "ghost",
3124
- shape: "square",
3125
- onClick: (e) => {
3126
- e.stopPropagation();
3127
- onRemove();
3128
- },
3129
- "aria-label": _t2({
3130
- id: "mU3j1b",
3131
- message: "Remove item {0}",
3132
- values: { 0: index + 1 }
3133
- }),
3134
- children: /* @__PURE__ */ jsx(Trash, { className: "h-3.5 w-3.5 text-kumo-danger" })
3135
- })
3136
- ]
3137
- }), !isCollapsed && /* @__PURE__ */ jsx("div", {
3138
- className: "p-3 space-y-3",
3139
- children: subFields.map((sf) => /* @__PURE__ */ jsx(SubFieldInput, {
3140
- subField: sf,
3141
- value: item[sf.slug],
3142
- onChange: (v) => onChange(sf.slug, v)
3143
- }, sf.slug))
3144
- })]
3145
- });
3146
- }
3147
- function SubFieldInput({ subField, value, onChange }) {
3148
- const { _: _t3 } = useLingui();
3149
- switch (subField.type) {
3150
- case "string": return /* @__PURE__ */ jsx(Input, {
3151
- label: subField.label,
3152
- value: typeof value === "string" ? value : "",
3153
- onChange: (e) => onChange(e.target.value),
3154
- required: subField.required,
3155
- dir: "auto"
3156
- });
3157
- case "text": return /* @__PURE__ */ jsx(InputArea, {
3158
- label: subField.label,
3159
- value: typeof value === "string" ? value : "",
3160
- onChange: (e) => onChange(e.target.value),
3161
- required: subField.required,
3162
- rows: 3,
3163
- dir: "auto"
3164
- });
3165
- case "number":
3166
- case "integer": return /* @__PURE__ */ jsx(Input, {
3167
- label: subField.label,
3168
- type: "number",
3169
- value: typeof value === "number" ? String(value) : "",
3170
- onChange: (e) => onChange(e.target.value ? Number(e.target.value) : null),
3171
- required: subField.required,
3172
- step: subField.type === "integer" ? "1" : "any"
3173
- });
3174
- case "boolean": return /* @__PURE__ */ jsxs("label", {
3175
- className: "flex items-center gap-2",
3176
- children: [/* @__PURE__ */ jsx("input", {
3177
- type: "checkbox",
3178
- checked: Boolean(value),
3179
- onChange: (e) => onChange(e.target.checked)
3180
- }), /* @__PURE__ */ jsx("span", {
3181
- className: "text-sm",
3182
- children: subField.label
3183
- })]
3184
- });
3185
- case "datetime": return /* @__PURE__ */ jsx(Input, {
3186
- label: subField.label,
3187
- type: "datetime-local",
3188
- value: typeof value === "string" ? value : "",
3189
- onChange: (e) => onChange(e.target.value),
3190
- required: subField.required
3191
- });
3192
- case "select": return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
3193
- className: "text-sm font-medium",
3194
- children: subField.label
3195
- }), /* @__PURE__ */ jsxs("select", {
3196
- className: "w-full mt-1 rounded-md border px-3 py-2 text-sm",
3197
- value: typeof value === "string" ? value : "",
3198
- onChange: (e) => onChange(e.target.value),
3199
- required: subField.required,
3200
- children: [/* @__PURE__ */ jsx("option", {
3201
- value: "",
3202
- children: _t3({
3203
- id: "O/7I0o",
3204
- message: "Select..."
3205
- })
3206
- }), subField.options?.map((opt) => /* @__PURE__ */ jsx("option", {
3207
- value: opt,
3208
- children: opt
3209
- }, opt))]
3210
- })] });
3211
- default: return /* @__PURE__ */ jsx(Input, {
3212
- label: subField.label,
3213
- value: typeof value === "string" ? value : "",
3214
- onChange: (e) => onChange(e.target.value)
3215
- });
3216
- }
3217
- }
3218
-
3219
- //#endregion
3220
- //#region src/lib/hooks.ts
3221
- /**
3222
- * Returns a stable function reference that always calls the latest version
3223
- * of the provided callback. Useful for event listeners in effects where
3224
- * you don't want the listener to be torn down and re-added when the
3225
- * callback identity changes.
3226
- */
3227
- function useStableCallback(callback) {
3228
- const ref = React.useRef(callback);
3229
- React.useLayoutEffect(() => {
3230
- ref.current = callback;
3231
- });
3232
- return React.useCallback((...args) => ref.current(...args), []);
3233
- }
3234
- /**
3235
- * Returns a debounced version of a value that only updates after the
3236
- * specified delay has elapsed since the last change. Useful for search
3237
- * inputs that trigger API calls.
3238
- */
3239
- function useDebouncedValue$1(value, delay) {
3240
- const [debouncedValue, setDebouncedValue] = React.useState(value);
3241
- React.useEffect(() => {
3242
- const timer = setTimeout(setDebouncedValue, delay, value);
3243
- return () => clearTimeout(timer);
3244
- }, [value, delay]);
3245
- return debouncedValue;
3246
- }
3247
-
3248
- //#endregion
3249
- //#region src/lib/media-utils.ts
3250
- function providerItemToMediaItem(providerId, item) {
3251
- return {
3252
- id: item.id,
3253
- filename: item.filename,
3254
- mimeType: item.mimeType,
3255
- url: item.previewUrl || "",
3256
- size: item.size || 0,
3257
- width: item.width,
3258
- height: item.height,
3259
- alt: item.alt,
3260
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3261
- provider: providerId,
3262
- meta: item.meta
3263
- };
3264
- }
3265
- function getFileIcon(mimeType) {
3266
- if (mimeType.startsWith("video/")) return "🎬";
3267
- if (mimeType.startsWith("audio/")) return "🎵";
3268
- if (mimeType.includes("pdf")) return "📄";
3269
- if (mimeType.includes("document") || mimeType.includes("word")) return "📝";
3270
- if (mimeType.includes("spreadsheet") || mimeType.includes("excel")) return "📊";
3271
- return "📁";
3272
- }
3273
- function formatFileSize(bytes) {
3274
- if (bytes === 0) return "0 B";
3275
- const k = 1024;
3276
- const sizes = [
3277
- "B",
3278
- "KB",
3279
- "MB",
3280
- "GB"
3281
- ];
3282
- const i = Math.floor(Math.log(bytes) / Math.log(k));
3283
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
3284
- }
3285
-
3286
- //#endregion
3287
- //#region src/components/MediaPickerModal.tsx
3288
- /**
3289
- * Media Picker Modal
3290
- *
3291
- * A modal dialog for selecting media from the library or uploading new files.
3292
- * Supports multiple media providers with tabbed navigation.
3293
- * Used by the rich text editor and image field components.
3294
- */
3295
- /**
3296
- * Probe image URL to get dimensions
3297
- */
3298
- function probeImageDimensions(url) {
3299
- return new Promise((resolve, reject) => {
3300
- const img = new window.Image();
3301
- img.onload = () => {
3302
- resolve({
3303
- width: img.naturalWidth,
3304
- height: img.naturalHeight
3305
- });
3306
- };
3307
- img.onerror = () => {
3308
- reject(/* @__PURE__ */ new Error("Failed to load image"));
3309
- };
3310
- img.src = url;
3311
- });
3312
- }
3313
- function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "image/", title: providedTitle }) {
3314
- const { _: _t } = useLingui();
3315
- const title = providedTitle ?? _t({
3316
- id: "bAiEJP",
3317
- message: "Select Image"
3318
- });
3319
- const queryClient = useQueryClient();
3320
- const [selectedItem, setSelectedItem] = React.useState(null);
3321
- const [activeProvider, setActiveProvider] = React.useState("local");
3322
- const [searchQuery, setSearchQuery] = React.useState("");
3323
- const fileInputRef = React.useRef(null);
3324
- const [imageUrl, setImageUrl] = React.useState("");
3325
- const [isProbing, setIsProbing] = React.useState(false);
3326
- const [urlError, setUrlError] = React.useState(null);
3327
- const [providerDimensions, setProviderDimensions] = React.useState({});
3328
- React.useEffect(() => {
3329
- if (open) {
3330
- setSelectedItem(null);
3331
- setActiveProvider("local");
3332
- setSearchQuery("");
3333
- setImageUrl("");
3334
- setUrlError(null);
3335
- setUploadError(null);
3336
- setProviderDimensions({});
3337
- }
3338
- }, [open]);
3339
- const { data: providers } = useQuery({
3340
- queryKey: ["media-providers"],
3341
- queryFn: fetchMediaProviders,
3342
- enabled: open,
3343
- placeholderData: []
3344
- });
3345
- const activeProviderInfo = React.useMemo(() => {
3346
- if (activeProvider === "local") return {
3347
- id: "local",
3348
- name: "Library",
3349
- icon: void 0,
3350
- capabilities: {
3351
- browse: true,
3352
- search: false,
3353
- upload: true,
3354
- delete: true
3355
- }
3356
- };
3357
- return providers?.find((p) => p.id === activeProvider);
3358
- }, [activeProvider, providers]);
3359
- const { data: localData, isLoading: localLoading } = useQuery({
3360
- queryKey: ["media", mimeTypeFilter],
3361
- queryFn: () => fetchMediaList({
3362
- mimeType: mimeTypeFilter,
3363
- limit: 50
3364
- }),
3365
- enabled: open && activeProvider === "local"
2867
+ const activeProviderInfo = React.useMemo(() => {
2868
+ if (activeProvider === "local") return {
2869
+ id: "local",
2870
+ name: "Library",
2871
+ icon: void 0,
2872
+ capabilities: {
2873
+ browse: true,
2874
+ search: false,
2875
+ upload: true,
2876
+ delete: true
2877
+ }
2878
+ };
2879
+ return providers?.find((p) => p.id === activeProvider);
2880
+ }, [activeProvider, providers]);
2881
+ const { data: localData, isLoading: localLoading } = useQuery({
2882
+ queryKey: ["media", mimeTypeFilter],
2883
+ queryFn: () => fetchMediaList({
2884
+ mimeType: mimeTypeFilter,
2885
+ limit: 50
2886
+ }),
2887
+ enabled: open && activeProvider === "local"
3366
2888
  });
3367
2889
  const { data: providerData, isLoading: providerLoading } = useQuery({
3368
2890
  queryKey: [
@@ -3813,180 +3335,846 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3813
3335
  }
3814
3336
  }, item.id))
3815
3337
  })
3816
- }),
3817
- /* @__PURE__ */ jsxs("div", {
3818
- className: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 border-t pt-4",
3819
- children: [
3820
- /* @__PURE__ */ jsx("div", {
3821
- className: "flex-1 text-sm text-kumo-subtle",
3822
- children: selectedItem && /* @__PURE__ */ jsxs("span", { children: [
3823
- _t({
3824
- id: "SVRHAR",
3825
- message: "Selected:"
3826
- }),
3827
- " ",
3828
- /* @__PURE__ */ jsx("strong", { children: selectedItem.item.filename }),
3829
- selectedItem.providerId !== "local" && /* @__PURE__ */ jsx("span", {
3830
- className: "ms-2 text-xs",
3831
- children: _t({
3832
- id: "qjriNt",
3833
- message: "(from {0})",
3834
- values: { 0: providers?.find((p) => p.id === selectedItem.providerId)?.name }
3835
- })
3836
- })
3837
- ] })
3838
- }),
3839
- /* @__PURE__ */ jsx(Button, {
3840
- variant: "outline",
3841
- onClick: handleClose,
3842
- children: _t({
3843
- id: "dEgA5A",
3844
- message: "Cancel"
3845
- })
3846
- }),
3847
- /* @__PURE__ */ jsx(Button, {
3848
- onClick: handleConfirm,
3849
- disabled: !selectedItem,
3850
- children: _t({
3851
- id: "S8Yqbl",
3852
- message: "Insert"
3853
- })
3854
- })
3855
- ]
3338
+ }),
3339
+ /* @__PURE__ */ jsxs("div", {
3340
+ className: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 border-t pt-4",
3341
+ children: [
3342
+ /* @__PURE__ */ jsx("div", {
3343
+ className: "flex-1 text-sm text-kumo-subtle",
3344
+ children: selectedItem && /* @__PURE__ */ jsxs("span", { children: [
3345
+ _t({
3346
+ id: "SVRHAR",
3347
+ message: "Selected:"
3348
+ }),
3349
+ " ",
3350
+ /* @__PURE__ */ jsx("strong", { children: selectedItem.item.filename }),
3351
+ selectedItem.providerId !== "local" && /* @__PURE__ */ jsx("span", {
3352
+ className: "ms-2 text-xs",
3353
+ children: _t({
3354
+ id: "qjriNt",
3355
+ message: "(from {0})",
3356
+ values: { 0: providers?.find((p) => p.id === selectedItem.providerId)?.name }
3357
+ })
3358
+ })
3359
+ ] })
3360
+ }),
3361
+ /* @__PURE__ */ jsx(Button, {
3362
+ variant: "outline",
3363
+ onClick: handleClose,
3364
+ children: _t({
3365
+ id: "dEgA5A",
3366
+ message: "Cancel"
3367
+ })
3368
+ }),
3369
+ /* @__PURE__ */ jsx(Button, {
3370
+ onClick: handleConfirm,
3371
+ disabled: !selectedItem,
3372
+ children: _t({
3373
+ id: "S8Yqbl",
3374
+ message: "Insert"
3375
+ })
3376
+ })
3377
+ ]
3378
+ })
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"
3856
3979
  })
3857
- ]
3858
- })
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
+ })]
3859
4020
  });
3860
4021
  }
3861
- function MediaPickerItem({ item, selected, onClick, onDoubleClick, onDimensionsDetected }) {
4022
+ function SortableRepeaterItem({ item, index, subFields, isCollapsed, onToggleCollapse, onRemove, onChange }) {
3862
4023
  const { _: _t2 } = useLingui();
3863
- const isImage = item.mimeType.startsWith("image/");
3864
- const needsDimensions = isImage && (!item.width || !item.height);
3865
- const handleImageLoad = React.useCallback((e) => {
3866
- if (needsDimensions && onDimensionsDetected) {
3867
- const img = e.currentTarget;
3868
- if (img.naturalWidth && img.naturalHeight) onDimensionsDetected(item.id, img.naturalWidth, img.naturalHeight);
3869
- }
3870
- }, [
3871
- needsDimensions,
3872
- onDimensionsDetected,
3873
- item.id
3874
- ]);
3875
- return /* @__PURE__ */ jsx("li", {
3876
- role: "option",
3877
- "aria-selected": selected,
3878
- children: /* @__PURE__ */ jsxs("button", {
3879
- type: "button",
3880
- 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"),
3881
- onClick,
3882
- onDoubleClick,
3883
- "aria-label": _t2({
3884
- id: "qSiUsc",
3885
- message: "{0}{1}",
3886
- values: {
3887
- 0: item.filename,
3888
- 1: selected ? _t2({
3889
- id: "QbxreS",
3890
- message: " (selected)"
3891
- }) : ""
3892
- }
3893
- }),
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,
3894
4042
  children: [
3895
- isImage ? /* @__PURE__ */ jsx("img", {
3896
- src: item.url,
3897
- alt: "",
3898
- className: "h-full w-full object-cover",
3899
- onLoad: handleImageLoad
3900
- }) : /* @__PURE__ */ jsx("div", {
3901
- className: "flex h-full w-full items-center justify-center bg-kumo-tint",
3902
- children: /* @__PURE__ */ jsx("span", {
3903
- className: "text-3xl",
3904
- "aria-hidden": "true",
3905
- children: getFileIcon(item.mimeType)
3906
- })
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()
3907
4048
  }),
3908
- selected && /* @__PURE__ */ jsx("div", {
3909
- className: "absolute inset-0 bg-kumo-brand/20 flex items-center justify-center",
3910
- "aria-hidden": "true",
3911
- children: /* @__PURE__ */ jsx("div", {
3912
- className: "bg-kumo-brand text-white rounded-full p-1",
3913
- children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
3914
- })
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
3915
4053
  }),
3916
- /* @__PURE__ */ jsx("div", {
3917
- className: "absolute bottom-0 start-0 end-0 bg-gradient-to-t from-black/60 to-transparent p-2",
3918
- "aria-hidden": "true",
3919
- children: /* @__PURE__ */ jsx("p", {
3920
- className: "text-xs text-white truncate",
3921
- children: item.filename
3922
- })
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" })
3923
4067
  })
3924
4068
  ]
3925
- })
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
+ })]
3926
4077
  });
3927
4078
  }
3928
- function ProviderMediaItem({ item, selected, onClick, onDoubleClick, onDimensionsLoaded }) {
4079
+ function SubFieldInput({ subField, value, onChange }) {
3929
4080
  const { _: _t3 } = useLingui();
3930
- const isImage = item.mimeType.startsWith("image/");
3931
- const needsDimensions = isImage && (!item.width || !item.height);
3932
- const handleImageLoad = React.useCallback((e) => {
3933
- if (needsDimensions && onDimensionsLoaded) {
3934
- const img = e.currentTarget;
3935
- if (img.naturalWidth && img.naturalHeight) onDimensionsLoaded(img.naturalWidth, img.naturalHeight);
3936
- }
3937
- }, [needsDimensions, onDimensionsLoaded]);
3938
- return /* @__PURE__ */ jsx("li", {
3939
- role: "option",
3940
- "aria-selected": selected,
3941
- children: /* @__PURE__ */ jsxs("button", {
3942
- type: "button",
3943
- 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"),
3944
- onClick,
3945
- onDoubleClick,
3946
- "aria-label": _t3({
3947
- id: "qSiUsc",
3948
- message: "{0}{1}",
3949
- values: {
3950
- 0: item.filename,
3951
- 1: selected ? _t3({
3952
- id: "QbxreS",
3953
- message: " (selected)"
3954
- }) : ""
3955
- }
3956
- }),
3957
- children: [
3958
- isImage && item.previewUrl ? /* @__PURE__ */ jsx("img", {
3959
- src: item.previewUrl,
3960
- alt: "",
3961
- className: "h-full w-full object-cover",
3962
- onLoad: handleImageLoad
3963
- }) : /* @__PURE__ */ jsx("div", {
3964
- className: "flex h-full w-full items-center justify-center bg-kumo-tint",
3965
- children: /* @__PURE__ */ jsx("span", {
3966
- className: "text-3xl",
3967
- "aria-hidden": "true",
3968
- children: getFileIcon(item.mimeType)
3969
- })
3970
- }),
3971
- selected && /* @__PURE__ */ jsx("div", {
3972
- className: "absolute inset-0 bg-kumo-brand/20 flex items-center justify-center",
3973
- "aria-hidden": "true",
3974
- children: /* @__PURE__ */ jsx("div", {
3975
- className: "bg-kumo-brand text-white rounded-full p-1",
3976
- children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
3977
- })
3978
- }),
3979
- /* @__PURE__ */ jsx("div", {
3980
- className: "absolute bottom-0 start-0 end-0 bg-gradient-to-t from-black/60 to-transparent p-2",
3981
- "aria-hidden": "true",
3982
- children: /* @__PURE__ */ jsx("p", {
3983
- className: "text-xs text-white truncate",
3984
- children: item.filename
3985
- })
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..."
3986
4137
  })
3987
- ]
3988
- })
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;
3989
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;
3990
4178
  }
3991
4179
 
3992
4180
  //#endregion
@@ -5098,7 +5286,7 @@ function BlockMenu({ editor, anchorElement, isOpen, onClose }) {
5098
5286
  type: "button",
5099
5287
  className: "flex items-center gap-2 w-full px-3 py-2 text-sm hover:bg-kumo-tint text-start",
5100
5288
  onClick: () => setShowTransforms(false),
5101
- 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" })]
5102
5290
  }),
5103
5291
  /* @__PURE__ */ jsx("div", { className: "h-px bg-kumo-line my-1" }),
5104
5292
  blockTransforms.map((transform) => /* @__PURE__ */ jsxs("button", {
@@ -5118,7 +5306,7 @@ function BlockMenu({ editor, anchorElement, isOpen, onClose }) {
5118
5306
  children: [/* @__PURE__ */ jsxs("span", {
5119
5307
  className: "flex items-center gap-2",
5120
5308
  children: [/* @__PURE__ */ jsx(Paragraph, { className: "h-4 w-4 text-kumo-subtle" }), /* @__PURE__ */ jsx("span", { children: "Turn into" })]
5121
- }), /* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4 text-kumo-subtle" })]
5309
+ }), /* @__PURE__ */ jsx(CaretNext, { className: "h-4 w-4 text-kumo-subtle" })]
5122
5310
  }),
5123
5311
  /* @__PURE__ */ jsxs("button", {
5124
5312
  type: "button",
@@ -6723,12 +6911,19 @@ function PluginBlockModal({ block, initialValues, onClose, onInsert }) {
6723
6911
  const isEditing = !!initialValues;
6724
6912
  const hasFields = block?.fields && block.fields.length > 0;
6725
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
+ })();
6726
6921
  return /* @__PURE__ */ jsx(Dialog.Root, {
6727
6922
  open: !!block,
6728
6923
  onOpenChange: (open) => !open && onClose(),
6729
6924
  children: /* @__PURE__ */ jsxs(Dialog, {
6730
6925
  className: "p-6",
6731
- size: "sm",
6926
+ size: dialogSize,
6732
6927
  children: [/* @__PURE__ */ jsxs("div", {
6733
6928
  className: "flex items-start justify-between gap-4 mb-4",
6734
6929
  children: [/* @__PURE__ */ jsxs(Dialog.Title, {
@@ -6764,6 +6959,7 @@ function PluginBlockModal({ block, initialValues, onClose, onInsert }) {
6764
6959
  }, field.action_id)) : /* @__PURE__ */ jsx(Input, {
6765
6960
  ref: inputRef,
6766
6961
  type: "url",
6962
+ className: "w-full",
6767
6963
  placeholder: block?.placeholder || "Enter URL...",
6768
6964
  value: typeof formValues.id === "string" ? formValues.id : "",
6769
6965
  onChange: (e) => handleFieldChange("id", e.target.value)
@@ -6805,6 +7001,7 @@ function BlockKitField({ field, pluginId, value, onChange }) {
6805
7001
  onChange: (e) => onChange(field.action_id, e.target.value)
6806
7002
  }) : /* @__PURE__ */ jsx(Input, {
6807
7003
  type: "text",
7004
+ className: "w-full",
6808
7005
  placeholder,
6809
7006
  value: typeof value === "string" ? value : "",
6810
7007
  onChange: (e) => onChange(field.action_id, e.target.value)
@@ -6818,6 +7015,7 @@ function BlockKitField({ field, pluginId, value, onChange }) {
6818
7015
  children: field.label
6819
7016
  }), /* @__PURE__ */ jsx(Input, {
6820
7017
  type: "number",
7018
+ className: "w-full",
6821
7019
  min,
6822
7020
  max,
6823
7021
  value: typeof value === "number" ? String(value) : "",
@@ -6848,6 +7046,14 @@ function BlockKitField({ field, pluginId, value, onChange }) {
6848
7046
  value,
6849
7047
  onChange
6850
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
+ });
6851
7057
  default: return /* @__PURE__ */ jsxs("div", {
6852
7058
  className: "text-sm text-kumo-subtle",
6853
7059
  children: ["Unknown field type: ", field.type]
@@ -7048,10 +7254,10 @@ function BlockKitRepeaterItem({ item, index, fields, pluginId, isCollapsed, onTo
7048
7254
  }),
7049
7255
  /* @__PURE__ */ jsxs("button", {
7050
7256
  type: "button",
7051
- className: "flex items-center gap-2 flex-1 min-w-0 text-left cursor-pointer",
7257
+ className: "flex items-center gap-2 flex-1 min-w-0 text-start cursor-pointer",
7052
7258
  onClick: onToggleCollapse,
7053
7259
  "aria-expanded": !isCollapsed,
7054
- children: [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" }), /* @__PURE__ */ jsx("span", {
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", {
7055
7261
  className: "text-sm font-medium flex-1 truncate",
7056
7262
  children: summaryLabel
7057
7263
  })]
@@ -7269,7 +7475,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
7269
7475
  }),
7270
7476
  icon: resolveIcon(block.icon),
7271
7477
  aliases: [block.type],
7272
- category: {
7478
+ category: block.category ?? {
7273
7479
  id: "aTofd0",
7274
7480
  message: "Embeds"
7275
7481
  },
@@ -7350,7 +7556,12 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
7350
7556
  }
7351
7557
  });
7352
7558
  React.useEffect(() => {
7353
- if (editor && onEditorReady) onEditorReady(editor);
7559
+ if (editor && onEditorReady) {
7560
+ onEditorReady(editor);
7561
+ return () => {
7562
+ onEditorReady(null);
7563
+ };
7564
+ }
7354
7565
  }, [editor, onEditorReady]);
7355
7566
  React.useEffect(() => {
7356
7567
  if (editor) registerPluginBlocks(editor, pluginBlocks);
@@ -8956,7 +9167,7 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
8956
9167
  type: "button",
8957
9168
  onClick: handleCreate,
8958
9169
  disabled: isCreating,
8959
- 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",
8960
9171
  children: [/* @__PURE__ */ jsx(Plus, { className: "w-3 h-3" }), isCreating ? "Creating..." : `Create "${trimmedInput}"`]
8961
9172
  })]
8962
9173
  })]
@@ -9229,6 +9440,25 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
9229
9440
  })) ?? []
9230
9441
  }));
9231
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
+ }
9232
9462
  const itemDataString = React.useMemo(() => item ? JSON.stringify(item.data) : "", [item?.data]);
9233
9463
  React.useEffect(() => {
9234
9464
  if (item) {
@@ -9395,7 +9625,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
9395
9625
  onSubmit: handleSubmit,
9396
9626
  className: cn("space-y-6 transition-all duration-300", isDistractionFree && "fixed inset-0 z-50 bg-kumo-base p-8 overflow-auto"),
9397
9627
  children: [/* @__PURE__ */ jsxs("div", {
9398
- 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"),
9399
9629
  children: [/* @__PURE__ */ jsxs("div", {
9400
9630
  className: "flex items-center space-x-4",
9401
9631
  children: [
@@ -9412,7 +9642,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
9412
9642
  variant: "ghost",
9413
9643
  shape: "square"
9414
9644
  }),
9415
- children: /* @__PURE__ */ jsx(ArrowLeft, {
9645
+ children: /* @__PURE__ */ jsx(ArrowPrev, {
9416
9646
  className: "h-5 w-5",
9417
9647
  "aria-hidden": "true"
9418
9648
  })
@@ -9607,6 +9837,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
9607
9837
  children: /* @__PURE__ */ jsx("div", {
9608
9838
  className: "space-y-4",
9609
9839
  children: Object.entries(fields).map(([name, field]) => {
9840
+ const fieldKey = `${name}:${item?.id ?? "new"}`;
9610
9841
  const fieldEl = /* @__PURE__ */ jsx(FieldRenderer, {
9611
9842
  name,
9612
9843
  field,
@@ -9618,14 +9849,14 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
9618
9849
  onBlockSidebarOpen: field.kind === "portableText" ? handleBlockSidebarOpen : void 0,
9619
9850
  onBlockSidebarClose: field.kind === "portableText" ? handleBlockSidebarClose : void 0,
9620
9851
  manifest
9621
- }, name);
9852
+ }, fieldKey);
9622
9853
  if (name === "featured_image" && field.kind === "image" && hasSeo && !isNew && onSeoChange) return /* @__PURE__ */ jsxs("div", {
9623
9854
  className: "grid grid-cols-1 gap-6 md:grid-cols-2",
9624
9855
  children: [/* @__PURE__ */ jsx("div", { children: fieldEl }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(SeoImageField, {
9625
9856
  seo: item?.seo,
9626
9857
  onChange: onSeoChange
9627
9858
  }) })]
9628
- }, `${name}-with-seo`);
9859
+ }, `${fieldKey}-with-seo`);
9629
9860
  return fieldEl;
9630
9861
  })
9631
9862
  })
@@ -10092,28 +10323,30 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
10092
10323
  })
10093
10324
  });
10094
10325
  case "select": {
10326
+ const selectOptions = Array.isArray(field.options) ? field.options : [];
10095
10327
  const selectItems = {};
10096
- for (const opt of field.options ?? []) selectItems[opt.value] = opt.label;
10328
+ for (const opt of selectOptions) selectItems[opt.value] = opt.label;
10097
10329
  return /* @__PURE__ */ jsx(Select, {
10098
10330
  id,
10099
10331
  label,
10100
10332
  value: typeof value === "string" ? value : "",
10101
10333
  onValueChange: (v) => handleChange(v ?? ""),
10102
10334
  items: selectItems,
10103
- children: field.options?.map((opt) => /* @__PURE__ */ jsx(Select.Option, {
10335
+ children: selectOptions.map((opt) => /* @__PURE__ */ jsx(Select.Option, {
10104
10336
  value: opt.value,
10105
10337
  children: opt.label
10106
10338
  }, opt.value))
10107
10339
  });
10108
10340
  }
10109
10341
  case "multiSelect": {
10342
+ const multiSelectOptions = Array.isArray(field.options) ? field.options : [];
10110
10343
  const selected = Array.isArray(value) ? value : [];
10111
10344
  return /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx(Label, {
10112
10345
  className: labelClass,
10113
10346
  children: label
10114
10347
  }), /* @__PURE__ */ jsx("div", {
10115
10348
  className: "mt-2 flex flex-wrap gap-x-4 gap-y-2",
10116
- children: field.options?.map((opt) => {
10349
+ children: multiSelectOptions.map((opt) => {
10117
10350
  const isChecked = selected.includes(opt.value);
10118
10351
  return /* @__PURE__ */ jsx(Checkbox, {
10119
10352
  label: opt.label,
@@ -10129,8 +10362,8 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
10129
10362
  label,
10130
10363
  id,
10131
10364
  type: "datetime-local",
10132
- value: typeof value === "string" ? value : "",
10133
- onChange: (e) => handleChange(e.target.value),
10365
+ value: toDatetimeLocalInputValue(value),
10366
+ onChange: (e) => handleChange(fromDatetimeLocalInputValue(e.target.value)),
10134
10367
  required: field.required
10135
10368
  });
10136
10369
  case "image": {
@@ -10984,7 +11217,17 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
10984
11217
  })
10985
11218
  })
10986
11219
  ]
10987
- }) }), /* @__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", {
10988
11231
  colSpan: i18n$1 ? 5 : 4,
10989
11232
  className: "px-4 py-8 text-center text-kumo-subtle",
10990
11233
  children: [
@@ -11055,7 +11298,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
11055
11298
  id: "aHKcKc",
11056
11299
  message: "Previous page"
11057
11300
  }),
11058
- children: /* @__PURE__ */ jsx(CaretLeft, {
11301
+ children: /* @__PURE__ */ jsx(CaretPrev, {
11059
11302
  className: "h-4 w-4",
11060
11303
  "aria-hidden": "true"
11061
11304
  })
@@ -11077,7 +11320,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
11077
11320
  id: "/dqduX",
11078
11321
  message: "Next page"
11079
11322
  }),
11080
- children: /* @__PURE__ */ jsx(CaretRight, {
11323
+ children: /* @__PURE__ */ jsx(CaretNext, {
11081
11324
  className: "h-4 w-4",
11082
11325
  "aria-hidden": "true"
11083
11326
  })
@@ -11132,7 +11375,17 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
11132
11375
  })
11133
11376
  })
11134
11377
  ]
11135
- }) }), /* @__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", {
11136
11389
  colSpan: 3,
11137
11390
  className: "px-4 py-8 text-center text-kumo-subtle",
11138
11391
  children: _t({
@@ -11491,6 +11744,37 @@ function StatusBadge({ status, hasPendingChanges }) {
11491
11744
  });
11492
11745
  }
11493
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
+
11494
11778
  //#endregion
11495
11779
  //#region src/components/FieldEditor.tsx
11496
11780
  const SLUG_INVALID_CHARS_REGEX = /[^a-z0-9]+/g;
@@ -12529,30 +12813,43 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
12529
12813
  return /* @__PURE__ */ jsxs("div", {
12530
12814
  className: "space-y-6",
12531
12815
  children: [
12532
- /* @__PURE__ */ jsxs("div", {
12533
- className: "flex items-center space-x-4",
12534
- children: [/* @__PURE__ */ jsx(Link$1, {
12816
+ /* @__PURE__ */ jsxs(EditorHeader, {
12817
+ leading: /* @__PURE__ */ jsx(Link$1, {
12535
12818
  to: "/content-types",
12536
- "aria-label": "Back to Content Types",
12819
+ "aria-label": _t({
12820
+ id: "oqi/nm",
12821
+ message: "Back to Content Types"
12822
+ }),
12537
12823
  className: buttonVariants({
12538
12824
  variant: "ghost",
12539
12825
  shape: "square"
12540
12826
  }),
12541
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-5 w-5" })
12542
- }), /* @__PURE__ */ jsxs("div", {
12543
- className: "flex-1",
12544
- children: [/* @__PURE__ */ jsx("h1", {
12545
- className: "text-2xl font-bold",
12546
- children: isNew ? "New Content Type" : collection?.label
12547
- }), !isNew && /* @__PURE__ */ jsxs("p", {
12548
- className: "text-kumo-subtle text-sm",
12549
- children: [/* @__PURE__ */ jsx("code", {
12550
- className: "bg-kumo-tint px-1.5 py-0.5 rounded",
12551
- children: collection?.slug
12552
- }), isFromCode && /* @__PURE__ */ jsx("span", {
12553
- className: "ms-2 text-purple-600 dark:text-purple-400",
12554
- children: "Defined in code"
12555
- })]
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
+ })
12556
12853
  })]
12557
12854
  })]
12558
12855
  }),
@@ -12571,6 +12868,7 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
12571
12868
  children: [/* @__PURE__ */ jsx("div", {
12572
12869
  className: "lg:col-span-1",
12573
12870
  children: /* @__PURE__ */ jsxs("form", {
12871
+ id: "content-type-editor-form",
12574
12872
  onSubmit: handleSubmit,
12575
12873
  className: "space-y-4",
12576
12874
  children: [
@@ -16278,7 +16576,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
16278
16576
  to: "/plugins/$pluginId/$",
16279
16577
  params: {
16280
16578
  pluginId: plugin.id,
16281
- _splat: "settings"
16579
+ _splat: ""
16282
16580
  },
16283
16581
  children: /* @__PURE__ */ jsxs(Button, {
16284
16582
  variant: "ghost",
@@ -16320,7 +16618,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
16320
16618
  }),
16321
16619
  onClick: () => setExpanded(!expanded),
16322
16620
  "aria-expanded": expanded,
16323
- 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", {
16324
16622
  className: "sr-only",
16325
16623
  children: [
16326
16624
  expanded ? _t2({
@@ -16923,7 +17221,7 @@ function BackLink() {
16923
17221
  return /* @__PURE__ */ jsxs(Link$1, {
16924
17222
  to: "/plugins/marketplace",
16925
17223
  className: "inline-flex items-center gap-1 text-sm text-kumo-subtle hover:text-kumo-default",
16926
- children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" }), _t2({
17224
+ children: [/* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" }), _t2({
16927
17225
  id: "x5HVV+",
16928
17226
  message: "Back to marketplace"
16929
17227
  })]
@@ -16970,7 +17268,7 @@ function ScreenshotLightbox({ screenshots, index, isBlurred = false, onClose, on
16970
17268
  id: "k7xxm/",
16971
17269
  message: "Previous screenshot"
16972
17270
  }),
16973
- children: /* @__PURE__ */ jsx(CaretLeft, { className: "h-5 w-5" })
17271
+ children: /* @__PURE__ */ jsx(CaretPrev, { className: "h-5 w-5" })
16974
17272
  }),
16975
17273
  /* @__PURE__ */ jsx("img", {
16976
17274
  src: screenshots[index],
@@ -16991,7 +17289,7 @@ function ScreenshotLightbox({ screenshots, index, isBlurred = false, onClose, on
16991
17289
  id: "tgnDjk",
16992
17290
  message: "Next screenshot"
16993
17291
  }),
16994
- children: /* @__PURE__ */ jsx(CaretRight, { className: "h-5 w-5" })
17292
+ children: /* @__PURE__ */ jsx(CaretNext, { className: "h-5 w-5" })
16995
17293
  }),
16996
17294
  /* @__PURE__ */ jsxs("div", {
16997
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",
@@ -18533,7 +18831,7 @@ function MenuEditor() {
18533
18831
  message: "Back"
18534
18832
  }),
18535
18833
  onClick: () => navigate({ to: "/menus" }),
18536
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
18834
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
18537
18835
  }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
18538
18836
  className: "text-3xl font-bold",
18539
18837
  children: menu.label
@@ -19097,7 +19395,7 @@ function MenuList() {
19097
19395
  name: "name",
19098
19396
  required: true,
19099
19397
  placeholder: "primary",
19100
- pattern: "[a-z0-9-]+",
19398
+ pattern: "[a-z0-9\\-]+",
19101
19399
  title: _t({
19102
19400
  id: "6ZQzu/",
19103
19401
  message: "Only lowercase letters, numbers, and hyphens"
@@ -19837,7 +20135,7 @@ function Redirects() {
19837
20135
  }),
19838
20136
  /* @__PURE__ */ jsx("div", {
19839
20137
  className: "w-8 text-center text-kumo-subtle",
19840
- children: /* @__PURE__ */ jsx(ArrowRight, { size: 14 })
20138
+ children: /* @__PURE__ */ jsx(ArrowNext, { size: 14 })
19841
20139
  }),
19842
20140
  /* @__PURE__ */ jsx("div", {
19843
20141
  className: "flex-1 font-mono text-xs truncate",
@@ -20117,7 +20415,7 @@ function SectionEditor() {
20117
20415
  id: "t4JBeQ",
20118
20416
  message: "Back to sections"
20119
20417
  }),
20120
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-5 w-5" })
20418
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-5 w-5" })
20121
20419
  })
20122
20420
  }), /* @__PURE__ */ jsx("h1", {
20123
20421
  className: "text-2xl font-bold",
@@ -20179,6 +20477,16 @@ function SectionEditorForm({ section, isSaving, onSave }) {
20179
20477
  keywords,
20180
20478
  content
20181
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
+ }, []);
20182
20490
  const handleSave = () => {
20183
20491
  const keywordsArray = keywords.split(",").map((k) => k.trim()).filter(Boolean);
20184
20492
  onSave({
@@ -20191,43 +20499,41 @@ function SectionEditorForm({ section, isSaving, onSave }) {
20191
20499
  };
20192
20500
  return /* @__PURE__ */ jsxs("div", {
20193
20501
  className: "space-y-6",
20194
- children: [/* @__PURE__ */ jsxs("div", {
20195
- className: "flex items-center justify-between",
20196
- children: [/* @__PURE__ */ jsxs("div", {
20197
- className: "flex items-center gap-4",
20198
- children: [/* @__PURE__ */ jsx(Link$1, {
20199
- to: "/sections",
20200
- children: /* @__PURE__ */ jsx(Button, {
20201
- variant: "ghost",
20202
- shape: "square",
20203
- "aria-label": _t2({
20204
- id: "t4JBeQ",
20205
- message: "Back to sections"
20206
- }),
20207
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-5 w-5" })
20208
- })
20209
- }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
20210
- className: "text-2xl font-bold",
20211
- children: section.title
20212
- }), /* @__PURE__ */ jsxs("p", {
20213
- className: "text-sm text-kumo-subtle",
20214
- children: [
20215
- section.source === "theme" ? _t2({
20216
- id: "wg2j4A",
20217
- message: "Theme Section"
20218
- }) : _t2({
20219
- id: "dqjp2n",
20220
- message: "Custom Section"
20221
- }),
20222
- " ·",
20223
- " ",
20224
- section.slug
20225
- ]
20226
- })] })]
20227
- }), /* @__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, {
20228
20516
  isSaving,
20229
20517
  isDirty,
20230
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
+ ]
20231
20537
  })]
20232
20538
  }), /* @__PURE__ */ jsxs("div", {
20233
20539
  className: "grid grid-cols-12 gap-6",
@@ -20243,12 +20549,24 @@ function SectionEditorForm({ section, isSaving, onSave }) {
20243
20549
  })
20244
20550
  }), /* @__PURE__ */ jsx(PortableTextEditor, {
20245
20551
  value: content,
20246
- onChange: (value) => setContent(value)
20552
+ onChange: (value) => setContent(value),
20553
+ onBlockSidebarOpen: handleBlockSidebarOpen,
20554
+ onBlockSidebarClose: handleBlockSidebarClose
20247
20555
  })]
20248
20556
  })
20249
- }), /* @__PURE__ */ jsxs("div", {
20557
+ }), /* @__PURE__ */ jsx("div", {
20250
20558
  className: "col-span-4 space-y-6",
20251
- 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", {
20252
20570
  className: "rounded-lg border bg-kumo-base p-6 space-y-4",
20253
20571
  children: [
20254
20572
  /* @__PURE__ */ jsx("h2", {
@@ -20281,7 +20599,7 @@ function SectionEditorForm({ section, isSaving, onSave }) {
20281
20599
  setSlugTouched(true);
20282
20600
  },
20283
20601
  placeholder: "section-slug",
20284
- pattern: "[a-z0-9-]+"
20602
+ pattern: "[a-z0-9\\-]+"
20285
20603
  }), /* @__PURE__ */ jsx("p", {
20286
20604
  className: "text-xs text-kumo-subtle mt-1",
20287
20605
  children: _t2({
@@ -20354,7 +20672,7 @@ function SectionEditorForm({ section, isSaving, onSave }) {
20354
20672
  })
20355
20673
  })
20356
20674
  ]
20357
- })]
20675
+ })] })
20358
20676
  })]
20359
20677
  })]
20360
20678
  });
@@ -20549,7 +20867,7 @@ function Sections() {
20549
20867
  },
20550
20868
  required: true,
20551
20869
  placeholder: "hero-banner",
20552
- pattern: "[a-z0-9-]+",
20870
+ pattern: "[a-z0-9\\-]+",
20553
20871
  title: _t({
20554
20872
  id: "BjF0Jv",
20555
20873
  message: "Lowercase letters, numbers, and hyphens only"
@@ -20878,7 +21196,7 @@ function SettingsLink({ to, icon, title, description }) {
20878
21196
  className: "text-sm text-kumo-subtle",
20879
21197
  children: description
20880
21198
  })] })]
20881
- }), /* @__PURE__ */ jsx(CaretRight, { className: "h-5 w-5 text-kumo-subtle" })]
21199
+ }), /* @__PURE__ */ jsx(CaretNext, { className: "h-5 w-5 text-kumo-subtle" })]
20882
21200
  });
20883
21201
  }
20884
21202
  /**
@@ -21316,7 +21634,7 @@ function AllowedDomainsSettings() {
21316
21634
  id: "9aZHfH",
21317
21635
  message: "Back to settings"
21318
21636
  }),
21319
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
21637
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
21320
21638
  })
21321
21639
  }), /* @__PURE__ */ jsx("h1", {
21322
21640
  className: "text-2xl font-bold",
@@ -21897,7 +22215,7 @@ function ApiTokenSettings() {
21897
22215
  id: "9aZHfH",
21898
22216
  message: "Back to settings"
21899
22217
  }),
21900
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
22218
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
21901
22219
  })
21902
22220
  }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
21903
22221
  className: "text-2xl font-bold",
@@ -22306,7 +22624,7 @@ function EmailSettings() {
22306
22624
  id: "9aZHfH",
22307
22625
  message: "Back to settings"
22308
22626
  }),
22309
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
22627
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
22310
22628
  })
22311
22629
  }), /* @__PURE__ */ jsx("h1", {
22312
22630
  className: "text-2xl font-bold",
@@ -22337,7 +22655,7 @@ function EmailSettings() {
22337
22655
  id: "9aZHfH",
22338
22656
  message: "Back to settings"
22339
22657
  }),
22340
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
22658
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
22341
22659
  })
22342
22660
  }), /* @__PURE__ */ jsx("h1", {
22343
22661
  className: "text-2xl font-bold",
@@ -22639,7 +22957,7 @@ function GeneralSettings() {
22639
22957
  id: "9aZHfH",
22640
22958
  message: "Back to settings"
22641
22959
  }),
22642
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
22960
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
22643
22961
  })
22644
22962
  }), /* @__PURE__ */ jsx("h1", {
22645
22963
  className: "text-2xl font-bold",
@@ -22662,9 +22980,8 @@ function GeneralSettings() {
22662
22980
  return /* @__PURE__ */ jsxs("div", {
22663
22981
  className: "space-y-6",
22664
22982
  children: [
22665
- /* @__PURE__ */ jsxs("div", {
22666
- className: "flex items-center gap-3",
22667
- children: [/* @__PURE__ */ jsx(Link$1, {
22983
+ /* @__PURE__ */ jsx(EditorHeader, {
22984
+ leading: /* @__PURE__ */ jsx(Link$1, {
22668
22985
  to: "/settings",
22669
22986
  children: /* @__PURE__ */ jsx(Button, {
22670
22987
  variant: "ghost",
@@ -22673,21 +22990,36 @@ function GeneralSettings() {
22673
22990
  id: "9aZHfH",
22674
22991
  message: "Back to settings"
22675
22992
  }),
22676
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
22993
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
22677
22994
  })
22678
- }), /* @__PURE__ */ jsx("h1", {
22679
- 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",
22680
23011
  children: _t({
22681
23012
  id: "Cb/Jr5",
22682
23013
  message: "General Settings"
22683
23014
  })
22684
- })]
23015
+ })
22685
23016
  }),
22686
23017
  saveStatus && /* @__PURE__ */ jsxs("div", {
22687
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"}`,
22688
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]
22689
23020
  }),
22690
23021
  /* @__PURE__ */ jsxs("form", {
23022
+ id: "general-settings-form",
22691
23023
  onSubmit: handleSubmit,
22692
23024
  className: "space-y-6",
22693
23025
  children: [
@@ -23270,7 +23602,7 @@ function SecuritySettings() {
23270
23602
  id: "9aZHfH",
23271
23603
  message: "Back to settings"
23272
23604
  }),
23273
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
23605
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
23274
23606
  })
23275
23607
  }), /* @__PURE__ */ jsx("h1", {
23276
23608
  className: "text-2xl font-bold",
@@ -23491,7 +23823,7 @@ function SeoSettings() {
23491
23823
  id: "9aZHfH",
23492
23824
  message: "Back to settings"
23493
23825
  }),
23494
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
23826
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
23495
23827
  })
23496
23828
  }), /* @__PURE__ */ jsx("h1", {
23497
23829
  className: "text-2xl font-bold",
@@ -23514,9 +23846,8 @@ function SeoSettings() {
23514
23846
  return /* @__PURE__ */ jsxs("div", {
23515
23847
  className: "space-y-6",
23516
23848
  children: [
23517
- /* @__PURE__ */ jsxs("div", {
23518
- className: "flex items-center gap-3",
23519
- children: [/* @__PURE__ */ jsx(Link$1, {
23849
+ /* @__PURE__ */ jsx(EditorHeader, {
23850
+ leading: /* @__PURE__ */ jsx(Link$1, {
23520
23851
  to: "/settings",
23521
23852
  children: /* @__PURE__ */ jsx(Button, {
23522
23853
  variant: "ghost",
@@ -23525,21 +23856,36 @@ function SeoSettings() {
23525
23856
  id: "9aZHfH",
23526
23857
  message: "Back to settings"
23527
23858
  }),
23528
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
23859
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
23529
23860
  })
23530
- }), /* @__PURE__ */ jsx("h1", {
23531
- 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",
23532
23877
  children: _t({
23533
23878
  id: "GfWoKv",
23534
23879
  message: "SEO Settings"
23535
23880
  })
23536
- })]
23881
+ })
23537
23882
  }),
23538
23883
  saveStatus && /* @__PURE__ */ jsxs("div", {
23539
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"}`,
23540
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]
23541
23886
  }),
23542
23887
  /* @__PURE__ */ jsxs("form", {
23888
+ id: "seo-settings-form",
23543
23889
  onSubmit: handleSubmit,
23544
23890
  className: "space-y-6",
23545
23891
  children: [/* @__PURE__ */ jsxs("div", {
@@ -23698,7 +24044,7 @@ function SocialSettings() {
23698
24044
  id: "9aZHfH",
23699
24045
  message: "Back to settings"
23700
24046
  }),
23701
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
24047
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
23702
24048
  })
23703
24049
  }), /* @__PURE__ */ jsx("h1", {
23704
24050
  className: "text-2xl font-bold",
@@ -23721,9 +24067,8 @@ function SocialSettings() {
23721
24067
  return /* @__PURE__ */ jsxs("div", {
23722
24068
  className: "space-y-6",
23723
24069
  children: [
23724
- /* @__PURE__ */ jsxs("div", {
23725
- className: "flex items-center gap-3",
23726
- children: [/* @__PURE__ */ jsx(Link$1, {
24070
+ /* @__PURE__ */ jsx(EditorHeader, {
24071
+ leading: /* @__PURE__ */ jsx(Link$1, {
23727
24072
  to: "/settings",
23728
24073
  children: /* @__PURE__ */ jsx(Button, {
23729
24074
  variant: "ghost",
@@ -23732,21 +24077,36 @@ function SocialSettings() {
23732
24077
  id: "9aZHfH",
23733
24078
  message: "Back to settings"
23734
24079
  }),
23735
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
24080
+ children: /* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" })
23736
24081
  })
23737
- }), /* @__PURE__ */ jsx("h1", {
23738
- 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",
23739
24098
  children: _t({
23740
24099
  id: "d0rUsW",
23741
24100
  message: "Social Links"
23742
24101
  })
23743
- })]
24102
+ })
23744
24103
  }),
23745
24104
  saveStatus && /* @__PURE__ */ jsxs("div", {
23746
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"}`,
23747
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]
23748
24107
  }),
23749
24108
  /* @__PURE__ */ jsxs("form", {
24109
+ id: "social-settings-form",
23750
24110
  onSubmit: handleSubmit,
23751
24111
  className: "space-y-6",
23752
24112
  children: [/* @__PURE__ */ jsxs("div", {
@@ -25197,7 +25557,7 @@ function SidebarNav({ manifest }) {
25197
25557
  collapsible: true,
25198
25558
  defaultOpen: true,
25199
25559
  children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
25200
- className: "[&>span]:text-start",
25560
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
25201
25561
  children: _t({
25202
25562
  id: "4b3oEV",
25203
25563
  message: "Content"
@@ -25209,7 +25569,7 @@ function SidebarNav({ manifest }) {
25209
25569
  collapsible: true,
25210
25570
  defaultOpen: true,
25211
25571
  children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
25212
- className: "[&>span]:text-start",
25572
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
25213
25573
  children: _t({
25214
25574
  id: "wckWOP",
25215
25575
  message: "Manage"
@@ -25221,7 +25581,7 @@ function SidebarNav({ manifest }) {
25221
25581
  collapsible: true,
25222
25582
  defaultOpen: true,
25223
25583
  children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
25224
- className: "[&>span]:text-start",
25584
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
25225
25585
  children: _t({
25226
25586
  id: "U3pytU",
25227
25587
  message: "Admin"
@@ -25232,7 +25592,7 @@ function SidebarNav({ manifest }) {
25232
25592
  collapsible: true,
25233
25593
  defaultOpen: true,
25234
25594
  children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
25235
- className: "[&>span]:text-start",
25595
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
25236
25596
  children: _t({
25237
25597
  id: "ohUJJM",
25238
25598
  message: "Plugins"
@@ -27240,7 +27600,7 @@ function ThemeMarketplaceDetail({ themeId }) {
27240
27600
  children: [/* @__PURE__ */ jsxs(Link$1, {
27241
27601
  to: "/themes/marketplace",
27242
27602
  className: "inline-flex items-center gap-1 text-sm text-kumo-subtle hover:text-kumo-default",
27243
- children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" }), _t({
27603
+ children: [/* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" }), _t({
27244
27604
  id: "6wOzc6",
27245
27605
  message: "Back to Themes"
27246
27606
  })]
@@ -27268,7 +27628,7 @@ function ThemeMarketplaceDetail({ themeId }) {
27268
27628
  /* @__PURE__ */ jsxs(Link$1, {
27269
27629
  to: "/themes/marketplace",
27270
27630
  className: "inline-flex items-center gap-1 text-sm text-kumo-subtle hover:text-kumo-default",
27271
- children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" }), _t({
27631
+ children: [/* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" }), _t({
27272
27632
  id: "6wOzc6",
27273
27633
  message: "Back to Themes"
27274
27634
  })]
@@ -27466,7 +27826,7 @@ function Lightbox({ urls, index, onClose, onPrev, onNext }) {
27466
27826
  id: "DHhJ7s",
27467
27827
  message: "Previous"
27468
27828
  }),
27469
- children: /* @__PURE__ */ jsx(CaretLeft, { className: "h-5 w-5" })
27829
+ children: /* @__PURE__ */ jsx(CaretPrev, { className: "h-5 w-5" })
27470
27830
  }), /* @__PURE__ */ jsx("button", {
27471
27831
  onClick: onNext,
27472
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",
@@ -27474,7 +27834,7 @@ function Lightbox({ urls, index, onClose, onPrev, onNext }) {
27474
27834
  id: "hXzOVo",
27475
27835
  message: "Next"
27476
27836
  }),
27477
- children: /* @__PURE__ */ jsx(CaretRight, { className: "h-5 w-5" })
27837
+ children: /* @__PURE__ */ jsx(CaretNext, { className: "h-5 w-5" })
27478
27838
  })] }),
27479
27839
  /* @__PURE__ */ jsxs("div", {
27480
27840
  className: "absolute bottom-2 start-1/2 -translate-x-1/2 rounded-full bg-kumo-base/80 px-3 py-1 text-xs",
@@ -27749,7 +28109,7 @@ function Widgets() {
27749
28109
  name: "name",
27750
28110
  required: true,
27751
28111
  placeholder: "sidebar",
27752
- pattern: "[a-z0-9-]+"
28112
+ pattern: "[a-z0-9\\-]+"
27753
28113
  }),
27754
28114
  /* @__PURE__ */ jsx(Input, {
27755
28115
  label: "Label",
@@ -28047,7 +28407,7 @@ function WidgetItem({ widget, areaName, isExpanded, onToggle, components, plugin
28047
28407
  children: /* @__PURE__ */ jsxs("div", {
28048
28408
  className: "flex items-center gap-2",
28049
28409
  children: [
28050
- 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" }),
28051
28411
  /* @__PURE__ */ jsx("span", {
28052
28412
  className: "font-medium",
28053
28413
  children: widget.title || "Untitled Widget"
@@ -30064,7 +30424,7 @@ function PostTypeRow({ postType, selection, expanded, onToggleExpand, onToggleEn
30064
30424
  onClick: onToggleExpand,
30065
30425
  className: "flex items-center gap-1 text-start",
30066
30426
  "aria-expanded": expanded,
30067
- 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", {
30068
30428
  className: "font-medium",
30069
30429
  children: postType.name
30070
30430
  }), /* @__PURE__ */ jsx("p", {
@@ -33654,5 +34014,5 @@ function AdminApp({ pluginAdmins = EMPTY_PLUGINS, authProviders = EMPTY_AUTH_PRO
33654
34014
  }
33655
34015
 
33656
34016
  //#endregion
33657
- export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, AuthProviderProvider, 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, 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 };
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 };
33658
34018
  //# sourceMappingURL=index.js.map