From 9ca74737291b7b52a0eb84d1fcfd3b2a0b37374f Mon Sep 17 00:00:00 2001 From: developer Date: Tue, 15 Sep 2026 18:50:34 +0000 Subject: [PATCH] feat(app): route every page with wouter Replace the single-scroll showcase and its anchor/scrollIntoView navigation with real routes: /, /patterns, /components/*, /foundations/*, /docs/*, plus a 404 fallback. - src/app/routes.tsx is the route table and the only source for paths, section headings, sidebar entries, breadcrumbs and the rail, so navigation cannot drift from the pages that exist - src/pages/* holds one component per route; App.tsx is now the shell (topbar, sidebar, breadcrumbs, Switch, right rail, scroll-to-top) - the active pattern moves into the URL (/patterns?pattern=table) and the sidebar/palette deep-link into it; SearchPalette results now carry a path - Sidebar/RightRail render with aria-current; Hero and Topbar use route links too (ButtonLink keeps the button visual) - PATTERNS + PatternId move to src/data/patterns.ts (no non-component export in a component file), pattern buttons gain aria-pressed - tests/routing.test.tsx: registry/nav invariants, per-route rendering, sidebar navigation, pattern deep links, 404 --- AGENTS.md | 14 +- README.md | 3 +- package-lock.json | 34 +++- package.json | 3 +- src/App.css | 57 ++++++ src/App.tsx | 257 +++++++-------------------- src/app/index.ts | 15 ++ src/app/routes.tsx | 197 ++++++++++++++++++++ src/components/AIWorkflowSection.tsx | 12 +- src/components/Button.tsx | 17 ++ src/components/Hero.tsx | 13 +- src/components/RightRail.tsx | 37 ++-- src/components/SearchPalette.tsx | 29 +-- src/components/Sidebar.tsx | 44 +++-- src/components/Topbar.tsx | 15 +- src/data/library.ts | 49 +---- src/data/patterns.ts | 12 ++ src/pages/ButtonsPage.tsx | 11 ++ src/pages/ColorsPage.tsx | 12 ++ src/pages/EditorPage.tsx | 5 + src/pages/FormsPage.tsx | 60 +++++++ src/pages/IconsPage.tsx | 5 + src/pages/MarkdownPage.tsx | 5 + src/pages/NotFoundPage.tsx | 24 +++ src/pages/OverviewPage.tsx | 37 ++++ src/pages/PatternsPage.tsx | 24 +++ src/pages/PrimitivesPage.tsx | 5 + src/pages/TypographyPage.tsx | 5 + tests/routing.test.tsx | 143 +++++++++++++++ 29 files changed, 827 insertions(+), 317 deletions(-) create mode 100644 src/app/routes.tsx create mode 100644 src/data/patterns.ts create mode 100644 src/pages/ButtonsPage.tsx create mode 100644 src/pages/ColorsPage.tsx create mode 100644 src/pages/EditorPage.tsx create mode 100644 src/pages/FormsPage.tsx create mode 100644 src/pages/IconsPage.tsx create mode 100644 src/pages/MarkdownPage.tsx create mode 100644 src/pages/NotFoundPage.tsx create mode 100644 src/pages/OverviewPage.tsx create mode 100644 src/pages/PatternsPage.tsx create mode 100644 src/pages/PrimitivesPage.tsx create mode 100644 src/pages/TypographyPage.tsx create mode 100644 tests/routing.test.tsx diff --git a/AGENTS.md b/AGENTS.md index f63ef94..f0b934e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,7 @@ Browser tooling here has no console access. Runtime errors therefore render into - **StatCard**: `trend` is sentiment (badge color), `direction` is the arrow and defaults to the sign of `change` — a "good" latency drop is `trend="up"` with `change="-4.2ms"` rendering a green `↓ -4.2ms`. - **Reusable Application Patterns**: `src/components/examples/` (`MetricsDashboard`, `SettingsPanel`, `ServiceTable`, `DeploymentModal`). - **Icon Library**: the registry in `src/components/icon-registry.ts` maps every `IconName` to a lucide component (strictly typed — typos fail `tsc`, unknown names at runtime **throw in dev** so the error sink surfaces them). 41 curated icons power the showcase; `lucide-react` ships 1,400+ for direct import. +- **Routing — wouter (3 kB)**: the route table is `src/app/routes.tsx` and it drives the `` in `App.tsx`, the sidebar, the breadcrumbs, the right rail and the ⌘K palette, so navigation cannot drift from the pages that exist. Adding a page = a `src/pages/*.tsx` component plus one registry entry (`path`, `group`, `label`, `index`, `kicker`, `title`, `description`, `meta`, `page`). Navigation is real links (``), never `onClick` + `scrollIntoView`; the shell scrolls to top on every location change. State that belongs in the URL lives in the query (`/patterns?pattern=table`), read with `useSearchParams` and validated by a registry helper (`patternFromSearch`). Deep links work because Vite serves the SPA fallback. ## Structure @@ -53,8 +54,14 @@ src/ utils.css ← token-compliant smart CSS utility classes index.css ← fonts, tokens & utils import, base resets App.css ← application component styles (token-driven, plain CSS) - App.tsx ← page composition & showcase layout + App.tsx ← app shell: topbar + sidebar + breadcrumbs + routes + right rail main.tsx ← mounts App inside the ErrorBoundary, installs the dev error sink + app/ + AppStateProvider.tsx ← global settings (theme, viewMode, sidebar, ⌘K search) + persistence + appState.ts ← context, types, useAppState() + routes.tsx ← route table: paths, headings, nav groups, URL helpers + persist.ts ← guarded localStorage read/write + storage keys + pages/ ← one component per route (OverviewPage, ButtonsPage, PatternsPage, …) dev/ ErrorBoundary.tsx, errorSink.ts ← render runtime errors into #dev-errors utils/ @@ -77,9 +84,10 @@ src/ Sidebar.tsx, Section.tsx, FormField.tsx, TokenGrid.tsx, MarkdownRenderer.tsx, CodeEditor.tsx, Button.tsx data/ - library.ts ← demo content, nav groups (patternId routing) & color tokens + library.ts ← demo content (button examples, color tokens) + patterns.ts ← pattern catalogue + PatternId (feeds /patterns?pattern=…) tests/ ← vitest suites (jsdom): overlay, icon-registry, useTimeout, - check-tokens guard + app-state, routing, check-tokens guard scripts/check-tokens.mjs ← token contract guard (part of npm run check) ``` diff --git a/README.md b/README.md index e39e01a..49313cd 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ src/ index.css ← fonts, token import, base resets utils.css ← token-compliant utility classes App.css ← application showcase styles - app/ ← AppStateProvider + useAppState(): theme, viewMode, sidebar, ⌘K search + app/ ← AppStateProvider + useAppState(), and the route table (wouter) + pages/ ← one page component per route components/ ui/ ← composable primitives (Overlay, Dialog, Sheet, Tabs, Alert, …) icon-registry.ts← the one icon name → lucide component map diff --git a/package-lock.json b/package-lock.json index d6eb898..e934d14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "lucide-react": "^1.45.0", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "wouter": "^3.11.0" }, "devDependencies": { "@testing-library/jest-dom": "^7.0.1", @@ -2007,6 +2008,15 @@ "node": ">=8" } }, + "node_modules/regexparam": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", + "integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -2235,6 +2245,15 @@ "dev": true, "license": "MIT" }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", @@ -2461,6 +2480,19 @@ "node": ">=8" } }, + "node_modules/wouter": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/wouter/-/wouter-3.11.0.tgz", + "integrity": "sha512-xyMLHhytdGhIjXVRcSjeYbXd7TR1lrxVTPLGBWmmyW52ZuWVQ1jXmsgoJ4G5s4tjloJCq7h4i2eREljWUQ4+/A==", + "license": "Unlicense", + "dependencies": { + "regexparam": "^3.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/package.json b/package.json index f4dfbe8..17574a1 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "dependencies": { "lucide-react": "^1.45.0", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "wouter": "^3.11.0" }, "devDependencies": { "@testing-library/jest-dom": "^7.0.1", diff --git a/src/App.css b/src/App.css index d94e0ca..5acb527 100644 --- a/src/App.css +++ b/src/App.css @@ -334,6 +334,7 @@ border: 0; background: none; color: var(--sidebar-foreground); + text-decoration: none; text-align: left; padding: 9px var(--space-5); border-radius: 0; @@ -467,6 +468,7 @@ border-radius: var(--radius-md); padding: var(--space-2) var(--space-4); color: var(--foreground); + text-decoration: none; font-size: var(--text-xs); display: inline-flex; align-items: center; @@ -2023,3 +2025,58 @@ footer { color: var(--foreground); } } + +/* ── Routing shell ──────────────────────────────────────────── */ +.brand-link { + display: flex; + align-items: center; + gap: 10px; + color: inherit; + text-decoration: none; +} + +/* Landing route: one card per group, on the hero's own rhythm. */ +.overview-grid { + max-width: 1180px; + margin: 0 auto; + padding: 0 var(--space-9) var(--space-11); + display: grid; + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); + gap: var(--space-5); +} + +.overview-links { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: var(--space-3); + font-size: var(--text-sm); +} + +.overview-links a { + display: inline-flex; + align-items: center; + gap: var(--space-2); + color: var(--muted-foreground); + text-decoration: none; +} + +.overview-links a:hover { + color: var(--primary); +} + +.not-found { + display: grid; + gap: var(--space-6); + justify-items: center; + padding: var(--space-9) 0; +} + +.not-found .overview-links { + justify-items: center; +} + +@media (max-width: 760px) { + .overview-grid { padding-bottom: var(--space-9); } +} diff --git a/src/App.tsx b/src/App.tsx index 7aa4ae1..cf08ba5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,56 +1,59 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' +import { Route, Switch, useLocation, useSearch } from 'wouter' import './App.css' -import { useAppState } from './app' +import { + OVERVIEW_LABEL, + OVERVIEW_PATH, + activeHref, + navigationGroups, + routeForPath, + routes, + useAppState, + type AppRoute, +} from './app' import { Topbar } from './components/Topbar' import { Sidebar } from './components/Sidebar' -import { Hero } from './components/Hero' -import { ButtonMatrix } from './components/ButtonMatrix' -import { TokenGrid } from './components/TokenGrid' -import { FormField, TextInput } from './components/FormField' -import { CodeSample } from './components/CodeSample' -import { MarkdownRenderer } from './components/MarkdownRenderer' -import { CodeEditor } from './components/CodeEditor' -import { AIWorkflowSection, type PatternId } from './components/AIWorkflowSection' -import { IconMatrix } from './components/IconMatrix' -import { TypographySection } from './components/TypographySection' import { Section } from './components/Section' import { RightRail } from './components/RightRail' import { SearchPalette, type SearchItem } from './components/SearchPalette' -import { Icon } from './components/Icon' -import { Avatar, Spinner, Switch, Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui' -import { buttonExamples, buttonTabs, colorTokens, navigationGroups, pageSections } from './data/library' +import { OverviewPage } from './pages/OverviewPage' +import { NotFoundPage } from './pages/NotFoundPage' -const INSTALL_CMD = 'pnpm add @northstar/ui @northstar/tokens' +/** Wraps a registry page in its Section — src/app/routes.tsx owns the copy. */ +function RoutePage({ route }: { route: AppRoute }) { + const Page = route.page + return ( +
+ +
+ ) +} export default function App() { - /* App-wide settings live in AppStateProvider; section state stays local. */ + /* App-wide settings live in AppStateProvider; shell state stays local. */ const { searchOpen, closeSearch, sidebarCollapsed, toggleSidebar } = useAppState() - const [active, setActive] = useState('Buttons') - const [buttonTab, setButtonTab] = useState('Preview') - const [visible, setVisible] = useState(false) + const [location, navigate] = useLocation() + const search = useSearch() const [menu, setMenu] = useState(false) - const [pattern, setPattern] = useState('metrics') - const handleNavigate = (item: { label: string; id?: string; patternId?: PatternId }) => { - setActive(item.label) - setMenu(false) + const current = routeForPath(location) + const active = current?.label ?? (location === OVERVIEW_PATH ? OVERVIEW_LABEL : 'Not found') + const currentHref = activeHref(location, search) + const groupItems = navigationGroups.find(group => group.label === current?.group)?.items ?? [] - // Synchronize active pattern — routing keys on patternId, never on display labels. - if (item.patternId) setPattern(item.patternId) - - const targetId = item.id || item.label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') - const el = document.getElementById(targetId) - if (el) { - el.scrollIntoView({ behavior: 'smooth', block: 'start' }) - window.history.replaceState(null, '', `#${targetId}`) - } - } + /* A client-side navigation keeps the previous scroll position otherwise. */ + useEffect(() => { + window.scrollTo({ top: 0 }) + }, [location]) + /* One palette result → one route; patterns carry their id in the query. */ const handleSearchSelect = (item: SearchItem) => { - if (item.patternId) { - setPattern(item.patternId) - } - handleNavigate({ label: item.title, id: item.targetId }) + navigate(item.patternId ? `${item.path}?pattern=${item.patternId}` : item.path) } return ( @@ -60,172 +63,40 @@ export default function App() {
setMenu(false)} collapsed={sidebarCollapsed} onToggleCollapse={toggleSidebar} />
- Components + {current ? current.group : 'Northstar'} / {active}
- + {location === OVERVIEW_PATH ? ( + + ) : ( +
+
+ + {routes.map(route => ( + + + + ))} + + + + +
-
-
-
4 Reusable Components} - > - -
- -
Button · 12 variants} - > - -
- -
iconRegistry · 41 curated · lucide 1,400+} - > - -
- -
- View all tokens - - } - > - -
- -
Type scale · 9→70px} - > - -
- -
-
- - - - -
- - -
-
- -
- - Primitives in action - Spinner, Avatar and Switch — token-driven, theme-aware components/ui primitives. - - - - - - -
-
- - Tabs primitive - Roving tabindex, arrow-key navigation, aria-selected wiring — try the keyboard. - - - - Preview - Code - - - Token-driven preview pane. - - - import {'{ Tabs }'} from components/ui - - -
-
-
- -
- -
- -
Markdown · styled} - > - -
- -
codemirror · dark} - > - -
+ {current && }
- - -
+ )}
northstar design system @@ -236,7 +107,3 @@ export default function App() {
) } - -function copyInstall() { - navigator.clipboard?.writeText(INSTALL_CMD) -} diff --git a/src/app/index.ts b/src/app/index.ts index e37b522..c06879f 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -11,3 +11,18 @@ export { type ViewMode, } from './appState' export { STORAGE_KEYS } from './persist' +export { + GROUP_BLURBS, + OVERVIEW_LABEL, + OVERVIEW_PATH, + PATTERNS_PATH, + ROUTE_GROUPS, + activeHref, + navigationGroups, + patternFromSearch, + routeForPath, + routes, + routesInGroup, + type AppRoute, + type RouteGroup, +} from './routes' diff --git a/src/app/routes.tsx b/src/app/routes.tsx new file mode 100644 index 0000000..0df3553 --- /dev/null +++ b/src/app/routes.tsx @@ -0,0 +1,197 @@ +import type { ComponentType, ReactNode } from 'react' +import { PATTERNS } from '../data/patterns' +import type { NavigationGroup } from '../components/Sidebar' +import { PatternsPage } from '../pages/PatternsPage' +import { ButtonsPage } from '../pages/ButtonsPage' +import { IconsPage } from '../pages/IconsPage' +import { ColorsPage } from '../pages/ColorsPage' +import { TypographyPage } from '../pages/TypographyPage' +import { FormsPage } from '../pages/FormsPage' +import { PrimitivesPage } from '../pages/PrimitivesPage' +import { MarkdownPage } from '../pages/MarkdownPage' +import { EditorPage } from '../pages/EditorPage' + +/* ── The route table ───────────────────────────────────────────────────────── + One registry drives the router, the sidebar, the breadcrumbs and the ⌘K + palette, so navigation can never drift from the pages that exist. Add a + page by adding an entry here plus a component in src/pages/. + ────────────────────────────────────────────────────────────────────────── */ + +export const PATTERNS_PATH = '/patterns' + +export const ROUTE_GROUPS = ['Application Patterns', 'Components', 'Foundations', 'Documentation'] as const + +export type RouteGroup = (typeof ROUTE_GROUPS)[number] + +export const GROUP_BLURBS: Record = { + 'Application Patterns': 'Production-grade composites that ship as one drop-in unit.', + Components: 'The atoms: buttons, icons, form controls and the primitives behind them.', + Foundations: 'The tokens that carry the system — color roles and the type scale.', + Documentation: 'Editorial and developer surfaces for docs, changelogs and embedded code.', +} + +export type AppRoute = { + path: string + group: RouteGroup + /** Sidebar entry, breadcrumb tail and search result title. */ + label: string + /** Rendered above the section heading as `02 / COMPONENTS`. */ + index: string + kicker: string + title: string + description: string + /** Right-hand action in the section heading (api pill, doc link, …). */ + meta?: ReactNode + page: ComponentType +} + +export const routes: AppRoute[] = [ + { + path: PATTERNS_PATH, + group: 'Application Patterns', + label: 'Application Patterns', + index: '01', + kicker: 'PATTERNS & EXAMPLES', + title: 'Reusable Application Patterns', + description: + 'Production-grade, fully functional composable components ready to drop into any project.', + meta: 4 Reusable Components, + page: PatternsPage, + }, + { + path: '/components/buttons', + group: 'Components', + label: 'Buttons', + index: '02', + kicker: 'COMPONENTS', + title: 'Buttons', + description: 'Actions with a clear hierarchy and dependable feedback.', + meta: Button · 12 variants, + page: ButtonsPage, + }, + { + path: '/components/icons', + group: 'Components', + label: 'Icons', + index: '03', + kicker: 'COMPONENTS', + title: 'Icons (Lucide)', + description: + 'Crisp, scalable SVG icons powered by Lucide. Tree-shakeable, stroke-matched, and token-aware.', + meta: iconRegistry · 41 curated · lucide 1,400+, + page: IconsPage, + }, + { + path: '/foundations/colors', + group: 'Foundations', + label: 'Color tokens', + index: '04', + kicker: 'FOUNDATIONS', + title: 'Color tokens', + description: 'Functional color roles in CSS Color 4 lab() keep every surface intentional.', + meta: 48 tokens · lab(), + page: ColorsPage, + }, + { + path: '/foundations/typography', + group: 'Foundations', + label: 'Typography', + index: '05', + kicker: 'FOUNDATIONS', + title: 'Typography', + description: 'Modular rem-based type scale with dual-family hierarchy (Manrope & DM Mono).', + meta: Type scale · 9→70px, + page: TypographyPage, + }, + { + path: '/components/forms', + group: 'Components', + label: 'Form controls', + index: '06', + kicker: 'PRIMITIVES', + title: 'Form controls', + description: 'Inputs designed for focus, flow, and the occasional messy edge case.', + page: FormsPage, + }, + { + path: '/components/code', + group: 'Components', + label: 'Composable code', + index: '07', + kicker: 'CODE', + title: 'Composable by default', + description: 'Small APIs. Strong defaults. No surprises.', + page: PrimitivesPage, + }, + { + path: '/docs/markdown', + group: 'Documentation', + label: 'Markdown rendering', + index: '08', + kicker: 'CONTENT', + title: 'Markdown rendering', + description: 'A readable editorial layer for changelogs, docs, and product notes.', + meta: Markdown · styled, + page: MarkdownPage, + }, + { + path: '/docs/editor', + group: 'Documentation', + label: 'CodeMirror theme', + index: '09', + kicker: 'DEVELOPER TOOLING', + title: 'CodeMirror theme', + description: 'A focused editor palette for examples, playgrounds, and embedded code.', + meta: codemirror · dark, + page: EditorPage, + }, +] + +/** Sidebar counts shown next to a few entries. */ +const NAV_COUNTS: Partial> = { + '/components/buttons': 12, + '/components/icons': 41, +} + +export const OVERVIEW_PATH = '/' +export const OVERVIEW_LABEL = 'Overview' + +export function routeForPath(path: string): AppRoute | undefined { + return routes.find(route => route.path === path) +} + +export function routesInGroup(group: RouteGroup): AppRoute[] { + return routes.filter(route => route.group === group) +} + +/** Sidebar navigation: every group plus the individual patterns as sub-entries. */ +export const navigationGroups: NavigationGroup[] = [ + { + label: 'Application Patterns', + items: PATTERNS.map(pattern => ({ + label: pattern.label, + href: `${PATTERNS_PATH}?pattern=${pattern.id}`, + })), + }, + ...ROUTE_GROUPS.filter(group => group !== 'Application Patterns').map(group => ({ + label: group, + items: routesInGroup(group).map(route => ({ + label: route.label, + href: route.path, + count: NAV_COUNTS[route.path], + })), + })), +] + +/** Validate `?pattern=` without trusting the URL. */ +export function patternFromSearch(search: string): (typeof PATTERNS)[number]['id'] { + const requested = new URLSearchParams(search).get('pattern') + return PATTERNS.some(pattern => pattern.id === requested) + ? (requested as (typeof PATTERNS)[number]['id']) + : 'metrics' +} + +/** The nav entry the current URL belongs to — pattern variants share a path. */ +export function activeHref(location: string, search: string): string { + return location === PATTERNS_PATH ? `${PATTERNS_PATH}?pattern=${patternFromSearch(search)}` : location +} diff --git a/src/components/AIWorkflowSection.tsx b/src/components/AIWorkflowSection.tsx index 37ceb16..031190d 100644 --- a/src/components/AIWorkflowSection.tsx +++ b/src/components/AIWorkflowSection.tsx @@ -6,15 +6,7 @@ import { DeploymentModal } from './examples/DeploymentModal' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from './ui/Card' import { Badge } from './ui/Badge' import { Icon } from './Icon' - -const PATTERNS = [ - { id: 'metrics', label: 'Metrics Dashboard', icon: 'sparkles' }, - { id: 'settings', label: 'Settings & Safeguards', icon: 'settings' }, - { id: 'table', label: 'Service Monitor Table', icon: 'layers' }, - { id: 'deploy', label: 'Deployment Flow', icon: 'terminal' }, -] as const - -export type PatternId = (typeof PATTERNS)[number]['id'] +import { PATTERNS, type PatternId } from '../data/patterns' type AIWorkflowSectionProps = { activePattern?: PatternId @@ -41,7 +33,9 @@ export function AIWorkflowSection({ {PATTERNS.map(p => ( ) } + +type ButtonLinkProps = { + href: string + variant?: ButtonVariant + className?: string + children: ReactNode +} + +/** The same visual action, rendered as a route link (``). */ +export function ButtonLink({ href, variant = 'secondary', className = '', children }: ButtonLinkProps) { + return ( + + {children} + + ) +} diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index f7550d2..af5abb7 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -1,4 +1,4 @@ -import { Button } from './Button' +import { ButtonLink } from './Button' import { Icon } from './Icon' export function Hero() { @@ -9,13 +9,12 @@ export function Hero() {

Build with clarity.

A considered set of accessible, composable components for building calm, capable interfaces.

- - + + + Read the principles +
diff --git a/src/components/RightRail.tsx b/src/components/RightRail.tsx index 2906985..ddcb2c8 100644 --- a/src/components/RightRail.tsx +++ b/src/components/RightRail.tsx @@ -1,22 +1,31 @@ +import { Link } from 'wouter' import { Button } from './Button' import { Icon } from './Icon' +import type { NavigationItem } from './Sidebar' -type RightRailProps = { sections: string[] } +type RightRailProps = { + /** Group the current page belongs to — becomes the rail heading. */ + title: string + links: NavigationItem[] + activeHref: string +} -export function RightRail({ sections }: RightRailProps) { +export function RightRail({ title, links, activeHref }: RightRailProps) { return (