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.
@@ -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,190 @@
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 folder barrel, plus the stylesheet once anywhere in the app:
155
+
156
+ ```ts
157
+ import { Input, OtpInput } from "component-lib/input";
158
+ import "component-lib/input/styles.css";
159
+ ```
160
+
161
+ Or import the CSS from your root stylesheet:
162
+
163
+ ```css
164
+ @import "../component-lib/input/styles.css";
165
+ ```
166
+
167
+ Most repos set a path alias. The library's own READMEs are written with
168
+ `@/components/<folder>`; the DataGrid README uses `component-lib/data-grid`.
169
+ Both mean the installed folder — follow whatever the host repo already uses.
170
+
171
+ ### Public entry points
172
+
173
+ | Path | Contents |
174
+ | --- | --- |
175
+ | `component-lib/<folder>` | Components, hooks, types, locale defaults. **Use this.** |
176
+ | `component-lib/<folder>/core` | Framework-free helpers; safe on a server. Documented per component. |
177
+ | `component-lib/<folder>/styles.css` | The stylesheet. |
178
+ | `component-lib/shared` | Shared helpers, mostly used by the components themselves. |
179
+
180
+ Anything deeper (`component-lib/input/react/Input`) is internal. It will resolve,
181
+ but it is not a supported path and re-running the CLI may move it.
182
+
183
+ ## Next.js
184
+
185
+ Every React file in the library starts with `"use client"`. A Server Component
186
+ can render them, but cannot pass function props (`onValueChange`, `onClick`,
187
+ `footer={({ close }) => …}`). Put the interactive part in a Client Component.
188
+
189
+ `toast()` does nothing during a server render. Call it in the browser, for
190
+ example after a Server Action resolves.
@@ -0,0 +1,209 @@
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
+ import "component-lib/modal/styles.css";
29
+
30
+ const [open, setOpen] = useState(false);
31
+
32
+ <Modal open={open} onOpenChange={setOpen} title="Edit profile"
33
+ footer={({ close }) => <Button onClick={close}>Done</Button>}>
34
+ …
35
+ </Modal>
36
+ ```
37
+
38
+ | Prop | Type | Default | Description |
39
+ | --- | --- | --- | --- |
40
+ | `open` / `defaultOpen` / `onOpenChange` | `boolean` / `boolean` / `(open, reason?) => void` | — / `false` | Controlled or uncontrolled. `reason` is `escape`, `backdrop`, `close-button` or `api`. |
41
+ | `title`, `description` | `ReactNode` | — | Header text. |
42
+ | `aria-label` | `string` | — | Names a modal that has no `title`. |
43
+ | `children`, `footer` | `ReactNode \| ({ close }) => ReactNode` | — | Body and footer. |
44
+ | `size` | `sm` `md` `lg` `xl` `full` | `md` | 400 / 520 / 720 / 960 px, or the whole screen. |
45
+ | `placement` | `center` `top` `left` `right` `bottom` | `center` | `left`, `right` and `bottom` are drawers. |
46
+ | `closeButton`, `closeOnEscape`, `closeOnBackdrop` | `boolean` | `true` | Dismiss options. |
47
+ | `onBeforeClose` | `(reason) => boolean \| Promise<boolean>` | — | Return `false` to stay open. |
48
+ | `initialFocus` | `RefObject<HTMLElement>` | — | Otherwise `[data-autofocus]`, then the first focusable element. |
49
+ | `keepMounted` | `boolean` | `false` | Keep the content's state while closed. |
50
+ | `className`, `classNames`, `style`, `localeText`, `id`, `ref` | — | — | Slots below. |
51
+
52
+ Slots: `root`, `header`, `title`, `description`, `close`, `body`, `footer`.
53
+ `ref` exposes `open()`, `close()`, `getElement()`.
54
+
55
+ Guard an unsaved form:
56
+
57
+ ```tsx
58
+ <Modal open={open} onOpenChange={setOpen}
59
+ onBeforeClose={async (reason) =>
60
+ reason === "api" || !dirty || dialog.confirm({ title: "Discard changes?" })}>
61
+ ```
62
+
63
+ Known limits: exit animations need `transition-behavior: allow-discrete`
64
+ (Chrome 117+, Safari 18+); elsewhere it closes instantly. Scroll lock uses
65
+ `:root:has(.md-root[open])`, which can shift content by the scrollbar's width.
66
+
67
+ ## dialog (alert / confirm / prompt)
68
+
69
+ A promise-based API for the decisions a Modal is too heavy for. Mount the host
70
+ **once** near the root; then call `dialog.*` from anywhere, including outside
71
+ React.
72
+
73
+ ```tsx
74
+ // once, e.g. app/layout.tsx or App.tsx
75
+ import { DialogHost } from "component-lib/dialog";
76
+ import "component-lib/dialog/styles.css";
77
+ <DialogHost />
78
+
79
+ // anywhere
80
+ import { dialog } from "component-lib/dialog";
81
+
82
+ if (await dialog.confirm({ title: "Delete 3 invoices?", intent: "danger", confirmLabel: "Delete" })) {
83
+ await deleteInvoices();
84
+ }
85
+ const name = await dialog.prompt({ title: "Rename", defaultValue: "report.pdf", required: true });
86
+ await dialog.alert("Export finished");
87
+ ```
88
+
89
+ - `alert` resolves when closed, `confirm` resolves `true`/`false`, `prompt`
90
+ resolves the text or `null`.
91
+ - `onConfirm` may be async: the dialog shows progress, and if it throws, shows
92
+ the error and stays open.
93
+ - Requests queue and show one at a time.
94
+ - Where there is no document (a server render) they resolve as canceled.
95
+
96
+ Options: `title`, `description`, `intent` (`default` `info` `success` `warning`
97
+ `danger`), `icon`, `confirmLabel`, `cancelLabel`, `dismissible`, `size`
98
+ (`sm` `md`), `onConfirm`. Prompt adds `defaultValue`, `placeholder`,
99
+ `inputLabel`, `inputType`, `required`, `validate`.
100
+
101
+ `<Dialog open onClose>` is the same dialog as an ordinary controlled component,
102
+ if you would rather not use the queue. `createDialogStore()` and
103
+ `createDialogApi(store)` build isolated instances; pass the store to
104
+ `<DialogHost store={store} />`.
105
+
106
+ ## Popover
107
+
108
+ ```tsx
109
+ <Popover trigger={<Button variant="outline">Filters</Button>} title="Filters">
110
+ {({ close }) => (
111
+ <>
112
+ <Checkbox label="Only active" />
113
+ <Button size="sm" onClick={close}>Apply</Button>
114
+ </>
115
+ )}
116
+ </Popover>
117
+ ```
118
+
119
+ | Prop | Default | Description |
120
+ | --- | --- | --- |
121
+ | `trigger` | required | A **single** element, cloned with a ref and the ARIA wiring. |
122
+ | `open` / `defaultOpen` / `onOpenChange` | — | Reason: `escape`, `outside`, `trigger`, `api`. |
123
+ | `title` / `description` | — | Heading inside the panel; `title` also names it. |
124
+ | `aria-label` | "More information" | Names the panel when there is no `title`. |
125
+ | `side` / `align` | `bottom` / `start` | Flips and clamps to stay on screen. |
126
+ | `gap` | `6` | Distance from the trigger, in pixels. |
127
+ | `scrollable` | `false` | Cap the height to the space available and scroll. |
128
+ | `autoFocus` | `true` | Move focus into the panel on open. |
129
+ | `className`, `classNames`, `style`, `localeText` | — | Slots: `root`, `panel`, `header`, `title`, `description`, `body`. |
130
+
131
+ `children` may be a function receiving `{ close }`. The browser provides light
132
+ dismiss and Escape; focus returns to the trigger. `data-side` on the root says
133
+ which side it settled on after flipping.
134
+
135
+ ## Menu
136
+
137
+ The WAI-ARIA menu button pattern. Use it for commands, not for a form control —
138
+ for picking a value use `Select`.
139
+
140
+ ```tsx
141
+ import {
142
+ Menu, MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem,
143
+ MenuGroup, MenuSeparator, MenuSub,
144
+ } from "component-lib/menu";
145
+ import "component-lib/menu/styles.css";
146
+
147
+ <Menu trigger={<Button variant="outline">Actions</Button>} label="Row actions">
148
+ <MenuItem icon={<EditIcon />} shortcut="⌘E" onSelect={edit}>Edit</MenuItem>
149
+ <MenuSub label="Export">
150
+ <MenuItem onSelect={() => download("csv")}>CSV</MenuItem>
151
+ <MenuItem onSelect={() => download("pdf")}>PDF</MenuItem>
152
+ </MenuSub>
153
+ <MenuSeparator />
154
+ <MenuCheckboxItem checked={compact} onCheckedChange={setCompact}>Compact rows</MenuCheckboxItem>
155
+ <MenuSeparator />
156
+ <MenuItem destructive onSelect={remove}>Delete</MenuItem>
157
+ </Menu>
158
+ ```
159
+
160
+ **Menu:** `trigger` (required, a single element), `open`/`defaultOpen`/
161
+ `onOpenChange` (reasons `escape`, `outside`, `trigger`, `select`, `api`),
162
+ `label` ("Menu"), `side`/`align`/`gap` (`bottom`/`start`/`4`), `closeOnSelect`
163
+ (true), `className`, `classNames`, `style`, `localeText`.
164
+
165
+ Slots: `root`, `list`, `item`, `icon`, `label`, `shortcut`, `separator`,
166
+ `group`, `groupLabel`, `indicator`, `submenu`.
167
+
168
+ | Item | Props |
169
+ | --- | --- |
170
+ | `MenuItem` | `onSelect`, `icon`, `shortcut`, `disabled`, `destructive`. Closes the menu by default. |
171
+ | `MenuCheckboxItem` | `checked` / `onCheckedChange`. Stays open, so several can be toggled. |
172
+ | `MenuRadioGroup` + `MenuRadioItem` | `value` / `onValueChange` on the group, `value` on each item. |
173
+ | `MenuGroup` | A titled section; the title is a label, never focused. |
174
+ | `MenuSeparator` | A rule between sections. |
175
+ | `MenuSub` | A nested menu; opens on hover, Enter, or the arrow pointing into it. |
176
+
177
+ `shortcut` only draws the hint — binding the key is yours.
178
+
179
+ Keyboard: ↓/↑ on the trigger open at the first/last item; ↓/↑ move and wrap;
180
+ Home/End jump; letters typeahead; Enter/Space choose; →/← open a submenu or go
181
+ back (mirrored in RTL); Escape closes and returns focus; Tab closes and moves on.
182
+ Disabled items stay focusable but arrow keys skip them.
183
+
184
+ ## Tooltip
185
+
186
+ ```tsx
187
+ <Tooltip content="Export as CSV">
188
+ <Button variant="ghost" aria-label="Export"><DownloadIcon /></Button>
189
+ </Tooltip>
190
+ ```
191
+
192
+ | Prop | Default | Description |
193
+ | --- | --- | --- |
194
+ | `content` | required | The label. A few words; longer content belongs in a Popover. |
195
+ | `children` | required | A **single** element, cloned with a ref, handlers and `aria-describedby`. |
196
+ | `side` / `align` / `gap` | `top` / `center` / `6` | Flips and clamps. |
197
+ | `delay` | `400` | Wait before showing on hover. Keyboard focus never waits. |
198
+ | `closeDelay` | `120` | Wait before hiding. |
199
+ | `disabled` | `false` | Never show it. |
200
+ | `className`, `classNames`, `style` | — | Slots: `root`, `content`. |
201
+
202
+ **It describes, it does not name.** The tooltip is wired with
203
+ `aria-describedby`, so an icon button still needs its own `aria-label`. It never
204
+ takes focus, is `pointer-events: none`, and touch devices never see it — so
205
+ nothing essential may live only there.
206
+
207
+ ## Toaster
208
+
209
+ 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.