@emdash-cms/admin 0.11.1 → 0.13.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 (90) hide show
  1. package/dist/{LocaleDirectionProvider-PhIYobDG.js → LocaleDirectionProvider-rC0KuZjM.js} +17 -17
  2. package/dist/{LocaleDirectionProvider-PhIYobDG.js.map → LocaleDirectionProvider-rC0KuZjM.js.map} +1 -1
  3. package/dist/index.d.ts +70 -18
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1231 -98
  6. package/dist/index.js.map +1 -1
  7. package/dist/locales/ar/messages.mjs +1 -1
  8. package/dist/locales/de/messages.mjs +1 -1
  9. package/dist/locales/en/messages.mjs +1 -1
  10. package/dist/locales/es-419/messages.mjs +1 -1
  11. package/dist/locales/es-ES/messages.mjs +1 -1
  12. package/dist/locales/eu/messages.mjs +1 -1
  13. package/dist/locales/fa/messages.mjs +1 -1
  14. package/dist/locales/fr/messages.mjs +1 -1
  15. package/dist/locales/id/messages.mjs +1 -1
  16. package/dist/locales/index.js +1 -1
  17. package/dist/locales/ja/messages.mjs +1 -1
  18. package/dist/locales/ko/messages.mjs +1 -1
  19. package/dist/locales/pl/messages.mjs +1 -1
  20. package/dist/locales/pseudo/messages.mjs +1 -1
  21. package/dist/locales/pt-BR/messages.mjs +1 -1
  22. package/dist/locales/zh-CN/messages.mjs +1 -1
  23. package/dist/locales/zh-TW/messages.mjs +1 -1
  24. package/dist/messages-BB7Ta5GU.js +6 -0
  25. package/dist/messages-BB7Ta5GU.js.map +1 -0
  26. package/dist/messages-BMyaLK4h.js +6 -0
  27. package/dist/messages-BMyaLK4h.js.map +1 -0
  28. package/dist/messages-BRIi_T5q.js +6 -0
  29. package/dist/messages-BRIi_T5q.js.map +1 -0
  30. package/dist/messages-Be0FUVz_.js +6 -0
  31. package/dist/messages-Be0FUVz_.js.map +1 -0
  32. package/dist/messages-BvE8RBIJ.js +6 -0
  33. package/dist/messages-BvE8RBIJ.js.map +1 -0
  34. package/dist/messages-CBHhMxhB.js +6 -0
  35. package/dist/messages-CBHhMxhB.js.map +1 -0
  36. package/dist/messages-CQJpnG4Q.js +6 -0
  37. package/dist/messages-CQJpnG4Q.js.map +1 -0
  38. package/dist/messages-CZpDTTb_.js +6 -0
  39. package/dist/messages-CZpDTTb_.js.map +1 -0
  40. package/dist/messages-CemjGhIt.js +6 -0
  41. package/dist/messages-CemjGhIt.js.map +1 -0
  42. package/dist/messages-Czya1QvU.js +6 -0
  43. package/dist/messages-Czya1QvU.js.map +1 -0
  44. package/dist/messages-DR7ouL8H.js +6 -0
  45. package/dist/messages-DR7ouL8H.js.map +1 -0
  46. package/dist/messages-DyiZEfUj.js +6 -0
  47. package/dist/messages-DyiZEfUj.js.map +1 -0
  48. package/dist/messages-JMuy3Tpu.js +6 -0
  49. package/dist/messages-JMuy3Tpu.js.map +1 -0
  50. package/dist/messages-h9IORlKh.js +6 -0
  51. package/dist/messages-h9IORlKh.js.map +1 -0
  52. package/dist/messages-iu8SG58F.js +6 -0
  53. package/dist/messages-iu8SG58F.js.map +1 -0
  54. package/dist/messages-uD16X789.js +6 -0
  55. package/dist/messages-uD16X789.js.map +1 -0
  56. package/dist/plugins-qrjvPK4g.js.map +1 -1
  57. package/dist/styles.css +1 -1
  58. package/package.json +7 -3
  59. package/dist/messages-0WS1kU2r.js +0 -6
  60. package/dist/messages-0WS1kU2r.js.map +0 -1
  61. package/dist/messages-B3nZXT9a.js +0 -6
  62. package/dist/messages-B3nZXT9a.js.map +0 -1
  63. package/dist/messages-BHe7DjK4.js +0 -6
  64. package/dist/messages-BHe7DjK4.js.map +0 -1
  65. package/dist/messages-BNm9xWl9.js +0 -6
  66. package/dist/messages-BNm9xWl9.js.map +0 -1
  67. package/dist/messages-Bf6-l0VR.js +0 -6
  68. package/dist/messages-Bf6-l0VR.js.map +0 -1
  69. package/dist/messages-C2QjBo2p.js +0 -6
  70. package/dist/messages-C2QjBo2p.js.map +0 -1
  71. package/dist/messages-CAYw_5r5.js +0 -6
  72. package/dist/messages-CAYw_5r5.js.map +0 -1
  73. package/dist/messages-CBAO8mkk.js +0 -6
  74. package/dist/messages-CBAO8mkk.js.map +0 -1
  75. package/dist/messages-CNsSgfXs.js +0 -6
  76. package/dist/messages-CNsSgfXs.js.map +0 -1
  77. package/dist/messages-CgrjHEi_.js +0 -6
  78. package/dist/messages-CgrjHEi_.js.map +0 -1
  79. package/dist/messages-DCoCdFuY.js +0 -6
  80. package/dist/messages-DCoCdFuY.js.map +0 -1
  81. package/dist/messages-DdvAysPo.js +0 -6
  82. package/dist/messages-DdvAysPo.js.map +0 -1
  83. package/dist/messages-Di1aYzxi.js +0 -6
  84. package/dist/messages-Di1aYzxi.js.map +0 -1
  85. package/dist/messages-NtPfr5Dr.js +0 -6
  86. package/dist/messages-NtPfr5Dr.js.map +0 -1
  87. package/dist/messages-Y1VN2Mk3.js +0 -6
  88. package/dist/messages-Y1VN2Mk3.js.map +0 -1
  89. package/dist/messages-cjsZTyVv.js +0 -6
  90. package/dist/messages-cjsZTyVv.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-qrjvPK4g.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-PhIYobDG.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-rC0KuZjM.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$1 from "react";
10
10
  import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
11
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
12
  import { Dialog as Dialog$1 } from "@cloudflare/kumo/primitives";
13
- import { ArrowCounterClockwise, ArrowRight, ArrowRightIcon, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, BracketsCurly, Calendar, CalendarBlank, CaretDown, CaretRightIcon, CaretUp, CaretUpDown, ChatCircle, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, Code, CodeBlock, Columns, Copy, Cube, Database, DeviceMobile, DotsSixVertical, DownloadSimple, Envelope, EnvelopeSimple, Eye, EyeSlash, File, FileArrowDown, FileText, FloppyDisk, FolderOpen, Gear, GithubLogo, Globe, GlobeSimple, GridFour, HardDrive, Hash, Image as Image$1, ImageSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, MagnifyingGlass, Minus, Monitor, Moon, Palette, PaperPlaneTilt, Paperclip, Paragraph, Pencil, PencilSimple, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Rows, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, SlidersHorizontal, Sparkle, SquaresFour, Stack, Storefront, Sun, Table, TextAlignCenter, TextAlignLeft, TextAlignRight, TextB, TextHOne, TextHThree, TextHTwo, TextItalic, TextStrikethrough, TextT, TextUnderline, ToggleLeft, Trash, Upload, User, UserCircle, UserPlus, Users, Warning, WarningCircle, WebhooksLogo, X, YoutubeLogo } from "@phosphor-icons/react";
13
+ import { ArrowCounterClockwise, ArrowRight, ArrowRightIcon, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, BracketsCurly, Calendar, CalendarBlank, CaretDown, CaretRightIcon, CaretUp, CaretUpDown, ChatCircle, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, Code, CodeBlock, Columns, Copy, Cube, Database, DeviceMobile, DotsSixVertical, DownloadSimple, Envelope, EnvelopeSimple, Eye, EyeSlash, File, FileArrowDown, FileText, FloppyDisk, FolderOpen, Gear, GithubLogo, Globe, GlobeSimple, GridFour, HardDrive, Hash, Image as Image$1, ImageBroken, ImageSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, MagnifyingGlass, Minus, Monitor, Moon, Palette, PaperPlaneTilt, Paperclip, Paragraph, Pencil, PencilSimple, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Rows, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, SlidersHorizontal, Sparkle, SquaresFour, Stack, Storefront, Sun, Table, TextAlignCenter, TextAlignLeft, TextAlignRight, TextB, TextHOne, TextHThree, TextHTwo, TextItalic, TextStrikethrough, TextT, TextUnderline, ToggleLeft, Trash, Upload, User, UserCircle, UserPlus, Users, Warning, WarningCircle, WebhooksLogo, X, YoutubeLogo } from "@phosphor-icons/react";
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";
@@ -2053,7 +2053,7 @@ async function createMenuItem(menuName, input, options = {}) {
2053
2053
  * Update a menu item
2054
2054
  */
2055
2055
  async function updateMenuItem(menuName, itemId, input, options = {}) {
2056
- return parseApiResponse(await apiFetch(withLocale$1(`${API_BASE}/menus/${menuName}/items?id=${itemId}`, options.locale), {
2056
+ return parseApiResponse(await apiFetch(withLocale$1(`${API_BASE}/menus/${menuName}/items/${itemId}`, options.locale), {
2057
2057
  method: "PUT",
2058
2058
  headers: { "Content-Type": "application/json" },
2059
2059
  body: JSON.stringify(input)
@@ -2063,7 +2063,7 @@ async function updateMenuItem(menuName, itemId, input, options = {}) {
2063
2063
  * Delete a menu item
2064
2064
  */
2065
2065
  async function deleteMenuItem(menuName, itemId, options = {}) {
2066
- const response = await apiFetch(withLocale$1(`${API_BASE}/menus/${menuName}/items?id=${itemId}`, options.locale), { method: "DELETE" });
2066
+ const response = await apiFetch(withLocale$1(`${API_BASE}/menus/${menuName}/items/${itemId}`, options.locale), { method: "DELETE" });
2067
2067
  if (!response.ok) await throwResponseError(response, i18n._({
2068
2068
  id: "HNaiZF",
2069
2069
  message: "Failed to delete menu item"
@@ -3122,7 +3122,7 @@ function probeImageDimensions(url, errorMessage) {
3122
3122
  img.src = url;
3123
3123
  });
3124
3124
  }
3125
- function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "image/", mimeTypeFilters, fieldId, title: providedTitle, hideUrlInput = false, mediaKind = "image" }) {
3125
+ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "image/", mimeTypeFilters, fieldId, title: providedTitle, hideUrlInput = false, mediaKind = "image", localOnly = false }) {
3126
3126
  const { _: _t } = useLingui();
3127
3127
  const isFileKind = mediaKind === "file";
3128
3128
  const filters = React$1.useMemo(() => {
@@ -3170,11 +3170,11 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3170
3170
  setUploadError(null);
3171
3171
  setProviderDimensions({});
3172
3172
  }
3173
- }, [open]);
3173
+ }, [open, localOnly]);
3174
3174
  const { data: providers } = useQuery({
3175
3175
  queryKey: ["media-providers"],
3176
3176
  queryFn: fetchMediaProviders,
3177
- enabled: open,
3177
+ enabled: open && !localOnly,
3178
3178
  placeholderData: []
3179
3179
  });
3180
3180
  const activeProviderInfo = React$1.useMemo(() => {
@@ -3191,12 +3191,15 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3191
3191
  };
3192
3192
  return providers?.find((p) => p.id === activeProvider);
3193
3193
  }, [activeProvider, providers]);
3194
- const { data: localData, isLoading: localLoading } = useQuery({
3194
+ const { data: localData, isLoading: localLoading, fetchNextPage: fetchNextLocalPage, hasNextPage: hasNextLocalPage, isFetchingNextPage: isFetchingNextLocalPage } = useInfiniteQuery({
3195
3195
  queryKey: ["media", filters?.join(",") ?? ""],
3196
- queryFn: () => fetchMediaList({
3196
+ queryFn: ({ pageParam }) => fetchMediaList({
3197
3197
  mimeType: filters,
3198
- limit: 50
3198
+ cursor: pageParam,
3199
+ limit: 100
3199
3200
  }),
3201
+ initialPageParam: void 0,
3202
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
3200
3203
  enabled: open && activeProvider === "local"
3201
3204
  });
3202
3205
  const { data: providerData, isLoading: providerLoading } = useQuery({
@@ -3211,9 +3214,9 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3211
3214
  limit: 50,
3212
3215
  query: searchQuery || void 0
3213
3216
  }),
3214
- enabled: open && activeProvider !== "local"
3217
+ enabled: open && !localOnly && activeProvider !== "local"
3215
3218
  });
3216
- const isLoading = activeProvider === "local" ? localLoading : providerLoading;
3219
+ const isLoading = activeProvider === "local" ? localLoading || isFetchingNextLocalPage : providerLoading;
3217
3220
  const [uploadError, setUploadError] = React$1.useState(null);
3218
3221
  const uploadLocalMutation = useMutation({
3219
3222
  mutationFn: (file) => uploadMedia(file, { fieldId }),
@@ -3255,11 +3258,14 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3255
3258
  if (!old) return old;
3256
3259
  return {
3257
3260
  ...old,
3258
- items: old.items.map((item) => item.id === id ? {
3259
- ...item,
3260
- width,
3261
- height
3262
- } : item)
3261
+ pages: old.pages.map((page) => ({
3262
+ ...page,
3263
+ items: page.items.map((item) => item.id === id ? {
3264
+ ...item,
3265
+ width,
3266
+ height
3267
+ } : item)
3268
+ }))
3263
3269
  };
3264
3270
  });
3265
3271
  if (selectedItem?.providerId === "local" && selectedItem.item.id === id) setSelectedItem({
@@ -3285,11 +3291,11 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3285
3291
  });
3286
3292
  }, [dimensionsMutation]);
3287
3293
  const items = React$1.useMemo(() => {
3288
- if (activeProvider === "local") return (localData?.items || []).filter((item) => matchesAnyFilter(item.mimeType, filters));
3294
+ if (activeProvider === "local") return (localData?.pages.flatMap((page) => page.items) || []).filter((item) => matchesAnyFilter(item.mimeType, filters));
3289
3295
  return providerData?.items || [];
3290
3296
  }, [
3291
3297
  activeProvider,
3292
- localData?.items,
3298
+ localData,
3293
3299
  providerData?.items,
3294
3300
  filters
3295
3301
  ]);
@@ -3399,7 +3405,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3399
3405
  name: "Library",
3400
3406
  icon: void 0
3401
3407
  }];
3402
- if (providers) {
3408
+ if (providers && !localOnly) {
3403
3409
  for (const p of providers) if (p.id !== "local") tabs.push({
3404
3410
  id: p.id,
3405
3411
  name: p.name,
@@ -3407,7 +3413,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3407
3413
  });
3408
3414
  }
3409
3415
  return tabs;
3410
- }, [providers]);
3416
+ }, [providers, localOnly]);
3411
3417
  return /* @__PURE__ */ jsx(Dialog.Root, {
3412
3418
  open,
3413
3419
  onOpenChange: handleClose,
@@ -3444,7 +3450,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3444
3450
  })
