@drawnagency/primitives 0.1.99 → 0.1.101
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-FAZFADJK.js → chunk-3TO4RRHY.js} +3 -1
- package/dist/{chunk-HKHEAH5F.js → chunk-6SLGKRLW.js} +8 -60
- package/dist/chunk-BS5AQHZG.js +45 -0
- package/dist/{chunk-GYCBWGVW.js → chunk-LBHQWIVL.js} +79 -24
- package/dist/{chunk-KP4DWTPR.js → chunk-P3NNI3QI.js} +328 -4
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/docfile/DocFileOverlay.d.ts +12 -0
- package/dist/components/docfile/DocFileOverlay.d.ts.map +1 -0
- package/dist/components/docfile/DocFilePane.d.ts +17 -0
- package/dist/components/docfile/DocFilePane.d.ts.map +1 -0
- package/dist/components/docfile/doc-file-state.d.ts +38 -0
- package/dist/components/docfile/doc-file-state.d.ts.map +1 -0
- package/dist/components/editor/SettingsForm.d.ts.map +1 -1
- package/dist/components/editor/index.js +1 -1
- package/dist/components/primitives/DocumentDropZone.d.ts +18 -0
- package/dist/components/primitives/DocumentDropZone.d.ts.map +1 -0
- package/dist/components/sections/Document/index.d.ts +24 -0
- package/dist/components/sections/Document/index.d.ts.map +1 -0
- package/dist/components/sections/all-sections.d.ts +10 -0
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.d.ts +10 -0
- package/dist/components/sections/brand-guide.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +3 -1
- package/dist/components/sections/register-schemas.js +5 -3
- package/dist/components/sections/register.js +5 -3
- package/dist/components/shared/DocumentSourceField.d.ts +13 -0
- package/dist/components/shared/DocumentSourceField.d.ts.map +1 -0
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shell/EditorModal.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/MediaBreadcrumb.d.ts.map +1 -1
- package/dist/components/shell/MediaFolderTile.d.ts +3 -1
- package/dist/components/shell/MediaFolderTile.d.ts.map +1 -1
- package/dist/components/shell/MediaFolderTree.d.ts +3 -0
- package/dist/components/shell/MediaFolderTree.d.ts.map +1 -1
- package/dist/components/shell/MediaLibraryContext.d.ts +24 -1
- package/dist/components/shell/MediaLibraryContext.d.ts.map +1 -1
- package/dist/components/shell/MediaLibraryModal.d.ts +21 -1
- package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
- package/dist/hooks/useDocumentUploads.d.ts +32 -0
- package/dist/hooks/useDocumentUploads.d.ts.map +1 -0
- package/dist/hooks/useEditorPublish.d.ts +18 -1
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts +14 -1
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/hooks/useResolvedDocument.d.ts +12 -0
- package/dist/hooks/useResolvedDocument.d.ts.map +1 -0
- package/dist/index.js +56 -32
- package/dist/lib/dexie.d.ts +13 -0
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +39 -2
- package/dist/lib/document-refs.d.ts +22 -0
- package/dist/lib/document-refs.d.ts.map +1 -0
- package/dist/lib/document-uploads.d.ts +38 -0
- package/dist/lib/document-uploads.d.ts.map +1 -0
- package/dist/lib/documents.d.ts +65 -0
- package/dist/lib/documents.d.ts.map +1 -0
- package/dist/lib/index.js +4 -4
- package/dist/lib/media-folders.d.ts +8 -0
- package/dist/lib/media-folders.d.ts.map +1 -1
- package/dist/lib/media-folders.js +2 -0
- package/dist/lib/nav.d.ts +29 -1
- package/dist/lib/nav.d.ts.map +1 -1
- package/dist/lib/platform-broker.d.ts +12 -0
- package/dist/lib/platform-broker.d.ts.map +1 -1
- package/dist/lib/platform-broker.js +12 -3
- package/dist/lib/registry.d.ts +10 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/save-chunks.d.ts +15 -0
- package/dist/lib/save-chunks.d.ts.map +1 -1
- package/dist/lib/scroll-lock.d.ts +2 -0
- package/dist/lib/scroll-lock.d.ts.map +1 -0
- package/dist/schemas/documents.d.ts +80 -0
- package/dist/schemas/documents.d.ts.map +1 -0
- package/dist/schemas/documents.js +25 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +23 -1
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +3 -1
- package/dist/storage/types.d.ts +59 -3
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/chat/ChatPanel.tsx +5 -4
- package/src/components/docfile/DocFileOverlay.tsx +54 -0
- package/src/components/docfile/DocFilePane.tsx +133 -0
- package/src/components/docfile/doc-file-state.ts +208 -0
- package/src/components/editor/SettingsForm.tsx +10 -0
- package/src/components/primitives/DocumentDropZone.tsx +79 -0
- package/src/components/sections/Document/index.tsx +194 -0
- package/src/components/sections/brand-guide.ts +2 -1
- package/src/components/shared/DocumentSourceField.tsx +51 -0
- package/src/components/shared/Navigation.tsx +53 -10
- package/src/components/shell/EditorModal.tsx +2 -5
- package/src/components/shell/EditorShell.tsx +132 -8
- package/src/components/shell/MediaBreadcrumb.tsx +14 -1
- package/src/components/shell/MediaFolderTile.tsx +4 -2
- package/src/components/shell/MediaFolderTree.tsx +20 -3
- package/src/components/shell/MediaLibraryContext.tsx +24 -1
- package/src/components/shell/MediaLibraryModal.tsx +223 -45
- package/src/hooks/useDocumentUploads.ts +336 -0
- package/src/hooks/useEditorPublish.ts +180 -8
- package/src/hooks/useMediaPipeline.ts +141 -5
- package/src/hooks/useResolvedDocument.ts +25 -0
- package/src/lib/dexie.ts +66 -1
- package/src/lib/document-refs.ts +83 -0
- package/src/lib/document-uploads.ts +66 -0
- package/src/lib/documents.ts +138 -0
- package/src/lib/media-folders.ts +9 -0
- package/src/lib/nav.ts +46 -2
- package/src/lib/platform-broker.ts +18 -3
- package/src/lib/registry.ts +9 -0
- package/src/lib/save-chunks.ts +42 -0
- package/src/lib/scroll-lock.ts +27 -0
- package/src/schemas/documents.ts +82 -0
- package/src/schemas/index.ts +1 -0
- package/src/storage/index.ts +2 -2
- package/src/storage/types.ts +58 -4
- /package/dist/{chunk-FOY5TF7D.js → chunk-ZDQ3TT47.js} +0 -0
|
@@ -9,6 +9,7 @@ import type { Section, SectionContent } from "../../schemas/sections";
|
|
|
9
9
|
import type { SiteIndex, SiteConfig, Page } from "../../schemas/site-config";
|
|
10
10
|
import type { Audience } from "../../auth/types";
|
|
11
11
|
import type { MediaManifest } from "../../media/types";
|
|
12
|
+
import type { DocumentManifest } from "../../schemas/documents";
|
|
12
13
|
import type { QueueItem } from "../../media/queue";
|
|
13
14
|
import { composePreviewImage, PREVIEW_COMPOSE_VERSION } from "../../media/compose-preview";
|
|
14
15
|
import { derivePreviewInputs, servedPreviewSize } from "../../media/preview-inputs";
|
|
@@ -30,7 +31,7 @@ import { EditorModal } from "./EditorModal";
|
|
|
30
31
|
import { SiteSettingsModal } from "./SiteSettingsModal";
|
|
31
32
|
import ChatWidget from "../chat/ChatWidget";
|
|
32
33
|
import { MediaLibraryModal } from "./MediaLibraryModal";
|
|
33
|
-
import { MediaLibraryContext } from "./MediaLibraryContext";
|
|
34
|
+
import { MediaLibraryContext, type LibraryKind } from "./MediaLibraryContext";
|
|
34
35
|
import { ProcessingIndicator } from "./ProcessingIndicator";
|
|
35
36
|
import { SectionSkeleton } from "./SectionSkeleton";
|
|
36
37
|
import { SectionTypePicker } from "./SectionTypePicker";
|
|
@@ -89,6 +90,8 @@ import { useBuildStatus } from "../../hooks/useBuildStatus";
|
|
|
89
90
|
import { useMediaPipeline } from "../../hooks/useMediaPipeline";
|
|
90
91
|
import { formatTimestamp } from "../../lib/timestamp";
|
|
91
92
|
import { generateSiteNav } from "../../lib/nav";
|
|
93
|
+
import { deriveSiteDocuments, type SiteDocument } from "../../lib/documents";
|
|
94
|
+
import { setSiteDocuments, closeDocFile } from "../docfile/doc-file-state";
|
|
92
95
|
import { pagesInNavOrder, moveNavEntry, addGroup, renameGroup, setGroupStartCollapsed, deleteGroup, type NavLocation } from "../../lib/nav-tree";
|
|
93
96
|
import { siteNavChangeEvent, pageSelectEvent, darkModeEvent, siteNameChangeEvent, historySelectEvent, historyRestoreEvent, historyRestoreFailedEvent, editorDirtyEvent } from "../../lib/events";
|
|
94
97
|
import {
|
|
@@ -137,6 +140,9 @@ interface SavedContent {
|
|
|
137
140
|
changedSectionIds: string[];
|
|
138
141
|
mainIndex: SiteIndex;
|
|
139
142
|
mediaManifest?: MediaManifest;
|
|
143
|
+
/** The document sidecar. Optional: absent until the storage track adds it to
|
|
144
|
+
* /api/content, and absent on any site with no documents. */
|
|
145
|
+
documentManifest?: DocumentManifest;
|
|
140
146
|
/** Indexed ids whose section file exists on the branch but is not valid JSON.
|
|
141
147
|
* Their bodies are absent from `sections` while their index slots survive —
|
|
142
148
|
* the editor must surface them, not silently render around them. */
|
|
@@ -196,9 +202,16 @@ export default function EditorShell({
|
|
|
196
202
|
const [dirtySectionIds, setDirtySectionIds] = useState<Set<string>>(new Set());
|
|
197
203
|
const [siteConfig, setSiteConfig] = useState<SiteConfig | null>(null);
|
|
198
204
|
const [mediaManifest, setMediaManifest] = useState<MediaManifest>({ images: {}, folders: {} });
|
|
205
|
+
// null = /api/content has not resolved yet. Distinct from {} (loaded, no
|
|
206
|
+
// documents): U6's registry publish skips null so document rows keep the
|
|
207
|
+
// overlay island's SSR seed instead of flashing "File missing" on mount.
|
|
208
|
+
const [documentManifest, setDocumentManifest] = useState<DocumentManifest | null>(null);
|
|
199
209
|
const [showMediaLibrary, setShowMediaLibrary] = useState(false);
|
|
200
210
|
const [mediaModalMode, setMediaModalMode] = useState<"select" | "manage">("manage");
|
|
201
211
|
const [mediaSelectCallback, setMediaSelectCallback] = useState<((id: string) => void) | null>(null);
|
|
212
|
+
// Kind filter for the current select-mode open (null = the historical
|
|
213
|
+
// image/animated/video picker). Consumed by MediaLibraryModal.
|
|
214
|
+
const [mediaSelectKinds, setMediaSelectKinds] = useState<ReadonlyArray<LibraryKind> | null>(null);
|
|
202
215
|
const [deletedSections, setDeletedSections] = useState<Set<string>>(new Set());
|
|
203
216
|
// Ref mirror so the debounced Dexie flush can persist the deletion set. Without
|
|
204
217
|
// it the snapshot records the index minus the deleted sections but not the
|
|
@@ -234,7 +247,8 @@ export default function EditorShell({
|
|
|
234
247
|
const [rejectedUploads, setRejectedUploads] = useState<{ name: string; reason: "size" | "type" }[]>([]);
|
|
235
248
|
// Surfaces useMediaPipeline's onUploadError (oversized/disallowed files,
|
|
236
249
|
// upload-intent failures, direct-to-bucket PUT failures) in the media
|
|
237
|
-
// library modal, alongside its existing client-side rejection banner
|
|
250
|
+
// library modal, alongside its existing client-side rejection banner — and at
|
|
251
|
+
// shell level while that modal is closed, for uploads started from a section.
|
|
238
252
|
const [uploadErrorMessage, setUploadErrorMessage] = useState<string | null>(null);
|
|
239
253
|
// Surfaces useMediaPipeline's onUploadNotice (e.g. "N files already exist
|
|
240
254
|
// and were left where they are" on dedup skip) in the media library modal.
|
|
@@ -410,6 +424,8 @@ export default function EditorShell({
|
|
|
410
424
|
siteConfig,
|
|
411
425
|
mediaManifest,
|
|
412
426
|
setMediaManifest,
|
|
427
|
+
documentManifest: documentManifest ?? {},
|
|
428
|
+
setDocumentManifest: (next) => setDocumentManifest((prev) => typeof next === "function" ? next(prev ?? {}) : next),
|
|
413
429
|
sections,
|
|
414
430
|
setSections,
|
|
415
431
|
setLocalChangesExist,
|
|
@@ -441,6 +457,7 @@ export default function EditorShell({
|
|
|
441
457
|
changedSectionIds: (data.changedSectionIds ?? []) as string[],
|
|
442
458
|
mainIndex: (data.mainIndex ?? data.index) as SiteIndex,
|
|
443
459
|
mediaManifest: data.mediaManifest as MediaManifest | undefined,
|
|
460
|
+
documentManifest: data.documentManifest as DocumentManifest | undefined,
|
|
444
461
|
unparseableSectionIds: (data.unparseableSectionIds ?? []) as string[],
|
|
445
462
|
};
|
|
446
463
|
}, []);
|
|
@@ -821,6 +838,22 @@ export default function EditorShell({
|
|
|
821
838
|
},
|
|
822
839
|
mediaManifest,
|
|
823
840
|
mediaManifestRef,
|
|
841
|
+
documentManifest: documentManifest ?? {},
|
|
842
|
+
pendingDocuments: mediaPipeline.pendingDocuments,
|
|
843
|
+
onDocumentsPublished: (ids) => { void mediaPipeline.publishDocuments(ids); },
|
|
844
|
+
pendingDocumentDeletions: mediaPipeline.pendingDocumentDeletions,
|
|
845
|
+
// Fires only for ids the commit actually carried — a failed save leaves the
|
|
846
|
+
// pending list intact for the retry.
|
|
847
|
+
onDocumentsDeleted: (ids) => {
|
|
848
|
+
const doomed = new Set(ids);
|
|
849
|
+
setDocumentManifest((prev) => {
|
|
850
|
+
if (prev === null) return prev;
|
|
851
|
+
const next = { ...prev };
|
|
852
|
+
for (const id of doomed) delete next[id];
|
|
853
|
+
return next;
|
|
854
|
+
});
|
|
855
|
+
mediaPipeline.setPendingDocumentDeletions((prev) => prev.filter((id) => !doomed.has(id)));
|
|
856
|
+
},
|
|
824
857
|
manifestDirty: mediaPipeline.manifestDirty,
|
|
825
858
|
clearManifestDirty: () => mediaPipeline.setManifestDirty(false),
|
|
826
859
|
pendingMediaItems: mediaPipeline.pendingMediaItems,
|
|
@@ -890,18 +923,61 @@ export default function EditorShell({
|
|
|
890
923
|
hasLocalChanges: localChangesExist,
|
|
891
924
|
});
|
|
892
925
|
|
|
893
|
-
//
|
|
926
|
+
// The live draft's documents. Editor mode: no audience filtering — an editor
|
|
927
|
+
// sees every document, including drafts and gated ones.
|
|
928
|
+
//
|
|
929
|
+
// Documents STAGED this session are merged on top of the committed manifest
|
|
930
|
+
// and their gated URL is swapped for the local blob URL. That substitution is
|
|
931
|
+
// the entire pre-save preview mechanism (reconciliation decision 3): the row
|
|
932
|
+
// and the pane render whatever `url` the registry holds, so neither of them
|
|
933
|
+
// needs MediaLibraryContext — which the viewer path does not provide at all.
|
|
934
|
+
// null until /api/content resolves (C3's state contract): publishing a list
|
|
935
|
+
// derived from a not-yet-loaded manifest would overwrite the overlay island's
|
|
936
|
+
// SSR seed with [] and flash every document row to "File missing".
|
|
937
|
+
const siteDocuments = useMemo<SiteDocument[] | null>(() => {
|
|
938
|
+
if (documentManifest === null) return null;
|
|
939
|
+
const pending = mediaPipeline.pendingDocuments;
|
|
940
|
+
const merged: DocumentManifest = {
|
|
941
|
+
...documentManifest,
|
|
942
|
+
...Object.fromEntries(pending.map((d) => [d.id, d.entry])),
|
|
943
|
+
};
|
|
944
|
+
// Deletions are pending until a save commits them, so `documentManifest`
|
|
945
|
+
// still holds them — this consumption point is where they disappear.
|
|
946
|
+
for (const id of mediaPipeline.pendingDocumentDeletions) delete merged[id];
|
|
947
|
+
const localUrls = mediaPipeline.pendingDocumentUrls;
|
|
948
|
+
return deriveSiteDocuments({
|
|
949
|
+
index: siteIndex, sections, manifest: merged, mode: "editor", audience: null,
|
|
950
|
+
}).map((doc) => {
|
|
951
|
+
const localUrl = localUrls[doc.documentId];
|
|
952
|
+
return localUrl ? { ...doc, url: localUrl } : doc;
|
|
953
|
+
});
|
|
954
|
+
}, [siteIndex, sections, documentManifest, mediaPipeline.pendingDocuments, mediaPipeline.pendingDocumentUrls, mediaPipeline.pendingDocumentDeletions]);
|
|
955
|
+
|
|
956
|
+
// Publish to the doc-file registry so the DocFileOverlay island (a separate
|
|
957
|
+
// island, mounted by the edit route) and every document row track the draft
|
|
958
|
+
// rather than the build-time SSR list.
|
|
959
|
+
useEffect(() => {
|
|
960
|
+
if (siteDocuments !== null) setSiteDocuments(siteDocuments);
|
|
961
|
+
}, [siteDocuments]);
|
|
962
|
+
|
|
963
|
+
// Push the current SiteNav (active page + its headings + the Documents group)
|
|
964
|
+
// to the Navigation island.
|
|
894
965
|
useEffect(() => {
|
|
895
966
|
if (!resolvedActivePage) return;
|
|
896
967
|
const nav = generateSiteNav({
|
|
897
968
|
index: siteIndex, activeSections, activePageId: resolvedActivePage.id, mode: "editor", audience: null,
|
|
969
|
+
documents: siteDocuments ?? [],
|
|
898
970
|
});
|
|
899
971
|
siteNavChangeEvent.dispatch(nav);
|
|
900
|
-
}, [siteIndex, activeSections, resolvedActivePage]);
|
|
972
|
+
}, [siteIndex, activeSections, resolvedActivePage, siteDocuments]);
|
|
901
973
|
|
|
902
974
|
const goToPage = useCallback((pageId: string) => {
|
|
903
975
|
const page = pageById(siteIndexRef.current, pageId);
|
|
904
976
|
if (!page) return;
|
|
977
|
+
// Client-side page switching pushes state without a hashchange, so a stale
|
|
978
|
+
// #doc fragment would survive and the pane would hang over the new page.
|
|
979
|
+
// The atom clears the fragment and closes.
|
|
980
|
+
closeDocFile();
|
|
905
981
|
setActivePageId(pageId);
|
|
906
982
|
if (typeof window !== "undefined") {
|
|
907
983
|
const path = page.slug === "" ? "/edit" : `/edit/${page.slug}`;
|
|
@@ -1014,6 +1090,10 @@ export default function EditorShell({
|
|
|
1014
1090
|
setMainSha(headSha);
|
|
1015
1091
|
setChangedSectionIds(new Set(cached.changedSectionIds ?? []));
|
|
1016
1092
|
setMainIndex(cached.mainIndex ?? loadedIndex);
|
|
1093
|
+
// Without this, a cache-hit load left documentManifest null forever —
|
|
1094
|
+
// the siteDocuments memo's null-guard then never lifted and every
|
|
1095
|
+
// document row (committed or staged) read "File missing".
|
|
1096
|
+
setDocumentManifest(cached.documentManifest ?? {});
|
|
1017
1097
|
} else {
|
|
1018
1098
|
const data = await fetchSavedContent();
|
|
1019
1099
|
loadedSections = data.sections;
|
|
@@ -1023,11 +1103,13 @@ export default function EditorShell({
|
|
|
1023
1103
|
savedBranchSha: data.savedBranchSha,
|
|
1024
1104
|
changedSectionIds: data.changedSectionIds,
|
|
1025
1105
|
mainIndex: data.mainIndex,
|
|
1106
|
+
documentManifest: data.documentManifest ?? {},
|
|
1026
1107
|
});
|
|
1027
1108
|
if (data.mediaManifest) {
|
|
1028
1109
|
loadedManifest = data.mediaManifest;
|
|
1029
1110
|
await persistMediaManifest(loadedManifest);
|
|
1030
1111
|
}
|
|
1112
|
+
setDocumentManifest(data.documentManifest ?? {});
|
|
1031
1113
|
setSavedSha(data.savedBranchSha);
|
|
1032
1114
|
setMainSha(headSha);
|
|
1033
1115
|
setChangedSectionIds(new Set(data.changedSectionIds));
|
|
@@ -1089,6 +1171,11 @@ export default function EditorShell({
|
|
|
1089
1171
|
return () => { cancelled = true; };
|
|
1090
1172
|
}, [headSha, draftHeadSha, siteId, applySiteConfigPreview, fetchSavedContent]);
|
|
1091
1173
|
|
|
1174
|
+
useEffect(() => {
|
|
1175
|
+
void mediaPipeline.hydratePendingDocuments();
|
|
1176
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one-shot restore of last session's staged documents
|
|
1177
|
+
}, []);
|
|
1178
|
+
|
|
1092
1179
|
// --- Recovery handlers ---
|
|
1093
1180
|
|
|
1094
1181
|
const handleRestore = useCallback(async () => {
|
|
@@ -1625,9 +1712,10 @@ export default function EditorShell({
|
|
|
1625
1712
|
<EditorModalProvider>
|
|
1626
1713
|
<MediaLibraryContext.Provider value={{
|
|
1627
1714
|
...mediaPipeline.contextValue,
|
|
1628
|
-
openSelectModal: (onSelect) => {
|
|
1715
|
+
openSelectModal: (onSelect, opts) => {
|
|
1629
1716
|
setMediaModalMode("select");
|
|
1630
1717
|
setMediaSelectCallback(() => onSelect);
|
|
1718
|
+
setMediaSelectKinds(opts?.kinds ?? null);
|
|
1631
1719
|
setShowMediaLibrary(true);
|
|
1632
1720
|
},
|
|
1633
1721
|
reportRejectedUploads: (files) => setRejectedUploads(files),
|
|
@@ -1742,6 +1830,20 @@ export default function EditorShell({
|
|
|
1742
1830
|
</div>
|
|
1743
1831
|
)}
|
|
1744
1832
|
|
|
1833
|
+
{/* Same onUploadError message the media modal shows in its own banner,
|
|
1834
|
+
surfaced at shell level when the modal is closed — an upload can now
|
|
1835
|
+
start from a document section's drop zone, where nothing else would
|
|
1836
|
+
ever render the rejection. Gated on the modal being shut so the two
|
|
1837
|
+
never show the same message twice. */}
|
|
1838
|
+
{uploadErrorMessage && !showMediaLibrary && (
|
|
1839
|
+
<div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
|
|
1840
|
+
<UploadRejectionAlert
|
|
1841
|
+
message={uploadErrorMessage}
|
|
1842
|
+
onDismiss={() => setUploadErrorMessage(null)}
|
|
1843
|
+
/>
|
|
1844
|
+
</div>
|
|
1845
|
+
)}
|
|
1846
|
+
|
|
1745
1847
|
{unparseableSectionIds.length > 0 && (
|
|
1746
1848
|
<div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
|
|
1747
1849
|
<UploadRejectionAlert
|
|
@@ -1855,6 +1957,7 @@ export default function EditorShell({
|
|
|
1855
1957
|
onClose={() => {
|
|
1856
1958
|
setShowMediaLibrary(false);
|
|
1857
1959
|
setMediaSelectCallback(null);
|
|
1960
|
+
setMediaSelectKinds(null);
|
|
1858
1961
|
}}
|
|
1859
1962
|
title="Media Library"
|
|
1860
1963
|
size="large"
|
|
@@ -1868,17 +1971,34 @@ export default function EditorShell({
|
|
|
1868
1971
|
...Object.values(mediaManifest.images),
|
|
1869
1972
|
...mediaPipeline.pendingMediaItems,
|
|
1870
1973
|
].filter((item) => !mediaPipeline.pendingDeletions.includes(item.id) && !item.hidden)}
|
|
1974
|
+
documents={Object.entries({
|
|
1975
|
+
...documentManifest,
|
|
1976
|
+
...Object.fromEntries(mediaPipeline.pendingDocuments.map((d) => [d.id, d.entry])),
|
|
1977
|
+
})
|
|
1978
|
+
// Deleted-but-unsaved documents stay in the manifest until the
|
|
1979
|
+
// save commits them; the library must not offer them again.
|
|
1980
|
+
.filter(([id]) => !mediaPipeline.pendingDocumentDeletions.includes(id))
|
|
1981
|
+
.map(([id, entry]) => ({
|
|
1982
|
+
id,
|
|
1983
|
+
filename: entry.filename,
|
|
1984
|
+
size: entry.size,
|
|
1985
|
+
kind: entry.kind,
|
|
1986
|
+
}))}
|
|
1987
|
+
kinds={mediaModalMode === "manage" ? undefined : (mediaSelectKinds ?? undefined)}
|
|
1871
1988
|
onSelect={(id) => {
|
|
1872
1989
|
if (mediaSelectCallback) {
|
|
1873
1990
|
mediaSelectCallback(id);
|
|
1874
1991
|
}
|
|
1875
1992
|
setShowMediaLibrary(false);
|
|
1876
1993
|
setMediaSelectCallback(null);
|
|
1994
|
+
setMediaSelectKinds(null);
|
|
1877
1995
|
}}
|
|
1878
1996
|
onUpload={mediaPipeline.handleMediaUpload}
|
|
1879
1997
|
onDelete={mediaPipeline.handleMediaDelete}
|
|
1998
|
+
onDeleteDocuments={(ids) => { void mediaPipeline.handleDocumentDelete(ids); }}
|
|
1880
1999
|
onAltChange={mediaPipeline.handleMediaAltChange}
|
|
1881
2000
|
referenceCountMap={mediaPipeline.referenceCountMap}
|
|
2001
|
+
documentReferenceCountMap={mediaPipeline.documentReferenceCountMap}
|
|
1882
2002
|
localUrlMap={mediaPipeline.pendingLocalUrls}
|
|
1883
2003
|
maxFileSize={siteConfig?.media.maxFileSize}
|
|
1884
2004
|
uploadProgress={mediaPipeline.uploadProgress}
|
|
@@ -2023,7 +2143,8 @@ function HistoryWatcher({
|
|
|
2023
2143
|
siteConfig: data.siteConfig,
|
|
2024
2144
|
});
|
|
2025
2145
|
siteNavChangeEvent.dispatch(
|
|
2026
|
-
|
|
2146
|
+
// History views hold no document manifest for that commit — no group.
|
|
2147
|
+
generateSiteNav({ index: data.index, activeSections: [], activePageId: null, mode: "editor", audience: null, documents: [] }),
|
|
2027
2148
|
);
|
|
2028
2149
|
} catch (err) {
|
|
2029
2150
|
console.error("Failed to load historical content:", err);
|
|
@@ -2074,7 +2195,10 @@ function HistoryOrEditorContent({
|
|
|
2074
2195
|
wasInHistory.current = false;
|
|
2075
2196
|
if (index.pages.length > 0) {
|
|
2076
2197
|
siteNavChangeEvent.dispatch(
|
|
2077
|
-
|
|
2198
|
+
// Leaving history: the live nav effect re-dispatches with the current
|
|
2199
|
+
// document list on the next render, so this one deliberately omits it
|
|
2200
|
+
// rather than briefly showing a stale group.
|
|
2201
|
+
generateSiteNav({ index, activeSections, activePageId, mode: "editor", audience: null, documents: [] }),
|
|
2078
2202
|
);
|
|
2079
2203
|
}
|
|
2080
2204
|
}
|
|
@@ -2086,7 +2210,7 @@ function HistoryOrEditorContent({
|
|
|
2086
2210
|
if (!historyState || !historyPage) return;
|
|
2087
2211
|
if (!historyPageId) setHistoryPageId(historyPage.id);
|
|
2088
2212
|
siteNavChangeEvent.dispatch(
|
|
2089
|
-
generateSiteNav({ index: historyState.index, activeSections: [], activePageId: historyPage.id, mode: "editor", audience: null }),
|
|
2213
|
+
generateSiteNav({ index: historyState.index, activeSections: [], activePageId: historyPage.id, mode: "editor", audience: null, documents: [] }),
|
|
2090
2214
|
);
|
|
2091
2215
|
}, [historyState, historyPage?.id]);
|
|
2092
2216
|
|
|
@@ -2,7 +2,7 @@ import { Fragment, useRef } from "react";
|
|
|
2
2
|
import { ChevronRight } from "lucide-react";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
4
|
import type { MediaFolder, MediaItem } from "../../media/types";
|
|
5
|
-
import { folderPathIds } from "../../lib/media-folders";
|
|
5
|
+
import { DOCUMENTS_FOLDER_ID, folderPathIds } from "../../lib/media-folders";
|
|
6
6
|
import { useFolderDropTarget } from "../../hooks/useMediaFolderDnd";
|
|
7
7
|
import type { MediaFolderDndBindings } from "./MediaFolderTree";
|
|
8
8
|
|
|
@@ -55,6 +55,19 @@ function Segment({
|
|
|
55
55
|
|
|
56
56
|
export function MediaBreadcrumb({ folders, currentFolderId, onNavigate, dnd }: MediaBreadcrumbProps) {
|
|
57
57
|
if (currentFolderId === null) return null;
|
|
58
|
+
|
|
59
|
+
// The Documents pseudo-folder has no `folders` entry, so the path helpers
|
|
60
|
+
// return nothing for it — its crumb is static, and always a leaf.
|
|
61
|
+
if (currentFolderId === DOCUMENTS_FOLDER_ID) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex flex-wrap items-center gap-0.5">
|
|
64
|
+
<Segment label="All media" target={null} onNavigate={onNavigate} dnd={dnd} isCurrent={false} />
|
|
65
|
+
<ChevronRight size={12} className="shrink-0 text-base-contrast-light" />
|
|
66
|
+
<Segment label="Documents" target={DOCUMENTS_FOLDER_ID} onNavigate={onNavigate} dnd={dnd} isCurrent />
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
const pathIds = folderPathIds(folders, currentFolderId);
|
|
59
72
|
|
|
60
73
|
return (
|
|
@@ -17,11 +17,13 @@ export interface MediaFolderTileProps {
|
|
|
17
17
|
dnd?: MediaFolderDndBindings;
|
|
18
18
|
/** Optional accessible label override for the root element (e.g. "Open folder Logos"). */
|
|
19
19
|
ariaLabel?: string;
|
|
20
|
+
/** Glyph override — the Documents pseudo-folder uses a file icon. */
|
|
21
|
+
icon?: React.ReactNode;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
export function MediaFolderTile({
|
|
24
|
-
id, name, count, onOpen, manage, onRename, onDeleteRequest, dnd, ariaLabel,
|
|
26
|
+
id, name, count, onOpen, manage, onRename, onDeleteRequest, dnd, ariaLabel, icon,
|
|
25
27
|
}: MediaFolderTileProps) {
|
|
26
28
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
27
29
|
const [renaming, setRenaming] = useState(false);
|
|
@@ -80,7 +82,7 @@ export function MediaFolderTile({
|
|
|
80
82
|
drop.isDropTarget && "border-primary ring-2 ring-primary ring-inset",
|
|
81
83
|
)}
|
|
82
84
|
>
|
|
83
|
-
<Folder size={16} className="shrink-0 text-base-contrast-light" />
|
|
85
|
+
{icon ?? <Folder size={16} className="shrink-0 text-base-contrast-light" />}
|
|
84
86
|
{renaming ? (
|
|
85
87
|
<input
|
|
86
88
|
autoFocus
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useRef } from "react";
|
|
2
|
-
import { ChevronRight, Folder, LayoutGrid } from "lucide-react";
|
|
2
|
+
import { ChevronRight, FileText, Folder, LayoutGrid } from "lucide-react";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
4
|
import type { MediaFolder, MediaItem } from "../../media/types";
|
|
5
|
-
import { buildFolderTree, type FolderTreeNode } from "../../lib/media-folders";
|
|
5
|
+
import { buildFolderTree, DOCUMENTS_FOLDER_ID, type FolderTreeNode } from "../../lib/media-folders";
|
|
6
6
|
import { useFolderDropTarget } from "../../hooks/useMediaFolderDnd";
|
|
7
7
|
|
|
8
8
|
export interface MediaFolderDndBindings {
|
|
@@ -22,6 +22,9 @@ export interface MediaFolderTreeProps {
|
|
|
22
22
|
onExpand: (id: string) => void;
|
|
23
23
|
onToggleExpand: (id: string) => void;
|
|
24
24
|
dnd?: MediaFolderDndBindings;
|
|
25
|
+
/** Documents pseudo-folder row. Omitted (or 0) renders no row; it is never a
|
|
26
|
+
* drop target — nothing can be moved into it. */
|
|
27
|
+
documentCount?: number;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
|
|
@@ -99,7 +102,7 @@ function TreeRow({
|
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
export function MediaFolderTree(props: MediaFolderTreeProps) {
|
|
102
|
-
const { folders, totalCount, currentFolderId, onNavigate, dnd } = props;
|
|
105
|
+
const { folders, totalCount, currentFolderId, onNavigate, dnd, documentCount = 0 } = props;
|
|
103
106
|
const tree = buildFolderTree(folders);
|
|
104
107
|
const root = useRowDropTarget(null, dnd);
|
|
105
108
|
|
|
@@ -119,6 +122,20 @@ export function MediaFolderTree(props: MediaFolderTreeProps) {
|
|
|
119
122
|
<span className="shrink-0 text-xs text-base-contrast-light">{totalCount}</span>
|
|
120
123
|
)}
|
|
121
124
|
</div>
|
|
125
|
+
{documentCount > 0 && (
|
|
126
|
+
<div
|
|
127
|
+
role="button"
|
|
128
|
+
tabIndex={0}
|
|
129
|
+
aria-label="Documents"
|
|
130
|
+
onClick={() => onNavigate(DOCUMENTS_FOLDER_ID)}
|
|
131
|
+
onKeyDown={(e) => { if (e.key === "Enter") onNavigate(DOCUMENTS_FOLDER_ID); }}
|
|
132
|
+
className={rowClass(currentFolderId === DOCUMENTS_FOLDER_ID, false)}
|
|
133
|
+
>
|
|
134
|
+
<FileText size={14} className="shrink-0 text-base-contrast-light" />
|
|
135
|
+
<span className="min-w-0 flex-1 truncate">Documents</span>
|
|
136
|
+
<span className="shrink-0 text-xs text-base-contrast-light">{documentCount}</span>
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
122
139
|
{tree.map((node) => (
|
|
123
140
|
<TreeRow key={node.id} node={node} depth={0} props={props} />
|
|
124
141
|
))}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
2
|
import type { MediaManifest, MediaItem } from "../../media/types";
|
|
3
3
|
import type { SiteConfig } from "../../schemas/site-config";
|
|
4
|
+
import type { DocumentManifest, DocumentManifestEntry } from "../../schemas/documents";
|
|
5
|
+
|
|
6
|
+
/** Kinds the media library can be filtered to when opened as a picker. */
|
|
7
|
+
export type LibraryKind = "image" | "animated" | "video" | "document";
|
|
8
|
+
|
|
9
|
+
/** A document staged locally this session — bytes (git tier) or bucket object
|
|
10
|
+
* already PUT — that is not yet in the committed document manifest. */
|
|
11
|
+
export interface PendingDocument {
|
|
12
|
+
id: string;
|
|
13
|
+
entry: DocumentManifestEntry;
|
|
14
|
+
}
|
|
4
15
|
|
|
5
16
|
export interface MediaLibraryContextValue {
|
|
6
|
-
|
|
17
|
+
/** `opts.kinds` restricts the picker; omitted keeps the historical
|
|
18
|
+
* image/animated/video behavior for every pre-existing call site. */
|
|
19
|
+
openSelectModal: (onSelect: (imageId: string) => void, opts?: { kinds?: ReadonlyArray<LibraryKind> }) => void;
|
|
7
20
|
uploadFile: (file: File) => void;
|
|
8
21
|
uploadFileWithCallback: (file: File, onComplete: (imageId: string) => void, opts?: { hidden?: boolean }) => string;
|
|
9
22
|
reportRejectedUploads: (files: { name: string; reason: "size" | "type" }[]) => void;
|
|
@@ -11,6 +24,16 @@ export interface MediaLibraryContextValue {
|
|
|
11
24
|
pendingItems: MediaItem[];
|
|
12
25
|
pendingLocalUrls: Record<string, string>;
|
|
13
26
|
siteConfig: SiteConfig | null;
|
|
27
|
+
/** Sidecar document manifest. Optional: viewer-side and test contexts
|
|
28
|
+
* predate documents and must keep compiling. */
|
|
29
|
+
documentManifest?: DocumentManifest;
|
|
30
|
+
pendingDocuments?: PendingDocument[];
|
|
31
|
+
/** Upload a PDF/HTML file through the document pipeline, calling back with the
|
|
32
|
+
* document id once it is staged. Optional for the same reason as the two
|
|
33
|
+
* fields above. Owns every rejection itself (unsupported type, too large, no
|
|
34
|
+
* documents store) — they surface on the pipeline's `onUploadError` channel,
|
|
35
|
+
* so a caller never needs to pre-validate or report. */
|
|
36
|
+
uploadDocument?: (file: File, onComplete?: (documentId: string) => void) => void | Promise<void>;
|
|
14
37
|
}
|
|
15
38
|
|
|
16
39
|
export const MediaLibraryContext = createContext<MediaLibraryContextValue | null>(null);
|