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.
- package/dist/WimProvider.cjs +1 -1
- package/dist/WimProvider.d.ts +9 -2
- package/dist/WimProvider.js +27 -21
- package/dist/components/layout/Navbar/Navbar.cjs +1 -1
- package/dist/components/layout/Navbar/Navbar.d.ts +2 -0
- package/dist/components/layout/Navbar/Navbar.js +7 -7
- package/dist/components/layout/Navbar/navbar.module.cjs +1 -1
- package/dist/components/layout/Navbar/navbar.module.js +15 -14
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -2
- package/dist/llms-full.txt +524 -1
- package/dist/llms.txt +103 -1
- package/dist/preset.cjs +2 -0
- package/dist/preset.d.ts +11 -0
- package/dist/preset.js +16 -0
- package/dist/styles.css +2 -2
- package/dist/tokens/generated/presets.cjs +2 -0
- package/dist/tokens/generated/presets.d.ts +19 -0
- package/dist/tokens/generated/presets.js +21 -0
- package/dist/wimui.umd.css +1 -1
- package/dist/wimui.umd.js +2 -2
- package/package.json +2 -2
package/dist/llms-full.txt
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## What this is
|
|
6
6
|
|
|
7
|
-
**wimui** v0.
|
|
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.
|
|
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"`
|
package/dist/preset.cjs
ADDED
|
@@ -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;
|
package/dist/preset.d.ts
ADDED
|
@@ -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;
|