@emdash-cms/admin 0.0.2 → 0.1.1

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
@@ -5,9 +5,12 @@ import { Link, Link as Link$1, Outlet, RouterProvider, createRootRouteWithContex
5
5
  import * as React from "react";
6
6
  import { createContext, useCallback, useContext, useEffect, useState } from "react";
7
7
  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";
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, 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
9
  import { clsx } from "clsx";
10
10
  import { twMerge } from "tailwind-merge";
11
+ import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter, rectIntersection, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
12
+ import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
13
+ import { CSS } from "@dnd-kit/utilities";
11
14
  import { autoUpdate, flip, offset, shift, useFloating } from "@floating-ui/react";
12
15
  import { Extension, InputRule, Node as Node$1, PasteRule, escapeForRegEx, mergeAttributes } from "@tiptap/core";
13
16
  import CharacterCount from "@tiptap/extension-character-count";
@@ -27,9 +30,6 @@ import { BlockRenderer } from "@emdash-cms/blocks";
27
30
  import DOMPurify from "dompurify";
28
31
  import { Marked, Renderer } from "marked";
29
32
  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
33
 
34
34
  //#region src/components/ThemeProvider.tsx
35
35
  const ThemeContext = React.createContext(void 0);
@@ -54,15 +54,8 @@ function ThemeProvider({ children, defaultTheme = "system" }) {
54
54
  return theme;
55
55
  });
56
56
  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
- }
57
+ if (theme === "system") setResolvedTheme(getSystemTheme());
58
+ else setResolvedTheme(theme);
66
59
  }, [theme]);
67
60
  React.useEffect(() => {
68
61
  if (theme !== "system") return;
@@ -73,6 +66,11 @@ function ThemeProvider({ children, defaultTheme = "system" }) {
73
66
  mediaQuery.addEventListener("change", handler);
74
67
  return () => mediaQuery.removeEventListener("change", handler);
75
68
  }, [theme]);
69
+ React.useEffect(() => {
70
+ const root = document.documentElement;
71
+ root.setAttribute("data-theme", "classic");
72
+ root.setAttribute("data-mode", resolvedTheme);
73
+ }, [resolvedTheme]);
76
74
  const setTheme = React.useCallback((newTheme) => {
77
75
  setThemeState(newTheme);
78
76
  localStorage.setItem(STORAGE_KEY, newTheme);
@@ -2416,6 +2414,58 @@ function useCurrentUser() {
2416
2414
  });
2417
2415
  }
2418
2416
 
2417
+ //#endregion
2418
+ //#region src/lib/url.ts
2419
+ /**
2420
+ * Shared URL validation and transformation utilities
2421
+ */
2422
+ const DEFAULT_REDIRECT = "/_emdash/admin";
2423
+ const LEADING_SLASHES = /^\/+/;
2424
+ /**
2425
+ * Sanitize a redirect URL to prevent open-redirect and javascript: XSS attacks.
2426
+ *
2427
+ * Only allows relative paths starting with `/`. Rejects protocol-relative
2428
+ * URLs (`//evil.com`), backslash tricks (`/\evil.com`), and non-path schemes
2429
+ * like `javascript:`.
2430
+ *
2431
+ * Returns the default admin URL when the input is unsafe.
2432
+ */
2433
+ function sanitizeRedirectUrl(raw) {
2434
+ if (raw.startsWith("/") && !raw.startsWith("//") && !raw.includes("\\")) return raw;
2435
+ return DEFAULT_REDIRECT;
2436
+ }
2437
+ /**
2438
+ * Build a public content URL from collection metadata and slug.
2439
+ *
2440
+ * Uses the collection's `urlPattern` when available (e.g. `/blog/{slug}`),
2441
+ * otherwise falls back to `/{collection}/{slug}`. Leading slashes are
2442
+ * stripped from the slug to prevent protocol-relative URLs.
2443
+ */
2444
+ function contentUrl(collection, slug, urlPattern) {
2445
+ const safe = slug.replace(LEADING_SLASHES, "");
2446
+ return urlPattern ? urlPattern.replace("{slug}", safe) : `/${collection}/${safe}`;
2447
+ }
2448
+ /** Matches http:// or https:// URLs */
2449
+ const SAFE_URL_RE = /^https?:\/\//i;
2450
+ /** Returns true if the URL uses a safe scheme (http/https) */
2451
+ function isSafeUrl$1(url) {
2452
+ return SAFE_URL_RE.test(url);
2453
+ }
2454
+ /**
2455
+ * Build an icon URL with a width query param, or return null for unsafe URLs.
2456
+ * Validates the URL scheme and appends `?w=<width>` for image resizing.
2457
+ */
2458
+ function safeIconUrl(url, width) {
2459
+ if (!SAFE_URL_RE.test(url)) return null;
2460
+ try {
2461
+ const u = new URL(url);
2462
+ u.searchParams.set("w", String(width));
2463
+ return u.href;
2464
+ } catch {
2465
+ return null;
2466
+ }
2467
+ }
2468
+
2419
2469
  //#endregion
2420
2470
  //#region src/components/BlockKitFieldWidget.tsx
2421
2471
  /**
@@ -2664,6 +2714,242 @@ var PluginFieldErrorBoundary = class extends React.Component {
2664
2714
  }
2665
2715
  };
2666
2716
 
2717
+ //#endregion
2718
+ //#region src/components/RepeaterField.tsx
2719
+ /**
2720
+ * RepeaterField — renders a list of repeating sub-field groups in the content editor.
2721
+ *
2722
+ * Each item is a collapsible card containing the defined sub-fields.
2723
+ * Items can be added, removed, and reordered via drag-and-drop.
2724
+ */
2725
+ function ensureKeys(items) {
2726
+ return items.map((item, i) => {
2727
+ const obj = typeof item === "object" && item !== null ? item : {};
2728
+ return {
2729
+ ...obj,
2730
+ _key: obj._key || `item-${i}-${Date.now()}`
2731
+ };
2732
+ });
2733
+ }
2734
+ function stripKeys(items) {
2735
+ return items.map(({ _key, ...rest }) => rest);
2736
+ }
2737
+ function RepeaterField({ label, id, value, onChange, subFields, minItems = 0, maxItems }) {
2738
+ const rawItems = Array.isArray(value) ? value : [];
2739
+ const [items, setItems] = React.useState(() => ensureKeys(rawItems));
2740
+ const [collapsedItems, setCollapsedItems] = React.useState(/* @__PURE__ */ new Set());
2741
+ React.useEffect(() => {
2742
+ setItems(ensureKeys(Array.isArray(value) ? value : []));
2743
+ }, [value]);
2744
+ const emitChange = (updated) => {
2745
+ setItems(updated);
2746
+ onChange(stripKeys(updated));
2747
+ };
2748
+ const handleAdd = () => {
2749
+ if (maxItems && items.length >= maxItems) return;
2750
+ const newItem = { _key: `item-${Date.now()}` };
2751
+ for (const sf of subFields) newItem[sf.slug] = sf.type === "boolean" ? false : sf.type === "number" || sf.type === "integer" ? null : "";
2752
+ emitChange([...items, newItem]);
2753
+ };
2754
+ const handleRemove = (key) => {
2755
+ if (items.length <= minItems) return;
2756
+ emitChange(items.filter((item) => item._key !== key));
2757
+ };
2758
+ const handleItemChange = (key, fieldSlug, fieldValue) => {
2759
+ emitChange(items.map((item) => item._key === key ? {
2760
+ ...item,
2761
+ [fieldSlug]: fieldValue
2762
+ } : item));
2763
+ };
2764
+ const handleDragEnd = (event) => {
2765
+ const { active, over } = event;
2766
+ if (!over || active.id === over.id) return;
2767
+ const oldIndex = items.findIndex((item) => item._key === active.id);
2768
+ const newIndex = items.findIndex((item) => item._key === over.id);
2769
+ if (oldIndex === -1 || newIndex === -1) return;
2770
+ emitChange(arrayMove(items, oldIndex, newIndex));
2771
+ };
2772
+ const toggleCollapse = (key) => {
2773
+ setCollapsedItems((prev) => {
2774
+ const next = new Set(prev);
2775
+ if (next.has(key)) next.delete(key);
2776
+ else next.add(key);
2777
+ return next;
2778
+ });
2779
+ };
2780
+ const canAdd = !maxItems || items.length < maxItems;
2781
+ const canRemove = items.length > minItems;
2782
+ return /* @__PURE__ */ jsxs("div", {
2783
+ className: "space-y-2",
2784
+ children: [/* @__PURE__ */ jsxs("div", {
2785
+ className: "flex items-center justify-between",
2786
+ children: [/* @__PURE__ */ jsxs("label", {
2787
+ htmlFor: id,
2788
+ className: "text-sm font-medium",
2789
+ children: [label, items.length > 0 && /* @__PURE__ */ jsxs("span", {
2790
+ className: "ml-2 text-kumo-subtle font-normal",
2791
+ children: [
2792
+ "(",
2793
+ items.length,
2794
+ " items)"
2795
+ ]
2796
+ })]
2797
+ }), canAdd && /* @__PURE__ */ jsx(Button, {
2798
+ variant: "outline",
2799
+ size: "sm",
2800
+ icon: /* @__PURE__ */ jsx(Plus, {}),
2801
+ onClick: handleAdd,
2802
+ children: "Add Item"
2803
+ })]
2804
+ }), items.length === 0 ? /* @__PURE__ */ jsxs("div", {
2805
+ className: "border-2 border-dashed rounded-lg p-6 text-center text-kumo-subtle",
2806
+ children: [/* @__PURE__ */ jsx("p", {
2807
+ className: "text-sm",
2808
+ children: "No items yet"
2809
+ }), canAdd && /* @__PURE__ */ jsx(Button, {
2810
+ variant: "outline",
2811
+ size: "sm",
2812
+ className: "mt-2",
2813
+ icon: /* @__PURE__ */ jsx(Plus, {}),
2814
+ onClick: handleAdd,
2815
+ children: "Add First Item"
2816
+ })]
2817
+ }) : /* @__PURE__ */ jsx(DndContext, {
2818
+ collisionDetection: closestCenter,
2819
+ onDragEnd: handleDragEnd,
2820
+ children: /* @__PURE__ */ jsx(SortableContext, {
2821
+ items: items.map((item) => item._key),
2822
+ strategy: verticalListSortingStrategy,
2823
+ children: /* @__PURE__ */ jsx("div", {
2824
+ className: "space-y-2",
2825
+ children: items.map((item, index) => /* @__PURE__ */ jsx(SortableRepeaterItem, {
2826
+ item,
2827
+ index,
2828
+ subFields,
2829
+ isCollapsed: collapsedItems.has(item._key),
2830
+ onToggleCollapse: () => toggleCollapse(item._key),
2831
+ onRemove: canRemove ? () => handleRemove(item._key) : void 0,
2832
+ onChange: (fieldSlug, fieldValue) => handleItemChange(item._key, fieldSlug, fieldValue)
2833
+ }, item._key))
2834
+ })
2835
+ })
2836
+ })]
2837
+ });
2838
+ }
2839
+ function SortableRepeaterItem({ item, index, subFields, isCollapsed, onToggleCollapse, onRemove, onChange }) {
2840
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item._key });
2841
+ const style = {
2842
+ transform: CSS.Transform.toString(transform),
2843
+ transition
2844
+ };
2845
+ const summaryField = subFields.find((sf) => sf.type === "string" || sf.type === "text");
2846
+ const summaryLabel = (summaryField ? item[summaryField.slug] || "" : "") || `Item ${index + 1}`;
2847
+ return /* @__PURE__ */ jsxs("div", {
2848
+ ref: setNodeRef,
2849
+ style,
2850
+ className: cn("border rounded-lg bg-kumo-base", isDragging && "opacity-50 ring-2 ring-kumo-brand"),
2851
+ children: [/* @__PURE__ */ jsxs("div", {
2852
+ className: "flex items-center gap-2 px-3 py-2 border-b cursor-pointer",
2853
+ onClick: onToggleCollapse,
2854
+ children: [
2855
+ /* @__PURE__ */ jsx(DotsSixVertical, {
2856
+ className: "h-4 w-4 text-kumo-subtle cursor-grab shrink-0",
2857
+ ...attributes,
2858
+ ...listeners,
2859
+ onClick: (e) => e.stopPropagation()
2860
+ }),
2861
+ 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" }),
2862
+ /* @__PURE__ */ jsx("span", {
2863
+ className: "text-sm font-medium flex-1 truncate",
2864
+ children: summaryLabel
2865
+ }),
2866
+ onRemove && /* @__PURE__ */ jsx(Button, {
2867
+ variant: "ghost",
2868
+ shape: "square",
2869
+ onClick: (e) => {
2870
+ e.stopPropagation();
2871
+ onRemove();
2872
+ },
2873
+ "aria-label": `Remove item ${index + 1}`,
2874
+ children: /* @__PURE__ */ jsx(Trash, { className: "h-3.5 w-3.5 text-kumo-danger" })
2875
+ })
2876
+ ]
2877
+ }), !isCollapsed && /* @__PURE__ */ jsx("div", {
2878
+ className: "p-3 space-y-3",
2879
+ children: subFields.map((sf) => /* @__PURE__ */ jsx(SubFieldInput, {
2880
+ subField: sf,
2881
+ value: item[sf.slug],
2882
+ onChange: (v) => onChange(sf.slug, v)
2883
+ }, sf.slug))
2884
+ })]
2885
+ });
2886
+ }
2887
+ function SubFieldInput({ subField, value, onChange }) {
2888
+ switch (subField.type) {
2889
+ case "string": return /* @__PURE__ */ jsx(Input, {
2890
+ label: subField.label,
2891
+ value: typeof value === "string" ? value : "",
2892
+ onChange: (e) => onChange(e.target.value),
2893
+ required: subField.required
2894
+ });
2895
+ case "text": return /* @__PURE__ */ jsx(InputArea, {
2896
+ label: subField.label,
2897
+ value: typeof value === "string" ? value : "",
2898
+ onChange: (e) => onChange(e.target.value),
2899
+ required: subField.required,
2900
+ rows: 3
2901
+ });
2902
+ case "number":
2903
+ case "integer": return /* @__PURE__ */ jsx(Input, {
2904
+ label: subField.label,
2905
+ type: "number",
2906
+ value: typeof value === "number" ? String(value) : "",
2907
+ onChange: (e) => onChange(e.target.value ? Number(e.target.value) : null),
2908
+ required: subField.required,
2909
+ step: subField.type === "integer" ? "1" : "any"
2910
+ });
2911
+ case "boolean": return /* @__PURE__ */ jsxs("label", {
2912
+ className: "flex items-center gap-2",
2913
+ children: [/* @__PURE__ */ jsx("input", {
2914
+ type: "checkbox",
2915
+ checked: Boolean(value),
2916
+ onChange: (e) => onChange(e.target.checked)
2917
+ }), /* @__PURE__ */ jsx("span", {
2918
+ className: "text-sm",
2919
+ children: subField.label
2920
+ })]
2921
+ });
2922
+ case "datetime": return /* @__PURE__ */ jsx(Input, {
2923
+ label: subField.label,
2924
+ type: "datetime-local",
2925
+ value: typeof value === "string" ? value : "",
2926
+ onChange: (e) => onChange(e.target.value),
2927
+ required: subField.required
2928
+ });
2929
+ case "select": return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
2930
+ className: "text-sm font-medium",
2931
+ children: subField.label
2932
+ }), /* @__PURE__ */ jsxs("select", {
2933
+ className: "w-full mt-1 rounded-md border px-3 py-2 text-sm",
2934
+ value: typeof value === "string" ? value : "",
2935
+ onChange: (e) => onChange(e.target.value),
2936
+ required: subField.required,
2937
+ children: [/* @__PURE__ */ jsx("option", {
2938
+ value: "",
2939
+ children: "Select..."
2940
+ }), subField.options?.map((opt) => /* @__PURE__ */ jsx("option", {
2941
+ value: opt,
2942
+ children: opt
2943
+ }, opt))]
2944
+ })] });
2945
+ default: return /* @__PURE__ */ jsx(Input, {
2946
+ label: subField.label,
2947
+ value: typeof value === "string" ? value : "",
2948
+ onChange: (e) => onChange(e.target.value)
2949
+ });
2950
+ }
2951
+ }
2952
+
2667
2953
  //#endregion
