@emdash-cms/admin 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/{LocaleDirectionProvider-C-w4lOU2.js → LocaleDirectionProvider-BNEVyvgW.js} +17 -17
  2. package/dist/{LocaleDirectionProvider-C-w4lOU2.js.map → LocaleDirectionProvider-BNEVyvgW.js.map} +1 -1
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +562 -193
  6. package/dist/index.js.map +1 -1
  7. package/dist/locales/ar/messages.mjs +1 -1
  8. package/dist/locales/de/messages.mjs +1 -1
  9. package/dist/locales/en/messages.mjs +1 -1
  10. package/dist/locales/es-419/messages.mjs +1 -1
  11. package/dist/locales/es-ES/messages.mjs +1 -1
  12. package/dist/locales/eu/messages.mjs +1 -1
  13. package/dist/locales/fa/messages.mjs +1 -1
  14. package/dist/locales/fr/messages.mjs +1 -1
  15. package/dist/locales/id/messages.mjs +1 -1
  16. package/dist/locales/index.js +1 -1
  17. package/dist/locales/ja/messages.mjs +1 -1
  18. package/dist/locales/ko/messages.mjs +1 -1
  19. package/dist/locales/pl/messages.mjs +1 -1
  20. package/dist/locales/pseudo/messages.mjs +1 -1
  21. package/dist/locales/pt-BR/messages.mjs +1 -1
  22. package/dist/locales/zh-CN/messages.mjs +1 -1
  23. package/dist/locales/zh-TW/messages.mjs +1 -1
  24. package/dist/messages-6iBQvP4l.js +6 -0
  25. package/dist/messages-6iBQvP4l.js.map +1 -0
  26. package/dist/messages-B3qgKgae.js +6 -0
  27. package/dist/messages-B3qgKgae.js.map +1 -0
  28. package/dist/messages-B8kb5Oko.js +6 -0
  29. package/dist/messages-B8kb5Oko.js.map +1 -0
  30. package/dist/messages-BhSzAEGL.js +6 -0
  31. package/dist/messages-BhSzAEGL.js.map +1 -0
  32. package/dist/messages-C0wfBdmD.js +6 -0
  33. package/dist/messages-C0wfBdmD.js.map +1 -0
  34. package/dist/messages-CboPeBM0.js +6 -0
  35. package/dist/messages-CboPeBM0.js.map +1 -0
  36. package/dist/messages-CehVxd9q.js +6 -0
  37. package/dist/messages-CehVxd9q.js.map +1 -0
  38. package/dist/messages-D2R4PbiQ.js +6 -0
  39. package/dist/messages-D2R4PbiQ.js.map +1 -0
  40. package/dist/messages-DHha7f8K.js +6 -0
  41. package/dist/messages-DHha7f8K.js.map +1 -0
  42. package/dist/messages-DIehX9BP.js +6 -0
  43. package/dist/messages-DIehX9BP.js.map +1 -0
  44. package/dist/messages-DKdkwHEw.js +6 -0
  45. package/dist/messages-DKdkwHEw.js.map +1 -0
  46. package/dist/messages-DMJobUj_.js +6 -0
  47. package/dist/messages-DMJobUj_.js.map +1 -0
  48. package/dist/messages-Ddgurz-D.js +6 -0
  49. package/dist/messages-Ddgurz-D.js.map +1 -0
  50. package/dist/messages-LeijTgr3.js +6 -0
  51. package/dist/messages-LeijTgr3.js.map +1 -0
  52. package/dist/messages-S5UPBqIz.js +6 -0
  53. package/dist/messages-S5UPBqIz.js.map +1 -0
  54. package/dist/messages-vQ-B-9ct.js +6 -0
  55. package/dist/messages-vQ-B-9ct.js.map +1 -0
  56. package/dist/plugins-BZzztFdK.js.map +1 -1
  57. package/dist/styles.css +2 -2
  58. package/package.json +9 -9
  59. package/dist/messages-B1vNWgt7.js +0 -6
  60. package/dist/messages-B1vNWgt7.js.map +0 -1
  61. package/dist/messages-B4SjKBpv.js +0 -6
  62. package/dist/messages-B4SjKBpv.js.map +0 -1
  63. package/dist/messages-BMFLZDHo.js +0 -6
  64. package/dist/messages-BMFLZDHo.js.map +0 -1
  65. package/dist/messages-BWDz4uQu.js +0 -6
  66. package/dist/messages-BWDz4uQu.js.map +0 -1
  67. package/dist/messages-BXt6OWYC.js +0 -6
  68. package/dist/messages-BXt6OWYC.js.map +0 -1
  69. package/dist/messages-CA-O-TjM.js +0 -6
  70. package/dist/messages-CA-O-TjM.js.map +0 -1
  71. package/dist/messages-CX3dih9c.js +0 -6
  72. package/dist/messages-CX3dih9c.js.map +0 -1
  73. package/dist/messages-CXZHQHKt.js +0 -6
  74. package/dist/messages-CXZHQHKt.js.map +0 -1
  75. package/dist/messages-Cz2f6Su-.js +0 -6
  76. package/dist/messages-Cz2f6Su-.js.map +0 -1
  77. package/dist/messages-DW1BWiJ5.js +0 -6
  78. package/dist/messages-DW1BWiJ5.js.map +0 -1
  79. package/dist/messages-DpMKfmmV.js +0 -6
  80. package/dist/messages-DpMKfmmV.js.map +0 -1
  81. package/dist/messages-hYPn9ilH.js +0 -6
  82. package/dist/messages-hYPn9ilH.js.map +0 -1
  83. package/dist/messages-qYhBOhtE.js +0 -6
  84. package/dist/messages-qYhBOhtE.js.map +0 -1
  85. package/dist/messages-rxCTcw_6.js +0 -6
  86. package/dist/messages-rxCTcw_6.js.map +0 -1
  87. package/dist/messages-wpqHOBfn.js +0 -6
  88. package/dist/messages-wpqHOBfn.js.map +0 -1
  89. package/dist/messages-zuRDwn8n.js +0 -6
  90. package/dist/messages-zuRDwn8n.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { c as fetchAuthMode, d as throwResponseError, i as fetchPlugins, l as fetchManifest, n as enablePlugin, o as API_BASE, r as fetchPlugin, s as apiFetch, t as disablePlugin, u as parseApiResponse } from "./plugins-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";
2
+ import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-BNEVyvgW.js";
3
3
  import "./locales/index.js";
4
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";
@@ -39,6 +39,7 @@ import { isAllowedUri } from "@tiptap/extension-link";
39
39
  import { BlockRenderer } from "@emdash-cms/blocks";
40
40
  import DOMPurify from "dompurify";
41
41
  import { Marked, Renderer } from "marked";
