@svgrid/grid 2.1.21 → 2.1.23

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 (219) hide show
  1. package/README.md +9 -2
  2. package/dist/DockNodeView.svelte +306 -0
  3. package/dist/DockNodeView.svelte.d.ts +7 -0
  4. package/dist/DockPopoutHost.svelte +17 -0
  5. package/dist/DockPopoutHost.svelte.d.ts +14 -0
  6. package/dist/GridMenus.svelte +83 -3
  7. package/dist/SvAutoComplete.svelte +5 -3
  8. package/dist/SvCarousel.svelte +20 -42
  9. package/dist/SvColorInput.svelte +4 -3
  10. package/dist/SvColorInput.svelte.d.ts +1 -1
  11. package/dist/SvComboBox.svelte +7 -3
  12. package/dist/SvCommand.svelte +28 -85
  13. package/dist/SvCountryInput.svelte +1 -1
  14. package/dist/SvDateRangeInput.svelte +1 -1
  15. package/dist/SvDateTimePicker.svelte +9 -3
  16. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  17. package/dist/SvDockLayout.svelte +244 -0
  18. package/dist/SvDockLayout.svelte.d.ts +77 -0
  19. package/dist/SvDockManager.svelte +922 -0
  20. package/dist/SvDockManager.svelte.d.ts +106 -0
  21. package/dist/SvDrawer.svelte +69 -47
  22. package/dist/SvDrawer.svelte.d.ts +4 -0
  23. package/dist/SvDropDownList.svelte +85 -13
  24. package/dist/SvDropDownList.svelte.d.ts +3 -0
  25. package/dist/SvField.svelte +21 -4
  26. package/dist/SvField.svelte.d.ts +1 -0
  27. package/dist/SvForm.svelte +201 -91
  28. package/dist/SvForm.svelte.d.ts +15 -22
  29. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  30. package/dist/SvGrid.controller.svelte.js +156 -27
  31. package/dist/SvGrid.css +193 -5
  32. package/dist/SvGrid.svelte +357 -12
  33. package/dist/SvGrid.types.d.ts +452 -1
  34. package/dist/SvGridDropdown.svelte +8 -1
  35. package/dist/SvGridSelect.svelte +1 -0
  36. package/dist/SvListBox.svelte +36 -14
  37. package/dist/SvMaskedInput.svelte +4 -3
  38. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  39. package/dist/SvMenu.svelte +1 -1
  40. package/dist/SvMenuList.svelte +23 -71
  41. package/dist/SvMenuList.svelte.d.ts +2 -13
  42. package/dist/SvModal.svelte +12 -29
  43. package/dist/SvMultiSelect.svelte +1 -0
  44. package/dist/SvNumberInput.svelte +4 -3
  45. package/dist/SvNumberInput.svelte.d.ts +1 -1
  46. package/dist/SvPagination.svelte +18 -18
  47. package/dist/SvPasswordInput.svelte +3 -2
  48. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  49. package/dist/SvPhoneInput.svelte +3 -2
  50. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  51. package/dist/SvPopover.svelte +1 -1
  52. package/dist/SvStepper.svelte +6 -9
  53. package/dist/SvTagsInput.svelte +3 -2
  54. package/dist/SvTagsInput.svelte.d.ts +1 -1
  55. package/dist/SvTooltip.svelte +12 -23
  56. package/dist/SvTour.svelte +32 -4
  57. package/dist/SvTreeSelect.svelte +1 -0
  58. package/dist/build-api.js +76 -54
  59. package/dist/cdn/svgrid.js +10907 -6444
  60. package/dist/cdn/svgrid.svelte-external.js +9622 -5475
  61. package/dist/columns.js +6 -0
  62. package/dist/conditional-formatting.d.ts +67 -3
  63. package/dist/conditional-formatting.js +124 -17
  64. package/dist/createCarousel.svelte.d.ts +72 -0
  65. package/dist/createCarousel.svelte.js +80 -0
  66. package/dist/createCommand.svelte.d.ts +62 -0
  67. package/dist/createCommand.svelte.js +109 -0
  68. package/dist/createForm.svelte.d.ts +62 -0
  69. package/dist/createForm.svelte.js +374 -0
  70. package/dist/createMenu.svelte.d.ts +88 -0
  71. package/dist/createMenu.svelte.js +153 -0
  72. package/dist/createOverlay.svelte.d.ts +38 -0
  73. package/dist/createOverlay.svelte.js +68 -0
  74. package/dist/createPagination.d.ts +74 -0
  75. package/dist/createPagination.js +71 -0
  76. package/dist/createPopoverSelect.svelte.js +6 -12
  77. package/dist/createStepper.d.ts +44 -0
  78. package/dist/createStepper.js +26 -0
  79. package/dist/createTooltip.svelte.d.ts +32 -0
  80. package/dist/createTooltip.svelte.js +61 -0
  81. package/dist/datetime/timezone.d.ts +36 -0
  82. package/dist/datetime/timezone.js +110 -0
  83. package/dist/dock-context.d.ts +52 -0
  84. package/dist/dock-context.js +1 -0
  85. package/dist/dock-manager-model.d.ts +136 -0
  86. package/dist/dock-manager-model.js +514 -0
  87. package/dist/dock-model.d.ts +105 -0
  88. package/dist/dock-model.js +288 -0
  89. package/dist/dock-popout.d.ts +30 -0
  90. package/dist/dock-popout.js +81 -0
  91. package/dist/editor-contract.d.ts +2 -0
  92. package/dist/filter-operators.d.ts +6 -0
  93. package/dist/filter-operators.js +35 -2
  94. package/dist/filtering/excel-filters.d.ts +18 -1
  95. package/dist/filtering/excel-filters.js +56 -0
  96. package/dist/form-field.d.ts +73 -0
  97. package/dist/form-field.js +15 -0
  98. package/dist/index.d.ts +25 -5
  99. package/dist/index.js +40 -4
  100. package/dist/list-nav.d.ts +18 -0
  101. package/dist/list-nav.js +30 -0
  102. package/dist/list-option.d.ts +2 -0
  103. package/dist/menu-item.d.ts +20 -0
  104. package/dist/menu-item.js +1 -0
  105. package/dist/menus.d.ts +5 -0
  106. package/dist/menus.js +68 -16
  107. package/dist/panel-resize.d.ts +25 -0
  108. package/dist/panel-resize.js +40 -0
  109. package/dist/popover.d.ts +29 -3
  110. package/dist/popover.js +22 -6
  111. package/dist/recurrence.d.ts +33 -7
  112. package/dist/recurrence.js +151 -13
  113. package/dist/row-drag.d.ts +8 -0
  114. package/dist/row-drag.js +11 -0
  115. package/dist/scheduler-ical.d.ts +30 -0
  116. package/dist/scheduler-ical.js +180 -0
  117. package/dist/scheduler-model.d.ts +269 -0
  118. package/dist/scheduler-model.js +602 -0
  119. package/dist/scheduler-view.svelte.d.ts +22 -0
  120. package/dist/scheduler-view.svelte.js +13 -0
  121. package/dist/spreadsheet.js +20 -12
  122. package/dist/ui-app.types.d.ts +3 -0
  123. package/package.json +5 -1
  124. package/src/DockNodeView.svelte +306 -0
  125. package/src/DockPopoutHost.svelte +17 -0
  126. package/src/GridMenus.svelte +83 -3
  127. package/src/SvAutoComplete.svelte +5 -3
  128. package/src/SvCarousel.svelte +20 -42
  129. package/src/SvCarousel.test.ts +68 -0
  130. package/src/SvColorInput.svelte +4 -3
  131. package/src/SvComboBox.svelte +7 -3
  132. package/src/SvCommand.svelte +28 -85
  133. package/src/SvCommand.test.ts +78 -0
  134. package/src/SvCountryInput.svelte +1 -1
  135. package/src/SvDateRangeInput.svelte +1 -1
  136. package/src/SvDateTimePicker.svelte +9 -3
  137. package/src/SvDockLayout.svelte +244 -0
  138. package/src/SvDockManager.svelte +922 -0
  139. package/src/SvDrawer.svelte +69 -47
  140. package/src/SvDropDownList.svelte +85 -13
  141. package/src/SvField.svelte +21 -4
  142. package/src/SvForm.svelte +201 -91
  143. package/src/SvForm.test.ts +299 -0
  144. package/src/SvGrid.controller.svelte.ts +152 -28
  145. package/src/SvGrid.css +193 -5
  146. package/src/SvGrid.svelte +357 -12
  147. package/src/SvGrid.types.ts +463 -1
  148. package/src/SvGridDropdown.svelte +8 -1
  149. package/src/SvGridSelect.svelte +1 -0
  150. package/src/SvListBox.svelte +36 -14
  151. package/src/SvMaskedInput.svelte +4 -3
  152. package/src/SvMenu.svelte +1 -1
  153. package/src/SvMenu.test.ts +24 -0
  154. package/src/SvMenuList.svelte +23 -71
  155. package/src/SvModal.svelte +12 -29
  156. package/src/SvModal.test.ts +82 -0
  157. package/src/SvMultiSelect.svelte +1 -0
  158. package/src/SvNumberInput.svelte +4 -3
  159. package/src/SvPagination.svelte +18 -18
  160. package/src/SvPasswordInput.svelte +3 -2
  161. package/src/SvPhoneInput.svelte +3 -2
  162. package/src/SvPopover.svelte +1 -1
  163. package/src/SvStepper.svelte +6 -9
  164. package/src/SvTagsInput.svelte +3 -2
  165. package/src/SvTooltip.svelte +12 -23
  166. package/src/SvTooltip.test.ts +73 -0
  167. package/src/SvTour.svelte +32 -4
  168. package/src/SvTreeSelect.svelte +1 -0
  169. package/src/build-api.ts +74 -51
  170. package/src/columns.ts +5 -0
  171. package/src/conditional-formatting.test.ts +104 -0
  172. package/src/conditional-formatting.ts +176 -19
  173. package/src/createCarousel.svelte.ts +104 -0
  174. package/src/createCommand.svelte.ts +125 -0
  175. package/src/createForm.svelte.ts +333 -0
  176. package/src/createMenu.svelte.ts +202 -0
  177. package/src/createOverlay.svelte.ts +101 -0
  178. package/src/createPagination.test.ts +65 -0
  179. package/src/createPagination.ts +84 -0
  180. package/src/createPopoverSelect.svelte.ts +5 -9
  181. package/src/createStepper.test.ts +62 -0
  182. package/src/createStepper.ts +61 -0
  183. package/src/createTooltip.svelte.ts +86 -0
  184. package/src/datetime/timezone.test.ts +69 -0
  185. package/src/datetime/timezone.ts +134 -0
  186. package/src/dock-context.ts +47 -0
  187. package/src/dock-manager-model.test.ts +275 -0
  188. package/src/dock-manager-model.ts +593 -0
  189. package/src/dock-model.test.ts +163 -0
  190. package/src/dock-model.ts +368 -0
  191. package/src/dock-popout.ts +94 -0
  192. package/src/editor-contract.ts +2 -0
  193. package/src/filter-operators.test.ts +59 -0
  194. package/src/filter-operators.ts +35 -2
  195. package/src/filtering/excel-filters.test.ts +86 -1
  196. package/src/filtering/excel-filters.ts +65 -0
  197. package/src/form-field.ts +87 -0
  198. package/src/index.ts +158 -4
  199. package/src/list-nav.test.ts +49 -0
  200. package/src/list-nav.ts +29 -0
  201. package/src/list-option.ts +2 -0
  202. package/src/menu-item.ts +21 -0
  203. package/src/menus.test.ts +48 -0
  204. package/src/menus.ts +73 -16
  205. package/src/panel-resize.ts +52 -0
  206. package/src/popover.bounds.test.ts +93 -0
  207. package/src/popover.ts +51 -7
  208. package/src/recurrence.test.ts +88 -1
  209. package/src/recurrence.ts +166 -15
  210. package/src/row-drag.ts +10 -0
  211. package/src/scheduler-ical.test.ts +68 -0
  212. package/src/scheduler-ical.ts +180 -0
  213. package/src/scheduler-model.test.ts +546 -0
  214. package/src/scheduler-model.ts +853 -0
  215. package/src/scheduler-view.svelte.ts +33 -0
  216. package/src/spreadsheet.ts +20 -12
  217. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  218. package/src/ui-app.types.ts +3 -0
  219. package/src/virtual.test.ts +6 -4