2668
2954
  //#region src/lib/hooks.ts
2669
2955
  /**
@@ -7185,6 +7471,74 @@ function SaveButton({ isDirty, isSaving, className, disabled, ...props }) {
7185
7471
  });
7186
7472
  }
7187
7473
 
7474
+ //#endregion
7475
+ //#region src/components/SeoImageField.tsx
7476
+ /**
7477
+ * SEO OG Image field for the content editor.
7478
+ *
7479
+ * Renders an image picker (reusing MediaPickerModal) that stores the
7480
+ * selected image URL in `seo.image`. Designed to sit next to the
7481
+ * Featured Image field in a two-column grid.
7482
+ */
7483
+ function SeoImageField({ seo, onChange }) {
7484
+ const [pickerOpen, setPickerOpen] = React.useState(false);
7485
+ const imageUrl = seo?.image || null;
7486
+ const handleSelect = (item) => {
7487
+ onChange({ image: !item.provider || item.provider === "local" ? `/_emdash/api/media/file/${item.storageKey || item.id}` : item.url });
7488
+ };
7489
+ const handleRemove = () => {
7490
+ onChange({ image: null });
7491
+ };
7492
+ return /* @__PURE__ */ jsxs("div", { children: [
7493
+ /* @__PURE__ */ jsx(Label, { children: "OG Image" }),
7494
+ imageUrl ? /* @__PURE__ */ jsxs("div", {
7495
+ className: "mt-2 relative group",
7496
+ children: [/* @__PURE__ */ jsx("img", {
7497
+ src: imageUrl,
7498
+ alt: "",
7499
+ className: "max-h-48 rounded-lg border object-cover"
7500
+ }), /* @__PURE__ */ jsxs("div", {
7501
+ className: "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1",
7502
+ children: [/* @__PURE__ */ jsx(Button, {
7503
+ type: "button",
7504
+ size: "sm",
7505
+ variant: "secondary",
7506
+ onClick: () => setPickerOpen(true),
7507
+ children: "Change"
7508
+ }), /* @__PURE__ */ jsx(Button, {
7509
+ type: "button",
7510
+ shape: "square",
7511
+ variant: "destructive",
7512
+ className: "h-8 w-8",
7513
+ onClick: handleRemove,
7514
+ "aria-label": "Remove image",
7515
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
7516
+ })]
7517
+ })]
7518
+ }) : /* @__PURE__ */ jsx(Button, {
7519
+ type: "button",
7520
+ variant: "outline",
7521
+ className: "mt-2 w-full h-32 border-dashed",
7522
+ onClick: () => setPickerOpen(true),
7523
+ children: /* @__PURE__ */ jsxs("div", {
7524
+ className: "flex flex-col items-center gap-2 text-kumo-subtle",
7525
+ children: [/* @__PURE__ */ jsx(Image$1, { className: "h-8 w-8" }), /* @__PURE__ */ jsx("span", { children: "Select OG image" })]
7526
+ })
7527
+ }),
7528
+ /* @__PURE__ */ jsx("p", {
7529
+ className: "text-xs text-kumo-subtle mt-1",
7530
+ children: "Image shown when this page is shared on social media"
7531
+ }),
7532
+ /* @__PURE__ */ jsx(MediaPickerModal, {
7533
+ open: pickerOpen,
7534
+ onOpenChange: setPickerOpen,
7535
+ onSelect: handleSelect,
7536
+ mimeTypeFilter: "image/",
7537
+ title: "Select OG Image"
7538
+ })
7539
+ ] });
7540
+ }
7541
+
7188
7542
  //#endregion
7189
7543
  //#region src/components/SeoPanel.tsx
7190
7544
  /**
@@ -7509,7 +7863,7 @@ function formatScheduledDate(dateStr) {
7509
7863
  /**
7510
7864
  * Content editor with dynamic field rendering
7511
7865
  */
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 }) {
7866
+ 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
7867
  const [formData, setFormData] = React.useState(item?.data || {});
7514
7868
  const [slug, setSlug] = React.useState(item?.slug || "");
7515
7869
  const [slugTouched, setSlugTouched] = React.useState(!!item?.slug);
@@ -7624,15 +7978,16 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
7624
7978
  });
7625
7979
  };
7626
7980
  const [isLoadingPreview, setIsLoadingPreview] = React.useState(false);
7981
+ const urlPattern = manifest?.collections[collection]?.urlPattern;
7627
7982
  const handlePreview = async () => {
7628
7983
  if (!item?.id) return;
7629
7984
  setIsLoadingPreview(true);
7630
7985
  try {
7631
7986
  const result = await getPreviewUrl(collection, item.id);
7632
7987
  if (result?.url) window.open(result.url, "_blank", "noopener,noreferrer");
7633
- else window.open(`/${collection}/${slug || item.id}`, "_blank", "noopener,noreferrer");
7988
+ else window.open(contentUrl(collection, slug || item.id, urlPattern), "_blank", "noopener,noreferrer");
7634
7989
  } catch {
7635
- window.open(`/${collection}/${slug || item?.id}`, "_blank", "noopener,noreferrer");
7990
+ window.open(contentUrl(collection, slug || item?.id || "", urlPattern), "_blank", "noopener,noreferrer");
7636
7991
  } finally {
7637
7992
  setIsLoadingPreview(false);
7638
7993
  }
@@ -7743,7 +8098,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
7743
8098
  "aria-hidden": "true"
7744
8099
  })
7745
8100
  }),
