@emdash-cms/admin 0.1.0 → 0.2.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.
package/dist/index.js CHANGED
@@ -1,13 +1,20 @@
1
1
  import { c as fetchManifest, i as fetchPlugins, l as parseApiResponse, n as enablePlugin, o as API_BASE, r as fetchPlugin, s as apiFetch, t as disablePlugin, u as throwResponseError } from "./plugins-XhZqfegd.js";
2
+ import { i as SUPPORTED_LOCALE_CODES, n as DEFAULT_LOCALE, o as resolveLocale, r as SUPPORTED_LOCALES, t as useLocale } from "./useLocale-CXsoFCFt.js";
3
+ import "./locales/index.js";
2
4
  import { Badge, Button, Checkbox, CommandPalette, Dialog, Input, InputArea, Label, LinkButton, Loader, Popover, Select, Sidebar as KumoSidebar, Switch, Tabs, Toast, Toasty, Tooltip, buttonVariants, useSidebar } from "@cloudflare/kumo";
5
+ import { i18n } from "@lingui/core";
6
+ import { I18nProvider, Trans, useLingui } from "@lingui/react";
3
7
  import { QueryClient, QueryClientProvider, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
8
  import { Link, Link as Link$1, Outlet, RouterProvider, createRootRouteWithContext, createRoute, createRouter, useLocation, useNavigate, useNavigate as useNavigate$1, useParams, useParams as useParams$1, useSearch } from "@tanstack/react-router";
5
9
  import * as React from "react";
6
10
  import { createContext, useCallback, useContext, useEffect, useState } from "react";
7
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
- import { ArrowCounterClockwise, ArrowLeft, ArrowRight, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, BracketsCurly, Calendar, CalendarBlank, CaretDown, CaretLeft, CaretRight, CaretUp, ChatCircle, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, Code, CodeBlock, Copy, Cube, Database, DeviceMobile, DotsSixVertical, DownloadSimple, Envelope, EnvelopeSimple, Eye, EyeSlash, File, FileArrowDown, FileText, FloppyDisk, FolderOpen, Gear, GithubLogo, Globe, GlobeSimple, GridFour, HardDrive, Hash, Image as Image$1, ImageSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, MagnifyingGlass, Minus, Monitor, Moon, Palette, PaperPlaneTilt, Paragraph, Pencil, PencilSimple, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, SlidersHorizontal, Sparkle, SquaresFour, Stack, Storefront, Sun, TextAlignCenter, TextAlignLeft, TextAlignRight, TextB, TextHOne, TextHThree, TextHTwo, TextItalic, TextStrikethrough, TextT, TextUnderline, ToggleLeft, Trash, Upload, User, UserCircle, UserPlus, Users, Warning, WarningCircle, WebhooksLogo, X, YoutubeLogo } from "@phosphor-icons/react";
12
+ import { ArrowCounterClockwise, ArrowLeft, ArrowRight, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, BracketsCurly, Calendar, CalendarBlank, CaretDown, CaretLeft, CaretRight, CaretUp, ChatCircle, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, Code, CodeBlock, Copy, Cube, Database, DeviceMobile, DotsSixVertical, DownloadSimple, Envelope, EnvelopeSimple, Eye, EyeSlash, File, FileArrowDown, FileText, FloppyDisk, FolderOpen, Gear, GithubLogo, Globe, GlobeSimple, GridFour, HardDrive, Hash, Image as Image$1, ImageSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, MagnifyingGlass, Minus, Monitor, Moon, Palette, PaperPlaneTilt, Paragraph, Pencil, PencilSimple, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Rows, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, SlidersHorizontal, Sparkle, SquaresFour, Stack, Storefront, Sun, TextAlignCenter, TextAlignLeft, TextAlignRight, TextB, TextHOne, TextHThree, TextHTwo, TextItalic, TextStrikethrough, TextT, TextUnderline, ToggleLeft, Trash, Upload, User, UserCircle, UserPlus, Users, Warning, WarningCircle, WebhooksLogo, X, YoutubeLogo } from "@phosphor-icons/react";
9
13
  import { clsx } from "clsx";
10
14
  import { twMerge } from "tailwind-merge";
15
+ import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter, rectIntersection, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
16
+ import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
17
+ import { CSS } from "@dnd-kit/utilities";
11
18
  import { autoUpdate, flip, offset, shift, useFloating } from "@floating-ui/react";
12
19
  import { Extension, InputRule, Node as Node$1, PasteRule, escapeForRegEx, mergeAttributes } from "@tiptap/core";
13
20
  import CharacterCount from "@tiptap/extension-character-count";
@@ -27,9 +34,6 @@ import { BlockRenderer } from "@emdash-cms/blocks";
27
34
  import DOMPurify from "dompurify";
28
35
  import { Marked, Renderer } from "marked";
29
36
  import { useHotkeys } from "react-hotkeys-hook";
30
- import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter, rectIntersection, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
31
- import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
32
- import { CSS } from "@dnd-kit/utilities";
33
37
 
34
38
  //#region src/components/ThemeProvider.tsx
35
39
  const ThemeContext = React.createContext(void 0);
@@ -54,15 +58,8 @@ function ThemeProvider({ children, defaultTheme = "system" }) {
54
58
  return theme;
55
59
  });
56
60
  React.useEffect(() => {
57
- const root = document.documentElement;
58
- root.setAttribute("data-theme", "classic");
59
- if (theme === "system") {
60
- root.removeAttribute("data-mode");
61
- setResolvedTheme(getSystemTheme());
62
- } else {
63
- root.setAttribute("data-mode", theme);
64
- setResolvedTheme(theme);
65
- }
61
+ if (theme === "system") setResolvedTheme(getSystemTheme());
62
+ else setResolvedTheme(theme);
66
63
  }, [theme]);
67
64
  React.useEffect(() => {
68
65
  if (theme !== "system") return;
@@ -73,6 +70,11 @@ function ThemeProvider({ children, defaultTheme = "system" }) {
73
70
  mediaQuery.addEventListener("change", handler);
74
71
  return () => mediaQuery.removeEventListener("change", handler);
75
72
  }, [theme]);
73
+ React.useEffect(() => {
74
+ const root = document.documentElement;
75
+ root.setAttribute("data-theme", "classic");
76
+ root.setAttribute("data-mode", resolvedTheme);
77
+ }, [resolvedTheme]);
76
78
  const setTheme = React.useCallback((newTheme) => {
77
79
  setThemeState(newTheme);
78
80
  localStorage.setItem(STORAGE_KEY, newTheme);
@@ -2416,6 +2418,58 @@ function useCurrentUser() {
2416
2418
  });
2417
2419
  }
2418
2420
 
2421
+ //#endregion
2422
+ //#region src/lib/url.ts
2423
+ /**
2424
+ * Shared URL validation and transformation utilities
2425
+ */
2426
+ const DEFAULT_REDIRECT = "/_emdash/admin";
2427
+ const LEADING_SLASHES = /^\/+/;
2428
+ /**
2429
+ * Sanitize a redirect URL to prevent open-redirect and javascript: XSS attacks.
2430
+ *
2431
+ * Only allows relative paths starting with `/`. Rejects protocol-relative
2432
+ * URLs (`//evil.com`), backslash tricks (`/\evil.com`), and non-path schemes
2433
+ * like `javascript:`.
2434
+ *
2435
+ * Returns the default admin URL when the input is unsafe.
2436
+ */
2437
+ function sanitizeRedirectUrl(raw) {
2438
+ if (raw.startsWith("/") && !raw.startsWith("//") && !raw.includes("\\")) return raw;
2439
+ return DEFAULT_REDIRECT;
2440
+ }
2441
+ /**
2442
+ * Build a public content URL from collection metadata and slug.
2443
+ *
2444
+ * Uses the collection's `urlPattern` when available (e.g. `/blog/{slug}`),
2445
+ * otherwise falls back to `/{collection}/{slug}`. Leading slashes are
2446
+ * stripped from the slug to prevent protocol-relative URLs.
2447
+ */
2448
+ function contentUrl(collection, slug, urlPattern) {
2449
+ const safe = slug.replace(LEADING_SLASHES, "");
2450
+ return urlPattern ? urlPattern.replace("{slug}", safe) : `/${collection}/${safe}`;
2451
+ }
2452
+ /** Matches http:// or https:// URLs */
2453
+ const SAFE_URL_RE = /^https?:\/\//i;
2454
+ /** Returns true if the URL uses a safe scheme (http/https) */
2455
+ function isSafeUrl$1(url) {
2456
+ return SAFE_URL_RE.test(url);
2457
+ }
2458
+ /**
2459
+ * Build an icon URL with a width query param, or return null for unsafe URLs.
2460
+ * Validates the URL scheme and appends `?w=<width>` for image resizing.
2461
+ */
2462
+ function safeIconUrl(url, width) {
2463
+ if (!SAFE_URL_RE.test(url)) return null;
2464
+ try {
2465
+ const u = new URL(url);
2466
+ u.searchParams.set("w", String(width));
2467
+ return u.href;
2468
+ } catch {
2469
+ return null;
2470
+ }
2471
+ }
2472
+
2419
2473
  //#endregion
2420
2474
  //#region src/components/BlockKitFieldWidget.tsx
2421
2475
  /**
@@ -2664,6 +2718,242 @@ var PluginFieldErrorBoundary = class extends React.Component {
2664
2718
  }
2665
2719
  };
2666
2720
 
2721
+ //#endregion
2722
+ //#region src/components/RepeaterField.tsx
2723
+ /**
2724
+ * RepeaterField — renders a list of repeating sub-field groups in the content editor.
2725
+ *
2726
+ * Each item is a collapsible card containing the defined sub-fields.
2727
+ * Items can be added, removed, and reordered via drag-and-drop.
2728
+ */
2729
+ function ensureKeys(items) {
2730
+ return items.map((item, i) => {
2731
+ const obj = typeof item === "object" && item !== null ? item : {};
2732
+ return {
2733
+ ...obj,
2734
+ _key: obj._key || `item-${i}-${Date.now()}`
2735
+ };
2736
+ });
2737
+ }
2738
+ function stripKeys(items) {
2739
+ return items.map(({ _key, ...rest }) => rest);
2740
+ }
2741
+ function RepeaterField({ label, id, value, onChange, subFields, minItems = 0, maxItems }) {
2742
+ const rawItems = Array.isArray(value) ? value : [];
2743
+ const [items, setItems] = React.useState(() => ensureKeys(rawItems));
2744
+ const [collapsedItems, setCollapsedItems] = React.useState(/* @__PURE__ */ new Set());
2745
+ React.useEffect(() => {
2746
+ setItems(ensureKeys(Array.isArray(value) ? value : []));
2747
+ }, [value]);
2748
+ const emitChange = (updated) => {
2749
+ setItems(updated);
2750
+ onChange(stripKeys(updated));
2751
+ };
2752
+ const handleAdd = () => {
2753
+ if (maxItems && items.length >= maxItems) return;
2754
+ const newItem = { _key: `item-${Date.now()}` };
2755
+ for (const sf of subFields) newItem[sf.slug] = sf.type === "boolean" ? false : sf.type === "number" || sf.type === "integer" ? null : "";
2756
+ emitChange([...items, newItem]);
2757
+ };
2758
+ const handleRemove = (key) => {
2759
+ if (items.length <= minItems) return;
2760
+ emitChange(items.filter((item) => item._key !== key));
2761
+ };
2762
+ const handleItemChange = (key, fieldSlug, fieldValue) => {
2763
+ emitChange(items.map((item) => item._key === key ? {
2764
+ ...item,
2765
+ [fieldSlug]: fieldValue
2766
+ } : item));
2767
+ };
2768
+ const handleDragEnd = (event) => {
2769
+ const { active, over } = event;
2770
+ if (!over || active.id === over.id) return;
2771
+ const oldIndex = items.findIndex((item) => item._key === active.id);
2772
+ const newIndex = items.findIndex((item) => item._key === over.id);
2773
+ if (oldIndex === -1 || newIndex === -1) return;
2774
+ emitChange(arrayMove(items, oldIndex, newIndex));
2775
+ };
2776
+ const toggleCollapse = (key) => {
2777
+ setCollapsedItems((prev) => {
2778
+ const next = new Set(prev);
2779
+ if (next.has(key)) next.delete(key);
2780
+ else next.add(key);
2781
+ return next;
2782
+ });
2783
+ };
2784
+ const canAdd = !maxItems || items.length < maxItems;
2785
+ const canRemove = items.length > minItems;
2786
+ return /* @__PURE__ */ jsxs("div", {
2787
+ className: "space-y-2",
2788
+ children: [/* @__PURE__ */ jsxs("div", {
2789
+ className: "flex items-center justify-between",
2790
+ children: [/* @__PURE__ */ jsxs("label", {
2791
+ htmlFor: id,
2792
+ className: "text-sm font-medium",
2793
+ children: [label, items.length > 0 && /* @__PURE__ */ jsxs("span", {
2794
+ className: "ml-2 text-kumo-subtle font-normal",
2795
+ children: [
2796
+ "(",
2797
+ items.length,
2798
+ " items)"
2799
+ ]
2800
+ })]
2801
+ }), canAdd && /* @__PURE__ */ jsx(Button, {
2802
+ variant: "outline",
2803
+ size: "sm",
2804
+ icon: /* @__PURE__ */ jsx(Plus, {}),
2805
+ onClick: handleAdd,
2806
+ children: "Add Item"
2807
+ })]
2808
+ }), items.length === 0 ? /* @__PURE__ */ jsxs("div", {
2809
+ className: "border-2 border-dashed rounded-lg p-6 text-center text-kumo-subtle",
2810
+ children: [/* @__PURE__ */ jsx("p", {
2811
+ className: "text-sm",
2812
+ children: "No items yet"
2813
+ }), canAdd && /* @__PURE__ */ jsx(Button, {
2814
+ variant: "outline",
2815
+ size: "sm",
2816
+ className: "mt-2",
2817
+ icon: /* @__PURE__ */ jsx(Plus, {}),
2818
+ onClick: handleAdd,
2819
+ children: "Add First Item"
2820
+ })]
2821
+ }) : /* @__PURE__ */ jsx(DndContext, {
2822
+ collisionDetection: closestCenter,
2823
+ onDragEnd: handleDragEnd,
2824
+ children: /* @__PURE__ */ jsx(SortableContext, {
2825
+ items: items.map((item) => item._key),
2826
+ strategy: verticalListSortingStrategy,
2827
+ children: /* @__PURE__ */ jsx("div", {
2828
+ className: "space-y-2",
2829
+ children: items.map((item, index) => /* @__PURE__ */ jsx(SortableRepeaterItem, {
2830
+ item,
2831
+ index,
2832
+ subFields,
2833
+ isCollapsed: collapsedItems.has(item._key),
2834
+ onToggleCollapse: () => toggleCollapse(item._key),
2835
+ onRemove: canRemove ? () => handleRemove(item._key) : void 0,
2836
+ onChange: (fieldSlug, fieldValue) => handleItemChange(item._key, fieldSlug, fieldValue)
2837
+ }, item._key))
2838
+ })
2839
+ })
2840
+ })]
2841
+ });
2842
+ }
2843
+ function SortableRepeaterItem({ item, index, subFields, isCollapsed, onToggleCollapse, onRemove, onChange }) {
2844
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item._key });
2845
+ const style = {
2846
+ transform: CSS.Transform.toString(transform),
2847
+ transition
2848
+ };
2849
+ const summaryField = subFields.find((sf) => sf.type === "string" || sf.type === "text");
2850
+ const summaryLabel = (summaryField ? item[summaryField.slug] || "" : "") || `Item ${index + 1}`;
2851
+ return /* @__PURE__ */ jsxs("div", {
2852
+ ref: setNodeRef,
2853
+ style,
2854
+ className: cn("border rounded-lg bg-kumo-base", isDragging && "opacity-50 ring-2 ring-kumo-brand"),
2855
+ children: [/* @__PURE__ */ jsxs("div", {
2856
+ className: "flex items-center gap-2 px-3 py-2 border-b cursor-pointer",
2857
+ onClick: onToggleCollapse,
2858
+ children: [
2859
+ /* @__PURE__ */ jsx(DotsSixVertical, {
2860
+ className: "h-4 w-4 text-kumo-subtle cursor-grab shrink-0",
2861
+ ...attributes,
2862
+ ...listeners,
2863
+ onClick: (e) => e.stopPropagation()
2864
+ }),
2865
+ isCollapsed ? /* @__PURE__ */ jsx(CaretRight, { className: "h-4 w-4 text-kumo-subtle shrink-0" }) : /* @__PURE__ */ jsx(CaretDown, { className: "h-4 w-4 text-kumo-subtle shrink-0" }),
2866
+ /* @__PURE__ */ jsx("span", {
2867
+ className: "text-sm font-medium flex-1 truncate",
2868
+ children: summaryLabel
2869
+ }),
2870
+ onRemove && /* @__PURE__ */ jsx(Button, {
2871
+ variant: "ghost",
2872
+ shape: "square",
2873
+ onClick: (e) => {
2874
+ e.stopPropagation();
2875
+ onRemove();
2876
+ },
2877
+ "aria-label": `Remove item ${index + 1}`,
2878
+ children: /* @__PURE__ */ jsx(Trash, { className: "h-3.5 w-3.5 text-kumo-danger" })
2879
+ })
2880
+ ]
2881
+ }), !isCollapsed && /* @__PURE__ */ jsx("div", {
2882
+ className: "p-3 space-y-3",
2883
+ children: subFields.map((sf) => /* @__PURE__ */ jsx(SubFieldInput, {
2884
+ subField: sf,
2885
+ value: item[sf.slug],
2886
+ onChange: (v) => onChange(sf.slug, v)
2887
+ }, sf.slug))
2888
+ })]
2889
+ });
2890
+ }
2891
+ function SubFieldInput({ subField, value, onChange }) {
2892
+ switch (subField.type) {
2893
+ case "string": return /* @__PURE__ */ jsx(Input, {
2894
+ label: subField.label,
2895
+ value: typeof value === "string" ? value : "",
2896
+ onChange: (e) => onChange(e.target.value),
2897
+ required: subField.required
2898
+ });
2899
+ case "text": return /* @__PURE__ */ jsx(InputArea, {
2900
+ label: subField.label,
2901
+ value: typeof value === "string" ? value : "",
2902
+ onChange: (e) => onChange(e.target.value),
2903
+ required: subField.required,
2904
+ rows: 3
2905
+ });
2906
+ case "number":
2907
+ case "integer": return /* @__PURE__ */ jsx(Input, {
2908
+ label: subField.label,
2909
+ type: "number",
2910
+ value: typeof value === "number" ? String(value) : "",
2911
+ onChange: (e) => onChange(e.target.value ? Number(e.target.value) : null),
2912
+ required: subField.required,
2913
+ step: subField.type === "integer" ? "1" : "any"
2914
+ });
2915
+ case "boolean": return /* @__PURE__ */ jsxs("label", {
2916
+ className: "flex items-center gap-2",
2917
+ children: [/* @__PURE__ */ jsx("input", {
2918
+ type: "checkbox",
2919
+ checked: Boolean(value),
2920
+ onChange: (e) => onChange(e.target.checked)
2921
+ }), /* @__PURE__ */ jsx("span", {
2922
+ className: "text-sm",
2923
+ children: subField.label
2924
+ })]
2925
+ });
2926
+ case "datetime": return /* @__PURE__ */ jsx(Input, {
2927
+ label: subField.label,
2928
+ type: "datetime-local",
2929
+ value: typeof value === "string" ? value : "",
2930
+ onChange: (e) => onChange(e.target.value),
2931
+ required: subField.required
2932
+ });
2933
+ case "select": return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
2934
+ className: "text-sm font-medium",
2935
+ children: subField.label
2936
+ }), /* @__PURE__ */ jsxs("select", {
2937
+ className: "w-full mt-1 rounded-md border px-3 py-2 text-sm",
2938
+ value: typeof value === "string" ? value : "",
2939
+ onChange: (e) => onChange(e.target.value),
2940
+ required: subField.required,
2941
+ children: [/* @__PURE__ */ jsx("option", {
2942
+ value: "",
2943
+ children: "Select..."
2944
+ }), subField.options?.map((opt) => /* @__PURE__ */ jsx("option", {
2945
+ value: opt,
2946
+ children: opt
2947
+ }, opt))]
2948
+ })] });
2949
+ default: return /* @__PURE__ */ jsx(Input, {
2950
+ label: subField.label,
2951
+ value: typeof value === "string" ? value : "",
2952
+ onChange: (e) => onChange(e.target.value)
2953
+ });
2954
+ }
2955
+ }
2956
+
2667
2957
  //#endregion
