@rokkit/forms 1.0.0-next.125 → 1.0.0-next.127

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.
Files changed (89) hide show
  1. package/README.md +251 -0
  2. package/dist/src/display/index.d.ts +5 -0
  3. package/dist/src/index.d.ts +9 -0
  4. package/dist/src/input/index.d.ts +3 -0
  5. package/dist/src/lib/builder.svelte.d.ts +114 -4
  6. package/dist/src/lib/lookup.svelte.d.ts +87 -0
  7. package/dist/src/lib/renderers.d.ts +23 -0
  8. package/package.json +6 -4
  9. package/src/FieldLayout.svelte +4 -11
  10. package/src/FormRenderer.svelte +202 -61
  11. package/src/InfoField.svelte +26 -0
  12. package/src/Input.svelte +17 -61
  13. package/src/InputField.svelte +15 -11
  14. package/src/ValidationReport.svelte +52 -0
  15. package/src/display/DisplayCardGrid.svelte +68 -0
  16. package/src/display/DisplayList.svelte +31 -0
  17. package/src/display/DisplaySection.svelte +20 -0
  18. package/src/display/DisplayTable.svelte +68 -0
  19. package/src/display/DisplayValue.svelte +44 -0
  20. package/src/display/index.js +5 -0
  21. package/src/index.js +14 -0
  22. package/src/input/ArrayEditor.svelte +108 -0
  23. package/src/input/InputCheckbox.svelte +2 -3
  24. package/src/input/InputColor.svelte +6 -1
  25. package/src/input/InputDate.svelte +6 -1
  26. package/src/input/InputDateTime.svelte +6 -1
  27. package/src/input/InputEmail.svelte +6 -1
  28. package/src/input/InputFile.svelte +6 -2
  29. package/src/input/InputMonth.svelte +6 -1
  30. package/src/input/InputNumber.svelte +6 -1
  31. package/src/input/InputPassword.svelte +6 -1
  32. package/src/input/InputRange.svelte +6 -1
  33. package/src/input/InputSelect.svelte +31 -53
  34. package/src/input/InputSwitch.svelte +4 -15
  35. package/src/input/InputTel.svelte +6 -1
  36. package/src/input/InputText.svelte +6 -1
  37. package/src/input/InputTextArea.svelte +6 -1
  38. package/src/input/InputTime.svelte +6 -1
  39. package/src/input/InputToggle.svelte +28 -0
  40. package/src/input/InputUrl.svelte +6 -1
  41. package/src/input/InputWeek.svelte +6 -1
  42. package/src/input/index.js +3 -1
  43. package/src/lib/Input.svelte +3 -3
  44. package/src/lib/builder.svelte.js +425 -30
  45. package/src/lib/fields.js +2 -2
  46. package/src/lib/layout.js +2 -2
  47. package/src/lib/lookup.svelte.js +334 -0
  48. package/src/lib/renderers.js +83 -0
  49. package/src/lib/schema.js +1 -1
  50. package/src/types.js +0 -9
  51. package/dist/src/forms-old/input/types.d.ts +0 -7
  52. package/dist/src/forms-old/lib/form.d.ts +0 -95
  53. package/dist/src/forms-old/lib/index.d.ts +0 -1
  54. package/dist/src/lib/deprecated/nested.d.ts +0 -48
  55. package/dist/src/lib/deprecated/nested.spec.d.ts +0 -1
  56. package/dist/src/lib/deprecated/validator.d.ts +0 -30
  57. package/dist/src/lib/deprecated/validator.spec.d.ts +0 -1
  58. package/src/DataEditor.svelte +0 -30
  59. package/src/ListEditor.svelte +0 -44
  60. package/src/NestedEditor.svelte +0 -85
  61. package/src/forms-old/CheckBox.svelte +0 -56
  62. package/src/forms-old/DataEditor.svelte +0 -30
  63. package/src/forms-old/FieldLayout.svelte +0 -48
  64. package/src/forms-old/Form.svelte +0 -17
  65. package/src/forms-old/Icon.svelte +0 -76
  66. package/src/forms-old/Item.svelte +0 -25
  67. package/src/forms-old/ListEditor.svelte +0 -44
  68. package/src/forms-old/Tabs.svelte +0 -57
  69. package/src/forms-old/Wrapper.svelte +0 -12
  70. package/src/forms-old/input/Input.svelte +0 -17
  71. package/src/forms-old/input/InputField.svelte +0 -70
  72. package/src/forms-old/input/InputSelect.svelte +0 -23
  73. package/src/forms-old/input/InputSwitch.svelte +0 -19
  74. package/src/forms-old/input/types.js +0 -29
  75. package/src/forms-old/lib/form.js +0 -72
  76. package/src/forms-old/lib/index.js +0 -12
  77. package/src/forms-old/mocks/CustomField.svelte +0 -7
  78. package/src/forms-old/mocks/CustomWrapper.svelte +0 -8
  79. package/src/forms-old/mocks/Register.svelte +0 -25
  80. package/src/inp/Input.svelte +0 -17
  81. package/src/inp/InputField.svelte +0 -69
  82. package/src/inp/InputSelect.svelte +0 -23
  83. package/src/inp/InputSwitch.svelte +0 -19
  84. package/src/lib/deprecated/Form.svelte +0 -17
  85. package/src/lib/deprecated/FormRenderer.svelte +0 -121
  86. package/src/lib/deprecated/nested.js +0 -192
  87. package/src/lib/deprecated/nested.spec.js +0 -512
  88. package/src/lib/deprecated/validator.js +0 -137
  89. package/src/lib/deprecated/validator.spec.js +0 -348
