@overdoser/react-toolkit 0.0.8 → 0.0.10

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/llms.txt CHANGED
@@ -193,6 +193,7 @@ Two distinct usage modes — pick one:
193
193
  - `align?: 'left' | 'right'` — default `'left'`. Menu alignment relative to trigger.
194
194
  - `error?: boolean` — default `false`
195
195
  - `fullWidth?: boolean` — default `true`
196
+ - `portal?: boolean` — default `true`. The open menu is rendered in a portal at `document.body` so it escapes `overflow: hidden` ancestors (Modal body, scroll containers). Uses `--crk-z-floating` (1060) which sits above `--crk-z-modal` (1050). Set to `false` if you need the menu in the trigger's DOM subtree (e.g., parent-scoped CSS theming).
196
197
  - `id?: string`
197
198
  - `onOpen?: () => void`
198
199
  - `onClose?: () => void`
@@ -367,6 +368,10 @@ Props:
367
368
  - `onValueChange?: (value: string) => void` — single-mode value-only callback.
368
369
  - `onValuesChange?: (values: string[]) => void` — multi-mode callback.
369
370
  - `clearSearchOnSelect?: boolean` — default `true`. Multi-mode only.
371
+ - `allowCreate?: boolean` — default `false`. Multi-mode only. Adds a "Create '<query>'" row when the search query doesn't match an existing option. Pressing Enter (or clicking the row) pushes the trimmed query into `onValuesChange` and fires `onCreate`.
372
+ - `onCreate?: (value: string) => void` — Multi-mode only, paired with `allowCreate`. Fires with the new value; consumers typically persist it (e.g. push it back into `options`).
373
+ - `createLabel?: (query: string) => ReactNode` — Multi-mode only. Custom render for the create row. Default: `Create "<query>"`.
374
+ - `portal?: boolean` — default `true`. Applies to `searchable` + `multiple` modes (native `<select>` is unaffected). The open menu is rendered in a portal at `document.body` with `position: fixed` + `--crk-z-floating` (1060), so it escapes `overflow: hidden` ancestors and sits above `--crk-z-modal` (1050). Means a `<Select>` inside a `<Modal>` works without needing to size the modal. Pass `false` if you need the menu inside the trigger's subtree.
370
375
  - `classes?: Partial<SelectClasses>` where `SelectClasses = { wrapper, root, arrow, search, menu, item, chip, chipRemove }`
371
376
 
372
377
  Searchable example:
package/manifest.json CHANGED
@@ -124,6 +124,7 @@
124
124
  "align": { "type": "enum", "values": ["left", "right"], "default": "left" },
125
125
  "error": { "type": "boolean", "default": false },
126
126
  "fullWidth": { "type": "boolean", "default": true },
127
+ "portal": { "type": "boolean", "default": true, "notes": "Renders the open menu in a portal at document.body using --crk-z-floating (1060). Escapes overflow:hidden ancestors and sits above modals. Set false to keep the menu in the trigger's subtree." },
127
128
  "id": { "type": "string" },
128
129
  "onOpen": { "type": "() => void" },
129
130
  "onClose": { "type": "() => void" },
@@ -253,6 +254,10 @@
253
254
  "onValueChange": { "type": "(value: string) => void", "notes": "Single-mode value-only callback." },
254
255
  "onValuesChange": { "type": "(values: string[]) => void", "notes": "Multi-mode callback." },
255
256
  "clearSearchOnSelect": { "type": "boolean", "default": true, "notes": "Multi-mode only." },
257
+ "allowCreate": { "type": "boolean", "default": false, "notes": "Multi-mode only. Adds a 'Create <query>' row when the query doesn't match an existing option; pressing Enter (or clicking it) pushes the trimmed query into onValuesChange and fires onCreate." },
258
+ "onCreate": { "type": "(value: string) => void", "notes": "Multi-mode only. Paired with allowCreate." },
259
+ "createLabel": { "type": "(query: string) => ReactNode", "notes": "Multi-mode only. Custom render for the Create row." },
260
+ "portal": { "type": "boolean", "default": true, "notes": "Applies to searchable + multiple modes. Renders the open menu in a portal at document.body using --crk-z-floating (1060) — escapes overflow:hidden ancestors and sits above modals. Set false to keep the menu in the trigger's subtree." },
256
261
  "classes": { "type": "Partial<SelectClasses>", "shape": ["wrapper", "root", "arrow", "search", "menu", "item", "chip", "chipRemove"] }
257
262
  }
258
263
  },
@@ -659,6 +664,8 @@
659
664
  "recipes/interactive-line-chart.tsx",
660
665
  "recipes/interactive-area-chart.tsx",
661
666
  "recipes/interactive-pie-chart.tsx",
662
- "recipes/trading-chart.tsx"
667
+ "recipes/trading-chart.tsx",
668
+ "recipes/tag-input.tsx",
669
+ "recipes/multi-select-in-modal.tsx"
663
670
  ]
