From 38bd77d03c8b1e92581bbc87068f0af690b4455c Mon Sep 17 00:00:00 2001 From: Phillip Carter Date: Thu, 23 Jul 2026 15:47:26 -0700 Subject: [PATCH] Big issue address --- src/app/(inbox)/email/[id]/page.tsx | 2 +- src/app/(inbox)/thread/[threadId]/page.tsx | 4 +- src/app/api/download/route.ts | 5 +- src/app/globals.css | 149 +++--- src/app/layout.tsx | 47 +- src/components/AttachmentList.tsx | 8 +- src/components/Composer.tsx | 42 +- src/components/EmailBody.tsx | 27 +- src/components/EmailDetailView.tsx | 6 +- src/components/EmailListPanel.tsx | 12 +- src/components/MobileBackButton.tsx | 8 +- src/components/MobileNav.tsx | 12 +- src/components/SenderAvatar.tsx | 10 +- src/components/ThreadView.tsx | 7 +- src/lib/__tests__/compose.test.ts | 8 +- src/lib/__tests__/composeHtml.test.ts | 55 +- src/lib/__tests__/emailHtml.test.ts | 583 +++++++-------------- src/lib/__tests__/jmap.test.ts | 1 + src/lib/compose.ts | 2 +- src/lib/composeHtml.ts | 245 ++++----- src/lib/emailHtml.ts | 435 +++++++-------- src/lib/emailRenderTheme.ts | 66 +-- src/lib/jmap.ts | 10 +- src/lib/senderAvatar.ts | 27 +- src/lib/types.ts | 3 + 25 files changed, 761 insertions(+), 1013 deletions(-) diff --git a/src/app/(inbox)/email/[id]/page.tsx b/src/app/(inbox)/email/[id]/page.tsx index 132c5c2..a64b381 100644 --- a/src/app/(inbox)/email/[id]/page.tsx +++ b/src/app/(inbox)/email/[id]/page.tsx @@ -24,7 +24,7 @@ export default async function EmailPage({ params, searchParams }: Props) { return (
-
+
-
+
-
+
diff --git a/src/app/api/download/route.ts b/src/app/api/download/route.ts index 18ea715..88ec303 100644 --- a/src/app/api/download/route.ts +++ b/src/app/api/download/route.ts @@ -79,7 +79,10 @@ export async function GET(req: NextRequest) { const headers: Record = { "Content-Type": contentType, "Content-Disposition": formatContentDisposition(disposition, name), - "Cross-Origin-Resource-Policy": "same-origin", + // Sandboxed srcdoc iframes have an opaque origin. Inline images must be + // embeddable there even though the authenticated route is same-site. + "Cross-Origin-Resource-Policy": + disposition === "inline" ? "cross-origin" : "same-origin", "X-Content-Type-Options": "nosniff", }; const contentLength = res.headers.get("Content-Length"); diff --git a/src/app/globals.css b/src/app/globals.css index b31216d..ec732a3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,114 +1,89 @@ @import "tailwindcss"; @theme inline { - /* Share Tech Mono — clean terminal font (not pixel, just monospace) */ - --font-sans: var(--font-share-tech-mono); - --font-mono: var(--font-share-tech-mono); + --font-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace; - /* "Dark terminal with green accents" palette - Primary text = off-white phosphor (near-white with green tint) - UI chrome = green (nav, buttons, indicators, labels) - Background = near-black + /* A quiet neutral scale. Existing stone-* utilities intentionally map here + so the visual refresh does not require noisy class churn throughout the app. */ + --color-stone-50: #f8fafc; + --color-stone-100: #f1f5f9; + --color-stone-200: #e2e8f0; + --color-stone-300: #cbd5e1; + --color-stone-400: #94a3b8; + --color-stone-500: #64748b; + --color-stone-600: #475569; + --color-stone-700: #334155; + --color-stone-800: #1e293b; + --color-stone-900: #0f172a; + --color-stone-950: #020617; - Dark mode text tiers (contrast vs #060e06 bg): - stone-100 = #e0ece0 off-white phosphor (~15:1) — unread senders, body text - stone-200 = #a8c8a8 light gray-green (~11:1) — unread subjects, key values - stone-300 = #80b880 medium-bright green (~9:1) — nav chrome labels - stone-400 = #7a8a7a neutral gray-green (~6:1) — read senders/subjects, muted - stone-500 = #607060 neutral grey-green (~4:1) — preview text, timestamps - Background tiers: - stone-800 = #0a1c0a hover / selection surface - stone-900 = #060e06 main dark bg - stone-950 = #030704 sidebar bg - Note: stone-400/500 are intentionally more neutral (less saturated) than - stone-300, so content text reads as "dimmer" while staying readable. */ - --color-stone-50: #f0f5f0; - --color-stone-100: #e0ece0; - --color-stone-200: #a8c8a8; - --color-stone-300: #80b880; - --color-stone-400: #7a8a7a; - --color-stone-500: #607060; - --color-stone-600: #1e3e1e; - --color-stone-700: #122a12; - --color-stone-800: #0a1c0a; - --color-stone-900: #060e06; - --color-stone-950: #030704; + --color-blue-50: #eff6ff; + --color-blue-100: #dbeafe; + --color-blue-200: #bfdbfe; + --color-blue-300: #93c5fd; + --color-blue-400: #60a5fa; + --color-blue-500: #3b82f6; + --color-blue-600: #2563eb; + --color-blue-700: #1d4ed8; + --color-blue-800: #1e40af; + --color-blue-900: #1e3a8a; + --color-blue-950: #172554; - /* Phosphor green — accent scale (blue → vivid terminal green) - These are the "active" / "interactive" greens: selected rows, button borders, - unread dots, focused outlines. Vivid so they stand out against off-white text. */ - --color-blue-50: #f0fff4; - --color-blue-100: #dcfce7; - --color-blue-200: #bbf7d0; - --color-blue-300: #4ade80; - --color-blue-400: #22c55e; - --color-blue-500: #16a34a; - --color-blue-600: #15803d; - --color-blue-700: #166534; - --color-blue-800: #14532d; - --color-blue-900: #052e16; - --color-blue-950: #022912; - - /* Sharp, blocky corners — no roundness */ - --radius-sm: 0px; - --radius: 0px; - --radius-md: 0px; - --radius-lg: 0px; - --radius-xl: 0px; - --radius-2xl: 0px; - --radius-3xl: 0px; + --radius-sm: 0.25rem; + --radius: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; + --radius-2xl: 1.25rem; + --radius-3xl: 1.5rem; } :root { color-scheme: light dark; + font-synthesis: none; + text-rendering: optimizeLegibility; } -/* Subtle scanlines — barely visible, just a hint */ -body::after { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - z-index: 9999; - background: repeating-linear-gradient( - 0deg, - transparent, - transparent 3px, - rgba(0, 0, 0, 0.03) 3px, - rgba(0, 0, 0, 0.03) 4px - ); +html { + overscroll-behavior: none; } -body.rich-content-open::after { - display: none; +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -/* Dark mode: subtle phosphor glow on body text */ -@media (prefers-color-scheme: dark) { - body { - text-shadow: 0 0 8px rgba(180, 220, 180, 0.12); - } - input, textarea, select, button { - text-shadow: none; - } +button, +a, +input, +textarea, +select { + -webkit-tap-highlight-color: transparent; +} + +input, +textarea { + caret-color: #2563eb; } -/* Green terminal caret */ -input, textarea { - caret-color: #22c55e; +:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; } -/* Focused inputs: vivid green accent outline */ -@media (prefers-color-scheme: dark) { - input:focus, textarea:focus { - outline: 1px solid #22c55e !important; - outline-offset: 0 !important; - box-shadow: 0 0 0 1px #22c55e, 0 0 6px rgba(34, 197, 94, 0.25) !important; +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; } } @media print { html, body { height: auto !important; overflow: visible !important; } body > div, main { height: auto !important; overflow: visible !important; } - body::after { display: none; } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e7d270b..076a737 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,4 @@ import type { Metadata } from "next"; -import { Share_Tech_Mono } from "next/font/google"; import Link from "next/link"; import { auth, signOut } from "@/auth"; import LiveUnreadCountBadge from "@/components/LiveUnreadCountBadge"; @@ -9,13 +8,6 @@ import UnreadCountProvider from "@/components/UnreadCountProvider"; import { getAccountId, getMailboxes, getSession as getJmapSession } from "@/lib/jmap"; import "./globals.css"; -const shareTechMono = Share_Tech_Mono({ - variable: "--font-share-tech-mono", - weight: "400", - subsets: ["latin"], - display: "swap", -}); - export const metadata: Metadata = { title: "Mail", description: "Personal email client", @@ -44,62 +36,59 @@ export default async function RootLayout({ } return ( - + {/* Row: desktop sidebar + main content */}
{/* Sidebar — desktop only */} -