@ampless/admin 1.0.0-alpha.70 → 1.0.0-alpha.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/api/index.d.ts +1 -1
  2. package/dist/{chunk-YPZWY7DZ.js → chunk-7ATGSNSZ.js} +2 -2
  3. package/dist/{chunk-CZQYHUH7.js → chunk-7TAT4AHW.js} +48 -0
  4. package/dist/{chunk-FTAGUNNZ.js → chunk-DDAGBC4N.js} +5 -1
  5. package/dist/{chunk-PIULVPF5.js → chunk-DSCNWIBN.js} +2 -2
  6. package/dist/{chunk-CAGGVZUP.js → chunk-DUSEHGSV.js} +1 -1
  7. package/dist/{chunk-WE2GQIMF.js → chunk-ECN2MBJN.js} +583 -135
  8. package/dist/{chunk-3CBZ5FIV.js → chunk-GNTGWI43.js} +2 -2
  9. package/dist/{chunk-UEX2BIGF.js → chunk-HOJJIABL.js} +2 -2
  10. package/dist/{chunk-JS4ULHOZ.js → chunk-IR4EB5C3.js} +1 -1
  11. package/dist/{chunk-WBE6K6WZ.js → chunk-LNX3I35F.js} +1 -1
  12. package/dist/{chunk-TM7WHB5Q.js → chunk-MPZ3GVZC.js} +1 -1
  13. package/dist/{chunk-L6U3X7GJ.js → chunk-NI4JSVXL.js} +1 -1
  14. package/dist/{chunk-DDKBL6N4.js → chunk-O7VJHAYM.js} +43 -3
  15. package/dist/{chunk-SIQ3B55P.js → chunk-PHV2FQFV.js} +1 -1
  16. package/dist/{chunk-PAATFYUJ.js → chunk-TBNETGQL.js} +1 -1
  17. package/dist/components/admin-dashboard.js +3 -3
  18. package/dist/components/edit-post-view.js +5 -5
  19. package/dist/components/index.d.ts +1 -1
  20. package/dist/components/index.js +6 -6
  21. package/dist/components/login-view.js +3 -3
  22. package/dist/components/mcp-tokens-view.js +3 -3
  23. package/dist/components/media-view.js +5 -5
  24. package/dist/components/new-post-view.js +5 -5
  25. package/dist/components/plugin-settings-form.js +3 -3
  26. package/dist/components/posts-list-view.js +3 -3
  27. package/dist/components/users-list-view.js +3 -3
  28. package/dist/{i18n-DTN5tkNf.d.ts → i18n-DBNV_Fgf.d.ts} +72 -0
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +1 -1
  31. package/dist/metafile-esm.json +1 -1
  32. package/dist/pages/index.d.ts +1 -1
  33. package/dist/pages/index.js +15 -15
  34. package/package.json +4 -4
@@ -1,15 +1,16 @@
1
1
  'use client';
2
2
  import {
3
3
  ImageUploadDialog,
4
+ getAdminCmsConfig,
4
5
  getMediaProcessingDefaults,
5
6
  uploadProcessedImage
6
- } from "./chunk-FTAGUNNZ.js";
7
+ } from "./chunk-DDAGBC4N.js";
7
8
  import {
8
9
  publicMediaUrl
9
10
  } from "./chunk-2ITWLRYF.js";
10
11
  import {
11
12
  useT
12
- } from "./chunk-CAGGVZUP.js";
13
+ } from "./chunk-DUSEHGSV.js";
13
14
 
14
15
  // src/components/media-picker.tsx
15
16
  import { useEffect, useRef, useState } from "react";
@@ -154,7 +155,7 @@ function MediaPicker({ trigger, onSelect }) {
154
155
  }
155
156
 
156
157
  // src/components/post-form.tsx
