wimui 0.3.0 → 0.4.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.
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## What this is
6
6
 
7
- **wimui** v0.3.0 — a React 19 component library: 216 documented components across 10 categories, with design tokens, dark mode, i18n (en/ja/pt-BR) and WAI-ARIA compliant a11y. Peer deps: react ^19, react-dom ^19 (plus optional peers for specific components — see package.json).
7
+ **wimui** v0.4.0 — a React 19 component library: 216 documented components across 10 categories, with design tokens, dark mode, i18n (en/ja/pt-BR) and WAI-ARIA compliant a11y. Peer deps: react ^19, react-dom ^19 (plus optional peers for specific components — see package.json).
8
8
 
9
9
  ## Install & required setup
10
10
 
@@ -71,6 +71,528 @@ Single components are judged by state/a11y/token compliance. **Composed screens*
71
71
  7. Give demo content real substance (product-context copy, internally consistent numbers/dates/names — active ≤ total, dates not evenly spaced).
72
72
  8. Add intentional "wobble": mix in 1–2 incomplete rows (a truncated long name, a missing optional field, an extreme value, an error/unread state) and show non-happy-path states (hover/focus/disabled/error/empty/loading).
73
73
 
74
+ ## Recipes — copy-paste starting points
75
+
76
+ ### 1. Required setup (the contract)
77
+
78
+ Without `styles.css` nothing is styled — this is the one step Storybook hides from you. Import it once at the app entry, wrap the tree in `WimProvider`, then build screens inside.
79
+
80
+ ```tsx
81
+ // main.tsx — app entry
82
+ import { createRoot } from "react-dom/client";
83
+ import "wimui/styles.css"; // REQUIRED: design tokens + component styles
84
+ import "wimui/reset.css"; // optional base reset
85
+ import { WimProvider } from "wimui";
86
+ import { App } from "./App";
87
+
88
+ // theme: "light" | "dark" | "system" (default). density: "comfortable" | "compact".
89
+ createRoot(document.getElementById("root")!).render(
90
+ <WimProvider theme="system" density="comfortable">
91
+ <App />
92
+ </WimProvider>,
93
+ );
94
+ ```
95
+
96
+ ```tsx
97
+ // App.tsx — app frame. AppShell wires header/sidebar; page content is children.
98
+ import { AppShell, Header, Sidebar, Stack, Title, Text, Button } from "wimui";
99
+
100
+ export function App() {
101
+ return (
102
+ <AppShell
103
+ header={<Header sticky bordered><Title tag="h1" size="md">Larkfield</Title></Header>}
104
+ sidebar={
105
+ <Sidebar width={240}>
106
+ <Stack gap="2xs" p="md">
107
+ <Button variant="ghost" justify="start" fullWidth>Overview</Button>
108
+ <Button variant="ghost" justify="start" fullWidth>Customers</Button>
109
+ <Button variant="ghost" justify="start" fullWidth>Settings</Button>
110
+ </Stack>
111
+ </Sidebar>
112
+ }
113
+ >
114
+ <Stack gap="lg">
115
+ <Title tag="h2" size="lg">Overview</Title>
116
+ <Text color="secondary">Spacing/size/color come from --wim-* tokens via props — never hardcode px/hex.</Text>
117
+ </Stack>
118
+ </AppShell>
119
+ );
120
+ }
121
+ ```
122
+
123
+ ### 2. A composed content screen
124
+
125
+ One protagonist (the KPI row), a dense table below, tokens via props, jagged real data, and one deliberately incomplete row. Note the compound components (`Stats.Value`, `Table.Head`) and that `Grid` uses `cols` (not `columns`).
126
+
127
+ ```tsx
128
+ import { Stack, Grid, Card, Stats, Table, Badge, Title, Text } from "wimui";
129
+
130
+ const rows = [
131
+ { id: "in_9f2a", name: "Marisol Okonkwo", plan: "Scale", amount: "$4,610.50", status: "paid" },
132
+ { id: "in_7b41", name: "Dmitri Sørensen", plan: "Enterprise", amount: "$12,199.00", status: "failed" },
133
+ { id: "in_2a90", name: "Thomas O'Reilly", plan: null, amount: "$89.00", status: "pending" }, // incomplete row
134
+ ];
135
+ const intent = { paid: "success", failed: "danger", pending: undefined } as const;
136
+
137
+ export function BillingOverview() {
138
+ return (
139
+ <Stack gap="lg">
140
+ <Title tag="h2" size="lg">Billing</Title>
141
+
142
+ {/* Protagonist: KPI row. Uneven content per tile — not three clones. */}
143
+ <Grid cols={{ base: 1, sm: 2, lg: 3 }} gap="md">
144
+ <Stats><Stats.Label>MRR</Stats.Label><Stats.Value>$48,210</Stats.Value><Stats.Trend>+6.4%</Stats.Trend></Stats>
145
+ <Stats><Stats.Label>Active workspaces</Stats.Label><Stats.Value>1,204</Stats.Value><Stats.Description>176 idle over 30 days</Stats.Description></Stats>
146
+ <Stats><Stats.Label>Failed webhooks</Stats.Label><Stats.Value>137</Stats.Value><Stats.Trend>+23 today</Stats.Trend></Stats>
147
+ </Grid>
148
+
149
+ {/* Dense data region */}
150
+ <Card padding="none">
151
+ <Table hoverable fullWidth>
152
+ <Table.Header>
153
+ <Table.Row>
154
+ <Table.Head>Customer</Table.Head><Table.Head>Plan</Table.Head>
155
+ <Table.Head>Amount</Table.Head><Table.Head>Status</Table.Head>
156
+ </Table.Row>
157
+ </Table.Header>
158
+ <Table.Body>
159
+ {rows.map((r) => (
160
+ <Table.Row key={r.id}>
161
+ <Table.Cell>{r.name}</Table.Cell>
162
+ <Table.Cell>{r.plan ?? <Text color="tertiary">—</Text>}</Table.Cell>
163
+ <Table.Cell>{r.amount}</Table.Cell>
164
+ <Table.Cell><Badge variant="subtle" intent={intent[r.status]}>{r.status}</Badge></Table.Cell>
165
+ </Table.Row>
166
+ ))}
167
+ </Table.Body>
168
+ </Table>
169
+ </Card>
170
+ </Stack>
171
+ );
172
+ }
173
+ ```
174
+
175
+ ### 3. Auth — sign-in screen
176
+
177
+ A focused single-protagonist screen: one centered card, a left-aligned form. Not the generic badge→heading→two-buttons hero. Real product context in the copy.
178
+
179
+ ```tsx
180
+ import { Center, Card, Stack, Group, Title, Text, Input, PasswordInput, Checkbox, Button, Link } from "wimui";
181
+
182
+ export function SignIn() {
183
+ return (
184
+ <Center h="100dvh" p="lg">
185
+ <Card padding="lg" style={{ width: "min(380px, 100%)" }}>
186
+ <Stack gap="lg">
187
+ <Stack gap="2xs">
188
+ <Title tag="h1" size="lg">Sign in to Larkfield</Title>
189
+ <Text color="secondary">Use your work email — SSO is enabled for Enterprise workspaces.</Text>
190
+ </Stack>
191
+ <form onSubmit={(e) => e.preventDefault()}>
192
+ <Stack gap="md">
193
+ <Input label="Work email" type="email" placeholder="you@company.com" fullWidth />
194
+ <PasswordInput label="Password" fullWidth />
195
+ <Group justify="between" align="center">
196
+ <Checkbox>Keep me signed in</Checkbox>
197
+ <Link href="#" priority="secondary">Forgot password?</Link>
198
+ </Group>
199
+ <Button type="submit" variant="solid" fullWidth>Sign in</Button>
200
+ </Stack>
201
+ </form>
202
+ <Text size="sm" color="tertiary">No account? <Link href="#">Start a 14-day trial</Link></Text>
203
+ </Stack>
204
+ </Card>
205
+ </Center>
206
+ );
207
+ }
208
+ ```
209
+
210
+ ### 4. Settings — sectioned form
211
+
212
+ Dense label-left / control-right rows grouped in one card, separated by `Divider`. Density comes from token gaps, not hardcoded spacing. Actions right-aligned at the bottom.
213
+
214
+ ```tsx
215
+ import { Stack, Group, Title, Text, Card, Divider, Select, Switch, Button } from "wimui";
216
+
217
+ export function NotificationSettings() {
218
+ return (
219
+ <Stack gap="lg" style={{ maxWidth: 720 }}>
220
+ <Stack gap="2xs">
221
+ <Title tag="h1" size="lg">Notifications</Title>
222
+ <Text color="secondary">Control what Larkfield emails you about. Changes apply immediately.</Text>
223
+ </Stack>
224
+
225
+ <Card padding="lg">
226
+ <Stack gap="md">
227
+ <SettingRow label="Deliverability alerts" hint="Bounce-rate spikes and blocklist hits.">
228
+ <Switch defaultChecked />
229
+ </SettingRow>
230
+ <Divider />
231
+ <SettingRow label="Weekly summary" hint="Every Monday, 09:00 in your timezone.">
232
+ <Switch />
233
+ </SettingRow>
234
+ <Divider />
235
+ <SettingRow label="Digest timezone">
236
+ <Select
237
+ aria-label="Digest timezone"
238
+ value="jst"
239
+ options={[
240
+ { label: "Asia/Tokyo (JST)", value: "jst" },
241
+ { label: "Europe/Berlin (CET)", value: "cet" },
242
+ { label: "UTC", value: "utc" },
243
+ ]}
244
+ />
245
+ </SettingRow>
246
+ </Stack>
247
+ </Card>
248
+
249
+ <Group justify="end" gap="sm">
250
+ <Button variant="ghost">Reset</Button>
251
+ <Button variant="solid">Save changes</Button>
252
+ </Group>
253
+ </Stack>
254
+ );
255
+ }
256
+
257
+ // Local helper: label-left / control-right row. One protagonist per row = the control.
258
+ function SettingRow({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) {
259
+ return (
260
+ <Group justify="between" align="center" gap="md">
261
+ <Stack gap="3xs">
262
+ <Text weight="medium">{label}</Text>
263
+ {hint ? <Text size="sm" color="secondary">{hint}</Text> : null}
264
+ </Stack>
265
+ {children}
266
+ </Group>
267
+ );
268
+ }
269
+ ```
270
+
271
+ ### 5. Empty state — a real zero state, not a shrug
272
+
273
+ One protagonist (the primary action), calm copy that says *why* it's empty and what happens next. Reuse the `EmptyState` component instead of hand-rolling centered divs.
274
+
275
+ ```tsx
276
+ import { Center, EmptyState, Button } from "wimui";
277
+ import { DocumentIcon } from "wimui/icons";
278
+
279
+ export function NoInvoices() {
280
+ return (
281
+ <Center h="60dvh" p="lg">
282
+ <EmptyState
283
+ icon={<DocumentIcon />}
284
+ title="No invoices yet"
285
+ description="Invoices show up here after your first billing cycle closes. Nothing is due today."
286
+ extra={<Button variant="solid">Create a manual invoice</Button>}
287
+ />
288
+ </Center>
289
+ );
290
+ }
291
+ ```
292
+
293
+ ### 6. Filtered data table — toolbar + table
294
+
295
+ A search/filter toolbar as the sparse region above a dense table. Filtering is client-side here; swap in your query. Note the deliberately jagged data: a failed row, a member with no team (incomplete), and a long name.
296
+
297
+ ```tsx
298
+ import { Stack, Group, SearchInput, Select, Table, Badge, Text } from "wimui";
299
+ import { useMemo, useState } from "react";
300
+
301
+ const members = [
302
+ { id: "u_1", name: "Marisol Okonkwo", team: "Growth", role: "admin", status: "active" },
303
+ { id: "u_2", name: "Dmitri Sørensen", team: "Platform", role: "member", status: "invited" },
304
+ { id: "u_3", name: "Aleksandra Wiśniewska-Nowak", team: null, role: "member", status: "active" }, // no team
305
+ { id: "u_4", name: "Thomas O'Reilly", team: "Growth", role: "member", status: "suspended" }, // non-happy path
306
+ ];
307
+ const roleIntent = { admin: "primary", member: undefined } as const;
308
+ const statusIntent = { active: "success", invited: undefined, suspended: "danger" } as const;
309
+
310
+ export function MembersTable() {
311
+ const [q, setQ] = useState("");
312
+ const [role, setRole] = useState("all");
313
+
314
+ const rows = useMemo(
315
+ () =>
316
+ members.filter(
317
+ (m) =>
318
+ (role === "all" || m.role === role) &&
319
+ m.name.toLowerCase().includes(q.trim().toLowerCase()),
320
+ ),
321
+ [q, role],
322
+ );
323
+
324
+ return (
325
+ <Stack gap="md">
326
+ {/* Sparse toolbar */}
327
+ <Group justify="between" align="center" gap="md">
328
+ <SearchInput
329
+ placeholder="Search members"
330
+ value={q}
331
+ onChange={(e) => setQ(e.target.value)}
332
+ allowClear
333
+ width={280}
334
+ />
335
+ <Select
336
+ aria-label="Filter by role"
337
+ value={role}
338
+ onChange={setRole}
339
+ options={[
340
+ { label: "All roles", value: "all" },
341
+ { label: "Admins", value: "admin" },
342
+ { label: "Members", value: "member" },
343
+ ]}
344
+ />
345
+ </Group>
346
+
347
+ {/* Dense data region */}
348
+ <Table hoverable fullWidth>
349
+ <Table.Header>
350
+ <Table.Row>
351
+ <Table.Head>Member</Table.Head><Table.Head>Team</Table.Head>
352
+ <Table.Head>Role</Table.Head><Table.Head>Status</Table.Head>
353
+ </Table.Row>
354
+ </Table.Header>
355
+ <Table.Body>
356
+ {rows.map((m) => (
357
+ <Table.Row key={m.id}>
358
+ <Table.Cell>{m.name}</Table.Cell>
359
+ <Table.Cell>{m.team ?? <Text color="tertiary">No team</Text>}</Table.Cell>
360
+ <Table.Cell><Badge variant="subtle" intent={roleIntent[m.role]}>{m.role}</Badge></Table.Cell>
361
+ <Table.Cell><Badge variant="subtle" intent={statusIntent[m.status]}>{m.status}</Badge></Table.Cell>
362
+ </Table.Row>
363
+ ))}
364
+ </Table.Body>
365
+ </Table>
366
+ </Stack>
367
+ );
368
+ }
369
+ ```
370
+
371
+ ### 7. Onboarding — a stepper flow
372
+
373
+ The `Stepper` shows where you are; the card is the single focused task for the current step. Starts mid-flow (step 1) so it reads like a real session, not a fresh render.
374
+
375
+ ```tsx
376
+ import { Stack, Card, Stepper, Title, Text, Group, Button, Input } from "wimui";
377
+ import { useState } from "react";
378
+
379
+ const steps = [
380
+ { title: "Account", description: "Your details" },
381
+ { title: "Workspace", description: "Name & URL" },
382
+ { title: "Invite", description: "Optional" },
383
+ ];
384
+
385
+ export function Onboarding() {
386
+ const [current, setCurrent] = useState(1); // mid-flow
387
+ const back = () => setCurrent((c) => Math.max(0, c - 1));
388
+ const next = () => setCurrent((c) => Math.min(steps.length - 1, c + 1));
389
+
390
+ return (
391
+ <Stack gap="lg" style={{ maxWidth: 640 }}>
392
+ <Stepper steps={steps} current={current} onChange={setCurrent} />
393
+
394
+ <Card padding="lg">
395
+ <Stack gap="md">
396
+ <Stack gap="2xs">
397
+ <Title tag="h2" size="md">Name your workspace</Title>
398
+ <Text color="secondary">You can change this later in Settings — existing links keep working.</Text>
399
+ </Stack>
400
+ <Input label="Workspace name" placeholder="Acme Inc." fullWidth />
401
+ </Stack>
402
+ </Card>
403
+
404
+ <Group justify="between" align="center">
405
+ <Button variant="ghost" onClick={back} disabled={current === 0}>Back</Button>
406
+ <Button variant="solid" onClick={next}>Continue</Button>
407
+ </Group>
408
+ </Stack>
409
+ );
410
+ }
411
+ ```
412
+
413
+ ## Idioms — per-category minimal combinations
414
+
415
+ Smaller than the full-screen recipes above: the canonical way to wire one
416
+ category together. Same rules apply (tokens via props, real copy, show a
417
+ non-happy-path state).
418
+
419
+ ### Form — field + validation + submit
420
+
421
+ The field owns its own label and error: pass `error` a **string** to render the message and the danger state together. No wrapper component needed.
422
+
423
+ ```tsx
424
+ import { Stack, Input, Textarea, Button } from "wimui";
425
+ import { useState } from "react";
426
+
427
+ export function ContactForm() {
428
+ const [email, setEmail] = useState("");
429
+ const [error, setError] = useState<string>();
430
+
431
+ function submit(e: React.FormEvent) {
432
+ e.preventDefault();
433
+ setError(email.includes("@") ? undefined : "Enter a valid work email.");
434
+ }
435
+
436
+ return (
437
+ <form onSubmit={submit} noValidate>
438
+ <Stack gap="md" style={{ maxWidth: 420 }}>
439
+ <Input
440
+ label="Work email"
441
+ type="email"
442
+ required
443
+ value={email}
444
+ onChange={(e) => setEmail(e.target.value)}
445
+ error={error} // string → message + danger intent
446
+ fullWidth
447
+ />
448
+ <Textarea label="What do you need help with?" fullWidth />
449
+ <Button type="submit" variant="solid">Send</Button>
450
+ </Stack>
451
+ </form>
452
+ );
453
+ }
454
+ ```
455
+
456
+ ### Navigation — sidebar (desktop) ⇄ tab bar (mobile)
457
+
458
+ Same destinations, two shells. `AppShell` takes a `sidebar` for wide viewports; `TabBar` (fixed, bottom) is the mobile equivalent — show/hide each with your breakpoint CSS. Active state is data, not duplicated markup.
459
+
460
+ ```tsx
461
+ import { AppShell, Sidebar, Stack, Button, TabBar } from "wimui";
462
+ import { HomeIcon, UserIcon, SettingsIcon } from "wimui/icons";
463
+
464
+ const nav = [
465
+ { id: "home", label: "Home", icon: <HomeIcon /> },
466
+ { id: "people", label: "People", icon: <UserIcon /> },
467
+ { id: "settings", label: "Settings", icon: <SettingsIcon /> },
468
+ ];
469
+
470
+ export function Shell({
471
+ tab,
472
+ onTab,
473
+ children,
474
+ }: {
475
+ tab: string;
476
+ onTab: (id: string) => void;
477
+ children: React.ReactNode;
478
+ }) {
479
+ return (
480
+ <AppShell
481
+ sidebar={
482
+ <Sidebar width={240}>
483
+ <Stack gap="2xs" p="md">
484
+ {nav.map((n) => (
485
+ <Button
486
+ key={n.id}
487
+ variant={tab === n.id ? "solid" : "ghost"}
488
+ justify="start"
489
+ icon={n.icon}
490
+ fullWidth
491
+ onClick={() => onTab(n.id)}
492
+ >
493
+ {n.label}
494
+ </Button>
495
+ ))}
496
+ </Stack>
497
+ </Sidebar>
498
+ }
499
+ >
500
+ {children}
501
+ {/* Mobile: same destinations as a fixed bottom bar */}
502
+ <TabBar>
503
+ {nav.map((n) => (
504
+ <TabBar.Item
505
+ key={n.id}
506
+ active={tab === n.id}
507
+ icon={n.icon}
508
+ label={n.label}
509
+ onClick={() => onTab(n.id)}
510
+ />
511
+ ))}
512
+ </TabBar>
513
+ </AppShell>
514
+ );
515
+ }
516
+ ```
517
+
518
+ ### Feedback — persistent Alert vs. transient toast
519
+
520
+ Use an inline `Alert` for state that stays true (a limit, an outage); use a toast (`useToast().show`) for a one-off confirmation. Wrap the app once in `ToastProvider`.
521
+
522
+ ```tsx
523
+ import { ToastProvider, useToast, Alert, Button, Stack } from "wimui";
524
+
525
+ // once, at the app root
526
+ export function Providers({ children }: { children: React.ReactNode }) {
527
+ return <ToastProvider position="top-right">{children}</ToastProvider>;
528
+ }
529
+
530
+ export function InvitePanel() {
531
+ const { show } = useToast();
532
+ return (
533
+ <Stack gap="md" style={{ maxWidth: 480 }}>
534
+ <Alert
535
+ intent="warning"
536
+ title="Seat limit reached"
537
+ description="Your plan includes 5 seats. Remove a member or upgrade to invite more."
538
+ />
539
+ <Button
540
+ variant="solid"
541
+ onClick={() =>
542
+ show({
543
+ intent: "success",
544
+ title: "Invitation sent",
545
+ description: "We emailed marisol@okonkwo.dev.",
546
+ })
547
+ }
548
+ >
549
+ Resend invite
550
+ </Button>
551
+ </Stack>
552
+ );
553
+ }
554
+ ```
555
+
556
+ ### Overlay — Dialog wrapping a form
557
+
558
+ `Dialog` is compound: a `DialogTrigger asChild` around your own button, and `DialogClose asChild` around the cancel action so it dismisses without wiring state. Keep the form inside `DialogContent`.
559
+
560
+ ```tsx
561
+ import {
562
+ Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle,
563
+ DialogDescription, DialogFooter, DialogClose, Button, Stack, Input,
564
+ } from "wimui";
565
+
566
+ export function RenameWorkspace() {
567
+ return (
568
+ <Dialog>
569
+ <DialogTrigger asChild>
570
+ <Button variant="outline">Rename workspace</Button>
571
+ </DialogTrigger>
572
+ <DialogContent>
573
+ <DialogHeader>
574
+ <DialogTitle>Rename workspace</DialogTitle>
575
+ <DialogDescription>
576
+ This changes the URL. Existing links keep working via a redirect.
577
+ </DialogDescription>
578
+ </DialogHeader>
579
+ <form onSubmit={(e) => e.preventDefault()}>
580
+ <Stack gap="md" p="md">
581
+ <Input label="Workspace name" defaultValue="Larkfield" fullWidth />
582
+ </Stack>
583
+ <DialogFooter>
584
+ <DialogClose asChild>
585
+ <Button variant="ghost">Cancel</Button>
586
+ </DialogClose>
587
+ <Button type="submit" variant="solid">Save</Button>
588
+ </DialogFooter>
589
+ </form>
590
+ </DialogContent>
591
+ </Dialog>
592
+ );
593
+ }
594
+ ```
595
+
74
596
  ## Components
