this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

pi cooked

+503 -120
+7 -4
src/components/console/status-dot.tsx
··· 18 18 return ( 19 19 <span 20 20 className={cn( 21 - "inline-block w-2 h-2 rounded-full flex-shrink-0", 21 + "inline-block w-2 h-2 rounded-full shrink-0", 22 22 status === "cached" && [ 23 23 "bg-(--status-cached)", 24 - "shadow-[0_0_0_2px_oklch(65%_0.2_145/0.2)]", 24 + "shadow-[0_0_0_2px_var(--status-cached-glow)]", 25 25 ], 26 26 status === "fetching" && ["bg-(--status-fetching)", "animate-pulse-dot"], 27 27 status === "idle" && "bg-(--status-idle)", 28 - status === "error" && ["bg-(--status-error)", "shadow-[0_0_0_2px_oklch(60%_0.2_25/0.2)]"], 28 + status === "error" && [ 29 + "bg-(--status-error)", 30 + "shadow-[0_0_0_2px_var(--status-error-glow)]", 31 + ], 29 32 className, 30 33 )} 31 34 aria-hidden="true" ··· 44 47 return ( 45 48 <span className={cn("inline-flex items-center gap-2", className)}> 46 49 <StatusDot status={status} /> 47 - <span className="text-sm text-(--text-muted)">{label}</span> 50 + <span className="text-sm font-mono text-(--text-muted)">{label}</span> 48 51 </span> 49 52 ); 50 53 }
+28 -11
src/components/header.tsx
··· 1 1 import { NavItem } from "./nav-item"; 2 2 import { ThemeToggle } from "./theme-toggle"; 3 3 4 + const navItems = [ 5 + { to: "/", label: "~/home" }, 6 + { to: "/basic", label: "01_basic" }, 7 + { to: "/preloading", label: "02_preloading" }, 8 + { to: "/intent-preloading", label: "03_intent-preloading", preload: "intent" as const }, 9 + { to: "/pagination", label: "04_pagination" }, 10 + { to: "/filters", label: "05_filters" }, 11 + { to: "/debounced-preload-filters", label: "06_debounced-filters" }, 12 + ]; 13 + 4 14 export function Header() { 5 15 return ( 6 - <header className="bg-(--bg-secondary) border-b border-(--border-default)"> 7 - <nav className="flex flex-row items-center"> 8 - <NavItem to="/" label="~/home" /> 9 - <NavItem to="/basic" label="01_basic" /> 10 - <NavItem to="/preloading" label="02_preloading" /> 11 - <NavItem to="/intent-preloading" label="03_intent-preloading" preload="intent" /> 12 - <NavItem to="/pagination" label="04_pagination" /> 13 - <NavItem to="/filters" label="05_filters" /> 14 - <NavItem to="/debounced-preload-filters" label="06_debounced-filters" /> 15 - <ThemeToggle /> 16 - </nav> 16 + <header className="sticky top-0 z-40 bg-(--bg-secondary) border-b border-(--border-default)"> 17 + <div className="flex items-center"> 18 + <nav 19 + className="flex flex-1 flex-row items-center overflow-x-auto scrollbar-thin" 20 + aria-label="Main navigation" 21 + > 22 + {navItems.map((item) => ( 23 + <NavItem key={item.to} to={item.to} label={item.label} preload={item.preload} /> 24 + ))} 25 + </nav> 26 + <div className="relative flex-hteshrink-0"> 27 + <div 28 + className="pointer-events-none absolute -left-6 top-0 bottom-0 w-6 bg-linear-to-l from-(--bg-secondary) to-transparent" 29 + aria-hidden="true" 30 + /> 31 + <ThemeToggle /> 32 + </div> 33 + </div> 17 34 </header> 18 35 ); 19 36 }
+10 -11
src/components/nav-item.tsx
··· 1 1 "use client"; 2 2 3 3 import { Link, useRouterState } from "@tanstack/react-router"; 4 - import { useQueryClient } from "@tanstack/react-query"; 5 4 import { StatusDot } from "~/components/console/status-dot"; 6 5 7 6 interface NavItemProps { ··· 13 12 14 13 export function NavItem({ to, label, preload, search }: NavItemProps) { 15 14 const state = useRouterState(); 16 - const queryClient = useQueryClient(); 17 15 const currentPath = state.location.pathname; 18 16 const isActive = currentPath === to; 19 - 20 - const hasCachedData = queryClient.getQueryCache().getAll().length > 0; 21 - const isFetching = state.isLoading; 17 + const isLoading = state.isLoading && isActive; 22 18 23 19 const showStatus = to !== "/"; 24 - let status: "cached" | "fetching" | "idle" = "idle"; 25 - if (isFetching) { 26 - status = "fetching"; 27 - } else if (hasCachedData && isActive) { 28 - status = "cached"; 29 - } 20 + const status = isLoading ? "fetching" : isActive ? "cached" : "idle"; 30 21 31 22 return ( 32 23 <Link ··· 34 25 search={search} 35 26 preload={preload} 36 27 className={`nav-link ${isActive ? "nav-link-active" : ""}`} 28 + aria-current={isActive ? "page" : undefined} 37 29 > 38 30 {showStatus && <StatusDot status={status} />} 39 31 <span>{label}</span> 32 + {showStatus && ( 33 + <span className="sr-only"> 34 + {status === "fetching" && "— loading"} 35 + {status === "cached" && "— cached"} 36 + {status === "idle" && "— idle"} 37 + </span> 38 + )} 40 39 </Link> 41 40 ); 42 41 }
+54 -9
src/components/theme-toggle.tsx
··· 1 1 "use client"; 2 2 3 - import { useEffect, useState } from "react"; 3 + import { useCallback, useEffect, useState } from "react"; 4 + 5 + type Theme = "system" | "light" | "dark"; 6 + 7 + function getSystemTheme(): "light" | "dark" { 8 + if (typeof window === "undefined") return "light"; 9 + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; 10 + } 11 + 12 + function getResolvedTheme(theme: Theme): "light" | "dark" { 13 + if (theme === "system") return getSystemTheme(); 14 + return theme; 15 + } 4 16 5 17 export function ThemeToggle() { 6 - const [theme, setTheme] = useState<"system" | "light" | "dark">("system"); 18 + const [theme, setTheme] = useState<Theme>("system"); 7 19 const [isClicked, setIsClicked] = useState(false); 20 + const [mounted, setMounted] = useState(false); 8 21 9 22 useEffect(() => { 10 - const saved = localStorage.getItem("theme") as "system" | "light" | "dark" | null; 23 + setMounted(true); 24 + const saved = localStorage.getItem("theme") as Theme | null; 11 25 if (saved) setTheme(saved); 12 26 }, []); 13 27 ··· 22 36 } 23 37 }, [theme]); 24 38 25 - const cycleTheme = () => { 39 + useEffect(() => { 40 + const media = window.matchMedia("(prefers-color-scheme: dark)"); 41 + const handler = () => { 42 + if (theme === "system") { 43 + // Trigger a re-render so any derived state updates 44 + setTheme("system"); 45 + } 46 + }; 47 + media.addEventListener("change", handler); 48 + return () => media.removeEventListener("change", handler); 49 + }, [theme]); 50 + 51 + const cycleTheme = useCallback(() => { 26 52 setIsClicked(true); 27 53 setTimeout(() => setIsClicked(false), 150); 28 54 setTheme((prev) => (prev === "system" ? "light" : prev === "light" ? "dark" : "system")); 29 - }; 55 + }, []); 30 56 31 57 const icon = theme === "system" ? "◐" : theme === "light" ? "○" : "●"; 58 + const resolved = getResolvedTheme(theme); 59 + 60 + // Prevent hydration mismatch by rendering a placeholder until mounted 61 + if (!mounted) { 62 + return ( 63 + <button 64 + className="flex items-center gap-2 px-3 py-2 text-sm font-mono text-(--text-muted) shrink-0" 65 + disabled 66 + aria-hidden="true" 67 + > 68 + <span>◐</span> 69 + <span className="hidden sm:inline">system</span> 70 + </button> 71 + ); 72 + } 32 73 33 74 return ( 34 75 <button 35 76 onClick={cycleTheme} 36 - className={`nav-link ml-auto ${isClicked ? "theme-toggle-clicked" : ""}`} 37 - title={`Theme: ${theme} (click to cycle)`} 38 - aria-label={`Current theme: ${theme}. Click to cycle.`} 77 + className="flex items-center gap-2 px-3 py-2 text-sm font-mono text-(--text-secondary) transition-colors duration-fast ease-default hover:text-(--accent-default) hover:bg-(--accent-subtle) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-(--bg-secondary) flex-shrink-0" 78 + title={`Theme: ${resolved} (click to cycle)`} 79 + aria-label={`Current theme: ${resolved}. Click to cycle.`} 39 80 > 40 - <span className="text-(--accent-default) theme-toggle-icon">{icon}</span> 81 + <span 82 + className={`text-(--accent-default) inline-block ${isClicked ? "motion-safe:scale-[1.3]" : ""} transition-transform duration-fast`} 83 + > 84 + {icon} 85 + </span> 41 86 <span className="hidden sm:inline">{theme}</span> 42 87 </button> 43 88 );
+50 -3
src/router.tsx
··· 6 6 // Import the generated route tree 7 7 import { routeTree } from "./routeTree.gen.ts"; 8 8 9 + function DefaultErrorComponent() { 10 + return ( 11 + <div className="min-h-screen bg-(--bg-primary) p-6"> 12 + <div className="max-w-4xl mx-auto"> 13 + <div className="bg-(--bg-card) border border-(--status-error) p-6"> 14 + <h1 className="text-lg font-mono font-semibold text-(--status-error) mb-2">Error</h1> 15 + <p className="text-sm font-mono text-(--text-secondary)"> 16 + Something went wrong. Try refreshing the page. 17 + </p> 18 + </div> 19 + </div> 20 + </div> 21 + ); 22 + } 23 + 24 + function DefaultPendingComponent() { 25 + return ( 26 + <div className="min-h-screen bg-(--bg-primary) p-6"> 27 + <div className="max-w-4xl mx-auto"> 28 + <div className="bg-(--bg-card) border border-(--border-default) p-6"> 29 + <div className="flex items-center gap-3"> 30 + <span className="inline-block w-2 h-2 rounded-full bg-(--status-fetching) animate-pulse-dot" /> 31 + <span className="text-sm font-mono text-(--text-muted)">Loading...</span> 32 + </div> 33 + </div> 34 + </div> 35 + </div> 36 + ); 37 + } 38 + 39 + function DefaultNotFoundComponent() { 40 + return ( 41 + <div className="min-h-screen bg-(--bg-primary) p-6"> 42 + <div className="max-w-4xl mx-auto"> 43 + <div className="bg-(--bg-card) border border-(--border-default) p-6"> 44 + <h1 className="text-lg font-mono font-semibold text-(--text-primary) mb-2"> 45 + 404 — Not Found 46 + </h1> 47 + <p className="text-sm font-mono text-(--text-secondary)"> 48 + The page you are looking for does not exist. 49 + </p> 50 + </div> 51 + </div> 52 + </div> 53 + ); 54 + } 55 + 9 56 export function getRouter() { 10 57 const queryClient = new QueryClient(); 11 58 ··· 17 64 defaultStructuralSharing: true, 18 65 defaultPreloadStaleTime: 0, 19 66 defaultPendingMs: 0, 20 - defaultErrorComponent: () => <div>Error</div>, 21 - defaultPendingComponent: () => <div>Loading...</div>, 22 - defaultNotFoundComponent: () => <div>Not Found</div>, 67 + defaultErrorComponent: DefaultErrorComponent, 68 + defaultPendingComponent: DefaultPendingComponent, 69 + defaultNotFoundComponent: DefaultNotFoundComponent, 23 70 context: { 24 71 queryClient, 25 72 },
+51 -24
src/routes/__root.tsx
··· 1 - import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 2 1 import { HeadContent, Outlet, Scripts, createRootRouteWithContext } from "@tanstack/react-router"; 3 - import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; 2 + import { TanStackDevtools } from "@tanstack/react-devtools"; 3 + import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; 4 + import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; 4 5 import type { QueryClient } from "@tanstack/react-query"; 5 - import { createServerFn } from "@tanstack/react-start"; 6 - import { renderServerComponent } from "@tanstack/react-start/rsc"; 7 6 8 7 import { Header } from "~/components/header"; 9 8 ··· 13 12 queryClient: QueryClient; 14 13 } 15 14 16 - const getHead = createServerFn({ method: "GET" }).handler(async () => { 17 - return { 18 - head: await renderServerComponent( 19 - <head> 20 - <HeadContent /> 21 - </head>, 22 - ), 23 - }; 24 - }); 15 + const themeHydrationScript = ` 16 + (function() { 17 + try { 18 + var theme = localStorage.getItem('theme'); 19 + if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { 20 + document.documentElement.setAttribute('data-theme', 'dark'); 21 + } else if (theme === 'light') { 22 + document.documentElement.setAttribute('data-theme', 'light'); 23 + } 24 + } catch (e) {} 25 + })(); 26 + `; 25 27 26 28 export const Route = createRootRouteWithContext<MyRouterContext>()({ 27 29 head: () => ({ ··· 36 38 { 37 39 title: "Prefetching Patterns", 38 40 }, 41 + { 42 + name: "description", 43 + content: 44 + "A developer console for exploring data prefetching techniques in modern React applications with TanStack Router and Query.", 45 + }, 39 46 ], 40 47 links: [ 41 48 { rel: "icon", href: "/favicon.ico" }, 42 49 { rel: "stylesheet", href: appCss }, 43 - // JetBrains Mono from Google Fonts 44 50 { rel: "preconnect", href: "https://fonts.googleapis.com" }, 45 51 { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }, 46 52 { 47 53 rel: "stylesheet", 48 54 href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap", 55 + }, 56 + { 57 + rel: "stylesheet", 58 + href: "https://fonts.googleapis.com/css2?family=Barlow:wght@400;600;700&display=swap", 49 59 }, 50 60 ], 51 61 }), 52 - loader: () => { 53 - return getHead(); 54 - }, 55 62 component: RootComponent, 56 63 }); 57 64 ··· 64 71 } 65 72 66 73 function RootDocument(props: Readonly<{ children: React.ReactNode }>) { 67 - const { head } = Route.useLoaderData(); 68 - 69 74 return ( 70 - <html lang="en"> 71 - {head} 75 + <html lang="en" suppressHydrationWarning> 76 + <head> 77 + <script dangerouslySetInnerHTML={{ __html: themeHydrationScript }} /> 78 + <HeadContent /> 79 + </head> 72 80 <body> 81 + <a 82 + href="#main-content" 83 + className="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:px-4 focus:py-2 focus:bg-(--bg-card) focus:border focus:border-(--accent-default) focus:text-(--text-primary) focus:font-mono focus:text-sm" 84 + > 85 + Skip to main content 86 + </a> 73 87 <Header /> 74 - {props.children} 75 - <TanStackRouterDevtools /> 76 - <ReactQueryDevtools buttonPosition="bottom-right" /> 88 + <div id="main-content">{props.children}</div> 89 + 90 + <TanStackDevtools 91 + plugins={[ 92 + { 93 + name: "TanStack Router", 94 + render: <TanStackRouterDevtoolsPanel />, 95 + defaultOpen: true, 96 + }, 97 + { 98 + name: "TanStack Query", 99 + render: <ReactQueryDevtoolsPanel />, 100 + defaultOpen: true, 101 + }, 102 + ]} 103 + /> 77 104 <Scripts /> 78 105 </body> 79 106 </html>
+146 -32
src/routes/index.tsx
··· 1 - import { createFileRoute } from "@tanstack/react-router"; 1 + import { createFileRoute, Link } from "@tanstack/react-router"; 2 2 import { createServerFn } from "@tanstack/react-start"; 3 3 import { renderServerComponent } from "@tanstack/react-start/rsc"; 4 - import { StatusDot } from "~/components/console/status-dot"; 4 + import { StatusDot, StatusDotWithLabel } from "~/components/console/status-dot"; 5 5 6 6 export const Route = createFileRoute("/")({ 7 7 loader: async () => { ··· 20 20 return <>{landingPage}</>; 21 21 } 22 22 23 + const fundamentalExamples = [ 24 + { 25 + to: "/basic", 26 + number: "01", 27 + title: "basic", 28 + description: "Baseline with no prefetching. Data loads only when the route renders.", 29 + recommended: true, 30 + }, 31 + { 32 + to: "/preloading", 33 + number: "02", 34 + title: "preloading", 35 + description: "Route-level prefetch. Data is fetched in the route loader before rendering.", 36 + }, 37 + { 38 + to: "/intent-preloading", 39 + number: "03", 40 + title: "intent-preloading", 41 + description: "Hover-based prefetch. Data loads when the user hovers over a navigation link.", 42 + }, 43 + ]; 44 + 45 + const advancedExamples = [ 46 + { 47 + to: "/pagination", 48 + number: "04", 49 + title: "pagination", 50 + description: 51 + "Preloading adjacent pages. Next and previous page data is prefetched automatically.", 52 + }, 53 + { 54 + to: "/filters", 55 + number: "05", 56 + title: "filters", 57 + description: "Search with prefetch. Filtered results are prefetched alongside pagination.", 58 + }, 59 + { 60 + to: "/debounced-preload-filters", 61 + number: "06", 62 + title: "debounced-filters", 63 + description: "Advanced filter prefetch. Results preload while typing with debounced requests.", 64 + }, 65 + ]; 66 + 23 67 function LandingPageDocument() { 24 68 return ( 25 - <main className="min-h-screen bg-(--bg-primary)"> 26 - {/* Hero Section */} 27 - <section className="border-b border-(--border-default) bg-(--bg-card)"> 28 - <div className="max-w-5xl mx-auto px-6 py-16"> 29 - <div className="flex items-center gap-3 mb-6"> 69 + <main className="min-h-screen flex flex-col bg-(--bg-primary)"> 70 + {/* Hero */} 71 + <section className="border-y border-(--border-default) bg-(--bg-secondary)"> 72 + <div className="max-w-4xl mx-auto px-6 py-[clamp(3rem,6vw,6rem)]"> 73 + <div className="flex items-center gap-3 mb-8"> 30 74 <StatusDot status="cached" /> 31 - <span className="text-(--text-muted) text-sm font-mono uppercase tracking-wider"> 75 + <span className="text-sm font-mono uppercase tracking-wider text-(--text-muted)"> 32 76 TanStack Router Demo 33 77 </span> 34 78 </div> 35 - <h1 className="text-4xl font-mono font-semibold text-(--text-primary) mb-4"> 79 + <h1 className="text-[clamp(2rem,5vw,3.5rem)] font-semibold text-(--text-primary) mb-6 leading-[1.1]"> 36 80 Prefetching Patterns 37 81 </h1> 38 - <p className="text-lg text-(--text-secondary) max-w-2xl leading-relaxed"> 82 + <p className="text-base text-(--text-secondary) max-w-2xl leading-relaxed"> 39 83 A developer console for exploring data prefetching techniques in modern React 40 84 applications. Learn how different patterns affect perceived performance and user 41 85 experience. 42 86 </p> 43 - <div className="mt-8 flex items-center gap-4 text-sm text-(--text-muted)"> 87 + <p className="mt-4 text-sm text-(--text-muted) leading-relaxed"> 88 + New here? Each example below builds on the last. Start with the first card to see how 89 + data fetching behavior changes as you add prefetching. 90 + </p> 91 + <div className="mt-8 flex items-center gap-6 text-sm font-mono text-(--text-muted)"> 44 92 <StatusDotWithLabel status="cached" label="Cached" /> 45 93 <StatusDotWithLabel status="fetching" label="Fetching" /> 46 94 <StatusDotWithLabel status="idle" label="Idle" /> ··· 48 96 </div> 49 97 </section> 50 98 99 + {/* Examples */} 100 + <section className="max-w-4xl mx-auto w-full px-6 py-12"> 101 + <h2 className="text-xl font-semibold uppercase tracking-wider text-(--text-primary) mb-8 pb-4 border-b border-(--border-default)"> 102 + Examples 103 + </h2> 104 + 105 + {/* Fundamental */} 106 + <div className="mb-12"> 107 + <h3 className="text-xs font-mono font-semibold uppercase tracking-wider text-(--text-muted) mb-4"> 108 + Fundamental 109 + </h3> 110 + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> 111 + {fundamentalExamples.map((example) => ( 112 + <Link 113 + key={example.to} 114 + to={example.to} 115 + className={`example-card group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-(--bg-primary) ${example.recommended ? "example-card--recommended" : ""}`} 116 + > 117 + {example.recommended && <span className="example-card__badge">Start here</span>} 118 + <div className="example-card__number">{example.number}</div> 119 + <div className="example-card__title group-hover:text-(--accent-default) transition-colors duration-fast"> 120 + {example.title} 121 + </div> 122 + <div className="example-card__description">{example.description}</div> 123 + <div className="mt-4 text-sm font-mono text-(--text-muted) group-hover:text-(--accent-default) transition-colors duration-fast"> 124 + <span aria-hidden="true">&gt;</span> open 125 + </div> 126 + </Link> 127 + ))} 128 + </div> 129 + </div> 130 + 131 + {/* Advanced */} 132 + <div> 133 + <h3 className="text-xs font-mono font-semibold uppercase tracking-wider text-(--text-muted) mb-4"> 134 + Advanced 135 + </h3> 136 + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> 137 + {advancedExamples.map((example) => ( 138 + <Link 139 + key={example.to} 140 + to={example.to} 141 + className="example-card group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-(--bg-primary)" 142 + > 143 + <div className="example-card__number">{example.number}</div> 144 + <div className="example-card__title group-hover:text-(--accent-default) transition-colors duration-fast"> 145 + {example.title} 146 + </div> 147 + <div className="example-card__description">{example.description}</div> 148 + <div className="mt-4 text-sm font-mono text-(--text-muted) group-hover:text-(--accent-default) transition-colors duration-fast"> 149 + <span aria-hidden="true">&gt;</span> open 150 + </div> 151 + </Link> 152 + ))} 153 + </div> 154 + </div> 155 + </section> 156 + 51 157 {/* Footer */} 52 - <footer className="border-t border-(--border-default) bg-(--bg-card)"> 53 - <div className="max-w-5xl mx-auto px-6 py-6 text-sm text-(--text-muted)"> 158 + <footer className="border-t border-(--border-default) bg-(--bg-card) mt-auto"> 159 + <div className="max-w-4xl mx-auto px-6 py-6 text-sm font-mono text-(--text-muted)"> 54 160 <p> 55 - Built with <span className="text-(--accent-default)">TanStack Router</span> +{" "} 56 - <span className="text-(--accent-default)">TanStack Query</span> +{" "} 57 - <span className="text-(--accent-default)">TanStack Start</span> 161 + Built with{" "} 162 + <a 163 + href="https://tanstack.com/router/latest" 164 + target="_blank" 165 + rel="noopener noreferrer" 166 + className="text-(--accent-default) hover:text-(--accent-hover) hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-(--bg-card) transition-colors duration-fast" 167 + > 168 + TanStack Router 169 + </a> 170 + {" · "} 171 + <a 172 + href="https://tanstack.com/query/latest" 173 + target="_blank" 174 + rel="noopener noreferrer" 175 + className="text-(--accent-default) hover:text-(--accent-hover) hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-(--bg-card) transition-colors duration-fast" 176 + > 177 + TanStack Query 178 + </a> 179 + {" · "} 180 + <a 181 + href="https://tanstack.com/start/latest" 182 + target="_blank" 183 + rel="noopener noreferrer" 184 + className="text-(--accent-default) hover:text-(--accent-hover) hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-(--bg-card) transition-colors duration-fast" 185 + > 186 + TanStack Start 187 + </a> 58 188 </p> 59 189 </div> 60 190 </footer> 61 191 </main> 62 192 ); 63 193 } 64 - 65 - // Helper component for the landing page 66 - function StatusDotWithLabel({ 67 - status, 68 - label, 69 - }: { 70 - status: "cached" | "fetching" | "idle"; 71 - label: string; 72 - }) { 73 - return ( 74 - <span className="inline-flex items-center gap-2"> 75 - <StatusDot status={status} /> 76 - <span>{label}</span> 77 - </span> 78 - ); 79 - }
+99 -26
src/styles/global.css
··· 4 4 @plugin "tailwindcss-animate"; 5 5 6 6 @custom-variant dark (&:is(.dark *)); 7 + @custom-variant motion-safe (@media (prefers-reduced-motion: no-preference)); 8 + @custom-variant motion-reduce (@media (prefers-reduced-motion: reduce)); 7 9 8 10 /* Shadcn/UI Theme Variables */ 9 11 :root { ··· 53 55 --radius-md: 0; 54 56 --radius-lg: 0; 55 57 --radius-xl: 0; 58 + --font-mono: var(--font-mono); 59 + --font-display: var(--font-display); 56 60 } 57 61 58 62 @layer base { ··· 67 71 body { 68 72 @apply bg-background text-foreground; 69 73 font-family: var(--font-mono); 70 - font-size: 14px; 74 + font-size: var(--text-sm); 71 75 line-height: var(--leading-normal); 72 76 -webkit-font-smoothing: antialiased; 73 77 -moz-osx-font-smoothing: grayscale; ··· 76 80 code { 77 81 font-family: var(--font-mono); 78 82 background: var(--bg-secondary); 79 - padding: 0.125rem 0.375rem; 83 + padding: var(--space-0-5) var(--space-1-5); 80 84 border-radius: 0; 81 85 font-size: 0.9em; 82 86 } 83 87 84 88 h1, 85 - h2, 89 + h2 { 90 + font-family: var(--font-display); 91 + font-weight: 600; 92 + line-height: var(--leading-tight); 93 + } 94 + 86 95 h3, 87 96 h4, 88 97 h5, ··· 93 102 } 94 103 95 104 h1 { 96 - font-size: var(--text-2xl); 105 + /* font-size is intentionally omitted; pages define their own hero sizes */ 97 106 } 98 107 99 108 h2 { ··· 107 116 a { 108 117 color: var(--text-primary); 109 118 text-decoration: none; 110 - transition: color var(--duration-fast) var(--easing-default); 111 119 } 112 120 113 121 a:hover { ··· 120 128 .nav-link { 121 129 display: flex; 122 130 align-items: center; 123 - gap: 0.5rem; 124 - padding: 0.5rem 1rem; 131 + gap: var(--space-2); 132 + padding: var(--space-2) var(--space-4); 125 133 font-size: var(--text-sm); 126 134 color: var(--text-secondary); 127 - transition: all var(--duration-fast) var(--easing-default); 135 + transition: 136 + color var(--duration-fast) var(--easing-default), 137 + background-color var(--duration-fast) var(--easing-default); 128 138 position: relative; 139 + white-space: nowrap; 129 140 } 130 141 131 142 .nav-link::before { ··· 139 150 .nav-link:hover { 140 151 color: var(--accent-default); 141 152 background: var(--accent-subtle); 153 + } 154 + 155 + .nav-link:focus-visible { 156 + outline: none; 157 + box-shadow: inset 0 0 0 2px var(--ring-color); 142 158 } 143 159 144 160 .nav-link-active { ··· 151 167 color: var(--accent-default); 152 168 } 153 169 154 - /* Theme toggle animation */ 155 - .theme-toggle-icon { 156 - display: inline-block; 157 - transition: transform var(--duration-fast) var(--easing-default); 158 - } 159 - 160 - .theme-toggle-clicked .theme-toggle-icon { 161 - transform: scale(1.3); 162 - } 163 - 164 170 /* Example card for landing page */ 165 171 .example-card { 166 172 background: var(--bg-card); 167 173 border: 1px solid var(--border-default); 168 - padding: 1.25rem; 169 - transition: all var(--duration-fast) var(--easing-default); 174 + padding: var(--space-5); 175 + transition: 176 + border-color var(--duration-fast) var(--easing-default), 177 + box-shadow var(--duration-fast) var(--easing-default); 178 + position: relative; 170 179 } 171 180 172 - .example-card:hover { 181 + .example-card:hover, 182 + .example-card:focus-visible { 173 183 border-color: var(--accent-default); 174 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); 184 + } 185 + 186 + .example-card:active { 187 + border-color: var(--accent-hover); 175 188 } 176 189 177 190 .example-card__number { ··· 179 192 color: var(--text-muted); 180 193 text-transform: uppercase; 181 194 letter-spacing: 0.1em; 182 - margin-bottom: 0.5rem; 195 + margin-bottom: var(--space-2); 183 196 } 184 197 185 198 .example-card__title { 186 199 font-size: var(--text-base); 187 200 font-weight: 600; 188 201 color: var(--text-primary); 189 - margin-bottom: 0.5rem; 202 + margin-bottom: var(--space-2); 190 203 } 191 204 192 205 .example-card__description { ··· 194 207 color: var(--text-muted); 195 208 line-height: var(--leading-normal); 196 209 } 210 + 211 + .example-card--recommended { 212 + background: var(--accent-surface); 213 + border-color: var(--border-accent); 214 + } 215 + 216 + .example-card--recommended .example-card__number { 217 + color: var(--accent-default); 218 + font-weight: 600; 219 + } 220 + 221 + .example-card__badge { 222 + position: absolute; 223 + top: var(--space-5); 224 + right: var(--space-5); 225 + font-size: var(--text-xs); 226 + font-weight: 600; 227 + color: var(--text-inverse); 228 + text-transform: uppercase; 229 + letter-spacing: 0.05em; 230 + padding: var(--space-1) var(--space-2); 231 + background: var(--accent-default); 232 + border: 1px solid var(--accent-default); 233 + line-height: 1; 234 + } 197 235 } 198 236 199 237 @layer utilities { 200 238 .font-mono { 201 239 font-family: var(--font-mono); 202 240 } 241 + 242 + .scrollbar-hidden { 243 + -ms-overflow-style: none; 244 + scrollbar-width: none; 245 + } 246 + 247 + .scrollbar-hidden::-webkit-scrollbar { 248 + display: none; 249 + } 250 + 251 + .scrollbar-thin { 252 + scrollbar-width: thin; 253 + scrollbar-color: var(--border-strong) transparent; 254 + } 255 + 256 + .scrollbar-thin::-webkit-scrollbar { 257 + height: 4px; 258 + } 259 + 260 + .scrollbar-thin::-webkit-scrollbar-track { 261 + background: transparent; 262 + } 263 + 264 + .scrollbar-thin::-webkit-scrollbar-thumb { 265 + background: var(--border-strong); 266 + border-radius: 0; 267 + } 203 268 } 204 269 205 270 /* Animations */ ··· 207 272 0%, 208 273 100% { 209 274 opacity: 1; 210 - box-shadow: 0 0 0 2px oklch(70% 0.15 85 / 0.2); 275 + box-shadow: 0 0 0 2px var(--status-fetching-glow); 211 276 } 212 277 50% { 213 278 opacity: 0.6; 214 - box-shadow: 0 0 0 4px oklch(70% 0.15 85 / 0.1); 279 + box-shadow: 0 0 0 4px var(--status-fetching-glow-subtle); 215 280 } 216 281 } 217 282 218 283 .animate-pulse-dot { 219 284 animation: pulse-dot 1.5s ease-in-out infinite; 220 285 } 286 + 287 + @media (prefers-reduced-motion: reduce) { 288 + .animate-pulse-dot { 289 + animation: none; 290 + opacity: 1; 291 + box-shadow: none; 292 + } 293 + }
+56
src/styles/tokens.css
··· 36 36 --color-red-600: oklch(54% 0.18 25); 37 37 38 38 /* Spacing scale (4pt base) */ 39 + --space-0-5: 0.125rem; /* 2px */ 39 40 --space-1: 0.25rem; /* 4px */ 41 + --space-1-5: 0.375rem; /* 6px */ 40 42 --space-2: 0.5rem; /* 8px */ 41 43 --space-3: 0.75rem; /* 12px */ 42 44 --space-4: 1rem; /* 16px */ 45 + --space-5: 1.25rem; /* 20px */ 43 46 --space-6: 1.5rem; /* 24px */ 44 47 --space-8: 2rem; /* 32px */ 45 48 --space-12: 3rem; /* 48px */ ··· 48 51 49 52 /* Typography */ 50 53 --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", Consolas, "Courier New", monospace; 54 + --font-display: "Barlow", "Helvetica Neue", Arial, sans-serif; 51 55 52 56 /* Font sizes */ 53 57 --text-xs: 0.75rem; ··· 90 94 --accent-hover: var(--color-amber-600); 91 95 --accent-subtle: var(--color-amber-100); 92 96 --accent-muted: var(--color-amber-200); 97 + --accent-surface: var(--color-amber-100); 93 98 94 99 /* Border */ 95 100 --border-default: var(--color-warm-300); ··· 102 107 --status-idle: var(--color-warm-400); 103 108 --status-error: var(--color-red-500); 104 109 110 + /* Status glow (for dot shadows) */ 111 + --status-cached-glow: oklch(65% 0.2 145 / 0.2); 112 + --status-fetching-glow: oklch(70% 0.15 85 / 0.2); 113 + --status-fetching-glow-subtle: oklch(70% 0.15 85 / 0.1); 114 + --status-error-glow: oklch(60% 0.2 25 / 0.2); 115 + 105 116 /* Focus ring */ 106 117 --ring-color: var(--color-amber-500); 107 118 --ring-offset: 2px; ··· 126 137 --accent-hover: var(--color-amber-300); 127 138 --accent-subtle: oklch(30% 0.03 85); 128 139 --accent-muted: oklch(35% 0.04 85); 140 + --accent-surface: oklch(32% 0.06 85); 129 141 130 142 --border-default: oklch(30% 0.012 85); 131 143 --border-strong: oklch(38% 0.014 85); ··· 135 147 --status-fetching: var(--color-amber-400); 136 148 --status-idle: oklch(40% 0.01 85); 137 149 --status-error: oklch(65% 0.18 25); 150 + 151 + --status-cached-glow: oklch(70% 0.18 145 / 0.2); 152 + --status-fetching-glow: oklch(76% 0.13 85 / 0.2); 153 + --status-fetching-glow-subtle: oklch(76% 0.13 85 / 0.1); 154 + --status-error-glow: oklch(65% 0.18 25 / 0.2); 138 155 } 139 156 } 140 157 ··· 154 171 --accent-hover: var(--color-amber-300); 155 172 --accent-subtle: oklch(30% 0.03 85); 156 173 --accent-muted: oklch(35% 0.04 85); 174 + --accent-surface: oklch(32% 0.06 85); 157 175 158 176 --border-default: oklch(30% 0.012 85); 159 177 --border-strong: oklch(38% 0.014 85); ··· 163 181 --status-fetching: var(--color-amber-400); 164 182 --status-idle: oklch(40% 0.01 85); 165 183 --status-error: oklch(65% 0.18 25); 184 + 185 + --status-cached-glow: oklch(70% 0.18 145 / 0.2); 186 + --status-fetching-glow: oklch(76% 0.13 85 / 0.2); 187 + --status-fetching-glow-subtle: oklch(76% 0.13 85 / 0.1); 188 + --status-error-glow: oklch(65% 0.18 25 / 0.2); 189 + } 190 + 191 + /* Explicit light mode override (when system is dark but user chooses light) */ 192 + [data-theme="light"] { 193 + --bg-primary: var(--color-warm-50); 194 + --bg-secondary: var(--color-warm-100); 195 + --bg-tertiary: var(--color-warm-200); 196 + --bg-card: var(--color-warm-0); 197 + 198 + --text-primary: var(--color-warm-900); 199 + --text-secondary: var(--color-warm-700); 200 + --text-muted: var(--color-warm-600); 201 + --text-inverse: var(--color-warm-0); 202 + 203 + --accent-default: var(--color-amber-500); 204 + --accent-hover: var(--color-amber-600); 205 + --accent-subtle: var(--color-amber-100); 206 + --accent-muted: var(--color-amber-200); 207 + --accent-surface: var(--color-amber-100); 208 + 209 + --border-default: var(--color-warm-300); 210 + --border-strong: var(--color-warm-400); 211 + --border-accent: var(--color-amber-500); 212 + 213 + --status-cached: var(--color-green-500); 214 + --status-fetching: var(--color-amber-500); 215 + --status-idle: var(--color-warm-400); 216 + --status-error: var(--color-red-500); 217 + 218 + --status-cached-glow: oklch(65% 0.2 145 / 0.2); 219 + --status-fetching-glow: oklch(70% 0.15 85 / 0.2); 220 + --status-fetching-glow-subtle: oklch(70% 0.15 85 / 0.1); 221 + --status-error-glow: oklch(60% 0.2 25 / 0.2); 166 222 }
+2
vite.config.ts
··· 4 4 import rsc from "@vitejs/plugin-rsc"; 5 5 import { tanstackStart } from "@tanstack/react-start/plugin/vite"; 6 6 import netlify from "@netlify/vite-plugin-tanstack-start"; 7 + import { devtools } from "@tanstack/devtools-vite"; 7 8 8 9 export default defineConfig({ 9 10 staged: { ··· 20 21 port: 3000, 21 22 }, 22 23 plugins: [ 24 + devtools(), 23 25 tanstackStart({ 24 26 rsc: { 25 27 enabled: true,