@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.
Files changed (117) hide show
  1. package/dist/{chunk-FAZFADJK.js → chunk-3TO4RRHY.js} +3 -1
  2. package/dist/{chunk-KP4DWTPR.js → chunk-72FMQQIA.js} +246 -3
  3. package/dist/chunk-BS5AQHZG.js +45 -0
  4. package/dist/{chunk-HKHEAH5F.js → chunk-GOCFTXR3.js} +8 -60
  5. package/dist/{chunk-GYCBWGVW.js → chunk-LBHQWIVL.js} +79 -24
  6. package/dist/components/chat/ChatPanel.d.ts.map +1 -1
  7. package/dist/components/docfile/DocFileOverlay.d.ts +12 -0
  8. package/dist/components/docfile/DocFileOverlay.d.ts.map +1 -0
  9. package/dist/components/docfile/DocFilePane.d.ts +17 -0
  10. package/dist/components/docfile/DocFilePane.d.ts.map +1 -0
  11. package/dist/components/docfile/doc-file-state.d.ts +33 -0
  12. package/dist/components/docfile/doc-file-state.d.ts.map +1 -0
  13. package/dist/components/editor/SettingsForm.d.ts.map +1 -1
  14. package/dist/components/editor/index.js +1 -1
  15. package/dist/components/sections/Document/index.d.ts +18 -0
  16. package/dist/components/sections/Document/index.d.ts.map +1 -0
  17. package/dist/components/sections/all-sections.d.ts +10 -0
  18. package/dist/components/sections/all-sections.d.ts.map +1 -1
  19. package/dist/components/sections/brand-guide.d.ts +10 -0
  20. package/dist/components/sections/brand-guide.d.ts.map +1 -1
  21. package/dist/components/sections/brand-guide.js +3 -1
  22. package/dist/components/sections/register-schemas.js +5 -3
  23. package/dist/components/sections/register.js +5 -3
  24. package/dist/components/shared/DocumentSourceField.d.ts +13 -0
  25. package/dist/components/shared/DocumentSourceField.d.ts.map +1 -0
  26. package/dist/components/shared/Navigation.d.ts.map +1 -1
  27. package/dist/components/shell/EditorModal.d.ts.map +1 -1
  28. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  29. package/dist/components/shell/MediaBreadcrumb.d.ts.map +1 -1
  30. package/dist/components/shell/MediaFolderTile.d.ts +3 -1
  31. package/dist/components/shell/MediaFolderTile.d.ts.map +1 -1
  32. package/dist/components/shell/MediaFolderTree.d.ts +3 -0
  33. package/dist/components/shell/MediaFolderTree.d.ts.map +1 -1
  34. package/dist/components/shell/MediaLibraryContext.d.ts +18 -1
  35. package/dist/components/shell/MediaLibraryContext.d.ts.map +1 -1
  36. package/dist/components/shell/MediaLibraryModal.d.ts +21 -1
  37. package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
  38. package/dist/hooks/useDocumentUploads.d.ts +32 -0
  39. package/dist/hooks/useDocumentUploads.d.ts.map +1 -0
  40. package/dist/hooks/useEditorPublish.d.ts +18 -1
  41. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  42. package/dist/hooks/useMediaPipeline.d.ts +14 -1
  43. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  44. package/dist/hooks/useResolvedDocument.d.ts +12 -0
  45. package/dist/hooks/useResolvedDocument.d.ts.map +1 -0
  46. package/dist/index.js +56 -32
  47. package/dist/lib/dexie.d.ts +11 -0
  48. package/dist/lib/dexie.d.ts.map +1 -1
  49. package/dist/lib/dexie.js +32 -0
  50. package/dist/lib/document-refs.d.ts +22 -0
  51. package/dist/lib/document-refs.d.ts.map +1 -0
  52. package/dist/lib/document-uploads.d.ts +38 -0
  53. package/dist/lib/document-uploads.d.ts.map +1 -0
  54. package/dist/lib/documents.d.ts +65 -0
  55. package/dist/lib/documents.d.ts.map +1 -0
  56. package/dist/lib/index.js +4 -4
  57. package/dist/lib/media-folders.d.ts +8 -0
  58. package/dist/lib/media-folders.d.ts.map +1 -1
  59. package/dist/lib/media-folders.js +2 -0
  60. package/dist/lib/nav.d.ts +29 -1
  61. package/dist/lib/nav.d.ts.map +1 -1
  62. package/dist/lib/platform-broker.d.ts +12 -0
  63. package/dist/lib/platform-broker.d.ts.map +1 -1
  64. package/dist/lib/platform-broker.js +12 -3
  65. package/dist/lib/registry.d.ts +10 -0
  66. package/dist/lib/registry.d.ts.map +1 -1
  67. package/dist/lib/save-chunks.d.ts +15 -0
  68. package/dist/lib/save-chunks.d.ts.map +1 -1
  69. package/dist/lib/scroll-lock.d.ts +2 -0
  70. package/dist/lib/scroll-lock.d.ts.map +1 -0
  71. package/dist/schemas/documents.d.ts +80 -0
  72. package/dist/schemas/documents.d.ts.map +1 -0
  73. package/dist/schemas/documents.js +25 -0
  74. package/dist/schemas/index.d.ts +1 -0
  75. package/dist/schemas/index.d.ts.map +1 -1
  76. package/dist/schemas/index.js +23 -1
  77. package/dist/storage/index.d.ts +2 -2
  78. package/dist/storage/index.d.ts.map +1 -1
  79. package/dist/storage/index.js +3 -1
  80. package/dist/storage/types.d.ts +47 -0
  81. package/dist/storage/types.d.ts.map +1 -1
  82. package/package.json +5 -1
  83. package/src/components/chat/ChatPanel.tsx +5 -4
  84. package/src/components/docfile/DocFileOverlay.tsx +54 -0
  85. package/src/components/docfile/DocFilePane.tsx +129 -0
  86. package/src/components/docfile/doc-file-state.ts +180 -0
  87. package/src/components/editor/SettingsForm.tsx +10 -0
  88. package/src/components/sections/Document/index.tsx +163 -0
  89. package/src/components/sections/brand-guide.ts +2 -1
  90. package/src/components/shared/DocumentSourceField.tsx +51 -0
  91. package/src/components/shared/Navigation.tsx +53 -10
  92. package/src/components/shell/EditorModal.tsx +2 -5
  93. package/src/components/shell/EditorShell.tsx +111 -7
  94. package/src/components/shell/MediaBreadcrumb.tsx +14 -1
  95. package/src/components/shell/MediaFolderTile.tsx +4 -2
  96. package/src/components/shell/MediaFolderTree.tsx +20 -3
  97. package/src/components/shell/MediaLibraryContext.tsx +18 -1
  98. package/src/components/shell/MediaLibraryModal.tsx +223 -45
  99. package/src/hooks/useDocumentUploads.ts +330 -0
  100. package/src/hooks/useEditorPublish.ts +180 -8
  101. package/src/hooks/useMediaPipeline.ts +136 -5
  102. package/src/hooks/useResolvedDocument.ts +25 -0
  103. package/src/lib/dexie.ts +54 -0
  104. package/src/lib/document-refs.ts +83 -0
  105. package/src/lib/document-uploads.ts +66 -0
  106. package/src/lib/documents.ts +138 -0
  107. package/src/lib/media-folders.ts +9 -0
  108. package/src/lib/nav.ts +46 -2
  109. package/src/lib/platform-broker.ts +18 -3
  110. package/src/lib/registry.ts +9 -0
  111. package/src/lib/save-chunks.ts +42 -0
  112. package/src/lib/scroll-lock.ts +27 -0
  113. package/src/schemas/documents.ts +82 -0
  114. package/src/schemas/index.ts +1 -0
  115. package/src/storage/index.ts +2 -2
  116. package/src/storage/types.ts +46 -0
  117. /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
- // Push the current SiteNav (active page + its headings) to the Navigation island.
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
- generateSiteNav({ index: data.index, activeSections: [], activePageId: null, mode: "editor", audience: null }),
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
- generateSiteNav({ index, activeSections, activePageId, mode: "editor", audience: null }),
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
- openSelectModal: (onSelect: (imageId: string) => void) => void;
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);