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 ;
+}