@emdash-cms/admin 0.14.0 → 0.16.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 (92) hide show
  1. package/dist/{LocaleDirectionProvider-D32XwRVp.js → LocaleDirectionProvider-BNEVyvgW.js} +17 -17
  2. package/dist/{LocaleDirectionProvider-D32XwRVp.js.map → LocaleDirectionProvider-BNEVyvgW.js.map} +1 -1
  3. package/dist/index.d.ts +58 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1940 -736
  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-6iBQvP4l.js +6 -0
  25. package/dist/messages-6iBQvP4l.js.map +1 -0
  26. package/dist/messages-B3qgKgae.js +6 -0
  27. package/dist/messages-B3qgKgae.js.map +1 -0
  28. package/dist/messages-B8kb5Oko.js +6 -0
  29. package/dist/messages-B8kb5Oko.js.map +1 -0
  30. package/dist/messages-BhSzAEGL.js +6 -0
  31. package/dist/messages-BhSzAEGL.js.map +1 -0
  32. package/dist/messages-C0wfBdmD.js +6 -0
  33. package/dist/messages-C0wfBdmD.js.map +1 -0
  34. package/dist/messages-CboPeBM0.js +6 -0
  35. package/dist/messages-CboPeBM0.js.map +1 -0
  36. package/dist/messages-CehVxd9q.js +6 -0
  37. package/dist/messages-CehVxd9q.js.map +1 -0
  38. package/dist/messages-D2R4PbiQ.js +6 -0
  39. package/dist/messages-D2R4PbiQ.js.map +1 -0
  40. package/dist/messages-DHha7f8K.js +6 -0
  41. package/dist/messages-DHha7f8K.js.map +1 -0
  42. package/dist/messages-DIehX9BP.js +6 -0
  43. package/dist/messages-DIehX9BP.js.map +1 -0
  44. package/dist/messages-DKdkwHEw.js +6 -0
  45. package/dist/messages-DKdkwHEw.js.map +1 -0
  46. package/dist/messages-DMJobUj_.js +6 -0
  47. package/dist/messages-DMJobUj_.js.map +1 -0
  48. package/dist/messages-Ddgurz-D.js +6 -0
  49. package/dist/messages-Ddgurz-D.js.map +1 -0
  50. package/dist/messages-LeijTgr3.js +6 -0
  51. package/dist/messages-LeijTgr3.js.map +1 -0
  52. package/dist/messages-S5UPBqIz.js +6 -0
  53. package/dist/messages-S5UPBqIz.js.map +1 -0
  54. package/dist/messages-vQ-B-9ct.js +6 -0
  55. package/dist/messages-vQ-B-9ct.js.map +1 -0
  56. package/dist/{plugins-qrjvPK4g.js → plugins-BZzztFdK.js} +3 -3
  57. package/dist/plugins-BZzztFdK.js.map +1 -0
  58. package/dist/styles.css +2 -2
  59. package/package.json +10 -9
  60. package/dist/messages-35lkat0c.js +0 -6
  61. package/dist/messages-35lkat0c.js.map +0 -1
  62. package/dist/messages-BSSaBU23.js +0 -6
  63. package/dist/messages-BSSaBU23.js.map +0 -1
  64. package/dist/messages-Boy6iMLn.js +0 -6
  65. package/dist/messages-Boy6iMLn.js.map +0 -1
  66. package/dist/messages-BwIV9n3i.js +0 -6
  67. package/dist/messages-BwIV9n3i.js.map +0 -1
  68. package/dist/messages-CGrAqIHP.js +0 -6
  69. package/dist/messages-CGrAqIHP.js.map +0 -1
  70. package/dist/messages-CgLmZ-Xe.js +0 -6
  71. package/dist/messages-CgLmZ-Xe.js.map +0 -1
  72. package/dist/messages-Ckz9OJjP.js +0 -6
  73. package/dist/messages-Ckz9OJjP.js.map +0 -1
  74. package/dist/messages-CrY9gw3p.js +0 -6
  75. package/dist/messages-CrY9gw3p.js.map +0 -1
  76. package/dist/messages-CtrOZaXa.js +0 -6
  77. package/dist/messages-CtrOZaXa.js.map +0 -1
  78. package/dist/messages-CxW3nwfb.js +0 -6
  79. package/dist/messages-CxW3nwfb.js.map +0 -1
  80. package/dist/messages-D7CyjcVC.js +0 -6
  81. package/dist/messages-D7CyjcVC.js.map +0 -1
  82. package/dist/messages-Dhy7Stmm.js +0 -6
  83. package/dist/messages-Dhy7Stmm.js.map +0 -1
  84. package/dist/messages-FyJi9vss.js +0 -6
  85. package/dist/messages-FyJi9vss.js.map +0 -1
  86. package/dist/messages-JMBMiRyI.js +0 -6
  87. package/dist/messages-JMBMiRyI.js.map +0 -1
  88. package/dist/messages-pPTbyAIO.js +0 -6
  89. package/dist/messages-pPTbyAIO.js.map +0 -1
  90. package/dist/messages-zl21KaEb.js +0 -6
  91. package/dist/messages-zl21KaEb.js.map +0 -1
  92. package/dist/plugins-qrjvPK4g.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
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-D32XwRVp.js";
1
+ import { c as fetchAuthMode, d as throwResponseError, i as fetchPlugins, l as fetchManifest, n as enablePlugin, o as API_BASE, r as fetchPlugin, s as apiFetch, t as disablePlugin, u as parseApiResponse } from "./plugins-BZzztFdK.js";
2
+ import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-BNEVyvgW.js";
3
3
  import "./locales/index.js";
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";
4
+ import { Autocomplete, Badge, Button, Checkbox, CommandPalette, Dialog, Input, InputArea, Label, LinkButton, Loader, Popover, Select, Sidebar as KumoSidebar, Switch, Tabs, Toast, Toasty, Tooltip, buttonVariants, useSidebar } from "@cloudflare/kumo";
5
5
  import { i18n } from "@lingui/core";
6
6
  import { I18nProvider, Trans, useLingui } from "@lingui/react";