package/README.md ADDED
@@ -0,0 +1,251 @@
1
+ # @rokkit/forms
2
+
3
+ Schema-driven form rendering for Svelte 5. Generate dynamic forms from data, schema, and layout definitions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @rokkit/forms
9
+ ```
10
+
11
+ ## Core Concepts
12
+
13
+ ### FormBuilder
14
+
15
+ Takes `(data, schema, layout)` and produces a reactive `elements[]` array. Each element describes a form field with its type, current value, and merged properties.
16
+
17
+ ```js
18
+ import { FormBuilder } from '@rokkit/forms'
19
+
20
+ const builder = new FormBuilder(
21
+ { name: 'Alice', age: 30 }, // data
22
+ schema, // optional — auto-derived from data
23
+ layout // optional — auto-derived from data
24
+ )
25
+
26
+ // builder.elements → [{ scope, type, value, props }, ...]
27
+ ```
28
+
29
+ If `schema` is `null`, it is auto-derived from the data using `deriveSchemaFromValue()`. If `layout` is `null`, it is auto-derived using `deriveLayoutFromValue()`.
30
+
31
+ ### Schema
32
+
33
+ JSON-Schema-like type definitions. Supports: `string`, `number`, `integer`, `boolean`, `array`, `object`, `date`.
34
+
35
+ ```js
36
+ const schema = {
37
+ type: 'object',
38
+ properties: {
39
+ name: { type: 'string' },
40
+ size: { type: 'string', enum: ['sm', 'md', 'lg'] },
41
+ count: { type: 'integer', min: 0, max: 100 },
42
+ active: { type: 'boolean' }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ### Layout
48
+
49
+ Controls rendering order, grouping, labels, and component-specific props. Uses JSON Pointer scopes (`#/fieldName`).
50
+
51
+ ```js
52
+ const layout = {
53
+ type: 'vertical',
54
+ elements: [
55
+ { scope: '#/name', label: 'Full Name', props: { placeholder: 'Enter name' } },
56
+ { scope: '#/size', label: 'Size', props: { options: ['sm', 'md', 'lg'] } },
57
+ { scope: '#/count', label: 'Count' },
58
+ { type: 'separator' },
59
+ { scope: '#/active', label: 'Active' },
60
+ { scope: '#/total', label: 'Total', readonly: true }
61
+ ]
62
+ }
63
+ ```
64
+
65
+ ### Type Resolution
66
+
67
+ The FormBuilder determines input type from the schema:
68
+
69
+ | Schema type | Condition | Input type |
70
+ |---|---|---|
71
+ | `string` | has `enum` or `options` | `select` |
72
+ | `string` | default | `text` |
73
+ | `boolean` | — | `checkbox` |
74
+ | `number`/`integer` | has `min` and `max` | `range` |
75
+ | `number`/`integer` | default | `number` |
76
+ | any | `readonly: true` | `info` |
77
+ | — | no `scope` | `separator` |
78
+
79
+ ### Layout Props
80
+
81
+ The `props` field in layout elements passes component-specific configuration:
82
+
83
+ ```js
84
+ // Select with string options
85
+ { scope: '#/size', props: { options: ['sm', 'md', 'lg'] } }
86
+
87
+ // Select with object options and field mapping
88
+ { scope: '#/color', props: {
89
+ options: [{ name: 'Red', hex: '#f00' }, { name: 'Blue', hex: '#00f' }],
90
+ fields: { text: 'name', value: 'hex' }
91
+ }}
92
+
93
+ // Readonly info display
94
+ { scope: '#/total', readonly: true }
95
+
96
+ // Separator (no scope)
97
+ { type: 'separator' }
98
+ ```
99
+
100
+ ## Components
101
+
102
+ ### FormRenderer
103
+
104
+ Renders a complete form from data + schema + layout:
105
+
106
+ ```svelte
107
+ <script>
108
+ import { FormRenderer } from '@rokkit/forms'
109
+
110
+ let data = $state({ name: '', size: 'md', active: true })
111
+ </script>
112
+
113
+ <FormRenderer bind:data {schema} {layout} />
114
+ ```
115
+
116
+ **Props:**
117
+ - `data` (bindable) — form data object
118
+ - `schema` — JSON schema (optional, auto-derived)
119
+ - `layout` — layout definition (optional, auto-derived)
120
+ - `onupdate` — callback when data changes
121
+ - `onvalidate` — callback for field validation
122
+ - `child` — snippet for custom field rendering (receives `element`)
123
+
124
+ **Custom field rendering:**
125
+
126
+ ```svelte
127
+ <FormRenderer bind:data {schema} {layout}>
128
+ {#snippet child(element)}
129
+ <MyCustomInput value={element.value} {...element.props} />
130
+ {/snippet}
131
+ </FormRenderer>
132
+ ```
133
+
134
+ Set `override: true` on layout elements to route them to the `child` snippet.
135
+
136
+ ### InputField
137
+
138
+ Wraps an input with label, description, and validation message:
139
+
140
+ ```svelte
141
+ <InputField name="email" type="email" value={email} label="Email" onchange={handleChange} />
142
+ ```
143
+
144
+ ### Input
145
+
146
+ Type dispatcher — renders the appropriate input component based on `type`:
147
+
148
+ - `text`, `email`, `password`, `url`, `tel` — text inputs
149
+ - `number` — numeric input
150
+ - `range` — slider
151
+ - `checkbox` — checkbox
152
+ - `select` — dropdown (uses `@rokkit/ui` Select)
153
+ - `date`, `time`, `datetime-local`, `month`, `week` — date/time inputs
154
+ - `color` — color picker
155
+ - `file` — file upload
156
+ - `textarea` — multiline text
157
+ - `radio` — radio group
158
+
159
+ ### InfoField
160
+
161
+ Read-only display of a label and value:
162
+
163
+ ```svelte
164
+ <InfoField label="Total" value={42} />
165
+ ```
166
+
167
+ ## Lib Utilities
168
+
169
+ ```js
170
+ import { getSchemaWithLayout, findAttributeByPath } from '@rokkit/forms'
171
+ import { deriveSchemaFromValue } from '@rokkit/forms/lib'
172
+ import { deriveLayoutFromValue } from '@rokkit/forms/lib'
173
+ ```
174
+
175
+ - `deriveSchemaFromValue(data)` — infer schema from a data object
176
+ - `deriveLayoutFromValue(data)` — generate default vertical layout from data
177
+ - `getSchemaWithLayout(schema, layout)` — merge schema attributes with layout elements
178
+ - `findAttributeByPath(scope, schema)` — find schema attribute by JSON Pointer path
179
+
180
+ ## Theming
181
+
182
+ Form field styles are provided by `@rokkit/themes`. The components use data-attribute selectors:
183
+
184
+ | Selector | Purpose |
185
+ |---|---|
186
+ | `[data-form-root]` | Form container |
187
+ | `[data-form-field]` | Field wrapper |
188
+ | `[data-form-separator]` | Separator between fields |
189
+ | `[data-field-root]` | Input field root |
190
+ | `[data-field]` | Field inner wrapper |
191
+ | `[data-field-type="..."]` | Type-specific layout (checkbox, info, select) |
192
+ | `[data-field-info]` | Info/readonly value display |
193
+ | `[data-input-root]` | Input element wrapper |
194
+ | `[data-description]` | Help text |
195
+ | `[data-message]` | Validation message |
196
+
197
+ ## Future Enhancements
198
+
199
+ ### Toggle/Switch as Checkbox Alternative
200
+
201
+ Use `@rokkit/ui` Toggle or Switch components as alternatives to the native checkbox for boolean fields. The layout could specify the preferred component:
202
+
203
+ ```js
204
+ { scope: '#/active', label: 'Active', props: { component: 'switch' } }
205
+ ```
206
+
207
+ ### Toggle as Select Alternative
208
+
209
+ For fields with a small number of options, a Toggle (radio-style button group) can replace Select for better UX:
210
+
211
+ ```js
212
+ { scope: '#/size', label: 'Size', props: { component: 'toggle', options: ['sm', 'md', 'lg'] } }
213
+ ```
214
+
215
+ **Constraints:**
216
+ - **Text options**: max 3 options (beyond 3, text labels overflow in compact layouts)
217
+ - **Icon-only options**: max 5 options (icons are more compact)
218
+ - When the option count exceeds these limits, fall back to Select automatically
219
+
220
+ ### MultiSelect for Array Values
221
+
222
+ When the value is an array and `options` is present in the layout, render a MultiSelect component instead of a single-value Select:
223
+
224
+ ```js
225
+ // Schema: tags is an array
226
+ { tags: { type: 'array', items: { type: 'string' } } }
227
+
228
+ // Layout: options provided → MultiSelect
229
+ { scope: '#/tags', label: 'Tags', props: { options: ['bug', 'feature', 'docs'] } }
230
+ ```
231
+
232
+ ### Connected/Dependent Props
233
+
234
+ Support cascading dependencies between fields — changing one field's value updates another field's options. For example, selecting a country updates the available cities:
235
+
236
+ ```js
237
+ const lookups = {
238
+ city: {
239
+ dependsOn: 'country',
240
+ fetch: async (country) => getCitiesForCountry(country)
241
+ }
242
+ }
243
+
244
+ const builder = new FormBuilder(data, schema, layout, lookups)
245
+ await builder.initializeLookups()
246
+ ```
247
+
248
+ The `FormBuilder` already has `lookupManager` infrastructure for this pattern. The remaining work is:
249
+ - Auto-wiring lookup state (options, loading) into element props
250
+ - UI indicators for loading state on dependent fields
251
+ - Debouncing rapid changes to parent fields
@@ -0,0 +1,5 @@
1
+ export { default as DisplayValue } from "./DisplayValue.svelte";
2
+ export { default as DisplaySection } from "./DisplaySection.svelte";
3
+ export { default as DisplayTable } from "./DisplayTable.svelte";
4
+ export { default as DisplayCardGrid } from "./DisplayCardGrid.svelte";
5
+ export { default as DisplayList } from "./DisplayList.svelte";
@@ -1,5 +1,14 @@
1
1
  export { FormBuilder } from "./lib/builder.svelte.js";
2
+ export { deriveSchemaFromValue } from "./lib/schema.js";
3
+ export { deriveLayoutFromValue } from "./lib/layout.js";
2
4
  export { default as FormRenderer } from "./FormRenderer.svelte";
3
5
  export { default as Input } from "./Input.svelte";
4
6
  export { default as InputField } from "./InputField.svelte";
7
+ export { default as InfoField } from "./InfoField.svelte";
8
+ export { default as ValidationReport } from "./ValidationReport.svelte";
9
+ export * from "./display";
5
10
  export * from "./input";
11
+ export { createLookup, createLookupManager, clearLookupCache } from "./lib/lookup.svelte.js";
12
+ export { validateField, validateAll, createMessage, patterns } from "./lib/validation.js";
13
+ export { defaultRenderers, resolveRenderer } from "./lib/renderers.js";
14
+ export { getSchemaWithLayout, findAttributeByPath } from "./lib/fields.js";
@@ -1,3 +1,4 @@
1
+ export { default as ArrayEditor } from "./ArrayEditor.svelte";
1
2
  export { default as InputCheckbox } from "./InputCheckbox.svelte";
2
3
  export { default as InputColor } from "./InputColor.svelte";
3
4
  export { default as InputDate } from "./InputDate.svelte";
@@ -10,9 +11,11 @@ export { default as InputPassword } from "./InputPassword.svelte";
10
11
  export { default as InputRadio } from "./InputRadio.svelte";
11
12
  export { default as InputRange } from "./InputRange.svelte";
12
13
  export { default as InputSelect } from "./InputSelect.svelte";
14
+ export { default as InputSwitch } from "./InputSwitch.svelte";
13
15
  export { default as InputTel } from "./InputTel.svelte";
14
16
  export { default as InputText } from "./InputText.svelte";
15
17
  export { default as InputTextArea } from "./InputTextArea.svelte";
16
18
  export { default as InputTime } from "./InputTime.svelte";
19
+ export { default as InputToggle } from "./InputToggle.svelte";
17
20
  export { default as InputUrl } from "./InputUrl.svelte";
18
21
  export { default as InputWeek } from "./InputWeek.svelte";
@@ -14,6 +14,7 @@
14
14
  * @property {Object} [props.message] - Validation message object
15
15
  * @property {string} [props.message.state] - Message state: 'error', 'warning', 'info', 'success'
16
16
  * @property {string} [props.message.text] - Message text content
17
+ * @property {boolean} [props.dirty] - Whether field value differs from initial
17
18
  */
18
19
  /**
19
20
  * FormBuilder class for dynamically generating forms from data structures
@@ -24,11 +25,15 @@ export class FormBuilder {
24
25
  * @param {Object} [data={}] - Initial data object
25
26
  * @param {Object|null} [schema=null] - Optional schema override
26
27
  * @param {Object|null} [layout=null] - Optional layout override
28
+ * @param {Object<string, import('./lookup.svelte.js').LookupConfig>} [lookups={}] - Lookup configurations
27
29
  */
28
- constructor(data?: Object, schema?: Object | null, layout?: Object | null);
30
+ constructor(data?: Object, schema?: Object | null, layout?: Object | null, lookups?: {
31
+ [x: string]: import("./lookup.svelte.js").LookupConfig;
32
+ });
29
33
  /** @type {FormElement[]} */
30
34
  elements: FormElement[];
31
- combined: any;
35
+ /** Combined schema+layout (scoped elements only) */
36
+ get combined(): any;
32
37
  /**
33
38
  * Set the data
34
39
  * @param {Object} value - New data object
@@ -69,12 +74,60 @@ export class FormBuilder {
69
74
  * @returns {Object} Current validation object
70
75
  */
71
76
  get validation(): Object;
77
+ /**
78
+ * Get the lookup manager
79
+ * @returns {ReturnType<typeof createLookupManager>|null}
80
+ */
81
+ get lookupManager(): ReturnType<typeof createLookupManager> | null;
82
+ /**
83
+ * Configure lookups for the form
84
+ * @param {Object<string, import('./lookup.svelte.js').LookupConfig>} lookups - Lookup configurations
85
+ */
86
+ setLookups(lookups: {
87
+ [x: string]: import("./lookup.svelte.js").LookupConfig;
88
+ }): void;
89
+ /**
90
+ * Get lookup state for a field
91
+ * @param {string} fieldPath - Field path
92
+ * @returns {{ options: any[], loading: boolean, error: string|null, fields: Object, disabled: boolean }|null}
93
+ */
94
+ getLookupState(fieldPath: string): {
95
+ options: any[];
96
+ loading: boolean;
97
+ error: string | null;
98
+ fields: Object;
99
+ disabled: boolean;
100
+ } | null;
101
+ /**
102
+ * Check if a field is disabled due to unmet lookup dependencies
103
+ * @param {string} path - Field path
104
+ * @returns {boolean}
105
+ */
106
+ isFieldDisabled(path: string): boolean;
107
+ /**
108
+ * Manually refresh a field's lookup with the current form data
109
+ * @param {string} path - Field path
110
+ * @returns {Promise<void>}
111
+ */
112
+ refreshLookup(path: string): Promise<void>;
113
+ /**
114
+ * Check if a field has a lookup configured
115
+ * @param {string} fieldPath - Field path
116
+ * @returns {boolean}
117
+ */
118
+ hasLookup(fieldPath: string): boolean;
119
+ /**
120
+ * Initialize all lookups
121
+ * @returns {Promise<void>}
122
+ */
123
+ initializeLookups(): Promise<void>;
72
124
  /**
73
125
  * Update a specific field value
74
126
  * @param {string} path - Field path (e.g., 'count', 'settings/distance')
75
127
  * @param {any} value - New value
128
+ * @param {boolean} [triggerLookups=true] - Whether to trigger dependent lookups
76
129
  */
77
- updateField(path: string, value: any): void;
130
+ updateField(path: string, value: any, triggerLookups?: boolean): void;
78
131
  /**
79
132
  * Get a field value by path
80
133
  * @param {string} path - Field path
@@ -94,7 +147,62 @@ export class FormBuilder {
94
147
  */
95
148
  clearValidation(): void;
96
149
  /**
97
- * Reset form to initial state
150
+ * Validate a single field by path
151
+ * @param {string} fieldPath - Field path (without '#/' prefix)
152
+ * @returns {import('./validation.js').ValidationMessage|null} Validation result
153
+ */
154
+ validateField(fieldPath: string): import("./validation.js").ValidationMessage | null;
155
+ /**
156
+ * Validate all fields, populate validation state
157
+ * @returns {Object} Validation results keyed by field path
158
+ */
159
+ validate(): Object;
160
+ /**
161
+ * Whether all fields pass validation (no error-state messages)
162
+ * @returns {boolean}
163
+ */
164
+ get isValid(): boolean;
165
+ /**
166
+ * Array of current error messages with paths
167
+ * @returns {Array<{path: string, state: string, text: string}>}
168
+ */
169
+ get errors(): Array<{
170
+ path: string;
171
+ state: string;
172
+ text: string;
173
+ }>;
174
+ /**
175
+ * Array of all validation messages with paths, ordered by severity
176
+ * @returns {Array<{path: string, state: string, text: string}>}
177
+ */
178
+ get messages(): Array<{
179
+ path: string;
180
+ state: string;
181
+ text: string;
182
+ }>;
183
+ /**
184
+ * Whether any field has been modified from its initial value
185
+ * @returns {boolean}
186
+ */
187
+ get isDirty(): boolean;
188
+ /**
189
+ * Set of field paths that differ from their initial values
190
+ * @returns {Set<string>}
191
+ */
192
+ get dirtyFields(): Set<string>;
193
+ /**
194
+ * Check if a single field has been modified from its initial value
195
+ * @param {string} fieldPath - Field path (without '#/' prefix)
196
+ * @returns {boolean}
197
+ */
198
+ isFieldDirty(fieldPath: string): boolean;
199
+ /**
200
+ * Update the initial data snapshot to the current data.
201
+ * Call after a successful save to clear dirty state.
202
+ */
203
+ snapshot(): void;
204
+ /**
205
+ * Reset form data to initial snapshot and clear validation
98
206
  */
99
207
  reset(): void;
100
208
  #private;
@@ -136,5 +244,7 @@ export type FormElement = {
136
244
  */
137
245
  text?: string | undefined;
138
246
  } | undefined;
247
+ dirty?: boolean | undefined;
139
248
  };
140
249
  };
