@drawnagency/primitives 0.1.76 → 0.1.78

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 (122) hide show
  1. package/dist/{chunk-Q3T23HVW.js → chunk-2DDWWTRX.js} +5 -5
  2. package/dist/chunk-2G2S36SY.js +13 -0
  3. package/dist/{chunk-S55OZ7F2.js → chunk-3LNKOLZP.js} +4 -0
  4. package/dist/{chunk-SWIHUIFT.js → chunk-4SIWBWKZ.js} +12 -2
  5. package/dist/{chunk-SWEVWC5K.js → chunk-BAQYZSFF.js} +22 -22
  6. package/dist/{chunk-IFHRHV7S.js → chunk-EMGLZQCM.js} +1 -1
  7. package/dist/{chunk-NKVZGPR6.js → chunk-K4JD6FD3.js} +1 -1
  8. package/dist/{chunk-SFKYUKVN.js → chunk-MK4POMLZ.js} +1 -1
  9. package/dist/{chunk-7T2WQSWF.js → chunk-NS3UTJ3E.js} +11 -11
  10. package/dist/chunk-REM2NH7K.js +44 -0
  11. package/dist/{chunk-T7MP7ANP.js → chunk-UA6OCJJK.js} +2 -2
  12. package/dist/{chunk-ZRWGV5O6.js → chunk-Z5IYWUW3.js} +3 -3
  13. package/dist/collab/provider-registry.d.ts +4 -0
  14. package/dist/collab/provider-registry.d.ts.map +1 -0
  15. package/dist/collab/provider-registry.js +9 -0
  16. package/dist/collab/rebase.d.ts +37 -0
  17. package/dist/collab/rebase.d.ts.map +1 -0
  18. package/dist/collab/rebase.js +64 -0
  19. package/dist/collab/recovery.d.ts +89 -0
  20. package/dist/collab/recovery.d.ts.map +1 -0
  21. package/dist/collab/types.d.ts +93 -0
  22. package/dist/collab/types.d.ts.map +1 -0
  23. package/dist/collab/types.js +19 -0
  24. package/dist/components/editor/AudienceIndicator.d.ts +3 -1
  25. package/dist/components/editor/AudienceIndicator.d.ts.map +1 -1
  26. package/dist/components/editor/ConflictDialog.d.ts +14 -0
  27. package/dist/components/editor/ConflictDialog.d.ts.map +1 -0
  28. package/dist/components/editor/IndicatorPill.d.ts +4 -1
  29. package/dist/components/editor/IndicatorPill.d.ts.map +1 -1
  30. package/dist/components/editor/LockBadge.d.ts +7 -0
  31. package/dist/components/editor/LockBadge.d.ts.map +1 -0
  32. package/dist/components/editor/PresenceAvatars.d.ts +9 -0
  33. package/dist/components/editor/PresenceAvatars.d.ts.map +1 -0
  34. package/dist/components/editor/SectionWrapper.d.ts +1 -1
  35. package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
  36. package/dist/components/editor/StatusIndicator.d.ts +3 -1
  37. package/dist/components/editor/StatusIndicator.d.ts.map +1 -1
  38. package/dist/components/editor/index.js +95 -47
  39. package/dist/components/primitives/index.js +5 -5
  40. package/dist/components/sections/brand-guide.js +5 -5
  41. package/dist/components/sections/register-schemas.js +11 -11
  42. package/dist/components/sections/register.js +11 -11
  43. package/dist/components/shell/EditorShell.d.ts +3 -1
  44. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  45. package/dist/components/shell/MediaBreadcrumb.d.ts +10 -0
  46. package/dist/components/shell/MediaBreadcrumb.d.ts.map +1 -0
  47. package/dist/components/shell/MediaFolderTile.d.ts +15 -0
  48. package/dist/components/shell/MediaFolderTile.d.ts.map +1 -0
  49. package/dist/components/shell/MediaFolderTree.d.ts +20 -0
  50. package/dist/components/shell/MediaFolderTree.d.ts.map +1 -0
  51. package/dist/components/shell/MediaLibraryModal.d.ts +15 -3
  52. package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
  53. package/dist/components/shell/MoveToFolderDialog.d.ts +16 -0
  54. package/dist/components/shell/MoveToFolderDialog.d.ts.map +1 -0
  55. package/dist/hooks/useCollabSession.d.ts +23 -0
  56. package/dist/hooks/useCollabSession.d.ts.map +1 -0
  57. package/dist/hooks/useEditorPersistence.d.ts +21 -1
  58. package/dist/hooks/useEditorPersistence.d.ts.map +1 -1
  59. package/dist/hooks/useEditorPublish.d.ts +39 -2
  60. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  61. package/dist/hooks/useMediaFolderDnd.d.ts +27 -0
  62. package/dist/hooks/useMediaFolderDnd.d.ts.map +1 -0
  63. package/dist/hooks/useMediaPipeline.d.ts +14 -2
  64. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +51 -25
  68. package/dist/lib/dexie.d.ts +12 -0
  69. package/dist/lib/dexie.d.ts.map +1 -1
  70. package/dist/lib/dexie.js +27 -0
  71. package/dist/lib/index.js +12 -12
  72. package/dist/lib/media-dnd.d.ts +26 -0
  73. package/dist/lib/media-dnd.d.ts.map +1 -0
  74. package/dist/lib/media-folders.d.ts +59 -0
  75. package/dist/lib/media-folders.d.ts.map +1 -0
  76. package/dist/lib/registry.d.ts +10 -0
  77. package/dist/lib/registry.d.ts.map +1 -1
  78. package/dist/media/folder-drop.d.ts +22 -0
  79. package/dist/media/folder-drop.d.ts.map +1 -0
  80. package/dist/media/index.js +6 -2
  81. package/dist/media/types.d.ts +1 -1
  82. package/dist/media/types.d.ts.map +1 -1
  83. package/dist/media/utils.d.ts +7 -0
  84. package/dist/media/utils.d.ts.map +1 -1
  85. package/dist/schemas/index.js +5 -3
  86. package/dist/schemas/media.d.ts +11 -0
  87. package/dist/schemas/media.d.ts.map +1 -1
  88. package/dist/types/database.d.ts +70 -0
  89. package/dist/types/database.d.ts.map +1 -1
  90. package/package.json +13 -1
  91. package/src/collab/provider-registry.ts +14 -0
  92. package/src/collab/rebase.ts +140 -0
  93. package/src/collab/recovery.ts +149 -0
  94. package/src/collab/types.ts +96 -0
  95. package/src/components/editor/AudienceIndicator.tsx +4 -1
  96. package/src/components/editor/ConflictDialog.tsx +61 -0
  97. package/src/components/editor/IndicatorPill.tsx +12 -6
  98. package/src/components/editor/LockBadge.tsx +31 -0
  99. package/src/components/editor/PresenceAvatars.tsx +53 -0
  100. package/src/components/editor/SectionWrapper.tsx +60 -29
  101. package/src/components/editor/StatusIndicator.tsx +4 -0
  102. package/src/components/shell/EditorShell.tsx +581 -22
  103. package/src/components/shell/MediaBreadcrumb.tsx +77 -0
  104. package/src/components/shell/MediaFolderTile.tsx +141 -0
  105. package/src/components/shell/MediaFolderTree.tsx +127 -0
  106. package/src/components/shell/MediaLibraryModal.tsx +611 -229
  107. package/src/components/shell/MoveToFolderDialog.tsx +78 -0
  108. package/src/hooks/useCollabSession.ts +220 -0
  109. package/src/hooks/useEditorPersistence.ts +104 -1
  110. package/src/hooks/useEditorPublish.ts +216 -71
  111. package/src/hooks/useMediaFolderDnd.ts +129 -0
  112. package/src/hooks/useMediaPipeline.ts +162 -6
  113. package/src/index.ts +2 -0
  114. package/src/lib/dexie.ts +44 -0
  115. package/src/lib/media-dnd.ts +62 -0
  116. package/src/lib/media-folders.ts +232 -0
  117. package/src/lib/registry.ts +8 -0
  118. package/src/media/folder-drop.ts +62 -0
  119. package/src/media/types.ts +2 -0
  120. package/src/media/utils.ts +10 -0
  121. package/src/schemas/media.ts +13 -0
  122. package/src/types/database.ts +55 -0
