@drawnagency/primitives 0.1.23 → 0.1.25

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 (29) hide show
  1. package/dist/{chunk-46QI4FDZ.js → chunk-KX7NRYQD.js} +1 -0
  2. package/dist/components/brandguide/ColorSwatchSettings.d.ts.map +1 -1
  3. package/dist/components/shared/HistoryPopover.d.ts +6 -0
  4. package/dist/components/shared/HistoryPopover.d.ts.map +1 -0
  5. package/dist/components/shared/Navigation.d.ts +2 -1
  6. package/dist/components/shared/Navigation.d.ts.map +1 -1
  7. package/dist/components/shell/EditorContext.d.ts +11 -0
  8. package/dist/components/shell/EditorContext.d.ts.map +1 -1
  9. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  10. package/dist/components/shell/HistoryToolbar.d.ts +8 -0
  11. package/dist/components/shell/HistoryToolbar.d.ts.map +1 -0
  12. package/dist/components/shell/RestoreModal.d.ts +10 -0
  13. package/dist/components/shell/RestoreModal.d.ts.map +1 -0
  14. package/dist/hooks/useBuildStatus.d.ts +1 -1
  15. package/dist/hooks/useBuildStatus.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/lib/events.d.ts +4 -0
  18. package/dist/lib/events.d.ts.map +1 -1
  19. package/dist/lib/index.js +1 -1
  20. package/package.json +1 -1
  21. package/src/components/brandguide/ColorSwatchSettings.tsx +18 -4
  22. package/src/components/shared/HistoryPopover.tsx +135 -0
  23. package/src/components/shared/Navigation.tsx +78 -31
  24. package/src/components/shell/EditorContext.tsx +17 -1
  25. package/src/components/shell/EditorShell.tsx +255 -108
  26. package/src/components/shell/HistoryToolbar.tsx +37 -0
  27. package/src/components/shell/RestoreModal.tsx +35 -0
  28. package/src/hooks/useBuildStatus.ts +7 -3
  29. package/src/lib/events.ts +1 -0
@@ -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":"AAUA,UAAU,mBAAmB;IAC3B,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAiCD,wBAAgB,cAAc,CAAC,EAAE,cAAc,EAAE,EAAE,mBAAmB,2CAyFrE"}
@@ -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;AAM7C,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,2CAkOvG"}
@@ -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":"AAEA,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,2CA4BvF"}
@@ -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":"AAGA,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,2CAuBhG"}
@@ -1,4 +1,4 @@
1
- type BuildState = "idle" | "building" | "ready" | "error";
1
+ type BuildState = "idle" | "building" | "ready" | "error" | "fading";
2
2
  interface BuildStatusResult {
3
3
  state: BuildState;
4
4
  deployUrl: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"useBuildStatus.d.ts","sourceRoot":"","sources":["../../src/hooks/useBuildStatus.ts"],"names":[],"mappings":"AAEA,KAAK,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;AAS1D,UAAU,iBAAiB;IACzB,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAKD,wBAAgB,cAAc,IAAI,iBAAiB,CAsHlD"}