7
7
  import { QueryClient, QueryClientProvider, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -27,17 +27,19 @@ import { TableHeader } from "@tiptap/extension-table-header";
27
27
  import { TableRow } from "@tiptap/extension-table-row";
28
28
  import TextAlign from "@tiptap/extension-text-align";
29
29
  import Typography from "@tiptap/extension-typography";
30
- import { EditorContent, Node as Node$2, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes as mergeAttributes$1, useEditor, useEditorState } from "@tiptap/react";
30
+ import { EditorContent, Node as Node$2, NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes as mergeAttributes$1, useEditor, useEditorState } from "@tiptap/react";
31
31
  import { BubbleMenu } from "@tiptap/react/menus";
32
32
  import StarterKit from "@tiptap/starter-kit";
33
33
  import { Plugin, PluginKey } from "@tiptap/pm/state";
34
34
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
35
35
  import { createPortal } from "react-dom";
36
+ import CodeBlock$1 from "@tiptap/extension-code-block";
36
37
  import { DragHandle } from "@tiptap/extension-drag-handle-react";
37
38
  import { isAllowedUri } from "@tiptap/extension-link";
38
39
  import { BlockRenderer } from "@emdash-cms/blocks";
39
40
  import DOMPurify from "dompurify";
40
41
  import { Marked, Renderer } from "marked";
42
+ import { checkEnvCompatibility, hostEnvFromVersions } from "@emdash-cms/registry-client/env";
41
43
  import { useHotkeys } from "react-hotkeys-hook";
42
44
 
43
45
  //#region src/components/ThemeProvider.tsx
@@ -1956,6 +1958,7 @@ async function fetchBylines(options) {
1956
1958
  if (options?.search) params.set("search", options.search);
1957
1959
  if (options?.isGuest !== void 0) params.set("isGuest", String(options.isGuest));
1958
1960
  if (options?.userId) params.set("userId", options.userId);
1961
+ if (options?.locale) params.set("locale", options.locale);
1959
1962
  if (options?.cursor) params.set("cursor", options.cursor);
1960
1963
  if (options?.limit) params.set("limit", String(options.limit));
1961
1964
  return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines${params.toString() ? `?${params}` : ""}`), "Failed to fetch bylines");
@@ -1984,6 +1987,26 @@ async function deleteByline(id) {
1984
1987
  message: "Failed to delete byline"
1985
1988
  }));
1986
1989
  }
1990
+ /**
1991
+ * Fetch every translation of a byline (siblings sharing the same
1992
+ * translation_group).
1993
+ */
1994
+ async function fetchBylineTranslations(id) {
1995
+ return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines/${id}/translations`), "Failed to fetch byline translations");
1996
+ }
1997
+ /**
1998
+ * Create a new locale variant of a byline. The new row joins the source's
1999
+ * `translation_group`. Body defaults — slug, display name, avatar, website —
2000
+ * inherit from the source when omitted, so editors only have to fill in the
2001
+ * localized bio (and optionally a localized display name).
2002
+ */
2003
+ async function createBylineTranslation(id, input) {
2004
+ return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines/${id}/translations`, {
2005
+ method: "POST",
2006
+ headers: { "Content-Type": "application/json" },
2007
+ body: JSON.stringify(input)
2008
+ }), "Failed to create byline translation");
2009
+ }
1987
2010
 
1988
2011
  //#endregion
1989
2012
  //#region src/lib/api/menus.ts
@@ -5525,6 +5548,392 @@ function Suggestion({ pluginKey = SuggestionPluginKey, editor, char = "@", allow
5525
5548
  }
5526
5549
  var index_default = Suggestion;
5527
5550
 
5551
+ //#endregion
5552
+ //#region src/components/editor/codeBlockLanguages.ts
5553
+ const CODE_BLOCK_LANGUAGES = [
5554
+ {
5555
+ id: "plaintext",
5556
+ label: "Plain text",
5557
+ aliases: [
5558
+ "text",
5559
+ "plain",
5560
+ "txt"
5561
+ ]
5562
+ },
5563
+ {
5564
+ id: "astro",
5565
+ label: "Astro"
5566
+ },
5567
+ {
5568
+ id: "bash",
5569
+ label: "Bash",
5570
+ aliases: [
5571
+ "sh",
5572
+ "shell",
5573
+ "zsh"
5574
+ ]
5575
+ },
5576
+ {
5577
+ id: "c",
5578
+ label: "C"
5579
+ },
5580
+ {
5581
+ id: "cpp",
5582
+ label: "C++",
5583
+ aliases: ["c++"]
5584
+ },
5585
+ {
5586
+ id: "csharp",
5587
+ label: "C#",
5588
+ aliases: ["cs", "c#"]
5589
+ },
5590
+ {
5591
+ id: "css",
5592
+ label: "CSS"
5593
+ },
5594
+ {
5595
+ id: "diff",
5596
+ label: "Diff",
5597
+ aliases: ["patch"]
5598
+ },
5599
+ {
5600
+ id: "dockerfile",
5601
+ label: "Dockerfile",
5602
+ aliases: ["docker"]
5603
+ },
5604
+ {
5605
+ id: "go",
5606
+ label: "Go",
5607
+ aliases: ["golang"]
5608
+ },
5609
+ {
5610
+ id: "graphql",
5611
+ label: "GraphQL",
5612
+ aliases: ["gql"]
5613
+ },
5614
+ {
5615
+ id: "html",
5616
+ label: "HTML"
5617
+ },
5618
+ {
5619
+ id: "java",
5620
+ label: "Java"
5621
+ },
5622
+ {
5623
+ id: "javascript",
5624
+ label: "JavaScript",
5625
+ aliases: ["js"]
5626
+ },
5627
+ {
5628
+ id: "json",
5629
+ label: "JSON"
5630
+ },
5631
+ {
5632
+ id: "jsx",
5633
+ label: "JSX"
5634
+ },
5635
+ {
5636
+ id: "kotlin",
5637
+ label: "Kotlin",
5638
+ aliases: ["kt"]
5639
+ },
5640
+ {
5641
+ id: "markdown",
5642
+ label: "Markdown",
5643
+ aliases: ["md"]
5644
+ },
5645
+ {
5646
+ id: "mdx",
5647
+ label: "MDX"
5648
+ },
5649
+ {
5650
+ id: "php",
5651
+ label: "PHP"
5652
+ },
5653
+ {
5654
+ id: "python",
5655
+ label: "Python",
5656
+ aliases: ["py"]
5657
+ },
5658
+ {
5659
+ id: "ruby",
5660
+ label: "Ruby",
5661
+ aliases: ["rb"]
5662
+ },
5663
+ {
5664
+ id: "rust",
5665
+ label: "Rust",
5666
+ aliases: ["rs"]
5667
+ },
5668
+ {
5669
+ id: "scss",
5670
+ label: "SCSS",
5671
+ aliases: ["sass"]
5672
+ },
5673
+ {
5674
+ id: "sql",
5675
+ label: "SQL"
5676
+ },
5677
+ {
5678
+ id: "svelte",
5679
+ label: "Svelte"
5680
+ },
5681
+ {
5682
+ id: "swift",
5683
+ label: "Swift"
5684
+ },
5685
+ {
5686
+ id: "toml",
5687
+ label: "TOML"
5688
+ },
5689
+ {
5690
+ id: "tsx",
5691
+ label: "TSX"
5692
+ },
5693
+ {
5694
+ id: "typescript",
5695
+ label: "TypeScript",
5696
+ aliases: ["ts"]
5697
+ },
5698
+ {
5699
+ id: "vue",
5700
+ label: "Vue"
5701
+ },
5702
+ {
5703
+ id: "xml",
5704
+ label: "XML"
5705
+ },
5706
+ {
5707
+ id: "yaml",
5708
+ label: "YAML",
5709
+ aliases: ["yml"]
5710
+ }
5711
+ ];
5712
+ /**
5713
+ * Look up a language by id or alias. Case-insensitive.
5714
+ * Returns the canonical entry, or `null` if not found in the curated list.
5715
+ */
5716
+ function findLanguage(value) {
5717
+ if (!value) return null;
5718
+ const needle = value.trim().toLowerCase();
5719
+ if (!needle) return null;
5720
+ for (const lang of CODE_BLOCK_LANGUAGES) {
5721
+ if (lang.id === needle) return lang;
5722
+ if (lang.aliases?.includes(needle)) return lang;
5723
+ }
5724
+ return null;
5725
+ }
5726
+ /**
5727
+ * Normalize a user-entered language string to a canonical id where possible.
5728
+ * Unknown inputs are sanitized to a single safe class token: lowercased,
5729
+ * trimmed, with any character outside `[a-z0-9_-]` (including whitespace,
5730
+ * dots, slashes, etc.) collapsed to `-`. This keeps the stored value safe
5731
+ * to interpolate into a `language-{id}` CSS class without splitting on
5732
+ * whitespace.
5733
+ *
5734
+ * Examples:
5735
+ * normalizeLanguage("TypeScript") -> "typescript" (canonical id)
5736
+ * normalizeLanguage("ts") -> "typescript" (alias)
5737
+ * normalizeLanguage("Objective C") -> "objective-c" (sanitized)
5738
+ * normalizeLanguage("F#") -> "f-" (sanitized)
5739
+ * normalizeLanguage("") -> undefined
5740
+ */
5741
+ const DISALLOWED_CHARS_RE = /[^a-z0-9_-]+/g;
5742
+ const LEADING_TRAILING_HYPHENS_RE = /^-+|-+$/g;
5743
+ function normalizeLanguage(value) {
5744
+ if (!value) return void 0;
5745
+ const trimmed = value.trim();
5746
+ if (!trimmed) return void 0;
5747
+ const match = findLanguage(trimmed);
5748
+ if (match) return match.id;
5749
+ return trimmed.toLowerCase().replace(DISALLOWED_CHARS_RE, "-").replace(LEADING_TRAILING_HYPHENS_RE, "") || void 0;
5750
+ }
5751
+ /**
5752
+ * Human-readable label for a stored language id. Falls back to the id itself
5753
+ * for unknown values so the editor never shows "undefined".
5754
+ */
5755
+ function languageLabel(value) {
5756
+ if (!value) return "Plain text";
5757
+ const match = findLanguage(value);
5758
+ if (match) return match.label;
5759
+ return value;
5760
+ }
5761
+
5762
+ //#endregion
5763
+ //#region src/components/editor/CodeBlockNode.tsx
5764
+ /**
5765
+ * Code block node with language picker.
5766
+ *
5767
+ * Wraps the base `@tiptap/extension-code-block` with a React node view that
5768
+ * overlays a small language chip in the top-right corner. Clicking the chip
5769
+ * opens a popover with a Kumo Autocomplete: a free-form text input plus a
5770
+ * filtered list of curated language suggestions. The value is persisted on
5771
+ * the node's `language` attribute and round-trips through Portable Text as
5772
+ * `block.language`.
5773
+ *
5774
+ * The picker accepts arbitrary strings (not restricted to the curated list)
5775
+ * so that less common languages can still be used. Free-form input is
5776
+ * sanitized to a single safe CSS class token via `normalizeLanguage` so the
5777
+ * frontend's `language-{id}` class stays well-formed.
5778
+ */
5779
+ const LANGUAGE_ITEMS = CODE_BLOCK_LANGUAGES.map((l) => l.label);
5780
+ function filterLanguages(item, query) {
5781
+ if (!query) return true;
5782
+ const needle = query.toLowerCase();
5783
+ const lang = CODE_BLOCK_LANGUAGES.find((l) => l.label === item);
5784
+ if (!lang) return false;
5785
+ if (lang.label.toLowerCase().includes(needle)) return true;
5786
+ if (lang.id.toLowerCase().includes(needle)) return true;
5787
+ return lang.aliases?.some((alias) => alias.toLowerCase().includes(needle)) ?? false;
5788
+ }
5789
+ function CodeBlockNodeView({ node, updateAttributes, selected }) {
5790
+ const { _: _t } = useLingui();
5791
+ const [isEditing, setIsEditing] = React$1.useState(false);
5792
+ const storedLanguage = typeof node.attrs.language === "string" ? node.attrs.language : "";
5793
+ const [draft, setDraft] = React$1.useState(() => languageLabel(storedLanguage));
5794
+ const popoverRef = React$1.useRef(null);
5795
+ React$1.useEffect(() => {
5796
+ if (!isEditing) setDraft(languageLabel(storedLanguage));
5797
+ }, [storedLanguage, isEditing]);
5798
+ const openPicker = React$1.useCallback(() => {
5799
+ setDraft(storedLanguage ? languageLabel(storedLanguage) : "");
5800
+ setIsEditing(true);
5801
+ }, [storedLanguage]);
5802
+ const closePicker = React$1.useCallback(() => {
5803
+ setIsEditing(false);
5804
+ setDraft(languageLabel(storedLanguage));
5805
+ }, [storedLanguage]);
5806
+ const commit = React$1.useCallback((value) => {
5807
+ updateAttributes({ language: normalizeLanguage(value ?? draft) ?? null });
5808
+ setIsEditing(false);
5809
+ }, [draft, updateAttributes]);
5810
+ const handleKeyDown = (e) => {
5811
+ if (e.key === "Enter") {
5812
+ e.preventDefault();
5813
+ commit();
5814
+ } else if (e.key === "Escape") {
5815
+ e.preventDefault();
5816
+ closePicker();
5817
+ }
5818
+ };
5819
+ React$1.useEffect(() => {
5820
+ if (!isEditing) return void 0;
5821
+ const onMouseDown = (event) => {
5822
+ const target = event.target instanceof Node ? event.target : null;
5823
+ if (popoverRef.current && target && !popoverRef.current.contains(target)) closePicker();
5824
+ };
5825
+ document.addEventListener("mousedown", onMouseDown);
5826
+ return () => document.removeEventListener("mousedown", onMouseDown);
5827
+ }, [isEditing, closePicker]);
5828
+ const label = languageLabel(storedLanguage);
5829
+ const chipPersistent = isEditing || Boolean(storedLanguage) || selected;
5830
+ return /* @__PURE__ */ jsxs(NodeViewWrapper, {
5831
+ className: "group relative my-4",
5832
+ "data-language": storedLanguage || void 0,
5833
+ children: [/* @__PURE__ */ jsx("pre", {
5834
+ className: "emdash-code-block",
5835
+ children: /* @__PURE__ */ jsx(NodeViewContent, { as: "code" })
5836
+ }), /* @__PURE__ */ jsx("div", {
5837
+ className: "absolute end-2 top-2 select-none",
5838
+ contentEditable: false,
5839
+ children: isEditing ? /* @__PURE__ */ jsxs("div", {
5840
+ ref: popoverRef,
5841
+ className: "flex items-center gap-1 rounded-md border bg-kumo-overlay p-1 shadow-lg",
5842
+ onKeyDown: handleKeyDown,
5843
+ children: [
5844
+ /* @__PURE__ */ jsxs(Autocomplete, {
5845
+ items: LANGUAGE_ITEMS,
5846
+ value: draft,
5847
+ onValueChange: (next) => setDraft(next),
5848
+ filter: filterLanguages,
5849
+ children: [/* @__PURE__ */ jsx(Autocomplete.InputGroup, {
5850
+ size: "sm",
5851
+ placeholder: _t({
5852
+ id: "vXIe7J",
5853
+ message: "Language"
5854
+ })
5855
+ }), /* @__PURE__ */ jsxs(Autocomplete.Content, {
5856
+ sideOffset: 4,
5857
+ children: [/* @__PURE__ */ jsx(Autocomplete.List, { children: (item) => /* @__PURE__ */ jsx(Autocomplete.Item, {
5858
+ value: item,
5859
+ children: item
5860
+ }, item) }), /* @__PURE__ */ jsx(Autocomplete.Empty, { children: _t({
5861
+ id: "4TVVsS",
5862
+ message: "No matches"
5863
+ }) })]
5864
+ })]
5865
+ }),
5866
+ /* @__PURE__ */ jsx(Button, {
5867
+ type: "button",
5868
+ variant: "ghost",
5869
+ shape: "square",
5870
+ className: "h-7 w-7",
5871
+ onMouseDown: (e) => e.preventDefault(),
5872
+ onClick: () => commit(),
5873
+ title: _t({
5874
+ id: "Ek42QX",
5875
+ message: "Apply language"
5876
+ }),
5877
+ "aria-label": _t({
5878
+ id: "Ek42QX",
5879
+ message: "Apply language"
5880
+ }),
5881
+ children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
5882
+ }),
5883
+ /* @__PURE__ */ jsx(Button, {
5884
+ type: "button",
5885
+ variant: "ghost",
5886
+ shape: "square",
5887
+ className: "h-7 w-7",
5888
+ onMouseDown: (e) => e.preventDefault(),
5889
+ onClick: closePicker,
5890
+ title: _t({
5891
+ id: "dEgA5A",
5892
+ message: "Cancel"
5893
+ }),
5894
+ "aria-label": _t({
5895
+ id: "dEgA5A",
5896
+ message: "Cancel"
5897
+ }),
5898
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
5899
+ })
5900
+ ]
5901
+ }) : /* @__PURE__ */ jsx("button", {
5902
+ type: "button",
5903
+ tabIndex: chipPersistent ? 0 : -1,
5904
+ onMouseDown: (e) => e.preventDefault(),
5905
+ onClick: openPicker,
5906
+ className: "rounded-md border bg-kumo-overlay/90 px-2 py-1 text-xs text-kumo-subtle opacity-0 transition-opacity hover:text-kumo-strong focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-kumo-brand group-hover:opacity-100 data-[persistent=true]:opacity-100",
5907
+ "data-persistent": chipPersistent ? "true" : "false",
5908
+ title: _t({
5909
+ id: "9cbOH7",
5910
+ message: "Set language"
5911
+ }),
5912
+ "aria-label": _t({
5913
+ id: "q/A3pF",
5914
+ message: "Set language (current: {label})",
5915
+ values: { label }
5916
+ }),
5917
+ "aria-hidden": chipPersistent ? void 0 : true,
5918
+ children: storedLanguage ? label : _t({
5919
+ id: "9cbOH7",
5920
+ message: "Set language"
5921
+ })
5922
+ })
5923
+ })]
5924
+ });
5925
+ }
5926
+ /**
5927
+ * TipTap extension: code block with an inline language picker node view.
5928
+ *
5929
+ * Drop-in replacement for StarterKit's default `codeBlock`. Configure
5930
+ * `StarterKit.configure({ codeBlock: false })` and add this extension to
5931
+ * the editor's extensions array.
5932
+ */
5933
+ const CodeBlockExtension = CodeBlock$1.extend({ addNodeView() {
5934
+ return ReactNodeViewRenderer(CodeBlockNodeView);
5935
+ } });
5936
+
5528
5937
  //#endregion
5529
5938
  //#region src/components/editor/BlockMenu.tsx
5530
5939
  const blockTransforms = [
@@ -7549,19 +7958,17 @@ function PluginBlockModal({ block, initialValues, onClose, onInsert }) {
7549
7958
  size: dialogSize,
7550
7959
  children: [/* @__PURE__ */ jsxs("div", {
7551
7960
  className: "flex items-start justify-between gap-4 mb-4",
7552
- children: [/* @__PURE__ */ jsxs(Dialog.Title, {
7961
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
7553
7962
  className: "text-lg font-semibold leading-none tracking-tight",
7554
- children: [
7555
- isEditing ? _t2({
7556
- id: "ePK91l",
7557
- message: "Edit"
7558
- }) : _t2({
7559
- id: "S8Yqbl",
7560
- message: "Insert"
7561
- }),
7562
- " ",
7563
- block?.label || ""
7564
- ]
7963
+ children: isEditing ? _t2({
7964
+ id: "N6j2JH",
7965
+ message: "Edit {0}",
7966
+ values: { 0: block?.label || "" }
7967
+ }) : _t2({
7968
+ id: "yQNfsE",
7969
+ message: "Insert {0}",
7970
+ values: { 0: block?.label || "" }
7971
+ })
7565
7972
  }), /* @__PURE__ */ jsx(Dialog.Close, {
7566
7973
  "aria-label": _t2({
7567
7974
  id: "yz7wBu",
@@ -8153,6 +8560,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8153
8560
  color: "#3b82f6",
8154
8561
  width: 2
8155
8562
  },
8563
+ codeBlock: false,
8156
8564
  link: {
8157
8565
  openOnClick: false,
8158
8566
  enableClickSelection: true,
@@ -8160,6 +8568,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8160
8568
  },
8161
8569
  underline: {}
8162
8570
  }),
8571
+ CodeBlockExtension,
8163
8572
  ImageExtension,
8164
8573
  MarkdownLinkExtension,
8165
8574
  PluginBlockExtension,
@@ -8262,8 +8671,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8262
8671
  if (!editor || !pluginBlockModal) return;
8263
8672
  const { id, ...data } = values;
8264
8673
  const editPos = editingBlockPosRef.current;
8265
- if (editPos !== null) {
8266
- const { tr } = editor.state;
8674
+ if (editPos !== null) editor.chain().command(({ tr }) => {
8267
8675
  const node = tr.doc.nodeAt(editPos);
8268
8676
  if (node?.type.name === "pluginBlock") {
8269
8677
  tr.setNodeMarkup(editPos, void 0, {
@@ -8271,9 +8679,11 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8271
8679
  id: typeof id === "string" ? id : node.attrs.id,
8272
8680
  data
8273
8681
  });
8274
- editor.view.dispatch(tr);
8682
+ return true;
8275
8683
  }
8276
- } else editor.chain().focus().insertContent({
8684
+ return false;
8685
+ }).run();
8686
+ else editor.chain().focus().insertContent({
8277
8687
  type: "pluginBlock",
8278
8688
  attrs: {
8279
8689
  blockType: pluginBlockModal.type,
@@ -9908,6 +10318,7 @@ function termExactMatches(term, input) {
9908
10318
  * - Checkbox tree for hierarchical taxonomies (categories)
9909
10319
  * - Tag input for flat taxonomies (tags)
9910
10320
  */
10321
+ const EMPTY_TERMS = [];
9911
10322
  /**
9912
10323
  * Fetch taxonomy definitions
9913
10324
  */
@@ -9975,11 +10386,13 @@ function CategoryCheckboxTree({ term, level = 0, selectedIds, onToggle }) {
9975
10386
  function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, label }) {
9976
10387
  const { _: _t } = useLingui();
9977
10388
  const [input, setInput] = React$1.useState("");
10389
+ const [isOpen, setIsOpen] = React$1.useState(false);
9978
10390
  const selectedTerms = terms.filter((term) => selectedIds.has(term.id));
9979
10391
  const trimmedInput = input.trim();
9980
10392
  const suggestions = React$1.useMemo(() => {
9981
- if (!trimmedInput) return [];
9982
- return terms.filter((term) => !selectedIds.has(term.id) && termMatches(term, trimmedInput)).slice(0, 5);
10393
+ const availableTerms = terms.filter((term) => !selectedIds.has(term.id));
10394
+ if (!trimmedInput) return availableTerms.slice(0, 5);
10395
+ return availableTerms.filter((term) => termMatches(term, trimmedInput)).slice(0, 5);
9983
10396
  }, [
9984
10397
  trimmedInput,
9985
10398
  terms,
@@ -9993,11 +10406,18 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
9993
10406
  const handleSelect = (term) => {
9994
10407
  onAdd(term.id);
9995
10408
  setInput("");
10409
+ setIsOpen(false);
9996
10410
  };
9997
10411
  const handleCreate = () => {
9998
10412
  if (!trimmedInput || isCreating) return;
9999
10413
  onCreate(trimmedInput);
10000
10414
  setInput("");
10415
+ setIsOpen(false);
10416
+ };
10417
+ const handleBlur = (e) => {
10418
+ const nextFocused = e.relatedTarget;
10419
+ if (nextFocused instanceof Node && e.currentTarget.contains(nextFocused)) return;
10420
+ setIsOpen(false);
10001
10421
  };
10002
10422
  const handleKeyDown = (e) => {
10003
10423
  if (e.key === "Enter") {
@@ -10026,9 +10446,14 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
10026
10446
  }, term.id))
10027
10447
  }), /* @__PURE__ */ jsxs("div", {
10028
10448
  className: "relative",
10449
+ onBlur: handleBlur,
10029
10450
  children: [/* @__PURE__ */ jsx(Input, {
10030
10451
  value: input,
10031
- onChange: (e) => setInput(e.target.value),
10452
+ onChange: (e) => {
10453
+ setInput(e.target.value);
10454
+ setIsOpen(true);
10455
+ },
10456
+ onFocus: () => setIsOpen(true),
10032
10457
  onKeyDown: handleKeyDown,
10033
10458
  placeholder: _t({
10034
10459
  id: "SnJVrE",
@@ -10040,15 +10465,17 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
10040
10465
  values: { label }
10041
10466
  }),
10042
10467
  className: "text-sm"
10043
- }), (suggestions.length > 0 || showCreateOption) && /* @__PURE__ */ jsxs("div", {
10468
+ }), isOpen && (suggestions.length > 0 || showCreateOption) && /* @__PURE__ */ jsxs("div", {
10044
10469
  className: "absolute top-full start-0 end-0 mt-1 bg-kumo-overlay border rounded-md shadow-lg z-10",
10045
10470
  children: [suggestions.map((term) => /* @__PURE__ */ jsx("button", {
10046
10471
  type: "button",
10472
+ onMouseDown: (e) => e.preventDefault(),
10047
10473
  onClick: () => handleSelect(term),
10048
10474
  className: "w-full text-start px-3 py-2 text-sm hover:bg-kumo-tint",
10049
10475
  children: term.label
10050
10476
  }, term.id)), showCreateOption && /* @__PURE__ */ jsxs("button", {
10051
10477
  type: "button",
10478
+ onMouseDown: (e) => e.preventDefault(),
10052
10479
  onClick: handleCreate,
10053
10480
  disabled: isCreating,
10054
10481
  className: "w-full text-start px-3 py-2 text-sm hover:bg-kumo-tint text-kumo-accent flex items-center gap-1 border-t",
@@ -10074,11 +10501,11 @@ function TaxonomySection({ taxonomy, collection, entryId, onChange }) {
10074
10501
  const toastManager = Toast.useToastManager();
10075
10502
  const [newCategoryLabel, setNewCategoryLabel] = React$1.useState("");
10076
10503
  const [showCategoryInput, setShowCategoryInput] = React$1.useState(false);
10077
- const { data: terms = [] } = useQuery({
10504
+ const { data: terms = EMPTY_TERMS } = useQuery({
10078
10505
  queryKey: ["taxonomy-terms", taxonomy.name],
10079
10506
  queryFn: () => fetchTerms$1(taxonomy.name)
10080
10507
  });
10081
- const { data: entryTerms = [] } = useQuery({
10508
+ const { data: entryTerms = EMPTY_TERMS } = useQuery({
10082
10509
  queryKey: [
10083
10510
  "entry-terms",
10084
10511
  collection,
@@ -10373,7 +10800,7 @@ function formatScheduledDate(dateStr) {
10373
10800
  /**
10374
10801
  * Content editor with dynamic field rendering
10375
10802
  */
10376
- function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSaving, onSave, onAutosave, isAutosaving, lastAutosaveAt, onPublish, onUnpublish, onDiscardDraft, onSchedule, onUnschedule, isScheduling, supportsDrafts = false, supportsRevisions = false, supportsPreview = false, currentUser, users, onAuthorChange, availableBylines, selectedBylines, onBylinesChange, onQuickCreateByline, onQuickEditByline, onDelete, isDeleting, i18n, translations, onTranslate, pluginBlocks, hasSeo = false, onSeoChange, manifest }) {
10803
+ function ContentEditor({ collection, collectionLabel, item, fields, isNew, entryLocale, isSaving, onSave, onAutosave, isAutosaving, lastAutosaveAt, onPublish, onUnpublish, onDiscardDraft, onSchedule, onUnschedule, isScheduling, supportsDrafts = false, supportsRevisions = false, supportsPreview = false, currentUser, users, onAuthorChange, availableBylines, availableBylinesLoaded, selectedBylines, onBylinesChange, onQuickCreateByline, onQuickEditByline, onDelete, isDeleting, i18n, translations, onTranslate, pluginBlocks, hasSeo = false, onSeoChange, manifest }) {
10377
10804
  const { _: _t } = useLingui();
10378
10805
  const navigate = useNavigate$1();
10379
10806
  const [formData, setFormData] = React$1.useState(item?.data || {});
@@ -10384,6 +10811,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10384
10811
  bylineId: entry.byline.id,
10385
10812
  roleLabel: entry.roleLabel
10386
10813
  })) ?? []);
10814
+ const [bylinesTouched, setBylinesTouched] = React$1.useState(false);
10387
10815
  const [portableTextEditor, setPortableTextEditor] = React$1.useState(null);
10388
10816
  const [blockSidebarPanel, setBlockSidebarPanel] = React$1.useState(null);
10389
10817
  const handleBlockSidebarOpen = React$1.useCallback((panel) => {
@@ -10425,6 +10853,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10425
10853
  bylines: nextBylines
10426
10854
  }));
10427
10855
  pendingAutosaveStateRef.current = null;
10856
+ setBylinesTouched(false);
10428
10857
  }
10429
10858
  const itemDataString = React$1.useMemo(() => item ? JSON.stringify(item.data) : "", [item?.data]);
10430
10859
  React$1.useEffect(() => {
@@ -10446,6 +10875,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10446
10875
  })) ?? []
10447
10876
  }));
10448
10877
  pendingAutosaveStateRef.current = null;
10878
+ setBylinesTouched(false);
10449
10879
  }
10450
10880
  }, [
10451
10881
  item?.updatedAt,
@@ -10455,6 +10885,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10455
10885
  ]);
10456
10886
  const activeBylines = isNew ? selectedBylines ?? [] : internalBylines;
10457
10887
  const handleBylinesChange = React$1.useCallback((next) => {
10888
+ setBylinesTouched(true);
10458
10889
  if (isNew) {
10459
10890
  onBylinesChange?.(next);
10460
10891
  return;
@@ -10497,13 +10928,13 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10497
10928
  if (hasInvalidUrls(formDataRef.current)) return;
10498
10929
  const payload = {
10499
10930
  data: formDataRef.current,
10500
- slug: slugRef.current || void 0,
10501
- bylines: activeBylines
10931
+ slug: slugRef.current || void 0
10502
10932
  };
10933
+ if (bylinesTouched) payload.bylines = activeBylines;
10503
10934
  pendingAutosaveStateRef.current = serializeEditorState({
10504
10935
  data: payload.data,
10505
10936
  slug: payload.slug || "",
10506
- bylines: payload.bylines
10937
+ bylines: activeBylines
10507
10938
  });
10508
10939
  onAutosave(payload);
10509
10940
  }, AUTOSAVE_DELAY);
@@ -10519,6 +10950,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10519
10950
  isSaving,
10520
10951
  isAutosaving,
10521
10952
  activeBylines,
10953
+ bylinesTouched,
10522
10954
  hasInvalidUrls
10523
10955
  ]);
10524
10956
  const handleSubmit = (e) => {
@@ -10528,11 +10960,12 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10528
10960
  clearTimeout(autosaveTimeoutRef.current);
10529
10961
  autosaveTimeoutRef.current = null;
10530
10962
  }
10531
- onSave?.({
10963
+ const payload = {
10532
10964
  data: formData,
10533
- slug: slug || void 0,
10534
- bylines: activeBylines
10535
- });
10965
+ slug: slug || void 0
10966
+ };
10967
+ if (isNew || bylinesTouched) payload.bylines = activeBylines;
10968
+ onSave?.(payload);
10536
10969
  };
10537
10970
  const [isLoadingPreview, setIsLoadingPreview] = React$1.useState(false);
10538
10971
  const urlPattern = manifest?.collections[collection]?.urlPattern;
@@ -11066,9 +11499,12 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
11066
11499
  }), /* @__PURE__ */ jsx(BylineCreditsEditor, {
11067
11500
  credits: activeBylines,
11068
11501
  bylines: availableBylines ?? [],
11502
+ bylinesLoaded: availableBylinesLoaded,
11069
11503
  onChange: handleBylinesChange,
11070
11504
  onQuickCreate: onQuickCreateByline,
11071
- onQuickEdit: onQuickEditByline
11505
+ onQuickEdit: onQuickEditByline,
11506
+ entryLocale: item?.locale ?? entryLocale,
11507
+ i18n
11072
11508
  })]
11073
11509
  }),
11074
11510
  i18n && item && !isNew && /* @__PURE__ */ jsx("div", {
@@ -11725,7 +12161,7 @@ function FileFieldRenderer({ id, label, value, onChange, required, allowedMimeTy
11725
12161
  ]
11726
12162
  });
11727
12163
  }
11728
- function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuickEdit }) {
12164
+ function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuickEdit, entryLocale, i18n, bylinesLoaded = true }) {
11729
12165
  const { _: _t7 } = useLingui();
11730
12166
  const [selectedBylineId, setSelectedBylineId] = React$1.useState("");
11731
12167
  const [quickName, setQuickName] = React$1.useState("");
@@ -11768,6 +12204,27 @@ function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuic
11768
12204
  return /* @__PURE__ */ jsxs("div", {
11769
12205
  className: "space-y-3",
11770
12206
  children: [
12207
+ !!i18n && i18n.locales.length > 1 && bylinesLoaded && bylines.length === 0 && !!entryLocale && /* @__PURE__ */ jsxs("div", {
12208
+ className: "rounded border border-dashed p-3 text-sm space-y-2",
12209
+ children: [/* @__PURE__ */ jsx("p", {
12210
+ className: "text-kumo-subtle",
12211
+ children: _t7({
12212
+ id: "Tmb8Fw",
12213
+ message: "No bylines available in {entryLocale}. Create a variant from the Bylines page before crediting one on this entry.",
12214
+ values: { entryLocale }
12215
+ })
12216
+ }), /* @__PURE__ */ jsx(RouterLinkButton, {
12217
+ to: "/bylines",
12218
+ search: { locale: entryLocale ?? void 0 },
12219
+ variant: "secondary",
12220
+ size: "sm",
12221
+ children: _t7({
12222
+ id: "yXD9HA",
12223
+ message: "Manage bylines in {entryLocale}",
12224
+ values: { entryLocale }
12225
+ })
12226
+ })]
12227
+ }),
11771
12228
  /* @__PURE__ */ jsxs("div", {
11772
12229
  className: "flex gap-2",
11773
12230
  children: [/* @__PURE__ */ jsx(Select, {
@@ -17591,6 +18048,85 @@ function formatInstallCount(count) {
17591
18048
  return String(count);
17592
18049
  }
17593
18050
 
18051
+ //#endregion
18052
+ //#region src/lib/markdown.ts
18053
+ const HTML_ESCAPE_MAP = {
18054
+ "&": "&",
18055
+ "<": "&lt;",
18056
+ ">": "&gt;",
18057
+ "\"": "&quot;",
18058
+ "'": "&#39;"
18059
+ };
18060
+ const HTML_ESCAPE_RE = /[&<>"']/g;
18061
+ function escapeHtml(str) {
18062
+ return str.replace(HTML_ESCAPE_RE, (ch) => HTML_ESCAPE_MAP[ch]);
18063
+ }
18064
+ const renderer = new Renderer();
18065
+ renderer.link = function({ href, tokens }) {
18066
+ const inner = this.parser.parseInline(tokens);
18067
+ if (!SAFE_URL_RE.test(href)) return inner;
18068
+ return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${inner}</a>`;
18069
+ };
18070
+ renderer.image = ({ text }) => escapeHtml(text);
18071
+ renderer.html = () => "";
18072
+ const md = new Marked({
18073
+ renderer,
18074
+ async: false
18075
+ });
18076
+ /** Allowed tags and attributes for DOMPurify — only standard markdown output. */
18077
+ const SANITIZE_CONFIG = {
18078
+ ALLOWED_TAGS: [
18079
+ "h1",
18080
+ "h2",
18081
+ "h3",
18082
+ "h4",
18083
+ "h5",
18084
+ "h6",
18085
+ "p",
18086
+ "a",
18087
+ "ul",
18088
+ "ol",
18089
+ "li",
18090
+ "blockquote",
18091
+ "pre",
18092
+ "code",
18093
+ "em",
18094
+ "strong",
18095
+ "del",
18096
+ "br",
18097
+ "hr",
18098
+ "table",
18099
+ "thead",
18100
+ "tbody",
18101
+ "tr",
18102
+ "th",
18103
+ "td",
18104
+ "details",
18105
+ "summary",
18106
+ "sup",
18107
+ "sub"
18108
+ ],
18109
+ ALLOWED_ATTR: [
18110
+ "href",
18111
+ "target",
18112
+ "rel"
18113
+ ]
18114
+ };
18115
+ /**
18116
+ * Render untrusted Markdown to a sanitized HTML string safe for
18117
+ * `dangerouslySetInnerHTML`.
18118
+ *
18119
+ * Defense in depth: the `marked` renderer drops raw HTML, drops images, and
18120
+ * emits only `https?:` links (escaped, with `rel="noopener noreferrer"`);
18121
+ * DOMPurify then re-sanitizes the output against a tag/attribute allowlist
18122
+ * (no `script`/`style`/`img`/`iframe`, no inline styles or event handlers).
18123
+ */
18124
+ function renderMarkdown(markdown) {
18125
+ const result = md.parse(markdown);
18126
+ const html = typeof result === "string" ? result : "";
18127
+ return DOMPurify.sanitize(html, SANITIZE_CONFIG);
18128
+ }
18129
+
17594
18130
  //#endregion
