@svgrid/grid 1.2.23 → 1.2.24
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/SvAutoComplete.svelte +23 -34
- package/dist/SvCalendar.svelte +74 -334
- package/dist/SvCalendar.svelte.d.ts +5 -23
- package/dist/SvCheckBox.svelte +11 -10
- package/dist/SvColorInput.svelte +31 -58
- package/dist/SvComboBox.svelte +32 -54
- package/dist/SvCountryInput.svelte +31 -29
- package/dist/SvDateTimePicker.svelte +53 -110
- package/dist/SvDateTimePicker.svelte.d.ts +4 -3
- package/dist/SvDropDownList.svelte +31 -54
- package/dist/SvGauge.svelte +21 -23
- package/dist/SvGauge.svelte.d.ts +0 -5
- package/dist/SvListBox.svelte +25 -58
- package/dist/SvListBox.svelte.d.ts +3 -0
- package/dist/SvMaskedInput.svelte +0 -0
- package/dist/SvNumberInput.svelte +0 -0
- package/dist/SvNumberInput.svelte.d.ts +2 -6
- package/dist/SvPasswordInput.svelte +18 -28
- package/dist/SvPasswordInput.svelte.d.ts +0 -4
- package/dist/SvPhoneInput.svelte +0 -0
- package/dist/SvRadioGroup.svelte +14 -37
- package/dist/SvRadioGroup.svelte.d.ts +6 -9
- package/dist/SvRating.svelte +22 -42
- package/dist/SvRating.svelte.d.ts +3 -5
- package/dist/SvSlider.svelte +30 -57
- package/dist/SvSlider.svelte.d.ts +0 -5
- package/dist/SvSwitchButton.svelte +10 -17
- package/dist/SvSwitchButton.svelte.d.ts +0 -5
- package/dist/SvTabs.svelte +26 -39
- package/dist/SvTabs.svelte.d.ts +5 -5
- package/dist/SvTagsInput.svelte +17 -28
- package/dist/SvTagsInput.svelte.d.ts +0 -5
- package/dist/SvTimePicker.svelte +52 -210
- package/dist/SvTimePicker.svelte.d.ts +11 -8
- package/dist/SvToggleButton.svelte +9 -9
- package/dist/SvTree.svelte +30 -109
- package/dist/SvTree.svelte.d.ts +9 -11
- package/dist/cdn/svgrid.js +7488 -5839
- package/dist/cdn/svgrid.svelte-external.js +4691 -3028
- package/dist/createAutocomplete.svelte.d.ts +77 -0
- package/dist/createAutocomplete.svelte.js +100 -0
- package/dist/createCalendar.svelte.d.ts +192 -0
- package/dist/createCalendar.svelte.js +402 -0
- package/dist/createCheckbox.svelte.d.ts +45 -0
- package/dist/createCheckbox.svelte.js +26 -0
- package/dist/createColorInput.svelte.d.ts +67 -0
- package/dist/createColorInput.svelte.js +100 -0
- package/dist/createCombobox.svelte.d.ts +102 -0
- package/dist/createCombobox.svelte.js +171 -0
- package/dist/createCountryInput.svelte.d.ts +86 -0
- package/dist/createCountryInput.svelte.js +126 -0
- package/dist/createDateTimePicker.svelte.d.ts +83 -0
- package/dist/createDateTimePicker.svelte.js +202 -0
- package/dist/createDropdownList.svelte.d.ts +78 -0
- package/dist/createDropdownList.svelte.js +117 -0
- package/dist/createGauge.svelte.d.ts +56 -0
- package/dist/createGauge.svelte.js +49 -0
- package/dist/createListbox.svelte.d.ts +79 -0
- package/dist/createListbox.svelte.js +117 -0
- package/dist/createMaskedInput.svelte.d.ts +30 -0
- package/dist/createMaskedInput.svelte.js +62 -0
- package/dist/createNumberInput.svelte.d.ts +72 -0
- package/dist/createNumberInput.svelte.js +167 -0
- package/dist/createPasswordInput.svelte.d.ts +60 -0
- package/dist/createPasswordInput.svelte.js +72 -0
- package/dist/createPhoneInput.svelte.d.ts +46 -0
- package/dist/createPhoneInput.svelte.js +105 -0
- package/dist/createRadioGroup.svelte.d.ts +64 -0
- package/dist/createRadioGroup.svelte.js +73 -0
- package/dist/createRating.svelte.d.ts +70 -0
- package/dist/createRating.svelte.js +85 -0
- package/dist/createSlider.svelte.d.ts +71 -0
- package/dist/createSlider.svelte.js +123 -0
- package/dist/createSwitch.svelte.d.ts +43 -0
- package/dist/createSwitch.svelte.js +41 -0
- package/dist/createTabs.svelte.d.ts +78 -0
- package/dist/createTabs.svelte.js +92 -0
- package/dist/createTagsInput.svelte.d.ts +65 -0
- package/dist/createTagsInput.svelte.js +93 -0
- package/dist/createTimePicker.svelte.d.ts +106 -0
- package/dist/createTimePicker.svelte.js +248 -0
- package/dist/createToggle.svelte.d.ts +43 -0
- package/dist/createToggle.svelte.js +39 -0
- package/dist/createTree.svelte.d.ts +96 -0
- package/dist/createTree.svelte.js +194 -0
- package/dist/editor-contract.d.ts +48 -0
- package/dist/editor-contract.js +26 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +31 -0
- package/package.json +1 -1
- package/src/SvAutoComplete.svelte +23 -34
- package/src/SvCalendar.svelte +74 -334
- package/src/SvCheckBox.svelte +11 -10
- package/src/SvColorInput.svelte +31 -58
- package/src/SvComboBox.svelte +32 -54
- package/src/SvCountryInput.svelte +31 -29
- package/src/SvDateTimePicker.svelte +53 -110
- package/src/SvDropDownList.svelte +31 -54
- package/src/SvGauge.svelte +21 -23
- package/src/SvListBox.svelte +25 -58
- package/src/SvMaskedInput.svelte +0 -0
- package/src/SvNumberInput.svelte +0 -0
- package/src/SvPasswordInput.svelte +18 -28
- package/src/SvPhoneInput.svelte +0 -0
- package/src/SvRadioGroup.svelte +14 -37
- package/src/SvRating.svelte +22 -42
- package/src/SvSlider.svelte +30 -57
- package/src/SvSwitchButton.svelte +10 -17
- package/src/SvTabs.svelte +26 -39
- package/src/SvTagsInput.svelte +17 -28
- package/src/SvTimePicker.svelte +52 -210
- package/src/SvToggleButton.svelte +9 -9
- package/src/SvTree.svelte +30 -109
- package/src/createAutocomplete.svelte.ts +104 -0
- package/src/createCalendar.svelte.ts +488 -0
- package/src/createCheckbox.svelte.ts +74 -0
- package/src/createColorInput.svelte.ts +115 -0
- package/src/createCombobox.svelte.ts +158 -0
- package/src/createCountryInput.svelte.ts +126 -0
- package/src/createDateTimePicker.svelte.ts +216 -0
- package/src/createDropdownList.svelte.ts +125 -0
- package/src/createGauge.svelte.ts +86 -0
- package/src/createListbox.svelte.ts +186 -0
- package/src/createMaskedInput.svelte.ts +81 -0
- package/src/createNumberInput.svelte.ts +193 -0
- package/src/createPasswordInput.svelte.ts +87 -0
- package/src/createPhoneInput.svelte.ts +119 -0
- package/src/createRadioGroup.svelte.ts +132 -0
- package/src/createRating.svelte.ts +145 -0
- package/src/createSlider.svelte.ts +160 -0
- package/src/createSwitch.svelte.ts +78 -0
- package/src/createTabs.svelte.ts +129 -0
- package/src/createTagsInput.svelte.ts +98 -0
- package/src/createTimePicker.svelte.ts +289 -0
- package/src/createToggle.svelte.ts +69 -0
- package/src/createTree.svelte.ts +216 -0
- package/src/editor-contract.test.ts +32 -0
- package/src/editor-contract.ts +60 -0
- package/src/index.ts +39 -0
- package/src/ui-inputs.test.ts +26 -0
- package/src/ui-selection.test.ts +13 -0
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
* SvAutoComplete - a free-text input with a live-filtered suggestion list.
|
|
4
4
|
* Unlike SvComboBox, it accepts ANY value (the text), suggestions are just
|
|
5
5
|
* shortcuts. Parity: Smart `smart-input` (autocomplete). Emits the text string.
|
|
6
|
+
*
|
|
7
|
+
* One styled renderer over the headless `createAutocomplete` core; only
|
|
8
|
+
* portal/measure render concerns live here.
|
|
6
9
|
*/
|
|
7
10
|
import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
|
|
8
|
-
import {
|
|
11
|
+
import { type ListOption } from './list-option'
|
|
12
|
+
import { createAutocomplete } from './createAutocomplete.svelte'
|
|
9
13
|
|
|
10
14
|
type Props = {
|
|
11
15
|
value?: string
|
|
@@ -22,38 +26,31 @@
|
|
|
22
26
|
|
|
23
27
|
let { value = '', onChange, suggestions = [], minChars = 1, placeholder, disabled = false, name, size = 'md', ariaLabel }: Props = $props()
|
|
24
28
|
|
|
25
|
-
const asOptions = $derived<ListOption[]>(
|
|
26
|
-
suggestions.map((s) => (typeof s === 'string' ? { value: s, label: s } : s)),
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
let open = $state(false)
|
|
30
|
-
let active = $state(0)
|
|
31
29
|
let fieldEl = $state<HTMLInputElement | null>(null)
|
|
32
30
|
let panelEl = $state<HTMLDivElement | null>(null)
|
|
33
31
|
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
|
|
34
32
|
|
|
35
|
-
const
|
|
33
|
+
const ac = createAutocomplete({
|
|
34
|
+
value: () => value,
|
|
35
|
+
onChange: (v) => onChange?.(v),
|
|
36
|
+
suggestions: () => suggestions,
|
|
37
|
+
minChars: () => minChars,
|
|
38
|
+
disabled: () => disabled,
|
|
39
|
+
ariaLabel: () => ariaLabel,
|
|
40
|
+
focusInput: () => fieldEl?.focus(),
|
|
41
|
+
})
|
|
36
42
|
|
|
37
43
|
function updatePos() {
|
|
38
44
|
if (!fieldEl) return
|
|
39
|
-
rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(filtered.length, 8) * 34 + 8 })
|
|
40
|
-
}
|
|
41
|
-
function maybeOpen() { open = filtered.length > 0; if (open) { active = 0; updatePos() } }
|
|
42
|
-
function pick(o: ListOption | undefined) { if (!o) return; onChange?.(String(o.value)); open = false; fieldEl?.focus() }
|
|
43
|
-
function onInput(e: Event) { onChange?.((e.currentTarget as HTMLInputElement).value); queueMicrotask(maybeOpen) }
|
|
44
|
-
function onKeydown(e: KeyboardEvent) {
|
|
45
|
-
if (!open) { if (e.key === 'ArrowDown') { queueMicrotask(maybeOpen) } return }
|
|
46
|
-
if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, filtered.length - 1) }
|
|
47
|
-
else if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(active - 1, 0) }
|
|
48
|
-
else if (e.key === 'Enter') { e.preventDefault(); pick(filtered[active]) }
|
|
49
|
-
else if (e.key === 'Escape') { open = false }
|
|
45
|
+
rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(ac.filtered.length, 8) * 34 + 8 })
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
$effect(() => {
|
|
53
|
-
if (!open) return
|
|
49
|
+
if (!ac.open) return
|
|
50
|
+
updatePos()
|
|
54
51
|
const rp = () => updatePos()
|
|
55
52
|
window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
|
|
56
|
-
const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (fieldEl?.contains(t) || panelEl?.contains(t))) return;
|
|
53
|
+
const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (fieldEl?.contains(t) || panelEl?.contains(t))) return; ac.close() }
|
|
57
54
|
document.addEventListener('pointerdown', od, true)
|
|
58
55
|
return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); document.removeEventListener('pointerdown', od, true) }
|
|
59
56
|
})
|
|
@@ -63,23 +60,15 @@
|
|
|
63
60
|
bind:this={fieldEl}
|
|
64
61
|
class="sv-ac sv-ac--{size}"
|
|
65
62
|
type="text"
|
|
66
|
-
role="combobox"
|
|
67
|
-
aria-expanded={open}
|
|
68
|
-
aria-autocomplete="list"
|
|
69
|
-
value={value}
|
|
70
63
|
{placeholder}
|
|
71
|
-
{
|
|
72
|
-
aria-label={ariaLabel}
|
|
73
|
-
oninput={onInput}
|
|
74
|
-
onfocus={maybeOpen}
|
|
75
|
-
onkeydown={onKeydown}
|
|
64
|
+
{...ac.inputProps()}
|
|
76
65
|
/>
|
|
77
66
|
|
|
78
|
-
{#if open}
|
|
79
|
-
<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`}
|
|
80
|
-
{#each filtered as opt, i (opt.value)}
|
|
67
|
+
{#if ac.open}
|
|
68
|
+
<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`} {...ac.listboxProps()}>
|
|
69
|
+
{#each ac.filtered as opt, i (opt.value)}
|
|
81
70
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
|
|
82
|
-
<div class="sv-ddl__opt" class:is-active={i
|
|
71
|
+
<div class="sv-ddl__opt" class:is-active={ac.isActive(i)} {...ac.optionProps(i)}>{opt.label}</div>
|
|
83
72
|
{/each}
|
|
84
73
|
</div>
|
|
85
74
|
{/if}
|
package/dist/SvCalendar.svelte
CHANGED
|
@@ -1,54 +1,32 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* (or an [start, end] range for range mode); pass a function for "today"-
|
|
6
|
-
* relative shortcuts that resolve at click time. */
|
|
7
|
-
export type CalendarPreset = {
|
|
8
|
-
label: string
|
|
9
|
-
value:
|
|
10
|
-
| Date | number | string
|
|
11
|
-
| readonly [Date | number | string, Date | number | string]
|
|
12
|
-
| (() => Date | number | string | readonly [Date | number | string, Date | number | string])
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** How the calendar animates a navigation / drill. */
|
|
16
|
-
export type CalendarAnimation = 'slide' | 'fade' | 'none'
|
|
2
|
+
// Public types now live with the headless core and are re-exported here so the
|
|
3
|
+
// component's module entry point (and @svgrid/grid) keeps the same surface.
|
|
4
|
+
export type { CalendarValue, CalendarPreset, CalendarAnimation, DisplayMode } from './createCalendar.svelte'
|
|
17
5
|
</script>
|
|
18
6
|
|
|
19
7
|
<script lang="ts">
|
|
20
8
|
/**
|
|
21
|
-
* SvCalendar - a themeable, accessible month/year/decade calendar
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
9
|
+
* SvCalendar - a themeable, accessible month/year/decade calendar. It is a thin
|
|
10
|
+
* styled renderer over the headless `createCalendar` core (the same split as
|
|
11
|
+
* `createSvGrid` / <SvGrid>): the core owns the reactive state + interaction +
|
|
12
|
+
* ARIA (built on the framework-free ./datetime engine), while this component
|
|
13
|
+
* keeps the render-only concerns - the WAAPI view-change animation, mouse-wheel
|
|
14
|
+
* navigation, and DOM refs. All visuals come from the grid's `--sg-*` tokens.
|
|
25
15
|
*
|
|
26
16
|
* Parity target: Smart `smart-calendar` (selectionModes, min/max, restricted /
|
|
27
17
|
* important dates, week numbers, firstDayOfWeek, drill navigation, keyboard).
|
|
28
18
|
*/
|
|
29
|
-
import {
|
|
30
|
-
addDays,
|
|
31
|
-
addMonths,
|
|
32
|
-
addYears,
|
|
33
|
-
monthMatrix,
|
|
34
|
-
weekdayOrder,
|
|
35
|
-
startOfDay,
|
|
36
|
-
isSameDay,
|
|
37
|
-
isSameMonth,
|
|
38
|
-
toDate,
|
|
39
|
-
decadeRange,
|
|
40
|
-
type DateLike,
|
|
41
|
-
} from './datetime/date-core'
|
|
42
|
-
import {
|
|
43
|
-
selectDate,
|
|
44
|
-
isSelected,
|
|
45
|
-
rangeDays,
|
|
46
|
-
emptySelection,
|
|
47
|
-
type SelectionMode,
|
|
48
|
-
type SelectionState,
|
|
49
|
-
} from './datetime/date-selection'
|
|
50
|
-
import { isDisabledDay, isImportant, type RestrictOptions } from './datetime/date-restrict'
|
|
51
19
|
import { untrack } from 'svelte'
|
|
20
|
+
import {
|
|
21
|
+
createCalendar,
|
|
22
|
+
type CalendarValue,
|
|
23
|
+
type CalendarPreset,
|
|
24
|
+
type CalendarAnimation,
|
|
25
|
+
type DisplayMode,
|
|
26
|
+
} from './createCalendar.svelte'
|
|
27
|
+
import type { SelectionMode } from './datetime/date-selection'
|
|
28
|
+
import type { RestrictOptions } from './datetime/date-restrict'
|
|
29
|
+
import type { DateLike } from './datetime/date-core'
|
|
52
30
|
|
|
53
31
|
type NameFormat = 'narrow' | 'short' | 'long'
|
|
54
32
|
|
|
@@ -96,8 +74,6 @@
|
|
|
96
74
|
presets?: ReadonlyArray<CalendarPreset>
|
|
97
75
|
}
|
|
98
76
|
|
|
99
|
-
type DisplayMode = 'month' | 'year' | 'decade'
|
|
100
|
-
|
|
101
77
|
let {
|
|
102
78
|
value = null,
|
|
103
79
|
onChange,
|
|
@@ -127,14 +103,37 @@
|
|
|
127
103
|
presets,
|
|
128
104
|
}: Props = $props()
|
|
129
105
|
|
|
106
|
+
// The headless core owns all reactive state, selection, navigation, keyboard
|
|
107
|
+
// and ARIA. Reactive inputs are passed as getters; callbacks as closures.
|
|
108
|
+
const cal = createCalendar({
|
|
109
|
+
value: () => value,
|
|
110
|
+
onChange: (d) => onChange?.(d),
|
|
111
|
+
onNavigate: (v, m) => onNavigate?.(v, m),
|
|
112
|
+
selectionMode: () => selectionMode,
|
|
113
|
+
min: () => min,
|
|
114
|
+
max: () => max,
|
|
115
|
+
restrictedDates: () => restrictedDates,
|
|
116
|
+
importantDates: () => importantDates,
|
|
117
|
+
firstDayOfWeek: () => firstDayOfWeek,
|
|
118
|
+
weeks: () => weeks,
|
|
119
|
+
months: () => months,
|
|
120
|
+
displayMode: () => displayMode,
|
|
121
|
+
disabled: () => disabled,
|
|
122
|
+
readonly: () => readonly,
|
|
123
|
+
locale: () => locale,
|
|
124
|
+
dayNameFormat: () => dayNameFormat,
|
|
125
|
+
monthNameFormat: () => monthNameFormat,
|
|
126
|
+
dateTooltip: () => dateTooltip,
|
|
127
|
+
})
|
|
128
|
+
|
|
130
129
|
// --- View-change animation (framework-native Web Animations API) -----------
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
130
|
+
// A render concern kept out of the core: we animate a wrapper element instead
|
|
131
|
+
// of re-keying the grid, so day cells stay mounted (keyboard focus +
|
|
132
|
+
// interactivity are preserved). The core exposes `navToken` (bumps on each
|
|
133
|
+
// navigation) + `navDir` (why it changed); an effect replays a short keyframe
|
|
134
|
+
// on the new content.
|
|
134
135
|
const animKind = $derived<CalendarAnimation>(animate === true ? 'slide' : animate || 'none')
|
|
135
136
|
let animEl = $state<HTMLElement | undefined>()
|
|
136
|
-
let animToken = $state(0)
|
|
137
|
-
let navDir: 'next' | 'prev' | 'drillDown' | 'drillUp' | 'fade' = 'fade'
|
|
138
137
|
let animPrimed = false // skip the very first (mount) run
|
|
139
138
|
// Note: `animate` is an explicit opt-in, so we honor it even under the OS
|
|
140
139
|
// "reduce motion" setting. Pass animate="none" (or gate it yourself on
|
|
@@ -146,6 +145,7 @@
|
|
|
146
145
|
// pronounced to read as a transition. Slide the new content a clear distance +
|
|
147
146
|
// fade; drills zoom. A mid-keyframe holds the offset briefly for legibility.
|
|
148
147
|
const D = 34
|
|
148
|
+
const navDir = cal.navDir
|
|
149
149
|
const from =
|
|
150
150
|
!slide ? { opacity: 0, transform: 'scale(0.98)' }
|
|
151
151
|
: navDir === 'next' ? { transform: `translateX(${D}px)`, opacity: 0 }
|
|
@@ -159,260 +159,21 @@
|
|
|
159
159
|
})
|
|
160
160
|
}
|
|
161
161
|
$effect(() => {
|
|
162
|
-
|
|
162
|
+
cal.navToken // re-run after each navigation's DOM update
|
|
163
163
|
if (!animPrimed) { animPrimed = true; return }
|
|
164
164
|
untrack(playAnim)
|
|
165
165
|
})
|
|
166
166
|
|
|
167
|
-
const restrict = $derived<RestrictOptions>({ min, max, restrictedDates })
|
|
168
|
-
|
|
169
|
-
/** Normalize the incoming value to an ordered day list. */
|
|
170
|
-
function normalizeValue(v: CalendarValue): Date[] {
|
|
171
|
-
if (v == null) return []
|
|
172
|
-
const list = Array.isArray(v) ? v : [v]
|
|
173
|
-
const out: Date[] = []
|
|
174
|
-
for (const item of list) {
|
|
175
|
-
const d = toDate(item as DateLike)
|
|
176
|
-
if (d) out.push(startOfDay(d))
|
|
177
|
-
}
|
|
178
|
-
return out
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Internal selection state. Seeded from `value`; range mode also tracks a
|
|
182
|
-
// pending rangeStart between the two clicks, which `value` alone cannot hold.
|
|
183
|
-
let sel = $state<SelectionState>(emptySelection())
|
|
184
|
-
// The month currently shown (first panel) + the drill level.
|
|
185
|
-
let view = $state<Date>(startOfDay(new Date()))
|
|
186
|
-
let mode = $state<DisplayMode>('month')
|
|
187
|
-
// Roving focus + range hover preview.
|
|
188
|
-
let focusDate = $state<Date>(startOfDay(new Date()))
|
|
189
|
-
let hoverDate = $state<Date | null>(null)
|
|
190
|
-
|
|
191
|
-
let lastSyncedKey = '' // sentinel so the first sync always fires (incl. null value)
|
|
192
|
-
let seeded = false
|
|
193
|
-
$effect(() => {
|
|
194
|
-
const dates = normalizeValue(value)
|
|
195
|
-
const key = dates.map((d) => d.getTime()).join(',')
|
|
196
|
-
if (key !== lastSyncedKey) {
|
|
197
|
-
lastSyncedKey = key
|
|
198
|
-
sel = { dates, anchor: dates[dates.length - 1] ?? null, rangeStart: null }
|
|
199
|
-
// Seed the visible page + roving focus once, from the initial value.
|
|
200
|
-
if (!seeded) {
|
|
201
|
-
seeded = true
|
|
202
|
-
const first = dates[0] ?? startOfDay(new Date())
|
|
203
|
-
view = startOfDay(first)
|
|
204
|
-
focusDate = startOfDay(first)
|
|
205
|
-
mode = displayMode
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
const isInteractive = $derived(!disabled && !readonly)
|
|
211
|
-
|
|
212
|
-
// --- Intl-driven labels (cached by the browser) ---
|
|
213
|
-
const weekdayFmt = $derived(new Intl.DateTimeFormat(locale, { weekday: dayNameFormat }))
|
|
214
|
-
const monthTitleFmt = $derived(new Intl.DateTimeFormat(locale, { month: monthNameFormat, year: 'numeric' }))
|
|
215
|
-
const monthShortFmt = $derived(new Intl.DateTimeFormat(locale, { month: 'short' }))
|
|
216
|
-
const fullDateFmt = $derived(new Intl.DateTimeFormat(locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }))
|
|
217
|
-
|
|
218
|
-
const weekdayHeaders = $derived(
|
|
219
|
-
weekdayOrder(firstDayOfWeek).map((wd) => {
|
|
220
|
-
// 2021-08-01 is a Sunday; offset to the requested weekday.
|
|
221
|
-
const d = new Date(2021, 7, 1 + wd)
|
|
222
|
-
return { label: weekdayFmt.format(d), weekday: wd }
|
|
223
|
-
}),
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
/** The month grids to render (1..months panels). */
|
|
227
|
-
const panels = $derived(
|
|
228
|
-
Array.from({ length: Math.max(1, months) }, (_, i) => {
|
|
229
|
-
const panelMonth = addMonths(view, i)
|
|
230
|
-
return { month: panelMonth, matrix: monthMatrix(panelMonth, firstDayOfWeek, weeks) }
|
|
231
|
-
}),
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
// Range preview: while a range is pending (or being hovered), show the band.
|
|
235
|
-
const previewRange = $derived.by(() => {
|
|
236
|
-
if (selectionMode !== 'range') return null
|
|
237
|
-
const start = sel.rangeStart
|
|
238
|
-
if (!start || !hoverDate) return null
|
|
239
|
-
return rangeDays(start, hoverDate)
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
function inPreview(d: Date): boolean {
|
|
243
|
-
return !!previewRange && previewRange.some((p) => isSameDay(p, d))
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function dayState(d: Date, panelMonth: Date) {
|
|
247
|
-
const disabledDay = isDisabledDay(d, restrict)
|
|
248
|
-
return {
|
|
249
|
-
disabled: disabledDay,
|
|
250
|
-
selected: isSelected(sel.dates, d),
|
|
251
|
-
important: isImportant(d, importantDates),
|
|
252
|
-
today: isSameDay(d, new Date()),
|
|
253
|
-
outside: !isSameMonth(d, panelMonth),
|
|
254
|
-
focused: isSameDay(d, focusDate),
|
|
255
|
-
preview: inPreview(d),
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// --- Actions ---
|
|
260
|
-
function commit(next: SelectionState) {
|
|
261
|
-
sel = next
|
|
262
|
-
lastSyncedKey = next.dates.map((x) => x.getTime()).join(',')
|
|
263
|
-
onChange?.(next.dates.map((x) => new Date(x.getTime())))
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function pickDay(d: Date, e?: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean }) {
|
|
267
|
-
if (!isInteractive || isDisabledDay(d, restrict)) return
|
|
268
|
-
focusDate = startOfDay(d)
|
|
269
|
-
commit(
|
|
270
|
-
selectDate(sel, d, selectionMode, { ctrl: e?.ctrlKey || e?.metaKey, shift: e?.shiftKey }, firstDayOfWeek),
|
|
271
|
-
)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function setView(next: Date, nextMode: DisplayMode = mode) {
|
|
275
|
-
view = startOfDay(next)
|
|
276
|
-
mode = nextMode
|
|
277
|
-
animToken++ // fire the view-change animation on the updated DOM
|
|
278
|
-
onNavigate?.(view, mode)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function drillUp() {
|
|
282
|
-
navDir = 'drillUp'
|
|
283
|
-
if (mode === 'month') setView(view, 'year')
|
|
284
|
-
else if (mode === 'year') setView(view, 'decade')
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function step(dir: -1 | 1) {
|
|
288
|
-
navDir = dir === 1 ? 'next' : 'prev'
|
|
289
|
-
if (mode === 'month') setView(addMonths(view, dir))
|
|
290
|
-
else if (mode === 'year') setView(addYears(view, dir))
|
|
291
|
-
else setView(addYears(view, dir * 10))
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function pickMonth(monthIndex: number) {
|
|
295
|
-
navDir = 'drillDown'
|
|
296
|
-
setView(new Date(view.getFullYear(), monthIndex, 1), 'month')
|
|
297
|
-
focusDate = new Date(view.getFullYear(), monthIndex, Math.min(focusDate.getDate(), 28))
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function pickYear(year: number) {
|
|
301
|
-
navDir = 'drillDown'
|
|
302
|
-
setView(new Date(year, view.getMonth(), 1), 'year')
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function goToday() {
|
|
306
|
-
navDir = 'fade'
|
|
307
|
-
const t = new Date()
|
|
308
|
-
setView(t, 'month')
|
|
309
|
-
focusDate = startOfDay(t)
|
|
310
|
-
if (isInteractive) pickDay(t)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
167
|
// Mouse-wheel navigation (opt-in). Non-passive so we can prevent page scroll.
|
|
314
168
|
function wheelNav(node: HTMLElement) {
|
|
315
169
|
const onWheel = (e: WheelEvent) => {
|
|
316
170
|
if (!wheelNavigation || disabled) return
|
|
317
171
|
e.preventDefault()
|
|
318
|
-
step(e.deltaY > 0 || e.deltaX > 0 ? 1 : -1)
|
|
172
|
+
cal.step(e.deltaY > 0 || e.deltaX > 0 ? 1 : -1)
|
|
319
173
|
}
|
|
320
174
|
node.addEventListener('wheel', onWheel, { passive: false })
|
|
321
175
|
return { destroy: () => node.removeEventListener('wheel', onWheel) }
|
|
322
176
|
}
|
|
323
|
-
|
|
324
|
-
// --- Presets (Today / Last 7 days / ...) -----------------------------------
|
|
325
|
-
function applyPreset(p: CalendarPreset) {
|
|
326
|
-
if (!isInteractive) return
|
|
327
|
-
const resolved = typeof p.value === 'function' ? p.value() : p.value
|
|
328
|
-
const isRange = Array.isArray(resolved)
|
|
329
|
-
const pair = (isRange ? resolved : [resolved, resolved]) as ReadonlyArray<DateLike>
|
|
330
|
-
const start = toDate(pair[0] as DateLike)
|
|
331
|
-
const end = toDate(pair[1] as DateLike)
|
|
332
|
-
if (!start) return
|
|
333
|
-
focusDate = startOfDay(start)
|
|
334
|
-
navDir = 'fade'
|
|
335
|
-
view = startOfMonthSafe(start)
|
|
336
|
-
animToken++
|
|
337
|
-
if (isRange && end) {
|
|
338
|
-
commit({ dates: rangeDays(start, end), anchor: startOfDay(end), rangeStart: null })
|
|
339
|
-
} else {
|
|
340
|
-
commit({ dates: [startOfDay(start)], anchor: startOfDay(start), rangeStart: null })
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
const startOfMonthSafe = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1)
|
|
344
|
-
|
|
345
|
-
function clearSelection() {
|
|
346
|
-
if (!isInteractive) return
|
|
347
|
-
commit(emptySelection())
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// --- Keyboard (month grid) ---
|
|
351
|
-
function onGridKeydown(e: KeyboardEvent) {
|
|
352
|
-
if (mode !== 'month') return onNonMonthKeydown(e)
|
|
353
|
-
let next: Date | null = null
|
|
354
|
-
switch (e.key) {
|
|
355
|
-
case 'ArrowLeft': next = addDays(focusDate, -1); break
|
|
356
|
-
case 'ArrowRight': next = addDays(focusDate, 1); break
|
|
357
|
-
case 'ArrowUp': next = addDays(focusDate, -7); break
|
|
358
|
-
case 'ArrowDown': next = addDays(focusDate, 7); break
|
|
359
|
-
case 'Home': next = addDays(focusDate, -((focusDate.getDay() - firstDayOfWeek + 7) % 7)); break
|
|
360
|
-
case 'End': next = addDays(focusDate, 6 - ((focusDate.getDay() - firstDayOfWeek + 7) % 7)); break
|
|
361
|
-
case 'PageUp': next = e.shiftKey ? addYears(focusDate, -1) : addMonths(focusDate, -1); break
|
|
362
|
-
case 'PageDown': next = e.shiftKey ? addYears(focusDate, 1) : addMonths(focusDate, 1); break
|
|
363
|
-
case 'Enter':
|
|
364
|
-
case ' ':
|
|
365
|
-
e.preventDefault()
|
|
366
|
-
pickDay(focusDate, e)
|
|
367
|
-
return
|
|
368
|
-
default:
|
|
369
|
-
return
|
|
370
|
-
}
|
|
371
|
-
if (next) {
|
|
372
|
-
e.preventDefault()
|
|
373
|
-
focusDate = startOfDay(next)
|
|
374
|
-
if (!isSameMonth(focusDate, view)) setView(focusDate, 'month')
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function onNonMonthKeydown(e: KeyboardEvent) {
|
|
379
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
380
|
-
e.preventDefault()
|
|
381
|
-
if (mode === 'year') pickMonth(focusDate.getMonth())
|
|
382
|
-
else pickYear(focusDate.getFullYear())
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const title = $derived(
|
|
387
|
-
mode === 'month'
|
|
388
|
-
? monthTitleFmt.format(view)
|
|
389
|
-
: mode === 'year'
|
|
390
|
-
? String(view.getFullYear())
|
|
391
|
-
: (() => {
|
|
392
|
-
const { start, end } = decadeRange(view.getFullYear())
|
|
393
|
-
return `${start} - ${end}`
|
|
394
|
-
})(),
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
// Year-mode cells (12 months) and decade-mode cells (10 years + 2 padding).
|
|
398
|
-
const yearCells = $derived(
|
|
399
|
-
Array.from({ length: 12 }, (_, m) => ({
|
|
400
|
-
index: m,
|
|
401
|
-
label: monthShortFmt.format(new Date(view.getFullYear(), m, 1)),
|
|
402
|
-
current: m === new Date().getMonth() && view.getFullYear() === new Date().getFullYear(),
|
|
403
|
-
})),
|
|
404
|
-
)
|
|
405
|
-
const decadeCells = $derived.by(() => {
|
|
406
|
-
const { start } = decadeRange(view.getFullYear())
|
|
407
|
-
return Array.from({ length: 12 }, (_, i) => {
|
|
408
|
-
const year = start - 1 + i
|
|
409
|
-
return { year, outside: i === 0 || i === 11, current: year === new Date().getFullYear() }
|
|
410
|
-
})
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
const canClear = $derived(
|
|
414
|
-
selectionMode === 'zeroOrOne' || selectionMode === 'zeroOrMany' || selectionMode === 'none',
|
|
415
|
-
)
|
|
416
177
|
</script>
|
|
417
178
|
|
|
418
179
|
<div
|
|
@@ -427,38 +188,32 @@
|
|
|
427
188
|
{#if presets && presets.length}
|
|
428
189
|
<div class="sv-cal__presets" role="group" aria-label="Shortcuts">
|
|
429
190
|
{#each presets as p, i (i)}
|
|
430
|
-
<button
|
|
191
|
+
<button class="sv-cal__preset" {...cal.presetProps(p)}>{p.label}</button>
|
|
431
192
|
{/each}
|
|
432
193
|
</div>
|
|
433
194
|
{/if}
|
|
434
195
|
|
|
435
196
|
<div class="sv-cal__main" use:wheelNav>
|
|
436
197
|
<div class="sv-cal__header">
|
|
437
|
-
<button
|
|
198
|
+
<button class="sv-cal__nav" {...cal.navProps(-1)}>
|
|
438
199
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6" /></svg>
|
|
439
200
|
</button>
|
|
440
|
-
<button
|
|
441
|
-
|
|
442
|
-
class="sv-cal__title"
|
|
443
|
-
onclick={drillUp}
|
|
444
|
-
disabled={disabled || mode === 'decade'}
|
|
445
|
-
aria-live="polite"
|
|
446
|
-
>{title}</button>
|
|
447
|
-
<button type="button" class="sv-cal__nav" onclick={() => step(1)} disabled={disabled} aria-label="Next">
|
|
201
|
+
<button class="sv-cal__title" {...cal.titleProps()}>{cal.title}</button>
|
|
202
|
+
<button class="sv-cal__nav" {...cal.navProps(1)}>
|
|
448
203
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6" /></svg>
|
|
449
204
|
</button>
|
|
450
205
|
</div>
|
|
451
206
|
|
|
452
207
|
<div class="sv-cal__view" bind:this={animEl}>
|
|
453
|
-
{#if mode === 'month'}
|
|
454
|
-
<div class="sv-cal__panels"
|
|
455
|
-
{#each panels as panel (panel.month.getTime())}
|
|
208
|
+
{#if cal.mode === 'month'}
|
|
209
|
+
<div class="sv-cal__panels" {...cal.gridProps()}>
|
|
210
|
+
{#each cal.panels as panel (panel.month.getTime())}
|
|
456
211
|
<div class="sv-cal__panel">
|
|
457
|
-
{#if months > 1}<div class="sv-cal__panel-title">{
|
|
212
|
+
{#if months > 1}<div class="sv-cal__panel-title">{panel.title}</div>{/if}
|
|
458
213
|
{#if !hideDayNames}
|
|
459
214
|
<div class="sv-cal__weekdays" role="row">
|
|
460
215
|
{#if weekNumbers}<span class="sv-cal__wk-head" aria-hidden="true">#</span>{/if}
|
|
461
|
-
{#each weekdayHeaders as wh (wh.weekday)}
|
|
216
|
+
{#each cal.weekdayHeaders as wh (wh.weekday)}
|
|
462
217
|
<span class="sv-cal__weekday" role="columnheader">{wh.label}</span>
|
|
463
218
|
{/each}
|
|
464
219
|
</div>
|
|
@@ -467,13 +222,11 @@
|
|
|
467
222
|
<div class="sv-cal__week" role="row">
|
|
468
223
|
{#if weekNumbers}<span class="sv-cal__wk" aria-hidden="true">{week[0]?.week}</span>{/if}
|
|
469
224
|
{#each week as cell (cell.date.getTime())}
|
|
470
|
-
{@const st = dayState(cell.date, panel.month)}
|
|
225
|
+
{@const st = cal.dayState(cell.date, panel.month)}
|
|
471
226
|
{#if st.outside && hideOtherMonthDays}
|
|
472
227
|
<span class="sv-cal__day sv-cal__day--empty" role="gridcell" aria-hidden="true"></span>
|
|
473
228
|
{:else}
|
|
474
229
|
<button
|
|
475
|
-
type="button"
|
|
476
|
-
role="gridcell"
|
|
477
230
|
class="sv-cal__day"
|
|
478
231
|
class:is-selected={st.selected}
|
|
479
232
|
class:is-today={st.today}
|
|
@@ -481,16 +234,7 @@
|
|
|
481
234
|
class:is-important={st.important}
|
|
482
235
|
class:is-preview={st.preview}
|
|
483
236
|
class:is-focused={st.focused}
|
|
484
|
-
|
|
485
|
-
aria-disabled={st.disabled}
|
|
486
|
-
aria-current={st.today ? 'date' : undefined}
|
|
487
|
-
aria-label={fullDateFmt.format(cell.date)}
|
|
488
|
-
title={dateTooltip?.(cell.date) ?? undefined}
|
|
489
|
-
tabindex={st.focused ? 0 : -1}
|
|
490
|
-
disabled={st.disabled || disabled}
|
|
491
|
-
onclick={(e) => pickDay(cell.date, e)}
|
|
492
|
-
onpointerenter={() => (hoverDate = cell.date)}
|
|
493
|
-
onpointerleave={() => (hoverDate = null)}
|
|
237
|
+
{...cal.dayProps(cell, panel.month)}
|
|
494
238
|
>{cell.date.getDate()}</button>
|
|
495
239
|
{/if}
|
|
496
240
|
{/each}
|
|
@@ -499,30 +243,26 @@
|
|
|
499
243
|
</div>
|
|
500
244
|
{/each}
|
|
501
245
|
</div>
|
|
502
|
-
{:else if mode === 'year'}
|
|
503
|
-
<div class="sv-cal__grid-mode"
|
|
504
|
-
{#each yearCells as yc (yc.index)}
|
|
246
|
+
{:else if cal.mode === 'year'}
|
|
247
|
+
<div class="sv-cal__grid-mode" {...cal.gridProps()}>
|
|
248
|
+
{#each cal.yearCells as yc (yc.index)}
|
|
505
249
|
<button
|
|
506
|
-
type="button"
|
|
507
250
|
class="sv-cal__cell"
|
|
508
251
|
class:is-today={yc.current}
|
|
509
|
-
class:is-selected={
|
|
510
|
-
|
|
511
|
-
disabled={disabled}
|
|
252
|
+
class:is-selected={yc.selected}
|
|
253
|
+
{...cal.monthCellProps(yc.index)}
|
|
512
254
|
>{yc.label}</button>
|
|
513
255
|
{/each}
|
|
514
256
|
</div>
|
|
515
257
|
{:else}
|
|
516
|
-
<div class="sv-cal__grid-mode"
|
|
517
|
-
{#each decadeCells as dc (dc.year)}
|
|
258
|
+
<div class="sv-cal__grid-mode" {...cal.gridProps()}>
|
|
259
|
+
{#each cal.decadeCells as dc (dc.year)}
|
|
518
260
|
<button
|
|
519
|
-
type="button"
|
|
520
261
|
class="sv-cal__cell"
|
|
521
262
|
class:is-outside={dc.outside}
|
|
522
263
|
class:is-today={dc.current}
|
|
523
|
-
class:is-selected={
|
|
524
|
-
|
|
525
|
-
disabled={disabled}
|
|
264
|
+
class:is-selected={dc.selected}
|
|
265
|
+
{...cal.yearCellProps(dc.year)}
|
|
526
266
|
>{dc.year}</button>
|
|
527
267
|
{/each}
|
|
528
268
|
</div>
|
|
@@ -531,15 +271,15 @@
|
|
|
531
271
|
|
|
532
272
|
{#if footer}
|
|
533
273
|
<div class="sv-cal__footer">
|
|
534
|
-
<button type="button" class="sv-cal__foot-btn" onclick={goToday} disabled={disabled}>Today</button>
|
|
535
|
-
{#if canClear}
|
|
536
|
-
<button type="button" class="sv-cal__foot-btn" onclick={clearSelection} disabled={disabled}>Clear</button>
|
|
274
|
+
<button type="button" class="sv-cal__foot-btn" onclick={cal.goToday} disabled={disabled}>Today</button>
|
|
275
|
+
{#if cal.canClear}
|
|
276
|
+
<button type="button" class="sv-cal__foot-btn" onclick={cal.clearSelection} disabled={disabled}>Clear</button>
|
|
537
277
|
{/if}
|
|
538
278
|
</div>
|
|
539
279
|
{/if}
|
|
540
280
|
</div>
|
|
541
281
|
|
|
542
|
-
{#if name}<input type="hidden" {name} value={
|
|
282
|
+
{#if name}<input type="hidden" {name} value={cal.selectedDates.map((d) => d.toISOString()).join(',')} />{/if}
|
|
543
283
|
</div>
|
|
544
284
|
|
|
545
285
|
<style>
|
|
@@ -1,27 +1,9 @@
|
|
|
1
|
-
export type CalendarValue
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
label: string;
|
|
7
|
-
value: Date | number | string | readonly [Date | number | string, Date | number | string] | (() => Date | number | string | readonly [Date | number | string, Date | number | string]);
|
|
8
|
-
};
|
|
9
|
-
/** How the calendar animates a navigation / drill. */
|
|
10
|
-
export type CalendarAnimation = 'slide' | 'fade' | 'none';
|
|
11
|
-
/**
|
|
12
|
-
* SvCalendar - a themeable, accessible month/year/decade calendar built on the
|
|
13
|
-
* framework-free date engine in ./datetime. It works standalone AND is the
|
|
14
|
-
* date cell editor for SvGrid (rich-by-default). All visuals come from the
|
|
15
|
-
* grid's `--sg-*` theme tokens, so every preset theme styles it for free.
|
|
16
|
-
*
|
|
17
|
-
* Parity target: Smart `smart-calendar` (selectionModes, min/max, restricted /
|
|
18
|
-
* important dates, week numbers, firstDayOfWeek, drill navigation, keyboard).
|
|
19
|
-
*/
|
|
20
|
-
import { type DateLike } from './datetime/date-core';
|
|
21
|
-
import { type SelectionMode } from './datetime/date-selection';
|
|
22
|
-
import { type RestrictOptions } from './datetime/date-restrict';
|
|
1
|
+
export type { CalendarValue, CalendarPreset, CalendarAnimation, DisplayMode } from './createCalendar.svelte';
|
|
2
|
+
import { type CalendarValue, type CalendarPreset, type CalendarAnimation, type DisplayMode } from './createCalendar.svelte';
|
|
3
|
+
import type { SelectionMode } from './datetime/date-selection';
|
|
4
|
+
import type { RestrictOptions } from './datetime/date-restrict';
|
|
5
|
+
import type { DateLike } from './datetime/date-core';
|
|
23
6
|
type NameFormat = 'narrow' | 'short' | 'long';
|
|
24
|
-
type DisplayMode = 'month' | 'year' | 'decade';
|
|
25
7
|
type Props = {
|
|
26
8
|
/** Selected value(s). Single Date for one/zeroOrOne, array for multi modes. */
|
|
27
9
|
value?: CalendarValue;
|