3445
3451
  })]
3446
3452
  }),
3447
- !hideUrlInput && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
3453
+ !hideUrlInput && !localOnly && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
3448
3454
  className: "border-b pb-4",
3449
3455
  children: [
3450
3456
  /* @__PURE__ */ jsx(Label, { children: _t({
@@ -3581,9 +3587,9 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3581
3587
  }) : null,
3582
3588
  className: "mb-3"
3583
3589
  }),
3584
- /* @__PURE__ */ jsx("div", {
3590
+ /* @__PURE__ */ jsxs("div", {
3585
3591
  className: "flex-1 overflow-y-auto min-h-0",
3586
- children: isLoading ? /* @__PURE__ */ jsx("div", {
3592
+ children: [isLoading && items.length === 0 ? /* @__PURE__ */ jsx("div", {
3587
3593
  className: "flex items-center justify-center h-full",
3588
3594
  children: /* @__PURE__ */ jsx(Loader, {})
3589
3595
  }) : items.length === 0 ? /* @__PURE__ */ jsxs("div", {
@@ -3662,7 +3668,22 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3662
3668
  }));
3663
3669
  }
3664
3670
  }, item.id))
3665
- })
3671
+ }), activeProvider === "local" && hasNextLocalPage && /* @__PURE__ */ jsx("div", {
3672
+ className: "flex justify-center py-3",
3673
+ children: /* @__PURE__ */ jsx(Button, {
3674
+ variant: "outline",
3675
+ size: "sm",
3676
+ onClick: () => void fetchNextLocalPage(),
3677
+ disabled: isFetchingNextLocalPage,
3678
+ children: isFetchingNextLocalPage ? _t({
3679
+ id: "Z3FXyt",
3680
+ message: "Loading..."
3681
+ }) : _t({
3682
+ id: "a4N/Bg",
3683
+ message: "Load More"
3684
+ })
3685
+ })
3686
+ })]
3666
3687
  }),