2668
2958
  //#region src/lib/hooks.ts
2669
2959
  /**
@@ -2998,7 +3288,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
2998
3288
  open,
2999
3289
  onOpenChange: handleClose,
3000
3290
  children: /* @__PURE__ */ jsxs(Dialog, {
3001
- className: "p-6 max-w-4xl max-h-[80vh] flex flex-col",
3291
+ className: "p-6 max-w-4xl max-h-[80vh] flex flex-col overflow-hidden",
3002
3292
  size: "xl",
3003
3293
  children: [
3004
3294
  /* @__PURE__ */ jsxs("div", {
@@ -3126,7 +3416,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
3126
3416
  className: "mb-3"
3127
3417
  }),
3128
3418
  /* @__PURE__ */ jsx("div", {
3129
- className: "flex-1 overflow-y-auto min-h-[300px]",
3419
+ className: "flex-1 overflow-y-auto min-h-0",
3130
3420
  children: isLoading ? /* @__PURE__ */ jsx("div", {
3131
3421
  className: "flex items-center justify-center h-full",
3132
3422
  children: /* @__PURE__ */ jsx(Loader, {})
@@ -7185,6 +7475,74 @@ function SaveButton({ isDirty, isSaving, className, disabled, ...props }) {
7185
7475
  });
7186
7476
  }
7187
7477
 
7478
+ //#endregion
7479
+ //#region src/components/SeoImageField.tsx
7480
+ /**
7481
+ * SEO OG Image field for the content editor.
7482
+ *
7483
+ * Renders an image picker (reusing MediaPickerModal) that stores the
7484
+ * selected image URL in `seo.image`. Designed to sit next to the
7485
+ * Featured Image field in a two-column grid.
7486
+ */
7487
+ function SeoImageField({ seo, onChange }) {
7488
+ const [pickerOpen, setPickerOpen] = React.useState(false);
7489
+ const imageUrl = seo?.image || null;
7490
+ const handleSelect = (item) => {
7491
+ onChange({ image: !item.provider || item.provider === "local" ? `/_emdash/api/media/file/${item.storageKey || item.id}` : item.url });
7492
+ };
7493
+ const handleRemove = () => {
7494
+ onChange({ image: null });
7495
+ };
7496
+ return /* @__PURE__ */ jsxs("div", { children: [
7497
+ /* @__PURE__ */ jsx(Label, { children: "OG Image" }),
7498
+ imageUrl ? /* @__PURE__ */ jsxs("div", {
7499
+ className: "mt-2 relative group",
7500
+ children: [/* @__PURE__ */ jsx("img", {
7501
+ src: imageUrl,
7502
+ alt: "",
7503
+ className: "max-h-48 rounded-lg border object-cover"
7504
+ }), /* @__PURE__ */ jsxs("div", {
7505
+ className: "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1",
7506
+ children: [/* @__PURE__ */ jsx(Button, {
7507
+ type: "button",
7508
+ size: "sm",
7509
+ variant: "secondary",
7510
+ onClick: () => setPickerOpen(true),
7511
+ children: "Change"
7512
+ }), /* @__PURE__ */ jsx(Button, {
7513
+ type: "button",
7514
+ shape: "square",
7515
+ variant: "destructive",
7516
+ className: "h-8 w-8",
7517
+ onClick: handleRemove,
7518
+ "aria-label": "Remove image",
7519
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
7520
+ })]
7521
+ })]
7522
+ }) : /* @__PURE__ */ jsx(Button, {
7523
+ type: "button",
7524
+ variant: "outline",
7525
+ className: "mt-2 w-full h-32 border-dashed",
7526
+ onClick: () => setPickerOpen(true),
7527
+ children: /* @__PURE__ */ jsxs("div", {
7528
+ className: "flex flex-col items-center gap-2 text-kumo-subtle",
7529
+ children: [/* @__PURE__ */ jsx(Image$1, { className: "h-8 w-8" }), /* @__PURE__ */ jsx("span", { children: "Select OG image" })]
7530
+ })
7531
+ }),
7532
+ /* @__PURE__ */ jsx("p", {
7533
+ className: "text-xs text-kumo-subtle mt-1",
7534
+ children: "Image shown when this page is shared on social media"
7535
+ }),
7536
+ /* @__PURE__ */ jsx(MediaPickerModal, {
7537
+ open: pickerOpen,
7538
+ onOpenChange: setPickerOpen,
7539
+ onSelect: handleSelect,
7540
+ mimeTypeFilter: "image/",
7541
+ title: "Select OG Image"
7542
+ })
7543
+ ] });
7544
+ }
7545
+
7188
7546
  //#endregion
7189
7547
  //#region src/components/SeoPanel.tsx
7190
7548
  /**
@@ -7509,7 +7867,7 @@ function formatScheduledDate(dateStr) {
7509
7867
  /**
7510
7868
  * Content editor with dynamic field rendering
7511
7869
  */
7512
- function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSaving, onSave, onAutosave, isAutosaving, lastAutosaveAt, onPublish, onUnpublish, onDiscardDraft, onSchedule, onUnschedule, isScheduling, supportsDrafts = false, supportsRevisions = false, currentUser, users, onAuthorChange, availableBylines, selectedBylines, onBylinesChange, onQuickCreateByline, onQuickEditByline, onDelete, isDeleting, i18n, translations, onTranslate, pluginBlocks, hasSeo = false, onSeoChange, manifest }) {
7870
+ 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 }) {
7513
7871
  const [formData, setFormData] = React.useState(item?.data || {});
7514
7872
  const [slug, setSlug] = React.useState(item?.slug || "");
7515
7873
  const [slugTouched, setSlugTouched] = React.useState(!!item?.slug);
@@ -7624,15 +7982,16 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
7624
7982
  });
7625
7983
  };
7626
7984
  const [isLoadingPreview, setIsLoadingPreview] = React.useState(false);
7985
+ const urlPattern = manifest?.collections[collection]?.urlPattern;
7627
7986
  const handlePreview = async () => {
7628
7987
  if (!item?.id) return;
7629
7988
  setIsLoadingPreview(true);
7630
7989
  try {
7631
7990
  const result = await getPreviewUrl(collection, item.id);
7632
7991
  if (result?.url) window.open(result.url, "_blank", "noopener,noreferrer");
7633
- else window.open(`/${collection}/${slug || item.id}`, "_blank", "noopener,noreferrer");
7992
+ else window.open(contentUrl(collection, slug || item.id, urlPattern), "_blank", "noopener,noreferrer");
7634
7993
  } catch {
7635
- window.open(`/${collection}/${slug || item?.id}`, "_blank", "noopener,noreferrer");
7994
+ window.open(contentUrl(collection, slug || item?.id || "", urlPattern), "_blank", "noopener,noreferrer");
7636
7995
  } finally {
7637
7996
  setIsLoadingPreview(false);
7638
7997
  }
@@ -7743,7 +8102,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
7743
8102
  "aria-hidden": "true"
7744
8103
  })
7745
8104
  }),
7746
- !isNew && /* @__PURE__ */ jsx(Button, {
8105
+ !isNew && supportsPreview && /* @__PURE__ */ jsx(Button, {
7747
8106
  variant: "outline",
7748
8107
  type: "button",
7749
8108
  onClick: handlePreview,
@@ -7756,9 +8115,8 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
7756
8115
  isDirty,
7757
8116
  isSaving: isSaving || false
7758
8117
  }),
7759
- !isNew && /* @__PURE__ */ jsxs(Fragment, { children: [supportsDrafts && hasPendingChanges && onDiscardDraft && /* @__PURE__ */ jsxs(Dialog.Root, {
7760
- disablePointerDismissal: true,
7761
- children: [/* @__PURE__ */ jsx(Dialog.Trigger, { render: (p) => /* @__PURE__ */ jsx(Button, {
8118
+ !isNew && /* @__PURE__ */ jsxs(Fragment, { children: [
8119
+ supportsDrafts && hasPendingChanges && onDiscardDraft && /* @__PURE__ */ jsxs(Dialog.Root, { children: [/* @__PURE__ */ jsx(Dialog.Trigger, { render: (p) => /* @__PURE__ */ jsx(Button, {
7762
8120
  ...p,
7763
8121
  type: "button",
7764
8122
  variant: "outline",
@@ -7791,23 +8149,34 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
7791
8149
  }) })]
7792
8150
  })
7793
8151
  ]
7794
- })]
7795
- }), isLive ? /* @__PURE__ */ jsx(Fragment, { children: hasPendingChanges ? /* @__PURE__ */ jsx(Button, {
7796
- type: "button",
7797
- variant: "primary",
7798
- onClick: onPublish,
7799
- children: "Publish changes"
7800
- }) : /* @__PURE__ */ jsx(Button, {
7801
- type: "button",
7802
- variant: "outline",
7803
- onClick: onUnpublish,
7804
- children: "Unpublish"
7805
- }) }) : /* @__PURE__ */ jsx(Button, {
7806
- type: "button",
7807
- variant: "secondary",
7808
- onClick: onPublish,
7809
- children: "Publish"
7810
- })] })
8152
+ })] }),
8153
+ isLive ? /* @__PURE__ */ jsx(Fragment, { children: hasPendingChanges ? /* @__PURE__ */ jsx(Button, {
8154
+ type: "button",
8155
+ variant: "primary",
8156
+ onClick: onPublish,
8157
+ children: "Publish changes"
8158
+ }) : /* @__PURE__ */ jsx(Button, {
8159
+ type: "button",
8160
+ variant: "outline",
8161
+ onClick: onUnpublish,
8162
+ children: "Unpublish"
8163
+ }) }) : /* @__PURE__ */ jsx(Button, {
8164
+ type: "button",
8165
+ variant: "secondary",
8166
+ onClick: onPublish,
8167
+ children: "Publish"
8168
+ }),
8169
+ isLive && item?.slug && /* @__PURE__ */ jsxs("a", {
8170
+ href: contentUrl(collection, item.slug, urlPattern),
8171
+ target: "_blank",
8172
+ rel: "noopener noreferrer",
8173
+ className: buttonVariants({ variant: "outline" }),
8174
+ children: [/* @__PURE__ */ jsx(ArrowSquareOut, {
8175
+ className: "mr-2 h-4 w-4",
8176
+ "aria-hidden": "true"
8177
+ }), "Live View"]
8178
+ })
8179
+ ] })
7811
8180
  ]
7812
8181
  })]
7813
8182
  }), /* @__PURE__ */ jsxs("div", {
@@ -7818,18 +8187,28 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
7818
8187
  className: cn("rounded-lg border bg-kumo-base p-6", isDistractionFree && "border-0 bg-transparent p-0"),
7819
8188
  children: /* @__PURE__ */ jsx("div", {
7820
8189
  className: "space-y-4",
7821
- children: Object.entries(fields).map(([name, field]) => /* @__PURE__ */ jsx(FieldRenderer, {
7822
- name,
7823
- field,
7824
- value: formData[name],
7825
- onChange: handleFieldChange,
7826
- onEditorReady: field.kind === "portableText" ? setPortableTextEditor : void 0,
7827
- minimal: isDistractionFree,
7828
- pluginBlocks,
7829
- onBlockSidebarOpen: field.kind === "portableText" ? handleBlockSidebarOpen : void 0,
7830
- onBlockSidebarClose: field.kind === "portableText" ? handleBlockSidebarClose : void 0,
7831
- manifest
7832
- }, name))
8190
+ children: Object.entries(fields).map(([name, field]) => {
8191
+ const fieldEl = /* @__PURE__ */ jsx(FieldRenderer, {
8192
+ name,
8193
+ field,
8194
+ value: formData[name],
8195
+ onChange: handleFieldChange,
8196
+ onEditorReady: field.kind === "portableText" ? setPortableTextEditor : void 0,
8197
+ minimal: isDistractionFree,
8198
+ pluginBlocks,
8199
+ onBlockSidebarOpen: field.kind === "portableText" ? handleBlockSidebarOpen : void 0,
8200
+ onBlockSidebarClose: field.kind === "portableText" ? handleBlockSidebarClose : void 0,
8201
+ manifest
8202
+ }, name);
8203
+ if (name === "featured_image" && field.kind === "image" && hasSeo && !isNew && onSeoChange) return /* @__PURE__ */ jsxs("div", {
8204
+ className: "grid grid-cols-1 gap-6 md:grid-cols-2",
8205
+ children: [/* @__PURE__ */ jsx("div", { children: fieldEl }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(SeoImageField, {
8206
+ seo: item?.seo,
8207
+ onChange: onSeoChange
8208
+ }) })]
8209
+ }, `${name}-with-seo`);
8210
+ return fieldEl;
8211
+ })
7833
8212
  })
7834
8213
  })
7835
8214
  }), /* @__PURE__ */ jsx("div", {
@@ -7868,6 +8247,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
7868
8247
  children: supportsDrafts ? /* @__PURE__ */ jsxs(Fragment, { children: [
7869
8248
  isLive && /* @__PURE__ */ jsx(Badge, {
7870
8249
  variant: "primary",
8250
+ className: "text-white",
7871
8251
  children: "Published"
7872
8252
  }),
7873
8253
  hasPendingChanges && /* @__PURE__ */ jsx(Badge, {
@@ -8160,27 +8540,31 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
8160
8540
  required: field.required
8161
8541
  });
8162
8542
  case "boolean": return /* @__PURE__ */ jsx(Switch, {
8543
+ id,
8163
8544
  label,
8164
8545
  checked: typeof value === "boolean" ? value : false,
8165
8546
  onCheckedChange: handleChange
8166
8547
  });
8167
8548
  case "portableText": {
8168
8549
  const labelId = `${id}-label`;
8169
- return /* @__PURE__ */ jsxs("div", { children: [!minimal && /* @__PURE__ */ jsx("span", {
8170
- id: labelId,
8171
- className: cn("text-sm font-medium leading-none text-kumo-default", labelClass),
8172
- children: label
8173
- }), /* @__PURE__ */ jsx(PortableTextEditor, {
8174
- value: Array.isArray(value) ? value : [],
8175
- onChange: handleChange,
8176
- placeholder: `Enter ${label.toLowerCase()}...`,
8177
- "aria-labelledby": labelId,
8178
- pluginBlocks,
8179
- onEditorReady,
8180
- minimal,
8181
- onBlockSidebarOpen,
8182
- onBlockSidebarClose
8183
- })] });
8550
+ return /* @__PURE__ */ jsxs("div", {
8551
+ id,
8552
+ children: [!minimal && /* @__PURE__ */ jsx("span", {
8553
+ id: labelId,
8554
+ className: cn("text-sm font-medium leading-none text-kumo-default", labelClass),
8555
+ children: label
8556
+ }), /* @__PURE__ */ jsx(PortableTextEditor, {
8557
+ value: Array.isArray(value) ? value : [],
8558
+ onChange: handleChange,
8559
+ placeholder: `Enter ${label.toLowerCase()}...`,
8560
+ "aria-labelledby": labelId,
8561
+ pluginBlocks,
8562
+ onEditorReady,
8563
+ minimal,
8564
+ onBlockSidebarOpen,
8565
+ onBlockSidebarClose
8566
+ })]
8567
+ });
8184
8568
  }
8185
8569
  case "richText": return /* @__PURE__ */ jsx(InputArea, {
8186
8570
  label,
@@ -8194,6 +8578,7 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
8194
8578
  const selectItems = {};
8195
8579
  for (const opt of field.options ?? []) selectItems[opt.value] = opt.label;
8196
8580
  return /* @__PURE__ */ jsx(Select, {
8581
+ id,
8197
8582
  label,
8198
8583
  value: typeof value === "string" ? value : "",
8199
8584
  onValueChange: (v) => handleChange(v ?? ""),
@@ -8204,6 +8589,25 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
8204
8589
  }, opt.value))
8205
8590
  });
8206
8591
  }
8592
+ case "multiSelect": {
8593
+ const selected = Array.isArray(value) ? value : [];
8594
+ return /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx(Label, {
8595
+ className: labelClass,
8596
+ children: label
8597
+ }), /* @__PURE__ */ jsx("div", {
8598
+ className: "mt-2 flex flex-wrap gap-x-4 gap-y-2",
8599
+ children: field.options?.map((opt) => {
8600
+ const isChecked = selected.includes(opt.value);
8601
+ return /* @__PURE__ */ jsx(Checkbox, {
8602
+ label: opt.label,
8603
+ checked: isChecked,
8604
+ onCheckedChange: (checked) => {
8605
+ handleChange(checked ? [...selected, opt.value] : selected.filter((v) => v !== opt.value));
8606
+ }
8607
+ }, opt.value);
8608
+ })
8609
+ })] });
8610
+ }
8207
8611
  case "datetime": return /* @__PURE__ */ jsx(Input, {
8208
8612
  label,
8209
8613
  id,
@@ -8213,11 +8617,27 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
8213
8617
  required: field.required
8214
8618
  });
