gbs-add-block 2.0.1 → 2.0.3
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 +134 -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 +116 -0
- package/.gbs/skills/gbs-components/references/forms.md +202 -0
- package/.gbs/skills/gbs-components/references/install.md +189 -0
- package/.gbs/skills/gbs-components/references/overlays.md +206 -0
- package/.gbs/skills/gbs-components/references/pickers.md +83 -0
- package/.gbs/skills/gbs-components/references/styling.md +182 -0
- package/.gbs/skills/gbs-components/references/toaster.md +57 -0
- package/README.md +46 -19
- package/index.cjs +1019 -665
- package/package.json +3 -2
- package/source/beta-components/data-grid/README.md +4 -3
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Forms
|
|
2
|
+
|
|
3
|
+
Every field is a real native control with the label, hint and error wired
|
|
4
|
+
around it. Pass `label`, `description` and `error` as props — never build that
|
|
5
|
+
markup yourself.
|
|
6
|
+
|
|
7
|
+
## The shared field contract
|
|
8
|
+
|
|
9
|
+
All of Input, Textarea, NumberInput, OtpInput, Checkbox, CheckboxGroup, Radio,
|
|
10
|
+
RadioGroup, Switch, Select, MultiSelect, DatePicker, DateRangePicker and
|
|
11
|
+
FileUploader accept:
|
|
12
|
+
|
|
13
|
+
- `label` (`ReactNode`) — the field's accessible name.
|
|
14
|
+
- `description` (`ReactNode`) — hint below the control, linked with `aria-describedby`.
|
|
15
|
+
- `error` (`ReactNode`) — message below the control; **also marks it invalid**.
|
|
16
|
+
- `size` — `"sm" | "md" | "lg"`, default `md`.
|
|
17
|
+
- `disabled`, `required` — native states.
|
|
18
|
+
- `classNames` — `Partial<Record<Slot, string>>`; slots differ per component.
|
|
19
|
+
- `localeText` — overrides the built-in UI strings.
|
|
20
|
+
- `className`, `style` — applied to the root element.
|
|
21
|
+
|
|
22
|
+
Ids come from `useId`; `describeField` (in `shared/core/field.ts`) builds the
|
|
23
|
+
describedby chain as `{id}-description` and `{id}-error`. Pass your own `id` to
|
|
24
|
+
override the generated one.
|
|
25
|
+
|
|
26
|
+
## Controlled vs uncontrolled
|
|
27
|
+
|
|
28
|
+
Every field supports both. Pass `value`/`checked` **with** its change handler
|
|
29
|
+
for controlled, or `defaultValue`/`defaultChecked` for uncontrolled. Mixing them
|
|
30
|
+
(a `value` with no handler) leaves the field frozen.
|
|
31
|
+
|
|
32
|
+
**The handler name differs per component:**
|
|
33
|
+
|
|
34
|
+
- `onValueChange(value: string)` — Input, OtpInput, Textarea
|
|
35
|
+
- `onValueChange(value: number | null)` — NumberInput
|
|
36
|
+
- `onValueChange(values: string[])` — CheckboxGroup
|
|
37
|
+
- `onValueChange(value: string | null)` — RadioGroup
|
|
38
|
+
- `onCheckedChange(checked: boolean)` — Checkbox
|
|
39
|
+
- `onCheckedChange(checked: boolean): void | Promise<unknown>` — Switch
|
|
40
|
+
- `onChange(value: V | null, option)` — Select
|
|
41
|
+
- `onChange(values: V[], options)` — MultiSelect
|
|
42
|
+
- `onChange(date: Date | null)` — DatePicker
|
|
43
|
+
- `onChange(range, complete: boolean)` — DateRangePicker
|
|
44
|
+
|
|
45
|
+
Input, Textarea and NumberInput also fire the native `onChange` alongside
|
|
46
|
+
`onValueChange`, so form libraries that hook the native event keep working.
|
|
47
|
+
|
|
48
|
+
## Native form posting
|
|
49
|
+
|
|
50
|
+
Give the field a `name` and it posts like a native control. `Select` and
|
|
51
|
+
`MultiSelect` render hidden inputs (one per value); FileUploader without an
|
|
52
|
+
`endpoint` posts the files with the form.
|
|
53
|
+
|
|
54
|
+
## Input
|
|
55
|
+
|
|
56
|
+
All `<input>` attributes, plus:
|
|
57
|
+
|
|
58
|
+
`value`, `defaultValue`, `onValueChange`, `label`, `description`, `error`,
|
|
59
|
+
`size`, `leading`, `trailing`, `clearable` (default `false`), `revealPassword`
|
|
60
|
+
(default `true`, for `type="password"`), `showCount`, `classNames`,
|
|
61
|
+
`localeText`, `ref` (the `<input>` element).
|
|
62
|
+
|
|
63
|
+
Slots: `root`, `label`, `control`, `input`, `description`, `error`, `count`.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<Input label="Email" type="email" value={email} onValueChange={setEmail} clearable />
|
|
67
|
+
<Input label="Weight" trailing="kg" showCount maxLength={6} />
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## OtpInput
|
|
71
|
+
|
|
72
|
+
One real `<input>` drawn as cells, so SMS autofill
|
|
73
|
+
(`autocomplete="one-time-code"`), paste and screen readers treat it as one field.
|
|
74
|
+
|
|
75
|
+
`length` (6), `mode` (`numeric` `alphanumeric` `alphabetic`), `uppercase`,
|
|
76
|
+
`value`, `defaultValue`, `onValueChange`, `onComplete`, `groups`, `mask`,
|
|
77
|
+
`label`, `description`, `error`, `size`, `name`, `required`, `disabled`,
|
|
78
|
+
`classNames` (`root` `label` `cells` `cell` `separator` `description` `error`),
|
|
79
|
+
`localeText`, `ref`.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<OtpInput label="Code" groups={[3, 3]} onComplete={verify} />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Textarea
|
|
86
|
+
|
|
87
|
+
All `<textarea>` attributes, plus `value`, `defaultValue`, `onValueChange`,
|
|
88
|
+
`label`, `description`, `error`, `size`, `autoResize`, `minRows`, `maxRows`,
|
|
89
|
+
`resize` (`none` `vertical` `horizontal` `both`), `showCount`, `localeText`,
|
|
90
|
+
`ref`, `classNames` (`root` `label` `textarea` `description` `error` `count`).
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
<Textarea label="Notes" value={notes} onValueChange={setNotes}
|
|
94
|
+
autoResize minRows={2} maxRows={8} maxLength={500} showCount />
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## NumberInput
|
|
98
|
+
|
|
99
|
+
A text input with `role="spinbutton"`, not `<input type="number">` — so the
|
|
100
|
+
wheel cannot silently edit it, a partially typed number is not lost, and
|
|
101
|
+
locale notation like `1.234,56` parses.
|
|
102
|
+
|
|
103
|
+
`value`/`defaultValue`/`onValueChange` (`number | null`), `min`, `max`, `step`,
|
|
104
|
+
`largeStep`, `snapToStep`, `decimals`, `locale`, `format` (`decimal` `currency`
|
|
105
|
+
`percent`), `currency`, `useGrouping`, `clampBehavior` (`blur` `strict` `none`),
|
|
106
|
+
`stepper`, `wheel`, `selectOnFocus`, `clearable`, `label`, `description`,
|
|
107
|
+
`error`, `size`, `leading`, `trailing`, `disabled`, `readOnly`, `required`,
|
|
108
|
+
`localeText`, `ref`, every `<input>` attribute, and `classNames` (`root`
|
|
109
|
+
`label` `control` `input` `stepper` `description` `error`).
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<NumberInput label="Quantity" min={1} max={99} value={qty} onValueChange={setQty} />
|
|
113
|
+
<NumberInput label="Amount" format="currency" currency="USD" decimals={2} locale="en-US" />
|
|
114
|
+
{/* 45% in the field is 0.45 in the value */}
|
|
115
|
+
<NumberInput label="Discount" format="percent" step={0.01} min={0} max={1} />
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Keyboard: ↑/↓ step, PageUp/PageDown by `largeStep`, Home/End jump to
|
|
119
|
+
`min`/`max`, Escape restores the last committed number.
|
|
120
|
+
|
|
121
|
+
## Checkbox and CheckboxGroup
|
|
122
|
+
|
|
123
|
+
The box is a real `<input type="checkbox">`.
|
|
124
|
+
|
|
125
|
+
**Checkbox:** `checked`/`defaultChecked` (`true`, `false` or `"indeterminate"`),
|
|
126
|
+
`onCheckedChange`, `value`, `label`, `description`, `error`, `size`, `ref`,
|
|
127
|
+
every `<input>` attribute, and `classNames` (`root` `control` `input` `label`
|
|
128
|
+
`description` `error`).
|
|
129
|
+
|
|
130
|
+
**CheckboxGroup:** `value`/`defaultValue`/`onValueChange` (string arrays),
|
|
131
|
+
`options` **or** `<Checkbox value>` children, `label` (a legend), `description`,
|
|
132
|
+
`error`, `required`, `disabled`, `name`, `size`, `orientation`, `selectAll`,
|
|
133
|
+
`localeText`, `classNames` (`root` `legend` `description` `items` `error`).
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<Checkbox label="Remember me" name="remember" checked={remember} onCheckedChange={setRemember} />
|
|
137
|
+
|
|
138
|
+
<CheckboxGroup label="Notify me by" name="channels" selectAll
|
|
139
|
+
options={[{ value: "email", label: "Email" }, { value: "sms", label: "SMS" }]}
|
|
140
|
+
value={channels} onValueChange={setChannels} />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Radio and RadioGroup
|
|
144
|
+
|
|
145
|
+
Real `<input type="radio">` elements sharing a `name`, so arrow keys roam the
|
|
146
|
+
group, the group is one tab stop, and the browser validates it as one required
|
|
147
|
+
field.
|
|
148
|
+
|
|
149
|
+
**RadioGroup:** `value`/`defaultValue`/`onValueChange` (`string | null`),
|
|
150
|
+
`options` (plain strings or full option objects) **or** `<Radio value>`
|
|
151
|
+
children, `label` (a legend), `description`, `error`, `required`, `disabled`,
|
|
152
|
+
`name`, `size`, `orientation`, `variant` (`default` `card`), `clearable`,
|
|
153
|
+
`localeText`, `classNames` (`root` `legend` `description` `items` `clear`
|
|
154
|
+
`error`).
|
|
155
|
+
|
|
156
|
+
**Radio:** `value` (required), `label`, `description`, `error`, `size`,
|
|
157
|
+
`disabled`, `classNames`, `ref`, every `<input>` attribute.
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
<RadioGroup label="Send the report" name="frequency"
|
|
161
|
+
options={["Daily", "Weekly", "Monthly"]} value={freq} onValueChange={setFreq} />
|
|
162
|
+
|
|
163
|
+
{/* variant="card" draws tiles, for choices that need explaining. */}
|
|
164
|
+
<RadioGroup label="Plan" variant="card" defaultValue="team" options={[
|
|
165
|
+
{ value: "starter", label: "Starter", description: "Up to 3 projects" },
|
|
166
|
+
{ value: "team", label: "Team", description: "Unlimited projects" },
|
|
167
|
+
]} />
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
A radio cannot be unselected by clicking, so an optional question needs
|
|
171
|
+
`clearable` or a "None" option.
|
|
172
|
+
|
|
173
|
+
## Switch
|
|
174
|
+
|
|
175
|
+
Use a Switch when flipping it **is** the action; use a Checkbox when the value
|
|
176
|
+
is submitted later with a form.
|
|
177
|
+
|
|
178
|
+
`checked`/`defaultChecked`/`onCheckedChange` (may return a promise), `value`,
|
|
179
|
+
`label`, `description`, `error`, `size`, `labelPosition` (`end` `start`),
|
|
180
|
+
`loading`, `disabled`, `readOnly`, `required`, `name`, `localeText`, `ref`,
|
|
181
|
+
every `<input>` attribute, and `classNames` (`root` `control` `input` `track`
|
|
182
|
+
`thumb` `label` `description` `error`).
|
|
183
|
+
|
|
184
|
+
Return a promise from `onCheckedChange` and the switch shows the new setting,
|
|
185
|
+
spins while it saves, and reverts if the save fails.
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
<Switch label="Two-factor authentication" checked={enabled}
|
|
189
|
+
onCheckedChange={(next) => api.setTwoFactor(next)} />
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Select, DatePicker, FileUploader
|
|
193
|
+
|
|
194
|
+
Those live in `pickers.md` — they are the fields that take `onChange` rather
|
|
195
|
+
than `onValueChange`.
|
|
196
|
+
|
|
197
|
+
## Validation
|
|
198
|
+
|
|
199
|
+
There is no form library here. Compute the message yourself and pass it as
|
|
200
|
+
`error`; the component handles `aria-invalid`, `aria-describedby` and the
|
|
201
|
+
styling. With React Hook Form or similar, wire `value` and the component's own
|
|
202
|
+
handler name, and pass `error={errors.field?.message}`.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Installing and updating GBS components
|
|
2
|
+
|
|
3
|
+
## The distribution model
|
|
4
|
+
|
|
5
|
+
`gbs-add-block` is **not a runtime dependency**. It is a CLI that copies
|
|
6
|
+
TypeScript source into the consuming repo, the way shadcn/ui does. Nothing is
|
|
7
|
+
imported from `node_modules` at runtime, and the copied files are yours to edit.
|
|
8
|
+
|
|
9
|
+
`package.json` of the library declares no `main` and no `exports` — only
|
|
10
|
+
`bin: { "gbs-add-block": "index.cjs" }` and `files: ["index.cjs", "source",
|
|
11
|
+
".gbs"]`. There is nothing to `import "gbs-add-block"` from.
|
|
12
|
+
|
|
13
|
+
Peer dependencies: `react@^19` and `react-dom@^19`. The beta components have no
|
|
14
|
+
other runtime dependencies.
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx gbs-add-block -a Button --beta # one component
|
|
20
|
+
npx gbs-add-block -a Button,Input,Modal --beta # several
|
|
21
|
+
npx gbs-add-block -i --beta # interactive picker
|
|
22
|
+
npx gbs-add-block -l --beta # list available
|
|
23
|
+
npx gbs-add-block -a Button --beta --force # overwrite edited shared/ files
|
|
24
|
+
npx gbs-add-block -skill # install this skill
|
|
25
|
+
npx gbs-add-block -skill --for claude # only the Claude Code copy
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Flag | Alias | Meaning |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `--add` | `-a` | Component, or comma-separated list. Case-insensitive. |
|
|
31
|
+
| `--interactive` | `-i` | Pick from a menu. |
|
|
32
|
+
| `--list` | `-l` | Print available components. |
|
|
33
|
+
| `--beta` | — | Install the 2.0 redesigned components. `-beta` also works. |
|
|
34
|
+
| `--skill` | — | Install this skill at the project root. `-skill` and `-a skill` also work. |
|
|
35
|
+
| `--for` | — | With `--skill`: which agents to write adapters for (`claude`, `codex`, `antigravity`, or `none`). Default: all. |
|
|
36
|
+
| `--force` | — | Replace files you have edited locally in `shared/` or `.gbs/`. |
|
|
37
|
+
|
|
38
|
+
**Always pass `--beta`.** Without it you get the 1.x set (`Select`, `SideBar`,
|
|
39
|
+
`FormRenderer`, `MaterialInput`, `ContextMenu`, `Navbar`, `Bargraph`,
|
|
40
|
+
`DarkMode`, `Toast`, `Uploader`, `UsePaginatedData`, `UseUploader` and others),
|
|
41
|
+
which is a different, older API with different props. The two sets are not
|
|
42
|
+
interchangeable.
|
|
43
|
+
|
|
44
|
+
## Installing this skill
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx gbs-add-block@latest -skill
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Writes to the project root — not into `component-lib/`. Each agent reads a
|
|
51
|
+
different location, so the CLI writes one copy per agent from the same source:
|
|
52
|
+
|
|
53
|
+
| Path | For |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `.gbs/skills/gbs-components/` | GBS SE Agent. Canonical; always written. |
|
|
56
|
+
| `.claude/skills/gbs-components/` | Claude Code. Full copy; `autoAttach` becomes `paths`. |
|
|
57
|
+
| `.agents/rules/gbs-components.md` | Antigravity. One file; set its glob in the IDE. |
|
|
58
|
+
| `AGENTS.md` | Codex. A pointer inside `gbs-add-block` markers. |
|
|
59
|
+
|
|
60
|
+
`--for claude,codex` narrows it; `--for none` writes only `.gbs/`. Switching
|
|
61
|
+
targets removes the adapters you dropped.
|
|
62
|
+
|
|
63
|
+
Re-running updates everything. A file edited since the CLI wrote it is never
|
|
64
|
+
replaced without `--force`, tracked by sha256 in `.gbs/.install-manifest.json`.
|
|
65
|
+
`AGENTS.md` is the exception: the project owns that file, so only the marked
|
|
66
|
+
block is rewritten.
|
|
67
|
+
|
|
68
|
+
It can be combined with a component install:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx gbs-add-block -a DataGrid -beta -skill
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Where files land
|
|
75
|
+
|
|
76
|
+
Always `<cwd>/component-lib/`. The destination is not configurable, so run the
|
|
77
|
+
CLI from the repo root (or move the folder afterwards and fix the imports).
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
component-lib/
|
|
81
|
+
shared/ installed with every beta component
|
|
82
|
+
button/
|
|
83
|
+
data-grid/
|
|
84
|
+
input/
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Folder names
|
|
88
|
+
|
|
89
|
+
The folder is the lowercased component name, with five exceptions:
|
|
90
|
+
|
|
91
|
+
| Component | Folder |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `DataGrid` | `data-grid` |
|
|
94
|
+
| `DatePicker` | `date-picker` |
|
|
95
|
+
| `FileUploader` | `file-uploader` |
|
|
96
|
+
| `NumberInput` | `number-input` |
|
|
97
|
+
| `RadioGroup` | `radio-group` |
|
|
98
|
+
|
|
99
|
+
Everything else: `Button` → `button`, `Combobox` → `combobox`, `Toaster` →
|
|
100
|
+
`toaster`, and so on.
|
|
101
|
+
|
|
102
|
+
Note the mismatch between the install name and the import name for two of them:
|
|
103
|
+
|
|
104
|
+
- Install `Combobox`; import `Select` and `MultiSelect` from `component-lib/combobox`.
|
|
105
|
+
- Install `Toaster`; import `toast` and `Toaster` from `component-lib/toaster`.
|
|
106
|
+
- Install `Dialog`; import `dialog` and `DialogHost` from `component-lib/dialog`.
|
|
107
|
+
|
|
108
|
+
## Available beta components
|
|
109
|
+
|
|
110
|
+
`DataGrid`, `Combobox`, `DatePicker`, `Toaster`, `FileUploader`, `Dialog`,
|
|
111
|
+
`Input`, `Modal`, `Textarea`, `Button`, `Breadcrumb`, `Checkbox`, `Tabs`,
|
|
112
|
+
`Spinner`, `Menu`, `Tooltip`, `Popover`, `Card`, `Skeleton`, `NumberInput`,
|
|
113
|
+
`RadioGroup`, `Switch`, `Accordion`, `Alert`, `Avatar`, `Badge`, `Progress`.
|
|
114
|
+
|
|
115
|
+
`Skeleton` also brings `Empty`; `Card` also brings `Stat`; `Badge` also brings
|
|
116
|
+
`Tag`; `Avatar` also brings `AvatarGroup`; `Input` also brings `OtpInput`;
|
|
117
|
+
`Progress` also brings `CircularProgress`.
|
|
118
|
+
|
|
119
|
+
## What each folder contains
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
component-lib/input/
|
|
123
|
+
index.ts the barrel — import from here
|
|
124
|
+
core/ framework-free logic, no React import
|
|
125
|
+
react/ the components
|
|
126
|
+
styles.css import once
|
|
127
|
+
README.md full prop documentation
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Tests are deliberately **not** copied: they import `vitest`, which the consuming
|
|
131
|
+
project has no reason to have.
|
|
132
|
+
|
|
133
|
+
## The shared folder
|
|
134
|
+
|
|
135
|
+
Every beta component imports `../../shared`. The CLI installs it alongside
|
|
136
|
+
whatever you picked, and it is versioned independently (`shared/version.json`).
|
|
137
|
+
|
|
138
|
+
Rules the installer enforces:
|
|
139
|
+
|
|
140
|
+
- `shared/` only moves forward. If the project has a newer version than the CLI
|
|
141
|
+
carries, the install aborts rather than downgrading it.
|
|
142
|
+
- A file you edited since the CLI wrote it is never replaced without `--force`.
|
|
143
|
+
It tracks this with sha256 hashes in `component-lib/shared/.install-manifest.json`.
|
|
144
|
+
|
|
145
|
+
**Do not edit `component-lib/shared/`.** Components are yours to change; shared
|
|
146
|
+
is replaced on update. Put your changes in your own module instead.
|
|
147
|
+
|
|
148
|
+
`shared` exports `cx`, `countCharacters`, `describeField`,
|
|
149
|
+
`useControllableState`, `AnchoredPopover`, `placePopover`, `icons` and
|
|
150
|
+
`sharedVersion`.
|
|
151
|
+
|
|
152
|
+
## Importing
|
|
153
|
+
|
|
154
|
+
Import the barrel in the component that uses it:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { Input, OtpInput } from "component-lib/input";
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Put the stylesheet in the project's global CSS, once — never in a component file:
|
|
161
|
+
|
|
162
|
+
```css
|
|
163
|
+
@import "../component-lib/input/styles.css";
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Most repos set a path alias. The library's own READMEs are written with
|
|
167
|
+
`@/components/<folder>`; the DataGrid README uses `component-lib/data-grid`.
|
|
168
|
+
Both mean the installed folder — follow whatever the host repo already uses.
|
|
169
|
+
|
|
170
|
+
### Public entry points
|
|
171
|
+
|
|
172
|
+
| Path | Contents |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `component-lib/<folder>` | Components, hooks, types, locale defaults. **Use this.** |
|
|
175
|
+
| `component-lib/<folder>/core` | Framework-free helpers; safe on a server. Documented per component. |
|
|
176
|
+
| `component-lib/<folder>/styles.css` | The stylesheet. |
|
|
177
|
+
| `component-lib/shared` | Shared helpers, mostly used by the components themselves. |
|
|
178
|
+
|
|
179
|
+
Anything deeper (`component-lib/input/react/Input`) is internal. It will resolve,
|
|
180
|
+
but it is not a supported path and re-running the CLI may move it.
|
|
181
|
+
|
|
182
|
+
## Next.js
|
|
183
|
+
|
|
184
|
+
Every React file in the library starts with `"use client"`. A Server Component
|
|
185
|
+
can render them, but cannot pass function props (`onValueChange`, `onClick`,
|
|
186
|
+
`footer={({ close }) => …}`). Put the interactive part in a Client Component.
|
|
187
|
+
|
|
188
|
+
`toast()` does nothing during a server render. Call it in the browser, for
|
|
189
|
+
example after a Server Action resolves.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Overlays
|
|
2
|
+
|
|
3
|
+
Modal, Dialog, Popover, Menu, Tooltip and Toaster. All render in the browser's
|
|
4
|
+
top layer (native `<dialog>` or the Popover API) or a portal on `<body>`, so no
|
|
5
|
+
parent's `overflow`, `transform` or `z-index` can clip them. You never need a
|
|
6
|
+
portal, a z-index or a focus trap of your own.
|
|
7
|
+
|
|
8
|
+
## Choosing one
|
|
9
|
+
|
|
10
|
+
| | Interrupts | Lives | Use for |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| **Modal** | Yes, blocks | Top layer | A task or form that owns the screen |
|
|
13
|
+
| **dialog.confirm** | Yes, blocks | Top layer | A decision that must happen now |
|
|
14
|
+
| **Popover** | No | Top layer | A panel of controls tied to a trigger |
|
|
15
|
+
| **Menu** | No | Top layer | A list of commands |
|
|
16
|
+
| **Tooltip** | No | Top layer | A few words of label on hover/focus |
|
|
17
|
+
| **toast** | No, and leaves | A corner | Confirming what already happened |
|
|
18
|
+
| **Alert** | No, and stays | In the layout | The state a page or section is in |
|
|
19
|
+
|
|
20
|
+
## Modal
|
|
21
|
+
|
|
22
|
+
Built on native `<dialog>`: the browser supplies the top layer, the inert page
|
|
23
|
+
behind, the focus trap and focus return. The component adds controlled state,
|
|
24
|
+
dismiss rules, a close guard, sizes, drawers and animation.
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { Modal } from "component-lib/modal";
|
|
28
|
+
|
|
29
|
+
const [open, setOpen] = useState(false);
|
|
30
|
+
|
|
31
|
+
<Modal open={open} onOpenChange={setOpen} title="Edit profile"
|
|
32
|
+
footer={({ close }) => <Button onClick={close}>Done</Button>}>
|
|
33
|
+
…
|
|
34
|
+
</Modal>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Prop | Type | Default | Description |
|
|
38
|
+
| --- | --- | --- | --- |
|
|
39
|
+
| `open` / `defaultOpen` / `onOpenChange` | `boolean` / `boolean` / `(open, reason?) => void` | — / `false` | Controlled or uncontrolled. `reason` is `escape`, `backdrop`, `close-button` or `api`. |
|
|
40
|
+
| `title`, `description` | `ReactNode` | — | Header text. |
|
|
41
|
+
| `aria-label` | `string` | — | Names a modal that has no `title`. |
|
|
42
|
+
| `children`, `footer` | `ReactNode \| ({ close }) => ReactNode` | — | Body and footer. |
|
|
43
|
+
| `size` | `sm` `md` `lg` `xl` `full` | `md` | 400 / 520 / 720 / 960 px, or the whole screen. |
|
|
44
|
+
| `placement` | `center` `top` `left` `right` `bottom` | `center` | `left`, `right` and `bottom` are drawers. |
|
|
45
|
+
| `closeButton`, `closeOnEscape`, `closeOnBackdrop` | `boolean` | `true` | Dismiss options. |
|
|
46
|
+
| `onBeforeClose` | `(reason) => boolean \| Promise<boolean>` | — | Return `false` to stay open. |
|
|
47
|
+
| `initialFocus` | `RefObject<HTMLElement>` | — | Otherwise `[data-autofocus]`, then the first focusable element. |
|
|
48
|
+
| `keepMounted` | `boolean` | `false` | Keep the content's state while closed. |
|
|
49
|
+
| `className`, `classNames`, `style`, `localeText`, `id`, `ref` | — | — | Slots below. |
|
|
50
|
+
|
|
51
|
+
Slots: `root`, `header`, `title`, `description`, `close`, `body`, `footer`.
|
|
52
|
+
`ref` exposes `open()`, `close()`, `getElement()`.
|
|
53
|
+
|
|
54
|
+
Guard an unsaved form:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<Modal open={open} onOpenChange={setOpen}
|
|
58
|
+
onBeforeClose={async (reason) =>
|
|
59
|
+
reason === "api" || !dirty || dialog.confirm({ title: "Discard changes?" })}>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Known limits: exit animations need `transition-behavior: allow-discrete`
|
|
63
|
+
(Chrome 117+, Safari 18+); elsewhere it closes instantly. Scroll lock uses
|
|
64
|
+
`:root:has(.md-root[open])`, which can shift content by the scrollbar's width.
|
|
65
|
+
|
|
66
|
+
## dialog (alert / confirm / prompt)
|
|
67
|
+
|
|
68
|
+
A promise-based API for the decisions a Modal is too heavy for. Mount the host
|
|
69
|
+
**once** near the root; then call `dialog.*` from anywhere, including outside
|
|
70
|
+
React.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
// once, e.g. app/layout.tsx or App.tsx
|
|
74
|
+
import { DialogHost } from "component-lib/dialog";
|
|
75
|
+
<DialogHost />
|
|
76
|
+
|
|
77
|
+
// anywhere
|
|
78
|
+
import { dialog } from "component-lib/dialog";
|
|
79
|
+
|
|
80
|
+
if (await dialog.confirm({ title: "Delete 3 invoices?", intent: "danger", confirmLabel: "Delete" })) {
|
|
81
|
+
await deleteInvoices();
|
|
82
|
+
}
|
|
83
|
+
const name = await dialog.prompt({ title: "Rename", defaultValue: "report.pdf", required: true });
|
|
84
|
+
await dialog.alert("Export finished");
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- `alert` resolves when closed, `confirm` resolves `true`/`false`, `prompt`
|
|
88
|
+
resolves the text or `null`.
|
|
89
|
+
- `onConfirm` may be async: the dialog shows progress, and if it throws, shows
|
|
90
|
+
the error and stays open.
|
|
91
|
+
- Requests queue and show one at a time.
|
|
92
|
+
- Where there is no document (a server render) they resolve as canceled.
|
|
93
|
+
|
|
94
|
+
Options: `title`, `description`, `intent` (`default` `info` `success` `warning`
|
|
95
|
+
`danger`), `icon`, `confirmLabel`, `cancelLabel`, `dismissible`, `size`
|
|
96
|
+
(`sm` `md`), `onConfirm`. Prompt adds `defaultValue`, `placeholder`,
|
|
97
|
+
`inputLabel`, `inputType`, `required`, `validate`.
|
|
98
|
+
|
|
99
|
+
`<Dialog open onClose>` is the same dialog as an ordinary controlled component,
|
|
100
|
+
if you would rather not use the queue. `createDialogStore()` and
|
|
101
|
+
`createDialogApi(store)` build isolated instances; pass the store to
|
|
102
|
+
`<DialogHost store={store} />`.
|
|
103
|
+
|
|
104
|
+
## Popover
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
<Popover trigger={<Button variant="outline">Filters</Button>} title="Filters">
|
|
108
|
+
{({ close }) => (
|
|
109
|
+
<>
|
|
110
|
+
<Checkbox label="Only active" />
|
|
111
|
+
<Button size="sm" onClick={close}>Apply</Button>
|
|
112
|
+
</>
|
|
113
|
+
)}
|
|
114
|
+
</Popover>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Prop | Default | Description |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| `trigger` | required | A **single** element, cloned with a ref and the ARIA wiring. |
|
|
120
|
+
| `open` / `defaultOpen` / `onOpenChange` | — | Reason: `escape`, `outside`, `trigger`, `api`. |
|
|
121
|
+
| `title` / `description` | — | Heading inside the panel; `title` also names it. |
|
|
122
|
+
| `aria-label` | "More information" | Names the panel when there is no `title`. |
|
|
123
|
+
| `side` / `align` | `bottom` / `start` | Flips and clamps to stay on screen. |
|
|
124
|
+
| `gap` | `6` | Distance from the trigger, in pixels. |
|
|
125
|
+
| `scrollable` | `false` | Cap the height to the space available and scroll. |
|
|
126
|
+
| `autoFocus` | `true` | Move focus into the panel on open. |
|
|
127
|
+
| `className`, `classNames`, `style`, `localeText` | — | Slots: `root`, `panel`, `header`, `title`, `description`, `body`. |
|
|
128
|
+
|
|
129
|
+
`children` may be a function receiving `{ close }`. The browser provides light
|
|
130
|
+
dismiss and Escape; focus returns to the trigger. `data-side` on the root says
|
|
131
|
+
which side it settled on after flipping.
|
|
132
|
+
|
|
133
|
+
## Menu
|
|
134
|
+
|
|
135
|
+
The WAI-ARIA menu button pattern. Use it for commands, not for a form control —
|
|
136
|
+
for picking a value use `Select`.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import {
|
|
140
|
+
Menu, MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem,
|
|
141
|
+
MenuGroup, MenuSeparator, MenuSub,
|
|
142
|
+
} from "component-lib/menu";
|
|
143
|
+
|
|
144
|
+
<Menu trigger={<Button variant="outline">Actions</Button>} label="Row actions">
|
|
145
|
+
<MenuItem icon={<EditIcon />} shortcut="⌘E" onSelect={edit}>Edit</MenuItem>
|
|
146
|
+
<MenuSub label="Export">
|
|
147
|
+
<MenuItem onSelect={() => download("csv")}>CSV</MenuItem>
|
|
148
|
+
<MenuItem onSelect={() => download("pdf")}>PDF</MenuItem>
|
|
149
|
+
</MenuSub>
|
|
150
|
+
<MenuSeparator />
|
|
151
|
+
<MenuCheckboxItem checked={compact} onCheckedChange={setCompact}>Compact rows</MenuCheckboxItem>
|
|
152
|
+
<MenuSeparator />
|
|
153
|
+
<MenuItem destructive onSelect={remove}>Delete</MenuItem>
|
|
154
|
+
</Menu>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Menu:** `trigger` (required, a single element), `open`/`defaultOpen`/
|
|
158
|
+
`onOpenChange` (reasons `escape`, `outside`, `trigger`, `select`, `api`),
|
|
159
|
+
`label` ("Menu"), `side`/`align`/`gap` (`bottom`/`start`/`4`), `closeOnSelect`
|
|
160
|
+
(true), `className`, `classNames`, `style`, `localeText`.
|
|
161
|
+
|
|
162
|
+
Slots: `root`, `list`, `item`, `icon`, `label`, `shortcut`, `separator`,
|
|
163
|
+
`group`, `groupLabel`, `indicator`, `submenu`.
|
|
164
|
+
|
|
165
|
+
| Item | Props |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `MenuItem` | `onSelect`, `icon`, `shortcut`, `disabled`, `destructive`. Closes the menu by default. |
|
|
168
|
+
| `MenuCheckboxItem` | `checked` / `onCheckedChange`. Stays open, so several can be toggled. |
|
|
169
|
+
| `MenuRadioGroup` + `MenuRadioItem` | `value` / `onValueChange` on the group, `value` on each item. |
|
|
170
|
+
| `MenuGroup` | A titled section; the title is a label, never focused. |
|
|
171
|
+
| `MenuSeparator` | A rule between sections. |
|
|
172
|
+
| `MenuSub` | A nested menu; opens on hover, Enter, or the arrow pointing into it. |
|
|
173
|
+
|
|
174
|
+
`shortcut` only draws the hint — binding the key is yours.
|
|
175
|
+
|
|
176
|
+
Keyboard: ↓/↑ on the trigger open at the first/last item; ↓/↑ move and wrap;
|
|
177
|
+
Home/End jump; letters typeahead; Enter/Space choose; →/← open a submenu or go
|
|
178
|
+
back (mirrored in RTL); Escape closes and returns focus; Tab closes and moves on.
|
|
179
|
+
Disabled items stay focusable but arrow keys skip them.
|
|
180
|
+
|
|
181
|
+
## Tooltip
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
<Tooltip content="Export as CSV">
|
|
185
|
+
<Button variant="ghost" aria-label="Export"><DownloadIcon /></Button>
|
|
186
|
+
</Tooltip>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
| Prop | Default | Description |
|
|
190
|
+
| --- | --- | --- |
|
|
191
|
+
| `content` | required | The label. A few words; longer content belongs in a Popover. |
|
|
192
|
+
| `children` | required | A **single** element, cloned with a ref, handlers and `aria-describedby`. |
|
|
193
|
+
| `side` / `align` / `gap` | `top` / `center` / `6` | Flips and clamps. |
|
|
194
|
+
| `delay` | `400` | Wait before showing on hover. Keyboard focus never waits. |
|
|
195
|
+
| `closeDelay` | `120` | Wait before hiding. |
|
|
196
|
+
| `disabled` | `false` | Never show it. |
|
|
197
|
+
| `className`, `classNames`, `style` | — | Slots: `root`, `content`. |
|
|
198
|
+
|
|
199
|
+
**It describes, it does not name.** The tooltip is wired with
|
|
200
|
+
`aria-describedby`, so an icon button still needs its own `aria-label`. It never
|
|
201
|
+
takes focus, is `pointer-events: none`, and touch devices never see it — so
|
|
202
|
+
nothing essential may live only there.
|
|
203
|
+
|
|
204
|
+
## Toaster
|
|
205
|
+
|
|
206
|
+
See `toaster.md`.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Pickers
|
|
2
|
+
|
|
3
|
+
Select, MultiSelect, DatePicker, DateRangePicker and FileUploader. They share the
|
|
4
|
+
field contract in `forms.md` (`label`, `description`, `error`, `size`,
|
|
5
|
+
`classNames`, `localeText`) but all four use **`onChange`**, not `onValueChange`.
|
|
6
|
+
|
|
7
|
+
## Select and MultiSelect
|
|
8
|
+
|
|
9
|
+
Import both from `component-lib/combobox`.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
interface ComboboxOption<V = string> {
|
|
13
|
+
value: V; // string or number
|
|
14
|
+
label: string;
|
|
15
|
+
description?: string; // second line
|
|
16
|
+
group?: string; // heading, in first-seen order
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
keywords?: string[]; // extra search terms
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Shared props: `options` (required), `mode` (`client` | `server`), `loading`,
|
|
24
|
+
`onSearchChange`, `searchDebounce` (250), `searchable` (true), `hasMore`,
|
|
25
|
+
`onLoadMore`, `filterFn`, `renderOption`, `allowCreate`, `onCreate`, `label`,
|
|
26
|
+
`description`, `error`, `placeholder` ("Select…"), `required`, `disabled`,
|
|
27
|
+
`clearable` (true), `size`, `name`, `maxHeight` (280), `virtualize` (true above
|
|
28
|
+
80 options), `emptyMessage`, `className`, `style`, `localeText`,
|
|
29
|
+
`onOpenChange`, `ref`, and `classNames` (`root` `label` `control` `value` `tag`
|
|
30
|
+
`popover` `search` `list` `option` `footer`).
|
|
31
|
+
|
|
32
|
+
`Select` adds `value`/`defaultValue` (`V | null`) and `closeOnSelect` (true).
|
|
33
|
+
`MultiSelect` adds `value`/`defaultValue` (`V[]`), `max`, `maxVisibleTags` (3),
|
|
34
|
+
`showSelectAll` (true) and `closeOnSelect` (false).
|
|
35
|
+
|
|
36
|
+
`ref` gives `open()`, `close()`, `toggle()`, `focus()`, `clear()`, `getValue()`,
|
|
37
|
+
`getSelectedOptions()`.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<Select label="Country" options={countries} value={country} onChange={setCountry} />
|
|
41
|
+
<MultiSelect label="Tags" options={tagOptions} value={tags} onChange={setTags} max={5} />
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Server mode: set `mode="server"`, feed the current results as `options`, and
|
|
45
|
+
handle `onSearchChange` (fires once on open, then debounced) and `onLoadMore`.
|
|
46
|
+
There is no async "load option by value" — pass options that already include the
|
|
47
|
+
selected values.
|
|
48
|
+
|
|
49
|
+
## DatePicker and DateRangePicker
|
|
50
|
+
|
|
51
|
+
`value`, `defaultValue`, `min` and `max` accept a `Date`, an ISO `yyyy-mm-dd`
|
|
52
|
+
string or a timestamp. `onChange` always gives `Date` objects at local midnight.
|
|
53
|
+
`DateRangePicker.onChange` also receives a `complete` flag, `false` after the
|
|
54
|
+
first of two clicks.
|
|
55
|
+
|
|
56
|
+
Shared props: `min`, `max`, `isDateDisabled`, `locale`, `weekStartsOn`,
|
|
57
|
+
`format`, `numberOfMonths`, `showWeekNumbers`, `showToday`, `allowInput` (true),
|
|
58
|
+
`fixedWeeks` (true), `label`, `description`, `error`, `placeholder`, `required`,
|
|
59
|
+
`disabled`, `readOnly`.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<DatePicker label="Start date" value={date} onChange={setDate} />
|
|
63
|
+
<DateRangePicker label="Period" value={range} onChange={setRange} />
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## FileUploader
|
|
67
|
+
|
|
68
|
+
`multiple`, `accept`, `maxSize`, `endpoint`, `autoUpload`, `params`,
|
|
69
|
+
`fieldNames`, `onChange`, `onUploadComplete`, plus `label`, `description`,
|
|
70
|
+
`error`. Without an `endpoint` the files post with the surrounding form.
|
|
71
|
+
|
|
72
|
+
Each chunk is a `multipart/form-data` POST carrying `uploadId`, `fileName`,
|
|
73
|
+
`chunkIndex`, `totalChunks`, `fileSize`, `additionalParams` and `chunk`. Answer
|
|
74
|
+
2xx; the last body becomes `item.response`. 5xx/408/429 and network errors retry
|
|
75
|
+
with backoff; other 4xx fail the file.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<FileUploader label="Attachments" multiple accept=".pdf,image/*"
|
|
79
|
+
maxSize={20 * 1024 * 1024} endpoint="/api/upload"
|
|
80
|
+
onUploadComplete={(items) => console.log(items.map((i) => i.response))} />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`useFileUploader()` exposes the same engine for a custom UI.
|