664
671
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overdoser/react-toolkit",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -0,0 +1,68 @@
1
+ import { useState } from 'react';
2
+ import { Button, Modal, Select } from '@overdoser/react-toolkit';
3
+
4
+ const TAGS = [
5
+ { value: 'react', label: 'React' },
6
+ { value: 'typescript', label: 'TypeScript' },
7
+ { value: 'rust', label: 'Rust' },
8
+ { value: 'go', label: 'Go' },
9
+ { value: 'python', label: 'Python' },
10
+ { value: 'graphql', label: 'GraphQL' },
11
+ { value: 'css', label: 'CSS' },
12
+ { value: 'docker', label: 'Docker' },
13
+ { value: 'kubernetes', label: 'Kubernetes' },
14
+ { value: 'postgres', label: 'PostgreSQL' },
15
+ ];
16
+
17
+ /**
18
+ * Demonstrates that a multi-select dropdown menu renders correctly inside a
19
+ * `<Modal>`. By default `Select` portals its open menu to `document.body` so
20
+ * it escapes the modal's `overflow: hidden` clipping — meaning **the modal
21
+ * does NOT need to be sized large enough** to contain the menu. The menu
22
+ * uses `--crk-z-floating` (1060) which sits above `--crk-z-modal` (1050).
23
+ *
24
+ * If you have a reason to keep the menu inside the modal subtree (e.g.,
25
+ * parent-scoped CSS theming), pass `portal={false}` and size the modal
26
+ * generously.
27
+ */
28
+ export function MultiSelectInModalRecipe() {
29
+ const [open, setOpen] = useState(false);
30
+ const [values, setValues] = useState<string[]>([]);
31
+
32
+ return (
33
+ <>
34
+ <Button onClick={() => setOpen(true)}>Edit tags</Button>
35
+ <Modal open={open} onClose={() => setOpen(false)} size="sm">
36
+ <Modal.Header onClose={() => setOpen(false)}>Tags</Modal.Header>
37
+ <Modal.Body>
38
+ <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
39
+ <span style={{ fontSize: 12, color: 'var(--crk-color-text-muted)' }}>
40
+ Pick all that apply
41
+ </span>
42
+ <Select
43
+ multiple
44
+ options={TAGS}
45
+ value={values}
46
+ onValuesChange={setValues}
47
+ placeholder="Pick tags…"
48
+ />
49
+ </label>
50
+ </Modal.Body>
51
+ <Modal.Footer>
52
+ <Button variant="ghost" onClick={() => setOpen(false)}>
53
+ Cancel
54
+ </Button>
55
+ <Button
56
+ variant="primary"
57
+ onClick={() => {
58
+ // ...persist values, then:
59
+ setOpen(false);
60
+ }}
61
+ >
62
+ Save
63
+ </Button>
64
+ </Modal.Footer>
65
+ </Modal>
66
+ </>
67
+ );
68
+ }
@@ -0,0 +1,71 @@
1
+ import { useState } from 'react';
2
+ import { useForm } from 'react-hook-form';
3
+ import { Button, Form, FormField, Select } from '@overdoser/react-toolkit';
4
+
5
+ interface TagOption {
6
+ value: string;
7
+ label: string;
8
+ }
9
+
10
+ const INITIAL_TAGS: TagOption[] = [
11
+ { value: 'react', label: 'React' },
12
+ { value: 'typescript', label: 'TypeScript' },
13
+ { value: 'rust', label: 'Rust' },
14
+ { value: 'go', label: 'Go' },
15
+ { value: 'python', label: 'Python' },
16
+ ];
17
+
18
+ /**
19
+ * Tag-input pattern using `<Select multiple allowCreate>`. Users can pick
20
+ * from existing tags or type a new one + Enter to create it. The freshly
21
+ * created value is selected immediately; the consumer persists it back into
22
+ * `options` via `onCreate` so future searches surface it.
23
+ *
24
+ * Wired through `<FormField>` to show react-hook-form integration. The
25
+ * standalone uncontrolled form (no `<Form>`) works just as well — just use
26
+ * `useState` for the selected values + the options list.
27
+ */
28
+ export function TagInputRecipe({
29
+ onSubmit,
30
+ }: {
31
+ onSubmit?: (values: { tags: string[] }) => void;
32
+ }) {
33
+ const form = useForm<{ tags: string[] }>({ defaultValues: { tags: [] } });
34
+ const [options, setOptions] = useState<TagOption[]>(INITIAL_TAGS);
35
+
36
+ return (
37
+ <Form
38
+ form={form}
39
+ onSubmit={(values) => onSubmit?.(values)}
40
+ >
41
+ <FormField
42
+ name="tags"
43
+ label="Tags"
44
+ helperText="Pick existing tags or type a new one and press Enter"
45
+ >
46
+ <Select
47
+ multiple
48
+ allowCreate
49
+ options={options}
50
+ onCreate={(value) => {
51
+ // Persist the new tag back into the option list so future searches
52
+ // find it. Avoid duplicating if a race added it elsewhere.
53
+ setOptions((prev) =>
54
+ prev.some((o) => o.value === value) ? prev : [...prev, { value, label: value }],
55
+ );
56
+ }}
57
+ placeholder="Pick or create tags…"
58
+ searchPlaceholder="Type a tag…"
59
+ createLabel={(q) => (
60
+ <>
61
+ Add tag &ldquo;<strong>{q}</strong>&rdquo;
62
+ </>
63
+ )}
64
+ />
65
+ </FormField>
66
+ <Button type="submit" variant="primary">
67
+ Save
68
+ </Button>
69
+ </Form>
70
+ );
71
+ }