17595
18131
  //#region src/components/CapabilityConsentDialog.tsx
17596
18132
  /**
@@ -17600,10 +18136,10 @@ function formatInstallCount(count) {
17600
18136
  * Lists each requested capability with a human-readable explanation.
17601
18137
  * User must explicitly confirm before the action proceeds.
17602
18138
  */
17603
- function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts, newCapabilities = [], auditVerdict, isPending = false, error, onConfirm, onCancel }) {
18139
+ function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts, newCapabilities = [], newlyPublicRoutes = [], auditVerdict, isPending = false, error, onConfirm, onCancel }) {
17604
18140
  const { _: _t } = useLingui();
17605
18141
  const newSet = new Set(newCapabilities);
17606
- const isUpdate = mode === "update" || newCapabilities.length > 0;
18142
+ const isUpdate = mode === "update" || newCapabilities.length > 0 || newlyPublicRoutes.length > 0;
17607
18143
  return /* @__PURE__ */ jsxs("div", {
17608
18144
  className: "fixed inset-0 z-50 flex items-center justify-center",
17609
18145
  role: "dialog",
@@ -17644,31 +18180,60 @@ function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts,
17644
18180
  }),
17645
18181
  /* @__PURE__ */ jsxs("div", {
17646
18182
  className: "px-6 py-4 space-y-3",
17647
- children: [capabilities.map((cap) => {
17648
- const isNew = newSet.has(cap);
17649
- return /* @__PURE__ */ jsxs("div", {
17650
- className: cn("flex items-start gap-3 rounded-md p-2 text-sm", isNew ? "bg-warning/10 border border-warning/30" : "bg-kumo-tint/50"),
17651
- children: [/* @__PURE__ */ jsx(ShieldCheck, { className: cn("mt-0.5 h-4 w-4 shrink-0", isNew ? "text-warning" : "text-kumo-subtle") }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
17652
- className: cn(isNew && "font-medium"),
17653
- children: describeCapability(cap, allowedHosts)
17654
- }), isNew && /* @__PURE__ */ jsx("span", {
17655
- className: "ms-2 text-xs text-warning font-medium",
17656
- children: _t({
17657
- id: "p6NueD",
17658
- message: "NEW"
18183
+ children: [
18184
+ capabilities.map((cap) => {
18185
+ const isNew = newSet.has(cap);
18186
+ return /* @__PURE__ */ jsxs("div", {
18187
+ className: cn("flex items-start gap-3 rounded-md p-2 text-sm", isNew ? "bg-warning/10 border border-warning/30" : "bg-kumo-tint/50"),
18188
+ children: [/* @__PURE__ */ jsx(ShieldCheck, { className: cn("mt-0.5 h-4 w-4 shrink-0", isNew ? "text-warning" : "text-kumo-subtle") }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
18189
+ className: cn(isNew && "font-medium"),
18190
+ children: describeCapability(cap, allowedHosts)
18191
+ }), isNew && /* @__PURE__ */ jsx("span", {
18192
+ className: "ms-2 text-xs text-warning font-medium",
18193
+ children: _t({
18194
+ id: "p6NueD",
18195
+ message: "NEW"
18196
+ })
18197
+ })] })]
18198
+ }, cap);
18199
+ }),
18200
+ newlyPublicRoutes.length > 0 && /* @__PURE__ */ jsxs("div", {
18201
+ className: "rounded-md border border-warning/30 bg-warning/10 p-3 text-sm",
18202
+ children: [
18203
+ /* @__PURE__ */ jsxs("div", {
18204
+ className: "flex items-center gap-2 font-medium text-warning",
18205
+ children: [/* @__PURE__ */ jsx(Warning, { className: "h-4 w-4 shrink-0" }), _t({
18206
+ id: "NXruDe",
18207
+ message: "New public routes"
18208
+ })]
18209
+ }),
18210
+ /* @__PURE__ */ jsx("p", {
18211
+ className: "mt-1 text-xs text-kumo-subtle",
18212
+ children: _t({
18213
+ id: "qCUL4n",
18214
+ message: "This update exposes the following routes without authentication:"
18215
+ })
18216
+ }),
18217
+ /* @__PURE__ */ jsx("ul", {
18218
+ className: "mt-2 space-y-1 ps-5 text-xs",
18219
+ children: newlyPublicRoutes.map((route) => /* @__PURE__ */ jsx("li", {
18220
+ className: "list-disc font-mono",
18221
+ children: route
18222
+ }, route))
17659
18223
  })
17660
- })] })]
17661
- }, cap);
17662
- }), auditVerdict && auditVerdict !== "pass" && /* @__PURE__ */ jsxs("div", {
17663
- className: cn("flex items-center gap-2 rounded-md p-3 text-sm mt-2", auditVerdict === "warn" ? "bg-warning/10 text-warning" : "bg-kumo-danger/10 text-kumo-danger"),
17664
- children: [auditVerdict === "warn" ? /* @__PURE__ */ jsx(Warning, { className: "h-4 w-4 shrink-0" }) : /* @__PURE__ */ jsx(ShieldWarning, { className: "h-4 w-4 shrink-0" }), /* @__PURE__ */ jsx("span", { children: auditVerdict === "warn" ? _t({
17665
- id: "zPUuMZ",
17666
- message: "Security audit flagged potential concerns with this plugin."
17667
- }) : _t({
17668
- id: "nvVzbM",
17669
- message: "Security audit flagged this plugin as potentially unsafe."
17670
- }) })]
17671
- })]
18224
+ ]
18225
+ }),
18226
+ auditVerdict && auditVerdict !== "pass" && /* @__PURE__ */ jsxs("div", {
18227
+ className: cn("flex items-center gap-2 rounded-md p-3 text-sm mt-2", auditVerdict === "warn" ? "bg-warning/10 text-warning" : "bg-kumo-danger/10 text-kumo-danger"),
18228
+ children: [auditVerdict === "warn" ? /* @__PURE__ */ jsx(Warning, { className: "h-4 w-4 shrink-0" }) : /* @__PURE__ */ jsx(ShieldWarning, { className: "h-4 w-4 shrink-0" }), /* @__PURE__ */ jsx("span", { children: auditVerdict === "warn" ? _t({
18229
+ id: "zPUuMZ",
18230
+ message: "Security audit flagged potential concerns with this plugin."
18231
+ }) : _t({
18232
+ id: "nvVzbM",
18233
+ message: "Security audit flagged this plugin as potentially unsafe."
18234
+ }) })]
18235
+ })
18236
+ ]
17672
18237
  }),
17673
18238
  /* @__PURE__ */ jsx(DialogError, {
17674
18239
  message: error,
@@ -17707,6 +18272,474 @@ function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts,
17707
18272
  });
17708
18273
  }
17709
18274
 
