diff --git a/app/file-browser.tsx b/app/file-browser.tsx index d0292e6..6dbf040 100644 --- a/app/file-browser.tsx +++ b/app/file-browser.tsx @@ -40,7 +40,7 @@ function Thumbnail({ fileKey }: { fileKey: string }) { Document thumbnail ); @@ -51,30 +51,28 @@ function FileCard({ file, onClick }: { file: FileItem; onClick: () => void }) { return ( ); @@ -83,7 +81,7 @@ function FileCard({ file, onClick }: { file: FileItem; onClick: () => void }) { // Get celebrities for a specific file and page function getCelebritiesForPage(filePath: string, pageNumber: number): { name: string; confidence: number }[] { const celebrities: { name: string; confidence: number }[] = []; - + for (const celebrity of CELEBRITY_DATA) { for (const appearance of celebrity.appearances) { if (appearance.file === filePath && appearance.page === pageNumber) { @@ -94,14 +92,14 @@ function getCelebritiesForPage(filePath: string, pageNumber: number): { name: st } } } - + return celebrities.sort((a, b) => b.confidence - a.confidence).filter(celeb => celeb.confidence > 99); } // Prefetch a PDF in the background async function prefetchPdf(filePath: string): Promise { if (getPdfPages(filePath)) return; - + try { const fileUrl = `${WORKER_URL}/${filePath}`; const pdfjsLib = await import("pdfjs-dist"); @@ -140,17 +138,17 @@ async function prefetchPdf(filePath: string): Promise { } // Modal component for viewing files -function FileModal({ - file, - onClose, - onPrev, +function FileModal({ + file, + onClose, + onPrev, onNext, hasPrev, hasNext, queryString, nextFile -}: { - file: FileItem; +}: { + file: FileItem; onClose: () => void; onPrev: () => void; onNext: () => void; @@ -162,7 +160,7 @@ function FileModal({ const [pages, setPages] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - + const filePath = file.key; const fileId = getFileId(filePath); const fileUrl = `${WORKER_URL}/${filePath}`; @@ -193,7 +191,7 @@ function FileModal({ // Load PDF useEffect(() => { const cached = getPdfPages(filePath); - + if (cached && cached.length > 0) { setPages(cached); setLoading(false); @@ -262,7 +260,7 @@ function FileModal({ cancelled = true; }; }, [fileUrl, filePath]); - + // Prefetch next PDF useEffect(() => { if (!loading && nextFile) { @@ -273,11 +271,11 @@ function FileModal({ return (
{/* Backdrop */} -
- + {/* Modal content */}
{/* Header */} @@ -293,14 +291,14 @@ function FileModal({ -

{fileId}

+

{fileId}

{/* Actions */}
@@ -310,7 +308,7 @@ function FileModal({ @@ -329,8 +327,8 @@ function FileModal({
-

Error loading PDF

-

{error}

+

Error loading PDF

+

{error}

)} @@ -342,7 +340,7 @@ function FileModal({
{pages.length > 1 && ( -
+
Page {index + 1}
)} @@ -362,16 +360,16 @@ function FileModal({
-

Detected in this image:

+

Detected in this image

{pageCelebrities.map((celeb, idx) => ( {celeb.name} - ({Math.round(celeb.confidence)}%) + {Math.round(celeb.confidence)}% ))}
@@ -384,12 +382,12 @@ function FileModal({
{loading && ( -
+
-
-
+
+
-

Loading PDF...

+

Loading document...

)}
@@ -399,14 +397,14 @@ function FileModal({ {hasPrev ? ( ) : ( -
- +
+ Prev
)} @@ -414,15 +412,15 @@ function FileModal({ {hasNext ? ( ) : ( -
+
Next - +
)}
@@ -471,30 +469,30 @@ export function FileBrowser() { const str = params.toString(); return str ? `?${str}` : ""; }, [collectionFilter, celebrityFilter]); - + // Modal state - find index from file key const selectedFileIndex = useMemo(() => { if (!openFile) return null; const index = filteredFiles.findIndex(f => f.key === openFile); return index >= 0 ? index : null; }, [openFile, filteredFiles]); - + const selectedFile = selectedFileIndex !== null ? filteredFiles[selectedFileIndex] : null; const hasPrev = selectedFileIndex !== null && selectedFileIndex > 0; const hasNext = selectedFileIndex !== null && selectedFileIndex < filteredFiles.length - 1; - + const handlePrev = useCallback(() => { if (selectedFileIndex !== null && selectedFileIndex > 0) { setOpenFile(filteredFiles[selectedFileIndex - 1].key); } }, [selectedFileIndex, filteredFiles, setOpenFile]); - + const handleNext = useCallback(() => { if (selectedFileIndex !== null && selectedFileIndex < filteredFiles.length - 1) { setOpenFile(filteredFiles[selectedFileIndex + 1].key); } }, [selectedFileIndex, filteredFiles, setOpenFile]); - + const handleClose = useCallback(() => { setOpenFile(null); }, [setOpenFile]); @@ -505,13 +503,9 @@ export function FileBrowser() {
-
-
-

- Epstein Files Browser -

-
-
+

+ Epstein Files Browser +

setCollectionFilter(e.target.value)} - className="appearance-none px-4 py-2.5 pr-10 bg-secondary border border-border rounded-xl text-foreground text-sm font-medium focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all cursor-pointer hover:bg-accent" + className="appearance-none px-4 py-2.5 pr-10 bg-secondary border border-border rounded-xl text-body text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all cursor-pointer hover:bg-accent" > @@ -559,13 +553,15 @@ export function FileBrowser() { onValueChange={(value) => setCelebrityFilter(value)} /> -
- - {filteredFiles.length.toLocaleString()} files - {collectionFilter !== "All" || celebrityFilter !== "All" - ? / {initialFiles.length.toLocaleString()} - : ""} - +
+ {filteredFiles.length.toLocaleString()} + {collectionFilter !== "All" || celebrityFilter !== "All" ? ( + <> + / + {initialFiles.length.toLocaleString()} + + ) : null} + documents
@@ -574,20 +570,17 @@ export function FileBrowser() { {/* Celebrity Detection Disclaimer */} {celebrityFilter !== "All" && (
- )} @@ -603,13 +596,13 @@ export function FileBrowser() { {/* Empty state */} {filteredFiles.length === 0 && (
-
- - +
+ +
-

No files found

-

Try adjusting your filters to find what you're looking for.

+

No documents found

+

Try adjusting your filters to find what you're looking for.

)} diff --git a/app/file/[...path]/page.tsx b/app/file/[...path]/page.tsx index ada2c30..1236135 100644 --- a/app/file/[...path]/page.tsx +++ b/app/file/[...path]/page.tsx @@ -90,23 +90,23 @@ function getCelebritiesForPage(filePath: string, pageNumber: number): { name: st } // Component to display a page with its celebrity info -function PageWithCelebrities({ - dataUrl, - pageNumber, - filePath -}: { - dataUrl: string; - pageNumber: number; +function PageWithCelebrities({ + dataUrl, + pageNumber, + filePath +}: { + dataUrl: string; + pageNumber: number; filePath: string; }) { const celebrities = useMemo(() => getCelebritiesForPage(filePath, pageNumber), [filePath, pageNumber]); - + return ( -
+
{/* Page number badge */} -
- Page {pageNumber} +
+ {pageNumber}
{/* eslint-disable-next-line @next/next/no-img-element */}
{celebrities.length > 0 && ( -
-
-
- - - -
-

Detected in this image:

-
-
- {celebrities - .map((celeb, idx) => ( +
+

Detected:

+
+ {celebrities.map((celeb, idx) => ( {celeb.name} - ({Math.round(celeb.confidence)}%) + {Math.round(celeb.confidence)}% ))}
@@ -328,13 +320,13 @@ export default function FilePage({ return (
{/* Header */} -
-
-
+
+
+
-

{fileId}

+

{fileId}

{totalPages > 0 && ( -
-
-
+
+
- + {pages.length}/{totalPages}
@@ -375,7 +367,7 @@ export default function FilePage({ @@ -398,7 +390,7 @@ export default function FilePage({ Next @@ -411,7 +403,7 @@ export default function FilePage({ @@ -420,7 +412,7 @@ export default function FilePage({ @@ -445,18 +437,18 @@ export default function FilePage({ {/* PDF Pages */}
{error && ( -
- - +
+ + -
+

Error loading PDF

-

{error}

+

{error}

)} -
+
{pages.map((dataUrl, index) => ( +
-
-
+
+
-

+

{pages.length > 0 ? `Rendering page ${pages.length + 1} of ${totalPages}` - : "Loading PDF..."} + : "Loading document..."}

)}
{/* Navigation bar */} -
+
{prevPath ? ( - + Prev ) : ( -
- +
+ Prev
)} -
+
{nextPath ? ( Next - + ) : ( -
+
Next - +
)}
diff --git a/app/globals.css b/app/globals.css index 96fc3a5..aecbfd9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -6,8 +6,9 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -37,82 +38,108 @@ --color-popover: var(--popover); --color-card-foreground: var(--card-foreground); --color-card: var(--card); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); + --radius-sm: calc(var(--radius) - 2px); + --radius-md: var(--radius); + --radius-lg: calc(var(--radius) + 2px); --radius-xl: calc(var(--radius) + 4px); - --radius-2xl: calc(var(--radius) + 8px); - --radius-3xl: calc(var(--radius) + 12px); - --radius-4xl: calc(var(--radius) + 16px); + --radius-2xl: calc(var(--radius) + 6px); } +/* + * Design System: Archival Document Browser + * A refined, professional aesthetic for legal document viewing + */ + :root { - --radius: 0.625rem; - --background: oklch(0.09 0.005 260); - --foreground: oklch(0.95 0 0); - --card: oklch(0.13 0.005 260); - --card-foreground: oklch(0.95 0 0); - --popover: oklch(0.13 0.005 260); - --popover-foreground: oklch(0.95 0 0); - --primary: oklch(0.65 0.2 250); - --primary-foreground: oklch(0.98 0 0); - --secondary: oklch(0.18 0.005 260); - --secondary-foreground: oklch(0.95 0 0); - --muted: oklch(0.18 0.005 260); - --muted-foreground: oklch(0.65 0 0); - --accent: oklch(0.22 0.01 260); - --accent-foreground: oklch(0.95 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.25 0.01 260); - --input: oklch(0.20 0.01 260); - --ring: oklch(0.65 0.2 250); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.11 0.005 260); - --sidebar-foreground: oklch(0.95 0 0); - --sidebar-primary: oklch(0.65 0.2 250); - --sidebar-primary-foreground: oklch(0.98 0 0); - --sidebar-accent: oklch(0.18 0.005 260); - --sidebar-accent-foreground: oklch(0.95 0 0); - --sidebar-border: oklch(0.25 0.01 260); - --sidebar-ring: oklch(0.65 0.2 250); + /* Tighter, more refined border radius */ + --radius: 0.375rem; + + /* Neutral slate palette - warmer and more archival */ + --background: oklch(0.10 0.004 250); + --foreground: oklch(0.93 0.005 250); + + /* Cards: subtle lift from background */ + --card: oklch(0.14 0.006 250); + --card-foreground: oklch(0.93 0.005 250); + + /* Popover matches card */ + --popover: oklch(0.14 0.006 250); + --popover-foreground: oklch(0.93 0.005 250); + + /* Primary: Warm amber accent - archival gold */ + --primary: oklch(0.75 0.14 75); + --primary-foreground: oklch(0.15 0.02 75); + + /* Secondary: Subtle elevated surface */ + --secondary: oklch(0.18 0.006 250); + --secondary-foreground: oklch(0.93 0.005 250); + + /* Muted text and surfaces */ + --muted: oklch(0.18 0.006 250); + --muted-foreground: oklch(0.55 0.01 250); + + /* Accent for hover states */ + --accent: oklch(0.20 0.008 250); + --accent-foreground: oklch(0.93 0.005 250); + + /* Destructive */ + --destructive: oklch(0.55 0.22 25); + + /* Borders: very subtle, refined */ + --border: oklch(0.22 0.008 250); + --input: oklch(0.18 0.008 250); + --ring: oklch(0.75 0.14 75); + + /* Chart colors */ + --chart-1: oklch(0.75 0.14 75); + --chart-2: oklch(0.6 0.12 185); + --chart-3: oklch(0.55 0.10 250); + --chart-4: oklch(0.70 0.15 150); + --chart-5: oklch(0.65 0.18 30); + + /* Sidebar */ + --sidebar: oklch(0.12 0.005 250); + --sidebar-foreground: oklch(0.93 0.005 250); + --sidebar-primary: oklch(0.75 0.14 75); + --sidebar-primary-foreground: oklch(0.15 0.02 75); + --sidebar-accent: oklch(0.18 0.006 250); + --sidebar-accent-foreground: oklch(0.93 0.005 250); + --sidebar-border: oklch(0.22 0.008 250); + --sidebar-ring: oklch(0.75 0.14 75); } .dark { - --background: oklch(0.09 0.005 260); - --foreground: oklch(0.95 0 0); - --card: oklch(0.13 0.005 260); - --card-foreground: oklch(0.95 0 0); - --popover: oklch(0.13 0.005 260); - --popover-foreground: oklch(0.95 0 0); - --primary: oklch(0.65 0.2 250); - --primary-foreground: oklch(0.98 0 0); - --secondary: oklch(0.18 0.005 260); - --secondary-foreground: oklch(0.95 0 0); - --muted: oklch(0.18 0.005 260); - --muted-foreground: oklch(0.65 0 0); - --accent: oklch(0.22 0.01 260); - --accent-foreground: oklch(0.95 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(0.25 0.01 260); - --input: oklch(0.20 0.01 260); - --ring: oklch(0.65 0.2 250); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.11 0.005 260); - --sidebar-foreground: oklch(0.95 0 0); - --sidebar-primary: oklch(0.65 0.2 250); - --sidebar-primary-foreground: oklch(0.98 0 0); - --sidebar-accent: oklch(0.18 0.005 260); - --sidebar-accent-foreground: oklch(0.95 0 0); - --sidebar-border: oklch(0.25 0.01 260); - --sidebar-ring: oklch(0.65 0.2 250); + --background: oklch(0.10 0.004 250); + --foreground: oklch(0.93 0.005 250); + --card: oklch(0.14 0.006 250); + --card-foreground: oklch(0.93 0.005 250); + --popover: oklch(0.14 0.006 250); + --popover-foreground: oklch(0.93 0.005 250); + --primary: oklch(0.75 0.14 75); + --primary-foreground: oklch(0.15 0.02 75); + --secondary: oklch(0.18 0.006 250); + --secondary-foreground: oklch(0.93 0.005 250); + --muted: oklch(0.18 0.006 250); + --muted-foreground: oklch(0.55 0.01 250); + --accent: oklch(0.20 0.008 250); + --accent-foreground: oklch(0.93 0.005 250); + --destructive: oklch(0.60 0.20 25); + --border: oklch(0.22 0.008 250); + --input: oklch(0.18 0.008 250); + --ring: oklch(0.75 0.14 75); + --chart-1: oklch(0.75 0.14 75); + --chart-2: oklch(0.6 0.12 185); + --chart-3: oklch(0.55 0.10 250); + --chart-4: oklch(0.70 0.15 150); + --chart-5: oklch(0.65 0.18 30); + --sidebar: oklch(0.12 0.005 250); + --sidebar-foreground: oklch(0.93 0.005 250); + --sidebar-primary: oklch(0.75 0.14 75); + --sidebar-primary-foreground: oklch(0.15 0.02 75); + --sidebar-accent: oklch(0.18 0.006 250); + --sidebar-accent-foreground: oklch(0.93 0.005 250); + --sidebar-border: oklch(0.22 0.008 250); + --sidebar-ring: oklch(0.75 0.14 75); } @layer base { @@ -124,26 +151,143 @@ } } -/* Custom scrollbar */ +/* Custom scrollbar - refined and subtle */ ::-webkit-scrollbar { - width: 8px; - height: 8px; + width: 6px; + height: 6px; } ::-webkit-scrollbar-track { - background: oklch(0.12 0.005 260); + background: transparent; } ::-webkit-scrollbar-thumb { - background: oklch(0.28 0.01 260); - border-radius: 4px; + background: oklch(0.30 0.008 250); + border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { - background: oklch(0.35 0.01 260); + background: oklch(0.40 0.01 250); } -/* Selection styling */ +/* Selection styling - warm amber tint */ ::selection { - background: oklch(0.65 0.2 250 / 0.3); + background: oklch(0.75 0.14 75 / 0.25); +} + +/* Shimmer animation for loading states */ +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.animate-shimmer { + animation: shimmer 1.5s ease-in-out infinite; +} + +/* + * Typography System + * Based on 1.25 modular scale with optimized letter-spacing and line-heights + * Designed for scannability and clear visual hierarchy + */ + +/* Display/Title - for main page headings */ +.text-title { + font-size: 1.5rem; /* 24px */ + line-height: 1.2; + letter-spacing: -0.025em; + font-weight: 600; +} + +@media (min-width: 640px) { + .text-title { + font-size: 1.875rem; /* 30px */ + } +} + +/* Heading - for section headings and modal titles */ +.text-heading { + font-size: 1.125rem; /* 18px */ + line-height: 1.3; + letter-spacing: -0.02em; + font-weight: 600; +} + +/* Subheading - for card titles, prominent labels */ +.text-subheading { + font-size: 0.9375rem; /* 15px */ + line-height: 1.4; + letter-spacing: -0.01em; + font-weight: 500; +} + +/* Body - standard readable text */ +.text-body { + font-size: 0.875rem; /* 14px */ + line-height: 1.5; + letter-spacing: 0; + font-weight: 400; +} + +/* Caption - for metadata, timestamps, secondary info */ +.text-caption { + font-size: 0.8125rem; /* 13px */ + line-height: 1.4; + letter-spacing: 0.005em; + font-weight: 400; +} + +/* Label - for UI labels, badges, small metadata */ +.text-label { + font-size: 0.75rem; /* 12px */ + line-height: 1.35; + letter-spacing: 0.01em; + font-weight: 500; +} + +/* Micro - for keyboard hints, fine print */ +.text-micro { + font-size: 0.6875rem; /* 11px */ + line-height: 1.3; + letter-spacing: 0.02em; + font-weight: 500; +} + +/* Mono text - for file IDs, code, technical info */ +.text-mono { + font-family: var(--font-mono), ui-monospace, monospace; + font-feature-settings: "tnum" on, "lnum" on; +} + +/* Display font for elegant headings */ +.font-display { + font-family: var(--font-serif), ui-serif, Georgia, serif; + letter-spacing: -0.02em; +} + +/* Tabular numbers for aligned data */ +.tabular-nums { + font-variant-numeric: tabular-nums; + font-feature-settings: "tnum" on; +} + +/* Improve text rendering */ +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +/* Card hover refinement */ +.card-hover { + transition: transform 150ms ease, box-shadow 150ms ease, border-color 150ms ease; +} + +.card-hover:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px -8px oklch(0 0 0 / 0.4); } diff --git a/app/layout.tsx b/app/layout.tsx index 344e090..6f92232 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,21 +1,27 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Inter, JetBrains_Mono, Newsreader } from "next/font/google"; import { NuqsAdapter } from "nuqs/adapters/next/app"; import { Analytics } from "@vercel/analytics/next"; import { FilesProvider } from "@/lib/files-context"; import { FileItem } from "@/lib/cache"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", +const inter = Inter({ + variable: "--font-sans", subsets: ["latin"], }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const jetbrainsMono = JetBrains_Mono({ + variable: "--font-mono", subsets: ["latin"], }); +const newsreader = Newsreader({ + variable: "--font-serif", + subsets: ["latin"], + style: ["normal", "italic"], +}); + export const metadata: Metadata = { title: "Epstein Files Browser", description: "Browse and view the released Epstein files", @@ -51,7 +57,7 @@ export default async function RootLayout({ return ( {children} diff --git a/components/celebrity-combobox.tsx b/components/celebrity-combobox.tsx index c09ba3e..08e3259 100644 --- a/components/celebrity-combobox.tsx +++ b/components/celebrity-combobox.tsx @@ -50,11 +50,11 @@ export function CelebrityCombobox({
{value === "All" ? ( - All People + All People ) : selectedCelebrity ? ( - + {selectedCelebrity.name} - ({selectedCelebrity.count}) + ({selectedCelebrity.count}) ) : ( Select person... @@ -67,10 +67,10 @@ export function CelebrityCombobox({ - + No person found. @@ -80,7 +80,7 @@ export function CelebrityCombobox({ onValueChange("All"); setOpen(false); }} - className="text-foreground hover:bg-accent data-[selected=true]:bg-accent rounded-lg px-3 py-2.5 cursor-pointer" + className="text-sm text-foreground hover:bg-accent data-[selected=true]:bg-accent rounded-lg px-3 py-2.5 cursor-pointer" > - All People + All People {celebrities.map((celebrity) => ( {celebrity.name} - + {celebrity.count} diff --git a/components/celebrity-disclaimer.tsx b/components/celebrity-disclaimer.tsx index 969447f..a95f713 100644 --- a/components/celebrity-disclaimer.tsx +++ b/components/celebrity-disclaimer.tsx @@ -1,6 +1,6 @@ export function CelebrityDisclaimer({ className = "" }: { className?: string }) { return ( -
+

Detection powered by{" "}