7746
- !isNew && /* @__PURE__ */ jsx(Button, {
8101
+ !isNew && supportsPreview && /* @__PURE__ */ jsx(Button, {
7747
8102
  variant: "outline",
7748
8103
  type: "button",
7749
8104
  onClick: handlePreview,
@@ -7756,58 +8111,71 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
7756
8111
  isDirty,
7757
8112
  isSaving: isSaving || false
7758
8113
  }),
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, {
7762
- ...p,
8114
+ !isNew && /* @__PURE__ */ jsxs(Fragment, { children: [
8115
+ supportsDrafts && hasPendingChanges && onDiscardDraft && /* @__PURE__ */ jsxs(Dialog.Root, {
8116
+ disablePointerDismissal: true,
8117
+ children: [/* @__PURE__ */ jsx(Dialog.Trigger, { render: (p) => /* @__PURE__ */ jsx(Button, {
8118
+ ...p,
8119
+ type: "button",
8120
+ variant: "outline",
8121
+ size: "sm",
8122
+ icon: /* @__PURE__ */ jsx(X, {}),
8123
+ children: "Discard changes"
8124
+ }) }), /* @__PURE__ */ jsxs(Dialog, {
8125
+ className: "p-6",
8126
+ size: "sm",
8127
+ children: [
8128
+ /* @__PURE__ */ jsx(Dialog.Title, {
8129
+ className: "text-lg font-semibold",
8130
+ children: "Discard draft changes?"
8131
+ }),
8132
+ /* @__PURE__ */ jsx(Dialog.Description, {
8133
+ className: "text-kumo-subtle",
8134
+ children: "This will revert to the published version. Your draft changes will be lost."
8135
+ }),
8136
+ /* @__PURE__ */ jsxs("div", {
8137
+ className: "mt-6 flex justify-end gap-2",
8138
+ children: [/* @__PURE__ */ jsx(Dialog.Close, { render: (p) => /* @__PURE__ */ jsx(Button, {
8139
+ ...p,
8140
+ variant: "secondary",
8141
+ children: "Cancel"
8142
+ }) }), /* @__PURE__ */ jsx(Dialog.Close, { render: (p) => /* @__PURE__ */ jsx(Button, {
8143
+ ...p,
8144
+ variant: "destructive",
8145
+ onClick: onDiscardDraft,
8146
+ children: "Discard changes"
8147
+ }) })]
8148
+ })
8149
+ ]
8150
+ })]
8151
+ }),
8152
+ isLive ? /* @__PURE__ */ jsx(Fragment, { children: hasPendingChanges ? /* @__PURE__ */ jsx(Button, {
8153
+ type: "button",
8154
+ variant: "primary",
8155
+ onClick: onPublish,
8156
+ children: "Publish changes"
8157
+ }) : /* @__PURE__ */ jsx(Button, {
7763
8158
  type: "button",
7764
8159
  variant: "outline",
7765
- size: "sm",
7766
- icon: /* @__PURE__ */ jsx(X, {}),
7767
- children: "Discard changes"
7768
- }) }), /* @__PURE__ */ jsxs(Dialog, {
7769
- className: "p-6",
7770
- size: "sm",
7771
- children: [
7772
- /* @__PURE__ */ jsx(Dialog.Title, {
7773
- className: "text-lg font-semibold",
7774
- children: "Discard draft changes?"
7775
- }),
7776
- /* @__PURE__ */ jsx(Dialog.Description, {
7777
- className: "text-kumo-subtle",
7778
- children: "This will revert to the published version. Your draft changes will be lost."
7779
- }),
7780
- /* @__PURE__ */ jsxs("div", {
7781
- className: "mt-6 flex justify-end gap-2",
7782
- children: [/* @__PURE__ */ jsx(Dialog.Close, { render: (p) => /* @__PURE__ */ jsx(Button, {
7783
- ...p,
7784
- variant: "secondary",
7785
- children: "Cancel"
7786
- }) }), /* @__PURE__ */ jsx(Dialog.Close, { render: (p) => /* @__PURE__ */ jsx(Button, {
7787
- ...p,
7788
- variant: "destructive",
7789
- onClick: onDiscardDraft,
7790
- children: "Discard changes"
7791
- }) })]
7792
- })
7793
- ]
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
- })] })
8160
+ onClick: onUnpublish,
8161
+ children: "Unpublish"
8162
+ }) }) : /* @__PURE__ */ jsx(Button, {
8163
+ type: "button",
8164
+ variant: "secondary",
8165
+ onClick: onPublish,
8166
+ children: "Publish"
8167
+ }),
8168
+ isLive && item?.slug && /* @__PURE__ */ jsxs("a", {
8169
+ href: contentUrl(collection, item.slug, urlPattern),
8170
+ target: "_blank",
8171
+ rel: "noopener noreferrer",
8172
+ className: buttonVariants({ variant: "outline" }),
8173
+ children: [/* @__PURE__ */ jsx(ArrowSquareOut, {
8174
+ className: "mr-2 h-4 w-4",
8175
+ "aria-hidden": "true"
8176
+ }), "Live View"]
8177
+ })
8178
+ ] })
7811
8179
  ]
7812
8180
  })]
7813
8181
  }), /* @__PURE__ */ jsxs("div", {
@@ -7818,18 +8186,28 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
7818
8186
  className: cn("rounded-lg border bg-kumo-base p-6", isDistractionFree && "border-0 bg-transparent p-0"),
7819
8187
  children: /* @__PURE__ */ jsx("div", {
7820
8188
  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))
8189
+ children: Object.entries(fields).map(([name, field]) => {
8190
+ const fieldEl = /* @__PURE__ */ jsx(FieldRenderer, {
8191
+ name,
8192
+ field,
8193
+ value: formData[name],
8194
+ onChange: handleFieldChange,
8195
+ onEditorReady: field.kind === "portableText" ? setPortableTextEditor : void 0,
8196
+ minimal: isDistractionFree,
8197
+ pluginBlocks,
8198
+ onBlockSidebarOpen: field.kind === "portableText" ? handleBlockSidebarOpen : void 0,
8199
+ onBlockSidebarClose: field.kind === "portableText" ? handleBlockSidebarClose : void 0,
8200
+ manifest
8201
+ }, name);
8202
+ if (name === "featured_image" && field.kind === "image" && hasSeo && !isNew && onSeoChange) return /* @__PURE__ */ jsxs("div", {
8203
+ className: "grid grid-cols-1 gap-6 md:grid-cols-2",
8204
+ children: [/* @__PURE__ */ jsx("div", { children: fieldEl }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(SeoImageField, {
8205
+ seo: item?.seo,
8206
+ onChange: onSeoChange
8207
+ }) })]
8208
+ }, `${name}-with-seo`);
8209
+ return fieldEl;
8210
+ })
7833
8211
  })
7834
8212
  })
7835
8213
  }), /* @__PURE__ */ jsx("div", {
@@ -8204,6 +8582,25 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
8204
8582
  }, opt.value))
8205
8583
  });
8206
8584
  }
8585
+ case "multiSelect": {
8586
+ const selected = Array.isArray(value) ? value : [];
8587
+ return /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx(Label, {
8588
+ className: labelClass,
8589
+ children: label
8590
+ }), /* @__PURE__ */ jsx("div", {
8591
+ className: "mt-2 flex flex-wrap gap-x-4 gap-y-2",
8592
+ children: field.options?.map((opt) => {
8593
+ const isChecked = selected.includes(opt.value);
8594
+ return /* @__PURE__ */ jsx(Checkbox, {
8595
+ label: opt.label,
8596
+ checked: isChecked,
8597
+ onCheckedChange: (checked) => {
8598
+ handleChange(checked ? [...selected, opt.value] : selected.filter((v) => v !== opt.value));
8599
+ }
8600
+ }, opt.value);
8601
+ })
8602
+ })] });
8603
+ }
8207
8604
  case "datetime": return /* @__PURE__ */ jsx(Input, {
8208
8605
  label,
8209
8606
  id,
@@ -8214,10 +8611,25 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
8214
8611
  });
8215
8612
  case "image": return /* @__PURE__ */ jsx(ImageFieldRenderer, {
8216
8613
  label,
8614
+ 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
8615
  value: value != null && typeof value === "object" ? value : void 0,
8218
8616
  onChange: handleChange,
8219
8617
  required: field.required
8220
8618
  });
8619
+ case "repeater": {
8620
+ const validation = field.validation;
8621
+ const subFields = validation?.subFields ?? [];
8622
+ return /* @__PURE__ */ jsx(RepeaterField, {
8623
+ label,
8624
+ id,
8625
+ value,
8626
+ onChange: handleChange,
8627
+ required: field.required,
8628
+ subFields,
8629
+ minItems: typeof validation?.minItems === "number" ? validation.minItems : void 0,
8630
+ maxItems: typeof validation?.maxItems === "number" ? validation.maxItems : void 0
8631
+ });
8632
+ }
8221
8633
  default: return /* @__PURE__ */ jsx(Input, {
8222
8634
  label,
8223
8635
  id,
@@ -8227,7 +8639,7 @@ function FieldRenderer({ name, field, value, onChange, onEditorReady, minimal, p
8227
8639
  });
8228
8640
  }
8229
8641
  }
8230
- function ImageFieldRenderer({ label, value, onChange, required }) {
8642
+ function ImageFieldRenderer({ label, description, value, onChange, required }) {
8231
8643
  const [pickerOpen, setPickerOpen] = React.useState(false);
8232
8644
  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
8645
  const handleSelect = (item) => {
@@ -8291,6 +8703,10 @@ function ImageFieldRenderer({ label, value, onChange, required }) {
8291
8703
  mimeTypeFilter: "image/",
8292
8704
  title: `Select ${label}`
8293
8705
  }),
8706
+ description && /* @__PURE__ */ jsx("p", {
8707
+ className: "text-xs text-kumo-subtle mt-1",
8708
+ children: description
8709
+ }),
8294
8710
  required && !displayUrl && /* @__PURE__ */ jsx("p", {
8295
8711
  className: "text-sm text-kumo-danger mt-1",
8296
8712
  children: "This field is required"
@@ -8651,7 +9067,7 @@ function getItemTitle$1(item) {
8651
9067
  /**
8652
9068
  * Content list view with table display and trash tab
8653
9069
  */
8654
- function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n, activeLocale, onLocaleChange }) {
9070
+ function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n, activeLocale, onLocaleChange, urlPattern }) {
8655
9071
  const [activeTab, setActiveTab] = React.useState("all");
8656
9072
  const [searchQuery, setSearchQuery] = React.useState("");
8657
9073
  const [page, setPage] = React.useState(0);
@@ -8793,7 +9209,8 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
8793
9209
  collection,
8794
9210
  onDelete,
8795
9211
  onDuplicate,
8796
- showLocale: !!i18n
9212
+ showLocale: !!i18n,
9213
+ urlPattern
8797
9214
  }, item.id)) })]
8798
9215
  })
8799
9216
  }),
@@ -8897,7 +9314,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
8897
9314
  ]
8898
9315
  });
8899
9316
  }