8215
8619
  case "image": return /* @__PURE__ */ jsx(ImageFieldRenderer, {
8620
+ id,
8216
8621
  label,
8622
+ description: name === "featured_image" ? "Used as the main visual for this post on listing pages and at the top of the post" : void 0,
8217
8623
  value: value != null && typeof value === "object" ? value : void 0,
8218
8624
  onChange: handleChange,
8219
8625
  required: field.required
8220
8626
  });
8627
+ case "repeater": {
8628
+ const validation = field.validation;
8629
+ const subFields = validation?.subFields ?? [];
8630
+ return /* @__PURE__ */ jsx(RepeaterField, {
8631
+ label,
8632
+ id,
8633
+ value,
8634
+ onChange: handleChange,
8635
+ required: field.required,
8636
+ subFields,
8637
+ minItems: typeof validation?.minItems === "number" ? validation.minItems : void 0,
8638
+ maxItems: typeof validation?.maxItems === "number" ? validation.maxItems : void 0
8639
+ });
8640
+ }
8221
8641
  default: return /* @__PURE__ */ jsx(Input, {
8222
8642
  label,
8223
8643
  id,
@@ -8227,7 +8647,7 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
8227
8647
  });
8228
8648
  }
8229
8649
  }
8230
- function ImageFieldRenderer({ label, value, onChange, required }) {
8650
+ function ImageFieldRenderer({ id, label, description, value, onChange, required }) {
8231
8651
  const [pickerOpen, setPickerOpen] = React.useState(false);
8232
8652
  const displayUrl = typeof value === "string" ? value : value?.previewUrl || value?.src || (value && (!value.provider || value.provider === "local") ? `/_emdash/api/media/file/${typeof value.meta?.storageKey === "string" ? value.meta.storageKey : value.id}` : void 0);
8233
8653
  const handleSelect = (item) => {
@@ -8248,54 +8668,61 @@ function ImageFieldRenderer({ label, value, onChange, required }) {
8248
8668
  const handleRemove = () => {
8249
8669
  onChange(void 0);
8250
8670
  };
8251
- return /* @__PURE__ */ jsxs("div", { children: [
8252
- /* @__PURE__ */ jsx(Label, { children: label }),
8253
- displayUrl ? /* @__PURE__ */ jsxs("div", {
8254
- className: "mt-2 relative group",
8255
- children: [/* @__PURE__ */ jsx("img", {
8256
- src: displayUrl,
8257
- alt: "",
8258
- className: "max-h-48 rounded-lg border object-cover"
8259
- }), /* @__PURE__ */ jsxs("div", {
8260
- className: "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1",
8261
- children: [/* @__PURE__ */ jsx(Button, {
8262
- type: "button",
8263
- size: "sm",
8264
- variant: "secondary",
8265
- onClick: () => setPickerOpen(true),
8266
- children: "Change"
8267
- }), /* @__PURE__ */ jsx(Button, {
8268
- type: "button",
8269
- shape: "square",
8270
- variant: "destructive",
8271
- className: "h-8 w-8",
8272
- onClick: handleRemove,
8273
- "aria-label": "Remove image",
8274
- children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
8671
+ return /* @__PURE__ */ jsxs("div", {
8672
+ id,
8673
+ children: [
8674
+ /* @__PURE__ */ jsx(Label, { children: label }),
8675
+ displayUrl ? /* @__PURE__ */ jsxs("div", {
8676
+ className: "mt-2 relative group",
8677
+ children: [/* @__PURE__ */ jsx("img", {
8678
+ src: displayUrl,
8679
+ alt: "",
8680
+ className: "max-h-48 rounded-lg border object-cover"
8681
+ }), /* @__PURE__ */ jsxs("div", {
8682
+ className: "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1",
8683
+ children: [/* @__PURE__ */ jsx(Button, {
8684
+ type: "button",
8685
+ size: "sm",
8686
+ variant: "secondary",
8687
+ onClick: () => setPickerOpen(true),
8688
+ children: "Change"
8689
+ }), /* @__PURE__ */ jsx(Button, {
8690
+ type: "button",
8691
+ shape: "square",
8692
+ variant: "destructive",
8693
+ className: "h-8 w-8",
8694
+ onClick: handleRemove,
8695
+ "aria-label": "Remove image",
8696
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
8697
+ })]
8275
8698
  })]
8276
- })]
8277
- }) : /* @__PURE__ */ jsx(Button, {
8278
- type: "button",
8279
- variant: "outline",
8280
- className: "mt-2 w-full h-32 border-dashed",
8281
- onClick: () => setPickerOpen(true),
8282
- children: /* @__PURE__ */ jsxs("div", {
8283
- className: "flex flex-col items-center gap-2 text-kumo-subtle",
8284
- children: [/* @__PURE__ */ jsx(Image$1, { className: "h-8 w-8" }), /* @__PURE__ */ jsx("span", { children: "Select image" })]
8699
+ }) : /* @__PURE__ */ jsx(Button, {
8700
+ type: "button",
8701
+ variant: "outline",
8702
+ className: "mt-2 w-full h-32 border-dashed",
8703
+ onClick: () => setPickerOpen(true),
8704
+ children: /* @__PURE__ */ jsxs("div", {
8705
+ className: "flex flex-col items-center gap-2 text-kumo-subtle",
8706
+ children: [/* @__PURE__ */ jsx(Image$1, { className: "h-8 w-8" }), /* @__PURE__ */ jsx("span", { children: "Select image" })]
8707
+ })
8708
+ }),
8709
+ /* @__PURE__ */ jsx(MediaPickerModal, {
8710
+ open: pickerOpen,
8711
+ onOpenChange: setPickerOpen,
8712
+ onSelect: handleSelect,
8713
+ mimeTypeFilter: "image/",
8714
+ title: `Select ${label}`
8715
+ }),
8716
+ description && /* @__PURE__ */ jsx("p", {
8717
+ className: "text-xs text-kumo-subtle mt-1",
8718
+ children: description
8719
+ }),
8720
+ required && !displayUrl && /* @__PURE__ */ jsx("p", {
8721
+ className: "text-sm text-kumo-danger mt-1",
8722
+ children: "This field is required"
8285
8723
  })
8286
- }),
8287
- /* @__PURE__ */ jsx(MediaPickerModal, {
8288
- open: pickerOpen,
8289
- onOpenChange: setPickerOpen,
8290
- onSelect: handleSelect,
8291
- mimeTypeFilter: "image/",
8292
- title: `Select ${label}`
8293
- }),
8294
- required && !displayUrl && /* @__PURE__ */ jsx("p", {
8295
- className: "text-sm text-kumo-danger mt-1",
8296
- children: "This field is required"
8297
- })
8298
- ] });
8724
+ ]
8725
+ });
8299
8726
  }
8300
8727
  function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuickEdit }) {
8301
8728
  const [selectedBylineId, setSelectedBylineId] = React.useState("");
@@ -8475,7 +8902,10 @@ function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuic
8475
8902
  children: [/* @__PURE__ */ jsx(Dialog.Close, { render: (p) => /* @__PURE__ */ jsx(Button, {
8476
8903
  ...p,
8477
8904
  variant: "secondary",
8478
- onClick: resetQuickCreate,
8905
+ onClick: (e) => {
8906
+ resetQuickCreate();
8907
+ p.onClick?.(e);
8908
+ },
8479
8909
  children: "Cancel"
8480
8910
  }) }), /* @__PURE__ */ jsx(Button, {
8481
8911
  type: "button",
@@ -8619,6 +9049,7 @@ function AuthorSelector({ authorId, users, onChange }) {
8619
9049
  * Only renders when i18n is configured (manifest.i18n is present).
8620
9050
  */
8621
9051
  function LocaleSwitcher({ locales, defaultLocale, value, onChange, showAll = false, className, size = "md" }) {
9052
+ const { _: _t } = useLingui();
8622
9053
  return /* @__PURE__ */ jsxs("div", {
8623
9054
  className: cn("flex items-center gap-1.5", className),
8624
9055
  children: [/* @__PURE__ */ jsx(GlobeSimple, {
@@ -8627,14 +9058,23 @@ function LocaleSwitcher({ locales, defaultLocale, value, onChange, showAll = fal
8627
9058
  }), /* @__PURE__ */ jsxs("select", {
8628
9059
  value,
8629
9060
  onChange: (e) => onChange(e.target.value),
8630
- "aria-label": "Locale",
9061
+ "aria-label": _t({
9062
+ id: "8NbHF7",
9063
+ message: "Locale"
9064
+ }),
8631
9065
  className: cn("rounded-md border bg-transparent font-medium transition-colors", "focus:ring-kumo-ring focus:outline-none focus:ring-2 focus:ring-offset-1", "hover:bg-kumo-tint/50 cursor-pointer", size === "sm" ? "px-1.5 py-0.5 text-xs" : "px-2 py-1 text-sm"),
8632
9066
  children: [showAll && /* @__PURE__ */ jsx("option", {
8633
9067
  value: "",
8634
- children: "All locales"
9068
+ children: _t({
9069
+ id: "JjOi48",
9070
+ message: "All locales"
9071
+ })
8635
9072
  }), locales.map((locale) => /* @__PURE__ */ jsxs("option", {
8636
9073
  value: locale,
8637
- children: [locale.toUpperCase(), locale === defaultLocale ? " (default)" : ""]
9074
+ children: [locale.toUpperCase(), locale === defaultLocale ? _t({
9075
+ id: "FozKV6",
9076
+ message: " (default)"
9077
+ }) : ""]
8638
9078
  }, locale))]
8639
9079
  })]
8640
9080
  });
@@ -8651,7 +9091,7 @@ function getItemTitle$1(item) {
8651
9091
  /**
8652
9092
  * Content list view with table display and trash tab
8653
9093
  */
8654
- function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n, activeLocale, onLocaleChange }) {
9094
+ function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n, activeLocale, onLocaleChange, urlPattern }) {
8655
9095
  const [activeTab, setActiveTab] = React.useState("all");
8656
9096
  const [searchQuery, setSearchQuery] = React.useState("");
8657
9097
  const [page, setPage] = React.useState(0);
@@ -8666,6 +9106,15 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
8666
9106
  }, [items, searchQuery]);
8667
9107
  const totalPages = Math.max(1, Math.ceil(filteredItems.length / PAGE_SIZE));
8668
9108
  const paginatedItems = filteredItems.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
9109
+ React.useEffect(() => {
9110
+ if (page >= totalPages - 1 && hasMore && onLoadMore && !searchQuery) onLoadMore();
9111
+ }, [
9112
+ page,
9113
+ totalPages,
9114
+ hasMore,
9115
+ onLoadMore,
9116
+ searchQuery
9117
+ ]);
8669
9118
  return /* @__PURE__ */ jsxs("div", {
8670
9119
  className: "space-y-4",
8671
9120
  children: [
@@ -8686,6 +9135,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
8686
9135
  }), /* @__PURE__ */ jsxs(Link$1, {
8687
9136
  to: "/content/$collection/new",
8688
9137
  params: { collection },
9138
+ search: { locale: activeLocale },
8689
9139
  className: buttonVariants(),
8690
9140
  children: [/* @__PURE__ */ jsx(Plus, {
8691
9141
  className: "mr-2 h-4 w-4",
@@ -8776,6 +9226,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
8776
9226
  /* @__PURE__ */ jsx(Link$1, {
8777
9227
  to: "/content/$collection/new",
8778
9228
  params: { collection },
9229
+ search: { locale: activeLocale },
8779
9230
  className: "text-kumo-brand underline",
8780
9231
  children: "Create your first one"
8781
9232
  })
@@ -8793,7 +9244,8 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
8793
9244
  collection,
8794
9245
  onDelete,
8795
9246
  onDuplicate,
8796
- showLocale: !!i18n
9247
+ showLocale: !!i18n,
9248
+ urlPattern
8797
9249
  }, item.id)) })]
8798
9250
  })
8799
9251
  }),
@@ -8803,6 +9255,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
8803
9255
  className: "text-sm text-kumo-subtle",
8804
9256
  children: [
8805
9257
  filteredItems.length,
9258
+ hasMore && !searchQuery ? "+" : "",
8806
9259
  " ",
8807
9260
  filteredItems.length === 1 ? "item" : "items",
8808
9261
  searchQuery && ` matching "${searchQuery}"`
@@ -8897,7 +9350,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
8897
9350
  ]
8898
9351
  });
8899
9352
  }
8900
- function ContentListItem({ item, collection, onDelete, onDuplicate, showLocale }) {
9353
+ function ContentListItem({ item, collection, onDelete, onDuplicate, showLocale, urlPattern }) {
8901
9354
  const title = getItemTitle$1(item);
8902
9355
  const date = new Date(item.updatedAt || item.createdAt);
8903
9356
  return /* @__PURE__ */ jsxs("tr", {
@@ -8938,6 +9391,20 @@ function ContentListItem({ item, collection, onDelete, onDuplicate, showLocale }
8938
9391
  children: /* @__PURE__ */ jsxs("div", {
8939
9392
  className: "flex items-center justify-end space-x-1",
8940
9393
  children: [
9394
+ item.status === "published" && item.slug && /* @__PURE__ */ jsx("a", {
9395
+ href: contentUrl(collection, item.slug, urlPattern),
9396
+ target: "_blank",
9397
+ rel: "noopener noreferrer",
9398
+ "aria-label": `View published ${title}`,
9399
+ className: buttonVariants({
9400
+ variant: "ghost",
9401
+ shape: "square"
9402
+ }),
9403
+ children: /* @__PURE__ */ jsx(ArrowSquareOut, {
9404
+ className: "h-4 w-4",
9405
+ "aria-hidden": "true"
9406
+ })
9407
+ }),
8941
9408
  /* @__PURE__ */ jsx(Link$1, {
8942
9409
  to: "/content/$collection/$id",
8943
9410
  params: {
@@ -9192,6 +9659,12 @@ const FIELD_TYPES = [
9192
9659
  label: "Slug",
9193
9660
  description: "URL-friendly identifier",
9194
9661
  icon: Link$2
9662
+ },
9663
+ {
9664
+ type: "repeater",
9665
+ label: "Repeater",
9666
+ description: "Repeating group of fields",
9667
+ icon: Rows
9195
9668
  }
9196
9669
  ];
9197
9670
  function getInitialFormState(field) {
@@ -9208,7 +9681,10 @@ function getInitialFormState(field) {
9208
9681
  min: field.validation?.min?.toString() ?? "",
9209
9682
  max: field.validation?.max?.toString() ?? "",
9210
9683
  pattern: field.validation?.pattern ?? "",
9211
- options: field.validation?.options?.join("\n") ?? ""
9684
+ options: field.validation?.options?.join("\n") ?? "",
9685
+ subFields: field.validation?.subFields ? field.validation.subFields : [],
9686
+ minItems: field.validation?.minItems?.toString() ?? "",
9687
+ maxItems: field.validation?.maxItems?.toString() ?? ""
9212
9688
  };
9213
9689
  return {
9214
9690
  step: "type",
@@ -9223,7 +9699,10 @@ function getInitialFormState(field) {
9223
9699
  min: "",
9224
9700
  max: "",
9225
9701
  pattern: "",
9226
- options: ""
9702
+ options: "",
9703
+ subFields: [],
9704
+ minItems: "",
9705
+ maxItems: ""
9227
9706
  };
9228
9707
  }
9229
9708
  /**
@@ -9267,6 +9746,16 @@ function FieldEditor({ open, onOpenChange, field, onSave, isSaving }) {
9267
9746
  const optionList = options.split("\n").map((o) => o.trim()).filter(Boolean);
9268
9747
  if (optionList.length > 0) validation.options = optionList;
9269
9748
  }
9749
+ if (selectedType === "repeater") {
9750
+ if (formState.subFields.length > 0) validation.subFields = formState.subFields.map((sf) => ({
9751
+ slug: sf.slug,
9752
+ type: sf.type,
9753
+ label: sf.label,
9754
+ required: sf.required || void 0
9755
+ }));
9756
+ if (formState.minItems) validation.minItems = parseInt(formState.minItems, 10);
9757
+ if (formState.maxItems) validation.maxItems = parseInt(formState.maxItems, 10);
9758
+ }
9270
9759
  onSave({
9271
9760
  slug,
9272
9761
  label,
@@ -9466,11 +9955,156 @@ function FieldEditor({ open, onOpenChange, field, onSave, isSaving }) {
9466
9955
  onChange: (e) => setField("options", e.target.value),
9467
9956
  placeholder: "Option 1\nOption 2\nOption 3",
9468
9957
  rows: 5
9958
+ }),
9959
+ selectedType === "repeater" && /* @__PURE__ */ jsxs("div", {
9960
+ className: "space-y-4",
9961
+ children: [
9962
+ /* @__PURE__ */ jsxs("div", {
9963
+ className: "flex items-center justify-between",
9964
+ children: [/* @__PURE__ */ jsx("h4", {
9965
+ className: "font-medium text-sm",
9966
+ children: "Sub-Fields"
9967
+ }), /* @__PURE__ */ jsx(Button, {
9968
+ variant: "outline",
9969
+ size: "sm",
9970
+ icon: /* @__PURE__ */ jsx(Plus, {}),
9971
+ onClick: () => setFormState((prev) => ({
9972
+ ...prev,
9973
+ subFields: [...prev.subFields, {
9974
+ slug: "",
9975
+ type: "string",
9976
+ label: "",
9977
+ required: false
9978
+ }]
9979
+ })),
9980
+ children: "Add Sub-Field"
9981
+ })]
9982
+ }),
9983
+ formState.subFields.length === 0 && /* @__PURE__ */ jsx("p", {
9984
+ className: "text-sm text-kumo-subtle text-center py-4",
9985
+ children: "Add at least one sub-field to define the repeater structure."
9986
+ }),
9987
+ formState.subFields.map((sf, i) => /* @__PURE__ */ jsxs("div", {
9988
+ className: "flex gap-2 items-start border rounded-lg p-3",
9989
+ children: [/* @__PURE__ */ jsxs("div", {
9990
+ className: "flex-1 space-y-2",
9991
+ children: [/* @__PURE__ */ jsxs("div", {
9992
+ className: "grid grid-cols-2 gap-2",
9993
+ children: [/* @__PURE__ */ jsx(Input, {
9994
+ label: "Label",
9995
+ value: sf.label,
9996
+ onChange: (e) => {
9997
+ const updated = [...formState.subFields];
9998
+ updated[i] = {
9999
+ ...sf,
10000
+ label: e.target.value,
10001
+ slug: e.target.value.toLowerCase().replace(SLUG_INVALID_CHARS_REGEX, "_").replace(SLUG_LEADING_TRAILING_REGEX, "")
10002
+ };
10003
+ setFormState((prev) => ({
10004
+ ...prev,
10005
+ subFields: updated
10006
+ }));
10007
+ },
10008
+ placeholder: "Field label"
10009
+ }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
10010
+ className: "text-sm font-medium",
10011
+ children: "Type"
10012
+ }), /* @__PURE__ */ jsxs("select", {
10013
+ className: "w-full mt-1 rounded-md border px-3 py-2 text-sm",
10014
+ value: sf.type,
10015
+ onChange: (e) => {
10016
+ const updated = [...formState.subFields];
10017
+ updated[i] = {
10018
+ ...sf,
10019
+ type: e.target.value
10020
+ };
10021
+ setFormState((prev) => ({
10022
+ ...prev,
10023
+ subFields: updated
10024
+ }));
10025
+ },
10026
+ children: [
10027
+ /* @__PURE__ */ jsx("option", {
10028
+ value: "string",
10029
+ children: "Short Text"
10030
+ }),
10031
+ /* @__PURE__ */ jsx("option", {
10032
+ value: "text",
10033
+ children: "Long Text"
10034
+ }),
10035
+ /* @__PURE__ */ jsx("option", {
10036
+ value: "number",
10037
+ children: "Number"
10038
+ }),
10039
+ /* @__PURE__ */ jsx("option", {
10040
+ value: "integer",
10041
+ children: "Integer"
10042
+ }),
10043
+ /* @__PURE__ */ jsx("option", {
10044
+ value: "boolean",
10045
+ children: "Boolean"
10046
+ }),
10047
+ /* @__PURE__ */ jsx("option", {
10048
+ value: "datetime",
10049
+ children: "Date & Time"
10050
+ }),
10051
+ /* @__PURE__ */ jsx("option", {
10052
+ value: "select",
10053
+ children: "Select"
10054
+ })
10055
+ ]
10056
+ })] })]
10057
+ }), /* @__PURE__ */ jsxs("label", {
10058
+ className: "flex items-center gap-2 text-sm",
10059
+ children: [/* @__PURE__ */ jsx("input", {
10060
+ type: "checkbox",
10061
+ checked: sf.required,
10062
+ onChange: (e) => {
10063
+ const updated = [...formState.subFields];
10064
+ updated[i] = {
10065
+ ...sf,
10066
+ required: e.target.checked
10067
+ };
10068
+ setFormState((prev) => ({
10069
+ ...prev,
10070
+ subFields: updated
10071
+ }));
10072
+ }
10073
+ }), "Required"]
10074
+ })]
10075
+ }), /* @__PURE__ */ jsx(Button, {
10076
+ variant: "ghost",
10077
+ shape: "square",
10078
+ onClick: () => setFormState((prev) => ({
10079
+ ...prev,
10080
+ subFields: prev.subFields.filter((_, j) => j !== i)
10081
+ })),
10082
+ "aria-label": "Remove sub-field",
10083
+ children: /* @__PURE__ */ jsx(Trash, { className: "h-4 w-4 text-kumo-danger" })
10084
+ })]
10085
+ }, i)),
10086
+ /* @__PURE__ */ jsxs("div", {
10087
+ className: "grid grid-cols-2 gap-4",
10088
+ children: [/* @__PURE__ */ jsx(Input, {
10089
+ label: "Min Items",
10090
+ type: "number",
10091
+ value: formState.minItems,
10092
+ onChange: (e) => setField("minItems", e.target.value),
10093
+ placeholder: "0"
10094
+ }), /* @__PURE__ */ jsx(Input, {
10095
+ label: "Max Items",
10096
+ type: "number",
10097
+ value: formState.maxItems,
10098
+ onChange: (e) => setField("maxItems", e.target.value),
10099
+ placeholder: "No limit"
10100
+ })]
10101
+ })
10102
+ ]
9469
10103
  })
9470
10104
  ]
9471
10105
  }),
9472
10106
  step === "config" && /* @__PURE__ */ jsxs("div", {
9473
- className: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
10107
+ className: "flex flex-col-reverse gap-2 py-2 sm:flex-row sm:justify-end sm:space-x-2",
9474
10108
  children: [/* @__PURE__ */ jsx(Button, {
9475
10109
  variant: "outline",
9476
10110
  onClick: () => onOpenChange(false),
@@ -9478,7 +10112,7 @@ function FieldEditor({ open, onOpenChange, field, onSave, isSaving }) {
9478
10112
  children: "Cancel"
9479
10113
  }), /* @__PURE__ */ jsx(Button, {
9480
10114
  onClick: handleSave,
9481
- disabled: !slug || !label || isSaving,
10115
+ disabled: !slug || !label || isSaving || selectedType === "repeater" && formState.subFields.length === 0,
9482
10116
  children: isSaving ? "Saving..." : field ? "Update Field" : "Add Field"
9483
10117
  })]
9484
10118
  })
@@ -9558,7 +10192,7 @@ const SYSTEM_FIELDS = [
9558
10192
  /**
9559
10193
  * Content Type editor for creating/editing collections
9560
10194
  */
9561
- function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, onUpdateField, onDeleteField, onReorderFields: _onReorderFields }) {
10195
+ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, onUpdateField, onDeleteField, onReorderFields }) {
9562
10196
  useNavigate$1();
9563
10197
  const [slug, setSlug] = React.useState(collection?.slug ?? "");
9564
10198
  const [label, setLabel] = React.useState(collection?.label ?? "");
@@ -9651,6 +10285,16 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
9651
10285
  };
9652
10286
  const isFromCode = collection?.source === "code";
9653
10287
  const fields = collection?.fields ?? [];
10288
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
10289
+ const handleDragEnd = (event) => {
10290
+ const { active, over } = event;
10291
+ if (!over || active.id === over.id) return;
10292
+ const oldIndex = fields.findIndex((f) => f.id === active.id);
10293
+ const newIndex = fields.findIndex((f) => f.id === over.id);
10294
+ if (oldIndex === -1 || newIndex === -1) return;
10295
+ const reordered = arrayMove(fields, oldIndex, newIndex);
10296
+ onReorderFields?.(reordered.map((f) => f.slug));
10297
+ };
9654
10298
  return /* @__PURE__ */ jsxs("div", {
9655
10299
  className: "space-y-6",
9656
10300
  children: [
@@ -9933,14 +10577,23 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
9933
10577
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
9934
10578
  className: "px-4 py-2 text-xs font-medium text-kumo-subtle uppercase tracking-wider border-b",
9935
10579
  children: "Custom Fields"
9936
- }), /* @__PURE__ */ jsx("div", {
9937
- className: "divide-y",
9938
- children: fields.map((field) => /* @__PURE__ */ jsx(FieldRow, {
9939
- field,
9940
- isFromCode,
9941
- onEdit: () => handleEditField(field),
9942
- onDelete: () => setDeleteFieldTarget(field)
9943
- }, field.id))
10580
+ }), /* @__PURE__ */ jsx(DndContext, {
10581
+ sensors,
10582
+ collisionDetection: closestCenter,
10583
+ onDragEnd: handleDragEnd,
10584
+ children: /* @__PURE__ */ jsx(SortableContext, {
10585
+ items: fields.map((f) => f.id),
10586
+ strategy: verticalListSortingStrategy,
10587
+ children: /* @__PURE__ */ jsx("div", {
10588
+ className: "divide-y",
10589
+ children: fields.map((field) => /* @__PURE__ */ jsx(FieldRow, {
10590
+ field,
10591
+ isFromCode,
10592
+ onEdit: () => handleEditField(field),
10593
+ onDelete: () => setDeleteFieldTarget(field)
10594
+ }, field.id))
10595
+ })
10596
+ })
9944
10597
  })] })
9945
10598
  ]
9946
10599
  })
@@ -9973,10 +10626,25 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
9973
10626
  });
