this repo has no description
0
fork

Configure Feed

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

Developer Console design: monospace typography, amber accents, code-style UI

+1041 -482
+3
.agents/skills/frontend-design/SKILL.md
··· 11 11 ## Design Thinking 12 12 13 13 Before coding, understand the context and commit to a BOLD aesthetic direction: 14 + 14 15 - **Purpose**: What problem does this interface solve? Who uses it? 15 16 - **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. 16 17 - **Constraints**: Technical requirements (framework, performance, accessibility). ··· 19 20 **CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. 20 21 21 22 Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: 23 + 22 24 - Production-grade and functional 23 25 - Visually striking and memorable 24 26 - Cohesive with a clear aesthetic point-of-view ··· 27 29 ## Frontend Aesthetics Guidelines 28 30 29 31 Focus on: 32 + 30 33 - **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. 31 34 - **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. 32 35 - **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
+48 -41
src/components/Header.tsx
··· 1 - import { Link } from "@tanstack/react-router"; 1 + import { Link, useRouterState } from "@tanstack/react-router"; 2 + import { useQueryClient } from "@tanstack/react-query"; 2 3 3 - export default function Header() { 4 - return ( 5 - <header className="p-2 flex gap-2 bg-white text-black justify-between"> 6 - <nav className="flex flex-row"> 7 - <div className="px-2"> 8 - <Link to="/" activeProps={{ className: "font-bold" }}> 9 - Home 10 - </Link> 11 - </div> 4 + interface NavItemProps { 5 + to: string; 6 + label: string; 7 + preload?: "intent" | false; 8 + search?: Record<string, unknown>; 9 + } 12 10 13 - <div className="px-2"> 14 - <Link to="/basic" activeProps={{ className: "font-bold" }}> 15 - Basic Example 16 - </Link> 17 - </div> 11 + function NavItem({ to, label, preload, search }: NavItemProps) { 12 + const state = useRouterState(); 13 + const queryClient = useQueryClient(); 14 + const currentPath = state.location.pathname; 15 + const isActive = currentPath === to; 18 16 19 - <div className="px-2"> 20 - <Link to="/preloading" activeProps={{ className: "font-bold" }}> 21 - Preloading Example 22 - </Link> 23 - </div> 17 + // Determine cache status based on query keys 18 + // This is a simplified check - in reality you'd check specific query keys 19 + const hasCachedData = queryClient.getQueryCache().getAll().length > 0; 20 + const isFetching = state.isLoading; 24 21 25 - <div className="px-2"> 26 - <Link to="/intent-preloading" preload="intent" activeProps={{ className: "font-bold" }}> 27 - Intent Preloading Example 28 - </Link> 29 - </div> 30 - 31 - <div className="px-2"> 32 - <Link to="/pagination" activeProps={{ className: "font-bold" }}> 33 - Pagination Example 34 - </Link> 35 - </div> 22 + // Show status dot for certain routes 23 + const showStatus = to !== "/"; 24 + let statusClass = "status-dot--idle"; 25 + if (isFetching) { 26 + statusClass = "status-dot--fetching"; 27 + } else if (hasCachedData && isActive) { 28 + statusClass = "status-dot--cached"; 29 + } 36 30 37 - <div className="px-2"> 38 - <Link to="/filters" activeProps={{ className: "font-bold" }}> 39 - Filters Example 40 - </Link> 41 - </div> 31 + return ( 32 + <Link 33 + to={to} 34 + search={search} 35 + preload={preload} 36 + className={`nav-link ${isActive ? "nav-link-active" : ""}`} 37 + > 38 + {showStatus && <span className={`status-dot ${statusClass}`} />} 39 + <span>{label}</span> 40 + </Link> 41 + ); 42 + } 42 43 43 - <div className="px-2"> 44 - <Link to="/debounced-preload-filters" activeProps={{ className: "font-bold" }}> 45 - Debounced Preload Filters Example 46 - </Link> 47 - </div> 44 + export default function Header() { 45 + return ( 46 + <header className="bg-warm border-b border-hairline"> 47 + <nav className="flex flex-row items-center"> 48 + <NavItem to="/" label="~/home" /> 49 + <NavItem to="/basic" label="01_basic" /> 50 + <NavItem to="/preloading" label="02_preloading" /> 51 + <NavItem to="/intent-preloading" label="03_intent-preloading" preload="intent" /> 52 + <NavItem to="/pagination" label="04_pagination" /> 53 + <NavItem to="/filters" label="05_filters" /> 54 + <NavItem to="/debounced-preload-filters" label="06_debounced" /> 48 55 </nav> 49 56 </header> 50 57 );
+5 -2
src/components/filter-form.tsx
··· 24 24 submitContext.handleSubmit(); 25 25 }} 26 26 > 27 - <Label htmlFor="name-filter" className="text-sm font-medium"> 27 + <Label 28 + htmlFor="name-filter" 29 + className="text-xs font-mono uppercase tracking-wider text-charcoal-light" 30 + > 28 31 Filter by Name 29 32 </Label> 30 33 <Input ··· 33 36 placeholder="Enter Pokemon name..." 34 37 value={submitContext.nameFilter} 35 38 onChange={(e) => submitContext.updateNameFilter(e.target.value)} 36 - className="mt-1" 39 + className="mt-2 bg-warm border-hairline focus:border-amber focus:ring-0 rounded-none font-mono text-sm" 37 40 /> 38 41 <Button type="submit" className="mt-2"> 39 42 Submit
+79 -31
src/components/pagination-nav.tsx
··· 1 1 import { type FileRoutesByPath, Link } from "@tanstack/react-router"; 2 2 import { cn } from "~/lib/utils"; 3 - import { Button } from "./ui/button"; 4 3 5 - export function PaginationNav(props: { 6 - prefetch?: "intent" | "viewport" | false; 4 + interface PaginationNavProps { 5 + prefetch?: "intent" | "viewport" | "render" | false; 7 6 prevOffset: number | undefined; 8 7 nextOffset: number | undefined; 9 8 to: keyof FileRoutesByPath; 10 - }) { 9 + currentOffset?: number; 10 + } 11 + 12 + export function PaginationNav(props: PaginationNavProps) { 13 + const { prefetch, prevOffset, nextOffset, to, currentOffset = 0 } = props; 14 + 15 + // Calculate page numbers for display 16 + const currentPage = Math.floor(currentOffset / 20) + 1; 17 + const prevPage = prevOffset !== undefined ? Math.floor(prevOffset / 20) + 1 : null; 18 + const nextPage = nextOffset !== undefined ? Math.floor(nextOffset / 20) + 1 : null; 19 + 11 20 return ( 12 - <nav className="flex justify-center gap-4 mt-4"> 13 - <Button 14 - variant="outline" 15 - className={cn(props.prevOffset == null && "opacity-50 cursor-not-allowed")} 16 - asChild 21 + <nav className="flex items-center justify-center gap-1 mt-8 font-mono"> 22 + {/* Previous button */} 23 + <Link 24 + preload={prefetch} 25 + to={to} 26 + search={{ offset: prevOffset }} 27 + disabled={prevOffset == null} 28 + className={cn( 29 + "code-button", 30 + prevOffset == null && "opacity-50 cursor-not-allowed pointer-events-none", 31 + )} 17 32 > 18 - <Link 19 - preload={props.prefetch} 20 - to={props.to} 21 - search={{ offset: props.prevOffset }} 22 - disabled={props.prevOffset == null} 23 - > 24 - Previous 25 - </Link> 26 - </Button> 27 - <Button 28 - variant="outline" 29 - className={cn(props.nextOffset == null && "opacity-50 cursor-not-allowed")} 30 - asChild 33 + <span>&lt;</span> 34 + <span>prev</span> 35 + </Link> 36 + 37 + {/* Separator */} 38 + <span className="px-2 text-charcoal-muted">|</span> 39 + 40 + {/* Page number indicators */} 41 + {prevPage && ( 42 + <> 43 + <Link 44 + preload={prefetch} 45 + to={to} 46 + search={{ offset: prevOffset }} 47 + className="page-number text-charcoal-muted hover:text-charcoal" 48 + > 49 + {prevPage} 50 + </Link> 51 + <span className="px-1 text-charcoal-muted">|</span> 52 + </> 53 + )} 54 + 55 + {/* Current page */} 56 + <span className="page-number page-number--active">{currentPage}</span> 57 + 58 + {nextPage && ( 59 + <> 60 + <span className="px-1 text-charcoal-muted">|</span> 61 + <Link 62 + preload={prefetch} 63 + to={to} 64 + search={{ offset: nextOffset }} 65 + className="page-number text-charcoal-muted hover:text-charcoal" 66 + > 67 + {nextPage} 68 + </Link> 69 + </> 70 + )} 71 + 72 + {/* Separator */} 73 + <span className="px-2 text-charcoal-muted">|</span> 74 + 75 + {/* Next button */} 76 + <Link 77 + preload={prefetch} 78 + to={to} 79 + search={{ offset: nextOffset }} 80 + disabled={nextOffset == null} 81 + className={cn( 82 + "code-button", 83 + nextOffset == null && "opacity-50 cursor-not-allowed pointer-events-none", 84 + )} 31 85 > 32 - <Link 33 - preload={props.prefetch} 34 - to={props.to} 35 - search={{ offset: props.nextOffset }} 36 - disabled={props.nextOffset == null} 37 - > 38 - Next 39 - </Link> 40 - </Button> 86 + <span>next</span> 87 + <span>&gt;</span> 88 + </Link> 41 89 </nav> 42 90 ); 43 91 }
+8 -1
src/routes/__root.tsx
··· 22 22 content: "width=device-width, initial-scale=1", 23 23 }, 24 24 { 25 - title: "fizzbuzz", 25 + title: "Prefetching Patterns", 26 26 }, 27 27 ], 28 28 links: [ 29 29 { rel: "icon", href: "/favicon.ico" }, 30 30 { rel: "stylesheet", href: appCss }, 31 + // JetBrains Mono from Google Fonts 32 + { rel: "preconnect", href: "https://fonts.googleapis.com" }, 33 + { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }, 34 + { 35 + rel: "stylesheet", 36 + href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap", 37 + }, 31 38 ], 32 39 }), 33 40 component: RootComponent,
+56 -37
src/routes/basic.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 2 import { useSuspenseQuery } from "@tanstack/react-query"; 3 + import { useServerFn } from "@tanstack/react-start"; 3 4 import * as v from "valibot"; 4 5 import { PaginationNav } from "~/components/pagination-nav"; 5 6 import { ··· 10 11 TableHeader, 11 12 TableRow, 12 13 } from "~/components/ui/table"; 13 - import { POKEMON_LIMIT, getPokemonListQueryFn, getPokemonListQueryKey } from "~/util/pokemon"; 14 + import { 15 + POKEMON_LIMIT, 16 + getPokemonListQueryFn, 17 + getPokemonListQueryKey, 18 + } from "~/util/pokemon"; 14 19 15 20 const searchParamsSchema = v.object({ 16 21 offset: v.optional(v.number(), 0), ··· 31 36 }); 32 37 33 38 return ( 34 - <main className="p-4"> 35 - <h1 className="text-2xl font-bold mb-4"> 36 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 37 - </h1> 38 - <Table> 39 - <TableHeader> 40 - <TableRow> 41 - <TableHead>#</TableHead> 42 - <TableHead>Name</TableHead> 43 - <TableHead>Details</TableHead> 44 - </TableRow> 45 - </TableHeader> 46 - <TableBody> 47 - {data.pokemon.map((pokemon) => ( 48 - <TableRow key={pokemon.name}> 49 - <TableCell>{pokemon.id}</TableCell> 50 - <TableCell className="capitalize">{pokemon.name}</TableCell> 51 - <TableCell> 52 - {pokemon.types.map((type) => ( 53 - <span 54 - key={type.name} 55 - className="inline-block px-2 py-1 mr-1 text-sm font-medium rounded-full bg-gray-100" 56 - > 57 - {type.name} 58 - </span> 59 - ))} 60 - </TableCell> 61 - </TableRow> 62 - ))} 63 - </TableBody> 64 - </Table> 65 - <PaginationNav 66 - prevOffset={data.prevOffset ?? undefined} 67 - nextOffset={data.nextOffset ?? undefined} 68 - to="/basic" 69 - /> 39 + <main className="min-h-screen bg-warm p-6"> 40 + <div className="max-w-4xl mx-auto"> 41 + <div className="section-header"> 42 + <span className="section-header__title">01_basic</span> 43 + <span className="text-charcoal-muted text-sm"> 44 + // No prefetching (baseline) 45 + </span> 46 + </div> 47 + 48 + <div className="console-card mb-6"> 49 + <h1 className="text-lg font-mono text-charcoal mb-4"> 50 + National Pokédex: Pokémon {currentOffset + 1}- 51 + {currentOffset + POKEMON_LIMIT} 52 + </h1> 53 + <Table className="data-table"> 54 + <TableHeader> 55 + <TableRow> 56 + <TableHead>#</TableHead> 57 + <TableHead>Name</TableHead> 58 + <TableHead>Details</TableHead> 59 + </TableRow> 60 + </TableHeader> 61 + <TableBody> 62 + {data.pokemon.map((pokemon) => ( 63 + <TableRow key={pokemon.name}> 64 + <TableCell className="font-mono text-charcoal-muted"> 65 + {pokemon.id} 66 + </TableCell> 67 + <TableCell className="capitalize text-charcoal"> 68 + {pokemon.name} 69 + </TableCell> 70 + <TableCell> 71 + {pokemon.types.map((type) => ( 72 + <span key={type.name} className="type-badge"> 73 + {type.name} 74 + </span> 75 + ))} 76 + </TableCell> 77 + </TableRow> 78 + ))} 79 + </TableBody> 80 + </Table> 81 + <PaginationNav 82 + prevOffset={data.prevOffset ?? undefined} 83 + nextOffset={data.nextOffset ?? undefined} 84 + to="/basic" 85 + currentOffset={currentOffset} 86 + /> 87 + </div> 88 + </div> 70 89 </main> 71 90 ); 72 91 }
+92 -60
src/routes/debounced-preload-filters.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 2 import { useDebouncedCallback } from "@tanstack/react-pacer"; 3 - import { queryOptions, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; 3 + import { 4 + queryOptions, 5 + useQueryClient, 6 + useSuspenseQuery, 7 + } from "@tanstack/react-query"; 4 8 import { useCallback } from "react"; 5 9 import { useState } from "react"; 6 10 import * as v from "valibot"; ··· 60 64 children: React.ReactNode; 61 65 }) { 62 66 const queryClient = useQueryClient(); 63 - const { pokemonListOptions: serverPokemonListOptions } = Route.useRouteContext(); 67 + const { pokemonListOptions: serverPokemonListOptions } = 68 + Route.useRouteContext(); 64 69 const [nameFilter, setNameFilter] = useState(props.initialName); 65 70 66 71 const debouncedNameFilter = useDebouncedCallback( ··· 92 97 }, [nameFilter, props]); 93 98 94 99 return ( 95 - <FilterSubmitContext.Provider value={{ handleSubmit, nameFilter, updateNameFilter }}> 100 + <FilterSubmitContext.Provider 101 + value={{ handleSubmit, nameFilter, updateNameFilter }} 102 + > 96 103 {props.children} 97 104 </FilterSubmitContext.Provider> 98 105 ); ··· 109 116 const filteredPokemon = data.pokemon; 110 117 111 118 return ( 112 - <div className="p-4"> 113 - <h1 className="text-2xl font-bold mb-4"> 114 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} (Filtered) 115 - </h1> 119 + <main className="min-h-screen bg-warm p-6"> 120 + <div className="max-w-4xl mx-auto"> 121 + <div className="section-header"> 122 + <span className="section-header__title">06_debounced</span> 123 + <span className="text-charcoal-muted text-sm"> 124 + // Advanced filter prefetch 125 + </span> 126 + </div> 116 127 117 - {/* Filter UI */} 118 - <div className="mb-6 p-4 border rounded-lg bg-gray-50"> 119 - <h2 className="text-lg font-semibold mb-3">Filters</h2> 120 - <PreloadFilterSubmitContextProvider 121 - initialName={nameFilter} 122 - handleSubmit={(nameFilter) => { 123 - void navigate({ 124 - search: { name: nameFilter }, 125 - }); 126 - }} 127 - > 128 - <FilterForm /> 129 - </PreloadFilterSubmitContextProvider> 130 - </div> 128 + {/* Filter UI */} 129 + <div className="console-card mb-6"> 130 + <h2 className="text-sm font-semibold mb-4 text-charcoal uppercase tracking-wider"> 131 + Filters 132 + </h2> 133 + <p className="text-sm text-charcoal-muted mb-4"> 134 + Preloads results while typing (debounced 100ms) 135 + </p> 136 + <PreloadFilterSubmitContextProvider 137 + initialName={nameFilter} 138 + handleSubmit={(newNameFilter) => { 139 + void navigate({ 140 + search: { name: newNameFilter }, 141 + }); 142 + }} 143 + > 144 + <FilterForm /> 145 + </PreloadFilterSubmitContextProvider> 146 + </div> 131 147 132 - <Table> 133 - <TableHeader> 134 - <TableRow> 135 - <TableHead>#</TableHead> 136 - <TableHead>Name</TableHead> 137 - <TableHead>Details</TableHead> 138 - </TableRow> 139 - </TableHeader> 140 - <TableBody> 141 - {filteredPokemon.map((pokemon) => ( 142 - <TableRow key={pokemon.name}> 143 - <TableCell>{pokemon.id}</TableCell> 144 - <TableCell className="capitalize">{pokemon.name}</TableCell> 145 - <TableCell> 146 - {pokemon.types.map((type) => ( 147 - <span 148 - key={type.name} 149 - className="inline-block px-2 py-1 mr-1 text-sm font-medium rounded-full bg-gray-100" 150 - > 151 - {type.name} 152 - </span> 153 - ))} 154 - </TableCell> 155 - </TableRow> 156 - ))} 157 - </TableBody> 158 - </Table> 148 + <div className="console-card"> 149 + <h1 className="text-lg font-mono text-charcoal mb-4"> 150 + National Pokédex: Pokémon {currentOffset + 1}- 151 + {currentOffset + POKEMON_LIMIT} 152 + {nameFilter && ( 153 + <span className="text-charcoal-muted"> 154 + {" "} 155 + (filtered: "{nameFilter}") 156 + </span> 157 + )} 158 + </h1> 159 159 160 - {filteredPokemon.length === 0 && nameFilter && ( 161 - <div className="text-center py-8 text-gray-500"> 162 - No Pokemon found matching "{nameFilter}" 163 - </div> 164 - )} 160 + <Table className="data-table"> 161 + <TableHeader> 162 + <TableRow> 163 + <TableHead>#</TableHead> 164 + <TableHead>Name</TableHead> 165 + <TableHead>Details</TableHead> 166 + </TableRow> 167 + </TableHeader> 168 + <TableBody> 169 + {filteredPokemon.map((pokemon) => ( 170 + <TableRow key={pokemon.name}> 171 + <TableCell className="font-mono text-charcoal-muted"> 172 + {pokemon.id} 173 + </TableCell> 174 + <TableCell className="capitalize text-charcoal"> 175 + {pokemon.name} 176 + </TableCell> 177 + <TableCell> 178 + {pokemon.types.map((type) => ( 179 + <span key={type.name} className="type-badge"> 180 + {type.name} 181 + </span> 182 + ))} 183 + </TableCell> 184 + </TableRow> 185 + ))} 186 + </TableBody> 187 + </Table> 165 188 166 - <PaginationNav 167 - prefetch="viewport" 168 - prevOffset={data.prevOffset ?? undefined} 169 - nextOffset={data.nextOffset ?? undefined} 170 - to="/debounced-preload-filters" 171 - /> 172 - </div> 189 + {filteredPokemon.length === 0 && nameFilter && ( 190 + <div className="text-center py-8 text-charcoal-muted font-mono"> 191 + No Pokemon found matching "{nameFilter}" 192 + </div> 193 + )} 194 + 195 + <PaginationNav 196 + prefetch="viewport" 197 + prevOffset={data.prevOffset ?? undefined} 198 + nextOffset={data.nextOffset ?? undefined} 199 + to="/debounced-preload-filters" 200 + currentOffset={currentOffset} 201 + /> 202 + </div> 203 + </div> 204 + </main> 173 205 ); 174 206 }
+86 -59
src/routes/filters.tsx
··· 43 43 </FilterSubmitContext.Provider> 44 44 ); 45 45 } 46 + 46 47 export const Route = createFileRoute("/filters")({ 47 48 validateSearch: searchParamsSchema, 48 49 loaderDeps: ({ search }) => ({ ··· 50 51 name: search.name, 51 52 }), 52 53 context: ({ deps }) => { 53 - const newKey = getFilteredPokemonListQueryKey("filters", deps.offset, deps.name); 54 + const newKey = getFilteredPokemonListQueryKey( 55 + "filters", 56 + deps.offset, 57 + deps.name, 58 + ); 54 59 55 60 const pokemonListOptions = queryOptions({ 56 61 queryKey: newKey, ··· 78 83 const filteredPokemon = data.pokemon; 79 84 80 85 return ( 81 - <div className="p-4"> 82 - <h1 className="text-2xl font-bold mb-4"> 83 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} (Filtered) 84 - </h1> 86 + <main className="min-h-screen bg-warm p-6"> 87 + <div className="max-w-4xl mx-auto"> 88 + <div className="section-header"> 89 + <span className="section-header__title">05_filters</span> 90 + <span className="text-charcoal-muted text-sm"> 91 + // Search with prefetch 92 + </span> 93 + </div> 94 + 95 + {/* Filter UI */} 96 + <div className="console-card mb-6"> 97 + <h2 className="text-sm font-semibold mb-4 text-charcoal uppercase tracking-wider"> 98 + Filters 99 + </h2> 100 + <FilterSubmitContextProvider 101 + key={`filter-submit-context-provider-${nameFilter}`} 102 + initialName={nameFilter} 103 + handleSubmit={(newNameFilter) => { 104 + void navigate({ 105 + search: { name: newNameFilter }, 106 + }); 107 + }} 108 + > 109 + <FilterForm /> 110 + </FilterSubmitContextProvider> 111 + </div> 112 + 113 + <div className="console-card"> 114 + <h1 className="text-lg font-mono text-charcoal mb-4"> 115 + National Pokédex: Pokémon {currentOffset + 1}- 116 + {currentOffset + POKEMON_LIMIT} 117 + {nameFilter && ( 118 + <span className="text-charcoal-muted"> 119 + {" "} 120 + (filtered: "{nameFilter}") 121 + </span> 122 + )} 123 + </h1> 85 124 86 - {/* Filter UI */} 87 - <div className="mb-6 p-4 border rounded-lg bg-gray-50"> 88 - <h2 className="text-lg font-semibold mb-3">Filters</h2> 89 - <FilterSubmitContextProvider 90 - key={`filter-submit-context-provider-${nameFilter}`} 91 - initialName={nameFilter} 92 - handleSubmit={(nameFilter) => { 93 - void navigate({ 94 - search: { name: nameFilter }, 95 - }); 96 - }} 97 - > 98 - <FilterForm /> 99 - </FilterSubmitContextProvider> 100 - </div> 125 + <Table className="data-table"> 126 + <TableHeader> 127 + <TableRow> 128 + <TableHead>#</TableHead> 129 + <TableHead>Name</TableHead> 130 + <TableHead>Details</TableHead> 131 + </TableRow> 132 + </TableHeader> 133 + <TableBody> 134 + {filteredPokemon.map((pokemon) => ( 135 + <TableRow key={pokemon.name}> 136 + <TableCell className="font-mono text-charcoal-muted"> 137 + {pokemon.id} 138 + </TableCell> 139 + <TableCell className="capitalize text-charcoal"> 140 + {pokemon.name} 141 + </TableCell> 142 + <TableCell> 143 + {pokemon.types.map((type) => ( 144 + <span key={type.name} className="type-badge"> 145 + {type.name} 146 + </span> 147 + ))} 148 + </TableCell> 149 + </TableRow> 150 + ))} 151 + </TableBody> 152 + </Table> 101 153 102 - <Table> 103 - <TableHeader> 104 - <TableRow> 105 - <TableHead>#</TableHead> 106 - <TableHead>Name</TableHead> 107 - <TableHead>Details</TableHead> 108 - </TableRow> 109 - </TableHeader> 110 - <TableBody> 111 - {filteredPokemon.map((pokemon) => ( 112 - <TableRow key={pokemon.name}> 113 - <TableCell>{pokemon.id}</TableCell> 114 - <TableCell className="capitalize">{pokemon.name}</TableCell> 115 - <TableCell> 116 - {pokemon.types.map((type) => ( 117 - <span 118 - key={type.name} 119 - className="inline-block px-2 py-1 mr-1 text-sm font-medium rounded-full bg-gray-100" 120 - > 121 - {type.name} 122 - </span> 123 - ))} 124 - </TableCell> 125 - </TableRow> 126 - ))} 127 - </TableBody> 128 - </Table> 154 + {filteredPokemon.length === 0 && nameFilter && ( 155 + <div className="text-center py-8 text-charcoal-muted font-mono"> 156 + No Pokemon found matching "{nameFilter}" 157 + </div> 158 + )} 129 159 130 - {filteredPokemon.length === 0 && nameFilter && ( 131 - <div className="text-center py-8 text-gray-500"> 132 - No Pokemon found matching "{nameFilter}" 160 + <PaginationNav 161 + prefetch="viewport" 162 + prevOffset={data.prevOffset ?? undefined} 163 + nextOffset={data.nextOffset ?? undefined} 164 + to="/filters" 165 + currentOffset={currentOffset} 166 + /> 133 167 </div> 134 - )} 135 - 136 - <PaginationNav 137 - prefetch="viewport" 138 - prevOffset={data.prevOffset ?? undefined} 139 - nextOffset={data.nextOffset ?? undefined} 140 - to="/filters" 141 - /> 142 - </div> 168 + </div> 169 + </main> 143 170 ); 144 171 }
+169 -41
src/routes/index.tsx
··· 1 - import { createFileRoute } from "@tanstack/react-router"; 2 - import { useState } from "react"; 3 - import logo from "../logo.svg"; 1 + import { createFileRoute, Link } from "@tanstack/react-router"; 4 2 5 3 export const Route = createFileRoute("/")({ 6 - component: App, 4 + component: LandingPage, 7 5 }); 8 6 9 - function App() { 10 - const [count, setCount] = useState(0); 7 + interface ExampleItemProps { 8 + number: string; 9 + title: string; 10 + path: string; 11 + description: string; 12 + codeSnippet: string; 13 + } 11 14 15 + function ExampleItem({ number, title, path, description, codeSnippet }: ExampleItemProps) { 12 16 return ( 13 - <div className="text-center"> 14 - <header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)]"> 15 - <img 16 - src={logo} 17 - className="h-[40vmin] pointer-events-none animate-[spin_20s_linear_infinite]" 18 - alt="logo" 19 - /> 20 - <p> 21 - Edit <code>src/routes/index.tsx</code> and save to reload. 22 - </p> 23 - <button 24 - type="button" 25 - className="mt-4 px-6 py-2 rounded bg-[#61dafb] text-[#282c34] font-bold hover:bg-[#21a1f3] transition" 26 - onClick={() => setCount((c) => c + 1)} 27 - > 28 - Counter: {count} 29 - </button> 30 - <a 31 - className="text-[#61dafb] hover:underline mt-4" 32 - href="https://reactjs.org" 33 - target="_blank" 34 - rel="noopener noreferrer" 35 - > 36 - Learn React 37 - </a> 38 - <a 39 - className="text-[#61dafb] hover:underline" 40 - href="https://tanstack.com" 41 - target="_blank" 42 - rel="noopener noreferrer" 43 - > 44 - Learn TanStack 45 - </a> 46 - </header> 47 - </div> 17 + <Link to={path} className="block no-underline"> 18 + <div className="example-card"> 19 + <div className="example-card__number">Example {number}</div> 20 + <h3 className="example-card__title">{title}</h3> 21 + <p className="example-card__description">{description}</p> 22 + <div className="mt-4 p-3 bg-warm border border-hairline font-mono text-xs text-charcoal-muted overflow-x-auto"> 23 + <code>{codeSnippet}</code> 24 + </div> 25 + </div> 26 + </Link> 27 + ); 28 + } 29 + 30 + function LandingPage() { 31 + const examples: ExampleItemProps[] = [ 32 + { 33 + number: "01", 34 + title: "Basic", 35 + path: "/basic", 36 + description: 37 + "Baseline implementation with no prefetching. Data loads only when the route renders, demonstrating the default behavior without optimizations.", 38 + codeSnippet: "useSuspenseQuery({ queryKey, queryFn })", 39 + }, 40 + { 41 + number: "02", 42 + title: "Preloading", 43 + path: "/preloading", 44 + description: 45 + "Route-level prefetch using loader and queryOptions. Data begins loading as soon as navigation starts, reducing perceived latency.", 46 + codeSnippet: "loader: ({ context }) => {\n void context.queryClient.prefetchQuery(...)\n}", 47 + }, 48 + { 49 + number: "03", 50 + title: "Intent Preloading", 51 + path: "/intent-preloading", 52 + description: 53 + "Hover-based prefetch using preload='intent' on Links. Data starts loading when the user hovers over a link, anticipating their next action.", 54 + codeSnippet: "<Link preload='intent' to='/route'>...</Link>", 55 + }, 56 + { 57 + number: "04", 58 + title: "Pagination", 59 + path: "/pagination", 60 + description: 61 + "Preloading next and previous pages in paginated lists. Adjacent pages are prefetched so navigation feels instant.", 62 + codeSnippet: "preload={props.prefetch}\nto={props.to}\nsearch={{ offset: props.nextOffset }}", 63 + }, 64 + { 65 + number: "05", 66 + title: "Filters", 67 + path: "/filters", 68 + description: 69 + "Search with URL-driven state and prefetching. Filter changes update the URL and trigger data refetching with intelligent caching.", 70 + codeSnippet: "validateSearch: searchParamsSchema", 71 + }, 72 + { 73 + number: "06", 74 + title: "Debounced Preload Filters", 75 + path: "/debounced-preload-filters", 76 + description: 77 + "Advanced filter prefetch with debouncing. Prevents excessive prefetch requests while typing by waiting for a pause in input.", 78 + codeSnippet: "useDebounce(value, delay)\n// Preload only after pause", 79 + }, 80 + ]; 81 + 82 + return ( 83 + <main className="min-h-screen bg-warm"> 84 + {/* Hero Section */} 85 + <section className="border-b border-hairline bg-white"> 86 + <div className="max-w-5xl mx-auto px-6 py-16"> 87 + <div className="flex items-center gap-3 mb-6"> 88 + <span className="status-dot status-dot--cached" /> 89 + <span className="text-charcoal-muted text-sm font-mono uppercase tracking-wider"> 90 + TanStack Router Demo 91 + </span> 92 + </div> 93 + <h1 className="text-4xl font-mono font-semibold text-charcoal mb-4"> 94 + Prefetching Patterns 95 + </h1> 96 + <p className="text-lg text-charcoal-light max-w-2xl leading-relaxed"> 97 + A developer console for exploring data prefetching techniques in modern React 98 + applications. Learn how different patterns affect perceived performance and user 99 + experience. 100 + </p> 101 + <div className="mt-8 flex items-center gap-4 text-sm text-charcoal-muted"> 102 + <span className="flex items-center gap-2"> 103 + <span className="status-dot status-dot--cached" /> 104 + Cached 105 + </span> 106 + <span className="flex items-center gap-2"> 107 + <span className="status-dot status-dot--fetching" /> 108 + Fetching 109 + </span> 110 + <span className="flex items-center gap-2"> 111 + <span className="status-dot status-dot--idle" /> 112 + Idle 113 + </span> 114 + </div> 115 + </div> 116 + </section> 117 + 118 + {/* Examples Grid */} 119 + <section className="max-w-5xl mx-auto px-6 py-12"> 120 + <div className="section-header"> 121 + <span className="section-header__title">Examples</span> 122 + <span className="text-charcoal-muted text-sm">// Select one to explore</span> 123 + </div> 124 + 125 + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> 126 + {examples.map((example) => ( 127 + <ExampleItem key={example.number} {...example} /> 128 + ))} 129 + </div> 130 + </section> 131 + 132 + {/* Concepts Section */} 133 + <section className="max-w-5xl mx-auto px-6 pb-16"> 134 + <div className="section-header"> 135 + <span className="section-header__title">Key Concepts</span> 136 + </div> 137 + 138 + <div className="console-card"> 139 + <div className="grid grid-cols-1 md:grid-cols-3 gap-8"> 140 + <div> 141 + <h4 className="font-mono font-semibold text-charcoal mb-2">Route Loaders</h4> 142 + <p className="text-sm text-charcoal-light leading-relaxed"> 143 + Loaders run before the route component renders, making them ideal for initiating 144 + data fetches early in the navigation lifecycle. 145 + </p> 146 + </div> 147 + <div> 148 + <h4 className="font-mono font-semibold text-charcoal mb-2">Intent Preloading</h4> 149 + <p className="text-sm text-charcoal-light leading-relaxed"> 150 + By observing user intent (hover, focus), we can predict navigation and preload data 151 + before the click event fires. 152 + </p> 153 + </div> 154 + <div> 155 + <h4 className="font-mono font-semibold text-charcoal mb-2">Query Caching</h4> 156 + <p className="text-sm text-charcoal-light leading-relaxed"> 157 + TanStack Query maintains a cache of fetched data. Subsequent requests for the same 158 + data return instantly from cache. 159 + </p> 160 + </div> 161 + </div> 162 + </div> 163 + </section> 164 + 165 + {/* Footer */} 166 + <footer className="border-t border-hairline bg-white"> 167 + <div className="max-w-5xl mx-auto px-6 py-6 text-sm text-charcoal-muted"> 168 + <p> 169 + Built with <span className="text-amber">TanStack Router</span> +{" "} 170 + <span className="text-amber">TanStack Query</span> +{" "} 171 + <span className="text-amber">TanStack Start</span> 172 + </p> 173 + </div> 174 + </footer> 175 + </main> 48 176 ); 49 177 }
+45 -38
src/routes/intent-preloading.tsx
··· 47 47 const { data } = useSuspenseQuery(pokemonListOptions); 48 48 49 49 return ( 50 - <div className="p-4"> 51 - <h1 className="text-2xl font-bold mb-4"> 52 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 53 - </h1> 54 - <Table> 55 - <TableHeader> 56 - <TableRow> 57 - <TableHead>#</TableHead> 58 - <TableHead>Name</TableHead> 59 - <TableHead>Details</TableHead> 60 - </TableRow> 61 - </TableHeader> 62 - <TableBody> 63 - {data.pokemon.map((pokemon) => ( 64 - <TableRow key={pokemon.name}> 65 - <TableCell>{pokemon.id}</TableCell> 66 - <TableCell className="capitalize">{pokemon.name}</TableCell> 67 - <TableCell> 68 - {pokemon.types.map((type) => ( 69 - <span 70 - key={type.name} 71 - className="inline-block px-2 py-1 mr-1 text-sm font-medium rounded-full bg-gray-100" 72 - > 73 - {type.name} 74 - </span> 75 - ))} 76 - </TableCell> 77 - </TableRow> 78 - ))} 79 - </TableBody> 80 - </Table> 81 - <PaginationNav 82 - prefetch="intent" 83 - prevOffset={data.prevOffset ?? undefined} 84 - nextOffset={data.nextOffset ?? undefined} 85 - to="/intent-preloading" 86 - /> 87 - </div> 50 + <main className="min-h-screen bg-warm p-6"> 51 + <div className="max-w-4xl mx-auto"> 52 + <div className="section-header"> 53 + <span className="section-header__title">03_intent-preloading</span> 54 + <span className="text-charcoal-muted text-sm">// Hover-based prefetch</span> 55 + </div> 56 + 57 + <div className="console-card mb-6"> 58 + <h1 className="text-lg font-mono text-charcoal mb-4"> 59 + National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 60 + </h1> 61 + <Table className="data-table"> 62 + <TableHeader> 63 + <TableRow> 64 + <TableHead>#</TableHead> 65 + <TableHead>Name</TableHead> 66 + <TableHead>Details</TableHead> 67 + </TableRow> 68 + </TableHeader> 69 + <TableBody> 70 + {data.pokemon.map((pokemon) => ( 71 + <TableRow key={pokemon.name}> 72 + <TableCell className="font-mono text-charcoal-muted">{pokemon.id}</TableCell> 73 + <TableCell className="capitalize text-charcoal">{pokemon.name}</TableCell> 74 + <TableCell> 75 + {pokemon.types.map((type) => ( 76 + <span key={type.name} className="type-badge"> 77 + {type.name} 78 + </span> 79 + ))} 80 + </TableCell> 81 + </TableRow> 82 + ))} 83 + </TableBody> 84 + </Table> 85 + <PaginationNav 86 + prefetch="intent" 87 + prevOffset={data.prevOffset ?? undefined} 88 + nextOffset={data.nextOffset ?? undefined} 89 + to="/intent-preloading" 90 + currentOffset={currentOffset} 91 + /> 92 + </div> 93 + </div> 94 + </main> 88 95 ); 89 96 }
+57 -39
src/routes/pagination.tsx
··· 10 10 TableHeader, 11 11 TableRow, 12 12 } from "~/components/ui/table"; 13 - import { POKEMON_LIMIT, getPokemonListQueryKey, getPokemonListQueryFn } from "~/util/pokemon"; 13 + import { 14 + POKEMON_LIMIT, 15 + getPokemonListQueryKey, 16 + getPokemonListQueryFn, 17 + } from "~/util/pokemon"; 14 18 15 19 const searchParamsSchema = v.object({ 16 20 offset: v.optional(v.number(), 0), ··· 47 51 const { data } = useSuspenseQuery(pokemonListOptions); 48 52 49 53 return ( 50 - <div className="p-4"> 51 - <h1 className="text-2xl font-bold mb-4"> 52 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 53 - </h1> 54 - <Table> 55 - <TableHeader> 56 - <TableRow> 57 - <TableHead>#</TableHead> 58 - <TableHead>Name</TableHead> 59 - <TableHead>Details</TableHead> 60 - </TableRow> 61 - </TableHeader> 62 - <TableBody> 63 - {data.pokemon.map((pokemon) => ( 64 - <TableRow key={pokemon.name}> 65 - <TableCell>{pokemon.id}</TableCell> 66 - <TableCell className="capitalize">{pokemon.name}</TableCell> 67 - <TableCell> 68 - {pokemon.types.map((type) => ( 69 - <span 70 - key={type.name} 71 - className="inline-block px-2 py-1 mr-1 text-sm font-medium rounded-full bg-gray-100" 72 - > 73 - {type.name} 74 - </span> 75 - ))} 76 - </TableCell> 77 - </TableRow> 78 - ))} 79 - </TableBody> 80 - </Table> 81 - <PaginationNav 82 - prefetch="viewport" 83 - prevOffset={data.prevOffset ?? undefined} 84 - nextOffset={data.nextOffset ?? undefined} 85 - to="/pagination" 86 - /> 87 - </div> 54 + <main className="min-h-screen bg-warm p-6"> 55 + <div className="max-w-4xl mx-auto"> 56 + <div className="section-header"> 57 + <span className="section-header__title">04_pagination</span> 58 + <span className="text-charcoal-muted text-sm"> 59 + // Preloading next/prev pages 60 + </span> 61 + </div> 62 + 63 + <div className="console-card mb-6"> 64 + <h1 className="text-lg font-mono text-charcoal mb-4"> 65 + National Pokédex: Pokémon {currentOffset + 1}- 66 + {currentOffset + POKEMON_LIMIT} 67 + </h1> 68 + <Table className="data-table"> 69 + <TableHeader> 70 + <TableRow> 71 + <TableHead>#</TableHead> 72 + <TableHead>Name</TableHead> 73 + <TableHead>Details</TableHead> 74 + </TableRow> 75 + </TableHeader> 76 + <TableBody> 77 + {data.pokemon.map((pokemon) => ( 78 + <TableRow key={pokemon.name}> 79 + <TableCell className="font-mono text-charcoal-muted"> 80 + {pokemon.id} 81 + </TableCell> 82 + <TableCell className="capitalize text-charcoal"> 83 + {pokemon.name} 84 + </TableCell> 85 + <TableCell> 86 + {pokemon.types.map((type) => ( 87 + <span key={type.name} className="type-badge"> 88 + {type.name} 89 + </span> 90 + ))} 91 + </TableCell> 92 + </TableRow> 93 + ))} 94 + </TableBody> 95 + </Table> 96 + <PaginationNav 97 + prefetch="viewport" 98 + prevOffset={data.prevOffset ?? undefined} 99 + nextOffset={data.nextOffset ?? undefined} 100 + to="/pagination" 101 + currentOffset={currentOffset} 102 + /> 103 + </div> 104 + </div> 105 + </main> 88 106 ); 89 107 }
+44 -37
src/routes/preloading.tsx
··· 50 50 }); 51 51 52 52 return ( 53 - <div className="p-4"> 54 - <h1 className="text-2xl font-bold mb-4"> 55 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 56 - </h1> 57 - <Table> 58 - <TableHeader> 59 - <TableRow> 60 - <TableHead>#</TableHead> 61 - <TableHead>Name</TableHead> 62 - <TableHead>Details</TableHead> 63 - </TableRow> 64 - </TableHeader> 65 - <TableBody> 66 - {data.pokemon.map((pokemon) => ( 67 - <TableRow key={pokemon.name}> 68 - <TableCell>{pokemon.id}</TableCell> 69 - <TableCell className="capitalize">{pokemon.name}</TableCell> 70 - <TableCell> 71 - {pokemon.types.map((type) => ( 72 - <span 73 - key={type.name} 74 - className="inline-block px-2 py-1 mr-1 text-sm font-medium rounded-full bg-gray-100" 75 - > 76 - {type.name} 77 - </span> 78 - ))} 79 - </TableCell> 80 - </TableRow> 81 - ))} 82 - </TableBody> 83 - </Table> 84 - <PaginationNav 85 - prevOffset={data.prevOffset ?? undefined} 86 - nextOffset={data.nextOffset ?? undefined} 87 - to="/preloading" 88 - /> 89 - </div> 53 + <main className="min-h-screen bg-warm p-6"> 54 + <div className="max-w-4xl mx-auto"> 55 + <div className="section-header"> 56 + <span className="section-header__title">02_preloading</span> 57 + <span className="text-charcoal-muted text-sm">// Route-level prefetch</span> 58 + </div> 59 + 60 + <div className="console-card mb-6"> 61 + <h1 className="text-lg font-mono text-charcoal mb-4"> 62 + National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 63 + </h1> 64 + <Table className="data-table"> 65 + <TableHeader> 66 + <TableRow> 67 + <TableHead>#</TableHead> 68 + <TableHead>Name</TableHead> 69 + <TableHead>Details</TableHead> 70 + </TableRow> 71 + </TableHeader> 72 + <TableBody> 73 + {data.pokemon.map((pokemon) => ( 74 + <TableRow key={pokemon.name}> 75 + <TableCell className="font-mono text-charcoal-muted">{pokemon.id}</TableCell> 76 + <TableCell className="capitalize text-charcoal">{pokemon.name}</TableCell> 77 + <TableCell> 78 + {pokemon.types.map((type) => ( 79 + <span key={type.name} className="type-badge"> 80 + {type.name} 81 + </span> 82 + ))} 83 + </TableCell> 84 + </TableRow> 85 + ))} 86 + </TableBody> 87 + </Table> 88 + <PaginationNav 89 + prevOffset={data.prevOffset ?? undefined} 90 + nextOffset={data.nextOffset ?? undefined} 91 + to="/preloading" 92 + currentOffset={currentOffset} 93 + /> 94 + </div> 95 + </div> 96 + </main> 90 97 ); 91 98 }
+349 -96
src/styles/global.css
··· 4 4 5 5 @custom-variant dark (&:is(.dark *)); 6 6 7 - body { 8 - @apply m-0; 9 - font-family: 10 - -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", 11 - "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 12 - -webkit-font-smoothing: antialiased; 13 - -moz-osx-font-smoothing: grayscale; 14 - } 15 - 16 - code { 17 - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 18 - } 7 + /* JetBrains Mono is loaded via Google Fonts in __root.tsx */ 19 8 20 9 :root { 21 - --background: oklch(1 0 0); 22 - --foreground: oklch(0.141 0.005 285.823); 23 - --card: oklch(1 0 0); 24 - --card-foreground: oklch(0.141 0.005 285.823); 25 - --popover: oklch(1 0 0); 26 - --popover-foreground: oklch(0.141 0.005 285.823); 27 - --primary: oklch(0.21 0.006 285.885); 28 - --primary-foreground: oklch(0.985 0 0); 29 - --secondary: oklch(0.967 0.001 286.375); 30 - --secondary-foreground: oklch(0.21 0.006 285.885); 31 - --muted: oklch(0.967 0.001 286.375); 32 - --muted-foreground: oklch(0.552 0.016 285.938); 33 - --accent: oklch(0.967 0.001 286.375); 34 - --accent-foreground: oklch(0.21 0.006 285.885); 35 - --destructive: oklch(0.577 0.245 27.325); 36 - --destructive-foreground: oklch(0.577 0.245 27.325); 37 - --border: oklch(0.92 0.004 286.32); 38 - --input: oklch(0.92 0.004 286.32); 39 - --ring: oklch(0.871 0.006 286.286); 40 - --chart-1: oklch(0.646 0.222 41.116); 41 - --chart-2: oklch(0.6 0.118 184.704); 42 - --chart-3: oklch(0.398 0.07 227.392); 43 - --chart-4: oklch(0.828 0.189 84.429); 44 - --chart-5: oklch(0.769 0.188 70.08); 45 - --radius: 0.625rem; 46 - --sidebar: oklch(0.985 0 0); 47 - --sidebar-foreground: oklch(0.141 0.005 285.823); 48 - --sidebar-primary: oklch(0.21 0.006 285.885); 49 - --sidebar-primary-foreground: oklch(0.985 0 0); 50 - --sidebar-accent: oklch(0.967 0.001 286.375); 51 - --sidebar-accent-foreground: oklch(0.21 0.006 285.885); 52 - --sidebar-border: oklch(0.92 0.004 286.32); 53 - --sidebar-ring: oklch(0.871 0.006 286.286); 54 - } 10 + /* Developer Console Color Palette */ 11 + --bg-warm: #f5f5f0; 12 + --bg-warm-dark: #e8e8e0; 13 + --text-charcoal: #2a2a2a; 14 + --text-charcoal-light: #4a4a4a; 15 + --text-charcoal-muted: #6a6a6a; 16 + --accent-amber: #f5a623; 17 + --accent-amber-hover: #e09520; 18 + --accent-amber-light: #fff3dc; 19 + --border-hairline: #e5e5e0; 20 + --border-hairline-dark: #d0d0c8; 21 + --status-green: #22c55e; 22 + --status-amber: #f5a623; 23 + --status-red: #ef4444; 55 24 56 - .dark { 57 - --background: oklch(0.141 0.005 285.823); 58 - --foreground: oklch(0.985 0 0); 59 - --card: oklch(0.141 0.005 285.823); 60 - --card-foreground: oklch(0.985 0 0); 61 - --popover: oklch(0.141 0.005 285.823); 62 - --popover-foreground: oklch(0.985 0 0); 63 - --primary: oklch(0.985 0 0); 64 - --primary-foreground: oklch(0.21 0.006 285.885); 65 - --secondary: oklch(0.274 0.006 286.033); 66 - --secondary-foreground: oklch(0.985 0 0); 67 - --muted: oklch(0.274 0.006 286.033); 68 - --muted-foreground: oklch(0.705 0.015 286.067); 69 - --accent: oklch(0.274 0.006 286.033); 70 - --accent-foreground: oklch(0.985 0 0); 71 - --destructive: oklch(0.396 0.141 25.723); 72 - --destructive-foreground: oklch(0.637 0.237 25.331); 73 - --border: oklch(0.274 0.006 286.033); 74 - --input: oklch(0.274 0.006 286.033); 75 - --ring: oklch(0.442 0.017 285.786); 76 - --chart-1: oklch(0.488 0.243 264.376); 77 - --chart-2: oklch(0.696 0.17 162.48); 78 - --chart-3: oklch(0.769 0.188 70.08); 79 - --chart-4: oklch(0.627 0.265 303.9); 80 - --chart-5: oklch(0.645 0.246 16.439); 81 - --sidebar: oklch(0.21 0.006 285.885); 82 - --sidebar-foreground: oklch(0.985 0 0); 83 - --sidebar-primary: oklch(0.488 0.243 264.376); 84 - --sidebar-primary-foreground: oklch(0.985 0 0); 85 - --sidebar-accent: oklch(0.274 0.006 286.033); 86 - --sidebar-accent-foreground: oklch(0.985 0 0); 87 - --sidebar-border: oklch(0.274 0.006 286.033); 88 - --sidebar-ring: oklch(0.442 0.017 285.786); 25 + /* CSS Variables for Tailwind compatibility */ 26 + --background: #f5f5f0; 27 + --foreground: #2a2a2a; 28 + --card: #ffffff; 29 + --card-foreground: #2a2a2a; 30 + --popover: #ffffff; 31 + --popover-foreground: #2a2a2a; 32 + --primary: #f5a623; 33 + --primary-foreground: #2a2a2a; 34 + --secondary: #e8e8e0; 35 + --secondary-foreground: #2a2a2a; 36 + --muted: #e8e8e0; 37 + --muted-foreground: #6a6a6a; 38 + --accent: #f5a623; 39 + --accent-foreground: #2a2a2a; 40 + --destructive: #ef4444; 41 + --destructive-foreground: #ffffff; 42 + --border: #e5e5e0; 43 + --input: #e5e5e0; 44 + --ring: #f5a623; 45 + --radius: 0; 46 + 47 + /* Typography */ 48 + --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", Consolas, "Courier New", monospace; 89 49 } 90 50 91 51 @theme inline { ··· 108 68 --color-border: var(--border); 109 69 --color-input: var(--input); 110 70 --color-ring: var(--ring); 111 - --color-chart-1: var(--chart-1); 112 - --color-chart-2: var(--chart-2); 113 - --color-chart-3: var(--chart-3); 114 - --color-chart-4: var(--chart-4); 115 - --color-chart-5: var(--chart-5); 116 - --radius-sm: calc(var(--radius) - 4px); 117 - --radius-md: calc(var(--radius) - 2px); 118 - --radius-lg: var(--radius); 119 - --radius-xl: calc(var(--radius) + 4px); 120 - --color-sidebar: var(--sidebar); 121 - --color-sidebar-foreground: var(--sidebar-foreground); 122 - --color-sidebar-primary: var(--sidebar-primary); 123 - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 124 - --color-sidebar-accent: var(--sidebar-accent); 125 - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 126 - --color-sidebar-border: var(--sidebar-border); 127 - --color-sidebar-ring: var(--sidebar-ring); 71 + --radius-sm: 0; 72 + --radius-md: 0; 73 + --radius-lg: 0; 74 + --radius-xl: 0; 128 75 } 129 76 130 77 @layer base { 131 78 * { 132 79 @apply border-border outline-ring/50; 133 80 } 81 + 82 + html { 83 + font-family: var(--font-mono); 84 + } 85 + 134 86 body { 135 87 @apply bg-background text-foreground; 88 + font-family: var(--font-mono); 89 + font-size: 14px; 90 + line-height: 1.6; 91 + -webkit-font-smoothing: antialiased; 92 + -moz-osx-font-smoothing: grayscale; 93 + } 94 + 95 + code { 96 + font-family: var(--font-mono); 97 + background: var(--bg-warm-dark); 98 + padding: 0.125rem 0.375rem; 99 + border-radius: 0; 100 + font-size: 0.9em; 101 + } 102 + 103 + h1, 104 + h2, 105 + h3, 106 + h4, 107 + h5, 108 + h6 { 109 + font-family: var(--font-mono); 110 + font-weight: 600; 111 + line-height: 1.3; 112 + } 113 + 114 + h1 { 115 + font-size: 1.75rem; 116 + } 117 + 118 + h2 { 119 + font-size: 1.5rem; 120 + } 121 + 122 + h3 { 123 + font-size: 1.25rem; 124 + } 125 + 126 + a { 127 + color: var(--text-charcoal); 128 + text-decoration: none; 129 + transition: color 0.15s ease; 130 + } 131 + 132 + a:hover { 133 + color: var(--accent-amber-hover); 134 + } 135 + } 136 + 137 + @layer components { 138 + /* Navigation link with left border accent */ 139 + .nav-link { 140 + display: flex; 141 + align-items: center; 142 + gap: 0.5rem; 143 + padding: 0.5rem 1rem; 144 + border-left: 2px solid transparent; 145 + font-size: 0.875rem; 146 + color: var(--text-charcoal-light); 147 + transition: all 0.15s ease; 148 + } 149 + 150 + .nav-link:hover { 151 + color: var(--accent-amber); 152 + border-left-color: var(--accent-amber); 153 + background: var(--accent-amber-light); 154 + } 155 + 156 + .nav-link-active { 157 + color: var(--text-charcoal); 158 + border-left-color: var(--accent-amber); 159 + font-weight: 500; 160 + } 161 + 162 + /* Status dot indicators */ 163 + .status-dot { 164 + width: 8px; 165 + height: 8px; 166 + border-radius: 50%; 167 + display: inline-block; 168 + flex-shrink: 0; 169 + } 170 + 171 + .status-dot--cached { 172 + background-color: var(--status-green); 173 + box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2); 174 + } 175 + 176 + .status-dot--fetching { 177 + background-color: var(--status-amber); 178 + animation: pulse-dot 1.5s ease-in-out infinite; 179 + } 180 + 181 + .status-dot--idle { 182 + background-color: var(--border-hairline-dark); 183 + } 184 + 185 + @keyframes pulse-dot { 186 + 0%, 187 + 100% { 188 + opacity: 1; 189 + box-shadow: 0 0 0 2px rgba(245, 166, 35, 0.2); 190 + } 191 + 50% { 192 + opacity: 0.6; 193 + box-shadow: 0 0 0 4px rgba(245, 166, 35, 0.1); 194 + } 195 + } 196 + 197 + /* Card container with hairline border */ 198 + .console-card { 199 + background: var(--card); 200 + border: 1px solid var(--border-hairline); 201 + padding: 1.5rem; 202 + } 203 + 204 + .console-card:hover { 205 + border-color: var(--border-hairline-dark); 206 + } 207 + 208 + /* Code-style button/pagination item */ 209 + .code-button { 210 + display: inline-flex; 211 + align-items: center; 212 + gap: 0.5rem; 213 + padding: 0.5rem 0.75rem; 214 + border: 1px solid var(--border-hairline); 215 + background: var(--bg-warm); 216 + font-family: var(--font-mono); 217 + font-size: 0.875rem; 218 + color: var(--text-charcoal); 219 + cursor: pointer; 220 + transition: all 0.15s ease; 221 + } 222 + 223 + .code-button:hover:not(:disabled) { 224 + border-color: var(--accent-amber); 225 + background: var(--accent-amber-light); 226 + } 227 + 228 + .code-button:disabled { 229 + opacity: 0.5; 230 + cursor: not-allowed; 231 + } 232 + 233 + .code-button--active { 234 + background: var(--accent-amber); 235 + border-color: var(--accent-amber); 236 + color: var(--text-charcoal); 237 + } 238 + 239 + /* Page number in pagination */ 240 + .page-number { 241 + display: inline-flex; 242 + align-items: center; 243 + justify-content: center; 244 + min-width: 2.5rem; 245 + height: 2rem; 246 + padding: 0 0.5rem; 247 + border: 1px solid var(--border-hairline); 248 + font-family: var(--font-mono); 249 + font-size: 0.875rem; 250 + transition: all 0.15s ease; 251 + } 252 + 253 + .page-number:hover { 254 + border-color: var(--accent-amber); 255 + background: var(--accent-amber-light); 256 + } 257 + 258 + .page-number--active { 259 + background: var(--accent-amber); 260 + border-color: var(--accent-amber); 261 + font-weight: 500; 262 + } 263 + 264 + /* Section header */ 265 + .section-header { 266 + display: flex; 267 + align-items: center; 268 + gap: 0.75rem; 269 + padding: 0.75rem 0; 270 + border-bottom: 1px solid var(--border-hairline); 271 + margin-bottom: 1rem; 272 + } 273 + 274 + .section-header__title { 275 + font-size: 1rem; 276 + font-weight: 600; 277 + text-transform: uppercase; 278 + letter-spacing: 0.05em; 279 + color: var(--text-charcoal-light); 280 + } 281 + 282 + /* Example card for landing page */ 283 + .example-card { 284 + background: var(--card); 285 + border: 1px solid var(--border-hairline); 286 + padding: 1.25rem; 287 + transition: all 0.15s ease; 288 + } 289 + 290 + .example-card:hover { 291 + border-color: var(--accent-amber); 292 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); 293 + } 294 + 295 + .example-card__number { 296 + font-size: 0.75rem; 297 + color: var(--text-charcoal-muted); 298 + text-transform: uppercase; 299 + letter-spacing: 0.1em; 300 + margin-bottom: 0.5rem; 301 + } 302 + 303 + .example-card__title { 304 + font-size: 1rem; 305 + font-weight: 600; 306 + color: var(--text-charcoal); 307 + margin-bottom: 0.5rem; 308 + } 309 + 310 + .example-card__description { 311 + font-size: 0.875rem; 312 + color: var(--text-charcoal-muted); 313 + line-height: 1.5; 314 + } 315 + 316 + /* Table styling */ 317 + .data-table { 318 + width: 100%; 319 + border-collapse: collapse; 320 + font-size: 0.875rem; 321 + } 322 + 323 + .data-table th { 324 + text-align: left; 325 + padding: 0.75rem; 326 + border-bottom: 1px solid var(--border-hairline-dark); 327 + font-weight: 600; 328 + color: var(--text-charcoal-light); 329 + text-transform: uppercase; 330 + font-size: 0.75rem; 331 + letter-spacing: 0.05em; 332 + } 333 + 334 + .data-table td { 335 + padding: 0.75rem; 336 + border-bottom: 1px solid var(--border-hairline); 337 + } 338 + 339 + .data-table tr:hover td { 340 + background: var(--bg-warm); 341 + } 342 + 343 + /* Type badge */ 344 + .type-badge { 345 + display: inline-block; 346 + padding: 0.25rem 0.5rem; 347 + font-size: 0.75rem; 348 + border: 1px solid var(--border-hairline); 349 + background: var(--bg-warm); 350 + margin-right: 0.25rem; 351 + } 352 + } 353 + 354 + @layer utilities { 355 + .font-mono { 356 + font-family: var(--font-mono); 357 + } 358 + 359 + .bg-warm { 360 + background-color: var(--bg-warm); 361 + } 362 + 363 + .bg-warm-dark { 364 + background-color: var(--bg-warm-dark); 365 + } 366 + 367 + .text-charcoal { 368 + color: var(--text-charcoal); 369 + } 370 + 371 + .text-charcoal-light { 372 + color: var(--text-charcoal-light); 373 + } 374 + 375 + .text-charcoal-muted { 376 + color: var(--text-charcoal-muted); 377 + } 378 + 379 + .text-amber { 380 + color: var(--accent-amber); 381 + } 382 + 383 + .border-hairline { 384 + border-color: var(--border-hairline); 385 + } 386 + 387 + .border-hairline-dark { 388 + border-color: var(--border-hairline-dark); 136 389 } 137 390 }