42
+ import { checkEnvCompatibility, hostEnvFromVersions } from "@emdash-cms/registry-client/env";
42
43
  import { useHotkeys } from "react-hotkeys-hook";
43
44
 
44
45
  //#region src/components/ThemeProvider.tsx
@@ -7957,19 +7958,17 @@ function PluginBlockModal({ block, initialValues, onClose, onInsert }) {
7957
7958
  size: dialogSize,
7958
7959
  children: [/* @__PURE__ */ jsxs("div", {
7959
7960
  className: "flex items-start justify-between gap-4 mb-4",
7960
- children: [/* @__PURE__ */ jsxs(Dialog.Title, {
7961
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
7961
7962
  className: "text-lg font-semibold leading-none tracking-tight",
7962
- children: [
7963
- isEditing ? _t2({
7964
- id: "ePK91l",
7965
- message: "Edit"
7966
- }) : _t2({
7967
- id: "S8Yqbl",
7968
- message: "Insert"
7969
- }),
7970
- " ",
7971
- block?.label || ""
7972
- ]
7963
+ children: isEditing ? _t2({
7964
+ id: "N6j2JH",
7965
+ message: "Edit {0}",
7966
+ values: { 0: block?.label || "" }
7967
+ }) : _t2({
7968
+ id: "yQNfsE",
7969
+ message: "Insert {0}",
7970
+ values: { 0: block?.label || "" }
7971
+ })
7973
7972
  }), /* @__PURE__ */ jsx(Dialog.Close, {
7974
7973
  "aria-label": _t2({
7975
7974
  id: "yz7wBu",
@@ -10319,6 +10318,7 @@ function termExactMatches(term, input) {
10319
10318
  * - Checkbox tree for hierarchical taxonomies (categories)
10320
10319
  * - Tag input for flat taxonomies (tags)
10321
10320
  */
10321
+ const EMPTY_TERMS = [];
10322
10322
  /**
10323
10323
  * Fetch taxonomy definitions
10324
10324
  */
@@ -10386,11 +10386,13 @@ function CategoryCheckboxTree({ term, level = 0, selectedIds, onToggle }) {
10386
10386
  function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, label }) {
10387
10387
  const { _: _t } = useLingui();
10388
10388
  const [input, setInput] = React$1.useState("");
10389
+ const [isOpen, setIsOpen] = React$1.useState(false);
10389
10390
  const selectedTerms = terms.filter((term) => selectedIds.has(term.id));
10390
10391
  const trimmedInput = input.trim();
10391
10392
  const suggestions = React$1.useMemo(() => {
10392
- if (!trimmedInput) return [];
10393
- return terms.filter((term) => !selectedIds.has(term.id) && termMatches(term, trimmedInput)).slice(0, 5);
10393
+ const availableTerms = terms.filter((term) => !selectedIds.has(term.id));
10394
+ if (!trimmedInput) return availableTerms.slice(0, 5);
10395
+ return availableTerms.filter((term) => termMatches(term, trimmedInput)).slice(0, 5);
10394
10396
  }, [
10395
10397
  trimmedInput,
10396
10398
  terms,
@@ -10404,11 +10406,18 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
10404
10406
  const handleSelect = (term) => {
10405
10407
  onAdd(term.id);
10406
10408
  setInput("");
10409
+ setIsOpen(false);
10407
10410
  };
10408
10411
  const handleCreate = () => {
10409
10412
  if (!trimmedInput || isCreating) return;
10410
10413
  onCreate(trimmedInput);
10411
10414
  setInput("");
10415
+ setIsOpen(false);
10416
+ };
10417
+ const handleBlur = (e) => {
10418
+ const nextFocused = e.relatedTarget;
10419
+ if (nextFocused instanceof Node && e.currentTarget.contains(nextFocused)) return;
10420
+ setIsOpen(false);
10412
10421
  };
10413
10422
  const handleKeyDown = (e) => {
10414
10423
  if (e.key === "Enter") {
@@ -10437,9 +10446,14 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
10437
10446
  }, term.id))
10438
10447
  }), /* @__PURE__ */ jsxs("div", {
10439
10448
  className: "relative",
10449
+ onBlur: handleBlur,
10440
10450
  children: [/* @__PURE__ */ jsx(Input, {
10441
10451
  value: input,
10442
- onChange: (e) => setInput(e.target.value),
10452
+ onChange: (e) => {
10453
+ setInput(e.target.value);
10454
+ setIsOpen(true);
10455
+ },
10456
+ onFocus: () => setIsOpen(true),
10443
10457
  onKeyDown: handleKeyDown,
10444
10458
  placeholder: _t({
10445
10459
  id: "SnJVrE",
@@ -10451,15 +10465,17 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
10451
10465
  values: { label }
10452
10466
  }),
10453
10467
  className: "text-sm"
10454
- }), (suggestions.length > 0 || showCreateOption) && /* @__PURE__ */ jsxs("div", {
10468
+ }), isOpen && (suggestions.length > 0 || showCreateOption) && /* @__PURE__ */ jsxs("div", {
10455
10469
  className: "absolute top-full start-0 end-0 mt-1 bg-kumo-overlay border rounded-md shadow-lg z-10",
10456
10470
  children: [suggestions.map((term) => /* @__PURE__ */ jsx("button", {
10457
10471
  type: "button",
10472
+ onMouseDown: (e) => e.preventDefault(),
10458
10473
  onClick: () => handleSelect(term),
10459
10474
  className: "w-full text-start px-3 py-2 text-sm hover:bg-kumo-tint",
10460
10475
  children: term.label
10461
10476
  }, term.id)), showCreateOption && /* @__PURE__ */ jsxs("button", {
10462
10477
  type: "button",
10478
+ onMouseDown: (e) => e.preventDefault(),
10463
10479
  onClick: handleCreate,
10464
10480
  disabled: isCreating,
10465
10481
  className: "w-full text-start px-3 py-2 text-sm hover:bg-kumo-tint text-kumo-accent flex items-center gap-1 border-t",
@@ -10485,11 +10501,11 @@ function TaxonomySection({ taxonomy, collection, entryId, onChange }) {
10485
10501
  const toastManager = Toast.useToastManager();
10486
10502
  const [newCategoryLabel, setNewCategoryLabel] = React$1.useState("");
10487
10503
  const [showCategoryInput, setShowCategoryInput] = React$1.useState(false);
10488
- const { data: terms = [] } = useQuery({
10504
+ const { data: terms = EMPTY_TERMS } = useQuery({
10489
10505
  queryKey: ["taxonomy-terms", taxonomy.name],
10490
10506
  queryFn: () => fetchTerms$1(taxonomy.name)
10491
10507
  });
10492
- const { data: entryTerms = [] } = useQuery({
10508
+ const { data: entryTerms = EMPTY_TERMS } = useQuery({
10493
10509
  queryKey: [
10494
10510
  "entry-terms",
10495
10511
  collection,
@@ -18032,6 +18048,85 @@ function formatInstallCount(count) {
18032
18048
  return String(count);
18033
18049
  }
18034
18050
 
18051
+ //#endregion
18052
+ //#region src/lib/markdown.ts
18053
+ const HTML_ESCAPE_MAP = {
18054
+ "&": "&",
18055
+ "<": "&lt;",
18056
+ ">": "&gt;",
18057
+ "\"": "&quot;",
18058
+ "'": "&#39;"
18059
+ };
18060
+ const HTML_ESCAPE_RE = /[&<>"']/g;
18061
+ function escapeHtml(str) {
18062
+ return str.replace(HTML_ESCAPE_RE, (ch) => HTML_ESCAPE_MAP[ch]);
18063
+ }
18064
+ const renderer = new Renderer();
18065
+ renderer.link = function({ href, tokens }) {
18066
+ const inner = this.parser.parseInline(tokens);
18067
+ if (!SAFE_URL_RE.test(href)) return inner;
18068
+ return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${inner}</a>`;
18069
+ };
18070
+ renderer.image = ({ text }) => escapeHtml(text);
18071
+ renderer.html = () => "";
18072
+ const md = new Marked({
18073
+ renderer,
18074
+ async: false
18075
+ });
18076
+ /** Allowed tags and attributes for DOMPurify — only standard markdown output. */
18077
+ const SANITIZE_CONFIG = {
18078
+ ALLOWED_TAGS: [
18079
+ "h1",
18080
+ "h2",
18081
+ "h3",
18082
+ "h4",
18083
+ "h5",
18084
+ "h6",
18085
+ "p",
18086
+ "a",
18087
+ "ul",
18088
+ "ol",
18089
+ "li",
18090
+ "blockquote",
18091
+ "pre",
18092
+ "code",
18093
+ "em",
18094
+ "strong",
18095
+ "del",
18096
+ "br",
18097
+ "hr",
18098
+ "table",
18099
+ "thead",
18100
+ "tbody",
18101
+ "tr",
18102
+ "th",
18103
+ "td",
18104
+ "details",
18105
+ "summary",
18106
+ "sup",
18107
+ "sub"
18108
+ ],
18109
+ ALLOWED_ATTR: [
18110
+ "href",
18111
+ "target",
18112
+ "rel"
18113
+ ]
18114
+ };
18115
+ /**
18116
+ * Render untrusted Markdown to a sanitized HTML string safe for
18117
+ * `dangerouslySetInnerHTML`.
18118
+ *
18119
+ * Defense in depth: the `marked` renderer drops raw HTML, drops images, and
18120
+ * emits only `https?:` links (escaped, with `rel="noopener noreferrer"`);
18121
+ * DOMPurify then re-sanitizes the output against a tag/attribute allowlist
18122
+ * (no `script`/`style`/`img`/`iframe`, no inline styles or event handlers).
18123
+ */
18124
+ function renderMarkdown(markdown) {
18125
+ const result = md.parse(markdown);
18126
+ const html = typeof result === "string" ? result : "";
18127
+ return DOMPurify.sanitize(html, SANITIZE_CONFIG);
18128
+ }
18129
+
18035
18130
  //#endregion
18036
18131
  //#region src/components/CapabilityConsentDialog.tsx
18037
18132
  /**
@@ -18284,6 +18379,77 @@ function releaseExemptFromMinimumAge(exclude, publisherDid, slug) {
18284
18379
  }
18285
18380
  return false;
18286
18381
  }
18382
+ /**
18383
+ * The FAIR-recognised long-form section keys, in display order. Publishers may
18384
+ * also ship unrecognised keys (the lexicon's `sections` map is open), but the
18385
+ * admin renders only this known set so an aggregator can't inject a section
18386
+ * with an attacker-chosen heading; everything else is ignored.
18387
+ */
18388
+ const SECTION_ORDER = [
18389
+ "description",
18390
+ "installation",
18391
+ "faq",
18392
+ "changelog",
18393
+ "security"
18394
+ ];
18395
+ /**
18396
+ * Select the non-empty long-form sections off a package profile, in
18397
+ * `SECTION_ORDER`. `profile.sections` is a lexicon-validated map of Markdown
18398
+ * strings (or `null` when the aggregator returned a non-conforming record), so
18399
+ * each value is narrowed to a non-whitespace string before inclusion. Empty,
18400
+ * missing, whitespace-only, and non-string entries are dropped, so callers can
18401
+ * suppress the whole sections UI when the result is empty.
18402
+ */
18403
+ function presentSections(profile) {
18404
+ const sections = profile?.sections;
18405
+ if (!sections || typeof sections !== "object") return [];
18406
+ const map = sections;
18407
+ const out = [];
18408
+ for (const key of SECTION_ORDER) {
18409
+ const value = map[key];
18410
+ if (typeof value === "string" && value.trim().length > 0) out.push({
18411
+ key,
18412
+ markdown: value
18413
+ });
18414
+ }
18415
+ return out;
18416
+ }
18417
+ /**
18418
+ * Narrow a release record's `sbom` field to the fields the admin renders.
18419
+ * Returns `null` unless the value is an object carrying at least one usable
18420
+ * field (`format` or `url`); every field is independently optional per the
18421
+ * lexicon. `sbom` is lexicon-validated at the DiscoveryClient boundary, but the
18422
+ * record is a publisher pass-through, so its inner shape still needs narrowing.
18423
+ */
18424
+ function extractSbom(value) {
18425
+ if (!value || typeof value !== "object") return null;
18426
+ const v = value;
18427
+ const sbom = {};
18428
+ if (typeof v.format === "string" && v.format.length > 0) sbom.format = v.format;
18429
+ if (typeof v.url === "string" && v.url.length > 0) sbom.url = v.url;
18430
+ if (typeof v.checksum === "string") sbom.checksum = v.checksum;
18431
+ if (!sbom.format && !sbom.url) return null;
18432
+ return sbom;
18433
+ }
18434
+ /**
18435
+ * Validate an SBOM document URL for use in a download `href`. Returns the
18436
+ * normalised URL only when it is an absolute `http(s)` URL; everything else
18437
+ * (relative, `javascript:`, `data:`, non-string) returns `null`. The release
18438
+ * record is a remote pass-through, so an unsanitised SBOM `href` would be
18439
+ * stored XSS in the authenticated admin origin. The browser fetches the SBOM
18440
+ * client-side on click — no server proxy, so SSRF isn't a concern here.
18441
+ */
18442
+ function sbomDownloadHref(value) {
18443
+ if (typeof value !== "string" || value.length === 0) return null;
18444
+ let parsed;
18445
+ try {
18446
+ parsed = new URL(value);
18447
+ } catch {
18448
+ return null;
18449
+ }
18450
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
18451
+ return parsed.href;
18452
+ }
18287
18453
  async function searchRegistryPackages(config, opts) {
18288
18454
  return (await getDiscoveryClient(config)).searchPackages(opts);
18289
18455
  }
@@ -18297,6 +18463,17 @@ async function listRegistryReleases(config, did, slug, opts) {
18297
18463
  return (await getDiscoveryClient(config)).listReleases(did, slug, opts);
18298
18464
  }
18299
18465
  /**
18466
+ * Derive the host environment versions (`env:emdash`, `env:astro`) the running
18467
+ * EmDash install advertises, so a release's `requires` constraints can be
18468
+ * evaluated client-side before offering install. Reads the already-fetched
18469
+ * admin manifest (`version`, `astroVersion`) rather than issuing a second
18470
+ * request. The dev-skip / astro-omit rule is shared with the server gate via
18471
+ * `hostEnvFromVersions`.
18472
+ */
18473
+ function hostEnvFromManifest(manifest) {
18474
+ return hostEnvFromVersions(manifest?.version, manifest?.astroVersion);
18475
+ }
18476
+ /**
18300
18477
  * Resolve a publisher DID to its claimed handle using the same
18301
18478
  * `LocalActorResolver` pattern as `@emdash-cms/plugin-cli` and
18302
18479
  * `@emdash-cms/auth-atproto`. Bidirectional verification (handle's
@@ -18388,6 +18565,66 @@ async function resolveDidToHandle(did) {
18388
18565
  writeHandleCache(did, result);
18389
18566
  return result;
18390
18567
  }
18568
+ const ARTIFACT_PROXY_ENDPOINT = `${API_BASE}/admin/plugins/registry/artifact`;
18569
+ /**
18570
+ * Build the URL of the server-side artifact proxy for an artifact addressed by
18571
+ * its `(did, slug, version, kind, index)` coordinates. The browser never sends
18572
+ * the publisher's URL — the proxy resolves the *declared* URL from the release
18573
+ * record, applies SSRF defences, enforces an image content-type allowlist, and
18574
+ * serves the bytes back same-origin.
18575
+ *
18576
+ * Empty `version` (latest) and `index` (non-screenshot kinds) are omitted.
18577
+ */
18578
+ function artifactProxyUrl(coords) {
18579
+ const params = new URLSearchParams();
18580
+ params.set("did", coords.did);
18581
+ params.set("slug", coords.slug);
18582
+ params.set("kind", coords.kind);
18583
+ if (coords.version) params.set("version", coords.version);
18584
+ if (coords.kind === "screenshot" && coords.index !== void 0) params.set("index", String(coords.index));
18585
+ return `${ARTIFACT_PROXY_ENDPOINT}?${params.toString()}`;
18586
+ }
18587
+ /**
18588
+ * Narrow one entry of a release's `artifacts` map to the fields we render.
18589
+ * Returns `null` when the value isn't an object carrying a usable `url`
18590
+ * (presence gate), keeping only the dimensions for layout.
18591
+ *
18592
+ * Records are lexicon-validated at the DiscoveryClient boundary, but
18593
+ * `artifacts` is an aggregator pass-through, so each entry still needs
18594
+ * shape-narrowing.
18595
+ */
18596
+ function asMediaArtifact(value) {
18597
+ if (!value || typeof value !== "object") return null;
18598
+ const v = value;
18599
+ if (typeof v.url !== "string" || v.url.length === 0) return null;
18600
+ const artifact = {};
18601
+ if (typeof v.width === "number") artifact.width = v.width;
18602
+ if (typeof v.height === "number") artifact.height = v.height;
18603
+ return artifact;
18604
+ }
18605
+ /**
18606
+ * Pull icon, banner, and the screenshot gallery out of a release's `artifacts`
18607
+ * map, keeping presence and dimensions only. The lexicon types `screenshots`
18608
+ * as an array of artifacts; entries without a usable `url` are dropped, and
18609
+ * gallery order is preserved so screenshot indices line up with the proxy's.
18610
+ */
18611
+ function extractMediaArtifacts(artifacts) {
18612
+ const result = { screenshots: [] };
18613
+ if (!artifacts || typeof artifacts !== "object") return result;
18614
+ const map = artifacts;
18615
+ const icon = asMediaArtifact(map.icon);
18616
+ if (icon) result.icon = icon;
18617
+ const banner = asMediaArtifact(map.banner);
18618
+ if (banner) result.banner = banner;
18619
+ if (Array.isArray(map.screenshots)) map.screenshots.forEach((entry, index) => {
18620
+ const artifact = asMediaArtifact(entry);
18621
+ if (artifact) result.screenshots.push({
18622
+ ...artifact,
18623
+ index
18624
+ });
18625
+ });
18626
+ return result;
18627
+ }
18391
18628
  const INSTALL_ENDPOINT = `${API_BASE}/admin/plugins/registry/install`;
18392
18629
  /**
18393
18630
  * Install a plugin from the registry.
@@ -18895,13 +19132,10 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
18895
19132
  const label = CAPABILITY_LABELS[c];
18896
19133
  return label ? _t2(label) : c;
18897
19134
  }).join(", "),
18898
- children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-3 w-3" }), _t2({
18899
- id: "gMTYze",
18900
- message: "{0} permission{1}",
18901
- values: {
18902
- 0: plugin.capabilities.length,
18903
- 1: plugin.capabilities.length !== 1 ? "s" : ""
18904
- }
19135
+ children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-3 w-3" }), i18n._({
19136
+ id: "dB/Iw4",
19137
+ message: "{0, plural, one {# permission} other {# permissions}}",
19138
+ values: { 0: plugin.capabilities.length }
18905
19139
  })]
18906
19140
  })
18907
19141
  ]
@@ -19662,72 +19896,6 @@ function ScreenshotLightbox({ screenshots, index, isBlurred = false, onClose, on
19662
19896
  ]
19663
19897
  });
19664
19898
  }
19665
- const HTML_ESCAPE_MAP = {
19666
- "&": "&amp;",
19667
- "<": "&lt;",
19668
- ">": "&gt;",
19669
- "\"": "&quot;",
19670
- "'": "&#39;"
19671
- };
19672
- const HTML_ESCAPE_RE = /[&<>"']/g;
19673
- function escapeHtml(str) {
19674
- return str.replace(HTML_ESCAPE_RE, (ch) => HTML_ESCAPE_MAP[ch]);
19675
- }
19676
- const renderer = new Renderer();
19677
- renderer.link = ({ href, text }) => {
19678
- if (!SAFE_URL_RE.test(href)) return escapeHtml(text);
19679
- return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(text)}</a>`;
19680
- };
19681
- renderer.image = ({ text }) => escapeHtml(text);
19682
- renderer.html = () => "";
19683
- const md = new Marked({
19684
- renderer,
19685
- async: false
19686
- });
19687
- /** Allowed tags and attributes for DOMPurify — only standard markdown output. */
19688
- const SANITIZE_CONFIG = {
19689
- ALLOWED_TAGS: [
19690
- "h1",
19691
- "h2",
19692
- "h3",
19693
- "h4",
19694
- "h5",
19695
- "h6",
19696
- "p",
19697
- "a",
19698
- "ul",
19699
- "ol",
19700
- "li",
19701
- "blockquote",
19702
- "pre",
19703
- "code",
19704
- "em",
19705
- "strong",
19706
- "del",
19707
- "br",
19708
- "hr",
19709
- "table",
19710
- "thead",
19711
- "tbody",
19712
- "tr",
19713
- "th",
19714
- "td",
19715
- "details",
19716
- "summary",
19717
- "sup",
19718
- "sub"
19719
- ],
19720
- ALLOWED_ATTR: [
19721
- "href",
19722
- "target",
19723
- "rel"
19724
- ]
19725
- };
19726
- function renderMarkdown(markdown) {
19727
- const result = md.parse(markdown);
19728
- const html = typeof result === "string" ? result : "";
19729
- return DOMPurify.sanitize(html, SANITIZE_CONFIG);
19730
- }
19731
19899
  function formatBytes(bytes) {
19732
19900
  if (bytes < 1024) return `${bytes} B`;
19733
19901
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -23060,6 +23228,11 @@ function RegistryPluginDetail({ pluginId, config }) {
23060
23228
  return fetchPlugins();
23061
23229
  }
23062
23230
  });
23231
+ const { data: manifest } = useQuery({
23232
+ queryKey: ["manifest"],
23233
+ queryFn: fetchManifest
23234
+ });
23235
+ const hostEnv = React$1.useMemo(() => hostEnvFromManifest(manifest), [manifest]);
23063
23236
  const slashIdx = pluginId.lastIndexOf("/");
23064
23237
  const publisher = slashIdx > 0 ? pluginId.slice(0, slashIdx) : "";
23065
23238
  const slug = slashIdx > 0 ? pluginId.slice(slashIdx + 1) : "";
@@ -23134,11 +23307,51 @@ function RegistryPluginDetail({ pluginId, config }) {
23134
23307
  }] : [];
23135
23308
  });
23136
23309
  const repoHref = safeExternalHref(release?.release?.repo);
23137
- const verified = (pkg?.labels ?? []).some((l) => l.val === "verified");
23310
+ const verifiedLabel = (pkg?.labels ?? []).find((l) => l.val === "verified");
23311
+ const verified = Boolean(verifiedLabel);
23312
+ const verifiedLabeller = typeof verifiedLabel?.src === "string" ? verifiedLabel.src : null;
23313
+ const sections = presentSections(pkgProfile);
23314
+ const [selectedSection, setSelectedSection] = React$1.useState(void 0);
23315
+ const defaultSection = sections[0]?.key;
23316
+ const activeSection = selectedSection && sections.some((s) => s.key === selectedSection) ? selectedSection : defaultSection;
23317
+ const activePane = sections.find((s) => s.key === activeSection);
23318
+ const sbom = extractSbom(release?.release?.sbom);
23319
+ const sbomHref = sbom ? sbomDownloadHref(sbom.url) : null;
23320
+ const lastUpdated = typeof pkgProfile?.lastUpdated === "string" ? pkgProfile.lastUpdated : null;
23321
+ const mediaArtifacts = extractMediaArtifacts(release?.release?.artifacts);
23322
+ const artifactDid = pkg?.did;
23323
+ const artifactVersion = release?.version;
23324
+ const iconSrc = mediaArtifacts.icon && artifactDid ? artifactProxyUrl({
23325
+ did: artifactDid,
23326
+ slug,
23327
+ version: artifactVersion,
23328
+ kind: "icon"
23329
+ }) : null;
23330
+ const bannerSrc = mediaArtifacts.banner && artifactDid ? artifactProxyUrl({
23331
+ did: artifactDid,
23332
+ slug,
23333
+ version: artifactVersion,
23334
+ kind: "banner"
23335
+ }) : null;
23336
+ const screenshots = artifactDid ? mediaArtifacts.screenshots.map((shot) => ({
23337
+ ...shot,
23338
+ src: artifactProxyUrl({
23339
+ did: artifactDid,
23340
+ slug,
23341
+ version: artifactVersion,
23342
+ kind: "screenshot",
23343
+ index: shot.index
23344
+ })
23345
+ })) : [];
23138
23346
  const policyOk = release && pkg ? releasePassesPolicy(release, {
23139
23347
  did: pkg.did,
23140
23348
  slug
23141
23349
  }, config.policy) : true;
23350
+ const envMismatches = React$1.useMemo(() => {
23351
+ if (!release) return [];
23352
+ return checkEnvCompatibility(release.release?.requires, hostEnv);
23353
+ }, [release, hostEnv]);
23354
+ const envOk = envMismatches.length === 0;
23142
23355
  const installedEntry = React$1.useMemo(() => {
23143
23356
  if (!pkg || !installedPlugins) return void 0;
23144
23357
  return installedPlugins.find((p) => p.source === "registry" && p.registryPublisherDid === pkg.did && p.registrySlug === slug);
@@ -23197,14 +23410,33 @@ function RegistryPluginDetail({ pluginId, config }) {
23197
23410
  className: "space-y-6",
23198
23411
  children: [
23199
23412
  /* @__PURE__ */ jsx(BackLink, {}),
23413
+ bannerSrc ? /* @__PURE__ */ jsx("img", {
23414
+ src: bannerSrc,
23415
+ alt: _t({
23416
+ id: "aOmjNp",
23417
+ message: "{0} banner",
23418
+ values: { 0: displayName ?? slug }
23419
+ }),
23420
+ className: "h-40 w-full rounded-xl object-cover",
23421
+ loading: "lazy"
23422
+ }) : null,
23200
23423
  /* @__PURE__ */ jsxs("div", {
23201
23424
  className: "flex flex-wrap items-start gap-4",
23202
23425
  children: [
23203
23426
  /* @__PURE__ */ jsx("div", {
23204
- className: "rounded-xl bg-kumo-subtle p-3 text-kumo-subtle",
23205
- children: /* @__PURE__ */ jsx("span", {
23427
+ className: "overflow-hidden rounded-xl bg-kumo-subtle text-kumo-subtle",
23428
+ children: iconSrc ? /* @__PURE__ */ jsx("img", {
23429
+ src: iconSrc,
23430
+ alt: _t({
23431
+ id: "dVaAiX",
23432
+ message: "{0} icon",
23433
+ values: { 0: displayName ?? slug }
23434
+ }),
23435
+ className: "block h-16 w-16 object-cover",
23436
+ loading: "lazy"
23437
+ }) : /* @__PURE__ */ jsx("span", {
23206
23438
  "aria-hidden": true,
23207
- className: "block h-10 w-10"
23439
+ className: "block h-16 w-16"
23208
23440
  })
23209
23441
  }),
23210
23442
  /* @__PURE__ */ jsxs("div", {
@@ -23215,11 +23447,30 @@ function RegistryPluginDetail({ pluginId, config }) {
23215
23447
  children: [/* @__PURE__ */ jsx("h1", {
23216
23448
  className: "truncate text-3xl font-bold",
23217
23449
  children: displayName ?? slug
23218
- }), verified ? /* @__PURE__ */ jsx(ShieldCheck, {
23219
- className: "h-5 w-5 shrink-0 text-kumo-brand",
23220
- "aria-label": _t({
23221
- id: "70QfpA",
23222
- message: "Verified publisher"
23450
+ }), verified ? /* @__PURE__ */ jsx(Tooltip, {
23451
+ content: verifiedLabeller ? _t({
23452
+ id: "/rITsG",
23453
+ message: "Verified publisher. A labeller ({verifiedLabeller}) has confirmed this publisher's identity.",
23454
+ values: { verifiedLabeller }
23455
+ }) : _t({
23456
+ id: "PCUvJ4",
23457
+ message: "Verified publisher. A labeller has confirmed this publisher's identity."
23458
+ }),
23459
+ render: /* @__PURE__ */ jsx("button", {
23460
+ type: "button",
23461
+ className: "inline-flex shrink-0 cursor-help rounded-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-kumo-brand",
23462
+ "aria-label": verifiedLabeller ? _t({
23463
+ id: "s7Woxs",
23464
+ message: "Verified publisher, confirmed by labeller {verifiedLabeller}",
23465
+ values: { verifiedLabeller }
23466
+ }) : _t({
23467
+ id: "70QfpA",
23468
+ message: "Verified publisher"
23469
+ }),
23470
+ children: /* @__PURE__ */ jsx(ShieldCheck, {
23471
+ className: "h-5 w-5 text-kumo-brand",
23472
+ "aria-hidden": true
23473
+ })
23223
23474
  })
23224
23475
  }) : null]
23225
23476
  }),
@@ -23240,25 +23491,61 @@ function RegistryPluginDetail({ pluginId, config }) {
23240
23491
  }),
23241
23492
  release ? /* @__PURE__ */ jsxs("div", {
23242
23493
  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({
23494
+ children: [
23495
+ /* @__PURE__ */ jsx("p", {
23496
+ className: "text-xs text-kumo-subtle",
23497
+ children: _t({
23247
23498
  id: "YYdC3A",
23248
23499
  message: "Version {0}",
23249
23500
  values: { 0: release.version }
23250
- }),
23251
- " · ",
23252
- _t({
23253
- id: "294VT5",
23254
- message: "indexed {0}",
23255
- values: { 0: formatDate(release.indexedAt) }
23256
23501
  })
23257
- ]
23258
- }), isPreRelease ? /* @__PURE__ */ jsx(Badge, { children: _t({
23259
- id: "omrja/",
23260
- message: "Pre-release"
23261
- }) }) : null]
23502
+ }),
23503
+ isPreRelease ? /* @__PURE__ */ jsx(Badge, { children: _t({
23504
+ id: "omrja/",
23505
+ message: "Pre-release"
23506
+ }) }) : null,
23507
+ sbom?.format ? /* @__PURE__ */ jsx(Badge, { children: _t({
23508
+ id: "mQmHhZ",
23509
+ message: "SBOM · {0}",
23510
+ values: { 0: sbom.format }
23511
+ }) }) : sbom ? /* @__PURE__ */ jsx(Badge, { children: _t({
23512
+ id: "SBWpbn",
23513
+ message: "SBOM"
23514
+ }) }) : null,
23515
+ sbomHref ? /* @__PURE__ */ jsx("a", {
23516
+ href: sbomHref,
23517
+ target: "_blank",
23518
+ rel: "noopener noreferrer",
23519
+ download: true,
23520
+ className: "text-xs text-kumo-brand hover:underline",
23521
+ children: _t({
23522
+ id: "gm9IDy",
23523
+ message: "Download SBOM"
23524
+ })
23525
+ }) : null
23526
+ ]
23527
+ }) : null,
23528
+ release ? /* @__PURE__ */ jsxs("dl", {
23529
+ className: "mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-kumo-subtle",
23530
+ children: [lastUpdated ? /* @__PURE__ */ jsxs("div", {
23531
+ className: "flex items-center gap-1",
23532
+ children: [/* @__PURE__ */ jsx("dt", {
23533
+ className: "font-medium",
23534
+ children: _t({
23535
+ id: "+b7T3G",
23536
+ message: "Updated"
23537
+ })
23538
+ }), /* @__PURE__ */ jsx("dd", { children: formatDate(lastUpdated) })]
23539
+ }) : null, /* @__PURE__ */ jsxs("div", {
23540
+ className: "flex items-center gap-1",
23541
+ children: [/* @__PURE__ */ jsx("dt", {
23542
+ className: "font-medium",
23543
+ children: _t({
23544
+ id: "I3/r1f",
23545
+ message: "Indexed"
23546
+ })
23547
+ }), /* @__PURE__ */ jsx("dd", { children: formatDate(release.indexedAt) })]
23548
+ })]
23262
23549
  }) : null
23263
23550
  ]
23264
23551
  }),
@@ -23312,7 +23599,7 @@ function RegistryPluginDetail({ pluginId, config }) {
23312
23599
  })
23313
23600
  }) : /* @__PURE__ */ jsx(Button, {
23314
23601
  variant: "primary",
23315
- disabled: !release || !policyOk || handleResult.status === "invalid",
23602
+ disabled: !release || !policyOk || !envOk || handleResult.status === "invalid",
23316
23603
  onClick: () => setShowConsent(true),
23317
23604
  children: _t({
23318
23605
  id: "TKQ7K+",
@@ -23374,10 +23661,66 @@ function RegistryPluginDetail({ pluginId, config }) {
23374
23661
  })
23375
23662
  })] })]
23376
23663
  }) : null,
23664
+ release && !envOk ? /* @__PURE__ */ jsxs("div", {
23665
+ className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
23666
+ role: "status",
23667
+ children: [/* @__PURE__ */ jsx(Warning, { className: "mt-0.5 h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [
23668
+ /* @__PURE__ */ jsx("p", {
23669
+ className: "font-medium",
23670
+ children: _t({
23671
+ id: "25SzC8",
23672
+ message: "Not compatible with this environment"
23673
+ })
23674
+ }),
23675
+ /* @__PURE__ */ jsx("p", {
23676
+ className: "mt-1 text-sm text-kumo-default",
23677
+ children: _t({
23678
+ id: "RwFsM0",
23679
+ message: "This release requires a newer environment than your site currently runs. Upgrade before installing."
23680
+ })
23681
+ }),
23682
+ /* @__PURE__ */ jsx("ul", {
23683
+ className: "mt-2 space-y-1 text-sm text-kumo-default",
23684
+ children: envMismatches.map((m) => /* @__PURE__ */ jsx("li", { children: _t({
23685
+ id: "XMcEUU",
23686
+ message: "{0} {1} required — you have {2}.",
23687
+ values: {
23688
+ 0: envLabel(m.key),
23689
+ 1: m.required,
23690
+ 2: m.host
23691
+ }
23692
+ }) }, m.key))
23693
+ })
23694
+ ] })]
23695
+ }) : null,
23377
23696
  description ? /* @__PURE__ */ jsx("p", {
23378
23697
  className: "text-base text-kumo-default",
23379
23698
  children: description
23380
23699
  }) : null,
23700
+ screenshots.length > 0 ? /* @__PURE__ */ jsx("section", {
23701
+ "aria-label": _t({
23702
+ id: "lqROaa",
23703
+ message: "Screenshots"
23704
+ }),
23705
+ children: /* @__PURE__ */ jsx("ul", {
23706
+ className: "flex snap-x gap-3 overflow-x-auto pb-2",
23707
+ children: screenshots.map((shot, i) => /* @__PURE__ */ jsx("li", {
23708
+ className: "shrink-0 snap-start",
23709
+ children: /* @__PURE__ */ jsx("img", {
23710
+ src: shot.src,
23711
+ alt: _t({
23712
+ id: "3rcds3",
23713
+ message: "Screenshot {0}",
23714
+ values: { 0: i + 1 }
23715
+ }),
23716
+ width: shot.width,
23717
+ height: shot.height,
23718
+ className: "h-48 w-auto rounded-lg border border-kumo-default object-cover",
23719
+ loading: "lazy"
23720
+ })
23721
+ }, shot.index))
23722
+ })
23723
+ }) : null,
23381
23724
  licenseText || repoHref || keywordList.length > 0 ? /* @__PURE__ */ jsxs("section", {
23382
23725
  className: "flex flex-wrap items-center gap-2",
23383
23726
  children: [
@@ -23460,6 +23803,27 @@ function RegistryPluginDetail({ pluginId, config }) {
23460
23803
  className: "mt-2 flex flex-wrap gap-2",
23461
23804
  children: capabilities.map((c) => /* @__PURE__ */ jsx(Badge, { children: c }, c))
23462
23805
  })] }) : null,
23806
+ sections.length > 0 && activePane ? /* @__PURE__ */ jsxs("section", {
23807
+ "aria-label": _t({
23808
+ id: "TYxt+3",
23809
+ message: "Plugin details"
23810
+ }),
23811
+ children: [sections.length > 1 ? /* @__PURE__ */ jsx(Tabs, {
23812
+ variant: "underline",
23813
+ value: activeSection,
23814
+ onValueChange: (v) => setSelectedSection(v),
23815
+ tabs: sections.map((s) => ({
23816
+ value: s.key,
23817
+ label: _t(SECTION_LABELS[s.key])
23818
+ }))
23819
+ }) : /* @__PURE__ */ jsx("h2", {
23820
+ className: "text-sm font-semibold text-kumo-subtle",
23821
+ children: _t(SECTION_LABELS[activePane.key])
23822
+ }), /* @__PURE__ */ jsx("div", {
23823
+ className: "prose prose-sm mt-4 max-w-none rounded-lg border bg-kumo-base p-6",
23824
+ children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderMarkdown(activePane.markdown) } })
23825
+ })]
23826
+ }) : null,
23463
23827
  showConsent && release ? /* @__PURE__ */ jsx(CapabilityConsentDialog, {
23464
23828
  mode: "install",
23465
23829
  pluginName: displayName ?? slug,
@@ -23475,6 +23839,28 @@ function RegistryPluginDetail({ pluginId, config }) {
23475
23839
  ]
23476
23840
  });
23477
23841
  }
23842
+ const SECTION_LABELS = {
23843
+ description: {
23844
+ id: "Nu4oKW",
23845
+ message: "Description"
23846
+ },
23847
+ installation: {
23848
+ id: "Xry+oU",
23849
+ message: "Installation"
23850
+ },
23851
+ faq: {
23852
+ id: "/lDBHm",
23853
+ message: "FAQ"
23854
+ },
23855
+ changelog: {
23856
+ id: "CWe7wB",
23857
+ message: "Changelog"
23858
+ },
23859
+ security: {
23860
+ id: "a3LDKx",
23861
+ message: "Security"
23862
+ }
23863
+ };
23478
23864
  function BackLink() {
23479
23865
  const { _: _t2 } = useLingui();
23480
23866
  return /* @__PURE__ */ jsxs(Link$1, {
@@ -23516,6 +23902,16 @@ const PRE_RELEASE_VERSION_RE = /^\d+\.\d+\.\d+-/;
23516
23902
  function isPreReleaseVersion(version) {
23517
23903
  return PRE_RELEASE_VERSION_RE.test(version);
23518
23904
  }
23905
+ /**
23906
+ * Human-readable name for a `requires` env key. The known EmDash environments
23907
+ * get their proper product names; anything else falls back to the key with the
23908
+ * `env:` prefix stripped (product names, not localised strings).
23909
+ */
23910
+ function envLabel(key) {
23911
+ if (key === "env:emdash") return "EmDash";
23912
+ if (key === "env:astro") return "Astro";
23913
+ return key.startsWith("env:") ? key.slice(4) : key;
23914
+ }
23519
23915
  const YANKED_LABEL_VALUE = "security:yanked";
23520
23916
  /**
23521
23917
  * Aggregators forward labels applied by their configured labellers. `security:yanked`
@@ -28847,15 +29243,6 @@ function SidebarNav({ manifest }) {
28847
29243
  border-top: 1px solid rgba(255,255,255,0.08);
28848
29244
  }
28849
29245
 
28850
- /* Keep all nav icons visible when sidebar collapses to icon mode */
28851
- .emdash-sidebar[data-state="collapsed"] [data-sidebar="group-content"] {
28852
- grid-template-rows: 1fr !important;
28853
- }
28854
- /* Mobile drawer: kumo's Sheet has no data-state attribute, so group-content
28855
- stays at grid-rows-[0fr] (hidden). Force it open in the mobile sidebar. */
28856
- .emdash-sidebar[data-mobile="true"] [data-sidebar="group-content"] {
28857
- grid-template-rows: 1fr !important;
28858
- }
28859
29246
  /* Collapsed separators — thin centered line */
28860
29247
  .emdash-sidebar[data-state="collapsed"] [data-sidebar="separator"] {
28861
29248
  margin: 0.375rem 0.625rem;
@@ -28939,52 +29326,36 @@ function SidebarNav({ manifest }) {
28939
29326
  isActive: isItemActive("/", currentPath)
28940
29327
  }) }) }),
28941
29328
  /* @__PURE__ */ jsx(KumoSidebar.Separator, {}),
28942
- visibleContent.length > 1 && /* @__PURE__ */ jsxs(KumoSidebar.Group, {
28943
- collapsible: true,
28944
- defaultOpen: true,
28945
- children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
28946
- className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
28947
- children: _t({
28948
- id: "4b3oEV",
28949
- message: "Content"
28950
- })
28951
- }), /* @__PURE__ */ jsx(KumoSidebar.GroupContent, { children: /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleContent.filter((i) => i.to !== "/")) }) })]
28952
- }),
29329
+ visibleContent.length > 1 && /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
29330
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
29331
+ children: _t({
29332
+ id: "4b3oEV",
29333
+ message: "Content"
29334
+ })
29335
+ }), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleContent.filter((i) => i.to !== "/")) })] }),
28953
29336
  /* @__PURE__ */ jsx(KumoSidebar.Separator, {}),