8900
- function ContentListItem({ item, collection, onDelete, onDuplicate, showLocale }) {
9317
+ function ContentListItem({ item, collection, onDelete, onDuplicate, showLocale, urlPattern }) {
8901
9318
  const title = getItemTitle$1(item);
8902
9319
  const date = new Date(item.updatedAt || item.createdAt);
8903
9320
  return /* @__PURE__ */ jsxs("tr", {
@@ -8938,6 +9355,20 @@ function ContentListItem({ item, collection, onDelete, onDuplicate, showLocale }
8938
9355
  children: /* @__PURE__ */ jsxs("div", {
8939
9356
  className: "flex items-center justify-end space-x-1",
8940
9357
  children: [
9358
+ item.status === "published" && item.slug && /* @__PURE__ */ jsx("a", {
9359
+ href: contentUrl(collection, item.slug, urlPattern),
9360
+ target: "_blank",
9361
+ rel: "noopener noreferrer",
9362
+ "aria-label": `View published ${title}`,
9363
+ className: buttonVariants({
9364
+ variant: "ghost",
9365
+ shape: "square"
9366
+ }),
9367
+ children: /* @__PURE__ */ jsx(ArrowSquareOut, {
9368
+ className: "h-4 w-4",
9369
+ "aria-hidden": "true"
9370
+ })
9371
+ }),
8941
9372
  /* @__PURE__ */ jsx(Link$1, {
8942
9373
  to: "/content/$collection/$id",
8943
9374
  params: {
@@ -9192,6 +9623,12 @@ const FIELD_TYPES = [
9192
9623
  label: "Slug",
9193
9624
  description: "URL-friendly identifier",
9194
9625
  icon: Link$2
9626
+ },
9627
+ {
9628
+ type: "repeater",
9629
+ label: "Repeater",
9630
+ description: "Repeating group of fields",
9631
+ icon: Rows
9195
9632
  }
9196
9633
  ];
9197
9634
  function getInitialFormState(field) {
@@ -9208,7 +9645,10 @@ function getInitialFormState(field) {
9208
9645
  min: field.validation?.min?.toString() ?? "",
9209
9646
  max: field.validation?.max?.toString() ?? "",
9210
9647
  pattern: field.validation?.pattern ?? "",
9211
- options: field.validation?.options?.join("\n") ?? ""
9648
+ options: field.validation?.options?.join("\n") ?? "",
9649
+ subFields: field.validation?.subFields ? field.validation.subFields : [],
9650
+ minItems: field.validation?.minItems?.toString() ?? "",
9651
+ maxItems: field.validation?.maxItems?.toString() ?? ""
9212
9652
  };
9213
9653
  return {
9214
9654
  step: "type",
@@ -9223,7 +9663,10 @@ function getInitialFormState(field) {
9223
9663
  min: "",
9224
9664
  max: "",
9225
9665
  pattern: "",
9226
- options: ""
9666
+ options: "",
9667
+ subFields: [],
9668
+ minItems: "",
9669
+ maxItems: ""
9227
9670
  };
9228
9671
  }
9229
9672
  /**
@@ -9267,6 +9710,16 @@ function FieldEditor({ open, onOpenChange, field, onSave, isSaving }) {
9267
9710
  const optionList = options.split("\n").map((o) => o.trim()).filter(Boolean);
9268
9711
  if (optionList.length > 0) validation.options = optionList;
9269
9712
  }
9713
+ if (selectedType === "repeater") {
9714
+ if (formState.subFields.length > 0) validation.subFields = formState.subFields.map((sf) => ({
9715
+ slug: sf.slug,
9716
+ type: sf.type,
9717
+ label: sf.label,
9718
+ required: sf.required || void 0
9719
+ }));
9720
+ if (formState.minItems) validation.minItems = parseInt(formState.minItems, 10);
9721
+ if (formState.maxItems) validation.maxItems = parseInt(formState.maxItems, 10);
9722
+ }
9270
9723
  onSave({
9271
9724
  slug,
9272
9725
  label,
@@ -9466,11 +9919,156 @@ function FieldEditor({ open, onOpenChange, field, onSave, isSaving }) {
9466
9919
  onChange: (e) => setField("options", e.target.value),
9467
9920
  placeholder: "Option 1\nOption 2\nOption 3",
9468
9921
  rows: 5
9922
+ }),
9923
+ selectedType === "repeater" && /* @__PURE__ */ jsxs("div", {
9924
+ className: "space-y-4",
9925
+ children: [
9926
+ /* @__PURE__ */ jsxs("div", {
9927
+ className: "flex items-center justify-between",
9928
+ children: [/* @__PURE__ */ jsx("h4", {
9929
+ className: "font-medium text-sm",
9930
+ children: "Sub-Fields"
9931
+ }), /* @__PURE__ */ jsx(Button, {
9932
+ variant: "outline",
9933
+ size: "sm",
9934
+ icon: /* @__PURE__ */ jsx(Plus, {}),
9935
+ onClick: () => setFormState((prev) => ({
9936
+ ...prev,
9937
+ subFields: [...prev.subFields, {
9938
+ slug: "",
9939
+ type: "string",
9940
+ label: "",
9941
+ required: false
9942
+ }]
9943
+ })),
9944
+ children: "Add Sub-Field"
9945
+ })]
9946
+ }),
9947
+ formState.subFields.length === 0 && /* @__PURE__ */ jsx("p", {
9948
+ className: "text-sm text-kumo-subtle text-center py-4",
9949
+ children: "Add at least one sub-field to define the repeater structure."
9950
+ }),
9951
+ formState.subFields.map((sf, i) => /* @__PURE__ */ jsxs("div", {
9952
+ className: "flex gap-2 items-start border rounded-lg p-3",
9953
+ children: [/* @__PURE__ */ jsxs("div", {
9954
+ className: "flex-1 space-y-2",
9955
+ children: [/* @__PURE__ */ jsxs("div", {
9956
+ className: "grid grid-cols-2 gap-2",
9957
+ children: [/* @__PURE__ */ jsx(Input, {
9958
+ label: "Label",
9959
+ value: sf.label,
9960
+ onChange: (e) => {
9961
+ const updated = [...formState.subFields];
9962
+ updated[i] = {
9963
+ ...sf,
9964
+ label: e.target.value,
9965
+ slug: e.target.value.toLowerCase().replace(SLUG_INVALID_CHARS_REGEX, "_").replace(SLUG_LEADING_TRAILING_REGEX, "")
9966
+ };
9967
+ setFormState((prev) => ({
9968
+ ...prev,
9969
+ subFields: updated
9970
+ }));
9971
+ },
9972
+ placeholder: "Field label"
9973
+ }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
9974
+ className: "text-sm font-medium",
9975
+ children: "Type"
9976
+ }), /* @__PURE__ */ jsxs("select", {
9977
+ className: "w-full mt-1 rounded-md border px-3 py-2 text-sm",
9978
+ value: sf.type,
9979
+ onChange: (e) => {
9980
+ const updated = [...formState.subFields];
9981
+ updated[i] = {
9982
+ ...sf,
9983
+ type: e.target.value
9984
+ };
9985
+ setFormState((prev) => ({
9986
+ ...prev,
9987
+ subFields: updated
9988
+ }));
9989
+ },
9990
+ children: [
9991
+ /* @__PURE__ */ jsx("option", {
9992
+ value: "string",
9993
+ children: "Short Text"
9994
+ }),
9995
+ /* @__PURE__ */ jsx("option", {
9996
+ value: "text",
9997
+ children: "Long Text"
9998
+ }),
9999
+ /* @__PURE__ */ jsx("option", {
10000
+ value: "number",
10001
+ children: "Number"
10002
+ }),
10003
+ /* @__PURE__ */ jsx("option", {
10004
+ value: "integer",
10005
+ children: "Integer"
10006
+ }),
10007
+ /* @__PURE__ */ jsx("option", {
10008
+ value: "boolean",
10009
+ children: "Boolean"
10010
+ }),
10011
+ /* @__PURE__ */ jsx("option", {
10012
+ value: "datetime",
10013
+ children: "Date & Time"
10014
+ }),
10015
+ /* @__PURE__ */ jsx("option", {
10016
+ value: "select",
10017
+ children: "Select"
10018
+ })
10019
+ ]
10020
+ })] })]
10021
+ }), /* @__PURE__ */ jsxs("label", {
10022
+ className: "flex items-center gap-2 text-sm",
10023
+ children: [/* @__PURE__ */ jsx("input", {
10024
+ type: "checkbox",
10025
+ checked: sf.required,
10026
+ onChange: (e) => {
10027
+ const updated = [...formState.subFields];
10028
+ updated[i] = {
10029
+ ...sf,
10030
+ required: e.target.checked
10031
+ };
10032
+ setFormState((prev) => ({
10033
+ ...prev,
10034
+ subFields: updated
10035
+ }));
10036
+ }
10037
+ }), "Required"]
10038
+ })]
10039
+ }), /* @__PURE__ */ jsx(Button, {
10040
+ variant: "ghost",
10041
+ shape: "square",
10042
+ onClick: () => setFormState((prev) => ({
10043
+ ...prev,
10044
+ subFields: prev.subFields.filter((_, j) => j !== i)
10045
+ })),
10046
+ "aria-label": "Remove sub-field",
10047
+ children: /* @__PURE__ */ jsx(Trash, { className: "h-4 w-4 text-kumo-danger" })
10048
+ })]
10049
+ }, i)),
10050
+ /* @__PURE__ */ jsxs("div", {
10051
+ className: "grid grid-cols-2 gap-4",
10052
+ children: [/* @__PURE__ */ jsx(Input, {
10053
+ label: "Min Items",
10054
+ type: "number",
10055
+ value: formState.minItems,
10056
+ onChange: (e) => setField("minItems", e.target.value),
10057
+ placeholder: "0"
10058
+ }), /* @__PURE__ */ jsx(Input, {
10059
+ label: "Max Items",
10060
+ type: "number",
10061
+ value: formState.maxItems,
10062
+ onChange: (e) => setField("maxItems", e.target.value),
10063
+ placeholder: "No limit"
10064
+ })]
10065
+ })
10066
+ ]
9469
10067
  })
9470
10068
  ]
9471
10069
  }),
9472
10070
  step === "config" && /* @__PURE__ */ jsxs("div", {
9473
- className: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
10071
+ className: "flex flex-col-reverse gap-2 py-2 sm:flex-row sm:justify-end sm:space-x-2",
9474
10072
  children: [/* @__PURE__ */ jsx(Button, {
9475
10073
  variant: "outline",
9476
10074
  onClick: () => onOpenChange(false),
@@ -9478,7 +10076,7 @@ function FieldEditor({ open, onOpenChange, field, onSave, isSaving }) {
9478
10076
  children: "Cancel"
9479
10077
  }), /* @__PURE__ */ jsx(Button, {
9480
10078
  onClick: handleSave,
9481
- disabled: !slug || !label || isSaving,
10079
+ disabled: !slug || !label || isSaving || selectedType === "repeater" && formState.subFields.length === 0,
9482
10080
  children: isSaving ? "Saving..." : field ? "Update Field" : "Add Field"
9483
10081
  })]
9484
10082
  })
@@ -9558,7 +10156,7 @@ const SYSTEM_FIELDS = [
9558
10156
  /**
9559
10157
  * Content Type editor for creating/editing collections
9560
10158
  */
9561
- function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, onUpdateField, onDeleteField, onReorderFields: _onReorderFields }) {
10159
+ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, onUpdateField, onDeleteField, onReorderFields }) {
9562
10160
  useNavigate$1();
9563
10161
  const [slug, setSlug] = React.useState(collection?.slug ?? "");
9564
10162
  const [label, setLabel] = React.useState(collection?.label ?? "");
@@ -9651,6 +10249,16 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
9651
10249
  };
9652
10250
  const isFromCode = collection?.source === "code";
9653
10251
  const fields = collection?.fields ?? [];
10252
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
10253
+ const handleDragEnd = (event) => {
10254
+ const { active, over } = event;
10255
+ if (!over || active.id === over.id) return;
10256
+ const oldIndex = fields.findIndex((f) => f.id === active.id);
10257
+ const newIndex = fields.findIndex((f) => f.id === over.id);
10258
+ if (oldIndex === -1 || newIndex === -1) return;
10259
+ const reordered = arrayMove(fields, oldIndex, newIndex);
10260
+ onReorderFields?.(reordered.map((f) => f.slug));
10261
+ };
9654
10262
  return /* @__PURE__ */ jsxs("div", {
9655
10263
  className: "space-y-6",
9656
10264
  children: [
@@ -9933,14 +10541,23 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
9933
10541
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
9934
10542
  className: "px-4 py-2 text-xs font-medium text-kumo-subtle uppercase tracking-wider border-b",
9935
10543
  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))
10544
+ }), /* @__PURE__ */ jsx(DndContext, {
10545
+ sensors,
10546
+ collisionDetection: closestCenter,
10547
+ onDragEnd: handleDragEnd,
10548
+ children: /* @__PURE__ */ jsx(SortableContext, {
10549
+ items: fields.map((f) => f.id),
10550
+ strategy: verticalListSortingStrategy,
10551
+ children: /* @__PURE__ */ jsx("div", {
10552
+ className: "divide-y",
10553
+ children: fields.map((field) => /* @__PURE__ */ jsx(FieldRow, {
10554
+ field,
10555
+ isFromCode,
10556
+ onEdit: () => handleEditField(field),
10557
+ onDelete: () => setDeleteFieldTarget(field)
10558
+ }, field.id))
10559
+ })
10560
+ })
9944
10561
  })] })