250
+ import { createLookupManager } from './lookup.svelte.js';
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Creates a lookup provider for a field
3
+ * @param {LookupConfig} config - Lookup configuration
4
+ * @returns {Object} Lookup provider with reactive state
5
+ */
6
+ export function createLookup(config: LookupConfig): Object;
7
+ /**
8
+ * Creates a lookup manager for a form with multiple lookups
9
+ * @param {Object<string, LookupConfig>} lookupConfigs - Map of field paths to lookup configs
10
+ * @returns {Object} Lookup manager
11
+ */
12
+ export function createLookupManager(lookupConfigs: {
13
+ [x: string]: LookupConfig;
14
+ }): Object;
15
+ /**
16
+ * Clears the entire lookup cache
17
+ */
18
+ export function clearLookupCache(): void;
19
+ export type LookupConfig = {
20
+ /**
21
+ * - URL template with optional placeholders (e.g., '/api/cities?country={country}')
22
+ */
23
+ url?: string | undefined;
24
+ /**
25
+ * - Async function replacing URL template
26
+ */
27
+ fetch?: ((formData: any) => Promise<any[]>) | undefined;
28
+ /**
29
+ * - Pre-loaded array for filter pattern
30
+ */
31
+ source?: any[] | undefined;
32
+ /**
33
+ * - Client-side filter applied to source
34
+ */
35
+ filter?: ((items: any[], formData: any) => any[]) | undefined;
36
+ /**
37
+ * - Custom cache key for fetch hooks (no caching when absent)
38
+ */
39
+ cacheKey?: ((formData: any) => string) | undefined;
40
+ /**
41
+ * - Field paths this lookup depends on
42
+ */
43
+ dependsOn?: string[] | undefined;
44
+ /**
45
+ * - Field mapping for the response data
46
+ */
47
+ fields?: Object | undefined;
48
+ /**
49
+ * - Cache duration in milliseconds (default: 5 minutes)
50
+ */
51
+ cacheTime?: number | undefined;
52
+ /**
53
+ * - Transform response data to options array
54
+ */
55
+ transform?: ((data: any) => any[]) | undefined;
56
+ };
57
+ export type LookupState = {
58
+ /**
59
+ * - Current options
60
+ */
61
+ options: any[];
62
+ /**
63
+ * - Whether lookup is in progress
64
+ */
65
+ loading: boolean;
66
+ /**
67
+ * - Error message if lookup failed
68
+ */
69
+ error: string | null;
70
+ /**
71
+ * - Whether field is disabled (deps not met)
72
+ */
73
+ disabled: boolean;
74
+ };
75
+ /**
76
+ * Cache entry structure
77
+ */
78
+ export type CacheEntry = {
79
+ /**
80
+ * - Cached options
81
+ */
82
+ data: any[];
83
+ /**
84
+ * - When the cache was created
85
+ */
86
+ timestamp: number;
87
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Resolve a renderer component for a form element.
3
+ *
4
+ * Resolution order:
5
+ * 1. Explicit `renderer` name in element props → custom registry
6
+ * 2. Element type → registry lookup
7
+ * 3. Fallback → InputText
8
+ *
9
+ * @param {{ type: string, props?: { renderer?: string } }} element
10
+ * @param {Record<string, import('svelte').Component>} renderers - Merged registry
11
+ * @returns {import('svelte').Component}
12
+ */
13
+ export function resolveRenderer(element: {
14
+ type: string;
15
+ props?: {
16
+ renderer?: string;
17
+ };
18
+ }, renderers: Record<string, import("svelte").Component>): import("svelte").Component;
19
+ /**
20
+ * Default renderer registry mapping type strings to components.
21
+ * @type {Record<string, import('svelte').Component>}
22
+ */
23
+ export const defaultRenderers: Record<string, import("svelte").Component>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/forms",
3
- "version": "1.0.0-next.125",
3
+ "version": "1.0.0-next.127",
4
4
  "module": "src/index.js",
5
5
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
6
  "license": "MIT",
@@ -25,14 +25,16 @@
25
25
  "types": "./dist/index.d.ts",
26
26
  "import": "./src/index.js",
27
27
  "svelte": "./src/index.js"
28
+ },
29
+ "./lib": {
30
+ "import": "./src/lib/index.js"
28
31
  }
