@emdash-cms/admin 0.30.0 → 0.31.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 (139) hide show
  1. package/dist/{LocaleDirectionProvider-McGbBpFz.js → LocaleDirectionProvider-CzUzBk_M.js} +27 -27
  2. package/dist/{LocaleDirectionProvider-McGbBpFz.js.map → LocaleDirectionProvider-CzUzBk_M.js.map} +1 -1
  3. package/dist/index.d.ts +7 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1353 -336
  6. package/dist/index.js.map +1 -1
  7. package/dist/locales/ar/messages.mjs +1 -1
  8. package/dist/locales/ca/messages.mjs +1 -1
  9. package/dist/locales/de/messages.mjs +1 -1
  10. package/dist/locales/en/messages.mjs +1 -1
  11. package/dist/locales/en-GB/messages.mjs +1 -1
  12. package/dist/locales/es-419/messages.mjs +1 -1
  13. package/dist/locales/es-ES/messages.mjs +1 -1
  14. package/dist/locales/eu/messages.mjs +1 -1
  15. package/dist/locales/fa/messages.mjs +1 -1
  16. package/dist/locales/fr/messages.mjs +1 -1
  17. package/dist/locales/hu/messages.mjs +1 -1
  18. package/dist/locales/id/messages.mjs +1 -1
  19. package/dist/locales/index.js +1 -1
  20. package/dist/locales/ja/messages.mjs +1 -1
  21. package/dist/locales/ko/messages.mjs +1 -1
  22. package/dist/locales/nb/messages.mjs +1 -1
  23. package/dist/locales/nl/messages.mjs +1 -1
  24. package/dist/locales/pl/messages.mjs +1 -1
  25. package/dist/locales/pseudo/messages.mjs +1 -1
  26. package/dist/locales/pt-BR/messages.mjs +1 -1
  27. package/dist/locales/sr-Latn/messages.mjs +1 -1
  28. package/dist/locales/sv/messages.mjs +1 -1
  29. package/dist/locales/th/messages.mjs +1 -1
  30. package/dist/locales/tr/messages.mjs +1 -1
  31. package/dist/locales/uk/messages.mjs +1 -1
  32. package/dist/locales/zh-CN/messages.mjs +1 -1
  33. package/dist/locales/zh-TW/messages.mjs +1 -1
  34. package/dist/messages-4E6Xx17o.js +6 -0
  35. package/dist/messages-4E6Xx17o.js.map +1 -0
  36. package/dist/messages-BAcc2d5l.js +6 -0
  37. package/dist/messages-BAcc2d5l.js.map +1 -0
  38. package/dist/messages-BKn4pcjV.js +6 -0
  39. package/dist/messages-BKn4pcjV.js.map +1 -0
  40. package/dist/messages-BbxsgV1o.js +6 -0
  41. package/dist/messages-BbxsgV1o.js.map +1 -0
  42. package/dist/messages-Bq0YC7aR.js +6 -0
  43. package/dist/messages-Bq0YC7aR.js.map +1 -0
  44. package/dist/messages-C5xrZ8Mt.js +6 -0
  45. package/dist/messages-C5xrZ8Mt.js.map +1 -0
  46. package/dist/messages-CK9KaPjU.js +6 -0
  47. package/dist/messages-CK9KaPjU.js.map +1 -0
  48. package/dist/messages-CQk8YErW.js +6 -0
  49. package/dist/messages-CQk8YErW.js.map +1 -0
  50. package/dist/messages-CSfIGWK3.js +6 -0
  51. package/dist/messages-CSfIGWK3.js.map +1 -0
  52. package/dist/messages-CYswqaSQ.js +6 -0
  53. package/dist/messages-CYswqaSQ.js.map +1 -0
  54. package/dist/messages-Cjo7q43O.js +6 -0
  55. package/dist/messages-Cjo7q43O.js.map +1 -0
  56. package/dist/messages-Cnh1qPI1.js +6 -0
  57. package/dist/messages-Cnh1qPI1.js.map +1 -0
  58. package/dist/messages-Cu49SHtz.js +6 -0
  59. package/dist/messages-Cu49SHtz.js.map +1 -0
  60. package/dist/messages-DJG5FSHH.js +6 -0
  61. package/dist/messages-DJG5FSHH.js.map +1 -0
  62. package/dist/messages-DhNXuJB7.js +6 -0
  63. package/dist/messages-DhNXuJB7.js.map +1 -0
  64. package/dist/messages-DiCVcFcv.js +6 -0
  65. package/dist/messages-DiCVcFcv.js.map +1 -0
  66. package/dist/messages-DivwCvnm.js +6 -0
  67. package/dist/messages-DivwCvnm.js.map +1 -0
  68. package/dist/messages-DknOk2F9.js +6 -0
  69. package/dist/messages-DknOk2F9.js.map +1 -0
  70. package/dist/messages-DtOKaZNy.js +6 -0
  71. package/dist/messages-DtOKaZNy.js.map +1 -0
  72. package/dist/messages-DwLBUVeM.js +6 -0
  73. package/dist/messages-DwLBUVeM.js.map +1 -0
  74. package/dist/messages-DwSraJxw.js +6 -0
  75. package/dist/messages-DwSraJxw.js.map +1 -0
  76. package/dist/messages-Qm7DJ8iu.js +6 -0
  77. package/dist/messages-Qm7DJ8iu.js.map +1 -0
  78. package/dist/messages-WBTztu_7.js +6 -0
  79. package/dist/messages-WBTztu_7.js.map +1 -0
  80. package/dist/messages-WqjpUMW1.js +6 -0
  81. package/dist/messages-WqjpUMW1.js.map +1 -0
  82. package/dist/messages-XOyOBv6J.js +6 -0
  83. package/dist/messages-XOyOBv6J.js.map +1 -0
  84. package/dist/messages-gHwLtggX.js +6 -0
  85. package/dist/messages-gHwLtggX.js.map +1 -0
  86. package/dist/styles.css +1 -1
  87. package/package.json +4 -4
  88. package/dist/messages-30qbnmRx.js +0 -6
  89. package/dist/messages-30qbnmRx.js.map +0 -1
  90. package/dist/messages-Axst6gLr.js +0 -6
  91. package/dist/messages-Axst6gLr.js.map +0 -1
  92. package/dist/messages-B-bWgBX8.js +0 -6
  93. package/dist/messages-B-bWgBX8.js.map +0 -1
  94. package/dist/messages-B0QaP8co.js +0 -6
  95. package/dist/messages-B0QaP8co.js.map +0 -1
  96. package/dist/messages-BGf8F1lR.js +0 -6
  97. package/dist/messages-BGf8F1lR.js.map +0 -1
  98. package/dist/messages-BMgCc4cZ.js +0 -6
  99. package/dist/messages-BMgCc4cZ.js.map +0 -1
  100. package/dist/messages-BNhQh9T8.js +0 -6
  101. package/dist/messages-BNhQh9T8.js.map +0 -1
  102. package/dist/messages-BR-TSB_9.js +0 -6
  103. package/dist/messages-BR-TSB_9.js.map +0 -1
  104. package/dist/messages-BSqQiKKo.js +0 -6
  105. package/dist/messages-BSqQiKKo.js.map +0 -1
  106. package/dist/messages-Be0xmdDi.js +0 -6
  107. package/dist/messages-Be0xmdDi.js.map +0 -1
  108. package/dist/messages-BkOa_w3b.js +0 -6
  109. package/dist/messages-BkOa_w3b.js.map +0 -1
  110. package/dist/messages-Bqzqrw4i.js +0 -6
  111. package/dist/messages-Bqzqrw4i.js.map +0 -1
  112. package/dist/messages-CBWpqPBL.js +0 -6
  113. package/dist/messages-CBWpqPBL.js.map +0 -1
  114. package/dist/messages-CIkqn_8n.js +0 -6
  115. package/dist/messages-CIkqn_8n.js.map +0 -1
  116. package/dist/messages-CMbNQ7lX.js +0 -6
  117. package/dist/messages-CMbNQ7lX.js.map +0 -1
  118. package/dist/messages-CSAH0lQI.js +0 -6
  119. package/dist/messages-CSAH0lQI.js.map +0 -1
  120. package/dist/messages-CXUOdBIL.js +0 -6
  121. package/dist/messages-CXUOdBIL.js.map +0 -1
  122. package/dist/messages-CnNncIUV.js +0 -6
  123. package/dist/messages-CnNncIUV.js.map +0 -1
  124. package/dist/messages-CpRrAHoJ.js +0 -6
  125. package/dist/messages-CpRrAHoJ.js.map +0 -1
  126. package/dist/messages-D0KmmMuH.js +0 -6
  127. package/dist/messages-D0KmmMuH.js.map +0 -1
  128. package/dist/messages-D6A5PrwK.js +0 -6
  129. package/dist/messages-D6A5PrwK.js.map +0 -1
  130. package/dist/messages-DD28du-X.js +0 -6
  131. package/dist/messages-DD28du-X.js.map +0 -1
  132. package/dist/messages-DWuyZMXt.js +0 -6
  133. package/dist/messages-DWuyZMXt.js.map +0 -1
  134. package/dist/messages-JApRIdzY.js +0 -6
  135. package/dist/messages-JApRIdzY.js.map +0 -1
  136. package/dist/messages-l-TFOm7q.js +0 -6
  137. package/dist/messages-l-TFOm7q.js.map +0 -1
  138. package/dist/messages-lfFNZ58n.js +0 -6
  139. package/dist/messages-lfFNZ58n.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as fetchPlugins, c as updatePluginSettings, d as fetchAuthMode, f as fetchManifest, i as fetchPluginSettings, l as API_BASE, m as throwResponseError, n as enablePlugin, p as parseApiResponse, r as fetchPlugin, s as setPluginMcpEnabled, t as disablePlugin, u as apiFetch } from "./plugins-CQAV8Ap0.js";
2
- import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, s as getLocaleDir, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-McGbBpFz.js";
2
+ import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, s as getLocaleDir, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-CzUzBk_M.js";
3
3
  import "./locales/index.js";
4
4
  import { Autocomplete, Badge, Banner, Button, Checkbox, ClipboardText, Collapsible, Combobox, CommandPalette, Dialog, DropdownMenu, Input, InputArea, Label, LayerCard, LinkButton, LinkProvider, Loader, Popover, Select, Sidebar, Sidebar as KumoSidebar, SkeletonLine, Switch, Tabs, Text, Toast, Toasty, Tooltip, TooltipProvider, buttonVariants, useKumoToastManager, useSidebar, useSidebar as useSidebar$1 } from "@cloudflare/kumo";
5
5
  import { i18n } from "@lingui/core";
@@ -13,11 +13,12 @@ import { Dialog as Dialog$1 } from "@cloudflare/kumo/primitives";
13
13
  import { Archive, ArrowCounterClockwise, ArrowDown, ArrowRightIcon, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowUp, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, Bell, BookOpen, BracketsAngle, BracketsCurly, Browser, Calendar, CalendarBlank, CardsThree, CaretDown, CaretRightIcon, CaretUp, CaretUpDown, ChartBar, ChartLine, Chats, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, CloudArrowUp, Code, CodeBlock, Columns, ColumnsPlusLeft, ColumnsPlusRight, Copy, Crop, Cube, Database, DeviceMobile, DotsSixVertical, Download, DownloadSimple, Envelope, EnvelopeSimple, Eye, EyeSlash, Faders, File, FileArrowDown, FileText, Files, FloppyDisk, Folder, FolderOpen, Folders, Gear, GithubLogo, Globe, GlobeSimple, GridFour, HardDrive, Hash, IdentificationCard, Image as Image$1, ImageBroken, ImageSquare, Images, ImagesSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, MagnifyingGlass, Medal, Minus, Monitor, Moon, Newspaper, Palette, PaperPlaneTilt, Paperclip, Paragraph, Path, Pencil, PencilSimple, Plug, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Robot, Rows, RowsPlusBottom, RowsPlusTop, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, Signature, SlidersHorizontal, Sparkle, SquaresFour, Stack, Star, Storefront, Sun, Table, Tag, TextAlignCenter, TextAlignLeft, TextAlignRight, TextB, TextH, TextHFive, TextHFour, TextHOne, TextHSix, TextHThree, TextHTwo, TextItalic, TextStrikethrough, TextT, TextUnderline, ToggleLeft, Trash, Trophy, Upload, User, UserCircle, UserPlus, Users, Warning, WarningCircle, WebhooksLogo, X, YoutubeLogo } from "@phosphor-icons/react";
14
14
  import { clsx } from "clsx";
15
15
  import { twMerge } from "tailwind-merge";
