@kode4/react-foundation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/DESIGN-SYSTEM.md +190 -0
  2. package/INSTRUCTIONS.md +694 -0
  3. package/LICENSE +21 -0
  4. package/README.md +53 -0
  5. package/errors.d.ts +24 -0
  6. package/forms/Form/Form.d.ts +30 -0
  7. package/forms/Form/index.d.ts +1 -0
  8. package/forms/index.d.ts +1 -0
  9. package/hooks/index.d.ts +1 -0
  10. package/hooks/useMediaQuery.d.ts +8 -0
  11. package/index.d.ts +24 -0
  12. package/index.js +1679 -0
  13. package/index.js.map +1 -0
  14. package/internal/env.d.ts +1 -0
  15. package/internal/registry.d.ts +3 -0
  16. package/internal/type-utils.d.ts +6 -0
  17. package/layout/AppLayout/AppLayout.d.ts +18 -0
  18. package/layout/AppLayout/index.d.ts +1 -0
  19. package/layout/CenteredLayout/CenteredLayout.d.ts +12 -0
  20. package/layout/CenteredLayout/index.d.ts +1 -0
  21. package/layout/Columns/Columns.d.ts +35 -0
  22. package/layout/Columns/index.d.ts +1 -0
  23. package/layout/Container/Container.d.ts +5 -0
  24. package/layout/Container/index.d.ts +1 -0
  25. package/layout/DashboardGrid/DashboardGrid.d.ts +37 -0
  26. package/layout/DashboardGrid/index.d.ts +1 -0
  27. package/layout/FillHeight/FillHeight.d.ts +16 -0
  28. package/layout/FillHeight/index.d.ts +1 -0
  29. package/layout/Footer/Footer.d.ts +5 -0
  30. package/layout/Footer/index.d.ts +1 -0
  31. package/layout/Header/Header.d.ts +13 -0
  32. package/layout/Header/index.d.ts +1 -0
  33. package/layout/SideBar/SideBar.d.ts +6 -0
  34. package/layout/SideBar/index.d.ts +1 -0
  35. package/layout/SiteLayout/SiteLayout.d.ts +27 -0
  36. package/layout/SiteLayout/index.d.ts +1 -0
  37. package/layout/SkipLink/SkipLink.d.ts +9 -0
  38. package/layout/SkipLink/index.d.ts +1 -0
  39. package/layout/TopBar/TopBar.d.ts +32 -0
  40. package/layout/TopBar/index.d.ts +1 -0
  41. package/layout/index.d.ts +12 -0
  42. package/package.json +52 -0
  43. package/query/index.d.ts +1 -0
  44. package/query/sort.d.ts +19 -0
  45. package/router/DefaultErrorBoundary.d.ts +5 -0
  46. package/router/TypedLink/TypedLink.d.ts +22 -0
  47. package/router/TypedLink/index.d.ts +1 -0
  48. package/router/adapter.d.ts +27 -0
  49. package/router/chain.d.ts +17 -0
  50. package/router/context.d.ts +24 -0
  51. package/router/createAppRouter.d.ts +15 -0
  52. package/router/defineRoute.d.ts +11 -0
  53. package/router/index.d.ts +11 -0
  54. package/router/menu.d.ts +51 -0
  55. package/router/pathTo.d.ts +21 -0
  56. package/router/types.d.ts +72 -0
  57. package/router/useRoute.d.ts +22 -0
  58. package/style.css +3 -0
  59. package/theme/ModeToggle/ModeToggle.d.ts +6 -0
  60. package/theme/ModeToggle/index.d.ts +1 -0
  61. package/theme/ThemeProvider/ThemeProvider.d.ts +25 -0
  62. package/theme/ThemeProvider/index.d.ts +1 -0
  63. package/theme/Toaster/Toaster.d.ts +9 -0
  64. package/theme/Toaster/index.d.ts +2 -0
  65. package/theme/Toaster/toast.d.ts +38 -0
  66. package/theme/index.d.ts +3 -0
  67. package/ui/Accordion/Accordion.d.ts +7 -0
  68. package/ui/Accordion/index.d.ts +1 -0
  69. package/ui/Alert/Alert.d.ts +11 -0
  70. package/ui/Alert/index.d.ts +1 -0
  71. package/ui/AlertDialog/AlertDialog.d.ts +19 -0
  72. package/ui/AlertDialog/index.d.ts +1 -0
  73. package/ui/Avatar/Avatar.d.ts +7 -0
  74. package/ui/Avatar/index.d.ts +1 -0
  75. package/ui/AwaitErrorBlock/AwaitErrorBlock.d.ts +11 -0
  76. package/ui/AwaitErrorBlock/index.d.ts +1 -0
  77. package/ui/Badge/Badge.d.ts +10 -0
  78. package/ui/Badge/index.d.ts +1 -0
  79. package/ui/Button/Button.d.ts +13 -0
  80. package/ui/Button/index.d.ts +1 -0
  81. package/ui/Card/Card.d.ts +11 -0
  82. package/ui/Card/index.d.ts +1 -0
  83. package/ui/Checkbox/Checkbox.d.ts +5 -0
  84. package/ui/Checkbox/index.d.ts +1 -0
  85. package/ui/Command/Command.d.ts +19 -0
  86. package/ui/Command/index.d.ts +1 -0
  87. package/ui/Dialog/Dialog.d.ts +17 -0
  88. package/ui/Dialog/index.d.ts +1 -0
  89. package/ui/DropdownMenu/DropdownMenu.d.ts +28 -0
  90. package/ui/DropdownMenu/index.d.ts +1 -0
  91. package/ui/ErrorBlock/ErrorBlock.d.ts +21 -0
  92. package/ui/ErrorBlock/index.d.ts +1 -0
  93. package/ui/Input/Input.d.ts +4 -0
  94. package/ui/Input/index.d.ts +1 -0
  95. package/ui/Label/Label.d.ts +5 -0
  96. package/ui/Label/index.d.ts +1 -0
  97. package/ui/Pagination/Pagination.d.ts +17 -0
  98. package/ui/Pagination/index.d.ts +1 -0
  99. package/ui/Popover/Popover.d.ts +7 -0
  100. package/ui/Popover/index.d.ts +1 -0
  101. package/ui/Progress/Progress.d.ts +16 -0
  102. package/ui/Progress/index.d.ts +1 -0
  103. package/ui/RadioGroup/RadioGroup.d.ts +7 -0
  104. package/ui/RadioGroup/index.d.ts +1 -0
  105. package/ui/Select/Select.d.ts +16 -0
  106. package/ui/Select/index.d.ts +1 -0
  107. package/ui/Separator/Separator.d.ts +5 -0
  108. package/ui/Separator/index.d.ts +1 -0
  109. package/ui/Sheet/Sheet.d.ts +17 -0
  110. package/ui/Sheet/index.d.ts +1 -0
  111. package/ui/Skeleton/Skeleton.d.ts +4 -0
  112. package/ui/Skeleton/index.d.ts +1 -0
  113. package/ui/Spinner/Spinner.d.ts +35 -0
  114. package/ui/Spinner/index.d.ts +1 -0
  115. package/ui/Switch/Switch.d.ts +5 -0
  116. package/ui/Switch/index.d.ts +1 -0
  117. package/ui/Table/Table.d.ts +11 -0
  118. package/ui/Table/index.d.ts +1 -0
  119. package/ui/Tabs/Tabs.d.ts +8 -0
  120. package/ui/Tabs/index.d.ts +1 -0
  121. package/ui/Textarea/Textarea.d.ts +4 -0
  122. package/ui/Textarea/index.d.ts +1 -0
  123. package/ui/Tooltip/Tooltip.d.ts +7 -0
  124. package/ui/Tooltip/index.d.ts +1 -0
  125. package/ui/index.d.ts +29 -0
  126. package/utils.d.ts +8 -0
