@emdash-cms/admin 0.14.0 → 0.15.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-C-w4lOU2.js} +17 -17
  2. package/dist/{LocaleDirectionProvider-D32XwRVp.js.map → LocaleDirectionProvider-C-w4lOU2.js.map} +1 -1
  3. package/dist/index.d.ts +56 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1398 -563
  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-B1vNWgt7.js +6 -0
  25. package/dist/messages-B1vNWgt7.js.map +1 -0
  26. package/dist/messages-B4SjKBpv.js +6 -0
  27. package/dist/messages-B4SjKBpv.js.map +1 -0
  28. package/dist/messages-BMFLZDHo.js +6 -0
  29. package/dist/messages-BMFLZDHo.js.map +1 -0
  30. package/dist/messages-BWDz4uQu.js +6 -0
  31. package/dist/messages-BWDz4uQu.js.map +1 -0
  32. package/dist/messages-BXt6OWYC.js +6 -0
  33. package/dist/messages-BXt6OWYC.js.map +1 -0
  34. package/dist/messages-CA-O-TjM.js +6 -0
  35. package/dist/messages-CA-O-TjM.js.map +1 -0
  36. package/dist/messages-CX3dih9c.js +6 -0
  37. package/dist/messages-CX3dih9c.js.map +1 -0
  38. package/dist/messages-CXZHQHKt.js +6 -0
  39. package/dist/messages-CXZHQHKt.js.map +1 -0
  40. package/dist/messages-Cz2f6Su-.js +6 -0
  41. package/dist/messages-Cz2f6Su-.js.map +1 -0
  42. package/dist/messages-DW1BWiJ5.js +6 -0
  43. package/dist/messages-DW1BWiJ5.js.map +1 -0
  44. package/dist/messages-DpMKfmmV.js +6 -0
  45. package/dist/messages-DpMKfmmV.js.map +1 -0
  46. package/dist/messages-hYPn9ilH.js +6 -0
  47. package/dist/messages-hYPn9ilH.js.map +1 -0
  48. package/dist/messages-qYhBOhtE.js +6 -0
  49. package/dist/messages-qYhBOhtE.js.map +1 -0
  50. package/dist/messages-rxCTcw_6.js +6 -0
  51. package/dist/messages-rxCTcw_6.js.map +1 -0
  52. package/dist/messages-wpqHOBfn.js +6 -0
  53. package/dist/messages-wpqHOBfn.js.map +1 -0
  54. package/dist/messages-zuRDwn8n.js +6 -0
  55. package/dist/messages-zuRDwn8n.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 +1 -1
  59. package/package.json +5 -4
  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-C-w4lOU2.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,12 +27,13 @@ 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";
