this repo has no description
0
fork

Configure Feed

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

renames

+488 -316
+6 -8
src/components/Header.tsx
··· 1 1 import { Link, useRouterState } from "@tanstack/react-router"; 2 2 import { useQueryClient } from "@tanstack/react-query"; 3 + import { StatusDot } from "~/components/console/status-dot"; 3 4 4 5 interface NavItemProps { 5 6 to: string; ··· 14 15 const currentPath = state.location.pathname; 15 16 const isActive = currentPath === to; 16 17 17 - // Determine cache status based on query keys 18 - // This is a simplified check - in reality you'd check specific query keys 19 18 const hasCachedData = queryClient.getQueryCache().getAll().length > 0; 20 19 const isFetching = state.isLoading; 21 20 22 - // Show status dot for certain routes 23 21 const showStatus = to !== "/"; 24 - let statusClass = "status-dot--idle"; 22 + let status: "cached" | "fetching" | "idle" = "idle"; 25 23 if (isFetching) { 26 - statusClass = "status-dot--fetching"; 24 + status = "fetching"; 27 25 } else if (hasCachedData && isActive) { 28 - statusClass = "status-dot--cached"; 26 + status = "cached"; 29 27 } 30 28 31 29 return ( ··· 35 33 preload={preload} 36 34 className={`nav-link ${isActive ? "nav-link-active" : ""}`} 37 35 > 38 - {showStatus && <span className={`status-dot ${statusClass}`} />} 36 + {showStatus && <StatusDot status={status} />} 39 37 <span>{label}</span> 40 38 </Link> 41 39 ); ··· 43 41 44 42 export default function Header() { 45 43 return ( 46 - <header className="bg-warm border-b border-hairline"> 44 + <header className="bg-[var(--bg-secondary)] border-b border-[var(--border-default)]"> 47 45 <nav className="flex flex-row items-center"> 48 46 <NavItem to="/" label="~/home" /> 49 47 <NavItem to="/basic" label="01_basic" />
+56
src/components/console/code-button.tsx
··· 1 + import { cn } from "~/lib/utils"; 2 + import type { ReactNode } from "react"; 3 + 4 + interface CodeButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { 5 + children: ReactNode; 6 + active?: boolean; 7 + } 8 + 9 + /** 10 + * Button styled for the console aesthetic. 11 + * 12 + * Uses monospace font, hairline border, and amber accent on hover/active. 13 + * Designed for pagination controls and technical actions. 14 + * 15 + * @example 16 + * <CodeButton onClick={prevPage}> 17 + * <span>&lt;</span> 18 + * <span>prev</span> 19 + * </CodeButton> 20 + * 21 + * <CodeButton active>Page 1</CodeButton> 22 + */ 23 + export function CodeButton({ 24 + children, 25 + className, 26 + active = false, 27 + disabled, 28 + ...props 29 + }: CodeButtonProps) { 30 + return ( 31 + <button 32 + className={cn( 33 + "inline-flex items-center gap-2", 34 + "px-3 py-2", 35 + "border border-[var(--border-default)]", 36 + "bg-[var(--bg-secondary)]", 37 + "font-mono text-sm", 38 + "text-[var(--text-primary)]", 39 + "transition-all duration-fast ease-default", 40 + "hover:border-[var(--accent-default)] hover:bg-[var(--accent-subtle)]", 41 + active && [ 42 + "bg-[var(--accent-default)]", 43 + "border-[var(--accent-default)]", 44 + "text-[var(--text-inverse)]", 45 + "font-medium", 46 + ], 47 + disabled && "opacity-50 cursor-not-allowed pointer-events-none", 48 + className, 49 + )} 50 + disabled={disabled} 51 + {...props} 52 + > 53 + {children} 54 + </button> 55 + ); 56 + }
+38
src/components/console/console-card.tsx
··· 1 + import { cn } from "~/lib/utils"; 2 + import type { ReactNode } from "react"; 3 + 4 + interface ConsoleCardProps { 5 + children: ReactNode; 6 + className?: string; 7 + hover?: boolean; 8 + } 9 + 10 + /** 11 + * Primary content container with hairline border. 12 + * 13 + * The console aesthetic uses zero border-radius and visible borders 14 + * to create a technical, information-dense feel. 15 + * 16 + * @example 17 + * <ConsoleCard> 18 + * <h2>Content</h2> 19 + * </ConsoleCard> 20 + */ 21 + export function ConsoleCard({ children, className, hover = false }: ConsoleCardProps) { 22 + return ( 23 + <div 24 + className={cn( 25 + "bg-[var(--bg-card)]", 26 + "border border-[var(--border-default)]", 27 + "p-6", 28 + hover && [ 29 + "transition-colors duration-fast ease-default", 30 + "hover:border-[var(--border-strong)]", 31 + ], 32 + className, 33 + )} 34 + > 35 + {children} 36 + </div> 37 + ); 38 + }
+6
src/components/console/index.ts
··· 1 + export { StatusDot, StatusDotWithLabel } from "./status-dot"; 2 + export { ConsoleCard } from "./console-card"; 3 + export { SectionHeader } from "./section-header"; 4 + export { TypeBadge } from "./type-badge"; 5 + export { CodeButton } from "./code-button"; 6 + export { PokemonTable } from "./pokemon-table";
+90
src/components/console/pokemon-table.tsx
··· 1 + import { 2 + Table, 3 + TableBody, 4 + TableCell, 5 + TableHead, 6 + TableHeader, 7 + TableRow, 8 + } from "~/components/ui/table"; 9 + import { TypeBadge } from "./type-badge"; 10 + import { cn } from "~/lib/utils"; 11 + 12 + interface Pokemon { 13 + id: number; 14 + name: string; 15 + types: Array<{ name: string }>; 16 + } 17 + 18 + interface PokemonTableProps { 19 + pokemon: Pokemon[]; 20 + className?: string; 21 + } 22 + 23 + /** 24 + * Pokemon data table with console styling. 25 + * 26 + * Displays Pokemon ID, name, and types in a monospace-styled table. 27 + * Used consistently across all example routes. 28 + * 29 + * @example 30 + * <PokemonTable pokemon={data.pokemon} /> 31 + */ 32 + export function PokemonTable({ pokemon, className }: PokemonTableProps) { 33 + return ( 34 + <Table className={cn("w-full border-collapse", "text-sm", "font-mono", className)}> 35 + <TableHeader> 36 + <TableRow className="border-b border-[var(--border-strong)]"> 37 + <TableHead 38 + className={cn( 39 + "text-left py-3 px-3", 40 + "font-semibold text-[var(--text-secondary)]", 41 + "uppercase text-xs tracking-wider", 42 + )} 43 + > 44 + # 45 + </TableHead> 46 + <TableHead 47 + className={cn( 48 + "text-left py-3 px-3", 49 + "font-semibold text-[var(--text-secondary)]", 50 + "uppercase text-xs tracking-wider", 51 + )} 52 + > 53 + Name 54 + </TableHead> 55 + <TableHead 56 + className={cn( 57 + "text-left py-3 px-3", 58 + "font-semibold text-[var(--text-secondary)]", 59 + "uppercase text-xs tracking-wider", 60 + )} 61 + > 62 + Details 63 + </TableHead> 64 + </TableRow> 65 + </TableHeader> 66 + <TableBody> 67 + {pokemon.map((p) => ( 68 + <TableRow 69 + key={p.name} 70 + className={cn( 71 + "border-b border-[var(--border-default)]", 72 + "transition-colors duration-fast ease-default", 73 + "hover:bg-[var(--bg-secondary)]", 74 + )} 75 + > 76 + <TableCell className="py-3 px-3 font-mono text-[var(--text-muted)]">{p.id}</TableCell> 77 + <TableCell className="py-3 px-3 capitalize text-[var(--text-primary)]"> 78 + {p.name} 79 + </TableCell> 80 + <TableCell className="py-3 px-3"> 81 + {p.types.map((type) => ( 82 + <TypeBadge key={type.name} type={type.name} /> 83 + ))} 84 + </TableCell> 85 + </TableRow> 86 + ))} 87 + </TableBody> 88 + </Table> 89 + ); 90 + }
+46
src/components/console/section-header.tsx
··· 1 + import { cn } from "~/lib/utils"; 2 + 3 + interface SectionHeaderProps { 4 + title: string; 5 + subtitle?: string; 6 + className?: string; 7 + } 8 + 9 + /** 10 + * Section header with monospace styling. 11 + * 12 + * Used at the top of route pages to display: 13 + * - The example number/title (e.g., "02_preloading") 14 + * - An optional subtitle explaining the pattern 15 + * 16 + * @example 17 + * <SectionHeader 18 + * title="02_preloading" 19 + * subtitle="// Route-level prefetch" 20 + * /> 21 + */ 22 + export function SectionHeader({ title, subtitle, className }: SectionHeaderProps) { 23 + return ( 24 + <div 25 + className={cn( 26 + "flex items-center gap-3", 27 + "py-3", 28 + "border-b border-[var(--border-default)]", 29 + "mb-4", 30 + className, 31 + )} 32 + > 33 + <span 34 + className={cn( 35 + "text-base font-semibold", 36 + "uppercase tracking-wider", 37 + "text-[var(--text-secondary)]", 38 + "font-mono", 39 + )} 40 + > 41 + {title} 42 + </span> 43 + {subtitle && <span className="text-sm text-[var(--text-muted)] font-mono">{subtitle}</span>} 44 + </div> 45 + ); 46 + }
+53
src/components/console/status-dot.tsx
··· 1 + import { cn } from "~/lib/utils"; 2 + 3 + interface StatusDotProps { 4 + status: "cached" | "fetching" | "idle" | "error"; 5 + className?: string; 6 + } 7 + 8 + /** 9 + * Status indicator dot for showing cache/fetching state. 10 + * 11 + * Used in navigation and data tables to indicate: 12 + * - cached: Data is available in cache (green) 13 + * - fetching: Data is currently loading (amber, animated) 14 + * - idle: No active request (neutral) 15 + * - error: Request failed (red) 16 + */ 17 + export function StatusDot({ status, className }: StatusDotProps) { 18 + return ( 19 + <span 20 + className={cn( 21 + "inline-block w-2 h-2 rounded-full flex-shrink-0", 22 + status === "cached" && [ 23 + "bg-[var(--status-cached)]", 24 + "shadow-[0_0_0_2px_oklch(65%_0.2_145/0.2)]", 25 + ], 26 + status === "fetching" && ["bg-[var(--status-fetching)]", "animate-pulse-dot"], 27 + status === "idle" && "bg-[var(--status-idle)]", 28 + status === "error" && [ 29 + "bg-[var(--status-error)]", 30 + "shadow-[0_0_0_2px_oklch(60%_0.2_25/0.2)]", 31 + ], 32 + className, 33 + )} 34 + aria-hidden="true" 35 + /> 36 + ); 37 + } 38 + 39 + /** 40 + * Status dot with label for accessibility 41 + */ 42 + export function StatusDotWithLabel({ 43 + status, 44 + label, 45 + className, 46 + }: StatusDotProps & { label: string }) { 47 + return ( 48 + <span className={cn("inline-flex items-center gap-2", className)}> 49 + <StatusDot status={status} /> 50 + <span className="text-sm text-[var(--text-muted)]">{label}</span> 51 + </span> 52 + ); 53 + }
+36
src/components/console/type-badge.tsx
··· 1 + import { cn } from "~/lib/utils"; 2 + 3 + interface TypeBadgeProps { 4 + type: string; 5 + className?: string; 6 + } 7 + 8 + /** 9 + * Badge for displaying Pokemon types. 10 + * 11 + * Simple bordered label that fits the console aesthetic. 12 + * Multiple badges can be displayed inline for dual-type Pokemon. 13 + * 14 + * @example 15 + * <TypeBadge type="fire" /> 16 + * <TypeBadge type="flying" /> 17 + */ 18 + export function TypeBadge({ type, className }: TypeBadgeProps) { 19 + return ( 20 + <span 21 + className={cn( 22 + "inline-block", 23 + "px-2 py-0.5", 24 + "text-xs font-mono", 25 + "border border-[var(--border-default)]", 26 + "bg-[var(--bg-secondary)]", 27 + "text-[var(--text-secondary)]", 28 + "capitalize", 29 + "mr-1", 30 + className, 31 + )} 32 + > 33 + {type} 34 + </span> 35 + ); 36 + }
+3 -3
src/components/filter-form.tsx
··· 1 1 import { createContext, useContext } from "react"; 2 2 import { Input } from "~/components/ui/input"; 3 3 import { Label } from "~/components/ui/label"; 4 - import { Button } from "./ui/button"; 4 + import { Button } from "~/components/ui/button"; 5 5 6 6 export const FilterSubmitContext = createContext<{ 7 7 handleSubmit: () => void; ··· 26 26 > 27 27 <Label 28 28 htmlFor="name-filter" 29 - className="text-xs font-mono uppercase tracking-wider text-charcoal-light" 29 + className="text-xs font-mono uppercase tracking-wider text-[var(--text-secondary)]" 30 30 > 31 31 Filter by Name 32 32 </Label> ··· 36 36 placeholder="Enter Pokemon name..." 37 37 value={submitContext.nameFilter} 38 38 onChange={(e) => submitContext.updateNameFilter(e.target.value)} 39 - className="mt-2 bg-warm border-hairline focus:border-amber focus:ring-0 rounded-none font-mono text-sm" 39 + className="mt-2 bg-[var(--bg-secondary)] border-[var(--border-default)] focus:border-[var(--accent-default)] focus:ring-0 rounded-none font-mono text-sm" 40 40 /> 41 41 <Button type="submit" className="mt-2"> 42 42 Submit
+17 -3
src/components/pagination-nav.tsx
··· 20 20 search={{ offset: prevOffset }} 21 21 disabled={prevOffset == null} 22 22 className={cn( 23 - "code-button", 23 + "inline-flex items-center gap-2", 24 + "px-3 py-2", 25 + "border border-[var(--border-default)]", 26 + "bg-[var(--bg-secondary)]", 27 + "font-mono text-sm", 28 + "text-[var(--text-primary)]", 29 + "transition-all duration-fast ease-default", 30 + "hover:border-[var(--accent-default)] hover:bg-[var(--accent-subtle)]", 24 31 prevOffset == null && "opacity-50 cursor-not-allowed pointer-events-none", 25 32 )} 26 33 > ··· 29 36 </Link> 30 37 31 38 {/* Separator */} 32 - <span className="px-2 text-charcoal-muted">|</span> 39 + <span className="px-2 text-[var(--text-muted)]">|</span> 33 40 34 41 {/* Next button */} 35 42 <Link ··· 38 45 search={{ offset: nextOffset }} 39 46 disabled={nextOffset == null} 40 47 className={cn( 41 - "code-button", 48 + "inline-flex items-center gap-2", 49 + "px-3 py-2", 50 + "border border-[var(--border-default)]", 51 + "bg-[var(--bg-secondary)]", 52 + "font-mono text-sm", 53 + "text-[var(--text-primary)]", 54 + "transition-all duration-fast ease-default", 55 + "hover:border-[var(--accent-default)] hover:bg-[var(--accent-subtle)]", 42 56 nextOffset == null && "opacity-50 cursor-not-allowed pointer-events-none", 43 57 )} 44 58 >
+9 -41
src/routes/basic.tsx
··· 2 2 import { useSuspenseQuery } from "@tanstack/react-query"; 3 3 import * as v from "valibot"; 4 4 import { PaginationNav } from "~/components/pagination-nav"; 5 - import { 6 - Table, 7 - TableBody, 8 - TableCell, 9 - TableHead, 10 - TableHeader, 11 - TableRow, 12 - } from "~/components/ui/table"; 5 + import { ConsoleCard } from "~/components/console/console-card"; 6 + import { SectionHeader } from "~/components/console/section-header"; 7 + import { PokemonTable } from "~/components/console/pokemon-table"; 13 8 import { POKEMON_LIMIT, getPokemonListQueryFn, getPokemonListQueryKey } from "~/util/pokemon"; 14 9 15 10 const searchParamsSchema = v.object({ 16 11 offset: v.optional(v.number(), 0), 17 12 }); 18 13 19 - // Complete shite 20 14 export const Route = createFileRoute("/basic")({ 21 15 validateSearch: searchParamsSchema, 22 16 component: RouteComponent, ··· 31 25 }); 32 26 33 27 return ( 34 - <main className="min-h-screen bg-warm p-6"> 28 + <main className="min-h-screen bg-[var(--bg-primary)] p-6"> 35 29 <div className="max-w-4xl mx-auto"> 36 - <div className="section-header"> 37 - <span className="section-header__title">01_basic</span> 38 - <span className="text-charcoal-muted text-sm">// No prefetching (baseline)</span> 39 - </div> 30 + <SectionHeader title="01_basic" subtitle="// No prefetching (baseline)" /> 40 31 41 - <div className="console-card mb-6"> 42 - <h1 className="text-lg font-mono text-charcoal mb-4"> 32 + <ConsoleCard className="mb-6"> 33 + <h1 className="text-lg font-mono text-[var(--text-primary)] mb-4"> 43 34 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 44 35 </h1> 45 - <Table className="data-table"> 46 - <TableHeader> 47 - <TableRow> 48 - <TableHead>#</TableHead> 49 - <TableHead>Name</TableHead> 50 - <TableHead>Details</TableHead> 51 - </TableRow> 52 - </TableHeader> 53 - <TableBody> 54 - {data.pokemon.map((pokemon) => ( 55 - <TableRow key={pokemon.name}> 56 - <TableCell className="font-mono text-charcoal-muted">{pokemon.id}</TableCell> 57 - <TableCell className="capitalize text-charcoal">{pokemon.name}</TableCell> 58 - <TableCell> 59 - {pokemon.types.map((type) => ( 60 - <span key={type.name} className="type-badge"> 61 - {type.name} 62 - </span> 63 - ))} 64 - </TableCell> 65 - </TableRow> 66 - ))} 67 - </TableBody> 68 - </Table> 36 + <PokemonTable pokemon={data.pokemon} /> 69 37 <PaginationNav 70 38 prevOffset={data.prevOffset ?? undefined} 71 39 nextOffset={data.nextOffset ?? undefined} 72 40 to="/basic" 73 41 /> 74 - </div> 42 + </ConsoleCard> 75 43 </div> 76 44 </main> 77 45 );
+22 -51
src/routes/debounced-preload-filters.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 2 import { useDebouncedCallback } from "@tanstack/react-pacer"; 3 3 import { queryOptions, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; 4 - import { useCallback } from "react"; 5 - import { useState } from "react"; 4 + import { useCallback, useState } from "react"; 6 5 import * as v from "valibot"; 7 6 import { FilterForm, FilterSubmitContext } from "~/components/filter-form"; 8 7 import { PaginationNav } from "~/components/pagination-nav"; 9 - import { 10 - Table, 11 - TableBody, 12 - TableCell, 13 - TableHead, 14 - TableHeader, 15 - TableRow, 16 - } from "~/components/ui/table"; 8 + import { ConsoleCard } from "~/components/console/console-card"; 9 + import { SectionHeader } from "~/components/console/section-header"; 10 + import { PokemonTable } from "~/components/console/pokemon-table"; 17 11 import { 18 12 POKEMON_LIMIT, 19 13 getFilteredPokemonListQueryKey, ··· 53 47 component: RouteComponent, 54 48 }); 55 49 56 - // And we have debounced preloading 57 50 function PreloadFilterSubmitContextProvider(props: { 58 51 initialName: string; 59 52 handleSubmit: (nameFilter: string) => void; ··· 105 98 106 99 const { data } = useSuspenseQuery(pokemonListOptions); 107 100 108 - // Use the filtered results directly from the server 109 101 const filteredPokemon = data.pokemon; 110 102 111 103 return ( 112 - <main className="min-h-screen bg-warm p-6"> 104 + <main className="min-h-screen bg-[var(--bg-primary)] p-6"> 113 105 <div className="max-w-4xl mx-auto"> 114 - <div className="section-header"> 115 - <span className="section-header__title">06_debounced</span> 116 - <span className="text-charcoal-muted text-sm">// Advanced filter prefetch</span> 117 - </div> 106 + <SectionHeader title="06_debounced" subtitle="// Advanced filter prefetch" /> 118 107 119 108 {/* Filter UI */} 120 - <div className="console-card mb-6"> 121 - <h2 className="text-sm font-semibold mb-4 text-charcoal uppercase tracking-wider"> 109 + <ConsoleCard className="mb-6"> 110 + <h2 className="text-sm font-semibold mb-4 text-[var(--text-primary)] uppercase tracking-wider"> 122 111 Filters 123 112 </h2> 124 - <p className="text-sm text-charcoal-muted mb-4"> 113 + <p className="text-sm text-[var(--text-muted)] mb-4"> 125 114 Preloads results while typing (debounced 100ms) 126 115 </p> 127 116 <PreloadFilterSubmitContextProvider ··· 134 123 > 135 124 <FilterForm /> 136 125 </PreloadFilterSubmitContextProvider> 137 - </div> 126 + </ConsoleCard> 138 127 139 - <div className="console-card"> 140 - <h1 className="text-lg font-mono text-charcoal mb-4"> 128 + <ConsoleCard> 129 + <h1 className="text-lg font-mono text-[var(--text-primary)] mb-4"> 141 130 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 142 - {nameFilter && <span className="text-charcoal-muted"> (filtered: "{nameFilter}")</span>} 131 + {nameFilter && ( 132 + <span className="text-[var(--text-muted)]"> 133 + {" "} 134 + (filtered: &quot;{nameFilter}&quot;) 135 + </span> 136 + )} 143 137 </h1> 144 138 145 - <Table className="data-table"> 146 - <TableHeader> 147 - <TableRow> 148 - <TableHead>#</TableHead> 149 - <TableHead>Name</TableHead> 150 - <TableHead>Details</TableHead> 151 - </TableRow> 152 - </TableHeader> 153 - <TableBody> 154 - {filteredPokemon.map((pokemon) => ( 155 - <TableRow key={pokemon.name}> 156 - <TableCell className="font-mono text-charcoal-muted">{pokemon.id}</TableCell> 157 - <TableCell className="capitalize text-charcoal">{pokemon.name}</TableCell> 158 - <TableCell> 159 - {pokemon.types.map((type) => ( 160 - <span key={type.name} className="type-badge"> 161 - {type.name} 162 - </span> 163 - ))} 164 - </TableCell> 165 - </TableRow> 166 - ))} 167 - </TableBody> 168 - </Table> 139 + <PokemonTable pokemon={filteredPokemon} /> 169 140 170 141 {filteredPokemon.length === 0 && nameFilter && ( 171 - <div className="text-center py-8 text-charcoal-muted font-mono"> 172 - No Pokemon found matching "{nameFilter}" 142 + <div className="text-center py-8 text-[var(--text-muted)] font-mono"> 143 + No Pokemon found matching &quot;{nameFilter}&quot; 173 144 </div> 174 145 )} 175 146 ··· 179 150 nextOffset={data.nextOffset ?? undefined} 180 151 to="/debounced-preload-filters" 181 152 /> 182 - </div> 153 + </ConsoleCard> 183 154 </div> 184 155 </main> 185 156 );
+20 -48
src/routes/filters.tsx
··· 4 4 import * as v from "valibot"; 5 5 import { FilterForm, FilterSubmitContext } from "~/components/filter-form"; 6 6 import { PaginationNav } from "~/components/pagination-nav"; 7 - import { 8 - Table, 9 - TableBody, 10 - TableCell, 11 - TableHead, 12 - TableHeader, 13 - TableRow, 14 - } from "~/components/ui/table"; 7 + import { ConsoleCard } from "~/components/console/console-card"; 8 + import { SectionHeader } from "~/components/console/section-header"; 9 + import { PokemonTable } from "~/components/console/pokemon-table"; 15 10 import { 16 11 POKEMON_LIMIT, 17 12 getFilteredPokemonListQueryKey, ··· 23 18 name: v.optional(v.string(), ""), 24 19 }); 25 20 26 - // Now we add in filtering, this is the basic version that does no preloading 27 21 function FilterSubmitContextProvider(props: { 28 22 initialName: string; 29 23 handleSubmit: (nameFilter: string) => void; ··· 75 69 76 70 const { data } = useSuspenseQuery(pokemonListOptions); 77 71 78 - // Use the filtered results directly from the server 79 72 const filteredPokemon = data.pokemon; 80 73 81 74 return ( 82 - <main className="min-h-screen bg-warm p-6"> 75 + <main className="min-h-screen bg-[var(--bg-primary)] p-6"> 83 76 <div className="max-w-4xl mx-auto"> 84 - <div className="section-header"> 85 - <span className="section-header__title">05_filters</span> 86 - <span className="text-charcoal-muted text-sm">// Search with prefetch</span> 87 - </div> 77 + <SectionHeader title="05_filters" subtitle="// Search with prefetch" /> 88 78 89 79 {/* Filter UI */} 90 - <div className="console-card mb-6"> 91 - <h2 className="text-sm font-semibold mb-4 text-charcoal uppercase tracking-wider"> 80 + <ConsoleCard className="mb-6"> 81 + <h2 className="text-sm font-semibold mb-4 text-[var(--text-primary)] uppercase tracking-wider"> 92 82 Filters 93 83 </h2> 94 84 <FilterSubmitContextProvider ··· 102 92 > 103 93 <FilterForm /> 104 94 </FilterSubmitContextProvider> 105 - </div> 95 + </ConsoleCard> 106 96 107 - <div className="console-card"> 108 - <h1 className="text-lg font-mono text-charcoal mb-4"> 97 + <ConsoleCard> 98 + <h1 className="text-lg font-mono text-[var(--text-primary)] mb-4"> 109 99 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 110 - {nameFilter && <span className="text-charcoal-muted"> (filtered: "{nameFilter}")</span>} 100 + {nameFilter && ( 101 + <span className="text-[var(--text-muted)]"> 102 + {" "} 103 + (filtered: &quot;{nameFilter}&quot;) 104 + </span> 105 + )} 111 106 </h1> 112 107 113 - <Table className="data-table"> 114 - <TableHeader> 115 - <TableRow> 116 - <TableHead>#</TableHead> 117 - <TableHead>Name</TableHead> 118 - <TableHead>Details</TableHead> 119 - </TableRow> 120 - </TableHeader> 121 - <TableBody> 122 - {filteredPokemon.map((pokemon) => ( 123 - <TableRow key={pokemon.name}> 124 - <TableCell className="font-mono text-charcoal-muted">{pokemon.id}</TableCell> 125 - <TableCell className="capitalize text-charcoal">{pokemon.name}</TableCell> 126 - <TableCell> 127 - {pokemon.types.map((type) => ( 128 - <span key={type.name} className="type-badge"> 129 - {type.name} 130 - </span> 131 - ))} 132 - </TableCell> 133 - </TableRow> 134 - ))} 135 - </TableBody> 136 - </Table> 108 + <PokemonTable pokemon={filteredPokemon} /> 137 109 138 110 {filteredPokemon.length === 0 && nameFilter && ( 139 - <div className="text-center py-8 text-charcoal-muted font-mono"> 140 - No Pokemon found matching "{nameFilter}" 111 + <div className="text-center py-8 text-[var(--text-muted)] font-mono"> 112 + No Pokemon found matching &quot;{nameFilter}&quot; 141 113 </div> 142 114 )} 143 115 ··· 147 119 nextOffset={data.nextOffset ?? undefined} 148 120 to="/filters" 149 121 /> 150 - </div> 122 + </ConsoleCard> 151 123 </div> 152 124 </main> 153 125 );
+59 -38
src/routes/index.tsx
··· 1 1 import { createFileRoute, Link } from "@tanstack/react-router"; 2 + import { StatusDot } from "~/components/console/status-dot"; 3 + import { ConsoleCard } from "~/components/console/console-card"; 2 4 3 5 export const Route = createFileRoute("/")({ 4 6 component: LandingPage, ··· 19 21 <div className="example-card__number">Example {number}</div> 20 22 <h3 className="example-card__title">{title}</h3> 21 23 <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"> 24 + <div className="mt-4 p-3 bg-[var(--bg-secondary)] border border-[var(--border-default)] font-mono text-xs text-[var(--text-muted)] overflow-x-auto"> 23 25 <code>{codeSnippet}</code> 24 26 </div> 25 27 </div> ··· 80 82 ]; 81 83 82 84 return ( 83 - <main className="min-h-screen bg-warm"> 85 + <main className="min-h-screen bg-[var(--bg-primary)]"> 84 86 {/* Hero Section */} 85 - <section className="border-b border-hairline bg-white"> 87 + <section className="border-b border-[var(--border-default)] bg-[var(--bg-card)]"> 86 88 <div className="max-w-5xl mx-auto px-6 py-16"> 87 89 <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 + <StatusDot status="cached" /> 91 + <span className="text-[var(--text-muted)] text-sm font-mono uppercase tracking-wider"> 90 92 TanStack Router Demo 91 93 </span> 92 94 </div> 93 - <h1 className="text-4xl font-mono font-semibold text-charcoal mb-4"> 95 + <h1 className="text-4xl font-mono font-semibold text-[var(--text-primary)] mb-4"> 94 96 Prefetching Patterns 95 97 </h1> 96 - <p className="text-lg text-charcoal-light max-w-2xl leading-relaxed"> 98 + <p className="text-lg text-[var(--text-secondary)] max-w-2xl leading-relaxed"> 97 99 A developer console for exploring data prefetching techniques in modern React 98 100 applications. Learn how different patterns affect perceived performance and user 99 101 experience. 100 102 </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> 103 + <div className="mt-8 flex items-center gap-4 text-sm text-[var(--text-muted)]"> 104 + <StatusDotWithLabel status="cached" label="Cached" /> 105 + <StatusDotWithLabel status="fetching" label="Fetching" /> 106 + <StatusDotWithLabel status="idle" label="Idle" /> 114 107 </div> 115 108 </div> 116 109 </section> 117 110 118 111 {/* Examples Grid */} 119 112 <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> 113 + <div className="flex items-center gap-3 py-3 border-b border-[var(--border-default)] mb-4"> 114 + <span className="text-base font-semibold uppercase tracking-wider text-[var(--text-secondary)] font-mono"> 115 + Examples 116 + </span> 117 + <span className="text-[var(--text-muted)] text-sm font-mono"> 118 + // Select one to explore 119 + </span> 123 120 </div> 124 121 125 122 <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> ··· 131 128 132 129 {/* Concepts Section */} 133 130 <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> 131 + <div className="flex items-center gap-3 py-3 border-b border-[var(--border-default)] mb-4"> 132 + <span className="text-base font-semibold uppercase tracking-wider text-[var(--text-secondary)] font-mono"> 133 + Key Concepts 134 + </span> 136 135 </div> 137 136 138 - <div className="console-card"> 137 + <ConsoleCard> 139 138 <div className="grid grid-cols-1 md:grid-cols-3 gap-8"> 140 139 <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"> 140 + <h4 className="font-mono font-semibold text-[var(--text-primary)] mb-2"> 141 + Route Loaders 142 + </h4> 143 + <p className="text-sm text-[var(--text-secondary)] leading-relaxed"> 143 144 Loaders run before the route component renders, making them ideal for initiating 144 145 data fetches early in the navigation lifecycle. 145 146 </p> 146 147 </div> 147 148 <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"> 149 + <h4 className="font-mono font-semibold text-[var(--text-primary)] mb-2"> 150 + Intent Preloading 151 + </h4> 152 + <p className="text-sm text-[var(--text-secondary)] leading-relaxed"> 150 153 By observing user intent (hover, focus), we can predict navigation and preload data 151 154 before the click event fires. 152 155 </p> 153 156 </div> 154 157 <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"> 158 + <h4 className="font-mono font-semibold text-[var(--text-primary)] mb-2"> 159 + Query Caching 160 + </h4> 161 + <p className="text-sm text-[var(--text-secondary)] leading-relaxed"> 157 162 TanStack Query maintains a cache of fetched data. Subsequent requests for the same 158 163 data return instantly from cache. 159 164 </p> 160 165 </div> 161 166 </div> 162 - </div> 167 + </ConsoleCard> 163 168 </section> 164 169 165 170 {/* 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"> 171 + <footer className="border-t border-[var(--border-default)] bg-[var(--bg-card)]"> 172 + <div className="max-w-5xl mx-auto px-6 py-6 text-sm text-[var(--text-muted)]"> 168 173 <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> 174 + Built with <span className="text-[var(--accent-default)]">TanStack Router</span> +{" "} 175 + <span className="text-[var(--accent-default)]">TanStack Query</span> +{" "} 176 + <span className="text-[var(--accent-default)]">TanStack Start</span> 172 177 </p> 173 178 </div> 174 179 </footer> 175 180 </main> 176 181 ); 177 182 } 183 + 184 + // Helper component for the landing page 185 + function StatusDotWithLabel({ 186 + status, 187 + label, 188 + }: { 189 + status: "cached" | "fetching" | "idle"; 190 + label: string; 191 + }) { 192 + return ( 193 + <span className="inline-flex items-center gap-2"> 194 + <StatusDot status={status} /> 195 + <span>{label}</span> 196 + </span> 197 + ); 198 + }
+9 -41
src/routes/intent-preloading.tsx
··· 2 2 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 3 3 import * as v from "valibot"; 4 4 import { PaginationNav } from "~/components/pagination-nav"; 5 - import { 6 - Table, 7 - TableBody, 8 - TableCell, 9 - TableHead, 10 - TableHeader, 11 - TableRow, 12 - } from "~/components/ui/table"; 5 + import { ConsoleCard } from "~/components/console/console-card"; 6 + import { SectionHeader } from "~/components/console/section-header"; 7 + import { PokemonTable } from "~/components/console/pokemon-table"; 13 8 import { POKEMON_LIMIT, getPokemonListQueryKey, getPokemonListQueryFn } from "~/util/pokemon"; 14 9 15 10 const searchParamsSchema = v.object({ 16 11 offset: v.optional(v.number(), 0), 17 12 }); 18 13 19 - // Now we're sucking diesel 20 14 export const Route = createFileRoute("/intent-preloading")({ 21 15 validateSearch: searchParamsSchema, 22 16 loaderDeps: ({ search }) => ({ ··· 47 41 const { data } = useSuspenseQuery(pokemonListOptions); 48 42 49 43 return ( 50 - <main className="min-h-screen bg-warm p-6"> 44 + <main className="min-h-screen bg-[var(--bg-primary)] p-6"> 51 45 <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> 46 + <SectionHeader title="03_intent-preloading" subtitle="// Hover-based prefetch" /> 56 47 57 - <div className="console-card mb-6"> 58 - <h1 className="text-lg font-mono text-charcoal mb-4"> 48 + <ConsoleCard className="mb-6"> 49 + <h1 className="text-lg font-mono text-[var(--text-primary)] mb-4"> 59 50 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 60 51 </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> 52 + <PokemonTable pokemon={data.pokemon} /> 85 53 <PaginationNav 86 54 prefetch="intent" 87 55 prevOffset={data.prevOffset ?? undefined} 88 56 nextOffset={data.nextOffset ?? undefined} 89 57 to="/intent-preloading" 90 58 /> 91 - </div> 59 + </ConsoleCard> 92 60 </div> 93 61 </main> 94 62 );
+9 -41
src/routes/pagination.tsx
··· 2 2 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 3 3 import * as v from "valibot"; 4 4 import { PaginationNav } from "~/components/pagination-nav"; 5 - import { 6 - Table, 7 - TableBody, 8 - TableCell, 9 - TableHead, 10 - TableHeader, 11 - TableRow, 12 - } from "~/components/ui/table"; 5 + import { ConsoleCard } from "~/components/console/console-card"; 6 + import { SectionHeader } from "~/components/console/section-header"; 7 + import { PokemonTable } from "~/components/console/pokemon-table"; 13 8 import { POKEMON_LIMIT, getPokemonListQueryKey, getPokemonListQueryFn } from "~/util/pokemon"; 14 9 15 10 const searchParamsSchema = v.object({ 16 11 offset: v.optional(v.number(), 0), 17 12 }); 18 13 19 - // Gold standard 20 14 export const Route = createFileRoute("/pagination")({ 21 15 validateSearch: searchParamsSchema, 22 16 loaderDeps: ({ search }) => ({ ··· 47 41 const { data } = useSuspenseQuery(pokemonListOptions); 48 42 49 43 return ( 50 - <main className="min-h-screen bg-warm p-6"> 44 + <main className="min-h-screen bg-[var(--bg-primary)] p-6"> 51 45 <div className="max-w-4xl mx-auto"> 52 - <div className="section-header"> 53 - <span className="section-header__title">04_pagination</span> 54 - <span className="text-charcoal-muted text-sm">// Preloading next/prev pages</span> 55 - </div> 46 + <SectionHeader title="04_pagination" subtitle="// Preloading next/prev pages" /> 56 47 57 - <div className="console-card mb-6"> 58 - <h1 className="text-lg font-mono text-charcoal mb-4"> 48 + <ConsoleCard className="mb-6"> 49 + <h1 className="text-lg font-mono text-[var(--text-primary)] mb-4"> 59 50 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 60 51 </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> 52 + <PokemonTable pokemon={data.pokemon} /> 85 53 <PaginationNav 86 54 prefetch="viewport" 87 55 prevOffset={data.prevOffset ?? undefined} 88 56 nextOffset={data.nextOffset ?? undefined} 89 57 to="/pagination" 90 58 /> 91 - </div> 59 + </ConsoleCard> 92 60 </div> 93 61 </main> 94 62 );
+9 -42
src/routes/preloading.tsx
··· 2 2 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 3 3 import * as v from "valibot"; 4 4 import { PaginationNav } from "~/components/pagination-nav"; 5 - import { 6 - Table, 7 - TableBody, 8 - TableCell, 9 - TableHead, 10 - TableHeader, 11 - TableRow, 12 - } from "~/components/ui/table"; 5 + import { ConsoleCard } from "~/components/console/console-card"; 6 + import { SectionHeader } from "~/components/console/section-header"; 7 + import { PokemonTable } from "~/components/console/pokemon-table"; 13 8 import { POKEMON_LIMIT, getPokemonListQueryKey, getPokemonListQueryFn } from "~/util/pokemon"; 14 9 15 10 const searchParamsSchema = v.object({ 16 11 offset: v.optional(v.number(), 0), 17 12 }); 18 13 19 - // Barely better than basic 20 - // If the component tree is large, the speedup would actually be important 21 14 export const Route = createFileRoute("/preloading")({ 22 15 validateSearch: searchParamsSchema, 23 16 loaderDeps: ({ search }) => ({ ··· 50 43 }); 51 44 52 45 return ( 53 - <main className="min-h-screen bg-warm p-6"> 46 + <main className="min-h-screen bg-[var(--bg-primary)] p-6"> 54 47 <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> 48 + <SectionHeader title="02_preloading" subtitle="// Route-level prefetch" /> 59 49 60 - <div className="console-card mb-6"> 61 - <h1 className="text-lg font-mono text-charcoal mb-4"> 50 + <ConsoleCard className="mb-6"> 51 + <h1 className="text-lg font-mono text-[var(--text-primary)] mb-4"> 62 52 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 63 53 </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> 54 + <PokemonTable pokemon={data.pokemon} /> 88 55 <PaginationNav 89 56 prevOffset={data.prevOffset ?? undefined} 90 57 nextOffset={data.nextOffset ?? undefined} 91 58 to="/preloading" 92 59 /> 93 - </div> 60 + </ConsoleCard> 94 61 </div> 95 62 </main> 96 63 );