Commite1db10eunknown_key
feat(design): Phase 4 — polish (error pages, prose, print)
feat(design): Phase 4 — polish (error pages, prose, print) 404 + 500 pages - Replace bare empty-state shells with branded .error-page layout: oversized gradient-fade numeric code, eyebrow, display title with gradient-text accent, lede, dual-button action row (retry/home), request-id meta in mono, dev-mode trace panel with red border. - 500 page surfaces the request ID so users can quote it in bug reports. - 404 echoes METHOD + path in mono so the user sees what was requested. CSS additions - .error-page family (page, -code, -code-err, -title, -sub, -actions, -meta, -trace) with radial gradient backdrop - .prose a / .markdown-body a animated underline (subtle, scaleX on hover) - @media print: hides chrome, switches to white bg, underlines links Tests: 1226 pass / 1 unrelated pre-existing fail.
2 files changed+147−12e1db10ef763cbc929a60c9b9f2b367607d4becb9
2 changed files+147−12
Modifiedsrc/app.tsx+38−12View fileUnifiedSplit
@@ -325,12 +325,23 @@ app.route("/", webRoutes);
325325app.notFound((c) => {
326326 return c.html(
327327 <Layout title="Not Found">
328 <div class="empty-state">
329 <h2>404</h2>
330 <p>Page not found.</p>
331 <a href="/" style="margin-top: 12px; display: inline-block">
332 Go home
333 </a>
328 <div class="error-page">
329 <div class="error-page-code">404</div>
330 <div class="eyebrow">Not found</div>
331 <h1 class="display error-page-title">
332 That page <span class="gradient-text">isn't here.</span>
333 </h1>
334 <p class="error-page-sub">
335 The URL might be wrong, the resource might have moved, or you
336 might not have permission to see it.
337 </p>
338 <div class="error-page-actions">
339 <a href="/" class="btn btn-primary btn-lg">Go home</a>
340 <a href="/explore" class="btn btn-ghost btn-lg">Explore repos</a>
341 </div>
342 <div class="error-page-meta">
343 <span class="meta-mono">{c.req.method} {c.req.path}</span>
344 </div>
334345 </div>
335346 </Layout>,
336347 404
@@ -344,15 +355,30 @@ app.onError((err, c) => {
344355 path: c.req.path,
345356 method: c.req.method,
346357 });
358 const requestId = c.get("requestId" as never) as string | undefined;
347359 return c.html(
348360 <Layout title="Error">
349 <div class="empty-state">
350 <h2>Something went wrong</h2>
351 <p>An unexpected error occurred.</p>
361 <div class="error-page">
362 <div class="error-page-code error-page-code-err">500</div>
363 <div class="eyebrow" style="color:var(--red)">Server error</div>
364 <h1 class="display error-page-title">
365 Something <span class="gradient-text">went wrong.</span>
366 </h1>
367 <p class="error-page-sub">
368 The error has been reported. Try again — if it persists, file
369 an issue with the request ID below.
370 </p>
371 <div class="error-page-actions">
372 <a href={c.req.path} class="btn btn-primary btn-lg">Retry</a>
373 <a href="/" class="btn btn-ghost btn-lg">Go home</a>
374 </div>
375 {requestId && (
376 <div class="error-page-meta">
377 <span class="meta-mono">request-id: {requestId}</span>
378 </div>
379 )}
352380 {process.env.NODE_ENV !== "production" && (
353 <pre style="margin-top: 16px; text-align: left; font-size: 12px; color: var(--red)">
354 {err.message}
355 </pre>
381 <pre class="error-page-trace">{err.message}</pre>
356382 )}
357383 </div>
358384 </Layout>,
Modifiedsrc/views/layout.tsx+109−0View fileUnifiedSplit
@@ -2025,6 +2025,115 @@ const css = `
20252025 .cmdk-item:hover { background: var(--bg-hover) !important; }
20262026 .cmdk-active { background: var(--accent-gradient-faint) !important; border-left: 2px solid var(--accent) !important; }
20272027
2028 /* ============================================================ */
2029 /* Error pages (404 / 500) */
2030 /* ============================================================ */
2031 .error-page {
2032 text-align: center;
2033 padding: 96px 24px 80px;
2034 max-width: 720px;
2035 margin: 0 auto;
2036 position: relative;
2037 }
2038 .error-page::before {
2039 content: '';
2040 position: absolute;
2041 top: 0; left: 50%;
2042 transform: translateX(-50%);
2043 width: 80%; height: 60%;
2044 background: radial-gradient(ellipse at center, rgba(140,109,255,0.10), transparent 65%);
2045 z-index: -1;
2046 pointer-events: none;
2047 }
2048 .error-page-code {
2049 font-family: var(--font-display);
2050 font-size: clamp(80px, 14vw, 160px);
2051 font-weight: 700;
2052 line-height: 0.95;
2053 letter-spacing: -0.05em;
2054 background: linear-gradient(180deg, var(--text) 0%, var(--text-faint) 100%);
2055 -webkit-background-clip: text;
2056 background-clip: text;
2057 -webkit-text-fill-color: transparent;
2058 margin-bottom: var(--s-4);
2059 opacity: 0.85;
2060 }
2061 .error-page-code-err {
2062 background: linear-gradient(180deg, var(--red) 0%, rgba(248,113,113,0.4) 100%);
2063 -webkit-background-clip: text;
2064 background-clip: text;
2065 -webkit-text-fill-color: transparent;
2066 }
2067 .error-page .eyebrow { justify-content: center; margin: 0 auto var(--s-3); }
2068 .error-page-title {
2069 font-size: clamp(28px, 4.5vw, 48px);
2070 line-height: 1.05;
2071 letter-spacing: -0.03em;
2072 margin-bottom: var(--s-4);
2073 }
2074 .error-page-sub {
2075 color: var(--text-muted);
2076 font-size: var(--t-md);
2077 line-height: 1.55;
2078 max-width: 480px;
2079 margin: 0 auto var(--s-8);
2080 }
2081 .error-page-actions {
2082 display: flex;
2083 gap: 12px;
2084 justify-content: center;
2085 flex-wrap: wrap;
2086 margin-bottom: var(--s-7);
2087 }
2088 .error-page-meta {
2089 color: var(--text-faint);
2090 font-size: 11px;
2091 }
2092 .error-page-trace {
2093 margin-top: var(--s-5);
2094 padding: 16px 20px;
2095 background: var(--bg-secondary);
2096 border: 1px solid rgba(248,113,113,0.30);
2097 border-radius: var(--r-md);
2098 font-family: var(--font-mono);
2099 font-size: 12px;
2100 color: var(--red);
2101 text-align: left;
2102 overflow-x: auto;
2103 white-space: pre-wrap;
2104 word-break: break-all;
2105 }
2106
2107 /* Animated underline on inline content links inside markdown / prose */
2108 .prose a, .markdown-body a {
2109 position: relative;
2110 transition: color var(--t-fast) var(--ease);
2111 }
2112 .prose a::after, .markdown-body a::after {
2113 content: '';
2114 position: absolute;
2115 left: 0; right: 0; bottom: -1px;
2116 height: 1px;
2117 background: currentColor;
2118 opacity: 0.35;
2119 transform-origin: left;
2120 transition: opacity var(--t-fast) var(--ease), transform var(--t-base) var(--ease);
2121 }
2122 .prose a:hover::after, .markdown-body a:hover::after {
2123 opacity: 1;
2124 transform: scaleX(1.02);
2125 }
2126
2127 /* Print: keep it readable */
2128 print {
2129 body::before, body::after { display: none; }
2130 header, footer, .prelaunch-banner, .repo-nav, .repo-header-actions,
2131 .nav-search, .nav-right, .btn { display: none !important; }
2132 body { background: #fff; color: #000; }
2133 a { color: #000; text-decoration: underline; }
2134 main { max-width: 100%; padding: 0; }
2135 }
2136
20282137 /* Reduced motion preference */
20292138 (prefers-reduced-motion: reduce) {
20302139 *, *::before, *::after {
20312140