@svgrid/grid 1.2.22 → 1.2.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 (190) hide show
  1. package/dist/GridFooter.svelte +34 -20
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/SvAutoComplete.svelte +100 -0
  4. package/dist/SvAutoComplete.svelte.d.ts +16 -0
  5. package/dist/SvButton.svelte +97 -0
  6. package/dist/SvButton.svelte.d.ts +23 -0
  7. package/dist/SvCalendar.svelte +669 -0
  8. package/dist/SvCalendar.svelte.d.ts +70 -0
  9. package/dist/SvCheckBox.svelte +81 -0
  10. package/dist/SvCheckBox.svelte.d.ts +19 -0
  11. package/dist/SvColorInput.svelte +187 -0
  12. package/dist/SvColorInput.svelte.d.ts +15 -0
  13. package/dist/SvComboBox.svelte +130 -0
  14. package/dist/SvComboBox.svelte.d.ts +15 -0
  15. package/dist/SvCountryInput.svelte +123 -0
  16. package/dist/SvCountryInput.svelte.d.ts +14 -0
  17. package/dist/SvDateTimePicker.svelte +322 -0
  18. package/dist/SvDateTimePicker.svelte.d.ts +37 -0
  19. package/dist/SvDropDownList.svelte +143 -0
  20. package/dist/SvDropDownList.svelte.d.ts +15 -0
  21. package/dist/SvForm.svelte +147 -0
  22. package/dist/SvForm.svelte.d.ts +29 -0
  23. package/dist/SvGauge.svelte +99 -0
  24. package/dist/SvGauge.svelte.d.ts +31 -0
  25. package/dist/SvGrid.controller.svelte.d.ts +16 -2
  26. package/dist/SvGrid.controller.svelte.js +63 -8
  27. package/dist/SvGrid.css +84 -31
  28. package/dist/SvGrid.helpers.d.ts +4 -0
  29. package/dist/SvGrid.helpers.js +40 -12
  30. package/dist/SvGrid.svelte +84 -5
  31. package/dist/SvGrid.types.d.ts +36 -0
  32. package/dist/SvGridDropdown.svelte +16 -30
  33. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  34. package/dist/SvListBox.svelte +122 -0
  35. package/dist/SvListBox.svelte.d.ts +19 -0
  36. package/dist/SvMaskedInput.svelte +0 -0
  37. package/dist/SvMaskedInput.svelte.d.ts +15 -0
  38. package/dist/SvNumberInput.svelte +0 -0
  39. package/dist/SvNumberInput.svelte.d.ts +22 -0
  40. package/dist/SvPasswordInput.svelte +110 -0
  41. package/dist/SvPasswordInput.svelte.d.ts +22 -0
  42. package/dist/SvPhoneInput.svelte +0 -0
  43. package/dist/SvPhoneInput.svelte.d.ts +19 -0
  44. package/dist/SvRadioGroup.svelte +108 -0
  45. package/dist/SvRadioGroup.svelte.d.ts +23 -0
  46. package/dist/SvRating.svelte +111 -0
  47. package/dist/SvRating.svelte.d.ts +20 -0
  48. package/dist/SvRepeatButton.svelte +89 -0
  49. package/dist/SvRepeatButton.svelte.d.ts +21 -0
  50. package/dist/SvSlider.svelte +182 -0
  51. package/dist/SvSlider.svelte.d.ts +25 -0
  52. package/dist/SvSwitchButton.svelte +94 -0
  53. package/dist/SvSwitchButton.svelte.d.ts +19 -0
  54. package/dist/SvTabs.svelte +122 -0
  55. package/dist/SvTabs.svelte.d.ts +26 -0
  56. package/dist/SvTagsInput.svelte +81 -0
  57. package/dist/SvTagsInput.svelte.d.ts +19 -0
  58. package/dist/SvTimePicker.svelte +359 -0
  59. package/dist/SvTimePicker.svelte.d.ts +27 -0
  60. package/dist/SvToggleButton.svelte +57 -0
  61. package/dist/SvToggleButton.svelte.d.ts +16 -0
  62. package/dist/SvTree.svelte +210 -0
  63. package/dist/SvTree.svelte.d.ts +29 -0
  64. package/dist/build-api.js +78 -1
  65. package/dist/cdn/svgrid.js +10022 -5904
  66. package/dist/cdn/svgrid.svelte-external.js +8531 -4429
  67. package/dist/cell-render.d.ts +4 -0
  68. package/dist/cell-render.js +35 -1
  69. package/dist/clipboard.js +19 -1
  70. package/dist/collaboration.d.ts +2 -0
  71. package/dist/collaboration.js +4 -0
  72. package/dist/core.d.ts +22 -1
  73. package/dist/countries.d.ts +15 -0
  74. package/dist/countries.js +61 -0
  75. package/dist/datetime/date-core.d.ts +88 -0
  76. package/dist/datetime/date-core.js +165 -0
  77. package/dist/datetime/date-format.d.ts +39 -0
  78. package/dist/datetime/date-format.js +371 -0
  79. package/dist/datetime/date-restrict.d.ts +23 -0
  80. package/dist/datetime/date-restrict.js +50 -0
  81. package/dist/datetime/date-selection.d.ts +25 -0
  82. package/dist/datetime/date-selection.js +94 -0
  83. package/dist/datetime/mask.d.ts +21 -0
  84. package/dist/datetime/mask.js +90 -0
  85. package/dist/editing.d.ts +1 -1
  86. package/dist/editing.js +7 -0
  87. package/dist/editors/cell-editors.d.ts +2 -2
  88. package/dist/editors/cell-editors.js +3 -1
  89. package/dist/export-format.d.ts +178 -0
  90. package/dist/export-format.js +419 -0
  91. package/dist/hyperformula-adapter.d.ts +4 -0
  92. package/dist/hyperformula-adapter.js +15 -6
  93. package/dist/index.d.ts +36 -0
  94. package/dist/index.js +41 -0
  95. package/dist/list-option.d.ts +10 -0
  96. package/dist/list-option.js +7 -0
  97. package/dist/menus.js +18 -2
  98. package/dist/popover.d.ts +48 -0
  99. package/dist/popover.js +65 -0
  100. package/dist/row-resize.d.ts +4 -4
  101. package/dist/row-resize.js +8 -5
  102. package/dist/selection.d.ts +6 -0
  103. package/dist/selection.js +31 -5
  104. package/dist/server-data-source.d.ts +43 -0
  105. package/dist/server-data-source.js +100 -0
  106. package/dist/sparkline.js +8 -4
  107. package/dist/svgrid-wrapper.types.d.ts +26 -1
  108. package/package.json +6 -1
  109. package/src/GridFooter.svelte +34 -20
  110. package/src/SvAutoComplete.svelte +100 -0
  111. package/src/SvButton.svelte +97 -0
  112. package/src/SvCalendar.svelte +669 -0
  113. package/src/SvCalendar.test.ts +211 -0
  114. package/src/SvCheckBox.svelte +81 -0
  115. package/src/SvColorInput.svelte +187 -0
  116. package/src/SvComboBox.svelte +130 -0
  117. package/src/SvCountryInput.svelte +123 -0
  118. package/src/SvDateTimePicker.svelte +322 -0
  119. package/src/SvDateTimePicker.test.ts +136 -0
  120. package/src/SvDropDownList.svelte +143 -0
  121. package/src/SvForm.svelte +147 -0
  122. package/src/SvGauge.svelte +99 -0
  123. package/src/SvGrid.controller.svelte.ts +74 -8
  124. package/src/SvGrid.css +84 -31
  125. package/src/SvGrid.helpers.ts +41 -12
  126. package/src/SvGrid.svelte +84 -5
  127. package/src/SvGrid.types.ts +33 -0
  128. package/src/SvGridDropdown.svelte +16 -30
  129. package/src/SvListBox.svelte +122 -0
  130. package/src/SvMaskedInput.svelte +0 -0
  131. package/src/SvNumberInput.svelte +0 -0
  132. package/src/SvPasswordInput.svelte +110 -0
  133. package/src/SvPhoneInput.svelte +0 -0
  134. package/src/SvRadioGroup.svelte +108 -0
  135. package/src/SvRating.svelte +111 -0
  136. package/src/SvRepeatButton.svelte +89 -0
  137. package/src/SvSlider.svelte +182 -0
  138. package/src/SvSwitchButton.svelte +94 -0
  139. package/src/SvTabs.svelte +122 -0
  140. package/src/SvTagsInput.svelte +81 -0
  141. package/src/SvTimePicker.svelte +359 -0
  142. package/src/SvTimePicker.test.ts +98 -0
  143. package/src/SvToggleButton.svelte +57 -0
  144. package/src/SvTree.svelte +210 -0
  145. package/src/build-api.ts +115 -0
  146. package/src/cell-render.test.ts +38 -0
  147. package/src/cell-render.ts +43 -0
  148. package/src/clipboard.test.ts +18 -0
  149. package/src/clipboard.ts +24 -1
  150. package/src/collaboration.test.ts +30 -0
  151. package/src/collaboration.ts +6 -0
  152. package/src/core.ts +27 -3
  153. package/src/countries.ts +71 -0
  154. package/src/datetime/date-core.test.ts +217 -0
  155. package/src/datetime/date-core.ts +204 -0
  156. package/src/datetime/date-format.test.ts +121 -0
  157. package/src/datetime/date-format.ts +317 -0
  158. package/src/datetime/date-restrict.ts +60 -0
  159. package/src/datetime/date-selection.test.ts +129 -0
  160. package/src/datetime/date-selection.ts +137 -0
  161. package/src/datetime/mask.test.ts +36 -0
  162. package/src/datetime/mask.ts +84 -0
  163. package/src/editing.test.ts +22 -0
  164. package/src/editing.ts +7 -1
  165. package/src/editors/cell-editors.ts +7 -1
  166. package/src/export-data-api.test.ts +126 -0
  167. package/src/export-format.test.ts +107 -0
  168. package/src/export-format.ts +598 -0
  169. package/src/hyperformula-adapter.test.ts +35 -1
  170. package/src/hyperformula-adapter.ts +18 -6
  171. package/src/index.ts +71 -0
  172. package/src/list-option.ts +15 -0
  173. package/src/menus.ts +21 -2
  174. package/src/popover.ts +79 -0
  175. package/src/row-resize.test.ts +25 -0
  176. package/src/row-resize.ts +8 -5
  177. package/src/selection.test.ts +36 -4
  178. package/src/selection.ts +30 -9
  179. package/src/server-data-source.test.ts +201 -0
  180. package/src/server-data-source.ts +148 -0
  181. package/src/sparkline.test.ts +9 -0
  182. package/src/sparkline.ts +9 -4
  183. package/src/svgrid-wrapper.types.ts +28 -1
  184. package/src/svgrid.behavior.test.ts +27 -0
  185. package/src/svgrid.new-features.wrapper.test.ts +27 -1
  186. package/src/ui-buttons.test.ts +105 -0
  187. package/src/ui-composite.test.ts +89 -0
  188. package/src/ui-inputs.test.ts +92 -0
  189. package/src/ui-range.test.ts +61 -0
  190. package/src/ui-selection.test.ts +106 -0