3667
3688
  /* @__PURE__ */ jsxs("div", {
3668
3689
  className: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 border-t pt-4",
@@ -3853,7 +3874,11 @@ function ProviderMediaItem({ item, selected, onClick, onDoubleClick, onDimension
3853
3874
  function BlockKitMediaPickerField({ actionId, label, placeholder, mimeTypeFilter, value, onChange }) {
3854
3875
  const { _: _t } = useLingui();
3855
3876
  const [pickerOpen, setPickerOpen] = React$1.useState(false);
3877
+ const [imageBroken, setImageBroken] = React$1.useState(false);
3856
3878
  const url = typeof value === "string" && value.length > 0 ? value : "";
3879
+ React$1.useEffect(() => {
3880
+ setImageBroken(false);
3881
+ }, [url]);
3857
3882
  const filter = mimeTypeFilter ?? "image/";
3858
3883
  const canPreview = isSafePreviewUrl(url);
3859
3884
  const handleSelect = (item) => {
@@ -3868,14 +3893,50 @@ function BlockKitMediaPickerField({ actionId, label, placeholder, mimeTypeFilter
3868
3893
  className: "text-sm font-medium mb-1.5 block",
3869
3894
  children: label
3870
3895
  }),
3871
- canPreview ? /* @__PURE__ */ jsxs("div", {
3896
+ canPreview ? imageBroken ? /* @__PURE__ */ jsxs("div", {
3897
+ className: "relative group min-h-20",
3898
+ children: [/* @__PURE__ */ jsxs("div", {
3899
+ className: "min-h-20 w-full rounded-md border border-kumo-line bg-kumo-muted flex items-center justify-center gap-2 text-kumo-subtle",
3900
+ children: [/* @__PURE__ */ jsx(ImageBroken, { className: "h-5 w-5" }), /* @__PURE__ */ jsx("span", {
3901
+ className: "text-sm",
3902
+ children: _t({
3903
+ id: "1pdLQw",
3904
+ message: "Image not found"
3905
+ })
3906
+ })]
3907
+ }), /* @__PURE__ */ jsxs("div", {
3908
+ 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",
3909
+ children: [/* @__PURE__ */ jsx(Button, {
3910
+ type: "button",
3911
+ size: "sm",
3912
+ variant: "secondary",
3913
+ onClick: () => setPickerOpen(true),
3914
+ children: _t({
3915
+ id: "o+XJ9D",
3916
+ message: "Change"
3917
+ })
3918
+ }), /* @__PURE__ */ jsx(Button, {
3919
+ type: "button",
3920
+ shape: "square",
3921
+ variant: "destructive",
3922
+ className: "h-8 w-8",
3923
+ onClick: () => onChange(actionId, ""),
3924
+ "aria-label": _t({
3925
+ id: "t/YqKh",
3926
+ message: "Remove"
3927
+ }),
3928
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
3929
+ })]
3930
+ })]
3931
+ }) : /* @__PURE__ */ jsxs("div", {
3872
3932
  className: "relative group",
3873
3933
  children: [/* @__PURE__ */ jsx("img", {
3874
3934
  src: url,
3875
3935
  alt: "",
3876
- className: "max-h-40 w-full rounded-md border border-kumo-line object-contain bg-kumo-muted",
3936
+ className: "max-h-40 min-h-20 w-full rounded-md border border-kumo-line object-contain bg-kumo-muted",
3877
3937
  referrerPolicy: "no-referrer",
3878
- loading: "lazy"
3938
+ loading: "lazy",
3939
+ onError: () => setImageBroken(true)
3879
3940
  }), /* @__PURE__ */ jsxs("div", {
3880
3941
  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",
3881
3942
  children: [/* @__PURE__ */ jsx(Button, {
@@ -7381,6 +7442,10 @@ function SlashCommandMenu({ state, onCommand, onClose: _onClose, setSelectedInde
7381
7442
  const selected = container.querySelector(`[data-index="${state.selectedIndex}"]`);
7382
7443
  if (selected) selected.scrollIntoView({ block: "nearest" });
7383
7444
  }, [state.selectedIndex, state.isOpen]);
7445
+ const hasMouseMovedRef = React$1.useRef(false);
7446
+ React$1.useEffect(() => {
7447
+ if (!state.isOpen) hasMouseMovedRef.current = false;
7448
+ }, [state.isOpen]);
7384
7449
  if (!state.isOpen) return null;
7385
7450
  return createPortal(/* @__PURE__ */ jsx("div", {
7386
7451
  ref: (node) => {
@@ -7389,6 +7454,9 @@ function SlashCommandMenu({ state, onCommand, onClose: _onClose, setSelectedInde
7389
7454
  },
7390
7455
  style: floatingStyles,
7391
7456
  className: "z-[100] rounded-lg border bg-kumo-overlay p-1 shadow-lg min-w-[220px] max-h-[300px] overflow-y-auto",
7457
+ onPointerMove: () => {
7458
+ hasMouseMovedRef.current = true;
7459
+ },
7392
7460
  children: state.items.length === 0 ? /* @__PURE__ */ jsx("p", {
7393
7461
  className: "text-sm text-kumo-subtle px-3 py-2",
7394
7462
  children: _t({
@@ -7400,7 +7468,9 @@ function SlashCommandMenu({ state, onCommand, onClose: _onClose, setSelectedInde
7400
7468
  "data-index": index,
7401
7469
  className: cn("flex items-center gap-3 w-full px-3 py-2 text-sm rounded text-start", index === state.selectedIndex ? "bg-kumo-tint text-kumo-default" : "hover:bg-kumo-tint/50"),
7402
7470
  onClick: () => onCommand(item),
7403
- onMouseEnter: () => setSelectedIndex(index),
7471
+ onMouseEnter: () => {
7472
+ if (hasMouseMovedRef.current) setSelectedIndex(index);
7473
+ },
7404
7474
  children: [/* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 text-kumo-subtle flex-shrink-0" }), /* @__PURE__ */ jsxs("div", {
7405
7475
  className: "flex flex-col",
7406
7476
  children: [/* @__PURE__ */ jsx("span", {
@@ -7969,7 +8039,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
7969
8039
  /** When editing an existing block, store the node position for updateAttributes */
7970
8040
  const editingBlockPosRef = React$1.useRef(null);
7971
8041
  const [sectionPickerOpen, setSectionPickerOpen] = React$1.useState(false);
7972
- const [slashMenuState, setSlashMenuState] = React$1.useState({
8042
+ const [slashMenuState, setSlashMenuStateRaw] = React$1.useState({
7973
8043
  isOpen: false,
7974
8044
  items: [],
7975
8045
  selectedIndex: 0,
@@ -7977,9 +8047,11 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
7977
8047
  range: null
7978
8048
  });
7979
8049
  const slashMenuStateRef = React$1.useRef(slashMenuState);
7980
- React$1.useEffect(() => {
7981
- slashMenuStateRef.current = slashMenuState;
7982
- }, [slashMenuState]);
8050
+ const setSlashMenuState = React$1.useCallback((action) => {
8051
+ const next = typeof action === "function" ? action(slashMenuStateRef.current) : action;
8052
+ slashMenuStateRef.current = next;
8053
+ setSlashMenuStateRaw(next);
8054
+ }, []);
7983
8055
  const slashCommands = React$1.useMemo(() => {
7984
8056
  const cmds = [...defaultSlashCommands];
7985
8057
  cmds.push({
@@ -11374,7 +11446,11 @@ function JsonFieldEditor({ label, id, value, onChange, required }) {
11374
11446
  function ImageFieldRenderer({ id, label, description, value, onChange, required, allowedMimeTypes, fieldId }) {
11375
11447
  const { _: _t5 } = useLingui();
11376
11448
  const [pickerOpen, setPickerOpen] = React$1.useState(false);
11449
+ const [imageBroken, setImageBroken] = React$1.useState(false);
11377
11450
  const displayUrl = typeof value === "string" ? value : value?.previewUrl || value?.src || (value && (!value.provider || value.provider === "local") ? `/_emdash/api/media/file/${typeof value.meta?.storageKey === "string" ? value.meta.storageKey : value.id}` : void 0);
11451
+ React$1.useEffect(() => {
11452
+ setImageBroken(false);
11453
+ }, [displayUrl]);
11378
11454
  const handleSelect = (item) => {
11379
11455
  const isLocalProvider = !item.provider || item.provider === "local";
11380
11456
  onChange({
@@ -11397,12 +11473,48 @@ function ImageFieldRenderer({ id, label, description, value, onChange, required,
11397
11473
  id,
11398
11474
  children: [
11399
11475
  /* @__PURE__ */ jsx(Label, { children: label }),
11400
- displayUrl ? /* @__PURE__ */ jsxs("div", {
11476
+ displayUrl ? imageBroken ? /* @__PURE__ */ jsxs("div", {
11477
+ className: "mt-2 relative group",
11478
+ children: [/* @__PURE__ */ jsxs("div", {
11479
+ className: "min-h-20 rounded-lg border bg-kumo-muted flex items-center justify-center gap-2 text-kumo-subtle",
11480
+ children: [/* @__PURE__ */ jsx(ImageBroken, { className: "h-5 w-5" }), /* @__PURE__ */ jsx("span", {
11481
+ className: "text-sm",
11482
+ children: _t5({
11483
+ id: "1pdLQw",
11484
+ message: "Image not found"
11485
+ })
11486
+ })]
11487
+ }), /* @__PURE__ */ jsxs("div", {
11488
+ className: "absolute top-2 end-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1",
11489
+ children: [/* @__PURE__ */ jsx(Button, {
11490
+ type: "button",
11491
+ size: "sm",
11492
+ variant: "secondary",
11493
+ onClick: () => setPickerOpen(true),
11494
+ children: _t5({
11495
+ id: "o+XJ9D",
11496
+ message: "Change"
11497
+ })
11498
+ }), /* @__PURE__ */ jsx(Button, {
11499
+ type: "button",
11500
+ shape: "square",
11501
+ variant: "destructive",
11502
+ className: "h-8 w-8",
11503
+ onClick: handleRemove,
11504
+ "aria-label": _t5({
11505
+ id: "5ywtDz",
11506
+ message: "Remove image"
11507
+ }),
11508
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
11509
+ })]
11510
+ })]
11511
+ }) : /* @__PURE__ */ jsxs("div", {
11401
11512
  className: "mt-2 relative group",
11402
11513
  children: [/* @__PURE__ */ jsx("img", {
11403
11514
  src: displayUrl,
11404
11515
  alt: "",
11405
- className: "max-h-48 rounded-lg border object-cover"
11516
+ className: "max-h-48 min-h-20 rounded-lg border object-cover",
11517
+ onError: () => setImageBroken(true)
11406
11518
  }), /* @__PURE__ */ jsxs("div", {
11407
11519
  className: "absolute top-2 end-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1",
11408
11520
  children: [/* @__PURE__ */ jsx(Button, {
@@ -12068,7 +12180,7 @@ function getItemTitle$1(item) {
12068
12180
  /**
12069
12181
  * Content list view with table display and trash tab
12070
12182
  */
12071
- function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n: i18n$2, activeLocale, onLocaleChange, urlPattern, sort, onSortChange }) {
12183
+ function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n, activeLocale, onLocaleChange, urlPattern, sort, onSortChange, total }) {
12072
12184
  const { _: _t } = useLingui();
12073
12185
  const [activeTab, setActiveTab] = React$1.useState("all");
12074
12186
  const [searchQuery, setSearchQuery] = React$1.useState("");
@@ -12082,13 +12194,16 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12082
12194
  const query = searchQuery.toLowerCase();
12083
12195
  return items.filter((item) => getItemTitle$1(item).toLowerCase().includes(query));
12084
12196
  }, [items, searchQuery]);
12085
- const totalPages = Math.max(1, Math.ceil(filteredItems.length / PAGE_SIZE));
12086
- const paginatedItems = filteredItems.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
12197
+ const effectiveTotal = typeof total === "number" && !searchQuery ? total : filteredItems.length;
12198
+ const totalPages = Math.max(1, Math.ceil(effectiveTotal / PAGE_SIZE));
12199
+ const clampedPage = Math.min(page, totalPages - 1);
12200
+ const paginatedItems = filteredItems.slice(clampedPage * PAGE_SIZE, (clampedPage + 1) * PAGE_SIZE);
12087
12201
  React$1.useEffect(() => {
12088
- if (page >= totalPages - 1 && hasMore && onLoadMore && !searchQuery) onLoadMore();
12202
+ if (!hasMore || !onLoadMore || searchQuery) return;
12203
+ if (clampedPage >= Math.ceil(filteredItems.length / PAGE_SIZE) - 1) onLoadMore();
12089
12204
  }, [
12090
- page,
12091
- totalPages,
12205
+ clampedPage,
12206
+ filteredItems.length,
12092
12207
  hasMore,
12093
12208
  onLoadMore,
12094
12209
  searchQuery
@@ -12103,9 +12218,9 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12103
12218
  children: [/* @__PURE__ */ jsx("h1", {
12104
12219
  className: "text-2xl font-bold",
12105
12220
  children: collectionLabel
12106
- }), i18n$2 && activeLocale && onLocaleChange && /* @__PURE__ */ jsx(LocaleSwitcher, {
12107
- locales: i18n$2.locales,
12108
- defaultLocale: i18n$2.defaultLocale,
12221
+ }), i18n && activeLocale && onLocaleChange && /* @__PURE__ */ jsx(LocaleSwitcher, {
12222
+ locales: i18n.locales,
12223
+ defaultLocale: i18n.defaultLocale,
12109
12224
  value: activeLocale,
12110
12225
  onChange: onLocaleChange,
12111
12226
  size: "sm"
@@ -12199,7 +12314,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12199
12314
  message: "Status"
12200
12315
  })
12201
12316
  }),
12202
- i18n$2 && /* @__PURE__ */ jsx(SortableTh, {
12317
+ i18n && /* @__PURE__ */ jsx(SortableTh, {
12203
12318
  field: "locale",
12204
12319
  sort,
12205
12320
  onSortChange,
@@ -12229,7 +12344,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12229
12344
  }) }), /* @__PURE__ */ jsx("tbody", {
12230
12345
  className: "divide-y divide-kumo-line",
12231
12346
  children: isLoading && items.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
12232
- colSpan: i18n$2 ? 5 : 4,
12347
+ colSpan: i18n ? 5 : 4,
12233
12348
  className: "px-4 py-8 text-center text-kumo-subtle",
12234
12349
  children: /* @__PURE__ */ jsxs("span", {
12235
12350
  className: "inline-flex items-center gap-2",
@@ -12239,7 +12354,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12239
12354
  })]
12240
12355
  })
12241
12356
  }) }) : items.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
12242
- colSpan: i18n$2 ? 5 : 4,
12357
+ colSpan: i18n ? 5 : 4,
12243
12358
  className: "px-4 py-8 text-center text-kumo-subtle",
12244
12359
  children: [
12245
12360
  _t({
@@ -12260,7 +12375,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12260
12375
  })
12261
12376
  ]
12262
12377
  }) }) : paginatedItems.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
12263
- colSpan: i18n$2 ? 5 : 4,
12378
+ colSpan: i18n ? 5 : 4,
12264
12379
  className: "px-4 py-8 text-center text-kumo-subtle",
12265
12380
  children: _t({
12266
12381
  id: "huTRDC",
@@ -12272,7 +12387,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12272
12387
  collection,
12273
12388
  onDelete,
12274
12389
  onDuplicate,
12275
- showLocale: !!i18n$2,
12390
+ showLocale: !!i18n,
12276
12391
  urlPattern
12277
12392
  }, item.id))
12278
12393
  })]
@@ -12282,21 +12397,11 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12282
12397
  className: "flex items-center justify-between",
12283
12398
  children: [/* @__PURE__ */ jsx("span", {
12284
12399
  className: "text-sm text-kumo-subtle",
12285
- children: searchQuery ? i18n._({
12286
- id: "Zzkfda",
12287
- message: "{0, plural, one {# item matching \"{searchQuery}\"} other {# items matching \"{searchQuery}\"}}",
12288
- values: {
12289
- 0: filteredItems.length,
12290
- searchQuery
12291
- }
12292
- }) : i18n._({
12293
- id: "Ufffi8",
12294
- message: "{0, plural, one {#{1} item} other {#{2} items}}",
12295
- values: {
12296
- 0: filteredItems.length,
12297
- 1: hasMore ? "+" : "",
12298
- 2: hasMore ? "+" : ""
12299
- }
12400
+ children: renderItemCount({
12401
+ searchQuery,
12402
+ filteredCount: filteredItems.length,
12403
+ total,
12404
+ hasMore
12300
12405
  })
12301
12406
  }), /* @__PURE__ */ jsxs("div", {
12302
12407
  className: "flex items-center gap-2",
@@ -12304,8 +12409,8 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12304
12409
  /* @__PURE__ */ jsx(Button, {
12305
12410
  variant: "outline",
12306
12411
  shape: "square",
12307
- disabled: page === 0,
12308
- onClick: () => setPage(page - 1),
12412
+ disabled: clampedPage === 0,
12413
+ onClick: () => setPage(clampedPage - 1),
12309
12414
  "aria-label": _t({
12310
12415
  id: "aHKcKc",
12311
12416
  message: "Previous page"
@@ -12318,7 +12423,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12318
12423
  /* @__PURE__ */ jsxs("span", {
12319
12424
  className: "text-sm",
12320
12425
  children: [
12321
- page + 1,
12426
+ clampedPage + 1,
12322
12427
  " / ",
12323
12428
  totalPages
12324
12429
  ]
@@ -12326,8 +12431,8 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
12326
12431
  /* @__PURE__ */ jsx(Button, {
12327
12432
  variant: "outline",
12328
12433
  shape: "square",
12329
- disabled: page >= totalPages - 1,
12330
- onClick: () => setPage(page + 1),
12434
+ disabled: clampedPage >= totalPages - 1,
12435
+ onClick: () => setPage(clampedPage + 1),
12331
12436
  "aria-label": _t({
12332
12437
  id: "/dqduX",
12333
12438
  message: "Next page"
@@ -12477,6 +12582,38 @@ function SortableTh({ field, sort, onSortChange, label }) {
12477
12582
  })
12478
12583
  });
12479
12584
  }
12585
+ /**
12586
+ * Render the row-count line above pagination. The rules are:
12587
+ * - A search query always wins — say how many matches there are.
12588
+ * - When the server reported a total, use it (no `+` suffix needed —
12589
+ * we know the count).
12590
+ * - Otherwise fall back to the pre-refactor behavior: loaded count,
12591
+ * with `+` when there are more pages the user hasn't fetched yet.
12592
+ */
12593
+ function renderItemCount({ searchQuery, filteredCount, total, hasMore }) {
12594
+ if (searchQuery) return i18n._({
12595
+ id: "WqhW35",
12596
+ message: "{filteredCount, plural, one {# item matching \"{searchQuery}\"} other {# items matching \"{searchQuery}\"}}",
12597
+ values: {
12598
+ filteredCount,
12599
+ searchQuery
12600
+ }
12601
+ });
12602
+ if (typeof total === "number") return i18n._({
12603
+ id: "Q/6//K",
12604
+ message: "{total, plural, one {# item} other {# items}}",
12605
+ values: { total }
12606
+ });
12607
+ return i18n._({
12608
+ id: "FWXiie",
12609
+ message: "{filteredCount, plural, one {#{0} item} other {#{1} items}}",
12610
+ values: {
12611
+ 0: hasMore ? "+" : "",
12612
+ 1: hasMore ? "+" : "",
12613
+ filteredCount
12614
+ }
12615
+ });
12616
+ }
12480
12617
  function ContentListItem({ item, collection, onDelete, onDuplicate, showLocale, urlPattern }) {
12481
12618
  const { _: _t2 } = useLingui();
12482
12619
  const title = getItemTitle$1(item);
@@ -17805,6 +17942,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
17805
17942
  const queryClient = useQueryClient();
17806
17943
  const toastManager = Toast.useToastManager();
17807
17944
  const isMarketplace = plugin.source === "marketplace";
17945
+ const isRegistry = plugin.source === "registry";
17808
17946
  const hasUpdate = !!updateInfo && updateInfo.installed !== updateInfo.latest;
17809
17947
  const updateMutation = useMutation({
17810
17948
  mutationFn: () => updateMarketplacePlugin(plugin.id, { confirmCapabilities: true }),
@@ -18136,6 +18274,13 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18136
18274
  message: "Uninstall"
18137
18275
  })
18138
18276
  })
18277
+ }),
18278
+ isRegistry && /* @__PURE__ */ jsx("div", {
18279
+ className: "pt-2 border-t text-xs text-kumo-subtle",
18280
+ children: _t2({
18281
+ id: "YvXVtr",
18282
+ message: "Uninstall is not yet available for registry plugins. Disable the plugin to stop it from running; full uninstall will land in a follow-up."
18283
+ })
18139
18284
  })
18140
18285
  ]
18141
18286
  })]
@@ -18284,7 +18429,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
18284
18429
  const isInstalled = installedPluginIds.has(pluginId);
18285
18430
  if (isLoading) return /* @__PURE__ */ jsxs("div", {
18286
18431
  className: "space-y-6",
18287
- children: [/* @__PURE__ */ jsx(BackLink, {}), /* @__PURE__ */ jsxs("div", {
18432
+ children: [/* @__PURE__ */ jsx(BackLink$1, {}), /* @__PURE__ */ jsxs("div", {
18288
18433
  className: "animate-pulse space-y-4",
18289
18434
  children: [
18290
18435
  /* @__PURE__ */ jsxs("div", {
@@ -18302,7 +18447,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
18302
18447
  });
18303
18448
  if (error || !plugin) return /* @__PURE__ */ jsxs("div", {
18304
18449
  className: "space-y-6",
18305
- children: [/* @__PURE__ */ jsx(BackLink, {}), /* @__PURE__ */ jsxs("div", {
18450
+ children: [/* @__PURE__ */ jsx(BackLink$1, {}), /* @__PURE__ */ jsxs("div", {
18306
18451
  className: "rounded-lg border border-kumo-danger/50 bg-kumo-danger/10 p-6 text-center",
18307
18452
  children: [
18308
18453
  /* @__PURE__ */ jsx(Warning, { className: "mx-auto h-8 w-8 text-kumo-danger" }),
@@ -18340,7 +18485,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
18340
18485
  return /* @__PURE__ */ jsxs("div", {
18341
18486
  className: "space-y-6",
18342
18487
  children: [
18343
- /* @__PURE__ */ jsx(BackLink, {}),
18488
+ /* @__PURE__ */ jsx(BackLink$1, {}),
18344
18489
  /* @__PURE__ */ jsxs("div", {
18345
18490
  className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
18346
18491
  children: [/* @__PURE__ */ jsxs("div", {
@@ -18619,7 +18764,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
18619
18764
  ]
18620
18765
  });
18621
18766
  }
18622
- function BackLink() {
18767
+ function BackLink$1() {
18623
18768
  const { _: _t2 } = useLingui();
18624
18769
  return /* @__PURE__ */ jsxs(Link$1, {
18625
18770
  to: "/plugins/marketplace",
@@ -18966,7 +19111,7 @@ function MediaDetailPanel({ item, onClose, onDeleted }) {
18966
19111
  message: "Uploaded:"
18967
19112
  })
18968
19113
  }),
18969
- /* @__PURE__ */ jsx("span", { children: formatDate(item.createdAt) })
19114
+ /* @__PURE__ */ jsx("span", { children: formatDate$1(item.createdAt) })
18970
19115
  ]
18971
19116
  })
18972
19117
  ]
@@ -19084,7 +19229,7 @@ function MediaDetailPanel({ item, onClose, onDeleted }) {
19084
19229
  onConfirm: () => deleteMutation.mutate()
19085
19230
  })] });
19086
19231
  }
19087
- function formatDate(isoString) {
19232
+ function formatDate$1(isoString) {
19088
19233
  return new Date(isoString).toLocaleDateString(void 0, {
19089
19234
  year: "numeric",
19090
19235
  month: "short",
@@ -19099,7 +19244,7 @@ function formatDate(isoString) {
19099
19244
  /**
19100
19245
  * Media library component with upload, provider tabs, and grid view
19101
19246
  */
19102
- function MediaLibrary({ items = [], isLoading, onUpload, onDelete, onItemUpdated }) {
19247
+ function MediaLibrary({ items = [], isLoading, onUpload, onDelete, onItemUpdated, hasMore, onLoadMore }) {
19103
19248
  const { _: _t } = useLingui();
19104
19249
  const [viewMode, setViewMode] = React$1.useState("grid");
19105
19250
  const [selectedItem, setSelectedItem] = React$1.useState(null);
@@ -19437,7 +19582,7 @@ function MediaLibrary({ items = [], isLoading, onUpload, onDelete, onItemUpdated
19437
19582
  className: "ps-9"
19438
19583
  })]
19439
19584
  }),
19440
- currentLoading ? /* @__PURE__ */ jsx("div", {
19585
+ currentLoading && currentItems.length === 0 && currentProviderItems.length === 0 ? /* @__PURE__ */ jsx("div", {
19441
19586
  className: "flex items-center justify-center py-12",
19442
19587
  children: /* @__PURE__ */ jsx(Loader, {})
19443
19588
  }) : activeProvider === "local" && currentItems.length === 0 ? /* @__PURE__ */ jsxs("div", {
@@ -19601,6 +19746,21 @@ function MediaLibrary({ items = [], isLoading, onUpload, onDelete, onItemUpdated
19601
19746
  })]
19602
19747
  })
19603
19748
  }),
19749
+ activeProvider === "local" && hasMore && onLoadMore && /* @__PURE__ */ jsx("div", {
19750
+ className: "flex justify-center",
19751
+ children: /* @__PURE__ */ jsx(Button, {
19752
+ variant: "outline",
19753
+ onClick: onLoadMore,
19754
+ disabled: isLoading,
19755
+ children: isLoading ? _t({
19756
+ id: "Z3FXyt",
19757
+ message: "Loading..."
19758
+ }) : _t({
19759
+ id: "a4N/Bg",
19760
+ message: "Load More"
19761
+ })
19762
+ })
19763
+ }),
19604
19764
  selectedItem && /* @__PURE__ */ jsx(MediaDetailPanel, {
19605
19765
  item: selectedItem,
19606
19766
  onClose: () => setSelectedItem(null),
@@ -20266,7 +20426,7 @@ function MenuEditor() {
20266
20426
  newItems[targetIndex] = currentItem;
20267
20427
  const reorderedItems = newItems.map((item, i) => ({
20268
20428
  id: item.id,
20269
- parentId: item.parent_id,
20429
+ parentId: item.parentId,
20270
20430
  sortOrder: i
20271
20431
  }));
20272
20432
  setLocalItems(newItems);
@@ -20545,9 +20705,9 @@ function MenuEditor() {
20545
20705
  children: item.label
20546
20706
  }), /* @__PURE__ */ jsxs("div", {
20547
20707
  className: "text-sm text-kumo-subtle",
20548
- children: [item.type === "custom" ? item.custom_url : /* @__PURE__ */ jsx("span", {
20708
+ children: [item.type === "custom" ? item.customUrl : /* @__PURE__ */ jsx("span", {
20549
20709
  className: "inline-flex items-center rounded-full bg-kumo-brand/10 px-2 py-0.5 text-xs font-medium text-kumo-brand",
20550
- children: item.reference_collection ?? item.type
20710
+ children: item.referenceCollection ?? item.type
20551
20711
  }), item.target === "_blank" && _t({
20552
20712
  id: "+Lmv8u",
20553
20713
  message: " (opens in new window)"
@@ -20669,7 +20829,7 @@ function MenuEditor() {
20669
20829
  id: "TQJouj",
20670
20830
  message: "Enter a URL (https://…) or a relative path (/…)"
20671
20831
  }),
20672
- defaultValue: editingItem.custom_url || ""
20832
+ defaultValue: editingItem.customUrl || ""
20673
20833
  }),
20674
20834
  /* @__PURE__ */ jsxs(Select, {
20675
20835
  label: _t({
@@ -21791,6 +21951,806 @@ function Redirects() {
21791
21951
  });
21792
21952
  }
21793
21953
 
21954
+ //#endregion
21955
+ //#region src/lib/api/registry.ts
21956
+ let cachedDiscovery = null;
21957
+ async function getDiscoveryClient(config) {
21958
+ if (cachedDiscovery && cachedDiscovery.config.aggregatorUrl === config.aggregatorUrl && cachedDiscovery.config.acceptLabelers === config.acceptLabelers) return cachedDiscovery.client;
21959
+ const DiscoveryClient = (await import("@emdash-cms/registry-client/discovery")).DiscoveryClient;
21960
+ const discovery = new DiscoveryClient({
21961
+ aggregatorUrl: config.aggregatorUrl,
21962
+ acceptLabelers: config.acceptLabelers
21963
+ });
21964
+ const wrapped = {
21965
+ async searchPackages(opts) {
21966
+ return await discovery.searchPackages({
21967
+ q: opts.q,
21968
+ cursor: opts.cursor,
21969
+ limit: opts.limit
21970
+ });
21971
+ },
21972
+ async resolvePackage(handle, slug) {
21973
+ return await discovery.resolvePackage({
21974
+ handle,
21975
+ slug
21976
+ });
21977
+ },
21978
+ async getPackage(did, slug) {
21979
+ return await discovery.getPackage({
21980
+ did,
21981
+ slug
21982
+ });
21983
+ },
21984
+ async getLatestRelease(did, slug) {
21985
+ return await discovery.getLatestRelease({
21986
+ did,
21987
+ package: slug
21988
+ });
21989
+ },
21990
+ async listReleases(did, slug, cursor) {
21991
+ return await discovery.listReleases({
21992
+ did,
21993
+ package: slug,
21994
+ cursor
21995
+ });
21996
+ }
21997
+ };
21998
+ cachedDiscovery = {
21999
+ config,
22000
+ client: wrapped
22001
+ };
22002
+ return wrapped;
22003
+ }
22004
+ /**
22005
+ * Returns whether a release should be considered installable given the
22006
+ * configured policy. Currently implements the minimum-release-age check
22007
+ * described in RFC 0001's "Pre-label gap and launch tempo" section,
22008
+ * plus the `minimumReleaseAgeExclude` allowlist.
22009
+ *
22010
+ * Returns `false` (release blocked) when the policy is configured but
22011
+ * the release is missing a valid `indexedAt` -- we fail closed rather
22012
+ * than silently letting unbounded-age releases through.
22013
+ */
22014
+ function releasePassesPolicy(release, pkg, policy, now = Date.now()) {
22015
+ if (!policy?.minimumReleaseAgeSeconds) return true;
22016
+ if (releaseExemptFromMinimumAge(policy.minimumReleaseAgeExclude, pkg.did, pkg.slug)) return true;
22017
+ const indexedAt = Date.parse(release.indexedAt);
22018
+ if (!Number.isFinite(indexedAt)) return false;
22019
+ return (now - indexedAt) / 1e3 >= policy.minimumReleaseAgeSeconds;
22020
+ }
22021
+ /**
22022
+ * Canonicalize a capabilities list for set-style comparison. Mirrors
22023
+ * the server-side helper `canonicalCapabilitiesForDriftCheck` in
22024
+ * `packages/core/src/registry/config.ts` -- both sides must produce
22025
+ * the same canonical shape so the install handler's drift check is
22026
+ * stable across reorderings, duplicates, and junk entries.
22027
+ *
22028
+ * Filters non-strings, deduplicates, and sorts lexically.
22029
+ */
22030
+ function canonicalCapabilitiesForDriftCheck(value) {
22031
+ if (!Array.isArray(value)) return [];
22032
+ const seen = /* @__PURE__ */ new Set();
22033
+ for (const entry of value) if (typeof entry === "string" && entry.length > 0) seen.add(entry);
22034
+ return [...seen].toSorted();
22035
+ }
22036
+ /**
22037
+ * Matches a `(publisher_did, slug)` against the
22038
+ * `minimumReleaseAgeExclude` allowlist. Mirrors the server-side helper
22039
+ * of the same name in `packages/core/src/registry/config.ts`.
22040
+ *
22041
+ * DID-only on purpose: handles are aggregator-supplied envelope data
22042
+ * and accepting them as a trust input would let a compromised
22043
+ * aggregator bypass the holdback by claiming any handle for any
22044
+ * package. DIDs are tied to the AT URI of the record itself.
22045
+ *
22046
+ * Entries from the config list have already been lowercased at
22047
+ * manifest build time, so this only needs to lowercase the runtime
22048
+ * values for comparison.
22049
+ */
22050
+ function releaseExemptFromMinimumAge(exclude, publisherDid, slug) {
22051
+ if (!exclude || exclude.length === 0) return false;
22052
+ const didLower = publisherDid.toLowerCase();
22053
+ const fullDid = `${didLower}/${slug.toLowerCase()}`;
22054
+ for (const entry of exclude) {
22055
+ if (entry === didLower) return true;
22056
+ if (entry === fullDid) return true;
22057
+ }
22058
+ return false;
22059
+ }
22060
+ async function searchRegistryPackages(config, opts) {
22061
+ return (await getDiscoveryClient(config)).searchPackages(opts);
22062
+ }
22063
+ async function resolveRegistryPackage(config, handle, slug) {
22064
+ return (await getDiscoveryClient(config)).resolvePackage(handle, slug);
22065
+ }
22066
+ async function getRegistryPackage(config, did, slug) {
22067
+ return (await getDiscoveryClient(config)).getPackage(did, slug);
22068
+ }
22069
+ async function getLatestRegistryRelease(config, did, slug) {
22070
+ return (await getDiscoveryClient(config)).getLatestRelease(did, slug);
22071
+ }
22072
+ /**
22073
+ * Resolve a publisher DID to its claimed handle using the same
22074
+ * `LocalActorResolver` pattern as `@emdash-cms/plugin-cli` and
22075
+ * `@emdash-cms/auth-atproto`. Bidirectional verification (handle's
22076
+ * domain points back to the same DID) is part of the resolver --
22077
+ * `LocalActorResolver` returns the sentinel `"handle.invalid"` when
22078
+ * the `alsoKnownAs` handle is present but doesn't round-trip.
22079
+ *
22080
+ * Three distinct outcomes the UI can render:
22081
+ *
22082
+ * - `{ status: "ok", handle }` — verified handle, round-trip OK.
22083
+ * - `{ status: "invalid" }` — DID claims a handle but it doesn't
22084
+ * resolve back. The publisher's handle setup is broken; the admin
22085
+ * should see a clear "Invalid handle" indicator rather than the
22086
+ * raw DID.
22087
+ * - `{ status: "missing" }` — no handle claimed at all (no
22088
+ * `alsoKnownAs`), or the DID document couldn't be fetched (network
22089
+ * error, unsupported DID method).
22090
+ */
22091
+ let actorResolver = null;
22092
+ async function getActorResolver() {
22093
+ if (actorResolver) return actorResolver;
22094
+ const { CompositeDidDocumentResolver, CompositeHandleResolver, DohJsonHandleResolver, LocalActorResolver, PlcDidDocumentResolver, WebDidDocumentResolver, WellKnownHandleResolver } = await import("@atcute/identity-resolver");
22095
+ actorResolver = new LocalActorResolver({
22096
+ handleResolver: new CompositeHandleResolver({ methods: {
22097
+ dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" }),
22098
+ http: new WellKnownHandleResolver()
22099
+ } }),
22100
+ didDocumentResolver: new CompositeDidDocumentResolver({ methods: {
22101
+ plc: new PlcDidDocumentResolver(),
22102
+ web: new WebDidDocumentResolver()
22103
+ } })
22104
+ });
22105
+ return actorResolver;
22106
+ }
22107
+ /**
22108
+ * localStorage-backed cache for DID→handle resolutions. Handles are
22109
+ * stable for hours-to-days in practice, but bound the cache so a
22110
+ * compromised handle eventually flips back to "invalid" without a
22111
+ * forced refresh. 24h matches the typical atproto handle TTL.
22112
+ *
22113
+ * Failures (network errors, unsupported DID method) are *not* cached --
22114
+ * those should retry on the next render.
22115
+ */
22116
+ const HANDLE_CACHE_TTL_MS = 1440 * 60 * 1e3;
22117
+ const HANDLE_CACHE_KEY_PREFIX = "emdash:did-handle:";
22118
+ function readHandleCache(did) {
22119
+ if (typeof localStorage === "undefined") return null;
22120
+ try {
22121
+ const raw = localStorage.getItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`);
22122
+ if (!raw) return null;
22123
+ const parsed = JSON.parse(raw);
22124
+ if (!parsed || typeof parsed.expiresAt !== "number" || parsed.expiresAt < Date.now()) return null;
22125
+ return parsed.resolution;
22126
+ } catch {
22127
+ return null;
22128
+ }
22129
+ }
22130
+ function writeHandleCache(did, resolution) {
22131
+ if (typeof localStorage === "undefined") return;
22132
+ try {
22133
+ const entry = {
22134
+ resolution,
22135
+ expiresAt: Date.now() + HANDLE_CACHE_TTL_MS
22136
+ };
22137
+ localStorage.setItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`, JSON.stringify(entry));
22138
+ } catch {}
22139
+ }
22140
+ async function resolveDidToHandle(did) {
22141
+ const cached = readHandleCache(did);
22142
+ if (cached) return cached;
22143
+ let result;
22144
+ try {
22145
+ const resolved = await (await getActorResolver()).resolve(did);
22146
+ if (resolved.handle === "handle.invalid") result = { status: "invalid" };
22147
+ else if (resolved.handle) result = {
22148
+ status: "ok",
22149
+ handle: resolved.handle
22150
+ };
22151
+ else result = { status: "missing" };
22152
+ } catch (err) {
22153
+ console.warn(`[registry] DID->handle resolution failed for ${did}:`, err);
22154
+ return { status: "missing" };
22155
+ }
22156
+ writeHandleCache(did, result);
22157
+ return result;
22158
+ }
22159
+ const INSTALL_ENDPOINT = `${API_BASE}/admin/plugins/registry/install`;
22160
+ /**
22161
+ * Install a plugin from the registry.
22162
+ *
22163
+ * Posts to the EmDash server, which re-resolves the same `(handle,
22164
+ * slug)` against the aggregator, re-verifies the bundle's checksum
22165
+ * against the signed release record, and writes the install. Surfaces
22166
+ * structured error codes (`RELEASE_YANKED`, `CHECKSUM_MISMATCH`,
22167
+ * `DECLARED_ACCESS_DRIFT`, etc.) that callers map to localized
22168
+ * messages.
22169
+ */
22170
+ async function installRegistryPlugin(body) {
22171
+ const response = await apiFetch(INSTALL_ENDPOINT, {
22172
+ method: "POST",
22173
+ headers: { "Content-Type": "application/json" },
22174
+ body: JSON.stringify(body)
22175
+ });
22176
+ if (!response.ok) await throwResponseError(response, i18n._({
22177
+ id: "/LWKyx",
22178
+ message: "Failed to install plugin"
22179
+ }));
22180
+ return (await response.json()).data;
22181
+ }
22182
+
22183
+ //#endregion
22184
+ //#region src/components/PublisherHandle.tsx
22185
+ /**
22186
+ * Renders an atproto publisher's identity, with three branches:
22187
+ *
22188
+ * - **Verified handle**: shows `@handle`. Our local
22189
+ * `LocalActorResolver` round-tripped the DID document's
22190
+ * `alsoKnownAs` back to the same DID (verified by DNS TXT or
22191
+ * `.well-known`, not by the aggregator).
22192
+ * - **Unverified publisher**: DID document claims a handle but the
22193
+ * handle's domain doesn't point back to the same DID, OR the
22194
+ * aggregator's claimed handle doesn't match the bidirectionally
22195
+ * verified one. Treat as untrusted -- the publisher might be
22196
+ * impersonating someone else, or the aggregator might be lying
22197
+ * about a handle. Surface as `Unverified publisher` in error
22198
+ * styling. Callers should also disable destructive actions
22199
+ * (install, etc.).
22200
+ * - **Missing handle**: no handle claimed in the DID document (no
22201
+ * `alsoKnownAs`), or the DID document couldn't be fetched
22202
+ * (network error, unsupported DID method).
22203
+ *
22204
+ * `aggregatorHandle` is what the registry's `searchPackages` /
22205
+ * `resolvePackage` endpoint returned for this DID. It is NEVER trusted
22206
+ * on its own -- the aggregator is an untrusted indexer that could be
22207
+ * compromised or buggy. We always run our own DID->handle round-trip
22208
+ * via `LocalActorResolver` (cached in localStorage for 24h) and use
22209
+ * the aggregator's value only to *cross-check*: if the aggregator
22210
+ * claims a handle that differs from what the DID document
22211
+ * bidirectionally verifies, the publisher is marked invalid.
22212
+ */
22213
+ /** Trailing dot(s) on an FQDN, stripped before handle comparison. */
22214
+ const TRAILING_DOT = /\.+$/;
22215
+ /**
22216
+ * Hook form: returns the same tri-state result without rendering. Use
22217
+ * when a parent needs to coordinate UI (e.g. disable install) based on
22218
+ * the resolution.
22219
+ */
22220
+ function usePublisherHandle(did, aggregatorHandle) {
22221
+ const { data: didHandleResolution, isPending } = useQuery({
22222
+ queryKey: [
22223
+ "registry",
22224
+ "did-handle",
22225
+ did
22226
+ ],
22227
+ queryFn: () => resolveDidToHandle(did),
22228
+ enabled: Boolean(did),
22229
+ staleTime: 300 * 1e3
22230
+ });
22231
+ if (isPending || !didHandleResolution) return { status: "missing" };
22232
+ if (didHandleResolution.status === "missing") return { status: "missing" };
22233
+ if (didHandleResolution.status === "invalid") return { status: "invalid" };
22234
+ const verifiedHandle = didHandleResolution.handle.toLowerCase();
22235
+ if (aggregatorHandle) {
22236
+ if (aggregatorHandle.toLowerCase().replace(TRAILING_DOT, "") !== verifiedHandle) return { status: "invalid" };
22237
+ }
22238
+ return {
22239
+ status: "ok",
22240
+ handle: didHandleResolution.handle
22241
+ };
22242
+ }
22243
+ function PublisherHandle({ did, aggregatorHandle, onResolved, variant = "card", className }) {
22244
+ const { _: _t } = useLingui();
22245
+ const result = usePublisherHandle(did, aggregatorHandle);
22246
+ const onResolvedRef = React$1.useRef(onResolved);
22247
+ onResolvedRef.current = onResolved;
22248
+ React$1.useEffect(() => {
22249
+ onResolvedRef.current?.(result);
22250
+ }, [result.status, result.handle]);
22251
+ const textClass = variant === "card" ? "text-xs" : "text-sm";
22252
+ if (result.status === "ok" && result.handle) return /* @__PURE__ */ jsxs("span", {
22253
+ className: `truncate ${textClass} text-kumo-subtle ${className ?? ""}`,
22254
+ children: ["@", result.handle]
22255
+ });
22256
+ if (result.status === "invalid") return /* @__PURE__ */ jsx("span", {
22257
+ className: `truncate ${textClass} font-medium text-kumo-error ${className ?? ""}`,
22258
+ children: _t({
22259
+ id: "N3r0DC",
22260
+ message: "Unverified publisher"
22261
+ })
22262
+ });
22263
+ return /* @__PURE__ */ jsx("span", {
22264
+ className: `truncate ${textClass} text-kumo-subtle ${className ?? ""}`,
22265
+ children: did
22266
+ });
22267
+ }
22268
+
22269
+ //#endregion
22270
+ //#region src/components/RegistryBrowse.tsx
22271
+ /**
22272
+ * Registry Browse
22273
+ *
22274
+ * Grid of plugin cards backed by the experimental decentralized plugin
22275
+ * registry's aggregator. Search box debounces directly into the
22276
+ * aggregator's `searchPackages` XRPC -- the aggregator is a public,
22277
+ * read-only service, so no server proxy is involved.
22278
+ *
22279
+ * Cards navigate to `/plugins/marketplace/$pluginId` (the same path the
22280
+ * marketplace browse uses); the router branches to the registry detail
22281
+ * component when `manifest.registry` is configured.
22282
+ */
22283
+ function RegistryBrowse({ config, installedRegistryUris = /* @__PURE__ */ new Set() }) {
22284
+ const { _: _t } = useLingui();
22285
+ const [searchQuery, setSearchQuery] = React$1.useState("");
22286
+ const [debouncedQuery, setDebouncedQuery] = React$1.useState("");
22287
+ React$1.useEffect(() => {
22288
+ const timer = setTimeout(setDebouncedQuery, 300, searchQuery);
22289
+ return () => clearTimeout(timer);
22290
+ }, [searchQuery]);
22291
+ const { data, isLoading, error, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
22292
+ queryKey: [
22293
+ "registry",
22294
+ "search",
22295
+ config.aggregatorUrl,
22296
+ debouncedQuery
22297
+ ],
22298
+ queryFn: ({ pageParam }) => searchRegistryPackages(config, {
22299
+ q: debouncedQuery || void 0,
22300
+ cursor: pageParam,
22301
+ limit: 20
22302
+ }),
22303
+ initialPageParam: void 0,
22304
+ getNextPageParam: (lastPage) => lastPage.cursor
22305
+ });
22306
+ const packages = data?.pages.flatMap((p) => p.packages);
22307
+ return /* @__PURE__ */ jsxs("div", {
22308
+ className: "space-y-6",
22309
+ children: [
22310
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
22311
+ className: "text-3xl font-bold",
22312
+ children: _t({
22313
+ id: "jNlCXv",
22314
+ message: "Plugin Registry"
22315
+ })
22316
+ }), /* @__PURE__ */ jsx("p", {
22317
+ className: "mt-1 text-kumo-subtle",
22318
+ children: _t({
22319
+ id: "ceMA7u",
22320
+ message: "Browse and install plugins published to the decentralized registry."
22321
+ })
22322
+ })] }),
22323
+ /* @__PURE__ */ jsx("div", {
22324
+ className: "flex flex-col gap-3 sm:flex-row sm:items-center",
22325
+ children: /* @__PURE__ */ jsxs("div", {
22326
+ className: "relative flex-1",
22327
+ children: [/* @__PURE__ */ jsx(MagnifyingGlass, { className: "absolute start-3 top-1/2 h-4 w-4 -translate-y-1/2 text-kumo-subtle" }), /* @__PURE__ */ jsx(Input, {
22328
+ type: "search",
22329
+ placeholder: _t({
22330
+ id: "slq1pb",
22331
+ message: "Search plugins..."
22332
+ }),
22333
+ value: searchQuery,
22334
+ onChange: (e) => setSearchQuery(e.target.value),
22335
+ className: "ps-9",
22336
+ "aria-label": _t({
22337
+ id: "S4qZX0",
22338
+ message: "Search plugins"
22339
+ })
22340
+ })]
22341
+ })
22342
+ }),
22343
+ error ? /* @__PURE__ */ jsx("div", {
22344
+ className: "rounded-md border border-kumo-error bg-kumo-error/10 p-4 text-kumo-error",
22345
+ role: "alert",
22346
+ children: _t({
22347
+ id: "obuqKa",
22348
+ message: "Failed to load plugins. The registry aggregator may be unreachable."
22349
+ })
22350
+ }) : null,
22351
+ isLoading ? /* @__PURE__ */ jsx("div", {
22352
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
22353
+ children: Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-32 animate-pulse rounded-md border border-kumo-border bg-kumo-subtle" }, i))
22354
+ }) : null,
22355
+ packages && packages.length === 0 ? /* @__PURE__ */ jsx("div", {
22356
+ className: "rounded-md border border-kumo-border bg-kumo-subtle p-8 text-center text-kumo-subtle",
22357
+ children: debouncedQuery ? _t({
22358
+ id: "5ZLwXV",
22359
+ message: "No plugins match \"{debouncedQuery}\".",
22360
+ values: { debouncedQuery }
22361
+ }) : _t({
22362
+ id: "+ugpTI",
22363
+ message: "No plugins have been published to this registry yet."
22364
+ })
22365
+ }) : null,
22366
+ packages && packages.length > 0 ? /* @__PURE__ */ jsx("div", {
22367
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
22368
+ children: packages.map((pkg) => /* @__PURE__ */ jsx(RegistryPackageCard, {
22369
+ pkg,
22370
+ installed: installedRegistryUris.has(pkg.uri)
22371
+ }, pkg.uri))
22372
+ }) : null,
22373
+ hasNextPage ? /* @__PURE__ */ jsx("div", {
22374
+ className: "flex justify-center",
22375
+ children: /* @__PURE__ */ jsx("button", {
22376
+ type: "button",
22377
+ className: "rounded-md border border-kumo-border bg-kumo-surface px-4 py-2 text-sm hover:bg-kumo-subtle disabled:opacity-50",
22378
+ onClick: () => fetchNextPage(),
22379
+ disabled: isFetchingNextPage,
22380
+ children: isFetchingNextPage ? _t({
22381
+ id: "Z3FXyt",
22382
+ message: "Loading..."
22383
+ }) : _t({
22384
+ id: "yQ2kGp",
22385
+ message: "Load more"
22386
+ })
22387
+ })
22388
+ }) : null
22389
+ ]
22390
+ });
22391
+ }
22392
+ function RegistryPackageCard({ pkg, installed }) {
22393
+ const { _: _t2 } = useLingui();
22394
+ const linkSegment = usePublisherHandle(pkg.did, pkg.handle).handle ?? pkg.did;
22395
+ const profile = pkg.profile;
22396
+ const verified = (pkg.labels ?? []).some((l) => l.val === "verified");
22397
+ return /* @__PURE__ */ jsx(Link$1, {
22398
+ to: "/plugins/marketplace/$pluginId",
22399
+ params: { pluginId: `${linkSegment}/${pkg.slug}` },
22400
+ className: "block rounded-md border border-kumo-border bg-kumo-surface p-4 transition-colors hover:bg-kumo-subtle focus:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand",
22401
+ children: /* @__PURE__ */ jsxs("div", {
22402
+ className: "flex items-start gap-3",
22403
+ children: [/* @__PURE__ */ jsx("div", {
22404
+ className: "mt-1 rounded-md bg-kumo-subtle p-2 text-kumo-subtle",
22405
+ children: /* @__PURE__ */ jsx(PuzzlePiece, { className: "h-5 w-5" })
22406
+ }), /* @__PURE__ */ jsxs("div", {
22407
+ className: "min-w-0 flex-1",
22408
+ children: [
22409
+ /* @__PURE__ */ jsxs("div", {
22410
+ className: "flex items-center gap-2",
22411
+ children: [/* @__PURE__ */ jsx("h2", {
22412
+ className: "truncate font-semibold",
22413
+ children: profile.name ?? pkg.slug
22414
+ }), verified ? /* @__PURE__ */ jsx(ShieldCheck, {
22415
+ className: "h-4 w-4 shrink-0 text-kumo-brand",
22416
+ "aria-label": _t2({
22417
+ id: "70QfpA",
22418
+ message: "Verified publisher"
22419
+ })
22420
+ }) : null]
22421
+ }),
22422
+ /* @__PURE__ */ jsx(PublisherHandle, {
22423
+ did: pkg.did,
22424
+ aggregatorHandle: pkg.handle,
22425
+ variant: "card"
22426
+ }),
22427
+ profile.description ? /* @__PURE__ */ jsx("p", {
22428
+ className: "mt-2 line-clamp-2 text-sm text-kumo-default",
22429
+ children: profile.description
22430
+ }) : null,
22431
+ installed ? /* @__PURE__ */ jsx("div", {
22432
+ className: "mt-3",
22433
+ children: /* @__PURE__ */ jsx(Badge, {
22434
+ variant: "success",
22435
+ children: _t2({
22436
+ id: "eQkgKV",
22437
+ message: "Installed"
22438
+ })
22439
+ })
22440
+ }) : null
22441
+ ]
22442
+ })]
22443
+ })
22444
+ });
22445
+ }
22446
+
22447
+ //#endregion
22448
+ //#region src/components/RegistryPluginDetail.tsx
22449
+ /**
22450
+ * Registry Plugin Detail
22451
+ *
22452
+ * Detail view for a plugin from the experimental decentralized plugin
22453
+ * registry. Resolves `(handle, slug)` directly against the configured
22454
+ * aggregator; install routes through the EmDash server's
22455
+ * `/_emdash/api/admin/plugins/registry/install` endpoint, which
22456
+ * re-resolves and re-verifies before writing the install.
22457
+ *
22458
+ * Identified in the URL by a `pluginId` that is `${handle}/${slug}`.
22459
+ * The router wraps this component when `manifest.registry` is set on
22460
+ * the same route the marketplace detail uses, so existing bookmarks /
22461
+ * sidebar entries stay stable.
22462
+ */
22463
+ function RegistryPluginDetail({ pluginId, config }) {
22464
+ const { _: _t } = useLingui();
22465
+ const queryClient = useQueryClient();
22466
+ const [showConsent, setShowConsent] = React$1.useState(false);
22467
+ const { data: installedPlugins } = useQuery({
22468
+ queryKey: ["plugins"],
22469
+ queryFn: async () => {
22470
+ const { fetchPlugins } = await import("./plugins-qrjvPK4g.js").then((n) => n.a);
22471
+ return fetchPlugins();
22472
+ }
22473
+ });
22474
+ const slashIdx = pluginId.lastIndexOf("/");
22475
+ const publisher = slashIdx > 0 ? pluginId.slice(0, slashIdx) : "";
22476
+ const slug = slashIdx > 0 ? pluginId.slice(slashIdx + 1) : "";
22477
+ const isDid = publisher.startsWith("did:");
22478
+ const { data: pkg, isLoading: isLoadingPkg } = useQuery({
22479
+ queryKey: [
22480
+ "registry",
22481
+ "package",
22482
+ config.aggregatorUrl,
22483
+ publisher,
22484
+ slug,
22485
+ isDid
22486
+ ],
22487
+ queryFn: () => isDid ? getRegistryPackage(config, publisher, slug) : resolveRegistryPackage(config, publisher, slug),
22488
+ enabled: Boolean(publisher && slug)
22489
+ });
22490
+ const handleResult = usePublisherHandle(pkg?.did ?? "", pkg?.handle);
22491
+ const { data: release } = useQuery({
22492
+ queryKey: [
22493
+ "registry",
22494
+ "latest-release",
22495
+ config.aggregatorUrl,
22496
+ pkg?.did,
22497
+ slug
22498
+ ],
22499
+ queryFn: () => getLatestRegistryRelease(config, pkg.did, slug),
22500
+ enabled: Boolean(pkg?.did && slug)
22501
+ });
22502
+ const ext = (release?.release)?.extensions?.["com.emdashcms.experimental.package.releaseExtension"];
22503
+ const capabilities = Array.isArray(ext?.capabilities) ? canonicalCapabilitiesForDriftCheck(ext?.capabilities) : canonicalCapabilitiesForDriftCheck(declaredAccessToCapabilityList(ext?.declaredAccess));
22504
+ const profile = pkg?.profile;
22505
+ const verified = (pkg?.labels ?? []).some((l) => l.val === "verified");
22506
+ const policyOk = release && pkg ? releasePassesPolicy(release, {
22507
+ did: pkg.did,
22508
+ slug
22509
+ }, config.policy) : true;
22510
+ const installedEntry = React$1.useMemo(() => {
22511
+ if (!pkg || !installedPlugins) return void 0;
22512
+ return installedPlugins.find((p) => p.source === "registry" && p.registryPublisherDid === pkg.did && p.registrySlug === slug);
22513
+ }, [
22514
+ pkg,
22515
+ installedPlugins,
22516
+ slug
22517
+ ]);
22518
+ const isInstalled = Boolean(installedEntry);
22519
+ const installMutation = useMutation({
22520
+ mutationFn: () => {
22521
+ if (!pkg) throw new Error("Package not loaded");
22522
+ return installRegistryPlugin({
22523
+ did: pkg.did,
22524
+ slug,
22525
+ version: release?.version,
22526
+ acknowledgedDeclaredAccess: capabilities
22527
+ });
22528
+ },
22529
+ onSuccess: () => {
22530
+ setShowConsent(false);
22531
+ queryClient.invalidateQueries({ queryKey: ["plugins"] });
22532
+ queryClient.invalidateQueries({ queryKey: ["manifest"] });
22533
+ queryClient.invalidateQueries({ queryKey: ["registry"] });
22534
+ }
22535
+ });
22536
+ if (isLoadingPkg) return /* @__PURE__ */ jsxs("div", {
22537
+ className: "space-y-6",
22538
+ children: [/* @__PURE__ */ jsx(BackLink, {}), /* @__PURE__ */ jsxs("div", {
22539
+ className: "animate-pulse space-y-4",
22540
+ children: [
22541
+ /* @__PURE__ */ jsxs("div", {
22542
+ className: "flex items-center gap-4",
22543
+ children: [/* @__PURE__ */ jsx("div", { className: "h-16 w-16 rounded-xl bg-kumo-subtle" }), /* @__PURE__ */ jsxs("div", {
22544
+ className: "space-y-2",
22545
+ children: [/* @__PURE__ */ jsx("div", { className: "h-6 w-48 rounded bg-kumo-subtle" }), /* @__PURE__ */ jsx("div", { className: "h-4 w-32 rounded bg-kumo-subtle" })]
22546
+ })]
22547
+ }),
22548
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-full rounded bg-kumo-subtle" }),
22549
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-3/4 rounded bg-kumo-subtle" })
22550
+ ]
22551
+ })]
22552
+ });
22553
+ if (!pkg) return /* @__PURE__ */ jsxs("div", {
22554
+ className: "space-y-6",
22555
+ children: [/* @__PURE__ */ jsx(BackLink, {}), /* @__PURE__ */ jsx("div", {
22556
+ className: "rounded-md border border-kumo-error bg-kumo-error/10 p-4 text-kumo-error",
22557
+ role: "alert",
22558
+ children: _t({
22559
+ id: "wZ5IqF",
22560
+ message: "Plugin not found. The publisher handle or slug may be incorrect."
22561
+ })
22562
+ })]
22563
+ });
22564
+ return /* @__PURE__ */ jsxs("div", {
22565
+ className: "space-y-6",
22566
+ children: [
22567
+ /* @__PURE__ */ jsx(BackLink, {}),
22568
+ /* @__PURE__ */ jsxs("div", {
22569
+ className: "flex items-start gap-4",
22570
+ children: [
22571
+ /* @__PURE__ */ jsx("div", {
22572
+ className: "rounded-xl bg-kumo-subtle p-3 text-kumo-subtle",
22573
+ children: /* @__PURE__ */ jsx("span", {
22574
+ "aria-hidden": true,
22575
+ className: "block h-10 w-10"
22576
+ })
22577
+ }),
22578
+ /* @__PURE__ */ jsxs("div", {
22579
+ className: "min-w-0 flex-1",
22580
+ children: [
22581
+ /* @__PURE__ */ jsxs("div", {
22582
+ className: "flex items-center gap-2",
22583
+ children: [/* @__PURE__ */ jsx("h1", {
22584
+ className: "truncate text-3xl font-bold",
22585
+ children: profile?.name ?? slug
22586
+ }), verified ? /* @__PURE__ */ jsx(ShieldCheck, {
22587
+ className: "h-5 w-5 shrink-0 text-kumo-brand",
22588
+ "aria-label": _t({
22589
+ id: "70QfpA",
22590
+ message: "Verified publisher"
22591
+ })
22592
+ }) : null]
22593
+ }),
22594
+ /* @__PURE__ */ jsxs("p", {
22595
+ className: "text-sm text-kumo-subtle",
22596
+ children: [
22597
+ _t({
22598
+ id: "D3P+jw",
22599
+ message: "Published by"
22600
+ }),
22601
+ " ",
22602
+ /* @__PURE__ */ jsx(PublisherHandle, {
22603
+ did: pkg.did,
22604
+ aggregatorHandle: pkg.handle,
22605
+ variant: "detail"
22606
+ })
22607
+ ]
22608
+ }),
22609
+ release ? /* @__PURE__ */ jsxs("p", {
22610
+ className: "text-xs text-kumo-subtle",
22611
+ children: [
22612
+ _t({
22613
+ id: "YYdC3A",
22614
+ message: "Version {0}",
22615
+ values: { 0: release.version }
22616
+ }),
22617
+ " · ",
22618
+ _t({
22619
+ id: "294VT5",
22620
+ message: "indexed {0}",
22621
+ values: { 0: formatDate(release.indexedAt) }
22622
+ })
22623
+ ]
22624
+ }) : null
22625
+ ]
22626
+ }),
22627
+ /* @__PURE__ */ jsx("div", { children: isInstalled ? /* @__PURE__ */ jsx(Button, {
22628
+ variant: "secondary",
22629
+ disabled: true,
22630
+ children: _t({
22631
+ id: "eQkgKV",
22632
+ message: "Installed"
22633
+ })
22634
+ }) : /* @__PURE__ */ jsx(Button, {
22635
+ variant: "primary",
22636
+ disabled: !release || !policyOk || handleResult.status === "invalid",
22637
+ onClick: () => setShowConsent(true),
22638
+ children: _t({
22639
+ id: "TKQ7K+",
22640
+ message: "Install"
22641
+ })
22642
+ }) })
22643
+ ]
22644
+ }),
22645
+ handleResult.status === "invalid" ? /* @__PURE__ */ jsxs("div", {
22646
+ className: "flex items-start gap-3 rounded-md border border-kumo-error bg-kumo-error/10 p-4 text-kumo-error",
22647
+ role: "alert",
22648
+ children: [/* @__PURE__ */ jsx(Warning, { className: "mt-0.5 h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
22649
+ className: "font-medium",
22650
+ children: _t({
22651
+ id: "R9fBA+",
22652
+ message: "We couldn't verify this publisher's identity"
22653
+ })
22654
+ }), /* @__PURE__ */ jsx("p", {
22655
+ className: "mt-1 text-sm text-kumo-default",
22656
+ children: _t({
22657
+ id: "Iu2xLX",
22658
+ message: "This publisher claims a name they couldn't prove they own — possibly impersonating someone else. Install is disabled. If you know the publisher and trust them, ask them to fix their identity setup before retrying."
22659
+ })
22660
+ })] })]
22661
+ }) : null,
22662
+ release && !policyOk ? /* @__PURE__ */ jsxs("div", {
22663
+ className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
22664
+ role: "status",
22665
+ children: [/* @__PURE__ */ jsx(Warning, { className: "mt-0.5 h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
22666
+ className: "font-medium",
22667
+ children: _t({
22668
+ id: "+2GRS6",
22669
+ message: "Release is too new to install"
22670
+ })
22671
+ }), /* @__PURE__ */ jsx("p", {
22672
+ className: "mt-1 text-sm text-kumo-default",
22673
+ children: _t({
22674
+ id: "77FdWO",
22675
+ message: "Your site requires releases to be at least {0} old before they can be installed. This release will become installable later.",
22676
+ values: { 0: formatHoldback(config.policy?.minimumReleaseAgeSeconds ?? 0) }
22677
+ })
22678
+ })] })]
22679
+ }) : null,
22680
+ profile?.description ? /* @__PURE__ */ jsx("p", {
22681
+ className: "text-base text-kumo-default",
22682
+ children: profile.description
22683
+ }) : null,
22684
+ capabilities.length > 0 ? /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h2", {
22685
+ className: "text-sm font-semibold text-kumo-subtle",
22686
+ children: _t({
22687
+ id: "FENDVB",
22688
+ message: "Declared permissions"
22689
+ })
22690
+ }), /* @__PURE__ */ jsx("div", {
22691
+ className: "mt-2 flex flex-wrap gap-2",
22692
+ children: capabilities.map((c) => /* @__PURE__ */ jsx(Badge, { children: c }, c))
22693
+ })] }) : null,
22694
+ showConsent && release ? /* @__PURE__ */ jsx(CapabilityConsentDialog, {
22695
+ mode: "install",
22696
+ pluginName: profile?.name ?? slug,
22697
+ capabilities,
22698
+ isPending: installMutation.isPending,
22699
+ error: getMutationError(installMutation.error),
22700
+ onConfirm: () => installMutation.mutate(),
22701
+ onCancel: () => {
22702
+ setShowConsent(false);
22703
+ installMutation.reset();
22704
+ }
22705
+ }) : null
22706
+ ]
22707
+ });
22708
+ }
22709
+ function BackLink() {
22710
+ const { _: _t2 } = useLingui();
22711
+ return /* @__PURE__ */ jsxs(Link$1, {
22712
+ to: "/plugins/marketplace",
22713
+ className: "inline-flex items-center gap-1 text-sm text-kumo-subtle hover:text-kumo-default",
22714
+ children: [/* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" }), _t2({
22715
+ id: "fnGsOh",
22716
+ message: "Back to plugins"
22717
+ })]
22718
+ });
22719
+ }
22720
+ /**
22721
+ * Flatten an RFC-0001 `declaredAccess` block (`{ content: { read: true },
22722
+ * email: { send: { allowedHosts: [...] } }, ... }`) into the legacy
22723
+ * `capabilities: string[]` shape that the existing sandbox runtime
22724
+ * enforces today. One entry per declared operation under each
22725
+ * category. Unknown values are skipped silently -- the consent dialog
22726
+ * shows only what the current runtime recognises.
22727
+ */
22728
+ function declaredAccessToCapabilityList(declaredAccess) {
22729
+ if (!declaredAccess || typeof declaredAccess !== "object") return [];
22730
+ const out = [];
22731
+ for (const [category, value] of Object.entries(declaredAccess)) {
22732
+ if (!value || typeof value !== "object") continue;
22733
+ for (const [operation, opValue] of Object.entries(value)) {
22734
+ if (opValue === false) continue;
22735
+ out.push(`${category}:${operation}`);
22736
+ }
22737
+ }
22738
+ return out;
22739
+ }
22740
+ function formatDate(iso) {
22741
+ try {
22742
+ return new Date(iso).toLocaleDateString();
22743
+ } catch {
22744
+ return iso;
22745
+ }
22746
+ }
22747
+ function formatHoldback(seconds) {
22748
+ if (seconds <= 0) return "0s";
22749
+ if (seconds < 3600) return `${Math.round(seconds / 60)} min`;
22750
+ if (seconds < 1440 * 60) return `${Math.round(seconds / 60 / 60)} h`;
22751
+ return `${Math.round(seconds / 60 / 60 / 24)} d`;
22752
+ }
22753
+
21794
22754
  //#endregion
21795
22755
  //#region src/components/SandboxedPluginPage.tsx
21796
22756
  /**
@@ -24556,15 +25516,25 @@ function GeneralSettings() {
24556
25516
  /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Label, { children: _t({
24557
25517
  id: "iG7KNr",
24558
25518
  message: "Logo"
24559
- }) }), formData.logo?.url ? /* @__PURE__ */ jsxs("div", {
25519
+ }) }), formData.logo?.mediaId ? /* @__PURE__ */ jsxs("div", {
24560
25520
  className: "mt-2 space-y-2",
24561
- children: [/* @__PURE__ */ jsx("img", {
25521
+ children: [formData.logo.url ? /* @__PURE__ */ jsx("img", {
24562
25522
  src: formData.logo.url,
24563
25523
  alt: formData.logo.alt || _t({
24564
25524
  id: "iG7KNr",
24565
25525
  message: "Logo"
24566
25526
  }),
24567
25527
  className: "h-16 rounded border bg-kumo-tint object-contain p-2"
25528
+ }) : /* @__PURE__ */ jsxs("div", {
25529
+ className: "flex min-h-16 items-center gap-2 rounded border border-dashed bg-kumo-tint px-3 py-2 text-sm text-kumo-subtle",
25530
+ role: "status",
25531
+ children: [/* @__PURE__ */ jsx(WarningCircle, {
25532
+ className: "h-4 w-4 flex-shrink-0",
25533
+ "aria-hidden": "true"
25534
+ }), /* @__PURE__ */ jsx("span", { children: _t({
25535
+ id: "Ja5IhY",
25536
+ message: "The referenced logo is no longer available. Pick a new one or remove the reference."
25537
+ }) })]
24568
25538
  }), /* @__PURE__ */ jsxs("div", {
24569
25539
  className: "flex gap-2",
24570
25540
  children: [/* @__PURE__ */ jsx(Button, {
@@ -24603,15 +25573,25 @@ function GeneralSettings() {
24603
25573
  /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Label, { children: _t({
24604
25574
  id: "+Qaboy",
24605
25575
  message: "Favicon"
24606
- }) }), formData.favicon?.url ? /* @__PURE__ */ jsxs("div", {
25576
+ }) }), formData.favicon?.mediaId ? /* @__PURE__ */ jsxs("div", {
24607
25577
  className: "mt-2 space-y-2",
24608
- children: [/* @__PURE__ */ jsx("img", {
25578
+ children: [formData.favicon.url ? /* @__PURE__ */ jsx("img", {
24609
25579
  src: formData.favicon.url,
24610
25580
  alt: _t({
24611
25581
  id: "+Qaboy",
24612
25582
  message: "Favicon"
24613
25583
  }),
24614
25584
  className: "h-8 w-8 rounded border bg-kumo-tint object-contain p-1"
25585
+ }) : /* @__PURE__ */ jsxs("div", {
25586
+ className: "flex min-h-8 items-center gap-2 rounded border border-dashed bg-kumo-tint px-2 py-1 text-xs text-kumo-subtle",
25587
+ role: "status",
25588
+ children: [/* @__PURE__ */ jsx(WarningCircle, {
25589
+ className: "h-3 w-3 flex-shrink-0",
25590
+ "aria-hidden": "true"
25591
+ }), /* @__PURE__ */ jsx("span", { children: _t({
25592
+ id: "z58F7a",
25593
+ message: "Referenced favicon unavailable."
25594
+ }) })]
24615
25595
  }), /* @__PURE__ */ jsxs("div", {
24616
25596
  className: "flex gap-2",
24617
25597
  children: [/* @__PURE__ */ jsx(Button, {
@@ -24722,6 +25702,7 @@ function GeneralSettings() {
24722
25702
  onOpenChange: setLogoPickerOpen,
24723
25703
  onSelect: handleLogoSelect,
24724
25704
  mimeTypeFilter: "image/",
25705
+ localOnly: true,
24725
25706
  title: _t({
24726
25707
  id: "JUZ3aC",
24727
25708
  message: "Select Logo"
@@ -24732,6 +25713,7 @@ function GeneralSettings() {
24732
25713
  onOpenChange: setFaviconPickerOpen,
24733
25714
  onSelect: handleFaviconSelect,
24734
25715
  mimeTypeFilter: "image/",
25716
+ localOnly: true,
24735
25717
  title: _t({
24736
25718
  id: "LmzxcB",
24737
25719
  message: "Select Favicon"
@@ -25237,6 +26219,7 @@ function SeoSettings() {
25237
26219
  });
25238
26220
  const [formData, setFormData] = React$1.useState({});
25239
26221
  const [saveStatus, setSaveStatus] = React$1.useState(null);
26222
+ const [ogImagePickerOpen, setOgImagePickerOpen] = React$1.useState(false);
25240
26223
  React$1.useEffect(() => {
25241
26224
  if (settings) setFormData(settings);
25242
26225
  }, [settings]);
@@ -25281,6 +26264,29 @@ function SeoSettings() {
25281
26264
  }
25282
26265
  }));
25283
26266
  };
26267
+ const handleDefaultOgImageSelect = (media) => {
26268
+ setFormData((prev) => ({
26269
+ ...prev,
26270
+ seo: {
26271
+ ...prev.seo,
26272
+ defaultOgImage: {
26273
+ mediaId: media.id,
26274
+ alt: media.alt || "",
26275
+ url: media.url
26276
+ }
26277
+ }
26278
+ }));
26279
+ setOgImagePickerOpen(false);
26280
+ };
26281
+ const handleDefaultOgImageRemove = () => {
26282
+ setFormData((prev) => ({
26283
+ ...prev,
26284
+ seo: {
26285
+ ...prev.seo,
26286
+ defaultOgImage: void 0
26287
+ }
26288
+ }));
26289
+ };
25284
26290
  if (isLoading) return /* @__PURE__ */ jsxs("div", {
25285
26291
  className: "space-y-6",
25286
26292
  children: [/* @__PURE__ */ jsxs("div", {
@@ -25363,6 +26369,73 @@ function SeoSettings() {
25363
26369
  message: "Character between page title and site name (e.g., \"My Post | My Site\")"
25364
26370
  })
25365
26371
  }),
26372
+ /* @__PURE__ */ jsxs("div", { children: [
26373
+ /* @__PURE__ */ jsx(Label, { children: _t({
26374
+ id: "rqCo6p",
26375
+ message: "Default Social Image"
26376
+ }) }),
26377
+ /* @__PURE__ */ jsx("p", {
26378
+ className: "mt-1 text-sm text-kumo-subtle",
26379
+ children: _t({
26380
+ id: "YkLTCK",
26381
+ message: "Used as the fallback Open Graph image when a page has none. Recommended size: 1200×630."
26382
+ })
26383
+ }),
26384
+ formData.seo?.defaultOgImage?.mediaId ? /* @__PURE__ */ jsxs("div", {
26385
+ className: "mt-2 space-y-2",
26386
+ children: [formData.seo.defaultOgImage.url ? /* @__PURE__ */ jsx("img", {
26387
+ src: formData.seo.defaultOgImage.url,
26388
+ alt: formData.seo.defaultOgImage.alt || _t({
26389
+ id: "GOoV+w",
26390
+ message: "Default social image"
26391
+ }),
26392
+ className: "h-32 rounded border bg-kumo-tint object-contain p-2"
26393
+ }) : /* @__PURE__ */ jsxs("div", {
26394
+ className: "flex min-h-32 items-center gap-2 rounded border border-dashed bg-kumo-tint px-3 py-2 text-sm text-kumo-subtle",
26395
+ role: "status",
26396
+ children: [/* @__PURE__ */ jsx(WarningCircle, {
26397
+ className: "h-4 w-4 flex-shrink-0",
26398
+ "aria-hidden": "true"
26399
+ }), /* @__PURE__ */ jsx("span", { children: _t({
26400
+ id: "xPSZCT",
26401
+ message: "The referenced image is no longer available. Pick a new one or remove the reference."
26402
+ }) })]
26403
+ }), /* @__PURE__ */ jsxs("div", {
26404
+ className: "flex gap-2",
26405
+ children: [/* @__PURE__ */ jsx(Button, {
26406
+ type: "button",
26407
+ variant: "outline",
26408
+ size: "sm",
26409
+ icon: /* @__PURE__ */ jsx(Upload, {}),
26410
+ onClick: () => setOgImagePickerOpen(true),
26411
+ children: _t({
26412
+ id: "mwX/w0",
26413
+ message: "Change Image"
26414
+ })
26415
+ }), /* @__PURE__ */ jsx(Button, {
26416
+ type: "button",
26417
+ variant: "outline",
26418
+ size: "sm",
26419
+ icon: /* @__PURE__ */ jsx(X, {}),
26420
+ onClick: handleDefaultOgImageRemove,
26421
+ children: _t({
26422
+ id: "t/YqKh",
26423
+ message: "Remove"
26424
+ })
26425
+ })]
26426
+ })]
26427
+ }) : /* @__PURE__ */ jsx(Button, {
26428
+ type: "button",
26429
+ variant: "outline",
26430
+ icon: /* @__PURE__ */ jsx(Upload, {}),
26431
+ onClick: () => setOgImagePickerOpen(true),
26432
+ className: "mt-2",
26433
+ children: _t({
26434
+ id: "bAiEJP",
26435
+ message: "Select Image"
26436
+ })
26437
+ })
26438
+ ] }),
25366
26439
  /* @__PURE__ */ jsx(Input, {
25367
26440
  label: _t({
25368
26441
  id: "uP6I1q",
@@ -25414,6 +26487,22 @@ function SeoSettings() {
25414
26487
  })
25415
26488
  })
25416
26489
  })]
26490
+ }),
26491
+ /* @__PURE__ */ jsx(MediaPickerModal, {
26492
+ open: ogImagePickerOpen,
26493
+ onOpenChange: setOgImagePickerOpen,
26494
+ onSelect: handleDefaultOgImageSelect,
26495
+ mimeTypeFilters: [
26496
+ "image/jpeg",
26497
+ "image/png",
26498
+ "image/webp",
26499
+ "image/gif"
26500
+ ],
26501
+ localOnly: true,
26502
+ title: _t({
26503
+ id: "lxsmBp",
26504
+ message: "Select Default Social Image"
26505
+ })
25417
26506
  })
25418
26507
  ]
25419
26508
  });
@@ -26790,7 +27879,16 @@ function SidebarNav({ manifest }) {
26790
27879
  minRole: ROLE_ADMIN$1
26791
27880
  }
26792
27881
  ];
26793
- if (manifest.marketplace) adminItems.push({
27882
+ if (manifest.registry) adminItems.push({
27883
+ to: "/plugins/marketplace",
27884
+ label: _t({
27885
+ id: "kVxO/O",
27886
+ message: "Registry"
27887
+ }),
27888
+ icon: Storefront,
27889
+ minRole: ROLE_ADMIN$1
27890
+ });
27891
+ else if (manifest.marketplace) adminItems.push({
26794
27892
  to: "/plugins/marketplace",
26795
27893
  label: _t({
26796
27894
  id: "Zt5PUS",
@@ -26798,7 +27896,8 @@ function SidebarNav({ manifest }) {
26798
27896
  }),
26799
27897
  icon: Storefront,
26800
27898
  minRole: ROLE_ADMIN$1
26801
- }, {
27899
+ });
27900
+ if (manifest.marketplace) adminItems.push({
26802
27901
  to: "/themes/marketplace",
26803
27902
  label: _t({
26804
27903
  id: "fJ3EbT",
@@ -34839,6 +35938,7 @@ function ContentListPage() {
34839
35938
  const items = React$1.useMemo(() => {
34840
35939
  return data?.pages.flatMap((page) => page.items) || [];
34841
35940
  }, [data]);
35941
+ const total = data?.pages[0]?.total ?? items.length;
34842
35942
  if (!manifest) return /* @__PURE__ */ jsx(LoadingScreen, {});
34843
35943
  const collectionConfig = manifest.collections[collection];
34844
35944
  if (!collectionConfig) return /* @__PURE__ */ jsx(NotFoundPage, { message: `Collection "${collection}" not found` });
@@ -34869,7 +35969,8 @@ function ContentListPage() {
34869
35969
  onLocaleChange: handleLocaleChange,
34870
35970
  urlPattern: collectionConfig.urlPattern,
34871
35971
  sort,
34872
- onSortChange: setSort
35972
+ onSortChange: setSort,
35973
+ total
34873
35974
  });
34874
35975
  }
34875
35976
  const contentNewRoute = createRoute({
@@ -35460,9 +36561,14 @@ const mediaRoute = createRoute({
35460
36561
  });
35461
36562
  function MediaPage() {
35462
36563
  const queryClient = useQueryClient();
35463
- const { data, isLoading, error } = useQuery({
36564
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error } = useInfiniteQuery({
35464
36565
  queryKey: ["media"],
35465
- queryFn: () => fetchMediaList()
36566
+ queryFn: ({ pageParam }) => fetchMediaList({
36567
+ cursor: pageParam,
36568
+ limit: 100
36569
+ }),
36570
+ initialPageParam: void 0,
36571
+ getNextPageParam: (lastPage) => lastPage.nextCursor
35466
36572
  });
35467
36573
  const uploadMutation = useMutation({
35468
36574
  mutationFn: (file) => uploadMedia(file),
@@ -35476,10 +36582,15 @@ function MediaPage() {
35476
36582
  queryClient.invalidateQueries({ queryKey: ["media"] });
35477
36583
  }
35478
36584
  });
36585
+ const items = React$1.useMemo(() => {
36586
+ return data?.pages.flatMap((page) => page.items) || [];
36587
+ }, [data]);
35479
36588
  if (error) return /* @__PURE__ */ jsx(ErrorScreen, { error: error.message });
35480
36589
  return /* @__PURE__ */ jsx(MediaLibrary, {
35481
- items: data?.items || [],
35482
- isLoading,
36590
+ items,
36591
+ isLoading: isLoading || isFetchingNextPage,
36592
+ hasMore: !!hasNextPage,
36593
+ onLoadMore: () => void fetchNextPage(),
35483
36594
  onUpload: (file) => uploadMutation.mutate(file),
35484
36595
  onDelete: (id) => deleteMutation.mutate(id)
35485
36596
  });
@@ -35716,6 +36827,10 @@ const marketplaceBrowseRoute = createRoute({
35716
36827
  component: MarketplaceBrowsePage
35717
36828
  });
35718
36829
  function MarketplaceBrowsePage() {
36830
+ const { data: manifest } = useQuery({
36831
+ queryKey: ["manifest"],
36832
+ queryFn: fetchManifest
36833
+ });
35719
36834
  const { data: plugins } = useQuery({
35720
36835
  queryKey: ["plugins"],
35721
36836
  queryFn: async () => {
@@ -35723,10 +36838,18 @@ function MarketplaceBrowsePage() {
35723
36838
  return fetchPlugins();
35724
36839
  }
35725
36840
  });
35726
- return /* @__PURE__ */ jsx(MarketplaceBrowse, { installedPluginIds: React$1.useMemo(() => {
36841
+ const installedIds = React$1.useMemo(() => {
35727
36842
  if (!plugins) return /* @__PURE__ */ new Set();
35728
36843
  return new Set(plugins.map((p) => p.id));
35729
- }, [plugins]) });
36844
+ }, [plugins]);
36845
+ if (manifest?.registry) {
36846
+ const installedRegistryUris = new Set((plugins ?? []).filter((p) => p.source === "registry" && p.registryPublisherDid && p.registrySlug).map((p) => `at://${p.registryPublisherDid}/com.emdashcms.experimental.package.profile/${p.registrySlug}`));
36847
+ return /* @__PURE__ */ jsx(RegistryBrowse, {
36848
+ config: manifest.registry,
36849
+ installedRegistryUris
36850
+ });
36851
+ }
36852
+ return /* @__PURE__ */ jsx(MarketplaceBrowse, { installedPluginIds: installedIds });
35730
36853
  }
35731
36854
  const marketplaceDetailRoute = createRoute({
35732
36855
  getParentRoute: () => adminLayoutRoute,
@@ -35735,6 +36858,10 @@ const marketplaceDetailRoute = createRoute({
35735
36858
  });
35736
36859
  function MarketplaceDetailPage() {
35737
36860
  const { pluginId } = useParams({ from: "/_admin/plugins/marketplace/$pluginId" });
36861
+ const { data: manifest } = useQuery({
36862
+ queryKey: ["manifest"],
36863
+ queryFn: fetchManifest
36864
+ });
35738
36865
  const { data: plugins } = useQuery({
35739
36866
  queryKey: ["plugins"],
35740
36867
  queryFn: async () => {
@@ -35742,12 +36869,18 @@ function MarketplaceDetailPage() {
35742
36869
  return fetchPlugins();
35743
36870
  }
35744
36871
  });
36872
+ const installedIds = React$1.useMemo(() => {
36873
+ if (!plugins) return /* @__PURE__ */ new Set();
36874
+ return new Set(plugins.map((p) => p.id));
36875
+ }, [plugins]);
36876
+ const looksLikeRegistryId = pluginId.includes("/");
36877
+ if (manifest?.registry && looksLikeRegistryId) return /* @__PURE__ */ jsx(RegistryPluginDetail, {
36878
+ pluginId,
36879
+ config: manifest.registry
36880
+ });
35745
36881
  return /* @__PURE__ */ jsx(MarketplacePluginDetail, {
35746
36882
  pluginId,
35747
- installedPluginIds: React$1.useMemo(() => {
35748
- if (!plugins) return /* @__PURE__ */ new Set();
35749
- return new Set(plugins.map((p) => p.id));
35750
- }, [plugins])
36883
+ installedPluginIds: installedIds
35751
36884
  });
35752
36885
  }
35753
36886
  const themeMarketplaceBrowseRoute = createRoute({