@@ -0,0 +1,694 @@
1
+ # Instructions — building apps with `@kode4/react-foundation`
2
+
3
+ This guide is written for someone (human or AI agent) **consuming** the library
4
+ to build an application. It captures the patterns, mental model, and idioms the
5
+ library expects, with copy-pasteable examples. If you follow these patterns the
6
+ app will be idiomatic, type-safe, and themeable.
7
+
8
+ > **Companion docs.** `DESIGN-SYSTEM.md` is the theming/token contract.
9
+ > `roadmap.md` lists planned components. `CLAUDE.md` is the primer for working
10
+ > *inside* the library repo (different audience — internal conventions). This
11
+ > file is the *consumer* contract and must be kept in sync as the library grows.
12
+
13
+ > **Import paths.** A published consumer imports from the package name and loads
14
+ > the stylesheet once:
15
+ >
16
+ > ```ts
17
+ > import { Button, defineRoute, useRoute, ThemeProvider } from '@kode4/react-foundation';
18
+ > import '@kode4/react-foundation/styles.css'; // once, near the app root
19
+ > ```
20
+ >
21
+ > Inside *this* repo the demo consumes the same barrel via the local alias
22
+ > `@/lib` (and the barrel side-effect-imports the CSS, so no separate style
23
+ > import is needed locally). Every example below uses `@kode4/react-foundation`;
24
+ > substitute `@/lib` when working in-repo.
25
+
26
+ ---
27
+
28
+ ## 1. Mental model (read this first)
29
+
30
+ The library is three things behind one barrel:
31
+
32
+ 1. **A typed router** — a thin layer over React Router DOM v7's data router. You
33
+ declare each route once with `defineRoute` (path, Zod schemas for
34
+ params/search, a loader, the component, menu/tabs metadata). From those
35
+ declarations the library derives the router config and a registry that powers
36
+ typed links and menus. **Routes are the single source of truth.**
37
+ 2. **A themed UI component library** — 30+ components (shadcn/ui as the design
38
+ blueprint) built on Radix primitives, styled entirely through CSS variables.
39
+ 3. **A design system** — semantic design tokens with light/dark mode; everything
40
+ themeable by re-declaring CSS variables, no rebuild.
41
+
42
+ Two rules that drive everything:
43
+
44
+ - **Read route state only through `useRoute(routeDef)`** — never reach into React
45
+ Router hooks for params/search you've declared. `useRoute` returns them parsed
46
+ and typed.
47
+ - **Never hand-write URLs** — use `TypedLink` / `pathTo`, which enforce params
48
+ and search against the route's schemas.
49
+
50
+ ---
51
+
52
+ ## 2. App bootstrap
53
+
54
+ Compose the providers in this order (outermost → innermost). `ThemeProvider`
55
+ must wrap everything so the mode class is on `<html>` before anything paints;
56
+ `Toaster` is a sibling of the router.
57
+
58
+ ```tsx
59
+ import { QueryClientProvider } from '@tanstack/react-query';
60
+ import { Suspense } from 'react';
61
+ import { RouterProvider } from 'react-router-dom';
62
+
63
+ import { SpinnerBlock, ThemeProvider, Toaster } from '@kode4/react-foundation';
64
+ import '@kode4/react-foundation/styles.css';
65
+
66
+ import { router } from './app/router';
67
+ import { queryClient } from './query/client';
68
+
69
+ export function App() {
70
+ return (
71
+ <ThemeProvider>
72
+ <QueryClientProvider client={queryClient}>
73
+ <Suspense fallback={<SpinnerBlock label="Loading…" />}>
74
+ <RouterProvider router={router} />
75
+ </Suspense>
76
+ <Toaster />
77
+ </QueryClientProvider>
78
+ </ThemeProvider>
79
+ );
80
+ }
81
+ ```
82
+
83
+ **Fonts.** The design system's `--font-sans` token points at `'Open Sans
84
+ Variable'` first, then system fallbacks. Load the font (self-hosted via
85
+ `@fontsource-variable/open-sans` keeps it GDPR-clean and offline) or override
86
+ `--font-sans` in your app CSS. See `DESIGN-SYSTEM.md` → Typography.
87
+
88
+ ---
89
+
90
+ ## 3. The typed router
91
+
92
+ ### 3.1 Declaring a route
93
+
94
+ `defineRoute` is the unit of declaration. Co-locate it with its page component.
95
+
96
+ ```tsx
97
+ import { z } from 'zod';
98
+ import { defineRoute, useRoute } from '@kode4/react-foundation';
99
+ import { fetchUser } from './api';
100
+
101
+ const userParamsSchema = z.object({
102
+ id: z.coerce.number().int().positive(), // URL params are STRINGS — coerce!
103
+ });
104
+
105
+ export const userDetailRoute = defineRoute({
106
+ path: 'users/:id',
107
+ component: UserDetailPage,
108
+ params: userParamsSchema,
109
+ loader: async ({ params }) => ({ user: await fetchUser(params.id) }),
110
+ });
111
+
112
+ export function UserDetailPage() {
113
+ const { params, data } = useRoute(userDetailRoute); // params.id is `number`
114
+ return <h1>{data.user.name} (#{params.id})</h1>;
115
+ }
116
+ ```
117
+
118
+ `defineRoute` accepts: `path`, `component`, `params` (Zod), `search` (Zod),
119
+ `loader`, `menu` (`{ label, icon }`), `tabs`, and `children` (nested routes).
120
+ Everything is inferred from the schemas + loader — no manual generics.
121
+
122
+ ### 3.2 Params vs. search
123
+
124
+ - **Path params** (`:id`) are always strings in the URL — wrap numeric ones in
125
+ `z.coerce.number()`. Validation runs once at the loader boundary; invalid input
126
+ yields a `400 Response`.
127
+ - **Search params** are typically `optional()` or `.default(...)`:
128
+
129
+ ```ts
130
+ const search = z.object({ q: z.string().optional(), tab: z.enum(TABS).default('overview') });
131
+ // route: defineRoute({ ..., search })
132
+ // read: const { search } = useRoute(route); // search.tab is the enum
133
+ ```
134
+
135
+ - **Writing search state**: use React Router's `useSearchParams` setter with
136
+ `{ replace: true }` — never `window.history`.
137
+
138
+ ```tsx
139
+ const [, setSearchParams] = useSearchParams();
140
+ setSearchParams((prev) => { prev.set('q', value); return prev; }, { replace: true });
141
+ ```
142
+
143
+ ### 3.3 Loaders, guards, and middleware chains
144
+
145
+ A loader is `({ params, search, context, request }) => data`. `context` carries
146
+ `queryClient` plus whatever your app augments in (e.g. `auth` — see 3.6).
147
+
148
+ Simple loader: a plain async function (see 3.1). For cross-cutting concerns
149
+ (auth, roles) compose typed middleware with `chain()`; each step's return value
150
+ **merges** into the accumulated, inferred `data`:
151
+
152
+ ```tsx
153
+ import { chain, defineRoute, useRoute } from '@kode4/react-foundation';
154
+ import { requireAuth, requireRole } from './auth/guards';
155
+
156
+ export const adminRoute = defineRoute({
157
+ path: 'admin',
158
+ component: AdminPage,
159
+ loader: chain()
160
+ .use(requireAuth) // contributes { user }
161
+ .use(requireRole('admin')) // requires { user }, contributes nothing
162
+ .use(() => ({ metrics: getMetrics() }))
163
+ .build(),
164
+ });
165
+
166
+ function AdminPage() {
167
+ const { data } = useRoute(adminRoute); // data: { user, metrics } — fully typed
168
+ }
169
+ ```
170
+
171
+ Write guards as `LoaderMiddleware<Contributes, Requires>`:
172
+
173
+ ```ts
174
+ import { redirect } from 'react-router-dom';
175
+ import type { LoaderMiddleware } from '@kode4/react-foundation';
176
+
177
+ export const requireAuth: LoaderMiddleware<{ user: User }> = ({ context, request }) => {
178
+ const user = context.auth.current;
179
+ if (!user) {
180
+ const url = new URL(request.url);
181
+ throw redirect(`/login?next=${encodeURIComponent(url.pathname + url.search)}`);
182
+ }
183
+ return { user };
184
+ };
185
+
186
+ // Factory that REQUIRES a prior step's { user }:
187
+ export function requireRole(role: Role): LoaderMiddleware<void, { user: User }> {
188
+ return ({ data }) => {
189
+ if (data.user.role !== role) throw new Response('Forbidden', { status: 403 });
190
+ };
191
+ }
192
+ ```
193
+
194
+ ### 3.4 Linking — never hand-write URLs
195
+
196
+ ```tsx
197
+ import { TypedLink, pathTo } from '@kode4/react-foundation';
198
+
199
+ <TypedLink route={userDetailRoute} params={{ id: 7 }}>Open user 7</TypedLink>
200
+ <TypedLink route={projectDetailRoute} params={{ id }} search={{ tab: 'tasks' }}>Tasks</TypedLink>
201
+
202
+ const href = pathTo(userDetailRoute, { params: { id: 7 } }); // for non-link uses
203
+ ```
204
+
205
+ `params`/`search` are checked against the route's schemas at the type level.
206
+ `pathTo` drops search args that fail their schema (with a dev-mode warning).
207
+
208
+ ### 3.5 Tabs
209
+
210
+ Tabs are just a search param plus `tabs` metadata on the route. The metadata
211
+ drives rendering; the search param holds the active tab:
212
+
213
+ ```tsx
214
+ const TABS = ['overview', 'tasks', 'team'] as const;
215
+ export const projectDetailRoute = defineRoute({
216
+ path: 'projects/:id',
217
+ params: z.object({ id: z.coerce.number() }),
218
+ search: z.object({ tab: z.enum(TABS).default('overview') }),
219
+ tabs: TABS.map((id) => ({ id, label: id[0].toUpperCase() + id.slice(1) })),
220
+ component: ProjectDetailPage,
221
+ });
222
+ // In the page: const { search } = useRoute(route); link tabs with <TypedLink ... search={{ tab }} />
223
+ ```
224
+
225
+ ### 3.6 Assembling the router + injecting app services
226
+
227
+ Build the router from your top-level routes. Pass `queryClient` and any app
228
+ services (here `auth`) as context; you can also override `hydrateFallback` /
229
+ `errorBoundary` here.
230
+
231
+ ```ts
232
+ import { createAppRouter } from '@kode4/react-foundation';
233
+
234
+ export const router = createAppRouter([shellRoute, loginRoute], {
235
+ queryClient,
236
+ auth: authState,
237
+ });
238
+ ```
239
+
240
+ Tell the library's typed `context` about your services by **augmenting the
241
+ declaring module** (not the barrel):
242
+
243
+ ```ts
244
+ // app/context.ts — import this once at startup (e.g. from main.tsx)
245
+ import type { AuthState } from './auth';
246
+
247
+ declare module '@kode4/react-foundation/router/context' {
248
+ interface AppContext {
249
+ auth: AuthState; // now context.auth is typed in every loader
250
+ }
251
+ }
252
+ ```
253
+
254
+ > Pitfalls: a route not included in the array passed to `createAppRouter` makes
255
+ > `pathTo()` throw. Don't call `pathTo()`/`resolveMenu()` at module load — the
256
+ > registry fills when the router config is built.
257
+
258
+ ---
259
+
260
+ ## 4. Menus
261
+
262
+ The menu is a hand-authored `MenuNode[]` tree — the single source of truth for
263
+ navigation. Top-level nodes render in the `TopBar`; their `children` render in
264
+ the `SideBar` when that top-level item is active.
265
+
266
+ ```tsx
267
+ import type { MenuNode } from '@kode4/react-foundation';
268
+
269
+ export const menuTree: MenuNode[] = [
270
+ { route: homeRoute },
271
+ { route: usersRoute, label: 'Manage', children: [
272
+ { route: usersRoute },
273
+ { route: settingsRoute, children: [{ route: profileRoute }, { route: securityRoute }] },
274
+ ]},
275
+ { group: 'Reports', children: [
276
+ { content: <small>Section note (arbitrary node)</small> },
277
+ 'separator',
278
+ { route: reportRoute },
279
+ ]},
280
+ ];
281
+ ```
282
+
283
+ Node kinds: `{ route, label?, icon?, children? }`, `{ group, children }`,
284
+ `'separator'`, and `{ content }` (arbitrary JSX). Labels default to the route's
285
+ `menu.label`. Resolve a tree to renderable items with `resolveMenu(menuTree)`.
286
+
287
+ ---
288
+
289
+ ## 5. App chrome & layout
290
+
291
+ There is no monolithic `<Shell>` — you **compose** the chrome from primitives, so
292
+ you control the structure. The canonical pattern:
293
+
294
+ ```tsx
295
+ import {
296
+ AppLayout, TopBar, SideBar, SkipLink, ModeToggle,
297
+ resolveMenu, findActiveTopItem,
298
+ } from '@kode4/react-foundation';
299
+
300
+ function Shell() {
301
+ const { pathname } = useLocation();
302
+ const topMenu = useMemo(() => resolveMenu(menuTree), []);
303
+ const sideItems = useMemo(
304
+ () => findActiveTopItem(topMenu, pathname)?.children ?? [],
305
+ [topMenu, pathname],
306
+ );
307
+
308
+ return (
309
+ <>
310
+ <SkipLink />
311
+ <AppLayout
312
+ header={
313
+ <TopBar
314
+ items={topMenu}
315
+ brand={<Link to="/">My App</Link>}
316
+ mobileMenu={topMenu} // full tree shown in the mobile drawer
317
+ end={<ModeToggle />}
318
+ />
319
+ }>
320
+ <div className="app-body">
321
+ <SideBar items={sideItems} />
322
+ <main><Outlet /></main>
323
+ </div>
324
+ </AppLayout>
325
+ </>
326
+ );
327
+ }
328
+ // Use this component as the `component` of a parent route whose `children` are
329
+ // the in-shell pages.
330
+ ```
331
+
332
+ ### Responsive navigation is automatic (mobile is a first-class requirement)
333
+
334
+ `TopBar` is **content-aware**: it measures whether its inline nav fits and
335
+ collapses into a hamburger + left-side drawer when it doesn't — so phones and
336
+ iPads in portrait get a usable drawer out of the box, with no per-app work.
337
+
338
+ - `mobileMenu` — the menu shown in the drawer; pass the full nested tree so
339
+ mobile users reach every page (the drawer renders a `SideBar` internally).
340
+ - `mobileQuery` — a *hard floor* (default `'(max-width: 768px)'`): at/below it the
341
+ nav always collapses; above it, collapse happens automatically on overflow.
342
+ Pass `null` to rely purely on overflow detection.
343
+ - On mobile, hide your in-content `SideBar` (the drawer already carries the full
344
+ nav) — e.g. `@media (max-width: 768px) { .app-body > .k4-sidebar { display: none } }`.
345
+
346
+ **Every component is expected to work on touch.** Avoid hover-only affordances as
347
+ the sole interaction.
348
+
349
+ ### Layout frames & primitives
350
+
351
+ - **Frames**: `AppLayout` (full-height app shell with a sticky `header` slot),
352
+ `SiteLayout` (constrained marketing-style column), `CenteredLayout` (centered
353
+ card, e.g. login).
354
+ - **Primitives**: `Container`, `Columns` (left/right asides + content; supports
355
+ independent scroll regions via a `scroll` mode), `FillHeight`, `Header`,
356
+ `Footer`, `TopBar`, `SideBar`, `SkipLink`.
357
+ - **DashboardGrid** — an equal-column grid with a base row height, for dashboards.
358
+ Drop blocks (typically `Card`s) in via `DashboardGridItem`; each defaults to one
359
+ cell and can span columns/rows. Spans are clamped to the column count (never
360
+ overflow), and the grid collapses to a single column on phones.
361
+
362
+ ```tsx
363
+ <DashboardGrid columns={4}> {/* columns + rowHeight have sensible defaults */}
364
+ <DashboardGridItem colSpan={2}><Card>…</Card></DashboardGridItem> {/* wide */}
365
+ <DashboardGridItem><Card>…</Card></DashboardGridItem> {/* 1×1 default */}
366
+ <DashboardGridItem colSpan={2} rowSpan={2}><Card>…</Card></DashboardGridItem> {/* big */}
367
+ </DashboardGrid>
368
+ ```
369
+
370
+ ---
371
+
372
+ ## 6. UI components
373
+
374
+ All components follow the shadcn/ui blueprint, are built on Radix primitives, and
375
+ expose their props type. Two conventions matter:
376
+
377
+ ### 6.1 `asChild`
378
+
379
+ Trigger-style components accept Radix's `asChild` to merge their behavior onto
380
+ your own element (commonly a `Button`):
381
+
382
+ ```tsx
383
+ <DialogTrigger asChild><Button appearance="outline">Open</Button></DialogTrigger>
384
+ ```
385
+
386
+ ### 6.2 The two-axis variant model (Button, Alert, Toast, Badge)
387
+
388
+ Color and fill are **orthogonal**:
389
+
390
+ - **`variant`** = color: `default` (neutral), `primary`, `secondary`,
391
+ `destructive`, `success` — plus `ghost` and `link` on `Button`.
392
+ - **`appearance`** = `filled` (default) or `outline`. `outline` is an
393
+ *appearance*, not a variant, and composes with every color.
394
+ - **`size`** (Button): `default`, `sm`, `lg`, `icon`.
395
+
396
+ ```tsx
397
+ <Button>Save</Button> {/* neutral, filled */}
398
+ <Button variant="primary">Save</Button>
399
+ <Button variant="destructive" appearance="outline">Delete</Button>
400
+ <Button variant="ghost" size="icon" aria-label="More"><MoreHorizontalIcon /></Button>
401
+ ```
402
+
403
+ ### 6.3 Confirm dialogs — `AlertDialog`
404
+
405
+ `AlertDialogAction` / `AlertDialogCancel` accept the same `variant` / `appearance`
406
+ / `size` props as `Button`, so no `asChild` plumbing is needed. They auto-close.
407
+
408
+ ```tsx
409
+ <AlertDialog>
410
+ <AlertDialogTrigger asChild><Button variant="destructive" appearance="outline">Delete</Button></AlertDialogTrigger>
411
+ <AlertDialogContent>
412
+ <AlertDialogHeader>
413
+ <AlertDialogTitle>Are you sure?</AlertDialogTitle>
414
+ <AlertDialogDescription>This cannot be undone.</AlertDialogDescription>
415
+ </AlertDialogHeader>
416
+ <AlertDialogFooter>
417
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
418
+ <AlertDialogAction variant="destructive">Delete</AlertDialogAction>
419
+ </AlertDialogFooter>
420
+ </AlertDialogContent>
421
+ </AlertDialog>
422
+ ```
423
+
424
+ ### 6.4 Pagination
425
+
426
+ Links styled like buttons; drive them with `href` (URL-based) or `onClick`
427
+ (client state). Mark the current page with `isActive`.
428
+
429
+ ```tsx
430
+ <Pagination>
431
+ <PaginationContent>
432
+ <PaginationItem><PaginationPrevious href="#" onClick={…} /></PaginationItem>
433
+ <PaginationItem><PaginationLink href="#" isActive>1</PaginationLink></PaginationItem>
434
+ <PaginationItem><PaginationEllipsis /></PaginationItem>
435
+ <PaginationItem><PaginationNext href="#" onClick={…} /></PaginationItem>
436
+ </PaginationContent>
437
+ </Pagination>
438
+ ```
439
+
440
+ ### 6.5 Component catalog
441
+
442
+ - **Inputs & forms**: `Input`, `Textarea`, `Label`, `Checkbox`, `Switch`,
443
+ `RadioGroup`, `Select`, `Form` (+ `FormField`/`FormItem`/`FormLabel`/
444
+ `FormControl`/`FormDescription`/`FormMessage`).
445
+ - **Actions & disclosure**: `Button`, `DropdownMenu`, `Dialog`, `AlertDialog`,
446
+ `Sheet`, `Popover`, `Tooltip`, `Command` (palette), `Accordion`, `Tabs`.
447
+ - **Display**: `Card`, `Table`, `Badge`, `Avatar`, `Separator`, `Skeleton`,
448
+ `Progress`, `Pagination`, `Alert`.
449
+ - **Feedback / async**: `Spinner` (+ `SpinnerBlock`, `DefaultHydrateFallback`),
450
+ `ErrorBlock`, `AwaitErrorBlock`, toasts (`Toaster` + `toast`).
451
+ - **Hooks**: `useMediaQuery('(max-width: 768px)')` for responsive logic that
452
+ can't be pure CSS.
453
+
454
+ ---
455
+
456
+ ## 7. Theming
457
+
458
+ Everything is themed through CSS variables — no rebuild. You re-declare tokens in
459
+ a stylesheet loaded **after** the library's.
460
+
461
+ - **Colors** use the unprefixed shadcn names (`--primary`, `--secondary`,
462
+ `--background`, `--foreground`, `--border`, `--destructive`, `--success`, …),
463
+ each often with a `-foreground` partner. Light values in `:root`, dark
464
+ overrides in `.dark`.
465
+ - **Dimensions** use the `--k4-` prefix where lib-specific. `--spacing` is the
466
+ global density lever (scales all padding/margin/gap/control sizes);
467
+ `--radius` controls corner rounding.
468
+ - **Mode toggle**: drop `<ModeToggle />` (typically in `TopBar`'s `end` slot);
469
+ read/set the mode with `useTheme()`. `ThemeProvider` manages the `.dark` class.
470
+
471
+ ```css
472
+ /* your app theme, loaded after the library */
473
+ :root { --primary: oklch(0.45 0.16 258); --radius: 0.4rem; --spacing: 0.25rem; }
474
+ .dark { --primary: oklch(0.68 0.14 258); }
475
+ ```
476
+
477
+ > **Don't reuse a system token name for an app-specific meaning.** Declaring e.g.
478
+ > `--accent` for your own purpose silently re-themes every component consuming
479
+ > that token. Name app variables distinctly (e.g. `--brand`).
480
+
481
+ The full contract — every token, the `-foreground` convention, custom fonts — is
482
+ in **`DESIGN-SYSTEM.md`**. A live token board ships in the demo at `/ui/tokens`.
483
+
484
+ ---
485
+
486
+ ## 8. Data fetching (TanStack Query)
487
+
488
+ The library doesn't impose a data layer, but it has a **standard blueprint** for
489
+ TanStack Query (v5). Follow it for every server resource — it gives one
490
+ definition per query that's reusable across components, route loaders, and the
491
+ query client, plus reliable cache invalidation.
492
+
493
+ Per entity (e.g. `article`), create four pieces, conventionally in the feature's
494
+ `<entity>-service.ts` (the fetchers themselves live in an `api.ts`):
495
+
496
+ ### 8.1 A hierarchical query-key factory
497
+
498
+ One source of truth for keys. The hierarchy is what makes **targeted
499
+ invalidation** possible: invalidating `lists()` refreshes every list variant at
500
+ once.
501
+
502
+ ```ts
503
+ import type { SortOrderInput } from '@kode4/react-foundation';
504
+
505
+ export const articleKeys = {
506
+ all: ['articles'] as const,
507
+ lists: () => [...articleKeys.all, 'list'] as const,
508
+ list: (sort?: SortOrderInput) => [...articleKeys.lists(), sort] as const,
509
+ items: () => [...articleKeys.all, 'item'] as const,
510
+ item: (id?: number) => [...articleKeys.items(), id] as const,
511
+ };
512
+ ```
513
+
514
+ ### 8.2 Query definitions with `queryOptions`
515
+
516
+ `queryOptions` colocates key + fn + options into a reusable object. The same
517
+ definition feeds `useQuery`, `useSuspenseQuery`, `ensureQueryData`,
518
+ `getQueryData`, etc. — never re-spell the key at each call site.
519
+
520
+ ```ts
521
+ import { queryOptions, skipToken } from '@tanstack/react-query';
522
+ import { fetchArticle, fetchArticleList } from './api';
523
+
524
+ export const articleQueries = {
525
+ list: (sort?: SortOrderInput) =>
526
+ queryOptions({ queryKey: articleKeys.list(sort), queryFn: () => fetchArticleList(sort) }),
527
+
528
+ // skipToken disables the query while keeping the data type non-undefined —
529
+ // prefer it over `enabled: false` for conditional fetches.
530
+ item: (id?: number) =>
531
+ queryOptions({
532
+ queryKey: articleKeys.item(id),
533
+ queryFn: id == null ? skipToken : () => fetchArticle(id),
534
+ retry: false, // a 404 on a detail fetch shouldn't be retried
535
+ }),
536
+ };
537
+ ```
538
+
539
+ Read them in components:
540
+
541
+ ```tsx
542
+ const { data } = useQuery(articleQueries.list(sort)); // or useSuspenseQuery(...)
543
+ const { data } = useQuery(articleQueries.item(selectedId)); // pauses until id is set
544
+ ```
545
+
546
+ ### 8.3 Mutations + colocated invalidation
547
+
548
+ Define mutations with `mutationOptions`, then a `use<Entity>Mutations()` hook
549
+ that wires `onSuccess` invalidation **through the key factory** (never hardcode a
550
+ key — that silently drifts when the shape changes):
551
+
552
+ ```ts
553
+ import { mutationOptions, useMutation, useQueryClient } from '@tanstack/react-query';
554
+
555
+ export const articleMutations = {
556
+ save: () => mutationOptions({ mutationKey: [...articleKeys.all, 'save'], mutationFn: saveArticle }),
557
+ remove: () => mutationOptions({ mutationKey: [...articleKeys.all, 'remove'], mutationFn: deleteArticle }),
558
+ };
559
+
560
+ export function useArticleMutations() {
561
+ const qc = useQueryClient();
562
+
563
+ const saveMutation = useMutation({
564
+ ...articleMutations.save(),
565
+ onSuccess: async (savedId) => {
566
+ await Promise.all([
567
+ qc.invalidateQueries({ queryKey: articleKeys.lists() }),
568
+ savedId != null ? qc.invalidateQueries({ queryKey: articleKeys.item(savedId) }) : undefined,
569
+ ]);
570
+ },
571
+ });
572
+
573
+ const deleteMutation = useMutation({
574
+ ...articleMutations.remove(),
575
+ onSuccess: async (_data, id) => {
576
+ await Promise.all([
577
+ qc.invalidateQueries({ queryKey: articleKeys.lists() }),
578
+ qc.invalidateQueries({ queryKey: articleKeys.item(id) }),
579
+ ]);
580
+ },
581
+ });
582
+
583
+ return { saveMutation, deleteMutation };
584
+ }
585
+ ```
586
+
587
+ ### 8.4 The loader integration (this library's payoff)
588
+
589
+ Because query definitions are reusable, a route loader can **prefetch** the exact
590
+ same definition the component reads — so the page renders synchronously, with no
591
+ loading flash, and stays in the shared cache:
592
+
593
+ ```tsx
594
+ export const articlesRoute = defineRoute({
595
+ path: 'articles',
596
+ component: ArticlesPage,
597
+ loader: ({ context }) => context.queryClient.ensureQueryData(articleQueries.list()),
598
+ });
599
+
600
+ function ArticlesPage() {
601
+ const { data } = useSuspenseQuery(articleQueries.list()); // already warmed by the loader
602
+ }
603
+ ```
604
+
605
+ ### 8.5 Rules
606
+
607
+ - **One key, one factory.** Every key — in queries, mutations, and invalidations —
608
+ comes from the `<entity>Keys` factory. A query that spells its own key lands in
609
+ a different cache bucket and **won't be invalidated** by the mutations.
610
+ - **Reuse, don't re-spell.** Pass `articleQueries.x()` to `useQuery`,
611
+ `useSuspenseQuery`, `ensureQueryData`, etc. Don't inline `{ queryKey, queryFn }`.
612
+ - **`skipToken`** for conditional fetches; **`retry: false`** for detail fetches
613
+ that 404 meaningfully.
614
+ - **Sorting**: reuse `SortOrder` / `SortDirection` / `SortOrderInput` from
615
+ `@kode4/react-foundation` for any sortable list, and include the sort in the
616
+ list key (`articleKeys.list(sort)`) so each sort variant caches independently.
617
+
618
+ ---
619
+
620
+ ## 9. Forms
621
+
622
+ `Form` wraps react-hook-form; pair it with a Zod resolver. `FormField` wires ids
623
+ and ARIA automatically.
624
+
625
+ ```tsx
626
+ const schema = z.object({ username: z.string().min(3, 'At least 3 characters.') });
627
+ const form = useForm<z.infer<typeof schema>>({ resolver: zodResolver(schema), defaultValues: { username: '' } });
628
+
629
+ <Form {...form}>
630
+ <form onSubmit={form.handleSubmit((v) => toast.success(`Hi ${v.username}`))}>
631
+ <FormField control={form.control} name="username" render={({ field }) => (
632
+ <FormItem>
633
+ <FormLabel>Username</FormLabel>
634
+ <FormControl><Input placeholder="ada" {...field} /></FormControl>
635
+ <FormDescription>Your public name.</FormDescription>
636
+ <FormMessage />
637
+ </FormItem>
638
+ )} />
639
+ <Button type="submit">Submit</Button>
640
+ </form>
641
+ </Form>
642
+ ```
643
+
644
+ ---
645
+
646
+ ## 10. Toasts
647
+
648
+ Render `<Toaster />` once near the root, then call `toast` anywhere. Toast styling
649
+ mirrors the Alert variant system (including `primary`/`secondary`).
650
+
651
+ ```tsx
652
+ import { toast } from '@kode4/react-foundation';
653
+ toast.success('Saved');
654
+ toast.error('Something went wrong');
655
+ toast.primary('Heads up');
656
+ ```
657
+
658
+ ---
659
+
660
+ ## 11. Errors & async boundaries
661
+
662
+ - Every route gets `DefaultErrorBoundary` unless it declares its own — thrown
663
+ `Response`s (e.g. a guard's `403`) and Zod `400`s render sensibly.
664
+ - `ErrorBlock` is the standalone error display; `AwaitErrorBlock` is for
665
+ `<Await>`/deferred boundaries.
666
+ - `SpinnerBlock` / `DefaultHydrateFallback` are the standard loading fallbacks.
667
+
668
+ ---
669
+
670
+ ## 12. Styling your own app
671
+
672
+ The library's internal rule "no Tailwind utility classes in markup" applies to
673
+ the *library source only*. **In your consuming app you may style however you
674
+ like** — Tailwind, CSS modules, plain CSS. Two guidelines:
675
+
676
+ 1. **Theme through tokens**, not by overriding internal `k4-*` classes (those are
677
+ implementation detail and may change).
678
+ 2. To make your own elements follow light/dark mode, style them against the same
679
+ semantic tokens (`var(--background)`, `var(--foreground)`, `var(--border)`, …).
680
+
681
+ ---
682
+
683
+ ## 13. Quick pitfall checklist
684
+
685
+ 1. Forgot `z.coerce` on a numeric path param → it's a string.
686
+ 2. Route missing from the `createAppRouter` array → `pathTo()` throws.
687
+ 3. Called `pathTo()`/`resolveMenu()` at module load → registry not filled yet.
688
+ 4. Augmented the barrel instead of `@kode4/react-foundation/router/context`.
689
+ 5. Hand-wrote a URL instead of `TypedLink`/`pathTo`.
690
+ 6. Reused a system token name (`--accent`, `--primary`) for an app-specific
691
+ value → re-themed every component. Use a distinct name.
692
+ 7. Wrote search state via `window.history` instead of `useSearchParams`'s setter
693
+ with `{ replace: true }`.
694
+ 8. Read params/search from React Router hooks instead of `useRoute(routeDef)`.