@@ -0,0 +1,322 @@
1
+ <script lang="ts" module>
2
+ export type DateTimeValue = Date | string | number | null
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvDateTimePicker - a text input (formatted via the date-format token engine)
8
+ * plus a dropdown that composes SvCalendar + SvTimePicker behind DATE / TIME
9
+ * tabs. Parity target: Smart `smart-date-time-picker`. Works standalone and as
10
+ * the SvGrid `datetime` cell editor. The dropdown portals to <body> (via
11
+ * popover.ts) so it is never clipped by the grid's scroll container.
12
+ */
13
+ import SvCalendar from './SvCalendar.svelte'
14
+ import SvTimePicker from './SvTimePicker.svelte'
15
+ import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
16
+ import { formatDate, parseDate } from './datetime/date-format'
17
+ import { toDate, clampDate, withTime, startOfDay, type DateLike } from './datetime/date-core'
18
+
19
+ type Props = {
20
+ value?: DateTimeValue
21
+ onChange?: (value: Date | null) => void
22
+ /** Fired when the value is finalized (Enter, blur, or a single-date pick).
23
+ * Used by the grid to save the cell. */
24
+ onCommit?: (value: Date | null) => void
25
+ /** Fired on Escape / dismiss without committing (grid cancels the edit). */
26
+ onCancel?: () => void
27
+ /** Display / parse mask (token engine). */
28
+ formatString?: string
29
+ min?: DateLike | null
30
+ max?: DateLike | null
31
+ nullable?: boolean
32
+ placeholder?: string
33
+ disabled?: boolean
34
+ readonly?: boolean
35
+ name?: string
36
+ locale?: string
37
+ firstDayOfWeek?: number
38
+ weekNumbers?: boolean
39
+ hourFormat?: '12-hour' | '24-hour'
40
+ minuteInterval?: number
41
+ /** Which tabs the dropdown shows. */
42
+ dropDownDisplayMode?: 'both' | 'calendar' | 'time'
43
+ /** Up/down spinner buttons that bump the value by `stepMinutes`. */
44
+ spinButtons?: boolean
45
+ stepMinutes?: number
46
+ /** Open the dropdown as soon as the field is focused (grid in-cell editing). */
47
+ autoOpen?: boolean
48
+ /** Animate the calendar's month/drill navigation (honors reduced-motion). */
49
+ animate?: boolean | 'slide' | 'fade'
50
+ }
51
+
52
+ let {
53
+ value = null,
54
+ onChange,
55
+ onCommit,
56
+ onCancel,
57
+ formatString = 'yyyy-MM-dd HH:mm',
58
+ min = null,
59
+ max = null,
60
+ nullable = true,
61
+ placeholder = 'Select date & time',
62
+ disabled = false,
63
+ readonly = false,
64
+ name,
65
+ locale = typeof navigator !== 'undefined' ? navigator.language : 'en-US',
66
+ firstDayOfWeek = 0,
67
+ weekNumbers = false,
68
+ hourFormat = '24-hour',
69
+ minuteInterval = 1,
70
+ dropDownDisplayMode = 'both',
71
+ spinButtons = false,
72
+ stepMinutes = 1,
73
+ autoOpen = false,
74
+ animate = false,
75
+ }: Props = $props()
76
+
77
+ const isInteractive = $derived(!disabled && !readonly)
78
+ const minD = $derived(toDate(min ?? null))
79
+ const maxD = $derived(toDate(max ?? null))
80
+
81
+ let current = $state<Date | null>(null)
82
+ let text = $state('')
83
+ let lastKey = '￿'
84
+ $effect(() => {
85
+ const d = toDate(value as DateLike)
86
+ const key = d ? String(d.getTime()) : 'null'
87
+ if (key !== lastKey) {
88
+ lastKey = key
89
+ current = d
90
+ text = d ? formatDate(d, formatString, locale) : ''
91
+ }
92
+ })
93
+
94
+ // --- Commit helpers ---
95
+ function commit(next: Date | null) {
96
+ const clamped = next ? clampDate(next, minD, maxD) : null
97
+ current = clamped
98
+ lastKey = clamped ? String(clamped.getTime()) : 'null'
99
+ text = clamped ? formatDate(clamped, formatString, locale) : ''
100
+ onChange?.(clamped)
101
+ }
102
+
103
+ function commitFromText() {
104
+ const raw = text.trim()
105
+ if (!raw) {
106
+ if (nullable) commit(null)
107
+ else text = current ? formatDate(current, formatString, locale) : ''
108
+ return
109
+ }
110
+ const parsed = parseDate(raw, formatString, locale, 2029, current ?? new Date())
111
+ if (parsed) commit(parsed)
112
+ else text = current ? formatDate(current, formatString, locale) : '' // revert on bad input
113
+ }
114
+
115
+ function bump(deltaMin: number) {
116
+ const base = current ?? new Date()
117
+ commit(new Date(base.getTime() + deltaMin * 60000))
118
+ }
119
+
120
+ // --- Sub-picker wiring ---
121
+ function onCalendarChange(dates: Date[]) {
122
+ const day = dates[0]
123
+ if (!day) return
124
+ commit(withTime(day, current ?? startOfDay(day)))
125
+ // Date-only picker: a single pick finalizes the value.
126
+ if (dropDownDisplayMode === 'calendar') { open = false; onCommit?.(current) }
127
+ else if (dropDownDisplayMode === 'both') tab = 'time'
128
+ }
129
+ function onTimeChange(t: Date) {
130
+ const base = current ?? new Date()
131
+ commit(withTime(base, t))
132
+ }
133
+
134
+ // --- Dropdown (portalled) ---
135
+ let open = $state(false)
136
+ let tab = $state<'date' | 'time'>('date')
137
+ let triggerEl = $state<HTMLDivElement | null>(null)
138
+ let panelEl = $state<HTMLDivElement | null>(null)
139
+ let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
140
+
141
+ function updatePos() {
142
+ if (!triggerEl) return
143
+ panelRect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 360, minWidth: 300 })
144
+ }
145
+ function openPanel() {
146
+ if (!isInteractive || open) return
147
+ tab = dropDownDisplayMode === 'time' ? 'time' : 'date'
148
+ open = true
149
+ updatePos()
150
+ }
151
+ function closePanel() { open = false }
152
+ function toggle() { (open ? closePanel : openPanel)() }
153
+
154
+ $effect(() => {
155
+ if (!open) return
156
+ const reposition = () => updatePos()
157
+ window.addEventListener('scroll', reposition, true)
158
+ window.addEventListener('resize', reposition)
159
+ const onDown = (e: PointerEvent) => {
160
+ const t = e.target as Node | null
161
+ if (!t) return
162
+ if (triggerEl?.contains(t) || panelEl?.contains(t)) return
163
+ closePanel()
164
+ }
165
+ document.addEventListener('pointerdown', onDown, true)
166
+ return () => {
167
+ window.removeEventListener('scroll', reposition, true)
168
+ window.removeEventListener('resize', reposition)
169
+ document.removeEventListener('pointerdown', onDown, true)
170
+ }
171
+ })
172
+
173
+ function onInputKeydown(e: KeyboardEvent) {
174
+ if (e.key === 'Enter') { e.preventDefault(); commitFromText(); closePanel(); onCommit?.(current) }
175
+ else if (e.key === 'Escape') { closePanel(); onCancel?.() }
176
+ else if (e.key === 'ArrowDown' && (e.altKey || !text)) { e.preventDefault(); openPanel() }
177
+ else if (spinButtons && e.key === 'ArrowUp') { e.preventDefault(); bump(stepMinutes) }
178
+ else if (spinButtons && e.key === 'ArrowDown') { e.preventDefault(); bump(-stepMinutes) }
179
+ }
180
+
181
+ function focusOpen(node: HTMLInputElement) {
182
+ if (!autoOpen) return
183
+ node.focus()
184
+ openPanel()
185
+ }
186
+
187
+ function onRootFocusOut(e: FocusEvent) {
188
+ const next = e.relatedTarget as Node | null
189
+ // Ignore focus moving within the field or into the portalled panel.
190
+ if (next && (triggerEl?.contains(next) || panelEl?.contains(next))) return
191
+ onCommit?.(current)
192
+ }
193
+
194
+ const showDateTab = $derived(dropDownDisplayMode !== 'time')
195
+ const showTimeTab = $derived(dropDownDisplayMode !== 'calendar')
196
+ </script>
197
+
198
+ <div class="sv-dtp" class:sv-dtp--disabled={disabled} onfocusout={onRootFocusOut}>
199
+ <div class="sv-dtp__field" bind:this={triggerEl}>
200
+ {#if spinButtons}
201
+ <div class="sv-dtp__spin">
202
+ <button type="button" tabindex="-1" aria-label="Increment" onclick={() => bump(stepMinutes)} disabled={!isInteractive}>▲</button>
203
+ <button type="button" tabindex="-1" aria-label="Decrement" onclick={() => bump(-stepMinutes)} disabled={!isInteractive}>▼</button>
204
+ </div>
205
+ {/if}
206
+ <input
207
+ class="sv-dtp__input"
208
+ type="text"
209
+ bind:value={text}
210
+ {placeholder}
211
+ {disabled}
212
+ {readonly}
213
+ onblur={commitFromText}
214
+ onkeydown={onInputKeydown}
215
+ use:focusOpen
216
+ />
217
+ {#if nullable && current && isInteractive}
218
+ <button type="button" class="sv-dtp__clear" aria-label="Clear" onclick={() => commit(null)}>&times;</button>
219
+ {/if}
220
+ <button type="button" class="sv-dtp__toggle" aria-label="Open picker" aria-haspopup="dialog" aria-expanded={open} onclick={toggle} disabled={!isInteractive} tabindex="-1">
221
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" /><path d="M16 2v4M8 2v4M3 10h18" /></svg>
222
+ </button>
223
+ </div>
224
+
225
+ {#if open}
226
+ <div
227
+ class="sv-dtp__panel"
228
+ bind:this={panelEl}
229
+ use:portalToBody
230
+ style:position="fixed"
231
+ style:top={`${panelRect.top}px`}
232
+ style:left={`${panelRect.left}px`}
233
+ role="dialog"
234
+ aria-label="Choose date and time"
235
+ >
236
+ {#if showDateTab && showTimeTab}
237
+ <div class="sv-dtp__tabs" role="tablist">
238
+ <button type="button" role="tab" aria-selected={tab === 'date'} class:is-active={tab === 'date'} onclick={() => (tab = 'date')}>DATE</button>
239
+ <button type="button" role="tab" aria-selected={tab === 'time'} class:is-active={tab === 'time'} onclick={() => (tab = 'time')}>TIME</button>
240
+ </div>
241
+ {/if}
242
+ <div class="sv-dtp__body">
243
+ {#if tab === 'date' && showDateTab}
244
+ <SvCalendar
245
+ value={current}
246
+ selectionMode="one"
247
+ min={min}
248
+ max={max}
249
+ {firstDayOfWeek}
250
+ {weekNumbers}
251
+ {locale}
252
+ {animate}
253
+ onChange={onCalendarChange}
254
+ />
255
+ {:else if showTimeTab}
256
+ <SvTimePicker
257
+ value={current}
258
+ format={hourFormat}
259
+ {minuteInterval}
260
+ footer
261
+ onChange={onTimeChange}
262
+ />
263
+ {/if}
264
+ </div>
265
+ </div>
266
+ {/if}
267
+
268
+ {#if name}<input type="hidden" {name} value={current ? current.toISOString() : ''} />{/if}
269
+ </div>
270
+
271
+ <style>
272
+ .sv-dtp {
273
+ --_accent: var(--sg-accent, #2563eb);
274
+ --_bg: var(--sg-input-bg, var(--sg-bg, #fff));
275
+ --_fg: var(--sg-fg, #0f172a);
276
+ --_muted: var(--sg-muted, #64748b);
277
+ --_border: var(--sg-input-border, var(--sg-border, #cbd5e1));
278
+ --_radius: var(--sg-radius, 8px);
279
+ display: inline-block; position: relative; width: 240px; color: var(--_fg);
280
+ }
281
+ .sv-dtp--disabled { opacity: 0.6; }
282
+ .sv-dtp__field {
283
+ display: flex; align-items: center; gap: 2px;
284
+ background: var(--_bg); border: 1px solid var(--_border); border-radius: var(--_radius);
285
+ padding: 0 4px 0 0; height: 34px;
286
+ }
287
+ .sv-dtp__field:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
288
+ .sv-dtp__input {
289
+ flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit;
290
+ font: inherit; font-size: 13px; padding: 0 8px; height: 100%;
291
+ }
292
+ .sv-dtp__spin { display: flex; flex-direction: column; border-right: 1px solid var(--_border); }
293
+ .sv-dtp__spin button {
294
+ flex: 1; width: 20px; border: 0; background: none; color: var(--_muted); cursor: pointer;
295
+ font-size: 7px; line-height: 1; padding: 0;
296
+ }
297
+ .sv-dtp__spin button:hover:not(:disabled) { color: var(--_accent); }
298
+ .sv-dtp__clear, .sv-dtp__toggle {
299
+ display: grid; place-items: center; width: 26px; height: 26px; flex: none;
300
+ background: none; border: 0; color: var(--_muted); cursor: pointer; border-radius: 6px;
301
+ }
302
+ .sv-dtp__clear { font-size: 17px; line-height: 1; }
303
+ .sv-dtp__clear:hover, .sv-dtp__toggle:hover { color: var(--_accent); background: color-mix(in srgb, var(--_accent) 10%, transparent); }
304
+
305
+ /* Portalled panel - lives under <body>, styled globally. */
306
+ :global(.sv-dtp__panel) {
307
+ z-index: 2147483647;
308
+ background: var(--sg-bg, #fff);
309
+ border: 1px solid var(--sg-border, #e2e8f0);
310
+ border-radius: 12px;
311
+ box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35);
312
+ overflow: hidden;
313
+ }
314
+ :global(.sv-dtp__tabs) { display: flex; border-bottom: 1px solid var(--sg-border, #e2e8f0); }
315
+ :global(.sv-dtp__tabs button) {
316
+ flex: 1; padding: 9px; font: inherit; font-size: 11px; font-weight: 700; letter-spacing: 0.04em;
317
+ background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer;
318
+ border-bottom: 2px solid transparent;
319
+ }
320
+ :global(.sv-dtp__tabs button.is-active) { color: var(--sg-accent, #2563eb); border-bottom-color: var(--sg-accent, #2563eb); }
321
+ :global(.sv-dtp__body) { display: grid; place-items: center; padding: 8px; }
322
+ </style>
@@ -0,0 +1,37 @@
1
+ export type DateTimeValue = Date | string | number | null;
2
+ import { type DateLike } from './datetime/date-core';
3
+ type Props = {
4
+ value?: DateTimeValue;
5
+ onChange?: (value: Date | null) => void;
6
+ /** Fired when the value is finalized (Enter, blur, or a single-date pick).
7
+ * Used by the grid to save the cell. */
8
+ onCommit?: (value: Date | null) => void;
9
+ /** Fired on Escape / dismiss without committing (grid cancels the edit). */
10
+ onCancel?: () => void;
11
+ /** Display / parse mask (token engine). */
12
+ formatString?: string;
13
+ min?: DateLike | null;
14
+ max?: DateLike | null;
15
+ nullable?: boolean;
16
+ placeholder?: string;
17
+ disabled?: boolean;
18
+ readonly?: boolean;
19
+ name?: string;
20
+ locale?: string;
21
+ firstDayOfWeek?: number;
22
+ weekNumbers?: boolean;
23
+ hourFormat?: '12-hour' | '24-hour';
24
+ minuteInterval?: number;
25
+ /** Which tabs the dropdown shows. */
26
+ dropDownDisplayMode?: 'both' | 'calendar' | 'time';
27
+ /** Up/down spinner buttons that bump the value by `stepMinutes`. */
28
+ spinButtons?: boolean;
29
+ stepMinutes?: number;
30
+ /** Open the dropdown as soon as the field is focused (grid in-cell editing). */
31
+ autoOpen?: boolean;
32
+ /** Animate the calendar's month/drill navigation (honors reduced-motion). */
33
+ animate?: boolean | 'slide' | 'fade';
34
+ };
35
+ declare const SvDateTimePicker: import("svelte").Component<Props, {}, "">;
36
+ type SvDateTimePicker = ReturnType<typeof SvDateTimePicker>;
37
+ export default SvDateTimePicker;
@@ -0,0 +1,143 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvDropDownList - a single-select dropdown (trigger button + portalled list,
4
+ * no typing). Parity: Smart `smart-drop-down-list`. Controlled via `value` +
5
+ * `onChange`. Popover escapes scroll clipping via popover.ts.
6
+ */
7
+ import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
8
+ import type { ListOption } from './list-option'
9
+
10
+ type Props = {
11
+ options: ReadonlyArray<ListOption>
12
+ value?: string | number | null
13
+ onChange?: (value: string | number) => void
14
+ placeholder?: string
15
+ disabled?: boolean
16
+ name?: string
17
+ size?: 'sm' | 'md' | 'lg'
18
+ ariaLabel?: string
19
+ autoOpen?: boolean
20
+ }
21
+
22
+ let { options, value = null, onChange, placeholder = 'Select…', disabled = false, name, size = 'md', ariaLabel, autoOpen = false }: Props = $props()
23
+
24
+ let open = $state(false)
25
+ let active = $state(-1)
26
+ let triggerEl = $state<HTMLButtonElement | null>(null)
27
+ let panelEl = $state<HTMLDivElement | null>(null)
28
+ let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
29
+
30
+ const selected = $derived(options.find((o) => o.value === value) ?? null)
31
+ const enabledIdx = $derived(options.map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0))
32
+
33
+ function updatePos() {
34
+ if (!triggerEl) return
35
+ rect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: Math.min(options.length, 8) * 34 + 8 })
36
+ }
37
+ function openPanel() {
38
+ if (disabled || open) return
39
+ open = true
40
+ active = Math.max(0, options.findIndex((o) => o.value === value))
41
+ updatePos()
42
+ }
43
+ function close() { open = false }
44
+ function toggle() { open ? close() : openPanel() }
45
+ function pick(i: number) {
46
+ const o = options[i]
47
+ if (!o || o.disabled) return
48
+ onChange?.(o.value); close(); triggerEl?.focus()
49
+ }
50
+ function move(d: number) {
51
+ const pos = enabledIdx.indexOf(active)
52
+ active = enabledIdx[(pos + d + enabledIdx.length) % enabledIdx.length] ?? active
53
+ queueMicrotask(() => panelEl?.querySelector<HTMLElement>(`[data-idx="${active}"]`)?.scrollIntoView({ block: 'nearest' }))
54
+ }
55
+ function onKeydown(e: KeyboardEvent) {
56
+ if (disabled) return
57
+ if (!open && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); openPanel(); return }
58
+ if (!open) return
59
+ if (e.key === 'ArrowDown') { e.preventDefault(); move(1) }
60
+ else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1) }
61
+ else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(active) }
62
+ else if (e.key === 'Escape') { e.preventDefault(); close(); triggerEl?.focus() }
63
+ else if (e.key === 'Home') { active = enabledIdx[0] ?? 0 }
64
+ else if (e.key === 'End') { active = enabledIdx.at(-1) ?? 0 }
65
+ }
66
+
67
+ $effect(() => {
68
+ if (!open) return
69
+ const rp = () => updatePos()
70
+ window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
71
+ const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (triggerEl?.contains(t) || panelEl?.contains(t))) return; close() }
72
+ document.addEventListener('pointerdown', od, true)
73
+ return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); document.removeEventListener('pointerdown', od, true) }
74
+ })
75
+ function focusOpen(node: HTMLButtonElement) { if (autoOpen) { node.focus(); openPanel() } }
76
+ </script>
77
+
78
+ <button
79
+ bind:this={triggerEl}
80
+ type="button"
81
+ class="sv-ddl sv-ddl--{size}"
82
+ class:is-open={open}
83
+ class:is-disabled={disabled}
84
+ aria-haspopup="listbox"
85
+ aria-expanded={open}
86
+ aria-label={ariaLabel}
87
+ {disabled}
88
+ onclick={toggle}
89
+ onkeydown={onKeydown}
90
+ use:focusOpen
91
+ >
92
+ <span class="sv-ddl__value" class:is-placeholder={!selected}>{selected?.label ?? placeholder}</span>
93
+ <svg class="sv-ddl__chev" viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
94
+ </button>
95
+
96
+ {#if open}
97
+ <div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} role="listbox" tabindex="-1">
98
+ {#each options as opt, i (opt.value)}
99
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
100
+ <div
101
+ class="sv-ddl__opt"
102
+ class:is-active={i === active}
103
+ class:is-selected={opt.value === value}
104
+ class:is-disabled={opt.disabled}
105
+ role="option"
106
+ tabindex="-1"
107
+ aria-selected={opt.value === value}
108
+ data-idx={i}
109
+ onclick={() => pick(i)}
110
+ onpointermove={() => { if (!opt.disabled) active = i }}
111
+ >{opt.label}</div>
112
+ {/each}
113
+ </div>
114
+ {/if}
115
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
116
+
117
+ <style>
118
+ .sv-ddl {
119
+ --_accent: var(--sg-accent, #2563eb);
120
+ display: inline-flex; align-items: center; justify-content: space-between; gap: 8px; width: 200px;
121
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); font: inherit; text-align: left;
122
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
123
+ padding: 0 10px; cursor: pointer;
124
+ }
125
+ .sv-ddl--sm { height: 28px; font-size: 12px; }
126
+ .sv-ddl--md { height: 34px; font-size: 13px; }
127
+ .sv-ddl--lg { height: 40px; font-size: 15px; }
128
+ .sv-ddl.is-open, .sv-ddl:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); outline: none; }
129
+ .sv-ddl.is-disabled { opacity: 0.6; cursor: not-allowed; }
130
+ .sv-ddl__value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
131
+ .sv-ddl__value.is-placeholder { color: var(--sg-muted, #94a3b8); }
132
+ .sv-ddl__chev { color: var(--sg-muted, #64748b); flex: none; }
133
+
134
+ :global(.sv-ddl__panel) {
135
+ z-index: 2147483647; max-height: 288px; overflow-y: auto; padding: 4px;
136
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
137
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
138
+ }
139
+ :global(.sv-ddl__opt) { padding: 7px 10px; border-radius: 6px; cursor: pointer; font-size: 13px; }
140
+ :global(.sv-ddl__opt.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
141
+ :global(.sv-ddl__opt.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
142
+ :global(.sv-ddl__opt.is-disabled) { opacity: 0.4; cursor: not-allowed; }
143
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { ListOption } from './list-option';
2
+ type Props = {
3
+ options: ReadonlyArray<ListOption>;
4
+ value?: string | number | null;
5
+ onChange?: (value: string | number) => void;
6
+ placeholder?: string;
7
+ disabled?: boolean;
8
+ name?: string;
9
+ size?: 'sm' | 'md' | 'lg';
10
+ ariaLabel?: string;
11
+ autoOpen?: boolean;
12
+ };
13
+ declare const SvDropDownList: import("svelte").Component<Props, {}, "">;
14
+ type SvDropDownList = ReturnType<typeof SvDropDownList>;
15
+ export default SvDropDownList;
@@ -0,0 +1,147 @@
1
+ <script lang="ts" module>
2
+ export type FormFieldType =
3
+ | 'text' | 'email' | 'tel' | 'textarea' | 'number' | 'password'
4
+ | 'select' | 'checkbox' | 'switch' | 'date' | 'color' | 'rating'
5
+
6
+ export type FormField = {
7
+ name: string
8
+ label: string
9
+ type?: FormFieldType
10
+ required?: boolean
11
+ placeholder?: string
12
+ options?: Array<{ value: string | number; label: string }>
13
+ /** Return an error message, or null/undefined when valid. */
14
+ validate?: (value: any, values: Record<string, any>) => string | null | undefined
15
+ /** Span full width in the grid. */
16
+ full?: boolean
17
+ }
18
+ </script>
19
+
20
+ <script lang="ts">
21
+ /**
22
+ * SvForm - a schema-driven form that renders the SvGrid UI-kit controls with
23
+ * labels, required + custom validation, and a submit handler. Parity: Smart
24
+ * `smart-form`. Emits `onSubmit(values)` only when valid; `onChange` on edits.
25
+ */
26
+ import SvNumberInput from './SvNumberInput.svelte'
27
+ import SvPasswordInput from './SvPasswordInput.svelte'
28
+ import SvDropDownList from './SvDropDownList.svelte'
29
+ import SvCheckBox from './SvCheckBox.svelte'
30
+ import SvSwitchButton from './SvSwitchButton.svelte'
31
+ import SvDateTimePicker from './SvDateTimePicker.svelte'
32
+ import SvColorInput from './SvColorInput.svelte'
33
+ import SvRating from './SvRating.svelte'
34
+ import SvButton from './SvButton.svelte'
35
+
36
+ type Props = {
37
+ fields: ReadonlyArray<FormField>
38
+ initial?: Record<string, any>
39
+ onSubmit?: (values: Record<string, any>) => void
40
+ onChange?: (values: Record<string, any>) => void
41
+ submitLabel?: string
42
+ /** Columns in the responsive field grid. */
43
+ columns?: number
44
+ disabled?: boolean
45
+ }
46
+
47
+ let { fields, initial = {}, onSubmit, onChange, submitLabel = 'Submit', columns = 1, disabled = false }: Props = $props()
48
+
49
+ let values = $state<Record<string, any>>({})
50
+ let errors = $state<Record<string, string>>({})
51
+ let touched = $state<Record<string, boolean>>({})
52
+ let seededForm = false
53
+ $effect(() => { if (!seededForm) { seededForm = true; values = { ...initial } } })
54
+
55
+ function setValue(name: string, v: any) {
56
+ values = { ...values, [name]: v }
57
+ onChange?.(values)
58
+ if (touched[name]) validateField(name)
59
+ }
60
+
61
+ function validateField(name: string): boolean {
62
+ const field = fields.find((f) => f.name === name)
63
+ if (!field) return true
64
+ const v = values[name]
65
+ let err: string | null | undefined = null
66
+ if (field.required && (v == null || v === '' || (Array.isArray(v) && !v.length))) err = `${field.label} is required`
67
+ else if (field.validate) err = field.validate(v, values)
68
+ const next = { ...errors }
69
+ if (err) next[name] = err
70
+ else delete next[name]
71
+ errors = next
72
+ return !err
73
+ }
74
+
75
+ function submit(e: Event) {
76
+ e.preventDefault()
77
+ let ok = true
78
+ const t: Record<string, boolean> = {}
79
+ for (const f of fields) { t[f.name] = true; if (!validateField(f.name)) ok = false }
80
+ touched = t
81
+ if (ok) onSubmit?.({ ...values })
82
+ }
83
+
84
+ const dateVal = (v: any): Date | null => (v instanceof Date ? v : v ? new Date(v) : null)
85
+ </script>
86
+
87
+ <form class="sv-form" style:--cols={columns} onsubmit={submit} novalidate>
88
+ <div class="sv-form__grid">
89
+ {#each fields as field (field.name)}
90
+ {@const type = field.type ?? 'text'}
91
+ <div class="sv-form__field" class:is-full={field.full || columns === 1} class:has-error={errors[field.name]}>
92
+ {#if type !== 'checkbox' && type !== 'switch'}
93
+ <label class="sv-form__label" for={`f-${field.name}`}>
94
+ {field.label}{#if field.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}
95
+ </label>
96
+ {/if}
97
+
98
+ {#if type === 'textarea'}
99
+ <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>
100
+ {:else if type === 'number'}
101
+ <SvNumberInput value={values[field.name] ?? null} {disabled} onChange={(v) => setValue(field.name, v)} />
102
+ {:else if type === 'password'}
103
+ <SvPasswordInput value={values[field.name] ?? ''} {disabled} onChange={(v) => setValue(field.name, v)} />
104
+ {:else if type === 'select'}
105
+ <SvDropDownList options={field.options ?? []} value={values[field.name] ?? null} {disabled} placeholder={field.placeholder} onChange={(v) => setValue(field.name, v)} />
106
+ {:else if type === 'checkbox'}
107
+ <SvCheckBox checked={!!values[field.name]} {disabled} onChange={(v) => setValue(field.name, v)}>{field.label}</SvCheckBox>
108
+ {:else if type === 'switch'}
109
+ <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>
110
+ {:else if type === 'date'}
111
+ <SvDateTimePicker value={dateVal(values[field.name])} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" {disabled} onChange={(v) => setValue(field.name, v)} />
112
+ {:else if type === 'color'}
113
+ <SvColorInput value={values[field.name] ?? '#3b82f6'} {disabled} onChange={(v) => setValue(field.name, v)} />
114
+ {:else if type === 'rating'}
115
+ <SvRating value={values[field.name] ?? 0} {disabled} onChange={(v) => setValue(field.name, v)} />
116
+ {:else}
117
+ <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) }} />
118
+ {/if}
119
+
120
+ {#if errors[field.name]}<span class="sv-form__error" role="alert">{errors[field.name]}</span>{/if}
121
+ </div>
122
+ {/each}
123
+ </div>
124
+ <div class="sv-form__actions">
125
+ <SvButton type="submit" {disabled}>{submitLabel}</SvButton>
126
+ </div>
127
+ </form>
128
+
129
+ <style>
130
+ .sv-form { --_accent: var(--sg-accent, #2563eb); width: 100%; }
131
+ .sv-form__grid { display: grid; grid-template-columns: repeat(var(--cols, 1), minmax(0, 1fr)); gap: 14px 18px; }
132
+ .sv-form__field { display: flex; flex-direction: column; gap: 5px; }
133
+ .sv-form__field.is-full { grid-column: 1 / -1; }
134
+ .sv-form__label { font-size: 12.5px; font-weight: 600; color: var(--sg-fg, #0f172a); }
135
+ .sv-form__req { color: var(--sg-danger, #dc2626); }
136
+ .sv-form__control {
137
+ width: 100%; box-sizing: border-box; height: 34px; padding: 0 10px; font: inherit; font-size: 13px;
138
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
139
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px); outline: none;
140
+ }
141
+ .sv-form__textarea { height: auto; padding: 8px 10px; resize: vertical; }
142
+ .sv-form__control:focus { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
143
+ .sv-form__field.has-error .sv-form__control { border-color: var(--sg-danger, #dc2626); }
144
+ .sv-form__inline { display: flex; align-items: center; gap: 8px; font-size: 13px; }
145
+ .sv-form__error { font-size: 11.5px; color: var(--sg-danger, #dc2626); }
146
+ .sv-form__actions { margin-top: 16px; }
147
+ </style>