@@ -1,31 +1,16 @@
1
1
  <script lang="ts" module>
2
- import type { Validator } from './validators'
3
-
4
- export type FormFieldType =
5
- | 'text' | 'email' | 'tel' | 'textarea' | 'number' | 'password'
6
- | 'select' | 'checkbox' | 'switch' | 'date' | 'color' | 'rating'
7
-
8
- export type FormField = {
9
- name: string
10
- label: string
11
- type?: FormFieldType
12
- required?: boolean
13
- placeholder?: string
14
- options?: Array<{ value: string | number; label: string }>
15
- /** Declarative validation rules (see `rules` - email/pattern/min/compare...). */
16
- rules?: ReadonlyArray<Validator>
17
- /** Return an error message, or null/undefined when valid. */
18
- validate?: (value: any, values: Record<string, any>) => string | null | undefined
19
- /** Span full width in the grid. */
20
- full?: boolean
21
- }
2
+ // FormField/FormFieldType live in a plain `.ts` (breaks a Vite dev import
3
+ // cycle); re-exported so `import { type FormField } from './SvForm.svelte'`
4
+ // sites keep working.
5
+ export type { FormField, FormFieldType, FormSection, FormEntry } from './form-field'
22
6
  </script>
23
7
 
24
8
  <script lang="ts">
