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,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 (
<div
onDragEnter={handleDrag}
onDragOver={handleDrag}
onDragLeave={handleDrag}
onDrop={handleDrop}
onClick={handleClick}
className={cn(
"flex flex-col items-center justify-center rounded-2xl border-2 border-dashed transition-colors cursor-pointer min-h-[400px] mx-auto max-w-2xl w-full",
isDragging
? "border-primary bg-primary/5"
: "border-border bg-muted/30 hover:border-primary/50 hover:bg-muted/50"
)}
>
<div className="flex flex-col items-center gap-4 p-8">
<div className="text-3xl font-bold text-muted-foreground/30">
Stirling <span className="text-primary/30">Image</span>
</div>
<button className="flex items-center gap-2 px-6 py-2.5 rounded-lg border border-primary text-primary hover:bg-primary/5 transition-colors text-sm font-medium">
<Upload className="h-4 w-4" />
Upload from computer
</button>
<p className="text-sm text-muted-foreground">
Drop files here or click the upload button
</p>
</div>
</div>
);
}
@@ -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 (
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
type="text"
value={value}
onChange={(e) => 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"
/>
</div>
);
}
@@ -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 (
<div className="group flex items-center gap-3 relative">
<button
className="opacity-0 group-hover:opacity-100 transition-opacity absolute -left-5"
title="Add to favourites"
>
<Star className="h-3 w-3 text-muted-foreground hover:text-yellow-500" />
</button>
<Link
to={tool.route}
className={cn(
"flex items-center gap-3 py-2 px-3 rounded-lg w-full transition-colors",
"hover:bg-muted",
tool.disabled && "opacity-50 pointer-events-none"
)}
>
<IconComponent className="h-5 w-5 text-muted-foreground" />
<span className="text-sm font-medium text-foreground">{tool.name}</span>
{tool.alpha && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-orange-100 text-orange-600 font-medium">
Alpha
</span>
)}
</Link>
</div>
);
}