@overdoser/react-toolkit 0.0.1 → 0.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/AGENTS.md +109 -0
- package/components/Dropdown/Dropdown.d.ts +3 -2
- package/components/Form/Form.d.ts +2 -1
- package/components/Form/Form.stories.d.ts +3 -0
- package/components/Form/FormRow.d.ts +7 -0
- package/components/Form/index.d.ts +2 -0
- package/components/Modal/Modal.d.ts +2 -0
- package/components/inputs/Radio/Radio.d.ts +5 -1
- package/components/inputs/Select/Select.d.ts +15 -1
- package/components/inputs/Select/Select.stories.d.ts +9 -0
- package/components/inputs/Textarea/Textarea.d.ts +1 -0
- package/hooks/useClickOutside.d.ts +1 -1
- package/index.css +1 -1
- package/index.d.ts +2 -2
- package/index.js +1247 -716
- package/llms.txt +480 -0
- package/manifest.json +362 -0
- package/package.json +1 -1
- package/recipes/confirm-modal.tsx +63 -0
- package/recipes/dropdown-menu.tsx +36 -0
- package/recipes/login-form.tsx +51 -0
- package/recipes/paginated-table.tsx +48 -0
- package/recipes/searchable-multi-select.tsx +42 -0
- package/recipes/server-side-table.tsx +92 -0
package/manifest.json
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
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": "Native color attribute is omitted in favor of these presets." },
|
|
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(). NOTE: prop is `form`, not `formMethods`." },
|
|
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": "Named `inputSize` so the native `size` attribute is preserved." },
|
|
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
|
+
"gotchas": [
|
|
343
|
+
"Form prop is `form`, not `formMethods`.",
|
|
344
|
+
"Button prop is `loadingStyle`, not `loadingType`.",
|
|
345
|
+
"Input/Select/Textarea size prop is `inputSize`, not `size` — native size attribute is preserved.",
|
|
346
|
+
"Typography color is restricted to enum presets; native `color` HTML attribute is omitted.",
|
|
347
|
+
"Dropdown is dual-mode: `options` triggers select-mode and `trigger` is ignored.",
|
|
348
|
+
"Popover.children is typed `never` — content goes through the `content` prop.",
|
|
349
|
+
"Modal contents must be wrapped in Modal.Header/Body/Footer.",
|
|
350
|
+
"FormField clones a single child element to inject form props — don't wire value/onChange manually.",
|
|
351
|
+
"Server-side Table mode (onSort provided) means Table renders data verbatim — no internal sort or pagination slicing.",
|
|
352
|
+
"Theme stylesheet must be imported separately: '@overdoser/react-toolkit/theme.css'."
|
|
353
|
+
],
|
|
354
|
+
"recipes": [
|
|
355
|
+
"recipes/login-form.tsx",
|
|
356
|
+
"recipes/paginated-table.tsx",
|
|
357
|
+
"recipes/server-side-table.tsx",
|
|
358
|
+
"recipes/confirm-modal.tsx",
|
|
359
|
+
"recipes/searchable-multi-select.tsx",
|
|
360
|
+
"recipes/dropdown-menu.tsx"
|
|
361
|
+
]
|
|
362
|
+
}
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|