gbs-add-block 2.0.1 → 2.0.2
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/.gbs/skills/gbs-components/SKILL.md +132 -0
- package/.gbs/skills/gbs-components/references/accessibility.md +102 -0
- package/.gbs/skills/gbs-components/references/data-display.md +185 -0
- package/.gbs/skills/gbs-components/references/data-grid.md +117 -0
- package/.gbs/skills/gbs-components/references/forms.md +202 -0
- package/.gbs/skills/gbs-components/references/install.md +190 -0
- package/.gbs/skills/gbs-components/references/overlays.md +209 -0
- package/.gbs/skills/gbs-components/references/pickers.md +83 -0
- package/.gbs/skills/gbs-components/references/styling.md +180 -0
- package/.gbs/skills/gbs-components/references/toaster.md +58 -0
- package/README.md +42 -19
- package/index.cjs +1019 -665
- package/package.json +3 -2
- package/source/beta-components/data-grid/README.md +4 -3
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gbs-components
|
|
3
|
+
description: Builds UI with the GBS headless component library (gbs-add-block). Use when adding or changing UI in apps that have a component-lib/ folder.
|
|
4
|
+
autoAttach: ["src/**/*.tsx", "src/**/*.jsx", "app/**/*.tsx", "components/**/*.tsx"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# GBS components (2.0 beta)
|
|
8
|
+
|
|
9
|
+
## Install before you import
|
|
10
|
+
|
|
11
|
+
Not an npm dependency — the CLI **copies source into the repo**:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx gbs-add-block -a Button,Input,Modal --beta
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Writes `component-lib/<folder>/` plus `component-lib/shared/`. Always pass
|
|
18
|
+
`--beta`. Import the folder barrel **and its stylesheet** (or the repo's alias):
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { Button } from "component-lib/button";
|
|
22
|
+
import "component-lib/button/styles.css";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Folder = lowercased name, except `data-grid`, `date-picker`, `file-uploader`,
|
|
26
|
+
`number-input`, `radio-group`.
|
|
27
|
+
|
|
28
|
+
## Rules most often missed
|
|
29
|
+
|
|
30
|
+
1. **Change handlers are named per component.** `onValueChange`: Input,
|
|
31
|
+
OtpInput, Textarea, NumberInput, CheckboxGroup, RadioGroup, Tabs, Accordion,
|
|
32
|
+
MenuRadioGroup. `onCheckedChange`: Checkbox, Switch, MenuCheckboxItem.
|
|
33
|
+
`onChange`: Select, MultiSelect, DatePicker, DateRangePicker, FileUploader.
|
|
34
|
+
`onOpenChange`: Modal, Popover, Menu.
|
|
35
|
+
2. **Never hand-roll `<label>`, hint or error markup.** Fields take `label`,
|
|
36
|
+
`description` and `error`; `error` also marks the control invalid and wires
|
|
37
|
+
`aria-describedby`. A `<label>` wrapper breaks it.
|
|
38
|
+
3. **Style via `classNames` slots** (`classNames={{ root, label }}`);
|
|
39
|
+
`className` hits the root only. Theme globally with `--gbs-*` on `:root`.
|
|
40
|
+
4. **Mount `<Toaster />` and `<DialogHost />` once at the app root**, or
|
|
41
|
+
`toast()` and `dialog.confirm()` do nothing.
|
|
42
|
+
5. **All are client components** (`"use client"`); a Next.js Server Component
|
|
43
|
+
can render them but not pass function props.
|
|
44
|
+
6. **Compound families throw outside their parent**: Tab/TabList/TabPanel need
|
|
45
|
+
Tabs, AccordionItem needs Accordion, MenuItem needs Menu.
|
|
46
|
+
|
|
47
|
+
## Inventory (required props in parens)
|
|
48
|
+
|
|
49
|
+
**Forms** — Input, Textarea, NumberInput (locale-aware), OtpInput; Checkbox +
|
|
50
|
+
CheckboxGroup; Radio (`value`) + RadioGroup; Switch (applies immediately);
|
|
51
|
+
Select and MultiSelect (`options`; searchable, client or server); DatePicker and
|
|
52
|
+
DateRangePicker; FileUploader (chunked).
|
|
53
|
+
|
|
54
|
+
**Actions** — Button: variants, sizes, icons, auto-loading from a returned
|
|
55
|
+
promise; `render` draws a router link.
|
|
56
|
+
|
|
57
|
+
**Overlays** — Modal (native `<dialog>`, also drawers); `dialog` + DialogHost
|
|
58
|
+
(`await dialog.confirm/alert/prompt`); Popover (`trigger`); Menu (`trigger`) with
|
|
59
|
+
MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuGroup,
|
|
60
|
+
MenuSeparator, MenuSub; Tooltip (`content`); `toast` + Toaster.
|
|
61
|
+
|
|
62
|
+
**Data** — DataGrid (`data`, `columns`) + createColumnHelper: virtualized;
|
|
63
|
+
sort, filter, edit, CSV/Excel/PDF export.
|
|
64
|
+
|
|
65
|
+
**Display** — Tabs/TabList/Tab (`value`)/TabPanel (`value`); Accordion +
|
|
66
|
+
AccordionItem (`value`); Card/CardHeader/CardBody/CardFooter/Stat; Alert; Badge
|
|
67
|
+
and Tag; Avatar and AvatarGroup; Progress and CircularProgress (`value={null}`
|
|
68
|
+
is indeterminate); Skeleton and Empty; Spinner; Breadcrumb (`items`,
|
|
69
|
+
`renderLink`).
|
|
70
|
+
|
|
71
|
+
## Example
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
"use client";
|
|
75
|
+
import { useState } from "react";
|
|
76
|
+
import { Button } from "component-lib/button";
|
|
77
|
+
import { Input } from "component-lib/input";
|
|
78
|
+
import { Select } from "component-lib/combobox";
|
|
79
|
+
import { Modal } from "component-lib/modal";
|
|
80
|
+
import { toast } from "component-lib/toaster";
|
|
81
|
+
// plus each styles.css, once
|
|
82
|
+
|
|
83
|
+
const ROLES = [{ value: "admin", label: "Admin" }, { value: "dev", label: "Dev" }];
|
|
84
|
+
|
|
85
|
+
export function InviteButton() {
|
|
86
|
+
const [open, setOpen] = useState(false);
|
|
87
|
+
const [email, setEmail] = useState("");
|
|
88
|
+
const [role, setRole] = useState<string | null>(null);
|
|
89
|
+
const invalid = !!email && !email.includes("@");
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<Button onClick={() => setOpen(true)}>Invite</Button>
|
|
94
|
+
<Modal open={open} onOpenChange={setOpen} title="Invite a teammate"
|
|
95
|
+
footer={({ close }) => (
|
|
96
|
+
<Button disabled={!email || invalid} onClick={async () => {
|
|
97
|
+
await invite({ email, role });
|
|
98
|
+
toast.success("Invitation sent");
|
|
99
|
+
close();
|
|
100
|
+
}}>Send</Button>
|
|
101
|
+
)}>
|
|
102
|
+
<Input label="Email" type="email" value={email} onValueChange={setEmail}
|
|
103
|
+
required error={invalid ? "Enter a valid email" : undefined} />
|
|
104
|
+
<Select label="Role" options={ROLES} value={role} onChange={setRole} />
|
|
105
|
+
</Modal>
|
|
106
|
+
</>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Don't
|
|
112
|
+
|
|
113
|
+
- Raw `<button>`, `<input>`, `<select>`, `<table>`, `<dialog>`, or a hand-built
|
|
114
|
+
modal/menu/tooltip, when a component exists.
|
|
115
|
+
- Deep imports (`.../input/react/Input`); import the barrel. Only `<name>/core`
|
|
116
|
+
is also public.
|
|
117
|
+
- Editing `component-lib/shared/`; the CLI replaces it on update.
|
|
118
|
+
- Invented props: no `asChild`, no `variant` on Input, no `onChange` on Switch.
|
|
119
|
+
- `!important` or internal class selectors; set `--gbs-*` or pass `classNames`.
|
|
120
|
+
- A Tooltip as a name; icon buttons need `aria-label`.
|
|
121
|
+
|
|
122
|
+
## Details (in `references/`)
|
|
123
|
+
|
|
124
|
+
- `install.md` — CLI flags, folders, `shared/`, 1.x vs beta.
|
|
125
|
+
- `forms.md` — text and choice fields, controlled state, form posting.
|
|
126
|
+
- `pickers.md` — Select, MultiSelect, DatePicker, FileUploader.
|
|
127
|
+
- `overlays.md` — Modal, dialog, Popover, Menu, Tooltip.
|
|
128
|
+
- `toaster.md` — `toast()` and `<Toaster />`.
|
|
129
|
+
- `data-grid.md` — columns, API, export.
|
|
130
|
+
- `data-display.md` — Card, Tabs, Accordion, Badge, Avatar, Progress.
|
|
131
|
+
- `styling.md` — tokens, layers, Tailwind, slots, `data-*`, dark.
|
|
132
|
+
- `accessibility.md` — what is supplied, what you add.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Accessibility contract
|
|
2
|
+
|
|
3
|
+
Each component supplies its own roles, states and keyboard behaviour. A short
|
|
4
|
+
list of things stays yours — mostly names and headings, which only the page
|
|
5
|
+
knows. Do not duplicate what is already supplied: a second `aria-describedby`
|
|
6
|
+
or your own `<label htmlFor>` will fight the built-in wiring.
|
|
7
|
+
|
|
8
|
+
## What the components supply
|
|
9
|
+
|
|
10
|
+
| Area | Supplied |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| **Form fields** | A real native control; `<label>` linked to it via a generated `useId`; `aria-describedby` assembled from the hint, description and error (`{id}-description`, `{id}-error`); `aria-invalid` when `error` is set; `aria-required`. |
|
|
13
|
+
| **CheckboxGroup / RadioGroup** | `role="group"` / `role="radiogroup"` with the `label` as the legend, one shared `name`, one tab stop, arrow-key roving. |
|
|
14
|
+
| **Switch** | `role="switch"` on a real checkbox, so it is announced "on"/"off"; Space toggles, → on and ← off (mirrored in RTL). |
|
|
15
|
+
| **NumberInput** | `role="spinbutton"` with `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, `aria-valuetext`. |
|
|
16
|
+
| **Select / MultiSelect** | `role="combobox"` owning a `role="listbox"`; `aria-expanded`, `aria-controls`, `aria-activedescendant` so focus stays in the search box; `aria-selected` per option. |
|
|
17
|
+
| **Modal** | Native `<dialog>`: top layer, the page behind inert, focus trap, focus return on close. `title` names it. |
|
|
18
|
+
| **Popover** | `aria-expanded` and `aria-controls` on the trigger; `title` names the panel; light dismiss and Escape from the browser; focus returns to the trigger. |
|
|
19
|
+
| **Menu** | The WAI-ARIA menu button pattern: `aria-haspopup`, `aria-expanded`, `role="menu"` / `menuitem`, arrow keys, typeahead, Home/End, submenu arrows, Escape. |
|
|
20
|
+
| **Tooltip** | `aria-describedby` on the child; Escape dismisses from anywhere. |
|
|
21
|
+
| **Toaster** | A labelled landmark, polite live region; error toasts use `role="alert"`; **Alt+T** moves focus to the stack. |
|
|
22
|
+
| **Alert** | `role="alert"` for `danger` / `warning`, `role="status"` otherwise. |
|
|
23
|
+
| **Progress** | `role="progressbar"` with `aria-valuenow` / `min` / `max`, omitted entirely when `value={null}` so it reads as busy. |
|
|
24
|
+
| **Tabs** | `role="tablist"` / `tab` / `tabpanel`, `aria-controls`, arrow keys (mirrored in RTL), Home/End, disabled tabs skipped. |
|
|
25
|
+
| **Accordion** | Native `<details>`/`<summary>`: focusable header, Enter and Space, panel out of the accessibility tree while closed. |
|
|
26
|
+
| **DataGrid** | The WAI-ARIA grid pattern: `role="row"` / `rowgroup`, `aria-colindex`, `aria-rowindex`, `aria-sort` on headers, `aria-multiselectable`, full cell keyboard navigation. |
|
|
27
|
+
| **Skeleton** | Bars are `aria-hidden` — a screen reader gains nothing from a grey box. |
|
|
28
|
+
| **Icons** | Decorative icons inside components are `aria-hidden`. |
|
|
29
|
+
|
|
30
|
+
## What you must still provide
|
|
31
|
+
|
|
32
|
+
1. **A `label` on every field.** It is the accessible name. Without it the
|
|
33
|
+
control is unnamed, whatever the placeholder says.
|
|
34
|
+
|
|
35
|
+
2. **`aria-label` on icon-only buttons.**
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<Button variant="ghost" icon={<TrashIcon />} aria-label="Delete" />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A Tooltip does **not** supply this: it is wired with `aria-describedby`, so
|
|
42
|
+
the control keeps its own name. An icon button inside a Tooltip still needs
|
|
43
|
+
`aria-label`.
|
|
44
|
+
|
|
45
|
+
3. **A name for every overlay.** `Modal` needs `title`, or `aria-label` when it
|
|
46
|
+
has no visible heading. `Popover` needs `title` or `aria-label` (it defaults
|
|
47
|
+
to "More information", which says nothing). `Menu` needs `label` (defaults to
|
|
48
|
+
"Menu").
|
|
49
|
+
|
|
50
|
+
4. **`aria-label` on `TabList`** — `<TabList aria-label="Project">` — and on
|
|
51
|
+
`DataGrid` where the grid's purpose is not obvious from the page.
|
|
52
|
+
|
|
53
|
+
5. **Your own heading levels.** `CardHeader` takes a `title` node, not a level:
|
|
54
|
+
pass `<h3>Revenue</h3>` so the page owns the document outline.
|
|
55
|
+
|
|
56
|
+
6. **A name for a set of avatars.** `<AvatarGroup label="Assigned to">` — a row
|
|
57
|
+
of faces with no name says nothing.
|
|
58
|
+
|
|
59
|
+
7. **`alt` semantics for Avatar** come from `name`. Set `decorative` when the
|
|
60
|
+
person's name is already printed next to it, so it is not announced twice.
|
|
61
|
+
|
|
62
|
+
8. **An accessible route to anything that only appears on hover.** Tooltips are
|
|
63
|
+
never shown on touch devices and never take focus.
|
|
64
|
+
|
|
65
|
+
9. **Keyboard bindings for `shortcut` hints.** `MenuItem shortcut="⌘E"` only
|
|
66
|
+
draws the hint; binding the key is yours.
|
|
67
|
+
|
|
68
|
+
10. **A visible label for `Stat`** — it takes `label` as a required prop for
|
|
69
|
+
that reason; a figure alone means nothing.
|
|
70
|
+
|
|
71
|
+
## Errors and validation
|
|
72
|
+
|
|
73
|
+
Pass the message as `error`. The component sets `aria-invalid`, renders the
|
|
74
|
+
message with an `{id}-error` id and appends that id to `aria-describedby`.
|
|
75
|
+
Do not add `aria-invalid` or `aria-describedby` yourself.
|
|
76
|
+
|
|
77
|
+
Order matters and is handled for you: any `aria-describedby` you pass is kept
|
|
78
|
+
first, then the hint, then the description, then the error — the error last
|
|
79
|
+
because it is the part that changes and the part a listener is waiting for.
|
|
80
|
+
|
|
81
|
+
## Live regions
|
|
82
|
+
|
|
83
|
+
- `Alert` announces only when its contents change. An alert rendered on first
|
|
84
|
+
paint says nothing — use a toast for something that just happened.
|
|
85
|
+
- `Toaster` is always in the page as a polite region, so new toasts are read
|
|
86
|
+
without interrupting; `type: "error"` interrupts.
|
|
87
|
+
- `Spinner` and `Skeleton` both take a `label`. Use one of them, or mark the
|
|
88
|
+
region busy — not both, or the wait is announced twice.
|
|
89
|
+
|
|
90
|
+
## Right-to-left
|
|
91
|
+
|
|
92
|
+
Stylesheets use logical properties (`padding-inline`, `margin-inline`,
|
|
93
|
+
`inset-inline`, `border-inline`, `text-align: start`), so layout follows the
|
|
94
|
+
document's `dir` with no configuration. Arrow-key behaviour mirrors in Menu,
|
|
95
|
+
Switch, Tabs and the DataGrid. `Toaster` accepts an explicit `dir` prop
|
|
96
|
+
(`ltr` `rtl` `auto`) for a region that must differ from the page.
|
|
97
|
+
|
|
98
|
+
## Reduced motion
|
|
99
|
+
|
|
100
|
+
Most stylesheets (21 of 27) disable their animations under
|
|
101
|
+
`@media (prefers-reduced-motion: reduce)`. Do not add motion of your own that
|
|
102
|
+
ignores it.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Data display
|
|
2
|
+
|
|
3
|
+
The DataGrid has its own file: `data-grid.md`.
|
|
4
|
+
|
|
5
|
+
## Tabs
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Tabs defaultValue="overview">
|
|
9
|
+
<TabList aria-label="Project">
|
|
10
|
+
<Tab value="overview">Overview</Tab>
|
|
11
|
+
<Tab value="activity" badge={3}>Activity</Tab>
|
|
12
|
+
<Tab value="billing" disabled>Billing</Tab>
|
|
13
|
+
</TabList>
|
|
14
|
+
<TabPanel value="overview">…</TabPanel>
|
|
15
|
+
<TabPanel value="activity">…</TabPanel>
|
|
16
|
+
</Tabs>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Tabs:** `value`/`defaultValue`/`onValueChange`, `orientation` (`horizontal`
|
|
20
|
+
`vertical`), `activation` (`automatic` `manual`), `variant` (`line` `pills`
|
|
21
|
+
`enclosed`), `size`, `keepMounted`.
|
|
22
|
+
**Tab:** `value` (required), `disabled`, `icon`, `badge`.
|
|
23
|
+
**TabPanel:** `value` (required), `keepMounted`.
|
|
24
|
+
|
|
25
|
+
`TabList`, `Tab` and `TabPanel` throw outside `<Tabs>`. Arrow keys move between
|
|
26
|
+
tabs (swapped in RTL), Home/End jump, disabled tabs are skipped. `keepMounted`
|
|
27
|
+
preserves hidden panels' state using React's `<Activity>`.
|
|
28
|
+
|
|
29
|
+
## Accordion
|
|
30
|
+
|
|
31
|
+
Built on native `<details>`/`<summary>`.
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<Accordion defaultValue={["shipping"]} items={[
|
|
35
|
+
{ value: "shipping", title: "Shipping", content: <p>Two to five days.</p> },
|
|
36
|
+
{ value: "returns", title: "Returns", content: <p>Thirty days.</p> },
|
|
37
|
+
]} />
|
|
38
|
+
|
|
39
|
+
<Accordion multiple variant="contained">
|
|
40
|
+
<AccordionItem value="one" title="Details" meta={<Badge count={3} />}>…</AccordionItem>
|
|
41
|
+
</Accordion>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Accordion:** `value`/`defaultValue`/`onValueChange` (string arrays), `items`
|
|
45
|
+
**or** `<AccordionItem>` children, `multiple`, `collapsible`, `variant`
|
|
46
|
+
(`separated` `contained` `plain`), `size`, `iconPosition`, `classNames`, `ref`.
|
|
47
|
+
**AccordionItem:** `value` (required), `title`, `description`, `children`,
|
|
48
|
+
`meta`, `icon`, `disabled`, `classNames` (`root` `header` `title` `description`
|
|
49
|
+
`icon` `content` `body`), `ref` (the `<details>`).
|
|
50
|
+
|
|
51
|
+
`AccordionItem` throws outside `<Accordion>`.
|
|
52
|
+
|
|
53
|
+
## Card and Stat
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<Card>
|
|
57
|
+
<CardHeader title={<h3>Revenue</h3>} actions={<Menu trigger={…}>…</Menu>} />
|
|
58
|
+
<CardBody>
|
|
59
|
+
<Stat label="This month" value="£48,120"
|
|
60
|
+
trend={{ direction: trendDirection(12.4), label: "12.4%", description: "vs last month" }} />
|
|
61
|
+
</CardBody>
|
|
62
|
+
</Card>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Card:** `variant` (`outlined` `elevated` `plain`), `padding` (`none` `sm` `md`
|
|
66
|
+
`lg`), `href`/`target`/`rel` (renders an `<a>`), `interactive`, `className`,
|
|
67
|
+
`classNames` (`root` `header` `title` `description` `actions` `body` `footer`),
|
|
68
|
+
`style`.
|
|
69
|
+
**CardHeader:** `title`, `description`, `actions`. Pass your own heading element
|
|
70
|
+
as `title` so the page owns the outline level.
|
|
71
|
+
**Stat:** `label` and `value` (both required), `trend`, `help`, `icon`,
|
|
72
|
+
`loading`, plus styling props.
|
|
73
|
+
|
|
74
|
+
With `href` the card is an `<a>` — keep other links and buttons out of it; use
|
|
75
|
+
`interactive` plus your own handler instead.
|
|
76
|
+
|
|
77
|
+
## Alert
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<Alert variant="warning" title="Your trial ends in 3 days">
|
|
81
|
+
After that the workspace becomes read-only.
|
|
82
|
+
</Alert>
|
|
83
|
+
<Alert variant="danger" title="We could not save your changes" onDismiss={hide} />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`variant` (`info` `success` `warning` `danger` `neutral`), `size` (`sm` `md`),
|
|
87
|
+
`title`, `description` or `children`, `icon` (a node, or `false`), `actions`,
|
|
88
|
+
`onDismiss`, `classNames` (`root` `icon` `content` `title` `description`
|
|
89
|
+
`actions` `dismiss`), `localeText`, `ref`, and every `<div>` attribute.
|
|
90
|
+
|
|
91
|
+
Problems get `role="alert"` and interrupt; everything else gets `role="status"`.
|
|
92
|
+
An alert present from first render announces nothing — live regions only speak
|
|
93
|
+
on change.
|
|
94
|
+
|
|
95
|
+
## Badge and Tag
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<Badge variant="success">Active</Badge>
|
|
99
|
+
<Badge variant="danger" appearance="solid" count={128} />
|
|
100
|
+
<Tag onRemove={() => remove("berlin")}>Berlin</Tag>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Badge:** `children` or `count`, `variant` (`neutral` `accent` `success`
|
|
104
|
+
`warning` `danger` `info`), `appearance` (`soft` `solid` `outline`), `size`
|
|
105
|
+
(`sm` `md`), `max`, `showZero`, `formatValue`, `dot`, `icon`, `classNames`
|
|
106
|
+
(`root` `dot` `icon` `label`), `ref`, and every `<span>` attribute.
|
|
107
|
+
**Tag:** the same variants plus `onRemove`, `label` (names the remove button when
|
|
108
|
+
the children are not a plain string), `disabled`, `classNames` (`root` `icon`
|
|
109
|
+
`label` `remove`), `localeText`.
|
|
110
|
+
|
|
111
|
+
A `count` of zero renders nothing unless `showZero`.
|
|
112
|
+
|
|
113
|
+
## Avatar and AvatarGroup
|
|
114
|
+
|
|
115
|
+
**Avatar:** `name` (alt text, initials and a stable colour), `src`, `initials`,
|
|
116
|
+
`children`, `size` (`md`, 32px), `shape` (`circle`), `status`, `decorative`,
|
|
117
|
+
`className`, `classNames`, `style`, `localeText`, `ref`, and every `<span>`
|
|
118
|
+
attribute. A failed image falls back to initials.
|
|
119
|
+
**AvatarGroup:** `children` (required), `max` (4, counting the overflow bubble),
|
|
120
|
+
`size`, `shape`, `label` (names the set), plus the usual styling props.
|
|
121
|
+
|
|
122
|
+
## Progress and CircularProgress
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<Progress label="Uploading report.pdf" value={62} showValue />
|
|
126
|
+
<Progress value={null} label="Preparing export" />
|
|
127
|
+
<CircularProgress value={62} size={56} showValue />
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Progress:** `value` (`number | null`), `max`, `label`, `showValue`,
|
|
131
|
+
`valueText`, `variant` (`accent` `success` `warning` `danger`), `size` (`sm`
|
|
132
|
+
`md` `lg`), `classNames` (`root` `header` `label` `value` `track` `bar`),
|
|
133
|
+
`localeText`, `ref`, and every `<div>` attribute.
|
|
134
|
+
**CircularProgress:** the same value props plus `size` (pixels), `thickness`,
|
|
135
|
+
`showValue` or `children` for the middle.
|
|
136
|
+
|
|
137
|
+
`value={null}` is indeterminate: `aria-valuenow` is omitted, so a screen reader
|
|
138
|
+
says "busy". Use a Spinner when there is nothing to measure at all.
|
|
139
|
+
|
|
140
|
+
## Skeleton and Empty
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
{loading ? <Skeleton lines={3} label="Loading activity" />
|
|
144
|
+
: items.length === 0 ? <Empty title="No activity yet" description="…" actions={<Button…/>} />
|
|
145
|
+
: <ActivityList items={items} />}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Skeleton:** `variant` (`text` `circle` `rect`), `lines` (1),
|
|
149
|
+
`lastLineWidth` (60), `width`, `height`, `radius` (number = pixels, string used
|
|
150
|
+
as given), `animation` (`pulse` `wave` `none`), `label`, `className`,
|
|
151
|
+
`classNames` (`root` `line`), `style`. Bars are hidden from assistive tech;
|
|
152
|
+
`label` is what gets announced.
|
|
153
|
+
**Empty:** `title`, `description`, `actions`.
|
|
154
|
+
|
|
155
|
+
## Spinner
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<Spinner />
|
|
159
|
+
<Spinner size="sm" showLabel label="Saving…" />
|
|
160
|
+
<Spinner loading={isPending} delay={250} minDuration={600}><Chart /></Spinner>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`loading` (true; with children, covers them), `size` (`xs` 12 · `sm` 16 · `md`
|
|
164
|
+
24 · `lg` 32 · `xl` 48, or a pixel number), `variant` (`ring` `dots`), `label`
|
|
165
|
+
("Loading"), `showLabel`, `delay` (0), `minDuration` (0), `className`,
|
|
166
|
+
`classNames` (`root` `status` `indicator` `label` `content` `overlay`), `style`,
|
|
167
|
+
`localeText`. `useDelayedLoading(loading, { delay, minDuration })` applies the
|
|
168
|
+
same timing to any loading UI.
|
|
169
|
+
|
|
170
|
+
## Breadcrumb
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
<Breadcrumb
|
|
174
|
+
items={[{ label: "Home", href: "/" }, { label: "Invoices", href: "/invoices" },
|
|
175
|
+
{ label: "INV-2026-0042" }]}
|
|
176
|
+
maxItems={4}
|
|
177
|
+
renderLink={(props) => <Link {...props} />}
|
|
178
|
+
structuredData={{ baseUrl: "https://app.example.com" }} />
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`items` (required; `{ label, href?, icon?, name?, onClick? }`, top level first,
|
|
182
|
+
last is the current page), `separator`, `maxItems`, `itemsBeforeCollapse` (1),
|
|
183
|
+
`itemsAfterCollapse` (1), `renderLink`, `structuredData`, `size` (`md`),
|
|
184
|
+
`className`, `classNames` (`root` `list` `item` `link` `current` `separator`
|
|
185
|
+
`ellipsis`), `localeText`. Use `renderLink` for a router link — no `asChild`.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# DataGrid
|
|
2
|
+
|
|
3
|
+
A virtualized grid. Import `DataGrid` and `createColumnHelper` from
|
|
4
|
+
`component-lib/data-grid` and the stylesheet once.
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import { createColumnHelper, DataGrid } from "component-lib/data-grid";
|
|
8
|
+
import "component-lib/data-grid/styles.css";
|
|
9
|
+
|
|
10
|
+
interface Employee { id: number; name: string; salary: number; active: boolean }
|
|
11
|
+
|
|
12
|
+
const col = createColumnHelper<Employee>();
|
|
13
|
+
|
|
14
|
+
// Define columns at module level, or memoize them.
|
|
15
|
+
const columns = [
|
|
16
|
+
col.field("id", { header: "ID", type: "number", width: 80, pin: "left" }),
|
|
17
|
+
col.field("name", { width: 200 }),
|
|
18
|
+
col.field("salary", { type: "number", format: (v) => `$${v.toLocaleString()}` }),
|
|
19
|
+
col.field("active", { type: "boolean" }),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function Employees({ data }: { data: Employee[] }) {
|
|
23
|
+
return <DataGrid data={data} columns={columns} getRowId="id" enableRowSelection height={600} />;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Keep `data`, `columns` and `getRowId` stable.** The grid memoizes on their
|
|
28
|
+
identity. Define columns at module level or in `useMemo`, and pass `getRowId` as
|
|
29
|
+
a property name (`getRowId="id"`). With React Compiler on, inline values are
|
|
30
|
+
memoized for you.
|
|
31
|
+
|
|
32
|
+
## Props
|
|
33
|
+
|
|
34
|
+
From `GridOptions<T>`: `data` (required), `columns` (required), `getRowId`,
|
|
35
|
+
`mode` (`client` | `server`), `rowCount` (server mode), `state`, `initialState`,
|
|
36
|
+
`onStateChange`, `onQueryChange`, `enableRowSelection`, `enableMultiSort`,
|
|
37
|
+
`exportFileName`, and the feature toggles.
|
|
38
|
+
|
|
39
|
+
From `DataGridProps<T>`: `ref` (`Ref<GridApi<T>>`), `height` (`number | string`,
|
|
40
|
+
520; ignored with `autoHeight`), `autoHeight`, `rowHeight` (defaults to the
|
|
41
|
+
density: compact 32, standard 40, comfortable 52), `headerHeight`, `loading`,
|
|
42
|
+
`toolbar` (`boolean | ToolbarOptions` — search, columns, export, density),
|
|
43
|
+
`pageSizeOptions` (`[25, 50, 100, 250]`), `emptyState`, `getRowClassName`,
|
|
44
|
+
`locale` (BCP 47), `localeText`, `className`, `classNames`, `style`,
|
|
45
|
+
`aria-label`.
|
|
46
|
+
|
|
47
|
+
Slots: `root`, `toolbar`, `viewport`, `header`, `headerCell`, `row`, `cell`,
|
|
48
|
+
`pagination`.
|
|
49
|
+
|
|
50
|
+
## Columns
|
|
51
|
+
|
|
52
|
+
| Option | Purpose |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `field` / `accessor` / `id` | Where the value comes from. Display-only columns need just `id` and `cell`. |
|
|
55
|
+
| `header`, `width`, `minWidth`, `maxWidth`, `align` | Presentation. |
|
|
56
|
+
| `type` | `string` (default), `number`, `date`, `boolean`. Drives filter operators, sorting, alignment, editors, Excel cell types. |
|
|
57
|
+
| `options` | `{ label, value }[]` for enum columns: "is any of" filter, select editor, label display. |
|
|
58
|
+
| `format(value, row)` | Display text. Also used by search, CSV, PDF and copy. |
|
|
59
|
+
| `cell` | Custom renderer. |
|
|
60
|
+
| `editable`, `validate` | Inline editing with validation. |
|
|
61
|
+
| `pin` | `"left"` or `"right"`. |
|
|
62
|
+
| `exportable` | `false` keeps the column out of exports and copy. |
|
|
63
|
+
|
|
64
|
+
`createColumnHelper<T>()` infers the value type for `cell`, `format`, `validate`
|
|
65
|
+
and `sortFn`. Plain `ColumnDef<T>[]` objects also work.
|
|
66
|
+
|
|
67
|
+
## Imperative API
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
const api = useRef<GridApi<Employee>>(null);
|
|
71
|
+
<DataGrid ref={api} … />
|
|
72
|
+
|
|
73
|
+
api.current?.setFilter("status", { operator: "in", value: ["active"] });
|
|
74
|
+
api.current?.exportExcel({ scope: "selected", fileName: "people" });
|
|
75
|
+
api.current?.focusCell(0, "name");
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Methods: `getState`, `setState`, `toggleSort`, `setSorting`, `setFilter`,
|
|
79
|
+
`clearFilters`, `setGlobalFilter`, `setPageIndex`, `setPageSize`,
|
|
80
|
+
`toggleRowSelected`, `toggleAllRowsSelected`, `clearSelection`,
|
|
81
|
+
`getSelectedRowIds`, `getSelectedRows`, `setColumnVisibility`, `setColumnWidth`,
|
|
82
|
+
`pinColumn`, `moveColumn`, `resetColumns`, `scrollToRow`, `focusCell`,
|
|
83
|
+
`startEditing`, `cancelEditing`, `getRows`, `exportCsv`, `exportExcel`,
|
|
84
|
+
`exportPdf`, `print`, `copyToClipboard`.
|
|
85
|
+
|
|
86
|
+
## Export
|
|
87
|
+
|
|
88
|
+
Export code is split into chunks loaded on first use. Scopes: `filtered`
|
|
89
|
+
(default), `all`, `selected`, `page`, or pass `rows`.
|
|
90
|
+
|
|
91
|
+
- `exportCsv` — UTF-8 with BOM; cells starting with `= + - @` get a `'` prefix
|
|
92
|
+
against formula injection.
|
|
93
|
+
- `exportExcel` — real `.xlsx`: typed numbers, booleans and dates, bold frozen
|
|
94
|
+
header, auto-filter, column widths.
|
|
95
|
+
- `exportPdf` — writes a real `.pdf` and downloads it: paper size, orientation,
|
|
96
|
+
repeated header row, page numbers, your own header/footer bands.
|
|
97
|
+
- `print` — opens the browser's print dialog instead, for paper or for text the
|
|
98
|
+
PDF's Latin-1 fonts cannot encode.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
api.current?.exportPdf({
|
|
102
|
+
title: "Q3 headcount",
|
|
103
|
+
orientation: "landscape",
|
|
104
|
+
paperSize: "A4", // A3 A4 A5 letter legal
|
|
105
|
+
footer: { left: "Confidential", right: "Page {page} of {pages}" },
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Band tokens: `{title}`, `{page}`, `{pages}`, `{date}`, `{time}`. Also `margin`
|
|
110
|
+
(points), `fontSize`, `header`, and `theme` (`text` `muted` `border` `headerBg`
|
|
111
|
+
`headerText` `stripe`). PDF text is WinAnsi/Latin-1; other scripts become `?` —
|
|
112
|
+
use `print()` for those.
|
|
113
|
+
|
|
114
|
+
## Server mode
|
|
115
|
+
|
|
116
|
+
Set `mode="server"`, pass the current page as `data` and the total as
|
|
117
|
+
`rowCount`, and fetch in `onQueryChange`.
|