this repo has no description
0
fork

Configure Feed

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

chapter redesign

+556 -159
+103
src/components/chapter-navigation.tsx
··· 1 + "use client"; 2 + 3 + import { Link, useNavigate, useRouterState } from "@tanstack/react-router"; 4 + import { chapters, getChapterByPath, getChapterNeighbors, type ChapterPath } from "~/lib/chapters"; 5 + import { cn } from "~/lib/utils"; 6 + 7 + export function ChapterSelect() { 8 + const navigate = useNavigate(); 9 + const pathname = useRouterState({ select: (state) => state.location.pathname }); 10 + const currentChapter = getChapterByPath(pathname); 11 + 12 + return ( 13 + <label className="chapter-select"> 14 + <span className="sr-only">Jump to chapter</span> 15 + <select 16 + value={currentChapter?.to ?? "/"} 17 + onChange={(event) => { 18 + const to = event.currentTarget.value; 19 + 20 + if (to === "/") { 21 + void navigate({ to: "/" }); 22 + return; 23 + } 24 + 25 + void navigate({ to: to as ChapterPath }); 26 + }} 27 + className="chapter-select__control" 28 + aria-label="Jump to chapter" 29 + > 30 + <option value="/">Contents</option> 31 + {chapters.map((chapter) => ( 32 + <option key={chapter.to} value={chapter.to}> 33 + {chapter.navLabel} 34 + </option> 35 + ))} 36 + </select> 37 + </label> 38 + ); 39 + } 40 + 41 + export function ChapterPager() { 42 + const pathname = useRouterState({ select: (state) => state.location.pathname }); 43 + const { current, previous, next } = getChapterNeighbors(pathname); 44 + 45 + if (!current) { 46 + return null; 47 + } 48 + 49 + return ( 50 + <nav className="chapter-pager" aria-label="Chapter navigation"> 51 + <ChapterPagerLink direction="previous" chapter={previous} /> 52 + 53 + <Link 54 + to="/" 55 + className="chapter-pager__contents focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color)" 56 + > 57 + Contents 58 + </Link> 59 + 60 + <ChapterPagerLink direction="next" chapter={next} /> 61 + </nav> 62 + ); 63 + } 64 + 65 + function ChapterPagerLink(props: { 66 + direction: "previous" | "next"; 67 + chapter: (typeof chapters)[number] | undefined; 68 + }) { 69 + const { direction, chapter } = props; 70 + const isPrevious = direction === "previous"; 71 + 72 + if (!chapter) { 73 + return ( 74 + <span 75 + className={cn( 76 + "chapter-pager__link chapter-pager__link--disabled", 77 + !isPrevious && "text-right", 78 + )} 79 + > 80 + <span className="chapter-pager__kicker">{isPrevious ? "Previous" : "Next"}</span> 81 + <span className="chapter-pager__title">End of sequence</span> 82 + </span> 83 + ); 84 + } 85 + 86 + return ( 87 + <Link 88 + to={chapter.to} 89 + preload="intent" 90 + className={cn( 91 + "chapter-pager__link focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color)", 92 + !isPrevious && "text-right", 93 + )} 94 + > 95 + <span className="chapter-pager__kicker">{isPrevious ? "Previous" : "Next"}</span> 96 + <span className="chapter-pager__title"> 97 + {isPrevious ? "< " : ""} 98 + {chapter.number} / {chapter.title} 99 + {!isPrevious ? " >" : ""} 100 + </span> 101 + </Link> 102 + ); 103 + }
+20 -27
src/components/header.tsx
··· 1 - import { NavItem } from "./nav-item"; 1 + import { Link } from "@tanstack/react-router"; 2 + import { ChapterSelect } from "./chapter-navigation"; 2 3 import { ThemeToggle } from "./theme-toggle"; 3 4 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", preload: "intent" as const }, 10 - { to: "/filters", label: "05_filters", preload: "intent" as const }, 11 - { to: "/debounced-preload-filters", label: "06_debounced-filters", preload: "intent" as const }, 12 - { to: "/live-query", label: "07_live-query", preload: "intent" as const }, 13 - { to: "/live-query-filters", label: "08_live-query-filters", preload: "intent" as const }, 14 - ]; 15 - 16 5 export function Header() { 17 6 return ( 18 7 <header className="sticky top-0 z-40 bg-(--bg-secondary) border-b border-(--border-default)"> 19 - <div className="flex items-center"> 20 - <nav 21 - className="flex flex-1 flex-row items-center overflow-x-auto scrollbar-thin" 22 - aria-label="Main navigation" 8 + <div className="mx-auto flex min-h-12 max-w-7xl items-center gap-3 px-4 sm:px-6"> 9 + <Link 10 + to="/" 11 + className="flex min-w-0 flex-1 items-baseline gap-3 py-3 text-(--text-primary) hover:text-(--accent-default) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color)" 23 12 > 24 - {navItems.map((item) => ( 25 - <NavItem key={item.to} to={item.to} label={item.label} preload={item.preload} /> 26 - ))} 13 + <span className="font-display text-base font-semibold leading-none sm:hidden"> 14 + Prefetching 15 + </span> 16 + <span className="hidden font-display text-lg font-semibold leading-none sm:inline"> 17 + Prefetching Patterns 18 + </span> 19 + <span className="hidden text-xs font-mono uppercase text-(--text-muted) sm:inline"> 20 + Contents 21 + </span> 22 + </Link> 23 + 24 + <nav className="flex shrink-0 items-center gap-2" aria-label="Reader navigation"> 25 + <ChapterSelect /> 26 + <ThemeToggle /> 27 27 </nav> 28 - <div className="relative flex-hteshrink-0"> 29 - <div 30 - className="pointer-events-none absolute -left-6 top-0 bottom-0 w-6 bg-linear-to-l from-(--bg-secondary) to-transparent" 31 - aria-hidden="true" 32 - /> 33 - <ThemeToggle /> 34 - </div> 35 28 </div> 36 29 </header> 37 30 );
+5 -7
src/components/strategy-article.tsx
··· 8 8 9 9 export function StrategyArticle({ eyebrow, title }: StrategyArticleProps) { 10 10 return ( 11 - <article className="sticky top-6 rounded-[2rem] border border-(--border-default) bg-(--bg-secondary) p-6 shadow-[8px_8px_0_var(--border-default)] md:p-8"> 12 - <p className="mb-5 font-mono text-xs uppercase tracking-[0.3em] text-(--accent-default)"> 13 - {eyebrow} 14 - </p> 15 - <h2 className="mb-6 text-4xl font-black uppercase leading-none tracking-tight text-(--text-primary) md:text-5xl"> 11 + <article className="border border-(--border-default) bg-(--bg-secondary) p-6 md:p-8 xl:sticky xl:top-20"> 12 + <p className="mb-5 font-mono text-xs uppercase text-(--accent-default)">{eyebrow}</p> 13 + <h2 className="mb-6 text-3xl font-semibold uppercase leading-tight text-(--text-primary) md:text-4xl"> 16 14 {title} 17 15 </h2> 18 - <div className="space-y-5 font-serif text-lg leading-8 text-(--text-secondary)"> 16 + <div className="space-y-5 text-sm leading-7 text-(--text-secondary)"> 19 17 <p>{loremIpsum}</p> 20 18 <p>{loremIpsum}</p> 21 19 </div> 22 - <div className="mt-8 border-t border-(--border-default) pt-5 font-mono text-xs uppercase tracking-widest text-(--text-muted)"> 20 + <div className="mt-8 border-t border-(--border-default) pt-5 font-mono text-xs uppercase text-(--text-muted)"> 23 21 Strategy notes / walkthrough 24 22 </div> 25 23 </article>
+8 -4
src/components/strategy-page-layout.tsx
··· 1 + import { ChapterPager } from "~/components/chapter-navigation"; 1 2 import { StrategyArticle } from "~/components/strategy-article"; 2 3 3 4 interface StrategyPageLayoutProps { ··· 12 13 children, 13 14 }: StrategyPageLayoutProps) { 14 15 return ( 15 - <div className="grid gap-8 xl:grid-cols-[minmax(0,0.9fr)_minmax(34rem,1.1fr)] xl:items-start"> 16 - <StrategyArticle eyebrow={articleEyebrow} title={articleTitle} /> 17 - <div className="min-w-0">{children}</div> 18 - </div> 16 + <> 17 + <div className="grid gap-8 xl:grid-cols-[minmax(0,0.9fr)_minmax(34rem,1.1fr)] xl:items-start"> 18 + <StrategyArticle eyebrow={articleEyebrow} title={articleTitle} /> 19 + <div className="min-w-0">{children}</div> 20 + </div> 21 + <ChapterPager /> 22 + </> 19 23 ); 20 24 }
+119
src/lib/chapters.ts
··· 1 + export const chapterGroups = [ 2 + { 3 + label: "Foundations", 4 + description: "Start with route loading, then add preloading before the reader clicks.", 5 + chapters: [ 6 + { 7 + number: "01", 8 + to: "/basic", 9 + navLabel: "01 / Basic", 10 + title: "Basic", 11 + routeTitle: "01_basic", 12 + summary: "Baseline fetching. Data loads only after the route renders.", 13 + tags: ["suspense", "query"], 14 + }, 15 + { 16 + number: "02", 17 + to: "/preloading", 18 + navLabel: "02 / Preloading", 19 + title: "Preloading", 20 + routeTitle: "02_preloading", 21 + summary: "Route loader prefetch. The next screen can render from warmed query data.", 22 + tags: ["loader", "prefetch"], 23 + }, 24 + { 25 + number: "03", 26 + to: "/intent-preloading", 27 + navLabel: "03 / Intent preloading", 28 + title: "Intent preloading", 29 + routeTitle: "03_intent-preloading", 30 + summary: "Hover and focus intent preloads the route before activation.", 31 + tags: ["intent", "link"], 32 + }, 33 + ], 34 + }, 35 + { 36 + label: "Route State", 37 + description: "Make search params part of the lesson with pages, filters, and typing intent.", 38 + chapters: [ 39 + { 40 + number: "04", 41 + to: "/pagination", 42 + navLabel: "04 / Pagination", 43 + title: "Pagination", 44 + routeTitle: "04_pagination", 45 + summary: "Adjacent pages preload so previous and next navigation stays warm.", 46 + tags: ["pagination", "viewport"], 47 + }, 48 + { 49 + number: "05", 50 + to: "/filters", 51 + navLabel: "05 / Filters", 52 + title: "Filters", 53 + routeTitle: "05_filters", 54 + summary: "Submitted search params drive filtered data and prefetch behavior.", 55 + tags: ["search params", "filter"], 56 + }, 57 + { 58 + number: "06", 59 + to: "/debounced-preload-filters", 60 + navLabel: "06 / Debounced filters", 61 + title: "Debounced filters", 62 + routeTitle: "06_debounced", 63 + summary: "Typing preloads likely filtered results before the form is submitted.", 64 + tags: ["debounce", "prefetch"], 65 + }, 66 + ], 67 + }, 68 + { 69 + label: "Local-First", 70 + description: "Move from fetching routes to subscribing to synced collections.", 71 + chapters: [ 72 + { 73 + number: "07", 74 + to: "/live-query", 75 + navLabel: "07 / Live query", 76 + title: "Live query", 77 + routeTitle: "07_live-query", 78 + summary: "TanStack DB reads from an Electric SQL synced collection.", 79 + tags: ["electric", "live query"], 80 + }, 81 + { 82 + number: "08", 83 + to: "/live-query-filters", 84 + navLabel: "08 / Live query filters", 85 + title: "Live query filters", 86 + routeTitle: "08_live-query-filters", 87 + summary: "Reactive filtered live queries update from local synced data.", 88 + tags: ["reactive", "sync"], 89 + }, 90 + ], 91 + }, 92 + ] as const; 93 + 94 + export const chapters = [ 95 + ...chapterGroups[0].chapters, 96 + ...chapterGroups[1].chapters, 97 + ...chapterGroups[2].chapters, 98 + ] as const; 99 + 100 + export type Chapter = (typeof chapters)[number]; 101 + export type ChapterPath = Chapter["to"]; 102 + 103 + export function getChapterByPath(pathname: string) { 104 + return chapters.find((chapter) => chapter.to === pathname); 105 + } 106 + 107 + export function getChapterNeighbors(pathname: string) { 108 + const index = chapters.findIndex((chapter) => chapter.to === pathname); 109 + 110 + if (index === -1) { 111 + return { current: undefined, previous: undefined, next: undefined }; 112 + } 113 + 114 + return { 115 + current: chapters[index], 116 + previous: chapters[index - 1], 117 + next: chapters[index + 1], 118 + }; 119 + }
+79 -121
src/routes/index.tsx
··· 2 2 import { createServerFn } from "@tanstack/react-start"; 3 3 import { renderServerComponent } from "@tanstack/react-start/rsc"; 4 4 import { StatusDot, StatusDotWithLabel } from "~/components/console/status-dot"; 5 + import { chapterGroups } from "~/lib/chapters"; 5 6 6 7 export const Route = createFileRoute("/")({ 7 8 loader: async () => { ··· 20 21 return <>{landingPage}</>; 21 22 } 22 23 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 - 67 24 function LandingPageDocument() { 68 25 return ( 69 26 <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"> 27 + <section className="border-b border-(--border-default) bg-(--bg-secondary)"> 28 + <div className="mx-auto grid max-w-6xl gap-10 px-6 py-12 lg:grid-cols-[minmax(0,0.78fr)_minmax(20rem,0.42fr)] lg:items-end"> 29 + <div> 30 + <div className="flex items-center gap-3 mb-8"> 31 + <StatusDot status="cached" /> 32 + <span className="text-sm font-mono uppercase text-(--text-muted)"> 33 + Interactive technical book 34 + </span> 35 + </div> 36 + <h1 className="max-w-3xl text-4xl font-semibold text-(--text-primary) mb-5 leading-[1.05] sm:text-5xl"> 37 + Prefetching Patterns 38 + </h1> 39 + <p className="text-base text-(--text-secondary) max-w-2xl leading-relaxed"> 40 + A chaptered learning lab for comparing no preloading, route preloading, intent 41 + preloading, search-param driven data, and synced local collections. 42 + </p> 43 + </div> 44 + 45 + <aside className="toc-status" aria-label="Reading status legend"> 74 46 <StatusDot status="cached" /> 75 - <span className="text-sm font-mono uppercase tracking-wider text-(--text-muted)"> 76 - TanStack Router Demo 47 + <span className="text-xs font-mono uppercase text-(--text-muted)"> 48 + Read in order. Each chapter keeps the console beside the explanation. 77 49 </span> 78 - </div> 79 - <h1 className="text-[clamp(2rem,5vw,3.5rem)] font-semibold text-(--text-primary) mb-6 leading-[1.1]"> 80 - Prefetching Patterns 81 - </h1> 82 - <p className="text-base text-(--text-secondary) max-w-2xl leading-relaxed"> 83 - A developer console for exploring data prefetching techniques in modern React 84 - applications. Learn how different patterns affect perceived performance and user 85 - experience. 86 - </p> 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)"> 92 - <StatusDotWithLabel status="cached" label="Cached" /> 93 - <StatusDotWithLabel status="fetching" label="Fetching" /> 94 - <StatusDotWithLabel status="idle" label="Idle" /> 95 - </div> 50 + <div className="mt-5 grid gap-3 text-sm font-mono text-(--text-muted)"> 51 + <StatusDotWithLabel status="cached" label="Cached route data" /> 52 + <StatusDotWithLabel status="fetching" label="Fetching in progress" /> 53 + <StatusDotWithLabel status="idle" label="Idle route" /> 54 + </div> 55 + </aside> 96 56 </div> 97 57 </section> 98 58 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 59 + <section className="mx-auto w-full max-w-6xl px-6 py-12"> 60 + <h2 className="mb-8 border-b border-(--border-default) pb-4 text-xl font-semibold text-(--text-primary)"> 61 + Table of contents 103 62 </h2> 104 63 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> 64 + <div className="grid gap-10"> 65 + {chapterGroups.map((group) => ( 66 + <section key={group.label} className="toc-group" aria-labelledby={`toc-${group.label}`}> 67 + <div className="toc-group__heading"> 68 + <h3 id={`toc-${group.label}`} className="toc-group__title"> 69 + {group.label} 70 + </h3> 71 + <p className="toc-group__description">{group.description}</p> 72 + </div> 73 + 74 + <ol className="toc-list"> 75 + {group.chapters.map((chapter, chapterIndex) => ( 76 + <li key={chapter.to}> 77 + <Link 78 + to={chapter.to} 79 + preload={chapterIndex === 0 ? false : "intent"} 80 + className="toc-link group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color)" 81 + > 82 + <span className="toc-link__number">{chapter.number}</span> 83 + <span className="toc-link__body"> 84 + <span className="toc-link__title">{chapter.title}</span> 85 + <span className="toc-link__summary">{chapter.summary}</span> 86 + <span className="toc-link__tags" aria-label="Concepts"> 87 + {chapter.tags.map((tag) => ( 88 + <span key={tag} className="toc-link__tag"> 89 + {tag} 90 + </span> 91 + ))} 92 + </span> 93 + </span> 94 + <span className="toc-link__open" aria-hidden="true"> 95 + &gt; 96 + </span> 97 + </Link> 98 + </li> 99 + ))} 100 + </ol> 101 + </section> 102 + ))} 129 103 </div> 130 104 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> 105 + <div className="mt-12 border-t border-(--border-default) pt-6"> 106 + <Link 107 + to="/basic" 108 + className="inline-flex items-center gap-2 border border-(--accent-default) bg-(--accent-surface) px-4 py-3 font-mono text-sm font-semibold text-(--text-primary) transition-colors duration-fast hover:bg-(--accent-subtle) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring-color)" 109 + > 110 + Start reading 111 + <span aria-hidden="true">&gt;</span> 112 + </Link> 154 113 </div> 155 114 </section> 156 115 157 - {/* Footer */} 158 116 <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)"> 117 + <div className="max-w-6xl mx-auto px-6 py-6 text-sm font-mono text-(--text-muted)"> 160 118 <p> 161 119 Built with{" "} 162 120 <a
+222
src/styles/global.css
··· 124 124 } 125 125 126 126 @layer components { 127 + .chapter-select { 128 + display: block; 129 + } 130 + 131 + .chapter-select__control { 132 + height: 2rem; 133 + max-width: min(42vw, 18rem); 134 + border: 1px solid var(--border-default); 135 + background: var(--bg-card); 136 + color: var(--text-primary); 137 + padding: 0 var(--space-8) 0 var(--space-3); 138 + font-family: var(--font-mono); 139 + font-size: var(--text-xs); 140 + line-height: 1; 141 + } 142 + 143 + .chapter-select__control:hover { 144 + border-color: var(--accent-default); 145 + } 146 + 147 + .chapter-select__control:focus-visible { 148 + outline: none; 149 + box-shadow: 0 0 0 2px var(--ring-color); 150 + } 151 + 152 + .chapter-pager { 153 + display: grid; 154 + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); 155 + align-items: stretch; 156 + gap: var(--space-3); 157 + margin-top: var(--space-8); 158 + border-top: 1px solid var(--border-default); 159 + padding-top: var(--space-6); 160 + } 161 + 162 + .chapter-pager__link, 163 + .chapter-pager__contents { 164 + border: 1px solid var(--border-default); 165 + background: var(--bg-secondary); 166 + padding: var(--space-3) var(--space-4); 167 + transition: 168 + border-color var(--duration-fast) var(--easing-default), 169 + background-color var(--duration-fast) var(--easing-default), 170 + color var(--duration-fast) var(--easing-default); 171 + } 172 + 173 + .chapter-pager__link { 174 + display: grid; 175 + gap: var(--space-1); 176 + min-width: 0; 177 + } 178 + 179 + .chapter-pager__contents { 180 + display: inline-flex; 181 + align-items: center; 182 + justify-content: center; 183 + color: var(--text-secondary); 184 + font-family: var(--font-mono); 185 + font-size: var(--text-xs); 186 + text-transform: uppercase; 187 + } 188 + 189 + .chapter-pager__link:hover, 190 + .chapter-pager__contents:hover { 191 + border-color: var(--accent-default); 192 + background: var(--accent-subtle); 193 + color: var(--text-primary); 194 + } 195 + 196 + .chapter-pager__link--disabled { 197 + opacity: 0.55; 198 + } 199 + 200 + .chapter-pager__kicker { 201 + color: var(--text-muted); 202 + font-family: var(--font-mono); 203 + font-size: var(--text-xs); 204 + text-transform: uppercase; 205 + } 206 + 207 + .chapter-pager__title { 208 + overflow: hidden; 209 + color: var(--text-primary); 210 + font-family: var(--font-mono); 211 + font-size: var(--text-sm); 212 + font-weight: 600; 213 + text-overflow: ellipsis; 214 + white-space: nowrap; 215 + } 216 + 217 + .toc-status { 218 + border: 1px solid var(--border-default); 219 + background: var(--bg-card); 220 + padding: var(--space-5); 221 + } 222 + 223 + .toc-group { 224 + display: grid; 225 + gap: var(--space-5); 226 + } 227 + 228 + .toc-group__heading { 229 + display: grid; 230 + gap: var(--space-2); 231 + max-width: 42rem; 232 + } 233 + 234 + .toc-group__title { 235 + color: var(--text-primary); 236 + font-size: var(--text-base); 237 + text-transform: uppercase; 238 + } 239 + 240 + .toc-group__description { 241 + color: var(--text-muted); 242 + font-size: var(--text-sm); 243 + line-height: var(--leading-normal); 244 + } 245 + 246 + .toc-list { 247 + display: grid; 248 + border-top: 1px solid var(--border-default); 249 + } 250 + 251 + .toc-link { 252 + display: grid; 253 + grid-template-columns: 4rem minmax(0, 1fr) auto; 254 + gap: var(--space-4); 255 + align-items: start; 256 + border-bottom: 1px solid var(--border-default); 257 + padding: var(--space-4) var(--space-2); 258 + transition: 259 + background-color var(--duration-fast) var(--easing-default), 260 + color var(--duration-fast) var(--easing-default); 261 + } 262 + 263 + .toc-link:hover { 264 + background: var(--accent-subtle); 265 + } 266 + 267 + .toc-link__number { 268 + color: var(--accent-default); 269 + font-family: var(--font-mono); 270 + font-size: var(--text-sm); 271 + font-weight: 600; 272 + } 273 + 274 + .toc-link__body { 275 + display: grid; 276 + gap: var(--space-2); 277 + min-width: 0; 278 + } 279 + 280 + .toc-link__title { 281 + color: var(--text-primary); 282 + font-family: var(--font-display); 283 + font-size: var(--text-xl); 284 + font-weight: 600; 285 + line-height: var(--leading-tight); 286 + } 287 + 288 + .toc-link__summary { 289 + max-width: 64ch; 290 + color: var(--text-secondary); 291 + font-size: var(--text-sm); 292 + line-height: var(--leading-normal); 293 + } 294 + 295 + .toc-link__tags { 296 + display: flex; 297 + flex-wrap: wrap; 298 + gap: var(--space-2); 299 + } 300 + 301 + .toc-link__tag { 302 + border: 1px solid var(--border-default); 303 + background: var(--bg-secondary); 304 + color: var(--text-muted); 305 + padding: var(--space-1) var(--space-2); 306 + font-family: var(--font-mono); 307 + font-size: var(--text-xs); 308 + line-height: 1; 309 + } 310 + 311 + .toc-link__open { 312 + color: var(--text-muted); 313 + font-family: var(--font-mono); 314 + font-size: var(--text-sm); 315 + transition: color var(--duration-fast) var(--easing-default); 316 + } 317 + 318 + .toc-link:hover .toc-link__open { 319 + color: var(--accent-default); 320 + } 321 + 127 322 /* Navigation link - terminal style */ 128 323 .nav-link { 129 324 display: flex; ··· 231 426 background: var(--accent-default); 232 427 border: 1px solid var(--accent-default); 233 428 line-height: 1; 429 + } 430 + } 431 + 432 + @media (max-width: 44rem) { 433 + @layer components { 434 + .chapter-pager { 435 + grid-template-columns: 1fr; 436 + } 437 + 438 + .chapter-pager__link, 439 + .chapter-pager__contents { 440 + text-align: left; 441 + } 442 + 443 + .toc-link { 444 + grid-template-columns: 2.5rem minmax(0, 1fr); 445 + gap: var(--space-3); 446 + padding: var(--space-4) 0; 447 + } 448 + 449 + .toc-link__open { 450 + display: none; 451 + } 452 + 453 + .toc-link__title { 454 + font-size: var(--text-lg); 455 + } 234 456 } 235 457 } 236 458