@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.
- package/dist/{chunk-46QI4FDZ.js → chunk-KX7NRYQD.js} +1 -0
- package/dist/components/brandguide/ColorSwatchSettings.d.ts.map +1 -1
- package/dist/components/shared/HistoryPopover.d.ts +6 -0
- package/dist/components/shared/HistoryPopover.d.ts.map +1 -0
- package/dist/components/shared/Navigation.d.ts +2 -1
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shell/EditorContext.d.ts +11 -0
- package/dist/components/shell/EditorContext.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/HistoryToolbar.d.ts +8 -0
- package/dist/components/shell/HistoryToolbar.d.ts.map +1 -0
- package/dist/components/shell/RestoreModal.d.ts +10 -0
- package/dist/components/shell/RestoreModal.d.ts.map +1 -0
- package/dist/hooks/useBuildStatus.d.ts +1 -1
- package/dist/hooks/useBuildStatus.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/events.d.ts +4 -0
- package/dist/lib/events.d.ts.map +1 -1
- package/dist/lib/index.js +1 -1
- package/package.json +1 -1
- package/src/components/brandguide/ColorSwatchSettings.tsx +18 -4
- package/src/components/shared/HistoryPopover.tsx +135 -0
- package/src/components/shared/Navigation.tsx +78 -31
- package/src/components/shell/EditorContext.tsx +17 -1
- package/src/components/shell/EditorShell.tsx +255 -108
- package/src/components/shell/HistoryToolbar.tsx +37 -0
- package/src/components/shell/RestoreModal.tsx +35 -0
- package/src/hooks/useBuildStatus.ts +7 -3
- 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":"
|
|
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;
|
|
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;
|
|
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;
|
|
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 +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;
|
|
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
package/dist/lib/events.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/events.d.ts.map
CHANGED
|
@@ -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
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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={
|
|
22
|
-
onChange={(
|
|
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(
|
|
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
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
<div className="
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
checked={isDark}
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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=
|
|
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
|
|
992
|
-
<div className="
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
-
|
|
1002
|
-
<
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
onClick={
|
|
1013
|
-
|
|
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
|
-
|
|
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
|
|
1058
|
-
<
|
|
1059
|
-
|
|
1060
|
-
{
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
<
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
<
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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 =
|
|
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(() =>
|
|
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");
|