diff --git a/app/components/FontCard.tsx b/app/components/FontCard.tsx index fe3955c..2f92fff 100644 --- a/app/components/FontCard.tsx +++ b/app/components/FontCard.tsx @@ -10,13 +10,26 @@ interface FontCardProps { previewText?: string; } +const FORMAT_CONFIG: Record = { + WOFF2: { label: 'WOFF2', color: 'text-emerald-700', bg: 'bg-emerald-50', dot: 'bg-emerald-400' }, + WOFF: { label: 'WOFF', color: 'text-blue-700', bg: 'bg-blue-50', dot: 'bg-blue-400' }, + TRUETYPE: { label: 'TTF', color: 'text-violet-700', bg: 'bg-violet-50', dot: 'bg-violet-400' }, + TTF: { label: 'TTF', color: 'text-violet-700', bg: 'bg-violet-50', dot: 'bg-violet-400' }, + OPENTYPE: { label: 'OTF', color: 'text-amber-700', bg: 'bg-amber-50', dot: 'bg-amber-400' }, + OTF: { label: 'OTF', color: 'text-amber-700', bg: 'bg-amber-50', dot: 'bg-amber-400' }, + EOT: { label: 'EOT', color: 'text-red-700', bg: 'bg-red-50', dot: 'bg-red-400' }, +}; + +function getFormatConfig(format: string) { + return FORMAT_CONFIG[format.toUpperCase()] || { label: format, color: 'text-zinc-600', bg: 'bg-zinc-100', dot: 'bg-zinc-400' }; +} + export default function FontCard({ font, index, previewText }: FontCardProps) { const [fontLoaded, setFontLoaded] = useState(false); const [showAlternatives, setShowAlternatives] = useState(false); const [alternatives, setAlternatives] = useState([]); const [loadingAlternatives, setLoadingAlternatives] = useState(false); - // Only proxy external URLs, keep data URLs as is const isDataUrl = font.url.startsWith('data:'); const displayUrl = isDataUrl ? font.url @@ -39,7 +52,6 @@ export default function FontCard({ font, index, previewText }: FontCardProps) { `; document.head.appendChild(style); - // Faster timeout for data URLs as they don't need network fetch setTimeout(() => setFontLoaded(true), isDataUrl ? 100 : 800); return () => { @@ -80,182 +92,186 @@ export default function FontCard({ font, index, previewText }: FontCardProps) { } }; - const formatBadgeStyle = (format: string): string => { - const styles: Record = { - 'WOFF2': 'bg-emerald-50 text-emerald-600', - 'WOFF': 'bg-blue-50 text-blue-600', - 'TRUETYPE': 'bg-violet-50 text-violet-600', - 'TTF': 'bg-violet-50 text-violet-600', - 'OPENTYPE': 'bg-amber-50 text-amber-600', - 'OTF': 'bg-amber-50 text-amber-600', - 'EOT': 'bg-red-50 text-red-600', - }; - return styles[format.toUpperCase()] || 'bg-gray-100 text-gray-600'; - }; - - + const fmt = getFormatConfig(font.format); return ( -
+ {/* Top accent line */} +
+ +
{/* Header */} -
+
-

+

{font.family}

-

+

{font.name}

- - {font.format} + + + {fmt.label}
{/* Font Preview */} -
+
{fontLoaded ? (

- {previewText || 'The quick brown fox jumps over the lazy dog'} + {previewText || 'The quick brown fox'}

) : ( -
- +
+ - Loading preview… + Loading preview…
)}
- {/* Meta Info */} -
- - - + {/* Meta row */} +
+ + + - {font.weight || '400'} + {font.weight || '400'} - - - - + + {font.style && font.style.toLowerCase() !== 'normal' ? ( + + + + ) : ( + + + + )} {font.style || 'Normal'}
- {/* Find Alternatives Button */} 0 && showAlternatives + ? 'border-zinc-200 bg-zinc-50 text-zinc-600 hover:bg-zinc-100' + : 'border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 hover:border-zinc-300' } - `} + `} > {loadingAlternatives ? ( - - + <> + Finding alternatives… - + ) : ( - - - + <> + + - {alternatives.length > 0 ? (showAlternatives ? 'Hide' : 'Show') + ' Free Alternatives' : 'Find Free Alternatives'} - + {alternatives.length > 0 + ? (showAlternatives ? 'Hide Alternatives' : 'Show Free Alternatives') + : 'Find Free Alternatives'} + {alternatives.length > 0 && ( + {alternatives.length} + )} + )} - {/* Alternatives Display */} + {/* Alternatives */} {showAlternatives && alternatives.length > 0 && ( -

- - - - Free Alternatives -

-
- {alternatives.map((alt, i) => ( - -
-
-
-

+

+

+ Free Alternatives +

+
+ {alternatives.map((alt, i) => ( + +
+
+ {alt.family.charAt(0)} +
+
+

{alt.family}

- {alt.similarity != null && alt.similarity > 0 && ( - = 80 ? 'bg-green-50 text-green-600' : - alt.similarity >= 60 ? 'bg-yellow-50 text-yellow-600' : - 'bg-gray-100 text-gray-500' - }`}> - {alt.similarity}% - + {(alt.reason || alt.category) && ( +

+ {alt.reason || alt.category} +

)}
-

- {alt.reason || alt.category} -

- - - -
- - ))} +
+ {alt.similarity != null && alt.similarity > 0 && ( + = 80 ? 'bg-emerald-50 text-emerald-600' : + alt.similarity >= 60 ? 'bg-amber-50 text-amber-600' : + 'bg-zinc-100 text-zinc-500' + }`}> + {alt.similarity}% + + )} + + + +
+ + ))} +
+

+ Free to use commercially via Google Fonts +

-

- These Google Fonts are free to use commercially and personally -

)} diff --git a/app/components/HeroSection.tsx b/app/components/HeroSection.tsx index e4cba29..6b381bd 100644 --- a/app/components/HeroSection.tsx +++ b/app/components/HeroSection.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import { motion, AnimatePresence } from 'motion/react'; +import { motion, AnimatePresence } from 'motion/react'; const fonts = [ { name: 'Heading', family: 'CustomHeading, sans-serif' }, @@ -29,65 +29,73 @@ export default function HeroSection() { const currentFont = fonts[currentFontIndex]; return ( -
+
- {/* Eyebrow */} - - Font Discovery Tool - +
+ + + Font Discovery Tool + +
+ - {/* Main Heading - Clickable to switch fonts */} + {/* Main Heading - clickable to switch fonts */} {/* Subtitle */} Enter any website URL to instantly discover every font it uses. Preview them live and find free alternatives. diff --git a/app/components/SearchInput.tsx b/app/components/SearchInput.tsx index 0f52ba8..5c2bd27 100644 --- a/app/components/SearchInput.tsx +++ b/app/components/SearchInput.tsx @@ -24,38 +24,46 @@ export default function SearchInput({ onSearch, loading }: SearchInputProps) { onSearch(targetUrl); }; + const examples = [ + { label: 'stripe.com', url: 'stripe.com' }, + { label: 'linear.app', url: 'linear.app' }, + { label: 'vercel.com', url: 'vercel.com' }, + ]; + return (
+ {/* Search bar */}
- {/* Search Icon */} -
+ {/* Globe icon */} +
@@ -68,78 +76,73 @@ export default function SearchInput({ onSearch, loading }: SearchInputProps) { onChange={(e) => setUrl(e.target.value)} onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} - placeholder="Enter website URL" - className=" - flex-1 - py-4 px-2 - text-base text-gray-900 - placeholder:text-gray-400 - bg-transparent - outline-none - min-w-0 - " - style={{ fontSize: '16px' }} /* Prevent iOS zoom */ + placeholder="Enter any website URL…" + className="flex-1 py-3.5 px-2 text-[15px] text-zinc-900 placeholder:text-zinc-400 bg-transparent outline-none min-w-0" + style={{ fontSize: '16px' }} /> - {/* Submit Button */} -
+ {/* Divider + Submit */} +
+ {url.trim() && ( + + )} {loading ? ( - - - - + <> + + + Scanning - + ) : ( - 'Extract' + <> + Extract + + + + )}
- {/* Helper text */} -

- Try , , or -

+ {/* Examples */} +
+ Try + {examples.map((ex, i) => ( + + + {i < examples.length - 1 && ·} + + ))} +
); diff --git a/app/globals.css b/app/globals.css index 898bbdd..b66ffe8 100644 --- a/app/globals.css +++ b/app/globals.css @@ -19,21 +19,22 @@ } :root { - --background: #ffffff; - --foreground: #0a0a0a; - - /* Gray scale */ - --gray-50: #fafafa; - --gray-100: #f5f5f5; - --gray-200: #e5e5e5; - --gray-300: #d4d4d4; - --gray-400: #a3a3a3; - --gray-500: #737373; - --gray-600: #525252; - --gray-700: #404040; - --gray-800: #262626; - --gray-900: #171717; - --gray-950: #0a0a0a; + --background: #fafafa; + --surface: #ffffff; + --foreground: #09090b; + + /* Zinc scale */ + --zinc-50: #fafafa; + --zinc-100: #f4f4f5; + --zinc-200: #e4e4e7; + --zinc-300: #d4d4d8; + --zinc-400: #a1a1aa; + --zinc-500: #71717a; + --zinc-600: #52525b; + --zinc-700: #3f3f46; + --zinc-800: #27272a; + --zinc-900: #18181b; + --zinc-950: #09090b; } @theme inline { @@ -43,6 +44,10 @@ --font-mono: var(--font-geist-mono); } +html { + background: var(--background); +} + body { background: var(--background); color: var(--foreground); @@ -74,13 +79,13 @@ h6 { /* Selection */ ::selection { background: rgba(0, 0, 0, 0.08); - color: var(--gray-900); + color: var(--zinc-900); } /* Scrollbar - only for specific containers */ .custom-scrollbar::-webkit-scrollbar { - width: 6px; - height: 6px; + width: 4px; + height: 4px; } .custom-scrollbar::-webkit-scrollbar-track { @@ -88,12 +93,12 @@ h6 { } .custom-scrollbar::-webkit-scrollbar-thumb { - background: var(--gray-300); - border-radius: 3px; + background: var(--zinc-300); + border-radius: 2px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { - background: var(--gray-400); + background: var(--zinc-400); } /* Reduced motion */ @@ -126,4 +131,33 @@ only screen and (min-resolution: 192dpi) { .hairline-border { border-width: 0.5px; } -} \ No newline at end of file +} + +/* Grid background */ +.bg-grid { + background-color: var(--background); + background-image: + linear-gradient(to right, rgba(0, 0, 0, 0.04) 1px, transparent 1px), + linear-gradient(to bottom, rgba(0, 0, 0, 0.04) 1px, transparent 1px); + background-size: 40px 40px; +} + +/* Format accent colors */ +.format-woff2 { --accent: #10b981; } +.format-woff { --accent: #3b82f6; } +.format-ttf { --accent: #8b5cf6; } +.format-otf { --accent: #f59e0b; } +.format-eot { --accent: #ef4444; } +.format-other { --accent: #71717a; } + +/* Shimmer loading effect */ +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.shimmer { + background: linear-gradient(90deg, #f4f4f5 25%, #e4e4e7 50%, #f4f4f5 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} diff --git a/app/page.tsx b/app/page.tsx index 9215eb6..c12d0c7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -33,7 +33,6 @@ export default function Home() { throw new Error(data.error || 'Failed to extract fonts'); } - // Sort fonts: normal style first, then regular weight (400) preferred const sortedFonts = [...data.fonts].sort((a: FontInfo, b: FontInfo) => { const aIsItalic = a.style?.toLowerCase().includes('italic') ? 1 : 0; const bIsItalic = b.style?.toLowerCase().includes('italic') ? 1 : 0; @@ -60,81 +59,107 @@ export default function Home() { }; return ( -
- {/* Background Pattern */} -
-
-
+
+ {/* Top fade mask over grid */} +
+ + {/* Top navigation bar */} +
+
+
+
+ + + +
+ Font Analyzer +
+
+ + WOFF · WOFF2 · TTF · OTF + +
+
+
{/* Content */}
- {/* Hero Section */} + {/* Hero */} - {/* Search Input */} -
+ {/* Search */} +
- {/* Results Section */} -
- {/* Error Message */} + {/* Results */} +
+ {/* Error */} {error && ( {error} )} - {/* Results Header */} + {/* Results header + preview input */} {fonts.length > 0 && ( -

- Found {fonts.length} {fonts.length === 1 ? 'font' : 'fonts'} -

+
+
+ + {fonts.length} {fonts.length === 1 ? 'font' : 'fonts'} found + + + sorted alphabetically +
+
+ + {/* Custom preview text */} +
+
+ + + +
+ setPreviewText(e.target.value)} + placeholder="Custom preview text…" + className="w-full pl-9 pr-10 py-2.5 bg-white border border-zinc-200 rounded-xl text-sm text-zinc-900 placeholder:text-zinc-400 focus:outline-none focus:border-zinc-400 transition-all duration-150 shadow-sm" + /> + {previewText && ( + + )} +
)}
- {/* Custom Preview Text */} - {fonts.length > 0 && ( -
-
- setPreviewText(e.target.value)} - placeholder="Type custom preview text…" - className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent transition-all duration-150" - /> - {previewText && ( - - )} -
-
- )} - {/* Font Grid */} {fonts.length > 0 && ( @@ -149,53 +174,62 @@ export default function Home() { )} - {/* Empty State */} + {/* Empty state */} {searched && !loading && fonts.length === 0 && !error && ( -
- +
+
-

No fonts found on this website

+

No fonts found on this website

+

Try a different URL

)}
{/* Footer */} -