@@ -0,0 +1,77 @@
1
+ import { Fragment, useRef } from "react";
2
+ import { ChevronRight } from "lucide-react";
3
+ import { cn } from "../../lib/cn";
4
+ import type { MediaFolder, MediaItem } from "../../media/types";
5
+ import { folderPathIds } from "../../lib/media-folders";
6
+ import { useFolderDropTarget } from "../../hooks/useMediaFolderDnd";
7
+ import type { MediaFolderDndBindings } from "./MediaFolderTree";
8
+
9
+ export interface MediaBreadcrumbProps {
10
+ folders: Record<string, MediaFolder>;
11
+ currentFolderId: string | null;
12
+ onNavigate: (id: string | null) => void;
13
+ dnd?: MediaFolderDndBindings;
14
+ }
15
+
16
+
17
+ function Segment({
18
+ label, target, onNavigate, dnd, isCurrent,
19
+ }: {
20
+ label: string;
21
+ target: string | null;
22
+ onNavigate: (id: string | null) => void;
23
+ dnd?: MediaFolderDndBindings;
24
+ isCurrent: boolean;
25
+ }) {
26
+ const emptyFoldersRef = useRef<Record<string, MediaFolder>>({});
27
+ const emptyItemsRef = useRef<Record<string, MediaItem>>({});
28
+ const { ref, isDropTarget } = useFolderDropTarget<HTMLButtonElement>({
29
+ targetFolderId: target,
30
+ enabled: dnd !== undefined && !isCurrent,
31
+ foldersRef: dnd?.foldersRef ?? emptyFoldersRef,
32
+ itemsByIdRef: dnd?.itemsByIdRef ?? emptyItemsRef,
33
+ onMoveMedia: dnd?.onMoveMedia ?? (() => {}),
34
+ onMoveFolder: dnd?.onMoveFolder ?? (() => {}),
35
+ });
36
+
37
+ return (
38
+ <button
39
+ ref={ref}
40
+ type="button"
41
+ disabled={isCurrent}
42
+ onClick={() => onNavigate(target)}
43
+ className={cn(
44
+ "max-w-40 truncate rounded px-1 py-0.5 text-sm",
45
+ isCurrent
46
+ ? "font-medium text-base-contrast"
47
+ : "cursor-pointer text-base-contrast-light hover:text-base-contrast",
48
+ isDropTarget && "ring-2 ring-primary ring-inset",
49
+ )}
50
+ >
51
+ {label}
52
+ </button>
53
+ );
54
+ }
55
+
56
+ export function MediaBreadcrumb({ folders, currentFolderId, onNavigate, dnd }: MediaBreadcrumbProps) {
57
+ if (currentFolderId === null) return null;
58
+ const pathIds = folderPathIds(folders, currentFolderId);
59
+
60
+ return (
61
+ <div className="flex flex-wrap items-center gap-0.5">
62
+ <Segment label="All media" target={null} onNavigate={onNavigate} dnd={dnd} isCurrent={false} />
63
+ {pathIds.map((id, i) => (
64
+ <Fragment key={id}>
65
+ <ChevronRight size={12} className="shrink-0 text-base-contrast-light" />
66
+ <Segment
67
+ label={folders[id].name}
68
+ target={id}
69
+ onNavigate={onNavigate}
70
+ dnd={dnd}
71
+ isCurrent={i === pathIds.length - 1}
72
+ />
73
+ </Fragment>
74
+ ))}
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,141 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { Folder, MoreVertical } from "lucide-react";
3
+ import { cn } from "../../lib/cn";
4
+ import { buildFolderDragData } from "../../lib/media-dnd";
5
+ import type { MediaFolder, MediaItem } from "../../media/types";
6
+ import { useFolderDropTarget, useMediaDraggable } from "../../hooks/useMediaFolderDnd";
7
+ import type { MediaFolderDndBindings } from "./MediaFolderTree";
8
+
9
+ export interface MediaFolderTileProps {
10
+ id: string;
11
+ name: string;
12
+ count: number;
13
+ onOpen: (id: string) => void;
14
+ manage: boolean;
15
+ onRename?: (id: string, name: string) => void;
16
+ onDeleteRequest?: (id: string) => void;
17
+ dnd?: MediaFolderDndBindings;
18
+ /** Optional accessible label override for the root element (e.g. "Open folder Logos"). */
19
+ ariaLabel?: string;
20
+ }
21
+
22
+
23
+ export function MediaFolderTile({
24
+ id, name, count, onOpen, manage, onRename, onDeleteRequest, dnd, ariaLabel,
25
+ }: MediaFolderTileProps) {
26
+ const [menuOpen, setMenuOpen] = useState(false);
27
+ const [renaming, setRenaming] = useState(false);
28
+ const [draft, setDraft] = useState(name);
29
+ const menuRef = useRef<HTMLDivElement>(null);
30
+ const emptyFoldersRef = useRef<Record<string, MediaFolder>>({});
31
+ const emptyItemsRef = useRef<Record<string, MediaItem>>({});
32
+
33
+ const drag = useMediaDraggable<HTMLDivElement>({
34
+ enabled: manage && dnd !== undefined && !renaming,
35
+ getDragData: () => buildFolderDragData(id),
36
+ });
37
+ const drop = useFolderDropTarget<HTMLDivElement>({
38
+ targetFolderId: id,
39
+ enabled: dnd !== undefined,
40
+ foldersRef: dnd?.foldersRef ?? emptyFoldersRef,
41
+ itemsByIdRef: dnd?.itemsByIdRef ?? emptyItemsRef,
42
+ onMoveMedia: dnd?.onMoveMedia ?? (() => {}),
43
+ onMoveFolder: dnd?.onMoveFolder ?? (() => {}),
44
+ });
45
+
46
+ // Dismiss the kebab menu on any pointerdown outside its container.
47
+ useEffect(() => {
48
+ if (!menuOpen) return;
49
+ const handler = (e: PointerEvent) => {
50
+ if (menuRef.current && e.target instanceof Node && !menuRef.current.contains(e.target)) {
51
+ setMenuOpen(false);
52
+ }
53
+ };
54
+ document.addEventListener("pointerdown", handler);
55
+ return () => document.removeEventListener("pointerdown", handler);
56
+ }, [menuOpen]);
57
+
58
+ const commitRename = () => {
59
+ setRenaming(false);
60
+ const next = draft.trim();
61
+ if (next && next !== name) onRename?.(id, next);
62
+ else setDraft(name);
63
+ };
64
+
65
+ return (
66
+ <div
67
+ // The tile is both a draggable and a drop target — a callback ref fills
68
+ // both hook refs at commit time, BEFORE either hook's registration
69
+ // effect runs (an effect-based sync would run after and never register).
70
+ ref={(el) => { drag.ref.current = el; drop.ref.current = el; }}
71
+ role="button"
72
+ tabIndex={0}
73
+ aria-label={ariaLabel}
74
+ onClick={() => { if (!renaming) onOpen(id); }}
75
+ onKeyDown={(e) => { if (e.key === "Enter" && !renaming) onOpen(id); }}
76
+ className={cn(
77
+ "group/tile relative flex items-center gap-2 rounded-lg border border-base-200 px-3 py-2.5 transition-colors",
78
+ "cursor-pointer hover:border-base-300 hover:bg-base-accent",
79
+ drag.isDragging && "opacity-40",
80
+ drop.isDropTarget && "border-primary ring-2 ring-primary ring-inset",
81
+ )}
82
+ >
83
+ <Folder size={16} className="shrink-0 text-base-contrast-light" />
84
+ {renaming ? (
85
+ <input
86
+ autoFocus
87
+ value={draft}
88
+ onChange={(e) => setDraft(e.target.value)}
89
+ onClick={(e) => e.stopPropagation()}
90
+ onKeyDown={(e) => {
91
+ if (e.key === "Enter") commitRename();
92
+ if (e.key === "Escape") { setRenaming(false); setDraft(name); }
93
+ }}
94
+ onBlur={commitRename}
95
+ className="min-w-0 flex-1 rounded border border-base-200 bg-base px-1 py-0.5 text-sm text-base-contrast focus:outline-none focus:ring-1 focus:ring-primary"
96
+ />
97
+ ) : (
98
+ <span className="min-w-0 flex-1 truncate text-sm text-base-contrast">{name}</span>
99
+ )}
100
+ {!renaming && count > 0 && (
101
+ <span className="shrink-0 text-xs text-base-contrast-light">{count}</span>
102
+ )}
103
+ {manage && !renaming && (
104
+ <div ref={menuRef} className="relative shrink-0">
105
+ <button
106
+ type="button"
107
+ aria-label={`Folder actions: ${name}`}
108
+ onClick={(e) => { e.stopPropagation(); setMenuOpen((v) => !v); }}
109
+ // Enter/Space keydown bubbles to the tile root BEFORE the button's
110
+ // synthetic click fires — stop it so the tile doesn't open (see
111
+ // the matching chevron fix in MediaFolderTree.tsx).
112
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") e.stopPropagation(); }}
113
+ className="rounded p-1 opacity-0 transition-opacity hover:bg-base-200/50 focus:opacity-100 group-hover/tile:opacity-100"
114
+ >
115
+ <MoreVertical size={16} />
116
+ </button>
117
+ {menuOpen && (
118
+ <div className="absolute right-0 top-6 z-20 w-28 rounded-md border border-base-200 bg-base py-1 shadow-lg">
119
+ <button
120
+ type="button"
121
+ className="block w-full px-3 py-1.5 text-left text-sm text-base-contrast hover:bg-base-accent"
122
+ onClick={(e) => { e.stopPropagation(); setMenuOpen(false); setDraft(name); setRenaming(true); }}
123
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") e.stopPropagation(); }}
124
+ >
125
+ Rename
126
+ </button>
127
+ <button
128
+ type="button"
129
+ className="block w-full px-3 py-1.5 text-left text-sm text-red-600 hover:bg-base-accent dark:text-red-400"
130
+ onClick={(e) => { e.stopPropagation(); setMenuOpen(false); onDeleteRequest?.(id); }}
131
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") e.stopPropagation(); }}
132
+ >
133
+ Delete
134
+ </button>
135
+ </div>
136
+ )}
137
+ </div>
138
+ )}
139
+ </div>
140
+ );
141
+ }
@@ -0,0 +1,127 @@
1
+ import { useRef } from "react";
2
+ import { ChevronRight, Folder, LayoutGrid } from "lucide-react";
3
+ import { cn } from "../../lib/cn";
4
+ import type { MediaFolder, MediaItem } from "../../media/types";
5
+ import { buildFolderTree, type FolderTreeNode } from "../../lib/media-folders";
6
+ import { useFolderDropTarget } from "../../hooks/useMediaFolderDnd";
7
+
8
+ export interface MediaFolderDndBindings {
9
+ foldersRef: React.RefObject<Record<string, MediaFolder>>;
10
+ itemsByIdRef: React.RefObject<Record<string, MediaItem>>;
11
+ onMoveMedia: (ids: string[], target: string | null) => void;
12
+ onMoveFolder: (id: string, target: string | null) => void;
13
+ }
14
+
15
+ export interface MediaFolderTreeProps {
16
+ folders: Record<string, MediaFolder>;
17
+ counts: Record<string, number>;
18
+ totalCount: number;
19
+ currentFolderId: string | null;
20
+ onNavigate: (id: string | null) => void;
21
+ expandedIds: Set<string>;
22
+ onExpand: (id: string) => void;
23
+ onToggleExpand: (id: string) => void;
24
+ dnd?: MediaFolderDndBindings;
25
+ }
26
+
27
+
28
+ function useRowDropTarget(
29
+ targetFolderId: string | null,
30
+ dnd: MediaFolderDndBindings | undefined,
31
+ onDragHover?: () => void,
32
+ ) {
33
+ const emptyFoldersRef = useRef<Record<string, MediaFolder>>({});
34
+ const emptyItemsRef = useRef<Record<string, MediaItem>>({});
35
+ return useFolderDropTarget<HTMLDivElement>({
36
+ targetFolderId,
37
+ enabled: dnd !== undefined,
38
+ foldersRef: dnd?.foldersRef ?? emptyFoldersRef,
39
+ itemsByIdRef: dnd?.itemsByIdRef ?? emptyItemsRef,
40
+ onMoveMedia: dnd?.onMoveMedia ?? (() => {}),
41
+ onMoveFolder: dnd?.onMoveFolder ?? (() => {}),
42
+ onDragHover,
43
+ });
44
+ }
45
+
46
+ const rowClass = (active: boolean, isDropTarget: boolean) =>
47
+ cn(
48
+ "flex w-full cursor-pointer items-center gap-1.5 rounded px-2 py-1.5 text-sm transition-colors",
49
+ active ? "bg-primary/10 font-medium text-primary" : "text-base-contrast hover:bg-base-accent",
50
+ isDropTarget && "ring-2 ring-primary ring-inset",
51
+ );
52
+
53
+ function TreeRow({
54
+ node, depth, props,
55
+ }: { node: FolderTreeNode; depth: number; props: MediaFolderTreeProps }) {
56
+ const { currentFolderId, onNavigate, expandedIds, onExpand, onToggleExpand, counts, dnd } = props;
57
+ const expanded = expandedIds.has(node.id);
58
+ const { ref, isDropTarget } = useRowDropTarget(node.id, dnd, () => onExpand(node.id));
59
+
60
+ return (
61
+ <div>
62
+ <div
63
+ ref={ref}
64
+ role="button"
65
+ tabIndex={0}
66
+ onClick={() => onNavigate(node.id)}
67
+ onKeyDown={(e) => { if (e.key === "Enter") onNavigate(node.id); }}
68
+ className={rowClass(currentFolderId === node.id, isDropTarget)}
69
+ style={{ paddingLeft: `${8 + depth * 14}px` }}
70
+ >
71
+ {node.children.length > 0 ? (
72
+ <button
73
+ type="button"
74
+ aria-label={`${expanded ? "Collapse" : "Expand"} ${node.name}`}
75
+ aria-expanded={expanded}
76
+ onClick={(e) => { e.stopPropagation(); onToggleExpand(node.id); }}
77
+ // Enter/Space keydown bubbles to the row BEFORE the button's
78
+ // synthetic click fires — stop it so the row doesn't navigate.
79
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") e.stopPropagation(); }}
80
+ className="shrink-0 rounded p-0.5 hover:bg-base-200"
81
+ >
82
+ <ChevronRight size={12} className={cn("transition-transform", expanded && "rotate-90")} />
83
+ </button>
84
+ ) : (
85
+ <span className="w-[17px] shrink-0" />
86
+ )}
87
+ <Folder size={14} className="shrink-0 text-base-contrast-light" />
88
+ <span className="min-w-0 flex-1 truncate">{node.name}</span>
89
+ {(counts[node.id] ?? 0) > 0 && (
90
+ <span className="shrink-0 text-xs text-base-contrast-light">{counts[node.id]}</span>
91
+ )}
92
+ </div>
93
+ {expanded &&
94
+ node.children.map((child) => (
95
+ <TreeRow key={child.id} node={child} depth={depth + 1} props={props} />
96
+ ))}
97
+ </div>
98
+ );
99
+ }
100
+
101
+ export function MediaFolderTree(props: MediaFolderTreeProps) {
102
+ const { folders, totalCount, currentFolderId, onNavigate, dnd } = props;
103
+ const tree = buildFolderTree(folders);
104
+ const root = useRowDropTarget(null, dnd);
105
+
106
+ return (
107
+ <nav aria-label="Media folders" className="flex flex-col gap-0.5">
108
+ <div
109
+ ref={root.ref}
110
+ role="button"
111
+ tabIndex={0}
112
+ onClick={() => onNavigate(null)}
113
+ onKeyDown={(e) => { if (e.key === "Enter") onNavigate(null); }}
114
+ className={rowClass(currentFolderId === null, root.isDropTarget)}
115
+ >
116
+ <LayoutGrid size={14} className="shrink-0 text-base-contrast-light" />
117
+ <span className="min-w-0 flex-1 truncate">All media</span>
118
+ {totalCount > 0 && (
119
+ <span className="shrink-0 text-xs text-base-contrast-light">{totalCount}</span>
120
+ )}
121
+ </div>
122
+ {tree.map((node) => (
123
+ <TreeRow key={node.id} node={node} depth={0} props={props} />
124
+ ))}
125
+ </nav>
126
+ );
127
+ }