25
9
  /**
26
10
  * SvForm - a schema-driven form that renders the SvGrid UI-kit controls with
27
- * labels, required + custom validation, and a submit handler. Parity: Smart
28
- * `smart-form`. Emits `onSubmit(values)` only when valid; `onChange` on edits.
11
+ * labels, required + custom validation, and a submit handler. The schema may be
12
+ * a flat `FormField[]` or mix in titled `FormSection`s; with `stepper` the
13
+ * sections become a validated wizard. Emits `onSubmit(values)` only when valid.
29
14
  */
30
15
  import SvNumberInput from './SvNumberInput.svelte'
31
16
  import SvPasswordInput from './SvPasswordInput.svelte'
@@ -36,105 +21,211 @@
36
21
  import SvColorInput from './SvColorInput.svelte'
37
22
  import SvRating from './SvRating.svelte'
38
23
  import SvButton from './SvButton.svelte'
39
- import { runRules } from './validators'
24
+ import SvStepper from './SvStepper.svelte'
25
+ import { isFormSection, type FormField, type FormEntry, type FormSection } from './form-field'
26
+ import { createForm } from './createForm.svelte'
40
27
 
41
28
  type Props = {
42
- fields: ReadonlyArray<FormField>
29
+ fields: ReadonlyArray<FormEntry>
43
30
  initial?: Record<string, any>
44
- onSubmit?: (values: Record<string, any>) => void
31
+ /** Called with the visible-field values on a valid submit. May be async -
32
+ * the Submit button shows a loading state until it settles. */
33
+ onSubmit?: (values: Record<string, any>) => void | Promise<void>
45
34
  onChange?: (values: Record<string, any>) => void
35
+ /** When set, a secondary Cancel button renders next to Submit. */
36
+ onCancel?: () => void
46
37
  submitLabel?: string
47
- /** Columns in the responsive field grid. */
38
+ cancelLabel?: string
39
+ /** Show a Reset button that restores the initial values. */
40
+ showReset?: boolean
41
+ resetLabel?: string
42
+ /** Render titled sections as a validated multi-step wizard (requires sections). */
43
+ stepper?: boolean
44
+ /** Columns in the responsive field grid (a section can override its own). */
48
45
  columns?: number
49
46
  disabled?: boolean
50
47
  }