9974
10627
  }
9975
10628
  function FieldRow({ field, isFromCode, onEdit, onDelete }) {
10629
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
10630
+ id: field.id,
10631
+ disabled: isFromCode
10632
+ });
9976
10633
  return /* @__PURE__ */ jsxs("div", {
9977
- className: "flex items-center px-4 py-3 hover:bg-kumo-tint/25",
10634
+ ref: setNodeRef,
10635
+ style: {
10636
+ transform: CSS.Transform.toString(transform),
10637
+ transition
10638
+ },
10639
+ className: cn("flex items-center px-4 py-3 hover:bg-kumo-tint/25", isDragging && "opacity-50"),
9978
10640
  children: [
9979
- !isFromCode && /* @__PURE__ */ jsx(DotsSixVertical, { className: "h-5 w-5 mr-3 text-kumo-subtle cursor-grab" }),
10641
+ !isFromCode && /* @__PURE__ */ jsx("button", {
10642
+ ...attributes,
10643
+ ...listeners,
10644
+ className: "cursor-grab active:cursor-grabbing mr-3",
10645
+ "aria-label": `Drag to reorder ${field.label}`,
10646
+ children: /* @__PURE__ */ jsx(DotsSixVertical, { className: "h-5 w-5 text-kumo-subtle" })
10647
+ }),
9980
10648
  /* @__PURE__ */ jsxs("div", {
9981
10649
  className: "flex-1 min-w-0",
9982
10650
  children: [/* @__PURE__ */ jsxs("div", {
@@ -10875,43 +11543,24 @@ function CheckIcon({ className }) {
10875
11543
  }
10876
11544
 
10877
11545
  //#endregion
10878
- //#region src/lib/url.ts
10879
- /**
10880
- * Shared URL validation and transformation utilities
10881
- */
10882
- const DEFAULT_REDIRECT = "/_emdash/admin";
11546
+ //#region src/lib/webauthn-environment.ts
10883
11547
  /**
10884
- * Sanitize a redirect URL to prevent open-redirect and javascript: XSS attacks.
10885
- *
10886
- * Only allows relative paths starting with `/`. Rejects protocol-relative
10887
- * URLs (`//evil.com`), backslash tricks (`/\evil.com`), and non-path schemes
10888
- * like `javascript:`.
11548
+ * WebAuthn is only available in a browser "secure context": HTTPS, or special-cased
11549
+ * loopback hosts such as `http://localhost` / `http://127.0.0.1`.
10889
11550
  *
10890
- * Returns the default admin URL when the input is unsafe.
11551
+ * An origin like `http://emdash.local:8081` resolves to 127.0.0.1 but is still
11552
+ * **not** a secure context, so `PublicKeyCredential` is hidden — the same symptom
11553
+ * as an unsupported browser.
10891
11554
  */
10892
- function sanitizeRedirectUrl(raw) {
10893
- if (raw.startsWith("/") && !raw.startsWith("//") && !raw.includes("\\")) return raw;
10894
- return DEFAULT_REDIRECT;
11555
+ function isWebAuthnSecureContext() {
11556
+ return typeof window !== "undefined" && window.isSecureContext;
10895
11557
  }
10896
- /** Matches http:// or https:// URLs */
10897
- const SAFE_URL_RE = /^https?:\/\//i;
10898
- /** Returns true if the URL uses a safe scheme (http/https) */
10899
- function isSafeUrl$1(url) {
10900
- return SAFE_URL_RE.test(url);
11558
+ function isPublicKeyCredentialConstructorAvailable() {
11559
+ return typeof window !== "undefined" && window.PublicKeyCredential !== void 0 && typeof window.PublicKeyCredential === "function";
10901
11560
  }
10902
- /**
10903
- * Build an icon URL with a width query param, or return null for unsafe URLs.
10904
- * Validates the URL scheme and appends `?w=<width>` for image resizing.
10905
- */
10906
- function safeIconUrl(url, width) {
10907
- if (!SAFE_URL_RE.test(url)) return null;
10908
- try {
10909
- const u = new URL(url);
10910
- u.searchParams.set("w", String(width));
10911
- return u.href;
10912
- } catch {
10913
- return null;
10914
- }
11561
+ /** True when the page can use `navigator.credentials` for passkeys. */
11562
+ function isPasskeyEnvironmentUsable() {
11563
+ return isWebAuthnSecureContext() && isPublicKeyCredentialConstructorAvailable();
10915
11564
  }
10916
11565
 
10917
11566
  //#endregion
@@ -10933,16 +11582,10 @@ const BASE64URL_UNDERSCORE_REGEX$1 = /_/g;
10933
11582
  const BASE64_PLUS_REGEX$1 = /\+/g;
10934
11583
  const BASE64_SLASH_REGEX$1 = /\//g;
10935
11584
  /**
10936
- * Check if WebAuthn is supported in the current browser
10937
- */
10938
- function isWebAuthnSupported$1() {
10939
- return typeof window !== "undefined" && window.PublicKeyCredential !== void 0 && typeof window.PublicKeyCredential === "function";
10940
- }
10941
- /**
10942
11585
  * Check if conditional mediation (autofill) is supported
10943
11586
  */
10944
11587
  async function isConditionalMediationSupported() {
10945
- if (!isWebAuthnSupported$1()) return false;
11588
+ if (!isPasskeyEnvironmentUsable()) return false;
10946
11589
  try {
10947
11590
  return await PublicKeyCredential.isConditionalMediationAvailable?.() ?? false;
10948
11591
  } catch {
@@ -10976,7 +11619,8 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
10976
11619
  const [state, setState] = React.useState({ status: "idle" });
10977
11620
  const [email, setEmail] = React.useState("");
10978
11621
  const [supportsConditional, setSupportsConditional] = React.useState(false);
10979
- const isSupported = React.useMemo(() => isWebAuthnSupported$1(), []);
11622
+ const isSupported = React.useMemo(() => isPasskeyEnvironmentUsable(), []);
11623
+ const insecureContext = React.useMemo(() => typeof window !== "undefined" && !isWebAuthnSecureContext(), []);
10980
11624
  React.useEffect(() => {
10981
11625
  isConditionalMediationSupported().then(setSupportsConditional);
10982
11626
  }, []);
@@ -10984,7 +11628,7 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
10984
11628
  if (!isSupported) {
10985
11629
  setState({
10986
11630
  status: "error",
10987
- message: "WebAuthn is not supported in this browser"
11631
+ message: insecureContext ? "Passkeys require HTTPS or http://localhost (with your port); this hostname is not a secure browser context." : "WebAuthn is not supported in this browser"
10988
11632
  });
10989
11633
  return;
10990
11634
  }
@@ -11017,7 +11661,15 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
11017
11661
  ...useConditional && supportsConditional ? { mediation: "conditional" } : {}
11018
11662
  };
11019
11663
  const rawCredential = await navigator.credentials.get(credentialOptions);
11020
- if (!rawCredential) throw new Error("No credential returned from authenticator");
11664
+ if (!rawCredential) {
11665
+ const message = "No credential returned from authenticator";
11666
+ setState({
11667
+ status: "error",
11668
+ message
11669
+ });
11670
+ onError?.(new Error(message));
11671
+ return;
11672
+ }
11021
11673
  setState({
11022
11674
  status: "loading",
11023
11675
  message: "Verifying..."
@@ -11073,6 +11725,7 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
11073
11725
  }
11074
11726
  }, [
11075
11727
  isSupported,
11728
+ insecureContext,
11076
11729
  optionsEndpoint,
11077
11730
  verifyEndpoint,
11078
11731
  email,
@@ -11084,10 +11737,34 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
11084
11737
  className: "rounded-lg border border-kumo-danger/50 bg-kumo-danger/10 p-4",
11085
11738
  children: [/* @__PURE__ */ jsx("h3", {
11086
11739
  className: "font-medium text-kumo-danger",
11087
- children: "Passkeys Not Supported"
11740
+ children: "Passkeys Not Available Here"
11088
11741
  }), /* @__PURE__ */ jsx("p", {
11089
11742
  className: "mt-1 text-sm text-kumo-subtle",
11090
- children: "Your browser doesn't support passkeys. Please use a modern browser like Chrome, Safari, Firefox, or Edge."
11743
+ children: insecureContext ? /* @__PURE__ */ jsxs(Fragment, { children: [
11744
+ "Passkeys require a ",
11745
+ /* @__PURE__ */ jsx("strong", {
11746
+ className: "text-kumo-default",
11747
+ children: "secure context"
11748
+ }),
11749
+ ": use",
11750
+ " ",
11751
+ /* @__PURE__ */ jsx("strong", {
11752
+ className: "text-kumo-default",
11753
+ children: "HTTPS"
11754
+ }),
11755
+ ", or open the admin at",
11756
+ " ",
11757
+ /* @__PURE__ */ jsx("strong", {
11758
+ className: "text-kumo-default",
11759
+ children: "http://localhost"
11760
+ }),
11761
+ " (with your dev port). Plain ",
11762
+ /* @__PURE__ */ jsx("code", {
11763
+ className: "text-xs",
11764
+ children: "http://"
11765
+ }),
11766
+ " on a custom hostname is not treated as secure, even on loopback."
11767
+ ] }) : /* @__PURE__ */ jsx(Fragment, { children: "Your browser doesn't support passkeys. Please use a modern browser like Chrome, Safari, Firefox, or Edge." })
11091
11768
  })]
11092
11769
  });
11093
11770
  return /* @__PURE__ */ jsxs("div", {
@@ -11109,18 +11786,270 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
11109
11786
  className: "rounded-lg bg-kumo-danger/10 p-4 text-sm text-kumo-danger",
11110
11787
  children: state.message
11111
11788
  }),
11112
- /* @__PURE__ */ jsx(Button, {
11113
- type: "button",
11114
- onClick: () => handleLogin(false),
11115
- loading: state.status === "loading",
11116
- className: "w-full justify-center",
11117
- variant: "primary",
11118
- children: state.status === "loading" ? /* @__PURE__ */ jsx(Fragment, { children: state.message }) : buttonText
11789
+ /* @__PURE__ */ jsx(Button, {
11790
+ type: "button",
11791
+ onClick: () => handleLogin(false),
11792
+ loading: state.status === "loading",
11793
+ className: "w-full justify-center",
11794
+ variant: "primary",
11795
+ children: state.status === "loading" ? /* @__PURE__ */ jsx(Fragment, { children: state.message }) : buttonText
11796
+ }),
11797
+ /* @__PURE__ */ jsx("p", {
11798
+ className: "text-xs text-kumo-subtle text-center",
11799
+ children: "Use your device's biometric authentication, security key, or PIN to sign in."
11800
+ })
11801
+ ]
11802
+ });
11803
+ }
11804
+
11805
+ //#endregion
11806
+ //#region src/components/Logo.tsx
11807
+ /**
11808
+ * EmDash icon mark — the rounded-rect em dash symbol.
11809
+ * Used in the sidebar brand and as favicon.
11810
+ */
11811
+ function LogoIcon(props) {
11812
+ return /* @__PURE__ */ jsxs("svg", {
11813
+ viewBox: "0 0 75 75",
11814
+ fill: "none",
11815
+ xmlns: "http://www.w3.org/2000/svg",
11816
+ ...props,
11817
+ children: [
11818
+ /* @__PURE__ */ jsx("rect", {
11819
+ x: "3",
11820
+ y: "3",
11821
+ width: "69",
11822
+ height: "69",
11823
+ rx: "10.518",
11824
+ stroke: "url(#emdash-icon-border)",
11825
+ strokeWidth: "6"
11826
+ }),
11827
+ /* @__PURE__ */ jsx("rect", {
11828
+ x: "18",
11829
+ y: "34",
11830
+ width: "39.3661",
11831
+ height: "6.56101",
11832
+ fill: "url(#emdash-icon-dash)"
11833
+ }),
11834
+ /* @__PURE__ */ jsxs("defs", { children: [/* @__PURE__ */ jsxs("linearGradient", {
11835
+ id: "emdash-icon-border",
11836
+ x1: "-42.9996",
11837
+ y1: "124",
11838
+ x2: "92.4233",
11839
+ y2: "-41.7456",
11840
+ gradientUnits: "userSpaceOnUse",
11841
+ children: [
11842
+ /* @__PURE__ */ jsx("stop", { stopColor: "#0F006B" }),
11843
+ /* @__PURE__ */ jsx("stop", {
11844
+ offset: "0.0833",
11845
+ stopColor: "#281A81"
11846
+ }),
11847
+ /* @__PURE__ */ jsx("stop", {
11848
+ offset: "0.1667",
11849
+ stopColor: "#5D0C83"
11850
+ }),
11851
+ /* @__PURE__ */ jsx("stop", {
11852
+ offset: "0.25",
11853
+ stopColor: "#911475"
11854
+ }),
11855
+ /* @__PURE__ */ jsx("stop", {
11856
+ offset: "0.3333",
11857
+ stopColor: "#CE2F55"
11858
+ }),
11859
+ /* @__PURE__ */ jsx("stop", {
11860
+ offset: "0.4167",
11861
+ stopColor: "#FF6633"
11862
+ }),
11863
+ /* @__PURE__ */ jsx("stop", {
11864
+ offset: "0.5",
11865
+ stopColor: "#F6821F"
11866
+ }),
11867
+ /* @__PURE__ */ jsx("stop", {
11868
+ offset: "0.5833",
11869
+ stopColor: "#FBAD41"
11870
+ }),
11871
+ /* @__PURE__ */ jsx("stop", {
11872
+ offset: "0.6667",
11873
+ stopColor: "#FFCD89"
11874
+ }),
11875
+ /* @__PURE__ */ jsx("stop", {
11876
+ offset: "0.75",
11877
+ stopColor: "#FFE9CB"
11878
+ }),
11879
+ /* @__PURE__ */ jsx("stop", {
11880
+ offset: "0.8333",
11881
+ stopColor: "#FFF7EC"
11882
+ }),
11883
+ /* @__PURE__ */ jsx("stop", {
11884
+ offset: "0.9167",
11885
+ stopColor: "#FFF8EE"
11886
+ }),
11887
+ /* @__PURE__ */ jsx("stop", {
11888
+ offset: "1",
11889
+ stopColor: "white"
11890
+ })
11891
+ ]
11892
+ }), /* @__PURE__ */ jsxs("linearGradient", {
11893
+ id: "emdash-icon-dash",
11894
+ x1: "91.4992",
11895
+ y1: "27.4982",
11896
+ x2: "28.1217",
11897
+ y2: "54.1775",
11898
+ gradientUnits: "userSpaceOnUse",
11899
+ children: [
11900
+ /* @__PURE__ */ jsx("stop", { stopColor: "white" }),
11901
+ /* @__PURE__ */ jsx("stop", {
11902
+ offset: "0.1293",
11903
+ stopColor: "#FFF8EE"
11904
+ }),
11905
+ /* @__PURE__ */ jsx("stop", {
11906
+ offset: "0.6171",
11907
+ stopColor: "#FBAD41"
11908
+ }),
11909
+ /* @__PURE__ */ jsx("stop", {
11910
+ offset: "0.848",
11911
+ stopColor: "#F6821F"
11912
+ }),
11913
+ /* @__PURE__ */ jsx("stop", {
11914
+ offset: "1",
11915
+ stopColor: "#FF6633"
11916
+ })
11917
+ ]
11918
+ })] })
11919
+ ]
11920
+ });
11921
+ }
11922
+ /**
11923
+ * Full logo lockup — icon + "EmDash" wordmark.
11924
+ * Renders both dark-text and light-text variants, switching via CSS `light-dark()`.
11925
+ */
11926
+ function LogoLockup({ className, ...props }) {
11927
+ return /* @__PURE__ */ jsxs("svg", {
11928
+ viewBox: "0 0 471 118",
11929
+ fill: "none",
11930
+ xmlns: "http://www.w3.org/2000/svg",
11931
+ className,
11932
+ role: "img",
11933
+ "aria-label": "EmDash",
11934
+ ...props,
11935
+ children: [
11936
+ /* @__PURE__ */ jsx("path", {
11937
+ d: "M0.410156 96.5125V21.2097C0.410156 9.48841 9.91245 -0.013916 21.6338 -0.013916V9.40601L21.3291 9.40991C14.9509 9.57133 9.83008 14.7927 9.83008 21.2097V96.5125C9.83008 102.93 14.9509 108.151 21.3291 108.312L21.6338 108.316H96.9365L97.2412 108.312C103.518 108.153 108.577 103.094 108.736 96.8171L108.74 96.5125V21.2097C108.74 14.6909 103.455 9.40601 96.9365 9.40601V-0.013916C108.658 -0.013916 118.16 9.48838 118.16 21.2097V96.5125C118.16 108.234 108.658 117.736 96.9365 117.736H21.6338C9.91248 117.736 0.410156 108.234 0.410156 96.5125ZM96.9365 -0.013916V9.40601H21.6338V-0.013916H96.9365Z",
11938
+ fill: "url(#emdash-lockup-icon)"
11939
+ }),
11940
+ /* @__PURE__ */ jsx("path", {
11941
+ d: "M28.6699 53.366H90.4746V63.6668H28.6699V53.366Z",
11942
+ fill: "url(#emdash-lockup-dash)"
11943
+ }),
11944
+ /* @__PURE__ */ jsx("path", {
11945
+ d: "M154.762 90V27.4834H194.447V35.8449H164.467V54.0844H192.844V62.2293H164.467V81.6385H194.447V90H154.762Z",
11946
+ fill: "currentColor"
11947
+ }),
11948
+ /* @__PURE__ */ jsx("path", {
11949
+ d: "M204.172 90V44.4231H213.53V51.4849H213.747C215.697 46.7193 220.332 43.5566 226.311 43.5566C232.593 43.5566 237.185 46.8059 239.005 52.5247H239.222C241.561 46.9792 246.933 43.5566 253.432 43.5566C262.443 43.5566 268.335 49.5353 268.335 58.6767V90H258.934V60.9296C258.934 54.9942 255.771 51.5716 250.226 51.5716C244.68 51.5716 240.825 55.7307 240.825 61.4928V90H231.64V60.2364C231.64 54.9508 228.304 51.5716 223.018 51.5716C217.473 51.5716 213.53 55.9473 213.53 61.8394V90H204.172Z",
11950
+ fill: "currentColor"
11951
+ }),
11952
+ /* @__PURE__ */ jsx("path", {
11953
+ d: "M279.404 90V27.4834H301.456C319.998 27.4834 331.046 38.8776 331.046 58.5467V58.6334C331.046 78.3892 320.085 90 301.456 90H279.404ZM289.108 81.5951H300.546C313.803 81.5951 321.125 73.4935 321.125 58.72V58.6334C321.125 43.9465 313.716 35.8449 300.546 35.8449H289.108V81.5951Z",
11954
+ fill: "currentColor"
11955
+ }),
11956
+ /* @__PURE__ */ jsx("path", {
11957
+ d: "M353.379 90.8232C344.281 90.8232 338.172 85.2344 338.172 77.0461V76.9595C338.172 69.0312 344.324 64.1789 355.112 63.529L367.502 62.7925V59.3699C367.502 54.3443 364.253 51.3116 358.448 51.3116C353.032 51.3116 349.696 53.8677 348.916 57.507L348.83 57.8969H339.992L340.035 57.4203C340.685 49.5787 347.487 43.5566 358.708 43.5566C369.842 43.5566 376.904 49.4487 376.904 58.5901V90H367.502V82.8082H367.329C364.686 87.7038 359.401 90.8232 353.379 90.8232ZM347.617 76.8295C347.617 80.8153 350.909 83.3281 355.935 83.3281C362.52 83.3281 367.502 78.8657 367.502 72.9303V69.3778L356.368 70.0709C350.736 70.4175 347.617 72.887 347.617 76.7428V76.8295Z",
11958
+ fill: "currentColor"
11959
+ }),
11960
+ /* @__PURE__ */ jsx("path", {
11961
+ d: "M403.959 90.9098C392.564 90.9098 385.893 85.2777 384.939 76.9595L384.896 76.5695H394.167L394.254 77.0028C395.121 81.2052 398.24 83.6747 404.002 83.6747C409.634 83.6747 413.013 81.3352 413.013 77.6527V77.6093C413.013 74.6633 411.367 72.9737 406.471 71.8039L399.02 70.1143C390.355 68.1214 386.066 63.9623 386.066 57.3337V57.2903C386.066 49.1454 393.171 43.5566 403.655 43.5566C414.443 43.5566 420.942 49.5787 421.418 57.3337L421.462 57.8536H412.667L412.624 57.5503C412.06 53.5645 408.941 50.7917 403.655 50.7917C398.63 50.7917 395.467 53.1746 395.467 56.8138V56.8571C395.467 59.6732 397.33 61.5794 402.226 62.7492L409.634 64.4388C418.949 66.605 422.501 70.2876 422.501 76.8295V76.8728C422.501 85.191 414.703 90.9098 403.959 90.9098Z",
11962
+ fill: "currentColor"
11119
11963
  }),
11120
- /* @__PURE__ */ jsx("p", {
11121
- className: "text-xs text-kumo-subtle text-center",
11122
- children: "Use your device's biometric authentication, security key, or PIN to sign in."
11123
- })
11964
+ /* @__PURE__ */ jsx("path", {
11965
+ d: "M431.014 90V27.4834H440.372V51.9182H440.588C443.014 46.6326 447.91 43.5566 454.712 43.5566C464.46 43.5566 470.872 50.8351 470.872 61.8394V90H461.514V63.6157C461.514 56.0773 457.701 51.5716 451.116 51.5716C444.661 51.5716 440.372 56.5105 440.372 63.6157V90H431.014Z",
11966
+ fill: "currentColor"
11967
+ }),
11968
+ /* @__PURE__ */ jsxs("defs", { children: [/* @__PURE__ */ jsxs("linearGradient", {
11969
+ id: "emdash-lockup-icon",
11970
+ x1: "-67.1002",
11971
+ y1: "194.666",
11972
+ x2: "145.514",
11973
+ y2: "-65.5554",
11974
+ gradientUnits: "userSpaceOnUse",
11975
+ children: [
11976
+ /* @__PURE__ */ jsx("stop", { stopColor: "#0F006B" }),
11977
+ /* @__PURE__ */ jsx("stop", {
11978
+ offset: "0.0833",
11979
+ stopColor: "#281A81"
11980
+ }),
11981
+ /* @__PURE__ */ jsx("stop", {
11982
+ offset: "0.1667",
11983
+ stopColor: "#5D0C83"
11984
+ }),
11985
+ /* @__PURE__ */ jsx("stop", {
11986
+ offset: "0.25",
11987
+ stopColor: "#911475"
11988
+ }),
11989
+ /* @__PURE__ */ jsx("stop", {
11990
+ offset: "0.3333",
11991
+ stopColor: "#CE2F55"
11992
+ }),
11993
+ /* @__PURE__ */ jsx("stop", {
11994
+ offset: "0.4167",
11995
+ stopColor: "#FF6633"
11996
+ }),
11997
+ /* @__PURE__ */ jsx("stop", {
11998
+ offset: "0.5",
11999
+ stopColor: "#F6821F"
12000
+ }),
12001
+ /* @__PURE__ */ jsx("stop", {
12002
+ offset: "0.5833",
12003
+ stopColor: "#FBAD41"
12004
+ }),
12005
+ /* @__PURE__ */ jsx("stop", {
12006
+ offset: "0.6667",
12007
+ stopColor: "#FFCD89"
12008
+ }),
12009
+ /* @__PURE__ */ jsx("stop", {
12010
+ offset: "0.75",
12011
+ stopColor: "#FFE9CB"
12012
+ }),
12013
+ /* @__PURE__ */ jsx("stop", {
12014
+ offset: "0.8333",
12015
+ stopColor: "#FFF7EC"
12016
+ }),
12017
+ /* @__PURE__ */ jsx("stop", {
12018
+ offset: "0.9167",
12019
+ stopColor: "#FFF8EE"
12020
+ }),
12021
+ /* @__PURE__ */ jsx("stop", {
12022
+ offset: "1",
12023
+ stopColor: "white"
12024
+ })
12025
+ ]
12026
+ }), /* @__PURE__ */ jsxs("linearGradient", {
12027
+ id: "emdash-lockup-dash",
12028
+ x1: "144.064",
12029
+ y1: "43.1581",
12030
+ x2: "44.5609",
12031
+ y2: "85.0447",
12032
+ gradientUnits: "userSpaceOnUse",
12033
+ children: [
12034
+ /* @__PURE__ */ jsx("stop", { stopColor: "white" }),
12035
+ /* @__PURE__ */ jsx("stop", {
12036
+ offset: "0.1293",
12037
+ stopColor: "#FFF8EE"
12038
+ }),
12039
+ /* @__PURE__ */ jsx("stop", {
12040
+ offset: "0.6171",
12041
+ stopColor: "#FBAD41"
12042
+ }),
12043
+ /* @__PURE__ */ jsx("stop", {
12044
+ offset: "0.848",
12045
+ stopColor: "#F6821F"
12046
+ }),
12047
+ /* @__PURE__ */ jsx("stop", {
12048
+ offset: "1",
12049
+ stopColor: "#FF6633"
12050
+ })
12051
+ ]
12052
+ })] })
11124
12053
  ]
11125
12054
  });
11126
12055
  }
@@ -11183,6 +12112,7 @@ const OAUTH_PROVIDERS = [{
11183
12112
  icon: /* @__PURE__ */ jsx(GoogleIcon, { className: "h-5 w-5" })
11184
12113
  }];
11185
12114
  function MagicLinkForm({ onBack }) {
12115
+ const { _: _t } = useLingui();
11186
12116
  const [email, setEmail] = React.useState("");
11187
12117
  const [isLoading, setIsLoading] = React.useState(false);
11188
12118
  const [error, setError] = React.useState(null);
@@ -11199,11 +12129,17 @@ function MagicLinkForm({ onBack }) {
11199
12129
  });
11200
12130
  if (!response.ok) {
11201
12131
  const body = await response.json().catch(() => ({}));
11202
- throw new Error(body?.error?.message || "Failed to send magic link");
12132
+ throw new Error(body?.error?.message || _t({
12133
+ id: "dsPiA2",
12134
+ message: "Failed to send magic link"
12135
+ }));
11203
12136
  }
11204
12137
  setSent(true);
11205
12138
  } catch (err) {
11206
- setError(err instanceof Error ? err.message : "Failed to send magic link");
12139
+ setError(err instanceof Error ? err.message : _t({
12140
+ id: "dsPiA2",
12141
+ message: "Failed to send magic link"
12142
+ }));
11207
12143
  } finally {
11208
12144
  setIsLoading(false);
11209
12145
  }
@@ -11228,30 +12164,40 @@ function MagicLinkForm({ onBack }) {
11228
12164
  }),
11229
12165
  /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
11230
12166
  className: "text-xl font-semibold",
11231
- children: "Check your email"
11232
- }), /* @__PURE__ */ jsxs("p", {
12167
+ children: _t({
12168
+ id: "v4fiSg",
12169
+ message: "Check your email"
12170
+ })
12171
+ }), /* @__PURE__ */ jsx("p", {
11233
12172
  className: "text-kumo-subtle mt-2",
11234
- children: [
11235
- "If an account exists for ",
11236
- /* @__PURE__ */ jsx("span", {
11237
- className: "font-medium text-kumo-default",
11238
- children: email
11239
- }),
11240
- ", we've sent a sign-in link."
11241
- ]
12173
+ children: /* @__PURE__ */ jsx(Trans, {
12174
+ id: "HvU4EW",
12175
+ message: "If an account exists for <0>{email}</0>, we've sent a sign-in link.",
12176
+ values: { email },
12177
+ components: { 0: /* @__PURE__ */ jsx("span", { className: "font-medium text-kumo-default" }) }
12178
+ })
11242
12179
  })] }),
11243
12180
  /* @__PURE__ */ jsxs("div", {
11244
12181
  className: "text-sm text-kumo-subtle",
11245
- children: [/* @__PURE__ */ jsx("p", { children: "Click the link in the email to sign in." }), /* @__PURE__ */ jsx("p", {
12182
+ children: [/* @__PURE__ */ jsx("p", { children: _t({
12183
+ id: "tfQcxX",
12184
+ message: "Click the link in the email to sign in."
12185
+ }) }), /* @__PURE__ */ jsx("p", {
11246
12186
  className: "mt-2",
11247
- children: "The link will expire in 15 minutes."
12187
+ children: _t({
12188
+ id: "Cw9Xfg",
12189
+ message: "The link will expire in 15 minutes."
12190
+ })
11248
12191
  })]
11249
12192
  }),
11250
12193
  /* @__PURE__ */ jsx(Button, {
11251
12194
  variant: "outline",
11252
12195
  onClick: onBack,
11253
12196
  className: "mt-4 w-full justify-center",
11254
- children: "Back to login"
12197
+ children: _t({
12198
+ id: "VCoEm+",
12199
+ message: "Back to login"
12200
+ })
11255
12201
  })
11256
12202
  ]
11257
12203
  });
@@ -11260,7 +12206,10 @@ function MagicLinkForm({ onBack }) {
11260
12206
  className: "space-y-4",
11261
12207
  children: [
11262
12208
  /* @__PURE__ */ jsx(Input, {
11263
- label: "Email address",
12209
+ label: _t({
12210
+ id: "ATGYL1",
12211
+ message: "Email address"
12212
+ }),
11264
12213
  type: "email",
11265
12214
  value: email,
11266
12215
  onChange: (e) => setEmail(e.target.value),
@@ -11281,14 +12230,23 @@ function MagicLinkForm({ onBack }) {
11281
12230
  variant: "primary",
11282
12231
  loading: isLoading,
11283
12232
  disabled: !email,
11284
- children: isLoading ? "Sending..." : "Send magic link"
12233
+ children: isLoading ? _t({
12234
+ id: "IoAuJG",
12235
+ message: "Sending..."
12236
+ }) : _t({
12237
+ id: "+Ni3Gv",
12238
+ message: "Send magic link"
12239
+ })
11285
12240
  }),
11286
12241
  /* @__PURE__ */ jsx(Button, {
11287
12242
  type: "button",
11288
12243
  variant: "ghost",
11289
12244
  className: "w-full justify-center",
11290
12245
  onClick: onBack,
11291
- children: "Back to login"
12246
+ children: _t({
12247
+ id: "VCoEm+",
12248
+ message: "Back to login"
12249
+ })
11292
12250
  })
11293
12251
  ]
11294
12252
  });
@@ -11298,6 +12256,8 @@ function handleOAuthClick(providerId) {
11298
12256
  }
11299
12257
  function LoginPage({ redirectUrl = "/_emdash/admin" }) {
11300
12258
  const safeRedirectUrl = sanitizeRedirectUrl(redirectUrl);
12259
+ const { _: _t2 } = useLingui();
12260
+ const { locale, setLocale } = useLocale();
11301
12261
  const [method, setMethod] = React.useState("passkey");
11302
12262
  const [urlError, setUrlError] = React.useState(null);
11303
12263
  const { data: manifest, isLoading: manifestLoading } = useQuery({
@@ -11312,7 +12272,11 @@ function LoginPage({ redirectUrl = "/_emdash/admin" }) {
11312
12272
  const error = params.get("error");
11313
12273
  const message = params.get("message");
11314
12274
  if (error) {
11315
- setUrlError(message || `Authentication error: ${error}`);
12275
+ setUrlError(message || _t2({
12276
+ id: "Xeb2Gt",
12277
+ message: "Authentication error: {error}",
12278
+ values: { error }
12279
+ }));
11316
12280
  window.history.replaceState({}, "", window.location.pathname);
11317
12281
  }
11318
12282
  }, []);
@@ -11323,10 +12287,7 @@ function LoginPage({ redirectUrl = "/_emdash/admin" }) {
11323
12287
  className: "min-h-screen flex items-center justify-center bg-kumo-base p-4",
11324
12288
  children: /* @__PURE__ */ jsxs("div", {
11325
12289
  className: "text-center",
11326
- children: [/* @__PURE__ */ jsx("div", {
11327
- className: "text-4xl font-bold mb-4",
11328
- children: "— EmDash"
11329
- }), /* @__PURE__ */ jsx(Loader, {})]
12290
+ children: [/* @__PURE__ */ jsx(LogoLockup, { className: "h-10 mx-auto mb-4" }), /* @__PURE__ */ jsx(Loader, {})]
11330
12291
  })
11331
12292
  });
11332
12293
  return /* @__PURE__ */ jsx("div", {
@@ -11336,12 +12297,15 @@ function LoginPage({ redirectUrl = "/_emdash/admin" }) {
11336
12297
  children: [
11337
12298
  /* @__PURE__ */ jsxs("div", {
11338
12299
  className: "text-center mb-8",
11339
- children: [/* @__PURE__ */ jsx("div", {
11340
- className: "text-4xl font-bold mb-2",
11341
- children: "— EmDash"
11342
- }), /* @__PURE__ */ jsxs("h1", {
12300
+ children: [/* @__PURE__ */ jsx(LogoLockup, { className: "h-10 mx-auto mb-2" }), /* @__PURE__ */ jsxs("h1", {
11343
12301
  className: "text-2xl font-semibold text-kumo-default",
11344
- children: [method === "passkey" && "Sign in to your site", method === "magic-link" && "Sign in with email"]
12302
+ children: [method === "passkey" && _t2({
12303
+ id: "lE0wHD",
12304
+ message: "Sign in to your site"
12305
+ }), method === "magic-link" && _t2({
12306
+ id: "me9L29",
12307
+ message: "Sign in with email"
12308
+ })]
11345
12309
  })]
11346
12310
  }),
11347
12311
  urlError && /* @__PURE__ */ jsx("div", {
@@ -11357,7 +12321,10 @@ function LoginPage({ redirectUrl = "/_emdash/admin" }) {
11357
12321
  optionsEndpoint: "/_emdash/api/auth/passkey/options",
11358
12322
  verifyEndpoint: "/_emdash/api/auth/passkey/verify",
11359
12323
  onSuccess: handleSuccess,
11360
- buttonText: "Sign in with Passkey"
12324
+ buttonText: _t2({
12325
+ id: "QjsOMP",
12326
+ message: "Sign in with Passkey"
12327
+ })
11361
12328
  }),
11362
12329
  /* @__PURE__ */ jsxs("div", {
11363
12330
  className: "relative",
@@ -11368,7 +12335,10 @@ function LoginPage({ redirectUrl = "/_emdash/admin" }) {
11368
12335
  className: "relative flex justify-center text-xs uppercase",
11369
12336
  children: /* @__PURE__ */ jsx("span", {
11370
12337
  className: "bg-kumo-base px-2 text-kumo-subtle",
11371
- children: "Or continue with"
12338
+ children: _t2({
12339
+ id: "zW+FpA",
12340
+ message: "Or continue with"
12341
+ })
11372
12342
  })
11373
12343
  })]
11374
12344
  }),
@@ -11390,26 +12360,42 @@ function LoginPage({ redirectUrl = "/_emdash/admin" }) {
11390
12360
  className: "w-full justify-center",
11391
12361
  type: "button",
11392
12362
  onClick: () => setMethod("magic-link"),
11393
- children: "Sign in with email link"
12363
+ children: _t2({
12364
+ id: "RxerEl",
12365
+ message: "Sign in with email link"
12366
+ })
11394
12367
  })
11395
12368
  ]
11396
12369
  }), method === "magic-link" && /* @__PURE__ */ jsx(MagicLinkForm, { onBack: () => setMethod("passkey") })]
11397
12370
  }),
11398
12371
  /* @__PURE__ */ jsx("p", {
11399
12372
  className: "text-center mt-6 text-sm text-kumo-subtle",
11400
- children: method === "passkey" ? "Use your registered passkey to sign in securely." : "We'll send you a link to sign in without a password."
12373
+ children: method === "passkey" ? _t2({
12374
+ id: "+ET/av",
12375
+ message: "Use your registered passkey to sign in securely."
12376
+ }) : _t2({
12377
+ id: "pv+wH3",
12378
+ message: "We'll send you a link to sign in without a password."
12379
+ })
11401
12380
  }),
11402
- manifest?.signupEnabled && /* @__PURE__ */ jsxs("p", {
12381
+ manifest?.signupEnabled && /* @__PURE__ */ jsx("p", {
11403
12382
  className: "text-center mt-4 text-sm text-kumo-subtle",
11404
- children: [
11405
- "Don't have an account?",
11406
- " ",
11407
- /* @__PURE__ */ jsx(Link$1, {
12383
+ children: /* @__PURE__ */ jsx(Trans, {
12384
+ id: "352VU2",
12385
+ message: "Don't have an account? <0>Sign up</0>",
12386
+ components: { 0: /* @__PURE__ */ jsx(Link$1, {
11408
12387
  to: "/signup",
11409
- className: "text-kumo-brand hover:underline font-medium",
11410
- children: "Sign up"
11411
- })
11412
- ]
12388
+ className: "text-kumo-brand hover:underline font-medium"
12389
+ }) }
12390
+ })
12391
+ }),
12392
+ SUPPORTED_LOCALES.length > 1 && /* @__PURE__ */ jsx("div", {
12393
+ className: "mt-6 flex justify-center gap-2 text-xs text-kumo-subtle",
12394
+ children: SUPPORTED_LOCALES.map((l, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [i > 0 && /* @__PURE__ */ jsx("span", { children: "·" }), /* @__PURE__ */ jsx("button", {
12395
+ onClick: () => setLocale(l.code),
12396
+ className: l.code === locale ? "font-medium text-kumo-default" : "hover:text-kumo-default transition-colors",
12397
+ children: l.label
12398
+ })] }, l.code))
11413
12399
  })
11414
12400
  ]
11415
12401
  })