1
+ {"version":3,"file":"useBuildStatus.d.ts","sourceRoot":"","sources":["../../src/hooks/useBuildStatus.ts"],"names":[],"mappings":"AAEA,KAAK,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AASrE,UAAU,iBAAiB;IACzB,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAMD,wBAAgB,cAAc,IAAI,iBAAiB,CAyHlD"}
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-KX7NRYQD.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-KX7NRYQD.js";
21
21
  import {
22
22
  clearRegistry,
23
23
  createRegistry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawnagency/primitives",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
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,135 @@
1
+ import { useState, useCallback, useEffect } from "react";
2
+ import { cn } from "../../lib/cn";
3
+ import { Button } from "./Button";
4
+
5
+ interface CommitItem {
6
+ sha: string;
7
+ date: string;
8
+ message: string;
9
+ }
10
+
11
+ interface HistoryPopoverProps {
12
+ onSelectCommit: (sha: string, date: string) => void;
13
+ }
14
+
15
+ function formatRelativeDate(dateStr: string): string {
16
+ const date = new Date(dateStr);
17
+ const now = new Date();
18
+ const diffMs = now.getTime() - date.getTime();
19
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
20
+
21
+ if (diffDays === 0) return "Today";
22
+ if (diffDays === 1) return "Yesterday";
23
+ if (diffDays < 7) return `${diffDays} days ago`;
24
+ if (diffDays < 30) {
25
+ const weeks = Math.floor(diffDays / 7);
26
+ return `${weeks} ${weeks === 1 ? "week" : "weeks"} ago`;
27
+ }
28
+ if (diffDays < 365) {
29
+ const months = Math.floor(diffDays / 30);
30
+ return `${months} ${months === 1 ? "month" : "months"} ago`;
31
+ }
32
+ const years = Math.floor(diffDays / 365);
33
+ return `${years} ${years === 1 ? "year" : "years"} ago`;
34
+ }
35
+
36
+ function formatAbsoluteDate(dateStr: string): string {
37
+ return new Date(dateStr).toLocaleDateString("en-US", {
38
+ month: "short",
39
+ day: "numeric",
40
+ year: "numeric",
41
+ hour: "numeric",
42
+ minute: "2-digit",
43
+ });
44
+ }
45
+
46
+ export function HistoryPopover({ onSelectCommit }: HistoryPopoverProps) {
47
+ const [commits, setCommits] = useState<CommitItem[]>([]);
48
+ const [loading, setLoading] = useState(true);
49
+ const [loadingMore, setLoadingMore] = useState(false);
50
+ const [page, setPage] = useState(1);
51
+ const [hasMore, setHasMore] = useState(true);
52
+ const [error, setError] = useState<string | null>(null);
53
+
54
+ const fetchPage = useCallback(async (pageNum: number) => {
55
+ const isFirst = pageNum === 1;
56
+ if (isFirst) setLoading(true);
57
+ else setLoadingMore(true);
58
+
59
+ try {
60
+ const res = await fetch(`/api/history?page=${pageNum}&per_page=20`);
61
+ if (!res.ok) throw new Error(`Failed to load history: ${res.status}`);
62
+ const data = await res.json();
63
+ const items = data.items as CommitItem[];
64
+ setCommits((prev) => isFirst ? items : [...prev, ...items]);
65
+ setHasMore(items.length === 20);
66
+ setPage(pageNum);
67
+ } catch (err) {
68
+ setError(err instanceof Error ? err.message : "Failed to load history");
69
+ } finally {
70
+ setLoading(false);
71
+ setLoadingMore(false);
72
+ }
73
+ }, []);
74
+
75
+ useEffect(() => {
76
+ fetchPage(1);
77
+ }, [fetchPage]);
78
+
79
+ if (loading) {
80
+ return (
81
+ <div className="p-4 text-center text-xs text-base-contrast-light">
82
+ Loading history...
83
+ </div>
84
+ );
85
+ }
86
+
87
+ if (error) {
88
+ return (
89
+ <div className="p-4 text-center text-xs text-red-600">
90
+ {error}
91
+ </div>
92
+ );
93
+ }
94
+
95
+ if (commits.length === 0) {
96
+ return (
97
+ <div className="p-4 text-center text-xs text-base-contrast-light">
98
+ No history found
99
+ </div>
100
+ );
101
+ }
102
+
103
+ return (
104
+ <div className="max-h-64 overflow-y-auto">
105
+ <ul className="py-1">
106
+ {commits.map((commit) => (
107
+ <li key={commit.sha}>
108
+ <button
109
+ onClick={() => onSelectCommit(commit.sha, commit.date)}
110
+ className={cn(
111
+ "w-full cursor-pointer px-4 py-2 text-left text-xs transition-colors",
112
+ "hover:bg-base-accent text-base-contrast-light hover:text-base-contrast",
113
+ )}
114
+ title={formatAbsoluteDate(commit.date)}
115
+ >
116
+ {formatRelativeDate(commit.date)}
117
+ </button>
118
+ </li>
119
+ ))}
120
+ </ul>
121
+ {hasMore && (
122
+ <div className="border-t border-base-200 p-2 text-center">
123
+ <Button
124
+ variant="ghost"
125
+ size="sm"
126
+ onClick={() => fetchPage(page + 1)}
127
+ disabled={loadingMore}
128
+ >
129
+ {loadingMore ? "Loading..." : "Load more"}
130
+ </Button>
131
+ </div>
132
+ )}
133
+ </div>
134
+ );
135
+ }
@@ -1,22 +1,27 @@
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 { Popover } from "./Popover";
8
+ import { HistoryPopover } from "./HistoryPopover";
7
9
 
8
10
  interface Props {
9
11
  navLinks: NavItem[];
10
12
  siteName: string;
11
13
  darkMode: "light" | "dark" | "optional";
14
+ lastUpdated: string | null;
12
15
  }
13
16
 
14
- export default function Navigation({ navLinks: initialNavLinks, siteName, darkMode }: Props) {
17
+ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMode, lastUpdated }: Props) {
15
18
  const [isOpen, setIsOpen] = useState(false);
16
- const [isEditMode, setIsEditMode] = useState(false);
19
+ const [isEditMode, setIsEditMode] = useState(() => typeof window !== "undefined" && window.location.pathname.startsWith("/edit"));
17
20
  const [currentDarkMode, setCurrentDarkMode] = useState(darkMode);
18
21
  const [isDark, setIsDark] = useState(false);
19
22
  const [navLinks, setNavLinks] = useState<NavItem[]>(initialNavLinks);
23
+ const [showHistory, setShowHistory] = useState(false);
24
+ const historyButtonRef = useRef<HTMLButtonElement>(null);
20
25
 
21
26
  useEffect(() => {
22
27
  const unlistenEdit = editModeEvent.listen(({ isEditMode }) => setIsEditMode(isEditMode));
@@ -73,20 +78,22 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
73
78
  return (
74
79
  <>
75
80
  {/* 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>
81
+ <header className="fixed top-0 left-0 right-0 z-50 bg-base lg:hidden">
82
+ <div className="mx-auto max-w-screen-xl flex h-16 items-center justify-between px-4">
83
+ <span className="text-lg font-bold text-primary">{siteName}</span>
84
+ <button
85
+ onClick={() => setIsOpen(!isOpen)}
86
+ className="cursor-pointer p-2 text-base-contrast"
87
+ aria-label="Toggle navigation"
88
+ >
89
+ <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
90
+ {isOpen
91
+ ? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
92
+ : <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
93
+ }
94
+ </svg>
95
+ </button>
96
+ </div>
90
97
  </header>
91
98
 
92
99
  {/* Backdrop */}
@@ -100,7 +107,7 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
100
107
  {/* Sidebar nav */}
101
108
  <nav
102
109
  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",
110
+ "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
111
  isOpen ? "translate-x-0" : "-translate-x-full",
105
112
  )}
106
113
  >
@@ -177,19 +184,59 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
177
184
  })}
