Skip to content

Commit 81eddda

Browse files
Merge pull request #100 from solid-contrib/94-shadcn-sidebar-folder-tree
Use shadcn sidebar component for the file manager sidebar/folder tree
2 parents 7511af2 + 48ed0d2 commit 81eddda

10 files changed

Lines changed: 930 additions & 208 deletions

File tree

‎app/components/FolderTree.tsx‎

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import { ChevronRight, ChevronDown, Folder } from "lucide-react";
55
import { SolidStorage } from "../lib/hooks/useSolidStorages";
66
import { FolderTreeChild, folderUrlsEqual, ensureTrailingSlash, getAuthenticatedSession, fetchContainerListing, foldersFromListing } from "../lib/helpers";
77
import { getContainerListing, loadContainerListing, subscribeContainerCache, getContainerCacheVersion } from "../lib/cache";
8+
import {
9+
SidebarMenu,
10+
SidebarMenuButton,
11+
SidebarMenuItem,
12+
SidebarMenuSub,
13+
SidebarMenuSubItem,
14+
SidebarMenuSkeleton,
15+
} from "@/components/ui/sidebar";
816

917
interface FolderTreeProps {
1018
storages: SolidStorage[];
@@ -30,6 +38,8 @@ export default function FolderTree({
3038
// Children come from the shared cache, not a local copy.
3139
// cacheVersion makes this recompute when listings are written or invalidated.
3240
const childrenByUrl = useMemo(() => {
41+
// cacheVersion is read so listings recompute after cache writes/invalidation.
42+
void cacheVersion;
3343
const next: Record<string, FolderTreeChild[]> = {};
3444
for (const url of expandedUrls) {
3545
const cached = getContainerListing(url);
@@ -124,7 +134,7 @@ export default function FolderTree({
124134
}, [expandedUrls, loadChildren]);
125135

126136
// Render one folder row and its nested children when expanded.
127-
const renderNode = useCallback((node: FolderTreeChild, depth: number) => {
137+
const renderNode = useCallback((node: FolderTreeChild) => {
128138
const nodeUrl = ensureTrailingSlash(node.url);
129139
const isExpanded = expandedUrls.has(nodeUrl);
130140
const isLoading = loadingUrls.has(nodeUrl);
@@ -133,16 +143,12 @@ export default function FolderTree({
133143
const isCurrent = normalizedCurrentFolderUrl != null && folderUrlsEqual(normalizedCurrentFolderUrl, nodeUrl);
134144

135145
return (
136-
<li key={nodeUrl}>
137-
<div
138-
className={`group flex items-center gap-1 rounded-md px-2 py-1 text-sm ${isCurrent ? "bg-accent text-foreground font-medium" : "text-foreground hover:bg-muted"
139-
}`}
140-
style={{ paddingLeft: `${depth * 12 + 8}px` }}
141-
>
146+
<SidebarMenuItem key={nodeUrl}>
147+
<div className="flex w-full items-center gap-0.5">
142148
<button
143149
type="button"
144150
onClick={() => void toggleExpand(nodeUrl)}
145-
className="rounded p-0.5 hover:bg-muted"
151+
className="flex size-7 shrink-0 items-center justify-center rounded-md text-sidebar-foreground hover:bg-sidebar-accent"
146152
aria-label={isExpanded ? `Collapse ${node.name}` : `Expand ${node.name}`}
147153
aria-expanded={isExpanded}
148154
>
@@ -153,44 +159,41 @@ export default function FolderTree({
153159
)}
154160
</button>
155161

156-
<button
157-
type="button"
162+
<SidebarMenuButton
163+
isActive={isCurrent}
164+
className="flex-1"
158165
onClick={() => onNavigate(nodeUrl)}
159-
className="flex min-w-0 flex-1 items-center gap-2 text-left"
160166
title={node.name}
161167
aria-current={isCurrent ? "page" : undefined}
162168
>
163-
<Folder className="h-4 w-4 shrink-0" aria-hidden="true" />
164-
<span className="truncate">{node.name}</span>
165-
</button>
169+
<Folder aria-hidden="true" />
170+
<span>{node.name}</span>
171+
</SidebarMenuButton>
166172
</div>
167173

168-
{isExpanded && (
169-
<ul className="space-y-0.5">
174+
{isExpanded && (isLoading || hasError || children.length > 0) && (
175+
<SidebarMenuSub>
170176
{isLoading && (
171-
<li
172-
className="px-2 py-1 text-xs text-muted-foreground"
173-
style={{ paddingLeft: `${(depth + 1) * 12 + 8}px` }}
174-
aria-live="polite"
175-
>
176-
Loading...
177-
</li>
177+
<SidebarMenuSubItem>
178+
<SidebarMenuSkeleton showIcon />
179+
</SidebarMenuSubItem>
178180
)}
179181

180182
{!isLoading && hasError && (
181-
<li
182-
className="px-2 py-1 text-xs text-destructive"
183-
style={{ paddingLeft: `${(depth + 1) * 12 + 8}px` }}
184-
>
185-
Failed to load folders
186-
</li>
183+
<SidebarMenuSubItem>
184+
<p className="px-2 py-1 text-xs text-destructive">
185+
Failed to load folders
186+
</p>
187+
</SidebarMenuSubItem>
187188
)}
188189

189-
{!isLoading && !hasError && children.map((child) => renderNode(child, depth + 1))}
190-
</ul>
190+
{!isLoading &&
191+
!hasError &&
192+
children.map((child) => renderNode(child))}
193+
</SidebarMenuSub>
191194
)}
192-
</li>
193-
)
195+
</SidebarMenuItem>
196+
);
194197
}, [
195198
childrenByUrl,
196199
errorByUrl,
@@ -212,12 +215,15 @@ export default function FolderTree({
212215
);
213216

214217
if (rootNodes.length === 0) {
215-
return <p className="px-3 py-2 text-sm text-muted-foreground">No storages found</p>
218+
return (
219+
<p className="px-3 py-2 text-sm text-muted-foreground">
220+
No storages found
221+
</p>
222+
);
216223
}
217-
218224
return (
219-
<ul className="space-y-0.5" aria-label="My Storages">
220-
{rootNodes.map((node) => renderNode(node, 0))}
221-
</ul>
225+
<SidebarMenu aria-label="My Storages">
226+
{rootNodes.map((node) => renderNode(node))}
227+
</SidebarMenu>
222228
);
223229
}

‎app/components/Header.tsx‎

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,16 @@
11
"use client";
22

3-
import Image from "next/image";
43
import ProfileIcon from "./ProfileIcon";
54
import ThemeToggle from "./ThemeToggle";
6-
import { Menu } from "lucide-react";
5+
import { SidebarTrigger } from "@/components/ui/sidebar";
76

8-
interface HeaderProps {
9-
onMenuClick?: () => void;
10-
sidebarOpen?: boolean;
11-
}
12-
13-
export default function Header({ onMenuClick }: HeaderProps) {
7+
export default function Header() {
148
return (
15-
<header className="sticky top-0 z-10 border-b border-border bg-background">
16-
<div className="flex h-14 items-center">
17-
{/* Desktop: center logo over the w-64 sidebar column */}
18-
<div className="hidden w-64 shrink-0 items-center justify-center lg:flex">
19-
<Image
20-
src="/file-manager-logo.svg"
21-
alt="Solid File Manager"
22-
width={48}
23-
height={48}
24-
className="h-12 w-12"
25-
priority
26-
/>
27-
</div>
28-
29-
{/* Mobile: menu only — logo lives in the sidebar Sheet */}
30-
{onMenuClick && (
31-
<div className="flex items-center px-2 lg:hidden">
32-
<button
33-
type="button"
34-
onClick={onMenuClick}
35-
className="flex h-9 w-9 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring"
36-
aria-label="Toggle menu"
37-
>
38-
<Menu className="h-5 w-5" />
39-
</button>
40-
</div>
41-
)}
42-
43-
<div className="ml-auto flex items-center gap-1 px-2 sm:gap-2 sm:px-4">
44-
<ThemeToggle />
45-
<ProfileIcon />
46-
</div>
9+
<header className="sticky top-0 z-10 flex h-14 items-center gap-2 border-b border-border bg-background px-2 sm:px-4">
10+
<SidebarTrigger className="lg:hidden" />
11+
<div className="ml-auto flex items-center gap-1 sm:gap-2">
12+
<ThemeToggle />
13+
<ProfileIcon />
4714
</div>
4815
</header>
4916
);

‎app/components/Sidebar.tsx‎

Lines changed: 55 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ import GitHubLinks from "./shared/GitHubLinks";
66
import FolderTree from "./FolderTree";
77
import { SolidStorage } from "../lib/hooks/useSolidStorages";
88
import {
9-
Sheet,
10-
SheetContent,
11-
SheetTitle,
12-
} from "@/components/ui/sheet";
9+
Sidebar as SidebarPrimitive,
10+
SidebarContent,
11+
SidebarFooter,
12+
SidebarHeader,
13+
SidebarGroup,
14+
SidebarGroupContent,
15+
SidebarGroupLabel,
16+
useSidebar,
17+
} from "@/components/ui/sidebar";
1318

14-
interface SidebarProps {
15-
isOpen?: boolean;
16-
onClose?: () => void;
19+
interface AppSidebarProps {
1720
currentContainerUrl?: string | null;
1821
storages?: SolidStorage[];
1922
onFolderNavigate?: (folderUrl: string) => void;
@@ -22,93 +25,61 @@ interface SidebarProps {
2225
onFolderUploadClick?: () => void;
2326
}
2427

25-
function SidebarNav({
28+
export default function AppSidebar({
2629
currentContainerUrl,
2730
storages,
2831
onFolderNavigate,
2932
onNewFolderClick,
3033
onFileUploadClick,
3134
onFolderUploadClick,
32-
}: Omit<SidebarProps, "isOpen" | "onClose">) {
33-
return (
34-
<nav className="flex h-full flex-col" aria-label="Navigation">
35-
<NewMenuButton
36-
currentContainerUrl={currentContainerUrl || null}
37-
onNewFolderClick={onNewFolderClick}
38-
onFileUploadClick={onFileUploadClick}
39-
onFolderUploadClick={onFolderUploadClick}
40-
/>
41-
42-
<div className="mt-2 flex-1 overflow-y-auto">
43-
<p className="px-3 py-1 text-xs font-medium text-muted-foreground">
44-
My Storages
45-
</p>
46-
{storages && onFolderNavigate ? (
47-
<FolderTree
48-
storages={storages}
49-
currentFolderUrl={currentContainerUrl}
50-
onNavigate={onFolderNavigate}
51-
/>
52-
) : null}
53-
</div>
54-
55-
<div className="mt-auto border-t border-border pt-4">
56-
<GitHubLinks layout="vertical" />
57-
</div>
58-
</nav>
59-
);
60-
}
35+
}: AppSidebarProps) {
36+
const { isMobile, setOpenMobile } = useSidebar();
6137

62-
export default function Sidebar({
63-
isOpen = false,
64-
onClose,
65-
currentContainerUrl,
66-
storages,
67-
onFolderNavigate,
68-
onNewFolderClick,
69-
onFileUploadClick,
70-
onFolderUploadClick,
71-
}: SidebarProps) {
72-
const navProps = {
73-
currentContainerUrl,
74-
storages,
75-
onFolderNavigate,
76-
onNewFolderClick,
77-
onFileUploadClick,
78-
onFolderUploadClick,
38+
const handleNavigate = (folderUrl: string) => {
39+
onFolderNavigate?.(folderUrl);
40+
if (isMobile) {
41+
setOpenMobile(false);
42+
}
7943
};
8044

8145
return (
82-
<>
83-
{/* Desktop: always visible */}
84-
<aside className="hidden h-full w-64 shrink-0 flex-col border-r border-sidebar-border bg-sidebar p-2 text-sidebar-foreground lg:flex">
85-
<SidebarNav {...navProps} />
86-
</aside>
46+
<SidebarPrimitive collapsible="offcanvas">
47+
<SidebarHeader className="items-center py-3">
48+
<Image
49+
src="/file-manager-logo.svg"
50+
alt="Solid File Manager"
51+
width={48}
52+
height={48}
53+
className="h-12 w-12"
54+
priority
55+
/>
56+
</SidebarHeader>
8757

88-
{/* Mobile: Sheet drawer */}
89-
<Sheet
90-
open={isOpen}
91-
onOpenChange={(open) => {
92-
if (!open) onClose?.();
93-
}}
94-
>
95-
<SheetContent
96-
side="left"
97-
className="w-64 gap-0 bg-sidebar p-2 text-sidebar-foreground sm:max-w-64"
98-
>
99-
<SheetTitle className="sr-only">Navigation</SheetTitle>
100-
<div className="flex items-center justify-center py-3">
101-
<Image
102-
src="/file-manager-logo.svg"
103-
alt="Solid File Manager"
104-
width={48}
105-
height={48}
106-
className="h-12 w-12"
107-
/>
108-
</div>
109-
<SidebarNav {...navProps} />
110-
</SheetContent>
111-
</Sheet>
112-
</>
58+
<SidebarContent className="p-2">
59+
<NewMenuButton
60+
currentContainerUrl={currentContainerUrl || null}
61+
onNewFolderClick={onNewFolderClick}
62+
onFileUploadClick={onFileUploadClick}
63+
onFolderUploadClick={onFolderUploadClick}
64+
/>
65+
66+
<SidebarGroup>
67+
<SidebarGroupLabel>My Storages</SidebarGroupLabel>
68+
<SidebarGroupContent>
69+
{storages && onFolderNavigate ? (
70+
<FolderTree
71+
storages={storages}
72+
currentFolderUrl={currentContainerUrl}
73+
onNavigate={handleNavigate}
74+
/>
75+
) : null}
76+
</SidebarGroupContent>
77+
</SidebarGroup>
78+
</SidebarContent>
79+
80+
<SidebarFooter className="border-t border-sidebar-border">
81+
<GitHubLinks layout="vertical" />
82+
</SidebarFooter>
83+
</SidebarPrimitive>
11384
);
11485
}

0 commit comments

Comments
 (0)