29
32
  },
30
33
  "dependencies": {
31
34
  "@rokkit/core": "latest",
35
+ "@rokkit/data": "latest",
32
36
  "@rokkit/states": "latest",
33
37
  "@rokkit/ui": "latest",
34
- "@rokkit/bits-ui": "latest",
35
- "ramda": "^0.31.3",
36
- "valibot": "^1.1.0"
38
+ "ramda": "^0.32.0"
37
39
  }
38
40
  }
@@ -4,16 +4,9 @@
4
4
  import InputField from './input/InputField.svelte'
5
5
  import FieldLayout from './FieldLayout.svelte'
6
6
 
7
- // const dispatch = createEventDispatcher()
8
7
  const registry = getContext('registry')
9
8
 
10
- export let value = {}
11
- export let schema = {}
12
- export let path = []
13
-
14
- function handle() {
15
- dispatch('change', value)
16
- }
9
+ let { value = $bindable({}), schema = {}, path = [], onchange } = $props()
17
10
 
18
11
  let Wrapper = registry.wrappers[schema.wrapper] ?? registry.wrappers.default
19
12
  let wrapperProps = omit(['wrapper', 'elements', 'key'], schema)
@@ -33,15 +26,15 @@
33
26
 
34
27
  {#if nested}
35
28
  {#if item.key}
36
- <FieldLayout {...props} schema={item} bind:value={value[item.key]} on:change={handle} />
29
+ <FieldLayout {...props} schema={item} bind:value={value[item.key]} {onchange} />
37
30
  {:else}
38
- <FieldLayout {...props} schema={item} bind:value on:change={handle} />
31
+ <FieldLayout {...props} schema={item} bind:value {onchange} />
39
32
  {/if}
40
33
  {:else if Component}
41
34
  <Component {...item.props} value={item.key ? value[item.key] : null} />
42
35
  {:else}
43
36
  {@const name = elementPath.join('.')}
44
- <InputField {name} bind:value={value[item.key]} {...item.props} on:change={handle} />
37
+ <InputField {name} bind:value={value[item.key]} {...item.props} {onchange} />
45
38
  {/if}
46
39
  {/each}
47
40
  </Wrapper>