@drawnagency/primitives 0.1.1 → 0.2.0
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/auth/index.js +26 -3
- package/dist/chunk-2VTPWODA.js +60 -0
- package/dist/chunk-CS7F6IOY.js +39 -0
- package/dist/chunk-HOJAF4VD.js +264 -0
- package/dist/chunk-IP6ODLXX.js +341 -0
- package/dist/chunk-T4BJ6RSB.js +58 -0
- package/dist/chunk-UKEVUCIZ.js +200 -0
- package/dist/chunk-UMSFICAC.js +36 -0
- package/dist/index.js +156 -4
- package/dist/lib/index.js +62 -12
- package/dist/lib/sanitize.d.ts.map +1 -1
- package/dist/media/index.js +36 -9
- package/dist/schemas/index.js +52 -7
- package/package.json +4 -4
- package/src/lib/sanitize.ts +6 -2
- package/dist/auth/cookies.js +0 -44
- package/dist/auth/errors.js +0 -10
- package/dist/auth/security.js +0 -48
- package/dist/auth/types.js +0 -1
- package/dist/components/brandguide/ColorSwatchSettings.js +0 -10
- package/dist/components/brandguide/Colors.js +0 -79
- package/dist/components/brandguide/DoDontList.js +0 -22
- package/dist/components/brandguide/DoDontMediaGrid.js +0 -5
- package/dist/components/editor/AudiencePicker.js +0 -24
- package/dist/components/editor/DeleteButton.js +0 -6
- package/dist/components/editor/DragHandle.js +0 -8
- package/dist/components/editor/InsertButton.js +0 -7
- package/dist/components/editor/SectionWrapper.js +0 -135
- package/dist/components/editor/SettingsButton.js +0 -6
- package/dist/components/editor/SettingsForm.js +0 -64
- package/dist/components/editor/StatusBadge.js +0 -10
- package/dist/components/editor/StatusPicker.js +0 -30
- package/dist/components/editor/index.js +0 -7
- package/dist/components/primitives/CustomParagraph.js +0 -24
- package/dist/components/primitives/EditableGrid.js +0 -90
- package/dist/components/primitives/EditableList.js +0 -54
- package/dist/components/primitives/EditablePlainText.js +0 -52
- package/dist/components/primitives/EditableRichText.js +0 -86
- package/dist/components/primitives/HeadingSection.js +0 -7
- package/dist/components/primitives/IconPicker.js +0 -21
- package/dist/components/primitives/LinkPopover.js +0 -48
- package/dist/components/primitives/MediaSettingsForms.js +0 -42
- package/dist/components/primitives/ResolvedMedia.js +0 -9
- package/dist/components/primitives/RichTextToolbar.js +0 -26
- package/dist/components/primitives/tiptap-presets.js +0 -44
- package/dist/components/primitives/useEditableCollection.js +0 -61
- package/dist/components/primitives/useEditablePlainText.js +0 -27
- package/dist/components/primitives/useEditableRichText.js +0 -52
- package/dist/components/sections/Button/CTAButton.js +0 -18
- package/dist/components/sections/Button/index.js +0 -28
- package/dist/components/sections/Colors/index.js +0 -34
- package/dist/components/sections/DoDontList/index.js +0 -33
- package/dist/components/sections/DoDontMediaGrid/index.js +0 -41
- package/dist/components/sections/IconList/IconList.js +0 -131
- package/dist/components/sections/IconList/IconListSettings.js +0 -22
- package/dist/components/sections/IconList/index.js +0 -27
- package/dist/components/sections/LinkHeading/index.js +0 -15
- package/dist/components/sections/MediaGrid/MediaGrid.js +0 -62
- package/dist/components/sections/MediaGrid/index.js +0 -35
- package/dist/components/sections/Prose/Prose.js +0 -11
- package/dist/components/sections/Prose/index.js +0 -15
- package/dist/components/sections/SectionLayout.js +0 -15
- package/dist/components/sections/SplitContent/SplitContent.js +0 -31
- package/dist/components/sections/SplitContent/SplitContentSettings.js +0 -17
- package/dist/components/sections/SplitContent/index.js +0 -27
- package/dist/components/sections/SubHeading/index.js +0 -18
- package/dist/components/sections/SubSubHeading/index.js +0 -18
- package/dist/components/sections/ViewRenderer.js +0 -13
- package/dist/components/sections/register-schemas.js +0 -15
- package/dist/components/sections/register.js +0 -15
- package/dist/components/shared/Button.js +0 -27
- package/dist/components/shared/Checkbox.js +0 -10
- package/dist/components/shared/ColorPicker.js +0 -5
- package/dist/components/shared/ErrorBoundary.js +0 -30
- package/dist/components/shared/FontPicker.js +0 -190
- package/dist/components/shared/FormLabel.js +0 -5
- package/dist/components/shared/IconButton.js +0 -16
- package/dist/components/shared/Input.js +0 -8
- package/dist/components/shared/Navigation.js +0 -71
- package/dist/components/shared/PasswordInput.js +0 -11
- package/dist/components/shared/Popover.js +0 -33
- package/dist/components/shared/PopoverItem.js +0 -6
- package/dist/components/shared/Select.js +0 -9
- package/dist/components/shared/Textarea.js +0 -8
- package/dist/components/shared/Toggle.js +0 -5
- package/dist/components/shared/Tooltip.js +0 -8
- package/dist/components/shared/icons.js +0 -23
- package/dist/components/shell/AudienceAddForm.js +0 -43
- package/dist/components/shell/AudienceRow.js +0 -74
- package/dist/components/shell/EditorContext.js +0 -24
- package/dist/components/shell/EditorLoginForm.js +0 -46
- package/dist/components/shell/EditorModal.js +0 -43
- package/dist/components/shell/EditorModalContext.js +0 -20
- package/dist/components/shell/EditorShell.js +0 -483
- package/dist/components/shell/MediaLibraryContext.js +0 -5
- package/dist/components/shell/MediaLibraryModal.js +0 -145
- package/dist/components/shell/ProcessingIndicator.js +0 -15
- package/dist/components/shell/SectionSkeleton.js +0 -22
- package/dist/components/shell/SectionTypePicker.js +0 -15
- package/dist/components/shell/SiteSettingsDisplay.js +0 -28
- package/dist/components/shell/SiteSettingsModal.js +0 -40
- package/dist/components/shell/SiteSettingsUsers.js +0 -87
- package/dist/components/shell/SiteSettingsViewerAccess.js +0 -94
- package/dist/components/shell/ViewerLoginForm.js +0 -40
- package/dist/data/google-fonts.json +0 -7718
- package/dist/hooks/index.js +0 -6
- package/dist/hooks/useActiveHeadings.js +0 -99
- package/dist/hooks/useEditorPersistence.js +0 -73
- package/dist/hooks/useEditorPublish.js +0 -145
- package/dist/hooks/useFocusTrap.js +0 -51
- package/dist/hooks/useMediaPipeline.js +0 -253
- package/dist/hooks/useResolvedMedia.js +0 -39
- package/dist/lib/cn.js +0 -5
- package/dist/lib/contrast.js +0 -11
- package/dist/lib/dexie.js +0 -236
- package/dist/lib/events.js +0 -15
- package/dist/lib/google-fonts.js +0 -11
- package/dist/lib/grid.js +0 -7
- package/dist/lib/icons.js +0 -27
- package/dist/lib/loader.js +0 -57
- package/dist/lib/nav.js +0 -58
- package/dist/lib/registry.js +0 -64
- package/dist/lib/safeRedirect.js +0 -11
- package/dist/lib/sanitize.js +0 -6
- package/dist/lib/timestamp.js +0 -28
- package/dist/media/github.js +0 -60
- package/dist/media/queue.js +0 -116
- package/dist/media/resolve.js +0 -50
- package/dist/media/types.js +0 -1
- package/dist/media/utils.js +0 -41
- package/dist/media/videoPoster.js +0 -44
- package/dist/media/worker.js +0 -73
- package/dist/schemas/audience.js +0 -19
- package/dist/schemas/auth.js +0 -22
- package/dist/schemas/media-grid-options.js +0 -7
- package/dist/schemas/media.js +0 -28
- package/dist/schemas/sections.js +0 -12
- package/dist/schemas/shared.js +0 -71
- package/dist/schemas/site-config.js +0 -26
|
@@ -1,483 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useState, useCallback, useEffect, useRef } from "react";
|
|
3
|
-
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
4
|
-
import { SiteConfigSchema } from "../../schemas/site-config";
|
|
5
|
-
import { EditorProvider, useEditorContext } from "./EditorContext";
|
|
6
|
-
import { EditorModalProvider, useEditorModal } from "./EditorModalContext";
|
|
7
|
-
import { EditorModal } from "./EditorModal";
|
|
8
|
-
import { SiteSettingsModal } from "./SiteSettingsModal";
|
|
9
|
-
import { MediaLibraryModal } from "./MediaLibraryModal";
|
|
10
|
-
import { MediaLibraryContext } from "./MediaLibraryContext";
|
|
11
|
-
import { ProcessingIndicator } from "./ProcessingIndicator";
|
|
12
|
-
import { SectionSkeleton } from "./SectionSkeleton";
|
|
13
|
-
import "../sections/register";
|
|
14
|
-
import { getSection, getAllSections } from "../../lib/registry";
|
|
15
|
-
import { SectionWrapper } from "../editor/SectionWrapper";
|
|
16
|
-
import { SectionLayout } from "../sections/SectionLayout";
|
|
17
|
-
import { initEditorStore, checkForLocalChanges, restoreLocalChanges, discardLocalChanges, getCachedContent, cacheContent, persistMediaManifest, getMediaManifest, getPendingMediaItems, getPendingMediaLocalUrls, getPendingMediaDeletions, } from "../../lib/dexie";
|
|
18
|
-
import { useEditorPersistence } from "../../hooks/useEditorPersistence";
|
|
19
|
-
import { useEditorPublish } from "../../hooks/useEditorPublish";
|
|
20
|
-
import { useMediaPipeline } from "../../hooks/useMediaPipeline";
|
|
21
|
-
import { formatTimestamp } from "../../lib/timestamp";
|
|
22
|
-
import { generateNavLinks } from "../../lib/nav";
|
|
23
|
-
import { navChangeEvent, darkModeEvent } from "../../lib/events";
|
|
24
|
-
import { cn } from "../../lib/cn";
|
|
25
|
-
import { Button } from "../shared/Button";
|
|
26
|
-
import { IconButton } from "../shared/IconButton";
|
|
27
|
-
import { SettingsIcon } from "../shared/icons";
|
|
28
|
-
import { ImageIcon } from "lucide-react";
|
|
29
|
-
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
|
30
|
-
export { useMediaLibrary } from "./MediaLibraryContext";
|
|
31
|
-
const typeOptions = getAllSections().map((def) => ({
|
|
32
|
-
type: def.type,
|
|
33
|
-
label: def.label,
|
|
34
|
-
icon: () => null,
|
|
35
|
-
}));
|
|
36
|
-
export default function EditorShell({ headSha, siteId, audiences: initialAudiences, capabilities, currentUser, }) {
|
|
37
|
-
const [shellState, setShellState] = useState({ phase: "loading-content" });
|
|
38
|
-
const [sections, setSections] = useState([]);
|
|
39
|
-
const [siteIndex, setSiteIndex] = useState({ siteId, order: [], sections: {} });
|
|
40
|
-
const [audiences, setAudiences] = useState(initialAudiences);
|
|
41
|
-
const [localChangesExist, setLocalChangesExist] = useState(false);
|
|
42
|
-
const [showDiscardConfirm, setShowDiscardConfirm] = useState(false);
|
|
43
|
-
const [showSiteSettings, setShowSiteSettings] = useState(false);
|
|
44
|
-
const [dirtySectionIds, setDirtySectionIds] = useState(new Set());
|
|
45
|
-
const [siteConfig, setSiteConfig] = useState(null);
|
|
46
|
-
const [mediaManifest, setMediaManifest] = useState({ images: {} });
|
|
47
|
-
const [showMediaLibrary, setShowMediaLibrary] = useState(false);
|
|
48
|
-
const [mediaModalMode, setMediaModalMode] = useState("manage");
|
|
49
|
-
const [mediaSelectCallback, setMediaSelectCallback] = useState(null);
|
|
50
|
-
const siteIndexRef = useRef({ siteId, order: [], sections: {} });
|
|
51
|
-
const fontLinkRef = useRef(null);
|
|
52
|
-
useEffect(() => { siteIndexRef.current = siteIndex; }, [siteIndex]);
|
|
53
|
-
const persistence = useEditorPersistence(siteIndexRef);
|
|
54
|
-
const mediaPipeline = useMediaPipeline({
|
|
55
|
-
siteConfig,
|
|
56
|
-
mediaManifest,
|
|
57
|
-
setMediaManifest,
|
|
58
|
-
sections,
|
|
59
|
-
setSections,
|
|
60
|
-
setLocalChangesExist,
|
|
61
|
-
setDirtySectionIds,
|
|
62
|
-
markSectionDirty: persistence.markSectionDirty,
|
|
63
|
-
});
|
|
64
|
-
const { isPublishing, publishFeedback, handlePublish } = useEditorPublish({
|
|
65
|
-
flushNow: persistence.flushNow,
|
|
66
|
-
cancelPendingFlush: persistence.cancelPendingFlush,
|
|
67
|
-
isConfigDirty: persistence.isConfigDirty,
|
|
68
|
-
clearConfigDirty: persistence.clearConfigDirty,
|
|
69
|
-
siteIndexRef,
|
|
70
|
-
siteConfig,
|
|
71
|
-
sections,
|
|
72
|
-
onSuccess: () => {
|
|
73
|
-
setDirtySectionIds(new Set());
|
|
74
|
-
setLocalChangesExist(false);
|
|
75
|
-
},
|
|
76
|
-
mediaManifest,
|
|
77
|
-
pendingMediaItems: mediaPipeline.pendingMediaItems,
|
|
78
|
-
pendingMediaDeletions: mediaPipeline.pendingDeletions,
|
|
79
|
-
onMediaPublished: (publishedItems, publishedDeletions) => {
|
|
80
|
-
mediaPipeline.setPendingMediaItems([]);
|
|
81
|
-
mediaPipeline.setPendingLocalUrls({});
|
|
82
|
-
mediaPipeline.setPendingDeletions([]);
|
|
83
|
-
setMediaManifest((prev) => {
|
|
84
|
-
const images = { ...prev.images };
|
|
85
|
-
for (const item of publishedItems) {
|
|
86
|
-
images[item.id] = item;
|
|
87
|
-
}
|
|
88
|
-
for (const id of publishedDeletions) {
|
|
89
|
-
delete images[id];
|
|
90
|
-
}
|
|
91
|
-
return { images };
|
|
92
|
-
});
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
if (sections.length === 0)
|
|
97
|
-
return;
|
|
98
|
-
const navLinks = generateNavLinks(sections);
|
|
99
|
-
navChangeEvent.dispatch(navLinks);
|
|
100
|
-
}, [sections]);
|
|
101
|
-
const applySiteConfigPreview = useCallback((config) => {
|
|
102
|
-
const root = document.documentElement;
|
|
103
|
-
root.style.setProperty("--color-primary", config.primaryColor);
|
|
104
|
-
root.style.setProperty("--color-primary-contrast", config.primaryContrast);
|
|
105
|
-
root.style.setProperty("--font-heading", `${config.headingFont}, system-ui, sans-serif`);
|
|
106
|
-
root.style.setProperty("--font-body", `${config.bodyFont}, system-ui, sans-serif`);
|
|
107
|
-
if (config.googleFontsUrl) {
|
|
108
|
-
if (fontLinkRef.current?.href !== config.googleFontsUrl) {
|
|
109
|
-
const link = document.createElement("link");
|
|
110
|
-
link.rel = "stylesheet";
|
|
111
|
-
link.href = config.googleFontsUrl;
|
|
112
|
-
document.head.appendChild(link);
|
|
113
|
-
fontLinkRef.current?.remove();
|
|
114
|
-
fontLinkRef.current = link;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
fontLinkRef.current?.remove();
|
|
119
|
-
fontLinkRef.current = null;
|
|
120
|
-
}
|
|
121
|
-
if (config.darkMode === "dark") {
|
|
122
|
-
root.classList.add("dark");
|
|
123
|
-
}
|
|
124
|
-
else if (config.darkMode === "light") {
|
|
125
|
-
root.classList.remove("dark");
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
const stored = localStorage.getItem("theme");
|
|
129
|
-
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
130
|
-
root.classList.toggle("dark", stored === "dark" || (!stored && prefersDark));
|
|
131
|
-
}
|
|
132
|
-
localStorage.setItem("portal-dark-mode", config.darkMode);
|
|
133
|
-
localStorage.setItem("portal-primary-color", config.primaryColor);
|
|
134
|
-
localStorage.setItem("portal-primary-contrast", config.primaryContrast);
|
|
135
|
-
darkModeEvent.dispatch(config.darkMode);
|
|
136
|
-
}, []);
|
|
137
|
-
// --- Content loading ---
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
let cancelled = false;
|
|
140
|
-
async function loadContent() {
|
|
141
|
-
initEditorStore(siteId);
|
|
142
|
-
let loadedSections;
|
|
143
|
-
let loadedIndex;
|
|
144
|
-
let loadedConfig;
|
|
145
|
-
let loadedManifest = { images: {} };
|
|
146
|
-
const cached = await getCachedContent();
|
|
147
|
-
if (cached && cached.sha === headSha) {
|
|
148
|
-
loadedSections = cached.sections;
|
|
149
|
-
loadedIndex = cached.index;
|
|
150
|
-
loadedConfig = SiteConfigSchema.parse(cached.siteConfig);
|
|
151
|
-
const savedManifest = await getMediaManifest();
|
|
152
|
-
if (savedManifest)
|
|
153
|
-
loadedManifest = savedManifest;
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
const response = await fetch("/api/content");
|
|
157
|
-
if (!response.ok)
|
|
158
|
-
throw new Error(`Failed to load content: ${response.status}`);
|
|
159
|
-
const data = await response.json();
|
|
160
|
-
loadedSections = data.sections;
|
|
161
|
-
loadedIndex = data.index;
|
|
162
|
-
loadedConfig = SiteConfigSchema.parse(data.siteConfig);
|
|
163
|
-
await cacheContent(data.sha, data.sections, data.index, data.siteConfig);
|
|
164
|
-
if (data.mediaManifest) {
|
|
165
|
-
loadedManifest = data.mediaManifest;
|
|
166
|
-
await persistMediaManifest(loadedManifest);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (cancelled)
|
|
170
|
-
return;
|
|
171
|
-
setSections(loadedSections);
|
|
172
|
-
setSiteIndex(loadedIndex);
|
|
173
|
-
setSiteConfig(loadedConfig);
|
|
174
|
-
setMediaManifest(loadedManifest);
|
|
175
|
-
siteIndexRef.current = loadedIndex;
|
|
176
|
-
applySiteConfigPreview(loadedConfig);
|
|
177
|
-
// Load pending media from Dexie
|
|
178
|
-
const savedPendingItems = await getPendingMediaItems();
|
|
179
|
-
if (!cancelled && savedPendingItems.length > 0) {
|
|
180
|
-
mediaPipeline.setPendingMediaItems(savedPendingItems);
|
|
181
|
-
const urlMap = {};
|
|
182
|
-
for (const pi of savedPendingItems) {
|
|
183
|
-
const urls = await getPendingMediaLocalUrls(pi.id);
|
|
184
|
-
if (urls) {
|
|
185
|
-
const smallestKey = Object.keys(urls).find((k) => k !== "primary") ?? "primary";
|
|
186
|
-
if (urls[smallestKey])
|
|
187
|
-
urlMap[pi.id] = urls[smallestKey];
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (!cancelled)
|
|
191
|
-
mediaPipeline.setPendingLocalUrls(urlMap);
|
|
192
|
-
}
|
|
193
|
-
const savedDeletions = await getPendingMediaDeletions();
|
|
194
|
-
if (!cancelled && savedDeletions.length > 0)
|
|
195
|
-
mediaPipeline.setPendingDeletions(savedDeletions);
|
|
196
|
-
const result = await checkForLocalChanges();
|
|
197
|
-
if (cancelled)
|
|
198
|
-
return;
|
|
199
|
-
if (result) {
|
|
200
|
-
setShellState({ phase: "recovery", latestTimestamp: result.latestTimestamp });
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
setShellState({ phase: "ready" });
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
loadContent().catch((err) => {
|
|
207
|
-
console.error("Failed to load editor content:", err);
|
|
208
|
-
setShellState({ phase: "error", message: err instanceof Error ? err.message : "Failed to load content" });
|
|
209
|
-
});
|
|
210
|
-
return () => { cancelled = true; };
|
|
211
|
-
}, [headSha, siteId, applySiteConfigPreview]);
|
|
212
|
-
// --- Recovery handlers ---
|
|
213
|
-
const handleRestore = useCallback(async () => {
|
|
214
|
-
const restored = await restoreLocalChanges();
|
|
215
|
-
if (restored.siteIndex) {
|
|
216
|
-
const restoredIndex = restored.siteIndex;
|
|
217
|
-
setSections(() => restoredIndex.order.map((id) => {
|
|
218
|
-
const restoredContent = restored.sections[id];
|
|
219
|
-
const existing = sections.find((s) => s.section.id === id);
|
|
220
|
-
if (restoredContent) {
|
|
221
|
-
return {
|
|
222
|
-
section: { id, ...restoredContent },
|
|
223
|
-
meta: restoredIndex.sections[id] ?? existing?.meta ?? { type: restoredContent.type, status: "draft", access: ["internal"] },
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
if (existing) {
|
|
227
|
-
return { ...existing, meta: restoredIndex.sections[id] ?? existing.meta };
|
|
228
|
-
}
|
|
229
|
-
return null;
|
|
230
|
-
}).filter((s) => s !== null));
|
|
231
|
-
setSiteIndex(restoredIndex);
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
setSections((prev) => prev.map((loaded) => {
|
|
235
|
-
const restoredContent = restored.sections[loaded.section.id];
|
|
236
|
-
if (restoredContent) {
|
|
237
|
-
return { ...loaded, section: { id: loaded.section.id, ...restoredContent } };
|
|
238
|
-
}
|
|
239
|
-
return loaded;
|
|
240
|
-
}));
|
|
241
|
-
}
|
|
242
|
-
if (restored.siteConfig) {
|
|
243
|
-
const parsed = SiteConfigSchema.safeParse(restored.siteConfig);
|
|
244
|
-
if (parsed.success) {
|
|
245
|
-
setSiteConfig(parsed.data);
|
|
246
|
-
persistence.persistConfig(parsed.data);
|
|
247
|
-
applySiteConfigPreview(parsed.data);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
setLocalChangesExist(true);
|
|
251
|
-
setDirtySectionIds(new Set(Object.keys(restored.sections)));
|
|
252
|
-
setShellState({ phase: "ready" });
|
|
253
|
-
}, [sections, applySiteConfigPreview, persistence]);
|
|
254
|
-
const handleDiscard = useCallback(async () => {
|
|
255
|
-
await discardLocalChanges();
|
|
256
|
-
setLocalChangesExist(false);
|
|
257
|
-
setDirtySectionIds(new Set());
|
|
258
|
-
mediaPipeline.setPendingMediaItems([]);
|
|
259
|
-
mediaPipeline.setPendingLocalUrls({});
|
|
260
|
-
mediaPipeline.setPendingDeletions([]);
|
|
261
|
-
const cached = await getCachedContent();
|
|
262
|
-
if (cached) {
|
|
263
|
-
setSections(cached.sections);
|
|
264
|
-
setSiteIndex(cached.index);
|
|
265
|
-
siteIndexRef.current = cached.index;
|
|
266
|
-
const parsed = SiteConfigSchema.safeParse(cached.siteConfig);
|
|
267
|
-
if (parsed.success) {
|
|
268
|
-
setSiteConfig(parsed.data);
|
|
269
|
-
applySiteConfigPreview(parsed.data);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
const savedManifest = await getMediaManifest();
|
|
273
|
-
setMediaManifest(savedManifest ?? { images: {} });
|
|
274
|
-
persistence.clearConfigDirty();
|
|
275
|
-
setShellState({ phase: "ready" });
|
|
276
|
-
}, [applySiteConfigPreview, persistence]);
|
|
277
|
-
const handleToolbarDiscard = useCallback(async () => {
|
|
278
|
-
setShowDiscardConfirm(false);
|
|
279
|
-
await handleDiscard();
|
|
280
|
-
}, [handleDiscard]);
|
|
281
|
-
// --- Section CRUD ---
|
|
282
|
-
const onSectionChange = useCallback((sectionId, newContent) => {
|
|
283
|
-
setSections((prev) => prev.map((loaded) => loaded.section.id === sectionId
|
|
284
|
-
? { ...loaded, section: { id: loaded.section.id, ...newContent } }
|
|
285
|
-
: loaded));
|
|
286
|
-
persistence.markSectionDirty(sectionId, newContent);
|
|
287
|
-
setLocalChangesExist(true);
|
|
288
|
-
setDirtySectionIds((prev) => new Set(prev).add(sectionId));
|
|
289
|
-
}, [persistence]);
|
|
290
|
-
const onAddSection = useCallback((insertIndex, type) => {
|
|
291
|
-
const definition = getSection(type);
|
|
292
|
-
if (!definition)
|
|
293
|
-
return;
|
|
294
|
-
const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
|
295
|
-
? crypto.randomUUID()
|
|
296
|
-
: `_id_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
297
|
-
const content = definition.defaults();
|
|
298
|
-
const newSection = {
|
|
299
|
-
section: { id, ...content },
|
|
300
|
-
meta: { type, status: "draft", access: ["internal"] },
|
|
301
|
-
};
|
|
302
|
-
setSections((prev) => {
|
|
303
|
-
const next = [...prev];
|
|
304
|
-
next.splice(insertIndex, 0, newSection);
|
|
305
|
-
return next;
|
|
306
|
-
});
|
|
307
|
-
setSiteIndex((prev) => {
|
|
308
|
-
const newOrder = [...prev.order];
|
|
309
|
-
newOrder.splice(insertIndex, 0, id);
|
|
310
|
-
return {
|
|
311
|
-
...prev,
|
|
312
|
-
order: newOrder,
|
|
313
|
-
sections: { ...prev.sections, [id]: { type, status: "draft", access: ["internal"] } },
|
|
314
|
-
};
|
|
315
|
-
});
|
|
316
|
-
persistence.markSectionDirty(id, content);
|
|
317
|
-
persistence.markIndexDirty();
|
|
318
|
-
setLocalChangesExist(true);
|
|
319
|
-
setDirtySectionIds((prev) => new Set(prev).add(id));
|
|
320
|
-
}, [persistence]);
|
|
321
|
-
const onDeleteSection = useCallback((sectionId) => {
|
|
322
|
-
setSections((prev) => prev.filter((s) => s.section.id !== sectionId));
|
|
323
|
-
setSiteIndex((prev) => {
|
|
324
|
-
const { [sectionId]: _, ...remainingSections } = prev.sections;
|
|
325
|
-
return {
|
|
326
|
-
...prev,
|
|
327
|
-
order: prev.order.filter((id) => id !== sectionId),
|
|
328
|
-
sections: remainingSections,
|
|
329
|
-
};
|
|
330
|
-
});
|
|
331
|
-
persistence.markSectionDeleted(sectionId);
|
|
332
|
-
setLocalChangesExist(true);
|
|
333
|
-
setDirtySectionIds((prev) => {
|
|
334
|
-
const next = new Set(prev);
|
|
335
|
-
next.delete(sectionId);
|
|
336
|
-
return next;
|
|
337
|
-
});
|
|
338
|
-
}, [persistence]);
|
|
339
|
-
const onReorderSections = useCallback((fromIndex, toIndex) => {
|
|
340
|
-
setSections((prev) => {
|
|
341
|
-
const next = [...prev];
|
|
342
|
-
const [moved] = next.splice(fromIndex, 1);
|
|
343
|
-
next.splice(toIndex, 0, moved);
|
|
344
|
-
return next;
|
|
345
|
-
});
|
|
346
|
-
setSiteIndex((prev) => {
|
|
347
|
-
const newOrder = [...prev.order];
|
|
348
|
-
const [movedId] = newOrder.splice(fromIndex, 1);
|
|
349
|
-
newOrder.splice(toIndex, 0, movedId);
|
|
350
|
-
return { ...prev, order: newOrder };
|
|
351
|
-
});
|
|
352
|
-
persistence.markIndexDirty();
|
|
353
|
-
setLocalChangesExist(true);
|
|
354
|
-
}, [persistence]);
|
|
355
|
-
const onAccessChange = useCallback((sectionId, access) => {
|
|
356
|
-
setSections((prev) => prev.map((loaded) => loaded.section.id === sectionId
|
|
357
|
-
? { ...loaded, meta: { ...loaded.meta, access } }
|
|
358
|
-
: loaded));
|
|
359
|
-
setSiteIndex((prev) => ({
|
|
360
|
-
...prev,
|
|
361
|
-
sections: { ...prev.sections, [sectionId]: { ...prev.sections[sectionId], access } },
|
|
362
|
-
}));
|
|
363
|
-
persistence.markIndexDirty();
|
|
364
|
-
setLocalChangesExist(true);
|
|
365
|
-
}, [persistence]);
|
|
366
|
-
const onStatusChange = useCallback((sectionId, status) => {
|
|
367
|
-
setSections((prev) => prev.map((loaded) => loaded.section.id === sectionId
|
|
368
|
-
? { ...loaded, meta: { ...loaded.meta, status } }
|
|
369
|
-
: loaded));
|
|
370
|
-
setSiteIndex((prev) => ({
|
|
371
|
-
...prev,
|
|
372
|
-
sections: { ...prev.sections, [sectionId]: { ...prev.sections[sectionId], status } },
|
|
373
|
-
}));
|
|
374
|
-
persistence.markIndexDirty();
|
|
375
|
-
setLocalChangesExist(true);
|
|
376
|
-
}, [persistence]);
|
|
377
|
-
const handleSiteConfigChange = useCallback((config) => {
|
|
378
|
-
setSiteConfig(config);
|
|
379
|
-
applySiteConfigPreview(config);
|
|
380
|
-
persistence.persistConfig(config);
|
|
381
|
-
setLocalChangesExist(true);
|
|
382
|
-
}, [applySiteConfigPreview, persistence]);
|
|
383
|
-
// --- Render ---
|
|
384
|
-
if (shellState.phase === "loading-content") {
|
|
385
|
-
return _jsx("div", { className: "min-h-screen", "aria-busy": "true", "aria-label": "Loading content" });
|
|
386
|
-
}
|
|
387
|
-
if (shellState.phase === "error") {
|
|
388
|
-
return (_jsx("div", { className: "flex min-h-[50vh] items-center justify-center", children: _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-sm text-red-600", children: shellState.message }), _jsx(Button, { variant: "secondary", size: "md", className: "mt-3", onClick: () => window.location.reload(), children: "Retry" })] }) }));
|
|
389
|
-
}
|
|
390
|
-
if (shellState.phase === "recovery") {
|
|
391
|
-
return (_jsxs(EditorModal, { isOpen: true, onClose: () => { }, title: "Unsaved Changes", blocking: true, children: [_jsxs("p", { className: "text-sm text-base-contrast-light", children: ["You have unsaved changes from ", _jsx("strong", { children: formatTimestamp(shellState.latestTimestamp) }), ". Would you like to restore them or start fresh?"] }), _jsxs("div", { className: "mt-4 flex justify-end gap-3", children: [_jsx(Button, { type: "button", variant: "secondary", size: "md", onClick: handleDiscard, children: "Discard" }), _jsx(Button, { type: "button", variant: "primary", size: "md", onClick: handleRestore, children: "Restore" })] })] }));
|
|
392
|
-
}
|
|
393
|
-
return (_jsx(EditorProvider, { children: _jsx(EditorModalProvider, { children: _jsx(MediaLibraryContext.Provider, { value: {
|
|
394
|
-
...mediaPipeline.contextValue,
|
|
395
|
-
openSelectModal: (onSelect) => {
|
|
396
|
-
setMediaModalMode("select");
|
|
397
|
-
setMediaSelectCallback(() => onSelect);
|
|
398
|
-
setShowMediaLibrary(true);
|
|
399
|
-
},
|
|
400
|
-
}, children: _jsxs("div", { className: "editor-shell relative", children: [_jsx(EditorToolbar, { localChangesExist: localChangesExist, isPublishing: isPublishing, publishFeedback: publishFeedback, onPublish: handlePublish, onDiscardClick: () => setShowDiscardConfirm(true), onSettingsClick: () => setShowSiteSettings(true), onMediaClick: () => {
|
|
401
|
-
setMediaModalMode("manage");
|
|
402
|
-
setShowMediaLibrary(true);
|
|
403
|
-
}, processingItems: mediaPipeline.processingItems }), _jsx(EditorContent, { sections: sections, audiences: audiences, dirtySectionIds: dirtySectionIds, isPublishing: isPublishing, onSectionChange: onSectionChange, onAddSection: onAddSection, onDeleteSection: onDeleteSection, onReorderSections: onReorderSections, onAccessChange: onAccessChange, onStatusChange: onStatusChange }), _jsx(GlobalModal, {}), _jsx(SiteSettingsModal, { isOpen: showSiteSettings, onClose: () => setShowSiteSettings(false), siteConfig: siteConfig, onSiteConfigChange: handleSiteConfigChange, onAudiencesChange: setAudiences, capabilities: capabilities, currentUser: currentUser }), _jsxs(EditorModal, { isOpen: showDiscardConfirm, onClose: () => setShowDiscardConfirm(false), title: "Discard Changes", children: [_jsx("p", { className: "mb-4 text-sm text-base-contrast-light", children: "Discard all unsaved changes? This will reload with the last published content." }), _jsxs("div", { className: "flex justify-end gap-3", children: [_jsx(Button, { variant: "secondary", size: "md", onClick: () => setShowDiscardConfirm(false), children: "Cancel" }), _jsx(Button, { variant: "destructive", size: "md", onClick: handleToolbarDiscard, children: "Discard" })] })] }), _jsx(EditorModal, { isOpen: showMediaLibrary, onClose: () => {
|
|
404
|
-
setShowMediaLibrary(false);
|
|
405
|
-
setMediaSelectCallback(null);
|
|
406
|
-
}, title: "Media Library", size: "large", children: _jsx(MediaLibraryModal, { mode: mediaModalMode, items: [
|
|
407
|
-
...Object.values(mediaManifest.images),
|
|
408
|
-
...mediaPipeline.pendingMediaItems,
|
|
409
|
-
].filter((item) => !mediaPipeline.pendingDeletions.includes(item.id)), onSelect: (id) => {
|
|
410
|
-
if (mediaSelectCallback) {
|
|
411
|
-
mediaSelectCallback(id);
|
|
412
|
-
}
|
|
413
|
-
setShowMediaLibrary(false);
|
|
414
|
-
setMediaSelectCallback(null);
|
|
415
|
-
}, onUpload: mediaPipeline.handleMediaUpload, onDelete: mediaPipeline.handleMediaDelete, onAltChange: mediaPipeline.handleMediaAltChange, referenceCountMap: mediaPipeline.referenceCountMap, localUrlMap: mediaPipeline.pendingLocalUrls, maxFileSize: siteConfig?.media.maxFileSize }) })] }) }) }) }));
|
|
416
|
-
}
|
|
417
|
-
function EditorContent({ sections, audiences, dirtySectionIds, isPublishing, onSectionChange, onAddSection, onDeleteSection, onReorderSections, onAccessChange, onStatusChange, }) {
|
|
418
|
-
const { isEditMode } = useEditorContext();
|
|
419
|
-
const { openModal, closeModal } = useEditorModal();
|
|
420
|
-
const [pendingInsertIndex, setPendingInsertIndex] = useState(null);
|
|
421
|
-
const dismissPendingInsert = useCallback(() => setPendingInsertIndex(null), []);
|
|
422
|
-
const editingEnabled = isEditMode && !isPublishing;
|
|
423
|
-
useEffect(() => {
|
|
424
|
-
return monitorForElements({
|
|
425
|
-
onDragStart: ({ source }) => {
|
|
426
|
-
if (source.data.dragType === "section") {
|
|
427
|
-
setPendingInsertIndex(null);
|
|
428
|
-
}
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
}, []);
|
|
432
|
-
return (_jsxs("div", { children: [sections.map(({ section, meta }, index) => {
|
|
433
|
-
const definition = getSection(section.type);
|
|
434
|
-
if (!definition) {
|
|
435
|
-
return (_jsxs("div", { className: "border border-amber-400 bg-amber-50 px-4 py-3 text-sm text-amber-800 dark:border-amber-600 dark:bg-amber-900/30 dark:text-amber-300", role: "alert", children: ["Unknown section type: ", _jsx("code", { className: "font-mono font-semibold", children: section.type })] }, section.id));
|
|
436
|
-
}
|
|
437
|
-
const Component = definition.component;
|
|
438
|
-
return (_jsxs(Fragment, { children: [editingEnabled && pendingInsertIndex === index && (_jsx(SectionSkeleton, { types: typeOptions, onSelect: (type) => {
|
|
439
|
-
onAddSection(index, type);
|
|
440
|
-
setPendingInsertIndex(null);
|
|
441
|
-
}, onDismiss: dismissPendingInsert })), _jsx(ErrorBoundary, { label: `${definition.label} (${section.type})`, children: _jsx(SectionLayout, { type: section.type, status: meta.status, dimNonPublished: !isEditMode, children: _jsx(SectionWrapper, { sectionId: section.id, sectionType: section.type, status: meta.status, dirty: dirtySectionIds.has(section.id), index: index, isLast: index === sections.length - 1, definition: definition, options: {
|
|
442
|
-
...section.content,
|
|
443
|
-
...("options" in section ? section.options : {}),
|
|
444
|
-
}, audiences: audiences, access: meta.access, onAccessChange: editingEnabled ? (a) => onAccessChange(section.id, a) : undefined, onStatusChange: editingEnabled ? (s) => onStatusChange(section.id, s) : undefined, onSectionChange: editingEnabled ? (settingsResult) => {
|
|
445
|
-
const result = settingsResult;
|
|
446
|
-
if (result && typeof result === "object" && "content" in result && "options" in result) {
|
|
447
|
-
const contentPatch = result.content;
|
|
448
|
-
const optionsPatch = result.options;
|
|
449
|
-
onSectionChange(section.id, {
|
|
450
|
-
...section,
|
|
451
|
-
content: { ...section.content, ...contentPatch },
|
|
452
|
-
options: { ...("options" in section ? section.options : {}), ...optionsPatch },
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
onSectionChange(section.id, {
|
|
457
|
-
...section,
|
|
458
|
-
...result,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
} : undefined, onReorder: editingEnabled ? onReorderSections : undefined, onRequestInsert: editingEnabled ? (i) => setPendingInsertIndex(i) : undefined, onDelete: editingEnabled ? () => {
|
|
462
|
-
openModal("Delete Section", (_jsxs("div", { children: [_jsxs("p", { className: "mb-4 text-sm text-base-contrast-light", children: ["Delete this ", _jsx("strong", { children: definition.label }), " section? This cannot be undone."] }), _jsxs("div", { className: "flex justify-end gap-3", children: [_jsx(Button, { variant: "secondary", size: "md", onClick: () => closeModal(), children: "Cancel" }), _jsx(Button, { variant: "destructive", size: "md", onClick: () => {
|
|
463
|
-
onDeleteSection(section.id);
|
|
464
|
-
closeModal();
|
|
465
|
-
}, children: "Confirm" })] })] })));
|
|
466
|
-
} : undefined, children: _jsx(Component, { content: section, options: "options" in section ? section.options : undefined, onChange: editingEnabled ? (newContent) => onSectionChange(section.id, newContent) : undefined, isEditMode: isEditMode, openModal: editingEnabled ? openModal : undefined }) }) }) })] }, section.id));
|
|
467
|
-
}), editingEnabled && pendingInsertIndex === sections.length && (_jsx(SectionSkeleton, { types: typeOptions, onSelect: (type) => {
|
|
468
|
-
onAddSection(sections.length, type);
|
|
469
|
-
setPendingInsertIndex(null);
|
|
470
|
-
}, onDismiss: dismissPendingInsert }))] }));
|
|
471
|
-
}
|
|
472
|
-
function GlobalModal() {
|
|
473
|
-
const { modalState, closeModal } = useEditorModal();
|
|
474
|
-
return (_jsx(EditorModal, { isOpen: modalState !== null, onClose: closeModal, title: modalState?.title ?? "", children: modalState?.content }));
|
|
475
|
-
}
|
|
476
|
-
function EditorToolbar({ localChangesExist, isPublishing, publishFeedback, onPublish, onDiscardClick, onSettingsClick, onMediaClick, processingItems, }) {
|
|
477
|
-
const { isEditMode, toggleEditMode } = useEditorContext();
|
|
478
|
-
return (_jsxs(_Fragment, { children: [isEditMode && (_jsxs("div", { className: "fixed top-0 right-0 left-0 z-50 flex items-center justify-between border-b border-base-200 bg-base px-4 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [publishFeedback && (_jsx("span", { className: cn("text-xs font-medium", publishFeedback === "Published" ? "text-green-600" : "text-red-600"), children: publishFeedback })), localChangesExist && (_jsx(Button, { type: "button", variant: "primary", onClick: onPublish, isLoading: isPublishing, loadingLabel: "Publishing...", children: "Publish" })), localChangesExist && (_jsx(Button, { type: "button", variant: "destructive", onClick: onDiscardClick, disabled: isPublishing, children: "Discard Changes" }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ProcessingIndicator, { items: processingItems }), _jsx(IconButton, { icon: _jsx(ImageIcon, { size: 16 }), label: "Media library", size: "md", onClick: onMediaClick, className: "border border-base-200 bg-base-accent" }), _jsx(IconButton, { icon: _jsx(SettingsIcon, { size: 16 }), label: "Site settings", size: "md", onClick: onSettingsClick, className: "border border-base-200 bg-base-accent" }), _jsx(ShowAllChromeToggle, {})] })] })), _jsx("button", { onClick: toggleEditMode, 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", "aria-label": isEditMode ? "Switch to view mode" : "Switch to edit mode", children: isEditMode ? (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", className: "h-5 w-5", viewBox: "0 0 20 20", fill: "currentColor", children: [_jsx("path", { d: "M10 12a2 2 0 100-4 2 2 0 000 4z" }), _jsx("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" })] })) : (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", className: "h-5 w-5", viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("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" }) })) })] }));
|
|
479
|
-
}
|
|
480
|
-
function ShowAllChromeToggle() {
|
|
481
|
-
const { showAllChrome, toggleShowAllChrome } = useEditorContext();
|
|
482
|
-
return (_jsx(Button, { variant: "secondary", size: "sm", onClick: toggleShowAllChrome, className: "bg-base-accent", "aria-label": "Toggle editor chrome visibility", children: showAllChrome ? "Hide Controls" : "Show Controls" }));
|
|
483
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useRef, useCallback } from "react";
|
|
3
|
-
import { Search, Upload, Trash2 } from "lucide-react";
|
|
4
|
-
import { cn } from "../../lib/cn";
|
|
5
|
-
import { Button } from "../shared/Button";
|
|
6
|
-
import { Select } from "../shared/Select";
|
|
7
|
-
import { displayFilenameExt, mimeToExt } from "../../media/utils";
|
|
8
|
-
function formatFileSize(bytes) {
|
|
9
|
-
if (bytes >= 1048576)
|
|
10
|
-
return `${Math.round(bytes / 1048576)}MB`;
|
|
11
|
-
if (bytes >= 1024)
|
|
12
|
-
return `${Math.round(bytes / 1024)}KB`;
|
|
13
|
-
return `${bytes}B`;
|
|
14
|
-
}
|
|
15
|
-
function thumbnailSrc(item, localUrls) {
|
|
16
|
-
const localUrl = localUrls[item.id];
|
|
17
|
-
if (localUrl)
|
|
18
|
-
return localUrl;
|
|
19
|
-
if (item.kind === "video") {
|
|
20
|
-
return `/api/media/${item.id}/original.${mimeToExt(item.mimeType)}`;
|
|
21
|
-
}
|
|
22
|
-
if (item.kind === "image" && item.variants.length > 0) {
|
|
23
|
-
const smallest = item.variants.reduce((a, b) => (a.width < b.width ? a : b));
|
|
24
|
-
return `/api/media/${item.id}/${smallest.width}.webp`;
|
|
25
|
-
}
|
|
26
|
-
return `/api/media/${item.id}/poster.webp`;
|
|
27
|
-
}
|
|
28
|
-
function displayFilename(item) {
|
|
29
|
-
return `${item.originalName}${displayFilenameExt(item.mimeType)}`;
|
|
30
|
-
}
|
|
31
|
-
function UploadZone({ onUpload }) {
|
|
32
|
-
const [dragging, setDragging] = useState(false);
|
|
33
|
-
const inputRef = useRef(null);
|
|
34
|
-
const handleFiles = useCallback((files) => {
|
|
35
|
-
if (!files || files.length === 0)
|
|
36
|
-
return;
|
|
37
|
-
onUpload(Array.from(files));
|
|
38
|
-
}, [onUpload]);
|
|
39
|
-
return (_jsxs("button", { type: "button", onClick: () => inputRef.current?.click(), onDragOver: (e) => {
|
|
40
|
-
e.preventDefault();
|
|
41
|
-
setDragging(true);
|
|
42
|
-
}, onDragLeave: () => setDragging(false), onDrop: (e) => {
|
|
43
|
-
e.preventDefault();
|
|
44
|
-
setDragging(false);
|
|
45
|
-
handleFiles(e.dataTransfer.files);
|
|
46
|
-
}, className: cn("flex w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-6 py-8 text-center transition-colors", dragging
|
|
47
|
-
? "border-primary bg-primary/5"
|
|
48
|
-
: "border-base-200 hover:border-base-300"), children: [_jsx(Upload, { size: 24, className: "text-base-contrast-light" }), _jsx("span", { className: "text-sm text-base-contrast-light", children: "Drag & drop files or click to browse" }), _jsx("input", { ref: inputRef, type: "file", multiple: true, accept: "image/*,video/*", className: "hidden", onChange: (e) => {
|
|
49
|
-
handleFiles(e.target.files);
|
|
50
|
-
e.target.value = "";
|
|
51
|
-
} })] }));
|
|
52
|
-
}
|
|
53
|
-
export function MediaLibraryModal({ mode, items, onSelect, onUpload, onDelete, onAltChange, referenceCountMap, localUrlMap = {}, maxFileSize, }) {
|
|
54
|
-
const [search, setSearch] = useState("");
|
|
55
|
-
const [kindFilter, setKindFilter] = useState("all");
|
|
56
|
-
const [selected, setSelected] = useState(new Set());
|
|
57
|
-
const [rejectedFiles, setRejectedFiles] = useState([]);
|
|
58
|
-
const handleUpload = useCallback((files) => {
|
|
59
|
-
if (!maxFileSize) {
|
|
60
|
-
onUpload(files);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const valid = [];
|
|
64
|
-
const rejected = [];
|
|
65
|
-
for (const file of files) {
|
|
66
|
-
if (file.size > maxFileSize) {
|
|
67
|
-
rejected.push(file.name);
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
valid.push(file);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (rejected.length > 0)
|
|
74
|
-
setRejectedFiles(rejected);
|
|
75
|
-
if (valid.length > 0)
|
|
76
|
-
onUpload(valid);
|
|
77
|
-
}, [maxFileSize, onUpload]);
|
|
78
|
-
const filtered = items.filter((item) => {
|
|
79
|
-
const matchesSearch = item.originalName
|
|
80
|
-
.toLowerCase()
|
|
81
|
-
.includes(search.toLowerCase());
|
|
82
|
-
const matchesKind = kindFilter === "all" || item.kind === kindFilter;
|
|
83
|
-
return matchesSearch && matchesKind;
|
|
84
|
-
});
|
|
85
|
-
function toggleSelect(id) {
|
|
86
|
-
setSelected((prev) => {
|
|
87
|
-
const next = new Set(prev);
|
|
88
|
-
if (next.has(id)) {
|
|
89
|
-
next.delete(id);
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
next.add(id);
|
|
93
|
-
}
|
|
94
|
-
return next;
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
98
|
-
const selectedIds = Array.from(selected);
|
|
99
|
-
const totalUsage = selectedIds.reduce((sum, id) => sum + (referenceCountMap[id] ?? 0), 0);
|
|
100
|
-
function handleBatchDelete() {
|
|
101
|
-
if (totalUsage > 0) {
|
|
102
|
-
setConfirmDelete(true);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
onDelete(selectedIds);
|
|
106
|
-
setSelected(new Set());
|
|
107
|
-
}
|
|
108
|
-
function handleConfirmDelete() {
|
|
109
|
-
onDelete(selectedIds);
|
|
110
|
-
setSelected(new Set());
|
|
111
|
-
setConfirmDelete(false);
|
|
112
|
-
}
|
|
113
|
-
return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(UploadZone, { onUpload: handleUpload }), _jsxs("div", { className: "sticky top-[-16px] z-10 -mx-6 space-y-3 bg-base px-6 pb-3 pt-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { size: 14, className: "pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-base-contrast-light" }), _jsx("input", { type: "text", placeholder: "Search media...", value: search, onChange: (e) => setSearch(e.target.value), className: "w-full rounded border border-base-200 bg-base py-1.5 pl-8 pr-3 text-sm text-base-contrast placeholder:text-base-contrast-light focus:outline-none focus:ring-1 focus:ring-primary" })] }), _jsx(Select, { value: kindFilter, onChange: (v) => setKindFilter(v), options: [
|
|
114
|
-
{ value: "all", label: "All types" },
|
|
115
|
-
{ value: "image", label: "Images" },
|
|
116
|
-
{ value: "animated", label: "Animated" },
|
|
117
|
-
{ value: "video", label: "Video" },
|
|
118
|
-
], selectClassName: "py-1.5 pr-7 pl-2.5" }), mode === "manage" && selected.size > 0 && (_jsxs(Button, { variant: "destructive", size: "sm", onClick: handleBatchDelete, className: "flex items-center gap-1.5", children: [_jsx(Trash2, { size: 13 }), "Delete ", selected.size] }))] }), confirmDelete && (_jsxs("div", { className: "flex flex-col items-center rounded-lg border border-red-200 bg-red-50 px-4 py-3 lg:flex-row lg:justify-between dark:border-red-900/50 dark:bg-red-950/30", children: [_jsx("p", { className: "mb-2 text-center text-sm text-red-800 lg:mb-0 lg:text-left dark:text-red-200", children: "Some of these images are being used. Deleting these will remove them from these references." }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsx(Button, { variant: "secondary", size: "sm", onClick: () => setConfirmDelete(false), children: "Cancel" }), _jsx(Button, { variant: "destructive", size: "sm", onClick: handleConfirmDelete, children: "Confirm Delete" })] })] })), rejectedFiles.length > 0 && maxFileSize && (_jsxs("div", { className: "flex flex-col items-center rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 lg:flex-row lg:justify-between dark:border-amber-900/50 dark:bg-amber-950/30", children: [_jsxs("p", { className: "mb-2 text-center text-sm text-amber-800 lg:mb-0 lg:text-left dark:text-amber-200", children: [rejectedFiles.length === 1
|
|
119
|
-
? `"${rejectedFiles[0]}" exceeds`
|
|
120
|
-
: `${rejectedFiles.length} files exceed`, " the ", formatFileSize(maxFileSize), " file size limit."] }), _jsx("div", { className: "flex shrink-0 items-center", children: _jsx(Button, { variant: "secondary", size: "sm", onClick: () => setRejectedFiles([]), children: "Dismiss" }) })] }))] }), filtered.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-12 text-center", children: [_jsx("p", { className: "text-sm font-medium text-base-contrast", children: items.length === 0 ? "No media uploaded yet" : "No results found" }), _jsx("p", { className: "text-xs text-base-contrast-light", children: items.length === 0
|
|
121
|
-
? "Upload files using the drop zone above."
|
|
122
|
-
: "Try a different search or filter." })] })) : (_jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5", children: filtered.map((item) => {
|
|
123
|
-
const isSelected = selected.has(item.id);
|
|
124
|
-
const refCount = referenceCountMap[item.id] ?? 0;
|
|
125
|
-
const usageLabel = refCount === 0 ? "Not used" : `Used ${refCount}x`;
|
|
126
|
-
return (_jsxs("div", { className: "group flex flex-col gap-1.5", onClick: () => {
|
|
127
|
-
if (mode === "select") {
|
|
128
|
-
onSelect(item.id);
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
toggleSelect(item.id);
|
|
132
|
-
}
|
|
133
|
-
}, children: [_jsxs("div", { className: cn("relative aspect-square cursor-pointer overflow-hidden rounded-lg border-2 transition-colors", mode === "select"
|
|
134
|
-
? "border-transparent hover:border-primary"
|
|
135
|
-
: isSelected
|
|
136
|
-
? "border-primary"
|
|
137
|
-
: "border-transparent hover:border-base-300"), children: [_jsx("div", { className: "h-full w-full bg-base-accent", children: item.kind === "video" ? (_jsx("video", { src: thumbnailSrc(item, localUrlMap), muted: true, playsInline: true, className: "h-full w-full object-cover", onMouseEnter: (e) => e.currentTarget.play().catch(() => { }), onMouseLeave: (e) => { e.currentTarget.pause(); e.currentTarget.currentTime = 0; } })) : (_jsx("img", { src: thumbnailSrc(item, localUrlMap), alt: item.originalName, className: "h-full w-full object-cover" })) }), mode === "manage" && (_jsx("div", { className: cn("absolute top-1.5 left-1.5 flex h-5 w-5 items-center justify-center rounded-full border-2 transition-colors", isSelected
|
|
138
|
-
? "border-primary bg-primary"
|
|
139
|
-
: "border-white/80 bg-white/40 opacity-0 group-hover:opacity-100"), children: isSelected && (_jsx("div", { className: "h-2 w-2 rounded-full bg-white" })) })), mode === "manage" && (_jsx("div", { className: cn("absolute top-1.5 right-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium transition-opacity", isSelected
|
|
140
|
-
? "opacity-100"
|
|
141
|
-
: "opacity-0 group-hover:opacity-100", refCount > 0
|
|
142
|
-
? "bg-black/50 text-white"
|
|
143
|
-
: "bg-black/50 text-white/70"), children: usageLabel })), _jsx("div", { className: "absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent px-2 pb-1.5 pt-4 opacity-0 transition-opacity group-hover:opacity-100", children: _jsx("p", { className: "truncate text-[11px] text-white", children: displayFilename(item) }) })] }), _jsxs("div", { className: "px-0.5", children: [_jsx("label", { className: "text-[10px] font-medium uppercase leading-none tracking-wide text-base-contrast-light/50", children: "Alt text" }), _jsx("textarea", { value: item.alt, placeholder: "Describe this image for accessibility", rows: 2, onClick: (e) => e.stopPropagation(), onChange: (e) => onAltChange?.(item.id, e.target.value), className: "w-full resize-none border-0 bg-transparent p-0 text-xs leading-snug text-base-contrast placeholder:text-base-contrast-light/40 focus:outline-none focus:ring-0" })] })] }, item.id));
|
|
144
|
-
}) }))] }));
|
|
145
|
-
}
|