Skip to content

Commit c372b7c

Browse files
esmcelroyCopilot
andcommitted
feat: add dark mode and before/after preview toggle
Phase 2 features: - Dark mode with light/dark/system toggle in header - Tailwind v4 class-based dark mode via @custom-variant - Theme preference persisted to localStorage - Before/after preview on processed photos (eye icon toggle) - Original/Padded label overlay on photo cards Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b330957 commit c372b7c

6 files changed

Lines changed: 148 additions & 63 deletions

File tree

src/App.tsx

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { PhotoUpload } from './components/PhotoUpload';
44
import { PaddingSettingsPanel } from './components/PaddingSettingsPanel';
55
import { PhotoGrid } from './components/PhotoGrid';
66
import { useLocalStorage } from './hooks/useLocalStorage';
7+
import { useDarkMode, type Theme } from './hooks/useDarkMode';
78
import type { UploadedPhoto, PaddingSettings } from './types';
89
import { ASPECT_RATIO_PRESETS } from './types';
910
import { getImageDimensions, findMaxAspectRatio, padImageToAspectRatio } from './lib/imageUtils';
1011
import { processFilesForHeic } from './lib/heicUtils';
11-
import { Download, Trash2, Layers } from 'lucide-react';
12+
import { Download, Trash2, Layers, Sun, Moon, Monitor } from 'lucide-react';
1213

1314
const DEFAULT_SETTINGS: PaddingSettings = {
1415
fillType: 'color',
@@ -43,6 +44,13 @@ export default function App() {
4344
const [isProcessing, setIsProcessing] = useState(false);
4445
const [isProcessed, setIsProcessed] = useState(false);
4546
const [progress, setProgress] = useState(0);
47+
const [theme, setTheme] = useDarkMode();
48+
49+
const THEME_OPTIONS: { value: Theme; icon: typeof Sun; label: string }[] = [
50+
{ value: 'light', icon: Sun, label: 'Light' },
51+
{ value: 'dark', icon: Moon, label: 'Dark' },
52+
{ value: 'system', icon: Monitor, label: 'System' },
53+
];
4654

4755
const maxAspectRatio = findMaxAspectRatio(photos);
4856

@@ -127,16 +135,33 @@ export default function App() {
127135
};
128136

129137
return (
130-
<div className="min-h-screen bg-gray-50">
138+
<div className="min-h-screen bg-gray-50 dark:bg-gray-950 transition-colors">
131139
{/* Header */}
132-
<header className="bg-white border-b border-gray-200 px-4 py-4">
140+
<header className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-4 py-4">
133141
<div className="max-w-6xl mx-auto flex items-center gap-3">
134142
<div className="bg-indigo-600 text-white rounded-xl p-2">
135143
<Layers className="w-5 h-5" />
136144
</div>
137-
<div>
138-
<h1 className="text-lg font-bold text-gray-900 leading-tight">Squarify</h1>
139-
<p className="text-xs text-gray-500">Pad photos to a uniform aspect ratio</p>
145+
<div className="flex-1">
146+
<h1 className="text-lg font-bold text-gray-900 dark:text-gray-100 leading-tight">Squarify</h1>
147+
<p className="text-xs text-gray-500 dark:text-gray-400">Pad photos to a uniform aspect ratio</p>
148+
</div>
149+
{/* Theme toggle */}
150+
<div className="flex items-center bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5">
151+
{THEME_OPTIONS.map(({ value, icon: Icon, label }) => (
152+
<button
153+
key={value}
154+
onClick={() => setTheme(value)}
155+
title={label}
156+
className={`p-1.5 rounded-md transition-colors ${
157+
theme === value
158+
? 'bg-white dark:bg-gray-700 text-indigo-600 dark:text-indigo-400 shadow-sm'
159+
: 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'
160+
}`}
161+
>
162+
<Icon className="w-4 h-4" />
163+
</button>
164+
))}
140165
</div>
141166
</div>
142167
</header>
@@ -149,7 +174,7 @@ export default function App() {
149174

150175
{/* Stats bar */}
151176
{photos.length > 0 && (
152-
<div className="flex items-center justify-between text-sm text-gray-600 bg-white border border-gray-200 rounded-lg px-4 py-2">
177+
<div className="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-2">
153178
<span>{photos.length} photo{photos.length !== 1 ? 's' : ''} · Target: <span className="font-mono font-medium">{settings.aspectRatio === 'auto' ? `Auto (${maxAspectRatio.toFixed(3)})` : settings.aspectRatio}</span>{settings.borderPadding > 0 && ` · Border: ${settings.borderPadding}px`}</span>
154179
<button onClick={handleClearAll} className="flex items-center gap-1 text-xs text-red-500 hover:text-red-700 transition-colors">
155180
<Trash2 className="w-3 h-3" /> Clear all
@@ -170,11 +195,11 @@ export default function App() {
170195
{/* Progress */}
171196
{isProcessing && (
172197
<div className="space-y-2">
173-
<div className="flex justify-between text-xs text-gray-600">
198+
<div className="flex justify-between text-xs text-gray-600 dark:text-gray-400">
174199
<span>Processing images…</span>
175200
<span>{progress}%</span>
176201
</div>
177-
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
202+
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
178203
<div
179204
className="h-full bg-indigo-500 rounded-full transition-all duration-200"
180205
style={{ width: `${progress}%` }}

0 commit comments

Comments
 (0)