@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.
Files changed (88) hide show
  1. package/dist/FieldLayout.svelte.d.ts +23 -0
  2. package/dist/FormRenderer.svelte.d.ts +59 -0
  3. package/dist/InfoField.svelte.d.ts +10 -0
  4. package/dist/Input.svelte.d.ts +14 -0
  5. package/dist/InputField.svelte.d.ts +37 -0
  6. package/dist/StatusList.svelte.d.ts +19 -0
  7. package/dist/StepIndicator.svelte.d.ts +12 -0
  8. package/dist/display/DisplayCardGrid.svelte.d.ts +17 -0
  9. package/dist/display/DisplayList.svelte.d.ts +14 -0
  10. package/dist/display/DisplaySection.svelte.d.ts +13 -0
  11. package/dist/display/DisplayTable.svelte.d.ts +19 -0
  12. package/dist/display/DisplayValue.svelte.d.ts +11 -0
  13. package/dist/input/ArrayEditor.svelte.d.ts +15 -0
  14. package/dist/input/InputCheckbox.svelte.d.ts +15 -0
  15. package/dist/input/InputColor.svelte.d.ts +8 -0
  16. package/dist/input/InputDate.svelte.d.ts +8 -0
  17. package/dist/input/InputDateTime.svelte.d.ts +8 -0
  18. package/dist/input/InputEmail.svelte.d.ts +8 -0
  19. package/dist/input/InputFile.svelte.d.ts +8 -0
  20. package/dist/input/InputMonth.svelte.d.ts +8 -0
  21. package/dist/input/InputNumber.svelte.d.ts +8 -0
  22. package/dist/input/InputPassword.svelte.d.ts +8 -0
  23. package/dist/input/InputRadio.svelte.d.ts +16 -0
  24. package/dist/input/InputRange.svelte.d.ts +8 -0
  25. package/dist/input/InputSelect.svelte.d.ts +13 -0
  26. package/dist/input/InputSwatch.svelte.d.ts +13 -0
  27. package/dist/input/InputSwitch.svelte.d.ts +7 -0
  28. package/dist/input/InputTel.svelte.d.ts +8 -0
  29. package/dist/input/InputText.svelte.d.ts +8 -0
  30. package/dist/input/InputTextArea.svelte.d.ts +8 -0
  31. package/dist/input/InputTime.svelte.d.ts +8 -0
  32. package/dist/input/InputToggle.svelte.d.ts +5 -0
  33. package/dist/input/InputUrl.svelte.d.ts +8 -0
  34. package/dist/input/InputWeek.svelte.d.ts +8 -0
  35. package/dist/lib/Input.svelte.d.ts +37 -0
  36. package/dist/{src/lib → lib}/builder.svelte.d.ts +8 -8
  37. package/dist/types.d.ts +14 -0
  38. package/package.json +14 -3
  39. package/src/FieldLayout.svelte +35 -7
  40. package/src/FormRenderer.svelte +87 -19
  41. package/src/InfoField.svelte +10 -2
  42. package/src/Input.svelte +14 -2
  43. package/src/InputField.svelte +41 -14
  44. package/src/StatusList.svelte +23 -4
  45. package/src/StepIndicator.svelte +10 -8
  46. package/src/display/DisplayCardGrid.svelte +24 -7
  47. package/src/display/DisplayList.svelte +15 -2
  48. package/src/display/DisplaySection.svelte +14 -2
  49. package/src/display/DisplayTable.svelte +36 -13
  50. package/src/display/DisplayValue.svelte +18 -9
  51. package/src/input/ArrayEditor.svelte +41 -23
  52. package/src/input/InputCheckbox.svelte +17 -25
  53. package/src/input/InputColor.svelte +10 -17
  54. package/src/input/InputDate.svelte +10 -21
  55. package/src/input/InputDateTime.svelte +10 -21
  56. package/src/input/InputEmail.svelte +10 -24
  57. package/src/input/InputFile.svelte +10 -18
  58. package/src/input/InputMonth.svelte +10 -21
  59. package/src/input/InputNumber.svelte +10 -22
  60. package/src/input/InputPassword.svelte +10 -23
  61. package/src/input/InputRadio.svelte +16 -21
  62. package/src/input/InputRange.svelte +10 -20
  63. package/src/input/InputSelect.svelte +20 -20
  64. package/src/input/InputSwatch.svelte +12 -14
  65. package/src/input/InputSwitch.svelte +6 -12
  66. package/src/input/InputTel.svelte +10 -23
  67. package/src/input/InputText.svelte +10 -23
  68. package/src/input/InputTextArea.svelte +10 -23
  69. package/src/input/InputTime.svelte +10 -21
  70. package/src/input/InputToggle.svelte +4 -14
  71. package/src/input/InputUrl.svelte +10 -23
  72. package/src/input/InputWeek.svelte +10 -21
  73. package/src/lib/Input.svelte +47 -13
  74. package/src/lib/builder.svelte.js +6 -6
  75. package/dist/src/types.d.ts +0 -5
  76. /package/dist/{src/display → display}/index.d.ts +0 -0
  77. /package/dist/{src/index.d.ts → index.d.ts} +0 -0
  78. /package/dist/{src/input → input}/index.d.ts +0 -0
  79. /package/dist/{src/lib → lib}/conditions.d.ts +0 -0
  80. /package/dist/{src/lib → lib}/conditions.spec.d.ts +0 -0
  81. /package/dist/{src/lib → lib}/fields.d.ts +0 -0
  82. /package/dist/{src/lib → lib}/fields.spec.d.ts +0 -0
  83. /package/dist/{src/lib → lib}/index.d.ts +0 -0
  84. /package/dist/{src/lib → lib}/layout.d.ts +0 -0
  85. /package/dist/{src/lib → lib}/lookup.svelte.d.ts +0 -0
  86. /package/dist/{src/lib → lib}/renderers.d.ts +0 -0
  87. /package/dist/{src/lib → lib}/schema.d.ts +0 -0
  88. /package/dist/{src/lib → lib}/validation.d.ts +0 -0