157
- import { useRef as useRef3, useState as useState4 } from "react";
158
+ import { useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
158
159
  import { useRouter } from "next/navigation";
159
160
  import { Image as ImageIcon3 } from "lucide-react";
160
161
  import {
@@ -164,13 +165,13 @@ import {
164
165
  formatDate
165
166
  } from "ampless";
166
167
  import {
167
- renderBody,
168
+ renderBody as renderBody2,
168
169
  tiptapToHtml,
169
170
  tiptapToMarkdown,
170
171
  markdownToHtml,
171
172
  htmlToMarkdown
172
173
  } from "@ampless/runtime";
173
- import { Button as Button6, Input, Label, Textarea } from "@ampless/runtime/ui";
174
+ import { Button as Button7, Input, Label, Textarea } from "@ampless/runtime/ui";
174
175
 
175
176
  // src/editor/tiptap-editor.tsx
176
177
  import { useEditor, EditorContent } from "@tiptap/react";
@@ -579,7 +580,7 @@ var EDITOR_STYLES = `
579
580
  border-radius: 2px;
580
581
  }
581
582
  `;
582
- function TiptapEditor({ initialContent, onChange }) {
583
+ function TiptapEditor({ initialContent, onChange, onUserEdit }) {
583
584
  const editor = useEditor({
584
585
  extensions: [
585
586
  StarterKit,
@@ -607,6 +608,7 @@ function TiptapEditor({ initialContent, onChange }) {
607
608
  },
608
609
  onUpdate: ({ editor: editor2 }) => {
609
610
  onChange?.(editor2.getJSON());
611
+ onUserEdit?.();
610
612
  }
611
613
  });
612
614
  return /* @__PURE__ */ jsxs5("div", { className: "rounded-md border", children: [
@@ -617,10 +619,187 @@ function TiptapEditor({ initialContent, onChange }) {
617
619
  ] });
618
620
  }
619
621
 
622
+ // src/components/post-history-panel.tsx
623
+ import { useCallback, useEffect as useEffect3, useState as useState3 } from "react";
624
+ import { listPostHistory } from "ampless";
625
+ import { renderBody } from "@ampless/runtime";
626
+ import { Button as Button5 } from "@ampless/runtime/ui";
627
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
628
+ var PAGE_SIZE = 20;
629
+ function formatRevisedAt(iso, timezone, locale) {
630
+ const d = new Date(iso);
631
+ if (Number.isNaN(d.getTime())) return iso;
632
+ try {
633
+ return new Intl.DateTimeFormat(locale || void 0, {
634
+ year: "numeric",
635
+ month: "short",
636
+ day: "2-digit",
637
+ hour: "2-digit",
638
+ minute: "2-digit",
639
+ timeZone: timezone
640
+ }).format(d);
641
+ } catch {
642
+ return iso;
643
+ }
644
+ }
645
+ function revisionAsPost(rev) {
646
+ return {
647
+ postId: rev.postId,
648
+ slug: rev.slug ?? "",
649
+ title: rev.title ?? "",
650
+ excerpt: rev.excerpt,
651
+ format: rev.format ?? "markdown",
652
+ body: rev.body,
653
+ status: rev.status ?? "draft",
654
+ publishedAt: rev.publishedAt,
655
+ tags: rev.tags ?? [],
656
+ metadata: rev.metadata
657
+ };
658
+ }
659
+ function PostHistoryPanel({ postId, onRestore }) {
660
+ const t = useT();
661
+ const config = getAdminCmsConfig();
662
+ const timezone = config?.timezone ?? "UTC";
663
+ const locale = config?.locale ?? "en";
664
+ const [open, setOpen] = useState3(false);
665
+ const [loaded, setLoaded] = useState3(false);
666
+ const [loading, setLoading] = useState3(false);
667
+ const [error, setError] = useState3(null);
668
+ const [revisions, setRevisions] = useState3([]);
669
+ const [nextToken, setNextToken] = useState3(void 0);
670
+ const [selected, setSelected] = useState3(null);
671
+ const loadPage = useCallback(
672
+ async (token) => {
673
+ setLoading(true);
674
+ setError(null);
675
+ try {
676
+ const conn = await listPostHistory(postId, { limit: PAGE_SIZE, nextToken: token });
677
+ setRevisions((prev) => token ? [...prev, ...conn.items] : conn.items);
678
+ setNextToken(conn.nextToken);
679
+ } catch (err) {
680
+ setError(err instanceof Error ? err.message : String(err));
681
+ } finally {
682
+ setLoading(false);
683
+ }
684
+ },
685
+ [postId]
686
+ );
687
+ useEffect3(() => {
688
+ if (open && !loaded) {
689
+ setLoaded(true);
690
+ void loadPage();
691
+ }
692
+ }, [open, loaded, loadPage]);
693
+ return /* @__PURE__ */ jsxs6("section", { className: "rounded-md border", children: [
694
+ /* @__PURE__ */ jsxs6(
695
+ "button",
696
+ {
697
+ type: "button",
698
+ onClick: () => setOpen((v) => !v),
699
+ "aria-expanded": open,
700
+ className: "flex w-full items-center justify-between px-4 py-3 text-left text-sm font-medium",
701
+ children: [
702
+ /* @__PURE__ */ jsx6("span", { children: t("posts.history.title") }),
703
+ /* @__PURE__ */ jsx6("span", { className: "text-muted-foreground", "aria-hidden": "true", children: open ? "\u2212" : "+" })
704
+ ]
705
+ }
706
+ ),
707
+ open && /* @__PURE__ */ jsxs6("div", { className: "space-y-4 border-t px-4 py-4", children: [
708
+ error && /* @__PURE__ */ jsx6("p", { className: "text-sm text-destructive", children: error }),
709
+ loading && revisions.length === 0 ? /* @__PURE__ */ jsx6("p", { className: "text-sm text-muted-foreground", children: t("common.loading") }) : revisions.length === 0 ? /* @__PURE__ */ jsx6("p", { className: "text-sm text-muted-foreground", children: t("posts.history.empty") }) : /* @__PURE__ */ jsx6("div", { className: "overflow-x-auto rounded-md border", children: /* @__PURE__ */ jsxs6("table", { className: "w-full text-sm", children: [
710
+ /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs6("tr", { className: "border-b text-left text-xs text-muted-foreground", children: [
711
+ /* @__PURE__ */ jsx6("th", { className: "px-3 py-2 font-medium", children: t("posts.history.columnRevisedAt") }),
712
+ /* @__PURE__ */ jsx6("th", { className: "px-3 py-2 font-medium", children: t("posts.history.columnStatus") }),
713
+ /* @__PURE__ */ jsx6("th", { className: "px-3 py-2 font-medium", children: t("posts.history.columnTitle") }),
714
+ /* @__PURE__ */ jsx6("th", { className: "px-3 py-2 font-medium text-right", children: t("common.actions") })
715
+ ] }) }),
716
+ /* @__PURE__ */ jsx6("tbody", { children: revisions.map((rev) => /* @__PURE__ */ jsxs6(
717
+ "tr",
718
+ {
719
+ className: selected?.postHistoryId === rev.postHistoryId ? "border-b bg-muted/50" : "border-b",
720
+ children: [
721
+ /* @__PURE__ */ jsx6("td", { className: "whitespace-nowrap px-3 py-2 font-mono text-xs", children: formatRevisedAt(rev.revisedAt, timezone, locale) }),
722
+ /* @__PURE__ */ jsx6("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsx6("span", { className: "text-xs text-muted-foreground", children: rev.status ? t(`common.${rev.status}`) : "\u2014" }) }),
723
+ /* @__PURE__ */ jsx6("td", { className: "px-3 py-2", children: rev.title || "\u2014" }),
724
+ /* @__PURE__ */ jsx6("td", { className: "px-3 py-2 text-right", children: /* @__PURE__ */ jsxs6("div", { className: "flex justify-end gap-2", children: [
725
+ /* @__PURE__ */ jsx6(
726
+ Button5,
727
+ {
728
+ type: "button",
729
+ variant: "outline",
730
+ size: "sm",
731
+ onClick: () => setSelected(rev),
732
+ children: t("posts.history.view")
733
+ }
734
+ ),
735
+ /* @__PURE__ */ jsx6(
736
+ Button5,
737
+ {
738
+ type: "button",
739
+ variant: "outline",
740
+ size: "sm",
741
+ onClick: () => {
742
+ if (confirm(t("posts.history.restoreConfirm"))) onRestore(rev);
743
+ },
744
+ children: t("posts.history.restore")
745
+ }
746
+ )
747
+ ] }) })
748
+ ]
749
+ },
750
+ rev.postHistoryId
751
+ )) })
752
+ ] }) }),
753
+ nextToken && /* @__PURE__ */ jsx6("div", { children: /* @__PURE__ */ jsx6(
754
+ Button5,
755
+ {
756
+ type: "button",
757
+ variant: "outline",
758
+ size: "sm",
759
+ disabled: loading,
760
+ onClick: () => void loadPage(nextToken),
761
+ children: loading ? t("common.loading") : t("posts.history.loadMore")
762
+ }
763
+ ) }),
764
+ selected && /* @__PURE__ */ jsxs6("div", { className: "space-y-3 rounded-md border p-4", children: [
765
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between", children: [
766
+ /* @__PURE__ */ jsx6("h4", { className: "text-sm font-semibold", children: t("posts.history.viewing", {
767
+ date: formatRevisedAt(selected.revisedAt, timezone, locale)
768
+ }) }),
769
+ /* @__PURE__ */ jsx6(
770
+ Button5,
771
+ {
772
+ type: "button",
773
+ variant: "ghost",
774
+ size: "sm",
775
+ onClick: () => setSelected(null),
776
+ children: t("common.cancel")
777
+ }
778
+ )
779
+ ] }),
780
+ /* @__PURE__ */ jsxs6("p", { className: "text-xs text-muted-foreground", children: [
781
+ /* @__PURE__ */ jsx6("span", { className: "font-mono uppercase", children: selected.format ?? "\u2014" }),
782
+ selected.slug && /* @__PURE__ */ jsxs6(Fragment2, { children: [
783
+ /* @__PURE__ */ jsx6("span", { className: "mx-2", children: "\xB7" }),
784
+ /* @__PURE__ */ jsx6("span", { className: "font-mono", children: selected.slug })
785
+ ] })
786
+ ] }),
787
+ selected.format === "static" ? /* @__PURE__ */ jsx6("p", { className: "rounded-md bg-amber-50 p-2 text-xs text-amber-700 dark:bg-amber-950 dark:text-amber-400", children: t("posts.history.staticCaveat") }) : /* @__PURE__ */ jsx6(
788
+ "div",
789
+ {
790
+ className: "prose prose-neutral dark:prose-invert max-w-none text-sm",
791
+ dangerouslySetInnerHTML: { __html: renderBody(revisionAsPost(selected)) }
792
+ }
793
+ )
794
+ ] })
795
+ ] })
796
+ ] });
797
+ }
798
+
620
799
  // src/components/static-uploader.tsx
621
- import { useState as useState3 } from "react";
800
+ import { useState as useState4 } from "react";
622
801
  import { FileText, AlertTriangle, FileArchive, X } from "lucide-react";
623
- import { Button as Button5 } from "@ampless/runtime/ui";
802
+ import { Button as Button6 } from "@ampless/runtime/ui";
624
803
 
625
804
  // src/lib/static-bundle.ts
626
805
  import JSZip from "jszip";
@@ -706,13 +885,13 @@ async function deleteBundle(slug) {
706
885
  }
707
886
 
708
887
  // src/components/static-uploader.tsx
709
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
888
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
710
889
  function StaticUploader({ initial, onFilesReady, onClear }) {
711
890
  const t = useT();
712
- const [pending, setPending] = useState3(null);
713
- const [issues, setIssues] = useState3([]);
714
- const [busy, setBusy] = useState3(false);
715
- const [error, setError] = useState3(null);
891
+ const [pending, setPending] = useState4(null);
892
+ const [issues, setIssues] = useState4([]);
893
+ const [busy, setBusy] = useState4(false);
894
+ const [error, setError] = useState4(null);
716
895
  async function handleZip(file) {
717
896
  setBusy(true);
718
897
  setError(null);
@@ -786,11 +965,11 @@ function StaticUploader({ initial, onFilesReady, onClear }) {
786
965
  onClear();
787
966
  }
788
967
  const showCurrent = !pending && initial && initial.files.length > 0;
789
- return /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
790
- /* @__PURE__ */ jsxs6("div", { className: "rounded-md border border-dashed p-4", children: [
791
- /* @__PURE__ */ jsxs6("label", { className: "flex flex-col items-start gap-2 text-sm", children: [
792
- /* @__PURE__ */ jsx6("span", { className: "font-medium", children: t("posts.form.static.pick") }),
793
- /* @__PURE__ */ jsx6(
968
+ return /* @__PURE__ */ jsxs7("div", { className: "space-y-4", children: [
969
+ /* @__PURE__ */ jsxs7("div", { className: "rounded-md border border-dashed p-4", children: [
970
+ /* @__PURE__ */ jsxs7("label", { className: "flex flex-col items-start gap-2 text-sm", children: [
971
+ /* @__PURE__ */ jsx7("span", { className: "font-medium", children: t("posts.form.static.pick") }),
972
+ /* @__PURE__ */ jsx7(
794
973
  "input",
795
974
  {
796
975
  type: "file",
@@ -800,13 +979,13 @@ function StaticUploader({ initial, onFilesReady, onClear }) {
800
979
  disabled: busy
801
980
  }
802
981
  ),
803
- /* @__PURE__ */ jsx6("span", { className: "text-xs text-muted-foreground", children: t("posts.form.static.pickHint") })
982
+ /* @__PURE__ */ jsx7("span", { className: "text-xs text-muted-foreground", children: t("posts.form.static.pickHint") })
804
983
  ] }),
805
- busy && /* @__PURE__ */ jsx6("p", { className: "mt-2 text-sm text-muted-foreground", children: t("common.loading") }),
806
- error && /* @__PURE__ */ jsx6("p", { className: "mt-2 text-sm text-destructive", children: error })
984
+ busy && /* @__PURE__ */ jsx7("p", { className: "mt-2 text-sm text-muted-foreground", children: t("common.loading") }),
985
+ error && /* @__PURE__ */ jsx7("p", { className: "mt-2 text-sm text-destructive", children: error })
807
986
  ] }),
808
- showCurrent && /* @__PURE__ */ jsx6(CurrentBundle, { body: initial }),
809
- pending && /* @__PURE__ */ jsx6(
987
+ showCurrent && /* @__PURE__ */ jsx7(CurrentBundle, { body: initial }),
988
+ pending && /* @__PURE__ */ jsx7(
810
989
  PendingBundle,
811
990
  {
812
991
  files: pending,
@@ -818,15 +997,15 @@ function StaticUploader({ initial, onFilesReady, onClear }) {
818
997
  }
819
998
  function CurrentBundle({ body }) {
820
999
  const t = useT();
821
- return /* @__PURE__ */ jsxs6("div", { className: "rounded-md border bg-muted/30 p-3", children: [
822
- /* @__PURE__ */ jsxs6("div", { className: "mb-2 flex items-center gap-2 text-sm font-medium", children: [
823
- /* @__PURE__ */ jsx6(FileArchive, { className: "h-4 w-4" }),
1000
+ return /* @__PURE__ */ jsxs7("div", { className: "rounded-md border bg-muted/30 p-3", children: [
1001
+ /* @__PURE__ */ jsxs7("div", { className: "mb-2 flex items-center gap-2 text-sm font-medium", children: [
1002
+ /* @__PURE__ */ jsx7(FileArchive, { className: "h-4 w-4" }),
824
1003
  t("posts.form.static.currentBundle", {
825
1004
  count: body.files.length,
826
1005
  entrypoint: body.entrypoint
827
1006
  })
828
1007
  ] }),
829
- /* @__PURE__ */ jsx6(FileList, { files: body.files })
1008
+ /* @__PURE__ */ jsx7(FileList, { files: body.files })
830
1009
  ] });
831
1010
  }
832
1011
  function PendingBundle({
@@ -836,49 +1015,49 @@ function PendingBundle({
836
1015
  }) {
837
1016
  const t = useT();
838
1017
  const totalBytes = files.reduce((sum, f) => sum + f.data.byteLength, 0);
839
- return /* @__PURE__ */ jsxs6("div", { className: "rounded-md border p-3", children: [
840
- /* @__PURE__ */ jsxs6("div", { className: "mb-2 flex items-center justify-between gap-2 text-sm font-medium", children: [
841
- /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
842
- /* @__PURE__ */ jsx6(FileArchive, { className: "h-4 w-4" }),
1018
+ return /* @__PURE__ */ jsxs7("div", { className: "rounded-md border p-3", children: [
1019
+ /* @__PURE__ */ jsxs7("div", { className: "mb-2 flex items-center justify-between gap-2 text-sm font-medium", children: [
1020
+ /* @__PURE__ */ jsxs7("span", { className: "flex items-center gap-2", children: [
1021
+ /* @__PURE__ */ jsx7(FileArchive, { className: "h-4 w-4" }),
843
1022
  t("posts.form.static.pendingBundle", {
844
1023
  count: files.length,
845
1024
  size: formatBytes(totalBytes)
846
1025
  })
847
1026
  ] }),
848
- /* @__PURE__ */ jsxs6(Button5, { type: "button", variant: "ghost", size: "sm", onClick: onClear, children: [
849
- /* @__PURE__ */ jsx6(X, { className: "mr-1 h-3 w-3" }),
1027
+ /* @__PURE__ */ jsxs7(Button6, { type: "button", variant: "ghost", size: "sm", onClick: onClear, children: [
1028
+ /* @__PURE__ */ jsx7(X, { className: "mr-1 h-3 w-3" }),
850
1029
  t("common.cancel")
851
1030
  ] })
852
1031
  ] }),
853
- issues.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "mb-3 rounded-md border border-destructive/40 bg-destructive/5 p-2 text-sm", children: [
854
- /* @__PURE__ */ jsxs6("div", { className: "mb-1 flex items-center gap-2 font-medium text-destructive", children: [
855
- /* @__PURE__ */ jsx6(AlertTriangle, { className: "h-4 w-4" }),
1032
+ issues.length > 0 && /* @__PURE__ */ jsxs7("div", { className: "mb-3 rounded-md border border-destructive/40 bg-destructive/5 p-2 text-sm", children: [
1033
+ /* @__PURE__ */ jsxs7("div", { className: "mb-1 flex items-center gap-2 font-medium text-destructive", children: [
1034
+ /* @__PURE__ */ jsx7(AlertTriangle, { className: "h-4 w-4" }),
856
1035
  t("posts.form.static.issuesTitle", { count: issues.length })
857
1036
  ] }),
858
- /* @__PURE__ */ jsxs6("ul", { className: "space-y-0.5 text-xs", children: [
859
- issues.slice(0, 20).map((issue, idx) => /* @__PURE__ */ jsxs6("li", { className: "font-mono", children: [
1037
+ /* @__PURE__ */ jsxs7("ul", { className: "space-y-0.5 text-xs", children: [
1038
+ issues.slice(0, 20).map((issue, idx) => /* @__PURE__ */ jsxs7("li", { className: "font-mono", children: [
860
1039
  issue.path,
861
1040
  ": ",
862
1041
  issue.reason
863
1042
  ] }, `${issue.path}-${idx}`)),
864
- issues.length > 20 && /* @__PURE__ */ jsxs6("li", { className: "font-mono text-muted-foreground", children: [
1043
+ issues.length > 20 && /* @__PURE__ */ jsxs7("li", { className: "font-mono text-muted-foreground", children: [
865
1044
  "\u2026 ",
866
1045
  issues.length - 20,
867
1046
  " more"
868
1047
  ] })
869
1048
  ] }),
870
- /* @__PURE__ */ jsx6("p", { className: "mt-2 text-xs text-muted-foreground", children: t("posts.form.static.issuesHint") })
1049
+ /* @__PURE__ */ jsx7("p", { className: "mt-2 text-xs text-muted-foreground", children: t("posts.form.static.issuesHint") })
871
1050
  ] }),
872
- /* @__PURE__ */ jsx6(FileList, { files: files.map((f) => f.path) })
1051
+ /* @__PURE__ */ jsx7(FileList, { files: files.map((f) => f.path) })
873
1052
  ] });
874
1053
  }
875
1054
  function FileList({ files }) {
876
- return /* @__PURE__ */ jsxs6("ul", { className: "space-y-0.5 text-xs", children: [
877
- files.slice(0, 40).map((path) => /* @__PURE__ */ jsxs6("li", { className: "flex items-center gap-1.5 font-mono text-muted-foreground", children: [
878
- /* @__PURE__ */ jsx6(FileText, { className: "h-3 w-3 shrink-0" }),
1055
+ return /* @__PURE__ */ jsxs7("ul", { className: "space-y-0.5 text-xs", children: [
1056
+ files.slice(0, 40).map((path) => /* @__PURE__ */ jsxs7("li", { className: "flex items-center gap-1.5 font-mono text-muted-foreground", children: [
1057
+ /* @__PURE__ */ jsx7(FileText, { className: "h-3 w-3 shrink-0" }),
879
1058
  path
880
1059
  ] }, path)),
881
- files.length > 40 && /* @__PURE__ */ jsxs6("li", { className: "font-mono text-xs text-muted-foreground", children: [
1060
+ files.length > 40 && /* @__PURE__ */ jsxs7("li", { className: "font-mono text-xs text-muted-foreground", children: [
882
1061
  "\u2026 ",
883
1062
  files.length - 40,
884
1063
  " more"
@@ -951,8 +1130,98 @@ function isFuture(iso) {
951
1130
  return t > Date.now();
952
1131
  }
953
1132
 
1133
+ // src/lib/post-draft.ts
1134
+ var DRAFT_KEY_PREFIX = "ampless:draft:";
1135
+ var NEW_POST_DRAFT_ID = "new";
1136
+ function draftKey(postId) {
1137
+ return `${DRAFT_KEY_PREFIX}${postId ?? NEW_POST_DRAFT_ID}`;
1138
+ }
1139
+ function hasStorage() {
1140
+ return typeof window !== "undefined" && !!window.localStorage;
1141
+ }
1142
+ function readDraft(postId) {
1143
+ if (!hasStorage()) return null;
1144
+ try {
1145
+ const raw = window.localStorage.getItem(draftKey(postId));
1146
+ if (!raw) return null;
1147
+ const parsed = JSON.parse(raw);
1148
+ if (!parsed || typeof parsed !== "object" || typeof parsed.format !== "string") {
1149
+ return null;
1150
+ }
1151
+ return parsed;
1152
+ } catch {
1153
+ return null;
1154
+ }
1155
+ }
1156
+ function writeDraft(postId, draft) {
1157
+ if (!hasStorage()) return;
1158
+ try {
1159
+ window.localStorage.setItem(draftKey(postId), JSON.stringify(draft));
1160
+ } catch {
1161
+ }
1162
+ }
1163
+ function clearDraft(postId) {
1164
+ if (!hasStorage()) return;
1165
+ try {
1166
+ window.localStorage.removeItem(draftKey(postId));
1167
+ } catch {
1168
+ }
1169
+ }
1170
+ function clearAllDrafts() {
1171
+ if (!hasStorage()) return;
1172
+ try {
1173
+ const keys = [];
1174
+ for (let i = 0; i < window.localStorage.length; i++) {
1175
+ const k = window.localStorage.key(i);
1176
+ if (k && k.startsWith(DRAFT_KEY_PREFIX)) keys.push(k);
1177
+ }
1178
+ for (const k of keys) window.localStorage.removeItem(k);
1179
+ } catch {
1180
+ }
1181
+ }
1182
+ function draftDiffersFromLoaded(draft, loaded) {
1183
+ if (draft.title !== loaded.title) return true;
1184
+ if (draft.slug !== loaded.slug) return true;
1185
+ if (draft.excerpt !== loaded.excerpt) return true;
1186
+ if (draft.format !== loaded.format) return true;
1187
+ if (draft.status !== loaded.status) return true;
1188
+ if (draft.tags !== loaded.tags) return true;
1189
+ if (draft.publishedAtInput !== loaded.publishedAtInput) return true;
1190
+ if (draft.noLayout !== loaded.noLayout) return true;
1191
+ return !deepEqual(draft.body, loaded.body);
1192
+ }
1193
+ function reconcileDraft(draft, loaded) {
1194
+ if (!draftDiffersFromLoaded(draft, loaded)) return "discard";
1195
+ if (draft.baseUpdatedAt !== loaded.updatedAt) return "stale";
1196
+ return "recover";
1197
+ }
1198
+ function deepEqual(a, b) {
1199
+ if (a === b) return true;
1200
+ if (typeof a !== typeof b) return false;
1201
+ if (a === null || b === null) return a === b;
1202
+ if (typeof a !== "object") return false;
1203
+ if (Array.isArray(a) || Array.isArray(b)) {
1204
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
1205
+ if (a.length !== b.length) return false;
1206
+ for (let i = 0; i < a.length; i++) {
1207
+ if (!deepEqual(a[i], b[i])) return false;
1208
+ }
1209
+ return true;
1210
+ }
1211
+ const ao = a;
1212
+ const bo = b;
1213
+ const ak = Object.keys(ao);
1214
+ const bk = Object.keys(bo);
1215
+ if (ak.length !== bk.length) return false;
1216
+ for (const k of ak) {
1217
+ if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
1218
+ if (!deepEqual(ao[k], bo[k])) return false;
1219
+ }
1220
+ return true;
1221
+ }
1222
+
954
1223
  // src/components/post-form.tsx
955
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1224
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
956
1225
  var EMPTY_TIPTAP_DOC = { type: "doc", content: [{ type: "paragraph" }] };
957
1226
  var IMAGE_URL_RE = /\.(jpe?g|png|gif|webp|avif|svg|bmp|tiff?)(\?|$)/i;
958
1227
  var STYLESHEET_URL_RE = /\.css(\?|$)/i;
@@ -978,23 +1247,51 @@ function defaultBodyForFormat(format) {
978
1247
  function isStaticBody(value) {
979
1248
  return !!value && typeof value === "object" && "entrypoint" in value && "files" in value && Array.isArray(value.files);
980
1249
  }
1250
+ function formatDraftTime(epochMs, timezone, locale) {
1251
+ const d = new Date(epochMs);
1252
+ if (Number.isNaN(d.getTime())) return "";
1253
+ try {
1254
+ return new Intl.DateTimeFormat(locale || void 0, {
1255
+ year: "numeric",
1256
+ month: "short",
1257
+ day: "2-digit",
1258
+ hour: "2-digit",
1259
+ minute: "2-digit",
1260
+ timeZone: timezone
1261
+ }).format(d);
1262
+ } catch {
1263
+ return d.toLocaleString();
1264
+ }
1265
+ }
981
1266
  function PostForm({ post }) {
982
1267
  const router = useRouter();
983
1268
  const t = useT();
984
1269
  const isEdit = !!post;
985
1270
  const bodyTextareaRef = useRef3(null);
986
- const [title, setTitle] = useState4(post?.title ?? "");
987
- const [slug, setSlug] = useState4(post?.slug ?? "");
988
- const [excerpt, setExcerpt] = useState4(post?.excerpt ?? "");
989
- const [format, setFormat] = useState4(post?.format ?? "tiptap");
990
- const [body, setBody] = useState4(post?.body ?? EMPTY_TIPTAP_DOC);
991
- const [status, setStatus] = useState4(post?.status ?? "draft");
992
- const [tagsInput, setTagsInput] = useState4((post?.tags ?? []).join(", "));
993
- const [noLayout, setNoLayout] = useState4(post?.metadata?.no_layout === true);
994
- const [publishedAtInput, setPublishedAtInput] = useState4(isoToLocalInput(post?.publishedAt));
995
- const [saving, setSaving] = useState4(false);
996
- const [error, setError] = useState4(null);
997
- const [view, setView] = useState4("edit");
1271
+ const [title, setTitle] = useState5(post?.title ?? "");
1272
+ const [slug, setSlug] = useState5(post?.slug ?? "");
1273
+ const [excerpt, setExcerpt] = useState5(post?.excerpt ?? "");
1274
+ const [format, setFormat] = useState5(post?.format ?? "tiptap");
1275
+ const [body, setBody] = useState5(post?.body ?? EMPTY_TIPTAP_DOC);
1276
+ const [status, setStatus] = useState5(post?.status ?? "draft");
1277
+ const [tagsInput, setTagsInput] = useState5((post?.tags ?? []).join(", "));
1278
+ const [noLayout, setNoLayout] = useState5(post?.metadata?.no_layout === true);
1279
+ const [publishedAtInput, setPublishedAtInput] = useState5(isoToLocalInput(post?.publishedAt));
1280
+ const [saving, setSaving] = useState5(false);
1281
+ const [error, setError] = useState5(null);
1282
+ const [view, setView] = useState5("edit");
1283
+ const [editorEpoch, setEditorEpoch] = useState5(0);
1284
+ const dirtyRef = useRef3(false);
1285
+ function markDirty() {
1286
+ dirtyRef.current = true;
1287
+ }
1288
+ const [recovery, setRecovery] = useState5(null);
1289
+ const [restoredHint, setRestoredHint] = useState5(false);
1290
+ const draftId = post?.postId ?? NEW_POST_DRAFT_ID;
1291
+ const isStaticFormat = format === "static";
1292
+ const cmsConfig = getAdminCmsConfig();
1293
+ const draftTimezone = cmsConfig?.timezone ?? "UTC";
1294
+ const draftLocale = cmsConfig?.locale ?? "en";
998
1295
  const initialPublishedAtInput = isoToLocalInput(post?.publishedAt);
999
1296
  const resolvedPublishedAt = resolvePublishedAtForSave({
1000
1297
  status,
@@ -1002,7 +1299,7 @@ function PostForm({ post }) {
1002
1299
  initialInput: initialPublishedAtInput,
1003
1300
  existing: post?.publishedAt
1004
1301
  });
1005
- const [pendingBundle, setPendingBundle] = useState4(null);
1302
+ const [pendingBundle, setPendingBundle] = useState5(null);
1006
1303
  const initialStaticBody = isStaticBody(post?.body) ? post.body : null;
1007
1304
  function buildMetadata() {
1008
1305
  const next = { ...post?.metadata ?? {} };
@@ -1017,9 +1314,87 @@ function PostForm({ post }) {
1017
1314
  )
1018
1315
  );
1019
1316
  }
1317
+ const loadedState = {
1318
+ title: post?.title ?? "",
1319
+ slug: post?.slug ?? "",
1320
+ excerpt: post?.excerpt ?? "",
1321
+ format: post?.format ?? "tiptap",
1322
+ body: post?.body ?? EMPTY_TIPTAP_DOC,
1323
+ status: post?.status ?? "draft",
1324
+ tags: (post?.tags ?? []).join(", "),
1325
+ publishedAtInput: isoToLocalInput(post?.publishedAt),
1326
+ noLayout: post?.metadata?.no_layout === true,
1327
+ updatedAt: post?.updatedAt ?? null
1328
+ };
1329
+ function buildCurrentDraft() {
1330
+ return {
1331
+ title,
1332
+ slug,
1333
+ excerpt,
1334
+ format,
1335
+ body,
1336
+ status,
1337
+ tags: tagsInput,
1338
+ publishedAtInput,
1339
+ noLayout,
1340
+ baseUpdatedAt: post?.updatedAt ?? null,
1341
+ savedAt: Date.now()
1342
+ };
1343
+ }
1344
+ function applyDraft(draft) {
1345
+ setTitle(draft.title);
1346
+ setSlug(draft.slug);
1347
+ setExcerpt(draft.excerpt);
1348
+ setFormat(draft.format);
1349
+ setBody(draft.body);
1350
+ setStatus(draft.status);
1351
+ setTagsInput(draft.tags);
1352
+ setPublishedAtInput(draft.publishedAtInput);
1353
+ setNoLayout(draft.noLayout && draft.format === "html");
1354
+ setPendingBundle(null);
1355
+ setEditorEpoch((e) => e + 1);
1356
+ setView("edit");
1357
+ markDirty();
1358
+ setRestoredHint(true);
1359
+ }
1360
+ function acceptRecovery() {
1361
+ if (!recovery) return;
1362
+ applyDraft(recovery.draft);
1363
+ setRecovery(null);
1364
+ }
1365
+ function discardRecovery() {
1366
+ clearDraft(draftId);
1367
+ dirtyRef.current = false;
1368
+ setRecovery(null);
1369
+ }
1370
+ useEffect4(() => {
1371
+ if (loadedState.format === "static") return;
1372
+ const draft = readDraft(draftId);
1373
+ if (!draft) return;
1374
+ if (draft.format === "static") {
1375
+ clearDraft(draftId);
1376
+ return;
1377
+ }
1378
+ const decision = reconcileDraft(draft, loadedState);
1379
+ if (decision === "discard") {
1380
+ clearDraft(draftId);
1381
+ return;
1382
+ }
1383
+ setRecovery({ draft, decision });
1384
+ }, []);
1385
+ useEffect4(() => {
1386
+ if (isStaticFormat) return;
1387
+ if (!dirtyRef.current) return;
1388
+ const handle = setTimeout(() => {
1389
+ if (!dirtyRef.current) return;
1390
+ writeDraft(draftId, buildCurrentDraft());
1391
+ }, 1200);
1392
+ return () => clearTimeout(handle);
1393
+ }, [title, slug, excerpt, format, body, status, tagsInput, publishedAtInput, noLayout]);
1020
1394
  function insertMediaSnippet(url) {
1021
1395
  if (format === "tiptap") return;
1022
1396
  const snippet = snippetFor(url, format);
1397
+ markDirty();
1023
1398
  const ta = bodyTextareaRef.current;
1024
1399
  const current = typeof body === "string" ? body : "";
1025
1400
  if (!ta) {
@@ -1072,6 +1447,23 @@ function PostForm({ post }) {
1072
1447
  setBody(nextBody);
1073
1448
  setPendingBundle(null);
1074
1449
  if (next !== "html") setNoLayout(false);
1450
+ markDirty();
1451
+ }
1452
+ function restoreRevision(rev) {
1453
+ const fmt = rev.format ?? "markdown";
1454
+ setTitle(rev.title ?? "");
1455
+ setSlug(rev.slug ?? "");
1456
+ setExcerpt(rev.excerpt ?? "");
1457
+ setFormat(fmt);
1458
+ setBody(rev.body);
1459
+ setStatus(rev.status ?? "draft");
1460
+ setTagsInput((rev.tags ?? []).join(", "));
1461
+ setPublishedAtInput(isoToLocalInput(rev.publishedAt));
1462
+ setNoLayout(rev.metadata?.no_layout === true && fmt === "html");
1463
+ setPendingBundle(null);
1464
+ setEditorEpoch((e) => e + 1);
1465
+ setView("edit");
1466
+ markDirty();
1075
1467
  }
1076
1468
  async function save(e) {
1077
1469
  e.preventDefault();
@@ -1097,10 +1489,10 @@ function PostForm({ post }) {
1097
1489
  });
1098
1490
  nextBody = result.body;
1099
1491
  metadata = { ...metadata ?? {}, files: result.filesMeta };
1100
- } else if (initialStaticBody) {
1101
- nextBody = initialStaticBody;
1102
1492
  } else {
1103
- throw new Error(t("posts.form.static.noBundle"));
1493
+ const fallbackManifest = isStaticBody(body) ? body : initialStaticBody;
1494
+ if (!fallbackManifest) throw new Error(t("posts.form.static.noBundle"));
1495
+ nextBody = fallbackManifest;
1104
1496
  }
1105
1497
  }
1106
1498
  if (isEdit) {
@@ -1128,6 +1520,9 @@ function PostForm({ post }) {
1128
1520
  metadata
1129
1521
  });
1130
1522
  }
1523
+ dirtyRef.current = false;
1524
+ clearDraft(draftId);
1525
+ if (!isEdit) clearDraft(NEW_POST_DRAFT_ID);
1131
1526
  router.push("/admin/posts");
1132
1527
  router.refresh();
1133
1528
  } catch (err) {
@@ -1163,9 +1558,9 @@ function PostForm({ post }) {
1163
1558
  publishedAt: resolvedPublishedAt,
1164
1559
  tags: parseTags(tagsInput)
1165
1560
  };
1166
- return /* @__PURE__ */ jsxs7("form", { onSubmit: save, className: "space-y-6", children: [
1167
- /* @__PURE__ */ jsxs7("div", { className: "flex gap-1 border-b", children: [
1168
- /* @__PURE__ */ jsx7(
1561
+ return /* @__PURE__ */ jsxs8("form", { onSubmit: save, className: "space-y-6", children: [
1562
+ /* @__PURE__ */ jsxs8("div", { className: "flex gap-1 border-b", children: [
1563
+ /* @__PURE__ */ jsx8(
1169
1564
  "button",
1170
1565
  {
1171
1566
  type: "button",
@@ -1175,7 +1570,7 @@ function PostForm({ post }) {
1175
1570
  children: t("posts.form.tabEdit")
1176
1571
  }
1177
1572
  ),
1178
- /* @__PURE__ */ jsx7(
1573
+ /* @__PURE__ */ jsx8(
1179
1574
  "button",
1180
1575
  {
1181
1576
  type: "button",
@@ -1186,24 +1581,24 @@ function PostForm({ post }) {
1186
1581
  }
1187
1582
  )
1188
1583
  ] }),
1189
- view === "preview" && /* @__PURE__ */ jsxs7("article", { className: "space-y-4", children: [
1190
- /* @__PURE__ */ jsxs7("header", { className: "border-b pb-4", children: [
1191
- /* @__PURE__ */ jsx7("h1", { className: "text-3xl font-bold tracking-tight", children: title || /* @__PURE__ */ jsx7("span", { className: "text-muted-foreground italic", children: t("posts.form.previewNoTitle") }) }),
1192
- /* @__PURE__ */ jsxs7("p", { className: "mt-2 text-sm text-muted-foreground", children: [
1193
- previewPost.publishedAt ? /* @__PURE__ */ jsx7("time", { dateTime: previewPost.publishedAt, children: formatDate(previewPost.publishedAt) }) : /* @__PURE__ */ jsx7("span", { children: t("common.draft") }),
1194
- /* @__PURE__ */ jsx7("span", { className: "mx-2", children: "\xB7" }),
1195
- /* @__PURE__ */ jsx7("span", { className: "font-mono text-xs uppercase", children: format })
1584
+ view === "preview" && /* @__PURE__ */ jsxs8("article", { className: "space-y-4", children: [
1585
+ /* @__PURE__ */ jsxs8("header", { className: "border-b pb-4", children: [
1586
+ /* @__PURE__ */ jsx8("h1", { className: "text-3xl font-bold tracking-tight", children: title || /* @__PURE__ */ jsx8("span", { className: "text-muted-foreground italic", children: t("posts.form.previewNoTitle") }) }),
1587
+ /* @__PURE__ */ jsxs8("p", { className: "mt-2 text-sm text-muted-foreground", children: [
1588
+ previewPost.publishedAt ? /* @__PURE__ */ jsx8("time", { dateTime: previewPost.publishedAt, children: formatDate(previewPost.publishedAt) }) : /* @__PURE__ */ jsx8("span", { children: t("common.draft") }),
1589
+ /* @__PURE__ */ jsx8("span", { className: "mx-2", children: "\xB7" }),
1590
+ /* @__PURE__ */ jsx8("span", { className: "font-mono text-xs uppercase", children: format })
1196
1591
  ] }),
1197
- excerpt && /* @__PURE__ */ jsx7("p", { className: "mt-3 text-base text-muted-foreground", children: excerpt })
1592
+ excerpt && /* @__PURE__ */ jsx8("p", { className: "mt-3 text-base text-muted-foreground", children: excerpt })
1198
1593
  ] }),
1199
- format === "static" ? /* @__PURE__ */ jsx7("p", { className: "text-sm text-muted-foreground", children: t("posts.form.static.previewHint") }) : /* @__PURE__ */ jsx7(
1594
+ format === "static" ? /* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground", children: t("posts.form.static.previewHint") }) : /* @__PURE__ */ jsx8(
1200
1595
  "div",
1201
1596
  {
1202
1597
  className: "prose prose-neutral dark:prose-invert max-w-none",
1203
- dangerouslySetInnerHTML: { __html: renderBody(previewPost) }
1598
+ dangerouslySetInnerHTML: { __html: renderBody2(previewPost) }
1204
1599
  }
1205
1600
  ),
1206
- previewPost.tags && previewPost.tags.length > 0 && /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-2 border-t pt-4 text-sm", children: previewPost.tags.map((tag) => /* @__PURE__ */ jsxs7(
1601
+ previewPost.tags && previewPost.tags.length > 0 && /* @__PURE__ */ jsx8("div", { className: "flex flex-wrap gap-2 border-t pt-4 text-sm", children: previewPost.tags.map((tag) => /* @__PURE__ */ jsxs8(
1207
1602
  "span",
1208
1603
  {
1209
1604
  className: "rounded-full border px-2 py-0.5 text-xs text-muted-foreground",
@@ -1214,12 +1609,33 @@ function PostForm({ post }) {
1214
1609
  },
1215
1610
  tag
1216
1611
  )) }),
1217
- /* @__PURE__ */ jsx7("p", { className: "text-xs text-muted-foreground", children: t("posts.form.previewHint") })
1612
+ /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: t("posts.form.previewHint") })
1218
1613
  ] }),
1219
- /* @__PURE__ */ jsxs7("div", { className: view === "edit" ? "space-y-6" : "hidden", children: [
1220
- /* @__PURE__ */ jsxs7("div", { className: "space-y-2", children: [
1221
- /* @__PURE__ */ jsx7(Label, { htmlFor: "title", children: t("posts.form.title") }),
1222
- /* @__PURE__ */ jsx7(
1614
+ /* @__PURE__ */ jsxs8("div", { className: view === "edit" ? "space-y-6" : "hidden", children: [
1615
+ recovery && /* @__PURE__ */ jsxs8(
1616
+ "div",
1617
+ {
1618
+ role: "alert",
1619
+ className: recovery.decision === "stale" ? "space-y-2 rounded-md border border-amber-300 bg-amber-50 p-4 text-sm dark:border-amber-800 dark:bg-amber-950" : "space-y-2 rounded-md border border-[var(--primary)]/40 bg-[var(--primary)]/5 p-4 text-sm",
1620
+ children: [
1621
+ /* @__PURE__ */ jsx8("p", { className: "font-medium", children: recovery.decision === "stale" ? t("posts.draft.staleTitle") : t("posts.draft.recoverTitle") }),
1622
+ /* @__PURE__ */ jsx8("p", { className: "text-muted-foreground", children: recovery.decision === "stale" ? t("posts.draft.staleBody", {
1623
+ when: formatDraftTime(recovery.draft.savedAt, draftTimezone, draftLocale)
1624
+ }) : t("posts.draft.recoverBody", {
1625
+ when: formatDraftTime(recovery.draft.savedAt, draftTimezone, draftLocale)
1626
+ }) }),
1627
+ /* @__PURE__ */ jsxs8("div", { className: "flex gap-2", children: [
1628
+ /* @__PURE__ */ jsx8(Button7, { type: "button", size: "sm", onClick: acceptRecovery, children: recovery.decision === "stale" ? t("posts.draft.restoreAnyway") : t("posts.draft.restore") }),
1629
+ /* @__PURE__ */ jsx8(Button7, { type: "button", size: "sm", variant: "outline", onClick: discardRecovery, children: t("posts.draft.discard") })
1630
+ ] })
1631
+ ]
1632
+ }
1633
+ ),
1634
+ restoredHint && !recovery && /* @__PURE__ */ jsx8("p", { className: "rounded-md border border-[var(--primary)]/40 bg-[var(--primary)]/5 px-3 py-2 text-xs text-muted-foreground", children: t("posts.draft.restored") }),
1635
+ isStaticFormat && /* @__PURE__ */ jsx8("p", { className: "rounded-md border bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: t("posts.draft.staticUnsupported") }),
1636
+ /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
1637
+ /* @__PURE__ */ jsx8(Label, { htmlFor: "title", children: t("posts.form.title") }),
1638
+ /* @__PURE__ */ jsx8(
1223
1639
  Input,
1224
1640
  {
1225
1641
  id: "title",
@@ -1228,37 +1644,44 @@ function PostForm({ post }) {
1228
1644
  onChange: (e) => {
1229
1645
  setTitle(e.target.value);
1230
1646
  if (!isEdit && !slug) setSlug(slugify(e.target.value));
1647
+ markDirty();
1231
1648
  }
1232
1649
  }
1233
1650
  )
1234
1651
  ] }),
1235
- /* @__PURE__ */ jsxs7("div", { className: "space-y-2", children: [
1236
- /* @__PURE__ */ jsx7(Label, { htmlFor: "slug", children: t("posts.form.slug") }),
1237
- /* @__PURE__ */ jsx7(
1652
+ /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
1653
+ /* @__PURE__ */ jsx8(Label, { htmlFor: "slug", children: t("posts.form.slug") }),
1654
+ /* @__PURE__ */ jsx8(
1238
1655
  Input,
1239
1656
  {
1240
1657
  id: "slug",
1241
1658
  value: slug,
1242
- onChange: (e) => setSlug(e.target.value),
1659
+ onChange: (e) => {
1660
+ setSlug(e.target.value);
1661
+ markDirty();
1662
+ },
1243
1663
  placeholder: slugify(title) || t("posts.form.slugPlaceholder")
1244
1664
  }
1245
1665
  )
1246
1666
  ] }),
1247
- /* @__PURE__ */ jsxs7("div", { className: "space-y-2", children: [
1248
- /* @__PURE__ */ jsx7(Label, { htmlFor: "excerpt", children: t("posts.form.excerpt") }),
1249
- /* @__PURE__ */ jsx7(
1667
+ /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
1668
+ /* @__PURE__ */ jsx8(Label, { htmlFor: "excerpt", children: t("posts.form.excerpt") }),
1669
+ /* @__PURE__ */ jsx8(
1250
1670
  Textarea,
1251
1671
  {
1252
1672
  id: "excerpt",
1253
1673
  rows: 2,
1254
1674
  value: excerpt,
1255
- onChange: (e) => setExcerpt(e.target.value)
1675
+ onChange: (e) => {
1676
+ setExcerpt(e.target.value);
1677
+ markDirty();
1678
+ }
1256
1679
  }
1257
1680
  )
1258
1681
  ] }),
1259
- /* @__PURE__ */ jsxs7("div", { className: "space-y-2", children: [
1260
- /* @__PURE__ */ jsx7(Label, { htmlFor: "format", children: t("posts.form.format") }),
1261
- /* @__PURE__ */ jsxs7(
1682
+ /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
1683
+ /* @__PURE__ */ jsx8(Label, { htmlFor: "format", children: t("posts.form.format") }),
1684
+ /* @__PURE__ */ jsxs8(
1262
1685
  "select",
1263
1686
  {
1264
1687
  id: "format",
@@ -1266,120 +1689,145 @@ function PostForm({ post }) {
1266
1689
  onChange: (e) => changeFormat(e.target.value),
1267
1690
  className: "flex h-9 w-full max-w-xs rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm",
1268
1691
  children: [
1269
- /* @__PURE__ */ jsx7("option", { value: "tiptap", children: "Tiptap (rich editor)" }),
1270
- /* @__PURE__ */ jsx7("option", { value: "markdown", children: "Markdown" }),
1271
- /* @__PURE__ */ jsx7("option", { value: "html", children: "HTML" }),
1272
- /* @__PURE__ */ jsx7("option", { value: "static", children: t("posts.form.formatStaticLabel") })
1692
+ /* @__PURE__ */ jsx8("option", { value: "tiptap", children: "Tiptap (rich editor)" }),
1693
+ /* @__PURE__ */ jsx8("option", { value: "markdown", children: "Markdown" }),
1694
+ /* @__PURE__ */ jsx8("option", { value: "html", children: "HTML" }),
1695
+ /* @__PURE__ */ jsx8("option", { value: "static", children: t("posts.form.formatStaticLabel") })
1273
1696
  ]
1274
1697
  }
1275
1698
  ),
1276
- /* @__PURE__ */ jsx7("p", { className: "text-xs text-muted-foreground", children: t("posts.form.formatHint") })
1699
+ /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: t("posts.form.formatHint") })
1277
1700
  ] }),
1278
- /* @__PURE__ */ jsxs7("div", { className: "space-y-2", children: [
1279
- /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [
1280
- /* @__PURE__ */ jsx7(Label, { children: t("posts.form.body") }),
1701
+ /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
1702
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between", children: [
1703
+ /* @__PURE__ */ jsx8(Label, { children: t("posts.form.body") }),
1281
1704
  format !== "tiptap" && format !== "static" && // For textarea-based formats (markdown / html) there's no
1282
1705
  // embedded toolbar, so we surface the MediaPicker as a
1283
1706
  // standalone button. Selecting an asset inserts a
1284
1707
  // format-aware snippet at the cursor.
1285
- /* @__PURE__ */ jsx7(
1708
+ /* @__PURE__ */ jsx8(
1286
1709
  MediaPicker,
1287
1710
  {
1288
1711
  onSelect: insertMediaSnippet,
1289
- trigger: /* @__PURE__ */ jsxs7(Button6, { type: "button", variant: "outline", size: "sm", children: [
1290
- /* @__PURE__ */ jsx7(ImageIcon3, { className: "mr-2 h-3 w-3" }),
1712
+ trigger: /* @__PURE__ */ jsxs8(Button7, { type: "button", variant: "outline", size: "sm", children: [
1713
+ /* @__PURE__ */ jsx8(ImageIcon3, { className: "mr-2 h-3 w-3" }),
1291
1714
  t("posts.form.insertMedia")
1292
1715
  ] })
1293
1716
  }
1294
1717
  )
1295
1718
  ] }),
1296
- format === "tiptap" ? /* @__PURE__ */ jsx7(TiptapEditor, { initialContent: body, onChange: setBody }) : format === "static" ? /* @__PURE__ */ jsx7(
1719
+ format === "tiptap" ? /* @__PURE__ */ jsx8(
1720
+ TiptapEditor,
1721
+ {
1722
+ initialContent: body,
1723
+ onChange: setBody,
1724
+ onUserEdit: markDirty
1725
+ },
1726
+ editorEpoch
1727
+ ) : format === "static" ? /* @__PURE__ */ jsx8(
1297
1728
  StaticUploader,
1298
1729
  {
1299
1730
  initial: initialStaticBody,
1300
1731
  onFilesReady: (files, entrypoint) => setPendingBundle({ files, entrypoint }),
1301
1732
  onClear: () => setPendingBundle(null)
1302
1733
  }
1303
- ) : /* @__PURE__ */ jsx7(
1734
+ ) : /* @__PURE__ */ jsx8(
1304
1735
  Textarea,
1305
1736
  {
1306
1737
  ref: bodyTextareaRef,
1307
1738
  rows: 20,
1308
1739
  value: typeof body === "string" ? body : "",
1309
- onChange: (e) => setBody(e.target.value),
1740
+ onChange: (e) => {
1741
+ setBody(e.target.value);
1742
+ markDirty();
1743
+ },
1310
1744
  className: "font-mono text-xs"
1311
1745
  }
1312
1746
  )
1313
1747
  ] }),
1314
- /* @__PURE__ */ jsxs7("div", { className: "space-y-2", children: [
1315
- /* @__PURE__ */ jsx7(Label, { htmlFor: "tags", children: t("posts.form.tags") }),
1316
- /* @__PURE__ */ jsx7(
1748
+ /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
1749
+ /* @__PURE__ */ jsx8(Label, { htmlFor: "tags", children: t("posts.form.tags") }),
1750
+ /* @__PURE__ */ jsx8(
1317
1751
  Input,
1318
1752
  {
1319
1753
  id: "tags",
1320
1754
  value: tagsInput,
1321
- onChange: (e) => setTagsInput(e.target.value),
1755
+ onChange: (e) => {
1756
+ setTagsInput(e.target.value);
1757
+ markDirty();
1758
+ },
1322
1759
  placeholder: t("posts.form.tagsPlaceholder")
1323
1760
  }
1324
1761
  ),
1325
- /* @__PURE__ */ jsx7("p", { className: "text-xs text-muted-foreground", children: t("posts.form.tagsHint") })
1762
+ /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: t("posts.form.tagsHint") })
1326
1763
  ] }),
1327
- /* @__PURE__ */ jsxs7("div", { className: "space-y-2", children: [
1328
- /* @__PURE__ */ jsx7(Label, { htmlFor: "status", children: t("posts.form.status") }),
1329
- /* @__PURE__ */ jsxs7(
1764
+ /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
1765
+ /* @__PURE__ */ jsx8(Label, { htmlFor: "status", children: t("posts.form.status") }),
1766
+ /* @__PURE__ */ jsxs8(
1330
1767
  "select",
1331
1768
  {
1332
1769
  id: "status",
1333
1770
  value: status,
1334
- onChange: (e) => setStatus(e.target.value),
1771
+ onChange: (e) => {
1772
+ setStatus(e.target.value);
1773
+ markDirty();
1774
+ },
1335
1775
  className: "flex h-9 w-full max-w-xs rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm",
1336
1776
  children: [
1337
- /* @__PURE__ */ jsx7("option", { value: "draft", children: t("common.draft") }),
1338
- /* @__PURE__ */ jsx7("option", { value: "published", children: t("common.published") })
1777
+ /* @__PURE__ */ jsx8("option", { value: "draft", children: t("common.draft") }),
1778
+ /* @__PURE__ */ jsx8("option", { value: "published", children: t("common.published") })
1339
1779
  ]
1340
1780
  }
1341
1781
  )
1342
1782
  ] }),
1343
- /* @__PURE__ */ jsxs7("div", { className: "space-y-2", children: [
1344
- /* @__PURE__ */ jsx7(Label, { htmlFor: "publishedAt", children: t("posts.form.publishedAt") }),
1345
- /* @__PURE__ */ jsx7(
1783
+ /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
1784
+ /* @__PURE__ */ jsx8(Label, { htmlFor: "publishedAt", children: t("posts.form.publishedAt") }),
1785
+ /* @__PURE__ */ jsx8(
1346
1786
  "input",
1347
1787
  {
1348
1788
  id: "publishedAt",
1349
1789
  type: "datetime-local",
1350
1790
  value: publishedAtInput,
1351
- onChange: (e) => setPublishedAtInput(e.target.value),
1791
+ onChange: (e) => {
1792
+ setPublishedAtInput(e.target.value);
1793
+ markDirty();
1794
+ },
1352
1795
  className: "flex h-9 w-full max-w-xs rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm"
1353
1796
  }
1354
1797
  ),
1355
- /* @__PURE__ */ jsx7("p", { className: "text-xs text-muted-foreground", children: t("posts.form.publishedAtHint") }),
1356
- status === "published" && isFuture(resolvedPublishedAt) && /* @__PURE__ */ jsx7("p", { className: "text-xs text-amber-600 dark:text-amber-400", children: t("posts.form.scheduledNotice", { date: publishedAtInput }) })
1798
+ /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: t("posts.form.publishedAtHint") }),
1799
+ status === "published" && isFuture(resolvedPublishedAt) && /* @__PURE__ */ jsx8("p", { className: "text-xs text-amber-600 dark:text-amber-400", children: t("posts.form.scheduledNotice", { date: publishedAtInput }) })
1357
1800
  ] }),
1358
- format === "html" && /* @__PURE__ */ jsx7("div", { className: "space-y-2", children: /* @__PURE__ */ jsxs7("label", { className: "flex items-start gap-2 text-sm", children: [
1359
- /* @__PURE__ */ jsx7(
1801
+ format === "html" && /* @__PURE__ */ jsx8("div", { className: "space-y-2", children: /* @__PURE__ */ jsxs8("label", { className: "flex items-start gap-2 text-sm", children: [
1802
+ /* @__PURE__ */ jsx8(
1360
1803
  "input",
1361
1804
  {
1362
1805
  type: "checkbox",
1363
1806
  checked: noLayout,
1364
- onChange: (e) => setNoLayout(e.target.checked),
1807
+ onChange: (e) => {
1808
+ setNoLayout(e.target.checked);
1809
+ markDirty();
1810
+ },
1365
1811
  className: "mt-1"
1366
1812
  }
1367
1813
  ),
1368
- /* @__PURE__ */ jsxs7("span", { children: [
1369
- /* @__PURE__ */ jsx7("span", { className: "font-medium", children: t("posts.form.noLayout") }),
1370
- /* @__PURE__ */ jsx7("span", { className: "block text-xs text-muted-foreground", children: t("posts.form.noLayoutHint") })
1814
+ /* @__PURE__ */ jsxs8("span", { children: [
1815
+ /* @__PURE__ */ jsx8("span", { className: "font-medium", children: t("posts.form.noLayout") }),
1816
+ /* @__PURE__ */ jsx8("span", { className: "block text-xs text-muted-foreground", children: t("posts.form.noLayoutHint") })
1371
1817
  ] })
1372
1818
  ] }) }),
1373
- error && /* @__PURE__ */ jsx7("p", { className: "text-sm text-destructive", children: error }),
1374
- /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
1375
- /* @__PURE__ */ jsx7(Button6, { type: "submit", disabled: saving, children: saving ? t("common.saving") : isEdit ? t("posts.form.saveChanges") : t("posts.form.createPost") }),
1376
- isEdit && /* @__PURE__ */ jsx7(Button6, { type: "button", variant: "destructive", onClick: handleDelete, disabled: saving, children: t("posts.form.delete") })
1377
- ] })
1819
+ error && /* @__PURE__ */ jsx8("p", { className: "text-sm text-destructive", children: error }),
1820
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
1821
+ /* @__PURE__ */ jsx8(Button7, { type: "submit", disabled: saving, children: saving ? t("common.saving") : isEdit ? t("posts.form.saveChanges") : t("posts.form.createPost") }),
1822
+ isEdit && /* @__PURE__ */ jsx8(Button7, { type: "button", variant: "destructive", onClick: handleDelete, disabled: saving, children: t("posts.form.delete") })
1823
+ ] }),
1824
+ isEdit && post && /* @__PURE__ */ jsx8(PostHistoryPanel, { postId: post.postId, onRestore: restoreRevision })
1378
1825
  ] })
1379
1826
  ] });
1380
1827
  }
1381
1828
 
1382
1829
  export {
1383
1830
  MediaPicker,
1831
+ clearAllDrafts,
1384
1832
  PostForm
1385
1833
  };