@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.
- package/dist/GridFooter.svelte +34 -20
- package/dist/GridFooter.svelte.d.ts +8 -0
- package/dist/SvAutoComplete.svelte +100 -0
- package/dist/SvAutoComplete.svelte.d.ts +16 -0
- package/dist/SvButton.svelte +97 -0
- package/dist/SvButton.svelte.d.ts +23 -0
- package/dist/SvCalendar.svelte +669 -0
- package/dist/SvCalendar.svelte.d.ts +70 -0
- package/dist/SvCheckBox.svelte +81 -0
- package/dist/SvCheckBox.svelte.d.ts +19 -0
- package/dist/SvColorInput.svelte +187 -0
- package/dist/SvColorInput.svelte.d.ts +15 -0
- package/dist/SvComboBox.svelte +130 -0
- package/dist/SvComboBox.svelte.d.ts +15 -0
- package/dist/SvCountryInput.svelte +123 -0
- package/dist/SvCountryInput.svelte.d.ts +14 -0
- package/dist/SvDateTimePicker.svelte +322 -0
- package/dist/SvDateTimePicker.svelte.d.ts +37 -0
- package/dist/SvDropDownList.svelte +143 -0
- package/dist/SvDropDownList.svelte.d.ts +15 -0
- package/dist/SvForm.svelte +147 -0
- package/dist/SvForm.svelte.d.ts +29 -0
- package/dist/SvGauge.svelte +99 -0
- package/dist/SvGauge.svelte.d.ts +31 -0
- package/dist/SvGrid.controller.svelte.d.ts +16 -2
- package/dist/SvGrid.controller.svelte.js +63 -8
- package/dist/SvGrid.css +84 -31
- package/dist/SvGrid.helpers.d.ts +4 -0
- package/dist/SvGrid.helpers.js +40 -12
- package/dist/SvGrid.svelte +84 -5
- package/dist/SvGrid.types.d.ts +36 -0
- package/dist/SvGridDropdown.svelte +16 -30
- package/dist/SvGridDropdown.svelte.d.ts +4 -0
- package/dist/SvListBox.svelte +122 -0
- package/dist/SvListBox.svelte.d.ts +19 -0
- package/dist/SvMaskedInput.svelte +0 -0
- package/dist/SvMaskedInput.svelte.d.ts +15 -0
- package/dist/SvNumberInput.svelte +0 -0
- package/dist/SvNumberInput.svelte.d.ts +22 -0
- package/dist/SvPasswordInput.svelte +110 -0
- package/dist/SvPasswordInput.svelte.d.ts +22 -0
- package/dist/SvPhoneInput.svelte +0 -0
- package/dist/SvPhoneInput.svelte.d.ts +19 -0
- package/dist/SvRadioGroup.svelte +108 -0
- package/dist/SvRadioGroup.svelte.d.ts +23 -0
- package/dist/SvRating.svelte +111 -0
- package/dist/SvRating.svelte.d.ts +20 -0
- package/dist/SvRepeatButton.svelte +89 -0
- package/dist/SvRepeatButton.svelte.d.ts +21 -0
- package/dist/SvSlider.svelte +182 -0
- package/dist/SvSlider.svelte.d.ts +25 -0
- package/dist/SvSwitchButton.svelte +94 -0
- package/dist/SvSwitchButton.svelte.d.ts +19 -0
- package/dist/SvTabs.svelte +122 -0
- package/dist/SvTabs.svelte.d.ts +26 -0
- package/dist/SvTagsInput.svelte +81 -0
- package/dist/SvTagsInput.svelte.d.ts +19 -0
- package/dist/SvTimePicker.svelte +359 -0
- package/dist/SvTimePicker.svelte.d.ts +27 -0
- package/dist/SvToggleButton.svelte +57 -0
- package/dist/SvToggleButton.svelte.d.ts +16 -0
- package/dist/SvTree.svelte +210 -0
- package/dist/SvTree.svelte.d.ts +29 -0
- package/dist/build-api.js +78 -1
- package/dist/cdn/svgrid.js +10022 -5904
- package/dist/cdn/svgrid.svelte-external.js +8531 -4429
- package/dist/cell-render.d.ts +4 -0
- package/dist/cell-render.js +35 -1
- package/dist/clipboard.js +19 -1
- package/dist/collaboration.d.ts +2 -0
- package/dist/collaboration.js +4 -0
- package/dist/core.d.ts +22 -1
- package/dist/countries.d.ts +15 -0
- package/dist/countries.js +61 -0
- package/dist/datetime/date-core.d.ts +88 -0
- package/dist/datetime/date-core.js +165 -0
- package/dist/datetime/date-format.d.ts +39 -0
- package/dist/datetime/date-format.js +371 -0
- package/dist/datetime/date-restrict.d.ts +23 -0
- package/dist/datetime/date-restrict.js +50 -0
- package/dist/datetime/date-selection.d.ts +25 -0
- package/dist/datetime/date-selection.js +94 -0
- package/dist/datetime/mask.d.ts +21 -0
- package/dist/datetime/mask.js +90 -0
- package/dist/editing.d.ts +1 -1
- package/dist/editing.js +7 -0
- package/dist/editors/cell-editors.d.ts +2 -2
- package/dist/editors/cell-editors.js +3 -1
- package/dist/export-format.d.ts +178 -0
- package/dist/export-format.js +419 -0
- package/dist/hyperformula-adapter.d.ts +4 -0
- package/dist/hyperformula-adapter.js +15 -6
- package/dist/index.d.ts +36 -0
- package/dist/index.js +41 -0
- package/dist/list-option.d.ts +10 -0
- package/dist/list-option.js +7 -0
- package/dist/menus.js +18 -2
- package/dist/popover.d.ts +48 -0
- package/dist/popover.js +65 -0
- package/dist/row-resize.d.ts +4 -4
- package/dist/row-resize.js +8 -5
- package/dist/selection.d.ts +6 -0
- package/dist/selection.js +31 -5
- package/dist/server-data-source.d.ts +43 -0
- package/dist/server-data-source.js +100 -0
- package/dist/sparkline.js +8 -4
- package/dist/svgrid-wrapper.types.d.ts +26 -1
- package/package.json +6 -1
- package/src/GridFooter.svelte +34 -20
- package/src/SvAutoComplete.svelte +100 -0
- package/src/SvButton.svelte +97 -0
- package/src/SvCalendar.svelte +669 -0
- package/src/SvCalendar.test.ts +211 -0
- package/src/SvCheckBox.svelte +81 -0
- package/src/SvColorInput.svelte +187 -0
- package/src/SvComboBox.svelte +130 -0
- package/src/SvCountryInput.svelte +123 -0
- package/src/SvDateTimePicker.svelte +322 -0
- package/src/SvDateTimePicker.test.ts +136 -0
- package/src/SvDropDownList.svelte +143 -0
- package/src/SvForm.svelte +147 -0
- package/src/SvGauge.svelte +99 -0
- package/src/SvGrid.controller.svelte.ts +74 -8
- package/src/SvGrid.css +84 -31
- package/src/SvGrid.helpers.ts +41 -12
- package/src/SvGrid.svelte +84 -5
- package/src/SvGrid.types.ts +33 -0
- package/src/SvGridDropdown.svelte +16 -30
- package/src/SvListBox.svelte +122 -0
- package/src/SvMaskedInput.svelte +0 -0
- package/src/SvNumberInput.svelte +0 -0
- package/src/SvPasswordInput.svelte +110 -0
- package/src/SvPhoneInput.svelte +0 -0
- package/src/SvRadioGroup.svelte +108 -0
- package/src/SvRating.svelte +111 -0
- package/src/SvRepeatButton.svelte +89 -0
- package/src/SvSlider.svelte +182 -0
- package/src/SvSwitchButton.svelte +94 -0
- package/src/SvTabs.svelte +122 -0
- package/src/SvTagsInput.svelte +81 -0
- package/src/SvTimePicker.svelte +359 -0
- package/src/SvTimePicker.test.ts +98 -0
- package/src/SvToggleButton.svelte +57 -0
- package/src/SvTree.svelte +210 -0
- package/src/build-api.ts +115 -0
- package/src/cell-render.test.ts +38 -0
- package/src/cell-render.ts +43 -0
- package/src/clipboard.test.ts +18 -0
- package/src/clipboard.ts +24 -1
- package/src/collaboration.test.ts +30 -0
- package/src/collaboration.ts +6 -0
- package/src/core.ts +27 -3
- package/src/countries.ts +71 -0
- package/src/datetime/date-core.test.ts +217 -0
- package/src/datetime/date-core.ts +204 -0
- package/src/datetime/date-format.test.ts +121 -0
- package/src/datetime/date-format.ts +317 -0
- package/src/datetime/date-restrict.ts +60 -0
- package/src/datetime/date-selection.test.ts +129 -0
- package/src/datetime/date-selection.ts +137 -0
- package/src/datetime/mask.test.ts +36 -0
- package/src/datetime/mask.ts +84 -0
- package/src/editing.test.ts +22 -0
- package/src/editing.ts +7 -1
- package/src/editors/cell-editors.ts +7 -1
- package/src/export-data-api.test.ts +126 -0
- package/src/export-format.test.ts +107 -0
- package/src/export-format.ts +598 -0
- package/src/hyperformula-adapter.test.ts +35 -1
- package/src/hyperformula-adapter.ts +18 -6
- package/src/index.ts +71 -0
- package/src/list-option.ts +15 -0
- package/src/menus.ts +21 -2
- package/src/popover.ts +79 -0
- package/src/row-resize.test.ts +25 -0
- package/src/row-resize.ts +8 -5
- package/src/selection.test.ts +36 -4
- package/src/selection.ts +30 -9
- package/src/server-data-source.test.ts +201 -0
- package/src/server-data-source.ts +148 -0
- package/src/sparkline.test.ts +9 -0
- package/src/sparkline.ts +9 -4
- package/src/svgrid-wrapper.types.ts +28 -1
- package/src/svgrid.behavior.test.ts +27 -0
- package/src/svgrid.new-features.wrapper.test.ts +27 -1
- package/src/ui-buttons.test.ts +105 -0
- package/src/ui-composite.test.ts +89 -0
- package/src/ui-inputs.test.ts +92 -0
- package/src/ui-range.test.ts +61 -0
- 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)}>×</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>
|