@@ -1956,6 +1957,7 @@ async function fetchBylines(options) {
1956
1957
  if (options?.search) params.set("search", options.search);
1957
1958
  if (options?.isGuest !== void 0) params.set("isGuest", String(options.isGuest));
1958
1959
  if (options?.userId) params.set("userId", options.userId);
1960
+ if (options?.locale) params.set("locale", options.locale);
1959
1961
  if (options?.cursor) params.set("cursor", options.cursor);
1960
1962
  if (options?.limit) params.set("limit", String(options.limit));
1961
1963
  return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines${params.toString() ? `?${params}` : ""}`), "Failed to fetch bylines");
@@ -1984,6 +1986,26 @@ async function deleteByline(id) {
1984
1986
  message: "Failed to delete byline"
1985
1987
  }));
1986
1988
  }
1989
+ /**
1990
+ * Fetch every translation of a byline (siblings sharing the same
1991
+ * translation_group).
1992
+ */
1993
+ async function fetchBylineTranslations(id) {
1994
+ return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines/${id}/translations`), "Failed to fetch byline translations");
1995
+ }
1996
+ /**
1997
+ * Create a new locale variant of a byline. The new row joins the source's
1998
+ * `translation_group`. Body defaults — slug, display name, avatar, website —
1999
+ * inherit from the source when omitted, so editors only have to fill in the
2000
+ * localized bio (and optionally a localized display name).
2001
+ */
2002
+ async function createBylineTranslation(id, input) {
2003
+ return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines/${id}/translations`, {
2004
+ method: "POST",
2005
+ headers: { "Content-Type": "application/json" },
2006
+ body: JSON.stringify(input)
2007
+ }), "Failed to create byline translation");
2008
+ }
1987
2009
 
1988
2010
  //#endregion
1989
2011
  //#region src/lib/api/menus.ts
@@ -5525,6 +5547,392 @@ function Suggestion({ pluginKey = SuggestionPluginKey, editor, char = "@", allow
5525
5547
  }
5526
5548
  var index_default = Suggestion;
5527
5549
 
5550
+ //#endregion
5551
+ //#region src/components/editor/codeBlockLanguages.ts
5552
+ const CODE_BLOCK_LANGUAGES = [
5553
+ {
5554
+ id: "plaintext",
5555
+ label: "Plain text",
5556
+ aliases: [
5557
+ "text",
5558
+ "plain",
5559
+ "txt"
5560
+ ]
5561
+ },
5562
+ {
5563
+ id: "astro",
5564
+ label: "Astro"
5565
+ },
5566
+ {
5567
+ id: "bash",
5568
+ label: "Bash",
5569
+ aliases: [
5570
+ "sh",
5571
+ "shell",
5572
+ "zsh"
5573
+ ]
5574
+ },
5575
+ {
5576
+ id: "c",
5577
+ label: "C"
5578
+ },
5579
+ {
5580
+ id: "cpp",
5581
+ label: "C++",
5582
+ aliases: ["c++"]
5583
+ },
5584
+ {
5585
+ id: "csharp",
5586
+ label: "C#",
5587
+ aliases: ["cs", "c#"]
5588
+ },
5589
+ {
5590
+ id: "css",
5591
+ label: "CSS"
5592
+ },
5593
+ {
5594
+ id: "diff",
5595
+ label: "Diff",
5596
+ aliases: ["patch"]
5597
+ },
5598
+ {
5599
+ id: "dockerfile",
5600
+ label: "Dockerfile",
5601
+ aliases: ["docker"]
5602
+ },
5603
+ {
5604
+ id: "go",
5605
+ label: "Go",
5606
+ aliases: ["golang"]
5607
+ },
5608
+ {
5609
+ id: "graphql",
5610
+ label: "GraphQL",
5611
+ aliases: ["gql"]
5612
+ },
5613
+ {
5614
+ id: "html",
5615
+ label: "HTML"
5616
+ },
5617
+ {
5618
+ id: "java",
5619
+ label: "Java"
5620
+ },
5621
+ {
5622
+ id: "javascript",
5623
+ label: "JavaScript",
5624
+ aliases: ["js"]
5625
+ },
5626
+ {
5627
+ id: "json",
5628
+ label: "JSON"
5629
+ },
5630
+ {
5631
+ id: "jsx",
5632
+ label: "JSX"
5633
+ },
5634
+ {
5635
+ id: "kotlin",
5636
+ label: "Kotlin",
5637
+ aliases: ["kt"]
5638
+ },
5639
+ {
5640
+ id: "markdown",
5641
+ label: "Markdown",
5642
+ aliases: ["md"]
5643
+ },
5644
+ {
5645
+ id: "mdx",
5646
+ label: "MDX"
5647
+ },
5648
+ {
5649
+ id: "php",
5650
+ label: "PHP"
5651
+ },
5652
+ {
5653
+ id: "python",
5654
+ label: "Python",
5655
+ aliases: ["py"]
5656
+ },
5657
+ {
5658
+ id: "ruby",
5659
+ label: "Ruby",
5660
+ aliases: ["rb"]
5661
+ },
5662
+ {
5663
+ id: "rust",
5664
+ label: "Rust",
5665
+ aliases: ["rs"]
5666
+ },
5667
+ {
5668
+ id: "scss",
5669
+ label: "SCSS",
5670
+ aliases: ["sass"]
5671
+ },
5672
+ {
5673
+ id: "sql",
5674
+ label: "SQL"
5675
+ },
5676
+ {
5677
+ id: "svelte",
5678
+ label: "Svelte"
5679
+ },
5680
+ {
5681
+ id: "swift",
5682
+ label: "Swift"
5683
+ },
5684
+ {
5685
+ id: "toml",
5686
+ label: "TOML"
5687
+ },
5688
+ {
5689
+ id: "tsx",
5690
+ label: "TSX"
5691
+ },
5692
+ {
5693
+ id: "typescript",
5694
+ label: "TypeScript",
5695
+ aliases: ["ts"]
5696
+ },
5697
+ {
5698
+ id: "vue",
5699
+ label: "Vue"
5700
+ },
5701
+ {
5702
+ id: "xml",
5703
+ label: "XML"
5704
+ },
5705
+ {
5706
+ id: "yaml",
5707
+ label: "YAML",
5708
+ aliases: ["yml"]
5709
+ }
5710
+ ];
5711
+ /**
5712
+ * Look up a language by id or alias. Case-insensitive.
5713
+ * Returns the canonical entry, or `null` if not found in the curated list.
5714
+ */
5715
+ function findLanguage(value) {
5716
+ if (!value) return null;
5717
+ const needle = value.trim().toLowerCase();
5718
+ if (!needle) return null;
5719
+ for (const lang of CODE_BLOCK_LANGUAGES) {
5720
+ if (lang.id === needle) return lang;
5721
+ if (lang.aliases?.includes(needle)) return lang;
5722
+ }
5723
+ return null;
5724
+ }
5725
+ /**
5726
+ * Normalize a user-entered language string to a canonical id where possible.
5727
+ * Unknown inputs are sanitized to a single safe class token: lowercased,
5728
+ * trimmed, with any character outside `[a-z0-9_-]` (including whitespace,
5729
+ * dots, slashes, etc.) collapsed to `-`. This keeps the stored value safe
5730
+ * to interpolate into a `language-{id}` CSS class without splitting on
5731
+ * whitespace.
5732
+ *
5733
+ * Examples:
5734
+ * normalizeLanguage("TypeScript") -> "typescript" (canonical id)
5735
+ * normalizeLanguage("ts") -> "typescript" (alias)
5736
+ * normalizeLanguage("Objective C") -> "objective-c" (sanitized)
5737
+ * normalizeLanguage("F#") -> "f-" (sanitized)
5738
+ * normalizeLanguage("") -> undefined
5739
+ */
5740
+ const DISALLOWED_CHARS_RE = /[^a-z0-9_-]+/g;
5741
+ const LEADING_TRAILING_HYPHENS_RE = /^-+|-+$/g;
5742
+ function normalizeLanguage(value) {
5743
+ if (!value) return void 0;
5744
+ const trimmed = value.trim();
5745
+ if (!trimmed) return void 0;
5746
+ const match = findLanguage(trimmed);
5747
+ if (match) return match.id;
5748
+ return trimmed.toLowerCase().replace(DISALLOWED_CHARS_RE, "-").replace(LEADING_TRAILING_HYPHENS_RE, "") || void 0;
5749
+ }
5750
+ /**
5751
+ * Human-readable label for a stored language id. Falls back to the id itself
5752
+ * for unknown values so the editor never shows "undefined".
5753
+ */
5754
+ function languageLabel(value) {
5755
+ if (!value) return "Plain text";
5756
+ const match = findLanguage(value);
5757
+ if (match) return match.label;
5758
+ return value;
5759
+ }
5760
+
5761
+ //#endregion
5762
+ //#region src/components/editor/CodeBlockNode.tsx
5763
+ /**
5764
+ * Code block node with language picker.
5765
+ *
5766
+ * Wraps the base `@tiptap/extension-code-block` with a React node view that
5767
+ * overlays a small language chip in the top-right corner. Clicking the chip
5768
+ * opens a popover with a Kumo Autocomplete: a free-form text input plus a
5769
+ * filtered list of curated language suggestions. The value is persisted on
5770
+ * the node's `language` attribute and round-trips through Portable Text as
5771
+ * `block.language`.
5772
+ *
5773
+ * The picker accepts arbitrary strings (not restricted to the curated list)
5774
+ * so that less common languages can still be used. Free-form input is
5775
+ * sanitized to a single safe CSS class token via `normalizeLanguage` so the
5776
+ * frontend's `language-{id}` class stays well-formed.
5777
+ */
5778
+ const LANGUAGE_ITEMS = CODE_BLOCK_LANGUAGES.map((l) => l.label);
5779
+ function filterLanguages(item, query) {
5780
+ if (!query) return true;
5781
+ const needle = query.toLowerCase();
5782
+ const lang = CODE_BLOCK_LANGUAGES.find((l) => l.label === item);
5783
+ if (!lang) return false;
5784
+ if (lang.label.toLowerCase().includes(needle)) return true;
5785
+ if (lang.id.toLowerCase().includes(needle)) return true;
5786
+ return lang.aliases?.some((alias) => alias.toLowerCase().includes(needle)) ?? false;
5787
+ }
5788
+ function CodeBlockNodeView({ node, updateAttributes, selected }) {
5789
+ const { _: _t } = useLingui();
5790
+ const [isEditing, setIsEditing] = React$1.useState(false);
5791
+ const storedLanguage = typeof node.attrs.language === "string" ? node.attrs.language : "";
5792
+ const [draft, setDraft] = React$1.useState(() => languageLabel(storedLanguage));
5793
+ const popoverRef = React$1.useRef(null);
5794
+ React$1.useEffect(() => {
5795
+ if (!isEditing) setDraft(languageLabel(storedLanguage));
5796
+ }, [storedLanguage, isEditing]);
5797
+ const openPicker = React$1.useCallback(() => {
5798
+ setDraft(storedLanguage ? languageLabel(storedLanguage) : "");
5799
+ setIsEditing(true);
5800
+ }, [storedLanguage]);
5801
+ const closePicker = React$1.useCallback(() => {
5802
+ setIsEditing(false);
5803
+ setDraft(languageLabel(storedLanguage));
5804
+ }, [storedLanguage]);
5805
+ const commit = React$1.useCallback((value) => {
5806
+ updateAttributes({ language: normalizeLanguage(value ?? draft) ?? null });
5807
+ setIsEditing(false);
5808
+ }, [draft, updateAttributes]);
5809
+ const handleKeyDown = (e) => {
5810
+ if (e.key === "Enter") {
5811
+ e.preventDefault();
5812
+ commit();
5813
+ } else if (e.key === "Escape") {
5814
+ e.preventDefault();
5815
+ closePicker();
5816
+ }
5817
+ };
5818
+ React$1.useEffect(() => {
5819
+ if (!isEditing) return void 0;
5820
+ const onMouseDown = (event) => {
5821
+ const target = event.target instanceof Node ? event.target : null;
5822
+ if (popoverRef.current && target && !popoverRef.current.contains(target)) closePicker();
5823
+ };
5824
+ document.addEventListener("mousedown", onMouseDown);
5825
+ return () => document.removeEventListener("mousedown", onMouseDown);
5826
+ }, [isEditing, closePicker]);
5827
+ const label = languageLabel(storedLanguage);
5828
+ const chipPersistent = isEditing || Boolean(storedLanguage) || selected;
5829
+ return /* @__PURE__ */ jsxs(NodeViewWrapper, {
5830
+ className: "group relative my-4",
5831
+ "data-language": storedLanguage || void 0,
5832
+ children: [/* @__PURE__ */ jsx("pre", {
5833
+ className: "emdash-code-block",
5834
+ children: /* @__PURE__ */ jsx(NodeViewContent, { as: "code" })
5835
+ }), /* @__PURE__ */ jsx("div", {
5836
+ className: "absolute end-2 top-2 select-none",
5837
+ contentEditable: false,
5838
+ children: isEditing ? /* @__PURE__ */ jsxs("div", {
5839
+ ref: popoverRef,
5840
+ className: "flex items-center gap-1 rounded-md border bg-kumo-overlay p-1 shadow-lg",
5841
+ onKeyDown: handleKeyDown,
5842
+ children: [
5843
+ /* @__PURE__ */ jsxs(Autocomplete, {
5844
+ items: LANGUAGE_ITEMS,
5845
+ value: draft,
5846
+ onValueChange: (next) => setDraft(next),
5847
+ filter: filterLanguages,
5848
+ children: [/* @__PURE__ */ jsx(Autocomplete.InputGroup, {
5849
+ size: "sm",
5850
+ placeholder: _t({
5851
+ id: "vXIe7J",
5852
+ message: "Language"
5853
+ })
5854
+ }), /* @__PURE__ */ jsxs(Autocomplete.Content, {
5855
+ sideOffset: 4,
5856
+ children: [/* @__PURE__ */ jsx(Autocomplete.List, { children: (item) => /* @__PURE__ */ jsx(Autocomplete.Item, {
5857
+ value: item,
5858
+ children: item
5859
+ }, item) }), /* @__PURE__ */ jsx(Autocomplete.Empty, { children: _t({
5860
+ id: "4TVVsS",
5861
+ message: "No matches"
5862
+ }) })]
5863
+ })]
5864
+ }),
5865
+ /* @__PURE__ */ jsx(Button, {
5866
+ type: "button",
5867
+ variant: "ghost",
5868
+ shape: "square",
5869
+ className: "h-7 w-7",
5870
+ onMouseDown: (e) => e.preventDefault(),
5871
+ onClick: () => commit(),
5872
+ title: _t({
5873
+ id: "Ek42QX",
5874
+ message: "Apply language"
5875
+ }),
5876
+ "aria-label": _t({
5877
+ id: "Ek42QX",
5878
+ message: "Apply language"
5879
+ }),
5880
+ children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
5881
+ }),
5882
+ /* @__PURE__ */ jsx(Button, {
5883
+ type: "button",
5884
+ variant: "ghost",
5885
+ shape: "square",
5886
+ className: "h-7 w-7",
5887
+ onMouseDown: (e) => e.preventDefault(),
5888
+ onClick: closePicker,
5889
+ title: _t({
5890
+ id: "dEgA5A",
5891
+ message: "Cancel"
5892
+ }),
5893
+ "aria-label": _t({
5894
+ id: "dEgA5A",
5895
+ message: "Cancel"
5896
+ }),
5897
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
5898
+ })
5899
+ ]
5900
+ }) : /* @__PURE__ */ jsx("button", {
5901
+ type: "button",
5902
+ tabIndex: chipPersistent ? 0 : -1,
5903
+ onMouseDown: (e) => e.preventDefault(),
5904
+ onClick: openPicker,
5905
+ 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",
5906
+ "data-persistent": chipPersistent ? "true" : "false",
5907
+ title: _t({
5908
+ id: "9cbOH7",
5909
+ message: "Set language"
5910
+ }),
5911
+ "aria-label": _t({
5912
+ id: "q/A3pF",
5913
+ message: "Set language (current: {label})",
5914
+ values: { label }
5915
+ }),
5916
+ "aria-hidden": chipPersistent ? void 0 : true,
5917
+ children: storedLanguage ? label : _t({
5918
+ id: "9cbOH7",
5919
+ message: "Set language"
5920
+ })
5921
+ })
5922
+ })]
5923
+ });
5924
+ }
5925
+ /**
5926
+ * TipTap extension: code block with an inline language picker node view.
5927
+ *
5928
+ * Drop-in replacement for StarterKit's default `codeBlock`. Configure
5929
+ * `StarterKit.configure({ codeBlock: false })` and add this extension to
5930
+ * the editor's extensions array.
5931
+ */
5932
+ const CodeBlockExtension = CodeBlock$1.extend({ addNodeView() {
5933
+ return ReactNodeViewRenderer(CodeBlockNodeView);
5934
+ } });
5935
+
5528
5936
  //#endregion
5529
5937
  //#region src/components/editor/BlockMenu.tsx
5530
5938
  const blockTransforms = [
@@ -8153,6 +8561,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8153
8561
  color: "#3b82f6",
8154
8562
  width: 2
8155
8563
  },
8564
+ codeBlock: false,
8156
8565
  link: {
8157
8566
  openOnClick: false,
8158
8567
  enableClickSelection: true,
@@ -8160,6 +8569,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8160
8569
  },
8161
8570
  underline: {}
8162
8571
  }),
8572
+ CodeBlockExtension,
8163
8573
  ImageExtension,
8164
8574
  MarkdownLinkExtension,
8165
8575
  PluginBlockExtension,
@@ -8262,8 +8672,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8262
8672
  if (!editor || !pluginBlockModal) return;
8263
8673
  const { id, ...data } = values;
8264
8674
  const editPos = editingBlockPosRef.current;
8265
- if (editPos !== null) {
8266
- const { tr } = editor.state;
8675
+ if (editPos !== null) editor.chain().command(({ tr }) => {
8267
8676
  const node = tr.doc.nodeAt(editPos);
8268
8677
  if (node?.type.name === "pluginBlock") {
8269
8678
  tr.setNodeMarkup(editPos, void 0, {
@@ -8271,9 +8680,11 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
8271
8680
  id: typeof id === "string" ? id : node.attrs.id,
8272
8681
  data
8273
8682
  });
8274
- editor.view.dispatch(tr);
8683
+ return true;
8275
8684
  }
8276
- } else editor.chain().focus().insertContent({
8685
+ return false;
8686
+ }).run();
8687
+ else editor.chain().focus().insertContent({
8277
8688
  type: "pluginBlock",
8278
8689
  attrs: {
8279
8690
  blockType: pluginBlockModal.type,
@@ -10373,7 +10784,7 @@ function formatScheduledDate(dateStr) {
10373
10784
  /**
10374
10785
  * Content editor with dynamic field rendering
10375
10786
  */
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 }) {
10787
+ 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
10788
  const { _: _t } = useLingui();
10378
10789
  const navigate = useNavigate$1();
10379
10790
  const [formData, setFormData] = React$1.useState(item?.data || {});
@@ -10384,6 +10795,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10384
10795
  bylineId: entry.byline.id,
10385
10796
  roleLabel: entry.roleLabel
10386
10797
  })) ?? []);
10798
+ const [bylinesTouched, setBylinesTouched] = React$1.useState(false);
10387
10799
  const [portableTextEditor, setPortableTextEditor] = React$1.useState(null);
10388
10800
  const [blockSidebarPanel, setBlockSidebarPanel] = React$1.useState(null);
10389
10801
  const handleBlockSidebarOpen = React$1.useCallback((panel) => {
@@ -10425,6 +10837,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10425
10837
  bylines: nextBylines
10426
10838
  }));
10427
10839
  pendingAutosaveStateRef.current = null;
10840
+ setBylinesTouched(false);
10428
10841
  }
10429
10842
  const itemDataString = React$1.useMemo(() => item ? JSON.stringify(item.data) : "", [item?.data]);
10430
10843
  React$1.useEffect(() => {
@@ -10446,6 +10859,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10446
10859
  })) ?? []
10447
10860
  }));
10448
10861
  pendingAutosaveStateRef.current = null;
10862
+ setBylinesTouched(false);
10449
10863
  }
10450
10864
  }, [
10451
10865
  item?.updatedAt,
@@ -10455,6 +10869,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10455
10869
  ]);
10456
10870
  const activeBylines = isNew ? selectedBylines ?? [] : internalBylines;
10457
10871
  const handleBylinesChange = React$1.useCallback((next) => {
10872
+ setBylinesTouched(true);
10458
10873
  if (isNew) {
10459
10874
  onBylinesChange?.(next);
10460
10875
  return;
@@ -10497,13 +10912,13 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10497
10912
  if (hasInvalidUrls(formDataRef.current)) return;
10498
10913
  const payload = {
10499
10914
  data: formDataRef.current,
10500
- slug: slugRef.current || void 0,
10501
- bylines: activeBylines
10915
+ slug: slugRef.current || void 0
10502
10916
  };
10917
+ if (bylinesTouched) payload.bylines = activeBylines;
10503
10918
  pendingAutosaveStateRef.current = serializeEditorState({
10504
10919
  data: payload.data,
10505
10920
  slug: payload.slug || "",
10506
- bylines: payload.bylines
10921
+ bylines: activeBylines
10507
10922
  });
10508
10923
  onAutosave(payload);
10509
10924
  }, AUTOSAVE_DELAY);
@@ -10519,6 +10934,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10519
10934
  isSaving,
10520
10935
  isAutosaving,
10521
10936
  activeBylines,
10937
+ bylinesTouched,
10522
10938
  hasInvalidUrls
10523
10939
  ]);
10524
10940
  const handleSubmit = (e) => {
@@ -10528,11 +10944,12 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
10528
10944
  clearTimeout(autosaveTimeoutRef.current);
10529
10945
  autosaveTimeoutRef.current = null;
10530
10946
  }
10531
- onSave?.({
10947
+ const payload = {
10532
10948
  data: formData,
10533
- slug: slug || void 0,
10534
- bylines: activeBylines
10535
- });
10949
+ slug: slug || void 0
10950
+ };
10951
+ if (isNew || bylinesTouched) payload.bylines = activeBylines;
10952
+ onSave?.(payload);
10536
10953
  };
10537
10954
  const [isLoadingPreview, setIsLoadingPreview] = React$1.useState(false);
10538
10955
  const urlPattern = manifest?.collections[collection]?.urlPattern;
@@ -11066,9 +11483,12 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
11066
11483
  }), /* @__PURE__ */ jsx(BylineCreditsEditor, {
11067
11484
  credits: activeBylines,
11068
11485
  bylines: availableBylines ?? [],
11486
+ bylinesLoaded: availableBylinesLoaded,
11069
11487
  onChange: handleBylinesChange,
11070
11488
  onQuickCreate: onQuickCreateByline,
11071
- onQuickEdit: onQuickEditByline
11489
+ onQuickEdit: onQuickEditByline,
11490
+ entryLocale: item?.locale ?? entryLocale,
11491
+ i18n
11072
11492
  })]
11073
11493
  }),
11074
11494
  i18n && item && !isNew && /* @__PURE__ */ jsx("div", {
@@ -11725,7 +12145,7 @@ function FileFieldRenderer({ id, label, value, onChange, required, allowedMimeTy
11725
12145
  ]
11726
12146
  });
11727
12147
  }
11728
- function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuickEdit }) {
12148
+ function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuickEdit, entryLocale, i18n, bylinesLoaded = true }) {
11729
12149
  const { _: _t7 } = useLingui();
11730
12150
  const [selectedBylineId, setSelectedBylineId] = React$1.useState("");
11731
12151
  const [quickName, setQuickName] = React$1.useState("");
@@ -11768,6 +12188,27 @@ function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuic
11768
12188
  return /* @__PURE__ */ jsxs("div", {
11769
12189
  className: "space-y-3",
11770
12190
  children: [
12191
+ !!i18n && i18n.locales.length > 1 && bylinesLoaded && bylines.length === 0 && !!entryLocale && /* @__PURE__ */ jsxs("div", {
12192
+ className: "rounded border border-dashed p-3 text-sm space-y-2",
12193
+ children: [/* @__PURE__ */ jsx("p", {
12194
+ className: "text-kumo-subtle",
12195
+ children: _t7({
12196
+ id: "Tmb8Fw",
12197
+ message: "No bylines available in {entryLocale}. Create a variant from the Bylines page before crediting one on this entry.",
12198
+ values: { entryLocale }
12199
+ })
12200
+ }), /* @__PURE__ */ jsx(RouterLinkButton, {
12201
+ to: "/bylines",
12202
+ search: { locale: entryLocale ?? void 0 },
12203
+ variant: "secondary",
12204
+ size: "sm",
12205
+ children: _t7({
12206
+ id: "yXD9HA",
12207
+ message: "Manage bylines in {entryLocale}",
12208
+ values: { entryLocale }
12209
+ })
12210
+ })]
12211
+ }),
11771
12212
  /* @__PURE__ */ jsxs("div", {
11772
12213
  className: "flex gap-2",
11773
12214
  children: [/* @__PURE__ */ jsx(Select, {
@@ -17600,10 +18041,10 @@ function formatInstallCount(count) {
17600
18041
  * Lists each requested capability with a human-readable explanation.
17601
18042
  * User must explicitly confirm before the action proceeds.
17602
18043
  */
17603
- function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts, newCapabilities = [], auditVerdict, isPending = false, error, onConfirm, onCancel }) {
18044
+ function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts, newCapabilities = [], newlyPublicRoutes = [], auditVerdict, isPending = false, error, onConfirm, onCancel }) {
17604
18045
  const { _: _t } = useLingui();
17605
18046
  const newSet = new Set(newCapabilities);
17606
- const isUpdate = mode === "update" || newCapabilities.length > 0;
18047
+ const isUpdate = mode === "update" || newCapabilities.length > 0 || newlyPublicRoutes.length > 0;
17607
18048
  return /* @__PURE__ */ jsxs("div", {
17608
18049
  className: "fixed inset-0 z-50 flex items-center justify-center",
17609
18050
  role: "dialog",
@@ -17644,31 +18085,60 @@ function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts,
17644
18085
  }),
17645
18086
  /* @__PURE__ */ jsxs("div", {
17646
18087
  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"
18088
+ children: [
18089
+ capabilities.map((cap) => {
18090
+ const isNew = newSet.has(cap);
18091
+ return /* @__PURE__ */ jsxs("div", {
18092
+ 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"),
18093
+ 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", {
18094
+ className: cn(isNew && "font-medium"),
18095
+ children: describeCapability(cap, allowedHosts)
18096
+ }), isNew && /* @__PURE__ */ jsx("span", {
18097
+ className: "ms-2 text-xs text-warning font-medium",
18098
+ children: _t({
18099
+ id: "p6NueD",
18100
+ message: "NEW"
18101
+ })
18102
+ })] })]
18103
+ }, cap);
18104
+ }),
18105
+ newlyPublicRoutes.length > 0 && /* @__PURE__ */ jsxs("div", {
18106
+ className: "rounded-md border border-warning/30 bg-warning/10 p-3 text-sm",
18107
+ children: [
18108
+ /* @__PURE__ */ jsxs("div", {
18109
+ className: "flex items-center gap-2 font-medium text-warning",
18110
+ children: [/* @__PURE__ */ jsx(Warning, { className: "h-4 w-4 shrink-0" }), _t({
18111
+ id: "NXruDe",
18112
+ message: "New public routes"
18113
+ })]
18114
+ }),
18115
+ /* @__PURE__ */ jsx("p", {
18116
+ className: "mt-1 text-xs text-kumo-subtle",
18117
+ children: _t({
18118
+ id: "qCUL4n",
18119
+ message: "This update exposes the following routes without authentication:"
18120
+ })
18121
+ }),
18122
+ /* @__PURE__ */ jsx("ul", {
18123
+ className: "mt-2 space-y-1 ps-5 text-xs",
18124
+ children: newlyPublicRoutes.map((route) => /* @__PURE__ */ jsx("li", {
18125
+ className: "list-disc font-mono",
18126
+ children: route
18127
+ }, route))
17659
18128
  })
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
- })]
18129
+ ]
18130
+ }),
18131
+ auditVerdict && auditVerdict !== "pass" && /* @__PURE__ */ jsxs("div", {
18132
+ 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"),
18133
+ 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({
18134
+ id: "zPUuMZ",
18135
+ message: "Security audit flagged potential concerns with this plugin."
18136
+ }) : _t({
18137
+ id: "nvVzbM",
18138
+ message: "Security audit flagged this plugin as potentially unsafe."
18139
+ }) })]
18140
+ })
18141
+ ]
17672
18142
  }),
17673
18143
  /* @__PURE__ */ jsx(DialogError, {
17674
18144
  message: error,
@@ -17707,6 +18177,332 @@ function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts,
17707
18177
  });
17708
18178
  }
17709
18179
 
18180
+ //#endregion
18181
+ //#region src/lib/api/registry.ts
18182
+ let cachedDiscovery = null;
18183
+ async function getDiscoveryClient(config) {
18184
+ if (cachedDiscovery && cachedDiscovery.config.aggregatorUrl === config.aggregatorUrl && cachedDiscovery.config.acceptLabelers === config.acceptLabelers) return cachedDiscovery.client;
18185
+ const DiscoveryClient = (await import("@emdash-cms/registry-client/discovery")).DiscoveryClient;
18186
+ const discovery = new DiscoveryClient({
18187
+ aggregatorUrl: config.aggregatorUrl,
18188
+ acceptLabelers: config.acceptLabelers
18189
+ });
18190
+ const wrapped = {
18191
+ async searchPackages(opts) {
18192
+ return discovery.searchPackages({
18193
+ q: opts.q,
18194
+ cursor: opts.cursor,
18195
+ limit: opts.limit
18196
+ });
18197
+ },
18198
+ async resolvePackage(handle, slug) {
18199
+ return discovery.resolvePackage({
18200
+ handle,
18201
+ slug
18202
+ });
18203
+ },
18204
+ async getPackage(did, slug) {
18205
+ return discovery.getPackage({
18206
+ did,
18207
+ slug
18208
+ });
18209
+ },
18210
+ async getLatestRelease(did, slug) {
18211
+ return discovery.getLatestRelease({
18212
+ did,
18213
+ package: slug
18214
+ });
18215
+ },
18216
+ async listReleases(did, slug, opts) {
18217
+ return discovery.listReleases({
18218
+ did,
18219
+ package: slug,
18220
+ cursor: opts?.cursor,
18221
+ limit: opts?.limit
18222
+ });
18223
+ }
18224
+ };
18225
+ cachedDiscovery = {
18226
+ config,
18227
+ client: wrapped
18228
+ };
18229
+ return wrapped;
18230
+ }
18231
+ /**
18232
+ * Returns whether a release should be considered installable given the
18233
+ * configured policy. Currently implements the minimum-release-age check
18234
+ * described in RFC 0001's "Pre-label gap and launch tempo" section,
18235
+ * plus the `minimumReleaseAgeExclude` allowlist.
18236
+ *
18237
+ * Returns `false` (release blocked) when the policy is configured but
18238
+ * the release is missing a valid `indexedAt` -- we fail closed rather
18239
+ * than silently letting unbounded-age releases through.
18240
+ */
18241
+ function releasePassesPolicy(release, pkg, policy, now = Date.now()) {
18242
+ if (!policy?.minimumReleaseAgeSeconds) return true;
18243
+ if (releaseExemptFromMinimumAge(policy.minimumReleaseAgeExclude, pkg.did, pkg.slug)) return true;
18244
+ const indexedAt = Date.parse(release.indexedAt);
18245
+ if (!Number.isFinite(indexedAt)) return false;
18246
+ return (now - indexedAt) / 1e3 >= policy.minimumReleaseAgeSeconds;
18247
+ }
18248
+ /**
18249
+ * Canonicalize a capabilities list for set-style comparison. Mirrors
18250
+ * the server-side helper `canonicalCapabilitiesForDriftCheck` in
18251
+ * `packages/core/src/registry/config.ts` -- both sides must produce
18252
+ * the same canonical shape so the install handler's drift check is
18253
+ * stable across reorderings, duplicates, and junk entries.
18254
+ *
18255
+ * Filters non-strings, deduplicates, and sorts lexically.
18256
+ */
18257
+ function canonicalCapabilitiesForDriftCheck(value) {
18258
+ if (!Array.isArray(value)) return [];
18259
+ const seen = /* @__PURE__ */ new Set();
18260
+ for (const entry of value) if (typeof entry === "string" && entry.length > 0) seen.add(entry);
18261
+ return [...seen].toSorted();
18262
+ }
18263
+ /**
18264
+ * Matches a `(publisher_did, slug)` against the
18265
+ * `minimumReleaseAgeExclude` allowlist. Mirrors the server-side helper
18266
+ * of the same name in `packages/core/src/registry/config.ts`.
18267
+ *
18268
+ * DID-only on purpose: handles are aggregator-supplied envelope data
18269
+ * and accepting them as a trust input would let a compromised
18270
+ * aggregator bypass the holdback by claiming any handle for any
18271
+ * package. DIDs are tied to the AT URI of the record itself.
18272
+ *
18273
+ * Entries from the config list have already been lowercased at
18274
+ * manifest build time, so this only needs to lowercase the runtime
18275
+ * values for comparison.
18276
+ */
18277
+ function releaseExemptFromMinimumAge(exclude, publisherDid, slug) {
18278
+ if (!exclude || exclude.length === 0) return false;
18279
+ const didLower = publisherDid.toLowerCase();
18280
+ const fullDid = `${didLower}/${slug.toLowerCase()}`;
18281
+ for (const entry of exclude) {
18282
+ if (entry === didLower) return true;
18283
+ if (entry === fullDid) return true;
18284
+ }
18285
+ return false;
18286
+ }
18287
+ async function searchRegistryPackages(config, opts) {
18288
+ return (await getDiscoveryClient(config)).searchPackages(opts);
18289
+ }
18290
+ async function resolveRegistryPackage(config, handle, slug) {
18291
+ return (await getDiscoveryClient(config)).resolvePackage(handle, slug);
18292
+ }
18293
+ async function getRegistryPackage(config, did, slug) {
18294
+ return (await getDiscoveryClient(config)).getPackage(did, slug);
18295
+ }
18296
+ async function listRegistryReleases(config, did, slug, opts) {
18297
+ return (await getDiscoveryClient(config)).listReleases(did, slug, opts);
18298
+ }
18299
+ /**
18300
+ * Resolve a publisher DID to its claimed handle using the same
18301
+ * `LocalActorResolver` pattern as `@emdash-cms/plugin-cli` and
18302
+ * `@emdash-cms/auth-atproto`. Bidirectional verification (handle's
18303
+ * domain points back to the same DID) is part of the resolver --
18304
+ * `LocalActorResolver` returns the sentinel `"handle.invalid"` when
18305
+ * the `alsoKnownAs` handle is present but doesn't round-trip.
18306
+ *
18307
+ * Three distinct outcomes the UI can render:
18308
+ *
18309
+ * - `{ status: "ok", handle }` — verified handle, round-trip OK.
18310
+ * - `{ status: "invalid" }` — DID claims a handle but it doesn't
18311
+ * resolve back. The publisher's handle setup is broken; the admin
18312
+ * should see a clear "Invalid handle" indicator rather than the
18313
+ * raw DID.
18314
+ * - `{ status: "missing" }` — no handle claimed at all (no
18315
+ * `alsoKnownAs`), or the DID document couldn't be fetched (network
18316
+ * error, unsupported DID method).
18317
+ */
18318
+ let actorResolver = null;
18319
+ async function getActorResolver() {
18320
+ if (actorResolver) return actorResolver;
18321
+ const { CompositeDidDocumentResolver, CompositeHandleResolver, DohJsonHandleResolver, LocalActorResolver, PlcDidDocumentResolver, WebDidDocumentResolver, WellKnownHandleResolver } = await import("@atcute/identity-resolver");
18322
+ actorResolver = new LocalActorResolver({
18323
+ handleResolver: new CompositeHandleResolver({ methods: {
18324
+ dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" }),
18325
+ http: new WellKnownHandleResolver()
18326
+ } }),
18327
+ didDocumentResolver: new CompositeDidDocumentResolver({ methods: {
18328
+ plc: new PlcDidDocumentResolver(),
18329
+ web: new WebDidDocumentResolver()
18330
+ } })
18331
+ });
18332
+ return actorResolver;
18333
+ }
18334
+ /**
18335
+ * localStorage-backed cache for DID→handle resolutions. Handles are
18336
+ * stable for hours-to-days in practice, but bound the cache so a
18337
+ * compromised handle eventually flips back to "invalid" without a
18338
+ * forced refresh. 24h matches the typical atproto handle TTL.
18339
+ *
18340
+ * Failures (network errors, unsupported DID method) are *not* cached --
18341
+ * those should retry on the next render.
18342
+ */
18343
+ const HANDLE_CACHE_TTL_MS = 1440 * 60 * 1e3;
18344
+ const HANDLE_CACHE_KEY_PREFIX = "emdash:did-handle:";
18345
+ function isCachedResolution(value) {
18346
+ if (typeof value !== "object" || value === null) return false;
18347
+ const candidate = value;
18348
+ return typeof candidate.expiresAt === "number" && typeof candidate.resolution === "object" && candidate.resolution !== null;
18349
+ }
18350
+ function readHandleCache(did) {
18351
+ if (typeof localStorage === "undefined") return null;
18352
+ try {
18353
+ const raw = localStorage.getItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`);
18354
+ if (!raw) return null;
18355
+ const parsed = JSON.parse(raw);
18356
+ if (!isCachedResolution(parsed) || parsed.expiresAt < Date.now()) return null;
18357
+ return parsed.resolution;
18358
+ } catch {
18359
+ return null;
18360
+ }
18361
+ }
18362
+ function writeHandleCache(did, resolution) {
18363
+ if (typeof localStorage === "undefined") return;
18364
+ try {
18365
+ const entry = {
18366
+ resolution,
18367
+ expiresAt: Date.now() + HANDLE_CACHE_TTL_MS
18368
+ };
18369
+ localStorage.setItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`, JSON.stringify(entry));
18370
+ } catch {}
18371
+ }
18372
+ async function resolveDidToHandle(did) {
18373
+ const cached = readHandleCache(did);
18374
+ if (cached) return cached;
18375
+ let result;
18376
+ try {
18377
+ const resolved = await (await getActorResolver()).resolve(did);
18378
+ if (resolved.handle === "handle.invalid") result = { status: "invalid" };
18379
+ else if (resolved.handle) result = {
18380
+ status: "ok",
18381
+ handle: resolved.handle
18382
+ };
18383
+ else result = { status: "missing" };
18384
+ } catch (err) {
18385
+ console.warn(`[registry] DID->handle resolution failed for ${did}:`, err);
18386
+ return { status: "missing" };
18387
+ }
18388
+ writeHandleCache(did, result);
18389
+ return result;
18390
+ }
18391
+ const INSTALL_ENDPOINT = `${API_BASE}/admin/plugins/registry/install`;
18392
+ /**
18393
+ * Install a plugin from the registry.
18394
+ *
18395
+ * Posts to the EmDash server, which re-resolves the same `(handle,
18396
+ * slug)` against the aggregator, re-verifies the bundle's checksum
18397
+ * against the signed release record, and writes the install. Surfaces
18398
+ * structured error codes (`RELEASE_YANKED`, `CHECKSUM_MISMATCH`,
18399
+ * `DECLARED_ACCESS_DRIFT`, etc.) that callers map to localized
18400
+ * messages.
18401
+ */
18402
+ async function installRegistryPlugin(body) {
18403
+ return parseApiResponse(await apiFetch(INSTALL_ENDPOINT, {
18404
+ method: "POST",
18405
+ headers: { "Content-Type": "application/json" },
18406
+ body: JSON.stringify(body)
18407
+ }), i18n._({
18408
+ id: "/LWKyx",
18409
+ message: "Failed to install plugin"
18410
+ }));
18411
+ }
18412
+ /**
18413
+ * Server-side escalation gate raised by the update endpoint when the
18414
+ * target version widens the trust contract. Carries the diff the user
18415
+ * needs to see in the consent dialog before the call is retried with the
18416
+ * matching `confirm*` flag.
18417
+ */
18418
+ var RegistryUpdateEscalationError = class extends Error {
18419
+ code;
18420
+ capabilityChanges;
18421
+ routeVisibilityChanges;
18422
+ constructor(code, message, capabilityChanges, routeVisibilityChanges) {
18423
+ super(message);
18424
+ this.name = "RegistryUpdateEscalationError";
18425
+ this.code = code;
18426
+ this.capabilityChanges = capabilityChanges;
18427
+ this.routeVisibilityChanges = routeVisibilityChanges;
18428
+ }
18429
+ };
18430
+ /**
18431
+ * Update a registry-source plugin to a newer version.
18432
+ * `POST /_emdash/api/admin/plugins/registry/:id/update`
18433
+ *
18434
+ * Called without `confirm*` flags first, this throws
18435
+ * `RegistryUpdateEscalationError` when the target version widens
18436
+ * permissions; the caller renders a consent dialog populated from the
18437
+ * error's diff, then re-calls with the matching `confirm*` flag once
18438
+ * the user agrees.
18439
+ */
18440
+ async function updateRegistryPlugin(pluginId, opts = {}) {
18441
+ const response = await apiFetch(`${API_BASE}/admin/plugins/registry/${encodeURIComponent(pluginId)}/update`, {
18442
+ method: "POST",
18443
+ headers: { "Content-Type": "application/json" },
18444
+ body: JSON.stringify(opts)
18445
+ });
18446
+ if (response.ok) return;
18447
+ const escalation = parseEscalation(await response.clone().json().catch(() => void 0));
18448
+ if (escalation) throw escalation;
18449
+ await throwResponseError(response, i18n._({
18450
+ id: "8m84rO",
18451
+ message: "Failed to update plugin"
18452
+ }));
18453
+ }
18454
+ function parseEscalation(body) {
18455
+ if (!body || typeof body !== "object" || !("error" in body)) return null;
18456
+ const error = body.error;
18457
+ if (!error || typeof error !== "object" || !("code" in error)) return null;
18458
+ const code = error.code;
18459
+ if (code !== "CAPABILITY_ESCALATION" && code !== "ROUTE_VISIBILITY_ESCALATION") return null;
18460
+ const details = "details" in error && error.details && typeof error.details === "object" ? error.details : {};
18461
+ const capabilityChanges = normaliseCapabilityChanges("capabilityChanges" in details ? details.capabilityChanges : void 0);
18462
+ const routeVisibilityChanges = normaliseRouteVisibilityChanges("routeVisibilityChanges" in details ? details.routeVisibilityChanges : void 0);
18463
+ return new RegistryUpdateEscalationError(code, "message" in error && typeof error.message === "string" ? error.message : i18n._({
18464
+ id: "dgD7yY",
18465
+ message: "Plugin update requires re-consent"
18466
+ }), capabilityChanges, routeVisibilityChanges);
18467
+ }
18468
+ function normaliseCapabilityChanges(value) {
18469
+ if (!value || typeof value !== "object") return {
18470
+ added: [],
18471
+ removed: []
18472
+ };
18473
+ const v = value;
18474
+ return {
18475
+ added: Array.isArray(v.added) ? v.added.filter((s) => typeof s === "string") : [],
18476
+ removed: Array.isArray(v.removed) ? v.removed.filter((s) => typeof s === "string") : []
18477
+ };
18478
+ }
18479
+ function normaliseRouteVisibilityChanges(value) {
18480
+ if (!value || typeof value !== "object") return void 0;
18481
+ const v = value;
18482
+ if (!Array.isArray(v.newlyPublic)) return void 0;
18483
+ const newlyPublic = v.newlyPublic.filter((s) => typeof s === "string");
18484
+ return newlyPublic.length > 0 ? { newlyPublic } : void 0;
18485
+ }
18486
+ /**
18487
+ * Uninstall a registry-source plugin.
18488
+ * `POST /_emdash/api/admin/plugins/registry/:id/uninstall`
18489
+ *
18490
+ * The server refuses to uninstall non-registry sources, so calling this
18491
+ * with a marketplace or config plugin id is a no-op error rather than a
18492
+ * destructive cross-source action.
18493
+ */
18494
+ async function uninstallRegistryPlugin(pluginId, opts = {}) {
18495
+ const response = await apiFetch(`${API_BASE}/admin/plugins/registry/${encodeURIComponent(pluginId)}/uninstall`, {
18496
+ method: "POST",
18497
+ headers: { "Content-Type": "application/json" },
18498
+ body: JSON.stringify(opts)
18499
+ });
18500
+ if (!response.ok) await throwResponseError(response, i18n._({
18501
+ id: "IDxl65",
18502
+ message: "Failed to uninstall plugin"
18503
+ }));
18504
+ }
18505
+
17710
18506
  //#endregion
17711
18507
  //#region src/components/PluginManager.tsx
17712
18508
  /**
@@ -17796,7 +18592,7 @@ function PluginManager({ manifest }) {
17796
18592
  if (!updates) return /* @__PURE__ */ new Map();
17797
18593
  return new Map(updates.map((u) => [u.pluginId, u]));
17798
18594
  }, [updates]);
17799
- const hasMarketplacePlugins = plugins?.some((p) => p.source === "marketplace");
18595
+ const hasUpdatableSources = plugins?.some((p) => p.source === "marketplace" || p.source === "registry");
17800
18596
  if (isLoading) return /* @__PURE__ */ jsxs("div", {
17801
18597
  className: "space-y-6",
17802
18598
  children: [/* @__PURE__ */ jsx("h1", {
@@ -17844,7 +18640,7 @@ function PluginManager({ manifest }) {
17844
18640
  }), /* @__PURE__ */ jsxs("div", {
17845
18641
  className: "flex items-center gap-3",
17846
18642
  children: [
17847
- hasMarketplacePlugins && /* @__PURE__ */ jsx(Button, {
18643
+ hasUpdatableSources && /* @__PURE__ */ jsx(Button, {
17848
18644
  variant: "ghost",
17849
18645
  onClick: () => void refetchUpdates(),
17850
18646
  disabled: isCheckingUpdates,
@@ -17939,15 +18735,17 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
17939
18735
  const [expanded, setExpanded] = React$1.useState(false);
17940
18736
  const [showUpdateConsent, setShowUpdateConsent] = React$1.useState(false);
17941
18737
  const [showUninstallConfirm, setShowUninstallConfirm] = React$1.useState(false);
18738
+ const [registryEscalation, setRegistryEscalation] = React$1.useState(null);
17942
18739
  const queryClient = useQueryClient();
17943
18740
  const toastManager = Toast.useToastManager();
17944
18741
  const isMarketplace = plugin.source === "marketplace";
17945
18742
  const isRegistry = plugin.source === "registry";
17946
18743
  const hasUpdate = !!updateInfo && updateInfo.installed !== updateInfo.latest;
17947
18744
  const updateMutation = useMutation({
17948
- mutationFn: () => updateMarketplacePlugin(plugin.id, { confirmCapabilities: true }),
18745
+ mutationFn: (opts) => isRegistry ? updateRegistryPlugin(plugin.id, opts) : updateMarketplacePlugin(plugin.id, { confirmCapabilities: true }),
17949
18746
  onSuccess: () => {
17950
18747
  setShowUpdateConsent(false);
18748
+ setRegistryEscalation(null);
17951
18749
  queryClient.invalidateQueries({ queryKey: ["plugins"] });
17952
18750
  queryClient.invalidateQueries({ queryKey: ["plugin-updates"] });
17953
18751
  queryClient.invalidateQueries({ queryKey: ["manifest"] });
@@ -17965,10 +18763,29 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
17965
18763
  }
17966
18764
  })
17967
18765
  });
18766
+ },
18767
+ onError: (err) => {
18768
+ if (err instanceof RegistryUpdateEscalationError) {
18769
+ setRegistryEscalation(err);
18770
+ setShowUpdateConsent(true);
18771
+ }
17968
18772
  }
17969
18773
  });
18774
+ const handleUpdateClick = () => {
18775
+ if (isRegistry) {
18776
+ setRegistryEscalation(null);
18777
+ updateMutation.mutate({});
18778
+ } else setShowUpdateConsent(true);
18779
+ };
18780
+ const handleUpdateConfirm = () => {
18781
+ if (isRegistry) {
18782
+ const opts = { confirmCapabilityChanges: true };
18783
+ if (registryEscalation?.code === "ROUTE_VISIBILITY_ESCALATION") opts.confirmRouteVisibilityChanges = true;
18784
+ updateMutation.mutate(opts);
18785
+ } else updateMutation.mutate({});
18786
+ };
17970
18787
  const uninstallMutation = useMutation({
17971
- mutationFn: (deleteData) => uninstallMarketplacePlugin(plugin.id, { deleteData }),
18788
+ mutationFn: (deleteData) => isRegistry ? uninstallRegistryPlugin(plugin.id, { deleteData }) : uninstallMarketplacePlugin(plugin.id, { deleteData }),
17972
18789
  onSuccess: () => {
17973
18790
  setShowUninstallConfirm(false);
17974
18791
  queryClient.invalidateQueries({ queryKey: ["plugins"] });
@@ -18097,7 +18914,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18097
18914
  hasUpdate && /* @__PURE__ */ jsx(Button, {
18098
18915
  variant: "outline",
18099
18916
  size: "sm",
18100
- onClick: () => setShowUpdateConsent(true),
18917
+ onClick: handleUpdateClick,
18101
18918
  disabled: updateMutation.isPending,
18102
18919
  children: updateMutation.isPending ? _t2({
18103
18920
  id: "RXbEvi",
@@ -18261,7 +19078,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18261
19078
  ] })
18262
19079
  ]
18263
19080
  }),
18264
- isMarketplace && /* @__PURE__ */ jsx("div", {
19081
+ (isMarketplace || isRegistry) && /* @__PURE__ */ jsx("div", {
18265
19082
  className: "pt-2 border-t",
18266
19083
  children: /* @__PURE__ */ jsx(Button, {
18267
19084
  variant: "ghost",
@@ -18274,13 +19091,6 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18274
19091
  message: "Uninstall"
18275
19092
  })
18276
19093
  })
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
19094
  })
18285
19095
  ]
18286
19096
  })]
@@ -18289,12 +19099,14 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18289
19099
  mode: "update",
18290
19100
  pluginName: plugin.name,
18291
19101
  capabilities: plugin.capabilities,
18292
- newCapabilities: [],
19102
+ newCapabilities: registryEscalation?.capabilityChanges.added ?? [],
19103
+ newlyPublicRoutes: registryEscalation?.routeVisibilityChanges?.newlyPublic ?? [],
18293
19104
  isPending: updateMutation.isPending,
18294
- error: getMutationError(updateMutation.error),
18295
- onConfirm: () => updateMutation.mutate(),
19105
+ error: updateMutation.error instanceof RegistryUpdateEscalationError ? null : getMutationError(updateMutation.error),
19106
+ onConfirm: handleUpdateConfirm,
18296
19107
  onCancel: () => {
18297
19108
  setShowUpdateConsent(false);
19109
+ setRegistryEscalation(null);
18298
19110
  updateMutation.reset();
18299
19111
  }
18300
19112
  }),
@@ -21951,235 +22763,6 @@ function Redirects() {
21951
22763
  });
21952
22764
  }
21953
22765
 
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
22766
  //#endregion
22184
22767
  //#region src/components/PublisherHandle.tsx
22185
22768
  /**
@@ -22473,7 +23056,7 @@ function RegistryPluginDetail({ pluginId, config }) {
22473
23056
  const { data: installedPlugins } = useQuery({
22474
23057
  queryKey: ["plugins"],
22475
23058
  queryFn: async () => {
22476
- const { fetchPlugins } = await import("./plugins-qrjvPK4g.js").then((n) => n.a);
23059
+ const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
22477
23060
  return fetchPlugins();
22478
23061
  }
22479
23062
  });
@@ -22494,17 +23077,42 @@ function RegistryPluginDetail({ pluginId, config }) {
22494
23077
  enabled: Boolean(publisher && slug)
22495
23078
  });
22496
23079
  const handleResult = usePublisherHandle(pkg?.did ?? "", pkg?.handle);
22497
- const { data: release } = useQuery({
23080
+ const { data: releasesData } = useQuery({
22498
23081
  queryKey: [
22499
23082
  "registry",
22500
- "latest-release",
23083
+ "releases",
22501
23084
  config.aggregatorUrl,
23085
+ config.acceptLabelers,
22502
23086
  pkg?.did,
22503
23087
  slug
22504
23088
  ],
22505
- queryFn: () => getLatestRegistryRelease(config, pkg.did, slug),
23089
+ queryFn: () => listRegistryReleases(config, pkg.did, slug, { limit: 100 }),
22506
23090
  enabled: Boolean(pkg?.did && slug)
22507
23091
  });
23092
+ const releases = React$1.useMemo(() => (releasesData?.releases ?? []).filter((r) => r.release !== null && !isYanked(r)), [releasesData]);
23093
+ const hasFilteredAllReleases = (releasesData?.releases.length ?? 0) > 0 && releases.length === 0;
23094
+ const defaultVersion = React$1.useMemo(() => {
23095
+ if (!pkg || releases.length === 0) return void 0;
23096
+ return (releases.find((r) => releasePassesPolicy(r, {
23097
+ did: pkg.did,
23098
+ slug
23099
+ }, config.policy)) ?? releases[0])?.version;
23100
+ }, [
23101
+ pkg,
23102
+ releases,
23103
+ slug,
23104
+ config.policy
23105
+ ]);
23106
+ const [selectedVersion, setSelectedVersion] = React$1.useState(void 0);
23107
+ const [prevPluginId, setPrevPluginId] = React$1.useState(pluginId);
23108
+ if (prevPluginId !== pluginId) {
23109
+ setPrevPluginId(pluginId);
23110
+ setSelectedVersion(void 0);
23111
+ }
23112
+ if (selectedVersion !== void 0 && releases.length > 0 && !releases.some((r) => r.version === selectedVersion)) setSelectedVersion(void 0);
23113
+ const effectiveVersion = selectedVersion ?? defaultVersion;
23114
+ const release = React$1.useMemo(() => releases.find((r) => r.version === effectiveVersion), [releases, effectiveVersion]);
23115
+ const isPreRelease = release ? isPreReleaseVersion(release.version) : false;
22508
23116
  const ext = (release?.release?.extensions)?.["com.emdashcms.experimental.package.releaseExtension"];
22509
23117
  const capabilities = Array.isArray(ext?.capabilities) ? canonicalCapabilitiesForDriftCheck(ext?.capabilities) : canonicalCapabilitiesForDriftCheck(declaredAccessToCapabilityList(ext?.declaredAccess));
22510
23118
  const pkgProfile = pkg?.profile ?? null;
@@ -22590,7 +23198,7 @@ function RegistryPluginDetail({ pluginId, config }) {
22590
23198
  children: [
22591
23199
  /* @__PURE__ */ jsx(BackLink, {}),
22592
23200
  /* @__PURE__ */ jsxs("div", {
22593
- className: "flex items-start gap-4",
23201
+ className: "flex flex-wrap items-start gap-4",
22594
23202
  children: [
22595
23203
  /* @__PURE__ */ jsx("div", {
22596
23204
  className: "rounded-xl bg-kumo-subtle p-3 text-kumo-subtle",
@@ -22630,40 +23238,88 @@ function RegistryPluginDetail({ pluginId, config }) {
22630
23238
  })
22631
23239
  ]
22632
23240
  }),
22633
- release ? /* @__PURE__ */ jsxs("p", {
22634
- className: "text-xs text-kumo-subtle",
22635
- children: [
22636
- _t({
22637
- id: "YYdC3A",
22638
- message: "Version {0}",
22639
- values: { 0: release.version }
22640
- }),
22641
- " · ",
22642
- _t({
22643
- id: "294VT5",
22644
- message: "indexed {0}",
22645
- values: { 0: formatDate(release.indexedAt) }
22646
- })
22647
- ]
23241
+ release ? /* @__PURE__ */ jsxs("div", {
23242
+ className: "mt-1 flex flex-wrap items-center gap-2",
23243
+ children: [/* @__PURE__ */ jsxs("p", {
23244
+ className: "text-xs text-kumo-subtle",
23245
+ children: [
23246
+ _t({
23247
+ id: "YYdC3A",
23248
+ message: "Version {0}",
23249
+ values: { 0: release.version }
23250
+ }),
23251
+ " · ",
23252
+ _t({
23253
+ id: "294VT5",
23254
+ message: "indexed {0}",
23255
+ values: { 0: formatDate(release.indexedAt) }
23256
+ })
23257
+ ]
23258
+ }), isPreRelease ? /* @__PURE__ */ jsx(Badge, { children: _t({
23259
+ id: "omrja/",
23260
+ message: "Pre-release"
23261
+ }) }) : null]
22648
23262
  }) : null
22649
23263
  ]
22650
23264
  }),
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
- }) })
23265
+ /* @__PURE__ */ jsxs("div", {
23266
+ className: "flex min-w-0 flex-wrap items-center gap-2",
23267
+ children: [releases.length > 1 ? /* @__PURE__ */ jsx(Select, {
23268
+ "aria-label": _t({
23269
+ id: "eE0JZ4",
23270
+ message: "Version"
23271
+ }),
23272
+ className: "w-full max-w-[220px]",
23273
+ value: effectiveVersion ?? "",
23274
+ onValueChange: (v) => setSelectedVersion(v ?? void 0),
23275
+ renderValue: (v) => typeof v === "string" ? v : "",
23276
+ children: releases.map((r) => {
23277
+ const preRelease = isPreReleaseVersion(r.version);
23278
+ const policyBlocked = !releasePassesPolicy(r, {
23279
+ did: pkg.did,
23280
+ slug
23281
+ }, config.policy);
23282
+ return /* @__PURE__ */ jsx(Select.Option, {
23283
+ value: r.version,
23284
+ children: /* @__PURE__ */ jsxs("span", {
23285
+ className: "flex items-center gap-2",
23286
+ children: [
23287
+ /* @__PURE__ */ jsx("span", { children: r.version }),
23288
+ preRelease ? /* @__PURE__ */ jsx("span", {
23289
+ className: "text-xs text-kumo-subtle",
23290
+ children: _t({
23291
+ id: "D2C1D+",
23292
+ message: "(pre-release)"
23293
+ })
23294
+ }) : null,
23295
+ policyBlocked ? /* @__PURE__ */ jsx("span", {
23296
+ className: "text-xs text-kumo-subtle",
23297
+ children: _t({
23298
+ id: "W3SUIF",
23299
+ message: "(too new)"
23300
+ })
23301
+ }) : null
23302
+ ]
23303
+ })
23304
+ }, r.version);
23305
+ })
23306
+ }) : null, isInstalled ? /* @__PURE__ */ jsx(Button, {
23307
+ variant: "secondary",
23308
+ disabled: true,
23309
+ children: _t({
23310
+ id: "eQkgKV",
23311
+ message: "Installed"
23312
+ })
23313
+ }) : /* @__PURE__ */ jsx(Button, {
23314
+ variant: "primary",
23315
+ disabled: !release || !policyOk || handleResult.status === "invalid",
23316
+ onClick: () => setShowConsent(true),
23317
+ children: _t({
23318
+ id: "TKQ7K+",
23319
+ message: "Install"
23320
+ })
23321
+ })]
23322
+ })
22667
23323
  ]
22668
23324
  }),
22669
23325
  handleResult.status === "invalid" ? /* @__PURE__ */ jsxs("div", {
@@ -22683,6 +23339,23 @@ function RegistryPluginDetail({ pluginId, config }) {
22683
23339
  })
22684
23340
  })] })]
22685
23341
  }) : null,
23342
+ hasFilteredAllReleases ? /* @__PURE__ */ jsxs("div", {
23343
+ className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
23344
+ role: "status",
23345
+ children: [/* @__PURE__ */ jsx(Warning, { className: "mt-0.5 h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
23346
+ className: "font-medium",
23347
+ children: _t({
23348
+ id: "rqsq95",
23349
+ message: "No installable releases"
23350
+ })
23351
+ }), /* @__PURE__ */ jsx("p", {
23352
+ className: "mt-1 text-sm text-kumo-default",
23353
+ children: _t({
23354
+ id: "o+La9d",
23355
+ message: "Every published release of this plugin has been withdrawn or could not be verified. Check back later, or contact the publisher."
23356
+ })
23357
+ })] })]
23358
+ }) : null,
22686
23359
  release && !policyOk ? /* @__PURE__ */ jsxs("div", {
22687
23360
  className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
22688
23361
  role: "status",
@@ -22833,6 +23506,33 @@ function declaredAccessToCapabilityList(declaredAccess) {
22833
23506
  }
22834
23507
  return out;
22835
23508
  }
23509
+ const PRE_RELEASE_VERSION_RE = /^\d+\.\d+\.\d+-/;
23510
+ /**
23511
+ * Detects semver pre-release identifiers (`1.0.0-alpha.1`, `2.0.0-rc.2`). The
23512
+ * release lexicon does not enforce semver shape, so a permissive `includes("-")`
23513
+ * check would light up the badge for malformed values like `-1.0.0` or
23514
+ * `abc-def`. Require a `MAJOR.MINOR.PATCH-` prefix instead.
23515
+ */
23516
+ function isPreReleaseVersion(version) {
23517
+ return PRE_RELEASE_VERSION_RE.test(version);
23518
+ }
23519
+ const YANKED_LABEL_VALUE = "security:yanked";
23520
+ /**
23521
+ * Aggregators forward labels applied by their configured labellers. `security:yanked`
23522
+ * is a hard-enforcement label that publishers can self-apply (or that a labeller
23523
+ * applies on their behalf) to retract a release after publication. Sites whose
23524
+ * `acceptLabelers` config includes the labeller never see yanked releases at all
23525
+ * (server filtering), but sites without it receive yanked releases interleaved
23526
+ * with installable ones — filter them out so they never reach the picker.
23527
+ *
23528
+ * `neg` (negated labels) is intentionally ignored to match the server install
23529
+ * handler, which only checks `l.val === "security:yanked"`. Diverging here would
23530
+ * let the UI surface an install affordance the server will reject with
23531
+ * `RELEASE_YANKED`. Honoring `neg` on both sides is a separate follow-up.
23532
+ */
23533
+ function isYanked(release) {
23534
+ return (release.labels ?? []).some((l) => l.val === YANKED_LABEL_VALUE);
23535
+ }
22836
23536
  function formatDate(iso) {
22837
23537
  try {
22838
23538
  return new Date(iso).toLocaleDateString();
@@ -34218,6 +34918,13 @@ function AuthorMappingStep({ authorMappings, emdashUsers, onMappingChange, onCon
34218
34918
 
34219
34919
  //#endregion
34220
34920
  //#region src/routes/bylines.tsx
34921
+ /**
34922
+ * True when the load-more snapshot still matches the current filter state.
34923
+ * Used to discard appends from requests whose filters have changed mid-flight.
34924
+ */
34925
+ function loadMoreSnapshotMatches(snapshot, current) {
34926
+ return snapshot.search === current.search && snapshot.guestFilter === current.guestFilter && snapshot.locale === current.locale;
34927
+ }
34221
34928
  function toFormState(byline) {
34222
34929
  if (!byline) return {
34223
34930
  slug: "",
@@ -34243,21 +34950,39 @@ function getUserLabel(user) {
34243
34950
  function BylinesPage() {
34244
34951
  const { _: _t } = useLingui();
34245
34952
  const queryClient = useQueryClient();
34953
+ const navigate = useNavigate$1();
34954
+ const { locale: routeLocale } = useSearch({ from: "/_admin/bylines" });
34246
34955
  const [search, setSearch] = React$1.useState("");
34247
34956
  const [guestFilter, setGuestFilter] = React$1.useState("all");
34248
34957
  const [selectedId, setSelectedId] = React$1.useState(null);
34249
34958
  const [showDeleteConfirm, setShowDeleteConfirm] = React$1.useState(false);
34250
34959
  const [allItems, setAllItems] = React$1.useState([]);
34251
34960
  const [nextCursor, setNextCursor] = React$1.useState(void 0);
34961
+ const { data: manifest } = useQuery({
34962
+ queryKey: ["manifest"],
34963
+ queryFn: fetchManifest
34964
+ });
34965
+ const i18n = useI18nConfig(manifest);
34966
+ const isMultiLocale = !!i18n && i18n.locales.length > 1;
34967
+ const activeLocale = routeLocale ?? i18n?.defaultLocale ?? void 0;
34968
+ const handleLocaleChange = (locale) => {
34969
+ navigate({
34970
+ to: "/bylines",
34971
+ search: { locale: locale || void 0 }
34972
+ });
34973
+ setSelectedId(null);
34974
+ };
34252
34975
  const { data, isLoading, error } = useQuery({
34253
34976
  queryKey: [
34254
34977
  "bylines",
34255
34978
  search,
34256
- guestFilter
34979
+ guestFilter,
34980
+ activeLocale ?? null
34257
34981
  ],
34258
34982
  queryFn: () => fetchBylines({
34259
34983
  search: search || void 0,
34260
34984
  isGuest: guestFilter === "all" ? void 0 : guestFilter === "guest",
34985
+ locale: activeLocale,
34261
34986
  limit: 50
34262
34987
  })
34263
34988
  });
@@ -34273,28 +34998,44 @@ function BylinesPage() {
34273
34998
  });
34274
34999
  const users = usersData?.items ?? [];
34275
35000
  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
- });
35001
+ mutationFn: async (snapshot) => {
35002
+ return {
35003
+ result: await fetchBylines({
35004
+ search: snapshot.search || void 0,
35005
+ isGuest: snapshot.guestFilter === "all" ? void 0 : snapshot.guestFilter === "guest",
35006
+ locale: snapshot.locale,
35007
+ limit: 50,
35008
+ cursor: snapshot.cursor
35009
+ }),
35010
+ snapshot
35011
+ };
34284
35012
  },
34285
- onSuccess: (result) => {
34286
- if (result) {
34287
- setAllItems((prev) => [...prev, ...result.items]);
34288
- setNextCursor(result.nextCursor);
34289
- }
35013
+ onSuccess: ({ result, snapshot }) => {
35014
+ if (!loadMoreSnapshotMatches(snapshot, {
35015
+ search,
35016
+ guestFilter,
35017
+ locale: activeLocale
35018
+ })) return;
35019
+ setAllItems((prev) => [...prev, ...result.items]);
35020
+ setNextCursor(result.nextCursor);
34290
35021
  }
34291
35022
  });
34292
35023
  const items = allItems;
34293
- const selected = items.find((item) => item.id === selectedId) ?? null;
35024
+ const { data: selectedRemote } = useQuery({
35025
+ queryKey: ["byline", selectedId],
35026
+ queryFn: () => selectedId ? fetchByline(selectedId) : Promise.resolve(null),
35027
+ enabled: !!selectedId
35028
+ });
35029
+ const selected = selectedRemote ?? items.find((item) => item.id === selectedId) ?? null;
34294
35030
  const [form, setForm] = React$1.useState(() => toFormState(null));
34295
35031
  React$1.useEffect(() => {
34296
35032
  setForm(toFormState(selected));
34297
- }, [selectedId, selected]);
35033
+ }, [selected]);
35034
+ const { data: translationsData } = useQuery({
35035
+ queryKey: ["byline-translations", selectedId],
35036
+ queryFn: () => selectedId ? fetchBylineTranslations(selectedId) : Promise.resolve({ items: [] }),
35037
+ enabled: !!selectedId && isMultiLocale
35038
+ });
34298
35039
  const createMutation = useMutation({
34299
35040
  mutationFn: () => createByline({
34300
35041
  slug: form.slug,
@@ -34302,7 +35043,8 @@ function BylinesPage() {
34302
35043
  bio: form.bio || null,
34303
35044
  websiteUrl: form.websiteUrl || null,
34304
35045
  userId: form.userId,
34305
- isGuest: form.isGuest
35046
+ isGuest: form.isGuest,
35047
+ locale: activeLocale
34306
35048
  }),
34307
35049
  onSuccess: (created) => {
34308
35050
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
@@ -34323,6 +35065,7 @@ function BylinesPage() {
34323
35065
  },
34324
35066
  onSuccess: () => {
34325
35067
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
35068
+ if (selectedId) queryClient.invalidateQueries({ queryKey: ["byline", selectedId] });
34326
35069
  }
34327
35070
  });
34328
35071
  const deleteMutation = useMutation({
@@ -34336,6 +35079,26 @@ function BylinesPage() {
34336
35079
  setShowDeleteConfirm(false);
34337
35080
  }
34338
35081
  });
35082
+ const [pendingTranslationLocale, setPendingTranslationLocale] = React$1.useState(null);
35083
+ const translateMutation = useMutation({
35084
+ mutationFn: (targetLocale) => {
35085
+ if (!selectedId) throw new Error("No byline selected");
35086
+ setPendingTranslationLocale(targetLocale);
35087
+ return createBylineTranslation(selectedId, { locale: targetLocale });
35088
+ },
35089
+ onSettled: () => {
35090
+ setPendingTranslationLocale(null);
35091
+ },
35092
+ onSuccess: (created) => {
35093
+ queryClient.invalidateQueries({ queryKey: ["bylines"] });
35094
+ if (selectedId) queryClient.invalidateQueries({ queryKey: ["byline-translations", selectedId] });
35095
+ navigate({
35096
+ to: "/bylines",
35097
+ search: { locale: created.locale }
35098
+ });
35099
+ setSelectedId(created.id);
35100
+ }
35101
+ });
34339
35102
  if (isLoading) return /* @__PURE__ */ jsx("div", {
34340
35103
  className: "flex items-center justify-center min-h-[30vh]",
34341
35104
  children: /* @__PURE__ */ jsx(Loader, {})
@@ -34349,230 +35112,273 @@ function BylinesPage() {
34349
35112
  })
34350
35113
  });
34351
35114
  const isSaving = createMutation.isPending || updateMutation.isPending;
34352
- const mutationError = createMutation.error || updateMutation.error || deleteMutation.error;
35115
+ const mutationError = createMutation.error || updateMutation.error || deleteMutation.error || translateMutation.error;
34353
35116
  return /* @__PURE__ */ jsxs("div", {
34354
- className: "grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]",
35117
+ className: "space-y-4",
34355
35118
  children: [
35119
+ isMultiLocale && i18n && activeLocale ? /* @__PURE__ */ jsxs("div", {
35120
+ className: "flex items-center justify-between",
35121
+ children: [/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("h1", {
35122
+ className: "text-2xl font-semibold",
35123
+ children: _t({
35124
+ id: "xLGa7J",
35125
+ message: "Bylines"
35126
+ })
35127
+ }) }), /* @__PURE__ */ jsx(LocaleSwitcher, {
35128
+ locales: i18n.locales,
35129
+ defaultLocale: i18n.defaultLocale,
35130
+ value: activeLocale,
35131
+ onChange: handleLocaleChange
35132
+ })]
35133
+ }) : null,
34356
35134
  /* @__PURE__ */ jsxs("div", {
34357
- className: "rounded-lg border p-4",
35135
+ className: "grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]",
34358
35136
  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"
35137
+ className: "rounded-lg border p-4",
35138
+ children: [/* @__PURE__ */ jsxs("div", {
35139
+ className: "mb-4 space-y-2",
35140
+ children: [/* @__PURE__ */ jsx(Input, {
35141
+ placeholder: _t({
35142
+ id: "4L2LjM",
35143
+ message: "Search bylines"
35144
+ }),
35145
+ value: search,
35146
+ onChange: (e) => setSearch(e.target.value)
35147
+ }), /* @__PURE__ */ jsxs("div", {
35148
+ className: "flex items-center gap-2",
35149
+ children: [/* @__PURE__ */ jsx("div", {
35150
+ className: "flex-1",
35151
+ children: /* @__PURE__ */ jsx(Select, {
35152
+ "aria-label": _t({
35153
+ id: "kYxs0a",
35154
+ message: "Filter byline type"
34386
35155
  }),
34387
- linked: _t({
34388
- id: "RZ5T1J",
34389
- message: "Linked only"
34390
- })
35156
+ value: guestFilter,
35157
+ onValueChange: (v) => setGuestFilter(v ?? "all"),
35158
+ items: {
35159
+ all: _t({
35160
+ id: "VK3dVJ",
35161
+ message: "All bylines"
35162
+ }),
35163
+ guest: _t({
35164
+ id: "+0jFzA",
35165
+ message: "Guest only"
35166
+ }),
35167
+ linked: _t({
35168
+ id: "RZ5T1J",
35169
+ message: "Linked only"
35170
+ })
35171
+ },
35172
+ className: "w-full"
35173
+ })
35174
+ }), /* @__PURE__ */ jsx(Button, {
35175
+ variant: "secondary",
35176
+ onClick: () => {
35177
+ setSelectedId(null);
35178
+ setForm(toFormState(null));
34391
35179
  },
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
- })
35180
+ children: _t({
35181
+ id: "isRobC",
35182
+ message: "New"
35183
+ })
35184
+ })]
34404
35185
  })]
35186
+ }), /* @__PURE__ */ jsxs("div", {
35187
+ className: "space-y-2 max-h-[70vh] overflow-auto",
35188
+ children: [
35189
+ items.map((item) => {
35190
+ return /* @__PURE__ */ jsxs("button", {
35191
+ type: "button",
35192
+ onClick: () => setSelectedId(item.id),
35193
+ className: `w-full rounded border p-3 text-start ${item.id === selectedId ? "border-kumo-brand bg-kumo-brand/10" : "border-kumo-line"}`,
35194
+ children: [/* @__PURE__ */ jsx("p", {
35195
+ className: "font-medium",
35196
+ children: item.displayName
35197
+ }), /* @__PURE__ */ jsxs("p", {
35198
+ className: "text-xs text-kumo-subtle",
35199
+ children: [item.slug, item.isGuest ? _t({
35200
+ id: "89z9Fh",
35201
+ message: " - Guest"
35202
+ }) : item.userId ? _t({
35203
+ id: "X4uMa1",
35204
+ message: " - Linked"
35205
+ }) : ""]
35206
+ })]
35207
+ }, item.id);
35208
+ }),
35209
+ items.length === 0 && /* @__PURE__ */ jsx("p", {
35210
+ className: "text-sm text-kumo-subtle",
35211
+ children: _t({
35212
+ id: "Qc/a/A",
35213
+ message: "No bylines found"
35214
+ })
35215
+ }),
35216
+ nextCursor && /* @__PURE__ */ jsx(Button, {
35217
+ variant: "secondary",
35218
+ className: "w-full mt-2",
35219
+ onClick: () => loadMoreMutation.mutate({
35220
+ search,
35221
+ guestFilter,
35222
+ locale: activeLocale,
35223
+ cursor: nextCursor
35224
+ }),
35225
+ disabled: loadMoreMutation.isPending,
35226
+ children: loadMoreMutation.isPending ? _t({
35227
+ id: "Z3FXyt",
35228
+ message: "Loading..."
35229
+ }) : _t({
35230
+ id: "yQ2kGp",
35231
+ message: "Load more"
35232
+ })
35233
+ })
35234
+ ]
34405
35235
  })]
34406
35236
  }), /* @__PURE__ */ jsxs("div", {
34407
- className: "space-y-2 max-h-[70vh] overflow-auto",
35237
+ className: "rounded-lg border p-6 space-y-6",
34408
35238
  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..."
35239
+ /* @__PURE__ */ jsx("h2", {
35240
+ className: "text-lg font-semibold",
35241
+ children: selected ? _t({
35242
+ id: "N6j2JH",
35243
+ message: "Edit {0}",
35244
+ values: { 0: selected.displayName }
34444
35245
  }) : _t({
34445
- id: "yQ2kGp",
34446
- message: "Load more"
35246
+ id: "d6HWlw",
35247
+ message: "Create byline"
34447
35248
  })
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
- }),
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
35249
  }),
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"
35250
+ /* @__PURE__ */ jsxs("div", {
35251
+ className: "space-y-4",
35252
+ children: [
35253
+ /* @__PURE__ */ jsx(Input, {
35254
+ label: _t({
35255
+ id: "pfa8F0",
35256
+ message: "Display name"
35257
+ }),
35258
+ value: form.displayName,
35259
+ onChange: (e) => setForm((prev) => ({
35260
+ ...prev,
35261
+ displayName: e.target.value
35262
+ }))
34530
35263
  }),
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
- }))
35264
+ /* @__PURE__ */ jsx(Input, {
35265
+ label: _t({
35266
+ id: "L85WcV",
35267
+ message: "Slug"
35268
+ }),
35269
+ value: form.slug,
35270
+ onChange: (e) => setForm((prev) => ({
35271
+ ...prev,
35272
+ slug: e.target.value
35273
+ }))
35274
+ }),
35275
+ /* @__PURE__ */ jsx(Input, {
35276
+ label: _t({
35277
+ id: "XYcw7G",
35278
+ message: "Website URL"
35279
+ }),
35280
+ value: form.websiteUrl,
35281
+ onChange: (e) => setForm((prev) => ({
35282
+ ...prev,
35283
+ websiteUrl: e.target.value
35284
+ }))
35285
+ }),
35286
+ /* @__PURE__ */ jsx(InputArea, {
35287
+ label: _t({
35288
+ id: "E+oKCO",
35289
+ message: "Bio"
35290
+ }),
35291
+ value: form.bio,
35292
+ onChange: (e) => setForm((prev) => ({
35293
+ ...prev,
35294
+ bio: e.target.value
35295
+ })),
35296
+ rows: 5
35297
+ }),
35298
+ /* @__PURE__ */ jsx(Select, {
35299
+ label: _t({
35300
+ id: "nti0N3",
35301
+ message: "Linked user"
35302
+ }),
35303
+ value: form.userId ?? "",
35304
+ onValueChange: (v) => {
35305
+ const val = v || null;
35306
+ setForm((prev) => ({
35307
+ ...prev,
35308
+ userId: val,
35309
+ isGuest: val ? false : prev.isGuest
35310
+ }));
35311
+ },
35312
+ items: {
35313
+ "": _t({
35314
+ id: "n9o2+L",
35315
+ message: "No linked user"
35316
+ }),
35317
+ ...Object.fromEntries(users.map((u) => [u.id, getUserLabel(u)]))
35318
+ },
35319
+ className: "w-full"
35320
+ }),
35321
+ /* @__PURE__ */ jsx(Switch, {
35322
+ label: _t({
35323
+ id: "6CRaxp",
35324
+ message: "Guest byline"
35325
+ }),
35326
+ checked: form.isGuest,
35327
+ onCheckedChange: (checked) => setForm((prev) => ({
35328
+ ...prev,
35329
+ isGuest: checked,
35330
+ userId: checked ? null : prev.userId
35331
+ }))
35332
+ }),
35333
+ /* @__PURE__ */ jsx(DialogError, { message: getMutationError(mutationError) }),
35334
+ /* @__PURE__ */ jsxs("div", {
35335
+ className: "flex gap-2 pt-2",
35336
+ children: [/* @__PURE__ */ jsx(Button, {
35337
+ onClick: () => {
35338
+ if (selected) updateMutation.mutate();
35339
+ else createMutation.mutate();
35340
+ },
35341
+ disabled: !form.displayName || !form.slug || isSaving,
35342
+ children: isSaving ? _t({
35343
+ id: "XvjC4F",
35344
+ message: "Saving..."
35345
+ }) : selected ? _t({
35346
+ id: "tfDRzk",
35347
+ message: "Save"
35348
+ }) : _t({
35349
+ id: "hYgDIe",
35350
+ message: "Create"
35351
+ })
35352
+ }), selected && /* @__PURE__ */ jsx(Button, {
35353
+ variant: "destructive",
35354
+ onClick: () => setShowDeleteConfirm(true),
35355
+ disabled: deleteMutation.isPending,
35356
+ children: _t({
35357
+ id: "cnGeoo",
35358
+ message: "Delete"
35359
+ })
35360
+ })]
35361
+ })
35362
+ ]
34546
35363
  }),
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();
35364
+ selected && isMultiLocale && i18n ? /* @__PURE__ */ jsx("div", {
35365
+ className: "border-t pt-6",
35366
+ children: /* @__PURE__ */ jsx(TranslationsPanel, {
35367
+ locales: i18n.locales,
35368
+ defaultLocale: i18n.defaultLocale,
35369
+ currentLocale: selected.locale,
35370
+ translations: translationsData?.items ?? [],
35371
+ onOpen: (summary) => {
35372
+ navigate({
35373
+ to: "/bylines",
35374
+ search: { locale: summary.locale }
35375
+ });
35376
+ setSelectedId(summary.id);
34554
35377
  },
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
- })
35378
+ onCreate: (locale) => translateMutation.mutate(locale),
35379
+ pendingLocale: pendingTranslationLocale
35380
+ })
35381
+ }) : null
34576
35382
  ]
34577
35383
  })]
34578
35384
  }),
@@ -36140,10 +36946,11 @@ function ContentNewPage() {
36140
36946
  queryKey: ["manifest"],
36141
36947
  queryFn: fetchManifest
36142
36948
  });
36949
+ const pickerLocale = locale ?? manifest?.i18n?.defaultLocale;
36143
36950
  const createMutation = useMutation({
36144
36951
  mutationFn: (data) => createContent(collection, {
36145
36952
  ...data,
36146
- locale
36953
+ locale: pickerLocale
36147
36954
  }),
36148
36955
  onSuccess: (result) => {
36149
36956
  queryClient.invalidateQueries({ queryKey: ["content", collection] });
@@ -36157,14 +36964,23 @@ function ContentNewPage() {
36157
36964
  }
36158
36965
  });
36159
36966
  const pluginBlocks = React$1.useMemo(() => manifest ? getPluginBlocks(manifest) : [], [manifest]);
36160
- const { data: bylinesData } = useQuery({
36161
- queryKey: ["bylines"],
36162
- queryFn: () => fetchBylines({ limit: 100 })
36967
+ const { data: bylinesData, isSuccess: bylinesLoaded } = useQuery({
36968
+ queryKey: [
36969
+ "bylines",
36970
+ "picker",
36971
+ pickerLocale ?? null
36972
+ ],
36973
+ queryFn: () => fetchBylines({
36974
+ locale: pickerLocale,
36975
+ limit: 100
36976
+ }),
36977
+ enabled: !!manifest
36163
36978
  });
36164
36979
  const createBylineMutation = useMutation({
36165
36980
  mutationFn: (input) => createByline({
36166
36981
  ...input,
36167
- isGuest: true
36982
+ isGuest: true,
36983
+ locale: pickerLocale
36168
36984
  }),
36169
36985
  onSuccess: () => {
36170
36986
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
@@ -36190,10 +37006,13 @@ function ContentNewPage() {
36190
37006
  collectionLabel: collectionConfig.labelSingular || collectionConfig.label,
36191
37007
  fields: collectionConfig.fields,
36192
37008
  isNew: true,
37009
+ entryLocale: pickerLocale,
37010
+ i18n: manifest?.i18n,
36193
37011
  isSaving: createMutation.isPending,
36194
37012
  onSave: handleSave,
36195
37013
  pluginBlocks,
36196
37014
  availableBylines: bylinesData?.items,
37015
+ availableBylinesLoaded: bylinesLoaded,
36197
37016
  selectedBylines,
36198
37017
  onBylinesChange: setSelectedBylines,
36199
37018
  onQuickCreateByline: async (input) => {
@@ -36303,14 +37122,24 @@ function ContentEditPage() {
36303
37122
  enabled: !!currentUser && currentUser.role >= ROLE_EDITOR,
36304
37123
  staleTime: 300 * 1e3
36305
37124
  });
36306
- const { data: bylinesData } = useQuery({
36307
- queryKey: ["bylines"],
36308
- queryFn: () => fetchBylines({ limit: 100 })
37125
+ const itemLocale = rawItem?.locale ?? void 0;
37126
+ const { data: bylinesData, isSuccess: bylinesLoaded } = useQuery({
37127
+ queryKey: [
37128
+ "bylines",
37129
+ "picker",
37130
+ itemLocale ?? null
37131
+ ],
37132
+ queryFn: () => fetchBylines({
37133
+ locale: itemLocale,
37134
+ limit: 100
37135
+ }),
37136
+ enabled: !!itemLocale
36309
37137
  });
36310
37138
  const createBylineMutation = useMutation({
36311
37139
  mutationFn: (input) => createByline({
36312
37140
  ...input,
36313
- isGuest: true
37141
+ isGuest: true,
37142
+ locale: itemLocale
36314
37143
  }),
36315
37144
  onSuccess: () => {
36316
37145
  queryClient.invalidateQueries({ queryKey: ["bylines"] });
@@ -36693,6 +37522,7 @@ function ContentEditPage() {
36693
37522
  hasSeo: collectionConfig.hasSeo,
36694
37523
  onSeoChange: handleSeoChange,
36695
37524
  availableBylines: bylinesData?.items,
37525
+ availableBylinesLoaded: bylinesLoaded,
36696
37526
  onQuickCreateByline: async (input) => {
36697
37527
  return await createBylineMutation.mutateAsync(input);
36698
37528
  },
@@ -36985,7 +37815,7 @@ function MarketplaceBrowsePage() {
36985
37815
  const { data: plugins } = useQuery({
36986
37816
  queryKey: ["plugins"],
36987
37817
  queryFn: async () => {
36988
- const { fetchPlugins } = await import("./plugins-qrjvPK4g.js").then((n) => n.a);
37818
+ const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
36989
37819
  return fetchPlugins();
36990
37820
  }
36991
37821
  });
@@ -37016,7 +37846,7 @@ function MarketplaceDetailPage() {
37016
37846
  const { data: plugins } = useQuery({
37017
37847
  queryKey: ["plugins"],
37018
37848
  queryFn: async () => {
37019
- const { fetchPlugins } = await import("./plugins-qrjvPK4g.js").then((n) => n.a);
37849
+ const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
37020
37850
  return fetchPlugins();
37021
37851
  }
37022
37852
  });
@@ -37100,10 +37930,15 @@ const usersRoute = createRoute({
37100
37930
  path: "/users",
37101
37931
  component: UsersPage
37102
37932
  });
37933
+ function parseBylinesLocaleSearch(search) {
37934
+ if (typeof search.locale === "string" && search.locale.length > 0) return { locale: search.locale };
37935
+ return { locale: void 0 };
37936
+ }
37103
37937
  const bylinesRoute = createRoute({
37104
37938
  getParentRoute: () => adminLayoutRoute,
37105
37939
  path: "/bylines",
37106
- component: BylinesPage
37940
+ component: BylinesPage,
37941
+ validateSearch: parseBylinesLocaleSearch
37107
37942
  });
37108
37943
  const contentTypesListRoute = createRoute({
37109
37944
  getParentRoute: () => adminLayoutRoute,
@@ -37488,5 +38323,5 @@ function AdminApp({ pluginAdmins = EMPTY_PLUGINS, authProviders = EMPTY_AUTH_PRO
37488
38323
  }
37489
38324
 
37490
38325
  //#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 };
38326
+ 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
38327
  //# sourceMappingURL=index.js.map