@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 +2280 -0
- package/dist/{variant-BGkGWsSy.d.mts → core-EVtvQ-ma.d.mts} +3522 -3488
- package/dist/{variant-B_L0opEi.d.ts → core-ZuVzbe2m.d.ts} +3522 -3488
- package/dist/extra.d.mts +134 -163
- package/dist/extra.d.ts +134 -163
- package/dist/extra.js +7838 -5940
- package/dist/extra.js.map +1 -1
- package/dist/extra.mjs +7763 -5862
- package/dist/extra.mjs.map +1 -1
- package/dist/index.d.mts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.js +3429 -3655
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3359 -3585
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -2
- package/Readme.md +0 -1380
package/Readme.md
DELETED
|
@@ -1,1380 +0,0 @@
|
|
|
1
|
-
### Form Palette: Build Forms with InputField (Consumer Guide)
|
|
2
|
-
|
|
3
|
-
This guide shows how to build forms with @timeax/form-palette. It focuses on InputField usage, variants, and practical recipes taken from the playground’s App.tsx.
|
|
4
|
-
|
|
5
|
-
If you just want to build forms, start here. Internals and source file locations are intentionally de‑emphasized.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
### Quick start
|
|
10
|
-
|
|
11
|
-
Install (from npm):
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npm install @timeax/form-palette
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Minimal form:
|
|
18
|
-
|
|
19
|
-
```tsx
|
|
20
|
-
import { Form, InputField } from "@timeax/form-palette";
|
|
21
|
-
|
|
22
|
-
export default function Example() {
|
|
23
|
-
function onSubmit(e: any) {
|
|
24
|
-
// e.form gives you programmatic access
|
|
25
|
-
// e.formData is the values snapshot
|
|
26
|
-
console.log("Submitted", e.formData);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<Form wrapped gap={12} onSubmit={onSubmit}>
|
|
31
|
-
<InputField name="email" label="Email" variant="text" required />
|
|
32
|
-
<InputField name="password" label="Password" variant="password" required />
|
|
33
|
-
<button type="submit">Submit</button>
|
|
34
|
-
</Form>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
### InputField basics
|
|
42
|
-
|
|
43
|
-
Use one component for all input types by switching the variant key. InputField wires the value, validation, and a consistent label/description/error layout for you.
|
|
44
|
-
|
|
45
|
-
Common props (apply to most variants):
|
|
46
|
-
- name: unique field key
|
|
47
|
-
- variant: which control to render (see Variant reference below)
|
|
48
|
-
- label, sublabel: title and a small inline hint
|
|
49
|
-
- description, helpText: helper copy under the control
|
|
50
|
-
- errorText: force an error message (visual override)
|
|
51
|
-
- required, disabled, readOnly
|
|
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
|
|
67
|
-
|
|
68
|
-
Example with decorations and validation:
|
|
69
|
-
|
|
70
|
-
```tsx
|
|
71
|
-
<InputField
|
|
72
|
-
name="username"
|
|
73
|
-
variant="text"
|
|
74
|
-
label="Username"
|
|
75
|
-
sublabel="public handle"
|
|
76
|
-
prefix="@"
|
|
77
|
-
validate={(value, report) => {
|
|
78
|
-
if (!value) return report ? "Required" : false;
|
|
79
|
-
if (value.length < 3) return report ? "Min 3 characters" : false;
|
|
80
|
-
return true;
|
|
81
|
-
}}
|
|
82
|
-
onChange={(e) => console.log("username:", e.value)}
|
|
83
|
-
/>
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Programmatic control during submit:
|
|
87
|
-
|
|
88
|
-
```tsx
|
|
89
|
-
function onSubmit(e: any) {
|
|
90
|
-
// Programmatically set a value
|
|
91
|
-
e.form.inputs.getByName("email").setValue("demo@example.com");
|
|
92
|
-
console.log(e.formData);
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
### Variant reference (what value they hold and unique props)
|
|
99
|
-
|
|
100
|
-
Below are the built‑in variants with the props you’ll use most often. Examples mirror the playground App.tsx.
|
|
101
|
-
|
|
102
|
-
Note on options: selection controls accept options as primitives ("US") or objects ({ label, value, ...extra }).
|
|
103
|
-
|
|
104
|
-
1) text
|
|
105
|
-
- Value: string | undefined
|
|
106
|
-
- Props: mask, slotChar, unmask, autoClear (phone-like masking); prefix, suffix, stripPrefix, stripSuffix, inputClassName
|
|
107
|
-
- Example:
|
|
108
|
-
```tsx
|
|
109
|
-
<InputField name="email" label="Email" variant="text" />
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
2) number
|
|
113
|
-
- Value: number | undefined
|
|
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
|
|
120
|
-
- Example:
|
|
121
|
-
```tsx
|
|
122
|
-
<InputField name="age" label="Age" variant="number" min={0} max={120} step={1} showButtons />
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
3) password
|
|
126
|
-
- Value: string | undefined
|
|
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
|
|
148
|
-
- Example:
|
|
149
|
-
```tsx
|
|
150
|
-
<InputField name="appointment" variant="date" kind="datetime" label="Appointment" />
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
5) color
|
|
154
|
-
- Value: string | undefined (hex or css color)
|
|
155
|
-
- Props: showPreview, showPickerToggle, previewSize, previewButtonClassName, previewSwatchClassName, pickerInputClassName, pickerToggleIcon, wrapperClassName
|
|
156
|
-
- Example:
|
|
157
|
-
```tsx
|
|
158
|
-
<InputField name="color" label="Favorite colour" variant="color" showPreview />
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
6) phone
|
|
162
|
-
- Value: string | undefined
|
|
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:
|
|
170
|
-
```tsx
|
|
171
|
-
<InputField
|
|
172
|
-
name="phone"
|
|
173
|
-
label="Phone"
|
|
174
|
-
variant="phone"
|
|
175
|
-
defaultCountry="US"
|
|
176
|
-
valueMode="e164"
|
|
177
|
-
/>
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
7) textarea
|
|
181
|
-
- Value: string | undefined
|
|
182
|
-
- Usual textarea props like placeholder, rows, cols, resize, etc.
|
|
183
|
-
- Example:
|
|
184
|
-
```tsx
|
|
185
|
-
<InputField name="bio" label="Bio" variant="textarea" description="Tell us about you" />
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
8) toggle
|
|
189
|
-
- Value: boolean | undefined
|
|
190
|
-
- Props: size, density, controlPlacement ("left" | "right"), onText, offText, switchClassName, switchThumbClassName
|
|
191
|
-
- Example:
|
|
192
|
-
```tsx
|
|
193
|
-
<InputField name="tos" variant="toggle" label="Accept Terms" onText="Yes" offText="No" required />
|
|
194
|
-
```
|
|
195
|
-
|
|
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
|
-
```
|
|
210
|
-
|
|
211
|
-
10) radio
|
|
212
|
-
- Value: any (selected value)
|
|
213
|
-
- Props:
|
|
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
|
|
221
|
-
- Example:
|
|
222
|
-
```tsx
|
|
223
|
-
<InputField
|
|
224
|
-
name="role"
|
|
225
|
-
label="Role"
|
|
226
|
-
variant="radio"
|
|
227
|
-
options={[
|
|
228
|
-
{ value: "reader", label: "Reader" },
|
|
229
|
-
{ value: "editor", label: "Editor" },
|
|
230
|
-
]}
|
|
231
|
-
/>
|
|
232
|
-
```
|
|
233
|
-
|
|
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
|
|
245
|
-
- Single boolean checkbox:
|
|
246
|
-
```tsx
|
|
247
|
-
<InputField variant="checkbox" label="Remember me" />
|
|
248
|
-
```
|
|
249
|
-
- Group example:
|
|
250
|
-
```tsx
|
|
251
|
-
<InputField
|
|
252
|
-
name="perms"
|
|
253
|
-
variant="checkbox"
|
|
254
|
-
label="Permissions"
|
|
255
|
-
options={[
|
|
256
|
-
{ value: "read", label: "Read content" },
|
|
257
|
-
{ value: "write", label: "Write content" },
|
|
258
|
-
]}
|
|
259
|
-
/>
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
12) select
|
|
263
|
-
- Value: string | number | undefined
|
|
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
|
|
273
|
-
- Example:
|
|
274
|
-
```tsx
|
|
275
|
-
<InputField
|
|
276
|
-
name="country"
|
|
277
|
-
variant="select"
|
|
278
|
-
label="Country"
|
|
279
|
-
options={[{ label: "USA", value: "US" }, { label: "Canada", value: "CA" }]}
|
|
280
|
-
searchable
|
|
281
|
-
clearable
|
|
282
|
-
/>
|
|
283
|
-
```
|
|
284
|
-
|
|
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
|
|
296
|
-
- Example:
|
|
297
|
-
```tsx
|
|
298
|
-
<InputField
|
|
299
|
-
name="tags"
|
|
300
|
-
label="Tags"
|
|
301
|
-
variant="multi-select"
|
|
302
|
-
options={["red", "green", "blue"]}
|
|
303
|
-
/>
|
|
304
|
-
```
|
|
305
|
-
|
|
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
|
|
316
|
-
- Example:
|
|
317
|
-
```tsx
|
|
318
|
-
<InputField name="keywords" variant="chips" label="Keywords" />
|
|
319
|
-
```
|
|
320
|
-
|
|
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
|
|
329
|
-
- optionLabel, optionValue, optionDescription, optionDisabled, optionIcon, optionKey
|
|
330
|
-
- renderOption, renderValue
|
|
331
|
-
- triggerClassName, contentClassName
|
|
332
|
-
- Example:
|
|
333
|
-
```tsx
|
|
334
|
-
<InputField name="regions" variant="treeselect" options={regionOptions} />
|
|
335
|
-
```
|
|
336
|
-
|
|
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
|
-
```
|
|
348
|
-
|
|
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:
|
|
361
|
-
```tsx
|
|
362
|
-
<InputField name="attachments" variant="file" multiple />
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
18) keyvalue
|
|
366
|
-
- Value: Record<string, string> | undefined
|
|
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
|
-
```
|
|
376
|
-
|
|
377
|
-
19) editor
|
|
378
|
-
- Value: string | undefined (HTML or Markdown)
|
|
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
|
|
404
|
-
- Example:
|
|
405
|
-
```tsx
|
|
406
|
-
<InputField
|
|
407
|
-
name="user"
|
|
408
|
-
variant="lister"
|
|
409
|
-
endpoint="/api/users"
|
|
410
|
-
optionLabel="fullName"
|
|
411
|
-
optionValue="id"
|
|
412
|
-
/>
|
|
413
|
-
```
|
|
414
|
-
|
|
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
|
-
```
|
|
455
|
-
|
|
456
|
-
---
|
|
457
|
-
|
|
458
|
-
### Recipes from the playground
|
|
459
|
-
|
|
460
|
-
Masking a phone‑like input:
|
|
461
|
-
```tsx
|
|
462
|
-
<InputField
|
|
463
|
-
name="phone"
|
|
464
|
-
label="Phone"
|
|
465
|
-
variant="text"
|
|
466
|
-
mask="+99 99 999 999? x999"
|
|
467
|
-
slotChar="_"
|
|
468
|
-
autoClear
|
|
469
|
-
leadingControl={<span>Leading control</span>}
|
|
470
|
-
prefix="number: "
|
|
471
|
-
/>
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
Password with strength meter:
|
|
475
|
-
```tsx
|
|
476
|
-
<InputField
|
|
477
|
-
name="password"
|
|
478
|
-
label="Password"
|
|
479
|
-
variant="password"
|
|
480
|
-
placeholder="Enter your password"
|
|
481
|
-
strengthMeter
|
|
482
|
-
meterStyle="rules"
|
|
483
|
-
revealToggle
|
|
484
|
-
/>
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
Selects and multi‑selects:
|
|
488
|
-
```tsx
|
|
489
|
-
<InputField
|
|
490
|
-
name="country"
|
|
491
|
-
variant="select"
|
|
492
|
-
label="Country"
|
|
493
|
-
options={[{ label: "USA", value: "US" }, { label: "Canada", value: "CA" }]}
|
|
494
|
-
placeholder="Select a country"
|
|
495
|
-
searchable
|
|
496
|
-
/>
|
|
497
|
-
|
|
498
|
-
<InputField
|
|
499
|
-
name="languages"
|
|
500
|
-
variant="multi-select"
|
|
501
|
-
label="Languages"
|
|
502
|
-
options={["English", "French", "German"]}
|
|
503
|
-
/>
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
Tree select with icons and descriptions:
|
|
507
|
-
```tsx
|
|
508
|
-
<InputField name="regions" label="Regions" variant="treeselect" options={regionOptions} />
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
Single checkbox:
|
|
512
|
-
```tsx
|
|
513
|
-
<InputField variant="checkbox" label="Remember me" />
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
Number with buttons:
|
|
517
|
-
```tsx
|
|
518
|
-
<InputField name="age" label="Age" variant="number" min={0} max={120} step={1} showButtons />
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
---
|
|
522
|
-
|
|
523
|
-
### Submitting the form
|
|
524
|
-
|
|
525
|
-
Form wraps your fields and provides a submit event that carries both the values and utilities.
|
|
526
|
-
|
|
527
|
-
```tsx
|
|
528
|
-
import { Form, InputField } from "@timeax/form-palette";
|
|
529
|
-
|
|
530
|
-
function Example() {
|
|
531
|
-
function handleSubmit(e: any) {
|
|
532
|
-
// values snapshot
|
|
533
|
-
console.log(e.formData);
|
|
534
|
-
// programmatic API
|
|
535
|
-
e.form.inputs.getByName("email").setValue("this is nice");
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
return (
|
|
539
|
-
<Form wrapped onSubmit={handleSubmit}>
|
|
540
|
-
<InputField name="email" label="Email" variant="text" />
|
|
541
|
-
<button type="submit">Submit</button>
|
|
542
|
-
</Form>
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
---
|
|
548
|
-
|
|
549
|
-
### Tips and best practices
|
|
550
|
-
- Prefer InputField over wiring controls by hand; it gives you consistent labels, descriptions, and error placement.
|
|
551
|
-
- Use options as primitives for quick setups, or objects when you need description/disabled/icon per item.
|
|
552
|
-
- Use validate for quick client checks; you can also set errorText manually.
|
|
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. |
|
|
1370
|
-
|
|
1371
|
-
---
|
|
1372
|
-
|
|
1373
|
-
### FAQ
|
|
1374
|
-
- Can I access values without submitting? Yes, via the programmatic API exposed in events like onChange at the form level, or by reading e.form.values() from handlers inside the form.
|
|
1375
|
-
- Can I bring my own input component? Yes. Use the "custom" variant or build a dedicated preset and still place it inside InputField to reuse layout and validation.
|
|
1376
|
-
- Do I need to register adapters? Not for basic local use. Adapters matter when you integrate with external routers or clients; see the package’s adapters folder if needed.
|
|
1377
|
-
|
|
1378
|
-
---
|
|
1379
|
-
|
|
1380
|
-
This document intentionally centers on how to use the package to build forms. For deeper internals and extension points, browse the source or the developer docs in the repository.
|