75
597
 
76
598
  ### layout — `import { … } from "wimui/layout"`
@@ -237,6 +759,7 @@ The container-query wrapper div is preserved to maintain responsive column behav
237
759
  - `transparent: boolean` — Whether the navbar background is transparent
238
760
  - `glass: boolean` — Whether to apply the frosted-glass effect
239
761
  - `bordered: boolean` — Whether to show a bottom border
762
+ - `fluid: boolean` — Expand content to full width (disable the centered max-width container)
240
763
  - `defaultMenuOpen: boolean` = false — Initial open state of the mobile menu (uncontrolled)
241
764
  - `isMenuOpen: boolean` — Open state of the mobile menu (controlled)
242
765
  - `onMenuOpenChange: (isOpen: boolean) => void` — Callback when the mobile menu open state changes
package/dist/llms.txt CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## What this is
6
6
 
7
- **wimui** v0.3.0 — a React 19 component library: 216 documented components across 10 categories, with design tokens, dark mode, i18n (en/ja/pt-BR) and WAI-ARIA compliant a11y. Peer deps: react ^19, react-dom ^19 (plus optional peers for specific components — see package.json).
7
+ **wimui** v0.4.0 — a React 19 component library: 216 documented components across 10 categories, with design tokens, dark mode, i18n (en/ja/pt-BR) and WAI-ARIA compliant a11y. Peer deps: react ^19, react-dom ^19 (plus optional peers for specific components — see package.json).
8
8
 
