@overdoser/react-toolkit 0.0.2 → 0.0.4

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/manifest.json ADDED
@@ -0,0 +1,360 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "name": "@overdoser/react-toolkit",
4
+ "version": "0.0.2",
5
+ "description": "Machine-readable manifest of components, props, allowed values, and hooks. Generated for tooling and AI assistants.",
6
+ "themeCssImport": "@overdoser/react-toolkit/theme.css",
7
+ "peerDependencies": {
8
+ "required": ["react", "react-dom"],
9
+ "optional": ["react-hook-form"]
10
+ },
11
+ "components": {
12
+ "Button": {
13
+ "import": "import { Button } from '@overdoser/react-toolkit'",
14
+ "element": "button",
15
+ "extendsNativeProps": "button",
16
+ "forwardsRef": true,
17
+ "props": {
18
+ "variant": { "type": "enum", "values": ["primary", "secondary", "danger", "ghost"], "default": "primary" },
19
+ "size": { "type": "enum", "values": ["sm", "md", "lg"], "default": "md" },
20
+ "loading": { "type": "boolean", "default": false, "notes": "Disables the button and sets aria-busy." },
21
+ "loadingStyle": { "type": "enum", "values": ["dots", "shimmer", "border"], "default": "dots", "notes": "Only used when loading=true." },
22
+ "fullWidth": { "type": "boolean", "default": false },
23
+ "classes": { "type": "Partial<ButtonClasses>", "shape": ["root", "content", "shimmer", "dots", "dot"] }
24
+ }
25
+ },
26
+ "Link": {
27
+ "import": "import { Link } from '@overdoser/react-toolkit'",
28
+ "element": "a",
29
+ "extendsNativeProps": "a",
30
+ "forwardsRef": true,
31
+ "props": {
32
+ "variant": { "type": "enum", "values": ["default", "muted", "danger"], "default": "default" },
33
+ "external": { "type": "boolean", "default": false, "notes": "Adds target=_blank, rel=noopener noreferrer, and an sr-only 'opens in a new tab' suffix." }
34
+ }
35
+ },
36
+ "Typography": {
37
+ "import": "import { Typography } from '@overdoser/react-toolkit'",
38
+ "element": "dynamic (variant)",
39
+ "extendsNativeProps": "p (without color)",
40
+ "forwardsRef": true,
41
+ "props": {
42
+ "variant": { "type": "enum", "values": ["h1", "h2", "h3", "h4", "h5", "h6", "p", "span", "label"], "default": "p" },
43
+ "weight": { "type": "enum", "values": ["normal", "medium", "semibold", "bold"] },
44
+ "color": { "type": "enum", "values": ["default", "muted", "primary", "danger", "success"], "notes": "For arbitrary colors, use style or className." },
45
+ "align": { "type": "enum", "values": ["left", "center", "right"] },
46
+ "truncate": { "type": "boolean", "default": false }
47
+ }
48
+ },
49
+ "List": {
50
+ "import": "import { List } from '@overdoser/react-toolkit'",
51
+ "element": "ul or ol",
52
+ "extendsNativeProps": "ul",
53
+ "forwardsRef": true,
54
+ "props": {
55
+ "variant": { "type": "enum", "values": ["unordered", "ordered", "none"], "default": "unordered" },
56
+ "spacing": { "type": "enum", "values": ["sm", "md", "lg"], "default": "md" }
57
+ }
58
+ },
59
+ "ListItem": {
60
+ "import": "import { ListItem } from '@overdoser/react-toolkit'",
61
+ "element": "li",
62
+ "extendsNativeProps": "li",
63
+ "forwardsRef": true,
64
+ "props": {}
65
+ },
66
+ "Table": {
67
+ "import": "import { Table, type ColumnDef, type SortConfig } from '@overdoser/react-toolkit'",
68
+ "element": "table (inside wrapper div)",
69
+ "generic": "T extends Record<string, unknown>",
70
+ "props": {
71
+ "data": { "type": "T[]", "required": true },
72
+ "columns": { "type": "ColumnDef<T>[]", "required": true },
73
+ "sortConfig": { "type": "SortConfig[]", "notes": "Controlled sort. Provide with onSort for server-side mode." },
74
+ "onSort": { "type": "(config: SortConfig[]) => void", "notes": "When set, Table is fully controlled — does not sort or paginate data internally." },
75
+ "multiSort": { "type": "boolean", "default": true, "notes": "Ctrl/Cmd+click a sortable header to add a secondary sort." },
76
+ "striped": { "type": "boolean", "default": false },
77
+ "hoverable": { "type": "boolean", "default": false },
78
+ "compact": { "type": "boolean", "default": false },
79
+ "rowKey": { "type": "keyof T & string" },
80
+ "emptyMessage": { "type": "ReactNode", "default": "No data" },
81
+ "pagination": { "type": "PaginationConfig" },
82
+ "classes": { "type": "Partial<TableClasses>", "shape": ["wrapper", "root", "headerCell", "row", "cell", "emptyCell", "paginator", "pageButton"] }
83
+ },
84
+ "subTypes": {
85
+ "ColumnDef<T>": {
86
+ "key": { "type": "keyof T & string", "required": true },
87
+ "header": { "type": "ReactNode", "required": true },
88
+ "sortable": { "type": "boolean" },
89
+ "render": { "type": "(value, row, index) => ReactNode" },
90
+ "width": { "type": "string | number" },
91
+ "align": { "type": "enum", "values": ["left", "center", "right"] }
92
+ },
93
+ "SortConfig": {
94
+ "key": { "type": "string", "required": true },
95
+ "direction": { "type": "enum", "values": ["asc", "desc"], "required": true }
96
+ },
97
+ "PaginationConfig": {
98
+ "page": { "type": "number", "required": true, "notes": "1-based" },
99
+ "pageSize": { "type": "number", "required": true },
100
+ "totalRows": { "type": "number", "notes": "Required for server-side mode; defaults to data.length client-side." },
101
+ "pageSizeOptions": { "type": "number[]", "default": [10, 25, 50, 100] },
102
+ "onPageChange": { "type": "(page: number, pageSize: number) => void", "required": true }
103
+ }
104
+ },
105
+ "behavior": {
106
+ "sortCycle": "none → asc → desc → none",
107
+ "controlledMode": "Activated when onSort is provided. Table renders data as-is and does not slice for pagination."
108
+ }
109
+ },
110
+ "Dropdown": {
111
+ "import": "import { Dropdown, DropdownItem } from '@overdoser/react-toolkit'",
112
+ "element": "div",
113
+ "modes": {
114
+ "menu": "Pass `trigger` plus <DropdownItem> children.",
115
+ "select": "Pass `options` + `value` + `onChange`. Trigger label shows the selected option."
116
+ },
117
+ "props": {
118
+ "trigger": { "type": "ReactNode", "notes": "Required for menu mode; ignored when `options` is provided." },
119
+ "options": { "type": "{ value: string; label: ReactNode; disabled?: boolean }[]" },
120
+ "value": { "type": "string", "notes": "Select-mode controlled value." },
121
+ "onChange": { "type": "(value: string) => void", "notes": "Select-mode callback." },
122
+ "placeholder": { "type": "ReactNode", "default": "Select..." },
123
+ "align": { "type": "enum", "values": ["left", "right"], "default": "left" },
124
+ "error": { "type": "boolean", "default": false },
125
+ "fullWidth": { "type": "boolean", "default": true },
126
+ "id": { "type": "string" },
127
+ "onOpen": { "type": "() => void" },
128
+ "onClose": { "type": "() => void" },
129
+ "classes": { "type": "Partial<DropdownClasses>", "shape": ["root", "trigger", "triggerLabel", "chevron", "menu", "item"] }
130
+ }
131
+ },
132
+ "DropdownItem": {
133
+ "import": "import { DropdownItem } from '@overdoser/react-toolkit'",
134
+ "element": "button",
135
+ "extendsNativeProps": "button",
136
+ "forwardsRef": true,
137
+ "props": {
138
+ "disabled": { "type": "boolean", "default": false }
139
+ }
140
+ },
141
+ "Popover": {
142
+ "import": "import { Popover } from '@overdoser/react-toolkit'",
143
+ "element": "div",
144
+ "props": {
145
+ "trigger": { "type": "ReactNode", "required": true },
146
+ "content": { "type": "ReactNode", "required": true },
147
+ "position": { "type": "enum", "values": ["top", "bottom", "left", "right"], "default": "bottom" },
148
+ "open": { "type": "boolean", "notes": "Controlled mode." },
149
+ "onOpenChange": { "type": "(open: boolean) => void" },
150
+ "classes": { "type": "Partial<PopoverClasses>", "shape": ["root", "trigger", "popover"] }
151
+ },
152
+ "behavior": {
153
+ "closesOn": ["outside click", "Escape"],
154
+ "childrenTyped": "never — pass via `content` prop"
155
+ }
156
+ },
157
+ "Modal": {
158
+ "import": "import { Modal } from '@overdoser/react-toolkit'",
159
+ "element": "div (portal at document.body)",
160
+ "compoundComponents": ["Modal.Header", "Modal.Body", "Modal.Footer"],
161
+ "props": {
162
+ "open": { "type": "boolean", "required": true },
163
+ "onClose": { "type": "() => void", "required": true },
164
+ "closeOnBackdrop": { "type": "boolean", "default": true },
165
+ "closeOnEscape": { "type": "boolean", "default": true },
166
+ "size": { "type": "enum", "values": ["sm", "md", "lg", "fullscreen"], "default": "md" },
167
+ "aria-label": { "type": "string" },
168
+ "aria-labelledby": { "type": "string" },
169
+ "classes": { "type": "Partial<ModalClasses>", "shape": ["backdrop", "modal", "header", "closeButton", "body", "footer"] }
170
+ },
171
+ "subComponents": {
172
+ "Modal.Header": {
173
+ "props": {
174
+ "children": { "type": "ReactNode", "required": true },
175
+ "onClose": { "type": "() => void", "notes": "When set, renders an × close button." }
176
+ }
177
+ },
178
+ "Modal.Body": { "props": { "children": { "type": "ReactNode", "required": true } } },
179
+ "Modal.Footer": { "props": { "children": { "type": "ReactNode", "required": true } } }
180
+ },
181
+ "behavior": ["body scroll lock", "focus trap", "restores prior focus on close"]
182
+ },
183
+ "Form": {
184
+ "import": "import { Form } from '@overdoser/react-toolkit'",
185
+ "requires": "react-hook-form",
186
+ "generic": "T extends FieldValues",
187
+ "props": {
188
+ "form": { "type": "UseFormReturn<T>", "required": true, "notes": "The result of useForm()." },
189
+ "onSubmit": { "type": "SubmitHandler<T>", "required": true },
190
+ "errors": { "type": "ReactNode[]", "notes": "Top-of-form errors rendered above children with role=alert." }
191
+ }
192
+ },
193
+ "FormField": {
194
+ "import": "import { FormField } from '@overdoser/react-toolkit'",
195
+ "requires": "react-hook-form (must be inside <Form>)",
196
+ "props": {
197
+ "name": { "type": "string", "required": true },
198
+ "label": { "type": "ReactNode" },
199
+ "helperText": { "type": "ReactNode" },
200
+ "required": { "type": "boolean", "notes": "Adds a * indicator next to the label." },
201
+ "rules": { "type": "Record<string, unknown>", "notes": "Passed to react-hook-form's useController." },
202
+ "children": { "type": "ReactElement", "required": true, "notes": "Exactly one input element. FormField clones it to inject value/onChange/id/name/error/aria-*." },
203
+ "classes": { "type": "Partial<FormFieldClasses>", "shape": ["field", "label", "error", "helperText"] }
204
+ },
205
+ "behavior": {
206
+ "supportedChildren": ["Input", "Textarea", "Select", "Dropdown (select-mode)", "Checkbox", "Radio", "RadioGroup"],
207
+ "bridges": {
208
+ "onValueChange": "Bridged to react-hook-form onChange (Select single, Dropdown).",
209
+ "onValuesChange": "Bridged to react-hook-form onChange (Select multi).",
210
+ "checked": "Auto-set from value when value is boolean (Checkbox)."
211
+ }
212
+ }
213
+ },
214
+ "FormRow": {
215
+ "import": "import { FormRow } from '@overdoser/react-toolkit'",
216
+ "props": { "children": { "type": "ReactNode", "required": true } }
217
+ },
218
+ "Input": {
219
+ "import": "import { Input } from '@overdoser/react-toolkit'",
220
+ "element": "input",
221
+ "extendsNativeProps": "input (without prefix)",
222
+ "forwardsRef": true,
223
+ "props": {
224
+ "inputSize": { "type": "enum", "values": ["sm", "md", "lg"], "default": "md", "notes": "Toolkit size scale; independent of the native `size` HTML attribute." },
225
+ "error": { "type": "boolean", "default": false },
226
+ "prefix": { "type": "ReactNode" },
227
+ "suffix": { "type": "ReactNode" },
228
+ "classes": { "type": "Partial<InputClasses>", "shape": ["root", "wrapper", "prefix", "suffix"] }
229
+ }
230
+ },
231
+ "Select": {
232
+ "import": "import { Select } from '@overdoser/react-toolkit'",
233
+ "element": "select (or custom for searchable/multi)",
234
+ "extendsNativeProps": "select (without value/onChange)",
235
+ "forwardsRef": true,
236
+ "modes": {
237
+ "native": "Default — wraps a native <select>.",
238
+ "searchable": "When `searchable=true`. Custom dropdown with text filter.",
239
+ "multi": "When `multiple=true`. Chip-based multi with text filter."
240
+ },
241
+ "props": {
242
+ "inputSize": { "type": "enum", "values": ["sm", "md", "lg"], "default": "md" },
243
+ "error": { "type": "boolean", "default": false },
244
+ "options": { "type": "{ value: string; label: string; content?: ReactNode; disabled?: boolean }[]" },
245
+ "placeholder": { "type": "string", "default": "Select..." },
246
+ "searchable": { "type": "boolean", "default": false },
247
+ "searchPlaceholder": { "type": "string", "default": "Search..." },
248
+ "multiple": { "type": "boolean", "default": false },
249
+ "value": { "type": "string | string[]", "notes": "string for single-mode, string[] for multi." },
250
+ "onChange": { "type": "(e: ChangeEvent<HTMLSelectElement>) => void", "notes": "Native + searchable (synthetic for searchable)." },
251
+ "onValueChange": { "type": "(value: string) => void", "notes": "Single-mode value-only callback." },
252
+ "onValuesChange": { "type": "(values: string[]) => void", "notes": "Multi-mode callback." },
253
+ "clearSearchOnSelect": { "type": "boolean", "default": true, "notes": "Multi-mode only." },
254
+ "classes": { "type": "Partial<SelectClasses>", "shape": ["wrapper", "root", "arrow", "search", "menu", "item", "chip", "chipRemove"] }
255
+ }
256
+ },
257
+ "Checkbox": {
258
+ "import": "import { Checkbox } from '@overdoser/react-toolkit'",
259
+ "element": "input[type=checkbox] inside label",
260
+ "extendsNativeProps": "input",
261
+ "forwardsRef": true,
262
+ "props": {
263
+ "label": { "type": "ReactNode" },
264
+ "indeterminate": { "type": "boolean", "default": false },
265
+ "classes": { "type": "Partial<CheckboxClasses>", "shape": ["root", "input", "label"] }
266
+ }
267
+ },
268
+ "Radio": {
269
+ "import": "import { Radio } from '@overdoser/react-toolkit'",
270
+ "element": "input[type=radio] inside label",
271
+ "extendsNativeProps": "input (without type)",
272
+ "forwardsRef": true,
273
+ "props": {
274
+ "value": { "type": "string", "required": true },
275
+ "label": { "type": "ReactNode" },
276
+ "classes": { "type": "Partial<RadioClasses>", "shape": ["root", "input", "label"] }
277
+ },
278
+ "behavior": "Inside a <RadioGroup>, name/checked/onChange come from context — do not pass them on Radio."
279
+ },
280
+ "RadioGroup": {
281
+ "import": "import { RadioGroup } from '@overdoser/react-toolkit'",
282
+ "element": "div[role=radiogroup]",
283
+ "props": {
284
+ "name": { "type": "string", "required": true },
285
+ "value": { "type": "string", "notes": "Controlled selected value." },
286
+ "onChange": { "type": "(value: string) => void" },
287
+ "id": { "type": "string" },
288
+ "aria-label": { "type": "string" },
289
+ "aria-labelledby": { "type": "string" },
290
+ "required": { "type": "boolean" }
291
+ }
292
+ },
293
+ "Textarea": {
294
+ "import": "import { Textarea } from '@overdoser/react-toolkit'",
295
+ "element": "textarea",
296
+ "extendsNativeProps": "textarea",
297
+ "forwardsRef": true,
298
+ "props": {
299
+ "inputSize": { "type": "enum", "values": ["sm", "md", "lg"], "default": "md" },
300
+ "error": { "type": "boolean", "default": false },
301
+ "resize": { "type": "enum", "values": ["none", "vertical", "horizontal", "both"], "default": "vertical", "notes": "Ignored when autoExpand=true." },
302
+ "autoExpand": { "type": "boolean", "default": true, "notes": "When true, auto-grows height to fit content." }
303
+ }
304
+ }
305
+ },
306
+ "hooks": {
307
+ "useClickOutside": {
308
+ "import": "import { useClickOutside } from '@overdoser/react-toolkit'",
309
+ "signature": "useClickOutside(ref: RefObject<HTMLElement | null>, handler: () => void, enabled?: boolean): void",
310
+ "params": {
311
+ "ref": "Element to monitor.",
312
+ "handler": "Called on mousedown outside the ref element.",
313
+ "enabled": "Pause the listener without unmounting (default true)."
314
+ }
315
+ },
316
+ "useFocusTrap": {
317
+ "import": "import { useFocusTrap } from '@overdoser/react-toolkit'",
318
+ "signature": "useFocusTrap(ref: RefObject<HTMLElement | null>, active: boolean): void",
319
+ "behavior": "Cycles Tab / Shift+Tab inside ref. Restores prior focus on deactivation."
320
+ },
321
+ "useKeyboard": {
322
+ "import": "import { useKeyboard } from '@overdoser/react-toolkit'",
323
+ "signature": "useKeyboard(handlers: Record<string, (e: KeyboardEvent) => void>, active?: boolean): void",
324
+ "params": {
325
+ "handlers": "Map keyed by KeyboardEvent.key (e.g., 'Escape', 'Enter').",
326
+ "active": "Default true. Pass false to disable without unmounting."
327
+ }
328
+ },
329
+ "useTableSort": {
330
+ "import": "import { useTableSort } from '@overdoser/react-toolkit'",
331
+ "signature": "useTableSort<T>(data: T[], initialSort?: SortConfig[])",
332
+ "returns": {
333
+ "sortedData": "T[]",
334
+ "sortConfig": "SortConfig[]",
335
+ "requestSort": "(key) => void — single-column; cycles asc → desc → cleared.",
336
+ "requestMultiSort": "(key) => void — adds/toggles a column in multi-sort.",
337
+ "resetSort": "() => void"
338
+ },
339
+ "notes": "Only needed if you want to manage sort state outside <Table>."
340
+ }
341
+ },
342
+ "conventions": [
343
+ "Inputs use `inputSize` for the toolkit size scale; the native `size` HTML attribute is independent.",
344
+ "Typography color accepts only the preset tokens; for arbitrary colors use `style` or `className`.",
345
+ "Dropdown has two modes: with `options` + `value` + `onChange` it acts as a select input (the `trigger` prop is unused); without `options`, pass `trigger` and `<DropdownItem>` children.",
346
+ "Popover content goes through the `content` prop.",
347
+ "Modal content lives in Modal.Header / Modal.Body / Modal.Footer compound components.",
348
+ "FormField clones a single child element to inject form props — pass exactly one input element.",
349
+ "Server-side Table mode (when `onSort` is provided): the consumer is responsible for sorting and slicing `data` server-side.",
350
+ "Theme stylesheet is imported separately: '@overdoser/react-toolkit/theme.css'."
351
+ ],
352
+ "recipes": [
353
+ "recipes/login-form.tsx",
354
+ "recipes/paginated-table.tsx",
355
+ "recipes/server-side-table.tsx",
356
+ "recipes/confirm-modal.tsx",
357
+ "recipes/searchable-multi-select.tsx",
358
+ "recipes/dropdown-menu.tsx"
359
+ ]
360
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overdoser/react-toolkit",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.mjs",
@@ -0,0 +1,63 @@
1
+ import { useState } from 'react';
2
+ import { Modal, Button, Typography } from '@overdoser/react-toolkit';
3
+
4
+ interface ConfirmModalProps {
5
+ open: boolean;
6
+ title: string;
7
+ message: React.ReactNode;
8
+ confirmLabel?: string;
9
+ cancelLabel?: string;
10
+ destructive?: boolean;
11
+ onConfirm: () => void | Promise<void>;
12
+ onClose: () => void;
13
+ }
14
+
15
+ export function ConfirmModal({
16
+ open,
17
+ title,
18
+ message,
19
+ confirmLabel = 'Confirm',
20
+ cancelLabel = 'Cancel',
21
+ destructive = false,
22
+ onConfirm,
23
+ onClose,
24
+ }: ConfirmModalProps) {
25
+ const [busy, setBusy] = useState(false);
26
+
27
+ const handleConfirm = async () => {
28
+ setBusy(true);
29
+ try {
30
+ await onConfirm();
31
+ onClose();
32
+ } finally {
33
+ setBusy(false);
34
+ }
35
+ };
36
+
37
+ return (
38
+ <Modal open={open} onClose={onClose} size="sm" closeOnBackdrop={!busy} closeOnEscape={!busy}>
39
+ <Modal.Header onClose={busy ? undefined : onClose}>
40
+ <Typography variant="h3" weight="semibold">
41
+ {title}
42
+ </Typography>
43
+ </Modal.Header>
44
+ <Modal.Body>
45
+ <Typography variant="p" color="muted">
46
+ {message}
47
+ </Typography>
48
+ </Modal.Body>
49
+ <Modal.Footer>
50
+ <Button variant="ghost" onClick={onClose} disabled={busy}>
51
+ {cancelLabel}
52
+ </Button>
53
+ <Button
54
+ variant={destructive ? 'danger' : 'primary'}
55
+ onClick={handleConfirm}
56
+ loading={busy}
57
+ >
58
+ {confirmLabel}
59
+ </Button>
60
+ </Modal.Footer>
61
+ </Modal>
62
+ );
63
+ }
@@ -0,0 +1,36 @@
1
+ import { Dropdown, DropdownItem } from '@overdoser/react-toolkit';
2
+
3
+ interface RowActionsProps {
4
+ rowId: string;
5
+ onEdit: (id: string) => void;
6
+ onArchive: (id: string) => void;
7
+ onDelete: (id: string) => void;
8
+ canDelete?: boolean;
9
+ }
10
+
11
+ export function RowActions({
12
+ rowId,
13
+ onEdit,
14
+ onArchive,
15
+ onDelete,
16
+ canDelete = true,
17
+ }: RowActionsProps) {
18
+ return (
19
+ <Dropdown
20
+ align="right"
21
+ fullWidth={false}
22
+ trigger={
23
+ <span aria-label="Row actions">
24
+ {/* Replace with an icon component from your codebase */}
25
+
26
+ </span>
27
+ }
28
+ >
29
+ <DropdownItem onClick={() => onEdit(rowId)}>Edit</DropdownItem>
30
+ <DropdownItem onClick={() => onArchive(rowId)}>Archive</DropdownItem>
31
+ <DropdownItem disabled={!canDelete} onClick={() => onDelete(rowId)}>
32
+ Delete
33
+ </DropdownItem>
34
+ </Dropdown>
35
+ );
36
+ }
@@ -0,0 +1,51 @@
1
+ import { useForm } from 'react-hook-form';
2
+ import { Form, FormField, Input, Button } from '@overdoser/react-toolkit';
3
+
4
+ interface LoginFields {
5
+ email: string;
6
+ password: string;
7
+ }
8
+
9
+ export function LoginForm({ onLogin }: { onLogin: (values: LoginFields) => Promise<void> }) {
10
+ const form = useForm<LoginFields>({
11
+ defaultValues: { email: '', password: '' },
12
+ mode: 'onTouched',
13
+ });
14
+
15
+ return (
16
+ <Form
17
+ form={form}
18
+ onSubmit={async (values) => {
19
+ await onLogin(values);
20
+ }}
21
+ >
22
+ <FormField
23
+ name="email"
24
+ label="Email"
25
+ required
26
+ rules={{
27
+ required: 'Email is required',
28
+ pattern: { value: /^\S+@\S+\.\S+$/, message: 'Enter a valid email' },
29
+ }}
30
+ >
31
+ <Input type="email" autoComplete="email" />
32
+ </FormField>
33
+
34
+ <FormField
35
+ name="password"
36
+ label="Password"
37
+ required
38
+ rules={{
39
+ required: 'Password is required',
40
+ minLength: { value: 8, message: 'Min 8 characters' },
41
+ }}
42
+ >
43
+ <Input type="password" autoComplete="current-password" />
44
+ </FormField>
45
+
46
+ <Button type="submit" variant="primary" loading={form.formState.isSubmitting} fullWidth>
47
+ Sign in
48
+ </Button>
49
+ </Form>
50
+ );
51
+ }
@@ -0,0 +1,48 @@
1
+ import { useState } from 'react';
2
+ import { Table, type ColumnDef } from '@overdoser/react-toolkit';
3
+
4
+ interface User extends Record<string, unknown> {
5
+ id: string;
6
+ name: string;
7
+ email: string;
8
+ role: 'admin' | 'member';
9
+ lastSeen: string;
10
+ }
11
+
12
+ const columns: ColumnDef<User>[] = [
13
+ { key: 'name', header: 'Name', sortable: true },
14
+ { key: 'email', header: 'Email', sortable: true },
15
+ { key: 'role', header: 'Role', sortable: true, width: 120 },
16
+ {
17
+ key: 'lastSeen',
18
+ header: 'Last seen',
19
+ sortable: true,
20
+ align: 'right',
21
+ width: 160,
22
+ render: (value) => new Date(value as string).toLocaleString(),
23
+ },
24
+ ];
25
+
26
+ export function PaginatedTable({ users }: { users: User[] }) {
27
+ const [page, setPage] = useState(1);
28
+ const [pageSize, setPageSize] = useState(10);
29
+
30
+ return (
31
+ <Table
32
+ data={users}
33
+ columns={columns}
34
+ rowKey="id"
35
+ striped
36
+ hoverable
37
+ pagination={{
38
+ page,
39
+ pageSize,
40
+ onPageChange: (nextPage, nextPageSize) => {
41
+ setPage(nextPage);
42
+ setPageSize(nextPageSize);
43
+ },
44
+ }}
45
+ emptyMessage="No users yet."
46
+ />
47
+ );
48
+ }
@@ -0,0 +1,42 @@
1
+ import { useForm } from 'react-hook-form';
2
+ import { Form, FormField, Select, Button } from '@overdoser/react-toolkit';
3
+
4
+ interface TagFormValues {
5
+ tags: string[];
6
+ }
7
+
8
+ const TAG_OPTIONS = [
9
+ { value: 'react', label: 'React' },
10
+ { value: 'typescript', label: 'TypeScript' },
11
+ { value: 'rust', label: 'Rust' },
12
+ { value: 'go', label: 'Go' },
13
+ { value: 'python', label: 'Python' },
14
+ { value: 'css', label: 'CSS' },
15
+ { value: 'graphql', label: 'GraphQL' },
16
+ ];
17
+
18
+ export function TagPicker({ onSave }: { onSave: (tags: string[]) => Promise<void> }) {
19
+ const form = useForm<TagFormValues>({ defaultValues: { tags: [] } });
20
+
21
+ return (
22
+ <Form
23
+ form={form}
24
+ onSubmit={async ({ tags }) => {
25
+ await onSave(tags);
26
+ }}
27
+ >
28
+ <FormField
29
+ name="tags"
30
+ label="Tags"
31
+ helperText="Pick all that apply"
32
+ rules={{ validate: (v: string[]) => v.length > 0 || 'Pick at least one tag' }}
33
+ >
34
+ <Select multiple options={TAG_OPTIONS} placeholder="Choose tags…" />
35
+ </FormField>
36
+
37
+ <Button type="submit" loading={form.formState.isSubmitting}>
38
+ Save tags
39
+ </Button>
40
+ </Form>
41
+ );
42
+ }