@rokkit/forms 1.1.18 → 1.1.19
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/dist/FieldLayout.svelte.d.ts +23 -0
- package/dist/FormRenderer.svelte.d.ts +59 -0
- package/dist/InfoField.svelte.d.ts +10 -0
- package/dist/Input.svelte.d.ts +14 -0
- package/dist/InputField.svelte.d.ts +37 -0
- package/dist/StatusList.svelte.d.ts +19 -0
- package/dist/StepIndicator.svelte.d.ts +12 -0
- package/dist/display/DisplayCardGrid.svelte.d.ts +17 -0
- package/dist/display/DisplayList.svelte.d.ts +14 -0
- package/dist/display/DisplaySection.svelte.d.ts +13 -0
- package/dist/display/DisplayTable.svelte.d.ts +19 -0
- package/dist/display/DisplayValue.svelte.d.ts +11 -0
- package/dist/input/ArrayEditor.svelte.d.ts +15 -0
- package/dist/input/InputCheckbox.svelte.d.ts +15 -0
- package/dist/input/InputColor.svelte.d.ts +8 -0
- package/dist/input/InputDate.svelte.d.ts +8 -0
- package/dist/input/InputDateTime.svelte.d.ts +8 -0
- package/dist/input/InputEmail.svelte.d.ts +8 -0
- package/dist/input/InputFile.svelte.d.ts +8 -0
- package/dist/input/InputMonth.svelte.d.ts +8 -0
- package/dist/input/InputNumber.svelte.d.ts +8 -0
- package/dist/input/InputPassword.svelte.d.ts +8 -0
- package/dist/input/InputRadio.svelte.d.ts +16 -0
- package/dist/input/InputRange.svelte.d.ts +8 -0
- package/dist/input/InputSelect.svelte.d.ts +13 -0
- package/dist/input/InputSwatch.svelte.d.ts +13 -0
- package/dist/input/InputSwitch.svelte.d.ts +7 -0
- package/dist/input/InputTel.svelte.d.ts +8 -0
- package/dist/input/InputText.svelte.d.ts +8 -0
- package/dist/input/InputTextArea.svelte.d.ts +8 -0
- package/dist/input/InputTime.svelte.d.ts +8 -0
- package/dist/input/InputToggle.svelte.d.ts +5 -0
- package/dist/input/InputUrl.svelte.d.ts +8 -0
- package/dist/input/InputWeek.svelte.d.ts +8 -0
- package/dist/lib/Input.svelte.d.ts +37 -0
- package/dist/{src/lib → lib}/builder.svelte.d.ts +8 -8
- package/dist/types.d.ts +14 -0
- package/package.json +14 -3
- package/src/FieldLayout.svelte +35 -7
- package/src/FormRenderer.svelte +87 -19
- package/src/InfoField.svelte +10 -2
- package/src/Input.svelte +14 -2
- package/src/InputField.svelte +41 -14
- package/src/StatusList.svelte +23 -4
- package/src/StepIndicator.svelte +10 -8
- package/src/display/DisplayCardGrid.svelte +24 -7
- package/src/display/DisplayList.svelte +15 -2
- package/src/display/DisplaySection.svelte +14 -2
- package/src/display/DisplayTable.svelte +36 -13
- package/src/display/DisplayValue.svelte +18 -9
- package/src/input/ArrayEditor.svelte +41 -23
- package/src/input/InputCheckbox.svelte +17 -25
- package/src/input/InputColor.svelte +10 -17
- package/src/input/InputDate.svelte +10 -21
- package/src/input/InputDateTime.svelte +10 -21
- package/src/input/InputEmail.svelte +10 -24
- package/src/input/InputFile.svelte +10 -18
- package/src/input/InputMonth.svelte +10 -21
- package/src/input/InputNumber.svelte +10 -22
- package/src/input/InputPassword.svelte +10 -23
- package/src/input/InputRadio.svelte +16 -21
- package/src/input/InputRange.svelte +10 -20
- package/src/input/InputSelect.svelte +20 -20
- package/src/input/InputSwatch.svelte +12 -14
- package/src/input/InputSwitch.svelte +6 -12
- package/src/input/InputTel.svelte +10 -23
- package/src/input/InputText.svelte +10 -23
- package/src/input/InputTextArea.svelte +10 -23
- package/src/input/InputTime.svelte +10 -21
- package/src/input/InputToggle.svelte +4 -14
- package/src/input/InputUrl.svelte +10 -23
- package/src/input/InputWeek.svelte +10 -21
- package/src/lib/Input.svelte +47 -13
- package/src/lib/builder.svelte.js +6 -6
- package/dist/src/types.d.ts +0 -5
- /package/dist/{src/display → display}/index.d.ts +0 -0
- /package/dist/{src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{src/input → input}/index.d.ts +0 -0
- /package/dist/{src/lib → lib}/conditions.d.ts +0 -0
- /package/dist/{src/lib → lib}/conditions.spec.d.ts +0 -0
- /package/dist/{src/lib → lib}/fields.d.ts +0 -0
- /package/dist/{src/lib → lib}/fields.spec.d.ts +0 -0
- /package/dist/{src/lib → lib}/index.d.ts +0 -0
- /package/dist/{src/lib → lib}/layout.d.ts +0 -0
- /package/dist/{src/lib → lib}/lookup.svelte.d.ts +0 -0
- /package/dist/{src/lib → lib}/renderers.d.ts +0 -0
- /package/dist/{src/lib → lib}/schema.d.ts +0 -0
- /package/dist/{src/lib → lib}/validation.d.ts +0 -0
package/src/FieldLayout.svelte
CHANGED
|
@@ -1,14 +1,38 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import { getContext } from 'svelte'
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext, type Component } from 'svelte'
|
|
3
3
|
import { omit } from 'ramda'
|
|
4
|
-
import InputField from './
|
|
4
|
+
import InputField from './InputField.svelte'
|
|
5
5
|
import FieldLayout from './FieldLayout.svelte'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type Registry = {
|
|
8
|
+
wrappers: Record<string, Component<Record<string, unknown>>>
|
|
9
|
+
components: Record<string, Component<Record<string, unknown>>>
|
|
10
|
+
}
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
type LayoutSchema = {
|
|
13
|
+
wrapper?: string
|
|
14
|
+
key?: string
|
|
15
|
+
component?: string
|
|
16
|
+
elements?: LayoutSchema[]
|
|
17
|
+
props?: Record<string, unknown>
|
|
18
|
+
[key: string]: unknown
|
|
19
|
+
}
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
/** Recursive field-value tree: each node is itself an indexable record of nodes. */
|
|
22
|
+
type FieldTree = { [key: string]: FieldTree }
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
value?: FieldTree
|
|
26
|
+
schema?: LayoutSchema
|
|
27
|
+
path?: string[]
|
|
28
|
+
onchange?: (value: unknown) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const registry = getContext<Registry>('registry')
|
|
32
|
+
|
|
33
|
+
let { value = $bindable({}), schema = {}, path = [], onchange }: Props = $props()
|
|
34
|
+
|
|
35
|
+
let Wrapper = registry.wrappers[schema.wrapper ?? ''] ?? registry.wrappers.default
|
|
12
36
|
let wrapperProps = omit(['wrapper', 'elements', 'key'], schema)
|
|
13
37
|
</script>
|
|
14
38
|
|
|
@@ -34,7 +58,11 @@
|
|
|
34
58
|
<Component {...item.props} value={item.key ? value[item.key] : null} />
|
|
35
59
|
{:else}
|
|
36
60
|
{@const name = elementPath.join('.')}
|
|
37
|
-
|
|
61
|
+
{#if item.key}
|
|
62
|
+
<InputField {name} bind:value={value[item.key]} {...item.props} {onchange} />
|
|
63
|
+
{:else}
|
|
64
|
+
<InputField {name} {...item.props} {onchange} />
|
|
65
|
+
{/if}
|
|
38
66
|
{/if}
|
|
39
67
|
{/each}
|
|
40
68
|
</Wrapper>
|
package/src/FormRenderer.svelte
CHANGED
|
@@ -1,20 +1,71 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
/**
|
|
3
3
|
* FormRenderer component with snippet-based rendering
|
|
4
4
|
* Handles defaultInput, info, separator, display components, and custom child snippet selection.
|
|
5
5
|
* Supports form submission with validate-before-submit, loading state, and optional action buttons.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { onMount, untrack } from 'svelte'
|
|
8
|
+
import { onMount, untrack, type Snippet, type Component } from 'svelte'
|
|
9
9
|
import InputField from './InputField.svelte'
|
|
10
10
|
import InfoField from './InfoField.svelte'
|
|
11
11
|
import { FormBuilder } from './lib/builder.svelte.js'
|
|
12
|
+
import type { LookupConfig } from './lib/lookup.svelte.js'
|
|
12
13
|
import { defaultRenderers } from './lib/renderers.js'
|
|
13
14
|
import DisplayTable from './display/DisplayTable.svelte'
|
|
14
15
|
import DisplayCardGrid from './display/DisplayCardGrid.svelte'
|
|
15
16
|
import DisplaySection from './display/DisplaySection.svelte'
|
|
16
17
|
import DisplayList from './display/DisplayList.svelte'
|
|
17
18
|
|
|
19
|
+
/** A single element produced by FormBuilder.elements. */
|
|
20
|
+
type FormElement = {
|
|
21
|
+
scope?: string
|
|
22
|
+
type?: string
|
|
23
|
+
value?: unknown
|
|
24
|
+
override?: boolean
|
|
25
|
+
props?: Record<string, unknown> & {
|
|
26
|
+
elements?: FormElement[]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ValidateMode = 'blur' | 'change' | 'manual'
|
|
31
|
+
|
|
32
|
+
type ExternalValidation = { state?: string; text?: string } | null | undefined
|
|
33
|
+
|
|
34
|
+
type ActionsContext = {
|
|
35
|
+
submitting: boolean
|
|
36
|
+
isValid: boolean
|
|
37
|
+
isDirty: boolean
|
|
38
|
+
submit: (e: Event) => void | Promise<void>
|
|
39
|
+
reset: () => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Props = {
|
|
43
|
+
/** Optional external builder instance */
|
|
44
|
+
builder?: FormBuilder
|
|
45
|
+
/** Direct data prop (alternative to builder) */
|
|
46
|
+
data?: Record<string, unknown>
|
|
47
|
+
schema?: Record<string, unknown> | null
|
|
48
|
+
layout?: Record<string, unknown> | null
|
|
49
|
+
/** Lookup configurations */
|
|
50
|
+
lookups?: Record<string, LookupConfig>
|
|
51
|
+
/** Validation mode */
|
|
52
|
+
validateOn?: ValidateMode
|
|
53
|
+
/** Custom type renderers (merged with defaults) */
|
|
54
|
+
renderers?: Record<string, Component<Record<string, unknown>>>
|
|
55
|
+
onupdate?: (data: Record<string, unknown>) => void
|
|
56
|
+
onvalidate?: (fieldPath: string, value: unknown, event: string) => ExternalValidation
|
|
57
|
+
onselect?: (selected: unknown, item: unknown) => void
|
|
58
|
+
onsubmit?: (
|
|
59
|
+
data: Record<string, unknown>,
|
|
60
|
+
meta: { isValid: boolean; errors: unknown[] }
|
|
61
|
+
) => void | Promise<void>
|
|
62
|
+
/** Custom actions snippet */
|
|
63
|
+
actions?: Snippet<[ActionsContext]>
|
|
64
|
+
className?: string
|
|
65
|
+
/** Custom snippet rendered when an element opts into an override */
|
|
66
|
+
child?: Snippet<[FormElement]>
|
|
67
|
+
} & Record<string, unknown>
|
|
68
|
+
|
|
18
69
|
let {
|
|
19
70
|
// Optional external builder instance
|
|
20
71
|
builder = undefined,
|
|
@@ -50,7 +101,7 @@
|
|
|
50
101
|
|
|
51
102
|
// Pass through any other props
|
|
52
103
|
...props
|
|
53
|
-
} = $props()
|
|
104
|
+
}: Props = $props()
|
|
54
105
|
|
|
55
106
|
// Merged renderer registry
|
|
56
107
|
const allRenderers = $derived({ ...defaultRenderers, ...renderers })
|
|
@@ -68,7 +119,7 @@
|
|
|
68
119
|
// Sync external data prop changes to builder (effect only tracks `data`, not formBuilder.data,
|
|
69
120
|
// to avoid a proxy-vs-raw-value comparison loop)
|
|
70
121
|
$effect(() => {
|
|
71
|
-
formBuilder.data = data
|
|
122
|
+
if (data !== undefined) formBuilder.data = data
|
|
72
123
|
})
|
|
73
124
|
|
|
74
125
|
$effect(() => {
|
|
@@ -80,7 +131,7 @@
|
|
|
80
131
|
})
|
|
81
132
|
|
|
82
133
|
// Apply external validation result for a field path
|
|
83
|
-
function applyExternalValidation(fieldPath, value, event) {
|
|
134
|
+
function applyExternalValidation(fieldPath: string, value: unknown, event: string) {
|
|
84
135
|
if (!onvalidate) return
|
|
85
136
|
const result = onvalidate(fieldPath, value, event)
|
|
86
137
|
if (result && typeof result === 'object' && result.state) {
|
|
@@ -89,8 +140,8 @@
|
|
|
89
140
|
}
|
|
90
141
|
|
|
91
142
|
// Handle field value changes
|
|
92
|
-
function handleFieldChange(element, newValue) {
|
|
93
|
-
const fieldPath = element.scope.replace(/^#\//, '')
|
|
143
|
+
function handleFieldChange(element: FormElement, newValue: unknown) {
|
|
144
|
+
const fieldPath = (element.scope ?? '').replace(/^#\//, '')
|
|
94
145
|
formBuilder.updateField(fieldPath, newValue)
|
|
95
146
|
data = formBuilder.data
|
|
96
147
|
if (onupdate) onupdate(data)
|
|
@@ -99,8 +150,8 @@
|
|
|
99
150
|
}
|
|
100
151
|
|
|
101
152
|
// Handle blur events for validation
|
|
102
|
-
function handleFieldBlur(element) {
|
|
103
|
-
const fieldPath = element.scope.replace(/^#\//, '')
|
|
153
|
+
function handleFieldBlur(element: FormElement) {
|
|
154
|
+
const fieldPath = (element.scope ?? '').replace(/^#\//, '')
|
|
104
155
|
const currentValue = formBuilder.getValue(fieldPath)
|
|
105
156
|
if (validateOn === 'blur') formBuilder.validateField(fieldPath)
|
|
106
157
|
applyExternalValidation(fieldPath, currentValue, 'blur')
|
|
@@ -108,20 +159,20 @@
|
|
|
108
159
|
|
|
109
160
|
// Submission state
|
|
110
161
|
let submitting = $state(false)
|
|
111
|
-
let formRoot = $state(null)
|
|
162
|
+
let formRoot = $state<HTMLElement | null>(null)
|
|
112
163
|
|
|
113
164
|
// Focus the first invalid field in the form
|
|
114
165
|
function focusFirstError() {
|
|
115
166
|
const firstError = formBuilder.errors[0]
|
|
116
167
|
if (!firstError || !formRoot) return
|
|
117
|
-
const field = formRoot.querySelector(
|
|
168
|
+
const field = formRoot.querySelector<HTMLElement>(
|
|
118
169
|
`[data-scope="#/${firstError.path}"] input, [data-scope="#/${firstError.path}"] select, [data-scope="#/${firstError.path}"] textarea`
|
|
119
170
|
)
|
|
120
171
|
field?.focus?.()
|
|
121
172
|
}
|
|
122
173
|
|
|
123
174
|
// Handle form submission: validate → focus first error → call onsubmit → snapshot
|
|
124
|
-
async function handleSubmit(e) {
|
|
175
|
+
async function handleSubmit(e: Event) {
|
|
125
176
|
e.preventDefault()
|
|
126
177
|
if (submitting || !onsubmit) return
|
|
127
178
|
|
|
@@ -149,6 +200,23 @@
|
|
|
149
200
|
formBuilder.reset()
|
|
150
201
|
data = formBuilder.data
|
|
151
202
|
}
|
|
203
|
+
|
|
204
|
+
// Narrow a dynamic display value into an array of records for list/table/card displays
|
|
205
|
+
function toRecordArray(value: unknown): Array<Record<string, unknown>> {
|
|
206
|
+
return Array.isArray(value) ? (value.filter((v) => v !== null && typeof v === 'object') as Array<Record<string, unknown>>) : []
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Narrow a dynamic display value into a record for section displays
|
|
210
|
+
function toRecord(value: unknown): Record<string, unknown> {
|
|
211
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
212
|
+
? (value as Record<string, unknown>)
|
|
213
|
+
: {}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Narrow a dynamic prop value into a display string
|
|
217
|
+
function asText(value: unknown): string | undefined {
|
|
218
|
+
return typeof value === 'string' ? value : undefined
|
|
219
|
+
}
|
|
152
220
|
</script>
|
|
153
221
|
|
|
154
222
|
<!-- Form container: <form> when onsubmit provided, <div> otherwise -->
|
|
@@ -229,7 +297,7 @@
|
|
|
229
297
|
{/if}
|
|
230
298
|
|
|
231
299
|
<!-- Render a single element by type -->
|
|
232
|
-
{#snippet renderElement(element)}
|
|
300
|
+
{#snippet renderElement(element: FormElement)}
|
|
233
301
|
{#if element.type === 'separator'}
|
|
234
302
|
<div data-form-separator></div>
|
|
235
303
|
{:else if element.type === 'group'}
|
|
@@ -242,20 +310,20 @@
|
|
|
242
310
|
{/each}
|
|
243
311
|
</fieldset>
|
|
244
312
|
{:else if element.type === 'display-table'}
|
|
245
|
-
<DisplayTable data={element.value} {...element.props} {onselect} />
|
|
313
|
+
<DisplayTable data={toRecordArray(element.value)} {...element.props} {onselect} />
|
|
246
314
|
{:else if element.type === 'display-cards'}
|
|
247
|
-
<DisplayCardGrid data={element.value} {...element.props} {onselect} />
|
|
315
|
+
<DisplayCardGrid data={toRecordArray(element.value)} {...element.props} {onselect} />
|
|
248
316
|
{:else if element.type === 'display-section'}
|
|
249
|
-
<DisplaySection data={element.value} {...element.props} />
|
|
317
|
+
<DisplaySection data={toRecord(element.value)} {...element.props} />
|
|
250
318
|
{:else if element.type === 'display-list'}
|
|
251
|
-
<DisplayList data={element.value} {...element.props} />
|
|
319
|
+
<DisplayList data={toRecordArray(element.value)} {...element.props} />
|
|
252
320
|
{:else if element.type === 'info'}
|
|
253
321
|
<div data-form-field data-scope={element.scope}>
|
|
254
322
|
<InfoField
|
|
255
323
|
name={element.scope}
|
|
256
324
|
value={element.value}
|
|
257
|
-
label={element.props?.label}
|
|
258
|
-
description={element.props?.description}
|
|
325
|
+
label={asText(element.props?.label)}
|
|
326
|
+
description={asText(element.props?.description)}
|
|
259
327
|
/>
|
|
260
328
|
</div>
|
|
261
329
|
{:else if element.override && child}
|
package/src/InfoField.svelte
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
type Props = {
|
|
3
|
+
class?: string
|
|
4
|
+
name?: string
|
|
5
|
+
value?: unknown
|
|
6
|
+
label?: string
|
|
7
|
+
description?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { class: className, name, value, label, description }: Props = $props()
|
|
3
11
|
</script>
|
|
4
12
|
|
|
5
13
|
<div
|
package/src/Input.svelte
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
import { defaultRenderers, resolveRenderer } from './lib/renderers.js'
|
|
3
|
+
import type { Component } from 'svelte'
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
type?: string
|
|
7
|
+
value?: unknown
|
|
8
|
+
onchange?: ((value: unknown) => void) | null
|
|
9
|
+
oninput?: ((value: unknown) => void) | null
|
|
10
|
+
onfocus?: ((event: FocusEvent) => void) | null
|
|
11
|
+
onblur?: ((event: FocusEvent) => void) | null
|
|
12
|
+
icon?: string | null
|
|
13
|
+
renderers?: Record<string, Component<Record<string, unknown>>>
|
|
14
|
+
} & Record<string, unknown>
|
|
3
15
|
|
|
4
16
|
let {
|
|
5
17
|
type = 'text',
|
|
@@ -11,7 +23,7 @@
|
|
|
11
23
|
icon = null,
|
|
12
24
|
renderers = {},
|
|
13
25
|
...restProps
|
|
14
|
-
} = $props()
|
|
26
|
+
}: Props = $props()
|
|
15
27
|
|
|
16
28
|
const allRenderers = $derived({ ...defaultRenderers, ...renderers })
|
|
17
29
|
const extraProps = $derived(type === 'integer' ? { step: '1' } : {})
|
package/src/InputField.svelte
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
import Input from './Input.svelte'
|
|
3
|
+
import type { Component } from 'svelte'
|
|
4
|
+
|
|
5
|
+
type FieldMessage = { state?: string; text?: string }
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
class?: string
|
|
9
|
+
name?: string
|
|
10
|
+
value?: unknown
|
|
11
|
+
type?: string
|
|
12
|
+
required?: boolean
|
|
13
|
+
status?: string
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
dirty?: boolean
|
|
16
|
+
message?: FieldMessage | string
|
|
17
|
+
nolabel?: boolean
|
|
18
|
+
icon?: string
|
|
19
|
+
label?: string
|
|
20
|
+
description?: string
|
|
21
|
+
onchange?: (value: unknown) => void
|
|
22
|
+
onblur?: () => void
|
|
23
|
+
id?: string
|
|
24
|
+
renderers?: Record<string, Component<Record<string, unknown>>>
|
|
25
|
+
/**
|
|
26
|
+
* Field layout — controls how the label and input are arranged.
|
|
27
|
+
*
|
|
28
|
+
* - `stacked` (default) — label above input. Best for form pages
|
|
29
|
+
* with full-width inputs and long labels.
|
|
30
|
+
* - `inline` — label and input on the same row (label takes a
|
|
31
|
+
* fixed leading column). Best for compact control panels —
|
|
32
|
+
* playground knob rows, settings dialogs, tweak editors.
|
|
33
|
+
*/
|
|
34
|
+
variant?: 'stacked' | 'inline'
|
|
35
|
+
} & Record<string, unknown>
|
|
3
36
|
|
|
4
37
|
let {
|
|
5
38
|
class: className,
|
|
@@ -18,18 +51,9 @@
|
|
|
18
51
|
onchange,
|
|
19
52
|
id,
|
|
20
53
|
renderers,
|
|
21
|
-
/**
|
|
22
|
-
* Field layout — controls how the label and input are arranged.
|
|
23
|
-
*
|
|
24
|
-
* - `stacked` (default) — label above input. Best for form pages
|
|
25
|
-
* with full-width inputs and long labels.
|
|
26
|
-
* - `inline` — label and input on the same row (label takes a
|
|
27
|
-
* fixed leading column). Best for compact control panels —
|
|
28
|
-
* playground knob rows, settings dialogs, tweak editors.
|
|
29
|
-
*/
|
|
30
54
|
variant = 'stacked',
|
|
31
55
|
...restProps
|
|
32
|
-
} = $props()
|
|
56
|
+
}: Props = $props()
|
|
33
57
|
|
|
34
58
|
let rootProps = $derived(id !== null && id !== undefined ? { id } : {})
|
|
35
59
|
let properties = $derived({
|
|
@@ -38,13 +62,16 @@
|
|
|
38
62
|
...restProps,
|
|
39
63
|
name
|
|
40
64
|
})
|
|
65
|
+
|
|
66
|
+
let messageState = $derived(typeof message === 'object' ? message?.state : undefined)
|
|
67
|
+
let messageText = $derived(typeof message === 'object' ? message?.text : message)
|
|
41
68
|
</script>
|
|
42
69
|
|
|
43
70
|
<div
|
|
44
71
|
data-field-root
|
|
45
72
|
{...rootProps}
|
|
46
73
|
class={className}
|
|
47
|
-
data-field-state={status ??
|
|
74
|
+
data-field-state={status ?? messageState}
|
|
48
75
|
data-field-type={type}
|
|
49
76
|
data-field-required={required}
|
|
50
77
|
data-field-disabled={disabled}
|
|
@@ -63,8 +90,8 @@
|
|
|
63
90
|
<div data-description>{description}</div>
|
|
64
91
|
{/if}
|
|
65
92
|
{#if message}
|
|
66
|
-
<div data-message data-message-state={
|
|
67
|
-
{
|
|
93
|
+
<div data-message data-message-state={messageState ?? 'info'}>
|
|
94
|
+
{messageText}
|
|
68
95
|
</div>
|
|
69
96
|
{/if}
|
|
70
97
|
</div>
|
package/src/StatusList.svelte
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
/**
|
|
3
3
|
* StatusList — grouped summary of validation messages
|
|
4
4
|
* Groups items by severity (error, warning, info, success) with count headers.
|
|
5
5
|
* Supports click-to-focus via onclick callback.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const SEVERITY_LABELS = { error: 'error', warning: 'warning', info: 'info', success: 'success' }
|
|
8
|
+
type Severity = 'error' | 'warning' | 'info' | 'success'
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
type StatusItem = {
|
|
11
|
+
state: Severity
|
|
12
|
+
path: string
|
|
13
|
+
text: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
items?: StatusItem[]
|
|
18
|
+
onclick?: (path: string) => void
|
|
19
|
+
class?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SEVERITY_ORDER: Severity[] = ['error', 'warning', 'info', 'success']
|
|
23
|
+
const SEVERITY_LABELS: Record<Severity, string> = {
|
|
24
|
+
error: 'error',
|
|
25
|
+
warning: 'warning',
|
|
26
|
+
info: 'info',
|
|
27
|
+
success: 'success'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let { items = [], onclick = undefined, class: className = '' }: Props = $props()
|
|
12
31
|
|
|
13
32
|
const grouped = $derived(
|
|
14
33
|
SEVERITY_ORDER.map((state) => ({
|
package/src/StepIndicator.svelte
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
/**
|
|
3
3
|
* StepIndicator — standalone presentational component showing multi-step form progress.
|
|
4
4
|
* Place anywhere on the page; wire to FormBuilder.currentStep and FormBuilder.goToStep.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
steps
|
|
9
|
-
current
|
|
10
|
-
onclick
|
|
11
|
-
}
|
|
7
|
+
type Props = {
|
|
8
|
+
steps?: string[]
|
|
9
|
+
current?: number
|
|
10
|
+
onclick?: (index: number) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { steps = [], current = 0, onclick = undefined }: Props = $props()
|
|
12
14
|
|
|
13
|
-
function stepState(index) {
|
|
15
|
+
function stepState(index: number): 'complete' | 'current' | 'upcoming' {
|
|
14
16
|
if (index < current) return 'complete'
|
|
15
17
|
if (index === current) return 'current'
|
|
16
18
|
return 'upcoming'
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
function handleClick(index) {
|
|
21
|
+
function handleClick(index: number) {
|
|
20
22
|
if (index < current) onclick?.(index)
|
|
21
23
|
}
|
|
22
24
|
</script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
/**
|
|
3
3
|
* Renders an array of objects as a responsive card grid.
|
|
4
4
|
* Each card displays fields with formatted values.
|
|
@@ -6,17 +6,34 @@
|
|
|
6
6
|
import DisplayValue from './DisplayValue.svelte'
|
|
7
7
|
import { SvelteSet } from 'svelte/reactivity'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
type DisplayField = {
|
|
10
|
+
key: string
|
|
11
|
+
label?: string
|
|
12
|
+
format?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type DisplayItem = Record<string, unknown>
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
data?: DisplayItem[]
|
|
19
|
+
fields?: DisplayField[]
|
|
20
|
+
select?: 'one' | 'many'
|
|
21
|
+
title?: string
|
|
22
|
+
onselect?: (selected: DisplayItem | DisplayItem[], item: DisplayItem) => void
|
|
23
|
+
class?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { data = [], fields = [], select, title, onselect, class: className = '' }: Props = $props()
|
|
10
27
|
|
|
11
28
|
let selectedIndex = $state(-1)
|
|
12
|
-
let selectedIndices = new SvelteSet()
|
|
29
|
+
let selectedIndices = new SvelteSet<number>()
|
|
13
30
|
|
|
14
|
-
function selectOne(item, index) {
|
|
31
|
+
function selectOne(item: DisplayItem, index: number) {
|
|
15
32
|
selectedIndex = index
|
|
16
33
|
onselect?.(item, item)
|
|
17
34
|
}
|
|
18
35
|
|
|
19
|
-
function selectMany(item, index) {
|
|
36
|
+
function selectMany(item: DisplayItem, index: number) {
|
|
20
37
|
const next = new SvelteSet(selectedIndices)
|
|
21
38
|
if (next.has(index)) {
|
|
22
39
|
next.delete(index)
|
|
@@ -28,13 +45,13 @@
|
|
|
28
45
|
onselect?.(selected, item)
|
|
29
46
|
}
|
|
30
47
|
|
|
31
|
-
function handleCardClick(item, index) {
|
|
48
|
+
function handleCardClick(item: DisplayItem, index: number) {
|
|
32
49
|
if (!select) return
|
|
33
50
|
if (select === 'one') selectOne(item, index)
|
|
34
51
|
else if (select === 'many') selectMany(item, index)
|
|
35
52
|
}
|
|
36
53
|
|
|
37
|
-
function isSelected(index) {
|
|
54
|
+
function isSelected(index: number) {
|
|
38
55
|
if (select === 'one') return selectedIndex === index
|
|
39
56
|
if (select === 'many') return selectedIndices.has(index)
|
|
40
57
|
return false
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
/**
|
|
3
3
|
* Renders an array of objects as a styled list.
|
|
4
4
|
*/
|
|
5
5
|
import DisplayValue from './DisplayValue.svelte'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type DisplayField = {
|
|
8
|
+
key: string
|
|
9
|
+
label?: string
|
|
10
|
+
format?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
data?: Array<Record<string, unknown>>
|
|
15
|
+
fields?: DisplayField[]
|
|
16
|
+
title?: string
|
|
17
|
+
class?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { data = [], fields = [], title, class: className = '' }: Props = $props()
|
|
8
21
|
</script>
|
|
9
22
|
|
|
10
23
|
<div data-display-list class={className}>
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
/**
|
|
3
3
|
* Renders an object as key-value pairs (detail/summary view).
|
|
4
4
|
*/
|
|
5
5
|
import DisplayValue from './DisplayValue.svelte'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type DisplayField = {
|
|
8
|
+
key: string
|
|
9
|
+
label?: string
|
|
10
|
+
format?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
data?: Record<string, unknown>
|
|
15
|
+
fields?: DisplayField[]
|
|
16
|
+
title?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { data = {}, fields = [], title }: Props = $props()
|
|
8
20
|
</script>
|
|
9
21
|
|
|
10
22
|
<div data-display-section>
|
|
@@ -1,14 +1,33 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
/**
|
|
3
3
|
* Wraps @rokkit/ui Table for display-only rendering from layout schema.
|
|
4
4
|
* Maps display columns (key/label/format) to TableColumn format.
|
|
5
5
|
*/
|
|
6
6
|
import { Table } from '@rokkit/ui'
|
|
7
|
+
import type { TableColumn } from '@rokkit/ui'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
type DisplayColumn = {
|
|
10
|
+
key: string
|
|
11
|
+
label?: string
|
|
12
|
+
sortable?: boolean
|
|
13
|
+
width?: string
|
|
14
|
+
align?: 'left' | 'center' | 'right'
|
|
15
|
+
format?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
data?: Array<Record<string, unknown>>
|
|
20
|
+
columns?: DisplayColumn[]
|
|
21
|
+
select?: 'one' | 'many'
|
|
22
|
+
title?: string
|
|
23
|
+
onselect?: (value: unknown, row: Record<string, unknown>) => void
|
|
24
|
+
class?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let { data = [], columns = [], select, title, onselect, class: className = '' }: Props = $props()
|
|
9
28
|
|
|
10
29
|
// Map display columns → Table columns with formatters
|
|
11
|
-
const tableColumns = $derived(
|
|
30
|
+
const tableColumns = $derived<TableColumn[]>(
|
|
12
31
|
columns.map((col) => ({
|
|
13
32
|
name: col.key,
|
|
14
33
|
label: col.label ?? col.key,
|
|
@@ -19,7 +38,7 @@
|
|
|
19
38
|
}))
|
|
20
39
|
)
|
|
21
40
|
|
|
22
|
-
function formatDuration(minutes) {
|
|
41
|
+
function formatDuration(minutes: number): string {
|
|
23
42
|
const h = Math.floor(minutes / 60)
|
|
24
43
|
const m = minutes % 60
|
|
25
44
|
if (h > 0 && m > 0) return `${h}h ${m}m`
|
|
@@ -27,23 +46,27 @@
|
|
|
27
46
|
return `${m}m`
|
|
28
47
|
}
|
|
29
48
|
|
|
30
|
-
|
|
49
|
+
function toDate(v: unknown): Date {
|
|
50
|
+
if (v instanceof Date) return v
|
|
51
|
+
if (typeof v === 'number') return new Date(v)
|
|
52
|
+
return new Date(String(v))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const formatters: Record<string, (v: unknown) => string> = {
|
|
31
56
|
currency: (v) =>
|
|
32
|
-
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(v),
|
|
33
|
-
datetime: (v) =>
|
|
34
|
-
duration: (v) => formatDuration(v),
|
|
35
|
-
number: (v) => new Intl.NumberFormat().format(v),
|
|
57
|
+
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(Number(v)),
|
|
58
|
+
datetime: (v) => toDate(v).toLocaleString(),
|
|
59
|
+
duration: (v) => formatDuration(Number(v)),
|
|
60
|
+
number: (v) => new Intl.NumberFormat().format(Number(v)),
|
|
36
61
|
boolean: (v) => (v ? '✓' : '✗')
|
|
37
62
|
}
|
|
38
63
|
|
|
39
64
|
/**
|
|
40
65
|
* Create a cell formatter function for a given display format.
|
|
41
|
-
* @param {string} format
|
|
42
|
-
* @returns {(value: unknown) => string}
|
|
43
66
|
*/
|
|
44
|
-
function createFormatter(format) {
|
|
67
|
+
function createFormatter(format: string): (value: unknown) => string {
|
|
45
68
|
const fn = formatters[format] ?? String
|
|
46
|
-
return (value) => {
|
|
69
|
+
return (value: unknown) => {
|
|
47
70
|
if (value === null || value === undefined) return '—'
|
|
48
71
|
return fn(value)
|
|
49
72
|
}
|