9
9
  ## Install & required setup
10
10
 
@@ -71,6 +71,108 @@ Single components are judged by state/a11y/token compliance. **Composed screens*
71
71
  7. Give demo content real substance (product-context copy, internally consistent numbers/dates/names — active ≤ total, dates not evenly spaced).
72
72
  8. Add intentional "wobble": mix in 1–2 incomplete rows (a truncated long name, a missing optional field, an extreme value, an error/unread state) and show non-happy-path states (hover/focus/disabled/error/empty/loading).
73
73
 
74
+ ## Recipes — copy-paste starting points
75
+
76
+ ### 1. Required setup (the contract)
77
+
78
+ Without `styles.css` nothing is styled — this is the one step Storybook hides from you. Import it once at the app entry, wrap the tree in `WimProvider`, then build screens inside.
79
+
80
+ ```tsx
81
+ // main.tsx — app entry
82
+ import { createRoot } from "react-dom/client";
83
+ import "wimui/styles.css"; // REQUIRED: design tokens + component styles
84
+ import "wimui/reset.css"; // optional base reset
85
+ import { WimProvider } from "wimui";
86
+ import { App } from "./App";
87
+
88
+ // theme: "light" | "dark" | "system" (default). density: "comfortable" | "compact".
89
+ createRoot(document.getElementById("root")!).render(
90
+ <WimProvider theme="system" density="comfortable">
91
+ <App />
92
+ </WimProvider>,
93
+ );
94
+ ```
95
+
96
+ ```tsx
97
+ // App.tsx — app frame. AppShell wires header/sidebar; page content is children.
98
+ import { AppShell, Header, Sidebar, Stack, Title, Text, Button } from "wimui";
99
+
100
+ export function App() {
101
+ return (
102
+ <AppShell
103
+ header={<Header sticky bordered><Title tag="h1" size="md">Larkfield</Title></Header>}
104
+ sidebar={
105
+ <Sidebar width={240}>
106
+ <Stack gap="2xs" p="md">
107
+ <Button variant="ghost" justify="start" fullWidth>Overview</Button>
108
+ <Button variant="ghost" justify="start" fullWidth>Customers</Button>
109
+ <Button variant="ghost" justify="start" fullWidth>Settings</Button>
110
+ </Stack>
111
+ </Sidebar>
112
+ }
113
+ >
114
+ <Stack gap="lg">
115
+ <Title tag="h2" size="lg">Overview</Title>
116
+ <Text color="secondary">Spacing/size/color come from --wim-* tokens via props — never hardcode px/hex.</Text>
117
+ </Stack>
118
+ </AppShell>
119
+ );
120
+ }
121
+ ```
122
+
123
+ ### 2. A composed content screen
124
+
125
+ One protagonist (the KPI row), a dense table below, tokens via props, jagged real data, and one deliberately incomplete row. Note the compound components (`Stats.Value`, `Table.Head`) and that `Grid` uses `cols` (not `columns`).
126
+
127
+ ```tsx
128
+ import { Stack, Grid, Card, Stats, Table, Badge, Title, Text } from "wimui";
129
+
130
+ const rows = [
131
+ { id: "in_9f2a", name: "Marisol Okonkwo", plan: "Scale", amount: "$4,610.50", status: "paid" },
132
+ { id: "in_7b41", name: "Dmitri Sørensen", plan: "Enterprise", amount: "$12,199.00", status: "failed" },
133
+ { id: "in_2a90", name: "Thomas O'Reilly", plan: null, amount: "$89.00", status: "pending" }, // incomplete row
134
+ ];
135
+ const intent = { paid: "success", failed: "danger", pending: undefined } as const;
136
+
137
+ export function BillingOverview() {
138
+ return (
139
+ <Stack gap="lg">
140
+ <Title tag="h2" size="lg">Billing</Title>
141
+
142
+ {/* Protagonist: KPI row. Uneven content per tile — not three clones. */}
143
+ <Grid cols={{ base: 1, sm: 2, lg: 3 }} gap="md">
144
+ <Stats><Stats.Label>MRR</Stats.Label><Stats.Value>$48,210</Stats.Value><Stats.Trend>+6.4%</Stats.Trend></Stats>
145
+ <Stats><Stats.Label>Active workspaces</Stats.Label><Stats.Value>1,204</Stats.Value><Stats.Description>176 idle over 30 days</Stats.Description></Stats>
146
+ <Stats><Stats.Label>Failed webhooks</Stats.Label><Stats.Value>137</Stats.Value><Stats.Trend>+23 today</Stats.Trend></Stats>
147
+ </Grid>
148
+
149
+ {/* Dense data region */}
150
+ <Card padding="none">
151
+ <Table hoverable fullWidth>
152
+ <Table.Header>
153
+ <Table.Row>
154
+ <Table.Head>Customer</Table.Head><Table.Head>Plan</Table.Head>
155
+ <Table.Head>Amount</Table.Head><Table.Head>Status</Table.Head>
156
+ </Table.Row>
157
+ </Table.Header>
158
+ <Table.Body>
159
+ {rows.map((r) => (
160
+ <Table.Row key={r.id}>
161
+ <Table.Cell>{r.name}</Table.Cell>
162
+ <Table.Cell>{r.plan ?? <Text color="tertiary">—</Text>}</Table.Cell>
163
+ <Table.Cell>{r.amount}</Table.Cell>
164
+ <Table.Cell><Badge variant="subtle" intent={intent[r.status]}>{r.status}</Badge></Table.Cell>
165
+ </Table.Row>
166
+ ))}
167
+ </Table.Body>
168
+ </Table>
169
+ </Card>
170
+ </Stack>
171
+ );
172
+ }
173
+ ```
174
+
175
+ > More verified full-screen recipes (auth sign-in, settings form, empty state, filtered data table, onboarding) and per-category idioms (form, navigation, feedback, overlay) are in `llms-full.txt`.
74
176
  ## Components
