From 4927fb9a933bf835902527bcf87624c50bafc522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sun, 1 Mar 2026 08:30:56 +0100 Subject: [PATCH] refactor: align favicon tool with Calculate/Media blueprint - FaviconGenerator: lg:grid-cols-5 layout (2/5 setup, 3/5 results); glass panels, native inputs, custom tab switcher (Icons/HTML/Manifest), native progress bar, empty state placeholder, mobile Setup|Results tabs - FaviconFileUpload: remove shadcn Button; match media FileUpload styling with file card, metadata chips (size, dimensions) - CodeSnippet: remove shadcn Button; dark terminal (#06060e) with hover copy button, consistent with ASCII/ExportMenu code blocks Co-Authored-By: Claude Sonnet 4.6 --- components/favicon/CodeSnippet.tsx | 25 +- components/favicon/FaviconFileUpload.tsx | 132 +++---- components/favicon/FaviconGenerator.tsx | 452 ++++++++++++----------- 3 files changed, 300 insertions(+), 309 deletions(-) diff --git a/components/favicon/CodeSnippet.tsx b/components/favicon/CodeSnippet.tsx index 484eab6..e084577 100644 --- a/components/favicon/CodeSnippet.tsx +++ b/components/favicon/CodeSnippet.tsx @@ -2,15 +2,13 @@ import * as React from 'react'; import { Copy, Check } from 'lucide-react'; -import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; interface CodeSnippetProps { code: string; - language?: string; } -export function CodeSnippet({ code, language }: CodeSnippetProps) { +export function CodeSnippet({ code }: CodeSnippetProps) { const [copied, setCopied] = React.useState(false); const handleCopy = () => { @@ -21,18 +19,15 @@ export function CodeSnippet({ code, language }: CodeSnippetProps) { }; return ( -
-
- -
-
+    
+ +
         {code}
       
diff --git a/components/favicon/FaviconFileUpload.tsx b/components/favicon/FaviconFileUpload.tsx index c558f5b..f7d2cb8 100644 --- a/components/favicon/FaviconFileUpload.tsx +++ b/components/favicon/FaviconFileUpload.tsx @@ -1,9 +1,8 @@ 'use client'; import * as React from 'react'; -import { Upload, X, FileImage, HardDrive } from 'lucide-react'; +import { Upload, X, FileImage, HardDrive, Film } from 'lucide-react'; import { cn } from '@/lib/utils/cn'; -import { Button } from '@/components/ui/button'; export interface FaviconFileUploadProps { onFileSelect: (file: File) => void; @@ -26,7 +25,7 @@ export function FaviconFileUpload({ if (selectedFile) { const img = new Image(); img.onload = () => { - setDimensions(`${img.width} × ${img.height}`); + setDimensions(`${img.width}×${img.height}`); URL.revokeObjectURL(img.src); }; img.src = URL.createObjectURL(selectedFile); @@ -35,49 +34,22 @@ export function FaviconFileUpload({ } }, [selectedFile]); - const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (!disabled) setIsDragging(true); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - }; - const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); - if (disabled) return; - const files = Array.from(e.dataTransfer.files); - if (files.length > 0 && files[0].type.startsWith('image/')) { - onFileSelect(files[0]); - } + if (files.length > 0 && files[0].type.startsWith('image/')) onFileSelect(files[0]); }; const handleFileInput = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); - if (files.length > 0 && files[0].type.startsWith('image/')) { - onFileSelect(files[0]); - } - }; - - const handleClick = () => { - if (!disabled) fileInputRef.current?.click(); + if (files.length > 0 && files[0].type.startsWith('image/')) onFileSelect(files[0]); }; return ( -
+
{selectedFile ? ( -
-
-
- +
+
+ +
+
+
+

+ {selectedFile.name} +

+
-
-
-

- {selectedFile.name} -

- -
-
-
- - {(selectedFile.size / 1024).toFixed(1)} KB -
- {dimensions && ( -
- - {dimensions} -
- )} -
+
+ + + {selectedFile.size < 1024 * 1024 + ? `${(selectedFile.size / 1024).toFixed(1)} KB` + : `${(selectedFile.size / (1024 * 1024)).toFixed(1)} MB`} + + {dimensions && ( + + {dimensions} + + )}
) : (
!disabled && fileInputRef.current?.click()} + onDragEnter={(e) => { e.preventDefault(); if (!disabled) setIsDragging(true); }} + onDragLeave={(e) => { e.preventDefault(); setIsDragging(false); }} + onDragOver={(e) => e.preventDefault()} onDrop={handleDrop} className={cn( - 'border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all duration-200', - 'hover:border-primary/40 hover:bg-primary/5', - { - 'border-primary bg-primary/10 scale-[0.98]': isDragging, - 'border-border/50': !isDragging, - 'opacity-50 cursor-not-allowed': disabled, - } + 'flex flex-col items-center justify-center rounded-xl border-2 border-dashed transition-all cursor-pointer text-center select-none py-8', + isDragging + ? 'border-primary bg-primary/10 scale-[0.99]' + : 'border-border/35 hover:border-primary/40 hover:bg-primary/5', + disabled && 'opacity-50 cursor-not-allowed pointer-events-none' )} > -
- +
+
-

- Drop icon source here +

+ {isDragging ? 'Drop to upload' : 'Drop icon here or click to browse'}

-

- 512x512 PNG or SVG recommended +

+ PNG · SVG · 512×512 recommended

)} diff --git a/components/favicon/FaviconGenerator.tsx b/components/favicon/FaviconGenerator.tsx index 96e40df..1d3d06f 100644 --- a/components/favicon/FaviconGenerator.tsx +++ b/components/favicon/FaviconGenerator.tsx @@ -1,24 +1,34 @@ 'use client'; import * as React from 'react'; -import { Download, Loader2, Code2, Globe, Layout } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Progress } from '@/components/ui/progress'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Download, Loader2, Code2, Globe, Layout, FileImage } from 'lucide-react'; import { FaviconFileUpload } from './FaviconFileUpload'; import { CodeSnippet } from './CodeSnippet'; import { generateFaviconSet } from '@/lib/favicon/faviconService'; import { downloadBlobsAsZip } from '@/lib/media/utils/fileUtils'; import type { FaviconSet, FaviconOptions } from '@/types/favicon'; import { toast } from 'sonner'; +import { cn } from '@/lib/utils/cn'; + +type Tab = 'icons' | 'html' | 'manifest'; +type MobileTab = 'setup' | 'results'; + +const TABS: { value: Tab; label: string; icon: React.ReactNode }[] = [ + { value: 'icons', label: 'Icons', icon: }, + { value: 'html', label: 'HTML', icon: }, + { value: 'manifest', label: 'Manifest', icon: }, +]; + +const actionBtn = + 'flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; + +const inputCls = + 'w-full bg-transparent border border-border/40 rounded-lg px-3 py-2 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 placeholder:text-muted-foreground/30'; export function FaviconGenerator() { const [sourceFile, setSourceFile] = React.useState(null); const [options, setOptions] = React.useState({ - name: 'My Awesome App', + name: 'My App', shortName: 'App', backgroundColor: '#ffffff', themeColor: '#3b82f6', @@ -26,22 +36,18 @@ export function FaviconGenerator() { const [isGenerating, setIsGenerating] = React.useState(false); const [progress, setProgress] = React.useState(0); const [result, setResult] = React.useState(null); + const [tab, setTab] = React.useState('icons'); + const [mobileTab, setMobileTab] = React.useState('setup'); const handleGenerate = async () => { - if (!sourceFile) { - toast.error('Please upload a source image'); - return; - } - + if (!sourceFile) { toast.error('Please upload a source image'); return; } setIsGenerating(true); setProgress(0); - try { - const resultSet = await generateFaviconSet(sourceFile, options, (p) => { - setProgress(p); - }); + const resultSet = await generateFaviconSet(sourceFile, options, (p) => setProgress(p)); setResult(resultSet); - toast.success('Favicon set generated successfully!'); + setMobileTab('results'); + toast.success('Favicon set generated!'); } catch (error) { console.error(error); toast.error('Failed to generate favicons'); @@ -52,229 +58,249 @@ export function FaviconGenerator() { const handleDownloadAll = async () => { if (!result) return; - - const files = result.icons.map((icon) => ({ - blob: icon.blob!, - filename: icon.name, - })); - - // Add manifest to ZIP + const files = result.icons.map((icon) => ({ blob: icon.blob!, filename: icon.name })); const manifestBlob = new Blob([result.manifest], { type: 'application/json' }); - files.push({ - blob: manifestBlob, - filename: 'site.webmanifest', - }); - + files.push({ blob: manifestBlob, filename: 'site.webmanifest' }); await downloadBlobsAsZip(files, 'favicons.zip'); - toast.success('Downloading favicons ZIP...'); + toast.success('Downloading favicons ZIP…'); }; const handleReset = () => { setSourceFile(null); setResult(null); setProgress(0); + setMobileTab('setup'); }; return ( -
- {/* Settings Column */} -
- - - App Details - - -
- - setOptions({ ...options, name: e.target.value })} - placeholder="e.g. My Awesome Website" - /> -
-
- - setOptions({ ...options, shortName: e.target.value })} - placeholder="e.g. My App" - /> -

