@timeax/form-palette 0.0.29 → 0.0.31

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 CHANGED
@@ -46,13 +46,24 @@ Common props (apply to most variants):
46
46
  - name: unique field key
47
47
  - variant: which control to render (see Variant reference below)
48
48
  - label, sublabel: title and a small inline hint
49
- - description/helpText: helper copy under the control
50
- - errorText: force an error message (or rely on validation)
49
+ - description, helpText: helper copy under the control
50
+ - errorText: force an error message (visual override)
51
51
  - required, disabled, readOnly
52
- - icon, prefix, suffix, leadingControl, trailingControl: decorate the input content
53
- - contain: force the input and label to share a tile-like container
54
- - validate(value, report): return true | false | string for simple inline validation
55
- - onChange(detail): detail.value holds the new value; prevent default if you need to override
52
+ - onChange(e): e.value holds the new value; e.detail provides extra context (source, meta)
53
+ - onValidate(value, field, form): custom validation logic (returns string | boolean)
54
+ - size: "sm" | "md" | "lg"
55
+ - density: "compact" | "comfortable" | "loose"
56
+ - labelPlacement, sublabelPlacement, descriptionPlacement, helpTextPlacement, errorTextPlacement: customize layout ("top" | "bottom" | "left" | "right" | "below")
57
+ - inline, fullWidth, contain: layout flags
58
+ - tags: array of tag objects { label, icon, className, color, bgColor }
59
+ - className, labelClassName, sublabelClassName, descriptionClassName, helpTextClassName, errorClassName, groupClassName, contentClassName, variantClassName: targeting specific parts of the field chrome
60
+
61
+ Input decoration props (available for text, number, color, phone, select, multi-select, date):
62
+ - icon, prefix, suffix, leadingControl, trailingControl: decorate the input box
63
+ - leadingIcons, trailingIcons: arrays of React nodes
64
+ - iconGap, leadingIconSpacing, trailingIconSpacing: spacing controls
65
+ - joinControls, extendBoxToControls: visual integration for controls
66
+ - leadingControlClassName, trailingControlClassName: decoration styling
56
67
 
57
68
  Example with decorations and validation:
58
69
 
@@ -92,7 +103,7 @@ Note on options: selection controls accept options as primitives ("US") or objec
92
103
 
93
104
  1) text
94
105
  - Value: string | undefined
95
- - Nice extras: mask, slotChar, unmask, autoClear (phone-like masking); icon/prefix/suffix; searchable isn’t applicable here
106
+ - Props: mask, slotChar, unmask, autoClear (phone-like masking); prefix, suffix, stripPrefix, stripSuffix, inputClassName
96
107
  - Example:
97
108
  ```tsx
98
109
  <InputField name="email" label="Email" variant="text" />
@@ -100,7 +111,12 @@ Note on options: selection controls accept options as primitives ("US") or objec
100
111
 
101
112
  2) number
102
113
  - Value: number | undefined
103
- - Props: min, max, step, showButtons
114
+ - Props:
115
+ - min, max, step, showButtons, buttonLayout ("stacked" | "inline")
116
+ - mode: "decimal" | "currency"
117
+ - currency, currencyDisplay, currencySymbol
118
+ - locale, useGrouping, minFractionDigits, maxFractionDigits
119
+ - roundingMode, allowEmpty
104
120
  - Example:
105
121
  ```tsx
106
122
  <InputField name="age" label="Age" variant="number" min={0} max={120} step={1} showButtons />
@@ -108,61 +124,100 @@ Note on options: selection controls accept options as primitives ("US") or objec
108
124
 
109
125
  3) password
110
126
  - Value: string | undefined
111
- - Props: showToggle; strengthMeter; meterStyle="rules" | "bar" (depending on preset)
127
+ - Props:
128
+ - revealToggle: boolean (show eye icon); default true
129
+ - defaultRevealed, onRevealChange, renderToggleIcon, toggleAriaLabel, toggleButtonClassName
130
+ - strengthMeter: boolean | StrengthOptions (calc, labels, thresholds, minScore, showLabel, display)
131
+ - meterStyle: "simple" | "rules"
132
+ - ruleDefinitions, ruleUses: customize validation rules
133
+ - meterWrapperClassName, meterContainerClassName, meterBarClassName, meterLabelClassName
134
+ - Example:
135
+ ```tsx
136
+ <InputField name="pwd" label="Password" variant="password" revealToggle strengthMeter meterStyle="rules" />
137
+ ```
138
+
139
+ 4) date
140
+ - Value: Date | DateRange | undefined
141
+ - Props:
142
+ - mode: "single" | "range"
143
+ - kind: "date" | "datetime" | "time" | "hour" | "monthYear" | "year"
144
+ - formatSingle, formatRange, rangeSeparator
145
+ - minDate, maxDate, disabledDays, stayOpenOnSelect
146
+ - showTime, timeMode ("dropdown" | "input"), timeStep, timeLabel
147
+ - clearable, calendarClassName, popoverClassName
112
148
  - Example:
113
149
  ```tsx
114
- <InputField name="pwd" label="Password" variant="password" showToggle strengthMeter meterStyle="rules" />
150
+ <InputField name="appointment" variant="date" kind="datetime" label="Appointment" />
115
151
  ```
116
152
 
117
- 4) color
153
+ 5) color
118
154
  - Value: string | undefined (hex or css color)
119
- - Props: showPreview, previewButtonClassName
155
+ - Props: showPreview, showPickerToggle, previewSize, previewButtonClassName, previewSwatchClassName, pickerInputClassName, pickerToggleIcon, wrapperClassName
120
156
  - Example:
121
157
  ```tsx
122
158
  <InputField name="color" label="Favorite colour" variant="color" showPreview />
123
159
  ```
124
160
 
125
- 5) phone
161
+ 6) phone
126
162
  - Value: string | undefined
127
- - Typical usage uses masking controls the same way as text: mask, slotChar, unmask, autoClear
128
- - Example (from playground labeled "Phone"):
163
+ - Props:
164
+ - countries: PhoneCountry[] (custom list of available countries)
165
+ - defaultCountry: string (ISO code, e.g. "US")
166
+ - valueMode: "masked" | "e164" | "national"
167
+ - showFlag, showSelectedDial, showDialInList, showCountry, showSelectedLabel
168
+ - keepCharPositions, countrySelectClassName, countryTriggerClassName
169
+ - Typical usage:
129
170
  ```tsx
130
171
  <InputField
131
172
  name="phone"
132
173
  label="Phone"
133
- variant="text" // or a dedicated phone variant if enabled in your build
134
- mask="+99 99 999 999? x999"
135
- slotChar="_"
136
- autoClear
174
+ variant="phone"
175
+ defaultCountry="US"
176
+ valueMode="e164"
137
177
  />
138
178
  ```
139
179
 
140
- 6) textarea
180
+ 7) textarea
141
181
  - Value: string | undefined
142
- - Usual textarea props like placeholder, rows, etc.
182
+ - Usual textarea props like placeholder, rows, cols, resize, etc.
143
183
  - Example:
144
184
  ```tsx
145
185
  <InputField name="bio" label="Bio" variant="textarea" description="Tell us about you" />
146
186
  ```
147
187
 
148
- 7) toggle
188
+ 8) toggle
149
189
  - Value: boolean | undefined
190
+ - Props: size, density, controlPlacement ("left" | "right"), onText, offText, switchClassName, switchThumbClassName
150
191
  - Example:
151
192
  ```tsx
152
- <InputField name="tos" variant="toggle" label="Accept Terms" required />
193
+ <InputField name="tos" variant="toggle" label="Accept Terms" onText="Yes" offText="No" required />
153
194
  ```
154
195
 
155
- 8) toggle-group
156
- - Value: string | number | undefined (selected item)
157
- - Props: options (primitives or objects), layout/density sizing depending on preset
196
+ 9) toggle-group
197
+ - Value: string | string[] | number | undefined
198
+ - Props:
199
+ - options: primitives or objects
200
+ - multiple, variant ("default" | "outline"), layout ("horizontal" | "vertical" | "grid"), gridCols, fillWidth
201
+ - optionValue, optionLabel, optionIcon, optionDisabled, optionTooltip, optionMeta: mapping keys
202
+ - Example:
203
+ ```tsx
204
+ <InputField
205
+ name="size"
206
+ variant="toggle-group"
207
+ options={["sm", "md", "lg"]}
208
+ />
209
+ ```
158
210
 
159
- 9) radio
160
- - Value: string | number | undefined
211
+ 10) radio
212
+ - Value: any (selected value)
161
213
  - Props:
162
- - options: primitives or objects with { value, label, description?, disabled? }
163
- - layout?: "list" | "grid" (default "list"); columns?: number (when layout="grid")
164
- - size?: "sm" | "md" | "lg"; density?: "compact" | "comfortable" | "loose"
165
- - You can also map custom item shapes via optionValue/optionLabel style mappers depending on preset
214
+ - options: primitives or objects
215
+ - layout: "list" | "grid"; columns, itemGapPx
216
+ - size, density, autoCap
217
+ - optionValue, optionLabel, optionDescription, optionDisabled
218
+ - mappers: { getValue, getLabel, getDescription, isDisabled, getKey }
219
+ - renderOption: (ctx) => ReactNode
220
+ - groupClassName, optionClassName, labelClassName, descriptionClassName
166
221
  - Example:
167
222
  ```tsx
168
223
  <InputField
@@ -176,8 +231,17 @@ Note on options: selection controls accept options as primitives ("US") or objec
176
231
  />
177
232
  ```
178
233
 
179
- 10) checkbox
180
- - Value: boolean | string[] | number[] depending on single vs group usage
234
+ 11) checkbox
235
+ - Value: boolean (single) | CheckboxGroupEntry[] (group)
236
+ - Props:
237
+ - options: primitives or objects (for group mode)
238
+ - single: boolean (switches to single-checkbox mode)
239
+ - tristate: boolean (enables indeterminate state)
240
+ - layout: "list" | "grid"; columns, itemGapPx
241
+ - size, density, autoCap
242
+ - optionValue, optionLabel, renderOption
243
+ - mappers: { getValue, getLabel, getDescription, isDisabled, getKey, getTristate }
244
+ - groupClassName, optionClassName, labelClassName, optionLabelClassName, descriptionClassName
181
245
  - Single boolean checkbox:
182
246
  ```tsx
183
247
  <InputField variant="checkbox" label="Remember me" />
@@ -191,30 +255,21 @@ Note on options: selection controls accept options as primitives ("US") or objec
191
255
  options={[
192
256
  { value: "read", label: "Read content" },
193
257
  { value: "write", label: "Write content" },
194
- { value: "delete", label: "Delete content" },
195
258
  ]}
196
259
  />
197
260
  ```
198
261
 
199
- - Extras:
200
- - single?: boolean switches to single‑checkbox mode (value becomes boolean | undefined)
201
- - tristate?: boolean enables an indeterminate state for single or per‑item
202
- - layout?: "list" | "grid"; columns?: number (grid mode)
203
- - size?: "sm" | "md" | "lg"; density?: "compact" | "comfortable" | "loose"
204
-
205
- 11) select
262
+ 12) select
206
263
  - Value: string | number | undefined
207
- - Props (high‑use):
208
- - options: (string|number)[] | { label?, value?, description?, disabled?, icon?, ... }[]
209
- - searchable?: boolean (inline search box)
210
- - searchPlaceholder?: string (placeholder inside the search box)
211
- - clearable?: boolean (show clear button)
212
- - placeholder?: string
213
- - autoCap?: boolean (capitalise label text)
214
- - emptyLabel?: React.ReactNode (shown when there are no options)
215
- - emptySearchText?: React.ReactNode (shown when search returns no results)
216
- - optionLabel, optionValue, optionDescription, optionDisabled, optionIcon, optionKey: map/compute option pieces
217
- - renderOption?: (ctx) => ReactNode (custom row rendering; per‑option render overrides this)
264
+ - Props:
265
+ - options: primitives or objects
266
+ - searchable, searchPlaceholder, clearable, placeholder, autoCap
267
+ - emptyLabel, emptySearchText
268
+ - mode: "default" | "button"; button: ReactNode | ((ctx) => ReactNode)
269
+ - virtualScroll, virtualScrollPageSize, virtualScrollThreshold
270
+ - optionLabel, optionValue, optionDescription, optionDisabled, optionIcon, optionKey
271
+ - renderOption, renderValue: custom renderers
272
+ - triggerClassName, contentClassName
218
273
  - Example:
219
274
  ```tsx
220
275
  <InputField
@@ -222,20 +277,22 @@ Note on options: selection controls accept options as primitives ("US") or objec
222
277
  variant="select"
223
278
  label="Country"
224
279
  options={[{ label: "USA", value: "US" }, { label: "Canada", value: "CA" }]}
225
- placeholder="Select a country"
226
280
  searchable
227
281
  clearable
228
282
  />
229
283
  ```
230
284
 
231
- 12) multi-select
232
- - Value: (string|number)[] | undefined
233
- - Props: same mapping props as select (optionLabel, optionValue, optionDescription, optionDisabled, optionIcon, optionKey)
234
- - searchable?: boolean; searchPlaceholder?: string
235
- - clearable?: boolean; placeholder?: React.ReactNode
236
- - autoCap?: boolean
237
- - emptyLabel?: React.ReactNode; emptySearchText?: React.ReactNode
238
- - renderOption?: (ctx) => ReactNode (custom row rendering; per‑option render overrides this)
285
+ 13) multi-select
286
+ - Value: (string | number)[] | undefined
287
+ - Props:
288
+ - options: primitives or objects
289
+ - searchable, searchPlaceholder, clearable, placeholder, autoCap
290
+ - showSelectAll, selectAllLabel, selectAllPosition
291
+ - mode: "default" | "button"; button: ReactNode | ((ctx) => ReactNode)
292
+ - maxListHeight
293
+ - optionLabel, optionValue, optionDescription, optionDisabled, optionIcon, optionKey
294
+ - renderOption, renderValue, renderCheckbox
295
+ - triggerClassName, contentClassName
239
296
  - Example:
240
297
  ```tsx
241
298
  <InputField
@@ -246,83 +303,155 @@ Note on options: selection controls accept options as primitives ("US") or objec
246
303
  />
247
304
  ```
248
305
 
249
- 13) chips
250
- - Value: string[] | number[] | undefined
251
- - Free‑form or from options; often used to add/remove tokens
252
-
253
- 14) treeselect
254
- - Value: (string|number)[] | string | number | undefined (single or multiple tree selection)
255
- - Option type: TreeSelectOption = { label, value, icon?, description?, children?: TreeSelectOption[] }
306
+ 14) chips
307
+ - Value: string[] | undefined
308
+ - Props:
309
+ - placeholder, separators (string | RegExp), allowDuplicates, maxChips
310
+ - addOnEnter, addOnTab, addOnBlur, backspaceRemovesLast
311
+ - clearable, textareaMode, placement ("inline" | "below")
312
+ - maxVisibleChips, maxChipChars, maxChipWidth
313
+ - renderChip, renderOverflowChip
314
+ - onAddChips, onRemoveChips
315
+ - chipsClassName, chipClassName, chipLabelClassName, chipRemoveClassName, inputClassName
256
316
  - Example:
257
317
  ```tsx
258
- import { TreeSelectOption } from "@timeax/form-palette/presets/shadcn-variants/tree-select-types";
259
-
260
- const regionOptions: TreeSelectOption[] = [
261
- { label: "Africa", value: "africa", children: [{ label: "Nigeria", value: "ng" }] },
262
- { label: "Europe", value: "europe" },
263
- ];
264
-
265
- <InputField
266
- name="regions"
267
- label="Regions"
268
- variant="treeselect"
269
- options={regionOptions}
270
- />
318
+ <InputField name="keywords" variant="chips" label="Keywords" />
271
319
  ```
272
320
 
273
- - Props (high‑use):
274
- - multiple?: boolean (default true). If false, single‑select behaviour.
275
- - searchable?: boolean; searchPlaceholder?: string
276
- - clearable?: boolean; placeholder?: React.ReactNode
277
- - autoCap?: boolean
321
+ 15) treeselect
322
+ - Value: TreeKey | TreeKey[] | undefined
323
+ - Props:
324
+ - options: TreeSelectOption[]
325
+ - multiple, searchable, clearable, placeholder, autoCap
326
+ - expandAll, defaultExpandedValues, leafOnly
327
+ - mode: "default" | "button"; button: ReactNode | ((ctx) => ReactNode)
328
+ - selectedBadge, selectedBadgeVariant, selectedBadgePlacement
278
329
  - optionLabel, optionValue, optionDescription, optionDisabled, optionIcon, optionKey
279
- - emptyLabel?: React.ReactNode; emptySearchText?: React.ReactNode
280
- - renderOption?: ({ item, selected, option, click }) => ReactNode
281
- - renderValue?: ({ selectedItems, placeholder }) => ReactNode (custom trigger content)
282
- - expandAll?: boolean; defaultExpandedValues?: (string|number)[]
283
- - leafOnly?: boolean (only leaf nodes are selectable)
284
- - mode?: "default" | "button"; when "button", you can provide a custom trigger and show a selected‑count badge
330
+ - renderOption, renderValue
331
+ - triggerClassName, contentClassName
332
+ - Example:
333
+ ```tsx
334
+ <InputField name="regions" variant="treeselect" options={regionOptions} />
335
+ ```
285
336
 
286
- 15) slider
287
- - Value: number | [number, number] depending on range mode
288
- - Props: min, max, step; possibly range/multiple depending on preset
337
+ 16) slider
338
+ - Value: number | undefined
339
+ - Props:
340
+ - min, max, step
341
+ - showValue, valuePlacement ("start" | "end")
342
+ - formatValue: (val) => ReactNode
343
+ - sliderClassName, valueClassName
344
+ - Example:
345
+ ```tsx
346
+ <InputField name="volume" variant="slider" min={0} max={100} />
347
+ ```
289
348
 
290
- 16) file
291
- - Value: File | File[] | Custom file shape depending on configuration
292
- - Types: FileItem, CustomFileLoader, FileLike are exported from the preset if you need advanced control
293
- - Example (simple):
349
+ 17) file
350
+ - Value: FileItem[] | undefined
351
+ - Props:
352
+ - multiple, accept, maxFiles, maxTotalSize
353
+ - showDropArea, dropIcon, dropTitle, dropDescription
354
+ - renderDropArea, renderFileItem, showCheckboxes
355
+ - onFilesAdded, customLoader, mergeMode ("append" | "replace")
356
+ - formatFileName, formatFileSize
357
+ - mode: "default" | "button"; button: ReactNode | ((ctx) => ReactNode)
358
+ - selectedBadge, selectedBadgeVariant, selectedBadgePlacement
359
+ - dropAreaClassName, listClassName, triggerClassName
360
+ - Example:
294
361
  ```tsx
295
- <InputField name="avatar" label="Avatar" variant="file" />
362
+ <InputField name="attachments" variant="file" multiple />
296
363
  ```
297
364
 
298
- 17) keyvalue
365
+ 18) keyvalue
299
366
  - Value: Record<string, string> | undefined
300
- - Use to capture arbitrary key/value pairs
367
+ - Props:
368
+ - min, max, minVisible, maxVisible
369
+ - showAddButton, showMenuButton, dialogTitle
370
+ - keyLabel, valueLabel, submitLabel, emptyLabel, moreLabel
371
+ - chipsClassName, chipClassName, renderChip
372
+ - Example:
373
+ ```tsx
374
+ <InputField name="headers" variant="keyvalue" label="HTTP Headers" />
375
+ ```
301
376
 
302
- 18) editor
377
+ 19) editor
303
378
  - Value: string | undefined (HTML or Markdown)
304
- - Requires host CSS import once in your app:
305
- - import "@toast-ui/editor/dist/toastui-editor.css";
306
- - Props (high‑use):
307
- - format?: "html" | "markdown" (stored value format; default "html")
308
- - toolbar?: "default" | "none" | ToastToolbarItem[][]
309
- - height?: string (e.g., "400px"), placeholder?: string
310
- - editType?: "wysiwyg" | "markdown"; previewStyle?: "vertical" | "tab"
311
- - pastePlainText?: boolean (force plain text on paste)
379
+ - Props:
380
+ - format: "html" | "markdown"
381
+ - toolbar: "default" | "none" | ToastToolbarItem[][]
382
+ - height, placeholder, editType ("wysiwyg" | "markdown"), previewStyle ("vertical" | "tab")
383
+ - pastePlainText, useCommandShortcut
384
+ - Example:
385
+ ```tsx
386
+ <InputField name="content" variant="editor" format="markdown" />
387
+ ```
388
+
389
+ 20) lister
390
+ - Value: ListerId | ListerId[] | undefined
391
+ - Advanced picker for remote/large datasets. Requires `ListerProvider` and `ListerUI` from `@timeax/form-palette/extra`.
392
+ - Props:
393
+ - def: `ListerDefinition` (the remote data engine)
394
+ - endpoint, method, buildRequest, selector: inline remote config
395
+ - filters, filtersSpec, initialQuery: initial state and filter UI configuration
396
+ - search, searchTarget, searchMode: control search behavior and targets
397
+ - mode: "single" | "multiple"
398
+ - confirm: boolean (for single mode); permissions: array of strings
399
+ - title, clearable, maxDisplayItems, showRefresh, refreshMode
400
+ - optionValue, optionLabel, optionIcon, optionDescription, optionDisabled, optionGroup, optionMeta: mapping keys (accepts key string or mapper function)
401
+ - renderTrigger, renderOption: custom rendering hooks
402
+ - panelClassName, contentClassName, triggerClassName
403
+ - leadingIcons, trailingIcons, icon, leadingControl, trailingControl, joinControls, extendBoxToControls: standard input decoration
312
404
  - Example:
313
405
  ```tsx
314
406
  <InputField
315
- name="content"
316
- label="Content"
317
- variant="editor"
318
- format="markdown"
319
- toolbar="default"
320
- height="400px"
407
+ name="user"
408
+ variant="lister"
409
+ endpoint="/api/users"
410
+ optionLabel="fullName"
411
+ optionValue="id"
321
412
  />
322
413
  ```
323
414
 
324
- 19) custom
325
- - Bring your own control but still benefit from InputField’s layout and validation chrome
415
+ 21) json-editor
416
+ - Value: JsonObject | undefined
417
+ - Advanced visual editor for complex JSON structures.
418
+ - Props:
419
+ - title, schema: header text and validation schema (JSON Schema or ID)
420
+ - fieldMap, layout, defaults: configure how JSON keys are rendered as form fields
421
+ - nav, filters, permissions, callbacks: behavior and access control
422
+ - mode: "popover" | "accordion" (wrapper display mode)
423
+ - triggerLabel, triggerVariant, triggerSize: customize the trigger button (popover mode)
424
+ - route, defaultRoute, onRouteChange: navigation control (JSON path)
425
+ - viewMode, defaultViewMode, onViewModeChange: "split" | "visual" | "raw"
426
+ - renderRouteLabel, renderField: custom rendering hooks
427
+ - contentClassName, navClassName, triggerClassName, popoverClassName, panelClassName
428
+ - leadingIcons, trailingIcons, icon, leadingControl, trailingControl, joinControls, extendBoxToControls: standard input decoration (for popover trigger)
429
+ - Example:
430
+ ```tsx
431
+ <InputField
432
+ name="config"
433
+ variant="json-editor"
434
+ title="App Configuration"
435
+ mode="popover"
436
+ triggerLabel="Edit App Config"
437
+ />
438
+ ```
439
+
440
+ 22) custom
441
+ - Props:
442
+ - component: ReactComponent to render
443
+ - valueProp, changeProp, disabledProp, readOnlyProp, errorProp, idProp, nameProp, placeholderProp
444
+ - mapValue, mapDetail: transform values on the way out/in
445
+ - Example:
446
+ ```tsx
447
+ <InputField
448
+ name="custom"
449
+ variant="custom"
450
+ component={MyCustomInput}
451
+ valueProp="currentValue"
452
+ changeProp="onMyChange"
453
+ />
454
+ ```
326
455
 
327
456
  ---
328
457
 
@@ -351,6 +480,7 @@ Password with strength meter:
351
480
  placeholder="Enter your password"
352
481
  strengthMeter
353
482
  meterStyle="rules"
483
+ revealToggle
354
484
  />
