@drawnagency/primitives 0.1.99 → 0.1.100
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-KP4DWTPR.js → chunk-72FMQQIA.js} +246 -3
- package/dist/chunk-BS5AQHZG.js +45 -0
- package/dist/{chunk-HKHEAH5F.js → chunk-GOCFTXR3.js} +8 -60
- package/dist/{chunk-GYCBWGVW.js → chunk-LBHQWIVL.js} +79 -24
- 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 +33 -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/sections/Document/index.d.ts +18 -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 +18 -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 +11 -0
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +32 -0
- 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 +47 -0
- 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 +129 -0
- package/src/components/docfile/doc-file-state.ts +180 -0
- package/src/components/editor/SettingsForm.tsx +10 -0
- package/src/components/sections/Document/index.tsx +163 -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 +111 -7
- 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 +18 -1
- package/src/components/shell/MediaLibraryModal.tsx +223 -45
- package/src/hooks/useDocumentUploads.ts +330 -0
- package/src/hooks/useEditorPublish.ts +180 -8
- package/src/hooks/useMediaPipeline.ts +136 -5
- package/src/hooks/useResolvedDocument.ts +25 -0
- package/src/lib/dexie.ts +54 -0
- 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 +46 -0
- /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
|
|
@@ -410,6 +423,8 @@ export default function EditorShell({
|
|
|
410
423
|
siteConfig,
|
|
411
424
|
mediaManifest,
|
|
412
425
|
setMediaManifest,
|
|
426
|
+
documentManifest: documentManifest ?? {},
|
|
427
|
+
setDocumentManifest: (next) => setDocumentManifest((prev) => typeof next === "function" ? next(prev ?? {}) : next),
|
|
413
428
|
sections,
|
|
414
429
|
setSections,
|
|
415
430
|
setLocalChangesExist,
|
|
@@ -441,6 +456,7 @@ export default function EditorShell({
|
|
|
441
456
|
changedSectionIds: (data.changedSectionIds ?? []) as string[],
|
|
442
457
|
mainIndex: (data.mainIndex ?? data.index) as SiteIndex,
|
|
443
458
|
mediaManifest: data.mediaManifest as MediaManifest | undefined,
|
|
459
|
+
documentManifest: data.documentManifest as DocumentManifest | undefined,
|
|
444
460
|
unparseableSectionIds: (data.unparseableSectionIds ?? []) as string[],
|
|
445
461
|
};
|
|
446
462
|
}, []);
|
|
@@ -821,6 +837,22 @@ export default function EditorShell({
|
|
|
821
837
|
},
|
|
822
838
|
mediaManifest,
|
|
823
839
|
mediaManifestRef,
|
|
840
|
+
documentManifest: documentManifest ?? {},
|
|
841
|
+
pendingDocuments: mediaPipeline.pendingDocuments,
|
|
842
|
+
onDocumentsPublished: (ids) => { void mediaPipeline.publishDocuments(ids); },
|
|
843
|
+
pendingDocumentDeletions: mediaPipeline.pendingDocumentDeletions,
|
|
844
|
+
// Fires only for ids the commit actually carried — a failed save leaves the
|
|
845
|
+
// pending list intact for the retry.
|
|
846
|
+
onDocumentsDeleted: (ids) => {
|
|
847
|
+
const doomed = new Set(ids);
|
|
848
|
+
setDocumentManifest((prev) => {
|
|
849
|
+
if (prev === null) return prev;
|
|
850
|
+
const next = { ...prev };
|
|
851
|
+
for (const id of doomed) delete next[id];
|
|
852
|
+
return next;
|
|
853
|
+
});
|
|
854
|
+
mediaPipeline.setPendingDocumentDeletions((prev) => prev.filter((id) => !doomed.has(id)));
|
|
855
|
+
},
|
|
824
856
|
manifestDirty: mediaPipeline.manifestDirty,
|
|
825
857
|
clearManifestDirty: () => mediaPipeline.setManifestDirty(false),
|
|
826
858
|
pendingMediaItems: mediaPipeline.pendingMediaItems,
|
|
@@ -890,18 +922,61 @@ export default function EditorShell({
|
|
|
890
922
|
hasLocalChanges: localChangesExist,
|
|
891
923
|
});
|
|
892
924
|
|
|
893
|
-
//
|
|
925
|
+
// The live draft's documents. Editor mode: no audience filtering — an editor
|
|
926
|
+
// sees every document, including drafts and gated ones.
|
|
927
|
+
//
|
|
928
|
+
// Documents STAGED this session are merged on top of the committed manifest
|
|
929
|
+
// and their gated URL is swapped for the local blob URL. That substitution is
|
|
930
|
+
// the entire pre-save preview mechanism (reconciliation decision 3): the row
|
|
931
|
+
// and the pane render whatever `url` the registry holds, so neither of them
|
|
932
|
+
// needs MediaLibraryContext — which the viewer path does not provide at all.
|
|
933
|
+
// null until /api/content resolves (C3's state contract): publishing a list
|
|
934
|
+
// derived from a not-yet-loaded manifest would overwrite the overlay island's
|
|
935
|
+
// SSR seed with [] and flash every document row to "File missing".
|
|
936
|
+
const siteDocuments = useMemo<SiteDocument[] | null>(() => {
|
|
937
|
+
if (documentManifest === null) return null;
|
|
938
|
+
const pending = mediaPipeline.pendingDocuments;
|
|
939
|
+
const merged: DocumentManifest = {
|
|
940
|
+
...documentManifest,
|
|
941
|
+
...Object.fromEntries(pending.map((d) => [d.id, d.entry])),
|
|
942
|
+
};
|
|
943
|
+
// Deletions are pending until a save commits them, so `documentManifest`
|
|
944
|
+
// still holds them — this consumption point is where they disappear.
|
|
945
|
+
for (const id of mediaPipeline.pendingDocumentDeletions) delete merged[id];
|
|
946
|
+
const localUrls = mediaPipeline.pendingDocumentUrls;
|
|
947
|
+
return deriveSiteDocuments({
|
|
948
|
+
index: siteIndex, sections, manifest: merged, mode: "editor", audience: null,
|
|
949
|
+
}).map((doc) => {
|
|
950
|
+
const localUrl = localUrls[doc.documentId];
|
|
951
|
+
return localUrl ? { ...doc, url: localUrl } : doc;
|
|
952
|
+
});
|
|
953
|
+
}, [siteIndex, sections, documentManifest, mediaPipeline.pendingDocuments, mediaPipeline.pendingDocumentUrls, mediaPipeline.pendingDocumentDeletions]);
|
|
954
|
+
|
|
955
|
+
// Publish to the doc-file registry so the DocFileOverlay island (a separate
|
|
956
|
+
// island, mounted by the edit route) and every document row track the draft
|
|
957
|
+
// rather than the build-time SSR list.
|
|
958
|
+
useEffect(() => {
|
|
959
|
+
if (siteDocuments !== null) setSiteDocuments(siteDocuments);
|
|
960
|
+
}, [siteDocuments]);
|
|
961
|
+
|
|
962
|
+
// Push the current SiteNav (active page + its headings + the Documents group)
|
|
963
|
+
// to the Navigation island.
|
|
894
964
|
useEffect(() => {
|
|
895
965
|
if (!resolvedActivePage) return;
|
|
896
966
|
const nav = generateSiteNav({
|
|
897
967
|
index: siteIndex, activeSections, activePageId: resolvedActivePage.id, mode: "editor", audience: null,
|
|
968
|
+
documents: siteDocuments ?? [],
|
|
898
969
|
});
|
|
899
970
|
siteNavChangeEvent.dispatch(nav);
|
|
900
|
-
}, [siteIndex, activeSections, resolvedActivePage]);
|
|
971
|
+
}, [siteIndex, activeSections, resolvedActivePage, siteDocuments]);
|
|
901
972
|
|
|
902
973
|
const goToPage = useCallback((pageId: string) => {
|
|
903
974
|
const page = pageById(siteIndexRef.current, pageId);
|
|
904
975
|
if (!page) return;
|
|
976
|
+
// Client-side page switching pushes state without a hashchange, so a stale
|
|
977
|
+
// #doc fragment would survive and the pane would hang over the new page.
|
|
978
|
+
// The atom clears the fragment and closes.
|
|
979
|
+
closeDocFile();
|
|
905
980
|
setActivePageId(pageId);
|
|
906
981
|
if (typeof window !== "undefined") {
|
|
907
982
|
const path = page.slug === "" ? "/edit" : `/edit/${page.slug}`;
|
|
@@ -1028,6 +1103,7 @@ export default function EditorShell({
|
|
|
1028
1103
|
loadedManifest = data.mediaManifest;
|
|
1029
1104
|
await persistMediaManifest(loadedManifest);
|
|
1030
1105
|
}
|
|
1106
|
+
setDocumentManifest(data.documentManifest ?? {});
|
|
1031
1107
|
setSavedSha(data.savedBranchSha);
|
|
1032
1108
|
setMainSha(headSha);
|
|
1033
1109
|
setChangedSectionIds(new Set(data.changedSectionIds));
|
|
@@ -1089,6 +1165,11 @@ export default function EditorShell({
|
|
|
1089
1165
|
return () => { cancelled = true; };
|
|
1090
1166
|
}, [headSha, draftHeadSha, siteId, applySiteConfigPreview, fetchSavedContent]);
|
|
1091
1167
|
|
|
1168
|
+
useEffect(() => {
|
|
1169
|
+
void mediaPipeline.hydratePendingDocuments();
|
|
1170
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one-shot restore of last session's staged documents
|
|
1171
|
+
}, []);
|
|
1172
|
+
|
|
1092
1173
|
// --- Recovery handlers ---
|
|
1093
1174
|
|
|
1094
1175
|
const handleRestore = useCallback(async () => {
|
|
@@ -1625,9 +1706,10 @@ export default function EditorShell({
|
|
|
1625
1706
|
<EditorModalProvider>
|
|
1626
1707
|
<MediaLibraryContext.Provider value={{
|
|
1627
1708
|
...mediaPipeline.contextValue,
|
|
1628
|
-
openSelectModal: (onSelect) => {
|
|
1709
|
+
openSelectModal: (onSelect, opts) => {
|
|
1629
1710
|
setMediaModalMode("select");
|
|
1630
1711
|
setMediaSelectCallback(() => onSelect);
|
|
1712
|
+
setMediaSelectKinds(opts?.kinds ?? null);
|
|
1631
1713
|
setShowMediaLibrary(true);
|
|
1632
1714
|
},
|
|
1633
1715
|
reportRejectedUploads: (files) => setRejectedUploads(files),
|
|
@@ -1855,6 +1937,7 @@ export default function EditorShell({
|
|
|
1855
1937
|
onClose={() => {
|
|
1856
1938
|
setShowMediaLibrary(false);
|
|
1857
1939
|
setMediaSelectCallback(null);
|
|
1940
|
+
setMediaSelectKinds(null);
|
|
1858
1941
|
}}
|
|
1859
1942
|
title="Media Library"
|
|
1860
1943
|
size="large"
|
|
@@ -1868,17 +1951,34 @@ export default function EditorShell({
|
|
|
1868
1951
|
...Object.values(mediaManifest.images),
|
|
1869
1952
|
...mediaPipeline.pendingMediaItems,
|
|
1870
1953
|
].filter((item) => !mediaPipeline.pendingDeletions.includes(item.id) && !item.hidden)}
|
|
1954
|
+
documents={Object.entries({
|
|
1955
|
+
...documentManifest,
|
|
1956
|
+
...Object.fromEntries(mediaPipeline.pendingDocuments.map((d) => [d.id, d.entry])),
|
|
1957
|
+
})
|
|
1958
|
+
// Deleted-but-unsaved documents stay in the manifest until the
|
|
1959
|
+
// save commits them; the library must not offer them again.
|
|
1960
|
+
.filter(([id]) => !mediaPipeline.pendingDocumentDeletions.includes(id))
|
|
1961
|
+
.map(([id, entry]) => ({
|
|
1962
|
+
id,
|
|
1963
|
+
filename: entry.filename,
|
|
1964
|
+
size: entry.size,
|
|
1965
|
+
kind: entry.kind,
|
|
1966
|
+
}))}
|
|
1967
|
+
kinds={mediaModalMode === "manage" ? undefined : (mediaSelectKinds ?? undefined)}
|
|
1871
1968
|
onSelect={(id) => {
|
|
1872
1969
|
if (mediaSelectCallback) {
|
|
1873
1970
|
mediaSelectCallback(id);
|
|
1874
1971
|
}
|
|
1875
1972
|
setShowMediaLibrary(false);
|
|
1876
1973
|
setMediaSelectCallback(null);
|
|
1974
|
+
setMediaSelectKinds(null);
|
|
1877
1975
|
}}
|
|
1878
1976
|
onUpload={mediaPipeline.handleMediaUpload}
|
|
1879
1977
|
onDelete={mediaPipeline.handleMediaDelete}
|
|
1978
|
+
onDeleteDocuments={(ids) => { void mediaPipeline.handleDocumentDelete(ids); }}
|
|
1880
1979
|
onAltChange={mediaPipeline.handleMediaAltChange}
|
|
1881
1980
|
referenceCountMap={mediaPipeline.referenceCountMap}
|
|
1981
|
+
documentReferenceCountMap={mediaPipeline.documentReferenceCountMap}
|
|
1882
1982
|
localUrlMap={mediaPipeline.pendingLocalUrls}
|
|
1883
1983
|
maxFileSize={siteConfig?.media.maxFileSize}
|
|
1884
1984
|
uploadProgress={mediaPipeline.uploadProgress}
|
|
@@ -2023,7 +2123,8 @@ function HistoryWatcher({
|
|
|
2023
2123
|
siteConfig: data.siteConfig,
|
|
2024
2124
|
});
|
|
2025
2125
|
siteNavChangeEvent.dispatch(
|
|
2026
|
-
|
|
2126
|
+
// History views hold no document manifest for that commit — no group.
|
|
2127
|
+
generateSiteNav({ index: data.index, activeSections: [], activePageId: null, mode: "editor", audience: null, documents: [] }),
|
|
2027
2128
|
);
|
|
2028
2129
|
} catch (err) {
|
|
2029
2130
|
console.error("Failed to load historical content:", err);
|
|
@@ -2074,7 +2175,10 @@ function HistoryOrEditorContent({
|
|
|
2074
2175
|
wasInHistory.current = false;
|
|
2075
2176
|
if (index.pages.length > 0) {
|
|
2076
2177
|
siteNavChangeEvent.dispatch(
|
|
2077
|
-
|
|
2178
|
+
// Leaving history: the live nav effect re-dispatches with the current
|
|
2179
|
+
// document list on the next render, so this one deliberately omits it
|
|
2180
|
+
// rather than briefly showing a stale group.
|
|
2181
|
+
generateSiteNav({ index, activeSections, activePageId, mode: "editor", audience: null, documents: [] }),
|
|
2078
2182
|
);
|
|
2079
2183
|
}
|
|
2080
2184
|
}
|
|
@@ -2086,7 +2190,7 @@ function HistoryOrEditorContent({
|
|
|
2086
2190
|
if (!historyState || !historyPage) return;
|
|
2087
2191
|
if (!historyPageId) setHistoryPageId(historyPage.id);
|
|
2088
2192
|
siteNavChangeEvent.dispatch(
|
|
2089
|
-
generateSiteNav({ index: historyState.index, activeSections: [], activePageId: historyPage.id, mode: "editor", audience: null }),
|
|
2193
|
+
generateSiteNav({ index: historyState.index, activeSections: [], activePageId: historyPage.id, mode: "editor", audience: null, documents: [] }),
|
|
2090
2194
|
);
|
|
2091
2195
|
}, [historyState, historyPage?.id]);
|
|
2092
2196
|
|
|
@@ -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,10 @@ 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[];
|
|
14
31
|
}
|
|
15
32
|
|
|
16
33
|
export const MediaLibraryContext = createContext<MediaLibraryContextValue | null>(null);
|