@@ -13584,7 +14570,7 @@ function ContentPickerModal({ open, onOpenChange, onSelect }) {
13584
14570
  open,
13585
14571
  onOpenChange,
13586
14572
  children: /* @__PURE__ */ jsxs(Dialog, {
13587
- className: "p-6 w-2xl h-[80vh] flex flex-col",
14573
+ className: "p-6 max-w-2xl h-[80vh] flex flex-col",
13588
14574
  size: "lg",
13589
14575
  children: [
13590
14576
  /* @__PURE__ */ jsxs("div", {
@@ -13934,9 +14920,11 @@ function MenuEditor() {
13934
14920
  /* @__PURE__ */ jsx(Input, {
13935
14921
  label: "URL",
13936
14922
  name: "url",
13937
- type: "url",
14923
+ type: "text",
13938
14924
  required: true,
13939
- placeholder: "https://example.com"
14925
+ pattern: "(https?://.+|/.*)",
14926
+ title: "Enter a URL (https://…) or a relative path (/…)",
14927
+ placeholder: "https://example.com or /about"
13940
14928
  }),
13941
14929
  /* @__PURE__ */ jsxs(Select, {
13942
14930
  label: "Target",
@@ -14100,8 +15088,10 @@ function MenuEditor() {
14100
15088
  editingItem.type === "custom" && /* @__PURE__ */ jsx(Input, {
14101
15089
  label: "URL",
14102
15090
  name: "url",
14103
- type: "url",
15091
+ type: "text",
14104
15092
  required: true,
15093
+ pattern: "(https?://.+|/.*)",
15094
+ title: "Enter a URL (https://…) or a relative path (/…)",
14105
15095
  defaultValue: editingItem.custom_url || ""
14106
15096
  }),
14107
15097
  /* @__PURE__ */ jsxs(Select, {
@@ -15576,13 +16566,18 @@ function Settings() {
15576
16566
  queryKey: ["manifest"],
15577
16567
  queryFn: fetchManifest
15578
16568
  });
16569
+ const { _: _t } = useLingui();
16570
+ const { locale, setLocale } = useLocale();
15579
16571
  const showSecuritySettings = manifest?.authMode === "passkey";
15580
16572
  return /* @__PURE__ */ jsxs("div", {
15581
16573
  className: "space-y-6",
15582
16574
  children: [
15583
16575
  /* @__PURE__ */ jsx("h1", {
15584
16576
  className: "text-2xl font-bold",
15585
- children: "Settings"
16577
+ children: _t({
16578
+ id: "Tz0i8g",
16579
+ message: "Settings"
16580
+ })
15586
16581
  }),
15587
16582
  /* @__PURE__ */ jsxs("div", {
15588
16583
  className: "space-y-2",
@@ -15590,20 +16585,38 @@ function Settings() {
15590
16585
  /* @__PURE__ */ jsx(SettingsLink, {
15591
16586
  to: "/settings/general",
15592
16587
  icon: /* @__PURE__ */ jsx(Gear, { className: "h-5 w-5" }),
15593
- title: "General",
15594
- description: "Site identity, logo, favicon, and reading preferences"
16588
+ title: _t({
16589
+ id: "Weq9zb",
16590
+ message: "General"
16591
+ }),
16592
+ description: _t({
16593
+ id: "RR0ADZ",
16594
+ message: "Site identity, logo, favicon, and reading preferences"
16595
+ })
15595
16596
  }),
15596
16597
  /* @__PURE__ */ jsx(SettingsLink, {
15597
16598
  to: "/settings/social",
15598
16599
  icon: /* @__PURE__ */ jsx(ShareNetwork, { className: "h-5 w-5" }),
15599
- title: "Social Links",
15600
- description: "Social media profile links"
16600
+ title: _t({
16601
+ id: "d0rUsW",
16602
+ message: "Social Links"
16603
+ }),
16604
+ description: _t({
16605
+ id: "qS3mgX",
16606
+ message: "Social media profile links"
16607
+ })
15601
16608
  }),
15602
16609
  /* @__PURE__ */ jsx(SettingsLink, {
15603
16610
  to: "/settings/seo",
15604
16611
  icon: /* @__PURE__ */ jsx(MagnifyingGlass, { className: "h-5 w-5" }),
15605
- title: "SEO",
15606
- description: "Search engine optimization and verification"
16612
+ title: _t({
16613
+ id: "4Ml90q",
16614
+ message: "SEO"
16615
+ }),
16616
+ description: _t({
16617
+ id: "zY0S+v",
16618
+ message: "Search engine optimization and verification"
16619
+ })
15607
16620
  })
15608
16621
  ]
15609
16622
  }),
@@ -15612,13 +16625,25 @@ function Settings() {
15612
16625
  children: [/* @__PURE__ */ jsx(SettingsLink, {
15613
16626
  to: "/settings/security",
15614
16627
  icon: /* @__PURE__ */ jsx(Shield, { className: "h-5 w-5" }),
15615
- title: "Security",
15616
- description: "Manage your passkeys and authentication"
16628
+ title: _t({
16629
+ id: "a3LDKx",
16630
+ message: "Security"
16631
+ }),
16632
+ description: _t({
16633
+ id: "OfnTKV",
16634
+ message: "Manage your passkeys and authentication"
16635
+ })
15617
16636
  }), /* @__PURE__ */ jsx(SettingsLink, {
15618
16637
  to: "/settings/allowed-domains",
15619
16638
  icon: /* @__PURE__ */ jsx(Globe, { className: "h-5 w-5" }),
15620
- title: "Self-Signup Domains",
15621
- description: "Allow users from specific domains to sign up"
16639
+ title: _t({
16640
+ id: "Tllxyd",
16641
+ message: "Self-Signup Domains"
16642
+ }),
16643
+ description: _t({
16644
+ id: "DsZc8w",
16645
+ message: "Allow users from specific domains to sign up"
16646
+ })
15622
16647
  })]
15623
16648
  }),
15624
16649
  /* @__PURE__ */ jsxs("div", {
@@ -15626,14 +16651,58 @@ function Settings() {
15626
16651
  children: [/* @__PURE__ */ jsx(SettingsLink, {
15627
16652
  to: "/settings/api-tokens",
15628
16653
  icon: /* @__PURE__ */ jsx(Key, { className: "h-5 w-5" }),
15629
- title: "API Tokens",
15630
- description: "Create personal access tokens for programmatic API access"
16654
+ title: _t({
16655
+ id: "ZiooJI",
16656
+ message: "API Tokens"
16657
+ }),
16658
+ description: _t({
16659
+ id: "dhH+RW",
16660
+ message: "Create personal access tokens for programmatic API access"
16661
+ })
15631
16662
  }), /* @__PURE__ */ jsx(SettingsLink, {
15632
16663
  to: "/settings/email",
15633
16664
  icon: /* @__PURE__ */ jsx(Envelope, { className: "h-5 w-5" }),
15634
- title: "Email",
15635
- description: "View email provider status and send test emails"
16665
+ title: _t({
16666
+ id: "O3oNi5",
16667
+ message: "Email"
16668
+ }),
16669
+ description: _t({
16670
+ id: "/JPN+P",
16671
+ message: "View email provider status and send test emails"
16672
+ })
15636
16673
  })]
16674
+ }),
16675
+ SUPPORTED_LOCALES.length > 1 && /* @__PURE__ */ jsx("div", {
16676
+ className: "space-y-2",
16677
+ children: /* @__PURE__ */ jsxs("div", {
16678
+ className: "flex items-center justify-between p-4 rounded-lg border bg-kumo-base",
16679
+ children: [/* @__PURE__ */ jsxs("div", {
16680
+ className: "flex items-center gap-3",
16681
+ children: [/* @__PURE__ */ jsx("div", {
16682
+ className: "text-kumo-subtle",
16683
+ children: /* @__PURE__ */ jsx(GlobeSimple, { className: "h-5 w-5" })
16684
+ }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
16685
+ className: "font-medium",
16686
+ children: _t({
16687
+ id: "vXIe7J",
16688
+ message: "Language"
16689
+ })
16690
+ }), /* @__PURE__ */ jsx("div", {
16691
+ className: "text-sm text-kumo-subtle",
16692
+ children: _t({
16693
+ id: "Y8S9QC",
16694
+ message: "Choose your preferred admin language"
16695
+ })
16696
+ })] })]
16697
+ }), /* @__PURE__ */ jsx("div", {
16698
+ className: "flex gap-1",
16699
+ children: SUPPORTED_LOCALES.map((l) => /* @__PURE__ */ jsx("button", {
16700
+ onClick: () => setLocale(l.code),
16701
+ className: cn("rounded-md px-3 py-1.5 text-sm transition-colors", l.code === locale ? "bg-kumo-brand/10 text-kumo-brand font-medium" : "hover:bg-kumo-tint"),
16702
+ children: l.label
16703
+ }, l.code))
16704
+ })]
16705
+ })
15637
16706
  })
15638
16707
  ]
15639
16708
  });
@@ -15768,12 +16837,24 @@ function AllowedDomainsSettings() {
15768
16837
  const handleDelete = () => {
15769
16838
  if (deletingDomain) deleteMutation.mutate(deletingDomain);
15770
16839
  };
15771
- if (manifestLoading || isLoading) return /* @__PURE__ */ jsxs("div", {
15772
- className: "space-y-6",
15773
- children: [/* @__PURE__ */ jsx("h1", {
16840
+ const settingsHeader = /* @__PURE__ */ jsxs("div", {
16841
+ className: "flex items-center gap-3",
16842
+ children: [/* @__PURE__ */ jsx(Link$1, {
16843
+ to: "/settings",
16844
+ children: /* @__PURE__ */ jsx(Button, {
16845
+ variant: "ghost",
16846
+ shape: "square",
16847
+ "aria-label": "Back to settings",
16848
+ children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
16849
+ })
16850
+ }), /* @__PURE__ */ jsx("h1", {
15774
16851
  className: "text-2xl font-bold",
15775
16852
  children: "Self-Signup Domains"
15776
- }), /* @__PURE__ */ jsx("div", {
16853
+ })]
16854
+ });
16855
+ if (manifestLoading || isLoading) return /* @__PURE__ */ jsxs("div", {
16856
+ className: "space-y-6",
16857
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
15777
16858
  className: "rounded-lg border bg-kumo-base p-6",
15778
16859
  children: /* @__PURE__ */ jsx("p", {
15779
16860
  className: "text-kumo-subtle",
@@ -15783,42 +16864,28 @@ function AllowedDomainsSettings() {
15783
16864
  });
15784
16865
  if (isExternalAuth) return /* @__PURE__ */ jsxs("div", {
15785
16866
  className: "space-y-6",
15786
- children: [/* @__PURE__ */ jsx("h1", {
15787
- className: "text-2xl font-bold",
15788
- children: "Self-Signup Domains"
15789
- }), /* @__PURE__ */ jsx("div", {
16867
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
15790
16868
  className: "rounded-lg border bg-kumo-base p-6",
15791
16869
  children: /* @__PURE__ */ jsxs("div", {
15792
16870
  className: "flex items-start gap-3",
15793
- children: [/* @__PURE__ */ jsx(Info, { className: "h-5 w-5 text-kumo-subtle mt-0.5 flex-shrink-0" }), /* @__PURE__ */ jsxs("div", {
16871
+ children: [/* @__PURE__ */ jsx(Info, { className: "h-5 w-5 text-kumo-subtle mt-0.5 flex-shrink-0" }), /* @__PURE__ */ jsx("div", {
15794
16872
  className: "space-y-2",
15795
- children: [/* @__PURE__ */ jsxs("p", {
16873
+ children: /* @__PURE__ */ jsxs("p", {
15796
16874
  className: "text-kumo-subtle",
15797
16875
  children: [
15798
16876
  "User access is managed by an external provider (",
15799
16877
  manifest?.authMode,
15800
16878
  "). Self-signup domain settings are not available when using external authentication."
15801
16879
  ]
15802
- }), /* @__PURE__ */ jsx(Link$1, {
15803
- to: "/settings",
15804
- children: /* @__PURE__ */ jsx(Button, {
15805
- variant: "outline",
15806
- size: "sm",
15807
- icon: /* @__PURE__ */ jsx(ArrowLeft, {}),
15808
- children: "Back to Settings"
15809
- })
15810
- })]
16880
+ })
15811
16881
  })]
15812
16882
  })
15813
16883
  })]
15814
16884
  });
15815
16885
  if (error) return /* @__PURE__ */ jsxs("div", {
15816
16886
  className: "space-y-6",
15817
- children: [/* @__PURE__ */ jsx("h1", {
15818
- className: "text-2xl font-bold",
15819
- children: "Self-Signup Domains"
15820
- }), /* @__PURE__ */ jsx("div", {
15821
- className: "rounded-lg border border-kumo-danger/50 bg-kumo-danger/10 p-6",
16887
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
16888
+ className: "rounded-lg border bg-kumo-base p-6",
15822
16889
  children: /* @__PURE__ */ jsx("p", {
15823
16890
  className: "text-kumo-danger",
15824
16891
  children: error instanceof Error ? error.message : "Failed to load allowed domains"
@@ -15828,13 +16895,10 @@ function AllowedDomainsSettings() {
15828
16895
  return /* @__PURE__ */ jsxs("div", {
15829
16896
  className: "space-y-6",
15830
16897
  children: [
15831
- /* @__PURE__ */ jsx("h1", {
15832
- className: "text-2xl font-bold",
15833
- children: "Self-Signup Domains"
15834
- }),
16898
+ settingsHeader,
15835
16899
  saveStatus && /* @__PURE__ */ jsxs("div", {
15836
- className: `rounded-lg border p-4 flex items-center gap-2 ${saveStatus.type === "success" ? "bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-400" : "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"}`,
15837
- children: [saveStatus.type === "success" ? /* @__PURE__ */ jsx(CheckCircle, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx(WarningCircle, { className: "h-5 w-5" }), /* @__PURE__ */ jsx("span", { children: saveStatus.message })]
16900
+ className: `flex items-center gap-2 rounded-lg border p-3 text-sm ${saveStatus.type === "success" ? "border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-200" : "border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950/30 dark:text-red-200"}`,
16901
+ children: [saveStatus.type === "success" ? /* @__PURE__ */ jsx(CheckCircle, { className: "h-4 w-4 flex-shrink-0" }) : /* @__PURE__ */ jsx(WarningCircle, { className: "h-4 w-4 flex-shrink-0" }), saveStatus.message]
15838
16902
  }),
15839
16903
  /* @__PURE__ */ jsxs("div", {
15840
16904
  className: "rounded-lg border bg-kumo-base p-6",
@@ -16123,8 +17187,12 @@ function ApiTokenSettings() {
16123
17187
  className: "flex items-center gap-3",
16124
17188
  children: [/* @__PURE__ */ jsx(Link$1, {
16125
17189
  to: "/settings",
16126
- className: "text-kumo-subtle hover:text-kumo-default transition-colors",
16127
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-5 w-5" })
17190
+ children: /* @__PURE__ */ jsx(Button, {
17191
+ variant: "ghost",
17192
+ shape: "square",
17193
+ "aria-label": "Back to settings",
17194
+ children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
17195
+ })
16128
17196
  }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
16129
17197
  className: "text-2xl font-bold",
16130
17198
  children: "API Tokens"
@@ -16390,7 +17458,7 @@ function EmailSettings() {
16390
17458
  const timer = setTimeout(setStatus, 5e3, null);
16391
17459
  return () => clearTimeout(timer);
16392
17460
  }, [status]);
16393
- const { data: settings, isLoading } = useQuery({
17461
+ const { data: settings, isLoading, error: fetchError } = useQuery({
16394
17462
  queryKey: ["email-settings"],
16395
17463
  queryFn: fetchEmailSettings
16396
17464
  });
@@ -16419,6 +17487,27 @@ function EmailSettings() {
16419
17487
  className: "flex items-center justify-center py-12",
16420
17488
  children: /* @__PURE__ */ jsx(Loader, { size: "lg" })
16421
17489
  });
17490
+ if (fetchError) return /* @__PURE__ */ jsxs("div", {
17491
+ className: "space-y-6",
17492
+ children: [/* @__PURE__ */ jsxs("div", {
17493
+ className: "flex items-center gap-3",
17494
+ children: [/* @__PURE__ */ jsx(Link$1, {
17495
+ to: "/settings",
17496
+ children: /* @__PURE__ */ jsx(Button, {
17497
+ variant: "ghost",
17498
+ shape: "square",
17499
+ "aria-label": "Back to settings",
17500
+ children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
17501
+ })
17502
+ }), /* @__PURE__ */ jsx("h1", {
17503
+ className: "text-2xl font-bold",
17504
+ children: "Email Settings"
17505
+ })]
17506
+ }), /* @__PURE__ */ jsxs("div", {
17507
+ className: "flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950/30 dark:text-red-200",
17508
+ children: [/* @__PURE__ */ jsx(WarningCircle, { className: "h-4 w-4 flex-shrink-0" }), getMutationError(fetchError) || "Failed to load email settings"]
17509
+ })]
17510
+ });
16422
17511
  return /* @__PURE__ */ jsxs("div", {
16423
17512
  className: "space-y-6",
16424
17513
  children: [
@@ -16877,12 +17966,6 @@ const BASE64_PLUS_REGEX = /\+/g;
16877
17966
  const BASE64_SLASH_REGEX = /\//g;
16878
17967
  const EMPTY_DATA = {};
16879
17968
  /**
16880
- * Check if WebAuthn is supported in the current browser
16881
- */
16882
- function isWebAuthnSupported() {
16883
- return typeof window !== "undefined" && window.PublicKeyCredential !== void 0 && typeof window.PublicKeyCredential === "function";
16884
- }
16885
- /**
16886
17969
  * Convert base64url to ArrayBuffer
16887
17970
  */
16888
17971
  function base64urlToBuffer(base64url) {
@@ -16908,7 +17991,8 @@ function bufferToBase64url(buffer) {
16908
17991
  function PasskeyRegistration({ optionsEndpoint, verifyEndpoint, onSuccess, onError, buttonText = "Register Passkey", showNameInput = false, additionalData = EMPTY_DATA }) {
16909
17992
  const [state, setState] = React.useState({ status: "idle" });
16910
17993
  const [passkeyName, setPasskeyName] = React.useState("");
16911
- const isSupported = React.useMemo(() => isWebAuthnSupported(), []);
17994
+ const isSupported = React.useMemo(() => isPasskeyEnvironmentUsable(), []);
17995
+ const insecureContext = React.useMemo(() => typeof window !== "undefined" && !isWebAuthnSecureContext(), []);
16912
17996
  const handleRegister = React.useCallback(async () => {
16913
17997
  if (!isSupported) {
16914
17998
  setState({
@@ -17017,10 +18101,34 @@ function PasskeyRegistration({ optionsEndpoint, verifyEndpoint, onSuccess, onErr
17017
18101
  className: "rounded-lg border border-kumo-danger/50 bg-kumo-danger/10 p-4",
17018
18102
  children: [/* @__PURE__ */ jsx("h3", {
17019
18103
  className: "font-medium text-kumo-danger",
17020
- children: "Passkeys Not Supported"
18104
+ children: "Passkeys Not Available Here"
17021
18105
  }), /* @__PURE__ */ jsx("p", {
17022
18106
  className: "mt-1 text-sm text-kumo-subtle",
17023
- children: "Your browser doesn't support passkeys. Please use a modern browser like Chrome, Safari, Firefox, or Edge."
18107
+ children: insecureContext ? /* @__PURE__ */ jsxs(Fragment, { children: [
18108
+ "Passkeys require a ",
18109
+ /* @__PURE__ */ jsx("strong", {
18110
+ className: "text-kumo-default",
18111
+ children: "secure context"
18112
+ }),
18113
+ ": use",
18114
+ " ",
18115
+ /* @__PURE__ */ jsx("strong", {
18116
+ className: "text-kumo-default",
18117
+ children: "HTTPS"
18118
+ }),
18119
+ ", or open the admin at",
18120
+ " ",
18121
+ /* @__PURE__ */ jsx("strong", {
18122
+ className: "text-kumo-default",
18123
+ children: "http://localhost"
18124
+ }),
18125
+ " (with your dev port). Plain ",
18126
+ /* @__PURE__ */ jsx("code", {
18127
+ className: "text-xs",
18128
+ children: "http://"
18129
+ }),
18130
+ " on a custom hostname is not treated as secure, even on loopback."
18131
+ ] }) : /* @__PURE__ */ jsx(Fragment, { children: "Your browser doesn't support passkeys. Please use a modern browser like Chrome, Safari, Firefox, or Edge." })
17024
18132
  })]
17025
18133
  });
17026
18134
  return /* @__PURE__ */ jsxs("div", {
@@ -17311,12 +18419,24 @@ function SecuritySettings() {
17311
18419
  message: "Passkey added successfully"
17312
18420
  });
17313
18421
  };
17314
- if (manifestLoading || isLoading) return /* @__PURE__ */ jsxs("div", {
17315
- className: "space-y-6",
17316
- children: [/* @__PURE__ */ jsx("h1", {
18422
+ const settingsHeader = /* @__PURE__ */ jsxs("div", {
18423
+ className: "flex items-center gap-3",
18424
+ children: [/* @__PURE__ */ jsx(Link$1, {
18425
+ to: "/settings",
18426
+ children: /* @__PURE__ */ jsx(Button, {
18427
+ variant: "ghost",
18428
+ shape: "square",
18429
+ "aria-label": "Back to settings",
18430
+ children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
18431
+ })
18432
+ }), /* @__PURE__ */ jsx("h1", {
17317
18433
  className: "text-2xl font-bold",
17318
18434
  children: "Security Settings"
17319
- }), /* @__PURE__ */ jsx("div", {
18435
+ })]
18436
+ });
18437
+ if (manifestLoading || isLoading) return /* @__PURE__ */ jsxs("div", {
18438
+ className: "space-y-6",
18439
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
17320
18440
  className: "rounded-lg border bg-kumo-base p-6",
17321
18441
  children: /* @__PURE__ */ jsx("p", {
17322
18442
  className: "text-kumo-subtle",
@@ -17326,42 +18446,28 @@ function SecuritySettings() {
17326
18446
  });
17327
18447
  if (isExternalAuth) return /* @__PURE__ */ jsxs("div", {
17328
18448
  className: "space-y-6",
17329
- children: [/* @__PURE__ */ jsx("h1", {
17330
- className: "text-2xl font-bold",
17331
- children: "Security Settings"
17332
- }), /* @__PURE__ */ jsx("div", {
18449
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
17333
18450
  className: "rounded-lg border bg-kumo-base p-6",
17334
18451
  children: /* @__PURE__ */ jsxs("div", {
17335
18452
  className: "flex items-start gap-3",
17336
- children: [/* @__PURE__ */ jsx(Info, { className: "h-5 w-5 text-kumo-subtle mt-0.5 flex-shrink-0" }), /* @__PURE__ */ jsxs("div", {
18453
+ children: [/* @__PURE__ */ jsx(Info, { className: "h-5 w-5 text-kumo-subtle mt-0.5 flex-shrink-0" }), /* @__PURE__ */ jsx("div", {
17337
18454
  className: "space-y-2",
17338
- children: [/* @__PURE__ */ jsxs("p", {
18455
+ children: /* @__PURE__ */ jsxs("p", {
17339
18456
  className: "text-kumo-subtle",
17340
18457
  children: [
17341
18458
  "Authentication is managed by an external provider (",
17342
18459
  manifest?.authMode,
17343
18460
  "). Passkey settings are not available when using external authentication."
17344
18461
  ]
17345
- }), /* @__PURE__ */ jsx(Link$1, {
17346
- to: "/settings",
17347
- children: /* @__PURE__ */ jsx(Button, {
17348
- variant: "outline",
17349
- size: "sm",
17350
- icon: /* @__PURE__ */ jsx(ArrowLeft, {}),
17351
- children: "Back to Settings"
17352
- })
17353
- })]
18462
+ })
17354
18463
  })]
17355
18464
  })
17356
18465
  })]
17357
18466
  });
17358
18467
  if (error) return /* @__PURE__ */ jsxs("div", {
17359
18468
  className: "space-y-6",
17360
- children: [/* @__PURE__ */ jsx("h1", {
17361
- className: "text-2xl font-bold",
17362
- children: "Security Settings"
17363
- }), /* @__PURE__ */ jsx("div", {
17364
- className: "rounded-lg border border-kumo-danger/50 bg-kumo-danger/10 p-6",
18469
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
18470
+ className: "rounded-lg border bg-kumo-base p-6",
17365
18471
  children: /* @__PURE__ */ jsx("p", {
17366
18472
  className: "text-kumo-danger",
17367
18473
  children: error instanceof Error ? error.message : "Failed to load passkeys"
@@ -17371,13 +18477,10 @@ function SecuritySettings() {
17371
18477
  return /* @__PURE__ */ jsxs("div", {
17372
18478
  className: "space-y-6",
17373
18479
  children: [
17374
- /* @__PURE__ */ jsx("h1", {
17375
- className: "text-2xl font-bold",
17376
- children: "Security Settings"
17377
- }),
18480
+ settingsHeader,
17378
18481
  saveStatus && /* @__PURE__ */ jsxs("div", {
17379
- className: `rounded-lg border p-4 flex items-center gap-2 ${saveStatus.type === "success" ? "bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-400" : "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"}`,
17380
- children: [saveStatus.type === "success" ? /* @__PURE__ */ jsx(CheckCircle, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx(WarningCircle, { className: "h-5 w-5" }), /* @__PURE__ */ jsx("span", { children: saveStatus.message })]
18482
+ className: `flex items-center gap-2 rounded-lg border p-3 text-sm ${saveStatus.type === "success" ? "border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-200" : "border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950/30 dark:text-red-200"}`,
18483
+ children: [saveStatus.type === "success" ? /* @__PURE__ */ jsx(CheckCircle, { className: "h-4 w-4 flex-shrink-0" }) : /* @__PURE__ */ jsx(WarningCircle, { className: "h-4 w-4 flex-shrink-0" }), saveStatus.message]
17381
18484
  }),
17382
18485
  /* @__PURE__ */ jsxs("div", {
17383
18486
  className: "rounded-lg border bg-kumo-base p-6",
@@ -18129,10 +19232,7 @@ function SetupWizard() {
18129
19232
  /* @__PURE__ */ jsxs("div", {
18130
19233
  className: "text-center mb-6",
18131
19234
  children: [
18132
- /* @__PURE__ */ jsx("div", {
18133
- className: "text-4xl font-bold mb-2",
18134
- children: "— EmDash"
18135
- }),
19235
+ /* @__PURE__ */ jsx(LogoLockup, { className: "h-10 mx-auto mb-2" }),
18136
19236
  /* @__PURE__ */ jsxs("h1", {
18137
19237
  className: "text-2xl font-semibold text-kumo-default",
18138
19238
  children: [
@@ -18592,20 +19692,13 @@ function SidebarNav({ manifest }) {
18592
19692
  icon: Stack,
18593
19693
  minRole: ROLE_EDITOR$1
18594
19694
  },
18595
- {
18596
- to: "/taxonomies/$taxonomy",
18597
- label: "Categories",
18598
- icon: FileText,
18599
- params: { taxonomy: "category" },
18600
- minRole: ROLE_EDITOR$1
18601
- },
18602
- {
19695
+ ...manifest.taxonomies.map((tax) => ({
18603
19696
  to: "/taxonomies/$taxonomy",
18604
- label: "Tags",
19697
+ label: tax.label,
18605
19698
  icon: FileText,
18606
- params: { taxonomy: "tag" },
19699
+ params: { taxonomy: tax.name },
18607
19700
  minRole: ROLE_EDITOR$1
18608
- },
19701
+ })),
18609
19702
  {
18610
19703
  to: "/bylines",
18611
19704
  label: "Bylines",
@@ -18770,10 +19863,9 @@ function SidebarNav({ manifest }) {
18770
19863
  /* @__PURE__ */ jsx(KumoSidebar.Header, { children: /* @__PURE__ */ jsxs(Link$1, {
18771
19864
  to: "/",
18772
19865
  className: "emdash-brand-link flex w-full min-w-0 items-center gap-2 px-3 py-1",
18773
- children: [/* @__PURE__ */ jsx("span", {
18774
- className: "text-base shrink-0",
18775
- "aria-hidden": "true",
18776
- children: "—"
19866
+ children: [/* @__PURE__ */ jsx(LogoIcon, {
19867
+ className: "size-5 shrink-0",
19868
+ "aria-hidden": "true"
18777
19869
  }), /* @__PURE__ */ jsx("span", {
18778
19870
  className: "emdash-brand-text font-semibold truncate",
18779
19871
  children: "EmDash"
@@ -18874,8 +19966,8 @@ function Header() {
18874
19966
  const initials = ((user?.name || user?.email || "U")[0] ?? "U").toUpperCase();
18875
19967
  return /* @__PURE__ */ jsxs("header", {
18876
19968
  className: "sticky top-0 z-10 flex h-16 items-center justify-between border-b bg-kumo-base px-4",
18877
- children: [/* @__PURE__ */ jsx(KumoSidebar.Trigger, {}), /* @__PURE__ */ jsxs("div", {
18878
- className: "flex items-center space-x-2",
19969
+ children: [/* @__PURE__ */ jsx(KumoSidebar.Trigger, { className: "cursor-pointer" }), /* @__PURE__ */ jsxs("div", {
19970
+ className: "flex items-center gap-2",
18879
19971
  children: [
18880
19972
  /* @__PURE__ */ jsxs(LinkButton, {
18881
19973
  variant: "ghost",
@@ -18893,7 +19985,7 @@ function Header() {
18893
19985
  children: /* @__PURE__ */ jsxs(Button, {
18894
19986
  variant: "ghost",
18895
19987
  size: "sm",
18896
- className: "gap-2",
19988
+ className: "gap-2 py-1 h-auto",
18897
19989
  children: [user?.avatarUrl ? /* @__PURE__ */ jsx("img", {
18898
19990
  src: user.avatarUrl,
18899
19991
  alt: "",
@@ -18998,7 +20090,6 @@ function WelcomeModal({ open, onClose, userName, userRole }) {
18998
20090
  onOpenChange: (isOpen) => !isOpen && handleGetStarted(),
18999
20091
  children: /* @__PURE__ */ jsxs(Dialog, {
19000
20092
  className: "p-6 sm:max-w-md",
19001
- size: "lg",
19002
20093
  children: [
19003
20094
  /* @__PURE__ */ jsxs("div", {
19004
20095
  className: "flex items-start justify-between gap-4",
@@ -19021,8 +20112,8 @@ function WelcomeModal({ open, onClose, userName, userRole }) {
19021
20112
  className: "flex flex-col space-y-1.5 text-center sm:text-center",
19022
20113
  children: [
19023
20114
  /* @__PURE__ */ jsx("div", {
19024
- className: "mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-kumo-brand/10",
19025
- children: /* @__PURE__ */ jsx(Sparkle, { className: "h-8 w-8 text-kumo-brand" })
20115
+ className: "mx-auto mb-4",
20116
+ children: /* @__PURE__ */ jsx(LogoIcon, { className: "h-16 w-16" })
19026
20117
  }),
19027
20118
  /* @__PURE__ */ jsxs(Dialog.Title, {
19028
20119
  className: "text-2xl font-semibold leading-none tracking-tight",
@@ -19467,10 +20558,7 @@ function SignupPage() {
19467
20558
  children: [
19468
20559
  /* @__PURE__ */ jsxs("div", {
19469
20560
  className: "text-center mb-8",
19470
- children: [/* @__PURE__ */ jsx("div", {
19471
- className: "text-4xl font-bold mb-2",
19472
- children: "— EmDash"
19473
- }), /* @__PURE__ */ jsxs("h1", {
20561
+ children: [/* @__PURE__ */ jsx(LogoLockup, { className: "h-10 mx-auto mb-2" }), /* @__PURE__ */ jsxs("h1", {
19474
20562
  className: "text-2xl font-semibold text-kumo-default",
19475
20563
  children: [
19476
20564
  step === "email" && "Create an account",
@@ -19601,6 +20689,14 @@ function TermFormDialog({ open, onClose, taxonomyName, taxonomyDef, term, allTer
19601
20689
  const [description, setDescription] = React.useState(term?.description || "");
19602
20690
  const [autoSlug, setAutoSlug] = React.useState(!term);
19603
20691
  const [error, setError] = React.useState(null);
20692
+ React.useEffect(() => {
20693
+ setLabel(term?.label || "");
20694
+ setSlug(term?.slug || "");
20695
+ setParentId(term?.parentId || "");
20696
+ setDescription(term?.description || "");
20697
+ setAutoSlug(!term);
20698
+ setError(null);
20699
+ }, [term]);
19604
20700
  React.useEffect(() => {
19605
20701
  if (autoSlug && label) setSlug(slugify(label));
19606
20702
  }, [label, autoSlug]);
@@ -24650,6 +25746,10 @@ const adminLayoutRoute = createRoute({
24650
25746
  id: "_admin",
24651
25747
  component: RootComponent
24652
25748
  });
25749
+ if (typeof window !== "undefined" && typeof window.requestIdleCallback === "undefined") {
25750
+ window.requestIdleCallback = (cb) => setTimeout(cb, 50);
25751
+ window.cancelIdleCallback = (id) => clearTimeout(id);
25752
+ }
24653
25753
  function RootComponent() {
24654
25754
  const { data: manifest, isLoading, error } = useQuery({
24655
25755
  queryKey: ["manifest"],
@@ -24693,13 +25793,20 @@ function ContentListPage() {
24693
25793
  });
24694
25794
  const i18n = manifest?.i18n;
24695
25795
  const activeLocale = i18n ? localeParam ?? i18n.defaultLocale : void 0;
24696
- const { data, isLoading, error } = useQuery({
25796
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error } = useInfiniteQuery({
24697
25797
  queryKey: [
24698
25798
  "content",
24699
25799
  collection,
24700
25800
  { locale: activeLocale }
24701
25801
  ],
24702
- queryFn: () => fetchContentList(collection, { locale: activeLocale })
25802
+ queryFn: ({ pageParam }) => fetchContentList(collection, {
25803
+ locale: activeLocale,
25804
+ cursor: pageParam,
25805
+ limit: 100
25806
+ }),
25807
+ initialPageParam: void 0,
25808
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
25809
+ enabled: !!manifest
24703
25810
  });
24704
25811
  const { data: trashedData, isLoading: isTrashedLoading } = useQuery({
24705
25812
  queryKey: [
@@ -24775,6 +25882,9 @@ function ContentListPage() {
24775
25882
  });
24776
25883
  }
24777
25884
  });
25885
+ const items = React.useMemo(() => {
25886
+ return data?.pages.flatMap((page) => page.items) || [];
25887
+ }, [data]);
24778
25888
  if (!manifest) return /* @__PURE__ */ jsx(LoadingScreen, {});
24779
25889
  const collectionConfig = manifest.collections[collection];
24780
25890
  if (!collectionConfig) return /* @__PURE__ */ jsx(NotFoundPage, { message: `Collection "${collection}" not found` });
@@ -24789,10 +25899,12 @@ function ContentListPage() {
24789
25899
  return /* @__PURE__ */ jsx(ContentList, {
24790
25900
  collection,
24791
25901
  collectionLabel: collectionConfig.label,
24792
- items: data?.items || [],
25902
+ items,
24793
25903
  trashedItems: trashedData?.items || [],
24794
- isLoading,
25904
+ isLoading: isLoading || isFetchingNextPage,
24795
25905
  isTrashedLoading,
25906
+ hasMore: !!hasNextPage,
25907
+ onLoadMore: React.useCallback(() => void fetchNextPage(), [fetchNextPage]),
24796
25908
  trashedCount: trashedData?.items?.length || 0,
24797
25909
  onDelete: (id) => deleteMutation.mutate(id),
24798
25910
  onRestore: (id) => restoreMutation.mutate(id),
@@ -24800,7 +25912,8 @@ function ContentListPage() {
24800
25912
  onDuplicate: (id) => duplicateMutation.mutate(id),
24801
25913
  i18n,
24802
25914
  activeLocale,
24803
- onLocaleChange: handleLocaleChange
25915
+ onLocaleChange: handleLocaleChange,
25916
+ urlPattern: collectionConfig.urlPattern
24804
25917
  });
24805
25918
  }
24806
25919
  /** Extract plugin block definitions from the manifest for Portable Text editor */
@@ -24815,10 +25928,12 @@ function getPluginBlocks(manifest) {
24815
25928
  const contentNewRoute = createRoute({
24816
25929
  getParentRoute: () => adminLayoutRoute,
24817
25930
  path: "/content/$collection/new",
24818
- component: ContentNewPage
25931
+ component: ContentNewPage,
25932
+ validateSearch: (search) => ({ locale: typeof search.locale === "string" ? search.locale : void 0 })
24819
25933
  });
24820
25934
  function ContentNewPage() {
24821
25935
  const { collection } = useParams({ from: "/_admin/content/$collection/new" });
25936
+ const { locale } = useSearch({ from: "/_admin/content/$collection/new" });
24822
25937
  const navigate = useNavigate();
24823
25938
  const queryClient = useQueryClient();
24824
25939
  const [selectedBylines, setSelectedBylines] = React.useState([]);
@@ -24827,7 +25942,10 @@ function ContentNewPage() {
24827
25942
  queryFn: fetchManifest
24828
25943
  });
24829
25944
  const createMutation = useMutation({
24830
- mutationFn: (data) => createContent(collection, data),
25945
+ mutationFn: (data) => createContent(collection, {
25946
+ ...data,
25947
+ locale
25948
+ }),
24831
25949
  onSuccess: (result) => {
24832
25950
  queryClient.invalidateQueries({ queryKey: ["content", collection] });
24833
25951
  navigate({
@@ -24894,11 +26012,13 @@ function ContentNewPage() {
24894
26012
  const contentEditRoute = createRoute({
24895
26013
  getParentRoute: () => adminLayoutRoute,
24896
26014
  path: "/content/$collection/$id",
24897
- component: ContentEditPage
26015
+ component: ContentEditPage,
26016
+ validateSearch: (search) => ({ ...typeof search.field === "string" && { field: search.field } })
24898
26017
  });
24899
26018
  const ROLE_EDITOR = 40;
24900
26019
  function ContentEditPage() {
24901
26020
  const { collection, id } = useParams({ from: "/_admin/content/$collection/$id" });
26021
+ const searchParams = useSearch({ from: "/_admin/content/$collection/$id" });
24902
26022
  const queryClient = useQueryClient();
24903
26023
  const navigate = useNavigate();
24904
26024
  const toastManager = Toast.useToastManager();
@@ -24915,6 +26035,29 @@ function ContentEditPage() {
24915
26035
  ],
24916
26036
  queryFn: () => fetchContent(collection, id)
24917
26037
  });
26038
+ React.useEffect(() => {
26039
+ if (typeof searchParams.field !== "string" || isLoading) return;
26040
+ const timeoutId = requestIdleCallback(() => {
26041
+ const el = document.getElementById(`field-${searchParams.field}`);
26042
+ if (el) {
26043
+ el.scrollIntoView({
26044
+ behavior: "smooth",
26045
+ block: "center"
26046
+ });
26047
+ el.focus();
26048
+ const { field: _, ...preservedSearch } = searchParams;
26049
+ navigate({
26050
+ search: preservedSearch,
26051
+ replace: true
26052
+ });
26053
+ }
26054
+ });
26055
+ return () => cancelIdleCallback(timeoutId);
26056
+ }, [
26057
+ searchParams,
26058
+ isLoading,
26059
+ navigate
26060
+ ]);
24918
26061
  const { data: translationsData } = useQuery({
24919
26062
  queryKey: [
24920
26063
  "translations",
@@ -24992,6 +26135,7 @@ function ContentEditPage() {
24992
26135
  collection,
24993
26136
  id
24994
26137
  ] });
26138
+ if (rawItem?.draftRevisionId) queryClient.invalidateQueries({ queryKey: ["revision", rawItem.draftRevisionId] });
24995
26139
  },
24996
26140
  onError: (error) => {
24997
26141
  toastManager.add({
@@ -25014,6 +26158,7 @@ function ContentEditPage() {
25014
26158
  collection,
25015
26159
  id
25016
26160
  ] });
26161
+ if (rawItem?.draftRevisionId) queryClient.invalidateQueries({ queryKey: ["revision", rawItem.draftRevisionId] });
25017
26162
  },
25018
26163
  onError: (err) => {
25019
26164
  toastManager.add({
@@ -25237,6 +26382,7 @@ function ContentEditPage() {
25237
26382
  isDeleting: deleteMutation.isPending,
25238
26383
  supportsDrafts: collectionConfig.supports.includes("drafts"),
25239
26384
  supportsRevisions: collectionConfig.supports.includes("revisions"),
26385
+ supportsPreview: collectionConfig.supports.includes("preview"),
25240
26386
  currentUser,
25241
26387
  users: usersData?.items,
25242
26388
  onAuthorChange: handleAuthorChange,
@@ -25732,6 +26878,17 @@ function ContentTypesEditPage() {
25732
26878
  queryClient.invalidateQueries({ queryKey: ["manifest"] });
25733
26879
  }
25734
26880
  });
26881
+ const reorderFieldsMutation = useMutation({
26882
+ mutationFn: (fieldSlugs) => reorderFields(slug, fieldSlugs),
26883
+ onSuccess: () => {
26884
+ queryClient.invalidateQueries({ queryKey: [
26885
+ "schema",
26886
+ "collections",
26887
+ slug
26888
+ ] });
26889
+ queryClient.invalidateQueries({ queryKey: ["manifest"] });
26890
+ }
26891
+ });
25735
26892
  if (error) return /* @__PURE__ */ jsx(ErrorScreen, { error: error.message });
25736
26893
  if (isLoading) return /* @__PURE__ */ jsx(LoadingScreen, {});
25737
26894
  return /* @__PURE__ */ jsx(ContentTypeEditor, {
@@ -25743,7 +26900,8 @@ function ContentTypesEditPage() {
25743
26900
  fieldSlug,
25744
26901
  input
25745
26902
  }),
25746
- onDeleteField: (fieldSlug) => deleteFieldMutation.mutate(fieldSlug)
26903
+ onDeleteField: (fieldSlug) => deleteFieldMutation.mutate(fieldSlug),
26904
+ onReorderFields: (fieldSlugs) => reorderFieldsMutation.mutate(fieldSlugs)
25747
26905
  });
25748
26906
  }
25749
26907
  const pluginRoute = createRoute({
@@ -25897,19 +27055,30 @@ const router = createAdminRouter(queryClient);
25897
27055
  * Main Admin Application
25898
27056
  */
25899
27057
  const EMPTY_PLUGINS = {};
25900
- function AdminApp({ pluginAdmins = EMPTY_PLUGINS }) {
27058
+ function AdminApp({ pluginAdmins = EMPTY_PLUGINS, locale = "en", messages = {} }) {
25901
27059
  React.useEffect(() => {
25902
27060
  document.getElementById("emdash-boot-loader")?.remove();
25903
27061
  }, []);
25904
- return /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(Toasty, { children: /* @__PURE__ */ jsx(PluginAdminProvider, {
25905
- pluginAdmins,
25906
- children: /* @__PURE__ */ jsx(QueryClientProvider, {
25907
- client: queryClient,
25908
- children: /* @__PURE__ */ jsx(RouterProvider, { router })
25909
- })
25910
- }) }) });
27062
+ const i18nInitialized = React.useRef(false);
27063
+ if (!i18nInitialized.current) {
27064
+ i18n.loadAndActivate({
27065
+ locale,
27066
+ messages
27067
+ });
27068
+ i18nInitialized.current = true;
27069
+ }
27070
+ return /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(I18nProvider, {
27071
+ i18n,
27072
+ children: /* @__PURE__ */ jsx(Toasty, { children: /* @__PURE__ */ jsx(PluginAdminProvider, {
27073
+ pluginAdmins,
27074
+ children: /* @__PURE__ */ jsx(QueryClientProvider, {
27075
+ client: queryClient,
27076
+ children: /* @__PURE__ */ jsx(RouterProvider, { router })
27077
+ })
27078
+ }) })
27079
+ }) });
25911
27080
  }
25912
27081
 
25913
27082
  //#endregion
25914
- export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, CAPABILITY_LABELS, ContentEditor, ContentList, Dashboard, Header, Link, LoginPage, MediaLibrary, MediaPickerModal, PasskeyLogin, PasskeyRegistration, PluginAdminProvider, PortableTextEditor, 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, createRedirect, createSection, createTaxonomy, createTerm, 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, fetchByline, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, 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, 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, useCurrentUser, useNavigate, useParams, usePluginAdmins, usePluginField, usePluginHasPages, usePluginHasWidgets, usePluginPage, usePluginWidget, verifySignupToken };
27083
+ export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, CAPABILITY_LABELS, ContentEditor, ContentList, DEFAULT_LOCALE, Dashboard, 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, createRedirect, createSection, createTaxonomy, createTerm, 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, fetchByline, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, 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, useCurrentUser, useLocale, useNavigate, useParams, usePluginAdmins, usePluginField, usePluginHasPages, usePluginHasWidgets, usePluginPage, usePluginWidget, verifySignupToken };
25915
27084
  //# sourceMappingURL=index.js.map