18275
+ //#endregion
18276
+ //#region src/lib/api/registry.ts
18277
+ let cachedDiscovery = null;
18278
+ async function getDiscoveryClient(config) {
18279
+ if (cachedDiscovery && cachedDiscovery.config.aggregatorUrl === config.aggregatorUrl && cachedDiscovery.config.acceptLabelers === config.acceptLabelers) return cachedDiscovery.client;
18280
+ const DiscoveryClient = (await import("@emdash-cms/registry-client/discovery")).DiscoveryClient;
18281
+ const discovery = new DiscoveryClient({
18282
+ aggregatorUrl: config.aggregatorUrl,
18283
+ acceptLabelers: config.acceptLabelers
18284
+ });
18285
+ const wrapped = {
18286
+ async searchPackages(opts) {
18287
+ return discovery.searchPackages({
18288
+ q: opts.q,
18289
+ cursor: opts.cursor,
18290
+ limit: opts.limit
18291
+ });
18292
+ },
18293
+ async resolvePackage(handle, slug) {
18294
+ return discovery.resolvePackage({
18295
+ handle,
18296
+ slug
18297
+ });
18298
+ },
18299
+ async getPackage(did, slug) {
18300
+ return discovery.getPackage({
18301
+ did,
18302
+ slug
18303
+ });
18304
+ },
18305
+ async getLatestRelease(did, slug) {
18306
+ return discovery.getLatestRelease({
18307
+ did,
18308
+ package: slug
18309
+ });
18310
+ },
18311
+ async listReleases(did, slug, opts) {
18312
+ return discovery.listReleases({
18313
+ did,
18314
+ package: slug,
18315
+ cursor: opts?.cursor,
18316
+ limit: opts?.limit
18317
+ });
18318
+ }
18319
+ };
18320
+ cachedDiscovery = {
18321
+ config,
18322
+ client: wrapped
18323
+ };
18324
+ return wrapped;
18325
+ }
18326
+ /**
18327
+ * Returns whether a release should be considered installable given the
18328
+ * configured policy. Currently implements the minimum-release-age check
18329
+ * described in RFC 0001's "Pre-label gap and launch tempo" section,
18330
+ * plus the `minimumReleaseAgeExclude` allowlist.
18331
+ *
18332
+ * Returns `false` (release blocked) when the policy is configured but
18333
+ * the release is missing a valid `indexedAt` -- we fail closed rather
18334
+ * than silently letting unbounded-age releases through.
18335
+ */
18336
+ function releasePassesPolicy(release, pkg, policy, now = Date.now()) {
18337
+ if (!policy?.minimumReleaseAgeSeconds) return true;
18338
+ if (releaseExemptFromMinimumAge(policy.minimumReleaseAgeExclude, pkg.did, pkg.slug)) return true;
18339
+ const indexedAt = Date.parse(release.indexedAt);
18340
+ if (!Number.isFinite(indexedAt)) return false;
18341
+ return (now - indexedAt) / 1e3 >= policy.minimumReleaseAgeSeconds;
18342
+ }
18343
+ /**
18344
+ * Canonicalize a capabilities list for set-style comparison. Mirrors
18345
+ * the server-side helper `canonicalCapabilitiesForDriftCheck` in
18346
+ * `packages/core/src/registry/config.ts` -- both sides must produce
18347
+ * the same canonical shape so the install handler's drift check is
18348
+ * stable across reorderings, duplicates, and junk entries.
18349
+ *
18350
+ * Filters non-strings, deduplicates, and sorts lexically.
18351
+ */
18352
+ function canonicalCapabilitiesForDriftCheck(value) {
18353
+ if (!Array.isArray(value)) return [];
18354
+ const seen = /* @__PURE__ */ new Set();
18355
+ for (const entry of value) if (typeof entry === "string" && entry.length > 0) seen.add(entry);
18356
+ return [...seen].toSorted();
18357
+ }
18358
+ /**
18359
+ * Matches a `(publisher_did, slug)` against the
18360
+ * `minimumReleaseAgeExclude` allowlist. Mirrors the server-side helper
18361
+ * of the same name in `packages/core/src/registry/config.ts`.
18362
+ *
18363
+ * DID-only on purpose: handles are aggregator-supplied envelope data
18364
+ * and accepting them as a trust input would let a compromised
18365
+ * aggregator bypass the holdback by claiming any handle for any
18366
+ * package. DIDs are tied to the AT URI of the record itself.
18367
+ *
18368
+ * Entries from the config list have already been lowercased at
18369
+ * manifest build time, so this only needs to lowercase the runtime
18370
+ * values for comparison.
18371
+ */
18372
+ function releaseExemptFromMinimumAge(exclude, publisherDid, slug) {
18373
+ if (!exclude || exclude.length === 0) return false;
18374
+ const didLower = publisherDid.toLowerCase();
18375
+ const fullDid = `${didLower}/${slug.toLowerCase()}`;
18376
+ for (const entry of exclude) {
18377
+ if (entry === didLower) return true;
18378
+ if (entry === fullDid) return true;
18379
+ }
18380
+ return false;
18381
+ }
18382
+ /**
18383
+ * The FAIR-recognised long-form section keys, in display order. Publishers may
18384
+ * also ship unrecognised keys (the lexicon's `sections` map is open), but the
18385
+ * admin renders only this known set so an aggregator can't inject a section
18386
+ * with an attacker-chosen heading; everything else is ignored.
18387
+ */
18388
+ const SECTION_ORDER = [
18389
+ "description",
18390
+ "installation",
18391
+ "faq",
18392
+ "changelog",
18393
+ "security"
18394
+ ];
18395
+ /**
18396
+ * Select the non-empty long-form sections off a package profile, in
18397
+ * `SECTION_ORDER`. `profile.sections` is a lexicon-validated map of Markdown
18398
+ * strings (or `null` when the aggregator returned a non-conforming record), so
18399
+ * each value is narrowed to a non-whitespace string before inclusion. Empty,
18400
+ * missing, whitespace-only, and non-string entries are dropped, so callers can
18401
+ * suppress the whole sections UI when the result is empty.
18402
+ */
18403
+ function presentSections(profile) {
18404
+ const sections = profile?.sections;
18405
+ if (!sections || typeof sections !== "object") return [];
18406
+ const map = sections;
18407
+ const out = [];
18408
+ for (const key of SECTION_ORDER) {
18409
+ const value = map[key];
18410
+ if (typeof value === "string" && value.trim().length > 0) out.push({
18411
+ key,
18412
+ markdown: value
18413
+ });
18414
+ }
18415
+ return out;
18416
+ }
18417
+ /**
18418
+ * Narrow a release record's `sbom` field to the fields the admin renders.
18419
+ * Returns `null` unless the value is an object carrying at least one usable
18420
+ * field (`format` or `url`); every field is independently optional per the
18421
+ * lexicon. `sbom` is lexicon-validated at the DiscoveryClient boundary, but the
18422
+ * record is a publisher pass-through, so its inner shape still needs narrowing.
18423
+ */
18424
+ function extractSbom(value) {
18425
+ if (!value || typeof value !== "object") return null;
18426
+ const v = value;
18427
+ const sbom = {};
18428
+ if (typeof v.format === "string" && v.format.length > 0) sbom.format = v.format;
18429
+ if (typeof v.url === "string" && v.url.length > 0) sbom.url = v.url;
18430
+ if (typeof v.checksum === "string") sbom.checksum = v.checksum;
18431
+ if (!sbom.format && !sbom.url) return null;
18432
+ return sbom;
18433
+ }
18434
+ /**
18435
+ * Validate an SBOM document URL for use in a download `href`. Returns the
18436
+ * normalised URL only when it is an absolute `http(s)` URL; everything else
18437
+ * (relative, `javascript:`, `data:`, non-string) returns `null`. The release
18438
+ * record is a remote pass-through, so an unsanitised SBOM `href` would be
18439
+ * stored XSS in the authenticated admin origin. The browser fetches the SBOM
18440
+ * client-side on click — no server proxy, so SSRF isn't a concern here.
18441
+ */
18442
+ function sbomDownloadHref(value) {
18443
+ if (typeof value !== "string" || value.length === 0) return null;
18444
+ let parsed;
18445
+ try {
18446
+ parsed = new URL(value);
18447
+ } catch {
18448
+ return null;
18449
+ }
18450
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
18451
+ return parsed.href;
18452
+ }
18453
+ async function searchRegistryPackages(config, opts) {
18454
+ return (await getDiscoveryClient(config)).searchPackages(opts);
18455
+ }
18456
+ async function resolveRegistryPackage(config, handle, slug) {
18457
+ return (await getDiscoveryClient(config)).resolvePackage(handle, slug);
18458
+ }
18459
+ async function getRegistryPackage(config, did, slug) {
18460
+ return (await getDiscoveryClient(config)).getPackage(did, slug);
18461
+ }
18462
+ async function listRegistryReleases(config, did, slug, opts) {
18463
+ return (await getDiscoveryClient(config)).listReleases(did, slug, opts);
18464
+ }
18465
+ /**
18466
+ * Derive the host environment versions (`env:emdash`, `env:astro`) the running
18467
+ * EmDash install advertises, so a release's `requires` constraints can be
18468
+ * evaluated client-side before offering install. Reads the already-fetched
18469
+ * admin manifest (`version`, `astroVersion`) rather than issuing a second
18470
+ * request. The dev-skip / astro-omit rule is shared with the server gate via
18471
+ * `hostEnvFromVersions`.
18472
+ */
18473
+ function hostEnvFromManifest(manifest) {
18474
+ return hostEnvFromVersions(manifest?.version, manifest?.astroVersion);
18475
+ }
18476
+ /**
18477
+ * Resolve a publisher DID to its claimed handle using the same
18478
+ * `LocalActorResolver` pattern as `@emdash-cms/plugin-cli` and
18479
+ * `@emdash-cms/auth-atproto`. Bidirectional verification (handle's
18480
+ * domain points back to the same DID) is part of the resolver --
18481
+ * `LocalActorResolver` returns the sentinel `"handle.invalid"` when
18482
+ * the `alsoKnownAs` handle is present but doesn't round-trip.
18483
+ *
18484
+ * Three distinct outcomes the UI can render:
18485
+ *
18486
+ * - `{ status: "ok", handle }` — verified handle, round-trip OK.
18487
+ * - `{ status: "invalid" }` — DID claims a handle but it doesn't
18488
+ * resolve back. The publisher's handle setup is broken; the admin
18489
+ * should see a clear "Invalid handle" indicator rather than the
18490
+ * raw DID.
18491
+ * - `{ status: "missing" }` — no handle claimed at all (no
18492
+ * `alsoKnownAs`), or the DID document couldn't be fetched (network
18493
+ * error, unsupported DID method).
18494
+ */
18495
+ let actorResolver = null;
18496
+ async function getActorResolver() {
18497
+ if (actorResolver) return actorResolver;
18498
+ const { CompositeDidDocumentResolver, CompositeHandleResolver, DohJsonHandleResolver, LocalActorResolver, PlcDidDocumentResolver, WebDidDocumentResolver, WellKnownHandleResolver } = await import("@atcute/identity-resolver");
18499
+ actorResolver = new LocalActorResolver({
18500
+ handleResolver: new CompositeHandleResolver({ methods: {
18501
+ dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" }),
18502
+ http: new WellKnownHandleResolver()
18503
+ } }),
18504
+ didDocumentResolver: new CompositeDidDocumentResolver({ methods: {
18505
+ plc: new PlcDidDocumentResolver(),
18506
+ web: new WebDidDocumentResolver()
18507
+ } })
18508
+ });
18509
+ return actorResolver;
18510
+ }
18511
+ /**
18512
+ * localStorage-backed cache for DID→handle resolutions. Handles are
18513
+ * stable for hours-to-days in practice, but bound the cache so a
18514
+ * compromised handle eventually flips back to "invalid" without a
18515
+ * forced refresh. 24h matches the typical atproto handle TTL.
18516
+ *
18517
+ * Failures (network errors, unsupported DID method) are *not* cached --
18518
+ * those should retry on the next render.
18519
+ */
18520
+ const HANDLE_CACHE_TTL_MS = 1440 * 60 * 1e3;
18521
+ const HANDLE_CACHE_KEY_PREFIX = "emdash:did-handle:";
18522
+ function isCachedResolution(value) {
18523
+ if (typeof value !== "object" || value === null) return false;
18524
+ const candidate = value;
18525
+ return typeof candidate.expiresAt === "number" && typeof candidate.resolution === "object" && candidate.resolution !== null;
18526
+ }
18527
+ function readHandleCache(did) {
18528
+ if (typeof localStorage === "undefined") return null;
18529
+ try {
18530
+ const raw = localStorage.getItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`);
18531
+ if (!raw) return null;
18532
+ const parsed = JSON.parse(raw);
18533
+ if (!isCachedResolution(parsed) || parsed.expiresAt < Date.now()) return null;
18534
+ return parsed.resolution;
18535
+ } catch {
18536
+ return null;
18537
+ }
18538
+ }
18539
+ function writeHandleCache(did, resolution) {
18540
+ if (typeof localStorage === "undefined") return;
18541
+ try {
18542
+ const entry = {
18543
+ resolution,
18544
+ expiresAt: Date.now() + HANDLE_CACHE_TTL_MS
18545
+ };
18546
+ localStorage.setItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`, JSON.stringify(entry));
18547
+ } catch {}
18548
+ }
18549
+ async function resolveDidToHandle(did) {
18550
+ const cached = readHandleCache(did);
18551
+ if (cached) return cached;
18552
+ let result;
18553
+ try {
18554
+ const resolved = await (await getActorResolver()).resolve(did);
18555
+ if (resolved.handle === "handle.invalid") result = { status: "invalid" };
18556
+ else if (resolved.handle) result = {
18557
+ status: "ok",
18558
+ handle: resolved.handle
18559
+ };
18560
+ else result = { status: "missing" };
18561
+ } catch (err) {
18562
+ console.warn(`[registry] DID->handle resolution failed for ${did}:`, err);
18563
+ return { status: "missing" };
18564
+ }
18565
+ writeHandleCache(did, result);
18566
+ return result;
18567
+ }
18568
+ const ARTIFACT_PROXY_ENDPOINT = `${API_BASE}/admin/plugins/registry/artifact`;
18569
+ /**
18570
+ * Build the URL of the server-side artifact proxy for an artifact addressed by
18571
+ * its `(did, slug, version, kind, index)` coordinates. The browser never sends
18572
+ * the publisher's URL — the proxy resolves the *declared* URL from the release
18573
+ * record, applies SSRF defences, enforces an image content-type allowlist, and
18574
+ * serves the bytes back same-origin.
18575
+ *
18576
+ * Empty `version` (latest) and `index` (non-screenshot kinds) are omitted.
18577
+ */
18578
+ function artifactProxyUrl(coords) {
18579
+ const params = new URLSearchParams();
18580
+ params.set("did", coords.did);
18581
+ params.set("slug", coords.slug);
18582
+ params.set("kind", coords.kind);
18583
+ if (coords.version) params.set("version", coords.version);
18584
+ if (coords.kind === "screenshot" && coords.index !== void 0) params.set("index", String(coords.index));
18585
+ return `${ARTIFACT_PROXY_ENDPOINT}?${params.toString()}`;
18586
+ }
18587
+ /**
18588
+ * Narrow one entry of a release's `artifacts` map to the fields we render.
18589
+ * Returns `null` when the value isn't an object carrying a usable `url`
18590
+ * (presence gate), keeping only the dimensions for layout.
18591
+ *
18592
+ * Records are lexicon-validated at the DiscoveryClient boundary, but
18593
+ * `artifacts` is an aggregator pass-through, so each entry still needs
18594
+ * shape-narrowing.
18595
+ */
18596
+ function asMediaArtifact(value) {
18597
+ if (!value || typeof value !== "object") return null;
18598
+ const v = value;
18599
+ if (typeof v.url !== "string" || v.url.length === 0) return null;
18600
+ const artifact = {};
18601
+ if (typeof v.width === "number") artifact.width = v.width;
18602
+ if (typeof v.height === "number") artifact.height = v.height;
18603
+ return artifact;
18604
+ }
18605
+ /**
18606
+ * Pull icon, banner, and the screenshot gallery out of a release's `artifacts`
18607
+ * map, keeping presence and dimensions only. The lexicon types `screenshots`
18608
+ * as an array of artifacts; entries without a usable `url` are dropped, and
18609
+ * gallery order is preserved so screenshot indices line up with the proxy's.
18610
+ */
18611
+ function extractMediaArtifacts(artifacts) {
18612
+ const result = { screenshots: [] };
18613
+ if (!artifacts || typeof artifacts !== "object") return result;
18614
+ const map = artifacts;
18615
+ const icon = asMediaArtifact(map.icon);
18616
+ if (icon) result.icon = icon;
18617
+ const banner = asMediaArtifact(map.banner);
18618
+ if (banner) result.banner = banner;
18619
+ if (Array.isArray(map.screenshots)) map.screenshots.forEach((entry, index) => {
18620
+ const artifact = asMediaArtifact(entry);
18621
+ if (artifact) result.screenshots.push({
18622
+ ...artifact,
18623
+ index
18624
+ });
18625
+ });
18626
+ return result;
18627
+ }
18628
+ const INSTALL_ENDPOINT = `${API_BASE}/admin/plugins/registry/install`;
18629
+ /**
18630
+ * Install a plugin from the registry.
18631
+ *
18632
+ * Posts to the EmDash server, which re-resolves the same `(handle,
18633
+ * slug)` against the aggregator, re-verifies the bundle's checksum
18634
+ * against the signed release record, and writes the install. Surfaces
18635
+ * structured error codes (`RELEASE_YANKED`, `CHECKSUM_MISMATCH`,
18636
+ * `DECLARED_ACCESS_DRIFT`, etc.) that callers map to localized
18637
+ * messages.
18638
+ */
18639
+ async function installRegistryPlugin(body) {
18640
+ return parseApiResponse(await apiFetch(INSTALL_ENDPOINT, {
18641
+ method: "POST",
18642
+ headers: { "Content-Type": "application/json" },
18643
+ body: JSON.stringify(body)
18644
+ }), i18n._({
18645
+ id: "/LWKyx",
18646
+ message: "Failed to install plugin"
18647
+ }));
18648
+ }
18649
+ /**
18650
+ * Server-side escalation gate raised by the update endpoint when the
18651
+ * target version widens the trust contract. Carries the diff the user
18652
+ * needs to see in the consent dialog before the call is retried with the
18653
+ * matching `confirm*` flag.
18654
+ */
18655
+ var RegistryUpdateEscalationError = class extends Error {
18656
+ code;
18657
+ capabilityChanges;
18658
+ routeVisibilityChanges;
18659
+ constructor(code, message, capabilityChanges, routeVisibilityChanges) {
18660
+ super(message);
18661
+ this.name = "RegistryUpdateEscalationError";
18662
+ this.code = code;
18663
+ this.capabilityChanges = capabilityChanges;
18664
+ this.routeVisibilityChanges = routeVisibilityChanges;
18665
+ }
18666
+ };
18667
+ /**
18668
+ * Update a registry-source plugin to a newer version.
18669
+ * `POST /_emdash/api/admin/plugins/registry/:id/update`
18670
+ *
18671
+ * Called without `confirm*` flags first, this throws
18672
+ * `RegistryUpdateEscalationError` when the target version widens
18673
+ * permissions; the caller renders a consent dialog populated from the
18674
+ * error's diff, then re-calls with the matching `confirm*` flag once
18675
+ * the user agrees.
18676
+ */
18677
+ async function updateRegistryPlugin(pluginId, opts = {}) {
18678
+ const response = await apiFetch(`${API_BASE}/admin/plugins/registry/${encodeURIComponent(pluginId)}/update`, {
18679
+ method: "POST",
18680
+ headers: { "Content-Type": "application/json" },
18681
+ body: JSON.stringify(opts)
18682
+ });
18683
+ if (response.ok) return;
18684
+ const escalation = parseEscalation(await response.clone().json().catch(() => void 0));
18685
+ if (escalation) throw escalation;
18686
+ await throwResponseError(response, i18n._({
18687
+ id: "8m84rO",
18688
+ message: "Failed to update plugin"
18689
+ }));
18690
+ }
18691
+ function parseEscalation(body) {
18692
+ if (!body || typeof body !== "object" || !("error" in body)) return null;
18693
+ const error = body.error;
18694
+ if (!error || typeof error !== "object" || !("code" in error)) return null;
18695
+ const code = error.code;
18696
+ if (code !== "CAPABILITY_ESCALATION" && code !== "ROUTE_VISIBILITY_ESCALATION") return null;
18697
+ const details = "details" in error && error.details && typeof error.details === "object" ? error.details : {};
18698
+ const capabilityChanges = normaliseCapabilityChanges("capabilityChanges" in details ? details.capabilityChanges : void 0);
18699
+ const routeVisibilityChanges = normaliseRouteVisibilityChanges("routeVisibilityChanges" in details ? details.routeVisibilityChanges : void 0);
18700
+ return new RegistryUpdateEscalationError(code, "message" in error && typeof error.message === "string" ? error.message : i18n._({
18701
+ id: "dgD7yY",
18702
+ message: "Plugin update requires re-consent"
18703
+ }), capabilityChanges, routeVisibilityChanges);
18704
+ }
18705
+ function normaliseCapabilityChanges(value) {
18706
+ if (!value || typeof value !== "object") return {
18707
+ added: [],
18708
+ removed: []
18709
+ };
18710
+ const v = value;
18711
+ return {
18712
+ added: Array.isArray(v.added) ? v.added.filter((s) => typeof s === "string") : [],
18713
+ removed: Array.isArray(v.removed) ? v.removed.filter((s) => typeof s === "string") : []
18714
+ };
18715
+ }
18716
+ function normaliseRouteVisibilityChanges(value) {
18717
+ if (!value || typeof value !== "object") return void 0;
18718
+ const v = value;
18719
+ if (!Array.isArray(v.newlyPublic)) return void 0;
18720
+ const newlyPublic = v.newlyPublic.filter((s) => typeof s === "string");
18721
+ return newlyPublic.length > 0 ? { newlyPublic } : void 0;
18722
+ }
18723
+ /**
18724
+ * Uninstall a registry-source plugin.
18725
+ * `POST /_emdash/api/admin/plugins/registry/:id/uninstall`
18726
+ *
18727
+ * The server refuses to uninstall non-registry sources, so calling this
18728
+ * with a marketplace or config plugin id is a no-op error rather than a
18729
+ * destructive cross-source action.
18730
+ */
18731
+ async function uninstallRegistryPlugin(pluginId, opts = {}) {
18732
+ const response = await apiFetch(`${API_BASE}/admin/plugins/registry/${encodeURIComponent(pluginId)}/uninstall`, {
18733
+ method: "POST",
18734
+ headers: { "Content-Type": "application/json" },
18735
+ body: JSON.stringify(opts)
18736
+ });
18737
+ if (!response.ok) await throwResponseError(response, i18n._({
18738
+ id: "IDxl65",
18739
+ message: "Failed to uninstall plugin"
18740
+ }));
18741
+ }
18742
+
17710
18743
  //#endregion
17711
18744
  //#region src/components/PluginManager.tsx
17712
18745
  /**
@@ -17796,7 +18829,7 @@ function PluginManager({ manifest }) {
17796
18829
  if (!updates) return /* @__PURE__ */ new Map();
17797
18830
  return new Map(updates.map((u) => [u.pluginId, u]));
17798
18831
  }, [updates]);
17799
- const hasMarketplacePlugins = plugins?.some((p) => p.source === "marketplace");
18832
+ const hasUpdatableSources = plugins?.some((p) => p.source === "marketplace" || p.source === "registry");
17800
18833
  if (isLoading) return /* @__PURE__ */ jsxs("div", {
17801
18834
  className: "space-y-6",
17802
18835
  children: [/* @__PURE__ */ jsx("h1", {
@@ -17844,7 +18877,7 @@ function PluginManager({ manifest }) {
17844
18877
  }), /* @__PURE__ */ jsxs("div", {
17845
18878
  className: "flex items-center gap-3",
17846
18879
  children: [
17847
- hasMarketplacePlugins && /* @__PURE__ */ jsx(Button, {
18880
+ hasUpdatableSources && /* @__PURE__ */ jsx(Button, {
17848
18881
  variant: "ghost",
17849
18882
  onClick: () => void refetchUpdates(),
17850
18883
  disabled: isCheckingUpdates,
@@ -17939,15 +18972,17 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
17939
18972
  const [expanded, setExpanded] = React$1.useState(false);
17940
18973
  const [showUpdateConsent, setShowUpdateConsent] = React$1.useState(false);
17941
18974
  const [showUninstallConfirm, setShowUninstallConfirm] = React$1.useState(false);
18975
+ const [registryEscalation, setRegistryEscalation] = React$1.useState(null);
17942
18976
  const queryClient = useQueryClient();
17943
18977
  const toastManager = Toast.useToastManager();
17944
18978
  const isMarketplace = plugin.source === "marketplace";
17945
18979
  const isRegistry = plugin.source === "registry";
17946
18980
  const hasUpdate = !!updateInfo && updateInfo.installed !== updateInfo.latest;
17947
18981
  const updateMutation = useMutation({
17948
- mutationFn: () => updateMarketplacePlugin(plugin.id, { confirmCapabilities: true }),
18982
+ mutationFn: (opts) => isRegistry ? updateRegistryPlugin(plugin.id, opts) : updateMarketplacePlugin(plugin.id, { confirmCapabilities: true }),
17949
18983
  onSuccess: () => {
17950
18984
  setShowUpdateConsent(false);
18985
+ setRegistryEscalation(null);
17951
18986
  queryClient.invalidateQueries({ queryKey: ["plugins"] });
17952
18987
  queryClient.invalidateQueries({ queryKey: ["plugin-updates"] });
17953
18988
  queryClient.invalidateQueries({ queryKey: ["manifest"] });
@@ -17965,10 +19000,29 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
17965
19000
  }
17966
19001
  })
17967
19002
  });
19003
+ },
19004
+ onError: (err) => {
19005
+ if (err instanceof RegistryUpdateEscalationError) {
19006
+ setRegistryEscalation(err);
19007
+ setShowUpdateConsent(true);
19008
+ }
17968
19009
  }
17969
19010
  });
19011
+ const handleUpdateClick = () => {
19012
+ if (isRegistry) {
19013
+ setRegistryEscalation(null);
19014
+ updateMutation.mutate({});
19015
+ } else setShowUpdateConsent(true);
19016
+ };
19017
+ const handleUpdateConfirm = () => {
19018
+ if (isRegistry) {
19019
+ const opts = { confirmCapabilityChanges: true };
19020
+ if (registryEscalation?.code === "ROUTE_VISIBILITY_ESCALATION") opts.confirmRouteVisibilityChanges = true;
19021
+ updateMutation.mutate(opts);
19022
+ } else updateMutation.mutate({});
19023
+ };
17970
19024
  const uninstallMutation = useMutation({
17971
- mutationFn: (deleteData) => uninstallMarketplacePlugin(plugin.id, { deleteData }),
19025
+ mutationFn: (deleteData) => isRegistry ? uninstallRegistryPlugin(plugin.id, { deleteData }) : uninstallMarketplacePlugin(plugin.id, { deleteData }),
17972
19026
  onSuccess: () => {
17973
19027
  setShowUninstallConfirm(false);
17974
19028
  queryClient.invalidateQueries({ queryKey: ["plugins"] });
@@ -18078,13 +19132,10 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18078
19132
  const label = CAPABILITY_LABELS[c];
18079
19133
  return label ? _t2(label) : c;
18080
19134
  }).join(", "),
18081
- children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-3 w-3" }), _t2({
18082
- id: "gMTYze",
18083
- message: "{0} permission{1}",
18084
- values: {
18085
- 0: plugin.capabilities.length,
18086
- 1: plugin.capabilities.length !== 1 ? "s" : ""
18087
- }
19135
+ children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-3 w-3" }), i18n._({
19136
+ id: "dB/Iw4",
19137
+ message: "{0, plural, one {# permission} other {# permissions}}",
19138
+ values: { 0: plugin.capabilities.length }
18088
19139
  })]
18089
19140
  })
18090
19141
  ]
@@ -18097,7 +19148,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18097
19148
  hasUpdate && /* @__PURE__ */ jsx(Button, {
18098
19149
  variant: "outline",
18099
19150
  size: "sm",
18100
- onClick: () => setShowUpdateConsent(true),
19151
+ onClick: handleUpdateClick,
18101
19152
  disabled: updateMutation.isPending,
18102
19153
  children: updateMutation.isPending ? _t2({
18103
19154
  id: "RXbEvi",
@@ -18261,7 +19312,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18261
19312
  ] })
18262
19313
  ]
18263
19314
  }),