28954
- visibleManage.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, {
28955
- collapsible: true,
28956
- defaultOpen: true,
28957
- children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
28958
- className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
28959
- children: _t({
28960
- id: "wckWOP",
28961
- message: "Manage"
28962
- })
28963
- }), /* @__PURE__ */ jsx(KumoSidebar.GroupContent, { children: /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleManage) }) })]
28964
- }),
29337
+ visibleManage.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
29338
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
29339
+ children: _t({
29340
+ id: "wckWOP",
29341
+ message: "Manage"
29342
+ })
29343
+ }), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleManage) })] }),
28965
29344
  /* @__PURE__ */ jsx(KumoSidebar.Separator, {}),
28966
- visibleAdmin.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, {
28967
- collapsible: true,
28968
- defaultOpen: true,
28969
- children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
28970
- className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
28971
- children: _t({
28972
- id: "U3pytU",
28973
- message: "Admin"
28974
- })
28975
- }), /* @__PURE__ */ jsx(KumoSidebar.GroupContent, { children: /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleAdmin) }) })]
28976
- }),
28977
- visiblePlugins.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(KumoSidebar.Separator, {}), /* @__PURE__ */ jsxs(KumoSidebar.Group, {
28978
- collapsible: true,
28979
- defaultOpen: true,
28980
- children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
28981
- className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
28982
- children: _t({
28983
- id: "ohUJJM",
28984
- message: "Plugins"
28985
- })
28986
- }), /* @__PURE__ */ jsx(KumoSidebar.GroupContent, { children: /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visiblePlugins) }) })]
28987
- })] })
29345
+ visibleAdmin.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
29346
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
29347
+ children: _t({
29348
+ id: "U3pytU",
29349
+ message: "Admin"
29350
+ })
29351
+ }), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleAdmin) })] }),
29352
+ visiblePlugins.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(KumoSidebar.Separator, {}), /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
29353
+ className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
29354
+ children: _t({
29355
+ id: "ohUJJM",
29356
+ message: "Plugins"
29357
+ })
29358
+ }), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visiblePlugins) })] })] })
28988
29359
  ] }),