16
- import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter, rectIntersection, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
17
- import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
16
+ import { DndContext, DragOverlay, KeyboardSensor, MeasuringStrategy, PointerSensor, closestCenter, rectIntersection, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
17
+ import { SortableContext, arrayMove, rectSortingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
18
18
  import { CSS } from "@dnd-kit/utilities";
19
+ import { EditorContent, Node as Node$1, NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes, useEditor, useEditorState } from "@tiptap/react";
19
20
  import { Popover as Popover$1 } from "@cloudflare/kumo/primitives/popover";
20
- import { Extension, InputRule, Node as Node$1, PasteRule, escapeForRegEx, mergeAttributes } from "@tiptap/core";
21
+ import { Extension, InputRule, Node as Node$2, PasteRule, escapeForRegEx, mergeAttributes as mergeAttributes$1 } from "@tiptap/core";
21
22
  import CharacterCount from "@tiptap/extension-character-count";
22
23
  import Focus from "@tiptap/extension-focus";
23
24
  import Placeholder from "@tiptap/extension-placeholder";
@@ -29,7 +30,6 @@ import TextAlign from "@tiptap/extension-text-align";
29
30
  import Typography from "@tiptap/extension-typography";
30
31
  import { AllSelection, NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
31
32
  import { CellSelection } from "@tiptap/pm/tables";
32
- import { EditorContent, Node as Node$2, NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes as mergeAttributes$1, useEditor, useEditorState } from "@tiptap/react";
33
33
  import { BubbleMenu } from "@tiptap/react/menus";
34
34
  import StarterKit from "@tiptap/starter-kit";
35
35
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
@@ -3673,7 +3673,7 @@ function probeImageDimensions(url, errorMessage) {
3673
3673
  img.src = url;
3674
3674
  });
3675
3675
  }
3676
- function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "image/", mimeTypeFilters, fieldId, title: providedTitle, hideUrlInput = false, mediaKind = "image", localOnly = false }) {
3676
+ function MediaPickerModal({ open, onOpenChange, onSelect, multiple = false, onSelectMany, mimeTypeFilter = "image/", mimeTypeFilters, fieldId, title: providedTitle, hideUrlInput = false, mediaKind = "image", localOnly = false }) {
3677
3677
  const { _: _t } = useLingui();
3678
3678
  const isFileKind = mediaKind === "file";
3679
3679
  const filters = React$1.useMemo(() => {
@@ -3704,6 +3704,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3704
3704
  const EmptyStateIcon = isFileKind ? Paperclip : Image$1;
3705
3705
  const queryClient = useQueryClient();
3706
3706
  const [selectedItem, setSelectedItem] = React$1.useState(null);
3707
+ const [selectedItems, setSelectedItems] = React$1.useState([]);
3707
3708
  const [activeProvider, setActiveProvider] = React$1.useState("local");
3708
3709
  const [searchQuery, setSearchQuery] = React$1.useState("");
3709
3710
  const debouncedSearch = useDebouncedValue$1(searchQuery, 300);
@@ -3715,6 +3716,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3715
3716
  React$1.useEffect(() => {
3716
3717
  if (open) {
3717
3718
  setSelectedItem(null);
3719
+ setSelectedItems([]);
3718
3720
  setActiveProvider("local");
3719
3721
  setSearchQuery("");
3720
3722
  setImageUrl("");
@@ -3787,7 +3789,11 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3787
3789
  mutationFn: (file) => uploadMedia(file, { fieldId }),
3788
3790
  onSuccess: (item) => {
3789
3791
  queryClient.invalidateQueries({ queryKey: ["media"] });
3790
- setSelectedItem({
3792
+ if (multiple) setSelectedItems((prev) => [...prev, {
3793
+ providerId: "local",
3794
+ item
3795
+ }]);
3796
+ else setSelectedItem({
3791
3797
  providerId: "local",
3792
3798
  item
3793
3799
  });
@@ -3801,7 +3807,11 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3801
3807
  mutationFn: ({ providerId, file }) => uploadToProvider(providerId, file),
3802
3808
  onSuccess: (item, { providerId }) => {
3803
3809
  queryClient.invalidateQueries({ queryKey: ["provider-media", providerId] });
3804
- setSelectedItem({
3810
+ if (multiple) setSelectedItems((prev) => [...prev, {
3811
+ providerId,
3812
+ item
3813
+ }]);
3814
+ else setSelectedItem({
3805
3815
  providerId,
3806
3816
  item
3807
3817
  });
@@ -3875,19 +3885,39 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3875
3885
  }
3876
3886
  if (fileInputRef.current) fileInputRef.current.value = "";
3877
3887
  };
3888
+ const toMediaItem = (selected) => {
3889
+ if (selected.providerId === "local") return selected.item;
3890
+ const providerItem = selected.item;
3891
+ const dims = providerDimensions[providerItem.id];
3892
+ const itemWithDims = dims ? {
3893
+ ...providerItem,
3894
+ width: providerItem.width ?? dims.width,
3895
+ height: providerItem.height ?? dims.height
3896
+ } : providerItem;
3897
+ return providerItemToMediaItem(selected.providerId, itemWithDims);
3898
+ };
3899
+ const isItemSelected = (providerId, id) => multiple ? selectedItems.some((s) => s.providerId === providerId && s.item.id === id) : selectedItem?.providerId === providerId && selectedItem.item.id === id;
3900
+ const handleItemClick = (providerId, item) => {
3901
+ if (multiple) setSelectedItems((prev) => prev.some((s) => s.providerId === providerId && s.item.id === item.id) ? prev.filter((s) => !(s.providerId === providerId && s.item.id === item.id)) : [...prev, {
3902
+ providerId,
3903
+ item
3904
+ }]);
3905
+ else setSelectedItem({
3906
+ providerId,
3907
+ item
3908
+ });
3909
+ };
3878
3910
  const handleConfirm = () => {
3911
+ if (multiple) {
3912
+ if (selectedItems.length === 0) return;
3913
+ onSelectMany?.(selectedItems.map(toMediaItem));
3914
+ onOpenChange(false);
3915
+ setSelectedItems([]);
3916
+ setImageUrl("");
3917
+ return;
3918
+ }
3879
3919
  if (selectedItem) {
3880
- if (selectedItem.providerId === "local") onSelect(selectedItem.item);
3881
- else {
3882
- const providerItem = selectedItem.item;
3883
- const dims = providerDimensions[providerItem.id];
3884
- const itemWithDims = dims ? {
3885
- ...providerItem,
3886
- width: providerItem.width ?? dims.width,
3887
- height: providerItem.height ?? dims.height
3888
- } : providerItem;
3889
- onSelect(providerItemToMediaItem(selectedItem.providerId, itemWithDims));
3890
- }
3920
+ onSelect(toMediaItem(selectedItem));
3891
3921
  onOpenChange(false);
3892
3922
  setSelectedItem(null);
3893
3923
  setImageUrl("");
@@ -3896,6 +3926,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3896
3926
  const handleClose = () => {
3897
3927
  onOpenChange(false);
3898
3928
  setSelectedItem(null);
3929
+ setSelectedItems([]);
3899
3930
  setImageUrl("");
3900
3931
  setUrlError(null);
3901
3932
  };
@@ -3934,7 +3965,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3934
3965
  id: "iC8xDQ",
3935
3966
  message: "Failed to load image"
3936
3967
  }));
3937
- onSelect({
3968
+ const externalItem = {
3938
3969
  id: "",
3939
3970
  filename: url.pathname.split("/").pop() || "external-image",
3940
3971
  mimeType: sniffedMime,
@@ -3944,7 +3975,9 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3944
3975
  width: dimensions.width,
3945
3976
  height: dimensions.height,
3946
3977
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3947
- });
3978
+ };
3979
+ if (multiple) onSelectMany?.([externalItem]);
3980
+ else onSelect(externalItem);
3948
3981
  onOpenChange(false);
3949
3982
  setImageUrl("");
3950
3983
  } catch {
@@ -4088,6 +4121,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
4088
4121
  onClick: () => {
4089
4122
  setActiveProvider(tab.id);
4090
4123
  setSelectedItem(null);
4124
+ setSelectedItems([]);
4091
4125
  setSearchQuery("");
4092
4126
  },
4093
4127
  className: cn("flex items-center gap-2 px-4 h-9 text-sm font-medium rounded-md transition-colors whitespace-nowrap", activeProvider === tab.id ? "bg-kumo-brand text-white" : "bg-kumo-tint hover:bg-kumo-tint/80 text-kumo-subtle"),
@@ -4208,24 +4242,26 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
4208
4242
  }),
4209
4243
  children: activeProvider === "local" ? items.map((item) => /* @__PURE__ */ jsx(MediaPickerItem, {
4210
4244
  item,
4211
- selected: selectedItem?.providerId === "local" && selectedItem.item.id === item.id,
4212
- onClick: () => setSelectedItem({
4213
- providerId: "local",
4214
- item
4215
- }),
4245
+ selected: isItemSelected("local", item.id),
4246
+ onClick: () => handleItemClick("local", item),
4216
4247
  onDoubleClick: () => {
4248
+ if (multiple) {
4249
+ handleItemClick("local", item);
4250
+ return;
4251
+ }
4217
4252
  onSelect(item);
4218
4253
  onOpenChange(false);
4219
4254
  },
4220
4255
  onDimensionsDetected: handleDimensionsDetected
4221
4256
  }, item.id)) : items.map((item) => /* @__PURE__ */ jsx(ProviderMediaItem, {
4222
4257
  item,
4223
- selected: selectedItem?.providerId === activeProvider && selectedItem.item.id === item.id,
4224
- onClick: () => setSelectedItem({
4225
- providerId: activeProvider,
4226
- item
4227
- }),
4258
+ selected: isItemSelected(activeProvider, item.id),
4259
+ onClick: () => handleItemClick(activeProvider, item),
4228
4260
  onDoubleClick: () => {
4261
+ if (multiple) {
4262
+ handleItemClick(activeProvider, item);
4263
+ return;
4264
+ }
4229
4265
  const dims = providerDimensions[item.id];
4230
4266
  onSelect(providerItemToMediaItem(activeProvider, dims ? {
4231
4267
  ...item,
@@ -4266,7 +4302,11 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
4266
4302
  children: [
4267
4303
  /* @__PURE__ */ jsx("div", {
4268
4304
  className: "flex-1 text-sm text-kumo-subtle",
4269
- children: selectedItem && /* @__PURE__ */ jsxs("span", { children: [
4305
+ children: multiple ? selectedItems.length > 0 && /* @__PURE__ */ jsx("span", { children: i18n._({
4306
+ id: "agSO3s",
4307
+ message: "{0, plural, one {# item selected} other {# items selected}}",
4308
+ values: { 0: selectedItems.length }
4309
+ }) }) : selectedItem && /* @__PURE__ */ jsxs("span", { children: [
4270
4310
  _t({
4271
4311
  id: "SVRHAR",
4272
4312
  message: "Selected:"
@@ -4293,7 +4333,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
4293
4333
  }),
4294
4334
  /* @__PURE__ */ jsx(Button, {
4295
4335
  onClick: handleConfirm,
4296
- disabled: !selectedItem,
4336
+ disabled: multiple ? selectedItems.length === 0 : !selectedItem,
4297
4337
  children: _t({
4298
4338
  id: "S8Yqbl",
4299
4339
  message: "Insert"
@@ -4732,7 +4772,7 @@ function findCurrentHeading(headings, cursorPos) {
4732
4772
  /**
4733
4773
  * Document outline component showing heading tree structure
4734
4774
  */
4735
- function DocumentOutline({ editor, className }) {
4775
+ function DocumentOutline({ editor, className, reserveHeaderEnd = false }) {
4736
4776
  const { _: _t } = useLingui();
4737
4777
  const [isExpanded, setIsExpanded] = React$1.useState(true);
4738
4778
  const [headings, setHeadings] = React$1.useState([]);
@@ -4774,7 +4814,7 @@ function DocumentOutline({ editor, className }) {
4774
4814
  variant: "ghost",
4775
4815
  className: "relative justify-between",
4776
4816
  style: {
4777
- width: "calc(100% + 1.5rem)",
4817
+ width: reserveHeaderEnd ? "calc(100% - 1.5rem)" : "calc(100% + 1.5rem)",
4778
4818
  insetInlineStart: "-0.75rem"
4779
4819
  }
4780
4820
  }),
@@ -4815,6 +4855,542 @@ function DocumentOutline({ editor, className }) {
4815
4855
  });
4816
4856
  }
4817
4857
 
4858
+ //#endregion
4859
+ //#region src/components/editor/GalleryNode.tsx
4860
+ /**
4861
+ * Gallery Node for TipTap
4862
+ *
4863
+ * Node view for the Portable Text `gallery` block (grid of images with
4864
+ * optional per-image captions and a column count). Provides a WYSIWYG grid
4865
+ * preview, selection state, and a settings button that opens the gallery
4866
+ * detail panel in the content sidebar (same wiring as ImageNode).
4867
+ */
4868
+ /** Resolve the admin preview URL for a gallery image. */
4869
+ function galleryImageUrl(image) {
4870
+ if (image.asset.url) return image.asset.url;
4871
+ if (image.asset._ref) return `/_emdash/api/media/file/${encodeURIComponent(image.asset._ref)}`;
4872
+ return "";
4873
+ }
4874
+ function GalleryNodeView({ node, updateAttributes, selected, deleteNode, editor, getPos }) {
4875
+ const { _: _t } = useLingui();
4876
+ const sidebarOpenRef = React$1.useRef(false);
4877
+ const images = node.attrs.images ?? [];
4878
+ const columns = typeof node.attrs.columns === "number" ? node.attrs.columns : 3;
4879
+ const getAttrs = () => ({
4880
+ images: node.attrs.images ?? [],
4881
+ columns: typeof node.attrs.columns === "number" ? node.attrs.columns : void 0
4882
+ });
4883
+ const openSidebar = (selectedImageKey) => {
4884
+ const onOpen = editor.storage.gallery?.onOpenBlockSidebar;
4885
+ if (onOpen) {
4886
+ sidebarOpenRef.current = true;
4887
+ onOpen({
4888
+ type: "gallery",
4889
+ attrs: {
4890
+ ...getAttrs(),
4891
+ selectedImageKey,
4892
+ nodeKey: String(getPos?.() ?? "")
4893
+ },
4894
+ onUpdate: (attrs) => updateAttributes(attrs),
4895
+ onReplace: (attrs) => updateAttributes(attrs),
4896
+ onDelete: () => deleteNode(),
4897
+ onClose: () => {
4898
+ sidebarOpenRef.current = false;
4899
+ }
4900
+ });
4901
+ }
4902
+ };
4903
+ const closeSidebar = () => {
4904
+ if (!sidebarOpenRef.current) return;
4905
+ const onClose = editor.storage.gallery?.onCloseBlockSidebar;
4906
+ if (onClose) {
4907
+ onClose();
4908
+ sidebarOpenRef.current = false;
4909
+ }
4910
+ };
4911
+ const toggleSidebar = () => {
4912
+ if (sidebarOpenRef.current) closeSidebar();
4913
+ else openSidebar();
4914
+ };
4915
+ React$1.useEffect(() => {
4916
+ if (!selected) closeSidebar();
4917
+ }, [selected]);
4918
+ return /* @__PURE__ */ jsxs(NodeViewWrapper, {
4919
+ className: cn("relative my-4 group", selected && "ring-2 ring-kumo-brand ring-offset-2 rounded-lg"),
4920
+ children: [images.length === 0 ? /* @__PURE__ */ jsx(Button, {
4921
+ type: "button",
4922
+ variant: "outline",
4923
+ className: "mt-2 w-full h-32 justify-center border-dashed",
4924
+ onMouseDown: (e) => e.preventDefault(),
4925
+ onClick: () => openSidebar(),
4926
+ children: /* @__PURE__ */ jsxs("div", {
4927
+ className: "flex flex-col items-center gap-2 text-kumo-subtle",
4928
+ children: [/* @__PURE__ */ jsx(Images, { className: "h-8 w-8" }), /* @__PURE__ */ jsx("span", {
4929
+ className: "text-sm",
4930
+ children: _t({
4931
+ id: "Bk1Tv6",
4932
+ message: "Empty gallery — open settings to add images"
4933
+ })
4934
+ })]
4935
+ })
4936
+ }) : /* @__PURE__ */ jsx("div", {
4937
+ className: "grid gap-2 rounded-lg",
4938
+ style: { gridTemplateColumns: `repeat(${Math.max(1, columns)}, 1fr)` },
4939
+ children: images.map((image, index) => /* @__PURE__ */ jsxs("figure", {
4940
+ className: "m-0",
4941
+ children: [/* @__PURE__ */ jsx("button", {
4942
+ type: "button",
4943
+ className: "block w-full cursor-pointer",
4944
+ onMouseDown: (e) => e.preventDefault(),
4945
+ onClick: (e) => {
4946
+ e.stopPropagation();
4947
+ openSidebar(image._key);
4948
+ },
4949
+ "aria-label": _t({
4950
+ id: "FjJx2y",
4951
+ message: "Edit image {0}",
4952
+ values: { 0: index + 1 }
4953
+ }),
4954
+ children: /* @__PURE__ */ jsx("img", {
4955
+ src: galleryImageUrl(image),
4956
+ alt: image.alt || "",
4957
+ className: "w-full aspect-square object-cover rounded-md border",
4958
+ draggable: false
4959
+ })
4960
+ }), image.caption && /* @__PURE__ */ jsx("figcaption", {
4961
+ className: "text-xs text-kumo-subtle mt-1 text-center truncate",
4962
+ children: image.caption
4963
+ })]
4964
+ }, image._key))
4965
+ }), selected && /* @__PURE__ */ jsxs("div", {
4966
+ className: "absolute top-2 end-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity",
4967
+ children: [/* @__PURE__ */ jsx(Button, {
4968
+ type: "button",
4969
+ variant: "secondary",
4970
+ shape: "square",
4971
+ className: "h-8 w-8",
4972
+ onMouseDown: (e) => e.preventDefault(),
4973
+ onClick: toggleSidebar,
4974
+ title: _t({
4975
+ id: "VmtOUn",
4976
+ message: "Gallery settings"
4977
+ }),
4978
+ "aria-label": _t({
4979
+ id: "VmtOUn",
4980
+ message: "Gallery settings"
4981
+ }),
4982
+ children: /* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-4 w-4" })
4983
+ }), /* @__PURE__ */ jsx(Button, {
4984
+ type: "button",
4985
+ variant: "destructive",
4986
+ shape: "square",
4987
+ className: "h-8 w-8",
4988
+ onMouseDown: (e) => e.preventDefault(),
4989
+ onClick: () => deleteNode(),
4990
+ title: _t({
4991
+ id: "HjItFk",
4992
+ message: "Delete gallery"
4993
+ }),
4994
+ "aria-label": _t({
4995
+ id: "HjItFk",
4996
+ message: "Delete gallery"
4997
+ }),
4998
+ children: /* @__PURE__ */ jsx(Trash, { className: "h-4 w-4" })
4999
+ })]
5000
+ })]
5001
+ });
5002
+ }
5003
+ const GalleryExtension = Node$1.create({
5004
+ name: "gallery",
5005
+ group: "block",
5006
+ atom: true,
5007
+ draggable: true,
5008
+ addStorage() {
5009
+ return {
5010
+ onOpenBlockSidebar: null,
5011
+ onCloseBlockSidebar: null
5012
+ };
5013
+ },
5014
+ addAttributes() {
5015
+ return {
5016
+ images: { default: [] },
5017
+ columns: { default: null }
5018
+ };
5019
+ },
5020
+ parseHTML() {
5021
+ return [{ tag: "div[data-type=\"gallery\"]" }];
5022
+ },
5023
+ renderHTML() {
5024
+ return ["div", { "data-type": "gallery" }];
5025
+ },
5026
+ addNodeView() {
5027
+ return ReactNodeViewRenderer(GalleryNodeView);
5028
+ },
5029
+ addCommands() {
5030
+ return { setGallery: (options) => ({ commands }) => {
5031
+ return commands.insertContent({
5032
+ type: this.name,
5033
+ attrs: options
5034
+ });
5035
+ } };
5036
+ }
5037
+ });
5038
+
5039
+ //#endregion
5040
+ //#region src/components/editor/GalleryDetailPanel.tsx
5041
+ /**
5042
+ * Gallery Detail Panel for Editor
5043
+ *
5044
+ * Sidebar panel for editing a gallery block: add images (multi-select media
5045
+ * picker), remove, drag-and-drop reorder, per-image alt/caption, and column
5046
+ * count. Changes apply immediately via onUpdate (reordering is inherently
5047
+ * live, so the whole panel follows suit instead of a save-button form).
5048
+ */
5049
+ function generateKey$1() {
5050
+ return Math.random().toString(36).substring(2, 11);
5051
+ }
5052
+ /** Map a picked MediaItem to the gallery's Portable Text image shape. */
5053
+ function mediaItemToGalleryImage(item) {
5054
+ return {
5055
+ _type: "image",
5056
+ _key: generateKey$1(),
5057
+ asset: {
5058
+ _type: "reference",
5059
+ _ref: item.id,
5060
+ url: item.url,
5061
+ provider: item.provider && item.provider !== "local" ? item.provider : void 0
5062
+ },
5063
+ alt: item.alt || "",
5064
+ width: item.width,
5065
+ height: item.height,
5066
+ blurhash: item.blurhash ?? metaString(item.meta, "blurhash"),
5067
+ dominantColor: item.dominantColor ?? metaString(item.meta, "dominantColor")
5068
+ };
5069
+ }
5070
+ function GalleryDetailPanel({ attributes, onUpdate, onDelete, onClose, inline = false }) {
5071
+ const { _: _t } = useLingui();
5072
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
5073
+ const [showMediaPicker, setShowMediaPicker] = React$1.useState(false);
5074
+ const selectedImageKey = attributes.selectedImageKey;
5075
+ const nodeKey = attributes.nodeKey;
5076
+ const [selectedKey, setSelectedKey] = React$1.useState(selectedImageKey ?? null);
5077
+ React$1.useEffect(() => {
5078
+ if (selectedImageKey != null) setSelectedKey(selectedImageKey);
5079
+ }, [selectedImageKey]);
5080
+ const [gallery, setGallery] = React$1.useState({
5081
+ images: attributes.images ?? [],
5082
+ columns: attributes.columns
5083
+ });
5084
+ React$1.useEffect(() => {
5085
+ setGallery({
5086
+ images: attributes.images ?? [],
5087
+ columns: attributes.columns
5088
+ });
5089
+ }, [nodeKey]);
5090
+ const images = gallery.images;
5091
+ const columns = gallery.columns ?? 3;
5092
+ const selectedImage = selectedKey ? images.find((image) => image._key === selectedKey) ?? null : null;
5093
+ const apply = (patch) => {
5094
+ setGallery((prev) => ({
5095
+ ...prev,
5096
+ ...patch
5097
+ }));
5098
+ onUpdate(patch);
5099
+ };
5100
+ const handleAdd = (items) => {
5101
+ apply({ images: [...images, ...items.map(mediaItemToGalleryImage)] });
5102
+ };
5103
+ const handleRemove = (key) => {
5104
+ apply({ images: images.filter((image) => image._key !== key) });
5105
+ setSelectedKey((prev) => prev === key ? null : prev);
5106
+ };
5107
+ const handleImageChange = (key, patch) => {
5108
+ apply({ images: images.map((image) => image._key === key ? {
5109
+ ...image,
5110
+ ...patch
5111
+ } : image) });
5112
+ };
5113
+ const handleReplace = (key, item) => {
5114
+ apply({ images: images.map((image) => image._key === key ? {
5115
+ ...image,
5116
+ asset: {
5117
+ _type: "reference",
5118
+ _ref: item.id,
5119
+ url: item.url,
5120
+ provider: item.provider && item.provider !== "local" ? item.provider : void 0
5121
+ },
5122
+ alt: item.alt || "",
5123
+ width: item.width,
5124
+ height: item.height,
5125
+ blurhash: item.blurhash ?? metaString(item.meta, "blurhash"),
5126
+ dominantColor: item.dominantColor ?? metaString(item.meta, "dominantColor")
5127
+ } : image) });
5128
+ };
5129
+ const handleDragEnd = (event) => {
5130
+ const { active, over } = event;
5131
+ if (!over || active.id === over.id) return;
5132
+ const oldIndex = images.findIndex((image) => image._key === active.id);
5133
+ const newIndex = images.findIndex((image) => image._key === over.id);
5134
+ if (oldIndex === -1 || newIndex === -1) return;
5135
+ apply({ images: arrayMove(images, oldIndex, newIndex) });
5136
+ };
5137
+ const body = /* @__PURE__ */ jsxs("div", {
5138
+ className: "space-y-4",
5139
+ children: [
5140
+ /* @__PURE__ */ jsxs("div", {
5141
+ className: "flex items-center justify-between",
5142
+ children: [/* @__PURE__ */ jsx("h3", {
5143
+ className: "text-sm font-semibold",
5144
+ children: _t({
5145
+ id: "U+kFad",
5146
+ message: "Gallery"
5147
+ })
5148
+ }), /* @__PURE__ */ jsx(Button, {
5149
+ type: "button",
5150
+ variant: "ghost",
5151
+ shape: "square",
5152
+ className: "h-8 w-8",
5153
+ onClick: onClose,
5154
+ "aria-label": _t({
5155
+ id: "A6xK0W",
5156
+ message: "Close gallery settings"
5157
+ }),
5158
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
5159
+ })]
5160
+ }),
5161
+ /* @__PURE__ */ jsx(Select, {
5162
+ label: _t({
5163
+ id: "jEu4bB",
5164
+ message: "Columns"
5165
+ }),
5166
+ value: String(columns),
5167
+ onValueChange: (v) => apply({ columns: v ? parseInt(v, 10) : void 0 }),
5168
+ items: {
5169
+ "1": "1",
5170
+ "2": "2",
5171
+ "3": "3",
5172
+ "4": "4",
5173
+ "5": "5",
5174
+ "6": "6"
5175
+ }
5176
+ }),
5177
+ /* @__PURE__ */ jsxs("div", {
5178
+ className: "flex items-center justify-between",
5179
+ children: [/* @__PURE__ */ jsx(Label, { children: _t({
5180
+ id: "an5hVd",
5181
+ message: "Images"
5182
+ }) }), /* @__PURE__ */ jsx(Button, {
5183
+ type: "button",
5184
+ variant: "outline",
5185
+ size: "sm",
5186
+ icon: /* @__PURE__ */ jsx(Plus, {}),
5187
+ onClick: () => setShowMediaPicker(true),
5188
+ children: _t({
5189
+ id: "9Pd+JF",
5190
+ message: "Add Images"
5191
+ })
5192
+ })]
5193
+ }),
5194
+ images.length === 0 ? /* @__PURE__ */ jsx("p", {
5195
+ className: "text-sm text-kumo-subtle text-center py-4",
5196
+ children: _t({
5197
+ id: "aETJnp",
5198
+ message: "No images in this gallery yet."
5199
+ })
5200
+ }) : /* @__PURE__ */ jsx(DndContext, {
5201
+ sensors,
5202
+ collisionDetection: closestCenter,
5203
+ onDragEnd: handleDragEnd,
5204
+ children: /* @__PURE__ */ jsx(SortableContext, {
5205
+ items: images.map((image) => image._key),
5206
+ strategy: rectSortingStrategy,
5207
+ children: /* @__PURE__ */ jsx("div", {
5208
+ className: "grid grid-cols-3 gap-2",
5209
+ children: images.map((image, index) => /* @__PURE__ */ jsx(SortableGalleryThumb, {
5210
+ image,
5211
+ index,
5212
+ selected: selectedKey === image._key,
5213
+ onSelect: () => setSelectedKey((prev) => prev === image._key ? null : image._key),
5214
+ onRemove: () => handleRemove(image._key)
5215
+ }, image._key))
5216
+ })
5217
+ })
5218
+ }),
5219
+ selectedImage && /* @__PURE__ */ jsx(GalleryImageSettings, {
5220
+ image: selectedImage,
5221
+ onChange: (patch) => handleImageChange(selectedImage._key, patch),
5222
+ onReplace: (item) => handleReplace(selectedImage._key, item)
5223
+ }, selectedImage._key),
5224
+ /* @__PURE__ */ jsx(Button, {
5225
+ type: "button",
5226
+ variant: "destructive",
5227
+ className: "w-full",
5228
+ onClick: onDelete,
5229
+ children: _t({
5230
+ id: "HjItFk",
5231
+ message: "Delete gallery"
5232
+ })
5233
+ }),
5234
+ /* @__PURE__ */ jsx(MediaPickerModal, {
5235
+ open: showMediaPicker,
5236
+ onOpenChange: setShowMediaPicker,
5237
+ multiple: true,
5238
+ onSelect: () => {},
5239
+ onSelectMany: handleAdd,
5240
+ mimeTypeFilters: ["image/"],
5241
+ title: _t({
5242
+ id: "yHTMc+",
5243
+ message: "Add images to gallery"
5244
+ })
5245
+ })
5246
+ ]
5247
+ });
5248
+ if (inline) return body;
5249
+ return /* @__PURE__ */ jsx("div", {
5250
+ className: "fixed inset-y-0 end-0 w-96 max-w-full bg-kumo-base border-s shadow-lg z-50 overflow-y-auto p-4",
5251
+ children: body
5252
+ });
5253
+ }
5254
+ function SortableGalleryThumb({ image, index, selected, onSelect, onRemove }) {
5255
+ const { _: _t2 } = useLingui();
5256
+ const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: image._key });
5257
+ return /* @__PURE__ */ jsxs("div", {
5258
+ ref: setNodeRef,
5259
+ style: {
5260
+ transform: CSS.Transform.toString(transform),
5261
+ transition
5262
+ },
5263
+ className: "relative group",
5264
+ children: [
5265
+ /* @__PURE__ */ jsx(Button, {
5266
+ type: "button",
5267
+ variant: "ghost",
5268
+ shape: "square",
5269
+ "aria-label": image.alt || _t2({
5270
+ id: "4lIvAN",
5271
+ message: "Image {0}",
5272
+ values: { 0: index + 1 }
5273
+ }),
5274
+ className: cn("aspect-square h-full w-full rounded-md border overflow-hidden", selected && "ring-2 ring-kumo-brand"),
5275
+ onClick: onSelect,
5276
+ ...attributes,
5277
+ ...listeners,
5278
+ children: /* @__PURE__ */ jsx("img", {
5279
+ src: galleryImageUrl(image),
5280
+ alt: image.alt || "",
5281
+ className: "object-cover w-full h-full",
5282
+ draggable: false
5283
+ })
5284
+ }),
5285
+ /* @__PURE__ */ jsx(Button, {
5286
+ type: "button",
5287
+ variant: "destructive",
5288
+ shape: "square",
5289
+ className: "absolute top-1 end-1 h-6 w-6 opacity-0 group-hover:opacity-100 focus:opacity-100",
5290
+ onClick: (e) => {
5291
+ e.stopPropagation();
5292
+ onRemove();
5293
+ },
5294
+ onPointerDown: (e) => e.stopPropagation(),
5295
+ "aria-label": _t2({
5296
+ id: "YMIArw",
5297
+ message: "Remove image {0}",
5298
+ values: { 0: index + 1 }
5299
+ }),
5300
+ children: /* @__PURE__ */ jsx(Trash, { className: "h-3 w-3" })
5301
+ }),
5302
+ /* @__PURE__ */ jsx("span", {
5303
+ className: "absolute bottom-1 start-1 text-[10px] bg-black/60 text-white rounded px-1",
5304
+ "aria-hidden": true,
5305
+ children: index + 1
5306
+ })
5307
+ ]
5308
+ });
5309
+ }
5310
+ function GalleryImageSettings({ image, onChange, onReplace }) {
5311
+ const { _: _t3 } = useLingui();
5312
+ const [showReplacePicker, setShowReplacePicker] = React$1.useState(false);
5313
+ const hasOriginalSize = typeof image.width === "number" && typeof image.height === "number";
5314
+ return /* @__PURE__ */ jsxs("div", {
5315
+ className: "border rounded-lg p-3 space-y-3",
5316
+ children: [
5317
+ /* @__PURE__ */ jsxs("div", {
5318
+ className: "aspect-video bg-kumo-tint rounded-lg overflow-hidden flex items-center justify-center relative group",
5319
+ children: [/* @__PURE__ */ jsx("img", {
5320
+ src: galleryImageUrl(image),
5321
+ alt: image.alt || "",
5322
+ className: "max-h-full max-w-full object-contain",
5323
+ draggable: false
5324
+ }), /* @__PURE__ */ jsx("div", {
5325
+ className: "absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center",
5326
+ children: /* @__PURE__ */ jsx(Button, {
5327
+ type: "button",
5328
+ variant: "outline",
5329
+ size: "sm",
5330
+ icon: /* @__PURE__ */ jsx(ImageSquare, {}),
5331
+ onClick: () => setShowReplacePicker(true),
5332
+ children: _t3({
5333
+ id: "YOzD/a",
5334
+ message: "Replace image"
5335
+ })
5336
+ })
5337
+ })]
5338
+ }),
5339
+ hasOriginalSize && /* @__PURE__ */ jsxs("div", {
5340
+ className: "flex items-center gap-2 text-sm",
5341
+ children: [/* @__PURE__ */ jsx("span", {
5342
+ className: "text-kumo-subtle",
5343
+ children: _t3({
5344
+ id: "9DNTTa",
5345
+ message: "Original:"
5346
+ })
5347
+ }), /* @__PURE__ */ jsxs("span", { children: [
5348
+ image.width,
5349
+ " × ",
5350
+ image.height
5351
+ ] })]
5352
+ }),
5353
+ /* @__PURE__ */ jsx(Input, {
5354
+ label: _t3({
5355
+ id: "u/DP73",
5356
+ message: "Alt text"
5357
+ }),
5358
+ value: image.alt ?? "",
5359
+ onChange: (e) => onChange({ alt: e.target.value }),
5360
+ placeholder: _t3({
5361
+ id: "BwNww/",
5362
+ message: "Describe the image..."
5363
+ })
5364
+ }),
5365
+ /* @__PURE__ */ jsx(Input, {
5366
+ label: _t3({
5367
+ id: "3Z1aAQ",
5368
+ message: "Caption"
5369
+ }),
5370
+ value: image.caption ?? "",
5371
+ onChange: (e) => onChange({ caption: e.target.value || void 0 }),
5372
+ placeholder: _t3({
5373
+ id: "0IiIv1",
5374
+ message: "Optional caption"
5375
+ })
5376
+ }),
5377
+ /* @__PURE__ */ jsx(MediaPickerModal, {
5378
+ open: showReplacePicker,
5379
+ onOpenChange: setShowReplacePicker,
5380
+ onSelect: (item) => {
5381
+ onReplace(item);
5382
+ setShowReplacePicker(false);
5383
+ },
5384
+ mimeTypeFilters: ["image/"],
5385
+ title: _t3({
5386
+ id: "YOzD/a",
5387
+ message: "Replace image"
5388
+ })
5389
+ })
5390
+ ]
5391
+ });
5392
+ }
5393
+
4818
5394
  //#endregion
4819
5395
  //#region src/components/editor/ImageDetailPanel.tsx
4820
5396
  /**
@@ -5611,7 +6187,7 @@ function formatFullDate(dateString) {
5611
6187
  * RevisionHistory component - displays revision history for a content item
5612
6188
  * with ability to restore previous versions.
5613
6189
  */
5614
- function RevisionHistory({ collection, entryId, onRestored }) {
6190
+ function RevisionHistory({ collection, entryId, onRestored, reserveHeaderEnd = false }) {
5615
6191
  const { _: _t } = useLingui();
5616
6192
  const [isExpanded, setIsExpanded] = React$1.useState(false);
5617
6193
  const [selectedRevision, setSelectedRevision] = React$1.useState(null);
@@ -5679,7 +6255,7 @@ function RevisionHistory({ collection, entryId, onRestored }) {
5679
6255
  variant: "ghost",
5680
6256
  className: "relative justify-between",
5681
6257
  style: {
5682
- width: "calc(100% + 1.5rem)",
6258
+ width: reserveHeaderEnd ? "calc(100% - 1.5rem)" : "calc(100% + 1.5rem)",
5683
6259
  insetInlineStart: "-0.75rem"
5684
6260
  }
5685
6261
  }),
@@ -6483,6 +7059,207 @@ function SeoPanel({ contentKey, seo, onChange }) {
6483
7059
  });
6484
7060
  }
6485
7061
 
7062
+ //#endregion
7063
+ //#region src/lib/content-settings-layout.ts
7064
+ const CONTENT_SETTINGS_LAYOUT_VERSION = 1;
7065
+ const DEFAULT_CONTENT_SETTINGS_SECTION_ORDER = [
7066
+ "publish",
7067
+ "ownership",
7068
+ "bylines",
7069
+ "translations",
7070
+ "taxonomies",
7071
+ "seo",
7072
+ "outline",
7073
+ "revisions"
7074
+ ];
7075
+ const KNOWN_SECTION_IDS = new Set(DEFAULT_CONTENT_SETTINGS_SECTION_ORDER);
7076
+ function isRecord(value) {
7077
+ return typeof value === "object" && value !== null;
7078
+ }
7079
+ function isKnownSectionId(value) {
7080
+ return typeof value === "string" && KNOWN_SECTION_IDS.has(value);
7081
+ }
7082
+ /** Parse a browser preference without allowing malformed state to break the editor. */
7083
+ function parseContentSettingsLayout(raw) {
7084
+ if (!raw) return null;
7085
+ try {
7086
+ const value = JSON.parse(raw);
7087
+ if (!isRecord(value) || value.version !== CONTENT_SETTINGS_LAYOUT_VERSION || !Array.isArray(value.order)) return null;
7088
+ return {
7089
+ version: CONTENT_SETTINGS_LAYOUT_VERSION,
7090
+ order: value.order.filter(isKnownSectionId)
7091
+ };
7092
+ } catch {
7093
+ return null;
7094
+ }
7095
+ }
7096
+ /**
7097
+ * Reconcile saved order with the current defaults. Duplicate and unknown ids
7098
+ * disappear, while sections introduced by a later EmDash version append.
7099
+ */
7100
+ function resolveContentSettingsLayout(stored) {
7101
+ const seen = /* @__PURE__ */ new Set();
7102
+ const order = (stored?.order ?? []).filter((id) => {
7103
+ if (seen.has(id)) return false;
7104
+ seen.add(id);
7105
+ return true;
7106
+ });
7107
+ for (const id of DEFAULT_CONTENT_SETTINGS_SECTION_ORDER) {
7108
+ if (seen.has(id)) continue;
7109
+ order.push(id);
7110
+ seen.add(id);
7111
+ }
7112
+ return {
7113
+ version: CONTENT_SETTINGS_LAYOUT_VERSION,
7114
+ order
7115
+ };
7116
+ }
7117
+ function reorderContentSettingsLayout(layout, activeId, overId) {
7118
+ if (activeId === overId) return layout;
7119
+ const from = layout.order.indexOf(activeId);
7120
+ const to = layout.order.indexOf(overId);
7121
+ if (from < 0 || to < 0) return layout;
7122
+ const order = [...layout.order];
7123
+ const [moved] = order.splice(from, 1);
7124
+ if (!moved) return layout;
7125
+ order.splice(to, 0, moved);
7126
+ return {
7127
+ ...layout,
7128
+ order
7129
+ };
7130
+ }
7131
+
7132
+ //#endregion
7133
+ //#region src/components/SortableContentSettingsSections.tsx
7134
+ const STORAGE_PREFIX = "emdash:content-settings-layout:v1";
7135
+ const restrictToVerticalAxis = ({ transform }) => ({
7136
+ ...transform,
7137
+ x: 0
7138
+ });
7139
+ function readStoredLayout(storageKey) {
7140
+ if (!storageKey || typeof window === "undefined") return null;
7141
+ try {
7142
+ return parseContentSettingsLayout(window.localStorage.getItem(storageKey));
7143
+ } catch {
7144
+ return null;
7145
+ }
7146
+ }
7147
+ function writeStoredLayout(storageKey, layout) {
7148
+ if (!storageKey || typeof window === "undefined") return;
7149
+ try {
7150
+ window.localStorage.setItem(storageKey, JSON.stringify(layout));
7151
+ } catch {}
7152
+ }
7153
+ function SortableContentSettingsSections({ collection, userId, onSortingChange, children }) {
7154
+ const storageKey = userId ? `${STORAGE_PREFIX}:${encodeURIComponent(userId)}:${encodeURIComponent(collection)}` : null;
7155
+ const [storedLayout, setStoredLayout] = React$1.useState(null);
7156
+ const [activeId, setActiveId] = React$1.useState(null);
7157
+ React$1.useEffect(() => {
7158
+ setStoredLayout(readStoredLayout(storageKey));
7159
+ }, [storageKey]);
7160
+ const layout = React$1.useMemo(() => resolveContentSettingsLayout(storedLayout), [storedLayout]);
7161
+ const sectionsById = React$1.useMemo(() => {
7162
+ const sections = React$1.Children.toArray(children).filter((child) => React$1.isValidElement(child));
7163
+ return new Map(sections.map((section) => [section.props.id, section]));
7164
+ }, [children]);
7165
+ const visibleIds = React$1.useMemo(() => layout.order.filter((id) => sectionsById.has(id)), [layout.order, sectionsById]);
7166
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
7167
+ const handleDragStart = React$1.useCallback((event) => {
7168
+ setActiveId(String(event.active.id));
7169
+ onSortingChange?.(true);
7170
+ }, [onSortingChange]);
7171
+ const handleDragCancel = React$1.useCallback(() => {
7172
+ setActiveId(null);
7173
+ onSortingChange?.(false);
7174
+ }, [onSortingChange]);
7175
+ const handleDragEnd = React$1.useCallback((event) => {
7176
+ setActiveId(null);
7177
+ onSortingChange?.(false);
7178
+ if (event.over && event.active.id !== event.over.id) {
7179
+ const movedId = String(event.active.id);
7180
+ const overId = String(event.over.id);
7181
+ setStoredLayout((current) => {
7182
+ const next = reorderContentSettingsLayout(resolveContentSettingsLayout(current), movedId, overId);
7183
+ writeStoredLayout(storageKey, next);
7184
+ return next;
7185
+ });
7186
+ }
7187
+ }, [onSortingChange, storageKey]);
7188
+ return /* @__PURE__ */ jsx(DndContext, {
7189
+ sensors,
7190
+ collisionDetection: closestCenter,
7191
+ modifiers: [restrictToVerticalAxis],
7192
+ measuring: { droppable: { strategy: MeasuringStrategy.Always } },
7193
+ onDragStart: handleDragStart,
7194
+ onDragCancel: handleDragCancel,
7195
+ onDragEnd: handleDragEnd,
7196
+ children: /* @__PURE__ */ jsx(SortableContext, {
7197
+ items: visibleIds,
7198
+ strategy: verticalListSortingStrategy,
7199
+ children: visibleIds.map((id) => {
7200
+ const section = sectionsById.get(id);
7201
+ return section ? React$1.cloneElement(section, {
7202
+ key: id,
7203
+ isSorting: activeId !== null
7204
+ }) : null;
7205
+ })
7206
+ })
7207
+ });
7208
+ }
7209
+ function SortableContentSettingsSection({ id, label, disclosure = false, children, isSorting = false }) {
7210
+ const { _: _t } = useLingui();
7211
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
7212
+ return /* @__PURE__ */ jsxs("section", {
7213
+ ref: setNodeRef,
7214
+ style: {
7215
+ transform: transform ? CSS.Transform.toString({
7216
+ ...transform,
7217
+ x: 0
7218
+ }) : void 0,
7219
+ transition,
7220
+ zIndex: isDragging ? 10 : void 0,
7221
+ inlineSize: "100%"
7222
+ },
7223
+ "data-sorting": isSorting ? "true" : "false",
7224
+ "data-disclosure": disclosure ? "true" : "false",
7225
+ className: cn("relative min-w-0 border-t bg-kumo-base first:border-t-0", isSorting && "[&>*:not([data-sortable-heading]):not([data-sortable-handle])]:hidden", isDragging && "bg-kumo-tint opacity-60"),
7226
+ children: [
7227
+ isSorting && /* @__PURE__ */ jsx("div", {
7228
+ "data-sortable-heading": true,
7229
+ className: "flex items-center px-4 pe-12",
7230
+ style: { minHeight: 48 },
7231
+ children: /* @__PURE__ */ jsx("span", {
7232
+ className: "text-[15px] font-semibold",
7233
+ children: label
7234
+ })
7235
+ }),
7236
+ children,
7237
+ /* @__PURE__ */ jsx("button", {
7238
+ type: "button",
7239
+ "data-sortable-handle": true,
7240
+ "data-sorting": isSorting ? "true" : "false",
7241
+ ...attributes,
7242
+ ...listeners,
7243
+ className: cn("absolute z-10 grid size-7 touch-none cursor-grab place-items-center rounded-md text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-kumo-accent active:cursor-grabbing", isSorting ? "end-3 top-1/2 -translate-y-1/2" : "top-3", !isSorting && "end-3"),
7244
+ "aria-label": _t({
7245
+ id: "LGF0vE",
7246
+ message: "Drag to reorder {label}",
7247
+ values: { label }
7248
+ }),
7249
+ title: _t({
7250
+ id: "LGF0vE",
7251
+ message: "Drag to reorder {label}",
7252
+ values: { label }
7253
+ }),
7254
+ children: /* @__PURE__ */ jsx(DotsSixVertical, {
7255
+ size: 16,
7256
+ "aria-hidden": "true"
7257
+ })
7258
+ })
7259
+ ]
7260
+ });
7261
+ }
7262
+
6486
7263
  //#endregion
6487
7264
  //#region src/lib/taxonomy-match.ts
6488
7265
  /**
@@ -6558,6 +7335,17 @@ const EMPTY_TERMS = [];
6558
7335
  async function fetchTaxonomyDefs$1() {
6559
7336
  return (await parseApiResponse(await apiFetch(`/_emdash/api/taxonomies`), "Failed to fetch taxonomies")).taxonomies;
6560
7337
  }
7338
+ function useApplicableTaxonomies(collection) {
7339
+ const { data: taxonomies = [] } = useQuery({
7340
+ queryKey: ["taxonomy-defs"],
7341
+ queryFn: fetchTaxonomyDefs$1
7342
+ });
7343
+ return taxonomies.filter((taxonomy) => taxonomy.collections.includes(collection));
7344
+ }
7345
+ /** Whether the editor should include a taxonomy settings section. */
7346
+ function useHasApplicableTaxonomies(collection) {
7347
+ return useApplicableTaxonomies(collection).length > 0;
7348
+ }
6561
7349
  /**
6562
7350
  * Fetch terms for a taxonomy, scoped to the entry's locale so only the matching
6563
7351
  * translation variants are offered.
@@ -6927,11 +7715,7 @@ function TaxonomySection({ taxonomy, collection, entryId, entryLocale, onChange
6927
7715
  */
6928
7716
  function TaxonomySidebar({ collection, entryId, entryLocale, onChange, className }) {
6929
7717
  const { _: _t3 } = useLingui();
6930
- const { data: taxonomies = [] } = useQuery({
6931
- queryKey: ["taxonomy-defs"],
6932
- queryFn: fetchTaxonomyDefs$1
6933
- });
6934
- const applicableTaxonomies = taxonomies.filter((tax) => tax.collections.includes(collection));
7718
+ const applicableTaxonomies = useApplicableTaxonomies(collection);
6935
7719
  if (applicableTaxonomies.length === 0) return null;
6936
7720
  return /* @__PURE__ */ jsx("div", {
6937
7721
  className: cn(className),
@@ -7228,7 +8012,9 @@ const ContentSettingsPanel = React$1.memo(function ContentSettingsPanel({ collec
7228
8012
  const navigate = useNavigate$1();
7229
8013
  const [scheduleDate, setScheduleDate] = React$1.useState("");
7230
8014
  const [showScheduler, setShowScheduler] = React$1.useState(false);
8015
+ const [isReorderingSections, setIsReorderingSections] = React$1.useState(false);
7231
8016
  const showDiscard = !isNew && supportsDrafts && hasPendingChanges && !!onDiscardDraft;
8017
+ const hasApplicableTaxonomies = useHasApplicableTaxonomies(collection);
7232
8018
  const handleScheduleSubmit = () => {
7233
8019
  if (scheduleDate && onSchedule) {
7234
8020
  onSchedule(new Date(scheduleDate).toISOString());
@@ -7246,318 +8032,395 @@ const ContentSettingsPanel = React$1.memo(function ContentSettingsPanel({ collec
7246
8032
  onClose: onBlockSidebarClose,
7247
8033
  inline: true
7248
8034
  })
8035
+ }) : blockSidebarPanel.type === "gallery" ? /* @__PURE__ */ jsx("div", {
8036
+ className: "p-4",
8037
+ children: /* @__PURE__ */ jsx(GalleryDetailPanel, {
8038
+ attributes: blockSidebarPanel.attrs,
8039
+ onUpdate: (attrs) => blockSidebarPanel.onUpdate(attrs),
8040
+ onDelete: onBlockSidebarDelete,
8041
+ onClose: onBlockSidebarClose,
8042
+ inline: true
8043
+ })
7249
8044
  }) : null;
7250
8045
  return /* @__PURE__ */ jsxs("div", {
7251
8046
  className: "flex flex-col whitespace-normal",
7252
- children: [
7253
- /* @__PURE__ */ jsxs("div", {
7254
- className: "p-4",
7255
- children: [
7256
- /* @__PURE__ */ jsx(Text, {
7257
- bold: true,
7258
- as: "h3",
7259
- DANGEROUS_className: "mb-4",
7260
- children: _t5({
7261
- id: "EEYbdt",
7262
- message: "Publish"
7263
- })
8047
+ children: [/* @__PURE__ */ jsxs(SortableContentSettingsSections, {
8048
+ collection,
8049
+ userId: currentUser?.id,
8050
+ onSortingChange: setIsReorderingSections,
8051
+ children: [
8052
+ /* @__PURE__ */ jsx(SortableContentSettingsSection, {
8053
+ id: "publish",
8054
+ label: _t5({
8055
+ id: "EEYbdt",
8056
+ message: "Publish"
7264
8057
  }),
7265
- /* @__PURE__ */ jsxs("div", {
7266
- className: "space-y-4",
8058
+ children: /* @__PURE__ */ jsxs("div", {
8059
+ className: "p-4",
7267
8060
  children: [
7268
- /* @__PURE__ */ jsx(Input, {
7269
- label: _t5({
7270
- id: "L85WcV",
7271
- message: "Slug"
7272
- }),
7273
- value: slug,
7274
- onChange: (e) => onSlugChange(e.target.value),
7275
- placeholder: "my-post-slug"
7276
- }),
7277
- /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
7278
- className: "flex flex-wrap items-center gap-x-3 gap-y-1.5",
7279
- children: [/* @__PURE__ */ jsx(Label, { children: _t5({
7280
- id: "uAQUqI",
7281
- message: "Status"
7282
- }) }), supportsDrafts ? /* @__PURE__ */ jsxs(Fragment, { children: [
7283
- isLive && /* @__PURE__ */ jsx(Badge, {
7284
- variant: "success",
7285
- children: _t5({
7286
- id: "u3wRF+",
7287
- message: "Published"
7288
- })
7289
- }),
7290
- hasPendingChanges && /* @__PURE__ */ jsx(Badge, {
7291
- variant: "secondary",
7292
- children: _t5({
7293
- id: "fkUmSB",
7294
- message: "Pending changes"
7295
- })
7296
- }),
7297
- !isLive && !hasSchedule && /* @__PURE__ */ jsx(Badge, {
7298
- variant: "secondary",
7299
- children: _t5({
7300
- id: "eneWvv",
7301
- message: "Draft"
7302
- })
7303
- }),
7304
- hasSchedule && /* @__PURE__ */ jsx(Badge, {
7305
- variant: "outline",
7306
- children: _t5({
7307
- id: "4ba0NE",
7308
- message: "Scheduled"
7309
- })
7310
- })
7311
- ] }) : /* @__PURE__ */ jsx(Badge, {
7312
- variant: "secondary",
7313
- children: status.charAt(0).toUpperCase() + status.slice(1)
7314
- })]
7315
- }), showDiscard && /* @__PURE__ */ jsx("div", {
7316
- className: "mt-2",
7317
- children: /* @__PURE__ */ jsx(DiscardDraftDialog, {
7318
- onDiscard: onDiscardDraft,
7319
- triggerVariant: "outline",
7320
- triggerSize: "sm"
8061
+ /* @__PURE__ */ jsx(Text, {
8062
+ bold: true,
8063
+ as: "h3",
8064
+ DANGEROUS_className: "mb-4",
8065
+ children: _t5({
8066
+ id: "EEYbdt",
8067
+ message: "Publish"
7321
8068
  })
7322
- })] }),
7323
- item?.scheduledAt && /* @__PURE__ */ jsxs("div", {
7324
- className: "flex items-center justify-between gap-2 rounded-lg border px-3 py-2",
7325
- children: [/* @__PURE__ */ jsx("p", {
7326
- className: "text-xs text-kumo-subtle",
7327
- children: _t5({
7328
- id: "MjRZEl",
7329
- message: "Scheduled for: {0}",
7330
- values: { 0: formatScheduledDate(item.scheduledAt) }
7331
- })
7332
- }), /* @__PURE__ */ jsx(Button, {
7333
- type: "button",
7334
- variant: "outline",
7335
- size: "sm",
7336
- onClick: onUnschedule,
7337
- children: _t5({
7338
- id: "ssbNAt",
7339
- message: "Unschedule"
7340
- })
7341
- })]
7342
8069
  }),
7343
- canSchedule && /* @__PURE__ */ jsx("div", {
7344
- className: "pt-2",
7345
- children: showScheduler ? /* @__PURE__ */ jsxs("div", {
7346
- className: "space-y-2",
7347
- children: [/* @__PURE__ */ jsx(Input, {
8070
+ /* @__PURE__ */ jsxs("div", {
8071
+ className: "space-y-4",
8072
+ children: [
8073
+ /* @__PURE__ */ jsx(Input, {
7348
8074
  label: _t5({
7349
- id: "x6lWtJ",
7350
- message: "Schedule for"
8075
+ id: "L85WcV",
8076
+ message: "Slug"
7351
8077
  }),
7352
- type: "datetime-local",
7353
- value: scheduleDate,
7354
- onChange: (e) => setScheduleDate(e.target.value),
7355
- min: (/* @__PURE__ */ new Date()).toISOString().slice(0, 16)
7356
- }), /* @__PURE__ */ jsxs("div", {
7357
- className: "flex gap-2",
7358
- children: [/* @__PURE__ */ jsx(Button, {
7359
- type: "button",
7360
- size: "sm",
7361
- onClick: handleScheduleSubmit,
7362
- disabled: !scheduleDate || isScheduling,
7363
- icon: isScheduling ? /* @__PURE__ */ jsx(Loader, { size: "sm" }) : void 0,
8078
+ value: slug,
8079
+ onChange: (e) => onSlugChange(e.target.value),
8080
+ placeholder: "my-post-slug"
8081
+ }),
8082
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
8083
+ className: "flex flex-wrap items-center gap-x-3 gap-y-1.5",
8084
+ children: [/* @__PURE__ */ jsx(Label, { children: _t5({
8085
+ id: "uAQUqI",
8086
+ message: "Status"
8087
+ }) }), supportsDrafts ? /* @__PURE__ */ jsxs(Fragment, { children: [
8088
+ isLive && /* @__PURE__ */ jsx(Badge, {
8089
+ variant: "success",
8090
+ children: _t5({
8091
+ id: "u3wRF+",
8092
+ message: "Published"
8093
+ })
8094
+ }),
8095
+ hasPendingChanges && /* @__PURE__ */ jsx(Badge, {
8096
+ variant: "secondary",
8097
+ children: _t5({
8098
+ id: "fkUmSB",
8099
+ message: "Pending changes"
8100
+ })
8101
+ }),
8102
+ !isLive && !hasSchedule && /* @__PURE__ */ jsx(Badge, {
8103
+ variant: "secondary",
8104
+ children: _t5({
8105
+ id: "eneWvv",
8106
+ message: "Draft"
8107
+ })
8108
+ }),
8109
+ hasSchedule && /* @__PURE__ */ jsx(Badge, {
8110
+ variant: "outline",
8111
+ children: _t5({
8112
+ id: "4ba0NE",
8113
+ message: "Scheduled"
8114
+ })
8115
+ })
8116
+ ] }) : /* @__PURE__ */ jsx(Badge, {
8117
+ variant: "secondary",
8118
+ children: status.charAt(0).toUpperCase() + status.slice(1)
8119
+ })]
8120
+ }), showDiscard && /* @__PURE__ */ jsx("div", {
8121
+ className: "mt-2",
8122
+ children: /* @__PURE__ */ jsx(DiscardDraftDialog, {
8123
+ onDiscard: onDiscardDraft,
8124
+ triggerVariant: "outline",
8125
+ triggerSize: "sm"
8126
+ })
8127
+ })] }),
8128
+ item?.scheduledAt && /* @__PURE__ */ jsxs("div", {
8129
+ className: "flex items-center justify-between gap-2 rounded-lg border px-3 py-2",
8130
+ children: [/* @__PURE__ */ jsx("p", {
8131
+ className: "text-xs text-kumo-subtle",
7364
8132
  children: _t5({
7365
- id: "gmB6oO",
7366
- message: "Schedule"
8133
+ id: "MjRZEl",
8134
+ message: "Scheduled for: {0}",
8135
+ values: { 0: formatScheduledDate(item.scheduledAt) }
7367
8136
  })
7368
8137
  }), /* @__PURE__ */ jsx(Button, {
7369
8138
  type: "button",
7370
8139
  variant: "outline",
7371
8140
  size: "sm",
7372
- onClick: () => {
7373
- setShowScheduler(false);
7374
- setScheduleDate("");
7375
- },
8141
+ onClick: onUnschedule,
7376
8142
  children: _t5({
7377
- id: "dEgA5A",
7378
- message: "Cancel"
8143
+ id: "ssbNAt",
8144
+ message: "Unschedule"
7379
8145
  })
7380
8146
  })]
7381
- })]
7382
- }) : /* @__PURE__ */ jsx(Button, {
7383
- type: "button",
7384
- variant: "outline",
7385
- size: "sm",
7386
- className: "w-full",
7387
- onClick: () => setShowScheduler(true),
7388
- children: _t5({
7389
- id: "QS1Nla",
7390
- message: "Schedule for later"
8147
+ }),
8148
+ canSchedule && /* @__PURE__ */ jsx("div", {
8149
+ className: "pt-2",
8150
+ children: showScheduler ? /* @__PURE__ */ jsxs("div", {
8151
+ className: "space-y-2",
8152
+ children: [/* @__PURE__ */ jsx(Input, {
8153
+ label: _t5({
8154
+ id: "x6lWtJ",
8155
+ message: "Schedule for"
8156
+ }),
8157
+ type: "datetime-local",
8158
+ value: scheduleDate,
8159
+ onChange: (e) => setScheduleDate(e.target.value),
8160
+ min: (/* @__PURE__ */ new Date()).toISOString().slice(0, 16)
8161
+ }), /* @__PURE__ */ jsxs("div", {
8162
+ className: "flex gap-2",
8163
+ children: [/* @__PURE__ */ jsx(Button, {
8164
+ type: "button",
8165
+ size: "sm",
8166
+ onClick: handleScheduleSubmit,
8167
+ disabled: !scheduleDate || isScheduling,
8168
+ icon: isScheduling ? /* @__PURE__ */ jsx(Loader, { size: "sm" }) : void 0,
8169
+ children: _t5({
8170
+ id: "gmB6oO",
8171
+ message: "Schedule"
8172
+ })
8173
+ }), /* @__PURE__ */ jsx(Button, {
8174
+ type: "button",
8175
+ variant: "outline",
8176
+ size: "sm",
8177
+ onClick: () => {
8178
+ setShowScheduler(false);
8179
+ setScheduleDate("");
8180
+ },
8181
+ children: _t5({
8182
+ id: "dEgA5A",
8183
+ message: "Cancel"
8184
+ })
8185
+ })]
8186
+ })]
8187
+ }) : /* @__PURE__ */ jsx(Button, {
8188
+ type: "button",
8189
+ variant: "outline",
8190
+ size: "sm",
8191
+ className: "w-full",
8192
+ onClick: () => setShowScheduler(true),
8193
+ children: _t5({
8194
+ id: "QS1Nla",
8195
+ message: "Schedule for later"
8196
+ })
8197
+ })
7391
8198
  })
7392
- })
8199
+ ]
8200
+ }),
8201
+ item && /* @__PURE__ */ jsxs("dl", {
8202
+ "data-testid": "content-timestamps",
8203
+ className: "mt-4 border-t pt-4 space-y-1 text-xs text-kumo-subtle",
8204
+ children: [/* @__PURE__ */ jsxs("div", {
8205
+ className: "flex items-center justify-between gap-2",
8206
+ children: [/* @__PURE__ */ jsx("dt", { children: _t5({
8207
+ id: "d+F6q9",
8208
+ message: "Created"
8209
+ }) }), /* @__PURE__ */ jsx("dd", { children: new Date(item.createdAt).toLocaleString() })]
8210
+ }), /* @__PURE__ */ jsxs("div", {
8211
+ className: "flex items-center justify-between gap-2",
8212
+ children: [/* @__PURE__ */ jsx("dt", { children: _t5({
8213
+ id: "+b7T3G",
8214
+ message: "Updated"
8215
+ }) }), /* @__PURE__ */ jsx("dd", { children: new Date(item.updatedAt).toLocaleString() })]
8216
+ })]
7393
8217
  })
7394
8218
  ]
7395
- }),
7396
- item && /* @__PURE__ */ jsxs("dl", {
7397
- "data-testid": "content-timestamps",
7398
- className: "mt-4 border-t pt-4 space-y-1 text-xs text-kumo-subtle",
7399
- children: [/* @__PURE__ */ jsxs("div", {
7400
- className: "flex items-center justify-between gap-2",
7401
- children: [/* @__PURE__ */ jsx("dt", { children: _t5({
7402
- id: "d+F6q9",
7403
- message: "Created"
7404
- }) }), /* @__PURE__ */ jsx("dd", { children: new Date(item.createdAt).toLocaleString() })]
7405
- }), /* @__PURE__ */ jsxs("div", {
7406
- className: "flex items-center justify-between gap-2",
7407
- children: [/* @__PURE__ */ jsx("dt", { children: _t5({
7408
- id: "+b7T3G",
7409
- message: "Updated"
7410
- }) }), /* @__PURE__ */ jsx("dd", { children: new Date(item.updatedAt).toLocaleString() })]
7411
- })]
7412
8219
  })
7413
- ]
7414
- }),
7415
- currentUser && currentUser.role >= ROLE_EDITOR$3 && users && users.length > 0 && /* @__PURE__ */ jsxs("div", {
7416
- className: "p-4 border-t",
7417
- children: [/* @__PURE__ */ jsx(Text, {
7418
- bold: true,
7419
- as: "h3",
7420
- DANGEROUS_className: "mb-4",
7421
- children: _t5({
8220
+ }),
8221
+ currentUser && currentUser.role >= ROLE_EDITOR$3 && users && users.length > 0 && /* @__PURE__ */ jsx(SortableContentSettingsSection, {
8222
+ id: "ownership",
8223
+ label: _t5({
7422
8224
  id: "oDAEQq",
7423
8225
  message: "Ownership"
8226
+ }),
8227
+ children: /* @__PURE__ */ jsxs("div", {
8228
+ className: "p-4",
8229
+ children: [/* @__PURE__ */ jsx(Text, {
8230
+ bold: true,
8231
+ as: "h3",
8232
+ DANGEROUS_className: "mb-4",
8233
+ children: _t5({
8234
+ id: "oDAEQq",
8235
+ message: "Ownership"
8236
+ })
8237
+ }), /* @__PURE__ */ jsx(AuthorSelector, {
8238
+ authorId: item?.authorId || null,
8239
+ users,
8240
+ onChange: onAuthorChange
8241
+ })]
7424
8242
  })
7425
- }), /* @__PURE__ */ jsx(AuthorSelector, {
7426
- authorId: item?.authorId || null,
7427
- users,
7428
- onChange: onAuthorChange
7429
- })]
7430
- }),
7431
- currentUser && currentUser.role >= ROLE_EDITOR$3 && /* @__PURE__ */ jsxs("div", {
7432
- className: "p-4 border-t",
7433
- children: [/* @__PURE__ */ jsx(Text, {
7434
- bold: true,
7435
- as: "h3",
7436
- DANGEROUS_className: "mb-4",
7437
- children: _t5({
8243
+ }),
8244
+ currentUser && currentUser.role >= ROLE_EDITOR$3 && /* @__PURE__ */ jsx(SortableContentSettingsSection, {
8245
+ id: "bylines",
8246
+ label: _t5({
7438
8247
  id: "xLGa7J",
7439
8248
  message: "Bylines"
8249
+ }),
8250
+ children: /* @__PURE__ */ jsxs("div", {
8251
+ className: "p-4",
8252
+ children: [/* @__PURE__ */ jsx(Text, {
8253
+ bold: true,
8254
+ as: "h3",
8255
+ DANGEROUS_className: "mb-4",
8256
+ children: _t5({
8257
+ id: "xLGa7J",
8258
+ message: "Bylines"
8259
+ })
8260
+ }), /* @__PURE__ */ jsx(BylineCreditsEditor, {
8261
+ credits: activeBylines,
8262
+ bylines: availableBylines ?? [],
8263
+ selectedBylineDetails: item?.bylines?.map((entry) => entry.byline),
8264
+ bylinesLoaded: availableBylinesLoaded,
8265
+ onChange: onBylinesChange,
8266
+ onQuickCreate: onQuickCreateByline,
8267
+ onQuickEdit: onQuickEditByline,
8268
+ entryLocale: item?.locale ?? entryLocale,
8269
+ i18n
8270
+ })]
7440
8271
  })
7441
- }), /* @__PURE__ */ jsx(BylineCreditsEditor, {
7442
- credits: activeBylines,
7443
- bylines: availableBylines ?? [],
7444
- selectedBylineDetails: item?.bylines?.map((entry) => entry.byline),
7445
- bylinesLoaded: availableBylinesLoaded,
7446
- onChange: onBylinesChange,
7447
- onQuickCreate: onQuickCreateByline,
7448
- onQuickEdit: onQuickEditByline,
7449
- entryLocale: item?.locale ?? entryLocale,
7450
- i18n
7451
- })]
7452
- }),
7453
- i18n && item && !isNew && /* @__PURE__ */ jsx("div", {
7454
- className: "p-4 border-t",
7455
- children: /* @__PURE__ */ jsx(TranslationsPanel, {
7456
- locales: i18n.locales,
7457
- defaultLocale: i18n.defaultLocale,
7458
- currentLocale: item.locale ?? void 0,
7459
- translations: translations ?? [],
7460
- onOpen: (tr) => navigate({
7461
- to: "/content/$collection/$id",
7462
- params: {
7463
- collection,
7464
- id: tr.id
7465
- },
7466
- search: { locale: tr.locale }
8272
+ }),
8273
+ i18n && item && !isNew && /* @__PURE__ */ jsx(SortableContentSettingsSection, {
8274
+ id: "translations",
8275
+ label: _t5({
8276
+ id: "kAy5R9",
8277
+ message: "Translations"
7467
8278
  }),
7468
- onCreate: onTranslate
7469
- })
7470
- }),
7471
- item && /* @__PURE__ */ jsx(TaxonomySidebar, {
7472
- className: "p-4 border-t",
7473
- collection,
7474
- entryId: item.id,
7475
- entryLocale: item.locale ?? entryLocale
7476
- }),
7477
- hasSeo && !isNew && onSeoChange && /* @__PURE__ */ jsxs("div", {
7478
- className: "p-4 border-t",
7479
- children: [/* @__PURE__ */ jsx(Text, {
7480
- bold: true,
7481
- as: "h3",
7482
- DANGEROUS_className: "mb-4",
7483
- children: _t5({
8279
+ children: /* @__PURE__ */ jsx("div", {
8280
+ className: "p-4",
8281
+ children: /* @__PURE__ */ jsx(TranslationsPanel, {
8282
+ locales: i18n.locales,
8283
+ defaultLocale: i18n.defaultLocale,
8284
+ currentLocale: item.locale ?? void 0,
8285
+ translations: translations ?? [],
8286
+ onOpen: (tr) => navigate({
8287
+ to: "/content/$collection/$id",
8288
+ params: {
8289
+ collection,
8290
+ id: tr.id
8291
+ },
8292
+ search: { locale: tr.locale }
8293
+ }),
8294
+ onCreate: onTranslate
8295
+ })
8296
+ })
8297
+ }),
8298
+ item && hasApplicableTaxonomies && /* @__PURE__ */ jsx(SortableContentSettingsSection, {
8299
+ id: "taxonomies",
8300
+ label: _t5({
8301
+ id: "sMNeA/",
8302
+ message: "Taxonomies"
8303
+ }),
8304
+ children: /* @__PURE__ */ jsx(TaxonomySidebar, {
8305
+ className: "p-4",
8306
+ collection,
8307
+ entryId: item.id,
8308
+ entryLocale: item.locale ?? entryLocale
8309
+ })
8310
+ }),
8311
+ hasSeo && !isNew && onSeoChange && /* @__PURE__ */ jsx(SortableContentSettingsSection, {
8312
+ id: "seo",
8313
+ label: _t5({
7484
8314
  id: "4Ml90q",
7485
8315
  message: "SEO"
8316
+ }),
8317
+ children: /* @__PURE__ */ jsxs("div", {
8318
+ className: "p-4",
8319
+ children: [/* @__PURE__ */ jsx(Text, {
8320
+ bold: true,
8321
+ as: "h3",
8322
+ DANGEROUS_className: "mb-4",
8323
+ children: _t5({
8324
+ id: "4Ml90q",
8325
+ message: "SEO"
8326
+ })
8327
+ }), /* @__PURE__ */ jsx(SeoPanel, {
8328
+ contentKey: item?.id ?? `new:${collection}`,
8329
+ seo: item?.seo,
8330
+ onChange: onSeoChange
8331
+ })]
7486
8332
  })
7487
- }), /* @__PURE__ */ jsx(SeoPanel, {
7488
- contentKey: item?.id ?? `new:${collection}`,
7489
- seo: item?.seo,
7490
- onChange: onSeoChange
7491
- })]
7492
- }),
7493
- portableTextEditor && /* @__PURE__ */ jsx("div", {
7494
- className: "p-4 border-t",
7495
- children: /* @__PURE__ */ jsx(DocumentOutline, { editor: portableTextEditor })
7496
- }),
7497
- !isNew && item && supportsRevisions && /* @__PURE__ */ jsx("div", {
7498
- className: "p-4 border-t",
7499
- children: /* @__PURE__ */ jsx(RevisionHistory, {
7500
- collection,
7501
- entryId: item.id
7502
- })
7503
- }),
7504
- !isNew && onDelete && /* @__PURE__ */ jsx("div", {
7505
- className: "border-t p-4",
7506
- children: /* @__PURE__ */ jsxs(Dialog.Root, {
7507
- disablePointerDismissal: true,
7508
- children: [/* @__PURE__ */ jsx(Dialog.Trigger, { render: (p) => /* @__PURE__ */ jsx(Button, {
7509
- ...p,
7510
- type: "button",
7511
- variant: "outline",
7512
- className: "w-full text-kumo-danger hover:text-kumo-danger",
7513
- disabled: isDeleting,
7514
- icon: isDeleting ? /* @__PURE__ */ jsx(Loader, { size: "sm" }) : /* @__PURE__ */ jsx(Trash, {}),
7515
- children: _t5({
7516
- id: "q3TWG7",
7517
- message: "Move to Trash"
8333
+ }),
8334
+ portableTextEditor && /* @__PURE__ */ jsx(SortableContentSettingsSection, {
8335
+ id: "outline",
8336
+ label: _t5({
8337
+ id: "dD7NPy",
8338
+ message: "Outline"
8339
+ }),
8340
+ disclosure: true,
8341
+ children: /* @__PURE__ */ jsx("div", {
8342
+ className: "p-4",
8343
+ children: /* @__PURE__ */ jsx(DocumentOutline, {
8344
+ editor: portableTextEditor,
8345
+ reserveHeaderEnd: true
7518
8346
  })
7519
- }) }), /* @__PURE__ */ jsxs(Dialog, {
7520
- className: "p-6",
7521
- size: "sm",
7522
- children: [
7523
- /* @__PURE__ */ jsx(Dialog.Title, {
7524
- className: "text-lg font-semibold",
8347
+ })
8348
+ }),
8349
+ !isNew && item && supportsRevisions && /* @__PURE__ */ jsx(SortableContentSettingsSection, {
8350
+ id: "revisions",
8351
+ label: _t5({
8352
+ id: "ZdWn/b",
8353
+ message: "Revisions"
8354
+ }),
8355
+ disclosure: true,
8356
+ children: /* @__PURE__ */ jsx("div", {
8357
+ className: "p-4",
8358
+ children: /* @__PURE__ */ jsx(RevisionHistory, {
8359
+ collection,
8360
+ entryId: item.id,
8361
+ reserveHeaderEnd: true
8362
+ })
8363
+ })
8364
+ })
8365
+ ]
8366
+ }), !isNew && onDelete && /* @__PURE__ */ jsx("div", {
8367
+ "data-testid": "content-trash-actions",
8368
+ "aria-hidden": isReorderingSections || void 0,
8369
+ className: cn("border-t p-4", isReorderingSections && "invisible pointer-events-none"),
8370
+ children: /* @__PURE__ */ jsxs(Dialog.Root, {
8371
+ disablePointerDismissal: true,
8372
+ children: [/* @__PURE__ */ jsx(Dialog.Trigger, { render: (p) => /* @__PURE__ */ jsx(Button, {
8373
+ ...p,
8374
+ type: "button",
8375
+ variant: "outline",
8376
+ className: "w-full text-kumo-danger hover:text-kumo-danger",
8377
+ disabled: isDeleting,
8378
+ icon: isDeleting ? /* @__PURE__ */ jsx(Loader, { size: "sm" }) : /* @__PURE__ */ jsx(Trash, {}),
8379
+ children: _t5({
8380
+ id: "q3TWG7",
8381
+ message: "Move to Trash"
8382
+ })
8383
+ }) }), /* @__PURE__ */ jsxs(Dialog, {
8384
+ className: "p-6",
8385
+ size: "sm",
8386
+ children: [
8387
+ /* @__PURE__ */ jsx(Dialog.Title, {
8388
+ className: "text-lg font-semibold",
8389
+ children: _t5({
8390
+ id: "eAnjuA",
8391
+ message: "Move to Trash?"
8392
+ })
8393
+ }),
8394
+ /* @__PURE__ */ jsx(Dialog.Description, {
8395
+ className: "text-kumo-subtle",
8396
+ children: _t5({
8397
+ id: "cXBP3V",
8398
+ message: "This will move the item to trash. You can restore it later from the trash."
8399
+ })
8400
+ }),
8401
+ /* @__PURE__ */ jsxs("div", {
8402
+ className: "mt-6 flex justify-end gap-2",
8403
+ children: [/* @__PURE__ */ jsx(Dialog.Close, { render: (p) => /* @__PURE__ */ jsx(Button, {
8404
+ ...p,
8405
+ variant: "secondary",
7525
8406
  children: _t5({
7526
- id: "eAnjuA",
7527
- message: "Move to Trash?"
8407
+ id: "dEgA5A",
8408
+ message: "Cancel"
7528
8409
  })
7529
- }),
7530
- /* @__PURE__ */ jsx(Dialog.Description, {
7531
- className: "text-kumo-subtle",
8410
+ }) }), /* @__PURE__ */ jsx(Dialog.Close, { render: (p) => /* @__PURE__ */ jsx(Button, {
8411
+ ...p,
8412
+ variant: "destructive",
8413
+ onClick: onDelete,
7532
8414
  children: _t5({
7533
- id: "cXBP3V",
7534
- message: "This will move the item to trash. You can restore it later from the trash."
8415
+ id: "q3TWG7",
8416
+ message: "Move to Trash"
7535
8417
  })
7536
- }),
7537
- /* @__PURE__ */ jsxs("div", {
7538
- className: "mt-6 flex justify-end gap-2",
7539
- children: [/* @__PURE__ */ jsx(Dialog.Close, { render: (p) => /* @__PURE__ */ jsx(Button, {
7540
- ...p,
7541
- variant: "secondary",
7542
- children: _t5({
7543
- id: "dEgA5A",
7544
- message: "Cancel"
7545
- })
7546
- }) }), /* @__PURE__ */ jsx(Dialog.Close, { render: (p) => /* @__PURE__ */ jsx(Button, {
7547
- ...p,
7548
- variant: "destructive",
7549
- onClick: onDelete,
7550
- children: _t5({
7551
- id: "q3TWG7",
7552
- message: "Move to Trash"
7553
- })
7554
- }) })]
7555
- })
7556
- ]
7557
- })]
7558
- })
8418
+ }) })]
8419
+ })
8420
+ ]
8421
+ })]
7559
8422
  })
7560
- ]
8423
+ })]
7561
8424
  });
7562
8425
  });
7563
8426
  function BylineCreditsEditor({ credits, bylines, selectedBylineDetails, onChange, onQuickCreate, onQuickEdit, entryLocale, i18n, bylinesLoaded = true }) {
@@ -9961,7 +10824,7 @@ function HtmlBlockNodeView({ node, updateAttributes, selected, deleteNode }) {
9961
10824
  * An atom node that stores raw HTML in a `html` attribute. Round-trips
9962
10825
  * through Portable Text as `{ _type: "htmlBlock", _key, html }`.
9963
10826
  */
9964
- const HtmlBlockExtension = Node$1.create({
10827
+ const HtmlBlockExtension = Node$2.create({
9965
10828
  name: "htmlBlock",
9966
10829
  group: "block",
9967
10830
  atom: true,
@@ -9982,7 +10845,7 @@ const HtmlBlockExtension = Node$1.create({
9982
10845
  return [{ tag: "div[data-html-block]" }];
9983
10846
  },
9984
10847
  renderHTML({ HTMLAttributes }) {
9985
- return ["div", mergeAttributes(HTMLAttributes, { "data-html-block": "" })];
10848
+ return ["div", mergeAttributes$1(HTMLAttributes, { "data-html-block": "" })];
9986
10849
  },
9987
10850
  addNodeView() {
9988
10851
  return ReactNodeViewRenderer(HtmlBlockNodeView);
@@ -10241,7 +11104,7 @@ function ImageNodeView({ node, updateAttributes, selected, deleteNode, editor })
10241
11104
  })
10242
11105
  });
10243
11106
  }
10244
- const ImageExtension = Node$2.create({
11107
+ const ImageExtension = Node$1.create({
10245
11108
  name: "image",
10246
11109
  addOptions() {
10247
11110
  return {
@@ -10284,7 +11147,7 @@ const ImageExtension = Node$2.create({
10284
11147
  return [{ tag: "img[src]" }];
10285
11148
  },
10286
11149
  renderHTML({ HTMLAttributes }) {
10287
- return ["img", mergeAttributes$1(this.options.HTMLAttributes, HTMLAttributes)];
11150
+ return ["img", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
10288
11151
  },
10289
11152
  addNodeView() {
10290
11153
  return ReactNodeViewRenderer(ImageNodeView);
@@ -10666,7 +11529,7 @@ function PluginBlockNodeView({ node, updateAttributes, selected, deleteNode, edi
10666
11529
  /**
10667
11530
  * TipTap Node extension for plugin blocks (embeds)
10668
11531
  */
10669
- const PluginBlockExtension = Node$1.create({
11532
+ const PluginBlockExtension = Node$2.create({
10670
11533
  name: "pluginBlock",
10671
11534
  group: "block",
10672
11535
  atom: true,
@@ -10693,7 +11556,7 @@ const PluginBlockExtension = Node$1.create({
10693
11556
  return [{ tag: "div[data-plugin-block]" }];
10694
11557
  },
10695
11558
  renderHTML({ HTMLAttributes }) {
10696
- return ["div", mergeAttributes(HTMLAttributes, { "data-plugin-block": "" })];
11559
+ return ["div", mergeAttributes$1(HTMLAttributes, { "data-plugin-block": "" })];
10697
11560
  },
10698
11561
  addNodeView() {
10699
11562
  return ReactNodeViewRenderer(PluginBlockNodeView);
@@ -10912,6 +11775,40 @@ const TABLE_BUBBLE_MENU_KEY = "emdashTableBubbleMenu";
10912
11775
  function generateKey() {
10913
11776
  return Math.random().toString(36).substring(2, 11);
10914
11777
  }
11778
+ /**
11779
+ * Normalize an untrusted gallery `images` value into well-formed entries.
11780
+ * Mirrors `sanitizeGalleryImages` in core's content/converters (duplicated
11781
+ * like the converters themselves — see note above).
11782
+ */
11783
+ function sanitizeGalleryImages(value, withKeys = false) {
11784
+ if (!Array.isArray(value)) return [];
11785
+ const images = [];
11786
+ for (const entry of value) {
11787
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
11788
+ const record = entry;
11789
+ const asset = record.asset;
11790
+ if (typeof asset !== "object" || asset === null) continue;
11791
+ const assetRecord = asset;
11792
+ const image = {
11793
+ _type: "image",
11794
+ _key: attrStr(record._key) ?? (withKeys ? generateKey() : ""),
11795
+ asset: {
11796
+ _type: "reference",
11797
+ _ref: typeof assetRecord._ref === "string" ? assetRecord._ref : "",
11798
+ ...attrStr(assetRecord.url) ? { url: attrStr(assetRecord.url) } : {},
11799
+ ...attrStr(assetRecord.provider) ? { provider: attrStr(assetRecord.provider) } : {}
11800
+ }
11801
+ };
11802
+ if (attrStr(record.alt)) image.alt = attrStr(record.alt);
11803
+ if (attrStr(record.caption)) image.caption = attrStr(record.caption);
11804
+ if (typeof record.width === "number") image.width = record.width;
11805
+ if (typeof record.height === "number") image.height = record.height;
11806
+ if (attrStr(record.blurhash)) image.blurhash = attrStr(record.blurhash);
11807
+ if (attrStr(record.dominantColor)) image.dominantColor = attrStr(record.dominantColor);
11808
+ images.push(image);
11809
+ }
11810
+ return images;
11811
+ }
10915
11812
  const attrStr = (v) => typeof v === "string" && v ? v : void 0;
10916
11813
  const attrNum = (v) => typeof v === "number" && v ? v : void 0;
10917
11814
  function prosemirrorToPortableText(doc) {
@@ -11022,6 +11919,15 @@ function convertPMNode(node) {
11022
11919
  _key: generateKey(),
11023
11920
  style: "lineBreak"
11024
11921
  };
11922
+ case "gallery": {
11923
+ const columns = node.attrs?.columns;
11924
+ return {
11925
+ _type: "gallery",
11926
+ _key: generateKey(),
11927
+ images: sanitizeGalleryImages(node.attrs?.images, true),
11928
+ ...typeof columns === "number" ? { columns } : {}
11929
+ };
11930
+ }
11025
11931
  case "table": {
11026
11932
  const tableKey = generateKey();
11027
11933
  const rows = (node.content || []).filter((row) => row.type === "tableRow").map((row, rowIndex) => {
@@ -11268,6 +12174,24 @@ function convertPTBlock(block) {
11268
12174
  };
11269
12175
  }
11270
12176
  case "break": return { type: "horizontalRule" };
12177
+ case "gallery": {
12178
+ const galleryBlock = block;
12179
+ if (!Array.isArray(galleryBlock.images)) return {
12180
+ type: "paragraph",
12181
+ content: [{
12182
+ type: "text",
12183
+ text: `[Unknown block type: ${block._type}]`,
12184
+ marks: [{ type: "code" }]
12185
+ }]
12186
+ };
12187
+ return {
12188
+ type: "gallery",
12189
+ attrs: {
12190
+ images: sanitizeGalleryImages(galleryBlock.images),
12191
+ columns: typeof galleryBlock.columns === "number" ? galleryBlock.columns : void 0
12192
+ }
12193
+ };
12194
+ }
11271
12195
  case "htmlBlock": return {
11272
12196
  type: "htmlBlock",
11273
12197
  attrs: { html: block.html || "" }
@@ -12433,6 +13357,7 @@ function PortableTextEditor({ value, onChange, placeholder, className, editable
12433
13357
  else setInternalFocusMode(mode);
12434
13358
  };
12435
13359
  const [mediaPickerOpen, setMediaPickerOpen] = React$1.useState(false);
13360
+ const [galleryPickerOpen, setGalleryPickerOpen] = React$1.useState(false);
12436
13361
  const [pluginBlockModal, setPluginBlockModal] = React$1.useState(null);
12437
13362
  const [pluginBlockInitialValues, setPluginBlockInitialValues] = React$1.useState(void 0);
12438
13363
  /** When editing an existing block, store the node position for updateAttributes */
@@ -12484,6 +13409,32 @@ function PortableTextEditor({ value, onChange, placeholder, className, editable
12484
13409
  setMediaPickerOpen(true);
12485
13410
  }
12486
13411
  });
13412
+ cmds.push({
13413
+ id: "gallery",
13414
+ title: {
13415
+ id: "U+kFad",
13416
+ message: "Gallery"
13417
+ },
13418
+ description: {
13419
+ id: "ojLvJk",
13420
+ message: "Insert an image gallery"
13421
+ },
13422
+ icon: Images,
13423
+ aliases: [
13424
+ "gal",
13425
+ "photos",
13426
+ "grid"
13427
+ ],
13428
+ category: {
13429
+ id: "xYilR2",
13430
+ message: "Media"
13431
+ },
13432
+ deferInsertion: true,
13433
+ command: ({ editor, range }) => {
13434
+ editor.chain().focus().deleteRange(range).run();
13435
+ setGalleryPickerOpen(true);
13436
+ }
13437
+ });
12487
13438
  cmds.push({
12488
13439
  id: "section",
12489
13440
  title: {
@@ -12569,6 +13520,7 @@ function PortableTextEditor({ value, onChange, placeholder, className, editable
12569
13520
  }),
12570
13521
  CodeBlockExtension,
12571
13522
  HtmlBlockExtension,
13523
+ GalleryExtension,
12572
13524
  ImageExtension,
12573
13525
  MarkdownLinkExtension,
12574
13526
  PluginBlockExtension,
@@ -12797,17 +13749,21 @@ function PortableTextEditor({ value, onChange, placeholder, className, editable
12797
13749
  onBlockSidebarCloseRef.current = onBlockSidebarClose;
12798
13750
  React$1.useEffect(() => {
12799
13751
  if (!editor) return;
12800
- const storage = editor.storage.image;
12801
- if (!storage) return;
12802
- storage.onOpenBlockSidebar = (panel) => {
12803
- onBlockSidebarOpenRef.current?.(panel);
12804
- };
12805
- storage.onCloseBlockSidebar = () => {
12806
- onBlockSidebarCloseRef.current?.();
12807
- };
13752
+ const editorStorage = editor.storage;
13753
+ const storages = [editorStorage.image, editorStorage.gallery].filter((storage) => storage !== void 0);
13754
+ for (const storage of storages) {
13755
+ storage.onOpenBlockSidebar = (panel) => {
13756
+ onBlockSidebarOpenRef.current?.(panel);
13757
+ };
13758
+ storage.onCloseBlockSidebar = () => {
13759
+ onBlockSidebarCloseRef.current?.();
13760
+ };
13761
+ }
12808
13762
  return () => {
12809
- storage.onOpenBlockSidebar = null;
12810
- storage.onCloseBlockSidebar = null;
13763
+ for (const storage of storages) {
13764
+ storage.onOpenBlockSidebar = null;
13765
+ storage.onCloseBlockSidebar = null;
13766
+ }
12811
13767
  };
12812
13768
  }, [editor]);
12813
13769
  const handleImageSelect = React$1.useCallback((item) => {
@@ -12833,6 +13789,23 @@ function PortableTextEditor({ value, onChange, placeholder, className, editable
12833
13789
  pendingBlockInsertPosRef.current = null;
12834
13790
  setMediaPickerOpen(false);
12835
13791
  }, [editor]);
13792
+ const handleGallerySelect = React$1.useCallback((items) => {
13793
+ if (editor && items.length > 0) {
13794
+ const attrs = {
13795
+ images: items.map(mediaItemToGalleryImage),
13796
+ columns: 3
13797
+ };
13798
+ const insertPos = pendingBlockInsertPosRef.current;
13799
+ const chain = editor.chain().focus();
13800
+ if (insertPos === null) chain.setGallery(attrs).run();
13801
+ else chain.insertContentAt(insertPos, {
13802
+ type: "gallery",
13803
+ attrs
13804
+ }).run();
13805
+ }
13806
+ pendingBlockInsertPosRef.current = null;
13807
+ setGalleryPickerOpen(false);
13808
+ }, [editor]);
12836
13809
  const handlePluginBlockInsert = React$1.useCallback((values) => {
12837
13810
  if (!editor || !pluginBlockModal) return;
12838
13811
  const { id, ...data } = values;
@@ -12948,6 +13921,21 @@ function PortableTextEditor({ value, onChange, placeholder, className, editable
12948
13921
  message: "Select Image"
12949
13922
  })
12950
13923
  }),
13924
+ /* @__PURE__ */ jsx(MediaPickerModal, {
13925
+ open: galleryPickerOpen,
13926
+ onOpenChange: (open) => {
13927
+ setGalleryPickerOpen(open);
13928
+ if (!open) pendingBlockInsertPosRef.current = null;
13929
+ },
13930
+ multiple: true,
13931
+ onSelect: () => {},
13932
+ onSelectMany: handleGallerySelect,
13933
+ mimeTypeFilter: "image/",
13934
+ title: _t6({
13935
+ id: "qgANyN",
13936
+ message: "Select Gallery Images"
13937
+ })
13938
+ }),
12951
13939
  /* @__PURE__ */ jsx(PluginBlockModal, {
12952
13940
  block: pluginBlockModal,
12953
13941
  initialValues: pluginBlockInitialValues,
@@ -14366,6 +15354,7 @@ function MobileSidebarPortalGuard() {
14366
15354
  if (!isMobile || !openMobile) return;
14367
15355
  const nestedOverlaySelector = "[role=\"dialog\"], [role=\"listbox\"], [role=\"menu\"], .kumo-tooltip-popup";
14368
15356
  const keepSheetOpen = () => queueMicrotask(() => setOpenMobile(true));
15357
+ const reopenSheetAfterDismiss = () => setTimeout(setOpenMobile, 0, true);
14369
15358
  const promotePortal = (element) => {
14370
15359
  const portal = (element.closest(nestedOverlaySelector) ?? element.querySelector(nestedOverlaySelector))?.closest("[data-base-ui-portal]");
14371
15360
  if (!portal) return;
@@ -14375,7 +15364,13 @@ function MobileSidebarPortalGuard() {
14375
15364
  const handleFocusOut = (event) => {
14376
15365
  const source = event.target;
14377
15366
  const destination = event.relatedTarget;
14378
- if (!(source instanceof Element) || !(destination instanceof Element)) return;
15367
+ if (!(source instanceof Element)) return;
15368
+ if (source.closest("[data-sortable-handle]") && destination === null) {
15369
+ event.stopPropagation();
15370
+ keepSheetOpen();
15371
+ return;
15372
+ }
15373
+ if (!(destination instanceof Element)) return;
14379
15374
  const sheet = source.closest("nav[data-sidebar=\"sidebar\"][data-mobile=\"true\"]");
14380
15375
  if (!sheet || sheet.contains(destination)) return;
14381
15376
  if (!destination.closest(nestedOverlaySelector)) return;
@@ -14385,7 +15380,12 @@ function MobileSidebarPortalGuard() {
14385
15380
  const handleKeyDown = (event) => {
14386
15381
  if (event.key !== "Escape") return;
14387
15382
  const target = event.target;
14388
- if (!(target instanceof Element) || !target.closest(nestedOverlaySelector)) return;
15383
+ if (!(target instanceof Element)) return;
15384
+ if (target.closest("[data-sortable-handle][data-sorting=\"true\"]")) {
15385
+ reopenSheetAfterDismiss();
15386
+ return;
15387
+ }
15388
+ if (!target.closest(nestedOverlaySelector)) return;
14389
15389
  keepSheetOpen();
14390
15390
  };
14391
15391
  document.addEventListener("focusout", handleFocusOut, true);
@@ -28220,6 +29220,15 @@ function SectionEditorForm({ section, isSaving, onSave }) {
28220
29220
  },
28221
29221
  onClose: handleBlockSidebarClose,
28222
29222
  inline: true
29223
+ }) : blockSidebarPanel?.type === "gallery" ? /* @__PURE__ */ jsx(GalleryDetailPanel, {
29224
+ attributes: blockSidebarPanel.attrs,
29225
+ onUpdate: (attrs) => blockSidebarPanel.onUpdate(attrs),
29226
+ onDelete: () => {
29227
+ blockSidebarPanel.onDelete();
29228
+ setBlockSidebarPanel(null);
29229
+ },
29230
+ onClose: handleBlockSidebarClose,
29231
+ inline: true
28223
29232
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
28224
29233
  className: "rounded-lg border bg-kumo-base p-6 space-y-4",
28225
29234
  children: [
@@ -36570,7 +37579,7 @@ function Widgets() {
36570
37579
  })
36571
37580
  })
36572
37581
  });
36573
- return /* @__PURE__ */ jsxs(DndContext, {
37582
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(DndContext, {
36574
37583
  sensors,
36575
37584
  collisionDetection,
36576
37585
  onDragStart: handleDragStart,
@@ -36817,7 +37826,15 @@ function Widgets() {
36817
37826
  onClose: handleBlockSidebarClose
36818
37827
  })
36819
37828
  ]
36820
- });
37829
+ }), blockSidebarPanel?.type === "gallery" && /* @__PURE__ */ jsx(GalleryDetailPanel, {
37830
+ attributes: blockSidebarPanel.attrs,
37831
+ onUpdate: (attrs) => blockSidebarPanel.onUpdate(attrs),
37832
+ onDelete: () => {
37833
+ blockSidebarPanel.onDelete();
37834
+ setBlockSidebarPanel(null);
37835
+ },
37836
+ onClose: handleBlockSidebarClose
37837
+ })] });
36821
37838
  }
36822
37839
  /** A draggable item in the available widgets palette */
36823
37840
  function DraggablePaletteItem({ id, label, description, widgetInput }) {