178
185
  </ul>
179
186
 
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>
187
+ <div className="mt-auto">
188
+ {currentDarkMode === "optional" && (
189
+ <div className="mx-4 border-t border-base-200 py-4">
190
+ <div className="flex items-center gap-2 text-sm text-base-contrast-light">
191
+ <span className={cn(!isDark && "text-base-contrast")}>Light</span>
192
+ <Toggle checked={isDark} onChange={handleThemeToggle} label="Toggle dark mode" />
193
+ <span className={cn(isDark && "text-base-contrast")}>Dark</span>
194
+ </div>
190
195
  </div>
191
- </div>
192
- )}
196
+ )}
197
+
198
+ {lastUpdated && (
199
+ <div className={cn(currentDarkMode !== "optional" && "border-t border-base-200", "mx-4 py-4 text-center")}>
200
+ {isEditMode ? (
201
+ <div className="relative">
202
+ <button
203
+ ref={historyButtonRef}
204
+ onClick={() => setShowHistory((prev) => !prev)}
205
+ className="cursor-pointer text-xs text-base-contrast-light hover:text-primary transition-colors"
206
+ aria-label="View history"
207
+ >
208
+ Last updated {new Date(lastUpdated).toLocaleDateString("en-US", {
209
+ month: "short",
210
+ day: "numeric",
211
+ year: "numeric",
212
+ })}
213
+ </button>
214
+ <Popover
215
+ isOpen={showHistory}
216
+ onClose={() => setShowHistory(false)}
217
+ anchorRef={historyButtonRef}
218
+ className="w-56 !bottom-full !top-auto !mb-1 !mt-0"
219
+ >
220
+ <HistoryPopover
221
+ onSelectCommit={(sha, date) => {
222
+ setShowHistory(false);
223
+ historySelectEvent.dispatch({ sha, date });
224
+ }}
225
+ />
226
+ </Popover>
227
+ </div>
228
+ ) : (
229
+ <p className="text-xs text-base-contrast-light">
230
+ Last updated {new Date(lastUpdated).toLocaleDateString("en-US", {
231
+ month: "short",
232
+ day: "numeric",
233
+ year: "numeric",
234
+ })}
235
+ </p>
236
+ )}
237
+ </div>
238
+ )}
239
+ </div>
193
240
  </nav>
194
241
  </>
195
242
  );