28989
29360
  /* @__PURE__ */ jsx(KumoSidebar.Footer, { children: /* @__PURE__ */ jsxs("p", {
28990
29361
  className: "emdash-nav-label px-3 py-2 text-[11px] text-white/30",
@@ -30222,22 +30593,23 @@ function TermFormDialog({ open, onClose, taxonomyName, taxonomyDef, term, allTer
30222
30593
  className: "flex items-start justify-between gap-4 mb-4",
30223
30594
  children: [/* @__PURE__ */ jsxs("div", {
30224
30595
  className: "flex flex-col space-y-1.5",
30225
- children: [/* @__PURE__ */ jsxs(Dialog.Title, {
30596
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
30226
30597
  className: "text-lg font-semibold leading-none tracking-tight",
30227
- children: [
30228
- term ? _t3({
30229
- id: "ePK91l",
30230
- message: "Edit"
30231
- }) : _t3({
30232
- id: "m16xKo",
30233
- message: "Add"
30234
- }),
30235
- " ",
30236
- taxonomyDef.labelSingular || _t3({
30598
+ children: term ? _t3({
30599
+ id: "N6j2JH",
30600
+ message: "Edit {0}",
30601
+ values: { 0: taxonomyDef.labelSingular || _t3({
30237
30602
  id: "k3xOSk",
30238
30603
  message: "Term"
30239
- })
30240
- ]
30604
+ }) }
30605
+ }) : _t3({
30606
+ id: "3nLdaX",
30607
+ message: "Add {0}",
30608
+ values: { 0: taxonomyDef.labelSingular || _t3({
30609
+ id: "k3xOSk",
30610
+ message: "Term"
30611
+ }) }
30612
+ })
30241
30613
  }), /* @__PURE__ */ jsx(Dialog.Description, {
30242
30614
  className: "text-sm text-kumo-subtle",
30243
30615
  children: term ? _t3({
@@ -30762,20 +31134,17 @@ function TaxonomyManager({ taxonomyName }) {
30762
31134
  message: "New Taxonomy"
30763
31135
  })
30764
31136
  }),
30765
- /* @__PURE__ */ jsxs(Button, {
31137
+ /* @__PURE__ */ jsx(Button, {
30766
31138
  icon: /* @__PURE__ */ jsx(Plus, {}),
30767
31139
  onClick: () => setFormOpen(true),
30768
- children: [
30769
- _t5({
30770
- id: "m16xKo",
30771
- message: "Add"
30772
- }),
30773
- " ",
30774
- taxonomyDef.labelSingular || _t5({
31140
+ children: _t5({
31141
+ id: "3nLdaX",
31142
+ message: "Add {0}",
31143
+ values: { 0: taxonomyDef.labelSingular || _t5({
30775
31144
  id: "k3xOSk",
30776
31145
  message: "Term"
30777
- })
30778
- ]
31146
+ }) }
31147
+ })
30779
31148
  })
30780
31149
  ]
30781
31150
  })]