@drawnagency/primitives 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-46QI4FDZ.js → chunk-FSVPD7TW.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/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/dist/lib/timestamp.d.ts +2 -0
- package/dist/lib/timestamp.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/brandguide/ColorSwatchSettings.tsx +18 -4
- package/src/components/shared/HistoryPopover.tsx +104 -0
- package/src/components/shared/Navigation.tsx +75 -31
- package/src/components/shell/EditorContext.tsx +17 -1
- package/src/components/shell/EditorShell.tsx +248 -104
- package/src/components/shell/HistoryToolbar.tsx +32 -0
- package/src/components/shell/RestoreModal.tsx +30 -0
- package/src/lib/events.ts +1 -0
- package/src/lib/timestamp.ts +23 -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":"AAWA,UAAU,mBAAmB;IAC3B,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED,wBAAgB,cAAc,CAAC,EAAE,cAAc,EAAE,EAAE,mBAAmB,2CAwFrE"}
|
|
@@ -3,7 +3,8 @@ interface Props {
|
|
|
3
3
|
navLinks: NavItem[];
|
|
4
4
|
siteName: string;
|
|
5
5
|
darkMode: "light" | "dark" | "optional";
|
|
6
|
+
lastUpdated: string | null;
|
|
6
7
|
}
|
|
7
|
-
export default function Navigation({ navLinks: initialNavLinks, siteName, darkMode }: Props): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export default function Navigation({ navLinks: initialNavLinks, siteName, darkMode, lastUpdated }: Props): import("react/jsx-runtime").JSX.Element;
|
|
8
9
|
export {};
|
|
9
10
|
//# sourceMappingURL=Navigation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Navigation.d.ts","sourceRoot":"","sources":["../../../src/components/shared/Navigation.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"Navigation.d.ts","sourceRoot":"","sources":["../../../src/components/shared/Navigation.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAO7C,UAAU,KAAK;IACb,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;IACxC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,KAAK,2CA8NvG"}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
import type { LoadedSection } from "../../lib/loader";
|
|
3
|
+
import type { SiteIndex, SiteConfig } from "../../schemas/site-config";
|
|
4
|
+
export type HistoryState = {
|
|
5
|
+
sha: string;
|
|
6
|
+
date: string;
|
|
7
|
+
sections: LoadedSection[];
|
|
8
|
+
index: SiteIndex;
|
|
9
|
+
siteConfig: SiteConfig;
|
|
10
|
+
} | null;
|
|
2
11
|
interface EditorContextValue {
|
|
3
12
|
isEditMode: boolean;
|
|
4
13
|
showAllChrome: boolean;
|
|
5
14
|
viewBranch: "saved" | "live";
|
|
15
|
+
historyState: HistoryState;
|
|
6
16
|
toggleEditMode: () => void;
|
|
7
17
|
toggleShowAllChrome: () => void;
|
|
8
18
|
setViewBranch: (branch: "saved" | "live") => void;
|
|
19
|
+
setHistoryState: (state: HistoryState) => void;
|
|
9
20
|
}
|
|
10
21
|
interface EditorProviderProps {
|
|
11
22
|
children: ReactNode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorContext.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
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":"AAGA,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,EAAE,mBAAmB,2CAsBvF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface RestoreModalProps {
|
|
2
|
+
isOpen: boolean;
|
|
3
|
+
onClose: () => void;
|
|
4
|
+
date: string;
|
|
5
|
+
onConfirm: () => void;
|
|
6
|
+
isRestoring: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function RestoreModal({ isOpen, onClose, date, onConfirm, isRestoring }: RestoreModalProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=RestoreModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RestoreModal.d.ts","sourceRoot":"","sources":["../../../src/components/shell/RestoreModal.tsx"],"names":[],"mappings":"AAIA,UAAU,iBAAiB;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,iBAAiB,2CAiBhG"}
|
package/dist/index.js
CHANGED
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/dist/lib/timestamp.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timestamp.d.ts","sourceRoot":"","sources":["../../src/lib/timestamp.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BnD"}
|
|
1
|
+
{"version":3,"file":"timestamp.d.ts","sourceRoot":"","sources":["../../src/lib/timestamp.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAalD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BnD"}
|
package/package.json
CHANGED
|
@@ -1,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,104 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import { formatDateTime } from "../../lib/timestamp";
|
|
4
|
+
import { Button } from "./Button";
|
|
5
|
+
|
|
6
|
+
interface CommitItem {
|
|
7
|
+
sha: string;
|
|
8
|
+
date: string;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface HistoryPopoverProps {
|
|
13
|
+
onSelectCommit: (sha: string, date: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function HistoryPopover({ onSelectCommit }: HistoryPopoverProps) {
|
|
17
|
+
const [commits, setCommits] = useState<CommitItem[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
20
|
+
const [page, setPage] = useState(1);
|
|
21
|
+
const [hasMore, setHasMore] = useState(true);
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const fetchPage = useCallback(async (pageNum: number) => {
|
|
25
|
+
const isFirst = pageNum === 1;
|
|
26
|
+
if (isFirst) setLoading(true);
|
|
27
|
+
else setLoadingMore(true);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(`/api/history?page=${pageNum}&per_page=20`);
|
|
31
|
+
if (!res.ok) throw new Error(`Failed to load history: ${res.status}`);
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
const items = data.items as CommitItem[];
|
|
34
|
+
setCommits((prev) => isFirst ? items : [...prev, ...items]);
|
|
35
|
+
setHasMore(items.length === 20);
|
|
36
|
+
setPage(pageNum);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
setError(err instanceof Error ? err.message : "Failed to load history");
|
|
39
|
+
} finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
setLoadingMore(false);
|
|
42
|
+
}
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
fetchPage(1);
|
|
47
|
+
}, [fetchPage]);
|
|
48
|
+
|
|
49
|
+
if (loading) {
|
|
50
|
+
return (
|
|
51
|
+
<div className="p-4 text-center text-xs text-base-contrast-light">
|
|
52
|
+
Loading history...
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (error) {
|
|
58
|
+
return (
|
|
59
|
+
<div className="p-4 text-center text-xs text-red-600">
|
|
60
|
+
{error}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (commits.length === 0) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="p-4 text-center text-xs text-base-contrast-light">
|
|
68
|
+
No history found
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="max-h-64 overflow-y-auto">
|
|
75
|
+
<ul className="py-1">
|
|
76
|
+
{commits.map((commit) => (
|
|
77
|
+
<li key={commit.sha}>
|
|
78
|
+
<button
|
|
79
|
+
onClick={() => onSelectCommit(commit.sha, commit.date)}
|
|
80
|
+
className={cn(
|
|
81
|
+
"w-full cursor-pointer px-4 py-2 text-left text-xs transition-colors",
|
|
82
|
+
"hover:bg-base-accent text-base-contrast-light hover:text-base-contrast",
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{formatDateTime(commit.date)}
|
|
86
|
+
</button>
|
|
87
|
+
</li>
|
|
88
|
+
))}
|
|
89
|
+
</ul>
|
|
90
|
+
{hasMore && (
|
|
91
|
+
<div className="border-t border-base-200 p-2 text-center">
|
|
92
|
+
<Button
|
|
93
|
+
variant="ghost"
|
|
94
|
+
size="sm"
|
|
95
|
+
onClick={() => fetchPage(page + 1)}
|
|
96
|
+
disabled={loadingMore}
|
|
97
|
+
>
|
|
98
|
+
{loadingMore ? "Loading..." : "Load more"}
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import { useState, useCallback, useEffect } from "react";
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { cn } from "../../lib/cn";
|
|
3
3
|
import { Toggle } from "./Toggle";
|
|
4
4
|
import type { NavItem } from "../../lib/nav";
|
|
5
|
-
import { editModeEvent, navChangeEvent, darkModeEvent } from "../../lib/events";
|
|
5
|
+
import { editModeEvent, navChangeEvent, darkModeEvent, historySelectEvent } from "../../lib/events";
|
|
6
6
|
import { useActiveHeadings } from "../../hooks/useActiveHeadings";
|
|
7
|
+
import { formatDate } from "../../lib/timestamp";
|
|
8
|
+
import { Popover } from "./Popover";
|
|
9
|
+
import { HistoryPopover } from "./HistoryPopover";
|
|
7
10
|
|
|
8
11
|
interface Props {
|
|
9
12
|
navLinks: NavItem[];
|
|
10
13
|
siteName: string;
|
|
11
14
|
darkMode: "light" | "dark" | "optional";
|
|
15
|
+
lastUpdated: string | null;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
export default function Navigation({ navLinks: initialNavLinks, siteName, darkMode }: Props) {
|
|
18
|
+
export default function Navigation({ navLinks: initialNavLinks, siteName, darkMode, lastUpdated }: Props) {
|
|
15
19
|
const [isOpen, setIsOpen] = useState(false);
|
|
16
|
-
const [isEditMode, setIsEditMode] = useState(
|
|
20
|
+
const [isEditMode, setIsEditMode] = useState(() => typeof window !== "undefined" && window.location.pathname.startsWith("/edit"));
|
|
17
21
|
const [currentDarkMode, setCurrentDarkMode] = useState(darkMode);
|
|
18
22
|
const [isDark, setIsDark] = useState(false);
|
|
19
23
|
const [navLinks, setNavLinks] = useState<NavItem[]>(initialNavLinks);
|
|
24
|
+
const [showHistory, setShowHistory] = useState(false);
|
|
25
|
+
const historyButtonRef = useRef<HTMLButtonElement>(null);
|
|
20
26
|
|
|
21
27
|
useEffect(() => {
|
|
22
28
|
const unlistenEdit = editModeEvent.listen(({ isEditMode }) => setIsEditMode(isEditMode));
|
|
@@ -73,20 +79,22 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
|
|
|
73
79
|
return (
|
|
74
80
|
<>
|
|
75
81
|
{/* Mobile header bar */}
|
|
76
|
-
<header className="fixed top-0 left-0 right-0 z-50
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
<header className="fixed top-0 left-0 right-0 z-50 bg-base lg:hidden">
|
|
83
|
+
<div className="mx-auto max-w-screen-xl flex h-16 items-center justify-between px-4">
|
|
84
|
+
<span className="text-lg font-bold text-primary">{siteName}</span>
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
87
|
+
className="cursor-pointer p-2 text-base-contrast"
|
|
88
|
+
aria-label="Toggle navigation"
|
|
89
|
+
>
|
|
90
|
+
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
91
|
+
{isOpen
|
|
92
|
+
? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
93
|
+
: <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
94
|
+
}
|
|
95
|
+
</svg>
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
90
98
|
</header>
|
|
91
99
|
|
|
92
100
|
{/* Backdrop */}
|
|
@@ -100,7 +108,7 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
|
|
|
100
108
|
{/* Sidebar nav */}
|
|
101
109
|
<nav
|
|
102
110
|
className={cn(
|
|
103
|
-
"fixed top-0 left-0 z-40 h-full w-64 overflow-y-auto bg-base pt-16 transition-transform lg:translate-x-0",
|
|
111
|
+
"fixed top-0 left-0 lg:left-auto z-40 h-full w-64 flex flex-col overflow-y-auto border-r border-base-200 bg-base pt-16 transition-transform lg:translate-x-0 nav-sidebar",
|
|
104
112
|
isOpen ? "translate-x-0" : "-translate-x-full",
|
|
105
113
|
)}
|
|
106
114
|
>
|
|
@@ -177,19 +185,55 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
|
|
|
177
185
|
})}
|
|
178
186
|
</ul>
|
|
179
187
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
<div className="
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
checked={isDark}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
/>
|
|
189
|
-
<span className={cn(isDark && "text-base-contrast")}>Dark</span>
|
|
188
|
+
<div className="mt-auto">
|
|
189
|
+
{currentDarkMode === "optional" && (
|
|
190
|
+
<div className="mx-4 border-t border-base-200 py-4">
|
|
191
|
+
<div className="flex items-center gap-2 text-sm text-base-contrast-light">
|
|
192
|
+
<span className={cn(!isDark && "text-base-contrast")}>Light</span>
|
|
193
|
+
<Toggle checked={isDark} onChange={handleThemeToggle} label="Toggle dark mode" />
|
|
194
|
+
<span className={cn(isDark && "text-base-contrast")}>Dark</span>
|
|
195
|
+
</div>
|
|
190
196
|
</div>
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{lastUpdated && (
|
|
200
|
+
<div className={cn(currentDarkMode !== "optional" && "border-t border-base-200", "mx-4 py-4 text-center")}>
|
|
201
|
+
{isEditMode ? (
|
|
202
|
+
<div className="relative">
|
|
203
|
+
<button
|
|
204
|
+
ref={historyButtonRef}
|
|
205
|
+
onClick={() => setShowHistory((prev) => !prev)}
|
|
206
|
+
className="cursor-pointer text-xs text-base-contrast-light hover:text-primary transition-colors"
|
|
207
|
+
aria-label="View history"
|
|
208
|
+
>
|
|
209
|
+
Last updated {formatDate(lastUpdated)}
|
|
210
|
+
</button>
|
|
211
|
+
<Popover
|
|
212
|
+
isOpen={showHistory}
|
|
213
|
+
onClose={() => setShowHistory(false)}
|
|
214
|
+
anchorRef={historyButtonRef}
|
|
215
|
+
className="w-56 !bottom-full !top-auto !mb-1 !mt-0"
|
|
216
|
+
>
|
|
217
|
+
<HistoryPopover
|
|
218
|
+
onSelectCommit={(sha, date) => {
|
|
219
|
+
setShowHistory(false);
|
|
220
|
+
historySelectEvent.dispatch({ sha, date });
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
</Popover>
|
|
224
|
+
</div>
|
|
225
|
+
) : (
|
|
226
|
+
<p className="text-xs text-base-contrast-light">
|
|
227
|
+
Last updated {new Date(lastUpdated).toLocaleDateString("en-US", {
|
|
228
|
+
month: "short",
|
|
229
|
+
day: "numeric",
|
|
230
|
+
year: "numeric",
|
|
231
|
+
})}
|
|
232
|
+
</p>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
193
237
|
</nav>
|
|
194
238
|
</>
|
|
195
239
|
);
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
|
|
2
2
|
import { editModeEvent } from "../../lib/events";
|
|
3
|
+
import type { LoadedSection } from "../../lib/loader";
|
|
4
|
+
import type { SiteIndex, SiteConfig } from "../../schemas/site-config";
|
|
5
|
+
|
|
6
|
+
export type HistoryState = {
|
|
7
|
+
sha: string;
|
|
8
|
+
date: string;
|
|
9
|
+
sections: LoadedSection[];
|
|
10
|
+
index: SiteIndex;
|
|
11
|
+
siteConfig: SiteConfig;
|
|
12
|
+
} | null;
|
|
3
13
|
|
|
4
14
|
interface EditorContextValue {
|
|
5
15
|
isEditMode: boolean;
|
|
6
16
|
showAllChrome: boolean;
|
|
7
17
|
viewBranch: "saved" | "live";
|
|
18
|
+
historyState: HistoryState;
|
|
8
19
|
toggleEditMode: () => void;
|
|
9
20
|
toggleShowAllChrome: () => void;
|
|
10
21
|
setViewBranch: (branch: "saved" | "live") => void;
|
|
22
|
+
setHistoryState: (state: HistoryState) => void;
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
const EditorContext = createContext<EditorContextValue | null>(null);
|
|
@@ -20,6 +32,10 @@ export function EditorProvider({ children }: EditorProviderProps) {
|
|
|
20
32
|
const [isEditMode, setIsEditMode] = useState(true);
|
|
21
33
|
const [showAllChrome, setShowAllChrome] = useState(false);
|
|
22
34
|
const [viewBranch, setViewBranchState] = useState<"saved" | "live">("saved");
|
|
35
|
+
const [historyState, setHistoryStateRaw] = useState<HistoryState>(null);
|
|
36
|
+
const setHistoryState = useCallback((state: HistoryState) => {
|
|
37
|
+
setHistoryStateRaw(state);
|
|
38
|
+
}, []);
|
|
23
39
|
|
|
24
40
|
const toggleEditMode = useCallback(() => {
|
|
25
41
|
setIsEditMode((prev) => {
|
|
@@ -33,7 +49,7 @@ export function EditorProvider({ children }: EditorProviderProps) {
|
|
|
33
49
|
const setViewBranch = useCallback((b: "saved" | "live") => setViewBranchState(b), []);
|
|
34
50
|
|
|
35
51
|
return (
|
|
36
|
-
<EditorContext.Provider value={{ isEditMode, showAllChrome, viewBranch, toggleEditMode, toggleShowAllChrome, setViewBranch }}>
|
|
52
|
+
<EditorContext.Provider value={{ isEditMode, showAllChrome, viewBranch, historyState, toggleEditMode, toggleShowAllChrome, setViewBranch, setHistoryState }}>
|
|
37
53
|
{children}
|
|
38
54
|
</EditorContext.Provider>
|
|
39
55
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment, useState, useCallback, useEffect, useRef } from "react";
|
|
1
|
+
import { Fragment, useState, useCallback, useEffect, useRef, type ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
4
4
|
import type { LoadedSection } from "../../lib/loader";
|
|
@@ -40,7 +40,7 @@ import { useBuildStatus } from "../../hooks/useBuildStatus";
|
|
|
40
40
|
import { useMediaPipeline } from "../../hooks/useMediaPipeline";
|
|
41
41
|
import { formatTimestamp } from "../../lib/timestamp";
|
|
42
42
|
import { generateNavLinks } from "../../lib/nav";
|
|
43
|
-
import { navChangeEvent, darkModeEvent } from "../../lib/events";
|
|
43
|
+
import { navChangeEvent, darkModeEvent, historySelectEvent } from "../../lib/events";
|
|
44
44
|
import { cn } from "../../lib/cn";
|
|
45
45
|
import { Button } from "../shared/Button";
|
|
46
46
|
import { SplitButton } from "../shared/SplitButton";
|
|
@@ -49,6 +49,9 @@ import { SegmentedControl } from "../shared/SegmentedControl";
|
|
|
49
49
|
import { SettingsIcon } from "../shared/icons";
|
|
50
50
|
import { ImageIcon, X } from "lucide-react";
|
|
51
51
|
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
|
52
|
+
import { HistoryToolbar } from "./HistoryToolbar";
|
|
53
|
+
import { RestoreModal } from "./RestoreModal";
|
|
54
|
+
import ViewRenderer from "../sections/ViewRenderer";
|
|
52
55
|
|
|
53
56
|
export { useMediaLibrary } from "./MediaLibraryContext";
|
|
54
57
|
|
|
@@ -106,6 +109,8 @@ export default function EditorShell({
|
|
|
106
109
|
const [mainIndex, setMainIndex] = useState<SiteIndex | null>(null);
|
|
107
110
|
const [viewSections, setViewSections] = useState<LoadedSection[] | null>(null);
|
|
108
111
|
const [isLoadingViewContent, setIsLoadingViewContent] = useState(false);
|
|
112
|
+
const [showRestoreModal, setShowRestoreModal] = useState(false);
|
|
113
|
+
const [isRestoring, setIsRestoring] = useState(false);
|
|
109
114
|
|
|
110
115
|
const siteIndexRef = useRef<SiteIndex>({ siteId, order: [], sections: {} });
|
|
111
116
|
const fontLinkRef = useRef<HTMLLinkElement | null>(null);
|
|
@@ -515,6 +520,38 @@ export default function EditorShell({
|
|
|
515
520
|
setLocalChangesExist(true);
|
|
516
521
|
}, [applySiteConfigPreview, persistence]);
|
|
517
522
|
|
|
523
|
+
const handleHistoryRestore = useCallback(async (historyContent: {
|
|
524
|
+
sections: LoadedSection[];
|
|
525
|
+
index: SiteIndex;
|
|
526
|
+
siteConfig: SiteConfig;
|
|
527
|
+
}) => {
|
|
528
|
+
setIsRestoring(true);
|
|
529
|
+
try {
|
|
530
|
+
const payload = {
|
|
531
|
+
sections: historyContent.sections.map((s) => ({
|
|
532
|
+
id: s.section.id,
|
|
533
|
+
content: s.section,
|
|
534
|
+
})),
|
|
535
|
+
siteIndex: historyContent.index,
|
|
536
|
+
siteConfig: historyContent.siteConfig,
|
|
537
|
+
targetBranch: "main",
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const response = await fetch("/api/save", {
|
|
541
|
+
method: "POST",
|
|
542
|
+
headers: { "Content-Type": "application/json" },
|
|
543
|
+
body: JSON.stringify(payload),
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
if (!response.ok) throw new Error(`Restore failed: ${response.status}`);
|
|
547
|
+
await discardLocalChanges();
|
|
548
|
+
window.location.reload();
|
|
549
|
+
} catch (err) {
|
|
550
|
+
console.error("Failed to restore:", err);
|
|
551
|
+
setIsRestoring(false);
|
|
552
|
+
}
|
|
553
|
+
}, []);
|
|
554
|
+
|
|
518
555
|
// --- Render ---
|
|
519
556
|
|
|
520
557
|
if (shellState.phase === "loading-content") {
|
|
@@ -559,6 +596,7 @@ export default function EditorShell({
|
|
|
559
596
|
setViewSections={setViewSections}
|
|
560
597
|
setIsLoadingViewContent={setIsLoadingViewContent}
|
|
561
598
|
/>
|
|
599
|
+
<HistoryWatcher />
|
|
562
600
|
<EditorModalProvider>
|
|
563
601
|
<MediaLibraryContext.Provider value={{
|
|
564
602
|
...mediaPipeline.contextValue,
|
|
@@ -587,25 +625,28 @@ export default function EditorShell({
|
|
|
587
625
|
buildState={buildStatus.state}
|
|
588
626
|
buildElapsed={buildStatus.elapsedSeconds}
|
|
589
627
|
onBuildDismiss={buildStatus.dismiss}
|
|
628
|
+
onRestoreClick={() => setShowRestoreModal(true)}
|
|
590
629
|
/>
|
|
591
630
|
|
|
592
|
-
<
|
|
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,
|
|
@@ -970,6 +1096,7 @@ function EditorToolbar({
|
|
|
970
1096
|
buildState,
|
|
971
1097
|
buildElapsed,
|
|
972
1098
|
onBuildDismiss,
|
|
1099
|
+
onRestoreClick,
|
|
973
1100
|
}: {
|
|
974
1101
|
buttonState: "synced" | "publish" | "saveAndPublish";
|
|
975
1102
|
localChangesExist: boolean;
|
|
@@ -985,105 +1112,122 @@ function EditorToolbar({
|
|
|
985
1112
|
buildState: "idle" | "building" | "ready" | "error" | "fading";
|
|
986
1113
|
buildElapsed: number;
|
|
987
1114
|
onBuildDismiss: () => void;
|
|
1115
|
+
onRestoreClick: () => void;
|
|
988
1116
|
}) {
|
|
989
|
-
const { isEditMode, viewBranch, setViewBranch, toggleEditMode } = useEditorContext();
|
|
1117
|
+
const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState } = useEditorContext();
|
|
1118
|
+
|
|
1119
|
+
if (historyState) {
|
|
1120
|
+
return (
|
|
1121
|
+
<HistoryToolbar
|
|
1122
|
+
date={historyState.date}
|
|
1123
|
+
onBackToCurrent={() => setHistoryState(null)}
|
|
1124
|
+
onRestore={onRestoreClick}
|
|
1125
|
+
/>
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
990
1128
|
|
|
991
1129
|
return (
|
|
992
1130
|
<>
|
|
993
1131
|
{isEditMode && (
|
|
994
|
-
<div className="fixed top-0 right-0 left-0 z-50
|
|
995
|
-
<div className="
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1132
|
+
<div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
|
|
1133
|
+
<div className="mx-auto max-w-screen-xl grid grid-cols-3 items-center px-4 py-2">
|
|
1134
|
+
<div className="flex items-center gap-2">
|
|
1135
|
+
{buttonState === "saveAndPublish" && (
|
|
1136
|
+
<SplitButton
|
|
1137
|
+
label="Save & Publish"
|
|
1138
|
+
onClick={onSaveAndPublish}
|
|
1139
|
+
disabled={publishAction !== "idle"}
|
|
1140
|
+
options={[{ label: "Save", onClick: onSave }]}
|
|
1141
|
+
/>
|
|
1142
|
+
)}
|
|
1143
|
+
{buttonState === "publish" && (
|
|
1144
|
+
<SplitButton
|
|
1145
|
+
label="Publish"
|
|
1146
|
+
onClick={onPublish}
|
|
1147
|
+
disabled={publishAction !== "idle"}
|
|
1148
|
+
options={[]}
|
|
1149
|
+
/>
|
|
1150
|
+
)}
|
|
1151
|
+
{buttonState === "synced" && (
|
|
1152
|
+
<SplitButton
|
|
1153
|
+
label="Up to date"
|
|
1154
|
+
onClick={() => {}}
|
|
1155
|
+
disabled
|
|
1156
|
+
options={[]}
|
|
1157
|
+
/>
|
|
1158
|
+
)}
|
|
1159
|
+
{localChangesExist && publishAction === "idle" && (
|
|
1160
|
+
<Button
|
|
1161
|
+
type="button"
|
|
1162
|
+
variant="destructive"
|
|
1163
|
+
onClick={onDiscardClick}
|
|
1164
|
+
>
|
|
1165
|
+
Discard Changes
|
|
1166
|
+
</Button>
|
|
1167
|
+
)}
|
|
1168
|
+
</div>
|
|
1169
|
+
<div className="flex items-center justify-center">
|
|
1170
|
+
<StatusText
|
|
1171
|
+
publishAction={publishAction}
|
|
1172
|
+
publishFeedback={publishFeedback}
|
|
1173
|
+
buildState={buildState}
|
|
1174
|
+
buildElapsed={buildElapsed}
|
|
1175
|
+
onDismiss={onBuildDismiss}
|
|
1002
1176
|
/>
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
<
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1177
|
+
</div>
|
|
1178
|
+
<div className="flex items-center justify-end gap-2">
|
|
1179
|
+
<ProcessingIndicator items={processingItems} />
|
|
1180
|
+
<IconButton
|
|
1181
|
+
icon={<ImageIcon size={16} />}
|
|
1182
|
+
label="Media library"
|
|
1183
|
+
size="md"
|
|
1184
|
+
onClick={onMediaClick}
|
|
1185
|
+
className="border border-base-200 bg-base-accent"
|
|
1010
1186
|
/>
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
onClick={
|
|
1016
|
-
|
|
1017
|
-
options={[]}
|
|
1187
|
+
<IconButton
|
|
1188
|
+
icon={<SettingsIcon size={16} />}
|
|
1189
|
+
label="Site settings"
|
|
1190
|
+
size="md"
|
|
1191
|
+
onClick={onSettingsClick}
|
|
1192
|
+
className="border border-base-200 bg-base-accent"
|
|
1018
1193
|
/>
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
<Button
|
|
1022
|
-
type="button"
|
|
1023
|
-
variant="destructive"
|
|
1024
|
-
onClick={onDiscardClick}
|
|
1025
|
-
>
|
|
1026
|
-
Discard Changes
|
|
1027
|
-
</Button>
|
|
1028
|
-
)}
|
|
1029
|
-
</div>
|
|
1030
|
-
<div className="flex items-center justify-center">
|
|
1031
|
-
<StatusText
|
|
1032
|
-
publishAction={publishAction}
|
|
1033
|
-
publishFeedback={publishFeedback}
|
|
1034
|
-
buildState={buildState}
|
|
1035
|
-
buildElapsed={buildElapsed}
|
|
1036
|
-
onDismiss={onBuildDismiss}
|
|
1037
|
-
/>
|
|
1038
|
-
</div>
|
|
1039
|
-
<div className="flex items-center justify-end gap-2">
|
|
1040
|
-
<ProcessingIndicator items={processingItems} />
|
|
1041
|
-
<IconButton
|
|
1042
|
-
icon={<ImageIcon size={16} />}
|
|
1043
|
-
label="Media library"
|
|
1044
|
-
size="md"
|
|
1045
|
-
onClick={onMediaClick}
|
|
1046
|
-
className="border border-base-200 bg-base-accent"
|
|
1047
|
-
/>
|
|
1048
|
-
<IconButton
|
|
1049
|
-
icon={<SettingsIcon size={16} />}
|
|
1050
|
-
label="Site settings"
|
|
1051
|
-
size="md"
|
|
1052
|
-
onClick={onSettingsClick}
|
|
1053
|
-
className="border border-base-200 bg-base-accent"
|
|
1054
|
-
/>
|
|
1055
|
-
<ShowAllChromeToggle />
|
|
1194
|
+
<ShowAllChromeToggle />
|
|
1195
|
+
</div>
|
|
1056
1196
|
</div>
|
|
1057
1197
|
</div>
|
|
1058
1198
|
)}
|
|
1059
1199
|
{!isEditMode && (
|
|
1060
|
-
<div className="fixed top-0 right-0 left-0 z-50
|
|
1061
|
-
<
|
|
1062
|
-
|
|
1063
|
-
{
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1200
|
+
<div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
|
|
1201
|
+
<div className="mx-auto max-w-screen-xl flex items-center justify-center px-4 py-2">
|
|
1202
|
+
<SegmentedControl
|
|
1203
|
+
options={[
|
|
1204
|
+
{ value: "saved", label: "Draft" },
|
|
1205
|
+
{ value: "live", label: "Live" },
|
|
1206
|
+
]}
|
|
1207
|
+
value={viewBranch}
|
|
1208
|
+
onChange={(v) => setViewBranch(v as "saved" | "live")}
|
|
1209
|
+
/>
|
|
1210
|
+
</div>
|
|
1069
1211
|
</div>
|
|
1070
1212
|
)}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
<
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
<
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1213
|
+
{!historyState && (
|
|
1214
|
+
<button
|
|
1215
|
+
onClick={toggleEditMode}
|
|
1216
|
+
className="cursor-pointer fixed bottom-4 right-4 lg:right-auto z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity fab-container-right"
|
|
1217
|
+
aria-label={isEditMode ? "Switch to view mode" : "Switch to edit mode"}
|
|
1218
|
+
>
|
|
1219
|
+
{isEditMode ? (
|
|
1220
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
1221
|
+
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
|
|
1222
|
+
<path fillRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clipRule="evenodd" />
|
|
1223
|
+
</svg>
|
|
1224
|
+
) : (
|
|
1225
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
1226
|
+
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
|
1227
|
+
</svg>
|
|
1228
|
+
)}
|
|
1229
|
+
</button>
|
|
1230
|
+
)}
|
|
1087
1231
|
</>
|
|
1088
1232
|
);
|
|
1089
1233
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { formatDateTime } from "../../lib/timestamp";
|
|
2
|
+
import { Button } from "../shared/Button";
|
|
3
|
+
|
|
4
|
+
interface HistoryToolbarProps {
|
|
5
|
+
date: string;
|
|
6
|
+
onBackToCurrent: () => void;
|
|
7
|
+
onRestore: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function HistoryToolbar({ date, onBackToCurrent, onRestore }: HistoryToolbarProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
|
|
13
|
+
<div className="mx-auto max-w-screen-xl grid grid-cols-3 items-center px-4 py-2">
|
|
14
|
+
<div className="flex items-center">
|
|
15
|
+
<Button variant="secondary" size="sm" onClick={onBackToCurrent}>
|
|
16
|
+
Back to current
|
|
17
|
+
</Button>
|
|
18
|
+
</div>
|
|
19
|
+
<div className="flex items-center justify-center">
|
|
20
|
+
<span className="text-xs font-medium text-base-contrast-light">
|
|
21
|
+
Viewing {formatDateTime(date)}
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="flex items-center justify-end">
|
|
25
|
+
<Button variant="primary" size="sm" onClick={onRestore}>
|
|
26
|
+
Restore this version
|
|
27
|
+
</Button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatDateTime } from "../../lib/timestamp";
|
|
2
|
+
import { EditorModal } from "./EditorModal";
|
|
3
|
+
import { Button } from "../shared/Button";
|
|
4
|
+
|
|
5
|
+
interface RestoreModalProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
date: string;
|
|
9
|
+
onConfirm: () => void;
|
|
10
|
+
isRestoring: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function RestoreModal({ isOpen, onClose, date, onConfirm, isRestoring }: RestoreModalProps) {
|
|
14
|
+
return (
|
|
15
|
+
<EditorModal isOpen={isOpen} onClose={onClose} title="Restore this version?">
|
|
16
|
+
<p className="mb-4 text-sm text-base-contrast-light">
|
|
17
|
+
This will publish the content from {formatDateTime(date)} as a new update.
|
|
18
|
+
Your current content will still be available in the history.
|
|
19
|
+
</p>
|
|
20
|
+
<div className="flex justify-end gap-3">
|
|
21
|
+
<Button variant="secondary" size="md" onClick={onClose} disabled={isRestoring}>
|
|
22
|
+
Cancel
|
|
23
|
+
</Button>
|
|
24
|
+
<Button variant="primary" size="md" onClick={onConfirm} disabled={isRestoring}>
|
|
25
|
+
{isRestoring ? "Restoring..." : "Restore"}
|
|
26
|
+
</Button>
|
|
27
|
+
</div>
|
|
28
|
+
</EditorModal>
|
|
29
|
+
);
|
|
30
|
+
}
|
package/src/lib/events.ts
CHANGED
|
@@ -21,3 +21,4 @@ export function createEvent<T>(name: string): TypedEvent<T> {
|
|
|
21
21
|
export const editModeEvent = createEvent<{ isEditMode: boolean }>("editmodechange");
|
|
22
22
|
export const navChangeEvent = createEvent<NavItem[]>("sitenavchange");
|
|
23
23
|
export const darkModeEvent = createEvent<string>("sitedarkmode");
|
|
24
|
+
export const historySelectEvent = createEvent<{ sha: string; date: string }>("history-select");
|
package/src/lib/timestamp.ts
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
export function formatDate(iso: string): string {
|
|
2
|
+
return new Date(iso).toLocaleDateString("en-US", {
|
|
3
|
+
month: "short",
|
|
4
|
+
day: "numeric",
|
|
5
|
+
year: "numeric",
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatDateTime(iso: string): string {
|
|
10
|
+
const date = new Date(iso);
|
|
11
|
+
const datePart = date.toLocaleDateString("en-US", {
|
|
12
|
+
month: "short",
|
|
13
|
+
day: "numeric",
|
|
14
|
+
year: "numeric",
|
|
15
|
+
});
|
|
16
|
+
const timePart = date.toLocaleTimeString("en-US", {
|
|
17
|
+
hour: "numeric",
|
|
18
|
+
minute: "2-digit",
|
|
19
|
+
hour12: true,
|
|
20
|
+
});
|
|
21
|
+
return `${datePart} | ${timePart}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
1
24
|
export function formatTimestamp(iso: string): string {
|
|
2
25
|
const date = new Date(iso);
|
|
3
26
|
const now = new Date();
|