@drawnagency/primitives 0.1.11 → 0.1.13
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-6SK5BLG3.js → chunk-46QI4FDZ.js} +1 -1
- package/dist/{chunk-XQXZHDNR.js → chunk-EAEX6DS7.js} +4 -1
- package/dist/{chunk-32H6Q6CX.js → chunk-P24YUT3O.js} +1 -1
- package/dist/components/editor/AudienceIndicator.d.ts +9 -0
- package/dist/components/editor/AudienceIndicator.d.ts.map +1 -0
- package/dist/components/editor/IndicatorPill.d.ts +18 -0
- package/dist/components/editor/IndicatorPill.d.ts.map +1 -0
- package/dist/components/editor/SectionWrapper.d.ts +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/editor/StatusIndicator.d.ts +29 -0
- package/dist/components/editor/StatusIndicator.d.ts.map +1 -0
- package/dist/components/editor/index.d.ts +3 -1
- package/dist/components/editor/index.d.ts.map +1 -1
- package/dist/components/shared/Popover.d.ts +1 -1
- package/dist/components/shared/Popover.d.ts.map +1 -1
- package/dist/components/shared/SegmentedControl.d.ts +13 -0
- package/dist/components/shared/SegmentedControl.d.ts.map +1 -0
- package/dist/components/shared/SplitButton.d.ts +17 -0
- package/dist/components/shared/SplitButton.d.ts.map +1 -0
- package/dist/components/shell/BuildStatusIndicator.d.ts +9 -0
- package/dist/components/shell/BuildStatusIndicator.d.ts.map +1 -0
- package/dist/components/shell/EditorContext.d.ts +2 -0
- package/dist/components/shell/EditorContext.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useBuildStatus.d.ts +11 -0
- package/dist/hooks/useBuildStatus.d.ts.map +1 -0
- package/dist/hooks/useContentLifecycle.d.ts +13 -0
- package/dist/hooks/useContentLifecycle.d.ts.map +1 -0
- package/dist/hooks/useEditorPublish.d.ts +6 -1
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/lib/dexie.d.ts +8 -1
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/index.js +2 -2
- package/dist/lib/registry.d.ts +6 -1
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -2
- package/dist/schemas/site-config.d.ts +8 -6
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/editor/{AudiencePicker.tsx → AudienceIndicator.tsx} +25 -49
- package/src/components/editor/IndicatorPill.tsx +119 -0
- package/src/components/editor/SectionWrapper.tsx +59 -13
- package/src/components/editor/StatusIndicator.tsx +148 -0
- package/src/components/editor/index.ts +3 -1
- package/src/components/sections/SectionLayout.tsx +1 -1
- package/src/components/shared/Navigation.tsx +3 -3
- package/src/components/shared/Popover.tsx +26 -4
- package/src/components/shared/PopoverItem.tsx +1 -1
- package/src/components/shared/SegmentedControl.tsx +43 -0
- package/src/components/shared/SplitButton.tsx +97 -0
- package/src/components/shell/BuildStatusIndicator.tsx +67 -0
- package/src/components/shell/EditorContext.tsx +5 -1
- package/src/components/shell/EditorShell.tsx +180 -52
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useBuildStatus.ts +139 -0
- package/src/hooks/useContentLifecycle.ts +34 -0
- package/src/hooks/useEditorPublish.ts +234 -66
- package/src/lib/dexie.ts +43 -2
- package/src/lib/registry.ts +6 -1
- package/src/schemas/site-config.ts +5 -1
- package/dist/components/editor/AudiencePicker.d.ts +0 -9
- package/dist/components/editor/AudiencePicker.d.ts.map +0 -1
- package/dist/components/editor/StatusBadge.d.ts +0 -7
- package/dist/components/editor/StatusBadge.d.ts.map +0 -1
- package/dist/components/editor/StatusPicker.d.ts +0 -9
- package/dist/components/editor/StatusPicker.d.ts.map +0 -1
- package/src/components/editor/StatusBadge.tsx +0 -30
- package/src/components/editor/StatusPicker.tsx +0 -86
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Popover } from "./Popover";
|
|
5
|
+
import { PopoverItem } from "./PopoverItem";
|
|
6
|
+
|
|
7
|
+
export interface SplitButtonOption {
|
|
8
|
+
label: string;
|
|
9
|
+
onClick: () => void;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SplitButtonProps {
|
|
14
|
+
label: string;
|
|
15
|
+
onClick: () => void;
|
|
16
|
+
options: SplitButtonOption[];
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
isLoading?: boolean;
|
|
19
|
+
loadingLabel?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function SplitButton({
|
|
24
|
+
label,
|
|
25
|
+
onClick,
|
|
26
|
+
options,
|
|
27
|
+
disabled = false,
|
|
28
|
+
isLoading = false,
|
|
29
|
+
loadingLabel,
|
|
30
|
+
className,
|
|
31
|
+
}: SplitButtonProps) {
|
|
32
|
+
const [open, setOpen] = useState(false);
|
|
33
|
+
const chevronRef = useRef<HTMLButtonElement>(null);
|
|
34
|
+
const resolvedDisabled = disabled || isLoading;
|
|
35
|
+
const displayLabel = isLoading && loadingLabel ? loadingLabel : label;
|
|
36
|
+
const hasOptions = options.length > 0 && !resolvedDisabled;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={cn("relative inline-flex", className)}>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={onClick}
|
|
43
|
+
disabled={resolvedDisabled}
|
|
44
|
+
className={cn(
|
|
45
|
+
"inline-flex items-center px-4 py-1.5 text-xs font-medium transition-colors",
|
|
46
|
+
"bg-base-contrast text-base-accent",
|
|
47
|
+
hasOptions ? "rounded-l" : "rounded",
|
|
48
|
+
resolvedDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:bg-base-contrast/90",
|
|
49
|
+
)}
|
|
50
|
+
aria-label={displayLabel}
|
|
51
|
+
>
|
|
52
|
+
{displayLabel}
|
|
53
|
+
</button>
|
|
54
|
+
{hasOptions && (
|
|
55
|
+
<>
|
|
56
|
+
<button
|
|
57
|
+
ref={chevronRef}
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => setOpen((v) => !v)}
|
|
60
|
+
disabled={resolvedDisabled}
|
|
61
|
+
className={cn(
|
|
62
|
+
"inline-flex items-center border-l border-base-accent/20 px-2 py-1.5 transition-colors",
|
|
63
|
+
"bg-base-contrast text-base-accent rounded-r",
|
|
64
|
+
resolvedDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:bg-base-contrast/90",
|
|
65
|
+
)}
|
|
66
|
+
aria-label="More actions"
|
|
67
|
+
aria-haspopup="true"
|
|
68
|
+
aria-expanded={open}
|
|
69
|
+
>
|
|
70
|
+
<ChevronDown size={14} />
|
|
71
|
+
</button>
|
|
72
|
+
<Popover isOpen={open} onClose={() => setOpen(false)} anchorRef={chevronRef} className="w-full">
|
|
73
|
+
<ul role="menu" className="py-1">
|
|
74
|
+
{options.map((option) => (
|
|
75
|
+
<li key={option.label}>
|
|
76
|
+
<PopoverItem
|
|
77
|
+
role="menuitem"
|
|
78
|
+
aria-disabled={option.disabled}
|
|
79
|
+
onClick={() => {
|
|
80
|
+
if (!option.disabled) {
|
|
81
|
+
option.onClick();
|
|
82
|
+
setOpen(false);
|
|
83
|
+
}
|
|
84
|
+
}}
|
|
85
|
+
className={cn(option.disabled && "opacity-50 cursor-not-allowed")}
|
|
86
|
+
>
|
|
87
|
+
<span className="font-medium text-base-contrast">{option.label}</span>
|
|
88
|
+
</PopoverItem>
|
|
89
|
+
</li>
|
|
90
|
+
))}
|
|
91
|
+
</ul>
|
|
92
|
+
</Popover>
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface BuildStatusIndicatorProps {
|
|
5
|
+
state: "idle" | "building" | "ready" | "error";
|
|
6
|
+
deployUrl: string | null;
|
|
7
|
+
visible: boolean;
|
|
8
|
+
onDismiss: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const stateConfig = {
|
|
12
|
+
building: {
|
|
13
|
+
label: "Deploying...",
|
|
14
|
+
className: "border-orange-500 text-orange-600 hover:bg-orange-50",
|
|
15
|
+
dotClassName: "bg-orange-500 animate-pulse",
|
|
16
|
+
},
|
|
17
|
+
ready: {
|
|
18
|
+
label: "Live",
|
|
19
|
+
className: "border-green-600 text-green-600 hover:bg-green-50",
|
|
20
|
+
dotClassName: "bg-green-600",
|
|
21
|
+
},
|
|
22
|
+
error: {
|
|
23
|
+
label: "Deploy failed",
|
|
24
|
+
className: "border-red-600 text-red-600 hover:bg-red-50",
|
|
25
|
+
dotClassName: "bg-red-600",
|
|
26
|
+
},
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export function BuildStatusIndicator({
|
|
30
|
+
state,
|
|
31
|
+
deployUrl,
|
|
32
|
+
visible,
|
|
33
|
+
onDismiss,
|
|
34
|
+
}: BuildStatusIndicatorProps) {
|
|
35
|
+
if (!visible || state === "idle") return null;
|
|
36
|
+
|
|
37
|
+
const config = stateConfig[state];
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<span className="inline-flex items-center gap-0">
|
|
41
|
+
<a
|
|
42
|
+
href={deployUrl ?? "#"}
|
|
43
|
+
target="_blank"
|
|
44
|
+
rel="noopener noreferrer"
|
|
45
|
+
aria-label={config.label}
|
|
46
|
+
className={cn(
|
|
47
|
+
"inline-flex items-center gap-1.5 rounded-l px-3 py-1.5 text-xs font-medium border transition-colors",
|
|
48
|
+
config.className,
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<span className={cn("h-1.5 w-1.5 rounded-full", config.dotClassName)} />
|
|
52
|
+
{config.label}
|
|
53
|
+
</a>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={onDismiss}
|
|
57
|
+
aria-label="Dismiss build status"
|
|
58
|
+
className={cn(
|
|
59
|
+
"inline-flex items-center rounded-r border border-l-0 px-1.5 py-1.5 transition-colors cursor-pointer",
|
|
60
|
+
config.className,
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
<X size={12} />
|
|
64
|
+
</button>
|
|
65
|
+
</span>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -4,8 +4,10 @@ import { editModeEvent } from "../../lib/events";
|
|
|
4
4
|
interface EditorContextValue {
|
|
5
5
|
isEditMode: boolean;
|
|
6
6
|
showAllChrome: boolean;
|
|
7
|
+
viewBranch: "saved" | "live";
|
|
7
8
|
toggleEditMode: () => void;
|
|
8
9
|
toggleShowAllChrome: () => void;
|
|
10
|
+
setViewBranch: (branch: "saved" | "live") => void;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
const EditorContext = createContext<EditorContextValue | null>(null);
|
|
@@ -17,6 +19,7 @@ interface EditorProviderProps {
|
|
|
17
19
|
export function EditorProvider({ children }: EditorProviderProps) {
|
|
18
20
|
const [isEditMode, setIsEditMode] = useState(true);
|
|
19
21
|
const [showAllChrome, setShowAllChrome] = useState(false);
|
|
22
|
+
const [viewBranch, setViewBranchState] = useState<"saved" | "live">("saved");
|
|
20
23
|
|
|
21
24
|
const toggleEditMode = useCallback(() => {
|
|
22
25
|
setIsEditMode((prev) => {
|
|
@@ -27,9 +30,10 @@ export function EditorProvider({ children }: EditorProviderProps) {
|
|
|
27
30
|
}, []);
|
|
28
31
|
|
|
29
32
|
const toggleShowAllChrome = useCallback(() => setShowAllChrome((prev) => !prev), []);
|
|
33
|
+
const setViewBranch = useCallback((b: "saved" | "live") => setViewBranchState(b), []);
|
|
30
34
|
|
|
31
35
|
return (
|
|
32
|
-
<EditorContext.Provider value={{ isEditMode, showAllChrome, toggleEditMode, toggleShowAllChrome }}>
|
|
36
|
+
<EditorContext.Provider value={{ isEditMode, showAllChrome, viewBranch, toggleEditMode, toggleShowAllChrome, setViewBranch }}>
|
|
33
37
|
{children}
|
|
34
38
|
</EditorContext.Provider>
|
|
35
39
|
);
|
|
@@ -35,13 +35,18 @@ import {
|
|
|
35
35
|
} from "../../lib/dexie";
|
|
36
36
|
import { useEditorPersistence } from "../../hooks/useEditorPersistence";
|
|
37
37
|
import { useEditorPublish } from "../../hooks/useEditorPublish";
|
|
38
|
+
import { useContentLifecycle } from "../../hooks/useContentLifecycle";
|
|
39
|
+
import { useBuildStatus } from "../../hooks/useBuildStatus";
|
|
40
|
+
import { BuildStatusIndicator } from "./BuildStatusIndicator";
|
|
38
41
|
import { useMediaPipeline } from "../../hooks/useMediaPipeline";
|
|
39
42
|
import { formatTimestamp } from "../../lib/timestamp";
|
|
40
43
|
import { generateNavLinks } from "../../lib/nav";
|
|
41
44
|
import { navChangeEvent, darkModeEvent } from "../../lib/events";
|
|
42
45
|
import { cn } from "../../lib/cn";
|
|
43
46
|
import { Button } from "../shared/Button";
|
|
47
|
+
import { SplitButton } from "../shared/SplitButton";
|
|
44
48
|
import { IconButton } from "../shared/IconButton";
|
|
49
|
+
import { SegmentedControl } from "../shared/SegmentedControl";
|
|
45
50
|
import { SettingsIcon } from "../shared/icons";
|
|
46
51
|
import { ImageIcon } from "lucide-react";
|
|
47
52
|
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
|
@@ -95,6 +100,13 @@ export default function EditorShell({
|
|
|
95
100
|
const [showMediaLibrary, setShowMediaLibrary] = useState(false);
|
|
96
101
|
const [mediaModalMode, setMediaModalMode] = useState<"select" | "manage">("manage");
|
|
97
102
|
const [mediaSelectCallback, setMediaSelectCallback] = useState<((id: string) => void) | null>(null);
|
|
103
|
+
const [deletedSections, setDeletedSections] = useState<Set<string>>(new Set());
|
|
104
|
+
const [savedSha, setSavedSha] = useState<string | null>(null);
|
|
105
|
+
const [mainSha, setMainSha] = useState<string | null>(null);
|
|
106
|
+
const [changedSectionIds, setChangedSectionIds] = useState<Set<string>>(new Set());
|
|
107
|
+
const [mainIndex, setMainIndex] = useState<SiteIndex | null>(null);
|
|
108
|
+
const [viewSections, setViewSections] = useState<LoadedSection[] | null>(null);
|
|
109
|
+
const [isLoadingViewContent, setIsLoadingViewContent] = useState(false);
|
|
98
110
|
|
|
99
111
|
const siteIndexRef = useRef<SiteIndex>({ siteId, order: [], sections: {} });
|
|
100
112
|
const fontLinkRef = useRef<HTMLLinkElement | null>(null);
|
|
@@ -113,7 +125,9 @@ export default function EditorShell({
|
|
|
113
125
|
markSectionDirty: persistence.markSectionDirty,
|
|
114
126
|
});
|
|
115
127
|
|
|
116
|
-
const
|
|
128
|
+
const buildStatus = useBuildStatus();
|
|
129
|
+
|
|
130
|
+
const { isPublishing, publishFeedback, handleSave, handlePublish, handleSaveAndPublish } = useEditorPublish({
|
|
117
131
|
flushNow: persistence.flushNow,
|
|
118
132
|
cancelPendingFlush: persistence.cancelPendingFlush,
|
|
119
133
|
isConfigDirty: persistence.isConfigDirty,
|
|
@@ -121,8 +135,10 @@ export default function EditorShell({
|
|
|
121
135
|
siteIndexRef,
|
|
122
136
|
siteConfig,
|
|
123
137
|
sections,
|
|
138
|
+
deletedSectionIds: Array.from(deletedSections),
|
|
124
139
|
onSuccess: () => {
|
|
125
140
|
setDirtySectionIds(new Set());
|
|
141
|
+
setDeletedSections(new Set());
|
|
126
142
|
setLocalChangesExist(false);
|
|
127
143
|
},
|
|
128
144
|
mediaManifest,
|
|
@@ -143,6 +159,17 @@ export default function EditorShell({
|
|
|
143
159
|
return { images };
|
|
144
160
|
});
|
|
145
161
|
},
|
|
162
|
+
onShasUpdated: (newSavedSha, newMainSha) => {
|
|
163
|
+
setSavedSha(newSavedSha);
|
|
164
|
+
setMainSha((prev) => newMainSha ?? prev);
|
|
165
|
+
},
|
|
166
|
+
onPublishComplete: buildStatus.startTracking,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const { buttonState } = useContentLifecycle({
|
|
170
|
+
savedSha,
|
|
171
|
+
mainSha,
|
|
172
|
+
hasLocalChanges: localChangesExist,
|
|
146
173
|
});
|
|
147
174
|
|
|
148
175
|
useEffect(() => {
|
|
@@ -208,7 +235,7 @@ export default function EditorShell({
|
|
|
208
235
|
const savedManifest = await getMediaManifest();
|
|
209
236
|
if (savedManifest) loadedManifest = savedManifest;
|
|
210
237
|
} else {
|
|
211
|
-
const response = await fetch("/api/content");
|
|
238
|
+
const response = await fetch("/api/content?branch=saved");
|
|
212
239
|
if (!response.ok) throw new Error(`Failed to load content: ${response.status}`);
|
|
213
240
|
const data = await response.json();
|
|
214
241
|
loadedSections = data.sections;
|
|
@@ -219,6 +246,12 @@ export default function EditorShell({
|
|
|
219
246
|
loadedManifest = data.mediaManifest as MediaManifest;
|
|
220
247
|
await persistMediaManifest(loadedManifest);
|
|
221
248
|
}
|
|
249
|
+
setSavedSha(data.savedBranchSha ?? null);
|
|
250
|
+
setMainSha(data.sha);
|
|
251
|
+
setChangedSectionIds(new Set(data.changedSectionIds ?? []));
|
|
252
|
+
if (data.mainIndex) {
|
|
253
|
+
setMainIndex(data.mainIndex);
|
|
254
|
+
}
|
|
222
255
|
}
|
|
223
256
|
|
|
224
257
|
if (cancelled) return;
|
|
@@ -399,17 +432,7 @@ export default function EditorShell({
|
|
|
399
432
|
|
|
400
433
|
const onDeleteSection = useCallback(
|
|
401
434
|
(sectionId: string) => {
|
|
402
|
-
|
|
403
|
-
setSiteIndex((prev) => {
|
|
404
|
-
const { [sectionId]: _, ...remainingSections } = prev.sections;
|
|
405
|
-
return {
|
|
406
|
-
...prev,
|
|
407
|
-
order: prev.order.filter((id) => id !== sectionId),
|
|
408
|
-
sections: remainingSections,
|
|
409
|
-
};
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
persistence.markSectionDeleted(sectionId);
|
|
435
|
+
setDeletedSections((prev) => new Set(prev).add(sectionId));
|
|
413
436
|
setLocalChangesExist(true);
|
|
414
437
|
setDirtySectionIds((prev) => {
|
|
415
438
|
const next = new Set(prev);
|
|
@@ -417,9 +440,17 @@ export default function EditorShell({
|
|
|
417
440
|
return next;
|
|
418
441
|
});
|
|
419
442
|
},
|
|
420
|
-
[
|
|
443
|
+
[],
|
|
421
444
|
);
|
|
422
445
|
|
|
446
|
+
const handleUndoDelete = useCallback((sectionId: string) => {
|
|
447
|
+
setDeletedSections((prev) => {
|
|
448
|
+
const next = new Set(prev);
|
|
449
|
+
next.delete(sectionId);
|
|
450
|
+
return next;
|
|
451
|
+
});
|
|
452
|
+
}, []);
|
|
453
|
+
|
|
423
454
|
const onReorderSections = useCallback(
|
|
424
455
|
(fromIndex: number, toIndex: number) => {
|
|
425
456
|
setSections((prev) => {
|
|
@@ -461,7 +492,7 @@ export default function EditorShell({
|
|
|
461
492
|
);
|
|
462
493
|
|
|
463
494
|
const onStatusChange = useCallback(
|
|
464
|
-
(sectionId: string, status: "draft" | "
|
|
495
|
+
(sectionId: string, status: "draft" | "live" | "archived") => {
|
|
465
496
|
setSections((prev) =>
|
|
466
497
|
prev.map((loaded) =>
|
|
467
498
|
loaded.section.id === sectionId
|
|
@@ -526,6 +557,10 @@ export default function EditorShell({
|
|
|
526
557
|
|
|
527
558
|
return (
|
|
528
559
|
<EditorProvider>
|
|
560
|
+
<ViewBranchWatcher
|
|
561
|
+
setViewSections={setViewSections}
|
|
562
|
+
setIsLoadingViewContent={setIsLoadingViewContent}
|
|
563
|
+
/>
|
|
529
564
|
<EditorModalProvider>
|
|
530
565
|
<MediaLibraryContext.Provider value={{
|
|
531
566
|
...mediaPipeline.contextValue,
|
|
@@ -537,10 +572,13 @@ export default function EditorShell({
|
|
|
537
572
|
}}>
|
|
538
573
|
<div className="editor-shell relative">
|
|
539
574
|
<EditorToolbar
|
|
575
|
+
buttonState={buttonState}
|
|
540
576
|
localChangesExist={localChangesExist}
|
|
541
577
|
isPublishing={isPublishing}
|
|
542
578
|
publishFeedback={publishFeedback}
|
|
579
|
+
onSave={handleSave}
|
|
543
580
|
onPublish={handlePublish}
|
|
581
|
+
onSaveAndPublish={handleSaveAndPublish}
|
|
544
582
|
onDiscardClick={() => setShowDiscardConfirm(true)}
|
|
545
583
|
onSettingsClick={() => setShowSiteSettings(true)}
|
|
546
584
|
onMediaClick={() => {
|
|
@@ -548,19 +586,28 @@ export default function EditorShell({
|
|
|
548
586
|
setShowMediaLibrary(true);
|
|
549
587
|
}}
|
|
550
588
|
processingItems={mediaPipeline.processingItems}
|
|
589
|
+
buildState={buildStatus.state}
|
|
590
|
+
buildDeployUrl={buildStatus.deployUrl}
|
|
591
|
+
buildVisible={buildStatus.visible}
|
|
592
|
+
onBuildDismiss={buildStatus.dismiss}
|
|
551
593
|
/>
|
|
552
594
|
|
|
553
595
|
<EditorContent
|
|
554
596
|
sections={sections}
|
|
555
597
|
audiences={audiences}
|
|
556
598
|
dirtySectionIds={dirtySectionIds}
|
|
599
|
+
deletedSections={deletedSections}
|
|
557
600
|
isPublishing={isPublishing}
|
|
558
601
|
onSectionChange={onSectionChange}
|
|
559
602
|
onAddSection={onAddSection}
|
|
560
603
|
onDeleteSection={onDeleteSection}
|
|
604
|
+
onUndoDelete={handleUndoDelete}
|
|
561
605
|
onReorderSections={onReorderSections}
|
|
562
606
|
onAccessChange={onAccessChange}
|
|
563
607
|
onStatusChange={onStatusChange}
|
|
608
|
+
mainIndex={mainIndex}
|
|
609
|
+
changedSectionIds={changedSectionIds}
|
|
610
|
+
viewSections={viewSections}
|
|
564
611
|
/>
|
|
565
612
|
<GlobalModal />
|
|
566
613
|
<SiteSettingsModal
|
|
@@ -626,36 +673,82 @@ export default function EditorShell({
|
|
|
626
673
|
);
|
|
627
674
|
}
|
|
628
675
|
|
|
676
|
+
function ViewBranchWatcher({
|
|
677
|
+
setViewSections,
|
|
678
|
+
setIsLoadingViewContent,
|
|
679
|
+
}: {
|
|
680
|
+
setViewSections: (sections: LoadedSection[] | null) => void;
|
|
681
|
+
setIsLoadingViewContent: (loading: boolean) => void;
|
|
682
|
+
}) {
|
|
683
|
+
const { isEditMode, viewBranch } = useEditorContext();
|
|
684
|
+
|
|
685
|
+
useEffect(() => {
|
|
686
|
+
if (isEditMode) {
|
|
687
|
+
setViewSections(null);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const branch = viewBranch === "live" ? "main" : "saved";
|
|
692
|
+
setIsLoadingViewContent(true);
|
|
693
|
+
|
|
694
|
+
fetch(`/api/content?branch=${branch}`)
|
|
695
|
+
.then((res) => res.json())
|
|
696
|
+
.then((data) => {
|
|
697
|
+
setViewSections(data.sections);
|
|
698
|
+
})
|
|
699
|
+
.catch(console.error)
|
|
700
|
+
.finally(() => setIsLoadingViewContent(false));
|
|
701
|
+
}, [viewBranch, isEditMode]);
|
|
702
|
+
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
|
|
629
706
|
function EditorContent({
|
|
630
707
|
sections,
|
|
631
708
|
audiences,
|
|
632
709
|
dirtySectionIds,
|
|
710
|
+
deletedSections,
|
|
633
711
|
isPublishing,
|
|
634
712
|
onSectionChange,
|
|
635
713
|
onAddSection,
|
|
636
714
|
onDeleteSection,
|
|
715
|
+
onUndoDelete,
|
|
637
716
|
onReorderSections,
|
|
638
717
|
onAccessChange,
|
|
639
718
|
onStatusChange,
|
|
719
|
+
mainIndex,
|
|
720
|
+
changedSectionIds,
|
|
721
|
+
viewSections,
|
|
640
722
|
}: {
|
|
641
723
|
sections: LoadedSection[];
|
|
642
724
|
audiences: Audience[];
|
|
643
725
|
dirtySectionIds: Set<string>;
|
|
726
|
+
deletedSections: Set<string>;
|
|
644
727
|
isPublishing: boolean;
|
|
645
728
|
onSectionChange: (sectionId: string, content: SectionContent) => void;
|
|
646
729
|
onAddSection: (insertIndex: number, type: string) => void;
|
|
647
730
|
onDeleteSection: (sectionId: string) => void;
|
|
731
|
+
onUndoDelete: (sectionId: string) => void;
|
|
648
732
|
onReorderSections: (fromIndex: number, toIndex: number) => void;
|
|
649
733
|
onAccessChange: (sectionId: string, access: string[]) => void;
|
|
650
|
-
onStatusChange: (sectionId: string, status: "draft" | "
|
|
734
|
+
onStatusChange: (sectionId: string, status: "draft" | "live" | "archived") => void;
|
|
735
|
+
mainIndex: SiteIndex | null;
|
|
736
|
+
changedSectionIds: Set<string>;
|
|
737
|
+
viewSections: LoadedSection[] | null;
|
|
651
738
|
}) {
|
|
652
|
-
const { isEditMode } = useEditorContext();
|
|
653
|
-
const { openModal
|
|
739
|
+
const { isEditMode, viewBranch } = useEditorContext();
|
|
740
|
+
const { openModal } = useEditorModal();
|
|
654
741
|
const [pendingInsertIndex, setPendingInsertIndex] = useState<number | null>(null);
|
|
655
742
|
const dismissPendingInsert = useCallback(() => setPendingInsertIndex(null), []);
|
|
656
743
|
|
|
657
744
|
const editingEnabled = isEditMode && !isPublishing;
|
|
658
745
|
|
|
746
|
+
// In view mode, render viewSections (fetched from the selected branch).
|
|
747
|
+
// When viewBranch is "live", additionally filter to only live sections.
|
|
748
|
+
const displaySections = !isEditMode && viewSections !== null
|
|
749
|
+
? (viewBranch === "live" ? viewSections.filter((s) => s.meta.status === "live") : viewSections)
|
|
750
|
+
: sections;
|
|
751
|
+
|
|
659
752
|
useEffect(() => {
|
|
660
753
|
return monitorForElements({
|
|
661
754
|
onDragStart: ({ source }) => {
|
|
@@ -668,7 +761,7 @@ function EditorContent({
|
|
|
668
761
|
|
|
669
762
|
return (
|
|
670
763
|
<div>
|
|
671
|
-
{
|
|
764
|
+
{displaySections.map(({ section, meta }, index) => {
|
|
672
765
|
const definition = getSection(section.type);
|
|
673
766
|
if (!definition) {
|
|
674
767
|
return (
|
|
@@ -704,14 +797,19 @@ function EditorContent({
|
|
|
704
797
|
status={meta.status}
|
|
705
798
|
dirty={dirtySectionIds.has(section.id)}
|
|
706
799
|
index={index}
|
|
707
|
-
isLast={index ===
|
|
800
|
+
isLast={index === displaySections.length - 1}
|
|
708
801
|
definition={definition}
|
|
802
|
+
mainStatus={mainIndex?.sections[section.id]?.status ?? null}
|
|
803
|
+
contentDiffersFromMain={changedSectionIds.has(section.id) || dirtySectionIds.has(section.id)}
|
|
804
|
+
isLocalOnly={dirtySectionIds.has(section.id) && !changedSectionIds.has(section.id)}
|
|
709
805
|
options={{
|
|
710
806
|
...(section.content as Record<string, unknown>),
|
|
711
807
|
...("options" in section ? (section.options as Record<string, unknown>) : {}),
|
|
712
808
|
}}
|
|
713
809
|
audiences={audiences}
|
|
714
810
|
access={meta.access}
|
|
811
|
+
isDeleted={deletedSections.has(section.id)}
|
|
812
|
+
onUndoDelete={() => onUndoDelete(section.id)}
|
|
715
813
|
onAccessChange={editingEnabled ? (a) => onAccessChange(section.id, a) : undefined}
|
|
716
814
|
onStatusChange={editingEnabled ? (s) => onStatusChange(section.id, s) : undefined}
|
|
717
815
|
onSectionChange={editingEnabled ? (settingsResult) => {
|
|
@@ -733,26 +831,7 @@ function EditorContent({
|
|
|
733
831
|
} : undefined}
|
|
734
832
|
onReorder={editingEnabled ? onReorderSections : undefined}
|
|
735
833
|
onRequestInsert={editingEnabled ? (i) => setPendingInsertIndex(i) : undefined}
|
|
736
|
-
onDelete={editingEnabled ? () =>
|
|
737
|
-
openModal("Delete Section", (
|
|
738
|
-
<div>
|
|
739
|
-
<p className="mb-4 text-sm text-base-contrast-light">
|
|
740
|
-
Delete this <strong>{definition.label}</strong> section? This cannot be undone.
|
|
741
|
-
</p>
|
|
742
|
-
<div className="flex justify-end gap-3">
|
|
743
|
-
<Button variant="secondary" size="md" onClick={() => closeModal()}>
|
|
744
|
-
Cancel
|
|
745
|
-
</Button>
|
|
746
|
-
<Button variant="destructive" size="md" onClick={() => {
|
|
747
|
-
onDeleteSection(section.id);
|
|
748
|
-
closeModal();
|
|
749
|
-
}}>
|
|
750
|
-
Confirm
|
|
751
|
-
</Button>
|
|
752
|
-
</div>
|
|
753
|
-
</div>
|
|
754
|
-
));
|
|
755
|
-
} : undefined}
|
|
834
|
+
onDelete={editingEnabled ? () => onDeleteSection(section.id) : undefined}
|
|
756
835
|
>
|
|
757
836
|
<Component
|
|
758
837
|
content={section}
|
|
@@ -768,11 +847,11 @@ function EditorContent({
|
|
|
768
847
|
);
|
|
769
848
|
})}
|
|
770
849
|
|
|
771
|
-
{editingEnabled && pendingInsertIndex ===
|
|
850
|
+
{editingEnabled && pendingInsertIndex === displaySections.length && (
|
|
772
851
|
<SectionSkeleton
|
|
773
852
|
types={typeOptions}
|
|
774
853
|
onSelect={(type) => {
|
|
775
|
-
onAddSection(
|
|
854
|
+
onAddSection(displaySections.length, type);
|
|
776
855
|
setPendingInsertIndex(null);
|
|
777
856
|
}}
|
|
778
857
|
onDismiss={dismissPendingInsert}
|
|
@@ -798,25 +877,39 @@ function GlobalModal() {
|
|
|
798
877
|
}
|
|
799
878
|
|
|
800
879
|
function EditorToolbar({
|
|
880
|
+
buttonState,
|
|
801
881
|
localChangesExist,
|
|
802
882
|
isPublishing,
|
|
803
883
|
publishFeedback,
|
|
884
|
+
onSave,
|
|
804
885
|
onPublish,
|
|
886
|
+
onSaveAndPublish,
|
|
805
887
|
onDiscardClick,
|
|
806
888
|
onSettingsClick,
|
|
807
889
|
onMediaClick,
|
|
808
890
|
processingItems,
|
|
891
|
+
buildState,
|
|
892
|
+
buildDeployUrl,
|
|
893
|
+
buildVisible,
|
|
894
|
+
onBuildDismiss,
|
|
809
895
|
}: {
|
|
896
|
+
buttonState: "synced" | "publish" | "saveAndPublish";
|
|
810
897
|
localChangesExist: boolean;
|
|
811
898
|
isPublishing: boolean;
|
|
812
899
|
publishFeedback: string | null;
|
|
900
|
+
onSave: () => void;
|
|
813
901
|
onPublish: () => void;
|
|
902
|
+
onSaveAndPublish: () => void;
|
|
814
903
|
onDiscardClick: () => void;
|
|
815
904
|
onSettingsClick: () => void;
|
|
816
905
|
onMediaClick: () => void;
|
|
817
906
|
processingItems: QueueItem[];
|
|
907
|
+
buildState: "idle" | "building" | "ready" | "error";
|
|
908
|
+
buildDeployUrl: string | null;
|
|
909
|
+
buildVisible: boolean;
|
|
910
|
+
onBuildDismiss: () => void;
|
|
818
911
|
}) {
|
|
819
|
-
const { isEditMode, toggleEditMode } = useEditorContext();
|
|
912
|
+
const { isEditMode, viewBranch, setViewBranch, toggleEditMode } = useEditorContext();
|
|
820
913
|
|
|
821
914
|
return (
|
|
822
915
|
<>
|
|
@@ -826,23 +919,40 @@ function EditorToolbar({
|
|
|
826
919
|
{publishFeedback && (
|
|
827
920
|
<span className={cn(
|
|
828
921
|
"text-xs font-medium",
|
|
829
|
-
publishFeedback === "Published"
|
|
922
|
+
publishFeedback === "Published" || publishFeedback === "Saved"
|
|
923
|
+
? "text-green-600"
|
|
924
|
+
: "text-red-600",
|
|
830
925
|
)}>
|
|
831
926
|
{publishFeedback}
|
|
832
927
|
</span>
|
|
833
928
|
)}
|
|
834
|
-
{
|
|
835
|
-
<
|
|
836
|
-
|
|
837
|
-
|
|
929
|
+
{buttonState === "saveAndPublish" && (
|
|
930
|
+
<SplitButton
|
|
931
|
+
label="Save & Publish"
|
|
932
|
+
onClick={onSaveAndPublish}
|
|
933
|
+
isLoading={isPublishing}
|
|
934
|
+
loadingLabel="Publishing..."
|
|
935
|
+
options={[{ label: "Save", onClick: onSave }]}
|
|
936
|
+
/>
|
|
937
|
+
)}
|
|
938
|
+
{buttonState === "publish" && (
|
|
939
|
+
<SplitButton
|
|
940
|
+
label="Publish"
|
|
838
941
|
onClick={onPublish}
|
|
839
942
|
isLoading={isPublishing}
|
|
840
943
|
loadingLabel="Publishing..."
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
944
|
+
options={[]}
|
|
945
|
+
/>
|
|
946
|
+
)}
|
|
947
|
+
{buttonState === "synced" && (
|
|
948
|
+
<SplitButton
|
|
949
|
+
label="Everything up-to-date"
|
|
950
|
+
onClick={() => {}}
|
|
951
|
+
disabled
|
|
952
|
+
options={[]}
|
|
953
|
+
/>
|
|
844
954
|
)}
|
|
845
|
-
{localChangesExist && (
|
|
955
|
+
{localChangesExist && !isPublishing && (
|
|
846
956
|
<Button
|
|
847
957
|
type="button"
|
|
848
958
|
variant="destructive"
|
|
@@ -852,6 +962,12 @@ function EditorToolbar({
|
|
|
852
962
|
Discard Changes
|
|
853
963
|
</Button>
|
|
854
964
|
)}
|
|
965
|
+
<BuildStatusIndicator
|
|
966
|
+
state={buildState}
|
|
967
|
+
deployUrl={buildDeployUrl}
|
|
968
|
+
visible={buildVisible}
|
|
969
|
+
onDismiss={onBuildDismiss}
|
|
970
|
+
/>
|
|
855
971
|
</div>
|
|
856
972
|
<div className="flex items-center gap-2">
|
|
857
973
|
<ProcessingIndicator items={processingItems} />
|
|
@@ -873,6 +989,18 @@ function EditorToolbar({
|
|
|
873
989
|
</div>
|
|
874
990
|
</div>
|
|
875
991
|
)}
|
|
992
|
+
{!isEditMode && (
|
|
993
|
+
<div className="fixed top-0 right-0 left-0 z-50 flex items-center justify-center border-b border-base-200 bg-base px-4 py-2">
|
|
994
|
+
<SegmentedControl
|
|
995
|
+
options={[
|
|
996
|
+
{ value: "saved", label: "Draft" },
|
|
997
|
+
{ value: "live", label: "Live" },
|
|
998
|
+
]}
|
|
999
|
+
value={viewBranch}
|
|
1000
|
+
onChange={(v) => setViewBranch(v as "saved" | "live")}
|
|
1001
|
+
/>
|
|
1002
|
+
</div>
|
|
1003
|
+
)}
|
|
876
1004
|
<button
|
|
877
1005
|
onClick={toggleEditMode}
|
|
878
1006
|
className="cursor-pointer fixed bottom-4 right-4 z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity"
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { useActiveHeadings } from "./useActiveHeadings";
|
|
2
|
+
export { useBuildStatus } from "./useBuildStatus";
|
|
3
|
+
export { useContentLifecycle } from "./useContentLifecycle";
|
|
2
4
|
export { useEditorPersistence } from "./useEditorPersistence";
|
|
3
5
|
export { useEditorPublish } from "./useEditorPublish";
|
|
4
6
|
export { useFocusTrap } from "./useFocusTrap";
|