@@ -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 './input/InputField.svelte'
4
+ import InputField from './InputField.svelte'
5
5
  import FieldLayout from './FieldLayout.svelte'
6
6
 
7
- const registry = getContext('registry')
7
+ type Registry = {
8
+ wrappers: Record<string, Component<Record<string, unknown>>>
9
+ components: Record<string, Component<Record<string, unknown>>>
10
+ }
8
11
 
9
- let { value = $bindable({}), schema = {}, path = [], onchange } = $props()
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
- let Wrapper = registry.wrappers[schema.wrapper] ?? registry.wrappers.default
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
- <InputField {name} bind:value={value[item.key]} {...item.props} {onchange} />
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>
@@ -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}
@@ -1,5 +1,13 @@
1
- <script>
2
- let { class: className, name, value, label, description } = $props()
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' } : {})
@@ -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 ?? message?.state}
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={message?.state ?? 'info'}>
67
- {message?.text ?? message}
93
+ <div data-message data-message-state={messageState ?? 'info'}>
94
+ {messageText}
68
95
  </div>
69
96
  {/if}
70
97
  </div>
@@ -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
- const SEVERITY_ORDER = ['error', 'warning', 'info', 'success']
9
- const SEVERITY_LABELS = { error: 'error', warning: 'warning', info: 'info', success: 'success' }
8
+ type Severity = 'error' | 'warning' | 'info' | 'success'
10
9
 
11
- let { items = [], onclick = undefined, class: className = '' } = $props()
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) => ({
@@ -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
- let {
8
- steps = [],
9
- current = 0,
10
- onclick = undefined
11
- } = $props()
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
- let { data = [], fields = [], select, title, onselect, class: className = '' } = $props()
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
- let { data = [], fields = [], title, class: className = '' } = $props()
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
- let { data = {}, fields = [], title } = $props()
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
- let { data = [], columns = [], select, title, onselect, class: className = '' } = $props()
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
- const formatters = {
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) => new Date(v).toLocaleString(),
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
  }