51
48
 
52
- let { fields, initial = {}, onSubmit, onChange, submitLabel = 'Submit', columns = 1, disabled = false }: Props = $props()
49
+ let {
50
+ fields, initial = {}, onSubmit, onChange, onCancel,
51
+ submitLabel = 'Submit', cancelLabel = 'Cancel', showReset = false, resetLabel = 'Reset',
52
+ stepper = false, columns = 1, disabled = false,
53
+ }: Props = $props()
53
54
 
54
- let values = $state<Record<string, any>>({})
55
- let errors = $state<Record<string, string>>({})
56
- let touched = $state<Record<string, boolean>>({})
57
- let seededForm = false
58
- $effect(() => { if (!seededForm) { seededForm = true; values = { ...initial } } })
55
+ const form = createForm({ fields: () => fields, initial, onSubmit, onChange })
56
+ const submit = (e: Event) => { e.preventDefault(); form.submit() }
57
+ const dateVal = (v: any): Date | null => (v instanceof Date ? v : v ? new Date(v) : null)
59
58
 
60
- function setValue(name: string, v: any) {
61
- values = { ...values, [name]: v }
62
- onChange?.(values)
63
- if (touched[name]) validateField(name)
64
- }
59
+ const sections = $derived(fields.filter(isFormSection) as FormSection[])
60
+ const wizard = $derived(stepper && sections.length > 0)
61
+ const stepItems = $derived(sections.map((s) => ({ label: s.section, description: s.description })))
65
62
 