355
485
  ```
356
486
 
@@ -420,8 +550,823 @@ function Example() {
420
550
  - Prefer InputField over wiring controls by hand; it gives you consistent labels, descriptions, and error placement.
421
551
  - Use options as primitives for quick setups, or objects when you need description/disabled/icon per item.
422
552
  - Use validate for quick client checks; you can also set errorText manually.
423
- - For grouped controls (radios/checkbox groups), pass options; for a single boolean, omit options.
424
- - Keep labels short and place longer helper copy into description/helpText.
553
+ - For grouped controls (radios/checkbox groups), pass `options`; for a single boolean, omit `options` and use `variant="checkbox"`.
554
+ - Use `variant="date"` with `kind` to switch between date, time, and datetime pickers.
555
+ - For complex data, `variant="lister"` provides a powerful remote search/picker UI.
556
+ - Use `onValidate` for quick client checks; return a string for the error message or `true` if valid.
557
+
558
+ ---
559
+
560
+ ### Advanced providers and standalone usage
561
+
562
+ Some components like `lister` or `json-editor` can be used as standalone utilities outside of `InputField` or even outside of a `Form`.
563
+
564
+ #### 1. Lister System (ListerProvider & ListerUI)
565
+
566
+ The `lister` variant relies on a global engine for remote data fetching, caching, and state management. You must place `ListerProvider` at the root of your app and include `ListerUI` to render the floating selection panels.
567
+
568
+ ```tsx
569
+ import { ListerProvider, ListerUI } from "@timeax/form-palette/extra";
570
+
571
+ const listerHost = {
572
+ can: (perms) => true,
573
+ log: (entry) => console.log(entry.message),
574
+ };
575
+
576
+ function App() {
577
+ return (
578
+ <ListerProvider host={listerHost}>
579
+ {/* ... your app ... */}
580
+ <ListerUI />
581
+ </ListerProvider>
582
+ );
583
+ }
584
+ ```
585
+
586
+ **ListerProvider props:**
587
+ - `host`: `ListerProviderHost` (Required) - Handles permissions (`can`) and global logging (`log`).
588
+ - `presets`: `PresetMap` - Registry of named `ListerDefinition` objects.
589
+ - `http`: `ListerHttpClient` - Custom HTTP client (e.g., axios wrapper).
590
+ - `remoteDebounceMs`: `number` (default: 300).
591
+
592
+ **Programmatic usage with `useLister`:**
593
+
594
+ You can trigger a lister picker from anywhere in your app (e.g., a custom button) using the `useLister` hook.
595
+
596
+ ```tsx
597
+ import { useLister } from "@timeax/form-palette/extra";
598
+
599
+ function CustomButton() {
600
+ const { api } = useLister();
601
+
602
+ async function pickUser() {
603
+ // Open the lister UI and wait for selection
604
+ const result = await api.open({
605
+ source: { endpoint: "/api/users" },
606
+ mapping: { optionLabel: "fullName", optionValue: "id" }
607
+ }, {}, {
608
+ title: "Select a User",
609
+ mode: "single"
610
+ });
611
+
612
+ if (result.reason === "apply") {
613
+ console.log("Selected user ID:", result.value);
614
+ console.log("Full user object:", result.details.raw);
615
+ }
616
+ }
617
+
618
+ return <button onClick={pickUser}>Choose User</button>;
619
+ }
620
+ ```
621
+
622
+ - `api.open(def | kind, filters, options)`: Returns a promise that resolves when the user applies, cancels, or closes the lister.
623
+ - `api.fetch(def | kind, filters, options)`: Fetches data using the lister engine without opening the UI.
624
+
625
+ ---
626
+
627
+ #### 2. JSON Editor Standalone
628
+
629
+ While `InputField variant="json-editor"` is convenient for forms, you can use the `JsonEditor` component directly for full-page editors or custom configuration screens.
630
+
631
+ ```tsx
632
+ import { JsonEditor } from "@timeax/form-palette/extra";
633
+
634
+ function ConfigPage() {
635
+ const [config, setConfig] = useState({ theme: "dark", notifications: true });
636
+
637
+ return (
638
+ <div className="h-[600px] border rounded">
639
+ <JsonEditor
640
+ root={config}
641
+ onRoot={setConfig}
642
+ title="Site Configuration"
643
+ mode="accordion"
644
+ viewMode="split"
645
+ fieldMap={{
646
+ "theme": { variant: "select", options: ["light", "dark"] },
647
+ "notifications": { variant: "toggle" }
648
+ }}
649
+ />
650
+ </div>
651
+ );
652
+ }
653
+ ```
654
+
655
+ ##### Props for `JsonEditor` (Standalone)
656
+
657
+ | Prop | Type | Description |
658
+ | :--- | :--- | :--- |
659
+ | `root` | `JsonObject` | The JSON object to edit (Controlled). |
660
+ | `onRoot` | `(next: JsonObject, detail: ChangeDetail) => void` | Callback when the JSON structure changes. |
661
+ | `title` | `ReactNode` | Header title displayed at the top. |
662
+ | `schema` | `string \| JsonObject` | Optional JSON Schema for validation. |
663
+ | `mode` | `"popover" \| "accordion"` | `"popover"` shows a trigger; `"accordion"` is inline. |
664
+ | `viewMode` | `"split" \| "visual" \| "raw"` | Visual vs Code editor vs both. |
665
+ | `fieldMap` | `JsonEditorFieldMap` | Map JSON paths to specific `InputField` variants. |
666
+ | `layout` | `JsonEditorLayoutMap` | Define the visual order and grouping of fields. |
667
+ | `defaults` | `JsonEditorDefaults` | Default values and variant-level defaults. |
668
+ | `nav` | `JsonEditorNavOptions` | Configure sidebar navigation and hierarchical views. |
669
+ | `filters` | `JsonEditorFilters` | Include or exclude specific JSON paths. |
670
+ | `permissions` | `JsonEditorPermissions` | Read-only vs Read-Write controls per path. |
671
+ | `callbacks` | `JsonEditorCallbacks` | Callbacks for `onAdd`, `onDelete`, `onEdit`. |
672
+
673
+ ---
674
+
675
+ ### Full Technical Reference (Advanced)
676
+
677
+ #### Lister System Types
678
+
679
+ **ListerProvider Props**
680
+
681
+ | Prop | Type | Description |
682
+ | :--- | :--- | :--- |
683
+ | `host` | `ListerProviderHost` | **Required**. Handles permissions (`can`) and logging (`log`). |
684
+ | `presets` | `PresetMap` | Map of pre-defined lister configurations. |
685
+ | `http` | `ListerHttpClient` | Custom client for data fetching. |
686
+ | `remoteDebounceMs`| `number` | Debounce for remote search (default 300ms). |
687
+
688
+ **ListerDefinition (Engine config)**
689
+
690
+ | Prop | Type | Description |
691
+ | :--- | :--- | :--- |
692
+ | `source` | `ListerSource` | Remote URL and request builder. |
693
+ | `mapping` | `ListerMapping` | How raw data maps to value/label/icon/etc. |
694
+ | `selector` | `Selector` | Extraction path from API response (default `body.data`). |
695
+ | `search` | `ListerSearchSpec` | Search behavior and column targets. |
696
+
697
+ **ListerOpenOptions (api.open / api.fetch options)**
698
+
699
+ | Prop | Type | Description |
700
+ | :--- | :--- | :--- |
701
+ | `mode` | `"single" \| "multiple"` | Selection mode. |
702
+ | `confirm` | `boolean` | Require explicit "Apply" button in single mode. |
703
+ | `defaultValue`| `any` | Initial selection. |
704
+ | `permissions`| `string[]` | Permission keys required for this session. |
705
+ | `searchMode` | `ListerSearchMode` | "local", "remote", or "hybrid". |
706
+ | `title` | `string` | UI title for the popover. |
707
+ | `filtersSpec` | `ListerFilterSpec` | Specification for the filter UI. |
708
+
709
+ ## `useData` hook (Lister preset) — detailed guide
710
+
711
+ `useData` is the lightweight data + search + selection hook used by the **Lister** preset. It’s meant for “fetch list → search it (remote/local/hybrid) → optionally select items by stable IDs”.
712
+
713
+ You’ll typically use it inside lister-style UIs or any picker/list component that wants the same semantics as the Lister engine.
714
+
715
+ ---
716
+
717
+ ### What it manages
718
+
719
+ `useData` manages these concerns for you:
720
+
721
+ 1. **Fetching**
722
+
723
+ * Uses the `ListerProvider` context (`Ctx`) to call `ctx.apiFetchAny(...)`.
724
+ * Supports request building via inline definition inputs (`endpoint`, `method`, `selector`, `buildRequest`, `search`).
725
+
726
+ 2. **Search modes**
727
+
728
+ * **remote**: typing triggers a debounced fetch with search payload.
729
+ * **local**: fetch a “base dataset” once, then search/filter client-side.
730
+ * **hybrid**: fetch remote on typing *and* also supports local filtering rules.
731
+
732
+ 3. **Search targeting**
733
+
734
+ * Uses `searchTarget` to build a provider-compatible search payload via `buildSearchPayloadFromTarget(...)`.
735
+ * Payload can express:
736
+
737
+ * `subject` (search only a specific field)
738
+ * `searchAll` (broad search)
739
+ * `searchOnly` (restrict results to allowed IDs / “only”)
740
+
741
+ 4. **Filters**
742
+
743
+ * Tracks `filters` state and can auto-refetch when filters change (remote/hybrid).
744
+
745
+ 5. **Selection (optional)**
746
+
747
+ * Select **by stable key** (`id`, `value`, or a custom key resolver).
748
+ * Supports `single` or `multiple` selection.
749
+ * Returns **selected objects**, not just IDs.
750
+ * Keeps a cache so selection can survive list changes.
751
+
752
+ ---
753
+
754
+ ### Basic usage
755
+
756
+ ```tsx
757
+ const {
758
+ data,
759
+ visible,
760
+ loading,
761
+
762
+ query,
763
+ setQuery,
764
+
765
+ searchMode,
766
+ setSearchMode,
767
+
768
+ searchTarget,
769
+ setSearchTarget,
770
+
771
+ filters,
772
+ setFilters,
773
+
774
+ refresh,
775
+ } = useData({
776
+ endpoint: "/api/users",
777
+ method: "GET",
778
+ search: { default: "fullName" }, // default subject for subject-search
779
+ });
780
+ ```
781
+
782
+ **Key outputs**
783
+
784
+ * `data`: the latest fetched list
785
+ * `visible`: the list after applying local/hybrid filtering rules
786
+ * `loading` / `error`
787
+ * `query` + `setQuery(...)`
788
+ * `searchMode` + `setSearchMode(...)`
789
+ * `searchTarget` + `setSearchTarget(...)`
790
+ * `filters` + filter helpers
791
+ * `refresh()` and `fetch()` for manual control
792
+
793
+ ---
794
+
795
+ ### Fetching details
796
+
797
+ `useData` builds an **inline lister definition** (via `makeInlineDef`) from:
798
+
799
+ * `endpoint`
800
+ * `method`
801
+ * `selector` (how to extract array from response)
802
+ * `buildRequest` (how to shape params/body/headers)
803
+ * `search` (search spec defaults)
804
+
805
+ Then it calls:
806
+
807
+ ```ts
808
+ ctx.apiFetchAny(inlineDef, filters, {
809
+ query,
810
+ search: buildSearchPayloadFromTarget(searchTarget),
811
+ });
812
+ ```
813
+
814
+ **Last-request-wins**
815
+
816
+ * The hook uses an internal request counter so only the latest request updates state.
817
+
818
+ ---
819
+
820
+ ### Search mode semantics
821
+
822
+ #### `remote`
823
+
824
+ * Every query change triggers a **debounced fetch**.
825
+ * The fetch includes a search payload derived from `searchTarget`.
826
+ * Best when the dataset is large or server-side search is needed.
827
+
828
+ #### `local`
829
+
830
+ * When you switch to `local`, the hook performs a one-time fetch for a “base list”:
831
+
832
+ * `query: ""`
833
+ * `search: undefined`
834
+ * After that, it searches locally over `data` to produce `visible`.
835
+ * No more fetches on query changes.
836
+
837
+ This is ideal when:
838
+
839
+ * you want “download once, search locally”
840
+ * your endpoint can return an appropriate base dataset
841
+
842
+ #### `hybrid`
843
+
844
+ * Behaves like remote (debounced fetch on query changes),
845
+ * but also allows local filtering semantics for `visible`
846
+ (useful if the UI wants to apply `searchOnly` / field search locally too).
847
+
848
+ ---
849
+
850
+ ### `searchTarget` and what it does
851
+
852
+ `searchTarget` is a structured way to describe *how* you want to search.
853
+ `buildSearchPayloadFromTarget(searchTarget)` converts it into a normalized payload the provider understands.
854
+
855
+ Common patterns:
856
+
857
+ * **Subject search** (search a single column/property):
858
+
859
+ * payload contains `subject: "fullName"`
860
+ * **Only restriction** (restrict to a subset of IDs):
861
+
862
+ * payload contains `searchOnly: [...]`
863
+
864
+ In `local`/`hybrid`, `visible` uses that payload to decide:
865
+
866
+ * whether to filter by `searchOnly`
867
+ * whether to search a specific subject field
868
+ * otherwise falls back to a broad string match
869
+
870
+ ---
871
+
872
+ ### Filters
873
+
874
+ You can pass initial filters:
875
+
876
+ ```tsx
877
+ useData({
878
+ endpoint: "/api/users",
879
+ filters: { status: "active" },
880
+ });
881
+ ```
882
+
883
+ And update them via:
884
+
885
+ * `setFilters(next)`
886
+ * `patchFilters(partial)`
887
+ * `clearFilters()`
888
+
889
+ **Auto-fetch on filter change**
890
+
891
+ * Controlled by `autoFetchOnFilterChange` (default: true)
892
+ * Only triggers fetches in `remote`/`hybrid` modes
893
+ * `local` mode does not re-fetch on filter change (by design)
894
+
895
+ ---
896
+
897
+ ### Selection support (optional)
898
+
899
+ Enable selection like:
900
+
901
+ ```tsx
902
+ const d = useData({
903
+ endpoint: "/api/users",
904
+ selection: {
905
+ mode: "multiple",
906
+ key: "id", // or (item) => item.user_id
907
+ prune: "never", // recommended default
908
+ },
909
+ });
910
+ ```
911
+
912
+ **What you get**
913
+
914
+ * `selectionMode`: `"none" | "single" | "multiple"`
915
+ * `selectedIds`: `id | id[] | null`
916
+ * `selected`: the resolved object(s) from the latest list/cache
917
+ * selection helpers:
918
+
919
+ * `select(id|ids)`
920
+ * `deselect(id|ids)`
921
+ * `toggle(id)`
922
+ * `clearSelection()`
923
+ * `isSelected(id)`
924
+ * `getSelection()` (returns object(s), not ids)
925
+
926
+ **Key resolution**
927
+
928
+ * If you don’t provide `selection.key`, it defaults to:
929
+
930
+ * `item.id ?? item.value`
931
+
932
+ **Cache behavior**
933
+
934
+ * The hook maintains an internal `Map<id, item>` cache.
935
+ * This allows `selected` to still return objects even if the latest `data` no longer contains them.
936
+
937
+ **Pruning**
938
+
939
+ * `prune: "missing"` will remove selection IDs that do not exist in the latest fetched list.
940
+ * Default is `"never"` (recommended), because remote searching can change the list and you don’t want selection wiped.
941
+
942
+ ---
943
+
944
+ ### When to use which mode
945
+
946
+ * Use **remote** when:
947
+
948
+ * dataset is large
949
+ * server-side filtering/search is required
950
+ * Use **local** when:
951
+
952
+ * you can fetch a reasonable base dataset once
953
+ * you want fast client-side searching
954
+ * Use **hybrid** when:
955
+
956
+ * you want remote search results but still need local-only behaviors (like `searchOnly` restrictions)
957
+
958
+ ## `useLister` hook (programmatic lister control)
959
+
960
+ `useLister` is the **low-level programmatic API** for the lister engine. Use it when you want to:
961
+
962
+ * Open a lister picker from anywhere (button click, context menu, shortcut)
963
+ * Fetch option lists using the same engine as the `lister` variant (without opening UI)
964
+ * Read and control active lister sessions (query, mode, filters, selection)
965
+ * Register/retrieve reusable lister presets at runtime
966
+
967
+ It must be used inside `<ListerProvider />`.
968
+
969
+ ---
970
+
971
+ ### Import
972
+
973
+ ```tsx
974
+ import { useLister } from "@timeax/form-palette/extra";
975
+ ```
976
+
977
+ ---
978
+
979
+ ### What you get back
980
+
981
+ ```ts
982
+ const { api, store, state, actions } = useLister();
983
+ ```
984
+
985
+ #### 1) `api`
986
+
987
+ A stable object exposing the two core operations:
988
+
989
+ * **`api.open(kindOrDef, filters?, opts?)`**
990
+ Opens the lister UI (popover/panel) and returns a promise that resolves when the user **Apply / Cancel / Close**.
991
+
992
+ * **`api.fetch(kindOrDef, filters?, opts?)`**
993
+ Performs a fetch through the same engine and returns `{ rawList, optionsList }`-style results (depending on your mapping/selector).
994
+ Use this when you want data but don’t want to show the picker UI.
995
+
996
+ Also includes:
997
+
998
+ * **`api.registerPreset(kind, def)`** — register a preset definition by string key
999
+ * **`api.getPreset(kind)`** — retrieve a preset
1000
+
1001
+ > `kindOrDef` can be either a preset key (string) or a full `ListerDefinition` object.
1002
+
1003
+ ---
1004
+
1005
+ #### 2) `store`
1006
+
1007
+ The **global lister store** maintained by the provider.
1008
+
1009
+ * `store.order`: session z-order / focus stack
1010
+ * `store.activeId`: currently focused session id
1011
+ * `store.sessions`: record of all sessions keyed by `sessionId`
1012
+
1013
+ This is useful if you’re building custom UI around sessions.
1014
+
1015
+ ---
1016
+
1017
+ #### 3) `state`
1018
+
1019
+ A convenience accessor for the **active session state**:
1020
+
1021
+ ```ts
1022
+ const active = state; // AnyState | undefined
1023
+ ```
1024
+
1025
+ * `undefined` when no session is open/focused
1026
+ * otherwise the session’s runtime state (query, lists, selection, filters, etc.)
1027
+
1028
+ ---
1029
+
1030
+ #### 4) `actions`
1031
+
1032
+ Direct actions wired to the provider.
1033
+
1034
+ These are **imperative controls** you can call from anywhere.
1035
+
1036
+ ##### Session lifecycle
1037
+
1038
+ * `focus(sessionId)` — bring session to front and make it active
1039
+ * `dispose(sessionId)` — destroy session state and timers
1040
+
1041
+ ##### Finalize
1042
+
1043
+ * `apply(sessionId)` — resolve promise with “apply” and (if ephemeral) close
1044
+ * `cancel(sessionId)` — resolve promise with “cancel” and close
1045
+ * `close(sessionId)` — resolve promise with “close” and close
1046
+
1047
+ ##### Selection
1048
+
1049
+ * `toggle(sessionId, value)`
1050
+ * `select(sessionId, value)`
1051
+ * `deselect(sessionId, value)`
1052
+ * `clear(sessionId)`
1053
+
1054
+ > These operate on the session’s **draftValue** (single id or array of ids).
1055
+
1056
+ ##### Search state
1057
+
1058
+ * `setQuery(sessionId, q)` — updates session query
1059
+ * `setSearchMode(sessionId, mode)` — local/remote/hybrid
1060
+ * `setSearchTarget(sessionId, target)` — persist subject/all/only targeting
1061
+
1062
+ ##### Search execution
1063
+
1064
+ You get two overload-friendly helpers:
1065
+
1066
+ * `searchRemote(sessionId, q, payload?)`
1067
+ * `searchLocal(sessionId, q, payload?)`
1068
+
1069
+ Both accept an optional **payload override** (`{ subject } | { searchAll: true } | { searchOnly: [...] }`).
1070
+ If you omit it, the provider derives payload from `searchTarget`.
1071
+
1072
+ ##### Refresh + positioning
1073
+
1074
+ * `refresh(sessionId)` — re-fetch using latest filters/query/target
1075
+ * `setPosition(sessionId, pos)` — store draggable panel position
1076
+
1077
+ ##### Filters
1078
+
1079
+ Filters are intentionally split into two layers:
1080
+
1081
+ 1. **Ctx-driven filters** (data state)
1082
+
1083
+ * `getFilterCtx(sessionId)` returns a small controller:
1084
+
1085
+ * `ctx.set(key, value)`
1086
+ * `ctx.merge(patch)`
1087
+ * `ctx.unset(key)`
1088
+ * `ctx.clear()`
1089
+ * `ctx.refresh()`
1090
+
1091
+ 2. **Filter option clicks** (UI option-id based)
1092
+
1093
+ * `applyFilterOption(sessionId, optionId)`
1094
+
1095
+ This method toggles a filter option by its **UI id** (not DB value) and recomputes the effective filter payload.
1096
+
1097
+ ##### Visible options (local/hybrid)
1098
+
1099
+ * `getVisibleOptions(sessionId)`
1100
+
1101
+ Returns the current “visible” list after applying:
1102
+
1103
+ * local/hybrid filtering rules
1104
+ * search payload (`subject/searchAll/searchOnly`)
1105
+ * filters
1106
+
1107
+ This is what you typically render in custom UIs.
1108
+
1109
+ ---
1110
+
1111
+ ### Common usage patterns
1112
+
1113
+ #### 1) Open a picker from a button
1114
+
1115
+ ```tsx
1116
+ function PickUserButton() {
1117
+ const { api } = useLister();
1118
+
1119
+ async function pick() {
1120
+ const res = await api.open(
1121
+ {
1122
+ source: { endpoint: "/api/users" },
1123
+ mapping: { optionLabel: "fullName", optionValue: "id" },
1124
+ },
1125
+ {},
1126
+ { title: "Select a user", mode: "single" }
1127
+ );
1128
+
1129
+ if (res.reason === "apply") {
1130
+ console.log("Selected id", res.value);
1131
+ console.log("Selected object", res.details.raw);
1132
+ }
1133
+ }
1134
+
1135
+ return <button onClick={pick}>Pick user</button>;
1136
+ }
1137
+ ```
1138
+
1139
+ #### 2) Fetch options without opening UI
1140
+
1141
+ ```tsx
1142
+ function useUserOptions() {
1143
+ const { api } = useLister();
1144
+
1145
+ return React.useCallback(async () => {
1146
+ const res = await api.fetch(
1147
+ {
1148
+ source: { endpoint: "/api/users" },
1149
+ mapping: { optionLabel: "fullName", optionValue: "id" },
1150
+ },
1151
+ { status: "active" },
1152
+ { query: "", search: { searchAll: true } }
1153
+ );
1154
+
1155
+ return res.options;
1156
+ }, [api]);
1157
+ }
1158
+ ```
1159
+
1160
+ #### 3) Drive the active session imperatively
1161
+
1162
+ ```tsx
1163
+ function ListerDebugControls() {
1164
+ const { state, actions } = useLister();
1165
+
1166
+ if (!state) return null;
1167
+
1168
+ return (
1169
+ <div className="flex gap-2">
1170
+ <button onClick={() => actions.refresh(state.sessionId)}>Refresh</button>
1171
+ <button onClick={() => actions.clear(state.sessionId)}>Clear</button>
1172
+ <button onClick={() => actions.close(state.sessionId)}>Close</button>
1173
+ </div>
1174
+ );
1175
+ }
1176
+ ```
1177
+
1178
+ ---
1179
+
1180
+ ### Notes and gotchas
1181
+
1182
+ * `useLister` does **not** render UI. The UI is rendered by `ListerUI` (and the `lister` variant) which reads the provider store.
1183
+ * Sessions can be **ephemeral** (default) or **persistent** when opened with `ownerKey` (so their filters/target/query survive popover reopen).
1184
+ * In **local** and **hybrid** modes, use `getVisibleOptions(sessionId)` for the UI list, not `state.optionsList`.
1185
+ * `applyFilterOption` expects the **option UI id** (path-like ids), not the DB value.
1186
+
1187
+ ---
1188
+
1189
+ ## `useData` hook (lightweight data fetch + local/remote search)
1190
+
1191
+ `useData` is a small hook that reuses the lister engine’s request semantics (`buildRequest`, `selector`, and `searchTarget → payload`) without opening the lister UI.
1192
+
1193
+ It’s designed for:
1194
+
1195
+ * Fetching and displaying remote lists in normal pages
1196
+ * Supporting remote/hybrid/local search modes
1197
+ * Optional lightweight selection state by stable item key
1198
+
1199
+ It must be used inside `<ListerProvider />` because it calls the provider’s `apiFetchAny` internally.
1200
+
1201
+ ---
1202
+
1203
+ ### When to use `useData` vs `useLister`
1204
+
1205
+ * Use **`useData`** when you want a simple list in your component (table, dropdown, cards) and don’t need the lister panel UI.
1206
+ * Use **`useLister`** when you need to open the lister UI and/or control sessions.
1207
+
1208
+ ---
1209
+
1210
+ ### Minimal usage
1211
+
1212
+ ```tsx
1213
+ function UsersList() {
1214
+ const users = useData({
1215
+ endpoint: "/api/users",
1216
+ selector: "data", // or (body) => body.data
1217
+ search: { default: "fullName" },
1218
+ searchMode: "remote",
1219
+ });
1220
+
1221
+ return (
1222
+ <div>
1223
+ <input
1224
+ value={users.query}
1225
+ onChange={(e) => users.setQuery(e.target.value)}
1226
+ placeholder="Search…"
1227
+ />
1228
+
1229
+ {users.loading ? (
1230
+ <div>Loading…</div>
1231
+ ) : (
1232
+ <ul>
1233
+ {users.visible.map((u: any) => (
1234
+ <li key={u.id}>{u.fullName}</li>
1235
+ ))}
1236
+ </ul>
1237
+ )}
1238
+ </div>
1239
+ );
1240
+ }
1241
+ ```
1242
+
1243
+ ---
1244
+
1245
+ ### Inputs (`UseDataOptions`)
1246
+
1247
+ #### Request configuration
1248
+
1249
+ * `endpoint` (required): URL to fetch
1250
+ * `method`: `GET | POST` (default `GET`)
1251
+ * `selector`: how to extract the list from the response (path or function)
1252
+ * `buildRequest(ctx)`: advanced request builder
1253
+
1254
+ * receives `{ filters, query, cursor }`
1255
+ * returns `{ params, body, headers }`
1256
+
1257
+ #### Search
1258
+
1259
+ * `search`: minimal search config
1260
+
1261
+ * `default`: the default **subject** key when `searchTarget.mode === "subject"`
1262
+ * `searchMode`: `local | remote | hybrid` (default `remote`)
1263
+ * `debounceMs`: debounce for remote/hybrid query typing
1264
+ * `fetchOnMount`: defaults to `true` unless `initial` is provided
1265
+
1266
+ #### Filters
1267
+
1268
+ * `filters`: base filters object
1269
+ * `autoFetchOnFilterChange`: default `true` (only meaningful for remote/hybrid)
1270
+
1271
+ #### Selection (optional)
1272
+
1273
+ If you provide `selection`, the hook exposes selection helpers (`select`, `toggle`, etc.) and returns selected objects (not just ids).
1274
+
1275
+ * `selection.mode`: `single | multiple`
1276
+ * `selection.key`: how to resolve item id
1277
+
1278
+ * string / keyof: `item[key]`
1279
+ * function: `(item) => id`
1280
+ * default: `item.id ?? item.value`
1281
+ * `selection.prune`:
1282
+
1283
+ * `never` (default): keep ids even if the latest list doesn’t include them
1284
+ * `missing`: remove ids not present in the latest fetched list
1285
+
1286
+ ---
1287
+
1288
+ ### Outputs (`UseDataResult`)
1289
+
1290
+ * `data`: last fetched list
1291
+ * `visible`: list after applying local/hybrid filtering using `searchTarget + query`
1292
+ * `loading`, `error`
1293
+ * `query`, `setQuery(q)`
1294
+ * `searchMode`, `setSearchMode(mode)`
1295
+ * `searchTarget`, `setSearchTarget(target)`
1296
+ * `filters`, `setFilters(next)`, `patchFilters(patch)`, `clearFilters()`
1297
+ * `refresh()`
1298
+ * `fetch(override?)`: imperative fetch; supports `{ query, filters, searchTarget }`
1299
+
1300
+ If selection is enabled:
1301
+
1302
+ * `selectedIds`, `selected`
1303
+ * `select(id|ids)`, `deselect(id|ids)`, `toggle(id)`
1304
+ * `isSelected(id)`, `clearSelection()`, `getSelection()`
1305
+
1306
+ ---
1307
+
1308
+ ### Search semantics
1309
+
1310
+ `useData` follows the lister payload model via `buildSearchPayloadFromTarget(searchTarget)`:
1311
+
1312
+ * `target.mode === "subject"` → `{ subject: "fieldName" }`
1313
+ * `target.mode === "all"` → `{ searchAll: true }`
1314
+ * `target.mode === "only"` → `{ searchOnly: [ids...] }`
1315
+
1316
+ **Remote/hybrid:** query typing triggers a debounced fetch.
1317
+
1318
+ **Local:** the hook fetches a base dataset once (when switching to local), then filters client-side.
1319
+
1320
+ ---
1321
+
1322
+ ### Selection example
1323
+
1324
+ ```tsx
1325
+ function UsersChooser() {
1326
+ const users = useData({
1327
+ endpoint: "/api/users",
1328
+ selector: "data",
1329
+ search: { default: "fullName" },
1330
+ searchMode: "remote",
1331
+ selection: { mode: "multiple", key: "id" },
1332
+ });
1333
+
1334
+ return (
1335
+ <div>
1336
+ <input value={users.query} onChange={(e) => users.setQuery(e.target.value)} />
1337
+
1338
+ <ul>
1339
+ {users.visible.map((u: any) => (
1340
+ <li key={u.id}>
1341
+ <label>
1342
+ <input
1343
+ type="checkbox"
1344
+ checked={users.isSelected(u.id)}
1345
+ onChange={() => users.toggle(u.id)}
1346
+ />
1347
+ {u.fullName}
1348
+ </label>
1349
+ </li>
1350
+ ))}
1351
+ </ul>
1352
+
1353
+ <pre>{JSON.stringify(users.getSelection(), null, 2)}</pre>
1354
+ </div>
1355
+ );
1356
+ }
1357
+ ```
1358
+
1359
+
1360
+ #### JSON Editor Types
1361
+
1362
+ **JsonEditorFieldMap Entry**
1363
+
1364
+ | Prop | Type | Description |
1365
+ | :--- | :--- | :--- |
1366
+ | `variant` | `VariantKey` | Which `@timeax/form-palette` variant to use. |
1367
+ | `props` | `VariantProps` | Props passed to the variant. |
1368
+ | `label` | `string` | Display label for this field. |
1369
+ | `description`| `string` | Help text shown under the field. |
425
1370
 
426
1371
  ---
427
1372