75
177
 
76
178
  ### layout — `import { … } from "wimui/layout"`
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ const e=require("./tokens/generated/presets.cjs");var t=`data-wim-preset`,n=new Set(e.WIM_PRESETS.map(e=>e.name));function r(e=void 0){if(typeof document>`u`)return`none`;let r=(e??document.documentElement).getAttribute?.(t);return r&&n.has(r)?r:`none`}function i(e,r=void 0){if(typeof document>`u`)return;let i=r??document.documentElement;e===`none`||!n.has(e)?i.removeAttribute(t):i.setAttribute(t,e)}exports.WIM_PRESETS=e.WIM_PRESETS,exports.getWimPreset=r,exports.setWimPreset=i;
@@ -0,0 +1,11 @@
1
+ import { WIM_PRESETS, WimPresetName } from './tokens/generated/presets';
2
+ export { WIM_PRESETS };
3
+ export type { WimPresetName };
4
+ /** A preset name, or `none` for the default (no preset). */
5
+ export type WimPreset = WimPresetName | "none";
6
+ export declare function getWimPreset(root?: ParentNode | null | undefined): WimPreset;
7
+ /**
8
+ * Apply a preset to an element (defaults to `document.documentElement`).
9
+ * `none` (or an unknown name) removes the attribute.
10
+ */
11
+ export declare function setWimPreset(preset: WimPreset, root?: Element | null | undefined): void;