66
- function validateField(name: string): boolean {
67
- const field = fields.find((f) => f.name === name)
68
- if (!field) return true
69
- const v = values[name]
70
- let err: string | null | undefined = null
71
- if (field.required && (v == null || v === '' || (Array.isArray(v) && !v.length))) err = `${field.label} is required`
72
- if (!err && field.rules) err = runRules(v, field.rules, values)
73
- if (!err && field.validate) err = field.validate(v, values)
74
- const next = { ...errors }
75
- if (err) next[name] = err
76
- else delete next[name]
77
- errors = next
78
- return !err
63
+ let step = $state(0)
64
+ async function next() {
65
+ const s = sections[step]
66
+ if (!s) return
67
+ const ok = await form.validateFields(s.fields.map((f) => f.name))
68
+ if (ok && step < sections.length - 1) step += 1
79
69
  }
70
+ const back = () => { if (step > 0) step -= 1 }
80
71
 
81
- function submit(e: Event) {
82
- e.preventDefault()
83
- let ok = true
84
- const t: Record<string, boolean> = {}
85
- for (const f of fields) { t[f.name] = true; if (!validateField(f.name)) ok = false }
86
- touched = t
87
- if (ok) onSubmit?.({ ...values })
72
+ // Shared shape passed to the `control` snippet so top-level fields and array
73
+ // item fields render the same set of controls without duplication.
74
+ type ControlOpts = {
75
+ id: string
76
+ type: string
77
+ label: string
78
+ value: any
79
+ onChange: (v: any) => void
80
+ onBlur?: () => void
81
+ disabled: boolean
82
+ placeholder?: string
83
+ options?: ReadonlyArray<{ value: string | number; label: string; color?: string }>
88
84
  }
89
-
90
- const dateVal = (v: any): Date | null => (v instanceof Date ? v : v ? new Date(v) : null)
91
85
  </script>
92
86
 
93
- <form class="sv-form" style:--cols={columns} onsubmit={submit} novalidate>
94
- <div class="sv-form__grid">
95
- {#each fields as field (field.name)}
96
- {@const type = field.type ?? 'text'}
97
- <div class="sv-form__field" class:is-full={field.full || columns === 1} class:has-error={errors[field.name]}>
87
+ {#snippet control(o: ControlOpts)}
88
+ {#if o.type === 'textarea'}
89
+ <textarea id={o.id} class="sv-form__control sv-form__textarea" rows="3" disabled={o.disabled} placeholder={o.placeholder} value={o.value ?? ''} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur}></textarea>
90
+ {:else if o.type === 'number'}
91
+ <SvNumberInput value={o.value ?? null} disabled={o.disabled} onChange={o.onChange} />
92
+ {:else if o.type === 'password'}
93
+ <SvPasswordInput value={o.value ?? ''} disabled={o.disabled} onChange={o.onChange} />
94
+ {:else if o.type === 'select'}
95
+ <SvDropDownList options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} placeholder={o.placeholder} onChange={o.onChange} />
96
+ {:else if o.type === 'checkbox'}
97
+ <SvCheckBox checked={!!o.value} disabled={o.disabled} onChange={o.onChange}>{o.label}</SvCheckBox>
98
+ {:else if o.type === 'switch'}
99
+ <div class="sv-form__inline"><SvSwitchButton checked={!!o.value} disabled={o.disabled} onChange={o.onChange} ariaLabel={o.label} /><span>{o.label}</span></div>
100
+ {:else if o.type === 'date'}
101
+ <SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" disabled={o.disabled} onChange={o.onChange} />
102
+ {:else if o.type === 'color'}
103
+ <SvColorInput value={o.value ?? '#3b82f6'} disabled={o.disabled} onChange={o.onChange} />
104
+ {:else if o.type === 'rating'}
105
+ <SvRating value={o.value ?? 0} disabled={o.disabled} onChange={o.onChange} />
106
+ {:else}
107
+ <input id={o.id} class="sv-form__control" type={o.type} disabled={o.disabled} placeholder={o.placeholder} value={o.value ?? ''} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur} />
108
+ {/if}
109
+ {/snippet}
110
+
111
+ {#snippet renderItemField(arrayName: string, i: number, itf: FormField)}
112
+ {@const type = itf.type ?? 'text'}
113
+ {@const id = `f-${arrayName}-${i}-${itf.name}`}
114
+ {@const err = form.itemError(arrayName, i, itf.name)}
115
+ <div class="sv-form__field" class:has-error={err}>
116
+ {#if type !== 'checkbox' && type !== 'switch'}
117
+ <label class="sv-form__label" for={id}>{itf.label}{#if itf.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}</label>
118
+ {/if}
119
+ {@render control({ id, type, label: itf.label, value: form.itemValue(arrayName, i, itf.name), onChange: (v) => form.setItemValue(arrayName, i, itf.name, v), onBlur: () => form.handleItemBlur(arrayName, i, itf.name), disabled, placeholder: itf.placeholder, options: itf.options })}
120
+ {#if err}<span class="sv-form__error" role="alert">{err}</span>{/if}
121
+ </div>
122
+ {/snippet}
123
+
124
+ {#snippet renderArray(field: FormField)}
125
+ <div class="sv-form__field is-full sv-form__array" class:has-error={form.error(field.name)}>
126
+ <label class="sv-form__label">{field.label}{#if field.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}</label>
127
+ {#each form.arrayItems(field.name) as _item, i (i)}
128
+ <div class="sv-form__arow">
129
+ <div class="sv-form__grid" style:--cols={field.itemFields?.length ?? 1}>
130
+ {#each field.itemFields ?? [] as itf (itf.name)}{@render renderItemField(field.name, i, itf)}{/each}
131
+ </div>
132
+ <button type="button" class="sv-form__aremove" disabled={disabled} onclick={() => form.removeItem(field.name, i)} aria-label="Remove item">&times;</button>
133
+ </div>
134
+ {/each}
135
+ <div><SvButton type="button" variant="outline" size="sm" disabled={disabled} onclick={() => form.addItem(field.name)}>{field.addLabel ?? '+ Add'}</SvButton></div>
136
+ {#if form.error(field.name)}<span class="sv-form__error" role="alert">{form.error(field.name)}</span>{/if}
137
+ </div>
138
+ {/snippet}
139
+
140
+ {#snippet renderField(field: FormField, cols: number)}
141
+ {#if form.isVisible(field.name)}
142
+ {@const type = field.type ?? 'text'}
143
+ {#if type === 'array'}
144
+ {@render renderArray(field)}
145
+ {:else}
146
+ {@const fieldDisabled = disabled || form.isDisabled(field.name)}
147
+ <div class="sv-form__field" class:is-full={field.full || cols === 1} class:has-error={form.error(field.name)}>
98
148
  {#if type !== 'checkbox' && type !== 'switch'}
99
149
  <label class="sv-form__label" for={`f-${field.name}`}>
100
150
  {field.label}{#if field.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}
101
151
  </label>
102
152
  {/if}
103
-
104
- {#if type === 'textarea'}
105
- <textarea id={`f-${field.name}`} class="sv-form__control sv-form__textarea" rows="3" {disabled} placeholder={field.placeholder} value={values[field.name] ?? ''} oninput={(e) => setValue(field.name, e.currentTarget.value)} onblur={() => { touched[field.name] = true; validateField(field.name) }}></textarea>
106
- {:else if type === 'number'}
107
- <SvNumberInput value={values[field.name] ?? null} {disabled} onChange={(v) => setValue(field.name, v)} />
108
- {:else if type === 'password'}
109
- <SvPasswordInput value={values[field.name] ?? ''} {disabled} onChange={(v) => setValue(field.name, v)} />
110
- {:else if type === 'select'}
111
- <SvDropDownList options={field.options ?? []} value={values[field.name] ?? null} {disabled} placeholder={field.placeholder} onChange={(v) => setValue(field.name, v)} />
112
- {:else if type === 'checkbox'}
113
- <SvCheckBox checked={!!values[field.name]} {disabled} onChange={(v) => setValue(field.name, v)}>{field.label}</SvCheckBox>
114
- {:else if type === 'switch'}
115
- <div class="sv-form__inline"><SvSwitchButton checked={!!values[field.name]} {disabled} onChange={(v) => setValue(field.name, v)} ariaLabel={field.label} /><span>{field.label}</span></div>
116
- {:else if type === 'date'}
117
- <SvDateTimePicker value={dateVal(values[field.name])} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" {disabled} onChange={(v) => setValue(field.name, v)} />
118
- {:else if type === 'color'}
119
- <SvColorInput value={values[field.name] ?? '#3b82f6'} {disabled} onChange={(v) => setValue(field.name, v)} />
120
- {:else if type === 'rating'}
121
- <SvRating value={values[field.name] ?? 0} {disabled} onChange={(v) => setValue(field.name, v)} />
122
- {:else}
123
- <input id={`f-${field.name}`} class="sv-form__control" type={type} {disabled} placeholder={field.placeholder} value={values[field.name] ?? ''} oninput={(e) => setValue(field.name, e.currentTarget.value)} onblur={() => { touched[field.name] = true; validateField(field.name) }} />
153
+ {@render control({ id: `f-${field.name}`, type, label: field.label, value: form.value(field.name), onChange: (v) => form.setValue(field.name, v), onBlur: () => form.handleBlur(field.name), disabled: fieldDisabled, placeholder: field.placeholder, options: field.options })}
154
+ {#if form.error(field.name)}
155
+ <span class="sv-form__error" role="alert">{form.error(field.name)}</span>
156
+ {:else if form.isValidating(field.name)}
157
+ <span class="sv-form__checking" role="status"><span class="sv-form__spinner" aria-hidden="true"></span>Checking…</span>
124
158
  {/if}
125
-
126
- {#if errors[field.name]}<span class="sv-form__error" role="alert">{errors[field.name]}</span>{/if}
127
159
  </div>
128
- {/each}
160
+ {/if}
161
+ {/if}
162
+ {/snippet}
163
+
164
+ {#snippet grid(flds: ReadonlyArray<FormField>, cols: number)}
165
+ <div class="sv-form__grid" style:--cols={cols}>
166
+ {#each flds as field (field.name)}{@render renderField(field, cols)}{/each}
129
167
  </div>
168
+ {/snippet}
169
+
170
+ {#snippet actions()}
130
171
  <div class="sv-form__actions">
131
- <SvButton type="submit" {disabled}>{submitLabel}</SvButton>
172
+ {#if showReset}
173
+ <SvButton type="button" variant="ghost" disabled={disabled || form.submitting} onclick={() => form.reset()}>{resetLabel}</SvButton>
174
+ {/if}
175
+ {#if onCancel}
176
+ <SvButton type="button" variant="secondary" disabled={disabled || form.submitting} onclick={onCancel}>{cancelLabel}</SvButton>
177
+ {/if}
178
+ <SvButton type="submit" loading={form.submitting} disabled={disabled || form.submitting}>{submitLabel}</SvButton>
132
179
  </div>
180
+ {/snippet}
181
+
182
+ <form class="sv-form" onsubmit={submit} novalidate>
183
+ {#if wizard}
184
+ {@const s = sections[Math.min(step, sections.length - 1)]!}
185
+ <SvStepper steps={stepItems} current={step} onChange={(n) => (step = n)} linear />
186
+ <fieldset class="sv-form__section">
187
+ {#if s.description}<p class="sv-form__section-desc">{s.description}</p>{/if}
188
+ {@render grid(s.fields, s.columns ?? columns)}
189
+ </fieldset>
190
+ <div class="sv-form__actions">
191
+ {#if step > 0}
192
+ <SvButton type="button" variant="secondary" disabled={form.submitting} onclick={back}>Back</SvButton>
193
+ {/if}
194
+ <span class="sv-form__spacer"></span>
195
+ {#if step < sections.length - 1}
196
+ <SvButton type="button" disabled={form.submitting} onclick={next}>Next</SvButton>
197
+ {:else}
198
+ {#if showReset}
199
+ <SvButton type="button" variant="ghost" disabled={disabled || form.submitting} onclick={() => { form.reset(); step = 0 }}>{resetLabel}</SvButton>
200
+ {/if}
201
+ <SvButton type="submit" loading={form.submitting} disabled={disabled || form.submitting}>{submitLabel}</SvButton>
202
+ {/if}
203
+ </div>
204
+ {:else if sections.length > 0}
205
+ {#each fields as entry (isFormSection(entry) ? entry.section : entry.name)}
206
+ {#if isFormSection(entry)}
207
+ <fieldset class="sv-form__section">
208
+ <legend class="sv-form__section-title">{entry.section}</legend>
209
+ {#if entry.description}<p class="sv-form__section-desc">{entry.description}</p>{/if}
210
+ {@render grid(entry.fields, entry.columns ?? columns)}
211
+ </fieldset>
212
+ {:else}
213
+ {@render grid([entry], columns)}
214
+ {/if}
215
+ {/each}
216
+ {@render actions()}
217
+ {:else}
218
+ {@render grid(fields as ReadonlyArray<FormField>, columns)}
219
+ {@render actions()}
220
+ {/if}
133
221
  </form>
134
222
 
135
223
  <style>
136
- .sv-form { --_accent: var(--sg-accent, #2563eb); width: 100%; }
224
+ .sv-form { --_accent: var(--sg-accent, #2563eb); width: 100%; display: flex; flex-direction: column; gap: 16px; }
137
225
  .sv-form__grid { display: grid; grid-template-columns: repeat(var(--cols, 1), minmax(0, 1fr)); gap: 14px 18px; }
226
+ .sv-form__section { border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; padding: 16px; margin: 0; min-inline-size: 0; }
227
+ .sv-form__section-title { font-size: 13px; font-weight: 700; color: var(--sg-fg, #0f172a); padding: 0 4px; }
228
+ .sv-form__section-desc { margin: 0 0 12px; font-size: 12px; color: var(--sg-muted, #64748b); }
138
229
  .sv-form__field { display: flex; flex-direction: column; gap: 5px; }
139
230
  .sv-form__field.is-full { grid-column: 1 / -1; }
140
231
  .sv-form__label { font-size: 12.5px; font-weight: 600; color: var(--sg-fg, #0f172a); }
@@ -149,5 +240,24 @@
149
240
  .sv-form__field.has-error .sv-form__control { border-color: var(--sg-danger, #dc2626); }
150
241
  .sv-form__inline { display: flex; align-items: center; gap: 8px; font-size: 13px; }
151
242
  .sv-form__error { font-size: 11.5px; color: var(--sg-danger, #dc2626); }
152
- .sv-form__actions { margin-top: 16px; }
243
+ .sv-form__checking { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--sg-muted, #64748b); }
244
+ .sv-form__spinner {
245
+ width: 11px; height: 11px; border-radius: 50%;
246
+ border: 2px solid color-mix(in srgb, var(--_accent) 30%, transparent);
247
+ border-top-color: var(--_accent); animation: sv-form-spin 0.6s linear infinite;
248
+ }
249
+ @keyframes sv-form-spin { to { transform: rotate(360deg); } }
250
+ @media (prefers-reduced-motion: reduce) { .sv-form__spinner { animation: none; } }
251
+ .sv-form__actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
252
+ .sv-form__spacer { flex: 1; }
253
+ .sv-form__array { gap: 8px; }
254
+ .sv-form__arow { display: flex; align-items: flex-start; gap: 8px; }
255
+ .sv-form__arow > .sv-form__grid { flex: 1; padding: 10px; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px; }
256
+ .sv-form__aremove {
257
+ flex: none; width: 28px; height: 28px; margin-top: 10px; border-radius: 6px; cursor: pointer;
258
+ border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff);
259
+ color: var(--sg-muted, #64748b); font-size: 16px; line-height: 1;
260
+ }
261
+ .sv-form__aremove:hover:not(:disabled) { color: var(--sg-danger, #dc2626); border-color: var(--sg-danger, #dc2626); }
262
+ .sv-form__aremove:disabled { opacity: 0.5; cursor: default; }
153
263
  </style>
@@ -1,29 +1,22 @@
1
- import type { Validator } from './validators';
2
- export type FormFieldType = 'text' | 'email' | 'tel' | 'textarea' | 'number' | 'password' | 'select' | 'checkbox' | 'switch' | 'date' | 'color' | 'rating';
3
- export type FormField = {
4
- name: string;
5
- label: string;
6
- type?: FormFieldType;
7
- required?: boolean;
8
- placeholder?: string;
9
- options?: Array<{
10
- value: string | number;
11
- label: string;
12
- }>;
13
- /** Declarative validation rules (see `rules` - email/pattern/min/compare...). */
14
- rules?: ReadonlyArray<Validator>;
15
- /** Return an error message, or null/undefined when valid. */
16
- validate?: (value: any, values: Record<string, any>) => string | null | undefined;
17
- /** Span full width in the grid. */
18
- full?: boolean;
19
- };
1
+ export type { FormField, FormFieldType, FormSection, FormEntry } from './form-field';
2
+ import { type FormEntry } from './form-field';
20
3
  type Props = {
21
- fields: ReadonlyArray<FormField>;
4
+ fields: ReadonlyArray<FormEntry>;
22
5
  initial?: Record<string, any>;
23
- onSubmit?: (values: Record<string, any>) => void;
6
+ /** Called with the visible-field values on a valid submit. May be async -
7
+ * the Submit button shows a loading state until it settles. */
8
+ onSubmit?: (values: Record<string, any>) => void | Promise<void>;
24
9
  onChange?: (values: Record<string, any>) => void;
10
+ /** When set, a secondary Cancel button renders next to Submit. */
11
+ onCancel?: () => void;
25
12
  submitLabel?: string;
26
- /** Columns in the responsive field grid. */
13
+ cancelLabel?: string;
14
+ /** Show a Reset button that restores the initial values. */
15
+ showReset?: boolean;
16
+ resetLabel?: string;
17
+ /** Render titled sections as a validated multi-step wizard (requires sections). */
18
+ stepper?: boolean;
19
+ /** Columns in the responsive field grid (a section can override its own). */
27
20
  columns?: number;
28
21
  disabled?: boolean;
29
22
  };
@@ -121,6 +121,11 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
121
121
  filterMenuPos: MenuPosition;
122
122
  operatorMenuFor: string | null;
123
123
  operatorMenuPos: MenuPosition;
124
+ inSuggestFor: string | null;
125
+ inSuggestPos: MenuPosition;
126
+ inSuggestQuery: string;
127
+ readonly inSuggestValues: string[];
128
+ readonly facetValuesForColumn: (columnId: string) => Array<string>;
124
129
  chooseColumnsPos: MenuPosition | null;
125
130
  contextMenuFor: ContextMenuTarget<TData> | null;
126
131
  contextMenuPos: MenuPosition;
@@ -155,6 +160,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
155
160
  internalData: readonly TData[];
156
161
  internalColumns: ColumnDef<TFeatures, TData>[];
157
162
  hiddenColumns: Record<string, boolean>;
163
+ readonly declaredHiddenColumns: Record<string, boolean>;
158
164
  readonly toggleColumnGroup: (groupId: string) => void;
159
165
  readonly isColumnGroupCollapsed: (groupId: string) => boolean;
160
166
  readonly externalSortEnabled: boolean;
@@ -188,6 +194,26 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
188
194
  collapsed: boolean;
189
195
  }[];
190
196
  }[];
197
+ readonly groupHeaderRowsWindowed: {
198
+ id: string;
199
+ cells: {
200
+ key: string;
201
+ label: string;
202
+ colSpan: number;
203
+ widthPx: number;
204
+ /** First leaf-column index this cell spans. */
205
+ firstLeafIndex: number;
206
+ /** True for the placeholder cells that fill the column above an
207
+ * early-bottoming leaf (e.g. the row-label column to the left of a
208
+ * multi-level value tree). They render as empty cells so the
209
+ * layout stays aligned without showing duplicate labels. */
210
+ isPlaceholder: boolean;
211
+ /** Set when this group cell has a collapse toggle. */
212
+ groupId?: string;
213
+ collapsible: boolean;
214
+ collapsed: boolean;
215
+ }[];
216
+ }[];
191
217
  readonly pinnedOffsets: {
192
218
  left: Record<string, number>;
193
219
  right: Record<string, number>;
@@ -435,7 +461,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
435
461
  readonly computePinnedCellClass: (rowData: TData, column: Column<TData>) => string;
436
462
  readonly isRowSelected: (rowId: string) => boolean;
437
463
  readonly toggleRowSelectionById: (rowId: string) => void;
438
- readonly headerSelectionState: "all" | "none" | "some";
464
+ readonly headerSelectionState: "none" | "all" | "some";
439
465
  readonly toggleSelectAllRows: () => void;
440
466
  userHasActivatedCell: boolean;
441
467
  readonly setActiveCell: (rowIndex: number, colIndex: number) => void;
@@ -552,6 +578,11 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
552
578
  readonly openColumnMenu: (event: MouseEvent, columnId: string) => void;
553
579
  readonly openFilterMenu: (event: MouseEvent, columnId: string) => void;
554
580
  readonly openOperatorMenu: (event: MouseEvent, columnId: string) => void;
581
+ readonly openInSuggest: (anchor: HTMLElement, columnId: string) => void;
582
+ readonly closeInSuggest: () => void;
583
+ readonly addFilterToken: (columnId: string, token: string) => void;
584
+ readonly removeFilterToken: (columnId: string, token: string) => void;
585
+ readonly toggleFilterToken: (columnId: string, token: string) => void;
555
586
  readonly sortColumnFromMenu: (columnId: string, desc: boolean) => void;
556
587
  readonly clearColumnSort: (columnId: string) => void;
557
588
  readonly groupByColumnFromMenu: (columnId: string) => void;