18264
- isMarketplace && /* @__PURE__ */ jsx("div", {
19315
+ (isMarketplace || isRegistry) && /* @__PURE__ */ jsx("div", {
18265
19316
  className: "pt-2 border-t",
18266
19317
  children: /* @__PURE__ */ jsx(Button, {
18267
19318
  variant: "ghost",
@@ -18274,13 +19325,6 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18274
19325
  message: "Uninstall"
18275
19326
  })
18276
19327
  })
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
- })
18284
19328
  })
18285
19329
  ]
18286
19330
  })]
@@ -18289,12 +19333,14 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18289
19333
  mode: "update",
18290
19334
  pluginName: plugin.name,
18291
19335
  capabilities: plugin.capabilities,
18292
- newCapabilities: [],
19336
+ newCapabilities: registryEscalation?.capabilityChanges.added ?? [],
19337
+ newlyPublicRoutes: registryEscalation?.routeVisibilityChanges?.newlyPublic ?? [],
18293
19338
  isPending: updateMutation.isPending,
18294
- error: getMutationError(updateMutation.error),
18295
- onConfirm: () => updateMutation.mutate(),
19339
+ error: updateMutation.error instanceof RegistryUpdateEscalationError ? null : getMutationError(updateMutation.error),
19340
+ onConfirm: handleUpdateConfirm,
18296
19341
  onCancel: () => {
18297
19342
  setShowUpdateConsent(false);
19343
+ setRegistryEscalation(null);
18298
19344
  updateMutation.reset();
18299
19345
  }
18300
19346
  }),
@@ -18850,72 +19896,6 @@ function ScreenshotLightbox({ screenshots, index, isBlurred = false, onClose, on
18850
19896
  ]
18851
19897
  });
18852
19898
  }
