feat(ui): make desktop sidebar collapsible

Add a header toggle and a panel close button so the desktop file
sidebar collapses with a width transition, mirroring the journal
panel.

- split onClose (close button) from onNavigate (file selection) so
  clicking a file no longer collapses the sidebar on desktop
- give the aside h-full so the file tree and outline keep bounded
  scroll regions inside the new wrapper
- swap the aside shadow for a gray right border to match the journal
This commit is contained in:
2026-09-13 14:09:18 +08:00
parent 49bed7d279
commit 12f8d8542c
2 changed files with 45 additions and 21 deletions
+27 -2
View File
@@ -11,6 +11,8 @@ import {
TbOutlineMenu2, TbOutlineMenu2,
TbOutlineClipboardList, TbOutlineClipboardList,
TbOutlineHelpCircle, TbOutlineHelpCircle,
TbOutlineLayoutSidebarLeftCollapse,
TbOutlineLayoutSidebarLeftExpand,
} from "solid-icons/tb"; } from "solid-icons/tb";
import { useJournalStream } from "./components/stores/journalStream"; import { useJournalStream } from "./components/stores/journalStream";
@@ -51,6 +53,7 @@ const App: Component = () => {
const location = useLocation(); const location = useLocation();
const stream = useJournalStream(); const stream = useJournalStream();
const [isSidebarOpen, setIsSidebarOpen] = createSignal(false); const [isSidebarOpen, setIsSidebarOpen] = createSignal(false);
const [isDesktopSidebarOpen, setIsDesktopSidebarOpen] = createSignal(true);
const [isDocOpen, setIsDocOpen] = createSignal(false); const [isDocOpen, setIsDocOpen] = createSignal(false);
const [isDataSourceOpen, setIsDataSourceOpen] = createSignal(false); const [isDataSourceOpen, setIsDataSourceOpen] = createSignal(false);
const [isJournalOpen, setIsJournalOpen] = createSignal(false); const [isJournalOpen, setIsJournalOpen] = createSignal(false);
@@ -98,7 +101,7 @@ const App: Component = () => {
{/* Header */} {/* Header */}
<header class="shrink-0 h-16 bg-white shadow z-30"> <header class="shrink-0 h-16 bg-white shadow z-30">
<div class="h-full max-w-4xl mx-auto px-4 flex items-center gap-4"> <div class="h-full max-w-4xl mx-auto px-4 flex items-center gap-4">
{/* 仅在移动端显示菜单按钮 */} {/* 移动端:打开抽屉式侧边栏 */}
<button <button
onClick={() => setIsSidebarOpen(true)} onClick={() => setIsSidebarOpen(true)}
class="md:hidden text-gray-600 hover:text-gray-900 p-1 rounded hover:bg-gray-100" class="md:hidden text-gray-600 hover:text-gray-900 p-1 rounded hover:bg-gray-100"
@@ -106,6 +109,18 @@ const App: Component = () => {
> >
<TbOutlineMenu2 class="inline-block align-[-0.125em]" /> <TbOutlineMenu2 class="inline-block align-[-0.125em]" />
</button> </button>
{/* 桌面端:折叠/展开固定侧边栏 */}
<button
onClick={() => setIsDesktopSidebarOpen((v) => !v)}
class="hidden md:block text-gray-600 hover:text-gray-900 p-1 rounded hover:bg-gray-100"
title={isDesktopSidebarOpen() ? "收起目录" : "展开目录"}
>
{isDesktopSidebarOpen() ? (
<TbOutlineLayoutSidebarLeftCollapse class="inline-block align-[-0.125em]" />
) : (
<TbOutlineLayoutSidebarLeftExpand class="inline-block align-[-0.125em]" />
)}
</button>
<h1 class="text-2xl font-bold text-gray-900">Tabletop Toolkit</h1> <h1 class="text-2xl font-bold text-gray-900">Tabletop Toolkit</h1>
<div class="flex-1" /> <div class="flex-1" />
<button <button
@@ -150,12 +165,22 @@ const App: Component = () => {
{/* Body: sidebar + main + journal */} {/* Body: sidebar + main + journal */}
<div class="flex flex-1 min-h-0"> <div class="flex flex-1 min-h-0">
{/* 桌面端固定侧边栏 (flex child) */} {/* 桌面端侧边栏 — flex child,可折叠 */}
<div
class="hidden md:block md:shrink-0 md:overflow-hidden md:transition-all md:duration-300"
classList={{
"md:w-64": isDesktopSidebarOpen(),
"md:w-0": !isDesktopSidebarOpen(),
}}
inert={!isDesktopSidebarOpen()}
>
<DesktopSidebar <DesktopSidebar
fileTree={fileTree()} fileTree={fileTree()}
pathHeadings={pathHeadings()} pathHeadings={pathHeadings()}
onDataSourceOpen={() => setIsDataSourceOpen(true)} onDataSourceOpen={() => setIsDataSourceOpen(true)}
onClose={() => setIsDesktopSidebarOpen(false)}
/> />
</div>
{/* Main content — the scrollable region */} {/* Main content — the scrollable region */}
<ScrollContainerCtx.Provider value={() => mainRef}> <ScrollContainerCtx.Provider value={() => mainRef}>
+6 -7
View File
@@ -18,7 +18,7 @@ interface SidebarContentProps {
pathHeadings: Record<string, TocNode[]>; pathHeadings: Record<string, TocNode[]>;
currentPath: string; currentPath: string;
onClose: () => void; onClose: () => void;
isDesktop?: boolean; onNavigate?: () => void;
onDataSourceOpen?: () => void; onDataSourceOpen?: () => void;
} }
@@ -60,7 +60,6 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
> >
<TbOutlineFolderOpen class="inline-block align-[-0.125em]" /> <TbOutlineFolderOpen class="inline-block align-[-0.125em]" />
</button> </button>
<Show when={!props.isDesktop}>
<button <button
onClick={props.onClose} onClick={props.onClose}
class="text-gray-500 hover:text-gray-700" class="text-gray-500 hover:text-gray-700"
@@ -68,7 +67,6 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
> >
<TbOutlineX class="inline-block align-[-0.125em]" /> <TbOutlineX class="inline-block align-[-0.125em]" />
</button> </button>
</Show>
</div> </div>
</div> </div>
</div> </div>
@@ -84,7 +82,7 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
currentPath={props.currentPath} currentPath={props.currentPath}
pathHeadings={props.pathHeadings} pathHeadings={props.pathHeadings}
depth={0} depth={0}
onClose={props.onClose} onClose={() => props.onNavigate?.()}
isHidden={isFileHidden} isHidden={isFileHidden}
/> />
))} ))}
@@ -151,6 +149,7 @@ export const MobileSidebar: Component<SidebarProps> = (props) => {
pathHeadings={pathHeadings()} pathHeadings={pathHeadings()}
currentPath={location.pathname} currentPath={location.pathname}
onClose={props.onClose} onClose={props.onClose}
onNavigate={props.onClose}
onDataSourceOpen={props.onDataSourceOpen} onDataSourceOpen={props.onDataSourceOpen}
/> />
</aside> </aside>
@@ -164,6 +163,7 @@ export const MobileSidebar: Component<SidebarProps> = (props) => {
export const DesktopSidebar: Component<{ export const DesktopSidebar: Component<{
fileTree?: FileNode[]; fileTree?: FileNode[];
pathHeadings?: Record<string, TocNode[]>; pathHeadings?: Record<string, TocNode[]>;
onClose: () => void;
onDataSourceOpen?: () => void; onDataSourceOpen?: () => void;
}> = (props) => { }> = (props) => {
const location = useLocation(); const location = useLocation();
@@ -183,13 +183,12 @@ export const DesktopSidebar: Component<{
}); });
return ( return (
<aside class="hidden md:flex flex-col w-64 shrink-0 bg-white shadow-lg overflow-hidden"> <aside class="hidden md:flex flex-col h-full w-64 shrink-0 bg-white border-r border-gray-200 overflow-hidden">
<SidebarContent <SidebarContent
fileTree={fileTree()} fileTree={fileTree()}
pathHeadings={pathHeadings()} pathHeadings={pathHeadings()}
currentPath={location.pathname} currentPath={location.pathname}
onClose={() => {}} onClose={props.onClose}
isDesktop
onDataSourceOpen={props.onDataSourceOpen} onDataSourceOpen={props.onDataSourceOpen}
/> />
</aside> </aside>