@drawnagency/primitives 0.1.24 → 0.1.26

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.
@@ -199,6 +199,7 @@ function createEvent(name) {
199
199
  var editModeEvent = createEvent("editmodechange");
200
200
  var navChangeEvent = createEvent("sitenavchange");
201
201
  var darkModeEvent = createEvent("sitedarkmode");
202
+ var historySelectEvent = createEvent("history-select");
202
203
 
203
204
  // src/lib/loader.ts
204
205
  function mergeSiteContent(index, sectionFiles) {
@@ -1 +1 @@
1
- {"version":3,"file":"ColorSwatchSettings.d.ts","sourceRoot":"","sources":["../../../src/components/brandguide/ColorSwatchSettings.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,sBAAsB,CAAC;AAElE,UAAU,KAAK;IACb,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,KAAK,2CAyBrE"}
1
+ {"version":3,"file":"ColorSwatchSettings.d.ts","sourceRoot":"","sources":["../../../src/components/brandguide/ColorSwatchSettings.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,sBAAsB,CAAC;AAElE,UAAU,KAAK;IACb,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC;AAMD,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,KAAK,2CAkCrE"}
@@ -0,0 +1,6 @@
1
+ interface HistoryPopoverProps {
2
+ onSelectCommit: (sha: string, date: string) => void;
3
+ }
4
+ export declare function HistoryPopover({ onSelectCommit }: HistoryPopoverProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
6
+ //# sourceMappingURL=HistoryPopover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HistoryPopover.d.ts","sourceRoot":"","sources":["../../../src/components/shared/HistoryPopover.tsx"],"names":[],"mappings":"AAWA,UAAU,mBAAmB;IAC3B,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED,wBAAgB,cAAc,CAAC,EAAE,cAAc,EAAE,EAAE,mBAAmB,2CAwFrE"}
@@ -3,7 +3,8 @@ interface Props {
3
3
  navLinks: NavItem[];
4
4
  siteName: string;
5
5
  darkMode: "light" | "dark" | "optional";
6
+ lastUpdated: string | null;
6
7
  }
7
- export default function Navigation({ navLinks: initialNavLinks, siteName, darkMode }: Props): import("react/jsx-runtime").JSX.Element;
8
+ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMode, lastUpdated }: Props): import("react/jsx-runtime").JSX.Element;
8
9
  export {};
9
10
  //# sourceMappingURL=Navigation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Navigation.d.ts","sourceRoot":"","sources":["../../../src/components/shared/Navigation.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAI7C,UAAU,KAAK;IACb,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;CACzC;AAED,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,KAAK,2CAsL1F"}
1
+ {"version":3,"file":"Navigation.d.ts","sourceRoot":"","sources":["../../../src/components/shared/Navigation.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAO7C,UAAU,KAAK;IACb,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;IACxC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,KAAK,2CA8NvG"}
@@ -1,11 +1,22 @@
1
1
  import { type ReactNode } from "react";
2
+ import type { LoadedSection } from "../../lib/loader";
3
+ import type { SiteIndex, SiteConfig } from "../../schemas/site-config";
4
+ export type HistoryState = {
5
+ sha: string;
6
+ date: string;
7
+ sections: LoadedSection[];
8
+ index: SiteIndex;
9
+ siteConfig: SiteConfig;
10
+ } | null;
2
11
  interface EditorContextValue {
3
12
  isEditMode: boolean;
4
13
  showAllChrome: boolean;
5
14
  viewBranch: "saved" | "live";
15
+ historyState: HistoryState;
6
16
  toggleEditMode: () => void;
7
17
  toggleShowAllChrome: () => void;
8
18
  setViewBranch: (branch: "saved" | "live") => void;
19
+ setHistoryState: (state: HistoryState) => void;
9
20
  }
10
21
  interface EditorProviderProps {
11
22
  children: ReactNode;
@@ -1 +1 @@
1
- {"version":3,"file":"EditorContext.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGzF,UAAU,kBAAkB;IAC1B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,OAAO,GAAG,MAAM,CAAC;IAC7B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,aAAa,EAAE,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;CACnD;AAID,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,EAAE,mBAAmB,2CAqB/D;AAED,wBAAgB,gBAAgB,IAAI,kBAAkB,CAMrD"}
1
+ {"version":3,"file":"EditorContext.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEzF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAEvE,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,SAAS,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;CACxB,GAAG,IAAI,CAAC;AAET,UAAU,kBAAkB;IAC1B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,OAAO,GAAG,MAAM,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,aAAa,EAAE,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;IAClD,eAAe,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CAChD;AAID,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,EAAE,mBAAmB,2CAyB/D;AAED,wBAAgB,gBAAgB,IAAI,kBAAkB,CAMrD"}
@@ -1 +1 @@
1
- {"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAYjD,OAAO,sBAAsB,CAAC;AAkC9B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAcxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,GACZ,EAAE,KAAK,2CAukBP"}
1
+ {"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAYjD,OAAO,sBAAsB,CAAC;AAqC9B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAcxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,GACZ,EAAE,KAAK,2CAmnBP"}
@@ -0,0 +1,8 @@
1
+ interface HistoryToolbarProps {
2
+ date: string;
3
+ onBackToCurrent: () => void;
4
+ onRestore: () => void;
5
+ }
6
+ export declare function HistoryToolbar({ date, onBackToCurrent, onRestore }: HistoryToolbarProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=HistoryToolbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HistoryToolbar.d.ts","sourceRoot":"","sources":["../../../src/components/shell/HistoryToolbar.tsx"],"names":[],"mappings":"AAGA,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,EAAE,mBAAmB,2CAsBvF"}
@@ -0,0 +1,10 @@
1
+ interface RestoreModalProps {
2
+ isOpen: boolean;
3
+ onClose: () => void;
4
+ date: string;
5
+ onConfirm: () => void;
6
+ isRestoring: boolean;
7
+ }
8
+ export declare function RestoreModal({ isOpen, onClose, date, onConfirm, isRestoring }: RestoreModalProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=RestoreModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RestoreModal.d.ts","sourceRoot":"","sources":["../../../src/components/shell/RestoreModal.tsx"],"names":[],"mappings":"AAIA,UAAU,iBAAiB;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,iBAAiB,2CAiBhG"}
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  safeRedirect,
28
28
  sanitizeHtml,
29
29
  toSectionId
30
- } from "./chunk-46QI4FDZ.js";
30
+ } from "./chunk-FSVPD7TW.js";
31
31
  import {
32
32
  ColorItemSchema,
33
33
  ColorSpaceSchema,
@@ -9,4 +9,8 @@ export declare const editModeEvent: TypedEvent<{
9
9
  }>;
10
10
  export declare const navChangeEvent: TypedEvent<NavItem[]>;
11
11
  export declare const darkModeEvent: TypedEvent<string>;
12
+ export declare const historySelectEvent: TypedEvent<{
13
+ sha: string;
14
+ date: string;
15
+ }>;
12
16
  //# sourceMappingURL=events.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/lib/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAErC,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACnD;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,CAW1D;AAED,eAAO,MAAM,aAAa;gBAA6B,OAAO;EAAqB,CAAC;AACpF,eAAO,MAAM,cAAc,uBAA0C,CAAC;AACtE,eAAO,MAAM,aAAa,oBAAsC,CAAC"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/lib/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAErC,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACnD;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,CAW1D;AAED,eAAO,MAAM,aAAa;gBAA6B,OAAO;EAAqB,CAAC;AACpF,eAAO,MAAM,cAAc,uBAA0C,CAAC;AACtE,eAAO,MAAM,aAAa,oBAAsC,CAAC;AACjE,eAAO,MAAM,kBAAkB;SAAsB,MAAM;UAAQ,MAAM;EAAqB,CAAC"}
package/dist/lib/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  safeRedirect,
18
18
  sanitizeHtml,
19
19
  toSectionId
20
- } from "../chunk-46QI4FDZ.js";
20
+ } from "../chunk-FSVPD7TW.js";
21
21
  import {
22
22
  clearRegistry,
23
23
  createRegistry,
@@ -1,2 +1,4 @@
1
+ export declare function formatDate(iso: string): string;
2
+ export declare function formatDateTime(iso: string): string;
1
3
  export declare function formatTimestamp(iso: string): string;
2
4
  //# sourceMappingURL=timestamp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"timestamp.d.ts","sourceRoot":"","sources":["../../src/lib/timestamp.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BnD"}
1
+ {"version":3,"file":"timestamp.d.ts","sourceRoot":"","sources":["../../src/lib/timestamp.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAalD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BnD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawnagency/primitives",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
@@ -1,3 +1,4 @@
1
+ import { useState } from "react";
1
2
  import { Input } from "../shared/Input";
2
3
  import type { ColorItem, ColorSpace } from "../../schemas/shared";
3
4
 
@@ -6,20 +7,33 @@ interface Props {
6
7
  onChange: (color: ColorItem) => void;
7
8
  }
8
9
 
10
+ function mergeSpaces(spaces: ColorSpace[]): ColorSpace {
11
+ return Object.assign({}, ...spaces);
12
+ }
13
+
9
14
  export default function ColorSwatchSettings({ color, onChange }: Props) {
10
- const space = color.spaces[0] || {};
15
+ const [localColor, setLocalColor] = useState(() => ({
16
+ ...color,
17
+ spaces: [mergeSpaces(color.spaces)],
18
+ }));
19
+ const space = localColor.spaces[0] || {};
20
+
21
+ const update = (updated: ColorItem) => {
22
+ setLocalColor(updated);
23
+ onChange(updated);
24
+ };
11
25
 
12
26
  const updateSpace = (key: keyof ColorSpace, value: string) => {
13
27
  const newSpace = { ...space, [key]: value || undefined };
14
- onChange({ ...color, spaces: [newSpace, ...color.spaces.slice(1)] });
28
+ update({ ...localColor, spaces: [newSpace] });
15
29
  };
16
30
 
17
31
  return (
18
32
  <div className="space-y-3">
19
33
  <Input
20
34
  label="Name"
21
- value={color.name || ""}
22
- onChange={(value) => onChange({ ...color, name: value })}
35
+ value={localColor.name || ""}
36
+ onChange={(name) => update({ ...localColor, name })}
23
37
  />
24
38
  {(["hex", "rgb", "cmyk", "pantone"] as const).map((key) => (
25
39
  <Input
@@ -0,0 +1,104 @@
1
+ import { useState, useCallback, useEffect } from "react";
2
+ import { cn } from "../../lib/cn";
3
+ import { formatDateTime } from "../../lib/timestamp";
4
+ import { Button } from "./Button";
5
+
6
+ interface CommitItem {
7
+ sha: string;
8
+ date: string;
9
+ message: string;
10
+ }
11
+
12
+ interface HistoryPopoverProps {
13
+ onSelectCommit: (sha: string, date: string) => void;
14
+ }
15
+
16
+ export function HistoryPopover({ onSelectCommit }: HistoryPopoverProps) {
17
+ const [commits, setCommits] = useState<CommitItem[]>([]);
18
+ const [loading, setLoading] = useState(true);
19
+ const [loadingMore, setLoadingMore] = useState(false);
20
+ const [page, setPage] = useState(1);
21
+ const [hasMore, setHasMore] = useState(true);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ const fetchPage = useCallback(async (pageNum: number) => {
25
+ const isFirst = pageNum === 1;
26
+ if (isFirst) setLoading(true);
27
+ else setLoadingMore(true);
28
+
29
+ try {
30
+ const res = await fetch(`/api/history?page=${pageNum}&per_page=20`);
31
+ if (!res.ok) throw new Error(`Failed to load history: ${res.status}`);
32
+ const data = await res.json();
33
+ const items = data.items as CommitItem[];
34
+ setCommits((prev) => isFirst ? items : [...prev, ...items]);
35
+ setHasMore(items.length === 20);
36
+ setPage(pageNum);
37
+ } catch (err) {
38
+ setError(err instanceof Error ? err.message : "Failed to load history");
39
+ } finally {
40
+ setLoading(false);
41
+ setLoadingMore(false);
42
+ }
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ fetchPage(1);
47
+ }, [fetchPage]);
48
+
49
+ if (loading) {
50
+ return (
51
+ <div className="p-4 text-center text-xs text-base-contrast-light">
52
+ Loading history...
53
+ </div>
54
+ );
55
+ }
56
+
57
+ if (error) {
58
+ return (
59
+ <div className="p-4 text-center text-xs text-red-600">
60
+ {error}
61
+ </div>
62
+ );
63
+ }
64
+
65
+ if (commits.length === 0) {
66
+ return (
67
+ <div className="p-4 text-center text-xs text-base-contrast-light">
68
+ No history found
69
+ </div>
70
+ );
71
+ }
72
+
73
+ return (
74
+ <div className="max-h-64 overflow-y-auto">
75
+ <ul className="py-1">
76
+ {commits.map((commit) => (
77
+ <li key={commit.sha}>
78
+ <button
79
+ onClick={() => onSelectCommit(commit.sha, commit.date)}
80
+ className={cn(
81
+ "w-full cursor-pointer px-4 py-2 text-left text-xs transition-colors",
82
+ "hover:bg-base-accent text-base-contrast-light hover:text-base-contrast",
83
+ )}
84
+ >
85
+ {formatDateTime(commit.date)}
86
+ </button>
87
+ </li>
88
+ ))}
89
+ </ul>
90
+ {hasMore && (
91
+ <div className="border-t border-base-200 p-2 text-center">
92
+ <Button
93
+ variant="ghost"
94
+ size="sm"
95
+ onClick={() => fetchPage(page + 1)}
96
+ disabled={loadingMore}
97
+ >
98
+ {loadingMore ? "Loading..." : "Load more"}
99
+ </Button>
100
+ </div>
101
+ )}
102
+ </div>
103
+ );
104
+ }
@@ -1,22 +1,28 @@
1
- import { useState, useCallback, useEffect } from "react";
1
+ import { useState, useCallback, useEffect, useRef } from "react";
2
2
  import { cn } from "../../lib/cn";
3
3
  import { Toggle } from "./Toggle";
4
4
  import type { NavItem } from "../../lib/nav";
5
- import { editModeEvent, navChangeEvent, darkModeEvent } from "../../lib/events";
5
+ import { editModeEvent, navChangeEvent, darkModeEvent, historySelectEvent } from "../../lib/events";
6
6
  import { useActiveHeadings } from "../../hooks/useActiveHeadings";
7
+ import { formatDate } from "../../lib/timestamp";
8
+ import { Popover } from "./Popover";
9
+ import { HistoryPopover } from "./HistoryPopover";
7
10
 
8
11
  interface Props {
9
12
  navLinks: NavItem[];
10
13
  siteName: string;
11
14
  darkMode: "light" | "dark" | "optional";
15
+ lastUpdated: string | null;
12
16
  }
13
17
 
14
- export default function Navigation({ navLinks: initialNavLinks, siteName, darkMode }: Props) {
18
+ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMode, lastUpdated }: Props) {
15
19
  const [isOpen, setIsOpen] = useState(false);
16
- const [isEditMode, setIsEditMode] = useState(false);
20
+ const [isEditMode, setIsEditMode] = useState(() => typeof window !== "undefined" && window.location.pathname.startsWith("/edit"));
17
21
  const [currentDarkMode, setCurrentDarkMode] = useState(darkMode);
18
22
  const [isDark, setIsDark] = useState(false);
19
23
  const [navLinks, setNavLinks] = useState<NavItem[]>(initialNavLinks);
24
+ const [showHistory, setShowHistory] = useState(false);
25
+ const historyButtonRef = useRef<HTMLButtonElement>(null);
20
26
 
21
27
  useEffect(() => {
22
28
  const unlistenEdit = editModeEvent.listen(({ isEditMode }) => setIsEditMode(isEditMode));
@@ -73,20 +79,22 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
73
79
  return (
74
80
  <>
75
81
  {/* Mobile header bar */}
76
- <header className="fixed top-0 left-0 right-0 z-50 flex h-16 items-center justify-between bg-base px-4 lg:hidden">
77
- <span className="text-lg font-bold text-primary">{siteName}</span>
78
- <button
79
- onClick={() => setIsOpen(!isOpen)}
80
- className="cursor-pointer p-2 text-base-contrast"
81
- aria-label="Toggle navigation"
82
- >
83
- <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
84
- {isOpen
85
- ? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
86
- : <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
87
- }
88
- </svg>
89
- </button>
82
+ <header className="fixed top-0 left-0 right-0 z-50 bg-base lg:hidden">
83
+ <div className="mx-auto max-w-screen-xl flex h-16 items-center justify-between px-4">
84
+ <span className="text-lg font-bold text-primary">{siteName}</span>
85
+ <button
86
+ onClick={() => setIsOpen(!isOpen)}
87
+ className="cursor-pointer p-2 text-base-contrast"
88
+ aria-label="Toggle navigation"
89
+ >
90
+ <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
91
+ {isOpen
92
+ ? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
93
+ : <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
94
+ }
95
+ </svg>
96
+ </button>
97
+ </div>
90
98
  </header>
91
99
 
92
100
  {/* Backdrop */}
@@ -100,7 +108,7 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
100
108
  {/* Sidebar nav */}
101
109
  <nav
102
110
  className={cn(
103
- "fixed top-0 left-0 z-40 h-full w-64 overflow-y-auto bg-base pt-16 transition-transform lg:translate-x-0",
111
+ "fixed top-0 left-0 lg:left-auto z-40 h-full w-64 flex flex-col overflow-y-auto border-r border-base-200 bg-base pt-16 transition-transform lg:translate-x-0 nav-sidebar",
104
112
  isOpen ? "translate-x-0" : "-translate-x-full",
105
113
  )}
106
114
  >
@@ -177,19 +185,55 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
177
185
  })}
178
186
  </ul>
179
187
 
180
- {currentDarkMode === "optional" && (
181
- <div className="mt-auto border-t border-base-200 px-4 py-4">
182
- <div className="flex items-center gap-2 text-sm text-base-contrast-light">
183
- <span className={cn(!isDark && "text-base-contrast")}>Light</span>
184
- <Toggle
185
- checked={isDark}
186
- onChange={handleThemeToggle}
187
- label="Toggle dark mode"
188
- />
189
- <span className={cn(isDark && "text-base-contrast")}>Dark</span>
188
+ <div className="mt-auto">
189
+ {currentDarkMode === "optional" && (
190
+ <div className="mx-4 border-t border-base-200 py-4">
191
+ <div className="flex items-center gap-2 text-sm text-base-contrast-light">
192
+ <span className={cn(!isDark && "text-base-contrast")}>Light</span>
193
+ <Toggle checked={isDark} onChange={handleThemeToggle} label="Toggle dark mode" />
194
+ <span className={cn(isDark && "text-base-contrast")}>Dark</span>
195
+ </div>
190
196
  </div>
191
- </div>
192
- )}
197
+ )}
198
+
199
+ {lastUpdated && (
200
+ <div className={cn(currentDarkMode !== "optional" && "border-t border-base-200", "mx-4 py-4 text-center")}>
201
+ {isEditMode ? (
202
+ <div className="relative">
203
+ <button
204
+ ref={historyButtonRef}
205
+ onClick={() => setShowHistory((prev) => !prev)}
206
+ className="cursor-pointer text-xs text-base-contrast-light hover:text-primary transition-colors"
207
+ aria-label="View history"
208
+ >
209
+ Last updated {formatDate(lastUpdated)}
210
+ </button>
211
+ <Popover
212
+ isOpen={showHistory}
213
+ onClose={() => setShowHistory(false)}
214
+ anchorRef={historyButtonRef}
215
+ className="w-56 !bottom-full !top-auto !mb-1 !mt-0"
216
+ >
217
+ <HistoryPopover
218
+ onSelectCommit={(sha, date) => {
219
+ setShowHistory(false);
220
+ historySelectEvent.dispatch({ sha, date });
221
+ }}
222
+ />
223
+ </Popover>
224
+ </div>
225
+ ) : (
226
+ <p className="text-xs text-base-contrast-light">
227
+ Last updated {new Date(lastUpdated).toLocaleDateString("en-US", {
228
+ month: "short",
229
+ day: "numeric",
230
+ year: "numeric",
231
+ })}
232
+ </p>
233
+ )}
234
+ </div>
235
+ )}
236
+ </div>
193
237
  </nav>
194
238
  </>
195
239
  );
@@ -1,13 +1,25 @@
1
1
  import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
2
2
  import { editModeEvent } from "../../lib/events";
3
+ import type { LoadedSection } from "../../lib/loader";
4
+ import type { SiteIndex, SiteConfig } from "../../schemas/site-config";
5
+
6
+ export type HistoryState = {
7
+ sha: string;
8
+ date: string;
9
+ sections: LoadedSection[];
10
+ index: SiteIndex;
11
+ siteConfig: SiteConfig;
12
+ } | null;
3
13
 
4
14
  interface EditorContextValue {
5
15
  isEditMode: boolean;
6
16
  showAllChrome: boolean;
7
17
  viewBranch: "saved" | "live";
18
+ historyState: HistoryState;
8
19
  toggleEditMode: () => void;
9
20
  toggleShowAllChrome: () => void;
10
21
  setViewBranch: (branch: "saved" | "live") => void;
22
+ setHistoryState: (state: HistoryState) => void;
11
23
  }
12
24
 
13
25
  const EditorContext = createContext<EditorContextValue | null>(null);
@@ -20,6 +32,10 @@ export function EditorProvider({ children }: EditorProviderProps) {
20
32
  const [isEditMode, setIsEditMode] = useState(true);
21
33
  const [showAllChrome, setShowAllChrome] = useState(false);
22
34
  const [viewBranch, setViewBranchState] = useState<"saved" | "live">("saved");
35
+ const [historyState, setHistoryStateRaw] = useState<HistoryState>(null);
36
+ const setHistoryState = useCallback((state: HistoryState) => {
37
+ setHistoryStateRaw(state);
38
+ }, []);
23
39
 
24
40
  const toggleEditMode = useCallback(() => {
25
41
  setIsEditMode((prev) => {
@@ -33,7 +49,7 @@ export function EditorProvider({ children }: EditorProviderProps) {
33
49
  const setViewBranch = useCallback((b: "saved" | "live") => setViewBranchState(b), []);
34
50
 
35
51
  return (
36
- <EditorContext.Provider value={{ isEditMode, showAllChrome, viewBranch, toggleEditMode, toggleShowAllChrome, setViewBranch }}>
52
+ <EditorContext.Provider value={{ isEditMode, showAllChrome, viewBranch, historyState, toggleEditMode, toggleShowAllChrome, setViewBranch, setHistoryState }}>
37
53
  {children}
38
54
  </EditorContext.Provider>
39
55
  );
@@ -1,4 +1,4 @@
1
- import { Fragment, useState, useCallback, useEffect, useRef } from "react";
1
+ import { Fragment, useState, useCallback, useEffect, useRef, type ReactNode } from "react";
2
2
 
3
3
  import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
4
4
  import type { LoadedSection } from "../../lib/loader";
@@ -40,7 +40,7 @@ import { useBuildStatus } from "../../hooks/useBuildStatus";
40
40
  import { useMediaPipeline } from "../../hooks/useMediaPipeline";
41
41
  import { formatTimestamp } from "../../lib/timestamp";
42
42
  import { generateNavLinks } from "../../lib/nav";
43
- import { navChangeEvent, darkModeEvent } from "../../lib/events";
43
+ import { navChangeEvent, darkModeEvent, historySelectEvent } from "../../lib/events";
44
44
  import { cn } from "../../lib/cn";
45
45
  import { Button } from "../shared/Button";
46
46
  import { SplitButton } from "../shared/SplitButton";
@@ -49,6 +49,9 @@ import { SegmentedControl } from "../shared/SegmentedControl";
49
49
  import { SettingsIcon } from "../shared/icons";
50
50
  import { ImageIcon, X } from "lucide-react";
51
51
  import { ErrorBoundary } from "../shared/ErrorBoundary";
52
+ import { HistoryToolbar } from "./HistoryToolbar";
53
+ import { RestoreModal } from "./RestoreModal";
54
+ import ViewRenderer from "../sections/ViewRenderer";
52
55
 
53
56
  export { useMediaLibrary } from "./MediaLibraryContext";
54
57
 
@@ -106,6 +109,8 @@ export default function EditorShell({
106
109
  const [mainIndex, setMainIndex] = useState<SiteIndex | null>(null);
107
110
  const [viewSections, setViewSections] = useState<LoadedSection[] | null>(null);
108
111
  const [isLoadingViewContent, setIsLoadingViewContent] = useState(false);
112
+ const [showRestoreModal, setShowRestoreModal] = useState(false);
113
+ const [isRestoring, setIsRestoring] = useState(false);
109
114
 
110
115
  const siteIndexRef = useRef<SiteIndex>({ siteId, order: [], sections: {} });
111
116
  const fontLinkRef = useRef<HTMLLinkElement | null>(null);
@@ -515,6 +520,38 @@ export default function EditorShell({
515
520
  setLocalChangesExist(true);
516
521
  }, [applySiteConfigPreview, persistence]);
517
522
 
523
+ const handleHistoryRestore = useCallback(async (historyContent: {
524
+ sections: LoadedSection[];
525
+ index: SiteIndex;
526
+ siteConfig: SiteConfig;
527
+ }) => {
528
+ setIsRestoring(true);
529
+ try {
530
+ const payload = {
531
+ sections: historyContent.sections.map((s) => ({
532
+ id: s.section.id,
533
+ content: s.section,
534
+ })),
535
+ siteIndex: historyContent.index,
536
+ siteConfig: historyContent.siteConfig,
537
+ targetBranch: "main",
538
+ };
539
+
540
+ const response = await fetch("/api/save", {
541
+ method: "POST",
542
+ headers: { "Content-Type": "application/json" },
543
+ body: JSON.stringify(payload),
544
+ });
545
+
546
+ if (!response.ok) throw new Error(`Restore failed: ${response.status}`);
547
+ await discardLocalChanges();
548
+ window.location.reload();
549
+ } catch (err) {
550
+ console.error("Failed to restore:", err);
551
+ setIsRestoring(false);
552
+ }
553
+ }, []);
554
+
518
555
  // --- Render ---
519
556
 
520
557
  if (shellState.phase === "loading-content") {
@@ -559,6 +596,7 @@ export default function EditorShell({
559
596
  setViewSections={setViewSections}
560
597
  setIsLoadingViewContent={setIsLoadingViewContent}
561
598
  />
599
+ <HistoryWatcher />
562
600
  <EditorModalProvider>
563
601
  <MediaLibraryContext.Provider value={{
564
602
  ...mediaPipeline.contextValue,
@@ -587,25 +625,28 @@ export default function EditorShell({
587
625
  buildState={buildStatus.state}
588
626
  buildElapsed={buildStatus.elapsedSeconds}
589
627
  onBuildDismiss={buildStatus.dismiss}
628
+ onRestoreClick={() => setShowRestoreModal(true)}
590
629
  />
591
630
 
592
- <EditorContent
593
- sections={sections}
594
- audiences={audiences}
595
- dirtySectionIds={dirtySectionIds}
596
- deletedSections={deletedSections}
597
- isPublishing={publishAction !== "idle"}
598
- onSectionChange={onSectionChange}
599
- onAddSection={onAddSection}
600
- onDeleteSection={onDeleteSection}
601
- onUndoDelete={handleUndoDelete}
602
- onReorderSections={onReorderSections}
603
- onAccessChange={onAccessChange}
604
- onStatusChange={onStatusChange}
605
- mainIndex={mainIndex}
606
- changedSectionIds={changedSectionIds}
607
- viewSections={viewSections}
608
- />
631
+ <HistoryOrEditorContent sections={sections}>
632
+ <EditorContent
633
+ sections={sections}
634
+ audiences={audiences}
635
+ dirtySectionIds={dirtySectionIds}
636
+ deletedSections={deletedSections}
637
+ isPublishing={publishAction !== "idle"}
638
+ onSectionChange={onSectionChange}
639
+ onAddSection={onAddSection}
640
+ onDeleteSection={onDeleteSection}
641
+ onUndoDelete={handleUndoDelete}
642
+ onReorderSections={onReorderSections}
643
+ onAccessChange={onAccessChange}
644
+ onStatusChange={onStatusChange}
645
+ mainIndex={mainIndex}
646
+ changedSectionIds={changedSectionIds}
647
+ viewSections={viewSections}
648
+ />
649
+ </HistoryOrEditorContent>
609
650
  <GlobalModal />
610
651
  <SiteSettingsModal
611
652
  isOpen={showSiteSettings}
@@ -663,6 +704,12 @@ export default function EditorShell({
663
704
  maxFileSize={siteConfig?.media.maxFileSize}
664
705
  />
665
706
  </EditorModal>
707
+ <RestoreHandler
708
+ showRestoreModal={showRestoreModal}
709
+ setShowRestoreModal={setShowRestoreModal}
710
+ isRestoring={isRestoring}
711
+ onRestore={handleHistoryRestore}
712
+ />
666
713
  </div>
667
714
  </MediaLibraryContext.Provider>
668
715
  </EditorModalProvider>
@@ -700,6 +747,85 @@ function ViewBranchWatcher({
700
747
  return null;
701
748
  }
702
749
 
750
+ function HistoryWatcher() {
751
+ const { setHistoryState } = useEditorContext();
752
+
753
+ useEffect(() => {
754
+ const unlisten = historySelectEvent.listen(async ({ sha, date }) => {
755
+ try {
756
+ const res = await fetch(`/api/content/history?sha=${sha}`);
757
+ if (!res.ok) throw new Error(`Failed to load history: ${res.status}`);
758
+ const data = await res.json();
759
+ setHistoryState({
760
+ sha,
761
+ date,
762
+ sections: data.sections,
763
+ index: data.index,
764
+ siteConfig: data.siteConfig,
765
+ });
766
+ const navLinks = generateNavLinks(data.sections);
767
+ navChangeEvent.dispatch(navLinks);
768
+ } catch (err) {
769
+ console.error("Failed to load historical content:", err);
770
+ }
771
+ });
772
+ return unlisten;
773
+ }, [setHistoryState]);
774
+
775
+ return null;
776
+ }
777
+
778
+ function HistoryOrEditorContent({ children, sections }: { children: ReactNode; sections: LoadedSection[] }) {
779
+ const { historyState } = useEditorContext();
780
+ const wasInHistory = useRef(false);
781
+
782
+ useEffect(() => {
783
+ if (historyState) {
784
+ wasInHistory.current = true;
785
+ } else if (wasInHistory.current) {
786
+ wasInHistory.current = false;
787
+ if (sections.length > 0) {
788
+ const navLinks = generateNavLinks(sections);
789
+ navChangeEvent.dispatch(navLinks);
790
+ }
791
+ }
792
+ }, [historyState, sections]);
793
+
794
+ if (historyState) {
795
+ return <ViewRenderer sections={historyState.sections} />;
796
+ }
797
+ return <>{children}</>;
798
+ }
799
+
800
+ function RestoreHandler({
801
+ showRestoreModal,
802
+ setShowRestoreModal,
803
+ isRestoring,
804
+ onRestore,
805
+ }: {
806
+ showRestoreModal: boolean;
807
+ setShowRestoreModal: (show: boolean) => void;
808
+ isRestoring: boolean;
809
+ onRestore: (content: { sections: LoadedSection[]; index: SiteIndex; siteConfig: SiteConfig }) => void;
810
+ }) {
811
+ const { historyState } = useEditorContext();
812
+ if (!historyState) return null;
813
+
814
+ return (
815
+ <RestoreModal
816
+ isOpen={showRestoreModal}
817
+ onClose={() => setShowRestoreModal(false)}
818
+ date={historyState.date}
819
+ onConfirm={() => onRestore({
820
+ sections: historyState.sections,
821
+ index: historyState.index,
822
+ siteConfig: historyState.siteConfig,
823
+ })}
824
+ isRestoring={isRestoring}
825
+ />
826
+ );
827
+ }
828
+
703
829
  function EditorContent({
704
830
  sections,
705
831
  audiences,
@@ -970,6 +1096,7 @@ function EditorToolbar({
970
1096
  buildState,
971
1097
  buildElapsed,
972
1098
  onBuildDismiss,
1099
+ onRestoreClick,
973
1100
  }: {
974
1101
  buttonState: "synced" | "publish" | "saveAndPublish";
975
1102
  localChangesExist: boolean;
@@ -985,105 +1112,122 @@ function EditorToolbar({
985
1112
  buildState: "idle" | "building" | "ready" | "error" | "fading";
986
1113
  buildElapsed: number;
987
1114
  onBuildDismiss: () => void;
1115
+ onRestoreClick: () => void;
988
1116
  }) {
989
- const { isEditMode, viewBranch, setViewBranch, toggleEditMode } = useEditorContext();
1117
+ const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState } = useEditorContext();
1118
+
1119
+ if (historyState) {
1120
+ return (
1121
+ <HistoryToolbar
1122
+ date={historyState.date}
1123
+ onBackToCurrent={() => setHistoryState(null)}
1124
+ onRestore={onRestoreClick}
1125
+ />
1126
+ );
1127
+ }
990
1128
 
991
1129
  return (
992
1130
  <>
993
1131
  {isEditMode && (
994
- <div className="fixed top-0 right-0 left-0 z-50 grid grid-cols-3 items-center border-b border-base-200 bg-base px-4 py-2">
995
- <div className="flex items-center gap-2">
996
- {buttonState === "saveAndPublish" && (
997
- <SplitButton
998
- label="Save & Publish"
999
- onClick={onSaveAndPublish}
1000
- disabled={publishAction !== "idle"}
1001
- options={[{ label: "Save", onClick: onSave }]}
1132
+ <div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
1133
+ <div className="mx-auto max-w-screen-xl grid grid-cols-3 items-center px-4 py-2">
1134
+ <div className="flex items-center gap-2">
1135
+ {buttonState === "saveAndPublish" && (
1136
+ <SplitButton
1137
+ label="Save & Publish"
1138
+ onClick={onSaveAndPublish}
1139
+ disabled={publishAction !== "idle"}
1140
+ options={[{ label: "Save", onClick: onSave }]}
1141
+ />
1142
+ )}
1143
+ {buttonState === "publish" && (
1144
+ <SplitButton
1145
+ label="Publish"
1146
+ onClick={onPublish}
1147
+ disabled={publishAction !== "idle"}
1148
+ options={[]}
1149
+ />
1150
+ )}
1151
+ {buttonState === "synced" && (
1152
+ <SplitButton
1153
+ label="Up to date"
1154
+ onClick={() => {}}
1155
+ disabled
1156
+ options={[]}
1157
+ />
1158
+ )}
1159
+ {localChangesExist && publishAction === "idle" && (
1160
+ <Button
1161
+ type="button"
1162
+ variant="destructive"
1163
+ onClick={onDiscardClick}
1164
+ >
1165
+ Discard Changes
1166
+ </Button>
1167
+ )}
1168
+ </div>
1169
+ <div className="flex items-center justify-center">
1170
+ <StatusText
1171
+ publishAction={publishAction}
1172
+ publishFeedback={publishFeedback}
1173
+ buildState={buildState}
1174
+ buildElapsed={buildElapsed}
1175
+ onDismiss={onBuildDismiss}
1002
1176
  />
1003
- )}
1004
- {buttonState === "publish" && (
1005
- <SplitButton
1006
- label="Publish"
1007
- onClick={onPublish}
1008
- disabled={publishAction !== "idle"}
1009
- options={[]}
1177
+ </div>
1178
+ <div className="flex items-center justify-end gap-2">
1179
+ <ProcessingIndicator items={processingItems} />
1180
+ <IconButton
1181
+ icon={<ImageIcon size={16} />}
1182
+ label="Media library"
1183
+ size="md"
1184
+ onClick={onMediaClick}
1185
+ className="border border-base-200 bg-base-accent"
1010
1186
  />
1011
- )}
1012
- {buttonState === "synced" && (
1013
- <SplitButton
1014
- label="Up to date"
1015
- onClick={() => {}}
1016
- disabled
1017
- options={[]}
1187
+ <IconButton
1188
+ icon={<SettingsIcon size={16} />}
1189
+ label="Site settings"
1190
+ size="md"
1191
+ onClick={onSettingsClick}
1192
+ className="border border-base-200 bg-base-accent"
1018
1193
  />
1019
- )}
1020
- {localChangesExist && publishAction === "idle" && (
1021
- <Button
1022
- type="button"
1023
- variant="destructive"
1024
- onClick={onDiscardClick}
1025
- >
1026
- Discard Changes
1027
- </Button>
1028
- )}
1029
- </div>
1030
- <div className="flex items-center justify-center">
1031
- <StatusText
1032
- publishAction={publishAction}
1033
- publishFeedback={publishFeedback}
1034
- buildState={buildState}
1035
- buildElapsed={buildElapsed}
1036
- onDismiss={onBuildDismiss}
1037
- />
1038
- </div>
1039
- <div className="flex items-center justify-end gap-2">
1040
- <ProcessingIndicator items={processingItems} />
1041
- <IconButton
1042
- icon={<ImageIcon size={16} />}
1043
- label="Media library"
1044
- size="md"
1045
- onClick={onMediaClick}
1046
- className="border border-base-200 bg-base-accent"
1047
- />
1048
- <IconButton
1049
- icon={<SettingsIcon size={16} />}
1050
- label="Site settings"
1051
- size="md"
1052
- onClick={onSettingsClick}
1053
- className="border border-base-200 bg-base-accent"
1054
- />
1055
- <ShowAllChromeToggle />
1194
+ <ShowAllChromeToggle />
1195
+ </div>
1056
1196
  </div>
1057
1197
  </div>
1058
1198
  )}
1059
1199
  {!isEditMode && (
1060
- <div className="fixed top-0 right-0 left-0 z-50 flex items-center justify-center border-b border-base-200 bg-base px-4 py-2">
1061
- <SegmentedControl
1062
- options={[
1063
- { value: "saved", label: "Draft" },
1064
- { value: "live", label: "Live" },
1065
- ]}
1066
- value={viewBranch}
1067
- onChange={(v) => setViewBranch(v as "saved" | "live")}
1068
- />
1200
+ <div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
1201
+ <div className="mx-auto max-w-screen-xl flex items-center justify-center px-4 py-2">
1202
+ <SegmentedControl
1203
+ options={[
1204
+ { value: "saved", label: "Draft" },
1205
+ { value: "live", label: "Live" },
1206
+ ]}
1207
+ value={viewBranch}
1208
+ onChange={(v) => setViewBranch(v as "saved" | "live")}
1209
+ />
1210
+ </div>
1069
1211
  </div>
1070
1212
  )}
1071
- <button
1072
- onClick={toggleEditMode}
1073
- className="cursor-pointer fixed bottom-4 right-4 z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity"
1074
- aria-label={isEditMode ? "Switch to view mode" : "Switch to edit mode"}
1075
- >
1076
- {isEditMode ? (
1077
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
1078
- <path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
1079
- <path fillRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clipRule="evenodd" />
1080
- </svg>
1081
- ) : (
1082
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
1083
- <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
1084
- </svg>
1085
- )}
1086
- </button>
1213
+ {!historyState && (
1214
+ <button
1215
+ onClick={toggleEditMode}
1216
+ className="cursor-pointer fixed bottom-4 right-4 lg:right-auto z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity fab-container-right"
1217
+ aria-label={isEditMode ? "Switch to view mode" : "Switch to edit mode"}
1218
+ >
1219
+ {isEditMode ? (
1220
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
1221
+ <path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
1222
+ <path fillRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clipRule="evenodd" />
1223
+ </svg>
1224
+ ) : (
1225
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
1226
+ <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
1227
+ </svg>
1228
+ )}
1229
+ </button>
1230
+ )}
1087
1231
  </>
1088
1232
  );
1089
1233
  }
@@ -0,0 +1,32 @@
1
+ import { formatDateTime } from "../../lib/timestamp";
2
+ import { Button } from "../shared/Button";
3
+
4
+ interface HistoryToolbarProps {
5
+ date: string;
6
+ onBackToCurrent: () => void;
7
+ onRestore: () => void;
8
+ }
9
+
10
+ export function HistoryToolbar({ date, onBackToCurrent, onRestore }: HistoryToolbarProps) {
11
+ return (
12
+ <div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
13
+ <div className="mx-auto max-w-screen-xl grid grid-cols-3 items-center px-4 py-2">
14
+ <div className="flex items-center">
15
+ <Button variant="secondary" size="sm" onClick={onBackToCurrent}>
16
+ Back to current
17
+ </Button>
18
+ </div>
19
+ <div className="flex items-center justify-center">
20
+ <span className="text-xs font-medium text-base-contrast-light">
21
+ Viewing {formatDateTime(date)}
22
+ </span>
23
+ </div>
24
+ <div className="flex items-center justify-end">
25
+ <Button variant="primary" size="sm" onClick={onRestore}>
26
+ Restore this version
27
+ </Button>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,30 @@
1
+ import { formatDateTime } from "../../lib/timestamp";
2
+ import { EditorModal } from "./EditorModal";
3
+ import { Button } from "../shared/Button";
4
+
5
+ interface RestoreModalProps {
6
+ isOpen: boolean;
7
+ onClose: () => void;
8
+ date: string;
9
+ onConfirm: () => void;
10
+ isRestoring: boolean;
11
+ }
12
+
13
+ export function RestoreModal({ isOpen, onClose, date, onConfirm, isRestoring }: RestoreModalProps) {
14
+ return (
15
+ <EditorModal isOpen={isOpen} onClose={onClose} title="Restore this version?">
16
+ <p className="mb-4 text-sm text-base-contrast-light">
17
+ This will publish the content from {formatDateTime(date)} as a new update.
18
+ Your current content will still be available in the history.
19
+ </p>
20
+ <div className="flex justify-end gap-3">
21
+ <Button variant="secondary" size="md" onClick={onClose} disabled={isRestoring}>
22
+ Cancel
23
+ </Button>
24
+ <Button variant="primary" size="md" onClick={onConfirm} disabled={isRestoring}>
25
+ {isRestoring ? "Restoring..." : "Restore"}
26
+ </Button>
27
+ </div>
28
+ </EditorModal>
29
+ );
30
+ }
package/src/lib/events.ts CHANGED
@@ -21,3 +21,4 @@ export function createEvent<T>(name: string): TypedEvent<T> {
21
21
  export const editModeEvent = createEvent<{ isEditMode: boolean }>("editmodechange");
22
22
  export const navChangeEvent = createEvent<NavItem[]>("sitenavchange");
23
23
  export const darkModeEvent = createEvent<string>("sitedarkmode");
24
+ export const historySelectEvent = createEvent<{ sha: string; date: string }>("history-select");
@@ -1,3 +1,26 @@
1
+ export function formatDate(iso: string): string {
2
+ return new Date(iso).toLocaleDateString("en-US", {
3
+ month: "short",
4
+ day: "numeric",
5
+ year: "numeric",
6
+ });
7
+ }
8
+
9
+ export function formatDateTime(iso: string): string {
10
+ const date = new Date(iso);
11
+ const datePart = date.toLocaleDateString("en-US", {
12
+ month: "short",
13
+ day: "numeric",
14
+ year: "numeric",
15
+ });
16
+ const timePart = date.toLocaleTimeString("en-US", {
17
+ hour: "numeric",
18
+ minute: "2-digit",
19
+ hour12: true,
20
+ });
21
+ return `${datePart} | ${timePart}`;
22
+ }
23
+
1
24
  export function formatTimestamp(iso: string): string {
2
25
  const date = new Date(iso);
3
26
  const now = new Date();