From e920021da1cc6e5fdb4e479205fe0861567621c7 Mon Sep 17 00:00:00 2001 From: Siddharth Kumar Sah Date: Sun, 22 Mar 2026 03:01:23 +0800 Subject: [PATCH] feat: add Stirling-PDF-style layout with sidebar, tool panel, dropzone, and theme toggle --- apps/web/src/App.tsx | 3 +- apps/web/src/components/common/dropzone.tsx | 72 +++++++++++++++++ apps/web/src/components/common/search-bar.tsx | 26 ++++++ apps/web/src/components/common/tool-card.tsx | 44 ++++++++++ apps/web/src/components/layout/app-layout.tsx | 30 +++++++ apps/web/src/components/layout/footer.tsx | 29 +++++++ apps/web/src/components/layout/sidebar.tsx | 80 +++++++++++++++++++ apps/web/src/components/layout/tool-panel.tsx | 57 +++++++++++++ apps/web/src/pages/home-page.tsx | 5 ++ 9 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/common/dropzone.tsx create mode 100644 apps/web/src/components/common/search-bar.tsx create mode 100644 apps/web/src/components/common/tool-card.tsx create mode 100644 apps/web/src/components/layout/app-layout.tsx create mode 100644 apps/web/src/components/layout/footer.tsx create mode 100644 apps/web/src/components/layout/sidebar.tsx create mode 100644 apps/web/src/components/layout/tool-panel.tsx create mode 100644 apps/web/src/pages/home-page.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index e372bbea..bcec26e3 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,10 +1,11 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { HomePage } from "./pages/home-page"; export function App() { return ( - Stirling Image} /> + } /> ); diff --git a/apps/web/src/components/common/dropzone.tsx b/apps/web/src/components/common/dropzone.tsx new file mode 100644 index 00000000..61eca6cf --- /dev/null +++ b/apps/web/src/components/common/dropzone.tsx @@ -0,0 +1,72 @@ +import { useCallback, useState, type DragEvent } from "react"; +import { Upload } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface DropzoneProps { + onFiles?: (files: File[]) => void; + accept?: string; + multiple?: boolean; +} + +export function Dropzone({ onFiles, accept, multiple = true }: DropzoneProps) { + const [isDragging, setIsDragging] = useState(false); + + const handleDrag = useCallback((e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === "dragenter" || e.type === "dragover") setIsDragging(true); + else if (e.type === "dragleave") setIsDragging(false); + }, []); + + const handleDrop = useCallback( + (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + const files = Array.from(e.dataTransfer.files); + if (files.length > 0) onFiles?.(files); + }, + [onFiles] + ); + + const handleClick = () => { + const input = document.createElement("input"); + input.type = "file"; + input.multiple = multiple; + if (accept) input.accept = accept; + input.onchange = (e) => { + const files = Array.from((e.target as HTMLInputElement).files || []); + if (files.length > 0) onFiles?.(files); + }; + input.click(); + }; + + return ( +
+
+
+ Stirling Image +
+ +

+ Drop files here or click the upload button +

+
+
+ ); +} diff --git a/apps/web/src/components/common/search-bar.tsx b/apps/web/src/components/common/search-bar.tsx new file mode 100644 index 00000000..57668cdc --- /dev/null +++ b/apps/web/src/components/common/search-bar.tsx @@ -0,0 +1,26 @@ +import { Search } from "lucide-react"; + +interface SearchBarProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; +} + +export function SearchBar({ + value, + onChange, + placeholder = "Search tools...", +}: SearchBarProps) { + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + className="w-full pl-10 pr-4 py-2 rounded-lg border border-border bg-background text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20" + /> +
+ ); +} diff --git a/apps/web/src/components/common/tool-card.tsx b/apps/web/src/components/common/tool-card.tsx new file mode 100644 index 00000000..e349b57e --- /dev/null +++ b/apps/web/src/components/common/tool-card.tsx @@ -0,0 +1,44 @@ +import { Link } from "react-router-dom"; +import { Star, FileImage } from "lucide-react"; +import * as icons from "lucide-react"; +import type { Tool } from "@stirling-image/shared"; +import { cn } from "@/lib/utils"; + +interface ToolCardProps { + tool: Tool; +} + +export function ToolCard({ tool }: ToolCardProps) { + const iconsMap = icons as unknown as Record< + string, + React.ComponentType<{ className?: string }> + >; + const IconComponent = iconsMap[tool.icon] || FileImage; + + return ( +
+ + + + {tool.name} + {tool.alpha && ( + + Alpha + + )} + +
+ ); +} diff --git a/apps/web/src/components/layout/app-layout.tsx b/apps/web/src/components/layout/app-layout.tsx new file mode 100644 index 00000000..57740a94 --- /dev/null +++ b/apps/web/src/components/layout/app-layout.tsx @@ -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 ( +
+ setSettingsOpen(true)} /> + {showToolPanel && } +
+
+ {children || } +
+
+ Privacy Policy +
+
+
+
+ ); +} diff --git a/apps/web/src/components/layout/footer.tsx b/apps/web/src/components/layout/footer.tsx new file mode 100644 index 00000000..448c80b3 --- /dev/null +++ b/apps/web/src/components/layout/footer.tsx @@ -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 ( +
+ + +
+ ); +} diff --git a/apps/web/src/components/layout/sidebar.tsx b/apps/web/src/components/layout/sidebar.tsx new file mode 100644 index 00000000..6e0a4bac --- /dev/null +++ b/apps/web/src/components/layout/sidebar.tsx @@ -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 = ( +
+ + {item.label} +
+ ); + + if (item.label === "Settings") { + return ( + + ); + } + return ( + + {content} + + ); + }; + + return ( + + ); +} diff --git a/apps/web/src/components/layout/tool-panel.tsx b/apps/web/src/components/layout/tool-panel.tsx new file mode 100644 index 00000000..d4ed061c --- /dev/null +++ b/apps/web/src/components/layout/tool-panel.tsx @@ -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(); + for (const tool of filteredTools) { + const list = groups.get(tool.category) || []; + list.push(tool); + groups.set(tool.category, list); + } + return groups; + }, [filteredTools]); + + return ( +
+
+ +
+
+ {CATEGORIES.filter((cat) => groupedTools.has(cat.id)).map( + (category) => ( +
+

+ {category.name} +

+
+ {groupedTools.get(category.id)!.map((tool) => ( + + ))} +
+
+ ) + )} + {filteredTools.length === 0 && ( +

+ No tools found +

+ )} +
+
+ ); +} diff --git a/apps/web/src/pages/home-page.tsx b/apps/web/src/pages/home-page.tsx new file mode 100644 index 00000000..ba9c2eb1 --- /dev/null +++ b/apps/web/src/pages/home-page.tsx @@ -0,0 +1,5 @@ +import { AppLayout } from "@/components/layout/app-layout"; + +export function HomePage() { + return ; +}