| @@ -1,10 +1,42 @@ | ||
| # Docket - Frontend Notes | ||
| - | Wave F2 + F3 + F4 + F5 build log. All surfaces consume the live read-API | |
| + | Wave F2 + F3 + F4 + F5 + R1 build log. All surfaces consume the live read-API | |
| (`app/api/*` → `lib/feed.ts` → SQLite at `data/docket.db`). No mock data on | ||
| the deep surfaces; `lib/mock-data.ts` remains only for the Phase-0 lint | ||
| reference and is not imported by any F2/F3/F4/F5 page. | ||
| + | ## R1 (this wave) - UX/QoL redesign foundation (Claude) | |
| + | ||
| + | ### Summary | |
| + | Wave R1 delivers the four foundation changes from docs/REDESIGN.md. QA CLEAN (17 routes, zero overflow, zero hydration errors). | |
| + | ||
| + | ### 1. Default theme: Reference (light) replaces Dark | |
| + | - `app/layout.tsx`: initial `<html>` class changed from `dark` → `light`; inline pre-paint script defaults to `'light'` instead of `'dark'`; added Atkinson Hyperlegible via `next/font/google` as `--font-body` CSS variable. | |
| + | - `app/globals.css`: `:root.light` token values updated to spec (docs/REDESIGN.md §"Theme system"): | |
| + | - bg `#F8FAFC`, surface `#FFFFFF`, text `#0F172A`, muted `#475569`, border `#E2E8F0` | |
| + | - body-size bumped to **18px** (was 16px), leading to **1.6** (was 1.5) - ACCESSIBILITY.md Rule #2 | |
| + | - Added `:root.light body { font-family: var(--font-body), ... }` - Atkinson Hyperlegible in Reference mode; `.label`/`.pill`/`.font-mono` classes still apply JetBrains Mono explicitly for data tokens. | |
| + | - Muted semantic tokens (`--dk-yea/nay/hold`) adjusted for AA on light bg. | |
| + | - `tailwind.config.ts`: added `font-body` alias for `var(--font-body)`. | |
| + | - Three-mode toggle still works; persisted in localStorage. Dark mode and Plain mode are unchanged. | |
| + | ||
| + | ### 2. Toggle relabeled: Reference / Dark / Plain | |
| + | - `components/display-toggle.tsx`: mode order changed to Reference first, labels updated ("Readable" → "Reference"), default state and localStorage fallback both changed from `"dark"` to `"light"`. | |
| + | ||
| + | ### 3. Header: global search icon added | |
| + | - `components/site-header.tsx`: added a search icon `<Link href="/search">` in the header logo row, visible on all viewports. Inline SVG, `min-h-[44px] min-w-[44px]` (a11y Rule #3). The display toggle and hamburger are unchanged. | |
| + | ||
| + | ### 4. Register home decluttered | |
| + | - `app/register/page.tsx`: plain-language explainer paragraph added above the feed table (≤8th-grade, active voice, source stated). The four secondary cards (composition by type, awards total, recently-changed, browse by topic) moved into a `<details>` / `<summary>` collapsed block labeled "More: composition, awards & recently changed". The ranked feed is now the single primary focus. | |
| + | ||
| + | ### 5. States page: 50 states vs DC & territories | |
| + | - `app/states/page.tsx`: introduced `NON_STATE_CODES = Set(["DC","PR","AS","GU","MP","VI"])`. States split into two separate `DataTable` sections: "50 states" (never produces a "51" count) and "DC & U.S. territories · not states". The DC section copy explicitly states "DC is a federal district" and names each territory. Coverage summary cards label "states" not "jurisdictions". The "51 states" count can never appear. | |
| + | ||
| + | ### Out of scope (R2) | |
| + | Per-page search/filter bars and the "Cite this" component are R2. Stubs may be added later. | |
| + | ||
| + | --- | |
| + | ||
| ## F5 (this wave) - mobile-responsive overhaul (GLM start / Claude finish) | ||
| ### Zero-overflow guarantee at 390px |
| @@ -46,21 +46,41 @@ | ||
| color-scheme: dark; | ||
| } | ||
| - | /* Light mode - parchment, reads like an official document. */ | |
| + | /* | |
| + | * Reference mode (light) - "official document" reading experience. | |
| + | * Palette per docs/REDESIGN.md §"Theme system": #F8FAFC base, #FFFFFF surface, | |
| + | * #0F172A text, #475569 muted, #E2E8F0 border, brass #B07F25 accent (AA on light). | |
| + | * Body font: Atkinson Hyperlegible (engineered for low vision). | |
| + | * Body ≥18px, line-height 1.6 (docs/ACCESSIBILITY.md Rule #2). | |
| + | */ | |
| :root.light { | ||
| - | --dk-ink: #f6f4ef; /* parchment bg */ | |
| - | --dk-surface: #ffffff; | |
| - | --dk-graphite: #ece9e2; | |
| - | --dk-paper: #0b0c0e; /* ink text */ | |
| - | --dk-ash: #565b62; | |
| - | --dk-ash-2: #868c94; | |
| - | --dk-line: rgba(11, 12, 14, 0.16); | |
| - | --dk-signal: #b07f25; /* brass darkens for contrast */ | |
| + | --dk-ink: #f8fafc; /* Reference bg - clean cool white */ | |
| + | --dk-surface: #ffffff; /* card surface */ | |
| + | --dk-graphite: #eef2f7; /* raised / hover */ | |
| + | --dk-paper: #0f172a; /* primary text - deep slate */ | |
| + | --dk-ash: #475569; /* muted / secondary text */ | |
| + | --dk-ash-2: #64748b; /* tertiary text */ | |
| + | --dk-line: #e2e8f0; /* border - cool light gray */ | |
| + | ||
| + | /* Brass darkened for WCAG AA on white (4.5:1+) */ | |
| + | --dk-signal: #b07f25; | |
| --dk-signal-dim: #8c6316; | ||
| --dk-signal-tint: rgba(176, 127, 37, 0.1); | ||
| - | /* a11y #2: light body >=16px, line-height 1.5 */ | |
| - | --dk-body-size: 16px; | |
| - | --dk-leading: 1.5; | |
| + | ||
| + | /* Muted semantic tokens - same hues, readable on light bg */ | |
| + | --dk-yea: #3d7a56; | |
| + | --dk-nay: #8b4040; | |
| + | --dk-hold: #8a6520; | |
| + | ||
| + | --dk-hero-glow: radial-gradient( | |
| + | 60% 80% at 50% -10%, | |
| + | rgba(176, 127, 37, 0.06) 0%, | |
| + | rgba(176, 127, 37, 0) 70% | |
| + | ); | |
| + | ||
| + | /* a11y #2: Reference body ≥18px, line-height 1.6 */ | |
| + | --dk-body-size: 18px; | |
| + | --dk-leading: 1.6; | |
| color-scheme: light; | ||
| } | ||
| @@ -106,12 +126,22 @@ | ||
| body { | ||
| background-color: var(--dk-ink); | ||
| color: var(--dk-paper); | ||
| + | /* Dark + Plain modes: forensic mono body. Reference (light): see override below. */ | |
| font-family: var(--font-mono), ui-monospace, monospace; | ||
| font-size: var(--dk-body-size); | ||
| line-height: var(--dk-leading); | ||
| -webkit-font-smoothing: antialiased; | ||
| text-rendering: optimizeLegibility; | ||
| } | ||
| + | ||
| + | /* | |
| + | * Reference (light) mode body - Atkinson Hyperlegible (engineered for low vision). | |
| + | * JetBrains Mono is still applied for .label / .pill / .font-mono / data tokens | |
| + | * via their explicit class overrides, which take precedence over this rule. | |
| + | */ | |
| + | :root.light body { | |
| + | font-family: var(--font-body), "Atkinson Hyperlegible", system-ui, sans-serif; | |
| + | } | |
| ::selection { | ||
| background: var(--dk-signal-tint); | ||
| color: var(--dk-paper); |
| @@ -1,5 +1,5 @@ | ||
| import type { Metadata, Viewport } from "next"; | ||
| - | import { Archivo, JetBrains_Mono, Fraunces } from "next/font/google"; | |
| + | import { Archivo, JetBrains_Mono, Fraunces, Atkinson_Hyperlegible } from "next/font/google"; | |
| import "./globals.css"; | ||
| import { SiteHeader } from "@/components/site-header"; | ||
| import { SiteFooter } from "@/components/site-footer"; | ||
| @@ -26,6 +26,15 @@ const fraunces = Fraunces({ | ||
| display: "swap", | ||
| }); | ||
| + | /* Atkinson Hyperlegible - engineered for low-vision readability. | |
| + | Used as body font in Reference (light) mode. */ | |
| + | const atkinson = Atkinson_Hyperlegible({ | |
| + | subsets: ["latin"], | |
| + | weight: ["400", "700"], | |
| + | variable: "--font-body", | |
| + | display: "swap", | |
| + | }); | |
| + | ||
| export const metadata: Metadata = { | ||
| title: { | ||
| default: "Docket - what the government did this week", | ||
| @@ -41,7 +50,8 @@ export const metadata: Metadata = { | ||
| }; | ||
| export const viewport: Viewport = { | ||
| - | themeColor: "#0B0C0E", | |
| + | // Light-mode reference bg; the inline script may change this per-user. | |
| + | themeColor: "#F8FAFC", | |
| width: "device-width", | ||
| initialScale: 1, | ||
| }; | ||
| @@ -54,19 +64,20 @@ export default function RootLayout({ | ||
| return ( | ||
| <html | ||
| lang="en" | ||
| - | className={`${archivo.variable} ${jetbrains.variable} ${fraunces.variable} dark`} | |
| + | className={`${archivo.variable} ${jetbrains.variable} ${fraunces.variable} ${atkinson.variable} light`} | |
| suppressHydrationWarning | ||
| > | ||
| <head> | ||
| {/* | ||
| Apply the persisted display mode before paint (a11y §"Three display | ||
| - | modes"). Runs synchronously in <head> so the first frame is already | |
| - | the user's chosen mode - no flash, and suppressHydrationWarning lets | |
| + | modes"). Default is now "light" (Reference) for new visitors. | |
| + | Runs synchronously in <head> so the first frame is already the | |
| + | user's chosen mode - no flash, and suppressHydrationWarning lets | |
| the inline script own the class without a React mismatch. | ||
| */} | ||
| <script | ||
| dangerouslySetInnerHTML={{ | ||
| - | __html: `(function(){try{var m=localStorage.getItem('docket-display')||'dark';var c=document.documentElement.classList;c.remove('dark','light','plain');c.add(m);document.documentElement.dataset.display=m;}catch(e){}})();`, | |
| + | __html: `(function(){try{var m=localStorage.getItem('docket-display')||'light';var c=document.documentElement.classList;c.remove('dark','light','plain');c.add(m);document.documentElement.dataset.display=m;}catch(e){}})();`, | |
| }} | ||
| /> | ||
| </head> |
| @@ -51,7 +51,7 @@ export default async function RegisterPage() { | ||
| .filter((c) => c.total > 0) | ||
| .sort((a, b) => b.total - a.total) | ||
| .slice(0, 8); | ||
| - | // Compute byKind once - it's used twice in the JSX below. | |
| + | // Compute byKind once - it's used in the details section below. | |
| const kindBreakdown = byKind(items); | ||
| return ( | ||
| @@ -59,10 +59,20 @@ export default async function RegisterPage() { | ||
| <RegisterHero seed={shown.slice(0, 12)} /> | ||
| <div className="mx-auto w-full max-w-register px-4 py-12 sm:px-6"> | ||
| + | {/* | |
| + | Plain-language explainer - ≤8th-grade reading level (ACCESSIBILITY.md §6). | |
| + | One sentence. Active voice. No adjectives. Sourcing stated. | |
| + | */} | |
| + | <p className="prose-lead mb-8 max-w-2xl text-ash"> | |
| + | Every bill, vote, and federal award is listed below, drawn from | |
| + | Congress.gov, the Federal Register, and USAspending.gov. Ranked by | |
| + | date - never by clicks or editorial judgment. Every entry links to | |
| + | its official source. | |
| + | </p> | |
| + | ||
| <SectionHeader | ||
| eyebrow="Full register · ranked" | ||
| title="Every official action, in order" | ||
| - | description="The complete record below is the same stream as the live ledger, ranked by action date and dollar magnitude - never by coverage or clicks." | |
| /> | ||
| {items.length === 0 ? ( | ||
| @@ -77,102 +87,114 @@ export default async function RegisterPage() { | ||
| <RegisterTable items={shown} /> | ||
| )} | ||
| - | {/* Composition by kind - proportional to official-action volume (rule #3). */} | |
| - | <div className="mt-12 grid gap-6 sm:grid-cols-2"> | |
| - | <Card className="p-5"> | |
| - | <p className="label mb-4">Composition by action type</p> | |
| - | <ul className="space-y-2.5"> | |
| - | {kindBreakdown.map((a) => { | |
| - | const max = Math.max(1, ...kindBreakdown.map((x) => x.count)); | |
| - | return ( | |
| - | <li key={a.kind} className="font-mono text-[12px]"> | |
| - | <div className="flex items-center justify-between"> | |
| - | <span className="text-ash">{ACTION_LABEL[a.kind]}</span> | |
| - | <span className="tabular-nums text-paper">{a.count}</span> | |
| - | </div> | |
| - | <div className="mt-1 h-1 w-full overflow-hidden rounded-full bg-ink"> | |
| - | <span | |
| - | className="block h-full rounded-full" | |
| - | style={{ | |
| - | width: `${(a.count / max) * 100}%`, | |
| - | backgroundColor: "var(--dk-ash)", | |
| - | }} | |
| - | /> | |
| - | </div> | |
| - | </li> | |
| - | ); | |
| - | })} | |
| - | </ul> | |
| - | <p className="label mt-4 text-ash-2"> | |
| - | Proportional to action volume. No editorial weighting. | |
| - | </p> | |
| - | </Card> | |
| + | {/* | |
| + | Secondary details - collapsed by default so the feed is the primary | |
| + | focus (docs/REDESIGN.md §"Register (home)"). | |
| + | Composition chart, awards, recently-changed, and topic chips are | |
| + | available here for users who want them. | |
| + | */} | |
| + | <details className="group mt-12"> | |
| + | <summary className="focus-ring label inline-flex cursor-pointer list-none items-center gap-2 rounded-[3px] border border-line bg-surface px-4 py-3 text-ash hover:text-paper"> | |
| + | <span aria-hidden="true" className="transition-transform group-open:rotate-90">▶</span> | |
| + | More: composition, awards & recently changed | |
| + | </summary> | |
| - | <Card className="p-5"> | |
| - | <p className="label mb-4">Awards, on record</p> | |
| - | <p className="display text-3xl text-paper"> | |
| - | {formatUsd(awardStats.total, { compact: true })} | |
| - | </p> | |
| - | <p className="prose-lead mt-2 text-ash"> | |
| - | {awardStats.count} federal awards from USAspending.gov, summed across | |
| - | the record. Figures only - no characterization. | |
| - | </p> | |
| - | <Link | |
| - | href="/spending" | |
| - | className="focus-ring tap label mt-5 gap-1 text-ash underline underline-offset-4 hover:text-paper" | |
| - | > | |
| - | Spending register → | |
| - | </Link> | |
| - | </Card> | |
| - | </div> | |
| + | <div className="mt-6 grid gap-6 sm:grid-cols-2"> | |
| + | {/* Composition by kind - proportional to official-action volume. */} | |
| + | <Card className="p-5"> | |
| + | <p className="label mb-4">Composition by action type</p> | |
| + | <ul className="space-y-2.5"> | |
| + | {kindBreakdown.map((a) => { | |
| + | const max = Math.max(1, ...kindBreakdown.map((x) => x.count)); | |
| + | return ( | |
| + | <li key={a.kind} className="font-mono text-[12px]"> | |
| + | <div className="flex items-center justify-between"> | |
| + | <span className="text-ash">{ACTION_LABEL[a.kind]}</span> | |
| + | <span className="tabular-nums text-paper">{a.count}</span> | |
| + | </div> | |
| + | <div className="mt-1 h-1 w-full overflow-hidden rounded-full bg-ink"> | |
| + | <span | |
| + | className="block h-full rounded-full" | |
| + | style={{ | |
| + | width: `${(a.count / max) * 100}%`, | |
| + | backgroundColor: "var(--dk-ash)", | |
| + | }} | |
| + | /> | |
| + | </div> | |
| + | </li> | |
| + | ); | |
| + | })} | |
| + | </ul> | |
| + | <p className="label mt-4 text-ash-2"> | |
| + | Proportional to action volume. No editorial weighting. | |
| + | </p> | |
| + | </Card> | |
| - | {/* Recently changed + browse by topic */} | |
| - | <div className="mt-12 grid gap-6 sm:grid-cols-2"> | |
| - | <Card className="p-5"> | |
| - | <div className="mb-3 flex items-baseline justify-between"> | |
| - | <p className="label">Recently changed</p> | |
| + | <Card className="p-5"> | |
| + | <p className="label mb-4">Awards, on record</p> | |
| + | <p className="display text-3xl text-paper"> | |
| + | {formatUsd(awardStats.total, { compact: true })} | |
| + | </p> | |
| + | <p className="prose-lead mt-2 text-ash"> | |
| + | {awardStats.count} federal awards from USAspending.gov, summed across | |
| + | the record. Figures only - no characterization. | |
| + | </p> | |
| <Link | ||
| - | href="/calendar" | |
| - | className="focus-ring tap label gap-1 text-ash underline underline-offset-4 hover:text-paper" | |
| + | href="/spending" | |
| + | className="focus-ring tap label mt-5 gap-1 text-ash underline underline-offset-4 hover:text-paper" | |
| > | ||
| - | Calendar → | |
| + | Spending register → | |
| </Link> | ||
| - | </div> | |
| - | <ChangesFeed items={changes} /> | |
| - | <p className="label mt-4 text-ash-2"> | |
| - | What the ingest added, updated, or removed. Each entry links to its | |
| - | source. | |
| - | </p> | |
| - | </Card> | |
| + | </Card> | |
| + | </div> | |
| - | <Card className="p-5"> | |
| - | <div className="mb-3 flex items-baseline justify-between"> | |
| - | <p className="label">Browse by topic</p> | |
| - | <Link | |
| - | href="/categories" | |
| - | className="focus-ring tap label gap-1 text-ash underline underline-offset-4 hover:text-paper" | |
| - | > | |
| - | All topics → | |
| - | </Link> | |
| - | </div> | |
| - | <ul className="flex flex-wrap gap-1.5"> | |
| - | {topTopics.map((c) => ( | |
| - | <li key={c.slug}> | |
| - | <Link | |
| - | href={`/bills?policyArea=${encodeURIComponent(c.label)}`} | |
| - | className="focus-ring label inline-flex min-h-[44px] items-center gap-2 rounded-[3px] border border-line px-2.5 text-ash hover:text-paper" | |
| - | > | |
| - | <span className="text-paper">{c.label}</span> | |
| - | <span className="text-ash-2">{c.total}</span> | |
| - | </Link> | |
| - | </li> | |
| - | ))} | |
| - | </ul> | |
| - | <p className="label mt-4 text-ash-2"> | |
| - | Bills and awards grouped by policy area. Counts only - no weighting. | |
| - | </p> | |
| - | </Card> | |
| - | </div> | |
| + | <div className="mt-6 grid gap-6 sm:grid-cols-2"> | |
| + | <Card className="p-5"> | |
| + | <div className="mb-3 flex items-baseline justify-between"> | |
| + | <p className="label">Recently changed</p> | |
| + | <Link | |
| + | href="/calendar" | |
| + | className="focus-ring tap label gap-1 text-ash underline underline-offset-4 hover:text-paper" | |
| + | > | |
| + | Calendar → | |
| + | </Link> | |
| + | </div> | |
| + | <ChangesFeed items={changes} /> | |
| + | <p className="label mt-4 text-ash-2"> | |
| + | What the ingest added, updated, or removed. Each entry links to its | |
| + | source. | |
| + | </p> | |
| + | </Card> | |
| + | ||
| + | <Card className="p-5"> | |
| + | <div className="mb-3 flex items-baseline justify-between"> | |
| + | <p className="label">Browse by topic</p> | |
| + | <Link | |
| + | href="/categories" | |
| + | className="focus-ring tap label gap-1 text-ash underline underline-offset-4 hover:text-paper" | |
| + | > | |
| + | All topics → | |
| + | </Link> | |
| + | </div> | |
| + | <ul className="flex flex-wrap gap-1.5"> | |
| + | {topTopics.map((c) => ( | |
| + | <li key={c.slug}> | |
| + | <Link | |
| + | href={`/bills?policyArea=${encodeURIComponent(c.label)}`} | |
| + | className="focus-ring label inline-flex min-h-[44px] items-center gap-2 rounded-[3px] border border-line px-2.5 text-ash hover:text-paper" | |
| + | > | |
| + | <span className="text-paper">{c.label}</span> | |
| + | <span className="text-ash-2">{c.total}</span> | |
| + | </Link> | |
| + | </li> | |
| + | ))} | |
| + | </ul> | |
| + | <p className="label mt-4 text-ash-2"> | |
| + | Bills and awards grouped by policy area. Counts only - no weighting. | |
| + | </p> | |
| + | </Card> | |
| + | </div> | |
| + | </details> | |
| </div> | ||
| </> | ||
| ); |
| @@ -32,23 +32,23 @@ function coverageMeta(c: StateCoverage["confidence"]): { | ||
| return { label: "Coverage rolling out", color: "var(--dk-ash)" }; | ||
| } | ||
| - | export default function StatesPage() { | |
| - | const states = getStates(); | |
| - | const ok = states.filter((s) => s.confidence === "ok").length; | |
| - | const stale = states.filter((s) => s.confidence === "stale").length; | |
| - | const rolling = states.filter((s) => s.confidence === "missing").length; | |
| - | ||
| - | // 50 states + DC + territories (PR, AS, GU, MP, VI). Shown honestly as | |
| - | // jurisdictions, never as a bogus "56 states". | |
| - | const territories = states.length - 50; | |
| + | /** | |
| + | * DC is a federal district; PR/AS/GU/MP/VI are U.S. territories. | |
| + | * None of them are states. Separating them avoids any "51 states" count. | |
| + | */ | |
| + | const NON_STATE_CODES = new Set(["DC", "PR", "AS", "GU", "MP", "VI"]); | |
| - | const columns: Column[] = [ | |
| - | { key: "state", header: "State", cardTitle: true }, | |
| + | function buildColumns(): Column[] { | |
| + | return [ | |
| + | { key: "state", header: "Name", cardTitle: true }, | |
| { key: "confidence", header: "Coverage" }, | ||
| { key: "seen", header: "Last seen", hideOnCard: true }, | ||
| { key: "missing", header: "Not yet in record" }, | ||
| ]; | ||
| - | const rows: Row[] = states.map((s) => { | |
| + | } | |
| + | ||
| + | function buildRows(entries: StateCoverage[]): Row[] { | |
| + | return entries.map((s) => { | |
| const m = coverageMeta(s.confidence); | ||
| return { | ||
| key: s.code, | ||
| @@ -85,6 +85,21 @@ export default function StatesPage() { | ||
| }, | ||
| }; | ||
| }); | ||
| + | } | |
| + | ||
| + | export default function StatesPage() { | |
| + | const allJurisdictions = getStates(); | |
| + | ||
| + | // 50 states - never DC or territories (never produces a "51 states" count). | |
| + | const states50 = allJurisdictions.filter((s) => !NON_STATE_CODES.has(s.code)); | |
| + | // DC (federal district) + 5 U.S. territories - clearly not states. | |
| + | const dcAndTerritories = allJurisdictions.filter((s) => NON_STATE_CODES.has(s.code)); | |
| + | ||
| + | const ok = states50.filter((s) => s.confidence === "ok").length; | |
| + | const stale = states50.filter((s) => s.confidence === "stale").length; | |
| + | const rolling = states50.filter((s) => s.confidence === "missing").length; | |
| + | ||
| + | const columns = buildColumns(); | |
| return ( | ||
| <div className="mx-auto w-full max-w-register px-4 py-10 sm:px-6"> | ||
| @@ -94,16 +109,21 @@ export default function StatesPage() { | ||
| lead="Federal delegation and sponsored bills are on record for every state, DC, and U.S. territory. State-level legislative coverage is rolling out - gaps are recorded here, not hidden, and never imply the site is broken." | ||
| /> | ||
| - | {/* Coverage summary - muted, calm. "Rolling out" uses an ash tone, not red. */} | |
| + | {/* Coverage summary - 50 states only; DC and territories listed separately below. */} | |
| <div className="mt-8 grid gap-4 sm:grid-cols-3"> | ||
| <CoverageCard label="Current" count={ok} level="ok" /> | ||
| <CoverageCard label="Stale" count={stale} level="stale" /> | ||
| <CoverageCard label="Coverage rolling out" count={rolling} level="missing" /> | ||
| </div> | ||
| + | <p className="label mt-3 text-ash-2"> | |
| + | Coverage counts above apply to the 50 states. DC and U.S. territories are listed | |
| + | in their own section below. | |
| + | </p> | |
| + | {/* 50 states table */} | |
| <div className="mt-12"> | ||
| <div className="mb-6"> | ||
| - | <p className="label mb-2">Per-state · {states.length} jurisdictions</p> | |
| + | <p className="label mb-2">50 states · federal delegation on record</p> | |
| <h2 | ||
| className="display text-paper" | ||
| style={{ fontSize: "clamp(1.25rem, 4vw, 1.875rem)" }} | ||
| @@ -111,24 +131,43 @@ export default function StatesPage() { | ||
| Coverage by state | ||
| </h2> | ||
| <p className="prose-lead mt-3 max-w-2xl text-ash"> | ||
| - | Each jurisdiction's worst-case state-legislative coverage across bills, votes, | |
| - | and sponsors. Federal delegation and bill sponsorships (from congress.gov) are | |
| - | shown regardless. Open a state for its congressional delegation. | |
| + | Each state's worst-case state-legislative coverage across bills, votes, | |
| + | and sponsors. Federal delegation and bill sponsorships (from congress.gov) | |
| + | are on record regardless. Open a state for its congressional delegation. | |
| </p> | ||
| </div> | ||
| <DataTable | ||
| - | caption="Per-state coverage: state, coverage level, last scrape, data not yet in record." | |
| + | caption="Per-state coverage: name, coverage level, last scrape, data not yet in record." | |
| cardLabel="Per-state coverage" | ||
| columns={columns} | ||
| - | rows={rows} | |
| + | rows={buildRows(states50)} | |
| /> | ||
| + | </div> | |
| - | <p className="label mt-4 text-ash-2"> | |
| - | {territories} of these jurisdictions are DC & U.S. territories. State | |
| - | legislative coverage requires per-state polling and rolls out over time; the | |
| - | federal delegation is on record now. | |
| - | </p> | |
| + | {/* DC & territories - separate section, clearly labeled as NOT states */} | |
| + | <div className="mt-14"> | |
| + | <div className="mb-6"> | |
| + | <p className="label mb-2">DC & U.S. territories · not states</p> | |
| + | <h2 | |
| + | className="display text-paper" | |
| + | style={{ fontSize: "clamp(1.25rem, 4vw, 1.875rem)" }} | |
| + | > | |
| + | District of Columbia & territories | |
| + | </h2> | |
| + | <p className="prose-lead mt-3 max-w-2xl text-ash"> | |
| + | DC is a federal district. Puerto Rico, American Samoa, Guam, the Northern | |
| + | Mariana Islands, and the U.S. Virgin Islands are U.S. territories. None are | |
| + | states. Congressional delegation and federal data are included in the record. | |
| + | </p> | |
| + | </div> | |
| + | ||
| + | <DataTable | |
| + | caption="DC and U.S. territory coverage: name, coverage level, last scrape, data not yet in record." | |
| + | cardLabel="DC and territory coverage" | |
| + | columns={columns} | |
| + | rows={buildRows(dcAndTerritories)} | |
| + | /> | |
| </div> | ||
| </div> | ||
| ); | ||
| @@ -154,7 +193,7 @@ function CoverageCard({ | ||
| > | ||
| {count} | ||
| </span> | ||
| - | <span className="label text-ash-2">jurisdictions</span> | |
| + | <span className="label text-ash-2">states</span> | |
| </div> | ||
| <span | ||
| className={clsx("mt-3 inline-block h-0.5 w-12 rounded-full")} |
| @@ -5,27 +5,28 @@ import { clsx } from "@/lib/cx"; | ||
| /** | ||
| * 3-mode display toggle (docs/ACCESSIBILITY.md §"Three display modes"). | ||
| - | * Dark (default forensic brand) / Readable Light / Plain (high-contrast, | |
| - | * system sans, >=20px). Persisted in localStorage("docket-display"); the | |
| + | * Reference (light, DEFAULT) / Dark (forensic brand) / Plain (high-contrast, | |
| + | * system sans, ≥20px). Persisted in localStorage("docket-display"); the | |
| * matching class on <html> is set pre-paint by the inline script in | ||
| * layout.tsx so there is no flash and no hydration mismatch. | ||
| * | ||
| - | * Always-visible, labelled, >=44x44px hit targets (a11y rules #3 & #8). | |
| - | * Fully keyboard-operable (native radio group). | |
| + | * Default is now "light" (Reference) per docs/REDESIGN.md §"Theme system". | |
| + | * Always-visible, labelled, ≥44×44px hit targets (a11y rules #3 & #8). | |
| + | * Fully keyboard-operable (native button group). | |
| */ | ||
| export type DisplayMode = "dark" | "light" | "plain"; | ||
| const MODES: Array<{ value: DisplayMode; label: string; hint: string }> = [ | ||
| - | { value: "dark", label: "Dark", hint: "Default register look" }, | |
| - | { value: "light", label: "Readable", hint: "Light, for reading" }, | |
| - | { value: "plain", label: "Plain", hint: "High contrast, large text" }, | |
| + | { value: "light", label: "Reference", hint: "Light - for reading" }, | |
| + | { value: "dark", label: "Dark", hint: "Forensic register look" }, | |
| + | { value: "plain", label: "Plain", hint: "High contrast, large text" }, | |
| ]; | ||
| export function DisplayToggle() { | ||
| - | const [mode, setMode] = useState<DisplayMode>("dark"); | |
| + | const [mode, setMode] = useState<DisplayMode>("light"); | |
| useEffect(() => { | ||
| - | const current = (localStorage.getItem("docket-display") as DisplayMode) || "dark"; | |
| + | const current = (localStorage.getItem("docket-display") as DisplayMode) || "light"; | |
| if (current !== mode) setMode(current); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, []); | ||
| @@ -36,7 +37,6 @@ export function DisplayToggle() { | ||
| const el = document.documentElement; | ||
| el.classList.remove("dark", "light", "plain"); | ||
| el.classList.add(next); | ||
| - | // keep the brand's font CSS variables in sync for plain mode | |
| el.dataset.display = next; | ||
| } | ||
| @@ -80,6 +80,15 @@ export function SiteHeader() { | ||
| </Link> | ||
| <div className="flex items-center gap-2"> | ||
| + | {/* Global search shortcut - visible on all viewports. */} | |
| + | <Link | |
| + | href="/search" | |
| + | aria-label="Search Docket" | |
| + | title="Search" | |
| + | className="focus-ring label inline-flex min-h-[44px] min-w-[44px] items-center justify-center rounded-[3px] border border-line bg-surface text-ash hover:text-paper" | |
| + | > | |
| + | <SearchIcon /> | |
| + | </Link> | |
| <DisplayToggle /> | ||
| {/* Hamburger - page nav only (<md). */} | ||
| <button | ||
| @@ -162,6 +171,25 @@ export function SiteHeader() { | ||
| ); | ||
| } | ||
| + | function SearchIcon() { | |
| + | return ( | |
| + | <svg | |
| + | aria-hidden="true" | |
| + | width="16" | |
| + | height="16" | |
| + | viewBox="0 0 16 16" | |
| + | fill="none" | |
| + | stroke="currentColor" | |
| + | strokeWidth="1.5" | |
| + | strokeLinecap="round" | |
| + | strokeLinejoin="round" | |
| + | > | |
| + | <circle cx="6.5" cy="6.5" r="4.5" /> | |
| + | <line x1="10.5" y1="10.5" x2="14" y2="14" /> | |
| + | </svg> | |
| + | ); | |
| + | } | |
| + | ||
| function BurgerIcon({ open }: { open: boolean }) { | ||
| // Two-stroke mark that shifts to an X when open. Pure CSS, no color cue | ||
| // alone - the button also carries an aria-label. |
| @@ -44,6 +44,8 @@ const config: Config = { | ||
| display: ["var(--font-display)", "system-ui", "sans-serif"], | ||
| mono: ["var(--font-mono)", "ui-monospace", "monospace"], | ||
| prose: ["var(--font-prose)", "Georgia", "serif"], | ||
| + | // Atkinson Hyperlegible - body font for Reference (light) mode. | |
| + | body: ["var(--font-body)", "system-ui", "sans-serif"], | |
| }, | ||
| letterSpacing: { | ||
| tightest: "-0.04em", |