18853
- const HTML_ESCAPE_MAP = {
18854
- "&": "&amp;",
18855
- "<": "&lt;",
18856
- ">": "&gt;",
18857
- "\"": "&quot;",
18858
- "'": "&#39;"
18859
- };
18860
- const HTML_ESCAPE_RE = /[&<>"']/g;
18861
- function escapeHtml(str) {
18862
- return str.replace(HTML_ESCAPE_RE, (ch) => HTML_ESCAPE_MAP[ch]);
18863
- }
18864
- const renderer = new Renderer();
18865
- renderer.link = ({ href, text }) => {
18866
- if (!SAFE_URL_RE.test(href)) return escapeHtml(text);
18867
- return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(text)}</a>`;
18868
- };
18869
- renderer.image = ({ text }) => escapeHtml(text);
18870
- renderer.html = () => "";
18871
- const md = new Marked({
18872
- renderer,
18873
- async: false
18874
- });
18875
- /** Allowed tags and attributes for DOMPurify — only standard markdown output. */
18876
- const SANITIZE_CONFIG = {
18877
- ALLOWED_TAGS: [
18878
- "h1",
18879
- "h2",
18880
- "h3",
18881
- "h4",
18882
- "h5",
18883
- "h6",
18884
- "p",
18885
- "a",
18886
- "ul",
18887
- "ol",
18888
- "li",
18889
- "blockquote",
18890
- "pre",
18891
- "code",
18892
- "em",
18893
- "strong",
18894
- "del",
18895
- "br",
18896
- "hr",
18897
- "table",
18898
- "thead",
18899
- "tbody",
18900
- "tr",
18901
- "th",
18902
- "td",
18903
- "details",
18904
- "summary",
18905
- "sup",
18906
- "sub"
18907
- ],
18908
- ALLOWED_ATTR: [
18909
- "href",
18910
- "target",
18911
- "rel"
18912
- ]
18913
- };
18914
- function renderMarkdown(markdown) {
18915
- const result = md.parse(markdown);
18916
- const html = typeof result === "string" ? result : "";
18917
- return DOMPurify.sanitize(html, SANITIZE_CONFIG);
18918
- }
18919
19899
  function formatBytes(bytes) {
18920
19900
  if (bytes < 1024) return `${bytes} B`;
18921
19901
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -21951,235 +22931,6 @@ function Redirects() {
21951
22931
  });
21952
22932
  }
21953
22933
 
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 discovery.searchPackages({
21967
- q: opts.q,
21968
- cursor: opts.cursor,
21969
- limit: opts.limit
21970
- });
21971
- },
21972
- async resolvePackage(handle, slug) {
21973
- return discovery.resolvePackage({
21974
- handle,
21975
- slug
21976
- });
21977
- },
21978
- async getPackage(did, slug) {
21979
- return discovery.getPackage({
21980
- did,
21981
- slug
21982
- });
21983
- },
21984
- async getLatestRelease(did, slug) {
21985
- return discovery.getLatestRelease({
21986
- did,
21987
- package: slug
21988
- });
21989
- },
21990
- async listReleases(did, slug, cursor) {
21991
- return 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
22934
  //#endregion
22184
22935
  //#region src/components/PublisherHandle.tsx
22185
22936
  /**
@@ -22473,10 +23224,15 @@ function RegistryPluginDetail({ pluginId, config }) {
22473
23224
  const { data: installedPlugins } = useQuery({
22474
23225
  queryKey: ["plugins"],
22475
23226
  queryFn: async () => {
22476
- const { fetchPlugins } = await import("./plugins-qrjvPK4g.js").then((n) => n.a);
23227
+ const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
22477
23228
  return fetchPlugins();
22478
23229
  }
22479
23230
  });
23231
+ const { data: manifest } = useQuery({
23232
+ queryKey: ["manifest"],
23233
+ queryFn: fetchManifest
23234
+ });
23235
+ const hostEnv = React$1.useMemo(() => hostEnvFromManifest(manifest), [manifest]);
22480
23236
  const slashIdx = pluginId.lastIndexOf("/");
22481
23237
  const publisher = slashIdx > 0 ? pluginId.slice(0, slashIdx) : "";
22482
23238
  const slug = slashIdx > 0 ? pluginId.slice(slashIdx + 1) : "";
@@ -22494,17 +23250,42 @@ function RegistryPluginDetail({ pluginId, config }) {
22494
23250
  enabled: Boolean(publisher && slug)
22495
23251
  });
22496
23252
  const handleResult = usePublisherHandle(pkg?.did ?? "", pkg?.handle);
22497
- const { data: release } = useQuery({
23253
+ const { data: releasesData } = useQuery({
22498
23254
  queryKey: [
22499
23255
  "registry",
22500
- "latest-release",
23256
+ "releases",
22501
23257
  config.aggregatorUrl,
23258
+ config.acceptLabelers,
22502
23259
  pkg?.did,
22503
23260
  slug
22504
23261
  ],
22505
- queryFn: () => getLatestRegistryRelease(config, pkg.did, slug),
23262
+ queryFn: () => listRegistryReleases(config, pkg.did, slug, { limit: 100 }),
22506
23263
  enabled: Boolean(pkg?.did && slug)
22507
23264
  });
23265
+ const releases = React$1.useMemo(() => (releasesData?.releases ?? []).filter((r) => r.release !== null && !isYanked(r)), [releasesData]);
23266
+ const hasFilteredAllReleases = (releasesData?.releases.length ?? 0) > 0 && releases.length === 0;
23267
+ const defaultVersion = React$1.useMemo(() => {
23268
+ if (!pkg || releases.length === 0) return void 0;
23269
+ return (releases.find((r) => releasePassesPolicy(r, {
23270
+ did: pkg.did,
23271
+ slug
23272
+ }, config.policy)) ?? releases[0])?.version;
23273
+ }, [
23274
+ pkg,
23275
+ releases,
23276
+ slug,
23277
+ config.policy
23278
+ ]);
23279
+ const [selectedVersion, setSelectedVersion] = React$1.useState(void 0);
23280
+ const [prevPluginId, setPrevPluginId] = React$1.useState(pluginId);
23281
+ if (prevPluginId !== pluginId) {
23282
+ setPrevPluginId(pluginId);
23283
+ setSelectedVersion(void 0);
23284
+ }
23285
+ if (selectedVersion !== void 0 && releases.length > 0 && !releases.some((r) => r.version === selectedVersion)) setSelectedVersion(void 0);
23286
+ const effectiveVersion = selectedVersion ?? defaultVersion;
23287
+ const release = React$1.useMemo(() => releases.find((r) => r.version === effectiveVersion), [releases, effectiveVersion]);
23288
+ const isPreRelease = release ? isPreReleaseVersion(release.version) : false;
22508
23289
  const ext = (release?.release?.extensions)?.["com.emdashcms.experimental.package.releaseExtension"];
22509
23290
  const capabilities = Array.isArray(ext?.capabilities) ? canonicalCapabilitiesForDriftCheck(ext?.capabilities) : canonicalCapabilitiesForDriftCheck(declaredAccessToCapabilityList(ext?.declaredAccess));
22510
23291
  const pkgProfile = pkg?.profile ?? null;
@@ -22526,11 +23307,51 @@ function RegistryPluginDetail({ pluginId, config }) {
22526
23307
  }] : [];
22527
23308
  });
22528
23309
  const repoHref = safeExternalHref(release?.release?.repo);
22529
- const verified = (pkg?.labels ?? []).some((l) => l.val === "verified");
23310
+ const verifiedLabel = (pkg?.labels ?? []).find((l) => l.val === "verified");
23311
+ const verified = Boolean(verifiedLabel);
23312
+ const verifiedLabeller = typeof verifiedLabel?.src === "string" ? verifiedLabel.src : null;
23313
+ const sections = presentSections(pkgProfile);
23314
+ const [selectedSection, setSelectedSection] = React$1.useState(void 0);
23315
+ const defaultSection = sections[0]?.key;
23316
+ const activeSection = selectedSection && sections.some((s) => s.key === selectedSection) ? selectedSection : defaultSection;
23317
+ const activePane = sections.find((s) => s.key === activeSection);
23318
+ const sbom = extractSbom(release?.release?.sbom);
23319
+ const sbomHref = sbom ? sbomDownloadHref(sbom.url) : null;
23320
+ const lastUpdated = typeof pkgProfile?.lastUpdated === "string" ? pkgProfile.lastUpdated : null;
23321
+ const mediaArtifacts = extractMediaArtifacts(release?.release?.artifacts);
23322
+ const artifactDid = pkg?.did;
23323
+ const artifactVersion = release?.version;
23324
+ const iconSrc = mediaArtifacts.icon && artifactDid ? artifactProxyUrl({
23325
+ did: artifactDid,
23326
+ slug,
23327
+ version: artifactVersion,
23328
+ kind: "icon"
23329
+ }) : null;
23330
+ const bannerSrc = mediaArtifacts.banner && artifactDid ? artifactProxyUrl({
23331
+ did: artifactDid,
23332
+ slug,
23333
+ version: artifactVersion,
23334
+ kind: "banner"
23335
+ }) : null;
23336
+ const screenshots = artifactDid ? mediaArtifacts.screenshots.map((shot) => ({
23337
+ ...shot,
23338
+ src: artifactProxyUrl({
23339
+ did: artifactDid,
23340
+ slug,
23341
+ version: artifactVersion,
23342
+ kind: "screenshot",
23343
+ index: shot.index
23344
+ })
23345
+ })) : [];
22530
23346
  const policyOk = release && pkg ? releasePassesPolicy(release, {
22531
23347
  did: pkg.did,
22532
23348
  slug
22533
23349
  }, config.policy) : true;
23350
+ const envMismatches = React$1.useMemo(() => {
23351
+ if (!release) return [];
23352
+ return checkEnvCompatibility(release.release?.requires, hostEnv);
23353
+ }, [release, hostEnv]);
23354
+ const envOk = envMismatches.length === 0;
22534
23355
  const installedEntry = React$1.useMemo(() => {
22535
23356
  if (!pkg || !installedPlugins) return void 0;
22536
23357
  return installedPlugins.find((p) => p.source === "registry" && p.registryPublisherDid === pkg.did && p.registrySlug === slug);
@@ -22589,14 +23410,33 @@ function RegistryPluginDetail({ pluginId, config }) {
22589
23410
  className: "space-y-6",
22590
23411
  children: [
22591
23412
  /* @__PURE__ */ jsx(BackLink, {}),
23413
+ bannerSrc ? /* @__PURE__ */ jsx("img", {
23414
+ src: bannerSrc,
23415
+ alt: _t({
23416
+ id: "aOmjNp",
23417
+ message: "{0} banner",
23418
+ values: { 0: displayName ?? slug }
23419
+ }),
23420
+ className: "h-40 w-full rounded-xl object-cover",
23421
+ loading: "lazy"
23422
+ }) : null,
22592
23423
  /* @__PURE__ */ jsxs("div", {
22593
- className: "flex items-start gap-4",
23424
+ className: "flex flex-wrap items-start gap-4",
22594
23425
  children: [
22595
23426
  /* @__PURE__ */ jsx("div", {
22596
- className: "rounded-xl bg-kumo-subtle p-3 text-kumo-subtle",
22597
- children: /* @__PURE__ */ jsx("span", {
23427
+ className: "overflow-hidden rounded-xl bg-kumo-subtle text-kumo-subtle",
23428
+ children: iconSrc ? /* @__PURE__ */ jsx("img", {
23429
+ src: iconSrc,
23430
+ alt: _t({
23431
+ id: "dVaAiX",
23432
+ message: "{0} icon",
23433
+ values: { 0: displayName ?? slug }
23434
+ }),
23435
+ className: "block h-16 w-16 object-cover",
23436
+ loading: "lazy"
23437
+ }) : /* @__PURE__ */ jsx("span", {
22598
23438
  "aria-hidden": true,
22599
- className: "block h-10 w-10"
23439
+ className: "block h-16 w-16"
22600
23440
  })
22601
23441
  }),
22602
23442
  /* @__PURE__ */ jsxs("div", {
@@ -22607,11 +23447,30 @@ function RegistryPluginDetail({ pluginId, config }) {
22607
23447
  children: [/* @__PURE__ */ jsx("h1", {
22608
23448
  className: "truncate text-3xl font-bold",
22609
23449
  children: displayName ?? slug
22610
- }), verified ? /* @__PURE__ */ jsx(ShieldCheck, {
22611
- className: "h-5 w-5 shrink-0 text-kumo-brand",
22612
- "aria-label": _t({
22613
- id: "70QfpA",
22614
- message: "Verified publisher"
23450
+ }), verified ? /* @__PURE__ */ jsx(Tooltip, {
23451
+ content: verifiedLabeller ? _t({
23452
+ id: "/rITsG",
23453
+ message: "Verified publisher. A labeller ({verifiedLabeller}) has confirmed this publisher's identity.",
23454
+ values: { verifiedLabeller }
23455
+ }) : _t({
23456
+ id: "PCUvJ4",
23457
+ message: "Verified publisher. A labeller has confirmed this publisher's identity."
23458
+ }),
23459
+ render: /* @__PURE__ */ jsx("button", {
23460
+ type: "button",
23461
+ className: "inline-flex shrink-0 cursor-help rounded-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-kumo-brand",
23462
+ "aria-label": verifiedLabeller ? _t({
23463
+ id: "s7Woxs",
23464
+ message: "Verified publisher, confirmed by labeller {verifiedLabeller}",
23465
+ values: { verifiedLabeller }
23466
+ }) : _t({
23467
+ id: "70QfpA",
23468
+ message: "Verified publisher"
23469
+ }),
23470
+ children: /* @__PURE__ */ jsx(ShieldCheck, {
23471
+ className: "h-5 w-5 text-kumo-brand",
23472
+ "aria-hidden": true
23473
+ })
22615
23474
  })
22616
23475
  }) : null]
22617
23476
  }),
@@ -22630,40 +23489,124 @@ function RegistryPluginDetail({ pluginId, config }) {
22630
23489
  })
22631
23490
  ]
22632
23491
  }),
22633
- release ? /* @__PURE__ */ jsxs("p", {
22634
- className: "text-xs text-kumo-subtle",
23492
+ release ? /* @__PURE__ */ jsxs("div", {
23493
+ className: "mt-1 flex flex-wrap items-center gap-2",
22635
23494
  children: [
22636
- _t({
22637
- id: "YYdC3A",
22638
- message: "Version {0}",
22639
- values: { 0: release.version }
23495
+ /* @__PURE__ */ jsx("p", {
23496
+ className: "text-xs text-kumo-subtle",
23497
+ children: _t({
23498
+ id: "YYdC3A",
23499
+ message: "Version {0}",
23500
+ values: { 0: release.version }
23501
+ })
22640
23502
  }),
22641
- " · ",
22642
- _t({
22643
- id: "294VT5",
22644
- message: "indexed {0}",
22645
- values: { 0: formatDate(release.indexedAt) }
22646
- })
23503
+ isPreRelease ? /* @__PURE__ */ jsx(Badge, { children: _t({
23504
+ id: "omrja/",
23505
+ message: "Pre-release"
23506
+ }) }) : null,
23507
+ sbom?.format ? /* @__PURE__ */ jsx(Badge, { children: _t({
23508
+ id: "mQmHhZ",
23509
+ message: "SBOM · {0}",
23510
+ values: { 0: sbom.format }
23511
+ }) }) : sbom ? /* @__PURE__ */ jsx(Badge, { children: _t({
23512
+ id: "SBWpbn",
23513
+ message: "SBOM"
23514
+ }) }) : null,
23515
+ sbomHref ? /* @__PURE__ */ jsx("a", {
23516
+ href: sbomHref,
23517
+ target: "_blank",
23518
+ rel: "noopener noreferrer",
23519
+ download: true,
23520
+ className: "text-xs text-kumo-brand hover:underline",
23521
+ children: _t({
23522
+ id: "gm9IDy",
23523
+ message: "Download SBOM"
23524
+ })
23525
+ }) : null
22647
23526
  ]
23527
+ }) : null,
23528
+ release ? /* @__PURE__ */ jsxs("dl", {
23529
+ className: "mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-kumo-subtle",
23530
+ children: [lastUpdated ? /* @__PURE__ */ jsxs("div", {
23531
+ className: "flex items-center gap-1",
23532
+ children: [/* @__PURE__ */ jsx("dt", {
23533
+ className: "font-medium",
23534
+ children: _t({
23535
+ id: "+b7T3G",
23536
+ message: "Updated"
23537
+ })
23538
+ }), /* @__PURE__ */ jsx("dd", { children: formatDate(lastUpdated) })]
23539
+ }) : null, /* @__PURE__ */ jsxs("div", {
23540
+ className: "flex items-center gap-1",
23541
+ children: [/* @__PURE__ */ jsx("dt", {
23542
+ className: "font-medium",
23543
+ children: _t({
23544
+ id: "I3/r1f",
23545
+ message: "Indexed"
23546
+ })
23547
+ }), /* @__PURE__ */ jsx("dd", { children: formatDate(release.indexedAt) })]
23548
+ })]
22648
23549
  }) : null
22649
23550
  ]
22650
23551
  }),
22651
- /* @__PURE__ */ jsx("div", { children: isInstalled ? /* @__PURE__ */ jsx(Button, {
22652
- variant: "secondary",
22653
- disabled: true,
22654
- children: _t({
22655
- id: "eQkgKV",
22656
- message: "Installed"
22657
- })
22658
- }) : /* @__PURE__ */ jsx(Button, {
22659
- variant: "primary",
22660
- disabled: !release || !policyOk || handleResult.status === "invalid",
22661
- onClick: () => setShowConsent(true),
22662
- children: _t({
22663
- id: "TKQ7K+",
22664
- message: "Install"
22665
- })
22666
- }) })
23552
+ /* @__PURE__ */ jsxs("div", {
23553
+ className: "flex min-w-0 flex-wrap items-center gap-2",
23554
+ children: [releases.length > 1 ? /* @__PURE__ */ jsx(Select, {
23555
+ "aria-label": _t({
23556
+ id: "eE0JZ4",
23557
+ message: "Version"
23558
+ }),
23559
+ className: "w-full max-w-[220px]",
23560
+ value: effectiveVersion ?? "",
23561
+ onValueChange: (v) => setSelectedVersion(v ?? void 0),
23562
+ renderValue: (v) => typeof v === "string" ? v : "",
23563
+ children: releases.map((r) => {
23564
+ const preRelease = isPreReleaseVersion(r.version);
23565
+ const policyBlocked = !releasePassesPolicy(r, {
23566
+ did: pkg.did,
23567
+ slug
23568
+ }, config.policy);
23569
+ return /* @__PURE__ */ jsx(Select.Option, {
23570
+ value: r.version,
23571
+ children: /* @__PURE__ */ jsxs("span", {
23572
+ className: "flex items-center gap-2",
23573
+ children: [
23574
+ /* @__PURE__ */ jsx("span", { children: r.version }),
23575
+ preRelease ? /* @__PURE__ */ jsx("span", {
23576
+ className: "text-xs text-kumo-subtle",
23577
+ children: _t({
23578
+ id: "D2C1D+",
23579
+ message: "(pre-release)"
23580
+ })
23581
+ }) : null,
23582
+ policyBlocked ? /* @__PURE__ */ jsx("span", {
23583
+ className: "text-xs text-kumo-subtle",
23584
+ children: _t({
23585
+ id: "W3SUIF",
23586
+ message: "(too new)"
23587
+ })
23588
+ }) : null
23589
+ ]
23590
+ })
23591
+ }, r.version);
23592
+ })
23593
+ }) : null, isInstalled ? /* @__PURE__ */ jsx(Button, {
23594
+ variant: "secondary",
23595
+ disabled: true,
23596
+ children: _t({
23597
+ id: "eQkgKV",
23598
+ message: "Installed"
23599
+ })
23600
+ }) : /* @__PURE__ */ jsx(Button, {
23601
+ variant: "primary",
23602
+ disabled: !release || !policyOk || !envOk || handleResult.status === "invalid",
23603
+ onClick: () => setShowConsent(true),
23604
+ children: _t({
23605
+ id: "TKQ7K+",
23606
+ message: "Install"
23607
+ })
23608
+ })]
23609
+ })
22667
23610
  ]
22668
23611
  }),
22669
23612
  handleResult.status === "invalid" ? /* @__PURE__ */ jsxs("div", {
@@ -22683,6 +23626,23 @@ function RegistryPluginDetail({ pluginId, config }) {
22683
23626
  })
22684
23627
  })] })]
22685
23628
  }) : null,
23629
+ hasFilteredAllReleases ? /* @__PURE__ */ jsxs("div", {
23630
+ className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
23631
+ role: "status",
23632
+ children: [/* @__PURE__ */ jsx(Warning, { className: "mt-0.5 h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
23633
+ className: "font-medium",
23634
+ children: _t({
23635
+ id: "rqsq95",
23636
+ message: "No installable releases"
23637
+ })
23638
+ }), /* @__PURE__ */ jsx("p", {
23639
+ className: "mt-1 text-sm text-kumo-default",
23640
+ children: _t({
23641
+ id: "o+La9d",
23642
+ message: "Every published release of this plugin has been withdrawn or could not be verified. Check back later, or contact the publisher."
23643
+ })
23644
+ })] })]
23645
+ }) : null,
22686
23646
  release && !policyOk ? /* @__PURE__ */ jsxs("div", {
22687
23647
  className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
22688
23648
  role: "status",
@@ -22701,10 +23661,66 @@ function RegistryPluginDetail({ pluginId, config }) {
22701
23661
  })
22702
23662
  })] })]
22703
23663
  }) : null,
23664
+ release && !envOk ? /* @__PURE__ */ jsxs("div", {
23665
+ className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
23666
+ role: "status",
23667
+ children: [/* @__PURE__ */ jsx(Warning, { className: "mt-0.5 h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [
23668
+ /* @__PURE__ */ jsx("p", {
23669
+ className: "font-medium",
23670
+ children: _t({
23671
+ id: "25SzC8",
23672
+ message: "Not compatible with this environment"
23673
+ })
23674
+ }),
23675
+ /* @__PURE__ */ jsx("p", {
23676
+ className: "mt-1 text-sm text-kumo-default",
23677
+ children: _t({
23678
+ id: "RwFsM0",
23679
+ message: "This release requires a newer environment than your site currently runs. Upgrade before installing."
23680
+ })
23681
+ }),
23682
+ /* @__PURE__ */ jsx("ul", {
23683
+ className: "mt-2 space-y-1 text-sm text-kumo-default",
23684
+ children: envMismatches.map((m) => /* @__PURE__ */ jsx("li", { children: _t({
23685
+ id: "XMcEUU",
23686
+ message: "{0} {1} required — you have {2}.",
23687
+ values: {
23688
+ 0: envLabel(m.key),
23689
+ 1: m.required,
23690
+ 2: m.host
23691
+ }
23692
+ }) }, m.key))
23693
+ })
23694
+ ] })]
23695
+ }) : null,
22704
23696
  description ? /* @__PURE__ */ jsx("p", {
22705
23697
  className: "text-base text-kumo-default",
22706
23698
  children: description
22707
23699
  }) : null,
23700
+ screenshots.length > 0 ? /* @__PURE__ */ jsx("section", {
23701
+ "aria-label": _t({
23702
+ id: "lqROaa",
23703
+ message: "Screenshots"
23704
+ }),
23705
+ children: /* @__PURE__ */ jsx("ul", {
23706
+ className: "flex snap-x gap-3 overflow-x-auto pb-2",
23707
+ children: screenshots.map((shot, i) => /* @__PURE__ */ jsx("li", {
23708
+ className: "shrink-0 snap-start",
23709
+ children: /* @__PURE__ */ jsx("img", {
23710
+ src: shot.src,
23711
+ alt: _t({
23712
+ id: "3rcds3",
23713
+ message: "Screenshot {0}",
23714
+ values: { 0: i + 1 }
23715
+ }),
23716
+ width: shot.width,
23717
+ height: shot.height,
23718
+ className: "h-48 w-auto rounded-lg border border-kumo-default object-cover",
23719
+ loading: "lazy"
23720
+ })
23721
+ }, shot.index))
23722
+ })
23723
+ }) : null,
22708
23724
  licenseText || repoHref || keywordList.length > 0 ? /* @__PURE__ */ jsxs("section", {
22709
23725
  className: "flex flex-wrap items-center gap-2",
22710
23726
  children: [
@@ -22787,6 +23803,27 @@ function RegistryPluginDetail({ pluginId, config }) {
22787
23803
  className: "mt-2 flex flex-wrap gap-2",
22788
23804
  children: capabilities.map((c) => /* @__PURE__ */ jsx(Badge, { children: c }, c))
22789
23805
  })] }) : null,
23806
+ sections.length > 0 && activePane ? /* @__PURE__ */ jsxs("section", {
23807
+ "aria-label": _t({
23808
+ id: "TYxt+3",
23809
+ message: "Plugin details"
23810
+ }),
23811
+ children: [sections.length > 1 ? /* @__PURE__ */ jsx(Tabs, {
23812
+ variant: "underline",
23813
+ value: activeSection,
23814
+ onValueChange: (v) => setSelectedSection(v),
23815
+ tabs: sections.map((s) => ({
23816
+ value: s.key,
23817
+ label: _t(SECTION_LABELS[s.key])
23818
+ }))
23819
+ }) : /* @__PURE__ */ jsx("h2", {
23820
+ className: "text-sm font-semibold text-kumo-subtle",
23821
+ children: _t(SECTION_LABELS[activePane.key])
23822
+ }), /* @__PURE__ */ jsx("div", {
23823
+ className: "prose prose-sm mt-4 max-w-none rounded-lg border bg-kumo-base p-6",
23824
+ children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderMarkdown(activePane.markdown) } })
23825
+ })]
23826
+ }) : null,
22790
23827
  showConsent && release ? /* @__PURE__ */ jsx(CapabilityConsentDialog, {
22791
23828
  mode: "install",
22792
23829
  pluginName: displayName ?? slug,
@@ -22802,6 +23839,28 @@ function RegistryPluginDetail({ pluginId, config }) {
22802
23839
  ]
22803
23840
  });
22804
23841
  }
23842
+ const SECTION_LABELS = {
23843
+ description: {
23844
+ id: "Nu4oKW",
23845
+ message: "Description"
23846
+ },
23847
+ installation: {
23848
+ id: "Xry+oU",
23849
+ message: "Installation"
23850
+ },
23851
+ faq: {
23852
+ id: "/lDBHm",
23853
+ message: "FAQ"
23854
+ },
23855
+ changelog: {
23856
+ id: "CWe7wB",
23857
+ message: "Changelog"
23858
+ },
23859
+ security: {
23860
+ id: "a3LDKx",
23861
+ message: "Security"
23862
+ }
23863
+ };
22805
23864
  function BackLink() {
22806
23865
  const { _: _t2 } = useLingui();
22807
23866
  return /* @__PURE__ */ jsxs(Link$1, {
@@ -22833,6 +23892,43 @@ function declaredAccessToCapabilityList(declaredAccess) {
22833
23892
  }
22834
23893
  return out;
22835
23894
  }
23895
+ const PRE_RELEASE_VERSION_RE = /^\d+\.\d+\.\d+-/;
23896
+ /**
23897
+ * Detects semver pre-release identifiers (`1.0.0-alpha.1`, `2.0.0-rc.2`). The
23898
+ * release lexicon does not enforce semver shape, so a permissive `includes("-")`
23899
+ * check would light up the badge for malformed values like `-1.0.0` or
23900
+ * `abc-def`. Require a `MAJOR.MINOR.PATCH-` prefix instead.
23901
+ */
23902
+ function isPreReleaseVersion(version) {
23903
+ return PRE_RELEASE_VERSION_RE.test(version);
23904
+ }
23905
+ /**
23906
+ * Human-readable name for a `requires` env key. The known EmDash environments
23907
+ * get their proper product names; anything else falls back to the key with the
23908
+ * `env:` prefix stripped (product names, not localised strings).
23909
+ */
23910
+ function envLabel(key) {
23911
+ if (key === "env:emdash") return "EmDash";
23912
+ if (key === "env:astro") return "Astro";
23913
+ return key.startsWith("env:") ? key.slice(4) : key;
23914
+ }
23915
+ const YANKED_LABEL_VALUE = "security:yanked";
23916
+ /**
23917
+ * Aggregators forward labels applied by their configured labellers. `security:yanked`
23918
+ * is a hard-enforcement label that publishers can self-apply (or that a labeller
23919
+ * applies on their behalf) to retract a release after publication. Sites whose
23920
+ * `acceptLabelers` config includes the labeller never see yanked releases at all
23921
+ * (server filtering), but sites without it receive yanked releases interleaved
23922
+ * with installable ones — filter them out so they never reach the picker.
23923
+ *
23924
+ * `neg` (negated labels) is intentionally ignored to match the server install
23925
+ * handler, which only checks `l.val === "security:yanked"`. Diverging here would
23926
+ * let the UI surface an install affordance the server will reject with
23927
+ * `RELEASE_YANKED`. Honoring `neg` on both sides is a separate follow-up.
23928
+ */
23929
+ function isYanked(release) {
23930
+ return (release.labels ?? []).some((l) => l.val === YANKED_LABEL_VALUE);
23931
+ }
22836
23932
  function formatDate(iso) {
22837
23933
  try {
22838
23934
  return new Date(iso).toLocaleDateString();
@@ -28147,15 +29243,6 @@ function SidebarNav({ manifest }) {
28147
29243
  border-top: 1px solid rgba(255,255,255,0.08);
28148
29244
  }
28149
29245
 
28150
- /* Keep all nav icons visible when sidebar collapses to icon mode */
28151
- .emdash-sidebar[data-state="collapsed"] [data-sidebar="group-content"] {
28152
- grid-template-rows: 1fr !important;
28153
- }
28154
- /* Mobile drawer: kumo's Sheet has no data-state attribute, so group-content
28155
- stays at grid-rows-[0fr] (hidden). Force it open in the mobile sidebar. */
28156
- .emdash-sidebar[data-mobile="true"] [data-sidebar="group-content"] {
28157
- grid-template-rows: 1fr !important;
28158
- }
28159
29246
  /* Collapsed separators — thin centered line */
28160
29247
  .emdash-sidebar[data-state="collapsed"] [data-sidebar="separator"] {
28161
29248
  margin: 0.375rem 0.625rem;
@@ -28239,52 +29326,36 @@ function SidebarNav({ manifest }) {
28239
29326
  isActive: isItemActive("/", currentPath)
28240
29327
  }) }) }),
28241
29328
  /* @__PURE__ */ jsx(KumoSidebar.Separator, {}),
28242
- visibleContent.length > 1 && /* @__PURE__ */ jsxs(KumoSidebar.Group, {
28243
- collapsible: true,
28244
- defaultOpen: true,
28245
- children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
28246
- className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
28247
- children: _t({
28248
- id: "4b3oEV",
28249
- message: "Content"
28250
- })
28251
- }), /* @__PURE__ */ jsx(KumoSidebar.GroupContent, { children: /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleContent.filter((i) => i.to !== "/")) }) })]
28252
- }),
29329
+ visibleContent.length > 1 && /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
29330
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
29331
+ children: _t({
29332
+ id: "4b3oEV",
29333
+ message: "Content"
29334
+ })
29335
+ }), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleContent.filter((i) => i.to !== "/")) })] }),
28253
29336
  /* @__PURE__ */ jsx(KumoSidebar.Separator, {}),
28254
- visibleManage.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, {
28255
- collapsible: true,
28256
- defaultOpen: true,
28257
- children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
28258
- className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
28259
- children: _t({
28260
- id: "wckWOP",
28261
- message: "Manage"
28262
- })
28263
- }), /* @__PURE__ */ jsx(KumoSidebar.GroupContent, { children: /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleManage) }) })]
28264
- }),
29337
+ visibleManage.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
29338
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
29339
+ children: _t({
29340
+ id: "wckWOP",
29341
+ message: "Manage"
29342
+ })
29343
+ }), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleManage) })] }),
28265
29344
  /* @__PURE__ */ jsx(KumoSidebar.Separator, {}),
28266
- visibleAdmin.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, {
28267
- collapsible: true,
28268
- defaultOpen: true,
28269
- children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
28270
- className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
28271
- children: _t({
28272
- id: "U3pytU",
28273
- message: "Admin"
28274
- })
28275
- }), /* @__PURE__ */ jsx(KumoSidebar.GroupContent, { children: /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleAdmin) }) })]
28276
- }),
28277
- visiblePlugins.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(KumoSidebar.Separator, {}), /* @__PURE__ */ jsxs(KumoSidebar.Group, {
28278
- collapsible: true,
28279
- defaultOpen: true,
28280
- children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
28281
- className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
28282
- children: _t({
28283
- id: "ohUJJM",
28284
- message: "Plugins"
28285
- })
28286
- }), /* @__PURE__ */ jsx(KumoSidebar.GroupContent, { children: /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visiblePlugins) }) })]
28287
- })] })
29345
+ visibleAdmin.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
29346
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
29347
+ children: _t({
29348
+ id: "U3pytU",
29349
+ message: "Admin"
29350
+ })
29351
+ }), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleAdmin) })] }),
29352
+ visiblePlugins.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(KumoSidebar.Separator, {}), /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
29353
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
29354
+ children: _t({
29355
+ id: "ohUJJM",
29356
+ message: "Plugins"
29357
+ })
29358
+ }), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visiblePlugins) })] })] })
28288
29359
  ] }),
28289
29360
  /* @__PURE__ */ jsx(KumoSidebar.Footer, { children: /* @__PURE__ */ jsxs("p", {
28290
29361
  className: "emdash-nav-label px-3 py-2 text-[11px] text-white/30",
@@ -29522,22 +30593,23 @@ function TermFormDialog({ open, onClose, taxonomyName, taxonomyDef, term, allTer
29522
30593
  className: "flex items-start justify-between gap-4 mb-4",
29523
30594
  children: [/* @__PURE__ */ jsxs("div", {
29524
30595
  className: "flex flex-col space-y-1.5",
29525
- children: [/* @__PURE__ */ jsxs(Dialog.Title, {
30596
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
29526
30597
  className: "text-lg font-semibold leading-none tracking-tight",
29527
- children: [
29528
- term ? _t3({
29529
- id: "ePK91l",
29530
- message: "Edit"
29531
- }) : _t3({
29532
- id: "m16xKo",
29533
- message: "Add"
29534
- }),
29535
- " ",
29536
- taxonomyDef.labelSingular || _t3({
30598
+ children: term ? _t3({
30599
+ id: "N6j2JH",
30600
+ message: "Edit {0}",
30601
+ values: { 0: taxonomyDef.labelSingular || _t3({
29537
30602
  id: "k3xOSk",
29538
30603
  message: "Term"
29539
- })
29540
- ]
30604
+ }) }
30605
+ }) : _t3({
30606
+ id: "3nLdaX",
30607
+ message: "Add {0}",
30608
+ values: { 0: taxonomyDef.labelSingular || _t3({
30609
+ id: "k3xOSk",
30610
+ message: "Term"
30611
+ }) }
30612
+ })
29541
30613
  }), /* @__PURE__ */ jsx(Dialog.Description, {
29542
30614
  className: "text-sm text-kumo-subtle",
29543
30615
  children: term ? _t3({
@@ -30062,20 +31134,17 @@ function TaxonomyManager({ taxonomyName }) {
30062
31134
  message: "New Taxonomy"
30063
31135
  })
30064
31136
  }),
30065
- /* @__PURE__ */ jsxs(Button, {
31137
+ /* @__PURE__ */ jsx(Button, {
30066
31138
  icon: /* @__PURE__ */ jsx(Plus, {}),
30067
31139
  onClick: () => setFormOpen(true),
30068
- children: [
30069
- _t5({
30070
- id: "m16xKo",
30071
- message: "Add"
30072
- }),
30073
- " ",
30074
- taxonomyDef.labelSingular || _t5({
31140
+ children: _t5({
31141
+ id: "3nLdaX",
31142
+ message: "Add {0}",
31143
+ values: { 0: taxonomyDef.labelSingular || _t5({
30075
31144
  id: "k3xOSk",
30076
31145
  message: "Term"
30077
- })
30078
- ]
31146
+ }) }
31147
+ })
30079
31148
  })
30080
31149
  ]
30081
31150
  })]
@@ -34218,6 +35287,13 @@ function AuthorMappingStep({ authorMappings, emdashUsers, onMappingChange, onCon
34218
35287
 
34219
35288
  //#endregion
34220
35289
  //#region src/routes/bylines.tsx
35290
+ /**
35291
+ * True when the load-more snapshot still matches the current filter state.
35292
+ * Used to discard appends from requests whose filters have changed mid-flight.
35293
+ */
35294
+ function loadMoreSnapshotMatches(snapshot, current) {
35295
+ return snapshot.search === current.search && snapshot.guestFilter === current.guestFilter && snapshot.locale === current.locale;
35296
+ }
34221
35297
  function toFormState(byline) {
34222
35298
  if (!byline) return {
34223
35299
  slug: "",
@@ -34243,21 +35319,39 @@ function getUserLabel(user) {
34243
35319
  function BylinesPage() {
34244
35320
  const { _: _t } = useLingui();
34245
35321
  const queryClient = useQueryClient();
35322
+ const navigate = useNavigate$1();
35323
+ const { locale: routeLocale } = useSearch({ from: "/_admin/bylines" });
34246
35324
  const [search, setSearch] = React$1.useState("");
34247
35325
  const [guestFilter, setGuestFilter] = React$1.useState("all");
34248
35326
  const [selectedId, setSelectedId] = React$1.useState(null);
34249
35327
  const [showDeleteConfirm, setShowDeleteConfirm] = React$1.useState(false);
34250
35328
  const [allItems, setAllItems] = React$1.useState([]);
34251
35329
  const [nextCursor, setNextCursor] = React$1.useState(void 0);
35330
+ const { data: manifest } = useQuery({
35331
+ queryKey: ["manifest"],
35332
+ queryFn: fetchManifest
35333
+ });
35334
+ const i18n = useI18nConfig(manifest);
35335
+ const isMultiLocale = !!i18n && i18n.locales.length > 1;
35336
+ const activeLocale = routeLocale ?? i18n?.defaultLocale ?? void 0;
35337
+ const handleLocaleChange = (locale) => {
35338
+ navigate({
35339
+ to: "/bylines",
35340
+ search: { locale: locale || void 0 }
35341
+ });
35342
+ setSelectedId(null);
35343
+ };
34252
35344
  const { data, isLoading, error } = useQuery({
34253
35345
  queryKey: [
34254
35346
  "bylines",
34255
35347
  search,
34256
- guestFilter
35348
+ guestFilter,
35349
+ activeLocale ?? null
34257
35350
  ],
34258
35351
  queryFn: () => fetchBylines({
34259
35352
  search: search || void 0,
34260
35353
  isGuest: guestFilter === "all" ? void 0 : guestFilter === "guest",
35354
+ locale: activeLocale,
34261
35355
  limit: 50
34262
35356
  })
34263
35357
  });
@@ -34273,28 +35367,44 @@ function BylinesPage() {
34273
35367
  });
34274
35368
  const users = usersData?.items ?? [];
34275
35369
  const loadMoreMutation = useMutation({
34276
- mutationFn: async () => {
34277
- if (!nextCursor) return null;
34278
- return fetchBylines({
34279
- search: search || void 0,
34280
- isGuest: guestFilter === "all" ? void 0 : guestFilter === "guest",
34281
- limit: 50,
34282
- cursor: nextCursor
34283
- });
35370
+ mutationFn: async (snapshot) => {
35371
+ return {
35372
+ result: await fetchBylines({
35373
+ search: snapshot.search || void 0,
35374
+ isGuest: snapshot.guestFilter === "all" ? void 0 : snapshot.guestFilter === "guest",
35375
+ locale: snapshot.locale,
35376
+ limit: 50,
35377
+ cursor: snapshot.cursor
35378
+ }),
35379
+ snapshot
35380
+ };
34284
35381
  },
34285
- onSuccess: (result) => {
34286
- if (result) {
34287
- setAllItems((prev) => [...prev, ...result.items]);
34288
- setNextCursor(result.nextCursor);
34289
- }
35382
+ onSuccess: ({ result, snapshot }) => {
35383
+ if (!loadMoreSnapshotMatches(snapshot, {
35384
+ search,
35385
+ guestFilter,
35386
+ locale: activeLocale
35387
+ })) return;
35388
+ setAllItems((prev) => [...prev, ...result.items]);
35389
+ setNextCursor(result.nextCursor);
34290
35390
  }
34291
35391
  });
34292
35392
  const items = allItems;
34293
- const selected = items.find((item) => item.id === selectedId) ?? null;
35393
+ const { data: selectedRemote } = useQuery({
35394
+ queryKey: ["byline", selectedId],
35395
+ queryFn: () => selectedId ? fetchByline(selectedId) : Promise.resolve(null),
35396
+ enabled: !!selectedId
35397
+ });
35398
+ const selected = selectedRemote ?? items.find((item) => item.id === selectedId) ?? null;
34294
35399
  const [form, setForm] = React$1.useState(() => toFormState(null));
34295
35400
  React$1.useEffect(() => {
34296
35401
  setForm(toFormState(selected));
34297
- }, [selectedId, selected]);
35402
+ }, [selected]);
35403
+ const { data: translationsData } = useQuery({
35404
+ queryKey: ["byline-translations", selectedId],
35405
+ queryFn: () => selectedId ? fetchBylineTranslations(selectedId) : Promise.resolve({ items: [] }),
35406
+ enabled: !!selectedId && isMultiLocale
35407
+ });
34298
35408
  const createMutation = useMutation({
34299
35409
  mutationFn: () => createByline({
34300
35410
  slug: form.slug,
@@ -34302,7 +35412,8 @@ function BylinesPage() {
34302
35412
  bio: form.bio || null,
34303
35413
  websiteUrl: form.websiteUrl || null,
34304
35414
  userId: form.userId,
34305
- isGuest: form.isGuest
35415
+ isGuest: form.isGuest,
35416
+ locale: activeLocale
34306
35417
  }),
34307
35418
  onSuccess: (created) => {
34308
35419
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
@@ -34323,6 +35434,7 @@ function BylinesPage() {
34323
35434
  },
34324
35435
  onSuccess: () => {
34325
35436
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
35437
+ if (selectedId) queryClient.invalidateQueries({ queryKey: ["byline", selectedId] });
34326
35438
  }
34327
35439
  });
34328
35440
  const deleteMutation = useMutation({
@@ -34336,6 +35448,26 @@ function BylinesPage() {
34336
35448
  setShowDeleteConfirm(false);
34337
35449
  }
34338
35450
  });
35451
+ const [pendingTranslationLocale, setPendingTranslationLocale] = React$1.useState(null);
35452
+ const translateMutation = useMutation({
35453
+ mutationFn: (targetLocale) => {
35454
+ if (!selectedId) throw new Error("No byline selected");
35455
+ setPendingTranslationLocale(targetLocale);
35456
+ return createBylineTranslation(selectedId, { locale: targetLocale });
35457
+ },
35458
+ onSettled: () => {
35459
+ setPendingTranslationLocale(null);
35460
+ },
35461
+ onSuccess: (created) => {
35462
+ queryClient.invalidateQueries({ queryKey: ["bylines"] });
35463
+ if (selectedId) queryClient.invalidateQueries({ queryKey: ["byline-translations", selectedId] });
35464
+ navigate({
35465
+ to: "/bylines",
35466
+ search: { locale: created.locale }
35467
+ });
35468
+ setSelectedId(created.id);
35469
+ }
35470
+ });
34339
35471
  if (isLoading) return /* @__PURE__ */ jsx("div", {
34340
35472
  className: "flex items-center justify-center min-h-[30vh]",
34341
35473
  children: /* @__PURE__ */ jsx(Loader, {})
@@ -34349,230 +35481,273 @@ function BylinesPage() {
34349
35481
  })
34350
35482
  });
34351
35483
  const isSaving = createMutation.isPending || updateMutation.isPending;
34352
- const mutationError = createMutation.error || updateMutation.error || deleteMutation.error;
35484
+ const mutationError = createMutation.error || updateMutation.error || deleteMutation.error || translateMutation.error;
34353
35485
  return /* @__PURE__ */ jsxs("div", {
34354
- className: "grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]",
35486
+ className: "space-y-4",
34355
35487
  children: [
35488
+ isMultiLocale && i18n && activeLocale ? /* @__PURE__ */ jsxs("div", {
35489
+ className: "flex items-center justify-between",
35490
+ children: [/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("h1", {
35491
+ className: "text-2xl font-semibold",
35492
+ children: _t({
35493
+ id: "xLGa7J",
35494
+ message: "Bylines"
35495
+ })
35496
+ }) }), /* @__PURE__ */ jsx(LocaleSwitcher, {
35497
+ locales: i18n.locales,
35498
+ defaultLocale: i18n.defaultLocale,
35499
+ value: activeLocale,
35500
+ onChange: handleLocaleChange
35501
+ })]
35502
+ }) : null,
34356
35503
  /* @__PURE__ */ jsxs("div", {
34357
- className: "rounded-lg border p-4",
35504
+ className: "grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]",
34358
35505
  children: [/* @__PURE__ */ jsxs("div", {
34359
- className: "mb-4 space-y-2",
34360
- children: [/* @__PURE__ */ jsx(Input, {
34361
- placeholder: _t({
34362
- id: "4L2LjM",
34363
- message: "Search bylines"
34364
- }),
34365
- value: search,
34366
- onChange: (e) => setSearch(e.target.value)
34367
- }), /* @__PURE__ */ jsxs("div", {
34368
- className: "flex items-center gap-2",
34369
- children: [/* @__PURE__ */ jsx("div", {
34370
- className: "flex-1",
34371
- children: /* @__PURE__ */ jsx(Select, {
34372
- "aria-label": _t({
34373
- id: "kYxs0a",
34374
- message: "Filter byline type"
34375
- }),
34376
- value: guestFilter,
34377
- onValueChange: (v) => setGuestFilter(v ?? "all"),
34378
- items: {
34379
- all: _t({
34380
- id: "VK3dVJ",
34381
- message: "All bylines"
34382
- }),
34383
- guest: _t({
34384
- id: "+0jFzA",
34385
- message: "Guest only"
35506
+ className: "rounded-lg border p-4",
35507
+ children: [/* @__PURE__ */ jsxs("div", {
35508
+ className: "mb-4 space-y-2",
35509
+ children: [/* @__PURE__ */ jsx(Input, {
35510
+ placeholder: _t({
35511
+ id: "4L2LjM",
35512
+ message: "Search bylines"
35513
+ }),
35514
+ value: search,
35515
+ onChange: (e) => setSearch(e.target.value)
35516
+ }), /* @__PURE__ */ jsxs("div", {
35517
+ className: "flex items-center gap-2",
35518
+ children: [/* @__PURE__ */ jsx("div", {
35519
+ className: "flex-1",
35520
+ children: /* @__PURE__ */ jsx(Select, {
35521
+ "aria-label": _t({
35522
+ id: "kYxs0a",
35523
+ message: "Filter byline type"
34386
35524
  }),
34387
- linked: _t({
34388
- id: "RZ5T1J",
34389
- message: "Linked only"
34390
- })
35525
+ value: guestFilter,
35526
+ onValueChange: (v) => setGuestFilter(v ?? "all"),
35527
+ items: {
35528
+ all: _t({
35529
+ id: "VK3dVJ",
35530
+ message: "All bylines"
35531
+ }),
35532
+ guest: _t({
35533
+ id: "+0jFzA",
35534
+ message: "Guest only"
35535
+ }),
35536
+ linked: _t({
35537
+ id: "RZ5T1J",
35538
+ message: "Linked only"
35539
+ })
35540
+ },
35541
+ className: "w-full"
35542
+ })
35543
+ }), /* @__PURE__ */ jsx(Button, {
35544
+ variant: "secondary",
35545
+ onClick: () => {
35546
+ setSelectedId(null);
35547
+ setForm(toFormState(null));
34391
35548
  },
34392
- className: "w-full"
34393
- })
34394
- }), /* @__PURE__ */ jsx(Button, {
34395
- variant: "secondary",
34396
- onClick: () => {
34397
- setSelectedId(null);
34398
- setForm(toFormState(null));
34399
- },
34400
- children: _t({
34401
- id: "isRobC",
34402
- message: "New"
34403
- })
35549
+ children: _t({
35550
+ id: "isRobC",
35551
+ message: "New"
35552
+ })
35553
+ })]
34404
35554
  })]
35555
+ }), /* @__PURE__ */ jsxs("div", {
35556
+ className: "space-y-2 max-h-[70vh] overflow-auto",
35557
+ children: [
35558
+ items.map((item) => {
35559
+ return /* @__PURE__ */ jsxs("button", {
35560
+ type: "button",
35561
+ onClick: () => setSelectedId(item.id),
35562
+ className: `w-full rounded border p-3 text-start ${item.id === selectedId ? "border-kumo-brand bg-kumo-brand/10" : "border-kumo-line"}`,
35563
+ children: [/* @__PURE__ */ jsx("p", {
35564
+ className: "font-medium",
35565
+ children: item.displayName
35566
+ }), /* @__PURE__ */ jsxs("p", {
35567
+ className: "text-xs text-kumo-subtle",
35568
+ children: [item.slug, item.isGuest ? _t({
35569
+ id: "89z9Fh",
35570
+ message: " - Guest"
35571
+ }) : item.userId ? _t({
35572
+ id: "X4uMa1",
35573
+ message: " - Linked"
35574
+ }) : ""]
35575
+ })]
35576
+ }, item.id);
35577
+ }),
35578
+ items.length === 0 && /* @__PURE__ */ jsx("p", {
35579
+ className: "text-sm text-kumo-subtle",
35580
+ children: _t({
35581
+ id: "Qc/a/A",
35582
+ message: "No bylines found"
35583
+ })
35584
+ }),
35585
+ nextCursor && /* @__PURE__ */ jsx(Button, {
35586
+ variant: "secondary",
35587
+ className: "w-full mt-2",
35588
+ onClick: () => loadMoreMutation.mutate({
35589
+ search,
35590
+ guestFilter,
35591
+ locale: activeLocale,
35592
+ cursor: nextCursor
35593
+ }),
35594
+ disabled: loadMoreMutation.isPending,
35595
+ children: loadMoreMutation.isPending ? _t({
35596
+ id: "Z3FXyt",
35597
+ message: "Loading..."
35598
+ }) : _t({
35599
+ id: "yQ2kGp",
35600
+ message: "Load more"
35601
+ })
35602
+ })
35603
+ ]
34405
35604
  })]
34406
35605
  }), /* @__PURE__ */ jsxs("div", {
34407
- className: "space-y-2 max-h-[70vh] overflow-auto",
35606
+ className: "rounded-lg border p-6 space-y-6",
34408
35607
  children: [
34409
- items.map((item) => {
34410
- return /* @__PURE__ */ jsxs("button", {
34411
- type: "button",
34412
- onClick: () => setSelectedId(item.id),
34413
- className: `w-full rounded border p-3 text-start ${item.id === selectedId ? "border-kumo-brand bg-kumo-brand/10" : "border-kumo-line"}`,
34414
- children: [/* @__PURE__ */ jsx("p", {
34415
- className: "font-medium",
34416
- children: item.displayName
34417
- }), /* @__PURE__ */ jsxs("p", {
34418
- className: "text-xs text-kumo-subtle",
34419
- children: [item.slug, item.isGuest ? _t({
34420
- id: "89z9Fh",
34421
- message: " - Guest"
34422
- }) : item.userId ? _t({
34423
- id: "X4uMa1",
34424
- message: " - Linked"
34425
- }) : ""]
34426
- })]
34427
- }, item.id);
34428
- }),
34429
- items.length === 0 && /* @__PURE__ */ jsx("p", {
34430
- className: "text-sm text-kumo-subtle",
34431
- children: _t({
34432
- id: "Qc/a/A",
34433
- message: "No bylines found"
34434
- })
34435
- }),
34436
- nextCursor && /* @__PURE__ */ jsx(Button, {
34437
- variant: "secondary",
34438
- className: "w-full mt-2",
34439
- onClick: () => loadMoreMutation.mutate(),
34440
- disabled: loadMoreMutation.isPending,
34441
- children: loadMoreMutation.isPending ? _t({
34442
- id: "Z3FXyt",
34443
- message: "Loading..."
35608
+ /* @__PURE__ */ jsx("h2", {
35609
+ className: "text-lg font-semibold",
35610
+ children: selected ? _t({
35611
+ id: "N6j2JH",
35612
+ message: "Edit {0}",
35613
+ values: { 0: selected.displayName }
34444
35614
  }) : _t({
34445
- id: "yQ2kGp",
34446
- message: "Load more"
35615
+ id: "d6HWlw",
35616
+ message: "Create byline"
34447
35617
  })
34448
- })
34449
- ]
34450
- })]
34451
- }),
34452
- /* @__PURE__ */ jsxs("div", {
34453
- className: "rounded-lg border p-6",
34454
- children: [/* @__PURE__ */ jsx("h2", {
34455
- className: "text-lg font-semibold mb-4",
34456
- children: selected ? _t({
34457
- id: "N6j2JH",
34458
- message: "Edit {0}",
34459
- values: { 0: selected.displayName }
34460
- }) : _t({
34461
- id: "d6HWlw",
34462
- message: "Create byline"
34463
- })
34464
- }), /* @__PURE__ */ jsxs("div", {
34465
- className: "space-y-4",
34466
- children: [
34467
- /* @__PURE__ */ jsx(Input, {
34468
- label: _t({
34469
- id: "pfa8F0",
34470
- message: "Display name"
34471
- }),
34472
- value: form.displayName,
34473
- onChange: (e) => setForm((prev) => ({
34474
- ...prev,
34475
- displayName: e.target.value
34476
- }))
34477
35618
  }),
34478
- /* @__PURE__ */ jsx(Input, {
34479
- label: _t({
34480
- id: "L85WcV",
34481
- message: "Slug"
34482
- }),
34483
- value: form.slug,
34484
- onChange: (e) => setForm((prev) => ({
34485
- ...prev,
34486
- slug: e.target.value
34487
- }))
34488
- }),
34489
- /* @__PURE__ */ jsx(Input, {
34490
- label: _t({
34491
- id: "XYcw7G",
34492
- message: "Website URL"
34493
- }),
34494
- value: form.websiteUrl,
34495
- onChange: (e) => setForm((prev) => ({
34496
- ...prev,
34497
- websiteUrl: e.target.value
34498
- }))
34499
- }),
34500
- /* @__PURE__ */ jsx(InputArea, {
34501
- label: _t({
34502
- id: "E+oKCO",
34503
- message: "Bio"
34504
- }),
34505
- value: form.bio,
34506
- onChange: (e) => setForm((prev) => ({
34507
- ...prev,
34508
- bio: e.target.value
34509
- })),
34510
- rows: 5
34511
- }),
34512
- /* @__PURE__ */ jsx(Select, {
34513
- label: _t({
34514
- id: "nti0N3",
34515
- message: "Linked user"
34516
- }),
34517
- value: form.userId ?? "",
34518
- onValueChange: (v) => {
34519
- const val = v || null;
34520
- setForm((prev) => ({
34521
- ...prev,
34522
- userId: val,
34523
- isGuest: val ? false : prev.isGuest
34524
- }));
34525
- },
34526
- items: {
34527
- "": _t({
34528
- id: "n9o2+L",
34529
- message: "No linked user"
35619
+ /* @__PURE__ */ jsxs("div", {
35620
+ className: "space-y-4",
35621
+ children: [
35622
+ /* @__PURE__ */ jsx(Input, {
35623
+ label: _t({
35624
+ id: "pfa8F0",
35625
+ message: "Display name"
35626
+ }),
35627
+ value: form.displayName,
35628
+ onChange: (e) => setForm((prev) => ({
35629
+ ...prev,
35630
+ displayName: e.target.value
35631
+ }))
34530
35632
  }),
34531
- ...Object.fromEntries(users.map((u) => [u.id, getUserLabel(u)]))
34532
- },
34533
- className: "w-full"
34534
- }),
34535
- /* @__PURE__ */ jsx(Switch, {
34536
- label: _t({
34537
- id: "6CRaxp",
34538
- message: "Guest byline"
34539
- }),
34540
- checked: form.isGuest,
34541
- onCheckedChange: (checked) => setForm((prev) => ({
34542
- ...prev,
34543
- isGuest: checked,
34544
- userId: checked ? null : prev.userId
34545
- }))
35633
+ /* @__PURE__ */ jsx(Input, {
35634
+ label: _t({
35635
+ id: "L85WcV",
35636
+ message: "Slug"
35637
+ }),
35638
+ value: form.slug,
35639
+ onChange: (e) => setForm((prev) => ({
35640
+ ...prev,
35641
+ slug: e.target.value
35642
+ }))
35643
+ }),
35644
+ /* @__PURE__ */ jsx(Input, {
35645
+ label: _t({
35646
+ id: "XYcw7G",
35647
+ message: "Website URL"
35648
+ }),
35649
+ value: form.websiteUrl,
35650
+ onChange: (e) => setForm((prev) => ({
35651
+ ...prev,
35652
+ websiteUrl: e.target.value
35653
+ }))
35654
+ }),
35655
+ /* @__PURE__ */ jsx(InputArea, {
35656
+ label: _t({
35657
+ id: "E+oKCO",
35658
+ message: "Bio"
35659
+ }),
35660
+ value: form.bio,
35661
+ onChange: (e) => setForm((prev) => ({
35662
+ ...prev,
35663
+ bio: e.target.value
35664
+ })),
35665
+ rows: 5
35666
+ }),
35667
+ /* @__PURE__ */ jsx(Select, {
35668
+ label: _t({
35669
+ id: "nti0N3",
35670
+ message: "Linked user"
35671
+ }),
35672
+ value: form.userId ?? "",
35673
+ onValueChange: (v) => {
35674
+ const val = v || null;
35675
+ setForm((prev) => ({
35676
+ ...prev,
35677
+ userId: val,
35678
+ isGuest: val ? false : prev.isGuest
35679
+ }));
35680
+ },
35681
+ items: {
35682
+ "": _t({
35683
+ id: "n9o2+L",
35684
+ message: "No linked user"
35685
+ }),
35686
+ ...Object.fromEntries(users.map((u) => [u.id, getUserLabel(u)]))
35687
+ },
35688
+ className: "w-full"
35689
+ }),
35690
+ /* @__PURE__ */ jsx(Switch, {
35691
+ label: _t({
35692
+ id: "6CRaxp",
35693
+ message: "Guest byline"
35694
+ }),
35695
+ checked: form.isGuest,
35696
+ onCheckedChange: (checked) => setForm((prev) => ({
35697
+ ...prev,
35698
+ isGuest: checked,
35699
+ userId: checked ? null : prev.userId
35700
+ }))
35701
+ }),
35702
+ /* @__PURE__ */ jsx(DialogError, { message: getMutationError(mutationError) }),
35703
+ /* @__PURE__ */ jsxs("div", {
35704
+ className: "flex gap-2 pt-2",
35705
+ children: [/* @__PURE__ */ jsx(Button, {
35706
+ onClick: () => {
35707
+ if (selected) updateMutation.mutate();
35708
+ else createMutation.mutate();
35709
+ },
35710
+ disabled: !form.displayName || !form.slug || isSaving,
35711
+ children: isSaving ? _t({
35712
+ id: "XvjC4F",
35713
+ message: "Saving..."
35714
+ }) : selected ? _t({
35715
+ id: "tfDRzk",
35716
+ message: "Save"
35717
+ }) : _t({
35718
+ id: "hYgDIe",
35719
+ message: "Create"
35720
+ })
35721
+ }), selected && /* @__PURE__ */ jsx(Button, {
35722
+ variant: "destructive",
35723
+ onClick: () => setShowDeleteConfirm(true),
35724
+ disabled: deleteMutation.isPending,
35725
+ children: _t({
35726
+ id: "cnGeoo",
35727
+ message: "Delete"
35728
+ })
35729
+ })]
35730
+ })
35731
+ ]
34546
35732
  }),
34547
- /* @__PURE__ */ jsx(DialogError, { message: getMutationError(mutationError) }),
34548
- /* @__PURE__ */ jsxs("div", {
34549
- className: "flex gap-2 pt-2",
34550
- children: [/* @__PURE__ */ jsx(Button, {
34551
- onClick: () => {
34552
- if (selected) updateMutation.mutate();
34553
- else createMutation.mutate();
35733
+ selected && isMultiLocale && i18n ? /* @__PURE__ */ jsx("div", {
35734
+ className: "border-t pt-6",
35735
+ children: /* @__PURE__ */ jsx(TranslationsPanel, {
35736
+ locales: i18n.locales,
35737
+ defaultLocale: i18n.defaultLocale,
35738
+ currentLocale: selected.locale,
35739
+ translations: translationsData?.items ?? [],
35740
+ onOpen: (summary) => {
35741
+ navigate({
35742
+ to: "/bylines",
35743
+ search: { locale: summary.locale }
35744
+ });
35745
+ setSelectedId(summary.id);
34554
35746
  },
34555
- disabled: !form.displayName || !form.slug || isSaving,
34556
- children: isSaving ? _t({
34557
- id: "XvjC4F",
34558
- message: "Saving..."
34559
- }) : selected ? _t({
34560
- id: "tfDRzk",
34561
- message: "Save"
34562
- }) : _t({
34563
- id: "hYgDIe",
34564
- message: "Create"
34565
- })
34566
- }), selected && /* @__PURE__ */ jsx(Button, {
34567
- variant: "destructive",
34568
- onClick: () => setShowDeleteConfirm(true),
34569
- disabled: deleteMutation.isPending,
34570
- children: _t({
34571
- id: "cnGeoo",
34572
- message: "Delete"
34573
- })
34574
- })]
34575
- })
35747
+ onCreate: (locale) => translateMutation.mutate(locale),
35748
+ pendingLocale: pendingTranslationLocale
35749
+ })
35750
+ }) : null
34576
35751
  ]
34577
35752
  })]
34578
35753
  }),
@@ -36140,10 +37315,11 @@ function ContentNewPage() {
36140
37315
  queryKey: ["manifest"],
36141
37316
  queryFn: fetchManifest
36142
37317
  });
37318
+ const pickerLocale = locale ?? manifest?.i18n?.defaultLocale;
36143
37319
  const createMutation = useMutation({
36144
37320
  mutationFn: (data) => createContent(collection, {
36145
37321
  ...data,
36146
- locale
37322
+ locale: pickerLocale
36147
37323
  }),
36148
37324
  onSuccess: (result) => {
36149
37325
  queryClient.invalidateQueries({ queryKey: ["content", collection] });
@@ -36157,14 +37333,23 @@ function ContentNewPage() {
36157
37333
  }
36158
37334
  });
36159
37335
  const pluginBlocks = React$1.useMemo(() => manifest ? getPluginBlocks(manifest) : [], [manifest]);
36160
- const { data: bylinesData } = useQuery({
36161
- queryKey: ["bylines"],
36162
- queryFn: () => fetchBylines({ limit: 100 })
37336
+ const { data: bylinesData, isSuccess: bylinesLoaded } = useQuery({
37337
+ queryKey: [
37338
+ "bylines",
37339
+ "picker",
37340
+ pickerLocale ?? null
37341
+ ],
37342
+ queryFn: () => fetchBylines({
37343
+ locale: pickerLocale,
37344
+ limit: 100
37345
+ }),
37346
+ enabled: !!manifest
36163
37347
  });
36164
37348
  const createBylineMutation = useMutation({
36165
37349
  mutationFn: (input) => createByline({
36166
37350
  ...input,
36167
- isGuest: true
37351
+ isGuest: true,
37352
+ locale: pickerLocale
36168
37353
  }),
36169
37354
  onSuccess: () => {
36170
37355
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
@@ -36190,10 +37375,13 @@ function ContentNewPage() {
36190
37375
  collectionLabel: collectionConfig.labelSingular || collectionConfig.label,
36191
37376
  fields: collectionConfig.fields,
36192
37377
  isNew: true,
37378
+ entryLocale: pickerLocale,
37379
+ i18n: manifest?.i18n,
36193
37380
  isSaving: createMutation.isPending,
36194
37381
  onSave: handleSave,
36195
37382
  pluginBlocks,
36196
37383
  availableBylines: bylinesData?.items,
37384
+ availableBylinesLoaded: bylinesLoaded,
36197
37385
  selectedBylines,
36198
37386
  onBylinesChange: setSelectedBylines,
36199
37387
  onQuickCreateByline: async (input) => {
@@ -36303,14 +37491,24 @@ function ContentEditPage() {
36303
37491
  enabled: !!currentUser && currentUser.role >= ROLE_EDITOR,
36304
37492
  staleTime: 300 * 1e3
36305
37493
  });
36306
- const { data: bylinesData } = useQuery({
36307
- queryKey: ["bylines"],
36308
- queryFn: () => fetchBylines({ limit: 100 })
37494
+ const itemLocale = rawItem?.locale ?? void 0;
37495
+ const { data: bylinesData, isSuccess: bylinesLoaded } = useQuery({
37496
+ queryKey: [
37497
+ "bylines",
37498
+ "picker",
37499
+ itemLocale ?? null
37500
+ ],
37501
+ queryFn: () => fetchBylines({
37502
+ locale: itemLocale,
37503
+ limit: 100
37504
+ }),
37505
+ enabled: !!itemLocale
36309
37506
  });
36310
37507
  const createBylineMutation = useMutation({
36311
37508
  mutationFn: (input) => createByline({
36312
37509
  ...input,
36313
- isGuest: true
37510
+ isGuest: true,
37511
+ locale: itemLocale
36314
37512
  }),
36315
37513
  onSuccess: () => {
36316
37514
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
@@ -36693,6 +37891,7 @@ function ContentEditPage() {
36693
37891
  hasSeo: collectionConfig.hasSeo,
36694
37892
  onSeoChange: handleSeoChange,
36695
37893
  availableBylines: bylinesData?.items,
37894
+ availableBylinesLoaded: bylinesLoaded,
36696
37895
  onQuickCreateByline: async (input) => {
36697
37896
  return await createBylineMutation.mutateAsync(input);
36698
37897
  },
@@ -36985,7 +38184,7 @@ function MarketplaceBrowsePage() {
36985
38184
  const { data: plugins } = useQuery({
36986
38185
  queryKey: ["plugins"],
36987
38186
  queryFn: async () => {
36988
- const { fetchPlugins } = await import("./plugins-qrjvPK4g.js").then((n) => n.a);
38187
+ const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
36989
38188
  return fetchPlugins();
36990
38189
  }
36991
38190
  });
@@ -37016,7 +38215,7 @@ function MarketplaceDetailPage() {
37016
38215
  const { data: plugins } = useQuery({
37017
38216
  queryKey: ["plugins"],
37018
38217
  queryFn: async () => {
37019
- const { fetchPlugins } = await import("./plugins-qrjvPK4g.js").then((n) => n.a);
38218
+ const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
37020
38219
  return fetchPlugins();
37021
38220
  }
37022
38221
  });
@@ -37100,10 +38299,15 @@ const usersRoute = createRoute({
37100
38299
  path: "/users",
37101
38300
  component: UsersPage
37102
38301
  });
38302
+ function parseBylinesLocaleSearch(search) {
38303
+ if (typeof search.locale === "string" && search.locale.length > 0) return { locale: search.locale };
38304
+ return { locale: void 0 };
38305
+ }
37103
38306
  const bylinesRoute = createRoute({
37104
38307
  getParentRoute: () => adminLayoutRoute,
37105
38308
  path: "/bylines",
37106
- component: BylinesPage
38309
+ component: BylinesPage,
38310
+ validateSearch: parseBylinesLocaleSearch
37107
38311
  });
37108
38312
  const contentTypesListRoute = createRoute({
37109
38313
  getParentRoute: () => adminLayoutRoute,
@@ -37488,5 +38692,5 @@ function AdminApp({ pluginAdmins = EMPTY_PLUGINS, authProviders = EMPTY_AUTH_PRO
37488
38692
  }
37489
38693
 
37490
38694
  //#endregion
37491
- export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, AuthProviderProvider, CAPABILITY_LABELS, ContentEditor, ContentList, DEFAULT_LOCALE, Dashboard, EditorHeader, Header, Link, LoginPage, MediaLibrary, MediaPickerModal, PasskeyLogin, PasskeyRegistration, PluginAdminProvider, PortableTextEditor, SUPPORTED_LOCALES, SUPPORTED_LOCALE_CODES, SaveButton, Settings, SetupWizard, Shell, KumoSidebar as Sidebar, SidebarNav, analyzeWpPluginSite, analyzeWxr, apiFetch, bulkCommentAction, checkPluginUpdates, cn, compareRevisions, completeSignup, createAdminRouter, createAllowedDomain, createApiToken, createByline, createCollection, createContent, createField, createMenu, createMenuItem, createMenuTranslation, createRedirect, createSection, createTaxonomy, createTerm, createTermTranslation, createWidget, createWidgetArea, deleteAllowedDomain, deleteByline, deleteCollection, deleteComment, deleteContent, deleteField, deleteFromProvider, deleteMedia, deleteMenu, deleteMenuItem, deletePasskey, deleteRedirect, deleteSection, deleteTerm, deleteWidget, deleteWidgetArea, describeCapability, disablePlugin, disableUser, discardDraft, duplicateContent, enablePlugin, enableUser, executeWpPluginImport, executeWxrImport, fetch404Summary, fetchAllowedDomains, fetchApiTokens, fetchAuthMode, fetchByline, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenuTranslations, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, fetchTermTranslations, fetchTerms, fetchTheme, fetchTranslations, fetchTrashedContent, fetchUser, fetchUsers, fetchWidgetArea, fetchWidgetAreas, fetchWidgetComponents, generatePreviewUrl, getDraftStatus, getPreviewUrl, hasAllowedDomains, importWxrMedia, installMarketplacePlugin, inviteUser, parseApiResponse, permanentDeleteContent, prepareWxrImport, probeImportUrl, publishContent, registerOrphanedTable, renamePasskey, reorderFields, reorderMenuItems, reorderWidgets, requestSignup, resolveLocale, restoreContent, restoreRevision, revokeApiToken, rewriteContentUrls, scheduleContent, searchMarketplace, searchThemes, sendRecoveryLink, sendTestEmail, setSearchEnabled, throwResponseError, uninstallMarketplacePlugin, unpublishContent, unscheduleContent, updateAllowedDomain, updateByline, updateCollection, updateCommentStatus, updateContent, updateField, updateMarketplacePlugin, updateMedia, updateMenu, updateMenuItem, updateRedirect, updateSection, updateSettings, updateTerm, updateUser, updateWidget, uploadMedia, uploadToProvider, useAuthProviderList, useAuthProviders, useCurrentUser, useLocale, useNavigate, useParams, usePluginAdmins, usePluginField, usePluginHasPages, usePluginHasWidgets, usePluginPage, usePluginWidget, validateInviteToken, verifySignupToken };
38695
+ export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, AuthProviderProvider, CAPABILITY_LABELS, ContentEditor, ContentList, DEFAULT_LOCALE, Dashboard, EditorHeader, Header, Link, LoginPage, MediaLibrary, MediaPickerModal, PasskeyLogin, PasskeyRegistration, PluginAdminProvider, PortableTextEditor, SUPPORTED_LOCALES, SUPPORTED_LOCALE_CODES, SaveButton, Settings, SetupWizard, Shell, KumoSidebar as Sidebar, SidebarNav, analyzeWpPluginSite, analyzeWxr, apiFetch, bulkCommentAction, checkPluginUpdates, cn, compareRevisions, completeSignup, createAdminRouter, createAllowedDomain, createApiToken, createByline, createBylineTranslation, createCollection, createContent, createField, createMenu, createMenuItem, createMenuTranslation, createRedirect, createSection, createTaxonomy, createTerm, createTermTranslation, createWidget, createWidgetArea, deleteAllowedDomain, deleteByline, deleteCollection, deleteComment, deleteContent, deleteField, deleteFromProvider, deleteMedia, deleteMenu, deleteMenuItem, deletePasskey, deleteRedirect, deleteSection, deleteTerm, deleteWidget, deleteWidgetArea, describeCapability, disablePlugin, disableUser, discardDraft, duplicateContent, enablePlugin, enableUser, executeWpPluginImport, executeWxrImport, fetch404Summary, fetchAllowedDomains, fetchApiTokens, fetchAuthMode, fetchByline, fetchBylineTranslations, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenuTranslations, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, fetchTermTranslations, fetchTerms, fetchTheme, fetchTranslations, fetchTrashedContent, fetchUser, fetchUsers, fetchWidgetArea, fetchWidgetAreas, fetchWidgetComponents, generatePreviewUrl, getDraftStatus, getPreviewUrl, hasAllowedDomains, importWxrMedia, installMarketplacePlugin, inviteUser, parseApiResponse, permanentDeleteContent, prepareWxrImport, probeImportUrl, publishContent, registerOrphanedTable, renamePasskey, reorderFields, reorderMenuItems, reorderWidgets, requestSignup, resolveLocale, restoreContent, restoreRevision, revokeApiToken, rewriteContentUrls, scheduleContent, searchMarketplace, searchThemes, sendRecoveryLink, sendTestEmail, setSearchEnabled, throwResponseError, uninstallMarketplacePlugin, unpublishContent, unscheduleContent, updateAllowedDomain, updateByline, updateCollection, updateCommentStatus, updateContent, updateField, updateMarketplacePlugin, updateMedia, updateMenu, updateMenuItem, updateRedirect, updateSection, updateSettings, updateTerm, updateUser, updateWidget, uploadMedia, uploadToProvider, useAuthProviderList, useAuthProviders, useCurrentUser, useLocale, useNavigate, useParams, usePluginAdmins, usePluginField, usePluginHasPages, usePluginHasWidgets, usePluginPage, usePluginWidget, validateInviteToken, verifySignupToken };
37492
38696
  //# sourceMappingURL=index.js.map