9945
10562
  ]
9946
10563
  })
@@ -9973,10 +10590,25 @@ function ContentTypeEditor({ collection, isNew, isSaving, onSave, onAddField, on
9973
10590
  });
9974
10591
  }
9975
10592
  function FieldRow({ field, isFromCode, onEdit, onDelete }) {
10593
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
10594
+ id: field.id,
10595
+ disabled: isFromCode
10596
+ });
9976
10597
  return /* @__PURE__ */ jsxs("div", {
9977
- className: "flex items-center px-4 py-3 hover:bg-kumo-tint/25",
10598
+ ref: setNodeRef,
10599
+ style: {
10600
+ transform: CSS.Transform.toString(transform),
10601
+ transition
10602
+ },
10603
+ className: cn("flex items-center px-4 py-3 hover:bg-kumo-tint/25", isDragging && "opacity-50"),
9978
10604
  children: [
9979
- !isFromCode && /* @__PURE__ */ jsx(DotsSixVertical, { className: "h-5 w-5 mr-3 text-kumo-subtle cursor-grab" }),
10605
+ !isFromCode && /* @__PURE__ */ jsx("button", {
10606
+ ...attributes,
10607
+ ...listeners,
10608
+ className: "cursor-grab active:cursor-grabbing mr-3",
10609
+ "aria-label": `Drag to reorder ${field.label}`,
10610
+ children: /* @__PURE__ */ jsx(DotsSixVertical, { className: "h-5 w-5 text-kumo-subtle" })
10611
+ }),
9980
10612
  /* @__PURE__ */ jsxs("div", {
9981
10613
  className: "flex-1 min-w-0",
9982
10614
  children: [/* @__PURE__ */ jsxs("div", {
@@ -10875,43 +11507,24 @@ function CheckIcon({ className }) {
10875
11507
  }
10876
11508
 
10877
11509
  //#endregion
10878
- //#region src/lib/url.ts
11510
+ //#region src/lib/webauthn-environment.ts
10879
11511
  /**
10880
- * Shared URL validation and transformation utilities
10881
- */
10882
- const DEFAULT_REDIRECT = "/_emdash/admin";
10883
- /**
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:`.
11512
+ * WebAuthn is only available in a browser "secure context": HTTPS, or special-cased
11513
+ * loopback hosts such as `http://localhost` / `http://127.0.0.1`.
10889
11514
  *
10890
- * Returns the default admin URL when the input is unsafe.
11515
+ * An origin like `http://emdash.local:8081` resolves to 127.0.0.1 but is still
11516
+ * **not** a secure context, so `PublicKeyCredential` is hidden — the same symptom
11517
+ * as an unsupported browser.
10891
11518
  */
10892
- function sanitizeRedirectUrl(raw) {
10893
- if (raw.startsWith("/") && !raw.startsWith("//") && !raw.includes("\\")) return raw;
10894
- return DEFAULT_REDIRECT;
11519
+ function isWebAuthnSecureContext() {
11520
+ return typeof window !== "undefined" && window.isSecureContext;
10895
11521
  }
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);
11522
+ function isPublicKeyCredentialConstructorAvailable() {
11523
+ return typeof window !== "undefined" && window.PublicKeyCredential !== void 0 && typeof window.PublicKeyCredential === "function";
10901
11524
  }
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
- }
11525
+ /** True when the page can use `navigator.credentials` for passkeys. */
11526
+ function isPasskeyEnvironmentUsable() {
11527
+ return isWebAuthnSecureContext() && isPublicKeyCredentialConstructorAvailable();
10915
11528
  }
10916
11529
 
10917
11530
  //#endregion
@@ -10933,16 +11546,10 @@ const BASE64URL_UNDERSCORE_REGEX$1 = /_/g;
10933
11546
  const BASE64_PLUS_REGEX$1 = /\+/g;
10934
11547
  const BASE64_SLASH_REGEX$1 = /\//g;
10935
11548
  /**
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
11549
  * Check if conditional mediation (autofill) is supported
10943
11550
  */
10944
11551
  async function isConditionalMediationSupported() {
10945
- if (!isWebAuthnSupported$1()) return false;
11552
+ if (!isPasskeyEnvironmentUsable()) return false;
10946
11553
  try {
10947
11554
  return await PublicKeyCredential.isConditionalMediationAvailable?.() ?? false;
10948
11555
  } catch {
@@ -10976,7 +11583,8 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
10976
11583
  const [state, setState] = React.useState({ status: "idle" });
10977
11584
  const [email, setEmail] = React.useState("");
10978
11585
  const [supportsConditional, setSupportsConditional] = React.useState(false);
10979
- const isSupported = React.useMemo(() => isWebAuthnSupported$1(), []);
11586
+ const isSupported = React.useMemo(() => isPasskeyEnvironmentUsable(), []);
11587
+ const insecureContext = React.useMemo(() => typeof window !== "undefined" && !isWebAuthnSecureContext(), []);
10980
11588
  React.useEffect(() => {
10981
11589
  isConditionalMediationSupported().then(setSupportsConditional);
10982
11590
  }, []);
@@ -10984,7 +11592,7 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
10984
11592
  if (!isSupported) {
10985
11593
  setState({
10986
11594
  status: "error",
10987
- message: "WebAuthn is not supported in this browser"
11595
+ 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
11596
  });
10989
11597
  return;
10990
11598
  }
@@ -11017,7 +11625,15 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
11017
11625
  ...useConditional && supportsConditional ? { mediation: "conditional" } : {}
11018
11626
  };
11019
11627
  const rawCredential = await navigator.credentials.get(credentialOptions);
11020
- if (!rawCredential) throw new Error("No credential returned from authenticator");
11628
+ if (!rawCredential) {
11629
+ const message = "No credential returned from authenticator";
11630
+ setState({
11631
+ status: "error",
11632
+ message
11633
+ });
11634
+ onError?.(new Error(message));
11635
+ return;
11636
+ }
11021
11637
  setState({
11022
11638
  status: "loading",
11023
11639
  message: "Verifying..."
@@ -11073,6 +11689,7 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
11073
11689
  }
11074
11690
  }, [
11075
11691
  isSupported,
11692
+ insecureContext,
11076
11693
  optionsEndpoint,
11077
11694
  verifyEndpoint,
11078
11695
  email,
@@ -11084,10 +11701,34 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
11084
11701
  className: "rounded-lg border border-kumo-danger/50 bg-kumo-danger/10 p-4",
11085
11702
  children: [/* @__PURE__ */ jsx("h3", {
11086
11703
  className: "font-medium text-kumo-danger",
11087
- children: "Passkeys Not Supported"
11704
+ children: "Passkeys Not Available Here"
11088
11705
  }), /* @__PURE__ */ jsx("p", {
11089
11706
  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."
11707
+ children: insecureContext ? /* @__PURE__ */ jsxs(Fragment, { children: [
11708
+ "Passkeys require a ",
11709
+ /* @__PURE__ */ jsx("strong", {
11710
+ className: "text-kumo-default",
11711
+ children: "secure context"
11712
+ }),
11713
+ ": use",
11714
+ " ",
11715
+ /* @__PURE__ */ jsx("strong", {
11716
+ className: "text-kumo-default",
11717
+ children: "HTTPS"
11718
+ }),
11719
+ ", or open the admin at",
11720
+ " ",
11721
+ /* @__PURE__ */ jsx("strong", {
11722
+ className: "text-kumo-default",
11723
+ children: "http://localhost"
11724
+ }),
11725
+ " (with your dev port). Plain ",
11726
+ /* @__PURE__ */ jsx("code", {
11727
+ className: "text-xs",
11728
+ children: "http://"
11729
+ }),
11730
+ " on a custom hostname is not treated as secure, even on loopback."
11731
+ ] }) : /* @__PURE__ */ jsx(Fragment, { children: "Your browser doesn't support passkeys. Please use a modern browser like Chrome, Safari, Firefox, or Edge." })
11091
11732
  })]
11092
11733
  });
11093
11734
  return /* @__PURE__ */ jsxs("div", {
@@ -11125,6 +11766,258 @@ function PasskeyLogin({ optionsEndpoint, verifyEndpoint, onSuccess, onError, sho
11125
11766
  });
11126
11767
  }
11127
11768
 
11769
+ //#endregion
11770
+ //#region src/components/Logo.tsx
11771
+ /**
11772
+ * EmDash icon mark — the rounded-rect em dash symbol.
11773
+ * Used in the sidebar brand and as favicon.
11774
+ */
11775
+ function LogoIcon(props) {
11776
+ return /* @__PURE__ */ jsxs("svg", {
11777
+ viewBox: "0 0 75 75",
11778
+ fill: "none",
11779
+ xmlns: "http://www.w3.org/2000/svg",
11780
+ ...props,
11781
+ children: [
11782
+ /* @__PURE__ */ jsx("rect", {
11783
+ x: "3",
11784
+ y: "3",
11785
+ width: "69",
11786
+ height: "69",
11787
+ rx: "10.518",
11788
+ stroke: "url(#emdash-icon-border)",
11789
+ strokeWidth: "6"
11790
+ }),
11791
+ /* @__PURE__ */ jsx("rect", {
11792
+ x: "18",
11793
+ y: "34",
11794
+ width: "39.3661",
11795
+ height: "6.56101",
11796
+ fill: "url(#emdash-icon-dash)"
11797
+ }),
11798
+ /* @__PURE__ */ jsxs("defs", { children: [/* @__PURE__ */ jsxs("linearGradient", {
11799
+ id: "emdash-icon-border",
11800
+ x1: "-42.9996",
11801
+ y1: "124",
11802
+ x2: "92.4233",
11803
+ y2: "-41.7456",
11804
+ gradientUnits: "userSpaceOnUse",
11805
+ children: [
11806
+ /* @__PURE__ */ jsx("stop", { stopColor: "#0F006B" }),
11807
+ /* @__PURE__ */ jsx("stop", {
11808
+ offset: "0.0833",
11809
+ stopColor: "#281A81"
11810
+ }),
11811
+ /* @__PURE__ */ jsx("stop", {
11812
+ offset: "0.1667",
11813
+ stopColor: "#5D0C83"
11814
+ }),
11815
+ /* @__PURE__ */ jsx("stop", {
11816
+ offset: "0.25",
11817
+ stopColor: "#911475"
11818
+ }),
11819
+ /* @__PURE__ */ jsx("stop", {
11820
+ offset: "0.3333",
11821
+ stopColor: "#CE2F55"
11822
+ }),
11823
+ /* @__PURE__ */ jsx("stop", {
11824
+ offset: "0.4167",
11825
+ stopColor: "#FF6633"
11826
+ }),
11827
+ /* @__PURE__ */ jsx("stop", {
11828
+ offset: "0.5",
11829
+ stopColor: "#F6821F"
11830
+ }),
11831
+ /* @__PURE__ */ jsx("stop", {
11832
+ offset: "0.5833",
11833
+ stopColor: "#FBAD41"
11834
+ }),
11835
+ /* @__PURE__ */ jsx("stop", {
11836
+ offset: "0.6667",
11837
+ stopColor: "#FFCD89"
11838
+ }),
11839
+ /* @__PURE__ */ jsx("stop", {
11840
+ offset: "0.75",
11841
+ stopColor: "#FFE9CB"
11842
+ }),
11843
+ /* @__PURE__ */ jsx("stop", {
11844
+ offset: "0.8333",
11845
+ stopColor: "#FFF7EC"
11846
+ }),
11847
+ /* @__PURE__ */ jsx("stop", {
11848
+ offset: "0.9167",
11849
+ stopColor: "#FFF8EE"
11850
+ }),
11851
+ /* @__PURE__ */ jsx("stop", {
11852
+ offset: "1",
11853
+ stopColor: "white"
11854
+ })
11855
+ ]
11856
+ }), /* @__PURE__ */ jsxs("linearGradient", {
11857
+ id: "emdash-icon-dash",
11858
+ x1: "91.4992",
11859
+ y1: "27.4982",
11860
+ x2: "28.1217",
11861
+ y2: "54.1775",
11862
+ gradientUnits: "userSpaceOnUse",
11863
+ children: [
11864
+ /* @__PURE__ */ jsx("stop", { stopColor: "white" }),
11865
+ /* @__PURE__ */ jsx("stop", {
11866
+ offset: "0.1293",
11867
+ stopColor: "#FFF8EE"
11868
+ }),
11869
+ /* @__PURE__ */ jsx("stop", {
11870
+ offset: "0.6171",
11871
+ stopColor: "#FBAD41"
11872
+ }),
11873
+ /* @__PURE__ */ jsx("stop", {
11874
+ offset: "0.848",
11875
+ stopColor: "#F6821F"
11876
+ }),
11877
+ /* @__PURE__ */ jsx("stop", {
11878
+ offset: "1",
11879
+ stopColor: "#FF6633"
11880
+ })
11881
+ ]
11882
+ })] })
11883
+ ]
11884
+ });
11885
+ }
11886
+ /**
11887
+ * Full logo lockup — icon + "EmDash" wordmark.
11888
+ * Renders both dark-text and light-text variants, switching via CSS `light-dark()`.
11889
+ */
11890
+ function LogoLockup({ className, ...props }) {
11891
+ return /* @__PURE__ */ jsxs("svg", {
11892
+ viewBox: "0 0 471 118",
11893
+ fill: "none",
11894
+ xmlns: "http://www.w3.org/2000/svg",
11895
+ className,
11896
+ role: "img",
11897
+ "aria-label": "EmDash",
11898
+ ...props,
11899
+ children: [
11900
+ /* @__PURE__ */ jsx("path", {
11901
+ 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",
11902
+ fill: "url(#emdash-lockup-icon)"
11903
+ }),
11904
+ /* @__PURE__ */ jsx("path", {
11905
+ d: "M28.6699 53.366H90.4746V63.6668H28.6699V53.366Z",
11906
+ fill: "url(#emdash-lockup-dash)"
11907
+ }),
11908
+ /* @__PURE__ */ jsx("path", {
11909
+ d: "M154.762 90V27.4834H194.447V35.8449H164.467V54.0844H192.844V62.2293H164.467V81.6385H194.447V90H154.762Z",
11910
+ fill: "currentColor"
11911
+ }),
11912
+ /* @__PURE__ */ jsx("path", {
11913
+ 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",
11914
+ fill: "currentColor"
11915
+ }),
11916
+ /* @__PURE__ */ jsx("path", {
11917
+ 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",
11918
+ fill: "currentColor"
11919
+ }),
11920
+ /* @__PURE__ */ jsx("path", {
11921
+ 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",
11922
+ fill: "currentColor"
11923
+ }),
11924
+ /* @__PURE__ */ jsx("path", {
11925
+ 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",
11926
+ fill: "currentColor"
11927
+ }),
11928
+ /* @__PURE__ */ jsx("path", {
11929
+ 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",
11930
+ fill: "currentColor"
11931
+ }),
11932
+ /* @__PURE__ */ jsxs("defs", { children: [/* @__PURE__ */ jsxs("linearGradient", {
11933
+ id: "emdash-lockup-icon",
11934
+ x1: "-67.1002",
11935
+ y1: "194.666",
11936
+ x2: "145.514",
11937
+ y2: "-65.5554",
11938
+ gradientUnits: "userSpaceOnUse",
11939
+ children: [
11940
+ /* @__PURE__ */ jsx("stop", { stopColor: "#0F006B" }),
11941
+ /* @__PURE__ */ jsx("stop", {
11942
+ offset: "0.0833",
11943
+ stopColor: "#281A81"
11944
+ }),
11945
+ /* @__PURE__ */ jsx("stop", {
11946
+ offset: "0.1667",
11947
+ stopColor: "#5D0C83"
11948
+ }),
11949
+ /* @__PURE__ */ jsx("stop", {
11950
+ offset: "0.25",
11951
+ stopColor: "#911475"
11952
+ }),
11953
+ /* @__PURE__ */ jsx("stop", {
11954
+ offset: "0.3333",
11955
+ stopColor: "#CE2F55"
11956
+ }),
11957
+ /* @__PURE__ */ jsx("stop", {
11958
+ offset: "0.4167",
11959
+ stopColor: "#FF6633"
11960
+ }),
11961
+ /* @__PURE__ */ jsx("stop", {
11962
+ offset: "0.5",
11963
+ stopColor: "#F6821F"
11964
+ }),
11965
+ /* @__PURE__ */ jsx("stop", {
11966
+ offset: "0.5833",
11967
+ stopColor: "#FBAD41"
11968
+ }),
11969
+ /* @__PURE__ */ jsx("stop", {
11970
+ offset: "0.6667",
11971
+ stopColor: "#FFCD89"
11972
+ }),
11973
+ /* @__PURE__ */ jsx("stop", {
11974
+ offset: "0.75",
11975
+ stopColor: "#FFE9CB"
11976
+ }),
11977
+ /* @__PURE__ */ jsx("stop", {
11978
+ offset: "0.8333",
11979
+ stopColor: "#FFF7EC"
11980
+ }),
11981
+ /* @__PURE__ */ jsx("stop", {
11982
+ offset: "0.9167",
11983
+ stopColor: "#FFF8EE"
11984
+ }),
11985
+ /* @__PURE__ */ jsx("stop", {
11986
+ offset: "1",
11987
+ stopColor: "white"
11988
+ })
11989
+ ]
11990
+ }), /* @__PURE__ */ jsxs("linearGradient", {
11991
+ id: "emdash-lockup-dash",
11992
+ x1: "144.064",
11993
+ y1: "43.1581",
11994
+ x2: "44.5609",
11995
+ y2: "85.0447",
11996
+ gradientUnits: "userSpaceOnUse",
11997
+ children: [
11998
+ /* @__PURE__ */ jsx("stop", { stopColor: "white" }),
11999
+ /* @__PURE__ */ jsx("stop", {
12000
+ offset: "0.1293",
12001
+ stopColor: "#FFF8EE"
12002
+ }),
12003
+ /* @__PURE__ */ jsx("stop", {
12004
+ offset: "0.6171",
12005
+ stopColor: "#FBAD41"
12006
+ }),
12007
+ /* @__PURE__ */ jsx("stop", {
12008
+ offset: "0.848",
12009
+ stopColor: "#F6821F"
12010
+ }),
12011
+ /* @__PURE__ */ jsx("stop", {
12012
+ offset: "1",
12013
+ stopColor: "#FF6633"
12014
+ })
12015
+ ]
12016
+ })] })
12017
+ ]
12018
+ });
12019
+ }
12020
+
11128
12021
  //#endregion
11129
12022
  //#region src/components/LoginPage.tsx
11130
12023
  /**
@@ -11323,10 +12216,7 @@ function LoginPage({ redirectUrl = "/_emdash/admin" }) {
11323
12216
  className: "min-h-screen flex items-center justify-center bg-kumo-base p-4",
11324
12217
  children: /* @__PURE__ */ jsxs("div", {
11325
12218
  className: "text-center",
11326
- children: [/* @__PURE__ */ jsx("div", {
11327
- className: "text-4xl font-bold mb-4",
11328
- children: "— EmDash"
11329
- }), /* @__PURE__ */ jsx(Loader, {})]
12219
+ children: [/* @__PURE__ */ jsx(LogoLockup, { className: "h-10 mx-auto mb-4" }), /* @__PURE__ */ jsx(Loader, {})]
11330
12220
  })
11331
12221
  });
11332
12222
  return /* @__PURE__ */ jsx("div", {
@@ -11336,10 +12226,7 @@ function LoginPage({ redirectUrl = "/_emdash/admin" }) {
11336
12226
  children: [
11337
12227
  /* @__PURE__ */ jsxs("div", {
11338
12228
  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", {
12229
+ children: [/* @__PURE__ */ jsx(LogoLockup, { className: "h-10 mx-auto mb-2" }), /* @__PURE__ */ jsxs("h1", {
11343
12230
  className: "text-2xl font-semibold text-kumo-default",
11344
12231
  children: [method === "passkey" && "Sign in to your site", method === "magic-link" && "Sign in with email"]
11345
12232
  })]
@@ -15768,12 +16655,24 @@ function AllowedDomainsSettings() {
15768
16655
  const handleDelete = () => {
15769
16656
  if (deletingDomain) deleteMutation.mutate(deletingDomain);
15770
16657
  };
15771
- if (manifestLoading || isLoading) return /* @__PURE__ */ jsxs("div", {
15772
- className: "space-y-6",
15773
- children: [/* @__PURE__ */ jsx("h1", {
16658
+ const settingsHeader = /* @__PURE__ */ jsxs("div", {
16659
+ className: "flex items-center gap-3",
16660
+ children: [/* @__PURE__ */ jsx(Link$1, {
16661
+ to: "/settings",
16662
+ children: /* @__PURE__ */ jsx(Button, {
16663
+ variant: "ghost",
16664
+ shape: "square",
16665
+ "aria-label": "Back to settings",
16666
+ children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
16667
+ })
16668
+ }), /* @__PURE__ */ jsx("h1", {
15774
16669
  className: "text-2xl font-bold",
15775
16670
  children: "Self-Signup Domains"
15776
- }), /* @__PURE__ */ jsx("div", {
16671
+ })]
16672
+ });
16673
+ if (manifestLoading || isLoading) return /* @__PURE__ */ jsxs("div", {
16674
+ className: "space-y-6",
16675
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
15777
16676
  className: "rounded-lg border bg-kumo-base p-6",
15778
16677
  children: /* @__PURE__ */ jsx("p", {
15779
16678
  className: "text-kumo-subtle",
@@ -15783,42 +16682,28 @@ function AllowedDomainsSettings() {
15783
16682
  });
15784
16683
  if (isExternalAuth) return /* @__PURE__ */ jsxs("div", {
15785
16684
  className: "space-y-6",
15786
- children: [/* @__PURE__ */ jsx("h1", {
15787
- className: "text-2xl font-bold",
15788
- children: "Self-Signup Domains"
15789
- }), /* @__PURE__ */ jsx("div", {
16685
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
15790
16686
  className: "rounded-lg border bg-kumo-base p-6",
15791
16687
  children: /* @__PURE__ */ jsxs("div", {
15792
16688
  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", {
16689
+ children: [/* @__PURE__ */ jsx(Info, { className: "h-5 w-5 text-kumo-subtle mt-0.5 flex-shrink-0" }), /* @__PURE__ */ jsx("div", {
15794
16690
  className: "space-y-2",
15795
- children: [/* @__PURE__ */ jsxs("p", {
16691
+ children: /* @__PURE__ */ jsxs("p", {
15796
16692
  className: "text-kumo-subtle",
15797
16693
  children: [
15798
16694
  "User access is managed by an external provider (",
15799
16695
  manifest?.authMode,
15800
16696
  "). Self-signup domain settings are not available when using external authentication."
15801
16697
  ]
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
- })]
16698
+ })
15811
16699
  })]
15812
16700
  })
15813
16701
  })]
15814
16702
  });
15815
16703
  if (error) return /* @__PURE__ */ jsxs("div", {
15816
16704
  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",
16705
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
16706
+ className: "rounded-lg border bg-kumo-base p-6",
15822
16707
  children: /* @__PURE__ */ jsx("p", {
15823
16708
  className: "text-kumo-danger",
15824
16709
  children: error instanceof Error ? error.message : "Failed to load allowed domains"
@@ -15828,13 +16713,10 @@ function AllowedDomainsSettings() {
15828
16713
  return /* @__PURE__ */ jsxs("div", {
15829
16714
  className: "space-y-6",
15830
16715
  children: [
15831
- /* @__PURE__ */ jsx("h1", {
15832
- className: "text-2xl font-bold",
15833
- children: "Self-Signup Domains"
15834
- }),
16716
+ settingsHeader,
15835
16717
  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 })]
16718
+ 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"}`,
16719
+ 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
16720
  }),
15839
16721
  /* @__PURE__ */ jsxs("div", {
15840
16722
  className: "rounded-lg border bg-kumo-base p-6",
@@ -16123,8 +17005,12 @@ function ApiTokenSettings() {
16123
17005
  className: "flex items-center gap-3",
16124
17006
  children: [/* @__PURE__ */ jsx(Link$1, {
16125
17007
  to: "/settings",
16126
- className: "text-kumo-subtle hover:text-kumo-default transition-colors",
16127
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-5 w-5" })
17008
+ children: /* @__PURE__ */ jsx(Button, {
17009
+ variant: "ghost",
17010
+ shape: "square",
17011
+ "aria-label": "Back to settings",
17012
+ children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
17013
+ })
16128
17014
  }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
16129
17015
  className: "text-2xl font-bold",
16130
17016
  children: "API Tokens"
@@ -16877,12 +17763,6 @@ const BASE64_PLUS_REGEX = /\+/g;
16877
17763
  const BASE64_SLASH_REGEX = /\//g;
16878
17764
  const EMPTY_DATA = {};
16879
17765
  /**
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
17766
  * Convert base64url to ArrayBuffer
16887
17767
  */
16888
17768
  function base64urlToBuffer(base64url) {
@@ -16908,7 +17788,8 @@ function bufferToBase64url(buffer) {
16908
17788
  function PasskeyRegistration({ optionsEndpoint, verifyEndpoint, onSuccess, onError, buttonText = "Register Passkey", showNameInput = false, additionalData = EMPTY_DATA }) {
16909
17789
  const [state, setState] = React.useState({ status: "idle" });
16910
17790
  const [passkeyName, setPasskeyName] = React.useState("");
16911
- const isSupported = React.useMemo(() => isWebAuthnSupported(), []);
17791
+ const isSupported = React.useMemo(() => isPasskeyEnvironmentUsable(), []);
17792
+ const insecureContext = React.useMemo(() => typeof window !== "undefined" && !isWebAuthnSecureContext(), []);
16912
17793
  const handleRegister = React.useCallback(async () => {
16913
17794
  if (!isSupported) {
16914
17795
  setState({
@@ -17017,10 +17898,34 @@ function PasskeyRegistration({ optionsEndpoint, verifyEndpoint, onSuccess, onErr
17017
17898
  className: "rounded-lg border border-kumo-danger/50 bg-kumo-danger/10 p-4",
17018
17899
  children: [/* @__PURE__ */ jsx("h3", {
17019
17900
  className: "font-medium text-kumo-danger",
17020
- children: "Passkeys Not Supported"
17901
+ children: "Passkeys Not Available Here"
17021
17902
  }), /* @__PURE__ */ jsx("p", {
17022
17903
  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."
17904
+ children: insecureContext ? /* @__PURE__ */ jsxs(Fragment, { children: [
17905
+ "Passkeys require a ",
17906
+ /* @__PURE__ */ jsx("strong", {
17907
+ className: "text-kumo-default",
17908
+ children: "secure context"
17909
+ }),
17910
+ ": use",
17911
+ " ",
17912
+ /* @__PURE__ */ jsx("strong", {
17913
+ className: "text-kumo-default",
17914
+ children: "HTTPS"
17915
+ }),
17916
+ ", or open the admin at",
17917
+ " ",
17918
+ /* @__PURE__ */ jsx("strong", {
17919
+ className: "text-kumo-default",
17920
+ children: "http://localhost"
17921
+ }),
17922
+ " (with your dev port). Plain ",
17923
+ /* @__PURE__ */ jsx("code", {
17924
+ className: "text-xs",
17925
+ children: "http://"
17926
+ }),
17927
+ " on a custom hostname is not treated as secure, even on loopback."
17928
+ ] }) : /* @__PURE__ */ jsx(Fragment, { children: "Your browser doesn't support passkeys. Please use a modern browser like Chrome, Safari, Firefox, or Edge." })
17024
17929
  })]
17025
17930
  });
17026
17931
  return /* @__PURE__ */ jsxs("div", {
@@ -17311,12 +18216,24 @@ function SecuritySettings() {
17311
18216
  message: "Passkey added successfully"
17312
18217
  });
17313
18218
  };
17314
- if (manifestLoading || isLoading) return /* @__PURE__ */ jsxs("div", {
17315
- className: "space-y-6",
17316
- children: [/* @__PURE__ */ jsx("h1", {
18219
+ const settingsHeader = /* @__PURE__ */ jsxs("div", {
18220
+ className: "flex items-center gap-3",
18221
+ children: [/* @__PURE__ */ jsx(Link$1, {
18222
+ to: "/settings",
18223
+ children: /* @__PURE__ */ jsx(Button, {
18224
+ variant: "ghost",
18225
+ shape: "square",
18226
+ "aria-label": "Back to settings",
18227
+ children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" })
18228
+ })
18229
+ }), /* @__PURE__ */ jsx("h1", {
17317
18230
  className: "text-2xl font-bold",
17318
18231
  children: "Security Settings"
17319
- }), /* @__PURE__ */ jsx("div", {
18232
+ })]
18233
+ });
18234
+ if (manifestLoading || isLoading) return /* @__PURE__ */ jsxs("div", {
18235
+ className: "space-y-6",
18236
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
17320
18237
  className: "rounded-lg border bg-kumo-base p-6",
17321
18238
  children: /* @__PURE__ */ jsx("p", {
17322
18239
  className: "text-kumo-subtle",
@@ -17326,42 +18243,28 @@ function SecuritySettings() {
17326
18243
  });
17327
18244
  if (isExternalAuth) return /* @__PURE__ */ jsxs("div", {
17328
18245
  className: "space-y-6",
17329
- children: [/* @__PURE__ */ jsx("h1", {
17330
- className: "text-2xl font-bold",
17331
- children: "Security Settings"
17332
- }), /* @__PURE__ */ jsx("div", {
18246
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
17333
18247
  className: "rounded-lg border bg-kumo-base p-6",
17334
18248
  children: /* @__PURE__ */ jsxs("div", {
17335
18249
  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", {
18250
+ children: [/* @__PURE__ */ jsx(Info, { className: "h-5 w-5 text-kumo-subtle mt-0.5 flex-shrink-0" }), /* @__PURE__ */ jsx("div", {
17337
18251
  className: "space-y-2",
17338
- children: [/* @__PURE__ */ jsxs("p", {
18252
+ children: /* @__PURE__ */ jsxs("p", {
17339
18253
  className: "text-kumo-subtle",
17340
18254
  children: [
17341
18255
  "Authentication is managed by an external provider (",
17342
18256
  manifest?.authMode,
17343
18257
  "). Passkey settings are not available when using external authentication."
17344
18258
  ]
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
- })]
18259
+ })
17354
18260
  })]
17355
18261
  })
17356
18262
  })]
17357
18263
  });
17358
18264
  if (error) return /* @__PURE__ */ jsxs("div", {
17359
18265
  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",
18266
+ children: [settingsHeader, /* @__PURE__ */ jsx("div", {
18267
+ className: "rounded-lg border bg-kumo-base p-6",
17365
18268
  children: /* @__PURE__ */ jsx("p", {
17366
18269
  className: "text-kumo-danger",
17367
18270
  children: error instanceof Error ? error.message : "Failed to load passkeys"
@@ -17371,13 +18274,10 @@ function SecuritySettings() {
17371
18274
  return /* @__PURE__ */ jsxs("div", {
17372
18275
  className: "space-y-6",
17373
18276
  children: [
17374
- /* @__PURE__ */ jsx("h1", {
17375
- className: "text-2xl font-bold",
17376
- children: "Security Settings"
17377
- }),
18277
+ settingsHeader,
17378
18278
  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 })]
18279
+ 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"}`,
18280
+ 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
18281
  }),
17382
18282
  /* @__PURE__ */ jsxs("div", {
17383
18283
  className: "rounded-lg border bg-kumo-base p-6",
@@ -18129,10 +19029,7 @@ function SetupWizard() {
18129
19029
  /* @__PURE__ */ jsxs("div", {
18130
19030
  className: "text-center mb-6",
18131
19031
  children: [
18132
- /* @__PURE__ */ jsx("div", {
18133
- className: "text-4xl font-bold mb-2",
18134
- children: "— EmDash"
18135
- }),
19032
+ /* @__PURE__ */ jsx(LogoLockup, { className: "h-10 mx-auto mb-2" }),
18136
19033
  /* @__PURE__ */ jsxs("h1", {
18137
19034
  className: "text-2xl font-semibold text-kumo-default",
18138
19035
  children: [
@@ -18770,10 +19667,9 @@ function SidebarNav({ manifest }) {
18770
19667
  /* @__PURE__ */ jsx(KumoSidebar.Header, { children: /* @__PURE__ */ jsxs(Link$1, {
18771
19668
  to: "/",
18772
19669
  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: "—"
19670
+ children: [/* @__PURE__ */ jsx(LogoIcon, {
19671
+ className: "size-5 shrink-0",
19672
+ "aria-hidden": "true"
18777
19673
  }), /* @__PURE__ */ jsx("span", {
18778
19674
  className: "emdash-brand-text font-semibold truncate",
18779
19675
  children: "EmDash"
@@ -18874,8 +19770,8 @@ function Header() {
18874
19770
  const initials = ((user?.name || user?.email || "U")[0] ?? "U").toUpperCase();
18875
19771
  return /* @__PURE__ */ jsxs("header", {
18876
19772
  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",
19773
+ children: [/* @__PURE__ */ jsx(KumoSidebar.Trigger, { className: "cursor-pointer" }), /* @__PURE__ */ jsxs("div", {
19774
+ className: "flex items-center gap-2",
18879
19775
  children: [
18880
19776
  /* @__PURE__ */ jsxs(LinkButton, {
18881
19777
  variant: "ghost",
@@ -18893,7 +19789,7 @@ function Header() {
18893
19789
  children: /* @__PURE__ */ jsxs(Button, {
18894
19790
  variant: "ghost",
18895
19791
  size: "sm",
18896
- className: "gap-2",
19792
+ className: "gap-2 py-1 h-auto",
18897
19793
  children: [user?.avatarUrl ? /* @__PURE__ */ jsx("img", {
18898
19794
  src: user.avatarUrl,
18899
19795
  alt: "",
@@ -19021,8 +19917,8 @@ function WelcomeModal({ open, onClose, userName, userRole }) {
19021
19917
  className: "flex flex-col space-y-1.5 text-center sm:text-center",
19022
19918
  children: [
19023
19919
  /* @__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" })
19920
+ className: "mx-auto mb-4",
19921
+ children: /* @__PURE__ */ jsx(LogoIcon, { className: "h-16 w-16" })
19026
19922
  }),
19027
19923
  /* @__PURE__ */ jsxs(Dialog.Title, {
19028
19924
  className: "text-2xl font-semibold leading-none tracking-tight",
@@ -19467,10 +20363,7 @@ function SignupPage() {
19467
20363
  children: [
19468
20364
  /* @__PURE__ */ jsxs("div", {
19469
20365
  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", {
20366
+ children: [/* @__PURE__ */ jsx(LogoLockup, { className: "h-10 mx-auto mb-2" }), /* @__PURE__ */ jsxs("h1", {
19474
20367
  className: "text-2xl font-semibold text-kumo-default",
19475
20368
  children: [
19476
20369
  step === "email" && "Create an account",
@@ -19601,6 +20494,14 @@ function TermFormDialog({ open, onClose, taxonomyName, taxonomyDef, term, allTer
19601
20494
  const [description, setDescription] = React.useState(term?.description || "");
19602
20495
  const [autoSlug, setAutoSlug] = React.useState(!term);
19603
20496
  const [error, setError] = React.useState(null);
20497
+ React.useEffect(() => {
20498
+ setLabel(term?.label || "");
20499
+ setSlug(term?.slug || "");
20500
+ setParentId(term?.parentId || "");
20501
+ setDescription(term?.description || "");
20502
+ setAutoSlug(!term);
20503
+ setError(null);
20504
+ }, [term]);
19604
20505
  React.useEffect(() => {
19605
20506
  if (autoSlug && label) setSlug(slugify(label));
19606
20507
  }, [label, autoSlug]);
@@ -24693,13 +25594,20 @@ function ContentListPage() {
24693
25594
  });
24694
25595
  const i18n = manifest?.i18n;
24695
25596
  const activeLocale = i18n ? localeParam ?? i18n.defaultLocale : void 0;
24696
- const { data, isLoading, error } = useQuery({
25597
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error } = useInfiniteQuery({
24697
25598
  queryKey: [
24698
25599
  "content",
24699
25600
  collection,
24700
25601
  { locale: activeLocale }
24701
25602
  ],
24702
- queryFn: () => fetchContentList(collection, { locale: activeLocale })
25603
+ queryFn: ({ pageParam }) => fetchContentList(collection, {
25604
+ locale: activeLocale,
25605
+ cursor: pageParam,
25606
+ limit: 100
25607
+ }),
25608
+ initialPageParam: void 0,
25609
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
25610
+ enabled: !!manifest
24703
25611
  });
24704
25612
  const { data: trashedData, isLoading: isTrashedLoading } = useQuery({
24705
25613
  queryKey: [
@@ -24786,13 +25694,18 @@ function ContentListPage() {
24786
25694
  search: { locale: locale || void 0 }
24787
25695
  });
24788
25696
  };
25697
+ const items = React.useMemo(() => {
25698
+ return data?.pages.flatMap((page) => page.items) || [];
25699
+ }, [data]);
24789
25700
  return /* @__PURE__ */ jsx(ContentList, {
24790
25701
  collection,
24791
25702
  collectionLabel: collectionConfig.label,
24792
- items: data?.items || [],
25703
+ items,
24793
25704
  trashedItems: trashedData?.items || [],
24794
- isLoading,
25705
+ isLoading: isLoading || isFetchingNextPage,
24795
25706
  isTrashedLoading,
25707
+ hasMore: !!hasNextPage,
25708
+ onLoadMore: () => void fetchNextPage(),
24796
25709
  trashedCount: trashedData?.items?.length || 0,
24797
25710
  onDelete: (id) => deleteMutation.mutate(id),
24798
25711
  onRestore: (id) => restoreMutation.mutate(id),
@@ -24800,7 +25713,8 @@ function ContentListPage() {
24800
25713
  onDuplicate: (id) => duplicateMutation.mutate(id),
24801
25714
  i18n,
24802
25715
  activeLocale,
24803
- onLocaleChange: handleLocaleChange
25716
+ onLocaleChange: handleLocaleChange,
25717
+ urlPattern: collectionConfig.urlPattern
24804
25718
  });
24805
25719
  }
24806
25720
  /** Extract plugin block definitions from the manifest for Portable Text editor */
@@ -24992,6 +25906,7 @@ function ContentEditPage() {
24992
25906
  collection,
24993
25907
  id
24994
25908
  ] });
25909
+ if (rawItem?.draftRevisionId) queryClient.invalidateQueries({ queryKey: ["revision", rawItem.draftRevisionId] });
24995
25910
  },
24996
25911
  onError: (error) => {
24997
25912
  toastManager.add({
@@ -25014,6 +25929,7 @@ function ContentEditPage() {
25014
25929
  collection,
25015
25930
  id
25016
25931
  ] });
25932
+ if (rawItem?.draftRevisionId) queryClient.invalidateQueries({ queryKey: ["revision", rawItem.draftRevisionId] });
25017
25933
  },
25018
25934
  onError: (err) => {
25019
25935
  toastManager.add({
@@ -25237,6 +26153,7 @@ function ContentEditPage() {
25237
26153
  isDeleting: deleteMutation.isPending,
25238
26154
  supportsDrafts: collectionConfig.supports.includes("drafts"),
25239
26155
  supportsRevisions: collectionConfig.supports.includes("revisions"),
26156
+ supportsPreview: collectionConfig.supports.includes("preview"),
25240
26157
  currentUser,
25241
26158
  users: usersData?.items,
25242
26159
  onAuthorChange: handleAuthorChange,
@@ -25732,6 +26649,17 @@ function ContentTypesEditPage() {
25732
26649
  queryClient.invalidateQueries({ queryKey: ["manifest"] });
25733
26650
  }
25734
26651
  });
26652
+ const reorderFieldsMutation = useMutation({
26653
+ mutationFn: (fieldSlugs) => reorderFields(slug, fieldSlugs),
26654
+ onSuccess: () => {
26655
+ queryClient.invalidateQueries({ queryKey: [
26656
+ "schema",
26657
+ "collections",
26658
+ slug
26659
+ ] });
26660
+ queryClient.invalidateQueries({ queryKey: ["manifest"] });
26661
+ }
26662
+ });
25735
26663
  if (error) return /* @__PURE__ */ jsx(ErrorScreen, { error: error.message });
25736
26664
  if (isLoading) return /* @__PURE__ */ jsx(LoadingScreen, {});
25737
26665
  return /* @__PURE__ */ jsx(ContentTypeEditor, {
@@ -25743,7 +26671,8 @@ function ContentTypesEditPage() {
25743
26671
  fieldSlug,
25744
26672
  input
25745
26673
  }),
25746
- onDeleteField: (fieldSlug) => deleteFieldMutation.mutate(fieldSlug)
26674
+ onDeleteField: (fieldSlug) => deleteFieldMutation.mutate(fieldSlug),
26675
+ onReorderFields: (fieldSlugs) => reorderFieldsMutation.mutate(fieldSlugs)
25747
26676
  });
25748
26677
  }
25749
26678
  const pluginRoute = createRoute({