@astherix/ui 0.40.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/README.md +933 -0
- package/dist/chunk-IZEGCTZW.js +588 -0
- package/dist/chunk-IZEGCTZW.js.map +1 -0
- package/dist/cli.js +345 -0
- package/dist/controlStyles-Bco2sOHE.d.ts +4 -0
- package/dist/editor.d.ts +43 -0
- package/dist/editor.js +373 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.d.ts +2470 -0
- package/dist/index.js +10582 -0
- package/dist/index.js.map +1 -0
- package/dist/theme.d.ts +78 -0
- package/dist/theme.js +287 -0
- package/dist/theme.js.map +1 -0
- package/package.json +69 -0
- package/theme.css +909 -0
- package/ui.theme.schema.json +462 -0
package/README.md
ADDED
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
# @astherix/ui
|
|
2
|
+
|
|
3
|
+
Tailwind v4 + React components for Next.js and Laravel (React + Inertia) apps.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
packages/ui/
|
|
9
|
+
theme.css Tokens (colors, font, radius), light/dark themes
|
|
10
|
+
src/lib/cn.ts Class merging helper
|
|
11
|
+
src/components/activity/ ActivityIndicator, ProgressBar, ProgressRing, Skeleton, ActivitySteps, LoadingOverlay
|
|
12
|
+
src/components/avatar/ Avatar, AvatarGroup, AvatarLabel, AvatarUpload
|
|
13
|
+
src/components/button/ Button
|
|
14
|
+
src/components/card/ Card, ChoiceCard
|
|
15
|
+
src/components/editor/ Editor (entry: @astherix/ui/editor)
|
|
16
|
+
src/components/input/ Field, Input, Textarea
|
|
17
|
+
src/components/menu/ DropdownMenu, SplitButton
|
|
18
|
+
src/components/modal/ Modal
|
|
19
|
+
src/components/pill/ Pill, PillGroup, PillOption
|
|
20
|
+
src/components/choice/ Checkbox, CheckboxGroup, RadioGroup, Radio, Switch
|
|
21
|
+
src/components/select/ Select
|
|
22
|
+
src/components/sidebar/ SidebarProvider, Sidebar, SidebarNav, SidebarTrigger …
|
|
23
|
+
src/components/table/ DataTable
|
|
24
|
+
src/components/tabs/ Tabs
|
|
25
|
+
src/components/timeline/ Timeline, Roadmap
|
|
26
|
+
src/components/upload/ FileDropzone, FileInput, FileUploadButton, useFileUploads, xhrUpload
|
|
27
|
+
src/components/toast/ Toaster, toast
|
|
28
|
+
src/components/typography/ Heading, Text, Lead, Link, Code, Kbd, Mark, Blockquote, List, Prose, Stat
|
|
29
|
+
docs/ User guide
|
|
30
|
+
index.html Built guide — open it in any browser, works offline
|
|
31
|
+
src/main.tsx Navigation and page list
|
|
32
|
+
src/pages/ One page per component (Getting started, Button, Modal…)
|
|
33
|
+
src/components/Doc.tsx Shared building blocks: Section, Code, PageHeader
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Develop
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
npm install
|
|
40
|
+
npm run build # builds packages/ui/dist
|
|
41
|
+
npm run typecheck
|
|
42
|
+
npm run docs # rebuilds docs/index.html
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Adding a component page to the guide
|
|
46
|
+
|
|
47
|
+
1. Create `docs/src/pages/InputPage.tsx` using `Section` blocks (demo + code example).
|
|
48
|
+
2. Add it to the `pages` list in `docs/src/main.tsx`.
|
|
49
|
+
3. Run `npm run docs`.
|
|
50
|
+
|
|
51
|
+
## Use in an app
|
|
52
|
+
|
|
53
|
+
Until the package is published, link it locally (`npm install ../ui-framework/packages/ui`)
|
|
54
|
+
or add it to the same workspace.
|
|
55
|
+
|
|
56
|
+
**1. Stylesheet** (`app/globals.css` in Next.js, `resources/css/app.css` in Laravel):
|
|
57
|
+
|
|
58
|
+
```css
|
|
59
|
+
@import "tailwindcss";
|
|
60
|
+
@import "@astherix/ui/theme.css";
|
|
61
|
+
@source "../node_modules/@astherix/ui/dist"; /* adjust the path relative to this file */
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**2. Font — Schibsted Grotesk**
|
|
65
|
+
|
|
66
|
+
Next.js (`app/layout.tsx`):
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { Schibsted_Grotesk } from "next/font/google";
|
|
70
|
+
const font = Schibsted_Grotesk({ subsets: ["latin"], variable: "--font-schibsted" });
|
|
71
|
+
// <html className={font.variable}>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then in your stylesheet, after the theme import:
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
@theme { --font-sans: var(--font-schibsted), ui-sans-serif, system-ui, sans-serif; }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Laravel (`resources/views/app.blade.php`, inside `<head>`):
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
84
|
+
<link href="https://fonts.bunny.net/css?family=schibsted-grotesk:400,500,600,700" rel="stylesheet">
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**3. Dark mode** — add the `dark` class to `<html>` (next-themes with `attribute="class"`,
|
|
88
|
+
or the appearance handling in Laravel's starter kit).
|
|
89
|
+
|
|
90
|
+
## Accordion
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
<Accordion type="single" collapsible defaultValue="refunds" variant="outline" linkToHash>
|
|
94
|
+
<AccordionItem value="refunds" title="How do refunds work?">…</AccordionItem>
|
|
95
|
+
</Accordion>
|
|
96
|
+
|
|
97
|
+
<AccordionItem value="shipping" title="Delivery" status="complete" summary="Express · ₱350">…</AccordionItem>
|
|
98
|
+
<AccordionItem value="reminders" title="Reminders" action={<Switch … />}>…</AccordionItem>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`Accordion`: `type` (`single` `multiple`), `value` / `defaultValue` / `onValueChange`, `collapsible`,
|
|
102
|
+
`variant` (`separated` `outline` `flush`), `size`, `headingLevel`, `linkToHash`. `AccordionItem`:
|
|
103
|
+
`value`, `title`, `description`, `summary` (while closed), `icon`, `meta`, `action` (header
|
|
104
|
+
controls), `status` (`complete` `current` `error` `locked`), `disabled`, `keepMounted`.
|
|
105
|
+
`AccordionToggleAll`, `useAccordion()` → `{ open, toggle, expandAll, collapseAll }`. Closed
|
|
106
|
+
sections use `hidden="until-found"`, so Ctrl/⌘+F finds and opens them (Chrome/Edge).
|
|
107
|
+
|
|
108
|
+
## Activity indicators
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
<ActivityIndicator variant="spinner" | "dots" | "bars" | "pulse" | "orbit" label="Loading invoices" showLabel />
|
|
112
|
+
<ProgressBar label="Uploading" value={pct} showValue striped />
|
|
113
|
+
<ProgressRing value={72} size="xl" label="Storage used" />
|
|
114
|
+
<Skeleton shape="text" lines={3} />
|
|
115
|
+
<ActivitySteps steps={[{ label: "Build", status: "done" }, { label: "Deploy", status: "active" }]} />
|
|
116
|
+
<LoadingOverlay loading={refreshing} label="Refreshing">…</LoadingOverlay>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`ActivityIndicator`: `variant`, `size` (`xs`–`xl`), `tone` (`primary` `neutral` `current` `success`
|
|
120
|
+
`warning` `danger` `info`), `label`, `showLabel`, `labelPosition`. `ProgressBar`: `value` (omit for
|
|
121
|
+
indeterminate), `max`, `label`, `showValue`, `valueText`, `size`, `tone`, `striped`.
|
|
122
|
+
`ProgressRing`: `value`, `max`, `size` (`sm`–`xl`), `tone`, `label`, `valueText`, center `children`.
|
|
123
|
+
`Skeleton`: `shape` (`rect` `circle` `text`), `lines`. `ActivitySteps`: `steps` (`label`,
|
|
124
|
+
`description`, `status`: `done` `active` `pending` `error` `skipped`, `meta`), `orientation`,
|
|
125
|
+
`size`. `LoadingOverlay`: `loading`, `label`, `variant`, `delay`.
|
|
126
|
+
|
|
127
|
+
## Avatar
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<Avatar src={user.avatarUrl} name="Maria Santos" status="online" />
|
|
131
|
+
<Avatar name="Dan Torres" /> {/* initials, color from the name */}
|
|
132
|
+
<Avatar shape="square" icon="bi bi-building" name="Northwind" />
|
|
133
|
+
<AvatarGroup max={5} onOverflowClick={openMembers}>{…}</AvatarGroup>
|
|
134
|
+
<AvatarLabel src={…} name="Maria Santos" description="Product designer" end={<Pill>Admin</Pill>} />
|
|
135
|
+
<AvatarUpload name={user.name} value={user.avatarUrl} onChange={setFile} inputName="avatar" />
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`Avatar`: `src`, `name`, `size` (`xs` `sm` `md` `lg` `xl` `2xl`), `shape` (`circle` `square`),
|
|
139
|
+
`status` (`online` `away` `busy` `offline`), `icon`, `badge`, `ring`, `decorative`, `fallback`.
|
|
140
|
+
`AvatarGroup`: `size`, `shape`, `max`, `spacing`, `onOverflowClick`. `AvatarLabel`: Avatar props
|
|
141
|
+
+ `description`, `end`. `AvatarUpload`: `value`, `onChange`, `name`, `size`, `shape`,
|
|
142
|
+
`maxSizeMB`, `accept`, `inputName`, `disabled`. `getInitials(name)` is exported too.
|
|
143
|
+
|
|
144
|
+
## Button
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { Button } from "@astherix/ui";
|
|
148
|
+
|
|
149
|
+
<Button>Save changes</Button>
|
|
150
|
+
<Button variant="secondary" leadingIcon={<DownloadIcon />}>Export CSV</Button>
|
|
151
|
+
<Button variant="danger" loading={isDeleting}>Delete project</Button>
|
|
152
|
+
<Button variant="ghost" iconOnly aria-label="More options"><MoreIcon /></Button>
|
|
153
|
+
|
|
154
|
+
// Links keep button styles with asChild
|
|
155
|
+
<Button asChild><Link href="/billing">Open billing</Link></Button>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| Prop | Values | Default |
|
|
159
|
+
| -------------- | ----------------------------------------------- | ----------- |
|
|
160
|
+
| `variant` | `primary` `secondary` `ghost` `danger` | `primary` |
|
|
161
|
+
| `size` | `sm` `md` `lg` | `md` |
|
|
162
|
+
| `rounded` | `none` `sm` `md` `lg` `full` | `md` |
|
|
163
|
+
| `raised` | boolean — darker bottom edge + press effect | `true` |
|
|
164
|
+
| `shadow` | `none` `sm` `md` `lg` `xl` | `none` |
|
|
165
|
+
| `iconOnly` | boolean (needs `aria-label`) | `false` |
|
|
166
|
+
| `fullWidth` | boolean | `false` |
|
|
167
|
+
| `loading` | boolean — show the spinner on demand | `false` |
|
|
168
|
+
| `spinnerPlacement` | `center` `start` `end` | `center` |
|
|
169
|
+
| `loadingIndicator` | `spinner` `orbit` `progress` (both experimental) | `spinner` |
|
|
170
|
+
| `progressStyle` | `fill` `bar` (with `progress` indicator) | `fill` |
|
|
171
|
+
| `progress` | 0–100 real progress; omit for automatic | |
|
|
172
|
+
| `loadingLabel` | label while loading, e.g. "Saving…" (start/end) | |
|
|
173
|
+
| `minLoadingTime` | ms the spinner stays visible at minimum | `0` |
|
|
174
|
+
| `onClick` | may return a Promise → spinner until it settles | |
|
|
175
|
+
| `leadingIcon` | element or icon-font class string | |
|
|
176
|
+
| `trailingIcon` | element or icon-font class string | |
|
|
177
|
+
| `icon` | icon for `iconOnly` buttons (same options) | |
|
|
178
|
+
| `asChild` | boolean — render the child (e.g. a Link) | `false` |
|
|
179
|
+
|
|
180
|
+
### Corners
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
<Button rounded="sm">Slightly rounded</Button>
|
|
184
|
+
<Button rounded="full">Pill</Button>
|
|
185
|
+
<Button iconOnly rounded="full" icon="bi bi-plus-lg" aria-label="Add" /> // circle
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Change the default for a whole app with `--radius-control` (and `--radius-control-sm`,
|
|
189
|
+
`--radius-control-lg`) in your stylesheet.
|
|
190
|
+
|
|
191
|
+
### Raised edge and shadows
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
<Button raised={false}>Flat</Button>
|
|
195
|
+
<Button shadow="lg">Raised with a large shadow</Button>
|
|
196
|
+
<Button raised={false} shadow="xl" iconOnly rounded="full" icon="bi bi-plus-lg" aria-label="Create" />
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Shadow weights are theme tokens (`--ui-shadow-sm` … `--ui-shadow-xl`, and
|
|
200
|
+
`--ui-shadow-color` for the tint), with separate values for dark mode.
|
|
201
|
+
|
|
202
|
+
### Loading (Ladda-style)
|
|
203
|
+
|
|
204
|
+
Return a Promise from `onClick` and the spinner shows until the response arrives,
|
|
205
|
+
whether the request succeeds or fails:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
<Button onClick={() => fetch("/api/report", { method: "POST" })}>Generate report</Button>
|
|
209
|
+
<Button spinnerPlacement="start" loadingLabel="Saving…" onClick={() => axios.put("/profile", data)}>
|
|
210
|
+
Save profile
|
|
211
|
+
</Button>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Or control it yourself with `loading`:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
<Button type="submit" loading={form.processing}>Save</Button> // Inertia useForm
|
|
218
|
+
<Button loading={isPending} onClick={() => startTransition(save)}>Save</Button> // Next.js
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
While loading, the button stays focusable, is marked `aria-busy`, and ignores
|
|
222
|
+
further clicks (so forms can't be submitted twice).
|
|
223
|
+
|
|
224
|
+
### Running light (experimental)
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
<Button loadingIndicator="orbit" loadingLabel="Saving…" onClick={save}>Save profile</Button>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
A light runs around the button's edge while loading; the label stays visible. Tune it with
|
|
231
|
+
`--ui-orbit-color`, `--ui-orbit-width` and `--ui-orbit-speed`. It needs CSS `@property`
|
|
232
|
+
(current Chrome, Edge, Safari and Firefox); older browsers show a still light.
|
|
233
|
+
|
|
234
|
+
### Progress (experimental)
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
<Button loadingIndicator="progress" onClick={generate}>Generate report</Button>
|
|
238
|
+
<Button loadingIndicator="progress" progressStyle="bar" loading={uploading} progress={pct}>
|
|
239
|
+
Upload
|
|
240
|
+
</Button>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`fill` sweeps a translucent fill across the button; `bar` runs a thin bar along the bottom.
|
|
244
|
+
Without `progress` it creeps toward ~92% and completes when loading ends. Tune with
|
|
245
|
+
`--ui-progress-fill`, `--ui-progress-bar`, `--ui-progress-height`, `--ui-progress-duration`.
|
|
246
|
+
|
|
247
|
+
### Icon fonts (Bootstrap Icons, Font Awesome, Glyphicons)
|
|
248
|
+
|
|
249
|
+
Pass the icon's class string and the Button renders `<i class="…" aria-hidden="true">`,
|
|
250
|
+
sized to match the label:
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
<Button leadingIcon="bi bi-cloud-arrow-up">Upload files</Button>
|
|
254
|
+
<Button variant="secondary" trailingIcon="bi bi-chevron-down">Sort by date</Button>
|
|
255
|
+
<Button iconOnly icon="bi bi-gear" aria-label="Settings" />
|
|
256
|
+
<Button leadingIcon="glyphicon glyphicon-print">Print</Button>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Load the icon font's CSS once in each app, e.g. `npm install bootstrap-icons`, then
|
|
260
|
+
`import "bootstrap-icons/font/bootstrap-icons.css"` in Next.js `app/layout.tsx`
|
|
261
|
+
or Laravel `resources/js/app.tsx`.
|
|
262
|
+
|
|
263
|
+
`type` defaults to `"button"`; set `type="submit"` on form submit buttons.
|
|
264
|
+
|
|
265
|
+
## Carousel
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
<Carousel aria-label="Photos" indicators="thumbnails">
|
|
269
|
+
<CarouselSlide label="Sagada" thumbnail={<img … />}><img … /></CarouselSlide>
|
|
270
|
+
</Carousel>
|
|
271
|
+
|
|
272
|
+
<Carousel aria-label="Plans" effect="coverflow" slidesPerView={{ base: 1.3, md: 2.4 }} defaultIndex={1} />
|
|
273
|
+
<Carousel aria-label="Highlights" indicators="stories" autoplay={4000} loop />
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Native scroll snapping (real swipe momentum, trackpads, no scroll-jacking) plus mouse dragging
|
|
277
|
+
and keyboard. `Carousel`: `slidesPerView` (number or `{ base, sm, md, lg }` by container width;
|
|
278
|
+
fractions peek), `gap`, `peek`, `align` (`start` `center`), `effect` (`coverflow`), `loop`,
|
|
279
|
+
`autoplay` (ms; pauses on hover/touch/focus, hidden tab or off-screen; off with reduced motion;
|
|
280
|
+
pause button), `indicators` (`dots` `counter` `stories` `thumbnails` `none`), `controls`
|
|
281
|
+
(`overlay` `below` `none`), `defaultIndex`, `onSlideChange`; ref → `{ next, prev, goTo, index }`.
|
|
282
|
+
`CarouselSlide`: `label`, `thumbnail`.
|
|
283
|
+
|
|
284
|
+
## Checkbox, radio & switch
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
<Checkbox label="Attach PDF" description="Adds the invoice as an attachment." />
|
|
288
|
+
<Checkbox label="All clients" checked={all} indeterminate={some && !all} onCheckedChange={toggleAll} />
|
|
289
|
+
<CheckboxGroup name="methods[]" options={[{ value: "card", label: "Card" }, …]} defaultValue={["card"]} />
|
|
290
|
+
<RadioGroup label="Send" defaultValue="now"><Radio value="now" label="Now" /><Radio value="draft" label="Save as draft" /></RadioGroup>
|
|
291
|
+
<Switch label="Weekly summary" onCheckedChange={(on) => api.save(on)} /> {/* spinner; flips back on failure */}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Real inputs underneath (keyboard, screen readers and form posts work natively). `Checkbox`:
|
|
295
|
+
`label`, `description`, `indeterminate`, `onCheckedChange`, `invalid`, `size` (`sm` `md`) + input
|
|
296
|
+
props. `CheckboxGroup`: `label`, `options`, `value` / `defaultValue` / `onValueChange`, `name`,
|
|
297
|
+
`orientation`. `RadioGroup`: `label`, `value` / `defaultValue` / `onValueChange`, `name`,
|
|
298
|
+
`orientation`, `size`, `disabled`, `invalid`, `required`; `Radio`: `value`, `label`, `description`.
|
|
299
|
+
`Switch`: `color` (`primary` `secondary` `tertiary` `success` `warning` `danger` `info` or any CSS
|
|
300
|
+
color; tokens `--ui-tone-secondary` / `--ui-tone-tertiary` + `-fg`), `colorForeground`, `variant` (`labelled` — the word in the pill, `mark` — × / ✓ on the knob, `liquid` —
|
|
301
|
+
blue floods from the knob), `label`, `description`, `onCheckedChange` (may return a Promise:
|
|
302
|
+
spinner, flips back on failure), `size`, `labelPosition`, `onLabel`, `offLabel`.
|
|
303
|
+
|
|
304
|
+
## Alert
|
|
305
|
+
|
|
306
|
+
A message that stays on the page. (For one that appears and leaves on its own, use Toast.)
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
<Alert tone="success" title="Invoice sent to Northwind Traders" />
|
|
310
|
+
<Alert tone="warning" variant="accent" title="Your card expires this month" dismissible onDismiss={hide}
|
|
311
|
+
actions={<Button size="sm">Update card</Button>}>
|
|
312
|
+
Update it before 30 September so invoices keep going out.
|
|
313
|
+
</Alert>
|
|
314
|
+
<Alert tone="danger" title="We couldn't save this invoice" items={Object.values(errors)} />
|
|
315
|
+
<Alert banner size="sm" tone="warning" title="Your trial ends in 3 days" />
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`tone` (`info` `success` `warning` `danger` `neutral`), `variant` (`soft` `outline` `accent`
|
|
319
|
+
`solid`), `title`, `children`, `items` (a bulleted summary — the shape Laravel's validation errors
|
|
320
|
+
arrive in), `actions`, `icon` (your own, or `false`), `dismissible` + `onDismiss`, `autoDismiss`
|
|
321
|
+
(ms; pauses on hover or focus), `size`, `banner` (edge to edge, square corners), `live`
|
|
322
|
+
(`auto` announces danger and warning immediately, everything else politely; `off` stays quiet).
|
|
323
|
+
Dismissal fades and collapses the space instead of snapping the page up.
|
|
324
|
+
|
|
325
|
+
## Toast
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
<Toaster position="bottom-right" /> {/* once, near the root */}
|
|
329
|
+
|
|
330
|
+
toast("Draft saved");
|
|
331
|
+
toast.success("Invoice sent", { description: "…" });
|
|
332
|
+
toast.promise(api.send(id), { loading: "Sending…", success: "Sent", error: (e) => e.message });
|
|
333
|
+
toast("Invoice deleted", { action: { label: "Undo", onClick: restore } });
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`toast(title, options)` and `toast.success / error / warning / info / loading / promise / dismiss`.
|
|
337
|
+
Options: `id` (update in place), `description`, `action`, `duration`, `icon`, `dismissible`,
|
|
338
|
+
`onDismiss`. `Toaster`: `position`, `expand`, `visibleToasts`, `richColors`, `showTimer`, `hotkey`.
|
|
339
|
+
Stacks and spreads on hover, pauses while hovered/focused/hidden, swipe or Escape to dismiss.
|
|
340
|
+
|
|
341
|
+
## File upload
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
<FileDropzone upload={xhrUpload("/api/receipts")} accept="image/*,.pdf" maxSize={10 * 1024 * 1024} maxFiles={6} />
|
|
345
|
+
<FileUploadButton upload={xhrUpload("/api/contracts")} accept=".pdf">Upload contract</FileUploadButton>
|
|
346
|
+
<FileInput name="resume" accept=".pdf" maxSize={5 * 1024 * 1024} /> {/* posts with the form */}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`FileDropzone`: `upload`, `accept`, `maxSize`, `maxFiles`, `multiple`, `concurrency`, `onChange`,
|
|
350
|
+
`onUploaded`, `title`, `hint`, `name`, `disabled`, `pasteable`, `size`. `FileUploadButton`: Button
|
|
351
|
+
props + `upload`, `accept`, `maxSize`, `onUploaded`, `onRemove`, `completeLabel`. `FileInput`:
|
|
352
|
+
native file-input props + `onFilesChange`, `buttonLabel`, `placeholder`, `size`, `rounded`,
|
|
353
|
+
`invalid`, `maxSize`, `clearable`. `xhrUpload(url, { fieldName, method, headers, data,
|
|
354
|
+
withCredentials, xsrfCookie })` gives real progress and reads Laravel's XSRF cookie and
|
|
355
|
+
validation errors. `useFileUploads(…)` and `FileList` for custom layouts.
|
|
356
|
+
|
|
357
|
+
## Field, Input and Textarea
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
import { Field, Input, Textarea } from "@astherix/ui";
|
|
361
|
+
|
|
362
|
+
<Field label="Email" description="We'll send receipts here." error={form.errors.email} required>
|
|
363
|
+
<Input type="email" leadingIcon="bi bi-envelope" />
|
|
364
|
+
</Field>
|
|
365
|
+
|
|
366
|
+
<Field label="Message">
|
|
367
|
+
<Textarea autoResize minRows={3} maxRows={8} showCount maxLength={500} />
|
|
368
|
+
</Field>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Field** — `label`, `description`, `error` (marks the control invalid), `required`, `optional`,
|
|
372
|
+
`disabled`, `id`. Wires the label, `aria-describedby` and `aria-invalid` for you. Custom
|
|
373
|
+
controls can read it with `useField()`.
|
|
374
|
+
|
|
375
|
+
**Input** — `size` (`sm` `md` `lg`, same heights as Button), `rounded` (`none` … `full`),
|
|
376
|
+
`leadingIcon` / `trailingIcon` (element or icon-font class), `prefix` / `suffix` text,
|
|
377
|
+
`invalid`, `clearable` + `onClear`, `revealable` (password show/hide, default on),
|
|
378
|
+
`frameClassName`. All native input props work; `ref` goes to the `<input>`.
|
|
379
|
+
|
|
380
|
+
**Textarea** — `size`, `rounded` (`none` … `lg`), `invalid`, `autoResize` with `minRows` /
|
|
381
|
+
`maxRows`, `showCount` (with `maxLength` shows "12 / 280"), `resize` (`vertical` `none`),
|
|
382
|
+
`frameClassName`.
|
|
383
|
+
|
|
384
|
+
## Pagination
|
|
385
|
+
|
|
386
|
+
```tsx
|
|
387
|
+
<Pagination page={page} pageCount={12} onPageChange={setPage} />
|
|
388
|
+
<Pagination page={p.current_page} pageCount={p.last_page} total={p.total} pageSize={p.per_page}
|
|
389
|
+
getHref={(n) => `${p.path}?page=${n}`} linkComponent={Link} showSummary />
|
|
390
|
+
<LoadMore loaded={items.length} total={total} onLoadMore={fetchNext} auto />
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
`Pagination`: `page`, `pageCount` or `total` + `pageSize`, `onPageChange`, `getHref` +
|
|
394
|
+
`linkComponent` (real links), `siblingCount`, `boundaryCount`, `showFirstLast`, `variant`
|
|
395
|
+
(`numbers` `simple`), `responsive` (fits its own width), `showSummary`, `pageSizeOptions` +
|
|
396
|
+
`onPageSizeChange`, `showJump`, `size`, `labels`. Gaps jump five pages; the highlight slides.
|
|
397
|
+
`LoadMore`: `loaded`, `total`, `onLoadMore` (Promise → progress), `hasMore`, `auto` (infinite
|
|
398
|
+
scroll), `label`. `getPageItems(page, count, siblings, boundaries)` is exported too.
|
|
399
|
+
|
|
400
|
+
## Pills
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
<Pill tone="success" dot>Paid</Pill>
|
|
404
|
+
<Pill tone="danger" appearance="solid">Overdue</Pill>
|
|
405
|
+
<Pill size="lg" onRemove={() => removeTag(tag)}>{tag}</Pill>
|
|
406
|
+
<Pill asChild tone="primary"><Link href="/topics/laravel">Laravel</Link></Pill>
|
|
407
|
+
|
|
408
|
+
<PillGroup value={category} onValueChange={setCategory} aria-label="Category">
|
|
409
|
+
<PillOption value="all" count={5}>All</PillOption>
|
|
410
|
+
<PillOption value="design">Design</PillOption>
|
|
411
|
+
</PillGroup>
|
|
412
|
+
<PillGroup type="multiple" value={tags} onValueChange={setTags}>…</PillGroup>
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
`Pill`: `tone` (`neutral` `primary` `success` `warning` `danger` `info`), `appearance`
|
|
416
|
+
(`soft` `solid` `outline`), `size` (`sm` `md` `lg`), `icon`, `dot` (`true` or `"pulse"`),
|
|
417
|
+
`count`, `onRemove`, `removeLabel`, `asChild`. `PillGroup`: `type` (`single` `multiple`),
|
|
418
|
+
`value` / `defaultValue` / `onValueChange`, `allowDeselect`, `size`, `tone`, `showCheck`,
|
|
419
|
+
`disabled`. `PillOption`: `value`, `icon`, `count`, `disabled`.
|
|
420
|
+
Status colors are theme tokens: `--ui-success`, `--ui-warning`, `--ui-info` (+ `-fg`).
|
|
421
|
+
|
|
422
|
+
## Autocomplete
|
|
423
|
+
|
|
424
|
+
A text field that suggests as you type — the value is free text (use Select when it must be from a list).
|
|
425
|
+
|
|
426
|
+
```tsx
|
|
427
|
+
<Autocomplete value={city} onChange={setCity} suggestions={cities} />
|
|
428
|
+
<Autocomplete loadSuggestions={(q, { signal }) => api.searchClients(q, signal)} minLength={2} />
|
|
429
|
+
<Autocomplete type="search" recentKey="recent-searches" onSubmit={runSearch} suggestions={items} />
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Props (plus Input's: `size`, `rounded`, icons, `clearable`, …): `value` / `defaultValue` / `onChange`
|
|
433
|
+
(string), `suggestions` (array of strings or `{ value, label, description, icon, group }`, or a
|
|
434
|
+
function of the query), `loadSuggestions`, `minLength`, `debounce`, `maxSuggestions`,
|
|
435
|
+
`inlineComplete` (faint completion; Tab/→ accepts), `onSelectSuggestion`, `onSubmit`, `recentKey`,
|
|
436
|
+
`maxRecent`, `emptyMessage`, `filter`.
|
|
437
|
+
|
|
438
|
+
## Slider
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
<Slider value={fee} onChange={setFee} max={25} formatValue={(v) => `${v}%`} />
|
|
442
|
+
<Slider value={[5000, 60000]} onChange={setRange} max={100000} step={1000} minDistance={5000} />
|
|
443
|
+
<Slider min={1} max={10} marks color="secondary" onValueCommit={save} />
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Press the thumb and it becomes a frosted glass lens with a glass value bubble above the finger;
|
|
447
|
+
drag past an end and it stretches, then springs back; crossing marks gives a haptic tick on
|
|
448
|
+
supporting phones. Props: `value` / `defaultValue` (number, or `[low, high]` for a range),
|
|
449
|
+
`onChange`, `onValueCommit` (on release), `min`, `max`, `step`, `marks` (`true`, values, or
|
|
450
|
+
`{ value, label }`), `formatValue`, `showValue` (`active` `always` `never`), `color` (Switch
|
|
451
|
+
palette or any CSS color), `size` (`sm` `md` `lg`), `disabled`, `minDistance`, `haptics`, `name`,
|
|
452
|
+
`thumbLabels`.
|
|
453
|
+
|
|
454
|
+
## Signature
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
const pad = useRef<SignaturePadHandle>(null);
|
|
458
|
+
|
|
459
|
+
<SignaturePad ref={pad} name="signature" allowTyped onChange={setSignature} />
|
|
460
|
+
|
|
461
|
+
pad.current.toDataURL({ trim: true, ink: "#111827", background: "#ffffff" }) // PNG
|
|
462
|
+
pad.current.toSVG({ trim: true, ink: "#111827" }) // vector, for PDFs
|
|
463
|
+
pad.current.clear(); pad.current.undo(); pad.current.isEmpty(); pad.current.fromDataURL(png);
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Ink thickens and thins with speed (and stylus pressure); strokes are kept as points, so it
|
|
467
|
+
redraws crisply on resize and undo works stroke by stroke. Props: `onChange` (PNG data URL or
|
|
468
|
+
null), `onBegin`, `onEnd`, `penColor`, `penWidth` `[min, max]`, `height`, `background`, `guide`,
|
|
469
|
+
`guideLabel`, `toolbar`, `actions`, `readOnly`, `defaultValue`, `name` (posts with the form),
|
|
470
|
+
`allowTyped` (a Type tab in a handwriting font — the accessible alternative), `typedFont`.
|
|
471
|
+
Export `ink` matters: the pen follows the theme, so force a dark color for people signing in
|
|
472
|
+
dark mode.
|
|
473
|
+
|
|
474
|
+
## Select
|
|
475
|
+
|
|
476
|
+
A searchable dropdown in the spirit of Select2.
|
|
477
|
+
|
|
478
|
+
```tsx
|
|
479
|
+
<Field label="Country">
|
|
480
|
+
<Select options={countries} value={country} onChange={setCountry} />
|
|
481
|
+
</Field>
|
|
482
|
+
|
|
483
|
+
<Select multiple options={skills} value={skills} onChange={setSkills} maxSelected={5} clearable />
|
|
484
|
+
<Select loadOptions={(q) => api.searchRepos(q)} minSearchLength={2} />
|
|
485
|
+
<Select multiple creatable options={labels} onCreateOption={(text) => api.createLabel(text)} />
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Options: `{ value, label, description?, icon?, group?, disabled?, keywords? }`.
|
|
489
|
+
|
|
490
|
+
Props: `options`, `value` / `defaultValue` / `onChange` (string | null, or string[] with
|
|
491
|
+
`multiple`), `multiple`, `maxSelected`, `searchable` (default true), `searchPlaceholder`,
|
|
492
|
+
`clearable`, `placeholder`, `loadOptions` + `minSearchLength` + `searchDelay`, `creatable` +
|
|
493
|
+
`onCreateOption`, `renderOption`, `noOptionsMessage`, `closeOnSelect`, `onOpenChange`,
|
|
494
|
+
`name` (hidden inputs for form posts), `size`, `rounded`, `invalid`, `disabled`, `required`.
|
|
495
|
+
|
|
496
|
+
Keyboard: Enter/Space/↓ open, type to search, ↑ ↓ Home End move, Enter picks, Backspace removes
|
|
497
|
+
the last chip, Escape closes (before a surrounding Modal), Tab closes and moves on.
|
|
498
|
+
|
|
499
|
+
## Card
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
<Card variant="outline" | "elevated" | "filled" | "ghost" padding="md">
|
|
503
|
+
<CardMedia src={cover} ratio="16/9" />
|
|
504
|
+
<CardHeader title="Website refresh" description="Due 30 September" action={<Button …/>} />
|
|
505
|
+
<CardContent>…</CardContent>
|
|
506
|
+
<CardFooter divided justify="between">…</CardFooter>
|
|
507
|
+
</Card>
|
|
508
|
+
|
|
509
|
+
// Whole card clickable; other buttons inside still work
|
|
510
|
+
<Card interactive><CardHeader><CardTitle><CardLink href="/posts/1">Title</CardLink></CardTitle></CardHeader></Card>
|
|
511
|
+
|
|
512
|
+
// Choices with real radios / checkboxes
|
|
513
|
+
<ChoiceCardGroup value={plan} onValueChange={setPlan} name="plan">
|
|
514
|
+
<ChoiceCard value="team" title="Team" description="…" meta="₱990 / month" />
|
|
515
|
+
</ChoiceCardGroup>
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
`Card`: `variant`, `tone` (`default` `primary` `danger`), `orientation` (`vertical` `horizontal`,
|
|
519
|
+
wrap text in `CardBody`), `interactive`, `padding` (`none` `sm` `md` `lg`), `asChild`.
|
|
520
|
+
`CardHeader`: `title`, `description`, `action`, `icon`, `titleLevel`. `CardFooter`: `divided`,
|
|
521
|
+
`justify`. `CardMedia`: `src`, `alt`, `ratio`, `inset`. `ChoiceCardGroup`: `type`, `value` /
|
|
522
|
+
`defaultValue` / `onValueChange`, `name`, `columns`, `disabled`, `invalid`, `required`.
|
|
523
|
+
`ChoiceCard`: `value`, `title`, `description`, `icon`, `meta`, `badge`, `disabled`.
|
|
524
|
+
Token: `--radius-card`.
|
|
525
|
+
|
|
526
|
+
## Dashboard widgets
|
|
527
|
+
|
|
528
|
+
Dependency-free SVG widgets for KPI dashboards — one look, one palette (`--ui-chart-1…6`),
|
|
529
|
+
responsive, keyboard- and screen-reader-friendly.
|
|
530
|
+
|
|
531
|
+
```tsx
|
|
532
|
+
<DashboardGrid columns={4}>
|
|
533
|
+
<KpiCard label="Revenue" value="₱318,400" change={8.2} sparkline={monthly} />
|
|
534
|
+
<WidgetCard data-span="3" title="Revenue" action={<PeriodPicker />} loading={isLoading}>
|
|
535
|
+
<AreaChart data={rows} index="month" series={[{ key: "invoiced" }, { key: "collected" }]}
|
|
536
|
+
referenceLine={{ value: 280000, label: "Target" }} aria-label="Revenue by month" />
|
|
537
|
+
</WidgetCard>
|
|
538
|
+
</DashboardGrid>
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
- `KpiCard`: `label`, `value`, `change`, `changeLabel`, `invertTrend`, `icon`, `sparkline`,
|
|
542
|
+
`sparklineType`, `progress`, `href`, `loading`
|
|
543
|
+
- `LineChart` / `AreaChart` / `BarChart`: `data`, `index`, `series` (`key`, `label`, `color`),
|
|
544
|
+
`height`, `formatValue`, `formatIndex`, `showLegend`, `showGrid`, `referenceLine`; line: `curve`,
|
|
545
|
+
`dots`; bar: `stacked`. Crosshair tooltip, legend toggles, ← → to read points
|
|
546
|
+
- `DonutChart` (`data`, `size`, `thickness`, `centerLabel`, `legend`), `Gauge` (`value`, `max`,
|
|
547
|
+
`target`, `bands`), `BarList` (`items`, `limit`), `CalendarHeatmap` (`data`, `weeks`),
|
|
548
|
+
`Sparkline` (`data`, `type`)
|
|
549
|
+
- `WidgetCard`: `title`, `description`, `action`, `value`, `loading`, `empty`, `error`,
|
|
550
|
+
`onRetry`, `footer`, `live`; `DashboardGrid`: `columns`, `animate`, children use
|
|
551
|
+
`data-span="2|3|4|full"`; `LiveIndicator`: `updatedAt`, `paused`, `reconnecting`
|
|
552
|
+
|
|
553
|
+
**Animate on first load (optional):** `<DashboardGrid animate>` — widgets rise in one after another,
|
|
554
|
+
KPI numbers count up (pass `value` as a number with `formatValue`), charts draw in, donuts and
|
|
555
|
+
gauges sweep. Per-widget `animate` overrides it. Skipped with reduced motion.
|
|
556
|
+
|
|
557
|
+
**Live data:** just pass new data. Numbers roll and glow (green good / red bad), lines and bars
|
|
558
|
+
morph, donut and gauge sweep, BarList rows slide into their new order (`transition={false}` on a
|
|
559
|
+
chart turns it off). Works with polling, Laravel Echo (Reverb/Pusher) or Server-Sent Events.
|
|
560
|
+
|
|
561
|
+
## Dropdown menu
|
|
562
|
+
|
|
563
|
+
```tsx
|
|
564
|
+
<DropdownMenu>
|
|
565
|
+
<DropdownMenuTrigger asChild><Button trailingIcon="bi bi-chevron-down">Actions</Button></DropdownMenuTrigger>
|
|
566
|
+
<DropdownMenuContent align="start">
|
|
567
|
+
<DropdownMenuItem icon="bi bi-pencil" shortcut="⌘E" onSelect={edit}>Edit</DropdownMenuItem>
|
|
568
|
+
<DropdownMenuItem asChild><Link href="/settings">Settings</Link></DropdownMenuItem>
|
|
569
|
+
<DropdownMenuSeparator />
|
|
570
|
+
<DropdownMenuItem destructive onSelect={remove}>Delete</DropdownMenuItem>
|
|
571
|
+
</DropdownMenuContent>
|
|
572
|
+
</DropdownMenu>
|
|
573
|
+
|
|
574
|
+
<SplitButton onClick={save} menu={<DropdownMenuItem onSelect={saveDraft}>Save as draft</DropdownMenuItem>}>Save</SplitButton>
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
`DropdownMenu`: `open` / `defaultOpen` / `onOpenChange`. `DropdownMenuContent`: `align`
|
|
578
|
+
(`start` `end`), `side` (`bottom` `top`, flips automatically), `matchTriggerWidth`.
|
|
579
|
+
`DropdownMenuItem`: `onSelect` (call `event.preventDefault()` to keep the menu open), `icon`,
|
|
580
|
+
`shortcut`, `description`, `destructive`, `disabled`, `inset`, `textValue`, `asChild`. Also
|
|
581
|
+
`DropdownMenuCheckboxItem` (`checked`, `onCheckedChange`), `DropdownMenuRadioGroup` +
|
|
582
|
+
`DropdownMenuRadioItem`, `DropdownMenuLabel`, `DropdownMenuSeparator`, `DropdownMenuGroup`.
|
|
583
|
+
`SplitButton`: Button props + `menu`, `menuLabel`, `align`.
|
|
584
|
+
|
|
585
|
+
## Editor (rich text)
|
|
586
|
+
|
|
587
|
+
A basic WYSIWYG editor built on [Tiptap](https://tiptap.dev). It has its own entry point so
|
|
588
|
+
apps that don't use it don't load it:
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
import { Editor } from "@astherix/ui/editor";
|
|
592
|
+
|
|
593
|
+
<Field label="Release notes">
|
|
594
|
+
<Editor value={html} onChange={setHtml} />
|
|
595
|
+
</Field>
|
|
596
|
+
|
|
597
|
+
<Editor toolbar={["bold", "italic", "link", "|", "bulletList"]} maxLength={280} />
|
|
598
|
+
<Editor name="body" /> {/* posts the HTML with the form */}
|
|
599
|
+
<Editor readOnly value={post.body} />
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Props: `value` / `defaultValue` / `onChange` (HTML; empty editor gives `""`), `placeholder`,
|
|
603
|
+
`toolbar` (tools: `paragraph heading2 heading3 bold italic underline strike code link
|
|
604
|
+
bulletList orderedList blockquote horizontalRule undo redo`, `"|"` for a divider),
|
|
605
|
+
`maxLength`, `showCount`, `minHeight`, `maxHeight`, `rounded`, `invalid`, `disabled`,
|
|
606
|
+
`readOnly`, `name`, `autoFocus`, `onReady(editor)` for the Tiptap instance.
|
|
607
|
+
|
|
608
|
+
Show saved HTML with the same styles: `<div className="ui-prose" dangerouslySetInnerHTML={…} />`.
|
|
609
|
+
Always sanitize HTML on the server (e.g. `stevebauman/purify` in Laravel).
|
|
610
|
+
|
|
611
|
+
## IconButton
|
|
612
|
+
|
|
613
|
+
The icon itself is the button. Press: the icon squishes and a ripple spreads; toggles swap to a
|
|
614
|
+
filled icon, pop and burst.
|
|
615
|
+
|
|
616
|
+
```tsx
|
|
617
|
+
<IconButton icon="bi bi-heart" pressedIcon="bi bi-heart-fill" tone="danger" label="Like"
|
|
618
|
+
pressed={liked} onPressedChange={setLiked} />
|
|
619
|
+
<IconButton icon="bi bi-bell" label="Notifications" badge={3} />
|
|
620
|
+
<IconButton icon="bi bi-download" label="Download INV-1047" onClick={download} /> {/* Promise → spinner */}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Props: `icon`, `label` (required; spoken and tooltip), `variant` (`plain` `soft` `solid`), `tone`
|
|
624
|
+
(Switch palette or any CSS color), `size` (`xs`–`xl`; small ones get a 44px tap area on touch),
|
|
625
|
+
`shape` (`circle` `square`), `pressed` / `defaultPressed` / `onPressedChange`, `pressedIcon`,
|
|
626
|
+
`burst`, `badge` (number or dot), `loading`, `href`, `tooltip`, `disabled`.
|
|
627
|
+
|
|
628
|
+
## Modal
|
|
629
|
+
|
|
630
|
+
```tsx
|
|
631
|
+
import { Modal, ModalTrigger, ModalContent, ModalHeader, ModalTitle,
|
|
632
|
+
ModalDescription, ModalBody, ModalFooter, ModalClose, useModal } from "@astherix/ui";
|
|
633
|
+
|
|
634
|
+
// Open with a button
|
|
635
|
+
<Modal>
|
|
636
|
+
<ModalTrigger asChild><Button>Rename project</Button></ModalTrigger>
|
|
637
|
+
<ModalContent size="sm">
|
|
638
|
+
<ModalHeader>
|
|
639
|
+
<ModalTitle>Rename project</ModalTitle>
|
|
640
|
+
<ModalDescription>The new name shows up for everyone.</ModalDescription>
|
|
641
|
+
</ModalHeader>
|
|
642
|
+
<ModalBody>…</ModalBody>
|
|
643
|
+
<ModalFooter>
|
|
644
|
+
<ModalClose asChild><Button variant="ghost">Cancel</Button></ModalClose>
|
|
645
|
+
<Button onClick={save}>Save name</Button>
|
|
646
|
+
</ModalFooter>
|
|
647
|
+
</ModalContent>
|
|
648
|
+
</Modal>
|
|
649
|
+
|
|
650
|
+
// Open from code
|
|
651
|
+
const session = useModal(); // { isOpen, open, close, toggle, modalProps }
|
|
652
|
+
<Modal {...session.modalProps}>…</Modal>
|
|
653
|
+
session.open();
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
| Component / prop | Values | Default |
|
|
657
|
+
| -------------------------------- | ---------------------------------------- | ------- |
|
|
658
|
+
| `Modal` `open` / `onOpenChange` | controlled state | |
|
|
659
|
+
| `Modal` `defaultOpen` | boolean | `false` |
|
|
660
|
+
| `ModalContent` `size` | `sm` `md` `lg` `xl` `full` | `md` |
|
|
661
|
+
| `ModalContent` `backdrop` | `default` `dark` `blur` `solid` | `default` |
|
|
662
|
+
| `ModalContent` `dismissible` | Escape, backdrop and × close it | `true` |
|
|
663
|
+
| `ModalContent` `showCloseButton` | boolean | `true` |
|
|
664
|
+
| `ModalContent` `closeLabel` | label for × | `"Close"` |
|
|
665
|
+
|
|
666
|
+
Built on the native `<dialog>`: focus moves in on open (honours `autoFocus`) and returns
|
|
667
|
+
to the opener on close, Tab stays inside, the page behind is inert and doesn't scroll.
|
|
668
|
+
Theme tokens: `--radius-modal`, `--ui-shadow-modal`, `--ui-backdrop`, `--ui-backdrop-dark`,
|
|
669
|
+
`--ui-backdrop-blur`, `--ui-backdrop-blur-radius`, `--ui-backdrop-solid`.
|
|
670
|
+
|
|
671
|
+
## Table (DataTable)
|
|
672
|
+
|
|
673
|
+
```tsx
|
|
674
|
+
const columns: DataTableColumn<Invoice>[] = [
|
|
675
|
+
{ key: "no", header: "Invoice", sortable: true, primary: true },
|
|
676
|
+
{ key: "client", header: "Client", sortable: true, cell: (r) => r.client },
|
|
677
|
+
{ key: "issued", header: "Issued", hideOnMobile: true },
|
|
678
|
+
{ key: "amount", header: "Amount", sortable: true, align: "end", cell: (r) => peso(r.amount) },
|
|
679
|
+
];
|
|
680
|
+
|
|
681
|
+
<DataTable caption="Invoices" data={rows} columns={columns} rowKey="id"
|
|
682
|
+
searchable selectable bulkActions={(rows, clear) => …} rowActions={(row) => <RowMenu row={row} />} />
|
|
683
|
+
|
|
684
|
+
// Server-side (e.g. a Laravel paginator)
|
|
685
|
+
<DataTable manual data={page.data} total={page.total} loading={loading} onQueryChange={fetchPage} … />
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
Columns: `key`, `header`, `accessor`, `cell`, `sortable`, `sortFn`, `align`, `width`, `primary`
|
|
689
|
+
(card title on mobile), `hideOnMobile`, `searchable`, `className`.
|
|
690
|
+
Table: `caption` (required, accessible name), `showCaption`, `searchable`, `searchPlaceholder`,
|
|
691
|
+
`pageSize` (0 = all), `pageSizeOptions`, `defaultSort`, `selectable`, `selected` /
|
|
692
|
+
`onSelectedChange`, `bulkActions`, `rowActions`, `onRowClick`, `toolbar`, `loading`,
|
|
693
|
+
`refreshIndicator` (`border` `bar` `shimmer` `none`), `onRefresh` (adds a refresh button; runs
|
|
694
|
+
until its Promise settles), `status` (`true` for automatic messages, your own text, or
|
|
695
|
+
`(state) => message`), `statusTone`, `error` (shown in the status line with Retry),
|
|
696
|
+
`lastUpdated`, `loadingOverlay` (`true` or a message), `overlayPosition` (`top` `center`),
|
|
697
|
+
`overlayIndicator`, `onCancelLoading`, `emptyState`, `density` (`comfortable` `compact`), `striped`, `mobile` (`cards` `scroll`),
|
|
698
|
+
`maxHeight`, `manual`, `total`, `onQueryChange`.
|
|
699
|
+
Narrow containers (under 40rem) switch to cards via a container query, so it adapts inside
|
|
700
|
+
sidebars and cards as well as on phones.
|
|
701
|
+
|
|
702
|
+
## Sidebar
|
|
703
|
+
|
|
704
|
+
```tsx
|
|
705
|
+
<SidebarProvider side="left" linkComponent={Link} persistKey="sidebar">
|
|
706
|
+
<Sidebar>
|
|
707
|
+
<SidebarHeader><Logo /></SidebarHeader>
|
|
708
|
+
<SidebarContent>
|
|
709
|
+
<SidebarGroup label="Workspace">
|
|
710
|
+
<SidebarNav items={nav} activeHref={pathname} />
|
|
711
|
+
</SidebarGroup>
|
|
712
|
+
</SidebarContent>
|
|
713
|
+
<SidebarFooter><AccountMenu /></SidebarFooter>
|
|
714
|
+
</Sidebar>
|
|
715
|
+
<SidebarInset>
|
|
716
|
+
<header><SidebarTrigger /></header>
|
|
717
|
+
…
|
|
718
|
+
</SidebarInset>
|
|
719
|
+
</SidebarProvider>
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
Nav items: `{ label, href?, icon?, badge?, children?, defaultOpen?, disabled?, onSelect?, external?, id? }`
|
|
723
|
+
— `children` nest to any depth. Wide: full sidebar ↔ icon rail (submenus open as a flyout);
|
|
724
|
+
narrow: off-canvas drawer. Chosen by the layout's own width.
|
|
725
|
+
`SidebarProvider`: `side`, `defaultCollapsed`, `collapsed` / `onCollapsedChange`, `drawerBelow` (768),
|
|
726
|
+
`railBelow` (1024), `persistKey`, `shortcut` ("b" → Ctrl/⌘+B), `linkComponent`, `onNavigate`, `contained`.
|
|
727
|
+
`Sidebar`: `width`, `railWidth`. `SidebarNav`: `items`, `activeHref`, `matchNested`. `SidebarGroup`:
|
|
728
|
+
`label`, `action`. `useSidebar()` → `{ mode, collapsed, drawerOpen, toggle, setCollapsed, setDrawerOpen, side }`.
|
|
729
|
+
|
|
730
|
+
## Tabs
|
|
731
|
+
|
|
732
|
+
```tsx
|
|
733
|
+
<Tabs defaultValue="overview" orientation="horizontal" variant="line">
|
|
734
|
+
<TabList aria-label="Project">
|
|
735
|
+
<Tab value="overview">Overview</Tab>
|
|
736
|
+
<Tab value="billing" disabled>Billing</Tab>
|
|
737
|
+
</TabList>
|
|
738
|
+
<TabPanel value="overview">…</TabPanel>
|
|
739
|
+
</Tabs>
|
|
740
|
+
|
|
741
|
+
// Editable: drag or Alt+arrows to move, double-click/F2 to rename, + to add, ×/Delete to close
|
|
742
|
+
<TabList onReorder={(ids) => …} onRename={(id, name) => …} onAdd={() => newId} onClose={(id) => …} renameOnAdd>
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
`Tabs`: `value` / `defaultValue` / `onValueChange`, `orientation` (`horizontal` `vertical`),
|
|
746
|
+
`variant` (`line` `enclosed` `pills`), `size` (`sm` `md`), `activation` (`automatic` `manual`).
|
|
747
|
+
`TabList`: `onReorder`, `onRename`, `onAdd` (return the new value to select it), `addLabel`,
|
|
748
|
+
`renameOnAdd`, `onClose`, `closeLabel`. `Tab`: `value`, `label`, `disabled`, `icon`, `badge`,
|
|
749
|
+
`renamable`, `closable`. `TabPanel`: `value`, `keepMounted`.
|
|
750
|
+
|
|
751
|
+
## Timeline
|
|
752
|
+
|
|
753
|
+
```tsx
|
|
754
|
+
<Timeline aria-label="Invoice activity" variant="feed" | "progress" layout="left" | "alternate">
|
|
755
|
+
<TimelineGroup date={new Date()}> {/* "Today" */}
|
|
756
|
+
<TimelineItem icon="bi bi-check-lg" tone="success" title="Payment received" time={at} />
|
|
757
|
+
<TimelineItem avatar={<Avatar … />} title="Maria commented" time={at} card>…</TimelineItem>
|
|
758
|
+
<TimelineCollapse>{quietItems}</TimelineCollapse>
|
|
759
|
+
</TimelineGroup>
|
|
760
|
+
</Timeline>
|
|
761
|
+
|
|
762
|
+
<Roadmap aria-label="Roadmap" items={[{ label: "Q3 2026", title: "Mobile app", status: "current" }]} />
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
`Timeline`: `layout`, `variant`, `size` (`sm` `md`), `animated` (the thread draws itself on
|
|
766
|
+
scroll where supported). `TimelineGroup`: `label` or `date`. `TimelineItem`: `title`, `time`
|
|
767
|
+
(shown as "2 hours ago") or `timeLabel`, `icon`, `avatar`, `tone`, `status` (`past` `current`
|
|
768
|
+
`upcoming`), `meta`, `card`, `last`. `TimelineCollapse`: `label`, `defaultOpen`.
|
|
769
|
+
`Roadmap`: `items` (`label`, `title`, `description`, `status` `done`/`current`/`upcoming`,
|
|
770
|
+
`footer`), `itemWidth`. Helpers: `formatTimelineTime`, `formatTimelineDay`.
|
|
771
|
+
|
|
772
|
+
## Typography
|
|
773
|
+
|
|
774
|
+
```tsx
|
|
775
|
+
<Heading level={1} size="display">Get paid faster</Heading>
|
|
776
|
+
<Lead>Create, send and track invoices from one place.</Lead>
|
|
777
|
+
<Text size="sm" tone="muted" numeric>₱48,200.00</Text>
|
|
778
|
+
<Link href="https://laravel.com/docs">Laravel docs</Link> {/* external: ↗, new tab */}
|
|
779
|
+
<Code>APP_ENV</Code> <Kbd keys={["⌘", "K"]} /> <Mark>match</Mark>
|
|
780
|
+
<Blockquote author="Maria Santos" source="Owner">…</Blockquote>
|
|
781
|
+
<List variant="check"><ListItem>Payment links</ListItem></List>
|
|
782
|
+
<Prose html={sanitizedHtml} />
|
|
783
|
+
<StatGroup divided><Stat label="Revenue" value="₱1.24M" change="12.5%" trend="up" /></StatGroup>
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
`Heading`: `level` (tag), `size` (`display` `h1`–`h6`), `tone`, `weight`, `align`, `truncate`,
|
|
787
|
+
`lineClamp`. `Text`: `as`, `size` (`xl` `lg` `md` `sm` `xs`), `tone` (`default` `muted` `subtle`
|
|
788
|
+
`primary` `success` `warning` `danger` `info` `inherit`), `weight`, `align`, `numeric`,
|
|
789
|
+
`truncate`, `lineClamp`. `Link`: `variant` (`inline` `subtle` `standalone`), `external`,
|
|
790
|
+
`asChild`. `Prose`: `size`, `html`. `Stat`: `label`, `value`, `change`, `trend`,
|
|
791
|
+
`invertTrend`, `description`, `size`. Set `--font-heading` for a separate heading font.
|
|
792
|
+
|
|
793
|
+
## Layout (Stack, Grid, Container…)
|
|
794
|
+
|
|
795
|
+
Small pieces for arranging things. Every size prop takes a plain value or an object keyed by
|
|
796
|
+
breakpoint, so a layout changes with the screen without you writing media queries.
|
|
797
|
+
|
|
798
|
+
```tsx
|
|
799
|
+
<Stack direction={{ base: "column", md: "row" }} gap={3} justify="between" align={{ md: "center" }}>…</Stack>
|
|
800
|
+
<Stack direction="row" gap={2} wrap divider>…</Stack>
|
|
801
|
+
|
|
802
|
+
<Grid columns={{ base: 1, sm: 2, lg: 4 }} gap={4}>…</Grid>
|
|
803
|
+
<Grid minChildWidth="16rem" gap={4}>…</Grid> {/* reacts to its own width */}
|
|
804
|
+
<GridItem span={{ base: 1, md: 3 }} rowSpan={2}>…</GridItem>
|
|
805
|
+
|
|
806
|
+
<Container size="xl" padding={{ base: 4, md: 8 }}>…</Container>
|
|
807
|
+
<Center minHeight="60vh">…</Center>
|
|
808
|
+
<Stack direction="row"><Logo /><Spacer /><Account /></Stack>
|
|
809
|
+
<Divider dashed spacing={4} /> <Divider>or</Divider>
|
|
810
|
+
<AspectRatio ratio="16/9"><img src={cover} alt="" /></AspectRatio>
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
`Stack`: `direction`, `gap`, `align`, `justify`, `wrap`, `divider`, `as`. `Grid`: `columns` or
|
|
814
|
+
`minChildWidth`, `gap`, `align`, `as`. `GridItem`: `span` (never exceeds the column count, so
|
|
815
|
+
nothing overflows on a phone), `rowSpan`. `Container`: `size` (`sm` `md` `lg` `xl` `2xl` `prose`
|
|
816
|
+
`full`, or any length), `padding`, `align`. `gap` and `padding` count in spacing steps, so they
|
|
817
|
+
follow the theme's density. Breakpoints follow the window, like Tailwind's own — for a layout that
|
|
818
|
+
reacts to the space it's actually in, use `minChildWidth` or `wrap`.
|
|
819
|
+
|
|
820
|
+
## Hero
|
|
821
|
+
|
|
822
|
+
The top of a landing page, in separate pieces so it can be quiet or loud.
|
|
823
|
+
|
|
824
|
+
```tsx
|
|
825
|
+
<Hero background="aurora" size="lg">
|
|
826
|
+
<HeroEyebrow href="/changelog" icon="bi bi-stars">New · Recurring invoices</HeroEyebrow>
|
|
827
|
+
<HeroTitle gradient>Get paid without chasing anyone</HeroTitle>
|
|
828
|
+
<HeroSubtitle>Send an invoice in thirty seconds and let the reminders go out on their own.</HeroSubtitle>
|
|
829
|
+
<HeroActions>
|
|
830
|
+
<Button size="lg">Start free</Button>
|
|
831
|
+
<Button size="lg" variant="secondary">See a demo</Button>
|
|
832
|
+
</HeroActions>
|
|
833
|
+
<HeroStats stats={[{ value: "12,400", label: "invoices sent" }, { value: "₱48M", label: "collected" }]} />
|
|
834
|
+
<HeroLogos logos={["Northwind", "Blue Harbor", "Luzon Freight"]} marquee />
|
|
835
|
+
</Hero>
|
|
836
|
+
|
|
837
|
+
<Hero background="image" image="/img/warehouse.jpg" overlay={0.6} size="screen" align="start"
|
|
838
|
+
media={<HeroMedia frame url="app.example.com/invoices" tilt float><img src={shot} alt="" /></HeroMedia>}>
|
|
839
|
+
…
|
|
840
|
+
</Hero>
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
`Hero`: `background` (`none` `aurora` `grid` `dots` `gradient` `image`), `image`, `overlay`, `dark`,
|
|
844
|
+
`align`, `size` (`sm` `md` `lg` `screen` — `screen` uses `100svh`), `media` (two columns from medium
|
|
845
|
+
screens), `colors`, `as`. `HeroTitle`: `as` (`h1` default), `gradient`, `size`, `colors`.
|
|
846
|
+
`HeroEyebrow`: `href` (makes it a link), `icon`. `HeroStats`: `stats`. `HeroLogos`: `logos`,
|
|
847
|
+
`label`, `marquee`, `speed` (pauses on hover). `HeroMedia`: `frame` (browser chrome), `url`,
|
|
848
|
+
`tilt`, `float`. Aurora, float and the marquee all stop under `prefers-reduced-motion`.
|
|
849
|
+
|
|
850
|
+
## Theme config (ui.theme.json)
|
|
851
|
+
|
|
852
|
+
Describe your brand once; every component follows.
|
|
853
|
+
|
|
854
|
+
```json
|
|
855
|
+
{
|
|
856
|
+
"$schema": "./node_modules/@astherix/ui/ui.theme.schema.json",
|
|
857
|
+
"colors": { "primary": "#047857", "secondary": "#0e7490", "tertiary": "#b45309" },
|
|
858
|
+
"fonts": { "sans": "\"DM Sans\", ui-sans-serif, system-ui, sans-serif", "googleFonts": ["DM Sans:wght@400..700"] },
|
|
859
|
+
"radius": "sm",
|
|
860
|
+
"density": "compact",
|
|
861
|
+
"shadows": "subtle"
|
|
862
|
+
}
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
Settings: `colors` (`primary` `secondary` `tertiary` `success` `warning` `danger` `info` as a hex or
|
|
866
|
+
`{ light, dark, foreground, darkForeground }`; `background` `surface` `foreground` `muted` `border`),
|
|
867
|
+
`fonts` (`sans` `heading` `mono` `googleFonts`), `radius` (`none` `sm` `md` `lg` or exact values),
|
|
868
|
+
`density` (`compact` `comfortable` `spacious` or a multiplier), `shadows` (`none` `subtle` `default`
|
|
869
|
+
`strong`), `baseFontSize`. Hover, pressed-edge, readable text and dark-mode shades are derived
|
|
870
|
+
from each color.
|
|
871
|
+
|
|
872
|
+
**Build time** (recommended):
|
|
873
|
+
|
|
874
|
+
```bash
|
|
875
|
+
npx jm-ui init # starter ui.theme.json
|
|
876
|
+
npx jm-ui theme ui.theme.json --out resources/css/ui-theme.css [--watch]
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
```css
|
|
880
|
+
@import "tailwindcss";
|
|
881
|
+
@import "@astherix/ui/theme.css";
|
|
882
|
+
@import "./ui-theme.css";
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
**Runtime**: `<ThemeProvider theme={json} selector?>` (e.g. a theme per customer).
|
|
886
|
+
**From code**: `import { createThemeCss, validateTheme, defineTheme } from "@astherix/ui/theme"`.
|
|
887
|
+
|
|
888
|
+
## Theming
|
|
889
|
+
|
|
890
|
+
Components only use semantic variables (`--ui-primary`, `--ui-border`, …). Override them in
|
|
891
|
+
your app to re-brand without rebuilding:
|
|
892
|
+
|
|
893
|
+
```css
|
|
894
|
+
:root { --ui-primary: #6d28d9; --ui-primary-hover: #5b21b6; --ui-primary-edge: #3b0764; }
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
## Animated components
|
|
898
|
+
|
|
899
|
+
### Skeleton
|
|
900
|
+
|
|
901
|
+
```tsx
|
|
902
|
+
<SkeletonSwap loading={isLoading} skeleton={<SkeletonCard media footer />} label="Loading the invoice">
|
|
903
|
+
<Invoice … />
|
|
904
|
+
</SkeletonSwap>
|
|
905
|
+
|
|
906
|
+
<SkeletonList rows={5} /> <SkeletonTable rows={5} columns={4} /> <SkeletonChart variant="bars" />
|
|
907
|
+
<SkeletonText lines={3} /> <SkeletonAvatar size="lg" /> <SkeletonButton /> <SkeletonImage ratio="4/3" />
|
|
908
|
+
<Skeleton width={140} height={28} radius="999px" animation="pulse" delay={120} />
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
`Skeleton`: `shape` (`rect` `circle` `text` `pill` `line`), `lines`, `lastLineWidth`, `width`,
|
|
912
|
+
`height`, `radius`, `animation` (`shimmer` `pulse` `none`), `delay` (stagger). Presets ripple down
|
|
913
|
+
stacks of rows. `SkeletonSwap`: `loading`, `skeleton`, `label`, `delay` (180ms before showing —
|
|
914
|
+
quick loads never flash one), `minDuration` (500ms minimum once shown), `fade`;
|
|
915
|
+
`useDelayedLoading(loading, { delay, minDuration })` for your own markup. Decorative shapes are
|
|
916
|
+
hidden from screen readers; reduced motion turns the shimmer into a slow fade.
|
|
917
|
+
|
|
918
|
+
### SpotlightText
|
|
919
|
+
|
|
920
|
+
Text lit by a moving spotlight — the lit part glows, the rest sits in shadow.
|
|
921
|
+
|
|
922
|
+
```tsx
|
|
923
|
+
<SpotlightText as="h1" className="text-5xl font-semibold">Get paid faster.</SpotlightText>
|
|
924
|
+
<SpotlightText mode="sweep" tint="linear-gradient(90deg, #fde68a, #f59e0b)">★ Most popular</SpotlightText>
|
|
925
|
+
<SpotlightText mode="reveal" radius={90}>Psst — use code EARLYBIRD.</SpotlightText>
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
`mode` (`follow` — follows the pointer or finger, drifts when idle; `sweep` — a beam passes every
|
|
929
|
+
few seconds; `reveal` — dark until lit), `speed` (`slow` `normal` `fast` or ms per pass; `fast`
|
|
930
|
+
suits status messages like "Syncing…"), `as`, `tint` (color or gradient; default the text's own
|
|
931
|
+
color), `radius`, `dim`, `glow`, `smoothing`, `duration` / `pause` (exact sweep pace). Real, selectable text for
|
|
932
|
+
screen readers; pauses off-screen; fully lit and still with reduced motion.
|
|
933
|
+
|