feat: add Stirling-PDF-style layout with sidebar, tool panel, dropzone, and theme toggle

This commit is contained in:
Siddharth Kumar Sah
2026-03-22 03:01:23 +08:00
parent 541e49576c
commit e920021da1
9 changed files with 345 additions and 1 deletions
@@ -0,0 +1,30 @@
import { useState } from "react";
import { Sidebar } from "./sidebar";
import { ToolPanel } from "./tool-panel";
import { Footer } from "./footer";
import { Dropzone } from "../common/dropzone";
interface AppLayoutProps {
children?: React.ReactNode;
showToolPanel?: boolean;
}
export function AppLayout({ children, showToolPanel = true }: AppLayoutProps) {
const [_settingsOpen, setSettingsOpen] = useState(false);
return (
<div className="flex h-screen bg-background text-foreground overflow-hidden">
<Sidebar onSettingsClick={() => setSettingsOpen(true)} />
{showToolPanel && <ToolPanel />}
<main className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto p-6 flex items-center justify-center">
{children || <Dropzone />}
</div>
<div className="text-center text-xs text-muted-foreground py-2 border-t border-border">
Privacy Policy
</div>
</main>
<Footer />
</div>
);
}
+29
View File
@@ -0,0 +1,29 @@
import { Moon, Sun, Globe } from "lucide-react";
import { useTheme } from "@/hooks/use-theme";
export function Footer() {
const { resolvedTheme, toggleTheme } = useTheme();
return (
<div className="fixed bottom-4 right-4 flex items-center gap-2 z-50">
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-card border border-border hover:bg-muted transition-colors"
title="Toggle Theme"
>
{resolvedTheme === "dark" ? (
<Sun className="h-4 w-4" />
) : (
<Moon className="h-4 w-4" />
)}
</button>
<button
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-card border border-border hover:bg-muted transition-colors text-sm"
title="Language"
>
<Globe className="h-4 w-4" />
English
</button>
</div>
);
}
@@ -0,0 +1,80 @@
import { Link, useLocation } from "react-router-dom";
import { cn } from "@/lib/utils";
import {
LayoutGrid,
BookOpen,
Workflow,
FolderOpen,
HelpCircle,
Settings,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
interface SidebarItem {
icon: LucideIcon;
label: string;
href?: string;
}
const topItems: SidebarItem[] = [
{ icon: LayoutGrid, label: "Tools", href: "/" },
{ icon: BookOpen, label: "Reader", href: "/reader" },
{ icon: Workflow, label: "Automate", href: "/automate" },
{ icon: FolderOpen, label: "Files", href: "/files" },
];
const bottomItems: SidebarItem[] = [
{ icon: HelpCircle, label: "Help", href: "/help" },
{ icon: Settings, label: "Settings" },
];
interface SidebarProps {
onSettingsClick: () => void;
}
export function Sidebar({ onSettingsClick }: SidebarProps) {
const location = useLocation();
const renderItem = (item: SidebarItem, isActive: boolean) => {
const content = (
<div
className={cn(
"flex flex-col items-center gap-1 p-2 rounded-lg cursor-pointer transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<item.icon className="h-6 w-6" />
<span className="text-[10px] font-medium">{item.label}</span>
</div>
);
if (item.label === "Settings") {
return (
<button key={item.label} onClick={onSettingsClick} className="w-full">
{content}
</button>
);
}
return (
<Link key={item.label} to={item.href || "/"}>
{content}
</Link>
);
};
return (
<aside className="flex flex-col items-center w-16 bg-sidebar border-r border-border py-3 gap-1 shrink-0">
<div className="flex flex-col gap-1 flex-1">
{topItems.map((item) =>
renderItem(item, location.pathname === item.href)
)}
</div>
<div className="border-t border-border w-10 my-2" />
<div className="flex flex-col gap-1">
{bottomItems.map((item) => renderItem(item, false))}
</div>
</aside>
);
}
@@ -0,0 +1,57 @@
import { useState, useMemo } from "react";
import { TOOLS, CATEGORIES } from "@stirling-image/shared";
import { SearchBar } from "../common/search-bar";
import { ToolCard } from "../common/tool-card";
export function ToolPanel() {
const [search, setSearch] = useState("");
const filteredTools = useMemo(() => {
if (!search) return TOOLS;
const q = search.toLowerCase();
return TOOLS.filter(
(t) =>
t.name.toLowerCase().includes(q) ||
t.description.toLowerCase().includes(q)
);
}, [search]);
const groupedTools = useMemo(() => {
const groups = new Map<string, typeof TOOLS>();
for (const tool of filteredTools) {
const list = groups.get(tool.category) || [];
list.push(tool);
groups.set(tool.category, list);
}
return groups;
}, [filteredTools]);
return (
<div className="w-72 border-r border-border bg-background overflow-y-auto flex flex-col shrink-0">
<div className="p-3 sticky top-0 bg-background z-10">
<SearchBar value={search} onChange={setSearch} />
</div>
<div className="px-3 pb-4 flex-1">
{CATEGORIES.filter((cat) => groupedTools.has(cat.id)).map(
(category) => (
<div key={category.id} className="mb-4">
<h3 className="text-xs font-semibold uppercase text-muted-foreground tracking-wider mb-2">
{category.name}
</h3>
<div className="space-y-0.5">
{groupedTools.get(category.id)!.map((tool) => (
<ToolCard key={tool.id} tool={tool} />
))}
</div>
</div>
)
)}
{filteredTools.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-8">
No tools found
</p>
)}
</div>
</div>
);
}