@svgrid/grid 1.2.21 → 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,669 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type CalendarValue = Date | number | string | ReadonlyArray<Date | number | string> | null
|
|
3
|
+
|
|
4
|
+
/** A one-click shortcut shown in the presets rail. `value` is a single date
|
|
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'
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script lang="ts">
|
|
20
|
+
/**
|
|
21
|
+
* SvCalendar - a themeable, accessible month/year/decade calendar built on the
|
|
22
|
+
* framework-free date engine in ./datetime. It works standalone AND is the
|
|
23
|
+
* date cell editor for SvGrid (rich-by-default). All visuals come from the
|
|
24
|
+
* grid's `--sg-*` theme tokens, so every preset theme styles it for free.
|
|
25
|
+
*
|
|
26
|
+
* Parity target: Smart `smart-calendar` (selectionModes, min/max, restricted /
|
|
27
|
+
* important dates, week numbers, firstDayOfWeek, drill navigation, keyboard).
|
|
28
|
+
*/
|
|
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
|
+
import { untrack } from 'svelte'
|
|
52
|
+
|
|
53
|
+
type NameFormat = 'narrow' | 'short' | 'long'
|
|
54
|
+
|
|
55
|
+
type Props = {
|
|
56
|
+
/** Selected value(s). Single Date for one/zeroOrOne, array for multi modes. */
|
|
57
|
+
value?: CalendarValue
|
|
58
|
+
/** Fires with the full selected-day list on every change. */
|
|
59
|
+
onChange?: (dates: Date[]) => void
|
|
60
|
+
/** Fires when the visible month/year/decade page changes. */
|
|
61
|
+
onNavigate?: (viewDate: Date, displayMode: DisplayMode) => void
|
|
62
|
+
selectionMode?: SelectionMode
|
|
63
|
+
min?: DateLike | null
|
|
64
|
+
max?: DateLike | null
|
|
65
|
+
/** Non-selectable dates (list or predicate). */
|
|
66
|
+
restrictedDates?: RestrictOptions['restrictedDates']
|
|
67
|
+
/** Highlighted (but still selectable) dates. */
|
|
68
|
+
importantDates?: ReadonlyArray<DateLike> | ((d: Date) => boolean) | null
|
|
69
|
+
/** 0 = Sunday .. 6 = Saturday. */
|
|
70
|
+
firstDayOfWeek?: number
|
|
71
|
+
weeks?: number
|
|
72
|
+
weekNumbers?: boolean
|
|
73
|
+
/** Number of month panels side by side. */
|
|
74
|
+
months?: number
|
|
75
|
+
hideDayNames?: boolean
|
|
76
|
+
hideOtherMonthDays?: boolean
|
|
77
|
+
dayNameFormat?: NameFormat
|
|
78
|
+
monthNameFormat?: NameFormat
|
|
79
|
+
/** Show the Today / Clear footer. */
|
|
80
|
+
footer?: boolean
|
|
81
|
+
disabled?: boolean
|
|
82
|
+
readonly?: boolean
|
|
83
|
+
locale?: string
|
|
84
|
+
name?: string
|
|
85
|
+
/** Which drill level to open on. */
|
|
86
|
+
displayMode?: DisplayMode
|
|
87
|
+
/** Animate month/year navigation + drill (opt-in). `true` = 'slide'. This is
|
|
88
|
+
* an explicit opt-in, so it plays regardless of the OS "reduce motion"
|
|
89
|
+
* setting - pass 'none' (or gate it yourself) to suppress it. */
|
|
90
|
+
animate?: boolean | CalendarAnimation
|
|
91
|
+
/** Change the visible month/year/decade with the mouse wheel over the grid. */
|
|
92
|
+
wheelNavigation?: boolean
|
|
93
|
+
/** Per-day tooltip text (shown as the native title on the day button). */
|
|
94
|
+
dateTooltip?: (date: Date) => string | null | undefined
|
|
95
|
+
/** One-click shortcuts (e.g. Today, Last 7 days) shown in a side rail. */
|
|
96
|
+
presets?: ReadonlyArray<CalendarPreset>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type DisplayMode = 'month' | 'year' | 'decade'
|
|
100
|
+
|
|
101
|
+
let {
|
|
102
|
+
value = null,
|
|
103
|
+
onChange,
|
|
104
|
+
onNavigate,
|
|
105
|
+
selectionMode = 'one',
|
|
106
|
+
min = null,
|
|
107
|
+
max = null,
|
|
108
|
+
restrictedDates = null,
|
|
109
|
+
importantDates = null,
|
|
110
|
+
firstDayOfWeek = 0,
|
|
111
|
+
weeks = 6,
|
|
112
|
+
weekNumbers = false,
|
|
113
|
+
months = 1,
|
|
114
|
+
hideDayNames = false,
|
|
115
|
+
hideOtherMonthDays = false,
|
|
116
|
+
dayNameFormat = 'short',
|
|
117
|
+
monthNameFormat = 'long',
|
|
118
|
+
footer = false,
|
|
119
|
+
disabled = false,
|
|
120
|
+
readonly = false,
|
|
121
|
+
locale = typeof navigator !== 'undefined' ? navigator.language : 'en-US',
|
|
122
|
+
name,
|
|
123
|
+
displayMode = 'month',
|
|
124
|
+
animate = false,
|
|
125
|
+
wheelNavigation = false,
|
|
126
|
+
dateTooltip,
|
|
127
|
+
presets,
|
|
128
|
+
}: Props = $props()
|
|
129
|
+
|
|
130
|
+
// --- View-change animation (framework-native Web Animations API) -----------
|
|
131
|
+
// We animate a wrapper element instead of re-keying the grid, so day cells stay
|
|
132
|
+
// mounted (keyboard focus + interactivity are preserved). `navDir` records why
|
|
133
|
+
// the view changed; an effect replays a short keyframe on the new content.
|
|
134
|
+
const animKind = $derived<CalendarAnimation>(animate === true ? 'slide' : animate || 'none')
|
|
135
|
+
let animEl = $state<HTMLElement | undefined>()
|
|
136
|
+
let animToken = $state(0)
|
|
137
|
+
let navDir: 'next' | 'prev' | 'drillDown' | 'drillUp' | 'fade' = 'fade'
|
|
138
|
+
let animPrimed = false // skip the very first (mount) run
|
|
139
|
+
// Note: `animate` is an explicit opt-in, so we honor it even under the OS
|
|
140
|
+
// "reduce motion" setting. Pass animate="none" (or gate it yourself on
|
|
141
|
+
// prefers-reduced-motion) if you want it suppressed for that audience.
|
|
142
|
+
function playAnim() {
|
|
143
|
+
if (animKind === 'none' || !animEl || typeof animEl.animate !== 'function') return
|
|
144
|
+
const slide = animKind === 'slide'
|
|
145
|
+
// A month grid barely changes shape between months, so the movement has to be
|
|
146
|
+
// pronounced to read as a transition. Slide the new content a clear distance +
|
|
147
|
+
// fade; drills zoom. A mid-keyframe holds the offset briefly for legibility.
|
|
148
|
+
const D = 34
|
|
149
|
+
const from =
|
|
150
|
+
!slide ? { opacity: 0, transform: 'scale(0.98)' }
|
|
151
|
+
: navDir === 'next' ? { transform: `translateX(${D}px)`, opacity: 0 }
|
|
152
|
+
: navDir === 'prev' ? { transform: `translateX(-${D}px)`, opacity: 0 }
|
|
153
|
+
: navDir === 'drillDown' ? { transform: 'scale(0.85)', opacity: 0 }
|
|
154
|
+
: navDir === 'drillUp' ? { transform: 'scale(1.14)', opacity: 0 }
|
|
155
|
+
: { opacity: 0 }
|
|
156
|
+
animEl.animate([from, { transform: 'none', opacity: 1 }], {
|
|
157
|
+
duration: slide && (navDir === 'next' || navDir === 'prev') ? 300 : 220,
|
|
158
|
+
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
$effect(() => {
|
|
162
|
+
animToken // re-run after each navigation's DOM update
|
|
163
|
+
if (!animPrimed) { animPrimed = true; return }
|
|
164
|
+
untrack(playAnim)
|
|
165
|
+
})
|
|
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
|
+
// Mouse-wheel navigation (opt-in). Non-passive so we can prevent page scroll.
|
|
314
|
+
function wheelNav(node: HTMLElement) {
|
|
315
|
+
const onWheel = (e: WheelEvent) => {
|
|
316
|
+
if (!wheelNavigation || disabled) return
|
|
317
|
+
e.preventDefault()
|
|
318
|
+
step(e.deltaY > 0 || e.deltaX > 0 ? 1 : -1)
|
|
319
|
+
}
|
|
320
|
+
node.addEventListener('wheel', onWheel, { passive: false })
|
|
321
|
+
return { destroy: () => node.removeEventListener('wheel', onWheel) }
|
|
322
|
+
}
|
|
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
|
+
</script>
|
|
417
|
+
|
|
418
|
+
<div
|
|
419
|
+
class="sv-cal"
|
|
420
|
+
class:sv-cal--disabled={disabled}
|
|
421
|
+
class:sv-cal--with-presets={presets && presets.length}
|
|
422
|
+
class:sv-cal--band={selectionMode === 'range' || selectionMode === 'week'}
|
|
423
|
+
role="group"
|
|
424
|
+
aria-label="Calendar"
|
|
425
|
+
aria-disabled={disabled}
|
|
426
|
+
>
|
|
427
|
+
{#if presets && presets.length}
|
|
428
|
+
<div class="sv-cal__presets" role="group" aria-label="Shortcuts">
|
|
429
|
+
{#each presets as p, i (i)}
|
|
430
|
+
<button type="button" class="sv-cal__preset" onclick={() => applyPreset(p)} disabled={!isInteractive}>{p.label}</button>
|
|
431
|
+
{/each}
|
|
432
|
+
</div>
|
|
433
|
+
{/if}
|
|
434
|
+
|
|
435
|
+
<div class="sv-cal__main" use:wheelNav>
|
|
436
|
+
<div class="sv-cal__header">
|
|
437
|
+
<button type="button" class="sv-cal__nav" onclick={() => step(-1)} disabled={disabled} aria-label="Previous">
|
|
438
|
+
<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
|
+
</button>
|
|
440
|
+
<button
|
|
441
|
+
type="button"
|
|
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">
|
|
448
|
+
<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
|
+
</button>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<div class="sv-cal__view" bind:this={animEl}>
|
|
453
|
+
{#if mode === 'month'}
|
|
454
|
+
<div class="sv-cal__panels" onkeydown={onGridKeydown} role="grid" aria-label={title} tabindex="-1">
|
|
455
|
+
{#each panels as panel (panel.month.getTime())}
|
|
456
|
+
<div class="sv-cal__panel">
|
|
457
|
+
{#if months > 1}<div class="sv-cal__panel-title">{monthTitleFmt.format(panel.month)}</div>{/if}
|
|
458
|
+
{#if !hideDayNames}
|
|
459
|
+
<div class="sv-cal__weekdays" role="row">
|
|
460
|
+
{#if weekNumbers}<span class="sv-cal__wk-head" aria-hidden="true">#</span>{/if}
|
|
461
|
+
{#each weekdayHeaders as wh (wh.weekday)}
|
|
462
|
+
<span class="sv-cal__weekday" role="columnheader">{wh.label}</span>
|
|
463
|
+
{/each}
|
|
464
|
+
</div>
|
|
465
|
+
{/if}
|
|
466
|
+
{#each panel.matrix as week, wi (wi)}
|
|
467
|
+
<div class="sv-cal__week" role="row">
|
|
468
|
+
{#if weekNumbers}<span class="sv-cal__wk" aria-hidden="true">{week[0]?.week}</span>{/if}
|
|
469
|
+
{#each week as cell (cell.date.getTime())}
|
|
470
|
+
{@const st = dayState(cell.date, panel.month)}
|
|
471
|
+
{#if st.outside && hideOtherMonthDays}
|
|
472
|
+
<span class="sv-cal__day sv-cal__day--empty" role="gridcell" aria-hidden="true"></span>
|
|
473
|
+
{:else}
|
|
474
|
+
<button
|
|
475
|
+
type="button"
|
|
476
|
+
role="gridcell"
|
|
477
|
+
class="sv-cal__day"
|
|
478
|
+
class:is-selected={st.selected}
|
|
479
|
+
class:is-today={st.today}
|
|
480
|
+
class:is-outside={st.outside}
|
|
481
|
+
class:is-important={st.important}
|
|
482
|
+
class:is-preview={st.preview}
|
|
483
|
+
class:is-focused={st.focused}
|
|
484
|
+
aria-selected={st.selected}
|
|
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)}
|
|
494
|
+
>{cell.date.getDate()}</button>
|
|
495
|
+
{/if}
|
|
496
|
+
{/each}
|
|
497
|
+
</div>
|
|
498
|
+
{/each}
|
|
499
|
+
</div>
|
|
500
|
+
{/each}
|
|
501
|
+
</div>
|
|
502
|
+
{:else if mode === 'year'}
|
|
503
|
+
<div class="sv-cal__grid-mode" onkeydown={onNonMonthKeydown} role="grid" aria-label={title} tabindex="-1">
|
|
504
|
+
{#each yearCells as yc (yc.index)}
|
|
505
|
+
<button
|
|
506
|
+
type="button"
|
|
507
|
+
class="sv-cal__cell"
|
|
508
|
+
class:is-today={yc.current}
|
|
509
|
+
class:is-selected={sel.dates.some((d) => d.getMonth() === yc.index && d.getFullYear() === view.getFullYear())}
|
|
510
|
+
onclick={() => pickMonth(yc.index)}
|
|
511
|
+
disabled={disabled}
|
|
512
|
+
>{yc.label}</button>
|
|
513
|
+
{/each}
|
|
514
|
+
</div>
|
|
515
|
+
{:else}
|
|
516
|
+
<div class="sv-cal__grid-mode" onkeydown={onNonMonthKeydown} role="grid" aria-label={title} tabindex="-1">
|
|
517
|
+
{#each decadeCells as dc (dc.year)}
|
|
518
|
+
<button
|
|
519
|
+
type="button"
|
|
520
|
+
class="sv-cal__cell"
|
|
521
|
+
class:is-outside={dc.outside}
|
|
522
|
+
class:is-today={dc.current}
|
|
523
|
+
class:is-selected={sel.dates.some((d) => d.getFullYear() === dc.year)}
|
|
524
|
+
onclick={() => pickYear(dc.year)}
|
|
525
|
+
disabled={disabled}
|
|
526
|
+
>{dc.year}</button>
|
|
527
|
+
{/each}
|
|
528
|
+
</div>
|
|
529
|
+
{/if}
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
{#if footer}
|
|
533
|
+
<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>
|
|
537
|
+
{/if}
|
|
538
|
+
</div>
|
|
539
|
+
{/if}
|
|
540
|
+
</div>
|
|
541
|
+
|
|
542
|
+
{#if name}<input type="hidden" {name} value={sel.dates.map((d) => d.toISOString()).join(',')} />{/if}
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
<style>
|
|
546
|
+
.sv-cal {
|
|
547
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
548
|
+
--_bg: var(--sg-bg, #fff);
|
|
549
|
+
--_fg: var(--sg-fg, #0f172a);
|
|
550
|
+
--_muted: var(--sg-muted, #64748b);
|
|
551
|
+
--_border: var(--sg-border, #e2e8f0);
|
|
552
|
+
--_hover: var(--sg-row-hover-bg, rgba(37, 99, 235, 0.08));
|
|
553
|
+
--_radius: var(--sg-radius, 8px);
|
|
554
|
+
display: inline-flex;
|
|
555
|
+
gap: 10px;
|
|
556
|
+
padding: 10px;
|
|
557
|
+
width: max-content;
|
|
558
|
+
background: var(--_bg);
|
|
559
|
+
color: var(--_fg);
|
|
560
|
+
border: 1px solid var(--_border);
|
|
561
|
+
border-radius: calc(var(--_radius) + 4px);
|
|
562
|
+
font-size: 13px;
|
|
563
|
+
user-select: none;
|
|
564
|
+
}
|
|
565
|
+
.sv-cal--disabled { opacity: 0.55; pointer-events: none; }
|
|
566
|
+
.sv-cal__main { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
567
|
+
/* Clip so animated content slides in from the edge (not past the border). Day
|
|
568
|
+
focus outlines use outline-offset:-2px, so they stay inside and aren't clipped. */
|
|
569
|
+
.sv-cal__view { display: block; overflow: hidden; }
|
|
570
|
+
/* Quick-shortcut rail (Today / Last 7 days / ...) shown when `presets` is set. */
|
|
571
|
+
.sv-cal__presets {
|
|
572
|
+
display: flex; flex-direction: column; gap: 2px;
|
|
573
|
+
padding-right: 10px; border-right: 1px solid var(--_border);
|
|
574
|
+
min-width: 118px; align-self: stretch;
|
|
575
|
+
}
|
|
576
|
+
.sv-cal__preset {
|
|
577
|
+
text-align: left; padding: 6px 10px; font: inherit; font-size: 12px; font-weight: 550;
|
|
578
|
+
background: none; border: 0; border-radius: var(--_radius); color: inherit; cursor: pointer; white-space: nowrap;
|
|
579
|
+
}
|
|
580
|
+
.sv-cal__preset:hover:not(:disabled) { background: var(--_hover); color: var(--_accent); }
|
|
581
|
+
.sv-cal__preset:disabled { opacity: 0.5; cursor: default; }
|
|
582
|
+
|
|
583
|
+
.sv-cal__header { display: flex; align-items: center; gap: 4px; }
|
|
584
|
+
.sv-cal__title {
|
|
585
|
+
flex: 1; text-align: center; font-weight: 650; font-size: 13.5px;
|
|
586
|
+
background: none; border: 0; color: inherit; cursor: pointer;
|
|
587
|
+
padding: 6px 8px; border-radius: var(--_radius);
|
|
588
|
+
}
|
|
589
|
+
.sv-cal__title:hover:not(:disabled) { background: var(--_hover); }
|
|
590
|
+
.sv-cal__title:disabled { cursor: default; }
|
|
591
|
+
.sv-cal__nav {
|
|
592
|
+
display: grid; place-items: center; width: 30px; height: 30px;
|
|
593
|
+
background: none; border: 0; color: var(--_muted); cursor: pointer; border-radius: var(--_radius);
|
|
594
|
+
}
|
|
595
|
+
.sv-cal__nav:hover:not(:disabled) { background: var(--_hover); color: var(--_fg); }
|
|
596
|
+
.sv-cal__nav:disabled { opacity: 0.4; cursor: default; }
|
|
597
|
+
|
|
598
|
+
.sv-cal__panels { display: flex; gap: 14px; }
|
|
599
|
+
.sv-cal__panel-title { text-align: center; font-weight: 600; font-size: 12.5px; margin-bottom: 4px; color: var(--_muted); }
|
|
600
|
+
.sv-cal__weekdays, .sv-cal__week {
|
|
601
|
+
display: grid;
|
|
602
|
+
grid-template-columns: repeat(7, 34px);
|
|
603
|
+
}
|
|
604
|
+
.sv-cal__weekdays:has(.sv-cal__wk-head), .sv-cal__week:has(.sv-cal__wk) {
|
|
605
|
+
grid-template-columns: 26px repeat(7, 34px);
|
|
606
|
+
}
|
|
607
|
+
.sv-cal__weekday, .sv-cal__wk-head {
|
|
608
|
+
display: grid; place-items: center; height: 28px;
|
|
609
|
+
font-size: 11px; font-weight: 600; color: var(--_muted); text-transform: uppercase; letter-spacing: 0.02em;
|
|
610
|
+
}
|
|
611
|
+
.sv-cal__wk {
|
|
612
|
+
display: grid; place-items: center; height: 34px;
|
|
613
|
+
font-size: 10.5px; color: var(--_muted); opacity: 0.7;
|
|
614
|
+
}
|
|
615
|
+
.sv-cal__day {
|
|
616
|
+
display: grid; place-items: center; width: 34px; height: 34px; box-sizing: border-box;
|
|
617
|
+
background: none; border: 0; color: inherit; cursor: pointer;
|
|
618
|
+
border-radius: var(--_radius); font-size: 12.5px; position: relative;
|
|
619
|
+
}
|
|
620
|
+
.sv-cal__day--empty { visibility: hidden; }
|
|
621
|
+
/* Hover only NON-selected days with the neutral hover fill; hovering a selected
|
|
622
|
+
day keeps the accent (a bare :hover would swap in the neutral bg while the
|
|
623
|
+
text stays --sg-on-accent -> invisible on themes like shadcn). */
|
|
624
|
+
.sv-cal__day:hover:not(:disabled):not(.is-selected) { background: var(--_hover); }
|
|
625
|
+
.sv-cal__day.is-selected:hover:not(:disabled) { background: color-mix(in srgb, var(--_accent) 86%, var(--_fg)); }
|
|
626
|
+
.sv-cal__day.is-outside { color: var(--_muted); opacity: 0.55; }
|
|
627
|
+
.sv-cal__day.is-today { font-weight: 750; box-shadow: inset 0 0 0 1px var(--_accent); }
|
|
628
|
+
.sv-cal__day.is-important::after {
|
|
629
|
+
content: ''; position: absolute; bottom: 4px; left: 50%; transform: translateX(-50%);
|
|
630
|
+
width: 4px; height: 4px; border-radius: 50%; background: var(--sg-danger, #dc2626);
|
|
631
|
+
}
|
|
632
|
+
.sv-cal__day.is-preview { background: color-mix(in srgb, var(--_accent) 16%, transparent); border-radius: 0; }
|
|
633
|
+
.sv-cal__day.is-selected {
|
|
634
|
+
background: var(--_accent); color: var(--sg-on-accent, #fff); font-weight: 650;
|
|
635
|
+
}
|
|
636
|
+
/* Discrete selection (single / many / ...): inset each selected pill so adjacent
|
|
637
|
+
picked days keep a visible gap. Range / week modes stay a connected band. */
|
|
638
|
+
.sv-cal:not(.sv-cal--band) .sv-cal__day.is-selected { background-clip: content-box; padding: 2px; }
|
|
639
|
+
.sv-cal__day.is-selected.is-important::after { background: var(--sg-on-accent, #fff); }
|
|
640
|
+
.sv-cal__day:disabled { color: var(--_muted); opacity: 0.35; cursor: default; text-decoration: line-through; }
|
|
641
|
+
.sv-cal__day.is-focused:focus-visible,
|
|
642
|
+
.sv-cal__day:focus-visible,
|
|
643
|
+
.sv-cal__cell:focus-visible {
|
|
644
|
+
outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: -2px;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.sv-cal__grid-mode {
|
|
648
|
+
display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px;
|
|
649
|
+
min-width: 252px;
|
|
650
|
+
}
|
|
651
|
+
.sv-cal__cell {
|
|
652
|
+
height: 44px; display: grid; place-items: center;
|
|
653
|
+
background: none; border: 0; color: inherit; cursor: pointer;
|
|
654
|
+
border-radius: var(--_radius); font-size: 12.5px;
|
|
655
|
+
}
|
|
656
|
+
.sv-cal__cell:hover:not(:disabled):not(.is-selected) { background: var(--_hover); }
|
|
657
|
+
.sv-cal__cell.is-selected:hover:not(:disabled) { background: color-mix(in srgb, var(--_accent) 86%, var(--_fg)); }
|
|
658
|
+
.sv-cal__cell.is-outside { color: var(--_muted); opacity: 0.5; }
|
|
659
|
+
.sv-cal__cell.is-today { box-shadow: inset 0 0 0 1px var(--_accent); font-weight: 700; }
|
|
660
|
+
.sv-cal__cell.is-selected { background: var(--_accent); color: var(--sg-on-accent, #fff); font-weight: 650; }
|
|
661
|
+
|
|
662
|
+
.sv-cal__footer { display: flex; justify-content: center; gap: 8px; border-top: 1px solid var(--_border); padding-top: 8px; margin-top: 2px; }
|
|
663
|
+
.sv-cal__foot-btn {
|
|
664
|
+
padding: 5px 12px; font: inherit; font-size: 12px; font-weight: 600;
|
|
665
|
+
background: none; border: 1px solid var(--_border); border-radius: var(--_radius);
|
|
666
|
+
color: var(--_accent); cursor: pointer;
|
|
667
|
+
}
|
|
668
|
+
.sv-cal__foot-btn:hover:not(:disabled) { background: var(--_hover); }
|
|
669
|
+
</style>
|