Used for mobile home screen labels

-
-
-
+
- - - Theme Colors - - -
-
- -
- setOptions({ ...options, backgroundColor: e.target.value })} - /> - setOptions({ ...options, backgroundColor: e.target.value })} - /> -
-
-
- -
- setOptions({ ...options, themeColor: e.target.value })} - /> - setOptions({ ...options, themeColor: e.target.value })} - /> -
-
-
-
-
+ {/* ── Mobile tab switcher ─────────────────────────────── */} +
+ {(['setup', 'results'] as MobileTab[]).map((t) => ( + + ))} +
- - + {/* ── Main layout ─────────────────────────────────────── */} +
+ + {/* Left: Setup */} +
+ + {/* Upload zone */} +
+ + Source Image + setSourceFile(null)} disabled={isGenerating} /> - + )} + +
+
+ + {/* Right: Results */} +
+
+ + {/* Tab bar + download button */} +
+
+ {TABS.map(({ value, label, icon }) => ( + + ))} +
+ {result && ( + + )} +
+ + {/* Scrollable content */} +
{isGenerating ? ( +
+ +
+
+ Processing… + {progress}% +
+
+
+
+
+
+ ) : result ? ( <> - - Generating... {progress}% + {tab === 'icons' && ( +
+ {result.icons.map((icon) => ( +
+
+ {icon.previewUrl ? ( + {icon.name} + ) : ( + + )} +
+
+

{icon.name}

+

{icon.width}×{icon.height} · {(icon.size / 1024).toFixed(1)} KB

+
+
+ ))} +
+ )} + {tab === 'html' && ( +
+ +
+

+ Place generated files in your site root or update the href paths. +

+
+
+ )} + {tab === 'manifest' && ( + + )} ) : ( - 'Generate Favicons' +
+
+ +
+
+

No assets yet

+

Upload an image and generate favicons

+
+
)} - - {result && ( - - )} - - -
- - {/* Results Column */} -
- {isGenerating ? ( - - -
-
-
- Processing... -
- {progress}% -
-
-
- ) : result ? ( -
-
-

Generated Assets

- -
- - - - - - Icons - - - - HTML - - - - Manifest - - - - -
- {result?.icons.map((icon) => ( - -
-
- {icon.previewUrl && ( - {icon.name} - )} -
-
-

- {icon.name} -

-

- {icon.width}x{icon.height} · {(icon.size / 1024).toFixed(1)} KB -

-
-
-
- ))} -
-
- - -
- - {result && } -
-
-

- Place generated files in your site root or update the href paths. -

-
-
- - -
- - {result && } -
-
-
- ) : null} +
);