@drawnagency/primitives 0.1.0 → 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 +5 -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
package/dist/hooks/index.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { useActiveHeadings } from "./useActiveHeadings";
|
|
2
|
-
export { useEditorPersistence } from "./useEditorPersistence";
|
|
3
|
-
export { useEditorPublish } from "./useEditorPublish";
|
|
4
|
-
export { useFocusTrap } from "./useFocusTrap";
|
|
5
|
-
export { useMediaPipeline } from "./useMediaPipeline";
|
|
6
|
-
export { useResolvedMedia } from "./useResolvedMedia";
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
|
2
|
-
export function useActiveHeadings(parentIds, childIdsByParent, grandchildIdsByChild, headerOffset = 80) {
|
|
3
|
-
const [activeParentId, setActiveParentId] = useState("");
|
|
4
|
-
const [activeChildId, setActiveChildId] = useState("");
|
|
5
|
-
const [activeGrandchildId, setActiveGrandchildId] = useState("");
|
|
6
|
-
const childKey = useMemo(() => Object.entries(childIdsByParent).map(([k, v]) => `${k}:${v.join(",")}`).join("|"), [childIdsByParent]);
|
|
7
|
-
const grandchildKey = useMemo(() => Object.entries(grandchildIdsByChild).map(([k, v]) => `${k}:${v.join(",")}`).join("|"), [grandchildIdsByChild]);
|
|
8
|
-
// Use refs for comparison inside the effect to avoid re-render loops
|
|
9
|
-
const activeParentRef = useRef("");
|
|
10
|
-
const activeChildRef = useRef("");
|
|
11
|
-
const activeGrandchildRef = useRef("");
|
|
12
|
-
const ticking = useRef(false);
|
|
13
|
-
const transitionTimeoutRef = useRef(null);
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (parentIds.length === 0)
|
|
16
|
-
return;
|
|
17
|
-
let parentEls = [];
|
|
18
|
-
const collect = () => {
|
|
19
|
-
parentEls = parentIds
|
|
20
|
-
.map((id) => ({ id, el: document.getElementById(id) }))
|
|
21
|
-
.filter((x) => !!x.el);
|
|
22
|
-
};
|
|
23
|
-
collect();
|
|
24
|
-
const findActive = (ids) => {
|
|
25
|
-
let active = "";
|
|
26
|
-
for (const id of ids) {
|
|
27
|
-
const el = document.getElementById(id);
|
|
28
|
-
if (!el)
|
|
29
|
-
continue;
|
|
30
|
-
if (el.getBoundingClientRect().top - headerOffset <= 0)
|
|
31
|
-
active = id;
|
|
32
|
-
else
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
return active;
|
|
36
|
-
};
|
|
37
|
-
const computeActive = () => {
|
|
38
|
-
const parent = findActive(parentIds) || parentEls[0]?.id || "";
|
|
39
|
-
if (!parent)
|
|
40
|
-
return;
|
|
41
|
-
if (activeParentRef.current !== parent) {
|
|
42
|
-
if (transitionTimeoutRef.current)
|
|
43
|
-
clearTimeout(transitionTimeoutRef.current);
|
|
44
|
-
if (activeParentRef.current) {
|
|
45
|
-
// Delay parent transitions to prevent flashing
|
|
46
|
-
transitionTimeoutRef.current = setTimeout(() => {
|
|
47
|
-
activeParentRef.current = parent;
|
|
48
|
-
setActiveParentId(parent);
|
|
49
|
-
}, 250);
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
activeParentRef.current = parent;
|
|
53
|
-
setActiveParentId(parent);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
const child = findActive(childIdsByParent[parent] || []);
|
|
57
|
-
if (activeChildRef.current !== child) {
|
|
58
|
-
activeChildRef.current = child;
|
|
59
|
-
setActiveChildId(child);
|
|
60
|
-
}
|
|
61
|
-
const grandchild = findActive(grandchildIdsByChild[child] || []);
|
|
62
|
-
if (activeGrandchildRef.current !== grandchild) {
|
|
63
|
-
activeGrandchildRef.current = grandchild;
|
|
64
|
-
setActiveGrandchildId(grandchild);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
const onScroll = () => {
|
|
68
|
-
if (!ticking.current) {
|
|
69
|
-
ticking.current = true;
|
|
70
|
-
requestAnimationFrame(() => {
|
|
71
|
-
computeActive();
|
|
72
|
-
ticking.current = false;
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
const onResize = () => {
|
|
77
|
-
collect();
|
|
78
|
-
computeActive();
|
|
79
|
-
};
|
|
80
|
-
window.addEventListener("scroll", onScroll, { passive: true });
|
|
81
|
-
window.addEventListener("resize", onResize);
|
|
82
|
-
// Initial compute
|
|
83
|
-
computeActive();
|
|
84
|
-
return () => {
|
|
85
|
-
window.removeEventListener("scroll", onScroll);
|
|
86
|
-
window.removeEventListener("resize", onResize);
|
|
87
|
-
if (transitionTimeoutRef.current)
|
|
88
|
-
clearTimeout(transitionTimeoutRef.current);
|
|
89
|
-
};
|
|
90
|
-
// Only stable values in dep array — NO state variables
|
|
91
|
-
}, [parentIds.join("|"), childKey, grandchildKey, headerOffset]);
|
|
92
|
-
const setActiveSection = useCallback((id) => {
|
|
93
|
-
const newHash = `#${id}`;
|
|
94
|
-
if (window.location.hash !== newHash) {
|
|
95
|
-
window.history.replaceState(null, "", newHash);
|
|
96
|
-
}
|
|
97
|
-
}, []);
|
|
98
|
-
return { activeParentId, activeChildId, activeGrandchildId, setActiveSection };
|
|
99
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { useCallback, useRef, useEffect } from "react";
|
|
2
|
-
import { persistAll, persistSiteConfig } from "../lib/dexie";
|
|
3
|
-
export function useEditorPersistence(siteIndexRef) {
|
|
4
|
-
const state = useRef({
|
|
5
|
-
pendingSections: new Map(),
|
|
6
|
-
pendingDeletes: new Set(),
|
|
7
|
-
indexDirty: false,
|
|
8
|
-
configDirty: false,
|
|
9
|
-
});
|
|
10
|
-
const flushTimerRef = useRef(null);
|
|
11
|
-
const flushToDexie = useCallback(async () => {
|
|
12
|
-
const s = state.current;
|
|
13
|
-
if (s.pendingSections.size === 0 && !s.indexDirty && s.pendingDeletes.size === 0)
|
|
14
|
-
return;
|
|
15
|
-
const entries = Array.from(s.pendingSections.entries()).map(([sectionId, content]) => ({
|
|
16
|
-
sectionId,
|
|
17
|
-
content,
|
|
18
|
-
}));
|
|
19
|
-
const deletedIds = Array.from(s.pendingDeletes);
|
|
20
|
-
s.pendingSections = new Map();
|
|
21
|
-
s.pendingDeletes = new Set();
|
|
22
|
-
const wasIndexDirty = s.indexDirty;
|
|
23
|
-
s.indexDirty = false;
|
|
24
|
-
await persistAll(entries, wasIndexDirty ? siteIndexRef.current : undefined, deletedIds.length > 0 ? deletedIds : undefined);
|
|
25
|
-
}, [siteIndexRef]);
|
|
26
|
-
const scheduleFlush = useCallback(() => {
|
|
27
|
-
if (flushTimerRef.current)
|
|
28
|
-
clearTimeout(flushTimerRef.current);
|
|
29
|
-
flushTimerRef.current = setTimeout(flushToDexie, 500);
|
|
30
|
-
}, [flushToDexie]);
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
return () => {
|
|
33
|
-
if (flushTimerRef.current)
|
|
34
|
-
clearTimeout(flushTimerRef.current);
|
|
35
|
-
};
|
|
36
|
-
}, []);
|
|
37
|
-
const markSectionDirty = useCallback((sectionId, content) => {
|
|
38
|
-
state.current.pendingSections.set(sectionId, content);
|
|
39
|
-
scheduleFlush();
|
|
40
|
-
}, [scheduleFlush]);
|
|
41
|
-
const markSectionDeleted = useCallback((sectionId) => {
|
|
42
|
-
state.current.pendingSections.delete(sectionId);
|
|
43
|
-
state.current.pendingDeletes.add(sectionId);
|
|
44
|
-
state.current.indexDirty = true;
|
|
45
|
-
scheduleFlush();
|
|
46
|
-
}, [scheduleFlush]);
|
|
47
|
-
const markIndexDirty = useCallback(() => {
|
|
48
|
-
state.current.indexDirty = true;
|
|
49
|
-
scheduleFlush();
|
|
50
|
-
}, [scheduleFlush]);
|
|
51
|
-
const persistConfig = useCallback(async (config) => {
|
|
52
|
-
state.current.configDirty = true;
|
|
53
|
-
await persistSiteConfig(config);
|
|
54
|
-
}, []);
|
|
55
|
-
const cancelPendingFlush = useCallback(() => {
|
|
56
|
-
if (flushTimerRef.current) {
|
|
57
|
-
clearTimeout(flushTimerRef.current);
|
|
58
|
-
flushTimerRef.current = null;
|
|
59
|
-
}
|
|
60
|
-
}, []);
|
|
61
|
-
const isConfigDirty = useCallback(() => state.current.configDirty, []);
|
|
62
|
-
const clearConfigDirty = useCallback(() => { state.current.configDirty = false; }, []);
|
|
63
|
-
return {
|
|
64
|
-
markSectionDirty,
|
|
65
|
-
markSectionDeleted,
|
|
66
|
-
markIndexDirty,
|
|
67
|
-
persistConfig,
|
|
68
|
-
flushNow: flushToDexie,
|
|
69
|
-
cancelPendingFlush,
|
|
70
|
-
isConfigDirty,
|
|
71
|
-
clearConfigDirty,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
-
import { getDirtySections, hasLocalChanges, discardLocalChanges, cacheContent, getPendingMediaLocalUrls, clearPendingMedia } from "../lib/dexie";
|
|
3
|
-
function blobToBase64(blob) {
|
|
4
|
-
return new Promise((resolve, reject) => {
|
|
5
|
-
const reader = new FileReader();
|
|
6
|
-
reader.onload = () => {
|
|
7
|
-
const dataUrl = reader.result;
|
|
8
|
-
resolve(dataUrl.split(",")[1]);
|
|
9
|
-
};
|
|
10
|
-
reader.onerror = reject;
|
|
11
|
-
reader.readAsDataURL(blob);
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
export function useEditorPublish({ flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished, }) {
|
|
15
|
-
const [isPublishing, setIsPublishing] = useState(false);
|
|
16
|
-
const [publishFeedback, setPublishFeedback] = useState(null);
|
|
17
|
-
const feedbackTimerRef = useRef(null);
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
return () => {
|
|
20
|
-
if (feedbackTimerRef.current)
|
|
21
|
-
clearTimeout(feedbackTimerRef.current);
|
|
22
|
-
};
|
|
23
|
-
}, []);
|
|
24
|
-
const handlePublish = useCallback(async () => {
|
|
25
|
-
if (!siteConfig)
|
|
26
|
-
return;
|
|
27
|
-
setIsPublishing(true);
|
|
28
|
-
setPublishFeedback(null);
|
|
29
|
-
try {
|
|
30
|
-
cancelPendingFlush();
|
|
31
|
-
await flushNow();
|
|
32
|
-
const hasChanges = await hasLocalChanges();
|
|
33
|
-
const hasMediaChanges = pendingMediaItems.length > 0 || pendingMediaDeletions.length > 0;
|
|
34
|
-
if (!hasChanges && !isConfigDirty() && !hasMediaChanges) {
|
|
35
|
-
setIsPublishing(false);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const dirty = await getDirtySections();
|
|
39
|
-
// Gather pending media for the commit
|
|
40
|
-
const mediaUploads = [];
|
|
41
|
-
const blobUrlsToRevoke = [];
|
|
42
|
-
for (const item of pendingMediaItems) {
|
|
43
|
-
const localUrls = await getPendingMediaLocalUrls(item.id);
|
|
44
|
-
if (!localUrls)
|
|
45
|
-
continue;
|
|
46
|
-
for (const url of Object.values(localUrls)) {
|
|
47
|
-
blobUrlsToRevoke.push(url);
|
|
48
|
-
}
|
|
49
|
-
const blobs = [];
|
|
50
|
-
const failedBlobFetches = [];
|
|
51
|
-
const mimeExt = {
|
|
52
|
-
"image/gif": "gif", "image/apng": "apng", "video/mp4": "mp4", "video/webm": "webm",
|
|
53
|
-
};
|
|
54
|
-
for (const [key, url] of Object.entries(localUrls)) {
|
|
55
|
-
if (key === "primary" && item.kind === "image")
|
|
56
|
-
continue;
|
|
57
|
-
try {
|
|
58
|
-
const resp = await fetch(url);
|
|
59
|
-
const blob = await resp.blob();
|
|
60
|
-
const base64 = await blobToBase64(blob);
|
|
61
|
-
if (key === "primary") {
|
|
62
|
-
const ext = mimeExt[item.mimeType] ?? "bin";
|
|
63
|
-
blobs.push({ path: `assets/images/${item.folder}/original.${ext}`, base64 });
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
blobs.push({ path: `assets/images/${item.folder}/${key}.webp`, base64 });
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
failedBlobFetches.push(`${item.id}/${key}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (failedBlobFetches.length > 0) {
|
|
74
|
-
throw new Error(`Media upload failed: could not read blob data for ${failedBlobFetches.join(", ")}`);
|
|
75
|
-
}
|
|
76
|
-
if (blobs.length > 0) {
|
|
77
|
-
mediaUploads.push({ item, blobs });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// Build updated manifest
|
|
81
|
-
let updatedManifest;
|
|
82
|
-
if (hasMediaChanges) {
|
|
83
|
-
const images = { ...mediaManifest.images };
|
|
84
|
-
for (const upload of mediaUploads) {
|
|
85
|
-
images[upload.item.id] = upload.item;
|
|
86
|
-
}
|
|
87
|
-
for (const id of pendingMediaDeletions) {
|
|
88
|
-
delete images[id];
|
|
89
|
-
}
|
|
90
|
-
updatedManifest = { images };
|
|
91
|
-
}
|
|
92
|
-
const response = await fetch("/api/save", {
|
|
93
|
-
method: "POST",
|
|
94
|
-
headers: { "Content-Type": "application/json" },
|
|
95
|
-
body: JSON.stringify({
|
|
96
|
-
sections: dirty.map(({ sectionId, content }) => ({
|
|
97
|
-
id: sectionId,
|
|
98
|
-
content,
|
|
99
|
-
})),
|
|
100
|
-
siteIndex: siteIndexRef.current,
|
|
101
|
-
...(isConfigDirty() ? { siteConfig } : {}),
|
|
102
|
-
...(hasMediaChanges ? {
|
|
103
|
-
media: {
|
|
104
|
-
uploads: mediaUploads.map(({ item, blobs }) => ({ item, blobs })),
|
|
105
|
-
deletions: pendingMediaDeletions.map((id) => ({
|
|
106
|
-
id,
|
|
107
|
-
folder: mediaManifest.images[id]?.folder,
|
|
108
|
-
})).filter((d) => d.folder),
|
|
109
|
-
manifest: updatedManifest,
|
|
110
|
-
},
|
|
111
|
-
} : {}),
|
|
112
|
-
}),
|
|
113
|
-
});
|
|
114
|
-
if (!response.ok) {
|
|
115
|
-
const errorBody = await response.json().catch(() => ({}));
|
|
116
|
-
throw new Error(errorBody.error || "Publish failed");
|
|
117
|
-
}
|
|
118
|
-
const { sha } = await response.json();
|
|
119
|
-
await discardLocalChanges();
|
|
120
|
-
await clearPendingMedia();
|
|
121
|
-
await cacheContent(sha, sections, siteIndexRef.current, siteConfig);
|
|
122
|
-
clearConfigDirty();
|
|
123
|
-
onSuccess();
|
|
124
|
-
onMediaPublished(pendingMediaItems, pendingMediaDeletions);
|
|
125
|
-
for (const url of blobUrlsToRevoke) {
|
|
126
|
-
URL.revokeObjectURL(url);
|
|
127
|
-
}
|
|
128
|
-
setPublishFeedback("Published");
|
|
129
|
-
if (feedbackTimerRef.current)
|
|
130
|
-
clearTimeout(feedbackTimerRef.current);
|
|
131
|
-
feedbackTimerRef.current = setTimeout(() => setPublishFeedback(null), 3000);
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
console.error("Publish failed:", error);
|
|
135
|
-
setPublishFeedback("Publish failed");
|
|
136
|
-
if (feedbackTimerRef.current)
|
|
137
|
-
clearTimeout(feedbackTimerRef.current);
|
|
138
|
-
feedbackTimerRef.current = setTimeout(() => setPublishFeedback(null), 5000);
|
|
139
|
-
}
|
|
140
|
-
finally {
|
|
141
|
-
setIsPublishing(false);
|
|
142
|
-
}
|
|
143
|
-
}, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished]);
|
|
144
|
-
return { isPublishing, publishFeedback, handlePublish };
|
|
145
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
2
|
-
const FOCUSABLE_SELECTOR = [
|
|
3
|
-
"a[href]",
|
|
4
|
-
"button:not([disabled])",
|
|
5
|
-
"input:not([disabled])",
|
|
6
|
-
"select:not([disabled])",
|
|
7
|
-
"textarea:not([disabled])",
|
|
8
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
9
|
-
].join(", ");
|
|
10
|
-
export function useFocusTrap(ref, active) {
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
if (!active || !ref.current)
|
|
13
|
-
return;
|
|
14
|
-
const container = ref.current;
|
|
15
|
-
const previouslyFocused = document.activeElement;
|
|
16
|
-
const getFocusable = () => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
17
|
-
// Focus first element on activation
|
|
18
|
-
const focusable = getFocusable();
|
|
19
|
-
if (focusable.length > 0) {
|
|
20
|
-
focusable[0].focus();
|
|
21
|
-
}
|
|
22
|
-
const handleKeyDown = (e) => {
|
|
23
|
-
if (e.key !== "Tab")
|
|
24
|
-
return;
|
|
25
|
-
const elements = getFocusable();
|
|
26
|
-
if (elements.length === 0)
|
|
27
|
-
return;
|
|
28
|
-
const first = elements[0];
|
|
29
|
-
const last = elements[elements.length - 1];
|
|
30
|
-
if (e.shiftKey) {
|
|
31
|
-
if (document.activeElement === first) {
|
|
32
|
-
e.preventDefault();
|
|
33
|
-
last.focus();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
if (document.activeElement === last) {
|
|
38
|
-
e.preventDefault();
|
|
39
|
-
first.focus();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
container.addEventListener("keydown", handleKeyDown);
|
|
44
|
-
return () => {
|
|
45
|
-
container.removeEventListener("keydown", handleKeyDown);
|
|
46
|
-
if (previouslyFocused && document.body.contains(previouslyFocused)) {
|
|
47
|
-
previouslyFocused.focus();
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
}, [active, ref]);
|
|
51
|
-
}
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
|
2
|
-
import { hashFileBuffer, sanitizeMediaName } from "../media/utils";
|
|
3
|
-
import { ProcessingQueue } from "../media/queue";
|
|
4
|
-
import { addPendingMediaItem, markPendingMediaDeleted, } from "../lib/dexie";
|
|
5
|
-
import { generateVideoPoster } from "../media/videoPoster";
|
|
6
|
-
function clearImageIds(obj, ids) {
|
|
7
|
-
if (Array.isArray(obj))
|
|
8
|
-
return obj.map((item) => clearImageIds(item, ids));
|
|
9
|
-
if (obj !== null && typeof obj === "object") {
|
|
10
|
-
const record = obj;
|
|
11
|
-
const result = {};
|
|
12
|
-
for (const [key, value] of Object.entries(record)) {
|
|
13
|
-
if (key === "imageId" && typeof value === "string" && ids.has(value)) {
|
|
14
|
-
result[key] = "";
|
|
15
|
-
}
|
|
16
|
-
else {
|
|
17
|
-
result[key] = clearImageIds(value, ids);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return result;
|
|
21
|
-
}
|
|
22
|
-
return obj;
|
|
23
|
-
}
|
|
24
|
-
export function useMediaPipeline({ siteConfig, mediaManifest, setMediaManifest, sections, setSections, setLocalChangesExist, setDirtySectionIds, markSectionDirty, }) {
|
|
25
|
-
const [processingItems, setProcessingItems] = useState([]);
|
|
26
|
-
const [pendingMediaItems, setPendingMediaItems] = useState([]);
|
|
27
|
-
const [pendingLocalUrls, setPendingLocalUrls] = useState({});
|
|
28
|
-
const [pendingDeletions, setPendingDeletions] = useState([]);
|
|
29
|
-
const queueRef = useRef(null);
|
|
30
|
-
const uploadCallbacksRef = useRef(new Map());
|
|
31
|
-
// --- Processing queue ---
|
|
32
|
-
const mediaConfigKey = siteConfig
|
|
33
|
-
? `${siteConfig.media.sizes.join(",")}_${siteConfig.media.quality}_${siteConfig.media.maxFileSize}`
|
|
34
|
-
: "";
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (!siteConfig)
|
|
37
|
-
return;
|
|
38
|
-
const mediaConfig = siteConfig.media;
|
|
39
|
-
const queue = new ProcessingQueue({
|
|
40
|
-
sizes: mediaConfig.sizes,
|
|
41
|
-
quality: mediaConfig.quality,
|
|
42
|
-
maxConcurrent: 3,
|
|
43
|
-
createWorker: () => new Worker(new URL("../media/worker.ts", import.meta.url), { type: "module" }),
|
|
44
|
-
onEvent: (event) => {
|
|
45
|
-
setProcessingItems((prev) => {
|
|
46
|
-
const next = prev.filter((i) => i.id !== event.item.id);
|
|
47
|
-
if (event.item.state !== "complete")
|
|
48
|
-
next.push(event.item);
|
|
49
|
-
return next;
|
|
50
|
-
});
|
|
51
|
-
if (event.type === "complete" && event.item.result) {
|
|
52
|
-
const result = event.item.result;
|
|
53
|
-
const kind = event.item.kind;
|
|
54
|
-
const sanitizedName = sanitizeMediaName(event.item.originalName);
|
|
55
|
-
const finalize = (localUrls, width, height) => {
|
|
56
|
-
const item = {
|
|
57
|
-
id: event.item.hash,
|
|
58
|
-
hash: event.item.hash,
|
|
59
|
-
kind,
|
|
60
|
-
originalName: sanitizedName,
|
|
61
|
-
width,
|
|
62
|
-
height,
|
|
63
|
-
mimeType: kind === "image" ? "image/webp" : event.item.mimeType,
|
|
64
|
-
size: result.primaryBlob.size,
|
|
65
|
-
folder: `${sanitizedName}-${event.item.hash}`,
|
|
66
|
-
variants: result.variants.map((v) => ({
|
|
67
|
-
width: v.width,
|
|
68
|
-
height: v.height,
|
|
69
|
-
size: v.size,
|
|
70
|
-
})),
|
|
71
|
-
alt: "",
|
|
72
|
-
};
|
|
73
|
-
addPendingMediaItem(item, localUrls);
|
|
74
|
-
setPendingMediaItems((prev) => [...prev, item]);
|
|
75
|
-
const displayKey = kind === "video"
|
|
76
|
-
? "primary"
|
|
77
|
-
: Object.keys(localUrls).find((k) => k !== "primary" && k !== "poster") ?? "primary";
|
|
78
|
-
if (localUrls[displayKey]) {
|
|
79
|
-
setPendingLocalUrls((prev) => ({ ...prev, [item.id]: localUrls[displayKey] }));
|
|
80
|
-
}
|
|
81
|
-
setLocalChangesExist(true);
|
|
82
|
-
const callback = uploadCallbacksRef.current.get(event.item.hash);
|
|
83
|
-
if (callback) {
|
|
84
|
-
callback(event.item.hash);
|
|
85
|
-
uploadCallbacksRef.current.delete(event.item.hash);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
const localUrls = {};
|
|
89
|
-
for (const v of result.variants) {
|
|
90
|
-
localUrls[String(v.width)] = URL.createObjectURL(v.blob);
|
|
91
|
-
}
|
|
92
|
-
if (result.primaryBlob) {
|
|
93
|
-
localUrls["primary"] = URL.createObjectURL(result.primaryBlob);
|
|
94
|
-
}
|
|
95
|
-
if (kind === "video") {
|
|
96
|
-
generateVideoPoster(result.primaryBlob, mediaConfig.quality).then(({ posterBlob, width, height }) => {
|
|
97
|
-
localUrls["poster"] = URL.createObjectURL(posterBlob);
|
|
98
|
-
finalize(localUrls, width, height);
|
|
99
|
-
}, () => {
|
|
100
|
-
finalize(localUrls, result.width, result.height);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
if (result.posterBlob) {
|
|
105
|
-
localUrls["poster"] = URL.createObjectURL(result.posterBlob);
|
|
106
|
-
}
|
|
107
|
-
finalize(localUrls, result.width, result.height);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
queueRef.current = queue;
|
|
113
|
-
return () => queue.destroy();
|
|
114
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- rebuild only when processing params change, not on every siteConfig reference change
|
|
115
|
-
}, [mediaConfigKey, setLocalChangesExist]);
|
|
116
|
-
// --- Media handlers ---
|
|
117
|
-
const handleMediaUpload = useCallback(async (files) => {
|
|
118
|
-
if (!siteConfig)
|
|
119
|
-
return;
|
|
120
|
-
const mediaConfig = siteConfig.media;
|
|
121
|
-
const queue = queueRef.current;
|
|
122
|
-
if (!queue)
|
|
123
|
-
return;
|
|
124
|
-
for (const file of files) {
|
|
125
|
-
if (file.size > mediaConfig.maxFileSize)
|
|
126
|
-
continue;
|
|
127
|
-
const buffer = await file.arrayBuffer();
|
|
128
|
-
const hash = await hashFileBuffer(buffer);
|
|
129
|
-
const existsInManifest = hash in mediaManifest.images;
|
|
130
|
-
const existsInPending = pendingMediaItems.some((i) => i.hash === hash);
|
|
131
|
-
if (existsInManifest || existsInPending)
|
|
132
|
-
continue;
|
|
133
|
-
let kind = "image";
|
|
134
|
-
if (file.type === "image/gif" || file.type === "image/apng")
|
|
135
|
-
kind = "animated";
|
|
136
|
-
if (file.type.startsWith("video/"))
|
|
137
|
-
kind = "video";
|
|
138
|
-
queue.add({ buffer, originalName: file.name, mimeType: file.type, hash, kind });
|
|
139
|
-
}
|
|
140
|
-
}, [siteConfig, mediaManifest, pendingMediaItems]);
|
|
141
|
-
const handleMediaDelete = useCallback(async (ids) => {
|
|
142
|
-
for (const id of ids) {
|
|
143
|
-
await markPendingMediaDeleted(id);
|
|
144
|
-
}
|
|
145
|
-
setPendingDeletions((prev) => [...prev, ...ids]);
|
|
146
|
-
const idSet = new Set(ids);
|
|
147
|
-
setSections((prev) => prev.map((loaded) => {
|
|
148
|
-
const json = JSON.stringify(loaded.section);
|
|
149
|
-
const hasRef = ids.some((id) => json.includes(`"${id}"`));
|
|
150
|
-
if (!hasRef)
|
|
151
|
-
return loaded;
|
|
152
|
-
const cleared = clearImageIds(loaded.section, idSet);
|
|
153
|
-
markSectionDirty(loaded.section.id, cleared);
|
|
154
|
-
setDirtySectionIds((prev) => new Set(prev).add(loaded.section.id));
|
|
155
|
-
return { ...loaded, section: { id: loaded.section.id, ...cleared } };
|
|
156
|
-
}));
|
|
157
|
-
setLocalChangesExist(true);
|
|
158
|
-
}, [markSectionDirty, setSections, setDirtySectionIds, setLocalChangesExist]);
|
|
159
|
-
const handleMediaAltChange = useCallback((id, alt) => {
|
|
160
|
-
setMediaManifest((prev) => {
|
|
161
|
-
const item = prev.images[id];
|
|
162
|
-
if (!item)
|
|
163
|
-
return prev;
|
|
164
|
-
return { images: { ...prev.images, [id]: { ...item, alt } } };
|
|
165
|
-
});
|
|
166
|
-
setPendingMediaItems((prev) => prev.map((item) => (item.id === id ? { ...item, alt } : item)));
|
|
167
|
-
setLocalChangesExist(true);
|
|
168
|
-
}, [setMediaManifest, setLocalChangesExist]);
|
|
169
|
-
const referenceCountMap = useMemo(() => {
|
|
170
|
-
const counts = {};
|
|
171
|
-
const allIds = [
|
|
172
|
-
...Object.keys(mediaManifest.images),
|
|
173
|
-
...pendingMediaItems.map((i) => i.id),
|
|
174
|
-
];
|
|
175
|
-
for (const { section } of sections) {
|
|
176
|
-
const json = JSON.stringify(section);
|
|
177
|
-
for (const id of allIds) {
|
|
178
|
-
const regex = new RegExp(`"imageId"\\s*:\\s*"${id}"`, "g");
|
|
179
|
-
const matches = json.match(regex);
|
|
180
|
-
if (matches)
|
|
181
|
-
counts[id] = (counts[id] ?? 0) + matches.length;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return counts;
|
|
185
|
-
}, [sections, mediaManifest, pendingMediaItems]);
|
|
186
|
-
// --- Unified enqueue ---
|
|
187
|
-
const enqueueFile = useCallback((file, onComplete) => {
|
|
188
|
-
if (!siteConfig)
|
|
189
|
-
return;
|
|
190
|
-
const mediaConfig = siteConfig.media;
|
|
191
|
-
if (file.size > mediaConfig.maxFileSize)
|
|
192
|
-
return;
|
|
193
|
-
(async () => {
|
|
194
|
-
try {
|
|
195
|
-
const buffer = await file.arrayBuffer();
|
|
196
|
-
const hash = await hashFileBuffer(buffer);
|
|
197
|
-
const existsInManifest = hash in mediaManifest.images;
|
|
198
|
-
const existsInPending = pendingMediaItems.some((i) => i.hash === hash);
|
|
199
|
-
if (existsInManifest || existsInPending) {
|
|
200
|
-
if (onComplete)
|
|
201
|
-
onComplete(hash);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (onComplete) {
|
|
205
|
-
uploadCallbacksRef.current.set(hash, onComplete);
|
|
206
|
-
}
|
|
207
|
-
let kind = "image";
|
|
208
|
-
if (file.type === "image/gif" || file.type === "image/apng")
|
|
209
|
-
kind = "animated";
|
|
210
|
-
if (file.type.startsWith("video/"))
|
|
211
|
-
kind = "video";
|
|
212
|
-
queueRef.current?.add({ buffer, originalName: file.name, mimeType: file.type, hash, kind });
|
|
213
|
-
}
|
|
214
|
-
catch (err) {
|
|
215
|
-
console.error(`[useMediaPipeline] Failed to enqueue file "${file.name}":`, err);
|
|
216
|
-
}
|
|
217
|
-
})();
|
|
218
|
-
}, [siteConfig, mediaManifest, pendingMediaItems]);
|
|
219
|
-
// --- Context value ---
|
|
220
|
-
const contextValue = useMemo(() => ({
|
|
221
|
-
openSelectModal: () => {
|
|
222
|
-
// Placeholder — EditorShell overrides this
|
|
223
|
-
},
|
|
224
|
-
uploadFile: (file) => handleMediaUpload([file]),
|
|
225
|
-
uploadFileWithCallback: (file, onComplete) => {
|
|
226
|
-
const blobUrl = URL.createObjectURL(file);
|
|
227
|
-
enqueueFile(file, (imageId) => {
|
|
228
|
-
URL.revokeObjectURL(blobUrl);
|
|
229
|
-
onComplete(imageId);
|
|
230
|
-
});
|
|
231
|
-
return blobUrl;
|
|
232
|
-
},
|
|
233
|
-
manifest: mediaManifest,
|
|
234
|
-
pendingItems: pendingMediaItems,
|
|
235
|
-
pendingLocalUrls: pendingLocalUrls,
|
|
236
|
-
siteConfig,
|
|
237
|
-
}), [handleMediaUpload, enqueueFile, mediaManifest, pendingMediaItems, pendingLocalUrls, siteConfig]);
|
|
238
|
-
return {
|
|
239
|
-
processingItems,
|
|
240
|
-
pendingMediaItems,
|
|
241
|
-
setPendingMediaItems,
|
|
242
|
-
pendingLocalUrls,
|
|
243
|
-
setPendingLocalUrls,
|
|
244
|
-
pendingDeletions,
|
|
245
|
-
setPendingDeletions,
|
|
246
|
-
handleMediaUpload,
|
|
247
|
-
handleMediaDelete,
|
|
248
|
-
handleMediaAltChange,
|
|
249
|
-
referenceCountMap,
|
|
250
|
-
contextValue,
|
|
251
|
-
enqueueFile,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
|
-
import { useMediaLibrary } from "../components/shell/MediaLibraryContext";
|
|
3
|
-
import { createMediaAdapter } from "../media";
|
|
4
|
-
export function useResolvedMedia(imageId) {
|
|
5
|
-
const ctx = useMediaLibrary();
|
|
6
|
-
return useMemo(() => {
|
|
7
|
-
if (!imageId || !ctx) {
|
|
8
|
-
return { src: undefined, srcset: undefined, poster: undefined, alt: "", kind: "image" };
|
|
9
|
-
}
|
|
10
|
-
const { manifest, pendingItems, pendingLocalUrls, siteConfig } = ctx;
|
|
11
|
-
const sizes = siteConfig?.media?.sizes ?? [640, 1080, 1920];
|
|
12
|
-
const manifestItem = manifest.images[imageId];
|
|
13
|
-
const pendingItem = pendingItems.find((i) => i.id === imageId);
|
|
14
|
-
const item = manifestItem ?? pendingItem;
|
|
15
|
-
const alt = item?.alt ?? "";
|
|
16
|
-
const kind = item?.kind ?? "image";
|
|
17
|
-
const localUrl = pendingLocalUrls[imageId];
|
|
18
|
-
if (localUrl) {
|
|
19
|
-
return { src: localUrl, srcset: undefined, poster: undefined, alt, kind };
|
|
20
|
-
}
|
|
21
|
-
if (manifestItem) {
|
|
22
|
-
const adapter = createMediaAdapter(manifest);
|
|
23
|
-
const resolved = adapter.resolve(imageId, sizes);
|
|
24
|
-
if (resolved && resolved.tag === "img") {
|
|
25
|
-
return {
|
|
26
|
-
src: resolved.src,
|
|
27
|
-
srcset: "srcSet" in resolved ? resolved.srcSet : undefined,
|
|
28
|
-
poster: undefined,
|
|
29
|
-
alt,
|
|
30
|
-
kind,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
if (resolved && resolved.tag === "video") {
|
|
34
|
-
return { src: resolved.src, srcset: undefined, poster: resolved.poster, alt, kind };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return { src: undefined, srcset: undefined, poster: undefined, alt, kind };
|
|
38
|
-
}, [imageId, ctx]);
|
|
39
|
-
}
|