@timeax/form-palette 0.1.2 → 0.1.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/README.md ADDED
@@ -0,0 +1,2280 @@
1
+ # Index
2
+ Included Sections: 119
3
+ - [Form Palette](#form-palette) 123-343
4
+ - [Quick start](#quick-start) 133-161
5
+ - [Installation:](#installation) 135-161
6
+ - [Form](#form) 163-237
7
+ - [Minimal “local” form](#minimal-local-form) 167-200
8
+ - [Axios adapter](#axios-adapter) 202-217
9
+ - [Inertia adapter](#inertia-adapter) 219-237
10
+ - [InputField](#inputfield) 239-294
11
+ - [Basic usage](#basic-usage) 252-263
12
+ - [Helper slots](#helper-slots) 265-277
13
+ - [Standalone mode](#standalone-mode) 279-294
14
+ - [Adapters](#adapters) 296-331
15
+ - [Built-in adapter keys](#built-in-adapter-keys) 300-304
16
+ - [Registering adapters](#registering-adapters) 306-325
17
+ - [Adapter-specific props](#adapter-specific-props) 327-331
18
+ - [Recommended boot order](#recommended-boot-order) 333-343
19
+ - [Variant props + InputField usage](#variant-props-inputfield-usage) 346-1707
20
+ - [text](#text) 377-434
21
+ - [Variant props](#variant-props) 379-399
22
+ - [Sample usage (InputField)](#sample-usage-inputfield) 401-434
23
+ - [textarea](#textarea) 436-465
24
+ - [Variant props](#variant-props-1) 438-442
25
+ - [Sample usage (InputField)](#sample-usage-inputfield-1) 444-465
26
+ - [toggle-group](#toggle-group) 467-529
27
+ - [Variant props](#variant-props-2) 469-499
28
+ - [Sample usage (InputField)](#sample-usage-inputfield-2) 501-529
29
+ - [number](#number) 531-559
30
+ - [password](#password) 561-599
31
+ - [phone](#phone) 601-638
32
+ - [Slider (`slider`)](#slider-slider) 640-699
33
+ - [Props](#props) 644-679
34
+ - [Example](#example) 681-699
35
+ - [Toggle (`toggle`)](#toggle-toggle) 701-734
36
+ - [Props](#props-1) 705-719
37
+ - [Example](#example-1) 721-734
38
+ - [TreeSelect (`treeselect`)](#treeselect-treeselect) 736-849
39
+ - [Base props](#base-props) 740-773
40
+ - [Mode: default (`mode` omitted or "default")](#mode-default-mode-omitted-or-default) 775-793
41
+ - [Mode: button (`mode="button"`)](#mode-button-modebutton) 795-811
42
+ - [Example (default mode)](#example-default-mode) 813-833
43
+ - [Example (multiple + button mode)](#example-multiple-button-mode) 835-849
44
+ - [multi-select](#multi-select) 851-938
45
+ - [Variant props](#variant-props-3) 853-884
46
+ - [Mode and trigger props](#mode-and-trigger-props) 886-908
47
+ - [Sample usage](#sample-usage) 910-938
48
+ - [radio](#radio) 940-999
49
+ - [Variant props](#variant-props-4) 942-967
50
+ - [Supported option shapes](#supported-option-shapes) 969-972
51
+ - [Sample usage](#sample-usage-1) 974-999
52
+ - [select](#select) 1001-1077
53
+ - [Variant props](#variant-props-5) 1003-1048
54
+ - [Sample usage](#sample-usage-2) 1050-1077
55
+ - [checkbox](#checkbox) 1079-1147
56
+ - [chips](#chips) 1149-1202
57
+ - [color](#color) 1204-1234
58
+ - [date](#date) 1236-1283
59
+ - [Variant props](#variant-props-6) 1238-1256
60
+ - [Sample usage](#sample-usage-3) 1258-1283
61
+ - [keyvalue](#keyvalue) 1285-1335
62
+ - [Variant props](#variant-props-7) 1287-1307
63
+ - [Sample usage](#sample-usage-4) 1309-1335
64
+ - [editor](#editor) 1337-1381
65
+ - [Variant props](#variant-props-8) 1339-1356
66
+ - [Sample usage](#sample-usage-5) 1358-1381
67
+ - [file](#file) 1383-1460
68
+ - [Variant props](#variant-props-9) 1385-1411
69
+ - [Mode and trigger props](#mode-and-trigger-props-1) 1413-1435
70
+ - [Sample usage](#sample-usage-6) 1437-1460
71
+ - [json-editor](#json-editor) 1462-1542
72
+ - [Wrapper / trigger props](#wrapper-trigger-props) 1464-1482
73
+ - [Editor props (passed into the JSON editor)](#editor-props-passed-into-the-json-editor) 1484-1504
74
+ - [Sample usage](#sample-usage-7) 1506-1542
75
+ - [lister](#lister) 1544-1644
76
+ - [Data + mapping props](#data-mapping-props) 1546-1563
77
+ - [Selection + behaviour props](#selection-behaviour-props) 1565-1584
78
+ - [Trigger styling + container props](#trigger-styling-container-props) 1586-1614
79
+ - [Sample usage](#sample-usage-8) 1616-1644
80
+ - [custom](#custom) 1646-1707
81
+ - [Variant props](#variant-props-10) 1648-1663
82
+ - [Sample usage](#sample-usage-9) 1665-1707
83
+ - [Form Palette — `extra` entrypoint (v2)](#form-palette-extra-entrypoint-v2) 1709-1723
84
+ - [1) Lister (runtime)](#1-lister-runtime) 1725-1849
85
+ - [What is Lister?](#what-is-lister) 1727-1738
86
+ - [Building blocks (what you actually mount/call)](#building-blocks-what-you-actually-mountcall) 1740-1771
87
+ - [✅ `ListerProvider`](#listerprovider) 1742-1747
88
+ - [✅ `ListerUI`](#listerui) 1749-1752
89
+ - [✅ `useLister()`](#uselister) 1754-1764
90
+ - [✅ `useData()`](#usedata) 1766-1771
91
+ - [Quick start (recommended)](#quick-start-recommended) 1773-1818
92
+ - [Step 1 — Mount provider + UI once](#step-1-mount-provider-ui-once) 1775-1794
93
+ - [Step 2 — Open a picker imperatively](#step-2-open-a-picker-imperatively) 1796-1818
94
+ - [`ListerProvider` API](#listerprovider-api) 1820-1849
95
+ - [`ListerProviderHost`](#listerproviderhost) 1832-1842
96
+ - [Practical usage](#practical-usage) 1844-1849
97
+ - [2) `useData()` — deep dive (extremely important)](#2-usedata-deep-dive-extremely-important) 1851-2247
98
+ - [What `useData()` returns (mental model)](#what-usedata-returns-mental-model) 1865-1877
99
+ - [`UseDataOptions` (inputs)](#usedataoptions-inputs) 1879-1910
100
+ - [Selection config (`selection`)](#selection-config-selection) 1898-1910
101
+ - [Search modes: remote vs local vs hybrid](#search-modes-remote-vs-local-vs-hybrid) 1912-1935
102
+ - [✅ `remote` (default)](#remote-default) 1914-1919
103
+ - [✅ `local`](#local) 1921-1926
104
+ - [✅ `hybrid`](#hybrid) 1928-1935
105
+ - [Search targeting (`searchTarget`)](#search-targeting-searchtarget) 1937-1947
106
+ - [Core returned API (what you’ll use most)](#core-returned-api-what-youll-use-most) 1949-1984
107
+ - [Data + status](#data-status) 1951-1954
108
+ - [Search](#search) 1956-1960
109
+ - [Filters](#filters) 1962-1966
110
+ - [Fetch](#fetch) 1968-1971
111
+ - [Selection (when enabled)](#selection-when-enabled) 1973-1984
112
+ - [`useData()` — practical use cases (full examples)](#usedata-practical-use-cases-full-examples) 1986-2238
113
+ - [Use case A — Remote search list (simple)](#use-case-a-remote-search-list-simple) 1988-2030
114
+ - [Use case B — Local mode (fetch once, instant client filtering)](#use-case-b-local-mode-fetch-once-instant-client-filtering) 2032-2066
115
+ - [Use case C — Filters with `patchFilters` (remote/hybrid auto-fetch)](#use-case-c-filters-with-patchfilters-remotehybrid-auto-fetch) 2068-2121
116
+ - [Use case D — Constrain to a known allow-list (`searchTarget: mode="only"`)](#use-case-d-constrain-to-a-known-allow-list-searchtarget-modeonly) 2123-2159
117
+ - [Use case E — Custom multi-select UI (selection enabled)](#use-case-e-custom-multi-select-ui-selection-enabled) 2161-2209
118
+ - [Use case F — Advanced request shaping (`buildRequest`)](#use-case-f-advanced-request-shaping-buildrequest) 2211-2238
119
+ - [Practical tips](#practical-tips) 2240-2247
120
+ - [3) JsonEditor (overview)](#3-jsoneditor-overview) 2249-2280
121
+ - [Standalone usage](#standalone-usage) 2259-2280
122
+
123
+ # Form Palette
124
+
125
+ A small but powerful React form runtime built around three ideas:
126
+
127
+ 1. **A single `<Form />` shell** that wires up state, submission and validation.
128
+ 2. **`<InputField />`** as the universal “field wrapper” that renders a registered **variant** (text, number, select, json-editor, etc.) and handles label / description / errors / layout.
129
+ 3. **Adapters** that decide what “submit” means (`local`, `axios`, `inertia`, or your own).
130
+
131
+ ---
132
+
133
+ ## Quick start
134
+
135
+ #### Installation:
136
+
137
+ ```bash
138
+ npm install @timeax/form-palette
139
+ ```
140
+
141
+ ---
142
+
143
+ ```tsx
144
+ import * as React from "react";
145
+ import {
146
+ Form,
147
+ InputField,
148
+ registerCoreVariants,
149
+ registerAxiosAdapter,
150
+ registerInertiaAdapter,
151
+ } from "@timeax/form-palette";
152
+
153
+ // App boot (once)
154
+ registerCoreVariants();
155
+ registerAxiosAdapter();
156
+ await registerInertiaAdapter();
157
+ ```
158
+
159
+ > If you only use one adapter, only register the one you need.
160
+
161
+ ---
162
+
163
+ ## Form
164
+
165
+ `Form` is the main form component exported from the package entrypoint (it is `CoreShell`, re-exported as `Form`).
166
+
167
+ ### Minimal “local” form
168
+
169
+ Use `adapter="local"` when you want submission to be handled purely in JS.
170
+
171
+ ```tsx
172
+ function Example() {
173
+ return (
174
+ <Form
175
+ name="profile"
176
+ adapter="local"
177
+ onSubmit={(e) => {
178
+ // Current outbound snapshot
179
+ console.log(e.formData);
180
+ // You can also mutate outbound data via e.editData(...)
181
+ }}
182
+ >
183
+ <InputField
184
+ name="email"
185
+ variant="text"
186
+ label="Email"
187
+ required
188
+ />
189
+
190
+ <InputField
191
+ name="age"
192
+ variant="number"
193
+ label="Age"
194
+ />
195
+
196
+ <button type="submit">Save</button>
197
+ </Form>
198
+ );
199
+ }
200
+ ```
201
+
202
+ ### Axios adapter
203
+
204
+ ```tsx
205
+ <Form
206
+ name="profile"
207
+ adapter="axios"
208
+ url="/api/profile"
209
+ method="post"
210
+ onSubmitted={(form, payload) => {
211
+ console.log(payload);
212
+ }}
213
+ >
214
+ <InputField name="email" variant="text" label="Email" required />
215
+ <button type="submit">Save</button>
216
+ </Form>
217
+ ```
218
+
219
+ ### Inertia adapter
220
+
221
+ ```tsx
222
+ <Form
223
+ name="profile"
224
+ adapter="inertia"
225
+ url="/profile"
226
+ method="post"
227
+ onSubmitted={(form, payload) => {
228
+ // payload is the resolved inertia Page (or normalized error on failure)
229
+ console.log(payload);
230
+ }}
231
+ >
232
+ <InputField name="email" variant="text" label="Email" required />
233
+ <button type="submit">Save</button>
234
+ </Form>
235
+ ```
236
+
237
+ ---
238
+
239
+ ## InputField
240
+
241
+ `InputField` is the form runtime’s “field wrapper”. It:
242
+
243
+ * Pulls the chosen `variant` from the variant registry and renders it.
244
+ * Connects to form state when used inside `<Form />` (by `name`).
245
+ * Computes layout (label placement, helper slots, spacing, etc.) by combining:
246
+
247
+ * variant defaults
248
+ * host overrides
249
+ * optional `variant.resolveLayout(...)`
250
+ * Normalizes validation results into a consistent list of errors.
251
+
252
+ ### Basic usage
253
+
254
+ ```tsx
255
+ <InputField
256
+ name="username"
257
+ variant="text"
258
+ label="Username"
259
+ description="Public handle"
260
+ required
261
+ placeholder="@davy"
262
+ />
263
+ ```
264
+
265
+ ### Helper slots
266
+
267
+ Most helper UI (description, help text, error text, tags, etc.) is rendered through a layout graph.
268
+
269
+ ```tsx
270
+ <InputField
271
+ name="bio"
272
+ variant="textarea"
273
+ label="Bio"
274
+ helpText="Keep it short"
275
+ errorText=""
276
+ />
277
+ ```
278
+
279
+ ### Standalone mode
280
+
281
+ `InputField` can run without a surrounding `<Form />` (it will fall back to a self-managed field state).
282
+
283
+ ```tsx
284
+ <InputField
285
+ variant="text"
286
+ label="Standalone"
287
+ defaultValue="Hello"
288
+ onChange={({ value }) => {
289
+ console.log(value);
290
+ }}
291
+ />
292
+ ```
293
+
294
+ ---
295
+
296
+ ## Adapters
297
+
298
+ Adapters define how the form submits.
299
+
300
+ ### Built-in adapter keys
301
+
302
+ * `local` – no network; calls your callbacks.
303
+ * `axios` – HTTP submit via Axios.
304
+ * `inertia` – submit via Inertia.
305
+
306
+ ### Registering adapters
307
+
308
+ ```ts
309
+ import { registerAdapter } from "@timeax/form-palette";
310
+
311
+ registerAdapter("my-adapter", (config) => {
312
+ return {
313
+ submit() {
314
+ // fire-and-forget
315
+ },
316
+ async send() {
317
+ // resolve a result shape that matches AdapterOk<"my-adapter">
318
+ return { data: config.data } as any;
319
+ },
320
+ run() {
321
+ this.submit();
322
+ },
323
+ };
324
+ });
325
+ ```
326
+
327
+ ### Adapter-specific props
328
+
329
+ Some adapters expose additional props on `<Form />` (e.g. `url`, `method`, `config`).
330
+
331
+ ---
332
+
333
+ ## Recommended boot order
334
+
335
+ 1. Register variants (so InputField can resolve `variant` → component).
336
+ 2. Register the adapters you will use.
337
+ 3. Render forms.
338
+
339
+ ```ts
340
+ registerCoreVariants();
341
+ registerAxiosAdapter();
342
+ await registerInertiaAdapter();
343
+ ```
344
+
345
+
346
+ # Variant props + InputField usage
347
+
348
+ Below are the **variant-specific props** you can pass to `<InputField />` for:
349
+
350
+ * `text`
351
+ * `textarea`
352
+ * `toggle-group`
353
+ * `number`
354
+ * `phone`
355
+ * `password`
356
+ * `slider`
357
+ * `toggle`
358
+ * `treeselect`
359
+ * `multi-select`
360
+ * `select`
361
+ * `radio`
362
+ * `checkbox`
363
+ * `chips`
364
+ * `color`
365
+ * `date`
366
+ * `keyvalue`
367
+ * `editor`
368
+ * `json-editor`
369
+ * `file`
370
+ * `lister`
371
+ * `custom`
372
+
373
+ > Note: Some props like `value`, `onValue`, `error`, `disabled`, `readOnly`, `size`, `density` are typically **injected by the core runtime/InputField**. The tables focus on the *props you usually configure*.
374
+
375
+ ---
376
+
377
+ ## text
378
+
379
+ ### Variant props
380
+
381
+ | Prop | Description |
382
+ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
383
+ | `trim?: boolean` | **boolean** — If `true`, the value is trimmed **before validation** (visual input stays as typed). |
384
+ | `minLength?: number` | **number** — Minimum allowed length (after optional trimming). |
385
+ | `maxLength?: number` | **number** — Maximum allowed length (after optional trimming). |
386
+ | `joinControls?: boolean` | **boolean** — If `true` and there are controls, the input + controls share one box (border/radius/focus). |
387
+ | `extendBoxToControls?: boolean` | **boolean** — When `joinControls` is true, controls are either visually “inside” the same box (`true`) or separate (`false`). |
388
+ | `inputClassName?: string` | **string** — Extra classes for the **inner** `<input>` element (not the wrapper). |
389
+ | `prefix?: string` | **string** — Fixed prefix rendered as part of the visible input string (e.g. `₦`, `ID: `). |
390
+ | `suffix?: string` | **string** — Fixed suffix rendered as part of the visible input string (e.g. `%`, `kg`). |
391
+ | `stripPrefix?: boolean` | **boolean** — If `true` (default), the prefix is stripped from the emitted model value before calling `onValue` internally. |
392
+ | `stripSuffix?: boolean` | **boolean** — If `true` (default), the suffix is stripped from the emitted model value before calling `onValue` internally. |
393
+ | `mask?: string` | **string** — Mask pattern (PrimeReact style), e.g. `"99/99/9999"`, `"(999) 999-9999"`. |
394
+ | `maskDefinitions?: Record<string, RegExp>` | **Record** — Per-symbol slot definitions (kept for future custom engine; unused by current implementation). |
395
+ | `slotChar?: string` | **string** — Placeholder slot character (default `_`). |
396
+ | `autoClear?: boolean` | **boolean** — If `true`, “empty” masked values emit `""` instead of a fully-masked placeholder string. |
397
+ | `unmask?: "raw" \| "masked" \| boolean` | **union** — Controls whether the **model value** is raw vs masked. (`"raw"`/`true` ⇒ emit unmasked; `"masked"`/`false`/`undefined` ⇒ emit masked). |
398
+ | `maskInsertMode?: "stream" \| "caret"` | **union** — Reserved for future caret-mode logic (currently unused; kept for API compatibility). |
399
+ | `...inputProps` | All other standard `React.InputHTMLAttributes<HTMLInputElement>` (except `value`, `defaultValue`, `onChange`, `size`) are forwarded. |
400
+
401
+ ### Sample usage (InputField)
402
+
403
+ ```tsx
404
+ import { InputField } from "@timeax/form-palette";
405
+
406
+ export function ExampleText() {
407
+ return (
408
+ <InputField
409
+ variant="text"
410
+ name="phone"
411
+ label="Phone number"
412
+ description="We’ll use this for account recovery."
413
+
414
+ // semantic validation flags (core layer)
415
+ trim
416
+ minLength={11}
417
+ maxLength={11}
418
+
419
+ // mask + UI props (preset layer)
420
+ prefix="+234 "
421
+ mask="999 999 9999"
422
+ unmask="raw"
423
+ autoClear
424
+
425
+ // regular input attributes
426
+ type="tel"
427
+ inputMode="tel"
428
+ placeholder="803 123 4567"
429
+ />
430
+ );
431
+ }
432
+ ```
433
+
434
+ ---
435
+
436
+ ## textarea
437
+
438
+ ### Variant props
439
+
440
+ | Prop | Description |
441
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
442
+ | `...textareaProps` | The textarea variant primarily forwards props from the underlying UI `Textarea` component (`UiTextareaProps`), excluding `value`, `defaultValue`, and `onChange` because the variant emits changes via the form runtime. |
443
+
444
+ ### Sample usage (InputField)
445
+
446
+ ```tsx
447
+ import { InputField } from "@timeax/form-palette";
448
+
449
+ export function ExampleTextarea() {
450
+ return (
451
+ <InputField
452
+ variant="textarea"
453
+ name="bio"
454
+ label="About you"
455
+ helpText="Keep it short and clear."
456
+
457
+ // typical textarea attributes (usually supported via UiTextareaProps)
458
+ rows={4}
459
+ placeholder="Tell us a little about yourself..."
460
+ />
461
+ );
462
+ }
463
+ ```
464
+
465
+ ---
466
+
467
+ ## toggle-group
468
+
469
+ ### Variant props
470
+
471
+ | Prop | Description |
472
+ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
473
+ | `options: (ToggleOption \| string \| number \| boolean)[]` | Options for the toggle group. You can pass full option objects or primitive shorthand (primitives are normalized to `{ value: String(x), label: String(x) }`). |
474
+ | `multiple?: boolean` | **boolean** — If `true`, enables multi-select (value becomes an array of strings internally). |
475
+ | `variant?: "default" \| "outline"` | **union** — Visual style passed to the underlying ToggleGroup. |
476
+ | `layout?: "horizontal" \| "vertical" \| "grid"` | **union** — Layout mode. |
477
+ | `gridCols?: number` | **number** — Column count when `layout="grid"` (defaults to `2` in the component). |
478
+ | `fillWidth?: boolean` | **boolean** — If `true`, makes the group/items stretch to fill available width (adds `w-full` and related item sizing). |
479
+ | `optionValue?: string` | **string** — When `options` are custom objects, the property name to read `value` from (fallback: `obj.value`). |
480
+ | `optionLabel?: string` | **string** — When `options` are custom objects, the property name to read `label` from (fallback: `obj.label` or `String(value)`). |
481
+ | `optionIcon?: string` | **string** — When `options` are custom objects, the property name to read `icon` from (fallback: `obj.icon`). |
482
+ | `optionDisabled?: string` | **string** — When `options` are custom objects, the property name to read disabled flag from (fallback: `obj.disabled`). |
483
+ | `optionTooltip?: string` | **string** — When `options` are custom objects, the property name to read tooltip content from (fallback: `obj.tooltip`). |
484
+ | `optionMeta?: string` | **string** — When `options` are custom objects, the property name to read meta from (fallback: `obj.meta`). |
485
+ | `renderOption?: (option, isSelected) => React.ReactNode` | Custom renderer per option (receives normalized option + selected state). |
486
+ | `className?: string` | Class for the toggle group container. |
487
+ | `itemClassName?: string` | Base class applied to **all** toggle items. |
488
+ | `activeClassName?: string` | Class applied **only** to selected items (merged with default active styles). |
489
+ | `autoCap?: boolean` | If `true`, capitalizes the first letter of string labels. |
490
+ | `gap?: number` | Gap between buttons in **pixels** (applies to flex + grid layouts). |
491
+
492
+ **ToggleOption shape** (when not using primitive shorthand):
493
+
494
+ * `label: React.ReactNode`
495
+ * `value: string`
496
+ * `icon?: React.ReactNode`
497
+ * `disabled?: boolean`
498
+ * `tooltip?: React.ReactNode`
499
+ * `meta?: any`
500
+
501
+ ### Sample usage (InputField)
502
+
503
+ ```tsx
504
+ import { InputField } from "@timeax/form-palette";
505
+
506
+ export function ExampleToggleGroup() {
507
+ return (
508
+ <InputField
509
+ variant="toggle-group"
510
+ name="plan"
511
+ label="Choose a plan"
512
+ required
513
+
514
+ options={[
515
+ { value: "basic", label: "Basic" },
516
+ { value: "pro", label: "Pro" },
517
+ { value: "team", label: "Team", disabled: true, tooltip: "Coming soon" },
518
+ ]}
519
+ layout="horizontal"
520
+ variant="outline"
521
+ fillWidth
522
+ gap={8}
523
+ activeClassName="ring-1 ring-primary"
524
+ />
525
+ );
526
+ }
527
+ ```
528
+
529
+ ---
530
+
531
+ ## number
532
+
533
+ | Prop | Description |
534
+ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
535
+ | `showButtons` | When `true`, renders built-in step controls (±) alongside the number input. |
536
+ | `buttonLayout` | Layout for the step controls when `showButtons` is enabled. Supported layouts: `"stacked"` (vertical on the right) and `"inline"` (`-` left, `+` right). |
537
+ | `step` | Step amount used by the built-in controls and stepping logic (forwarded to the underlying number input). |
538
+ | `min` | Minimum numeric value constraint (used by the stepping logic and forwarded to the underlying number input). |
539
+ | `max` | Maximum numeric value constraint (used by the stepping logic and forwarded to the underlying number input). |
540
+
541
+ > Also accepts the rest of the underlying `InputNumberProps` (they’re forwarded to the number input).
542
+
543
+ **Sample**
544
+
545
+ ```tsx
546
+ <InputField
547
+ variant="number"
548
+ name="quantity"
549
+ label="Quantity"
550
+ description="How many items?"
551
+ min={1}
552
+ max={99}
553
+ step={1}
554
+ showButtons
555
+ buttonLayout="inline"
556
+ />
557
+ ```
558
+
559
+ ---
560
+
561
+ ## password
562
+
563
+ | Prop | Description |
564
+ | ----------------------- | --------------------------------------------------------- |
565
+ | `autoComplete` | Sets the input `autoComplete` hint for password managers. |
566
+ | `minLength` | Minimum allowed length (HTML constraint). |
567
+ | `maxLength` | Maximum allowed length (HTML constraint). |
568
+ | `revealToggle` | Show / hide the reveal toggle button. |
569
+ | `defaultRevealed` | Initial revealed state (defaults to hidden). |
570
+ | `onRevealChange` | Called when revealed state changes. |
571
+ | `renderToggleIcon` | Custom renderer for the toggle icon. |
572
+ | `toggleAriaLabel` | ARIA label for the toggle button. |
573
+ | `toggleButtonClassName` | ClassName hook for the toggle button. |
574
+ | `strengthMeter` | Enable the strength meter UI. |
575
+ | `ruleDefinitions` | Custom rule definitions used by the strength meter. |
576
+ | `ruleUses` | Which rules should be considered when computing strength. |
577
+ | `meterStyle` | Visual style of the strength meter. |
578
+ | `renderMeter` | Custom renderer for the full meter block. |
579
+ | `meterWrapperClassName` | ClassName hook for the meter wrapper. |
580
+
581
+ > Password inherits the *visual* props from the `text` variant (it reuses the text UI), but controls `type`, value wiring, and trailing controls internally.
582
+
583
+ **Sample**
584
+
585
+ ```tsx
586
+ <InputField
587
+ variant="password"
588
+ name="password"
589
+ label="Password"
590
+ required
591
+ minLength={8}
592
+ autoComplete="new-password"
593
+ revealToggle
594
+ strengthMeter
595
+ ruleUses={["minLen", "upper", "lower", "number", "symbol"]}
596
+ />
597
+ ```
598
+
599
+ ---
600
+
601
+ ## phone
602
+
603
+ | Prop | Description |
604
+ | ---------------------- | ------------------------------------------------------------------------------- |
605
+ | `countries` | List of allowed countries (and their dial codes) shown in the country selector. |
606
+ | `defaultCountryCode` | The default selected country code (e.g. `"NG"`). |
607
+ | `allowCountrySearch` | Enable searching in the country list. |
608
+ | `allowCountryClear` | Allow clearing the selected country. |
609
+ | `countryPlaceholder` | Placeholder text for the country selector. |
610
+ | `showFlag` | Show the flag in the country selector. |
611
+ | `showCountryName` | Show the country name in the selector / list. |
612
+ | `showDialCode` | Show dial codes in the country list. |
613
+ | `showSelectedDialCode` | Show the selected dial code next to the input. |
614
+ | `dialCodeDelimiter` | Delimiter between dial code and the input number (e.g. `" "`, `"-"`). |
615
+ | `valueMode` | Controls how the field value is emitted (e.g. E.164 vs local formats). |
616
+ | `mask` | Optional input mask (string or resolver function). |
617
+ | `lazy` | IMask “lazy” mode (placeholder chars hidden until typed). |
618
+ | `keepCharPositions` | IMask option to keep character positions stable. |
619
+ | `unmask` | How the underlying mask value is emitted (IMask option). |
620
+
621
+ > Phone inherits the *visual* props from the `text` variant, but controls value parsing/formatting and the country selector internally.
622
+
623
+ **Sample**
624
+
625
+ ```tsx
626
+ <InputField
627
+ variant="phone"
628
+ name="phone"
629
+ label="Phone number"
630
+ defaultCountryCode="NG"
631
+ allowCountrySearch
632
+ showSelectedDialCode
633
+ dialCodeDelimiter=" "
634
+ valueMode="e164"
635
+ />
636
+ ```
637
+
638
+ ---
639
+
640
+ ## Slider (`slider`)
641
+
642
+ Value type: `number | undefined`
643
+
644
+ ### Props
645
+
646
+ | Prop | Description |
647
+ | -------------------------- | ------------------------------------------------------ |
648
+ | `value` | Current slider value (number). |
649
+ | `onValue` | Called when the value changes. |
650
+ | `error` | Validation/error message for the field. |
651
+ | `disabled` | Disables interaction. |
652
+ | `readOnly` | Prevents changes but still displays the value. |
653
+ | `size` | Sizing preset for the control. |
654
+ | `density` | Density preset for the control (spacing). |
655
+ | `min` | Minimum value (default 0). |
656
+ | `max` | Maximum value (default 100). |
657
+ | `step` | Step size (default 1). |
658
+ | `showValue` | Show the current numeric value next to the slider. |
659
+ | `valuePlacement` | Where to render the value when `showValue` is enabled. |
660
+ | `formatValue` | Format the displayed value. |
661
+ | `className` | Root wrapper className. |
662
+ | `sliderClassName` | Slider track/handle className. |
663
+ | `valueClassName` | Value label className. |
664
+ | `leadingIcons` | Icons rendered before the slider/value. |
665
+ | `trailingIcons` | Icons rendered after the slider/value. |
666
+ | `icon` | Single icon (shorthand). |
667
+ | `iconGap` | Gap between icon(s) and content. |
668
+ | `leadingIconSpacing` | Spacing between multiple leading icons. |
669
+ | `trailingIconSpacing` | Spacing between multiple trailing icons. |
670
+ | `leadingControl` | Optional control element rendered before the slider. |
671
+ | `trailingControl` | Optional control element rendered after the slider. |
672
+ | `leadingControlClassName` | Wrapper className for the leading control. |
673
+ | `trailingControlClassName` | Wrapper className for the trailing control. |
674
+ | `joinControls` | Join controls visually to the slider box. |
675
+ | `extendBoxToControls` | Extend slider “box” background behind controls. |
676
+ | `controlVariant` | Variant for the +/- controls (if shown). |
677
+ | `controlStep` | Step used by +/- controls (falls back to `step`). |
678
+ | `controlDecrementIcon` | Custom icon node for decrement control. |
679
+ | `controlIncrementIcon` | Custom icon node for increment control. |
680
+
681
+ ### Example
682
+
683
+ ```tsx
684
+ <InputField
685
+ name="rating"
686
+ label="Rating"
687
+ variant="slider"
688
+ min={0}
689
+ max={100}
690
+ step={5}
691
+ showValue
692
+ valuePlacement="right"
693
+ formatValue={(v) => `${v}%`}
694
+ controlVariant="ghost"
695
+ controlStep={5}
696
+ />
697
+ ```
698
+
699
+ ---
700
+
701
+ ## Toggle (`toggle`)
702
+
703
+ Value type: `boolean | undefined`
704
+
705
+ ### Props
706
+
707
+ | Prop | Description |
708
+ | ---------------------- | ------------------------------------------ |
709
+ | `value` | Current toggle value (boolean). |
710
+ | `onValue` | Called when the value changes. |
711
+ | `error` | Validation/error message for the field. |
712
+ | `size` | Visual size of the switch. |
713
+ | `density` | Spacing density for the wrapper. |
714
+ | `onText` | Text shown when the value is `true`. |
715
+ | `offText` | Text shown when the value is `false`. |
716
+ | `label` | Optional label rendered beside the switch. |
717
+ | `containerClassName` | Wrapper className. |
718
+ | `switchRootClassName` | ClassName for the Switch root element. |
719
+ | `switchThumbClassName` | ClassName for the Switch thumb. |
720
+
721
+ ### Example
722
+
723
+ ```tsx
724
+ <InputField
725
+ name="enabled"
726
+ label="Enabled"
727
+ variant="toggle"
728
+ onText="On"
729
+ offText="Off"
730
+ density="sm"
731
+ />
732
+ ```
733
+
734
+ ---
735
+
736
+ ## TreeSelect (`treeselect`)
737
+
738
+ Value type: `TreeKey | TreeKey[] | undefined` (where `TreeKey` is `string | number`)
739
+
740
+ ### Base props
741
+
742
+ | Prop | Description |
743
+ | ----------------------- | ------------------------------------------------------------------------------ |
744
+ | `value` | Selected key(s). Single value is a key; multi is an array of keys. |
745
+ | `onValue` | Called when selection changes. |
746
+ | `error` | Validation/error message for the field. |
747
+ | `disabled` | Disables interaction. |
748
+ | `readOnly` | Prevents changes but still displays the selection. |
749
+ | `size` | Sizing preset for trigger/list rows. |
750
+ | `density` | Density preset for trigger/list rows. |
751
+ | `options` | Tree of options to render. |
752
+ | `multiple` | Allow selecting multiple keys (returns `TreeKey[]`). |
753
+ | `autoCap` | (Option mapping helper) Auto-capitalize generated labels when mapping options. |
754
+ | `optionLabel` | (Option mapping helper) Label accessor (key name or function). |
755
+ | `optionValue` | (Option mapping helper) Value accessor (key name or function). |
756
+ | `optionDescription` | (Option mapping helper) Description accessor. |
757
+ | `optionDisabled` | (Option mapping helper) Disabled accessor. |
758
+ | `optionIcon` | (Option mapping helper) Icon accessor. |
759
+ | `optionKey` | (Option mapping helper) Key accessor. |
760
+ | `searchable` | Enable search input in the dropdown. |
761
+ | `searchPlaceholder` | Placeholder text for the search input. |
762
+ | `emptyLabel` | Content shown when there are no options. |
763
+ | `emptySearchText` | Content shown when search returns no matches. |
764
+ | `clearable` | Show a clear/reset action. |
765
+ | `placeholder` | Text shown when nothing is selected. |
766
+ | `className` | Wrapper className for the whole field. |
767
+ | `triggerClassName` | ClassName for the trigger/button area. |
768
+ | `contentClassName` | ClassName for the dropdown content. |
769
+ | `renderOption` | Custom renderer for an option row. |
770
+ | `renderValue` | Custom renderer for the trigger's current value display. |
771
+ | `expandAll` | Expand all nodes by default. |
772
+ | `defaultExpandedValues` | Keys that should start expanded by default. |
773
+ | `leafOnly` | Restrict selection to leaf nodes only. |
774
+
775
+ ### Mode: default (`mode` omitted or "default")
776
+
777
+ | Prop | Description |
778
+ | -------------------------- | --------------------------------------------------------------- |
779
+ | `mode` | Omit or set to `'default'` to use the standard field trigger. |
780
+ | `button` | Optional custom trigger button renderer. |
781
+ | `selectedBadge` | Optional selected-count badge renderer. |
782
+ | `icon` | Single icon rendered near the trigger value. |
783
+ | `iconGap` | Gap between icon and content. |
784
+ | `leadingIcons` | One or more icons before the value. |
785
+ | `trailingIcons` | One or more icons after the value. |
786
+ | `leadingControl` | Custom control element before the trigger (e.g., clear button). |
787
+ | `trailingControl` | Custom control element after the trigger. |
788
+ | `leadingControlClassName` | ClassName for leading control wrapper. |
789
+ | `trailingControlClassName` | ClassName for trailing control wrapper. |
790
+ | `joinControls` | Visually join controls to the trigger box (shared border). |
791
+ | `extendBoxToControls` | Extend trigger background behind controls. |
792
+ | `rootClassName` | Wrapper className around controls + trigger. |
793
+ | `triggerInnerClassName` | ClassName for the trigger’s inner content. |
794
+
795
+ ### Mode: button (`mode="button"`)
796
+
797
+ | Prop | Description |
798
+ | -------------------------- | --------------------------------------------------------------- |
799
+ | `mode` | Set to `'button'` to render a button-style trigger. |
800
+ | `button` | (mode='button') If provided, this is the trigger renderer. |
801
+ | `selectedBadge` | (mode='button') Selected-count badge renderer. |
802
+ | `icon` | Single icon rendered near the trigger value. |
803
+ | `iconGap` | Gap between icon and content. |
804
+ | `leadingIcons` | One or more icons before the value. |
805
+ | `trailingIcons` | One or more icons after the value. |
806
+ | `leadingControl` | Custom control element before the trigger (e.g., clear button). |
807
+ | `trailingControl` | Custom control element after the trigger. |
808
+ | `leadingControlClassName` | ClassName for leading control wrapper. |
809
+ | `trailingControlClassName` | ClassName for trailing control wrapper. |
810
+ | `joinControls` | Visually join controls to the trigger box (shared border). |
811
+ | `extendBoxToControls` | Extend trigger background behind controls. |
812
+
813
+ ### Example (default mode)
814
+
815
+ ```tsx
816
+ <InputField
817
+ name="category"
818
+ label="Category"
819
+ variant="treeselect"
820
+ options={[
821
+ {
822
+ key: "social",
823
+ label: "Social",
824
+ children: [
825
+ { key: "twitter", label: "Twitter" },
826
+ { key: "instagram", label: "Instagram" },
827
+ ],
828
+ },
829
+ ]}
830
+ searchable
831
+ placeholder="Pick one…"
832
+ />
833
+ ```
834
+
835
+ ### Example (multiple + button mode)
836
+
837
+ ```tsx
838
+ <InputField
839
+ name="tags"
840
+ variant="treeselect"
841
+ mode="button"
842
+ multiple
843
+ options={[
844
+ { key: 1, label: "Starter" },
845
+ { key: 2, label: "Pro" },
846
+ { key: 3, label: "Enterprise" },
847
+ ]}
848
+ />
849
+ ```
850
+
851
+ ## multi-select
852
+
853
+ ### Variant props
854
+
855
+ | Prop | Description |
856
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
857
+ | `options` | Options for the multi-select. Accepts primitives or objects. |
858
+ | `autoCap` | Capitalise the first letter of the label (when the resolved label is a string). |
859
+ | `optionLabel` | How to read the label from each option (string key or mapper function). If omitted: uses `label`, else `String(value)`. |
860
+ | `optionValue` | How to read the value from each option (string key or mapper function). If omitted: primitives are used directly, else `value`. |
861
+ | `optionDescription` | How to read the description from each option (string key or mapper function). If omitted: uses `description`. |
862
+ | `optionIcon` | How to read the icon from each option (string key or mapper function). If omitted: uses `icon`. |
863
+ | `optionDisabled` | How to detect disabled options (string key or mapper function). If omitted: uses `disabled`. |
864
+ | `optionKey` | How to compute stable keys for items (string key or mapper function). If omitted: uses index. |
865
+ | `searchable` | Enable search field in the list. |
866
+ | `searchPlaceholder` | Placeholder for the search field. |
867
+ | `emptySearchText` | Text when there are no matches for the current search. |
868
+ | `showSelectAll` | Show a “Select all” row. |
869
+ | `selectAllLabel` | Label for the “Select all” row. |
870
+ | `selectAllPosition` | Where to render the “Select all” row. |
871
+ | `clearable` | Show a clear action when there is at least one selection. |
872
+ | `placeholder` | Placeholder when nothing is selected. |
873
+ | `renderOption` | Optional global renderer for an option row. (An option may also provide its own per-option `render`.) |
874
+ | `renderCheckbox` | Optional renderer for the checkbox element used by each option row. |
875
+ | `renderValue` | Custom renderer for the trigger summary (selected values). |
876
+ | `maxListHeight` | Max height for the list (px). |
877
+ | `className` | Wrapper class for the whole variant. |
878
+ | `triggerClassName` | Class for the trigger button. |
879
+ | `contentClassName` | Class for the popover content container. |
880
+
881
+ **Options can be passed as:**
882
+
883
+ * primitives: `['ng', 'gh', 'ke']`
884
+ * objects: `[{ label, value, ...extra }]`
885
+
886
+ ### Mode and trigger props
887
+
888
+ | Prop | Description |
889
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
890
+ | `mode` | Choose trigger style: `"default"` (standard input-like trigger) or `"button"` (custom trigger node). |
891
+ | `leadingIcons` | Icons shown before the summary text inside the trigger (default mode). |
892
+ | `trailingIcons` | Icons shown after the summary / clear button inside the trigger. |
893
+ | `icon` | Single icon shorthand (falls into `leadingIcons`). |
894
+ | `iconGap` | Base gap (px) used between icon groups and text. |
895
+ | `leadingIconSpacing` | Override spacing (px) between leading icons and text. |
896
+ | `trailingIconSpacing` | Override spacing (px) between trailing icons and the right-side controls. |
897
+ | `leadingControl` | Custom node rendered on the far-left *outside* the trigger (e.g., a compact action button). |
898
+ | `trailingControl` | Custom node rendered on the far-right *outside* the trigger. |
899
+ | `leadingControlClassName` | ClassName for the leading control wrapper. |
900
+ | `trailingControlClassName` | ClassName for the trailing control wrapper. |
901
+ | `joinControls` | Visually joins leading/trailing controls with the trigger (no gaps). |
902
+ | `extendBoxToControls` | Extends the input box styling (border/background) around the joined controls. |
903
+ | `button` | Used when `mode="button"`. If provided, this is the trigger. If not provided, `children` is used. |
904
+ | `children` | When `mode="button"` and `button` is not provided, `children` is used as the trigger content. |
905
+ | `selectedBadge` | Selected-count badge (mode="button" only). |
906
+ | `selectedBadgeHiddenWhenZero` | Hide the badge when selected count is 0 (mode="button"). |
907
+ | `selectedBadgeClassName` | ClassName for the selected-count badge. |
908
+ | `selectedBadgePlacement` | Where to place the badge relative to the trigger content (mode="button"). |
909
+
910
+ ### Sample usage
911
+
912
+ ```tsx
913
+ import { InputField } from "@timeax/form-palette"; // adjust import to your project
914
+
915
+ export function MultiSelectExample() {
916
+ return (
917
+ <InputField
918
+ variant="multi-select"
919
+ name="countries"
920
+ label="Countries"
921
+ description="Pick one or more countries."
922
+ options={[
923
+ { label: "Nigeria", value: "ng" },
924
+ { label: "Ghana", value: "gh" },
925
+ { label: "Kenya", value: "ke" },
926
+ ]}
927
+ searchable
928
+ searchPlaceholder="Search countries..."
929
+ showSelectAll
930
+ selectAllLabel="Select all"
931
+ clearable
932
+ placeholder="Select countries..."
933
+ />
934
+ );
935
+ }
936
+ ```
937
+
938
+ ---
939
+
940
+ ## radio
941
+
942
+ ### Variant props
943
+
944
+ | Prop | Description |
945
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------- |
946
+ | `items` | Alias of `options` (list of items to render). |
947
+ | `options` | Options to render. Supports `RadioItem` objects or custom items via mappers. |
948
+ | `mappers` | Mapping functions for `TItem → value/label/description/disabled/key/render`. Takes precedence over `option*` props. |
949
+ | `optionValue` | Shortcut mapping for **value** (used only if `mappers` is not provided). |
950
+ | `optionLabel` | Shortcut mapping for **label** (used only if `mappers` is not provided). |
951
+ | `renderOption` | Global option renderer (can be overridden per item via `item.render`). |
952
+ | `layout` | Layout mode: `"stack"` or `"grid"`. |
953
+ | `columns` | Number of columns when `layout="grid"`. |
954
+ | `itemGapPx` | Gap (px) between items. |
955
+ | `size` | Variant size override for the radio control. |
956
+ | `density` | Variant density override for spacing. |
957
+ | `autoCap` | Auto-capitalise labels when the resolved label is a string. |
958
+ | `aria-label` | ARIA label forwarded to the radio group wrapper. |
959
+ | `aria-labelledby` | ARIA `aria-labelledby` forwarded to the radio group wrapper. |
960
+ | `aria-describedby` | ARIA `aria-describedby` forwarded to the radio group wrapper. |
961
+ | `groupClassName` | ClassName for the group wrapper. |
962
+ | `optionClassName` | ClassName for each option container. |
963
+ | `labelClassName` | ClassName for the option label. |
964
+ | `descriptionClassName` | ClassName for the option description. |
965
+ | `id` | Optional `id` for the group wrapper. |
966
+ | `name` | HTML `name` attribute to group radio inputs. |
967
+ | `className` | Alias for `groupClassName`. |
968
+
969
+ ### Supported option shapes
970
+
971
+ * `RadioItem<TValue>`: `{ value, label, description?, disabled?, key?, render? }`
972
+ * Any `TItem` shape, as long as you provide `mappers` (or `optionLabel`/`optionValue` shortcuts)
973
+
974
+ ### Sample usage
975
+
976
+ ```tsx
977
+ import { InputField } from "@timeax/form-palette"; // adjust import to your project
978
+
979
+ export function RadioExample() {
980
+ return (
981
+ <InputField
982
+ variant="radio"
983
+ name="plan"
984
+ label="Plan"
985
+ description="Choose a plan."
986
+ items={[
987
+ { value: "free", label: "Free", description: "Basic features" },
988
+ { value: "pro", label: "Pro", description: "Everything included" },
989
+ { value: "team", label: "Team", description: "For small teams" },
990
+ ]}
991
+ layout="grid"
992
+ columns={3}
993
+ autoCap
994
+ />
995
+ );
996
+ }
997
+ ```
998
+
999
+ ---
1000
+
1001
+ ## select
1002
+
1003
+ ### Variant props
1004
+
1005
+ | Prop | Description |
1006
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
1007
+ | `options` | Options for the select. Accepts primitives or objects. |
1008
+ | `autoCap` | Capitalise the first letter of the label (when the resolved label is a string). |
1009
+ | `optionLabel` | How to read the label from each option (string key or mapper function). If omitted: uses `label`, else `String(value)`. |
1010
+ | `optionValue` | How to read the value from each option (string key or mapper function). If omitted: primitives are used directly, else `value`. |
1011
+ | `optionDescription` | How to read the description from each option (string key or mapper function). If omitted: uses `description`. |
1012
+ | `optionIcon` | How to read the icon from each option (string key or mapper function). If omitted: uses `icon`. |
1013
+ | `optionDisabled` | How to detect disabled options (string key or mapper function). If omitted: uses `disabled`. |
1014
+ | `optionKey` | How to compute stable keys for items (string key or mapper function). If omitted: uses index. |
1015
+ | `searchable` | Enable search field in the list. |
1016
+ | `searchPlaceholder` | Placeholder for the search field. |
1017
+ | `emptySearchText` | Text shown when there are no matches for the current search. |
1018
+ | `clearable` | Show a clear action (x) when a value is selected. |
1019
+ | `emptyLabel` | Label to show when no value is selected (acts like “none”). |
1020
+ | `placeholder` | Placeholder when no value is selected (and `emptyLabel` not shown). |
1021
+ | `renderOption` | Optional global renderer for a list option. (An option may also provide its own per-option `render`.) |
1022
+ | `renderValue` | Custom renderer for the trigger display (selected option). |
1023
+ | `virtualScroll` | Enable virtual scrolling for large option lists. |
1024
+ | `virtualScrollThreshold` | Number of items after which virtual scroll is enabled (when `virtualScroll=true`). |
1025
+ | `virtualScrollPageSize` | How many items to render per virtual page/chunk. |
1026
+ | `leadingControl` | Custom node rendered on the far-left *outside* the trigger. |
1027
+ | `leadingControlClassName` | ClassName for the leading control wrapper. |
1028
+ | `leadingIconSpacing` | Override spacing (px) between leading icons and the selected value. |
1029
+ | `trailingIcons` | Icons shown on the right side of the trigger. |
1030
+ | `trailingControl` | Custom node rendered on the far-right *outside* the trigger. |
1031
+ | `trailingControlClassName` | ClassName for the trailing control wrapper. |
1032
+ | `trailingIconSpacing` | Override spacing (px) between selected value and trailing icons. |
1033
+ | `joinControls` | Visually joins leading/trailing controls with the trigger (no gaps). |
1034
+ | `extendBoxToControls` | Extends the input box styling (border/background) around the joined controls. |
1035
+ | `icon` | Single icon shorthand (used in default mode). |
1036
+ | `iconGap` | Base gap (px) used between icon and text. |
1037
+ | `className` | Wrapper class for the whole variant. |
1038
+ | `triggerClassName` | Class for the trigger button. |
1039
+ | `contentClassName` | Class for the popover content container. |
1040
+ | `mode` | Choose trigger style: `"default"` (normal select trigger) or `"button"` (custom trigger node). |
1041
+ | `leadingIcons` | Icons shown before the selected value inside the trigger (default mode). |
1042
+ | `button` | Used when `mode="button"`. If provided, this is the trigger. If not provided, `children` is used. |
1043
+ | `children` | When `mode="button"` and `button` is not provided, `children` is used as the trigger content. |
1044
+
1045
+ **Options can be passed as:**
1046
+
1047
+ * primitives: `['ng', 'gh', 'ke']`
1048
+ * objects: `[{ label, value, ...extra }]`
1049
+
1050
+ ### Sample usage
1051
+
1052
+ ```tsx
1053
+ import { InputField } from "@timeax/form-palette"; // adjust import to your project
1054
+
1055
+ export function SelectExample() {
1056
+ return (
1057
+ <InputField
1058
+ variant="select"
1059
+ name="country"
1060
+ label="Country"
1061
+ options={[
1062
+ { label: "Nigeria", value: "ng", description: "NG" },
1063
+ { label: "Ghana", value: "gh", description: "GH" },
1064
+ { label: "Kenya", value: "ke", description: "KE" },
1065
+ ]}
1066
+ searchable
1067
+ searchPlaceholder="Search..."
1068
+ clearable
1069
+ emptyLabel="No selection"
1070
+ placeholder="Select a country..."
1071
+ virtualScroll
1072
+ virtualScrollThreshold={80}
1073
+ virtualScrollPageSize={30}
1074
+ />
1075
+ );
1076
+ }
1077
+ ```
1078
+
1079
+ ## checkbox
1080
+
1081
+ | Prop | Description | | |
1082
+ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ---------------------- |
1083
+ | `single` | Render a single boolean checkbox instead of an option group. | | |
1084
+ | `singleLabel` | Label text shown next to the checkbox in single mode. | | |
1085
+ | `singleDescription` | Optional helper text shown under the single checkbox label. | | |
1086
+ | `options` | Option list for group mode. Accepts primitives or objects (normalized to `{ value, label, description?, disabled?, key? }`). | | |
1087
+ | `items` | Alias for `options` (alternate naming). | | |
1088
+ | `mappers` | Override normalization: `{ mapValue?, mapLabel?, mapDescription?, mapDisabled?, mapKey? }`. | | |
1089
+ | `optionValue` | Map item → option value (overrides `mappers.mapValue`). | | |
1090
+ | `optionLabel` | Map item → option label (overrides `mappers.mapLabel`). | | |
1091
+ | `optionDescription` | Map item → option description (overrides `mappers.mapDescription`). | | |
1092
+ | `optionDisabled` | Map item → disabled boolean (overrides `mappers.mapDisabled`). | | |
1093
+ | `optionKey` | Map item → stable React key (overrides `mappers.mapKey`). | | |
1094
+ | `renderOption` | Custom option renderer (gets `{ item, index, state, effectiveTristate, disabled, size, density, checkboxId, click(), checkbox }`). | | |
1095
+ | `tristate` | Enable tri-state cycling for group options (`none → true → false → none`). | | |
1096
+ | `layout` | Group layout: `"list"` or `"grid"`. | | |
1097
+ | `columns` | Grid columns when `layout="grid"` (default: `2`). | | |
1098
+ | `itemGapPx` | Gap between options in px (defaults vary by layout). | | |
1099
+ | `size` | Checkbox size: `"sm" | "md" | "lg"`(default:`"md"`). |
1100
+ | `density` | Spacing preset: `"compact" | "normal"`(default:`"normal"`). | |
1101
+ | `autoCap` | Auto-capitalize option labels. | | |
1102
+ | `groupClassName` | Class applied to the options wrapper. | | |
1103
+ | `className` | Alias for `groupClassName`. | | |
1104
+ | `optionClassName` | Class applied to each option container. | | |
1105
+ | `labelClassName` | Class applied to label text (single + option labels). | | |
1106
+ | `optionLabelClassName` | Extra class applied to each option label. | | |
1107
+ | `descriptionClassName` | Extra class applied to option descriptions. | | |
1108
+ | `id` | Wrapper id. | | |
1109
+ | `name` | Base `name` used for hidden inputs in group mode. | | |
1110
+ | `aria-label` | Accessibility label for the group wrapper. | | |
1111
+ | `aria-labelledby` | Id of an element that labels the group wrapper. | | |
1112
+ | `aria-describedby` | Id of an element that describes the group wrapper. | | |
1113
+
1114
+ **Value shape notes**
1115
+
1116
+ * **Single mode:** `boolean | undefined`
1117
+ * **Group mode:** `CheckboxGroupEntry[] | undefined`, where each entry is `{ value, state }`.
1118
+
1119
+ * `"none"` is an internal state only (it never appears in the public value).
1120
+
1121
+ **Sample usage**
1122
+
1123
+ ```tsx
1124
+ // single (boolean)
1125
+ <InputField
1126
+ name="agree_tos"
1127
+ label="Terms"
1128
+ variant="checkbox"
1129
+ single
1130
+ singleLabel="I agree to the Terms of Service"
1131
+ />
1132
+
1133
+ // group (with tri-state)
1134
+ <InputField
1135
+ name="notify_prefs"
1136
+ label="Notify me"
1137
+ variant="checkbox"
1138
+ tristate
1139
+ layout="grid"
1140
+ columns={2}
1141
+ options={[
1142
+ { label: "Email", value: "email", description: "Marketing + account alerts" },
1143
+ { label: "SMS", value: "sms" },
1144
+ { label: "Push", value: "push" },
1145
+ ]}
1146
+ />
1147
+ ```
1148
+
1149
+ ## chips
1150
+
1151
+ | Prop | Description | |
1152
+ | ---------------------- | ------------------------------------------------------------------------ | ----------------------------- |
1153
+ | `placeholder` | Placeholder shown when there are no chips and input is empty. | |
1154
+ | `separators` | Separators used to split raw input into chips. Default: `[",", ";"]`. | |
1155
+ | `addOnEnter` | Commit chips on **Enter**. Default: `true`. | |
1156
+ | `addOnTab` | Commit chips on **Tab**. Default: `true`. | |
1157
+ | `addOnBlur` | Commit chips on **blur**. Default: `true`. | |
1158
+ | `allowDuplicates` | When `false`, duplicate chips are ignored. Default: `false`. | |
1159
+ | `maxChips` | Maximum number of chips allowed (`undefined` → unlimited). | |
1160
+ | `backspaceRemovesLast` | Remove last chip on Backspace when input is empty. Default: `true`. | |
1161
+ | `clearable` | Show a clear-all button. Default: `false`. | |
1162
+ | `onAddChips` | Callback: `(added, next) => void` after chips are added. | |
1163
+ | `onRemoveChips` | Callback: `(removed, next) => void` after chips are removed. | |
1164
+ | `renderChip` | Custom chip renderer. You handle remove UI by calling `onRemove(index)`. | |
1165
+ | `renderOverflowChip` | Custom renderer for the overflow chip (when some chips are hidden). | |
1166
+ | `maxVisibleChips` | Maximum number of chips visible before showing an overflow chip. | |
1167
+ | `maxChipChars` | Soft cap for chip display label length (UI-only). | |
1168
+ | `maxChipWidth` | Soft cap for chip width (UI-only). | |
1169
+ | `textareaMode` | Use textarea input instead of single-line input. Default: `false`. | |
1170
+ | `placement` | Chips placement: `"inline" | "below"`(default:`"inline"`). |
1171
+ | `className` | Class applied to the main wrapper. | |
1172
+ | `chipsClassName` | Class applied to the chips container. | |
1173
+ | `chipClassName` | Class applied to each chip wrapper. | |
1174
+ | `chipLabelClassName` | Class applied to each chip label text. | |
1175
+ | `chipRemoveClassName` | Class applied to each chip remove button. | |
1176
+ | `inputClassName` | Class applied to the input/textarea element. | |
1177
+
1178
+ > **Also supports** most `ShadcnTextVariantProps` (size, density, icons, etc.).
1179
+
1180
+ **Sample usage**
1181
+
1182
+ ```tsx
1183
+ <InputField
1184
+ name="tags"
1185
+ label="Tags"
1186
+ variant="chips"
1187
+ placeholder="Add tags…"
1188
+ separators={[",", ";"]}
1189
+ maxChips={10}
1190
+ clearable
1191
+ />
1192
+
1193
+ <InputField
1194
+ name="emails"
1195
+ label="Allowed emails"
1196
+ variant="chips"
1197
+ textareaMode
1198
+ placement="below"
1199
+ addOnEnter
1200
+ addOnBlur
1201
+ />
1202
+ ```
1203
+
1204
+ ## color
1205
+
1206
+ | Prop | Description |
1207
+ | ------------------------ | -------------------------------------------------------------------------- |
1208
+ | `showPreview` | Show a color preview button / swatch. Default: `true`. |
1209
+ | `showPickerToggle` | Show the small picker toggle icon. Default: `true`. |
1210
+ | `previewSize` | Preview swatch size in px. Default: `16`. |
1211
+ | `wrapperClassName` | Class applied to the wrapper. |
1212
+ | `previewButtonClassName` | Class applied to the preview button. |
1213
+ | `previewSwatchClassName` | Class applied to the swatch element inside the preview. |
1214
+ | `pickerInputClassName` | Class applied to the picker input wrapper. |
1215
+ | `pickerToggleIcon` | Icon component for the toggle (a `React.ElementType`). Default: `Palette`. |
1216
+
1217
+ > **Also supports** `ShadcnTextVariantProps`, minus: `type`, `inputMode`, `autoComplete`, `autoCap`, `leadingIcon`, `trailingIcon`, `invalid`, `mono`.
1218
+
1219
+ **Sample usage**
1220
+
1221
+ ```tsx
1222
+ import { Palette } from "lucide-react";
1223
+
1224
+ <InputField
1225
+ name="brand_color"
1226
+ label="Brand color"
1227
+ variant="color"
1228
+ placeholder="#1d4ed8"
1229
+ showPreview
1230
+ previewSize={18}
1231
+ showPickerToggle
1232
+ pickerToggleIcon={Palette}
1233
+ />
1234
+ ```
1235
+
1236
+ ## date
1237
+
1238
+ ### Variant props
1239
+
1240
+ | Prop | Description | |
1241
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
1242
+ | `mode` | Selection mode: `"single"` or `"range"`. | |
1243
+ | `placeholder` | Placeholder content shown when there’s no selection. | |
1244
+ | `clearable` | If `true`, shows a clear action when a value is set. | |
1245
+ | `minDate` | Minimum selectable date (inclusive). | |
1246
+ | `maxDate` | Maximum selectable date (inclusive). | |
1247
+ | `disabledDays` | Disabled-day matcher forwarded to the calendar wrapper (`Calendar["disabled"]`). | |
1248
+ | `formatSingle` | Display pattern for single values. Supports tokens: `yyyy`, `MM`, `dd`, `HH`, `mm`. Defaults depend on `kind`. | |
1249
+ | `formatRange` | Range display formatter: either a pattern string (same tokens as `formatSingle`) or a custom `(range) => string` formatter. | |
1250
+ | `rangeSeparator` | Separator used between `from` and `to` when `formatRange` is a string pattern. Default: `" – "`. | |
1251
+ | `stayOpenOnSelect` | If `true`, keeps the popover open after selecting. In range mode, stays open until both ends are chosen. | |
1252
+ | `open` | Controlled open state for the popover. | |
1253
+ | `onOpenChange` | Called when the popover open state changes. | |
1254
+ | `kind` | Temporal kind that drives default mask + parsing/formatting (e.g. `"date"`, `"datetime"`, `"time"`, `"hour"`, `"monthYear"`, `"year"`). Default: `"date"`. | |
1255
+ | `inputMask` | Optional explicit mask pattern for the input (`9` = digit, `a` = letter, `*` = alphanumeric). If omitted, a default based on `kind` is used. | |
1256
+ | `showCalendar` | Whether to render the calendar popover. Defaults: `true` for `kind="date" | "datetime"`, `false` for time-only kinds. |
1257
+
1258
+ ### Sample usage
1259
+
1260
+ ```tsx
1261
+ import { InputField } from "@timeax/form-palette"; // adjust import to your project
1262
+
1263
+ export function DateRangeExample() {
1264
+ return (
1265
+ <InputField
1266
+ variant="date"
1267
+ name="period"
1268
+ label="Billing period"
1269
+ description="Select a date range."
1270
+ mode="range"
1271
+ kind="date"
1272
+ placeholder="YYYY-MM-DD – YYYY-MM-DD"
1273
+ clearable
1274
+ minDate={new Date(2025, 0, 1)}
1275
+ maxDate={new Date(2025, 11, 31)}
1276
+ // defaultValue can be a Date or { from?: Date; to?: Date }
1277
+ defaultValue={{ from: new Date(2025, 0, 1), to: new Date(2025, 0, 31) }}
1278
+ />
1279
+ );
1280
+ }
1281
+ ```
1282
+
1283
+ ---
1284
+
1285
+ ## keyvalue
1286
+
1287
+ ### Variant props
1288
+
1289
+ | Prop | Description |
1290
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------- |
1291
+ | `min` | Minimum number of pairs allowed (enforced by the UI controls). |
1292
+ | `max` | Maximum number of pairs allowed. |
1293
+ | `minVisible` | Minimum number of chips to show before collapsing into a “more” indicator. |
1294
+ | `maxVisible` | Maximum number of chips to show before collapsing into a “more” indicator. |
1295
+ | `showAddButton` | Toggle visibility of the “Add” action. |
1296
+ | `showMenuButton` | Toggle visibility of the overflow/menu action (if supported by the preset). |
1297
+ | `placeholder` | Placeholder shown when there are no items. |
1298
+ | `dialogTitle` | Title for the edit/add dialog UI. |
1299
+ | `keyLabel` | Label used for the “key” input. |
1300
+ | `valueLabel` | Label used for the “value” input. |
1301
+ | `submitLabel` | Text for the dialog submit button. |
1302
+ | `moreLabel` | Label renderer for the collapsed “more” indicator: `(count) => ReactNode`. |
1303
+ | `emptyLabel` | Label shown when there are no entries (fallback text). |
1304
+ | `className` | Wrapper class for the whole variant. |
1305
+ | `chipsClassName` | Class for the chips container. |
1306
+ | `chipClassName` | Class for each chip. |
1307
+ | `renderChip` | Custom chip renderer. Receives `{ pair, index, onEdit, onRemove, defaultChip }` and should return a `ReactNode`. |
1308
+
1309
+ ### Sample usage
1310
+
1311
+ ```tsx
1312
+ import { InputField } from "@timeax/form-palette"; // adjust import to your project
1313
+
1314
+ export function KeyValueExample() {
1315
+ return (
1316
+ <InputField
1317
+ variant="keyvalue"
1318
+ name="headers"
1319
+ label="Headers"
1320
+ description="Key-value headers to include in requests."
1321
+ placeholder="No headers"
1322
+ dialogTitle="Edit headers"
1323
+ keyLabel="Header"
1324
+ valueLabel="Value"
1325
+ submitLabel="Save"
1326
+ moreLabel={(count) => `+${count} more`}
1327
+ min={0}
1328
+ max={20}
1329
+ defaultValue={{ "X-Client": "timeax", "X-Mode": "dev" }}
1330
+ />
1331
+ );
1332
+ }
1333
+ ```
1334
+
1335
+ ---
1336
+
1337
+ ## editor
1338
+
1339
+ ### Variant props
1340
+
1341
+ | Prop | Description |
1342
+ | ------------------ | ----------------------------------------------------------------- |
1343
+ | `placeholder` | Placeholder content when the editor is empty. |
1344
+ | `minHeight` | Minimum height (px) for the editor surface. |
1345
+ | `maxHeight` | Maximum height (px) for the editor surface (scrolls beyond this). |
1346
+ | `rows` | Row hint for initial sizing (when using a textarea-like layout). |
1347
+ | `toolbar` | Toolbar preset/variant (e.g. `"minimal"`, `"default"`, `"full"`). |
1348
+ | `allowLinks` | Whether links are allowed/enabled. |
1349
+ | `allowImages` | Whether images are allowed/enabled. |
1350
+ | `allowTables` | Whether tables are allowed/enabled. |
1351
+ | `allowLists` | Whether lists are allowed/enabled. |
1352
+ | `allowCode` | Whether code blocks/inline code are allowed/enabled. |
1353
+ | `sanitizeHtml` | If `true`, sanitizes HTML before emitting/storing it. |
1354
+ | `className` | Wrapper class for the whole variant. |
1355
+ | `editorClassName` | Class for the editor surface. |
1356
+ | `toolbarClassName` | Class for the toolbar wrapper. |
1357
+
1358
+ ### Sample usage
1359
+
1360
+ ```tsx
1361
+ import { InputField } from "@timeax/form-palette"; // adjust import to your project
1362
+
1363
+ export function EditorExample() {
1364
+ return (
1365
+ <InputField
1366
+ variant="editor"
1367
+ name="bio"
1368
+ label="Bio"
1369
+ description="Write a short bio."
1370
+ placeholder="Start typing..."
1371
+ rows={8}
1372
+ minHeight={160}
1373
+ maxHeight={420}
1374
+ toolbar="default"
1375
+ allowLinks
1376
+ allowLists
1377
+ sanitizeHtml
1378
+ />
1379
+ );
1380
+ }
1381
+ ```
1382
+
1383
+ ## file
1384
+
1385
+ ### Variant props
1386
+
1387
+ | Prop | Description |
1388
+ | ------------------- | -------------------------------------------------------------------- |
1389
+ | `multiple` | Allow selecting multiple files. |
1390
+ | `accept` | Accepted file types (input accept string / list). |
1391
+ | `maxFiles` | Max number of files allowed. |
1392
+ | `maxTotalSize` | Max total size allowed for all files (bytes). |
1393
+ | `showDropArea` | Show the drop-area UI section. |
1394
+ | `dropIcon` | Optional icon shown in the drop area. |
1395
+ | `dropTitle` | Title text shown in the drop area. |
1396
+ | `dropDescription` | Helper text shown in the drop area. |
1397
+ | `custom` | Use a fully custom “picker” UI instead of the built-in drop/trigger. |
1398
+ | `asRaw` | Treat values as raw `File` objects (native picker flow). |
1399
+ | `renderDropArea` | Custom renderer for the drop area section. |
1400
+ | `renderFileItem` | Custom renderer for each file item row. |
1401
+ | `showCheckboxes` | Show checkboxes next to file items (when supported by the UI). |
1402
+ | `onFilesAdded` | Callback fired when files are added. |
1403
+ | `customLoader` | Provide your own file loader (e.g. resolve URLs → metadata). |
1404
+ | `mergeMode` | Merge strategy when adding files (e.g. append/replace/dedupe). |
1405
+ | `formatFileName` | Custom formatter for displaying a file name. |
1406
+ | `formatFileSize` | Custom formatter for displaying a file size. |
1407
+ | `placeholder` | Placeholder text when nothing is selected. |
1408
+ | `className` | Wrapper class for the whole variant. |
1409
+ | `dropAreaClassName` | ClassName for the drop-area wrapper. |
1410
+ | `listClassName` | ClassName for the file list container. |
1411
+ | `triggerClassName` | ClassName for the trigger (when using the built-in trigger). |
1412
+
1413
+ ### Mode and trigger props
1414
+
1415
+ | Prop | Description |
1416
+ | ----------------------------- | ----------------------------------------------------------------------- |
1417
+ | `mode` | Trigger style: `"default"` (input-like) or `"button"` (custom trigger). |
1418
+ | `leadingIcons` | Icons shown before the summary (default mode). |
1419
+ | `trailingIcons` | Icons shown after the summary / clear action. |
1420
+ | `icon` | Single icon shorthand (falls into leading icons). |
1421
+ | `iconGap` | Base gap (px) between icon groups and text. |
1422
+ | `leadingIconSpacing` | Override spacing (px) between leading icons and text. |
1423
+ | `trailingIconSpacing` | Override spacing (px) between summary and trailing controls. |
1424
+ | `leadingControl` | Custom node on the far-left *outside* the trigger. |
1425
+ | `trailingControl` | Custom node on the far-right *outside* the trigger. |
1426
+ | `leadingControlClassName` | ClassName for the leading control wrapper. |
1427
+ | `trailingControlClassName` | ClassName for the trailing control wrapper. |
1428
+ | `joinControls` | Visually “joins” leading/trailing controls with the trigger. |
1429
+ | `extendBoxToControls` | Extends the input box styling around joined controls. |
1430
+ | `button` | When `mode="button"`: explicit trigger node. |
1431
+ | `children` | When `mode="button"` and `button` is not provided: trigger content. |
1432
+ | `selectedBadge` | Selected-count badge (button mode). |
1433
+ | `selectedBadgeHiddenWhenZero` | Hide badge when selected count is 0. |
1434
+ | `selectedBadgeClassName` | ClassName for the selected-count badge. |
1435
+ | `selectedBadgePlacement` | Where to place the badge relative to the trigger content. |
1436
+
1437
+ ### Sample usage
1438
+
1439
+ ```tsx
1440
+ import { InputField } from "@timeax/form-palette"; // adjust import to your project
1441
+
1442
+ export function FileExample() {
1443
+ return (
1444
+ <InputField
1445
+ variant="file"
1446
+ name="attachments"
1447
+ label="Attachments"
1448
+ description="Upload up to 3 files (max 10MB total)."
1449
+ multiple
1450
+ accept={["image/*", "application/pdf"]}
1451
+ maxFiles={3}
1452
+ maxTotalSize={10 * 1024 * 1024}
1453
+ showDropArea
1454
+ placeholder="Choose files..."
1455
+ />
1456
+ );
1457
+ }
1458
+ ```
1459
+
1460
+ ---
1461
+
1462
+ ## json-editor
1463
+
1464
+ ### Wrapper / trigger props
1465
+
1466
+ | Prop | Description |
1467
+ | ------------------ | --------------------------------------------------------- |
1468
+ | `mode` | Display mode: `"popover"` or `"accordion"`. |
1469
+ | `trigger` | Custom trigger node (popover mode). |
1470
+ | `triggerLabel` | Default trigger label text (popover mode). |
1471
+ | `triggerVariant` | Visual variant for the trigger button. |
1472
+ | `triggerSize` | Size for the trigger button. |
1473
+ | `openLabel` | Label text when opening the popover. |
1474
+ | `closeLabel` | Label text when closing the popover. |
1475
+ | `open` | Controlled open state (popover mode). |
1476
+ | `onOpenChange` | Open-state change callback. |
1477
+ | `onClose` | Callback fired when the popover closes. |
1478
+ | `id` | Optional id for accessibility wiring. |
1479
+ | `describedBy` | Optional `aria-describedby` target id. |
1480
+ | `wrapperClassName` | ClassName for the outer wrapper. |
1481
+ | `popoverClassName` | ClassName for the popover content wrapper (popover mode). |
1482
+ | `panelClassName` | ClassName for the editor panel wrapper. |
1483
+
1484
+ ### Editor props (passed into the JSON editor)
1485
+
1486
+ | Prop | Description |
1487
+ | ------------------ | ----------------------------------------------------------------------------------- |
1488
+ | `title` | Title displayed in the editor header. |
1489
+ | `fieldMap` | Field mapping rules (wildcards supported) → picks a field variant + props per path. |
1490
+ | `layout` | Layout rules (grid/rows + route/page rules). |
1491
+ | `defaults` | Default values / behaviours for missing keys and created fields. |
1492
+ | `filters` | Include/exclude filters for routes/fields. |
1493
+ | `permissions` | Permissions (add/delete/view/edit raw, etc.). |
1494
+ | `callbacks` | Hooks for events like add/delete/edit / route changes. |
1495
+ | `route` | Controlled “page route” (e.g. `"config.headers"`). |
1496
+ | `defaultRoute` | Starting route when uncontrolled. |
1497
+ | `onRouteChange` | Route change callback. |
1498
+ | `viewMode` | Controlled view mode (e.g. raw vs structured UI). |
1499
+ | `defaultViewMode` | Default view mode when uncontrolled. |
1500
+ | `onViewModeChange` | View mode change callback. |
1501
+ | `className` | Root class for the editor. |
1502
+ | `contentClassName` | Class for the main content region. |
1503
+ | `navClassName` | Class for the navigation region. |
1504
+ | `bodyClassName` | Class for the body region. |
1505
+
1506
+ ### Sample usage
1507
+
1508
+ ```tsx
1509
+ import { InputField } from "@timeax/form-palette"; // adjust import to your project
1510
+
1511
+ export function JsonEditorExample() {
1512
+ return (
1513
+ <InputField
1514
+ variant="json-editor"
1515
+ name="settings"
1516
+ label="Settings"
1517
+ description="Edit advanced settings as a structured UI."
1518
+ mode="popover"
1519
+ triggerLabel="Edit settings"
1520
+ title="Settings"
1521
+ defaultValue={{
1522
+ projectName: "",
1523
+ config: { apiUrl: "", enabled: true },
1524
+ }}
1525
+ fieldMap={{
1526
+ projectName: { variant: "text", props: { label: "Project name" } },
1527
+ "config.apiUrl": { variant: "text", props: { label: "API URL" } },
1528
+ "config.enabled": { variant: "toggle", props: { label: "Enabled" } },
1529
+ "**.*token*": { variant: "password", props: { label: "Token" } },
1530
+ }}
1531
+ permissions={{
1532
+ canViewRaw: true,
1533
+ canEditRaw: false,
1534
+ canAdd: true,
1535
+ canDelete: true,
1536
+ }}
1537
+ />
1538
+ );
1539
+ }
1540
+ ```
1541
+
1542
+ ---
1543
+
1544
+ ## lister
1545
+
1546
+ ### Data + mapping props
1547
+
1548
+ | Prop | Description |
1549
+ | ------------------- | ----------------------------------------------------------------------- |
1550
+ | `def` | Base lister definition (columns, source, mapping, etc.). |
1551
+ | `endpoint` | Inline source endpoint (standalone inline mode). |
1552
+ | `method` | Inline HTTP method: `"GET"` or `"POST"`. |
1553
+ | `buildRequest` | Custom request builder (params/body/headers). |
1554
+ | `selector` | How to extract the array from the response (function or selector path). |
1555
+ | `optionValue` | How to map a raw row → option value (key or function). |
1556
+ | `optionLabel` | How to map a raw row → label. |
1557
+ | `optionIcon` | How to map a raw row → icon. |
1558
+ | `optionDescription` | How to map a raw row → description. |
1559
+ | `optionDisabled` | How to map a raw row → disabled. |
1560
+ | `optionGroup` | How to map a raw row → group label. |
1561
+ | `optionMeta` | How to map a raw row → meta payload. |
1562
+ | `search` | Search override (inline). |
1563
+ | `searchTarget` | Search target override (inline). |
1564
+
1565
+ ### Selection + behaviour props
1566
+
1567
+ | Prop | Description |
1568
+ | ------------------ | ---------------------------------------------------------- |
1569
+ | `filters` | Filters payload used by the lister source. |
1570
+ | `confirm` | Optional confirm behaviour (e.g. confirm selection). |
1571
+ | `permissions` | Permissions object used by the lister UI (actions, views). |
1572
+ | `placeholder` | Placeholder text when nothing is selected. |
1573
+ | `maxDisplayItems` | Max chips/labels to show before collapsing into “+N”. |
1574
+ | `renderTrigger` | Custom trigger renderer. |
1575
+ | `title` | Title displayed when opening the lister UI. |
1576
+ | `searchMode` | Search mode for the open UI. |
1577
+ | `initialQuery` | Initial query state. |
1578
+ | `showRefresh` | Show refresh button. |
1579
+ | `refreshMode` | Refresh behaviour/mode. |
1580
+ | `filtersSpec` | Filters spec for the open UI. |
1581
+ | `renderOption` | Custom renderer for option rows. |
1582
+ | `host` | Override lister host implementation. |
1583
+ | `presets` | Override preset map used by lister internals. |
1584
+ | `remoteDebounceMs` | Debounce (ms) for remote search requests. |
1585
+
1586
+ ### Trigger styling + container props
1587
+
1588
+ | Prop | Description |
1589
+ | ----------------------------- | ----------------------------------------------------------------------- |
1590
+ | `mode` | Trigger style: `"default"` (input-like) or `"button"` (custom trigger). |
1591
+ | `clearable` | Show clear action when a selection exists. |
1592
+ | `leadingIcons` | Icons shown before the summary (default mode). |
1593
+ | `trailingIcons` | Icons shown after the summary / clear action. |
1594
+ | `icon` | Single icon shorthand. |
1595
+ | `iconGap` | Base gap (px) between icon groups and text. |
1596
+ | `leadingIconSpacing` | Override spacing (px) between leading icons and text. |
1597
+ | `trailingIconSpacing` | Override spacing (px) between summary and trailing controls. |
1598
+ | `leadingControl` | Custom node on the far-left *outside* the trigger. |
1599
+ | `trailingControl` | Custom node on the far-right *outside* the trigger. |
1600
+ | `leadingControlClassName` | ClassName for the leading control wrapper. |
1601
+ | `trailingControlClassName` | ClassName for the trailing control wrapper. |
1602
+ | `joinControls` | Visually “joins” leading/trailing controls with the trigger. |
1603
+ | `extendBoxToControls` | Extends the input box styling around joined controls. |
1604
+ | `maxListHeight` | Max height for the open list/panel (px). |
1605
+ | `className` | Wrapper class for the whole variant. |
1606
+ | `triggerClassName` | ClassName for the trigger. |
1607
+ | `contentClassName` | ClassName for the popover/content container. |
1608
+ | `panelClassName` | ClassName for the panel surface wrapper. |
1609
+ | `button` | When `mode="button"`: explicit trigger node. |
1610
+ | `children` | When `mode="button"` and `button` is not provided: trigger content. |
1611
+ | `selectedBadge` | Selected-count badge (button mode). |
1612
+ | `selectedBadgeHiddenWhenZero` | Hide badge when selected count is 0. |
1613
+ | `selectedBadgeClassName` | ClassName for the selected-count badge. |
1614
+ | `selectedBadgePlacement` | Where to place the badge relative to the trigger content. |
1615
+
1616
+ ### Sample usage
1617
+
1618
+ ```tsx
1619
+ import { InputField } from "@timeax/form-palette"; // adjust import to your project
1620
+
1621
+ export function ListerExample() {
1622
+ return (
1623
+ <InputField
1624
+ variant="lister"
1625
+ name="user_id"
1626
+ label="User"
1627
+ description="Pick a user from a remote list."
1628
+
1629
+ // standalone inline source (no base `def` required)
1630
+ endpoint="/api/admin/users"
1631
+ method="GET"
1632
+ selector="data" // or (res) => res.data
1633
+ optionValue="id"
1634
+ optionLabel={(u: any) => u.name}
1635
+ optionDescription={(u: any) => u.email}
1636
+
1637
+ searchable
1638
+ clearable
1639
+ placeholder="Select a user..."
1640
+ title="Select user"
1641
+ />
1642
+ );
1643
+ }
1644
+ ```
1645
+
1646
+ ## custom
1647
+
1648
+ ### Variant props
1649
+
1650
+ | Prop | Description |
1651
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1652
+ | `component` | React component to render. **Required.** |
1653
+ | `valueProp` | Prop name that receives the current value. Default: `"value"`. |
1654
+ | `changeProp` | Prop name for the change handler the component calls. Default: `"onChange"`. The component is expected to call `props[changeProp](nextValue, ...args)`; the **first** argument is treated as the next value. |
1655
+ | `disabledProp` | Prop name for disabled state. Default: `"disabled"`. |
1656
+ | `readOnlyProp` | Prop name for read-only state. Default: `"readOnly"`. |
1657
+ | `errorProp` | Optional prop name to pass the field `error` into the component (if it cares). |
1658
+ | `idProp` | Prop name for the `id` attribute. Default: `"id"`. |
1659
+ | `nameProp` | Prop name for the `name` attribute. Default: `"name"`. |
1660
+ | `placeholderProp` | Prop name for the `placeholder` attribute. Default: `"placeholder"`. |
1661
+ | `mapValue` | Optional transform for the raw next value before it hits the field: `(raw, ...args) => TValue`. |
1662
+ | `mapDetail` | Optional builder for `ChangeDetail` given the raw next value: `(raw, ...args) => ChangeDetail`. If omitted, a default `{ source: "variant", raw }` detail is used. |
1663
+ | `...rest` | Any other props are forwarded to your `component`. |
1664
+
1665
+ ### Sample usage
1666
+
1667
+ ```tsx
1668
+ import * as React from "react";
1669
+ import { InputField } from "@timeax/form-palette";
1670
+
1671
+ // Example custom component (Radix-like API)
1672
+ function MyToggle(props: {
1673
+ checked?: boolean;
1674
+ onCheckedChange?: (next: boolean) => void;
1675
+ disabled?: boolean;
1676
+ }) {
1677
+ const { checked, onCheckedChange, disabled } = props;
1678
+
1679
+ return (
1680
+ <button
1681
+ type="button"
1682
+ disabled={disabled}
1683
+ aria-pressed={checked}
1684
+ onClick={() => onCheckedChange?.(!checked)}
1685
+ className="rounded border px-3 py-1"
1686
+ >
1687
+ {checked ? "On" : "Off"}
1688
+ </button>
1689
+ );
1690
+ }
1691
+
1692
+ export function CustomExample() {
1693
+ return (
1694
+ <InputField
1695
+ variant="custom"
1696
+ name="marketing_opt_in"
1697
+ label="Marketing emails"
1698
+ description="Toggle to opt in."
1699
+ component={MyToggle}
1700
+ valueProp="checked"
1701
+ changeProp="onCheckedChange"
1702
+ />
1703
+ );
1704
+ }
1705
+ ```
1706
+
1707
+ ---
1708
+
1709
+ # Form Palette — `extra` entrypoint (v2)
1710
+
1711
+ The `extra` entrypoint exposes two “power tools” that sit beside the normal **Form / InputField** flow:
1712
+
1713
+ 1. **Lister runtime** (provider + global UI + hooks) — a reusable, app-wide **picker** system for single/multi selection with search/filter + **remote / local / hybrid** data.
1714
+ 2. **JsonEditor** — an interactive JSON editor UI (also used by the `json-editor` InputField variant).
1715
+
1716
+ > This README is written against the `extra.ts` export surface:
1717
+ >
1718
+ > ```ts
1719
+ > export * from "@/presets/lister/index";
1720
+ > export { default as JsonEditor } from "@/presets/shadcn-variants/json-editor";
1721
+ > ```
1722
+
1723
+ ---
1724
+
1725
+ # 1) Lister (runtime)
1726
+
1727
+ ## What is Lister?
1728
+
1729
+ Lister is a small runtime that lets you open a **picker UI** from anywhere in your app:
1730
+
1731
+ * **Single** selection (“choose one user”) or **multi** selection (“choose many tags”).
1732
+ * **Local**, **remote**, or **hybrid** search.
1733
+ * A consistent session model: **open → search/filter → select → apply/cancel**.
1734
+ * App-level integration via a provider and a single UI renderer.
1735
+
1736
+ If you use the `lister` InputField variant, it’s powered by this same runtime.
1737
+
1738
+ ---
1739
+
1740
+ ## Building blocks (what you actually mount/call)
1741
+
1742
+ ### ✅ `ListerProvider`
1743
+
1744
+ * Holds the Lister store/context.
1745
+ * Receives the **host** (permission checks + logging).
1746
+ * Can register a **presets map** (reusable picker definitions).
1747
+ * Supports provider-side remote debounce via `remoteDebounceMs` (default **300ms**).
1748
+
1749
+ ### ✅ `ListerUI`
1750
+
1751
+ * Renders any **open sessions** (popovers) from the provider store.
1752
+ * Mount this **once** under the provider.
1753
+
1754
+ ### ✅ `useLister()`
1755
+
1756
+ * Imperative controller + access to store.
1757
+
1758
+ Provides:
1759
+
1760
+ * `api.open(...)` / `api.fetch(...)` (def/preset-based)
1761
+ * session actions (apply/cancel/close)
1762
+ * selection actions (toggle/select/deselect/clear)
1763
+ * search actions (setQuery/searchLocal/searchRemote)
1764
+ * filter helpers
1765
+
1766
+ ### ✅ `useData()`
1767
+
1768
+ * Lower-level hook used for **fetching + searching + filters + selection state**.
1769
+ * Exported so you can build custom list UIs that still behave like Lister.
1770
+
1771
+ ---
1772
+
1773
+ ## Quick start (recommended)
1774
+
1775
+ ### Step 1 — Mount provider + UI once
1776
+
1777
+ ```tsx
1778
+ import * as React from "react";
1779
+ import { ListerProvider, ListerUI } from "@timeax/form-palette/extra";
1780
+
1781
+ const host = {
1782
+ can: (permissions: string[], ctx: any) => true,
1783
+ log: (entry: any) => console.log("[lister]", entry),
1784
+ };
1785
+
1786
+ export function AppShell({ children }: { children: React.ReactNode }) {
1787
+ return (
1788
+ <ListerProvider host={host}>
1789
+ {children}
1790
+ <ListerUI />
1791
+ </ListerProvider>
1792
+ );
1793
+ }
1794
+ ```
1795
+
1796
+ ### Step 2 — Open a picker imperatively
1797
+
1798
+ ```tsx
1799
+ import * as React from "react";
1800
+ import { useLister } from "@timeax/form-palette/extra";
1801
+
1802
+ export function PickUserButton() {
1803
+ const { api } = useLister<any>();
1804
+
1805
+ return (
1806
+ <button
1807
+ onClick={() => {
1808
+ // "users" can be a preset key, or you can pass a definition inline
1809
+ api.open("users", { status: "active" }, { title: "Select a user" });
1810
+ }}
1811
+ >
1812
+ Pick user
1813
+ </button>
1814
+ );
1815
+ }
1816
+ ```
1817
+
1818
+ ---
1819
+
1820
+ ## `ListerProvider` API
1821
+
1822
+ ```ts
1823
+ export function ListerProvider(props: {
1824
+ host: ListerProviderHost;
1825
+ presets?: PresetMap;
1826
+ http?: ListerHttpClient;
1827
+ remoteDebounceMs?: number; // default 300
1828
+ children: React.ReactNode;
1829
+ })
1830
+ ```
1831
+
1832
+ ### `ListerProviderHost`
1833
+
1834
+ ```ts
1835
+ export interface ListerProviderHost {
1836
+ /** Host decides permission logic. Mandatory permissions end with '!' */
1837
+ can: (permissions: string[], ctx: ListerPermissionCtx) => boolean;
1838
+
1839
+ /** Host decides notification/diagnostic surface */
1840
+ log: (entry: ListerLogEntry) => void;
1841
+ }
1842
+ ```
1843
+
1844
+ ### Practical usage
1845
+
1846
+ * Use `can()` to gate actions like **open**, **refresh**, or **apply**.
1847
+ * Use `log()` to send diagnostics to console, Sentry, or an in-app toast.
1848
+
1849
+ ---
1850
+
1851
+ # 2) `useData()` — deep dive (extremely important)
1852
+
1853
+ `useData()` is the engine behind Lister-style **data fetching + searching + filtering + selection**.
1854
+
1855
+ Use it when:
1856
+
1857
+ * You want a **custom picker UI** (your own layout, rows, pagination).
1858
+ * You want a **data-backed selection** UX but not the standard Lister popover.
1859
+ * You want Lister’s semantics (remote/local/hybrid search + filters) in other UIs.
1860
+
1861
+ > It works only under `<ListerProvider />` because it uses the provider’s fetch engine.
1862
+
1863
+ ---
1864
+
1865
+ ## What `useData()` returns (mental model)
1866
+
1867
+ `useData()` returns two lists:
1868
+
1869
+ * `data`: the raw fetched list from the provider.
1870
+ * `visible`: what your UI should render.
1871
+
1872
+ * In **remote** mode: `visible === data`.
1873
+ * In **local / hybrid**: `visible` is client-filtered using `query + searchTarget`.
1874
+
1875
+ It also returns **selection state** (optional) and **fetch/search/filter helpers**.
1876
+
1877
+ ---
1878
+
1879
+ ## `UseDataOptions` (inputs)
1880
+
1881
+ | Option | Description |
1882
+ | -------------------------- | ---------------------------------------------------------------------------- |
1883
+ | `id?` | Optional identifier used when building the inline def. |
1884
+ | `endpoint` | URL/path to fetch items from. |
1885
+ | `method?` | HTTP method (default: `"GET"`). |
1886
+ | `selector?` | How to extract the list from your response (string path or mapper function). |
1887
+ | `buildRequest?` | Build `{ params?, body?, headers? }` from `{ filters, query, cursor }`. |
1888
+ | `search?` | `{ default?: string }` sets a default `searchTarget` subject key. |
1889
+ | `filters?` | Initial filters object. |
1890
+ | `initial?` | Initial items to avoid immediate fetch. |
1891
+ | `enabled?` | Disable all fetching/effects when false (default: `true`). |
1892
+ | `fetchOnMount?` | Auto-fetch on mount (default: `!initial`). |
1893
+ | `searchMode?` | `"remote"` (default) or `"local"` or `"hybrid"`. |
1894
+ | `debounceMs?` | Debounce for query/target changes in remote/hybrid (default: **300ms**). |
1895
+ | `autoFetchOnFilterChange?` | In remote/hybrid: auto-fetch when filters change (default: `true`). |
1896
+ | `selection?` | Enable selection helpers: `{ mode, key?, prune? }`. |
1897
+
1898
+ ### Selection config (`selection`)
1899
+
1900
+ | Key | Meaning |
1901
+ | ------- | ------------------------------------------------------------------------------------------------------------------------ |
1902
+ | `mode` | `"single"` or `"multiple"` (omit = no selection). |
1903
+ | `key` | How to compute the item ID. String key (e.g. `"id"`) or `(item) => id`. |
1904
+ | `prune` | `"never"` (default) keeps selection across new fetches. `"missing"` removes selected IDs not present in the latest list. |
1905
+
1906
+ **Defaults**
1907
+
1908
+ * Default `key` behavior is effectively: `item.id ?? item.value`.
1909
+
1910
+ ---
1911
+
1912
+ ## Search modes: remote vs local vs hybrid
1913
+
1914
+ ### ✅ `remote` (default)
1915
+
1916
+ * Query changes trigger a **debounced fetch**.
1917
+ * The server is responsible for searching.
1918
+
1919
+ Best for: **large datasets**, server ranking, true search endpoints.
1920
+
1921
+ ### ✅ `local`
1922
+
1923
+ * Switching to local fetches a **base list once** using an empty query.
1924
+ * After that, `visible` is filtered client-side.
1925
+
1926
+ Best for: **small-medium datasets** you can cache (countries, categories, roles).
1927
+
1928
+ ### ✅ `hybrid`
1929
+
1930
+ * Query changes still fetch remotely.
1931
+ * But `visible` also applies the local filtering rules.
1932
+
1933
+ Best for: “server list + extra client constraints” (e.g. `searchOnly`).
1934
+
1935
+ ---
1936
+
1937
+ ## Search targeting (`searchTarget`)
1938
+
1939
+ `useData()` supports Lister-style targeting via `searchTarget`:
1940
+
1941
+ * `mode: "all"` → search across everything
1942
+ * `mode: "subject"` → search only against one subject field (e.g. `name`)
1943
+ * `mode: "only"` → constrain results to a known list of IDs
1944
+
1945
+ If you pass `search={{ default: "name" }}`, the default target becomes `mode:"subject"` on that key.
1946
+
1947
+ ---
1948
+
1949
+ ## Core returned API (what you’ll use most)
1950
+
1951
+ ### Data + status
1952
+
1953
+ * `data`, `visible`
1954
+ * `loading`, `error`
1955
+
1956
+ ### Search
1957
+
1958
+ * `query`, `setQuery(query)`
1959
+ * `searchMode`, `setSearchMode(mode)`
1960
+ * `searchTarget`, `setSearchTarget(target)`
1961
+
1962
+ ### Filters
1963
+
1964
+ * `filters`, `setFilters(next)`
1965
+ * `patchFilters(partial)`
1966
+ * `clearFilters()`
1967
+
1968
+ ### Fetch
1969
+
1970
+ * `refresh()` — refetch using current query/filters/target
1971
+ * `fetch({ query?, filters?, searchTarget?, search? })` — manual override fetch
1972
+
1973
+ ### Selection (when enabled)
1974
+
1975
+ * `selectionMode` (`none | single | multiple`)
1976
+ * `selectedIds` (array)
1977
+ * `selected` (array of objects resolved from cache)
1978
+ * `select(id)`, `deselect(id)`, `toggle(id)`, `clearSelection()`
1979
+ * `isSelected(id)`
1980
+ * `getSelection()` — returns the best current selection shape
1981
+
1982
+ > Important: selection maintains an internal cache so selected objects can be returned even when the current page/list no longer contains them.
1983
+
1984
+ ---
1985
+
1986
+ ## `useData()` — practical use cases (full examples)
1987
+
1988
+ ### Use case A — Remote search list (simple)
1989
+
1990
+ ```tsx
1991
+ import * as React from "react";
1992
+ import { useData } from "@timeax/form-palette/extra";
1993
+
1994
+ type User = { id: string; name: string; email: string };
1995
+
1996
+ export function RemoteUserSearch() {
1997
+ const { visible, loading, error, query, setQuery, refresh } = useData<User>({
1998
+ endpoint: "/api/users",
1999
+ method: "GET",
2000
+ selector: "data", // or (res) => res.data
2001
+ search: { default: "name" },
2002
+ });
2003
+
2004
+ return (
2005
+ <div>
2006
+ <input
2007
+ value={query}
2008
+ onChange={(e) => setQuery(e.target.value)}
2009
+ placeholder="Search users…"
2010
+ />
2011
+ <button onClick={refresh} disabled={loading}>
2012
+ Refresh
2013
+ </button>
2014
+
2015
+ {loading && <p>Loading…</p>}
2016
+ {error && <p style={{ color: "crimson" }}>{String(error)}</p>}
2017
+
2018
+ <ul>
2019
+ {visible.map((u) => (
2020
+ <li key={u.id}>
2021
+ <b>{u.name}</b> <small>{u.email}</small>
2022
+ </li>
2023
+ ))}
2024
+ </ul>
2025
+ </div>
2026
+ );
2027
+ }
2028
+ ```
2029
+
2030
+ ---
2031
+
2032
+ ### Use case B — Local mode (fetch once, instant client filtering)
2033
+
2034
+ ```tsx
2035
+ import * as React from "react";
2036
+ import { useData } from "@timeax/form-palette/extra";
2037
+
2038
+ type Country = { code: string; name: string };
2039
+
2040
+ export function CountryPickerLocal() {
2041
+ const { visible, query, setQuery } = useData<Country>({
2042
+ endpoint: "/api/countries",
2043
+ method: "GET",
2044
+ selector: "data",
2045
+ search: { default: "name" },
2046
+ searchMode: "local",
2047
+ });
2048
+
2049
+ return (
2050
+ <div>
2051
+ <input
2052
+ value={query}
2053
+ onChange={(e) => setQuery(e.target.value)}
2054
+ placeholder="Search countries…"
2055
+ />
2056
+ <ul>
2057
+ {visible.map((c) => (
2058
+ <li key={c.code}>{c.name}</li>
2059
+ ))}
2060
+ </ul>
2061
+ </div>
2062
+ );
2063
+ }
2064
+ ```
2065
+
2066
+ ---
2067
+
2068
+ ### Use case C — Filters with `patchFilters` (remote/hybrid auto-fetch)
2069
+
2070
+ ```tsx
2071
+ import * as React from "react";
2072
+ import { useData } from "@timeax/form-palette/extra";
2073
+
2074
+ type Filters = { status?: string; role?: string };
2075
+
2076
+ export function FilteredUsers() {
2077
+ const { visible, filters, patchFilters, clearFilters, loading } = useData<any, Filters>({
2078
+ endpoint: "/api/users",
2079
+ method: "GET",
2080
+ selector: "data",
2081
+ filters: { status: "active" },
2082
+ autoFetchOnFilterChange: true,
2083
+ });
2084
+
2085
+ return (
2086
+ <div>
2087
+ <div style={{ display: "flex", gap: 8 }}>
2088
+ <select
2089
+ value={filters?.status ?? ""}
2090
+ onChange={(e) => patchFilters({ status: e.target.value || undefined })}
2091
+ >
2092
+ <option value="">Any status</option>
2093
+ <option value="active">Active</option>
2094
+ <option value="blocked">Blocked</option>
2095
+ </select>
2096
+
2097
+ <select
2098
+ value={filters?.role ?? ""}
2099
+ onChange={(e) => patchFilters({ role: e.target.value || undefined })}
2100
+ >
2101
+ <option value="">Any role</option>
2102
+ <option value="admin">Admin</option>
2103
+ <option value="user">User</option>
2104
+ </select>
2105
+
2106
+ <button onClick={clearFilters} disabled={loading}>
2107
+ Clear
2108
+ </button>
2109
+ </div>
2110
+
2111
+ <ul>
2112
+ {visible.map((u: any) => (
2113
+ <li key={u.id}>{u.name}</li>
2114
+ ))}
2115
+ </ul>
2116
+ </div>
2117
+ );
2118
+ }
2119
+ ```
2120
+
2121
+ ---
2122
+
2123
+ ### Use case D — Constrain to a known allow-list (`searchTarget: mode="only"`)
2124
+
2125
+ ```tsx
2126
+ import * as React from "react";
2127
+ import { useData } from "@timeax/form-palette/extra";
2128
+
2129
+ export function AllowedIdsOnly() {
2130
+ const allowed = React.useMemo(() => ["u_1", "u_8", "u_12"], []);
2131
+
2132
+ const { visible, setSearchTarget, searchMode, setSearchMode } = useData<any>({
2133
+ endpoint: "/api/users",
2134
+ method: "GET",
2135
+ selector: "data",
2136
+ searchMode: "local",
2137
+ });
2138
+
2139
+ React.useEffect(() => {
2140
+ setSearchTarget({ mode: "only", only: allowed, subject: null });
2141
+ }, [allowed, setSearchTarget]);
2142
+
2143
+ return (
2144
+ <div>
2145
+ <small>mode: {searchMode}</small>
2146
+ <button onClick={() => setSearchMode("local")}>Local</button>
2147
+ <button onClick={() => setSearchMode("remote")}>Remote</button>
2148
+
2149
+ <ul>
2150
+ {visible.map((u: any) => (
2151
+ <li key={u.id}>{u.name}</li>
2152
+ ))}
2153
+ </ul>
2154
+ </div>
2155
+ );
2156
+ }
2157
+ ```
2158
+
2159
+ ---
2160
+
2161
+ ### Use case E — Custom multi-select UI (selection enabled)
2162
+
2163
+ ```tsx
2164
+ import * as React from "react";
2165
+ import { useData } from "@timeax/form-palette/extra";
2166
+
2167
+ type Tag = { id: string; name: string };
2168
+
2169
+ export function TagMultiSelect() {
2170
+ const { visible, isSelected, toggle, selectedIds, selected } = useData<Tag>({
2171
+ endpoint: "/api/tags",
2172
+ method: "GET",
2173
+ selector: "data",
2174
+ searchMode: "remote",
2175
+ selection: {
2176
+ mode: "multiple",
2177
+ key: "id",
2178
+ prune: "never", // keep selection stable while remote results change
2179
+ },
2180
+ });
2181
+
2182
+ return (
2183
+ <div>
2184
+ <ul>
2185
+ {visible.map((t) => (
2186
+ <li key={t.id}>
2187
+ <label style={{ display: "flex", gap: 8, alignItems: "center" }}>
2188
+ <input
2189
+ type="checkbox"
2190
+ checked={isSelected(t.id)}
2191
+ onChange={() => toggle(t.id)}
2192
+ />
2193
+ {t.name}
2194
+ </label>
2195
+ </li>
2196
+ ))}
2197
+ </ul>
2198
+
2199
+ <pre style={{ marginTop: 12 }}>
2200
+ selectedIds: {JSON.stringify(selectedIds, null, 2)}
2201
+
2202
+ selected objects: {JSON.stringify(selected, null, 2)}
2203
+ </pre>
2204
+ </div>
2205
+ );
2206
+ }
2207
+ ```
2208
+
2209
+ ---
2210
+
2211
+ ### Use case F — Advanced request shaping (`buildRequest`)
2212
+
2213
+ ```tsx
2214
+ import { useData } from "@timeax/form-palette/extra";
2215
+
2216
+ export function CustomPayloadExample() {
2217
+ const { visible } = useData<any, { status?: string }>({
2218
+ endpoint: "/api/users/search",
2219
+ method: "POST",
2220
+ selector: "items",
2221
+
2222
+ buildRequest: ({ query, filters, cursor }) => ({
2223
+ body: {
2224
+ q: query,
2225
+ filters,
2226
+ cursor,
2227
+ },
2228
+ headers: {
2229
+ "X-Search-Mode": "users",
2230
+ },
2231
+ }),
2232
+ });
2233
+
2234
+ return <pre>{JSON.stringify(visible, null, 2)}</pre>;
2235
+ }
2236
+ ```
2237
+
2238
+ ---
2239
+
2240
+ ## Practical tips
2241
+
2242
+ * Want instant search UX and your dataset is small → `searchMode: "local"`.
2243
+ * Want selection to persist while users search remotely → `selection.prune = "never"`.
2244
+ * Want selection to strictly match what’s visible in the current list → `selection.prune = "missing"`.
2245
+ * Want lazy fetch (open a modal first) → `enabled: false`, then call `fetch()` when needed.
2246
+
2247
+ ---
2248
+
2249
+ # 3) JsonEditor (overview)
2250
+
2251
+ JsonEditor is exported from `extra` as:
2252
+
2253
+ ```ts
2254
+ import { JsonEditor } from "@timeax/form-palette/extra";
2255
+ ```
2256
+
2257
+ Use it when you want a structured JSON editing experience (often used behind the `json-editor` InputField variant).
2258
+
2259
+ ## Standalone usage
2260
+
2261
+ ```tsx
2262
+ import * as React from "react";
2263
+ import { JsonEditor } from "@timeax/form-palette/extra";
2264
+
2265
+ export function JsonEditorStandalone() {
2266
+ const [root, setRoot] = React.useState({
2267
+ user: { name: "Ada", roles: ["admin"] },
2268
+ });
2269
+
2270
+ return (
2271
+ <JsonEditor
2272
+ root={root}
2273
+ onRoot={setRoot}
2274
+ open
2275
+ onClose={() => console.log("closed")}
2276
+ title="User JSON"
2277
+ />
2278
+ );
2279
+ }
2280
+ ```