@@ -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,
@@ -905,7 +1031,7 @@ function StatusText({
905
1031
  }: {
906
1032
  publishAction: "idle" | "saving" | "publishing";
907
1033
  publishFeedback: string | null;
908
- buildState: "idle" | "building" | "ready" | "error";
1034
+ buildState: "idle" | "building" | "ready" | "error" | "fading";
909
1035
  buildElapsed: number;
910
1036
  onDismiss: () => void;
911
1037
  }) {
@@ -923,9 +1049,12 @@ function StatusText({
923
1049
  </span>
924
1050
  );
925
1051
  }
926
- if (buildState === "ready") {
1052
+ if (buildState === "ready" || buildState === "fading") {
927
1053
  return (
928
- <span className="inline-flex items-center gap-1.5 text-xs font-medium text-green-600">
1054
+ <span className={cn(
1055
+ "inline-flex items-center gap-1.5 text-xs font-medium text-green-600 transition-opacity duration-1000",
1056
+ buildState === "fading" ? "opacity-0" : "opacity-100",
1057
+ )}>
929
1058
  Published in {formatElapsed(buildElapsed)}
930
1059
  <DismissButton onClick={onDismiss} />
931
1060
  </span>
@@ -967,6 +1096,7 @@ function EditorToolbar({
967
1096
  buildState,
968
1097
  buildElapsed,
969
1098
  onBuildDismiss,
1099
+ onRestoreClick,
970
1100
  }: {
971
1101
  buttonState: "synced" | "publish" | "saveAndPublish";
972
1102
  localChangesExist: boolean;
@@ -979,108 +1109,125 @@ function EditorToolbar({
979
1109
  onSettingsClick: () => void;
980
1110
  onMediaClick: () => void;
981
1111
  processingItems: QueueItem[];
982
- buildState: "idle" | "building" | "ready" | "error";
1112
+ buildState: "idle" | "building" | "ready" | "error" | "fading";
983
1113
  buildElapsed: number;
984
1114
  onBuildDismiss: () => void;
1115
+ onRestoreClick: () => void;
985
1116
  }) {
986
- 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
+ }
987
1128
 
988
1129
  return (
989
1130
  <>
990
1131
  {isEditMode && (
991
- <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">
992
- <div className="flex items-center gap-2">
993
- {buttonState === "saveAndPublish" && (
994
- <SplitButton
995
- label="Save & Publish"
996
- onClick={onSaveAndPublish}
997
- disabled={publishAction !== "idle"}
998
- 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}
999
1176
  />
1000
- )}
1001
- {buttonState === "publish" && (
1002
- <SplitButton
1003
- label="Publish"
1004
- onClick={onPublish}
1005
- disabled={publishAction !== "idle"}
1006
- 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"
1007
1186
  />
1008
- )}
1009
- {buttonState === "synced" && (
1010
- <SplitButton
1011
- label="Up to date"
1012
- onClick={() => {}}
1013
- disabled
1014
- 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"
1015
1193
  />
1016
- )}
1017
- {localChangesExist && publishAction === "idle" && (
1018
- <Button
1019
- type="button"
1020
- variant="destructive"
1021
- onClick={onDiscardClick}
1022
- >
1023
- Discard Changes
1024
- </Button>
1025
- )}
1026
- </div>
1027
- <div className="flex items-center justify-center">
1028
- <StatusText
1029
- publishAction={publishAction}
1030
- publishFeedback={publishFeedback}
1031
- buildState={buildState}
1032
- buildElapsed={buildElapsed}
1033
- onDismiss={onBuildDismiss}
1034
- />
1035
- </div>
1036
- <div className="flex items-center justify-end gap-2">
1037
- <ProcessingIndicator items={processingItems} />
1038
- <IconButton
1039
- icon={<ImageIcon size={16} />}
1040
- label="Media library"
1041
- size="md"
1042
- onClick={onMediaClick}
1043
- className="border border-base-200 bg-base-accent"
1044
- />
1045
- <IconButton
1046
- icon={<SettingsIcon size={16} />}
1047
- label="Site settings"
1048
- size="md"
1049
- onClick={onSettingsClick}
1050
- className="border border-base-200 bg-base-accent"
1051
- />
1052
- <ShowAllChromeToggle />
1194
+ <ShowAllChromeToggle />
1195
+ </div>
1053
1196
  </div>
1054
1197
  </div>
1055
1198
  )}
1056
1199
  {!isEditMode && (
1057
- <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">
1058
- <SegmentedControl
1059
- options={[
1060
- { value: "saved", label: "Draft" },
1061
- { value: "live", label: "Live" },
1062
- ]}
1063
- value={viewBranch}
1064
- onChange={(v) => setViewBranch(v as "saved" | "live")}
1065
- />
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>
1066
1211
  </div>
1067
1212
  )}
1068
- <button
1069
- onClick={toggleEditMode}
1070
- 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"
1071
- aria-label={isEditMode ? "Switch to view mode" : "Switch to edit mode"}
1072
- >
1073
- {isEditMode ? (
1074
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
1075
- <path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
1076
- <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" />
1077
- </svg>
1078
- ) : (
1079
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
1080
- <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" />
1081
- </svg>
1082
- )}
1083
- </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
+ )}
1084
1231
  </>
1085
1232
  );
1086
1233
  }
@@ -0,0 +1,37 @@
1
+ import { Button } from "../shared/Button";
2
+
3
+ interface HistoryToolbarProps {
4
+ date: string;
5
+ onBackToCurrent: () => void;
6
+ onRestore: () => void;
7
+ }
8
+
9
+ export function HistoryToolbar({ date, onBackToCurrent, onRestore }: HistoryToolbarProps) {
10
+ const formattedDate = new Date(date).toLocaleDateString("en-US", {
11
+ month: "short",
12
+ day: "numeric",
13
+ year: "numeric",
14
+ });
15
+
16
+ return (
17
+ <div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
18
+ <div className="mx-auto max-w-screen-xl grid grid-cols-3 items-center px-4 py-2">
19
+ <div className="flex items-center">
20
+ <Button variant="secondary" size="sm" onClick={onBackToCurrent}>
21
+ Back to current
22
+ </Button>
23
+ </div>
24
+ <div className="flex items-center justify-center">
25
+ <span className="text-xs font-medium text-base-contrast-light">
26
+ Viewing {formattedDate}
27
+ </span>
28
+ </div>
29
+ <div className="flex items-center justify-end">
30
+ <Button variant="primary" size="sm" onClick={onRestore}>
31
+ Restore this version
32
+ </Button>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,35 @@
1
+ import { EditorModal } from "./EditorModal";
2
+ import { Button } from "../shared/Button";
3
+
4
+ interface RestoreModalProps {
5
+ isOpen: boolean;
6
+ onClose: () => void;
7
+ date: string;
8
+ onConfirm: () => void;
9
+ isRestoring: boolean;
10
+ }
11
+
12
+ export function RestoreModal({ isOpen, onClose, date, onConfirm, isRestoring }: RestoreModalProps) {
13
+ const formattedDate = new Date(date).toLocaleDateString("en-US", {
14
+ month: "short",
15
+ day: "numeric",
16
+ year: "numeric",
17
+ });
18
+
19
+ return (
20
+ <EditorModal isOpen={isOpen} onClose={onClose} title="Restore this version?">
21
+ <p className="mb-4 text-sm text-base-contrast-light">
22
+ This will publish the content from {formattedDate} as a new update.
23
+ Your current content will still be available in the history.
24
+ </p>
25
+ <div className="flex justify-end gap-3">
26
+ <Button variant="secondary" size="md" onClick={onClose} disabled={isRestoring}>
27
+ Cancel
28
+ </Button>
29
+ <Button variant="primary" size="md" onClick={onConfirm} disabled={isRestoring}>
30
+ {isRestoring ? "Restoring..." : "Restore"}
31
+ </Button>
32
+ </div>
33
+ </EditorModal>
34
+ );
35
+ }
@@ -1,6 +1,6 @@
1
1
  import { useState, useCallback, useEffect, useRef } from "react";
2
2
 
3
- type BuildState = "idle" | "building" | "ready" | "error";
3
+ type BuildState = "idle" | "building" | "ready" | "error" | "fading";
4
4
 
5
5
  interface BuildStatusResponse {
6
6
  state: "building" | "ready" | "error";
@@ -18,7 +18,8 @@ interface BuildStatusResult {
18
18
  }
19
19
 
20
20
  const POLL_INTERVAL = 5000;
21
- const AUTO_CLEAR_DELAY = 5000;
21
+ const AUTO_CLEAR_DELAY = 10000;
22
+ const FADE_DURATION = 1000;
22
23
 
23
24
  export function useBuildStatus(): BuildStatusResult {
24
25
  const [state, setState] = useState<BuildState>("idle");
@@ -81,7 +82,10 @@ export function useBuildStatus(): BuildStatusResult {
81
82
  stopPolling();
82
83
  stopTimer();
83
84
  if (data.state === "ready") {
84
- clearRef.current = setTimeout(() => setState("idle"), AUTO_CLEAR_DELAY);
85
+ clearRef.current = setTimeout(() => {
86
+ setState("fading");
87
+ clearRef.current = setTimeout(() => setState("idle"), FADE_DURATION);
88
+ }, AUTO_CLEAR_DELAY);
85
89
  }
86
90
  }
87
91
  },
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");