@drawnagency/primitives 0.1.25 → 0.1.27
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-UMSFICAC.js → chunk-DKOUFIP6.js} +0 -1
- package/dist/{chunk-KX7NRYQD.js → chunk-HXXZBTPF.js} +12 -5
- package/dist/{chunk-IP6ODLXX.js → chunk-JHSYLVKI.js} +19 -84
- package/dist/{chunk-P24YUT3O.js → chunk-MNK7XA6S.js} +1 -1
- package/dist/{chunk-EAEX6DS7.js → chunk-V43WVSVS.js} +3 -2
- package/dist/components/editor/SectionOrderingModal.d.ts +10 -0
- package/dist/components/editor/SectionOrderingModal.d.ts.map +1 -0
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/primitives/EditableRichText.d.ts.map +1 -1
- package/dist/components/sections/Button/index.d.ts.map +1 -1
- package/dist/components/sections/Colors/index.d.ts.map +1 -1
- package/dist/components/sections/DoDontMediaGrid/index.d.ts.map +1 -1
- package/dist/components/sections/IconList/index.d.ts.map +1 -1
- package/dist/components/sections/LinkHeading/index.d.ts.map +1 -1
- package/dist/components/sections/MediaGrid/index.d.ts.map +1 -1
- package/dist/components/sections/Prose/index.d.ts.map +1 -1
- package/dist/components/sections/SplitContent/index.d.ts.map +1 -1
- package/dist/components/sections/SubHeading/index.d.ts.map +1 -1
- package/dist/components/sections/SubSubHeading/index.d.ts.map +1 -1
- package/dist/components/sections/ViewRenderer.d.ts +0 -1
- package/dist/components/sections/ViewRenderer.d.ts.map +1 -1
- package/dist/components/sections/register-schemas.d.ts.map +1 -1
- package/dist/components/sections/register.d.ts.map +1 -1
- package/dist/components/shared/HistoryPopover.d.ts.map +1 -1
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts +0 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/HistoryToolbar.d.ts.map +1 -1
- package/dist/components/shell/RestoreModal.d.ts.map +1 -1
- package/dist/deploy/index.d.ts +2 -0
- package/dist/deploy/index.d.ts.map +1 -0
- package/dist/deploy/types.d.ts +12 -0
- package/dist/deploy/types.d.ts.map +1 -0
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -8
- package/dist/lib/dexie.d.ts +4 -1
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +319 -0
- package/dist/lib/index.js +3 -3
- package/dist/lib/nav.d.ts +2 -6
- package/dist/lib/nav.d.ts.map +1 -1
- package/dist/lib/registry.d.ts +14 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/text.d.ts +3 -0
- package/dist/lib/text.d.ts.map +1 -0
- package/dist/lib/timestamp.d.ts +2 -0
- package/dist/lib/timestamp.d.ts.map +1 -1
- package/dist/media/index.d.ts +4 -2
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +8 -6
- package/dist/media/provider.d.ts +7 -0
- package/dist/media/provider.d.ts.map +1 -0
- package/dist/media/resolve.d.ts +3 -2
- package/dist/media/resolve.d.ts.map +1 -1
- package/dist/media/types.d.ts +0 -9
- package/dist/media/types.d.ts.map +1 -1
- package/dist/schemas/index.js +3 -3
- package/dist/schemas/media.d.ts +0 -3
- package/dist/schemas/media.d.ts.map +1 -1
- package/dist/schemas/site-config.d.ts +1 -3
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/types.d.ts +21 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/components/editor/DragHandle.tsx +1 -1
- package/src/components/editor/SectionOrderingModal.tsx +215 -0
- package/src/components/editor/SectionWrapper.tsx +3 -1
- package/src/components/primitives/EditableRichText.tsx +4 -2
- package/src/components/sections/Button/index.tsx +1 -0
- package/src/components/sections/Colors/index.tsx +8 -0
- package/src/components/sections/DoDontMediaGrid/index.tsx +8 -0
- package/src/components/sections/IconList/index.tsx +4 -0
- package/src/components/sections/LinkHeading/index.tsx +2 -0
- package/src/components/sections/MediaGrid/index.tsx +8 -0
- package/src/components/sections/Prose/index.tsx +2 -0
- package/src/components/sections/SplitContent/index.tsx +16 -2
- package/src/components/sections/SubHeading/index.tsx +2 -0
- package/src/components/sections/SubSubHeading/index.tsx +2 -0
- package/src/components/sections/ViewRenderer.tsx +3 -1
- package/src/components/sections/register-schemas.ts +0 -2
- package/src/components/sections/register.ts +0 -2
- package/src/components/shared/HistoryPopover.tsx +2 -33
- package/src/components/shared/Navigation.tsx +2 -5
- package/src/components/shell/EditorShell.tsx +40 -9
- package/src/components/shell/HistoryToolbar.tsx +2 -7
- package/src/components/shell/RestoreModal.tsx +2 -7
- package/src/deploy/index.ts +1 -0
- package/src/deploy/types.ts +12 -0
- package/src/hooks/useEditorPublish.ts +18 -43
- package/src/hooks/useMediaPipeline.ts +41 -11
- package/src/hooks/useResolvedMedia.ts +3 -3
- package/src/index.ts +2 -0
- package/src/lib/dexie.ts +28 -1
- package/src/lib/nav.ts +16 -9
- package/src/lib/registry.ts +10 -0
- package/src/lib/text.ts +8 -0
- package/src/lib/timestamp.ts +23 -0
- package/src/media/index.ts +13 -4
- package/src/media/provider.ts +7 -0
- package/src/media/resolve.ts +9 -6
- package/src/media/types.ts +0 -9
- package/src/schemas/media.ts +0 -1
- package/src/schemas/site-config.ts +1 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/types.ts +23 -0
- package/dist/components/sections/SplitContent/SplitContentSettings.d.ts +0 -9
- package/dist/components/sections/SplitContent/SplitContentSettings.d.ts.map +0 -1
- package/dist/media/github.d.ts +0 -3
- package/dist/media/github.d.ts.map +0 -1
- package/src/components/sections/SplitContent/SplitContentSettings.d.ts +0 -9
- package/src/components/sections/SplitContent/SplitContentSettings.d.ts.map +0 -1
- package/src/components/sections/SplitContent/SplitContentSettings.tsx +0 -42
- package/src/media/github.ts +0 -72
|
@@ -16,9 +16,12 @@ import { MediaLibraryModal } from "./MediaLibraryModal";
|
|
|
16
16
|
import { MediaLibraryContext } from "./MediaLibraryContext";
|
|
17
17
|
import { ProcessingIndicator } from "./ProcessingIndicator";
|
|
18
18
|
import { SectionSkeleton } from "./SectionSkeleton";
|
|
19
|
-
import "../sections/register";
|
|
19
|
+
import { ensureSectionsRegistered } from "../sections/register";
|
|
20
20
|
import { getSection, getAllSections } from "../../lib/registry";
|
|
21
|
+
|
|
22
|
+
ensureSectionsRegistered();
|
|
21
23
|
import { SectionWrapper } from "../editor/SectionWrapper";
|
|
24
|
+
import { SectionOrderingModal } from "../editor/SectionOrderingModal";
|
|
22
25
|
import { SectionLayout } from "../sections/SectionLayout";
|
|
23
26
|
import {
|
|
24
27
|
initEditorStore,
|
|
@@ -30,7 +33,7 @@ import {
|
|
|
30
33
|
persistMediaManifest,
|
|
31
34
|
getMediaManifest,
|
|
32
35
|
getPendingMediaItems,
|
|
33
|
-
|
|
36
|
+
getPendingMediaBlobs,
|
|
34
37
|
getPendingMediaDeletions,
|
|
35
38
|
} from "../../lib/dexie";
|
|
36
39
|
import { useEditorPersistence } from "../../hooks/useEditorPersistence";
|
|
@@ -47,7 +50,7 @@ import { SplitButton } from "../shared/SplitButton";
|
|
|
47
50
|
import { IconButton } from "../shared/IconButton";
|
|
48
51
|
import { SegmentedControl } from "../shared/SegmentedControl";
|
|
49
52
|
import { SettingsIcon } from "../shared/icons";
|
|
50
|
-
import { ImageIcon, X } from "lucide-react";
|
|
53
|
+
import { ImageIcon, ListOrderedIcon, X } from "lucide-react";
|
|
51
54
|
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
|
52
55
|
import { HistoryToolbar } from "./HistoryToolbar";
|
|
53
56
|
import { RestoreModal } from "./RestoreModal";
|
|
@@ -110,6 +113,7 @@ export default function EditorShell({
|
|
|
110
113
|
const [viewSections, setViewSections] = useState<LoadedSection[] | null>(null);
|
|
111
114
|
const [isLoadingViewContent, setIsLoadingViewContent] = useState(false);
|
|
112
115
|
const [showRestoreModal, setShowRestoreModal] = useState(false);
|
|
116
|
+
const [showOrderingModal, setShowOrderingModal] = useState(false);
|
|
113
117
|
const [isRestoring, setIsRestoring] = useState(false);
|
|
114
118
|
|
|
115
119
|
const siteIndexRef = useRef<SiteIndex>({ siteId, order: [], sections: {} });
|
|
@@ -149,6 +153,9 @@ export default function EditorShell({
|
|
|
149
153
|
pendingMediaItems: mediaPipeline.pendingMediaItems,
|
|
150
154
|
pendingMediaDeletions: mediaPipeline.pendingDeletions,
|
|
151
155
|
onMediaPublished: (publishedItems, publishedDeletions) => {
|
|
156
|
+
for (const url of Object.values(mediaPipeline.pendingLocalUrls)) {
|
|
157
|
+
URL.revokeObjectURL(url);
|
|
158
|
+
}
|
|
152
159
|
mediaPipeline.setPendingMediaItems([]);
|
|
153
160
|
mediaPipeline.setPendingLocalUrls({});
|
|
154
161
|
mediaPipeline.setPendingDeletions([]);
|
|
@@ -265,16 +272,16 @@ export default function EditorShell({
|
|
|
265
272
|
siteIndexRef.current = loadedIndex;
|
|
266
273
|
applySiteConfigPreview(loadedConfig);
|
|
267
274
|
|
|
268
|
-
// Load pending media from Dexie
|
|
275
|
+
// Load pending media from Dexie — recreate blob URLs from stored blobs
|
|
269
276
|
const savedPendingItems = await getPendingMediaItems();
|
|
270
277
|
if (!cancelled && savedPendingItems.length > 0) {
|
|
271
278
|
mediaPipeline.setPendingMediaItems(savedPendingItems);
|
|
272
279
|
const urlMap: Record<string, string> = {};
|
|
273
280
|
for (const pi of savedPendingItems) {
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
281
|
+
const blobs = await getPendingMediaBlobs(pi.id);
|
|
282
|
+
if (blobs) {
|
|
283
|
+
const displayKey = Object.keys(blobs).find((k) => k !== "primary" && k !== "poster") ?? "primary";
|
|
284
|
+
if (blobs[displayKey]) urlMap[pi.id] = URL.createObjectURL(blobs[displayKey]);
|
|
278
285
|
}
|
|
279
286
|
}
|
|
280
287
|
if (!cancelled) mediaPipeline.setPendingLocalUrls(urlMap);
|
|
@@ -352,6 +359,9 @@ export default function EditorShell({
|
|
|
352
359
|
await discardLocalChanges();
|
|
353
360
|
setLocalChangesExist(false);
|
|
354
361
|
setDirtySectionIds(new Set());
|
|
362
|
+
for (const url of Object.values(mediaPipeline.pendingLocalUrls)) {
|
|
363
|
+
URL.revokeObjectURL(url);
|
|
364
|
+
}
|
|
355
365
|
mediaPipeline.setPendingMediaItems([]);
|
|
356
366
|
mediaPipeline.setPendingLocalUrls({});
|
|
357
367
|
mediaPipeline.setPendingDeletions([]);
|
|
@@ -534,7 +544,6 @@ export default function EditorShell({
|
|
|
534
544
|
})),
|
|
535
545
|
siteIndex: historyContent.index,
|
|
536
546
|
siteConfig: historyContent.siteConfig,
|
|
537
|
-
targetBranch: "main",
|
|
538
547
|
};
|
|
539
548
|
|
|
540
549
|
const response = await fetch("/api/save", {
|
|
@@ -626,6 +635,7 @@ export default function EditorShell({
|
|
|
626
635
|
buildElapsed={buildStatus.elapsedSeconds}
|
|
627
636
|
onBuildDismiss={buildStatus.dismiss}
|
|
628
637
|
onRestoreClick={() => setShowRestoreModal(true)}
|
|
638
|
+
onOrderingClick={() => setShowOrderingModal(true)}
|
|
629
639
|
/>
|
|
630
640
|
|
|
631
641
|
<HistoryOrEditorContent sections={sections}>
|
|
@@ -704,6 +714,18 @@ export default function EditorShell({
|
|
|
704
714
|
maxFileSize={siteConfig?.media.maxFileSize}
|
|
705
715
|
/>
|
|
706
716
|
</EditorModal>
|
|
717
|
+
<EditorModal
|
|
718
|
+
isOpen={showOrderingModal}
|
|
719
|
+
onClose={() => setShowOrderingModal(false)}
|
|
720
|
+
title="Reorder Sections"
|
|
721
|
+
size="settings"
|
|
722
|
+
>
|
|
723
|
+
<SectionOrderingModal
|
|
724
|
+
sections={sections}
|
|
725
|
+
mediaManifest={mediaManifest}
|
|
726
|
+
onReorder={onReorderSections}
|
|
727
|
+
/>
|
|
728
|
+
</EditorModal>
|
|
707
729
|
<RestoreHandler
|
|
708
730
|
showRestoreModal={showRestoreModal}
|
|
709
731
|
setShowRestoreModal={setShowRestoreModal}
|
|
@@ -1097,6 +1119,7 @@ function EditorToolbar({
|
|
|
1097
1119
|
buildElapsed,
|
|
1098
1120
|
onBuildDismiss,
|
|
1099
1121
|
onRestoreClick,
|
|
1122
|
+
onOrderingClick,
|
|
1100
1123
|
}: {
|
|
1101
1124
|
buttonState: "synced" | "publish" | "saveAndPublish";
|
|
1102
1125
|
localChangesExist: boolean;
|
|
@@ -1113,6 +1136,7 @@ function EditorToolbar({
|
|
|
1113
1136
|
buildElapsed: number;
|
|
1114
1137
|
onBuildDismiss: () => void;
|
|
1115
1138
|
onRestoreClick: () => void;
|
|
1139
|
+
onOrderingClick: () => void;
|
|
1116
1140
|
}) {
|
|
1117
1141
|
const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState } = useEditorContext();
|
|
1118
1142
|
|
|
@@ -1177,6 +1201,13 @@ function EditorToolbar({
|
|
|
1177
1201
|
</div>
|
|
1178
1202
|
<div className="flex items-center justify-end gap-2">
|
|
1179
1203
|
<ProcessingIndicator items={processingItems} />
|
|
1204
|
+
<IconButton
|
|
1205
|
+
icon={<ListOrderedIcon size={16} />}
|
|
1206
|
+
label="Reorder sections"
|
|
1207
|
+
size="md"
|
|
1208
|
+
onClick={onOrderingClick}
|
|
1209
|
+
className="border border-base-200 bg-base-accent"
|
|
1210
|
+
/>
|
|
1180
1211
|
<IconButton
|
|
1181
1212
|
icon={<ImageIcon size={16} />}
|
|
1182
1213
|
label="Media library"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatDateTime } from "../../lib/timestamp";
|
|
1
2
|
import { Button } from "../shared/Button";
|
|
2
3
|
|
|
3
4
|
interface HistoryToolbarProps {
|
|
@@ -7,12 +8,6 @@ interface HistoryToolbarProps {
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export function HistoryToolbar({ date, onBackToCurrent, onRestore }: HistoryToolbarProps) {
|
|
10
|
-
const formattedDate = new Date(date).toLocaleDateString("en-US", {
|
|
11
|
-
month: "short",
|
|
12
|
-
day: "numeric",
|
|
13
|
-
year: "numeric",
|
|
14
|
-
});
|
|
15
|
-
|
|
16
11
|
return (
|
|
17
12
|
<div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
|
|
18
13
|
<div className="mx-auto max-w-screen-xl grid grid-cols-3 items-center px-4 py-2">
|
|
@@ -23,7 +18,7 @@ export function HistoryToolbar({ date, onBackToCurrent, onRestore }: HistoryTool
|
|
|
23
18
|
</div>
|
|
24
19
|
<div className="flex items-center justify-center">
|
|
25
20
|
<span className="text-xs font-medium text-base-contrast-light">
|
|
26
|
-
Viewing {
|
|
21
|
+
Viewing {formatDateTime(date)}
|
|
27
22
|
</span>
|
|
28
23
|
</div>
|
|
29
24
|
<div className="flex items-center justify-end">
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatDateTime } from "../../lib/timestamp";
|
|
1
2
|
import { EditorModal } from "./EditorModal";
|
|
2
3
|
import { Button } from "../shared/Button";
|
|
3
4
|
|
|
@@ -10,16 +11,10 @@ interface RestoreModalProps {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function RestoreModal({ isOpen, onClose, date, onConfirm, isRestoring }: RestoreModalProps) {
|
|
13
|
-
const formattedDate = new Date(date).toLocaleDateString("en-US", {
|
|
14
|
-
month: "short",
|
|
15
|
-
day: "numeric",
|
|
16
|
-
year: "numeric",
|
|
17
|
-
});
|
|
18
|
-
|
|
19
14
|
return (
|
|
20
15
|
<EditorModal isOpen={isOpen} onClose={onClose} title="Restore this version?">
|
|
21
16
|
<p className="mb-4 text-sm text-base-contrast-light">
|
|
22
|
-
This will publish the content from {
|
|
17
|
+
This will publish the content from {formatDateTime(date)} as a new update.
|
|
23
18
|
Your current content will still be available in the history.
|
|
24
19
|
</p>
|
|
25
20
|
<div className="flex justify-end gap-3">
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { DeployStatus, DeployStatusProvider } from "./types";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface DeployStatus {
|
|
2
|
+
deployId: string;
|
|
3
|
+
state: "building" | "ready" | "error";
|
|
4
|
+
deployUrl: string;
|
|
5
|
+
commitSha: string | null;
|
|
6
|
+
updatedAt: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DeployStatusProvider {
|
|
10
|
+
get(siteId: string): Promise<DeployStatus | null>;
|
|
11
|
+
upsert(siteId: string, data: Omit<DeployStatus, "updatedAt">): Promise<void>;
|
|
12
|
+
}
|
|
@@ -2,7 +2,7 @@ import { useState, useCallback, useRef, useEffect } from "react";
|
|
|
2
2
|
import type { SiteIndex, SiteConfig } from "../schemas/site-config";
|
|
3
3
|
import type { LoadedSection } from "../lib/loader";
|
|
4
4
|
import type { MediaManifest, MediaItem } from "../media/types";
|
|
5
|
-
import { getDirtySections, hasLocalChanges, discardLocalChanges, cacheContent,
|
|
5
|
+
import { getDirtySections, hasLocalChanges, discardLocalChanges, cacheContent, getPendingMediaBlobs, clearPendingMedia } from "../lib/dexie";
|
|
6
6
|
|
|
7
7
|
function blobToBase64(blob: Blob): Promise<string> {
|
|
8
8
|
return new Promise((resolve, reject) => {
|
|
@@ -36,7 +36,6 @@ interface PublishDeps {
|
|
|
36
36
|
|
|
37
37
|
interface GatheredMedia {
|
|
38
38
|
mediaUploads: { item: MediaItem; blobs: { path: string; base64: string }[] }[];
|
|
39
|
-
blobUrlsToRevoke: string[];
|
|
40
39
|
updatedManifest: MediaManifest | undefined;
|
|
41
40
|
hasMediaChanges: boolean;
|
|
42
41
|
}
|
|
@@ -79,44 +78,31 @@ export function useEditorPublish({
|
|
|
79
78
|
async function gatherMediaPayload(): Promise<GatheredMedia> {
|
|
80
79
|
const hasMediaChanges = pendingMediaItems.length > 0 || pendingMediaDeletions.length > 0;
|
|
81
80
|
const mediaUploads: { item: MediaItem; blobs: { path: string; base64: string }[] }[] = [];
|
|
82
|
-
const blobUrlsToRevoke: string[] = [];
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (!localUrls) continue;
|
|
82
|
+
const deletionSet = new Set(pendingMediaDeletions);
|
|
83
|
+
const itemsToUpload = pendingMediaItems.filter((i) => !deletionSet.has(i.id));
|
|
87
84
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
for (const item of itemsToUpload) {
|
|
86
|
+
const storedBlobs = await getPendingMediaBlobs(item.id);
|
|
87
|
+
if (!storedBlobs) continue;
|
|
91
88
|
|
|
92
|
-
const
|
|
93
|
-
const failedBlobFetches: string[] = [];
|
|
89
|
+
const blobPayloads: { path: string; base64: string }[] = [];
|
|
94
90
|
const mimeExt: Record<string, string> = {
|
|
95
91
|
"image/gif": "gif", "image/apng": "apng", "video/mp4": "mp4", "video/webm": "webm",
|
|
96
92
|
};
|
|
97
|
-
for (const [key,
|
|
93
|
+
for (const [key, blob] of Object.entries(storedBlobs)) {
|
|
98
94
|
if (key === "primary" && item.kind === "image") continue;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
blobs.push({ path: `assets/images/${item.folder}/original.${ext}`, base64 });
|
|
106
|
-
} else {
|
|
107
|
-
blobs.push({ path: `assets/images/${item.folder}/${key}.webp`, base64 });
|
|
108
|
-
}
|
|
109
|
-
} catch {
|
|
110
|
-
failedBlobFetches.push(`${item.id}/${key}`);
|
|
95
|
+
const base64 = await blobToBase64(blob);
|
|
96
|
+
if (key === "primary") {
|
|
97
|
+
const ext = mimeExt[item.mimeType] ?? "bin";
|
|
98
|
+
blobPayloads.push({ path: `assets/images/${item.folder}/original.${ext}`, base64 });
|
|
99
|
+
} else {
|
|
100
|
+
blobPayloads.push({ path: `assets/images/${item.folder}/${key}.webp`, base64 });
|
|
111
101
|
}
|
|
112
102
|
}
|
|
113
103
|
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (blobs.length > 0) {
|
|
119
|
-
mediaUploads.push({ item, blobs });
|
|
104
|
+
if (blobPayloads.length > 0) {
|
|
105
|
+
mediaUploads.push({ item, blobs: blobPayloads });
|
|
120
106
|
}
|
|
121
107
|
}
|
|
122
108
|
|
|
@@ -132,7 +118,7 @@ export function useEditorPublish({
|
|
|
132
118
|
updatedManifest = { images };
|
|
133
119
|
}
|
|
134
120
|
|
|
135
|
-
return { mediaUploads,
|
|
121
|
+
return { mediaUploads, updatedManifest, hasMediaChanges };
|
|
136
122
|
}
|
|
137
123
|
|
|
138
124
|
const handleSave = useCallback(async () => {
|
|
@@ -154,7 +140,7 @@ export function useEditorPublish({
|
|
|
154
140
|
}
|
|
155
141
|
|
|
156
142
|
const dirty = await getDirtySections();
|
|
157
|
-
const { mediaUploads,
|
|
143
|
+
const { mediaUploads, updatedManifest, hasMediaChanges: mediaChanged } = await gatherMediaPayload();
|
|
158
144
|
|
|
159
145
|
// Build a filtered siteIndex if there are deletions
|
|
160
146
|
let siteIndex = siteIndexRef.current;
|
|
@@ -175,7 +161,6 @@ export function useEditorPublish({
|
|
|
175
161
|
method: "POST",
|
|
176
162
|
headers: { "Content-Type": "application/json" },
|
|
177
163
|
body: JSON.stringify({
|
|
178
|
-
targetBranch: "saved",
|
|
179
164
|
sections: dirty.map(({ sectionId, content }) => ({
|
|
180
165
|
id: sectionId,
|
|
181
166
|
content,
|
|
@@ -209,9 +194,6 @@ export function useEditorPublish({
|
|
|
209
194
|
onSuccess();
|
|
210
195
|
onMediaPublished(pendingMediaItems, pendingMediaDeletions);
|
|
211
196
|
onShasUpdated(sha, null);
|
|
212
|
-
for (const url of blobUrlsToRevoke) {
|
|
213
|
-
URL.revokeObjectURL(url);
|
|
214
|
-
}
|
|
215
197
|
|
|
216
198
|
showFeedback("Saved", 3000);
|
|
217
199
|
} catch (error) {
|
|
@@ -264,12 +246,9 @@ export function useEditorPublish({
|
|
|
264
246
|
const hasDeletedSections = (deletedSectionIds?.length ?? 0) > 0;
|
|
265
247
|
const hasLocalEdits = hasChanges || isConfigDirty() || hasMediaChanges || hasDeletedSections;
|
|
266
248
|
|
|
267
|
-
let blobUrlsToRevoke: string[] = [];
|
|
268
|
-
|
|
269
249
|
if (hasLocalEdits) {
|
|
270
250
|
const dirty = await getDirtySections();
|
|
271
251
|
const gathered = await gatherMediaPayload();
|
|
272
|
-
blobUrlsToRevoke = gathered.blobUrlsToRevoke;
|
|
273
252
|
|
|
274
253
|
// Build a filtered siteIndex if there are deletions
|
|
275
254
|
let siteIndex = siteIndexRef.current;
|
|
@@ -290,7 +269,6 @@ export function useEditorPublish({
|
|
|
290
269
|
method: "POST",
|
|
291
270
|
headers: { "Content-Type": "application/json" },
|
|
292
271
|
body: JSON.stringify({
|
|
293
|
-
targetBranch: "saved",
|
|
294
272
|
sections: dirty.map(({ sectionId, content }) => ({
|
|
295
273
|
id: sectionId,
|
|
296
274
|
content,
|
|
@@ -338,9 +316,6 @@ export function useEditorPublish({
|
|
|
338
316
|
clearConfigDirty();
|
|
339
317
|
onSuccess();
|
|
340
318
|
onMediaPublished(pendingMediaItems, pendingMediaDeletions);
|
|
341
|
-
for (const url of blobUrlsToRevoke) {
|
|
342
|
-
URL.revokeObjectURL(url);
|
|
343
|
-
}
|
|
344
319
|
}
|
|
345
320
|
|
|
346
321
|
onShasUpdated(null, sha);
|
|
@@ -8,7 +8,9 @@ import type { LoadedSection } from "../lib/loader";
|
|
|
8
8
|
import type { MediaLibraryContextValue } from "../components/shell/MediaLibraryContext";
|
|
9
9
|
import {
|
|
10
10
|
addPendingMediaItem,
|
|
11
|
+
removePendingMediaItem,
|
|
11
12
|
markPendingMediaDeleted,
|
|
13
|
+
removePendingMediaDeletion,
|
|
12
14
|
} from "../lib/dexie";
|
|
13
15
|
import { generateVideoPoster } from "../media/videoPoster";
|
|
14
16
|
|
|
@@ -87,6 +89,7 @@ export function useMediaPipeline({
|
|
|
87
89
|
|
|
88
90
|
const finalize = (
|
|
89
91
|
localUrls: Record<string, string>,
|
|
92
|
+
blobsMap: Record<string, Blob>,
|
|
90
93
|
width: number,
|
|
91
94
|
height: number,
|
|
92
95
|
) => {
|
|
@@ -108,7 +111,9 @@ export function useMediaPipeline({
|
|
|
108
111
|
alt: "",
|
|
109
112
|
};
|
|
110
113
|
|
|
111
|
-
addPendingMediaItem(item, localUrls);
|
|
114
|
+
addPendingMediaItem(item, localUrls, blobsMap);
|
|
115
|
+
removePendingMediaDeletion(item.id);
|
|
116
|
+
setPendingDeletions((prev) => prev.filter((d) => d !== item.id));
|
|
112
117
|
setPendingMediaItems((prev) => [...prev, item]);
|
|
113
118
|
const displayKey = kind === "video"
|
|
114
119
|
? "primary"
|
|
@@ -126,28 +131,33 @@ export function useMediaPipeline({
|
|
|
126
131
|
};
|
|
127
132
|
|
|
128
133
|
const localUrls: Record<string, string> = {};
|
|
134
|
+
const blobsMap: Record<string, Blob> = {};
|
|
129
135
|
for (const v of result.variants) {
|
|
136
|
+
blobsMap[String(v.width)] = v.blob;
|
|
130
137
|
localUrls[String(v.width)] = URL.createObjectURL(v.blob);
|
|
131
138
|
}
|
|
132
139
|
if (result.primaryBlob) {
|
|
140
|
+
blobsMap["primary"] = result.primaryBlob;
|
|
133
141
|
localUrls["primary"] = URL.createObjectURL(result.primaryBlob);
|
|
134
142
|
}
|
|
135
143
|
|
|
136
144
|
if (kind === "video") {
|
|
137
145
|
generateVideoPoster(result.primaryBlob, mediaConfig.quality).then(
|
|
138
146
|
({ posterBlob, width, height }) => {
|
|
147
|
+
blobsMap["poster"] = posterBlob;
|
|
139
148
|
localUrls["poster"] = URL.createObjectURL(posterBlob);
|
|
140
|
-
finalize(localUrls, width, height);
|
|
149
|
+
finalize(localUrls, blobsMap, width, height);
|
|
141
150
|
},
|
|
142
151
|
() => {
|
|
143
|
-
finalize(localUrls, result.width, result.height);
|
|
152
|
+
finalize(localUrls, blobsMap, result.width, result.height);
|
|
144
153
|
},
|
|
145
154
|
);
|
|
146
155
|
} else {
|
|
147
156
|
if (result.posterBlob) {
|
|
157
|
+
blobsMap["poster"] = result.posterBlob;
|
|
148
158
|
localUrls["poster"] = URL.createObjectURL(result.posterBlob);
|
|
149
159
|
}
|
|
150
|
-
finalize(localUrls, result.width, result.height);
|
|
160
|
+
finalize(localUrls, blobsMap, result.width, result.height);
|
|
151
161
|
}
|
|
152
162
|
}
|
|
153
163
|
},
|
|
@@ -171,9 +181,10 @@ export function useMediaPipeline({
|
|
|
171
181
|
const buffer = await file.arrayBuffer();
|
|
172
182
|
const hash = await hashFileBuffer(buffer);
|
|
173
183
|
|
|
184
|
+
const isDeleted = pendingDeletions.includes(hash);
|
|
174
185
|
const existsInManifest = hash in mediaManifest.images;
|
|
175
186
|
const existsInPending = pendingMediaItems.some((i) => i.hash === hash);
|
|
176
|
-
if (existsInManifest || existsInPending) continue;
|
|
187
|
+
if ((existsInManifest || existsInPending) && !isDeleted) continue;
|
|
177
188
|
|
|
178
189
|
let kind: "image" | "animated" | "video" = "image";
|
|
179
190
|
if (file.type === "image/gif" || file.type === "image/apng") kind = "animated";
|
|
@@ -181,15 +192,33 @@ export function useMediaPipeline({
|
|
|
181
192
|
|
|
182
193
|
queue.add({ buffer, originalName: file.name, mimeType: file.type, hash, kind });
|
|
183
194
|
}
|
|
184
|
-
}, [siteConfig, mediaManifest, pendingMediaItems]);
|
|
195
|
+
}, [siteConfig, mediaManifest, pendingMediaItems, pendingDeletions]);
|
|
185
196
|
|
|
186
197
|
const handleMediaDelete = useCallback(async (ids: string[]) => {
|
|
198
|
+
const idSet = new Set(ids);
|
|
199
|
+
const pendingIds = new Set(pendingMediaItems.filter((i) => idSet.has(i.id)).map((i) => i.id));
|
|
200
|
+
|
|
187
201
|
for (const id of ids) {
|
|
188
|
-
|
|
202
|
+
if (pendingIds.has(id)) {
|
|
203
|
+
await removePendingMediaItem(id);
|
|
204
|
+
} else {
|
|
205
|
+
await markPendingMediaDeleted(id);
|
|
206
|
+
}
|
|
189
207
|
}
|
|
208
|
+
|
|
209
|
+
setPendingMediaItems((prev) => prev.filter((i) => !idSet.has(i.id)));
|
|
210
|
+
setPendingLocalUrls((prev) => {
|
|
211
|
+
const next = { ...prev };
|
|
212
|
+
for (const id of ids) {
|
|
213
|
+
if (next[id]) {
|
|
214
|
+
URL.revokeObjectURL(next[id]);
|
|
215
|
+
delete next[id];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return next;
|
|
219
|
+
});
|
|
190
220
|
setPendingDeletions((prev) => [...prev, ...ids]);
|
|
191
221
|
|
|
192
|
-
const idSet = new Set(ids);
|
|
193
222
|
setSections((prev) =>
|
|
194
223
|
prev.map((loaded) => {
|
|
195
224
|
const json = JSON.stringify(loaded.section);
|
|
@@ -204,7 +233,7 @@ export function useMediaPipeline({
|
|
|
204
233
|
);
|
|
205
234
|
|
|
206
235
|
setLocalChangesExist(true);
|
|
207
|
-
}, [markSectionDirty, setSections, setDirtySectionIds, setLocalChangesExist]);
|
|
236
|
+
}, [pendingMediaItems, markSectionDirty, setSections, setDirtySectionIds, setLocalChangesExist]);
|
|
208
237
|
|
|
209
238
|
const handleMediaAltChange = useCallback((id: string, alt: string) => {
|
|
210
239
|
setMediaManifest((prev) => {
|
|
@@ -247,10 +276,11 @@ export function useMediaPipeline({
|
|
|
247
276
|
const buffer = await file.arrayBuffer();
|
|
248
277
|
const hash = await hashFileBuffer(buffer);
|
|
249
278
|
|
|
279
|
+
const isDeleted = pendingDeletions.includes(hash);
|
|
250
280
|
const existsInManifest = hash in mediaManifest.images;
|
|
251
281
|
const existsInPending = pendingMediaItems.some((i) => i.hash === hash);
|
|
252
282
|
|
|
253
|
-
if (existsInManifest || existsInPending) {
|
|
283
|
+
if ((existsInManifest || existsInPending) && !isDeleted) {
|
|
254
284
|
if (onComplete) onComplete(hash);
|
|
255
285
|
return;
|
|
256
286
|
}
|
|
@@ -268,7 +298,7 @@ export function useMediaPipeline({
|
|
|
268
298
|
console.error(`[useMediaPipeline] Failed to enqueue file "${file.name}":`, err);
|
|
269
299
|
}
|
|
270
300
|
})();
|
|
271
|
-
}, [siteConfig, mediaManifest, pendingMediaItems]);
|
|
301
|
+
}, [siteConfig, mediaManifest, pendingMediaItems, pendingDeletions]);
|
|
272
302
|
|
|
273
303
|
// --- Context value ---
|
|
274
304
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { useMediaLibrary } from "../components/shell/MediaLibraryContext";
|
|
3
|
-
import {
|
|
3
|
+
import { getMediaProvider } from "../media";
|
|
4
4
|
|
|
5
5
|
export interface ResolvedMediaResult {
|
|
6
6
|
src: string | undefined;
|
|
@@ -33,8 +33,8 @@ export function useResolvedMedia(imageId: string | undefined): ResolvedMediaResu
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
if (manifestItem) {
|
|
36
|
-
const
|
|
37
|
-
const resolved =
|
|
36
|
+
const provider = getMediaProvider();
|
|
37
|
+
const resolved = provider.resolve(manifestItem, sizes);
|
|
38
38
|
if (resolved && resolved.tag === "img") {
|
|
39
39
|
return {
|
|
40
40
|
src: resolved.src,
|
package/src/index.ts
CHANGED
package/src/lib/dexie.ts
CHANGED
|
@@ -52,6 +52,7 @@ interface PendingMediaRow {
|
|
|
52
52
|
id: string;
|
|
53
53
|
item: MediaItem;
|
|
54
54
|
localUrls: Record<string, string>;
|
|
55
|
+
blobs: Record<string, Blob>;
|
|
55
56
|
updatedAt: string;
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -110,6 +111,18 @@ class EditorDatabase extends Dexie {
|
|
|
110
111
|
pendingMedia: "id",
|
|
111
112
|
pendingMediaDeletions: "id",
|
|
112
113
|
});
|
|
114
|
+
this.version(6).stores({
|
|
115
|
+
sections: "sectionId",
|
|
116
|
+
siteIndex: "key",
|
|
117
|
+
meta: "key",
|
|
118
|
+
siteConfig: "key",
|
|
119
|
+
contentCache: "key",
|
|
120
|
+
mediaManifest: "key",
|
|
121
|
+
pendingMedia: "id",
|
|
122
|
+
pendingMediaDeletions: "id",
|
|
123
|
+
}).upgrade((tx) => {
|
|
124
|
+
return tx.table("pendingMedia").clear();
|
|
125
|
+
});
|
|
113
126
|
}
|
|
114
127
|
}
|
|
115
128
|
|
|
@@ -354,9 +367,10 @@ export async function getMediaManifest(): Promise<MediaManifest | null> {
|
|
|
354
367
|
export async function addPendingMediaItem(
|
|
355
368
|
item: MediaItem,
|
|
356
369
|
localUrls: Record<string, string> = {},
|
|
370
|
+
blobs: Record<string, Blob> = {},
|
|
357
371
|
): Promise<void> {
|
|
358
372
|
const now = new Date().toISOString();
|
|
359
|
-
await getDb().pendingMedia.put({ id: item.id, item, localUrls, updatedAt: now });
|
|
373
|
+
await getDb().pendingMedia.put({ id: item.id, item, localUrls, blobs, updatedAt: now });
|
|
360
374
|
}
|
|
361
375
|
|
|
362
376
|
export async function getPendingMediaItems(): Promise<MediaItem[]> {
|
|
@@ -369,11 +383,24 @@ export async function getPendingMediaLocalUrls(id: string): Promise<Record<strin
|
|
|
369
383
|
return row?.localUrls ?? null;
|
|
370
384
|
}
|
|
371
385
|
|
|
386
|
+
export async function getPendingMediaBlobs(id: string): Promise<Record<string, Blob> | null> {
|
|
387
|
+
const row = await getDb().pendingMedia.get(id);
|
|
388
|
+
return row?.blobs ?? null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function removePendingMediaItem(id: string): Promise<void> {
|
|
392
|
+
await getDb().pendingMedia.delete(id);
|
|
393
|
+
}
|
|
394
|
+
|
|
372
395
|
export async function markPendingMediaDeleted(id: string): Promise<void> {
|
|
373
396
|
const now = new Date().toISOString();
|
|
374
397
|
await getDb().pendingMediaDeletions.put({ id, deletedAt: now });
|
|
375
398
|
}
|
|
376
399
|
|
|
400
|
+
export async function removePendingMediaDeletion(id: string): Promise<void> {
|
|
401
|
+
await getDb().pendingMediaDeletions.delete(id);
|
|
402
|
+
}
|
|
403
|
+
|
|
377
404
|
export async function getPendingMediaDeletions(): Promise<string[]> {
|
|
378
405
|
const rows = await getDb().pendingMediaDeletions.toArray();
|
|
379
406
|
return rows.map((r) => r.id);
|
package/src/lib/nav.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LoadedSection } from "./loader";
|
|
2
|
+
import { getSection, type SectionRegistry } from "./registry";
|
|
2
3
|
|
|
3
4
|
export interface NavItem {
|
|
4
5
|
href: string;
|
|
@@ -15,21 +16,27 @@ export function toSectionId(text: string): string {
|
|
|
15
16
|
.replace(/\s+/g, "-");
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*/
|
|
23
|
-
export function generateNavLinks(sections: LoadedSection[]): NavItem[] {
|
|
19
|
+
export function generateNavLinks(
|
|
20
|
+
sections: LoadedSection[],
|
|
21
|
+
registry?: SectionRegistry,
|
|
22
|
+
): NavItem[] {
|
|
24
23
|
const nav: NavItem[] = [];
|
|
25
24
|
let currentParent: NavItem | null = null;
|
|
26
25
|
let currentChild: NavItem | null = null;
|
|
27
26
|
|
|
27
|
+
const lookupRole = (type: string) => {
|
|
28
|
+
const def = registry ? registry.getSection(type) : getSection(type);
|
|
29
|
+
return def?.navRole;
|
|
30
|
+
};
|
|
31
|
+
|
|
28
32
|
for (const { section, meta } of sections) {
|
|
29
33
|
const content = section.content as { heading?: string; excludeFromNav?: boolean };
|
|
30
34
|
if (!content.heading) continue;
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
const role = lookupRole(section.type);
|
|
37
|
+
if (!role) continue;
|
|
38
|
+
|
|
39
|
+
if (role === "h1") {
|
|
33
40
|
currentParent = {
|
|
34
41
|
href: `#${toSectionId(content.heading)}`,
|
|
35
42
|
label: content.heading,
|
|
@@ -38,7 +45,7 @@ export function generateNavLinks(sections: LoadedSection[]): NavItem[] {
|
|
|
38
45
|
};
|
|
39
46
|
currentChild = null;
|
|
40
47
|
nav.push(currentParent);
|
|
41
|
-
} else if (
|
|
48
|
+
} else if (role === "h2") {
|
|
42
49
|
if (content.excludeFromNav) continue;
|
|
43
50
|
if (!currentParent) continue;
|
|
44
51
|
currentChild = {
|
|
@@ -48,7 +55,7 @@ export function generateNavLinks(sections: LoadedSection[]): NavItem[] {
|
|
|
48
55
|
children: [],
|
|
49
56
|
};
|
|
50
57
|
currentParent.children.push(currentChild);
|
|
51
|
-
} else if (
|
|
58
|
+
} else if (role === "h3") {
|
|
52
59
|
if (content.excludeFromNav) continue;
|
|
53
60
|
if (!currentChild) continue;
|
|
54
61
|
currentChild.children.push({
|
package/src/lib/registry.ts
CHANGED
|
@@ -46,6 +46,10 @@ export type SettingsFieldDef =
|
|
|
46
46
|
|
|
47
47
|
export type SettingsSchema = Record<string, SettingsFieldDef>;
|
|
48
48
|
|
|
49
|
+
export type Thumbnail =
|
|
50
|
+
| { type: "image"; src: string; alt?: string }
|
|
51
|
+
| { type: "color"; value: string };
|
|
52
|
+
|
|
49
53
|
// --- Component prop types ---
|
|
50
54
|
|
|
51
55
|
// Custom settings forms receive their fields as individual spread props plus onChange.
|
|
@@ -99,6 +103,9 @@ export interface SectionDefinition<T = unknown> {
|
|
|
99
103
|
settings?: SettingsSchema;
|
|
100
104
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
105
|
settingsForm?: ComponentType<any>;
|
|
106
|
+
getLabel?(content: T): string;
|
|
107
|
+
getThumbnails?(content: T): Thumbnail[];
|
|
108
|
+
navRole?: "h1" | "h2" | "h3";
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
// --- defineSection ---
|
|
@@ -114,6 +121,9 @@ type DefineSectionInput<S extends ZodType> = {
|
|
|
114
121
|
settings?: SettingsSchema;
|
|
115
122
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
123
|
settingsForm?: ComponentType<any>;
|
|
124
|
+
getLabel?(content: z.infer<S>): string;
|
|
125
|
+
getThumbnails?(content: z.infer<S>): Thumbnail[];
|
|
126
|
+
navRole?: "h1" | "h2" | "h3";
|
|
117
127
|
};
|
|
118
128
|
|
|
119
129
|
export function defineSection<S extends ZodType>(
|