@rokkit/forms 1.0.0-next.124 → 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.
- package/README.md +251 -0
- package/dist/src/display/index.d.ts +5 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/input/index.d.ts +3 -0
- package/dist/src/lib/builder.svelte.d.ts +114 -4
- package/dist/src/lib/lookup.svelte.d.ts +87 -0
- package/dist/src/lib/renderers.d.ts +23 -0
- package/package.json +6 -4
- package/src/FieldLayout.svelte +4 -11
- package/src/FormRenderer.svelte +202 -61
- package/src/InfoField.svelte +26 -0
- package/src/Input.svelte +17 -61
- package/src/InputField.svelte +15 -11
- package/src/ValidationReport.svelte +52 -0
- package/src/display/DisplayCardGrid.svelte +68 -0
- package/src/display/DisplayList.svelte +31 -0
- package/src/display/DisplaySection.svelte +20 -0
- package/src/display/DisplayTable.svelte +68 -0
- package/src/display/DisplayValue.svelte +44 -0
- package/src/display/index.js +5 -0
- package/src/index.js +14 -0
- package/src/input/ArrayEditor.svelte +108 -0
- package/src/input/InputCheckbox.svelte +2 -3
- package/src/input/InputColor.svelte +6 -1
- package/src/input/InputDate.svelte +6 -1
- package/src/input/InputDateTime.svelte +6 -1
- package/src/input/InputEmail.svelte +6 -1
- package/src/input/InputFile.svelte +6 -2
- package/src/input/InputMonth.svelte +6 -1
- package/src/input/InputNumber.svelte +6 -1
- package/src/input/InputPassword.svelte +6 -1
- package/src/input/InputRange.svelte +6 -1
- package/src/input/InputSelect.svelte +31 -53
- package/src/input/InputSwitch.svelte +4 -15
- package/src/input/InputTel.svelte +6 -1
- package/src/input/InputText.svelte +6 -1
- package/src/input/InputTextArea.svelte +6 -1
- package/src/input/InputTime.svelte +6 -1
- package/src/input/InputToggle.svelte +28 -0
- package/src/input/InputUrl.svelte +6 -1
- package/src/input/InputWeek.svelte +6 -1
- package/src/input/index.js +3 -1
- package/src/lib/Input.svelte +3 -3
- package/src/lib/builder.svelte.js +425 -30
- package/src/lib/fields.js +2 -2
- package/src/lib/layout.js +2 -2
- package/src/lib/lookup.svelte.js +334 -0
- package/src/lib/renderers.js +83 -0
- package/src/lib/schema.js +1 -1
- package/src/types.js +0 -9
- package/dist/src/forms-old/input/types.d.ts +0 -7
- package/dist/src/forms-old/lib/form.d.ts +0 -95
- package/dist/src/forms-old/lib/index.d.ts +0 -1
- package/dist/src/lib/deprecated/nested.d.ts +0 -48
- package/dist/src/lib/deprecated/nested.spec.d.ts +0 -1
- package/dist/src/lib/deprecated/validator.d.ts +0 -30
- package/dist/src/lib/deprecated/validator.spec.d.ts +0 -1
- package/src/DataEditor.svelte +0 -30
- package/src/ListEditor.svelte +0 -44
- package/src/NestedEditor.svelte +0 -85
- package/src/forms-old/CheckBox.svelte +0 -56
- package/src/forms-old/DataEditor.svelte +0 -30
- package/src/forms-old/FieldLayout.svelte +0 -48
- package/src/forms-old/Form.svelte +0 -17
- package/src/forms-old/Icon.svelte +0 -76
- package/src/forms-old/Item.svelte +0 -25
- package/src/forms-old/ListEditor.svelte +0 -44
- package/src/forms-old/Tabs.svelte +0 -57
- package/src/forms-old/Wrapper.svelte +0 -12
- package/src/forms-old/input/Input.svelte +0 -17
- package/src/forms-old/input/InputField.svelte +0 -70
- package/src/forms-old/input/InputSelect.svelte +0 -23
- package/src/forms-old/input/InputSwitch.svelte +0 -19
- package/src/forms-old/input/types.js +0 -29
- package/src/forms-old/lib/form.js +0 -72
- package/src/forms-old/lib/index.js +0 -12
- package/src/forms-old/mocks/CustomField.svelte +0 -7
- package/src/forms-old/mocks/CustomWrapper.svelte +0 -8
- package/src/forms-old/mocks/Register.svelte +0 -25
- package/src/inp/Input.svelte +0 -17
- package/src/inp/InputField.svelte +0 -69
- package/src/inp/InputSelect.svelte +0 -23
- package/src/inp/InputSwitch.svelte +0 -19
- package/src/lib/deprecated/Form.svelte +0 -17
- package/src/lib/deprecated/FormRenderer.svelte +0 -121
- package/src/lib/deprecated/nested.js +0 -192
- package/src/lib/deprecated/nested.spec.js +0 -512
- package/src/lib/deprecated/validator.js +0 -137
- package/src/lib/deprecated/validator.spec.js +0 -348
package/src/FormRenderer.svelte
CHANGED
|
@@ -1,23 +1,46 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
/**
|
|
3
3
|
* FormRenderer component with snippet-based rendering
|
|
4
|
-
* Handles defaultInput and custom child snippet selection
|
|
4
|
+
* Handles defaultInput, info, separator, display components, and custom child snippet selection.
|
|
5
|
+
* Supports form submission with validate-before-submit, loading state, and optional action buttons.
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
import { onMount, untrack } from 'svelte'
|
|
7
9
|
import InputField from './InputField.svelte'
|
|
10
|
+
import InfoField from './InfoField.svelte'
|
|
8
11
|
import { FormBuilder } from './lib/builder.svelte.js'
|
|
12
|
+
import { defaultRenderers } from './lib/renderers.js'
|
|
13
|
+
import DisplayTable from './display/DisplayTable.svelte'
|
|
14
|
+
import DisplayCardGrid from './display/DisplayCardGrid.svelte'
|
|
15
|
+
import DisplaySection from './display/DisplaySection.svelte'
|
|
16
|
+
import DisplayList from './display/DisplayList.svelte'
|
|
9
17
|
|
|
10
18
|
let {
|
|
11
|
-
//
|
|
19
|
+
// Optional external builder instance
|
|
20
|
+
builder = undefined,
|
|
12
21
|
|
|
13
22
|
// Direct props (alternative to builder)
|
|
14
23
|
data = $bindable(),
|
|
15
24
|
schema = null,
|
|
16
25
|
layout = null,
|
|
17
26
|
|
|
27
|
+
// Lookup configurations
|
|
28
|
+
lookups = {},
|
|
29
|
+
|
|
30
|
+
// Validation mode: 'blur' | 'change' | 'manual'
|
|
31
|
+
validateOn = 'blur',
|
|
32
|
+
|
|
33
|
+
// Custom type renderers (merged with defaults)
|
|
34
|
+
renderers = {},
|
|
35
|
+
|
|
18
36
|
// Event handlers
|
|
19
37
|
onupdate = undefined,
|
|
20
38
|
onvalidate = undefined,
|
|
39
|
+
onselect = undefined,
|
|
40
|
+
onsubmit = undefined,
|
|
41
|
+
|
|
42
|
+
// Custom actions snippet: receives { submitting, isValid, isDirty, submit, reset }
|
|
43
|
+
actions = undefined,
|
|
21
44
|
|
|
22
45
|
// Styling
|
|
23
46
|
className = '',
|
|
@@ -29,90 +52,208 @@
|
|
|
29
52
|
...props
|
|
30
53
|
} = $props()
|
|
31
54
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
// Merged renderer registry
|
|
56
|
+
const allRenderers = $derived({ ...defaultRenderers, ...renderers })
|
|
57
|
+
|
|
58
|
+
// Stable FormBuilder instance — created once, updated via $effect
|
|
59
|
+
let formBuilder = untrack(() => builder ?? new FormBuilder(data, schema, layout, lookups))
|
|
60
|
+
|
|
61
|
+
// Initialize lookups after mount (async fire-and-forget; $state updates trigger re-derive)
|
|
62
|
+
onMount(() => {
|
|
63
|
+
if (Object.keys(lookups).length > 0) {
|
|
64
|
+
formBuilder.initializeLookups()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Sync prop changes to builder
|
|
69
|
+
$effect(() => {
|
|
70
|
+
if (formBuilder.data !== data) {
|
|
71
|
+
formBuilder.data = data
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
$effect(() => {
|
|
76
|
+
if (schema !== null && schema !== undefined) formBuilder.schema = schema
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
$effect(() => {
|
|
80
|
+
if (layout !== null && layout !== undefined) formBuilder.layout = layout
|
|
81
|
+
})
|
|
36
82
|
|
|
37
83
|
// Handle field value changes
|
|
38
84
|
function handleFieldChange(element, newValue) {
|
|
39
85
|
const fieldPath = element.scope.replace(/^#\//, '')
|
|
40
86
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
updateNestedValue(data, fieldPath, newValue)
|
|
47
|
-
}
|
|
87
|
+
// Update builder (source of truth)
|
|
88
|
+
formBuilder.updateField(fieldPath, newValue)
|
|
89
|
+
|
|
90
|
+
// Sync builder data back to bindable prop
|
|
91
|
+
data = formBuilder.data
|
|
48
92
|
|
|
49
93
|
// Call onupdate callback if provided
|
|
50
94
|
if (onupdate) {
|
|
51
|
-
|
|
52
|
-
|
|
95
|
+
onupdate(data)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Built-in validation on change
|
|
99
|
+
if (validateOn === 'change') {
|
|
100
|
+
formBuilder.validateField(fieldPath)
|
|
53
101
|
}
|
|
54
102
|
|
|
55
|
-
//
|
|
103
|
+
// External validation callback (backward compat)
|
|
56
104
|
if (onvalidate) {
|
|
57
|
-
onvalidate(fieldPath, newValue)
|
|
105
|
+
const result = onvalidate(fieldPath, newValue, 'change')
|
|
106
|
+
if (result && typeof result === 'object' && result.state) {
|
|
107
|
+
formBuilder.setFieldValidation(fieldPath, result)
|
|
108
|
+
}
|
|
58
109
|
}
|
|
59
110
|
}
|
|
60
111
|
|
|
61
|
-
//
|
|
62
|
-
function
|
|
63
|
-
const
|
|
64
|
-
|
|
112
|
+
// Handle blur events for validation
|
|
113
|
+
function handleFieldBlur(element) {
|
|
114
|
+
const fieldPath = element.scope.replace(/^#\//, '')
|
|
115
|
+
const currentValue = formBuilder.getValue(fieldPath)
|
|
65
116
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
current = current[keys[i]]
|
|
117
|
+
// Built-in validation on blur (default mode)
|
|
118
|
+
if (validateOn === 'blur') {
|
|
119
|
+
formBuilder.validateField(fieldPath)
|
|
71
120
|
}
|
|
72
121
|
|
|
73
|
-
|
|
122
|
+
// External validation callback (backward compat)
|
|
123
|
+
if (onvalidate) {
|
|
124
|
+
const result = onvalidate(fieldPath, currentValue, 'blur')
|
|
125
|
+
if (result && typeof result === 'object' && result.state) {
|
|
126
|
+
formBuilder.setFieldValidation(fieldPath, result)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
74
129
|
}
|
|
75
130
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
131
|
+
// Submission state
|
|
132
|
+
let submitting = $state(false)
|
|
133
|
+
let formRoot = $state(null)
|
|
80
134
|
|
|
81
|
-
// Handle
|
|
82
|
-
function
|
|
135
|
+
// Handle form submission: validate → focus first error → call onsubmit → snapshot
|
|
136
|
+
async function handleSubmit(e) {
|
|
137
|
+
e.preventDefault()
|
|
138
|
+
if (submitting || !onsubmit) return
|
|
139
|
+
|
|
140
|
+
// Validate all fields
|
|
141
|
+
formBuilder.validate()
|
|
142
|
+
|
|
143
|
+
// External form-level validation
|
|
83
144
|
if (onvalidate) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
145
|
+
const result = onvalidate('*', data, 'submit')
|
|
146
|
+
if (result && typeof result === 'object' && result.state) {
|
|
147
|
+
formBuilder.setFieldValidation('*', result)
|
|
148
|
+
}
|
|
87
149
|
}
|
|
150
|
+
|
|
151
|
+
// If invalid, focus first error field
|
|
152
|
+
if (!formBuilder.isValid) {
|
|
153
|
+
const firstError = formBuilder.errors[0]
|
|
154
|
+
if (firstError && formRoot) {
|
|
155
|
+
const field = formRoot.querySelector(
|
|
156
|
+
`[data-scope="#/${firstError.path}"] input, [data-scope="#/${firstError.path}"] select, [data-scope="#/${firstError.path}"] textarea`
|
|
157
|
+
)
|
|
158
|
+
field?.focus?.()
|
|
159
|
+
}
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Submit
|
|
164
|
+
submitting = true
|
|
165
|
+
try {
|
|
166
|
+
await onsubmit(data, { isValid: true, errors: [] })
|
|
167
|
+
formBuilder.snapshot()
|
|
168
|
+
} catch {
|
|
169
|
+
// Consumer handles errors in their onsubmit callback
|
|
170
|
+
} finally {
|
|
171
|
+
submitting = false
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Handle form reset
|
|
176
|
+
function handleReset() {
|
|
177
|
+
formBuilder.reset()
|
|
178
|
+
data = formBuilder.data
|
|
88
179
|
}
|
|
89
180
|
</script>
|
|
90
181
|
|
|
91
|
-
<!-- Form container -->
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
182
|
+
<!-- Form container: <form> when onsubmit provided, <div> otherwise -->
|
|
183
|
+
{#if onsubmit}
|
|
184
|
+
<form
|
|
185
|
+
bind:this={formRoot}
|
|
186
|
+
data-form-root
|
|
187
|
+
data-form-submitting={submitting || undefined}
|
|
188
|
+
class={className}
|
|
189
|
+
onsubmit={handleSubmit}
|
|
190
|
+
{...props}
|
|
191
|
+
>
|
|
192
|
+
{#each formBuilder.elements as element, index (index)}
|
|
193
|
+
{@render renderElement(element)}
|
|
194
|
+
{/each}
|
|
195
|
+
{#if actions}
|
|
196
|
+
{@render actions({ submitting, isValid: formBuilder.isValid, isDirty: formBuilder.isDirty, submit: handleSubmit, reset: handleReset })}
|
|
197
|
+
{:else}
|
|
198
|
+
<div data-form-actions>
|
|
199
|
+
<button type="button" data-form-reset disabled={!formBuilder.isDirty || submitting} onclick={handleReset}>Reset</button>
|
|
200
|
+
<button type="submit" data-form-submit disabled={!formBuilder.isDirty || submitting}>Submit</button>
|
|
201
|
+
</div>
|
|
202
|
+
{/if}
|
|
203
|
+
</form>
|
|
204
|
+
{:else}
|
|
205
|
+
<div bind:this={formRoot} data-form-root class={className} {...props}>
|
|
206
|
+
{#each formBuilder.elements as element, index (index)}
|
|
207
|
+
{@render renderElement(element)}
|
|
208
|
+
{/each}
|
|
209
|
+
</div>
|
|
210
|
+
{/if}
|
|
211
|
+
|
|
212
|
+
<!-- Render a single element by type -->
|
|
213
|
+
{#snippet renderElement(element)}
|
|
214
|
+
{#if element.type === 'separator'}
|
|
215
|
+
<div data-form-separator></div>
|
|
216
|
+
{:else if element.type === 'group'}
|
|
217
|
+
<fieldset data-form-group data-scope={element.scope}>
|
|
218
|
+
{#if element.props?.label}
|
|
219
|
+
<legend data-form-group-label>{element.props.label}</legend>
|
|
102
220
|
{/if}
|
|
221
|
+
{#each element.props?.elements ?? [] as child_element, i (child_element.scope ?? i)}
|
|
222
|
+
{@render renderElement(child_element)}
|
|
223
|
+
{/each}
|
|
224
|
+
</fieldset>
|
|
225
|
+
{:else if element.type === 'display-table'}
|
|
226
|
+
<DisplayTable data={element.value} {...element.props} {onselect} />
|
|
227
|
+
{:else if element.type === 'display-cards'}
|
|
228
|
+
<DisplayCardGrid data={element.value} {...element.props} {onselect} />
|
|
229
|
+
{:else if element.type === 'display-section'}
|
|
230
|
+
<DisplaySection data={element.value} {...element.props} />
|
|
231
|
+
{:else if element.type === 'display-list'}
|
|
232
|
+
<DisplayList data={element.value} {...element.props} />
|
|
233
|
+
{:else if element.type === 'info'}
|
|
234
|
+
<div data-form-field data-scope={element.scope}>
|
|
235
|
+
<InfoField
|
|
236
|
+
name={element.scope}
|
|
237
|
+
value={element.value}
|
|
238
|
+
label={element.props?.label}
|
|
239
|
+
description={element.props?.description}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
{:else if element.override && child}
|
|
243
|
+
<div data-form-field data-scope={element.scope}>
|
|
244
|
+
{@render child(element)}
|
|
245
|
+
</div>
|
|
246
|
+
{:else}
|
|
247
|
+
<div data-form-field data-scope={element.scope}>
|
|
248
|
+
<InputField
|
|
249
|
+
name={element.scope}
|
|
250
|
+
type={element.type}
|
|
251
|
+
value={element.value}
|
|
252
|
+
{...element.props}
|
|
253
|
+
renderers={allRenderers}
|
|
254
|
+
onchange={(newValue) => handleFieldChange(element, newValue)}
|
|
255
|
+
onblur={() => handleFieldBlur(element)}
|
|
256
|
+
/>
|
|
103
257
|
</div>
|
|
104
|
-
{/
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<!-- Default input snippet -->
|
|
108
|
-
{#snippet defaultInput(element)}
|
|
109
|
-
<InputField
|
|
110
|
-
name={element.scope}
|
|
111
|
-
type={element.type}
|
|
112
|
-
value={element.value}
|
|
113
|
-
{...element.props}
|
|
114
|
-
onchange={(newValue) => handleFieldChange(element, newValue)}
|
|
115
|
-
onfocus={() => handleFieldFocus(element)}
|
|
116
|
-
onblur={() => handleFieldBlur(element)}
|
|
117
|
-
/>
|
|
258
|
+
{/if}
|
|
118
259
|
{/snippet}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let {
|
|
3
|
+
class: className,
|
|
4
|
+
name,
|
|
5
|
+
value,
|
|
6
|
+
label,
|
|
7
|
+
description
|
|
8
|
+
} = $props()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div
|
|
12
|
+
data-field-root
|
|
13
|
+
class={className}
|
|
14
|
+
data-field-type="info"
|
|
15
|
+
data-field-empty={value === null || value === undefined}
|
|
16
|
+
>
|
|
17
|
+
<div data-field aria-label={description ?? label ?? name}>
|
|
18
|
+
{#if label}
|
|
19
|
+
<label for={name}>{label}</label>
|
|
20
|
+
{/if}
|
|
21
|
+
<span data-field-info>{value === undefined ? '—' : String(value)}</span>
|
|
22
|
+
</div>
|
|
23
|
+
{#if description}
|
|
24
|
+
<div data-description>{description}</div>
|
|
25
|
+
{/if}
|
|
26
|
+
</div>
|
package/src/Input.svelte
CHANGED
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import {
|
|
3
|
-
import InputCheckbox from './input/InputCheckbox.svelte'
|
|
4
|
-
import InputColor from './input/InputColor.svelte'
|
|
5
|
-
import InputDate from './input/InputDate.svelte'
|
|
6
|
-
import InputDateTime from './input/InputDateTime.svelte'
|
|
7
|
-
import InputEmail from './input/InputEmail.svelte'
|
|
8
|
-
import InputFile from './input/InputFile.svelte'
|
|
9
|
-
import InputMonth from './input/InputMonth.svelte'
|
|
10
|
-
import InputNumber from './input/InputNumber.svelte'
|
|
11
|
-
import InputPassword from './input/InputPassword.svelte'
|
|
12
|
-
import InputRange from './input/InputRange.svelte'
|
|
13
|
-
import InputTel from './input/InputTel.svelte'
|
|
14
|
-
import InputText from './input/InputText.svelte'
|
|
15
|
-
import InputTextArea from './input/InputTextArea.svelte'
|
|
16
|
-
import InputTime from './input/InputTime.svelte'
|
|
17
|
-
import InputUrl from './input/InputUrl.svelte'
|
|
18
|
-
import InputWeek from './input/InputWeek.svelte'
|
|
19
|
-
import InputSelect from './input/InputSelect.svelte'
|
|
2
|
+
import { defaultRenderers, resolveRenderer } from './lib/renderers.js'
|
|
20
3
|
|
|
21
4
|
let {
|
|
22
5
|
type = 'text',
|
|
@@ -26,50 +9,23 @@
|
|
|
26
9
|
onfocus = null,
|
|
27
10
|
onblur = null,
|
|
28
11
|
icon = null,
|
|
12
|
+
renderers = {},
|
|
29
13
|
...restProps
|
|
30
14
|
} = $props()
|
|
15
|
+
|
|
16
|
+
const allRenderers = $derived({ ...defaultRenderers, ...renderers })
|
|
17
|
+
const extraProps = $derived(type === 'integer' ? { step: '1' } : {})
|
|
31
18
|
</script>
|
|
32
19
|
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{:else if type === 'color'}
|
|
46
|
-
<InputColor bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
47
|
-
{:else if type === 'date'}
|
|
48
|
-
<InputDate bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
49
|
-
{:else if type === 'email'}
|
|
50
|
-
<InputEmail bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
51
|
-
{:else if type === 'tel'}
|
|
52
|
-
<InputTel bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
53
|
-
{:else if type === 'time'}
|
|
54
|
-
<InputTime bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
55
|
-
{:else if ['datetime', 'datetime-local'].includes(type)}
|
|
56
|
-
<InputDateTime bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
57
|
-
{:else if type === 'month'}
|
|
58
|
-
<InputMonth bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
59
|
-
{:else if type === 'range'}
|
|
60
|
-
<InputRange bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
61
|
-
{:else if type === 'select'}
|
|
62
|
-
<InputSelect bind:value {onchange} {onfocus} {onblur} {...restProps} />
|
|
63
|
-
{:else if type === 'textarea'}
|
|
64
|
-
<InputTextArea bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
65
|
-
{:else if type === 'url'}
|
|
66
|
-
<InputUrl bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
67
|
-
{:else if type === 'week'}
|
|
68
|
-
<InputWeek bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
69
|
-
<!-- {:else if type === 'switch'}
|
|
70
|
-
<InputWeek bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} /> -->
|
|
71
|
-
{:else}
|
|
72
|
-
<!-- Default to text input -->
|
|
73
|
-
<InputText bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
74
|
-
{/if}
|
|
75
|
-
</div>
|
|
20
|
+
{#if type === 'checkbox'}
|
|
21
|
+
{@const Component = resolveRenderer({ type: 'checkbox' }, allRenderers)}
|
|
22
|
+
<Component bind:value {onchange} {oninput} {onfocus} {onblur} {...restProps} />
|
|
23
|
+
{:else}
|
|
24
|
+
{@const Component = resolveRenderer({ type, props: restProps }, allRenderers)}
|
|
25
|
+
<div data-input-root>
|
|
26
|
+
{#if icon}
|
|
27
|
+
<span class={icon} aria-hidden="true"></span>
|
|
28
|
+
{/if}
|
|
29
|
+
<Component bind:value {onchange} {oninput} {onfocus} {onblur} {...extraProps} {...restProps} />
|
|
30
|
+
</div>
|
|
31
|
+
{/if}
|
package/src/InputField.svelte
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { pick, omit, isNil } from 'ramda'
|
|
3
|
-
import { Icon } from '@rokkit/ui'
|
|
4
2
|
import Input from './Input.svelte'
|
|
5
3
|
|
|
6
4
|
let {
|
|
@@ -11,45 +9,51 @@
|
|
|
11
9
|
required,
|
|
12
10
|
status,
|
|
13
11
|
disabled,
|
|
12
|
+
dirty,
|
|
14
13
|
message,
|
|
15
14
|
nolabel,
|
|
16
15
|
icon,
|
|
17
16
|
label,
|
|
18
17
|
description,
|
|
19
18
|
onchange,
|
|
19
|
+
id,
|
|
20
|
+
renderers,
|
|
20
21
|
...restProps
|
|
21
22
|
} = $props()
|
|
22
23
|
|
|
23
|
-
let rootProps =
|
|
24
|
-
let properties = {
|
|
24
|
+
let rootProps = $derived(id !== null && id !== undefined ? { id } : {})
|
|
25
|
+
let properties = $derived({
|
|
25
26
|
required,
|
|
26
27
|
readOnly: disabled,
|
|
27
|
-
...
|
|
28
|
+
...restProps,
|
|
28
29
|
name
|
|
29
|
-
}
|
|
30
|
+
})
|
|
30
31
|
</script>
|
|
31
32
|
|
|
32
33
|
<div
|
|
33
34
|
data-field-root
|
|
34
35
|
{...rootProps}
|
|
35
36
|
class={className}
|
|
36
|
-
data-field-state={status}
|
|
37
|
+
data-field-state={status ?? message?.state}
|
|
37
38
|
data-field-type={type}
|
|
38
39
|
data-field-required={required}
|
|
39
40
|
data-field-disabled={disabled}
|
|
40
|
-
data-field-
|
|
41
|
-
data-
|
|
41
|
+
data-field-dirty={dirty || undefined}
|
|
42
|
+
data-field-empty={value === null || value === undefined}
|
|
43
|
+
data-has-icon={icon !== null && icon !== undefined}
|
|
42
44
|
>
|
|
43
45
|
<div data-field aria-label={description ?? label ?? name}>
|
|
44
46
|
{#if label && !nolabel}
|
|
45
47
|
<label for={name}>{label}</label>
|
|
46
48
|
{/if}
|
|
47
|
-
<Input id={name} bind:value {type} {...properties} {onchange} {icon} />
|
|
49
|
+
<Input id={name} bind:value {type} {...properties} {onchange} {icon} {renderers} />
|
|
48
50
|
</div>
|
|
49
51
|
{#if description}
|
|
50
52
|
<div data-description>{description}</div>
|
|
51
53
|
{/if}
|
|
52
54
|
{#if message}
|
|
53
|
-
<div data-message
|
|
55
|
+
<div data-message data-message-state={message?.state ?? 'info'}>
|
|
56
|
+
{message?.text ?? message}
|
|
57
|
+
</div>
|
|
54
58
|
{/if}
|
|
55
59
|
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* ValidationReport — grouped summary of validation messages
|
|
4
|
+
* Groups items by severity (error, warning, info, success) with count headers.
|
|
5
|
+
* Supports click-to-focus via onclick callback.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const SEVERITY_ORDER = ['error', 'warning', 'info', 'success']
|
|
9
|
+
const SEVERITY_LABELS = { error: 'error', warning: 'warning', info: 'info', success: 'success' }
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
items = [],
|
|
13
|
+
onclick = undefined,
|
|
14
|
+
class: className = ''
|
|
15
|
+
} = $props()
|
|
16
|
+
|
|
17
|
+
const grouped = $derived(
|
|
18
|
+
SEVERITY_ORDER.map((state) => ({
|
|
19
|
+
state,
|
|
20
|
+
items: items.filter((item) => item.state === state)
|
|
21
|
+
})).filter((group) => group.items.length > 0)
|
|
22
|
+
)
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
{#if items.length > 0}
|
|
26
|
+
<div data-validation-report class={className} role="status">
|
|
27
|
+
{#each grouped as group (group.state)}
|
|
28
|
+
<div data-validation-group data-severity={group.state}>
|
|
29
|
+
<div data-validation-group-header>
|
|
30
|
+
<span data-validation-count>{group.items.length}</span>
|
|
31
|
+
<span>{group.items.length === 1 ? SEVERITY_LABELS[group.state] : `${SEVERITY_LABELS[group.state]}s`}</span>
|
|
32
|
+
</div>
|
|
33
|
+
{#each group.items as item (item.path)}
|
|
34
|
+
{#if onclick}
|
|
35
|
+
<button
|
|
36
|
+
data-validation-item
|
|
37
|
+
data-status={item.state}
|
|
38
|
+
onclick={() => onclick(item.path)}
|
|
39
|
+
type="button"
|
|
40
|
+
>
|
|
41
|
+
{item.text}
|
|
42
|
+
</button>
|
|
43
|
+
{:else}
|
|
44
|
+
<div data-validation-item data-status={item.state}>
|
|
45
|
+
{item.text}
|
|
46
|
+
</div>
|
|
47
|
+
{/if}
|
|
48
|
+
{/each}
|
|
49
|
+
</div>
|
|
50
|
+
{/each}
|
|
51
|
+
</div>
|
|
52
|
+
{/if}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* Renders an array of objects as a responsive card grid.
|
|
4
|
+
* Each card displays fields with formatted values.
|
|
5
|
+
*/
|
|
6
|
+
import DisplayValue from './DisplayValue.svelte'
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
data = [],
|
|
10
|
+
fields = [],
|
|
11
|
+
select,
|
|
12
|
+
title,
|
|
13
|
+
onselect,
|
|
14
|
+
class: className = ''
|
|
15
|
+
} = $props()
|
|
16
|
+
|
|
17
|
+
let selectedIndex = $state(-1)
|
|
18
|
+
let selectedIndices = $state(new Set())
|
|
19
|
+
|
|
20
|
+
function handleCardClick(item, index) {
|
|
21
|
+
if (!select) return
|
|
22
|
+
|
|
23
|
+
if (select === 'one') {
|
|
24
|
+
selectedIndex = index
|
|
25
|
+
onselect?.(item, item)
|
|
26
|
+
} else if (select === 'many') {
|
|
27
|
+
const next = new Set(selectedIndices)
|
|
28
|
+
if (next.has(index)) {
|
|
29
|
+
next.delete(index)
|
|
30
|
+
} else {
|
|
31
|
+
next.add(index)
|
|
32
|
+
}
|
|
33
|
+
selectedIndices = next
|
|
34
|
+
const selected = data.filter((_, i) => next.has(i))
|
|
35
|
+
onselect?.(selected, item)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isSelected(index) {
|
|
40
|
+
if (select === 'one') return selectedIndex === index
|
|
41
|
+
if (select === 'many') return selectedIndices.has(index)
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<div data-display-cards data-selectable={select || undefined} class={className}>
|
|
47
|
+
{#if title}
|
|
48
|
+
<div data-display-title>{title}</div>
|
|
49
|
+
{/if}
|
|
50
|
+
<div data-display-grid>
|
|
51
|
+
{#each data as item, index (index)}
|
|
52
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
53
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
54
|
+
<div
|
|
55
|
+
data-display-card
|
|
56
|
+
data-selected={isSelected(index) || undefined}
|
|
57
|
+
onclick={select ? () => handleCardClick(item, index) : undefined}
|
|
58
|
+
>
|
|
59
|
+
{#each fields as field (field.key)}
|
|
60
|
+
<div data-display-field>
|
|
61
|
+
<span data-display-label>{field.label ?? field.key}</span>
|
|
62
|
+
<DisplayValue value={item[field.key]} format={field.format} />
|
|
63
|
+
</div>
|
|
64
|
+
{/each}
|
|
65
|
+
</div>
|
|
66
|
+
{/each}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* Renders an array of objects as a styled list.
|
|
4
|
+
*/
|
|
5
|
+
import DisplayValue from './DisplayValue.svelte'
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
data = [],
|
|
9
|
+
fields = [],
|
|
10
|
+
title,
|
|
11
|
+
class: className = ''
|
|
12
|
+
} = $props()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div data-display-list class={className}>
|
|
16
|
+
{#if title}
|
|
17
|
+
<div data-display-title>{title}</div>
|
|
18
|
+
{/if}
|
|
19
|
+
<ul>
|
|
20
|
+
{#each data as item, index (index)}
|
|
21
|
+
<li data-display-list-item>
|
|
22
|
+
{#each fields as field (field.key)}
|
|
23
|
+
<span data-display-field>
|
|
24
|
+
<span data-display-label>{field.label ?? field.key}</span>
|
|
25
|
+
<DisplayValue value={item[field.key]} format={field.format} />
|
|
26
|
+
</span>
|
|
27
|
+
{/each}
|
|
28
|
+
</li>
|
|
29
|
+
{/each}
|
|
30
|
+
</ul>
|
|
31
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* Renders an object as key-value pairs (detail/summary view).
|
|
4
|
+
*/
|
|
5
|
+
import DisplayValue from './DisplayValue.svelte'
|
|
6
|
+
|
|
7
|
+
let { data = {}, fields = [], title } = $props()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div data-display-section>
|
|
11
|
+
{#if title}
|
|
12
|
+
<div data-display-title>{title}</div>
|
|
13
|
+
{/if}
|
|
14
|
+
{#each fields as field (field.key)}
|
|
15
|
+
<div data-display-field>
|
|
16
|
+
<span data-display-label>{field.label ?? field.key}</span>
|
|
17
|
+
<DisplayValue value={data[field.key]} format={field.format} />
|
|
18
|
+
</div>
|
|
19
|
+
{/each}
|
|
20
|
+
</div>
|