@joewinke/jatui 0.41.1 → 0.42.0
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/CLAUDE.md +9 -3
- package/dist/components/BookingScheduler.svelte +412 -0
- package/dist/components/BookingScheduler.svelte.d.ts +66 -0
- package/dist/components/CalendarPicker.svelte +190 -205
- package/dist/components/CalendarPicker.svelte.d.ts +19 -1
- package/dist/components/DatePicker.svelte +419 -0
- package/dist/components/DatePicker.svelte.d.ts +44 -0
- package/dist/components/DateRangePicker.svelte +471 -5
- package/dist/components/DateRangePicker.svelte.d.ts +32 -3
- package/dist/components/EventCalendar.svelte +1324 -0
- package/dist/components/EventCalendar.svelte.d.ts +101 -0
- package/dist/components/TimeSlotPicker.svelte +174 -108
- package/dist/components/TimeSlotPicker.svelte.d.ts +27 -0
- package/dist/components/calendar/CalendarHeader.svelte +98 -0
- package/dist/components/calendar/CalendarHeader.svelte.d.ts +24 -0
- package/dist/components/calendar/MonthGrid.svelte +361 -0
- package/dist/components/calendar/MonthGrid.svelte.d.ts +55 -0
- package/dist/components/calendar/calendarGrid.d.ts +219 -0
- package/dist/components/calendar/calendarGrid.js +494 -0
- package/dist/components/calendar/createCalendarGrid.d.ts +25 -0
- package/dist/components/calendar/createCalendarGrid.js +57 -0
- package/dist/components/calendar/dayOverlap.d.ts +92 -0
- package/dist/components/calendar/dayOverlap.js +147 -0
- package/dist/components/calendar/eventCalendar.d.ts +75 -0
- package/dist/components/calendar/eventCalendar.js +187 -0
- package/dist/components/calendar/index.d.ts +32 -0
- package/dist/components/calendar/index.js +25 -0
- package/dist/components/calendar/monthPacking.d.ts +97 -0
- package/dist/components/calendar/monthPacking.js +254 -0
- package/dist/components/calendar/timeSlots.d.ts +57 -0
- package/dist/components/calendar/timeSlots.js +163 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +11 -0
- package/package.json +6 -1
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { DateRange, Weekday } from './calendarGrid';
|
|
4
|
+
|
|
5
|
+
/** What a `day` snippet receives for each rendered cell. */
|
|
6
|
+
export interface MonthGridDay {
|
|
7
|
+
date: Date;
|
|
8
|
+
iso: string;
|
|
9
|
+
inMonth: boolean;
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
selected: boolean;
|
|
12
|
+
today: boolean;
|
|
13
|
+
inRange: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MonthGridProps {
|
|
17
|
+
/** Any date inside the month to show. */
|
|
18
|
+
month: Date;
|
|
19
|
+
/** The roving-focus day. Only the cell for this day is in the tab order. */
|
|
20
|
+
focusedDate: Date;
|
|
21
|
+
/** First column: 0 = Sunday … 6 = Saturday. */
|
|
22
|
+
weekStart?: Weekday;
|
|
23
|
+
locale?: string;
|
|
24
|
+
/** Single selection. */
|
|
25
|
+
selected?: Date | null;
|
|
26
|
+
/** Range band (pass the hover preview as `end` while picking). */
|
|
27
|
+
range?: DateRange | null;
|
|
28
|
+
isDisabled?: (date: Date) => boolean;
|
|
29
|
+
/** Keyboard clamps (selection rules belong to `isDisabled`). */
|
|
30
|
+
min?: Date | null;
|
|
31
|
+
max?: Date | null;
|
|
32
|
+
/** Render leading/trailing days from the adjacent months (dimmed). */
|
|
33
|
+
showOutsideDays?: boolean;
|
|
34
|
+
/** Always six rows, so the panel never changes height. */
|
|
35
|
+
fixedWeeks?: boolean;
|
|
36
|
+
/** Hide the weekday header row. */
|
|
37
|
+
hideWeekdays?: boolean;
|
|
38
|
+
/** Tint every enabled in-month day (booking calendars: "these days are open"). */
|
|
39
|
+
highlightEnabled?: boolean;
|
|
40
|
+
size?: 'sm' | 'md' | 'lg';
|
|
41
|
+
/** Accessible name; defaults to "March 2026". */
|
|
42
|
+
ariaLabel?: string;
|
|
43
|
+
rtl?: boolean;
|
|
44
|
+
/** Click / Enter / Space on an enabled day. */
|
|
45
|
+
onSelect?: (date: Date) => void;
|
|
46
|
+
/** Focus moved (keyboard or pointer). The host shifts `month` when it leaves the view. */
|
|
47
|
+
onFocusDate?: (date: Date) => void;
|
|
48
|
+
/** Pointer over a day (null on leave) — drives the range hover preview. */
|
|
49
|
+
onHover?: (date: Date | null) => void;
|
|
50
|
+
/** Extra words for a day's accessible name ("3 slots left"); the `day` snippet is visual only. */
|
|
51
|
+
describeDay?: (date: Date) => string | undefined;
|
|
52
|
+
/** Extra content under the day number (availability dots, counts). */
|
|
53
|
+
day?: Snippet<[MonthGridDay]>;
|
|
54
|
+
class?: string;
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<script lang="ts">
|
|
59
|
+
import { tick } from 'svelte';
|
|
60
|
+
import {
|
|
61
|
+
buildMonthMatrix,
|
|
62
|
+
formatLongDate,
|
|
63
|
+
formatMonthYear,
|
|
64
|
+
isSameDay,
|
|
65
|
+
nextFocusDate,
|
|
66
|
+
rangeCellState,
|
|
67
|
+
toIsoDate,
|
|
68
|
+
weekdayLabels
|
|
69
|
+
} from './calendarGrid';
|
|
70
|
+
|
|
71
|
+
let {
|
|
72
|
+
month,
|
|
73
|
+
focusedDate,
|
|
74
|
+
weekStart = 0,
|
|
75
|
+
locale,
|
|
76
|
+
selected = null,
|
|
77
|
+
range = null,
|
|
78
|
+
isDisabled = () => false,
|
|
79
|
+
min = null,
|
|
80
|
+
max = null,
|
|
81
|
+
showOutsideDays = true,
|
|
82
|
+
fixedWeeks = false,
|
|
83
|
+
hideWeekdays = false,
|
|
84
|
+
highlightEnabled = false,
|
|
85
|
+
size = 'md',
|
|
86
|
+
ariaLabel,
|
|
87
|
+
rtl = false,
|
|
88
|
+
onSelect,
|
|
89
|
+
onFocusDate,
|
|
90
|
+
onHover,
|
|
91
|
+
describeDay,
|
|
92
|
+
day,
|
|
93
|
+
class: className = ''
|
|
94
|
+
}: MonthGridProps = $props();
|
|
95
|
+
|
|
96
|
+
let gridEl = $state<HTMLTableElement | null>(null);
|
|
97
|
+
|
|
98
|
+
const weeks = $derived(buildMonthMatrix(month, weekStart, fixedWeeks));
|
|
99
|
+
const labels = $derived(weekdayLabels(weekStart, locale, size === 'sm' ? 'narrow' : 'short'));
|
|
100
|
+
const focusIso = $derived(toIsoDate(focusedDate));
|
|
101
|
+
const label = $derived(ariaLabel ?? formatMonthYear(month, locale));
|
|
102
|
+
// The focused day may sit in a sibling month panel (two-month range view). When
|
|
103
|
+
// it is not in THIS panel, the first in-month day keeps the panel reachable by Tab.
|
|
104
|
+
// Each date appears once per matrix, so the iso alone identifies the tab stop.
|
|
105
|
+
const tabIso = $derived.by(() => {
|
|
106
|
+
const cells = weeks.flat();
|
|
107
|
+
if (cells.some((c) => c.inMonth && c.iso === focusIso)) return focusIso;
|
|
108
|
+
return cells.find((c) => c.inMonth)?.iso ?? '';
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const today = new Date();
|
|
112
|
+
|
|
113
|
+
/** Move DOM focus to `date`'s cell, searching the whole calendar so a sibling panel can take it. */
|
|
114
|
+
async function focusCell(date: Date) {
|
|
115
|
+
await tick();
|
|
116
|
+
const iso = toIsoDate(date);
|
|
117
|
+
const root = gridEl?.closest('[data-calendar-root]') ?? gridEl;
|
|
118
|
+
const cell = root?.querySelector<HTMLElement>(`[data-iso="${iso}"]:not([data-outside])`) ??
|
|
119
|
+
root?.querySelector<HTMLElement>(`[data-iso="${iso}"]`);
|
|
120
|
+
cell?.focus();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function handleKeydown(e: KeyboardEvent, date: Date, disabled: boolean) {
|
|
124
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
if (!disabled) onSelect?.(date);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
|
130
|
+
const next = nextFocusDate(date, e.key, { weekStart, min, max, rtl, shiftKey: e.shiftKey });
|
|
131
|
+
if (!next) return;
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
onFocusDate?.(next);
|
|
134
|
+
void focusCell(next);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function handleClick(date: Date, disabled: boolean) {
|
|
138
|
+
onFocusDate?.(date);
|
|
139
|
+
if (!disabled) onSelect?.(date);
|
|
140
|
+
}
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<table
|
|
144
|
+
bind:this={gridEl}
|
|
145
|
+
class="jatui-month-grid jatui-month-grid--{size} {className}"
|
|
146
|
+
role="grid"
|
|
147
|
+
aria-label={label}
|
|
148
|
+
dir={rtl ? 'rtl' : undefined}
|
|
149
|
+
onpointerleave={() => onHover?.(null)}
|
|
150
|
+
>
|
|
151
|
+
{#if !hideWeekdays}
|
|
152
|
+
<thead>
|
|
153
|
+
<tr>
|
|
154
|
+
{#each labels as l (l.long)}
|
|
155
|
+
<th scope="col" abbr={l.long} class="jatui-month-grid__weekday">{l.short}</th>
|
|
156
|
+
{/each}
|
|
157
|
+
</tr>
|
|
158
|
+
</thead>
|
|
159
|
+
{/if}
|
|
160
|
+
<tbody>
|
|
161
|
+
{#each weeks as week (week[0].iso)}
|
|
162
|
+
<tr>
|
|
163
|
+
{#each week as cell (cell.iso)}
|
|
164
|
+
{#if !cell.inMonth && !showOutsideDays}
|
|
165
|
+
<td role="gridcell" aria-hidden="true" class="jatui-month-grid__cell"></td>
|
|
166
|
+
{:else}
|
|
167
|
+
{@const disabled = isDisabled(cell.date)}
|
|
168
|
+
{@const isSelected = isSameDay(cell.date, selected)}
|
|
169
|
+
{@const band = rangeCellState(cell.date, cell.column, range)}
|
|
170
|
+
{@const isToday = isSameDay(cell.date, today)}
|
|
171
|
+
{@const solid = isSelected || band.isStart || band.isEnd}
|
|
172
|
+
<td
|
|
173
|
+
role="gridcell"
|
|
174
|
+
aria-selected={solid || band.inRange ? 'true' : 'false'}
|
|
175
|
+
class="jatui-month-grid__cell"
|
|
176
|
+
class:is-band={band.inRange && !(band.isStart && band.isEnd)}
|
|
177
|
+
class:band-start={band.bandStart}
|
|
178
|
+
class:band-end={band.bandEnd}
|
|
179
|
+
class:band-from-start={band.isStart}
|
|
180
|
+
class:band-to-end={band.isEnd}
|
|
181
|
+
>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
class="jatui-month-grid__day"
|
|
185
|
+
class:is-solid={solid}
|
|
186
|
+
class:is-today={isToday}
|
|
187
|
+
class:is-outside={!cell.inMonth}
|
|
188
|
+
class:is-disabled={disabled}
|
|
189
|
+
class:is-open={highlightEnabled && cell.inMonth && !disabled}
|
|
190
|
+
data-iso={cell.iso}
|
|
191
|
+
data-outside={cell.inMonth ? undefined : ''}
|
|
192
|
+
tabindex={cell.iso === tabIso ? 0 : -1}
|
|
193
|
+
aria-label={[formatLongDate(cell.date, locale), describeDay?.(cell.date)].filter(Boolean).join(', ')}
|
|
194
|
+
aria-disabled={disabled ? 'true' : undefined}
|
|
195
|
+
aria-current={isToday ? 'date' : undefined}
|
|
196
|
+
onclick={() => handleClick(cell.date, disabled)}
|
|
197
|
+
onkeydown={(e) => handleKeydown(e, cell.date, disabled)}
|
|
198
|
+
onpointerenter={() => onHover?.(cell.date)}
|
|
199
|
+
>
|
|
200
|
+
<span class="jatui-month-grid__num">{cell.date.getDate()}</span>
|
|
201
|
+
{#if day}
|
|
202
|
+
<span class="jatui-month-grid__extra" aria-hidden="true">{@render day({
|
|
203
|
+
date: cell.date,
|
|
204
|
+
iso: cell.iso,
|
|
205
|
+
inMonth: cell.inMonth,
|
|
206
|
+
disabled,
|
|
207
|
+
selected: solid,
|
|
208
|
+
today: isToday,
|
|
209
|
+
inRange: band.inRange
|
|
210
|
+
})}</span>
|
|
211
|
+
{/if}
|
|
212
|
+
</button>
|
|
213
|
+
</td>
|
|
214
|
+
{/if}
|
|
215
|
+
{/each}
|
|
216
|
+
</tr>
|
|
217
|
+
{/each}
|
|
218
|
+
</tbody>
|
|
219
|
+
</table>
|
|
220
|
+
|
|
221
|
+
<style>
|
|
222
|
+
.jatui-month-grid {
|
|
223
|
+
--_cell: 2.25rem;
|
|
224
|
+
--_accent: var(--jatui-cal-accent, var(--jatui-selection-solid, var(--color-primary)));
|
|
225
|
+
--_accent-fg: var(--jatui-cal-accent-fg, var(--jatui-selection-solid-fg, var(--color-primary-content)));
|
|
226
|
+
--_band: var(--jatui-cal-band, var(--jatui-selection-bg, color-mix(in oklab, var(--color-primary) 14%, transparent)));
|
|
227
|
+
border-collapse: separate;
|
|
228
|
+
border-spacing: 0 2px;
|
|
229
|
+
table-layout: fixed;
|
|
230
|
+
width: 100%;
|
|
231
|
+
min-width: calc(var(--_cell) * 7);
|
|
232
|
+
}
|
|
233
|
+
.jatui-month-grid--sm {
|
|
234
|
+
--_cell: 2rem;
|
|
235
|
+
}
|
|
236
|
+
.jatui-month-grid--lg {
|
|
237
|
+
--_cell: 2.75rem;
|
|
238
|
+
}
|
|
239
|
+
.jatui-month-grid__weekday {
|
|
240
|
+
padding-block: 0.25rem;
|
|
241
|
+
text-align: center;
|
|
242
|
+
font-size: 0.75rem;
|
|
243
|
+
font-weight: 500;
|
|
244
|
+
color: color-mix(in oklab, var(--color-base-content) 55%, transparent);
|
|
245
|
+
}
|
|
246
|
+
.jatui-month-grid__cell {
|
|
247
|
+
position: relative;
|
|
248
|
+
padding: 0;
|
|
249
|
+
text-align: center;
|
|
250
|
+
}
|
|
251
|
+
/* The range band lives on the cell, so it bridges the gap between days. */
|
|
252
|
+
.jatui-month-grid__cell.is-band::before {
|
|
253
|
+
content: '';
|
|
254
|
+
position: absolute;
|
|
255
|
+
inset-block: 0;
|
|
256
|
+
inset-inline: 0;
|
|
257
|
+
background: var(--_band);
|
|
258
|
+
pointer-events: none;
|
|
259
|
+
}
|
|
260
|
+
.jatui-month-grid__cell.is-band.band-from-start::before {
|
|
261
|
+
inset-inline-start: 50%;
|
|
262
|
+
}
|
|
263
|
+
.jatui-month-grid__cell.is-band.band-to-end::before {
|
|
264
|
+
inset-inline-end: 50%;
|
|
265
|
+
}
|
|
266
|
+
.jatui-month-grid__cell.is-band.band-start:not(.band-from-start)::before {
|
|
267
|
+
border-start-start-radius: var(--radius-field, 0.5rem);
|
|
268
|
+
border-end-start-radius: var(--radius-field, 0.5rem);
|
|
269
|
+
}
|
|
270
|
+
.jatui-month-grid__cell.is-band.band-end:not(.band-to-end)::before {
|
|
271
|
+
border-start-end-radius: var(--radius-field, 0.5rem);
|
|
272
|
+
border-end-end-radius: var(--radius-field, 0.5rem);
|
|
273
|
+
}
|
|
274
|
+
.jatui-month-grid__day {
|
|
275
|
+
position: relative;
|
|
276
|
+
display: inline-flex;
|
|
277
|
+
flex-direction: column;
|
|
278
|
+
align-items: center;
|
|
279
|
+
justify-content: center;
|
|
280
|
+
gap: 1px;
|
|
281
|
+
width: var(--_cell);
|
|
282
|
+
max-width: 100%;
|
|
283
|
+
aspect-ratio: 1;
|
|
284
|
+
border-radius: var(--radius-field, 0.5rem);
|
|
285
|
+
font-size: 0.875rem;
|
|
286
|
+
font-variant-numeric: tabular-nums;
|
|
287
|
+
color: var(--color-base-content);
|
|
288
|
+
background: transparent;
|
|
289
|
+
cursor: pointer;
|
|
290
|
+
/* Both layers always render; selection fades on colour and radius only. */
|
|
291
|
+
transition:
|
|
292
|
+
background-color var(--motion-fast, 120ms) var(--motion-standard, ease),
|
|
293
|
+
color var(--motion-fast, 120ms) var(--motion-standard, ease);
|
|
294
|
+
}
|
|
295
|
+
.jatui-month-grid__day:hover:not(.is-solid):not(.is-disabled) {
|
|
296
|
+
background: var(--jatui-hover-bg, color-mix(in oklab, var(--color-base-content) 7%, transparent));
|
|
297
|
+
}
|
|
298
|
+
.jatui-month-grid__day:focus-visible {
|
|
299
|
+
outline: var(--jatui-focus-ring-width, 2px) solid var(--jatui-focus-ring, var(--color-primary));
|
|
300
|
+
outline-offset: calc(var(--jatui-focus-ring-width, 2px) * -1 + 1px);
|
|
301
|
+
z-index: 1;
|
|
302
|
+
}
|
|
303
|
+
.jatui-month-grid__extra {
|
|
304
|
+
display: inline-flex;
|
|
305
|
+
line-height: 0;
|
|
306
|
+
}
|
|
307
|
+
.jatui-month-grid__day.is-today:not(.is-solid) {
|
|
308
|
+
font-weight: 700;
|
|
309
|
+
color: var(--_accent);
|
|
310
|
+
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--_accent) 55%, transparent);
|
|
311
|
+
}
|
|
312
|
+
.jatui-month-grid__day.is-open:not(.is-solid) {
|
|
313
|
+
background: var(--_band);
|
|
314
|
+
color: var(--_accent);
|
|
315
|
+
font-weight: 600;
|
|
316
|
+
}
|
|
317
|
+
.jatui-month-grid__day.is-open:not(.is-solid):hover {
|
|
318
|
+
background: color-mix(in oklab, var(--_accent) 26%, transparent);
|
|
319
|
+
}
|
|
320
|
+
.jatui-month-grid__day.is-solid {
|
|
321
|
+
background: var(--_accent);
|
|
322
|
+
color: var(--_accent-fg);
|
|
323
|
+
font-weight: 600;
|
|
324
|
+
}
|
|
325
|
+
.jatui-month-grid__day.is-outside:not(.is-solid) {
|
|
326
|
+
color: color-mix(in oklab, var(--color-base-content) 40%, transparent);
|
|
327
|
+
}
|
|
328
|
+
.jatui-month-grid__day.is-disabled {
|
|
329
|
+
cursor: not-allowed;
|
|
330
|
+
color: color-mix(in oklab, var(--color-base-content) 30%, transparent);
|
|
331
|
+
}
|
|
332
|
+
.jatui-month-grid__day.is-disabled .jatui-month-grid__num {
|
|
333
|
+
text-decoration: line-through;
|
|
334
|
+
text-decoration-color: color-mix(in oklab, var(--color-base-content) 25%, transparent);
|
|
335
|
+
}
|
|
336
|
+
@media (prefers-reduced-motion: reduce) {
|
|
337
|
+
.jatui-month-grid__day {
|
|
338
|
+
transition: none;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
:global([data-motion='calm']) .jatui-month-grid__day {
|
|
342
|
+
transition-duration: 0ms;
|
|
343
|
+
}
|
|
344
|
+
@media (forced-colors: active) {
|
|
345
|
+
.jatui-month-grid__day.is-open:not(.is-solid) {
|
|
346
|
+
background: var(--_band);
|
|
347
|
+
color: var(--_accent);
|
|
348
|
+
font-weight: 600;
|
|
349
|
+
}
|
|
350
|
+
.jatui-month-grid__day.is-open:not(.is-solid):hover {
|
|
351
|
+
background: color-mix(in oklab, var(--_accent) 26%, transparent);
|
|
352
|
+
}
|
|
353
|
+
.jatui-month-grid__day.is-solid {
|
|
354
|
+
outline: 2px solid Highlight;
|
|
355
|
+
}
|
|
356
|
+
.jatui-month-grid__cell.is-band::before {
|
|
357
|
+
background: Highlight;
|
|
358
|
+
opacity: 0.25;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
</style>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { DateRange, Weekday } from './calendarGrid';
|
|
3
|
+
/** What a `day` snippet receives for each rendered cell. */
|
|
4
|
+
export interface MonthGridDay {
|
|
5
|
+
date: Date;
|
|
6
|
+
iso: string;
|
|
7
|
+
inMonth: boolean;
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
selected: boolean;
|
|
10
|
+
today: boolean;
|
|
11
|
+
inRange: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface MonthGridProps {
|
|
14
|
+
/** Any date inside the month to show. */
|
|
15
|
+
month: Date;
|
|
16
|
+
/** The roving-focus day. Only the cell for this day is in the tab order. */
|
|
17
|
+
focusedDate: Date;
|
|
18
|
+
/** First column: 0 = Sunday … 6 = Saturday. */
|
|
19
|
+
weekStart?: Weekday;
|
|
20
|
+
locale?: string;
|
|
21
|
+
/** Single selection. */
|
|
22
|
+
selected?: Date | null;
|
|
23
|
+
/** Range band (pass the hover preview as `end` while picking). */
|
|
24
|
+
range?: DateRange | null;
|
|
25
|
+
isDisabled?: (date: Date) => boolean;
|
|
26
|
+
/** Keyboard clamps (selection rules belong to `isDisabled`). */
|
|
27
|
+
min?: Date | null;
|
|
28
|
+
max?: Date | null;
|
|
29
|
+
/** Render leading/trailing days from the adjacent months (dimmed). */
|
|
30
|
+
showOutsideDays?: boolean;
|
|
31
|
+
/** Always six rows, so the panel never changes height. */
|
|
32
|
+
fixedWeeks?: boolean;
|
|
33
|
+
/** Hide the weekday header row. */
|
|
34
|
+
hideWeekdays?: boolean;
|
|
35
|
+
/** Tint every enabled in-month day (booking calendars: "these days are open"). */
|
|
36
|
+
highlightEnabled?: boolean;
|
|
37
|
+
size?: 'sm' | 'md' | 'lg';
|
|
38
|
+
/** Accessible name; defaults to "March 2026". */
|
|
39
|
+
ariaLabel?: string;
|
|
40
|
+
rtl?: boolean;
|
|
41
|
+
/** Click / Enter / Space on an enabled day. */
|
|
42
|
+
onSelect?: (date: Date) => void;
|
|
43
|
+
/** Focus moved (keyboard or pointer). The host shifts `month` when it leaves the view. */
|
|
44
|
+
onFocusDate?: (date: Date) => void;
|
|
45
|
+
/** Pointer over a day (null on leave) — drives the range hover preview. */
|
|
46
|
+
onHover?: (date: Date | null) => void;
|
|
47
|
+
/** Extra words for a day's accessible name ("3 slots left"); the `day` snippet is visual only. */
|
|
48
|
+
describeDay?: (date: Date) => string | undefined;
|
|
49
|
+
/** Extra content under the day number (availability dots, counts). */
|
|
50
|
+
day?: Snippet<[MonthGridDay]>;
|
|
51
|
+
class?: string;
|
|
52
|
+
}
|
|
53
|
+
declare const MonthGrid: import("svelte").Component<MonthGridProps, {}, "">;
|
|
54
|
+
type MonthGrid = ReturnType<typeof MonthGrid>;
|
|
55
|
+
export default MonthGrid;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar grid core — the pure half of every jatui date surface.
|
|
3
|
+
*
|
|
4
|
+
* CalendarPicker, DatePicker, DateRangePicker (calendar mode), BookingScheduler
|
|
5
|
+
* and EventCalendar's month switcher all render the same month panel. This
|
|
6
|
+
* module owns everything about that panel that is not markup, so it can be
|
|
7
|
+
* unit-tested without a DOM:
|
|
8
|
+
*
|
|
9
|
+
* - Local-calendar date math on plain `Date`s (always local midnight) and the
|
|
10
|
+
* ISO `YYYY-MM-DD` strings jatui uses at its edges (native `<input
|
|
11
|
+
* type="date">` values, `AvailabilityCounts` keys).
|
|
12
|
+
* - Locale-aware week start (`Intl.Locale#getWeekInfo`, with a region table
|
|
13
|
+
* fallback for engines that lack it).
|
|
14
|
+
* - The month matrix (leading/trailing days from the adjacent months).
|
|
15
|
+
* - The APG date-grid keyboard model: arrows, Home/End, PageUp/PageDown
|
|
16
|
+
* (+Shift for years), clamped to min/max, mirrored for RTL.
|
|
17
|
+
* - Range state: normalising, membership, band edges, day counts, presets.
|
|
18
|
+
* - Typed entry: parse and format in the locale's own field order, so en-US
|
|
19
|
+
* reads MM/DD/YYYY and en-GB DD/MM/YYYY (never a hardcoded order).
|
|
20
|
+
*
|
|
21
|
+
* No `@internationalized/date`: everything here is plain `Date` + `Intl`.
|
|
22
|
+
*/
|
|
23
|
+
/** 0 = Sunday … 6 = Saturday (the `Date#getDay` numbering). */
|
|
24
|
+
export type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
25
|
+
/** An inclusive date range. Either end may be open while a range is being picked. */
|
|
26
|
+
export interface DateRange {
|
|
27
|
+
start: Date | null;
|
|
28
|
+
end: Date | null;
|
|
29
|
+
}
|
|
30
|
+
/** The ISO-string form of a range — what DateRangePicker binds. */
|
|
31
|
+
export interface IsoDateRange {
|
|
32
|
+
start: string | null;
|
|
33
|
+
end: string | null;
|
|
34
|
+
}
|
|
35
|
+
/** Local midnight of `d`. */
|
|
36
|
+
export declare function startOfDay(d: Date): Date;
|
|
37
|
+
export declare function isSameDay(a: Date | null | undefined, b: Date | null | undefined): boolean;
|
|
38
|
+
export declare function isSameMonth(a: Date, b: Date): boolean;
|
|
39
|
+
/** `d` plus `n` calendar days (DST-safe: works on the date fields, not ms). */
|
|
40
|
+
export declare function addDays(d: Date, n: number): Date;
|
|
41
|
+
/**
|
|
42
|
+
* `d` plus `n` months, clamping the day so Jan 31 + 1 month is Feb 28/29
|
|
43
|
+
* rather than rolling into March.
|
|
44
|
+
*/
|
|
45
|
+
export declare function addMonths(d: Date, n: number): Date;
|
|
46
|
+
export declare function startOfMonth(d: Date): Date;
|
|
47
|
+
export declare function endOfMonth(d: Date): Date;
|
|
48
|
+
export declare function daysInMonth(year: number, month: number): number;
|
|
49
|
+
/** Whole calendar days from `a` to `b` (positive when `b` is later). */
|
|
50
|
+
export declare function dayDiff(a: Date, b: Date): number;
|
|
51
|
+
/** First day of the week containing `d`. */
|
|
52
|
+
export declare function startOfWeek(d: Date, weekStart?: Weekday): Date;
|
|
53
|
+
export declare function endOfWeek(d: Date, weekStart?: Weekday): Date;
|
|
54
|
+
/** Compare by calendar day: negative, 0 or positive. */
|
|
55
|
+
export declare function compareDays(a: Date, b: Date): number;
|
|
56
|
+
/** Clamp a day into [min, max] (either bound optional). */
|
|
57
|
+
export declare function clampDay(d: Date, min?: Date | null, max?: Date | null): Date;
|
|
58
|
+
/** `YYYY-MM-DD` for the LOCAL calendar day (never `toISOString`, which shifts to UTC). */
|
|
59
|
+
export declare function toIsoDate(d: Date): string;
|
|
60
|
+
/**
|
|
61
|
+
* Parse `YYYY-MM-DD` as LOCAL midnight (`new Date('2026-03-04')` is UTC
|
|
62
|
+
* midnight, a day early west of Greenwich). Anything else, or an impossible
|
|
63
|
+
* date like 2026-02-30, returns null.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseIsoDate(s: string | null | undefined): Date | null;
|
|
66
|
+
/** Accept a Date, an ISO string or nothing; always hand back local midnight or null. */
|
|
67
|
+
export declare function toDay(v: Date | string | null | undefined): Date | null;
|
|
68
|
+
/**
|
|
69
|
+
* The first day of the week for `locale` (default: the runtime locale).
|
|
70
|
+
* `Intl.Locale#getWeekInfo` (or the older `weekInfo` getter) when the engine
|
|
71
|
+
* has it, else the CLDR region table above.
|
|
72
|
+
*/
|
|
73
|
+
export declare function weekStartForLocale(locale?: string): Weekday;
|
|
74
|
+
/** Resolve a `weekStart` prop: a number wins, `'locale'` (or undefined) asks the locale. */
|
|
75
|
+
export declare function resolveWeekStart(weekStart: Weekday | 'locale' | undefined, locale?: string): Weekday;
|
|
76
|
+
/** Weekday column labels in display order, e.g. `['Mo', 'Tu', …]` for a Monday start. */
|
|
77
|
+
export declare function weekdayLabels(weekStart?: Weekday, locale?: string, width?: 'narrow' | 'short' | 'long'): {
|
|
78
|
+
short: string;
|
|
79
|
+
long: string;
|
|
80
|
+
}[];
|
|
81
|
+
export declare function formatMonthYear(d: Date, locale?: string): string;
|
|
82
|
+
/** "Wednesday, March 4, 2026" — the accessible name of a day cell. */
|
|
83
|
+
export declare function formatLongDate(d: Date, locale?: string): string;
|
|
84
|
+
/** "Mar 4, 2026" — trigger labels and range chips. */
|
|
85
|
+
export declare function formatMediumDate(d: Date, locale?: string): string;
|
|
86
|
+
export interface MonthCell {
|
|
87
|
+
date: Date;
|
|
88
|
+
iso: string;
|
|
89
|
+
/** Belongs to the displayed month (false for leading/trailing days). */
|
|
90
|
+
inMonth: boolean;
|
|
91
|
+
/** 0-6 column in display order. */
|
|
92
|
+
column: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The weeks of `month` (any date inside it), each exactly seven cells, padded
|
|
96
|
+
* with days from the adjacent months. `fixedWeeks` always returns six rows so
|
|
97
|
+
* a panel never changes height between months.
|
|
98
|
+
*/
|
|
99
|
+
export declare function buildMonthMatrix(month: Date, weekStart?: Weekday, fixedWeeks?: boolean): MonthCell[][];
|
|
100
|
+
export interface GridKeyOptions {
|
|
101
|
+
weekStart?: Weekday;
|
|
102
|
+
min?: Date | null;
|
|
103
|
+
max?: Date | null;
|
|
104
|
+
/** Mirror Left/Right for right-to-left layouts. */
|
|
105
|
+
rtl?: boolean;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The day focus moves to for `key`, or null when the key is not a grid key.
|
|
109
|
+
*
|
|
110
|
+
* | Key | Move |
|
|
111
|
+
* |---|---|
|
|
112
|
+
* | ←/→ | ∓1 day (mirrored in RTL) |
|
|
113
|
+
* | ↑/↓ | ∓1 week |
|
|
114
|
+
* | Home/End | first/last day of the week |
|
|
115
|
+
* | PageUp/PageDown | ∓1 month (Shift: ∓1 year), day clamped to the month length |
|
|
116
|
+
*
|
|
117
|
+
* The result is clamped to [min, max]. Disabled dates stay focusable (APG:
|
|
118
|
+
* they are announced as unavailable rather than skipped), so the caller only
|
|
119
|
+
* gates selection.
|
|
120
|
+
*/
|
|
121
|
+
export declare function nextFocusDate(current: Date, key: string, opts?: GridKeyOptions & {
|
|
122
|
+
shiftKey?: boolean;
|
|
123
|
+
}): Date | null;
|
|
124
|
+
export interface DateRules {
|
|
125
|
+
min?: Date | string | null;
|
|
126
|
+
max?: Date | string | null;
|
|
127
|
+
/** Return false to disable a date. */
|
|
128
|
+
isDateEnabled?: (date: Date) => boolean;
|
|
129
|
+
/** Disable Saturdays and Sundays. */
|
|
130
|
+
weekdaysOnly?: boolean;
|
|
131
|
+
}
|
|
132
|
+
/** One predicate out of min/max/weekday/custom rules. */
|
|
133
|
+
export declare function makeIsDisabled(rules: DateRules): (date: Date) => boolean;
|
|
134
|
+
/** Order a range's ends (a range picked right-to-left is still start ≤ end). */
|
|
135
|
+
export declare function normalizeRange(range: DateRange): DateRange;
|
|
136
|
+
export declare function isInRange(d: Date, range: DateRange | null | undefined): boolean;
|
|
137
|
+
/** Inclusive day count of a complete range, else 0. */
|
|
138
|
+
export declare function rangeDayCount(range: DateRange | null | undefined): number;
|
|
139
|
+
/**
|
|
140
|
+
* How a cell paints inside a range band.
|
|
141
|
+
*
|
|
142
|
+
* `bandStart`/`bandEnd` round the band's own ends AND the row ends (the first
|
|
143
|
+
* and last column), so a range wrapping onto the next week reads as two
|
|
144
|
+
* capsules rather than one bar running off the grid edge.
|
|
145
|
+
*/
|
|
146
|
+
export interface RangeCellState {
|
|
147
|
+
inRange: boolean;
|
|
148
|
+
isStart: boolean;
|
|
149
|
+
isEnd: boolean;
|
|
150
|
+
bandStart: boolean;
|
|
151
|
+
bandEnd: boolean;
|
|
152
|
+
}
|
|
153
|
+
export declare function rangeCellState(d: Date, column: number, range: DateRange | null | undefined): RangeCellState;
|
|
154
|
+
/**
|
|
155
|
+
* The next range after clicking `date`, picking ends in order:
|
|
156
|
+
* no start (or a complete range) → start a new range; a start → close it
|
|
157
|
+
* (swapping when the second click is earlier).
|
|
158
|
+
*/
|
|
159
|
+
export declare function pickRangeDate(range: DateRange, date: Date): DateRange;
|
|
160
|
+
export declare function toIsoRange(range: DateRange): IsoDateRange;
|
|
161
|
+
export declare function fromIsoRange(range: IsoDateRange | null | undefined): DateRange;
|
|
162
|
+
export interface RangePreset {
|
|
163
|
+
/** Stable id (used for the active highlight). */
|
|
164
|
+
id: string;
|
|
165
|
+
label: string;
|
|
166
|
+
/** The range, or a function of "now" so presets stay current. `start: null` = open ("All time"). */
|
|
167
|
+
range: IsoDateRange | ((now: Date) => IsoDateRange);
|
|
168
|
+
}
|
|
169
|
+
/** Resolve a preset's range against `now`. */
|
|
170
|
+
export declare function resolvePreset(preset: RangePreset, now?: Date): IsoDateRange;
|
|
171
|
+
/** Today, Yesterday, Last 7 days, Last 30 days, This month, Last month, This year, Last year. */
|
|
172
|
+
export declare function defaultRangePresets(): RangePreset[];
|
|
173
|
+
/** The id of the preset `range` equals, if any. */
|
|
174
|
+
export declare function matchPreset(range: IsoDateRange, presets: RangePreset[], now?: Date): string | null;
|
|
175
|
+
type Field = 'year' | 'month' | 'day';
|
|
176
|
+
/** The locale's numeric field order, e.g. `['month','day','year']` for en-US. */
|
|
177
|
+
export declare function dateFieldOrder(locale?: string): Field[];
|
|
178
|
+
/** The separator the locale uses between numeric date fields ("/", ".", "-"). */
|
|
179
|
+
export declare function dateSeparator(locale?: string): string;
|
|
180
|
+
/** A placeholder hint, e.g. "MM/DD/YYYY" (en-US) or "DD.MM.YYYY" (de-DE). */
|
|
181
|
+
export declare function datePlaceholder(locale?: string): string;
|
|
182
|
+
/** Format for the typed-entry field: numeric, zero-padded, locale order. */
|
|
183
|
+
export declare function formatTypedDate(d: Date | null, locale?: string): string;
|
|
184
|
+
/**
|
|
185
|
+
* Parse what a person typed. Accepts ISO `YYYY-MM-DD` anywhere, else three
|
|
186
|
+
* numeric groups in the locale's field order with any separator. Two-digit
|
|
187
|
+
* years land in 2000–2099. Impossible dates (31/02) return null.
|
|
188
|
+
*/
|
|
189
|
+
export declare function parseTypedDate(text: string, locale?: string): Date | null;
|
|
190
|
+
export type CalendarAccent = 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error' | 'neutral';
|
|
191
|
+
/**
|
|
192
|
+
* Inline custom properties that re-tint a month panel. `'primary'` (and
|
|
193
|
+
* anything unknown) returns '' so the jatui selection tokens apply as-is.
|
|
194
|
+
* The old `accentColor` prop interpolated `bg-${accentColor}` classes, which
|
|
195
|
+
* Tailwind's scanner never emits; this replaces it with token vars.
|
|
196
|
+
*/
|
|
197
|
+
export declare function calendarAccentStyle(accent: string | undefined): string;
|
|
198
|
+
/**
|
|
199
|
+
* "Mar 4 – 10, 2026" via `Intl.DateTimeFormat#formatRange` (which collapses
|
|
200
|
+
* the shared month/year), "From Mar 4, 2026" / "Until …" for open ends, '' for
|
|
201
|
+
* an empty range.
|
|
202
|
+
*/
|
|
203
|
+
export declare function formatRangeLabel(range: DateRange, locale?: string): string;
|
|
204
|
+
/** The default calendar-mode presets plus the "All time" (open) sentinel. */
|
|
205
|
+
export declare function defaultRangePresetsWithAll(): RangePreset[];
|
|
206
|
+
/**
|
|
207
|
+
* The first visible month after focus moves to `date`, for `months` panels
|
|
208
|
+
* starting at `viewMonth`: unchanged while the day is on screen, otherwise
|
|
209
|
+
* the minimum shift that brings it back (it lands in the first panel when
|
|
210
|
+
* moving backwards, the last when moving forwards).
|
|
211
|
+
*/
|
|
212
|
+
export declare function followFocus(viewMonth: Date, date: Date, months?: number): Date;
|
|
213
|
+
/** Whether the panels may step back / forward without leaving [min, max]. */
|
|
214
|
+
export declare function canStepMonth(viewMonth: Date, dir: -1 | 1, opts?: {
|
|
215
|
+
min?: Date | null;
|
|
216
|
+
max?: Date | null;
|
|
217
|
+
months?: number;
|
|
218
|
+
}): boolean;
|
|
219
|
+
export {};
|