@nomideusz/svelte-calendar 1.4.0 → 1.4.1
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/README.md +31 -8
- package/dist/calendar/Calendar.svelte +33 -2
- package/dist/core/locale.d.ts +5 -0
- package/dist/core/locale.js +68 -17
- package/dist/primitives/DayHeader.svelte +2 -2
- package/dist/primitives/EmptySlot.svelte +1 -1
- package/dist/theme/auto.js +16 -1
- package/dist/views/agenda/AgendaDay.svelte +13 -4
- package/dist/views/agenda/AgendaWeek.svelte +8 -8
- package/dist/views/mobile/MobileDay.svelte +2 -2
- package/dist/views/mobile/MobileWeek.svelte +11 -14
- package/dist/views/month/MonthGrid.svelte +7 -6
- package/dist/views/planner/PlannerScroll.svelte +4 -4
- package/dist/views/planner/PlannerWeek.svelte +4 -4
- package/package.json +1 -1
- package/widget/widget.js +145 -65
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@nomideusz/svelte-calendar) [](https://github.com/nomideusz/svelte-calendar/blob/main/LICENSE)
|
|
4
4
|
|
|
5
|
-
A themeable **Svelte 5** calendar: day and week **Planner** time grids, a multi-week **Scroll** view, **Agenda** lists, a **Month** grid and touch-first **Mobile** layouts, with drag
|
|
5
|
+
A themeable **Svelte 5** calendar: day and week **Planner** time grids, a multi-week **Scroll** view, **Agenda** lists, a **Month** grid and touch-first **Mobile** layouts, with drag to move, resize and create events, timezones, a headless API, an embeddable web component, and a smart auto-theme that adapts to any page.
|
|
6
6
|
|
|
7
7
|
**[Live demo → svelte-calendar.xyz](https://svelte-calendar.xyz/)**
|
|
8
8
|
|
|
@@ -28,24 +28,37 @@ Requirements:
|
|
|
28
28
|
const today = new Date();
|
|
29
29
|
const at = (hour) => new Date(today.getFullYear(), today.getMonth(), today.getDate(), hour);
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
let events = $state([
|
|
32
32
|
{ id: '1', title: 'Team Sync', start: at(9), end: at(10) },
|
|
33
33
|
{ id: '2', title: 'Lunch', start: at(12), end: at(13) },
|
|
34
34
|
]);
|
|
35
|
+
// A new adapter whenever the events change; the calendar reloads without blanking
|
|
36
|
+
const adapter = $derived(createMemoryAdapter(events));
|
|
37
|
+
|
|
38
|
+
// Drag on empty time (or press Enter on a day column) to create
|
|
39
|
+
function create({ start, end }) {
|
|
40
|
+
events = [...events, { id: crypto.randomUUID(), title: 'New event', start, end }];
|
|
41
|
+
}
|
|
42
|
+
// Drag an event to move it, or its edge to resize it
|
|
43
|
+
function move(event, start, end) {
|
|
44
|
+
events = events.map((e) => (e.id === event.id ? { ...e, start, end } : e));
|
|
45
|
+
}
|
|
35
46
|
</script>
|
|
36
47
|
|
|
37
|
-
<Calendar {adapter} view="week-planner" />
|
|
48
|
+
<Calendar {adapter} view="week-planner" oneventcreate={create} oneventmove={move} />
|
|
38
49
|
```
|
|
39
50
|
|
|
40
|
-
That's
|
|
51
|
+
That's a working, editable calendar. The calendar stores nothing on create: without `oneventcreate`, dragging on empty time does nothing. Moves and resizes write to the adapter by themselves, and `oneventmove` keeps your own data in step, so a move survives the next new adapter. The default `auto` theme probes your page's background, accent color and fonts, so the calendar adapts to any design. Events without a `color` draw in ink; to color them automatically, give the adapter a [`palette`](#event-colors).
|
|
41
52
|
|
|
42
53
|
## Views
|
|
43
54
|
|
|
55
|
+
Without `view` the calendar opens on `day-planner`, the first registered view. The Quick Start, the demo, the widget and `createCalendar` open on `week-planner`, so pass `view="week-planner"` for the week grid.
|
|
56
|
+
|
|
44
57
|
The default registry (`defaultViews`) holds eight views. Six are the ones you choose with `view`; the two `*-mobile` views are what the others swap to in a narrow container (see [Mobile](#mobile)), so the demo's view list shows six.
|
|
45
58
|
|
|
46
59
|
| View ID | Mode | What it is |
|
|
47
60
|
|---------|------|------------|
|
|
48
|
-
| `day-planner` | day | Vertical time grid, one column.
|
|
61
|
+
| `day-planner` | day | Vertical time grid, one column. The default when `view` is unset. |
|
|
49
62
|
| `week-planner` | week | The same time grid, one column per day (`days`, default 7). |
|
|
50
63
|
| `week-scroll` | week | Weeks stacked in one infinite vertical scroller (Hey-Calendar style): each chip sits at the height of its start time, month names run down the side. Drag to move, drop onto a day; it scrolls under a drag. |
|
|
51
64
|
| `day-agenda` | day | Live list for one day: now / up next / done. |
|
|
@@ -112,7 +125,7 @@ The component receives `CalendarViewProps` (plus your `props`); declare only the
|
|
|
112
125
|
|------|------|-------|
|
|
113
126
|
| `events` | `TimelineEvent[]` | Everything loaded for the current range |
|
|
114
127
|
| `style` | `string` | The active `--dt-*` theme string |
|
|
115
|
-
| `height` | `number \| null` |
|
|
128
|
+
| `height` | `number \| null` | Always `null`: the view fills its container, sized by the Calendar's own [`height`](#display) (default `600`) |
|
|
116
129
|
| `mode` | `'day' \| 'week' \| 'month'` | |
|
|
117
130
|
| `mondayStart` | `boolean` | |
|
|
118
131
|
| `locale` | `string \| undefined` | |
|
|
@@ -320,7 +333,7 @@ Disable specific dates:
|
|
|
320
333
|
/>
|
|
321
334
|
```
|
|
322
335
|
|
|
323
|
-
Disabled dates refuse creating or moving events into those days.
|
|
336
|
+
Disabled dates refuse creating or moving events into those days, which suits holidays or fully booked days. In the planner, agenda and mobile views their existing events stay visible and clickable; `month-grid` dims a disabled day, and its chips ignore clicks.
|
|
324
337
|
|
|
325
338
|
Custom day headers and hover previews:
|
|
326
339
|
|
|
@@ -1057,7 +1070,7 @@ Every `<Calendar>` prop (`CalendarProps`), grouped by what it controls.
|
|
|
1057
1070
|
| `minDuration` | `number` | — | Minimum duration in minutes; clamps creates and resizes |
|
|
1058
1071
|
| `maxDuration` | `number` | — | Maximum duration in minutes; clamps creates and resizes |
|
|
1059
1072
|
| `blockedSlots` | `BlockedSlot[]` | — | `{ day?, start, end, label? }` hour ranges that can't be booked (hatched in planner views) |
|
|
1060
|
-
| `disabledDates` | `Date[]` | — | Dates that refuse creates/moves; existing events stay visible and clickable |
|
|
1073
|
+
| `disabledDates` | `Date[]` | — | Dates that refuse creates/moves; existing events stay visible and clickable (dimmed and inert in `month-grid`) |
|
|
1061
1074
|
|
|
1062
1075
|
### View and navigation
|
|
1063
1076
|
|
|
@@ -1127,6 +1140,16 @@ Every `<Calendar>` prop (`CalendarProps`), grouped by what it controls.
|
|
|
1127
1140
|
| `oneventhover` | `(event) => void` | — | Pointer enters an event |
|
|
1128
1141
|
| `onerror` | `(error: Error) => void` | — | Load and commit failures |
|
|
1129
1142
|
|
|
1143
|
+
## Troubleshooting
|
|
1144
|
+
|
|
1145
|
+
- **Dragging on empty time does nothing.** Creating needs an `oneventcreate` handler (and no `readOnly`); see the [Quick Start](#quick-start).
|
|
1146
|
+
- **A created event doesn't appear.** The calendar stores nothing on create: add the event to your data and hand the Calendar a new adapter (`$derived(createMemoryAdapter(events))`).
|
|
1147
|
+
- **Moved events jump back after the next change.** A new adapter is built from your array, so write each move into it in `oneventmove`.
|
|
1148
|
+
- **The `auto` theme picked the wrong colours.** It probes the host page around the calendar, and probes again when `<html>`/`<body>` change class, style or `data-theme`, when the system scheme flips and when a background transition ends. Steer it with [`autoTheme`](#smart-auto-theme), or set `theme` to a preset or your own tokens to opt out.
|
|
1149
|
+
- **Events are all one colour.** Since 1.4 events without a `color` draw in ink; give the adapter a [`palette`](#event-colors).
|
|
1150
|
+
- **The calendar stops at 600px.** That's the default `height`; pass a number, or `'auto'` to grow with the content.
|
|
1151
|
+
- **The phone layout shows on a wide page (or the desktop one in a narrow column).** The switch reads the calendar's container, not the window; force it with `mobile={false}` or `mobile={true}`.
|
|
1152
|
+
|
|
1130
1153
|
## Versioning
|
|
1131
1154
|
|
|
1132
1155
|
The package follows [semver](https://semver.org/) from 1.0:
|
|
@@ -623,6 +623,28 @@ function handleShortcuts(e) {
|
|
|
623
623
|
else viewState.prev();
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
|
+
// Each pill group is one tab stop (radio-group pattern): arrows (reading
|
|
627
|
+
// direction), Home and End move the selection and focus with it.
|
|
628
|
+
function pillKeys(e) {
|
|
629
|
+
const radios = [...e.currentTarget.querySelectorAll("[role=\"radio\"]")];
|
|
630
|
+
const i = radios.indexOf(e.target);
|
|
631
|
+
if (i < 0) return;
|
|
632
|
+
const rtl = getComputedStyle(e.currentTarget).direction === "rtl";
|
|
633
|
+
const step = {
|
|
634
|
+
ArrowRight: rtl ? -1 : 1,
|
|
635
|
+
ArrowLeft: rtl ? 1 : -1,
|
|
636
|
+
ArrowDown: 1,
|
|
637
|
+
ArrowUp: -1
|
|
638
|
+
};
|
|
639
|
+
let n;
|
|
640
|
+
if (e.key === "Home") n = 0;
|
|
641
|
+
else if (e.key === "End") n = radios.length - 1;
|
|
642
|
+
else if (e.key in step) n = (i + step[e.key] + radios.length) % radios.length;
|
|
643
|
+
else return;
|
|
644
|
+
e.preventDefault();
|
|
645
|
+
radios[n].focus();
|
|
646
|
+
radios[n].click();
|
|
647
|
+
}
|
|
626
648
|
/** Header context for custom header snippet */
|
|
627
649
|
const headerCtx = $derived({
|
|
628
650
|
dateLabel,
|
|
@@ -671,7 +693,9 @@ const navCtx = $derived({
|
|
|
671
693
|
<span class="cal-sr" role="status" aria-live="polite" aria-atomic="true">{noteText || dateLabel}</span>
|
|
672
694
|
{#if showModePills && !useMobile && labelsForMode.length > 1}
|
|
673
695
|
<!-- View-type switcher (e.g. Planner ↔ Agenda) for the current mode -->
|
|
674
|
-
|
|
696
|
+
{@const activeLabel = labelsForMode.includes(activeView?.label ?? '') ? activeView?.label : labelsForMode[0]}
|
|
697
|
+
<!-- svelte-ignore a11y_interactive_supports_focus -- the checked radio is the tab stop; the group only routes its arrow keys -->
|
|
698
|
+
<div class="cal-pills cal-pills--labels" role="radiogroup" aria-label={L.viewMode} onkeydown={pillKeys}>
|
|
675
699
|
{#each labelsForMode as label (label)}
|
|
676
700
|
<button
|
|
677
701
|
type="button"
|
|
@@ -679,6 +703,7 @@ const navCtx = $derived({
|
|
|
679
703
|
class:cal-pill--active={activeView?.label === label}
|
|
680
704
|
role="radio"
|
|
681
705
|
aria-checked={activeView?.label === label}
|
|
706
|
+
tabindex={label === activeLabel ? 0 : -1}
|
|
682
707
|
onclick={() => switchLabel(label)}
|
|
683
708
|
>
|
|
684
709
|
{viewLabel(label)}
|
|
@@ -687,7 +712,9 @@ const navCtx = $derived({
|
|
|
687
712
|
</div>
|
|
688
713
|
{/if}
|
|
689
714
|
{#if showModePills && modes.length > 1}
|
|
690
|
-
|
|
715
|
+
{@const activeMode = modes.includes(viewState.mode) ? viewState.mode : modes[0]}
|
|
716
|
+
<!-- svelte-ignore a11y_interactive_supports_focus -- the checked radio is the tab stop; the group only routes its arrow keys -->
|
|
717
|
+
<div class="cal-pills" role="radiogroup" aria-label={L.viewMode} onkeydown={pillKeys}>
|
|
691
718
|
{#each modes as g (g)}
|
|
692
719
|
<button
|
|
693
720
|
type="button"
|
|
@@ -695,6 +722,7 @@ const navCtx = $derived({
|
|
|
695
722
|
class:cal-pill--active={viewState.mode === g}
|
|
696
723
|
role="radio"
|
|
697
724
|
aria-checked={viewState.mode === g}
|
|
725
|
+
tabindex={g === activeMode ? 0 : -1}
|
|
698
726
|
onclick={() => switchMode(g)}
|
|
699
727
|
>
|
|
700
728
|
{g === 'day' ? L.day : g === 'week' ? L.week : L.month}
|
|
@@ -952,6 +980,9 @@ const navCtx = $derived({
|
|
|
952
980
|
.cal-hd {
|
|
953
981
|
--cal-ctl-h: 44px;
|
|
954
982
|
}
|
|
983
|
+
.cal-pill {
|
|
984
|
+
min-width: 44px;
|
|
985
|
+
}
|
|
955
986
|
}
|
|
956
987
|
|
|
957
988
|
@media (prefers-reduced-motion: reduce) {
|
package/dist/core/locale.d.ts
CHANGED
|
@@ -98,6 +98,10 @@ export declare function setDefaultLocale(tag: string): void;
|
|
|
98
98
|
/** Get the current default locale */
|
|
99
99
|
export declare function getDefaultLocale(): string;
|
|
100
100
|
export declare function is24HourLocale(locale?: string): boolean;
|
|
101
|
+
/** A number in the locale's own digits ("٧" in ar-SA), optionally zero-padded. */
|
|
102
|
+
export declare function fmtNum(n: number, locale?: string, minDigits?: number): string;
|
|
103
|
+
/** Day-of-month in the locale's digits: "19", "١٩". */
|
|
104
|
+
export declare function fmtDayNum(ms: number, locale?: string): string;
|
|
101
105
|
/** Format hour index (0-23) as compact label: 12h ("12a", "1p") or 24h ("0", "13") */
|
|
102
106
|
export declare function fmtH(h: number, locale?: string): string;
|
|
103
107
|
/**
|
|
@@ -137,6 +141,7 @@ export declare function fmtWeekRange(weekStartMs: number, locale?: string, weekE
|
|
|
137
141
|
*
|
|
138
142
|
* 12-hour locales → "9a", "12:30p"
|
|
139
143
|
* 24-hour locales → "9:00", "14:30"
|
|
144
|
+
* other 12-hour locales → Intl's own, in the locale's digits ("٧:٠٠ ص")
|
|
140
145
|
*/
|
|
141
146
|
export declare function fmtTime(d: Date, locale?: string): string;
|
|
142
147
|
/**
|
package/dist/core/locale.js
CHANGED
|
@@ -106,16 +106,61 @@ export function is24HourLocale(locale) {
|
|
|
106
106
|
hourCycleCache.set(loc, is24);
|
|
107
107
|
return is24;
|
|
108
108
|
}
|
|
109
|
+
// ─── Numerals ───────────────────────────────────────────
|
|
110
|
+
const numCache = new Map();
|
|
111
|
+
/** A number in the locale's own digits ("٧" in ar-SA), optionally zero-padded. */
|
|
112
|
+
export function fmtNum(n, locale, minDigits = 1) {
|
|
113
|
+
const loc = locale ?? defaultLocale;
|
|
114
|
+
const key = `${loc}|${minDigits}`;
|
|
115
|
+
let f = numCache.get(key);
|
|
116
|
+
if (!f) {
|
|
117
|
+
f = new Intl.NumberFormat(loc, { useGrouping: false, minimumIntegerDigits: minDigits });
|
|
118
|
+
numCache.set(key, f);
|
|
119
|
+
}
|
|
120
|
+
return f.format(n);
|
|
121
|
+
}
|
|
122
|
+
/** Day-of-month in the locale's digits: "19", "١٩". */
|
|
123
|
+
export function fmtDayNum(ms, locale) {
|
|
124
|
+
return fmtNum(new Date(ms).getDate(), locale);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Whether the compact "9a" / "12:30p" style fits the locale: a 12-hour clock
|
|
128
|
+
* whose day periods are Latin AM/PM. Other 12-hour locales (ar "ص"/"م")
|
|
129
|
+
* get Intl's own day period instead.
|
|
130
|
+
*/
|
|
131
|
+
const compactCache = new Map();
|
|
132
|
+
function isCompact12h(locale) {
|
|
133
|
+
const loc = locale ?? defaultLocale;
|
|
134
|
+
if (is24HourLocale(loc))
|
|
135
|
+
return false;
|
|
136
|
+
let c = compactCache.get(loc);
|
|
137
|
+
if (c === undefined) {
|
|
138
|
+
const period = new Intl.DateTimeFormat(loc, { hour: 'numeric', hour12: true })
|
|
139
|
+
.formatToParts(new Date(2000, 0, 1, 9))
|
|
140
|
+
.find((p) => p.type === 'dayPeriod')?.value ?? '';
|
|
141
|
+
c = /^a\W*m\W*$/i.test(period);
|
|
142
|
+
compactCache.set(loc, c);
|
|
143
|
+
}
|
|
144
|
+
return c;
|
|
145
|
+
}
|
|
146
|
+
const dtfCache = new Map();
|
|
147
|
+
function dtf(locale, withMinutes) {
|
|
148
|
+
const loc = locale ?? defaultLocale;
|
|
149
|
+
const key = `${loc}|${withMinutes}`;
|
|
150
|
+
let f = dtfCache.get(key);
|
|
151
|
+
if (!f) {
|
|
152
|
+
f = new Intl.DateTimeFormat(loc, withMinutes ? { hour: 'numeric', minute: '2-digit' } : { hour: 'numeric' });
|
|
153
|
+
dtfCache.set(key, f);
|
|
154
|
+
}
|
|
155
|
+
return f;
|
|
156
|
+
}
|
|
109
157
|
/** Format hour index (0-23) as compact label: 12h ("12a", "1p") or 24h ("0", "13") */
|
|
110
158
|
export function fmtH(h, locale) {
|
|
111
|
-
if (is24HourLocale(locale))
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (h === 12)
|
|
117
|
-
return '12p';
|
|
118
|
-
return h < 12 ? h + 'a' : h - 12 + 'p';
|
|
159
|
+
if (is24HourLocale(locale))
|
|
160
|
+
return fmtNum(h, locale);
|
|
161
|
+
if (!isCompact12h(locale))
|
|
162
|
+
return dtf(locale, false).format(new Date(2000, 0, 1, h));
|
|
163
|
+
return fmtNum(h % 12 || 12, locale) + (h < 12 ? 'a' : 'p');
|
|
119
164
|
}
|
|
120
165
|
/**
|
|
121
166
|
* Hour index (0-23) split for a timetable gutter: the numeral and the small
|
|
@@ -123,8 +168,15 @@ export function fmtH(h, locale) {
|
|
|
123
168
|
*/
|
|
124
169
|
export function fmtHParts(h, locale) {
|
|
125
170
|
if (is24HourLocale(locale))
|
|
126
|
-
return [
|
|
127
|
-
|
|
171
|
+
return [fmtNum(h, locale), fmtNum(0, locale, 2)];
|
|
172
|
+
if (!isCompact12h(locale)) {
|
|
173
|
+
const parts = dtf(locale, false).formatToParts(new Date(2000, 0, 1, h));
|
|
174
|
+
return [
|
|
175
|
+
parts.find((p) => p.type === 'hour')?.value ?? fmtNum(h % 12 || 12, locale),
|
|
176
|
+
parts.find((p) => p.type === 'dayPeriod')?.value ?? '',
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
return [fmtNum(h % 12 || 12, locale), h < 12 ? 'am' : 'pm'];
|
|
128
180
|
}
|
|
129
181
|
/** Short weekday name for a timestamp: "Mon", "Tue", etc. */
|
|
130
182
|
export function weekdayShort(ms, locale) {
|
|
@@ -198,18 +250,17 @@ export function fmtWeekRange(weekStartMs, locale, weekEndMs) {
|
|
|
198
250
|
*
|
|
199
251
|
* 12-hour locales → "9a", "12:30p"
|
|
200
252
|
* 24-hour locales → "9:00", "14:30"
|
|
253
|
+
* other 12-hour locales → Intl's own, in the locale's digits ("٧:٠٠ ص")
|
|
201
254
|
*/
|
|
202
255
|
export function fmtTime(d, locale) {
|
|
203
|
-
if (is24HourLocale(locale)) {
|
|
204
|
-
const h = d.getHours();
|
|
205
|
-
const m = d.getMinutes();
|
|
206
|
-
return `${h}:${String(m).padStart(2, '0')}`;
|
|
207
|
-
}
|
|
208
256
|
const h = d.getHours();
|
|
209
257
|
const m = d.getMinutes();
|
|
258
|
+
if (is24HourLocale(locale))
|
|
259
|
+
return `${fmtNum(h, locale)}:${fmtNum(m, locale, 2)}`;
|
|
260
|
+
if (!isCompact12h(locale))
|
|
261
|
+
return dtf(locale, true).format(d);
|
|
210
262
|
const suffix = h >= 12 ? 'p' : 'a';
|
|
211
|
-
|
|
212
|
-
return `${h12}:${String(m).padStart(2, '0')}${suffix}`;
|
|
263
|
+
return `${fmtNum(h % 12 || 12, locale)}:${fmtNum(m, locale, 2)}${suffix}`;
|
|
213
264
|
}
|
|
214
265
|
/**
|
|
215
266
|
* Format the duration between two Dates as a compact string.
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
- Relative labels ("Today", "Tomorrow") for day views via fmtDay
|
|
6
6
|
- Short/long weekday + date number for week views
|
|
7
7
|
-->
|
|
8
|
-
<script lang="ts">import { weekdayShort, weekdayLong, monthShort, fmtDay } from "../core/locale.js";
|
|
8
|
+
<script lang="ts">import { weekdayShort, weekdayLong, monthShort, fmtDay, fmtDayNum } from "../core/locale.js";
|
|
9
9
|
import { sod } from "../core/time.js";
|
|
10
10
|
let { dayMs, todayMs = sod(Date.now()), format = "short", isToday = false, isPast = false } = $props();
|
|
11
|
-
const dayNum = $derived(
|
|
11
|
+
const dayNum = $derived(fmtDayNum(dayMs));
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
14
|
<div
|
|
@@ -53,7 +53,7 @@ function handleKeydown(e) {
|
|
|
53
53
|
background: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
|
|
54
54
|
}
|
|
55
55
|
.es:focus-visible {
|
|
56
|
-
outline: 2px solid var(--dt-
|
|
56
|
+
outline: 2px solid var(--dt-text, #0f0f0f);
|
|
57
57
|
outline-offset: 2px;
|
|
58
58
|
border-color: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
|
|
59
59
|
}
|
package/dist/theme/auto.js
CHANGED
|
@@ -588,7 +588,20 @@ export function observeHostTheme(el, callback, options = {}) {
|
|
|
588
588
|
attributes: true,
|
|
589
589
|
attributeFilter: ['class', 'style', 'data-theme', 'data-mode', 'color-scheme'],
|
|
590
590
|
});
|
|
591
|
-
// 3.
|
|
591
|
+
// 3. Hosts often crossfade their ground (`body { transition: background 300ms }`),
|
|
592
|
+
// so the rAF probe above reads a mid-transition colour and keeps the old
|
|
593
|
+
// host's light/dark. Re-probe when a colour transition on <html>/<body> ends.
|
|
594
|
+
const onTransitionEnd = (e) => {
|
|
595
|
+
const t = e.target;
|
|
596
|
+
if (t !== document.documentElement && t !== document.body)
|
|
597
|
+
return;
|
|
598
|
+
const prop = e.propertyName;
|
|
599
|
+
if (prop === undefined || /^(background|color)/.test(prop))
|
|
600
|
+
scheduleUpdate();
|
|
601
|
+
};
|
|
602
|
+
document.addEventListener('transitionend', onTransitionEnd);
|
|
603
|
+
document.addEventListener('animationend', onTransitionEnd);
|
|
604
|
+
// 4. Re-probe once the document settles. A probe during initial load can
|
|
592
605
|
// read pre-stylesheet values (wrong background → wrong light/dark call);
|
|
593
606
|
// late webfonts change computed font stacks. Both are one-shot events.
|
|
594
607
|
if (document.readyState !== 'complete') {
|
|
@@ -602,5 +615,7 @@ export function observeHostTheme(el, callback, options = {}) {
|
|
|
602
615
|
mql?.removeEventListener('change', onScheme);
|
|
603
616
|
window.removeEventListener('load', scheduleUpdate);
|
|
604
617
|
observer.disconnect();
|
|
618
|
+
document.removeEventListener('transitionend', onTransitionEnd);
|
|
619
|
+
document.removeEventListener('animationend', onTransitionEnd);
|
|
605
620
|
};
|
|
606
621
|
}
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* Clean numbered schedule list.
|
|
13
13
|
*/
|
|
14
14
|
import { createClock } from "../../core/clock.svelte.js";
|
|
15
|
-
import { sod, addDaysMs,
|
|
16
|
-
import { weekdayLong, monthLong } from "../../core/locale.js";
|
|
15
|
+
import { sod, addDaysMs, isAllDay } from "../../core/time.js";
|
|
16
|
+
import { weekdayLong, monthLong, fmtDayNum } from "../../core/locale.js";
|
|
17
17
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
18
18
|
import EventContent from "../shared/EventContent.svelte";
|
|
19
19
|
import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from "../shared/format.js";
|
|
@@ -136,7 +136,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
136
136
|
<!-- ─── In-view date header (only when the chrome doesn't label the day) ─── -->
|
|
137
137
|
{#if !hideDayHead}
|
|
138
138
|
<div class="ag-day-head" class:ag-day-head--today={!equalDays && isToday}>
|
|
139
|
-
<span class="ag-day-head-num">{
|
|
139
|
+
<span class="ag-day-head-num">{fmtDayNum(dayMs, locale)}</span>
|
|
140
140
|
{#if !equalDays && isToday}
|
|
141
141
|
<span class="ag-day-head-badge">{L.today}</span>
|
|
142
142
|
{:else if !equalDays && isTomorrow}
|
|
@@ -562,6 +562,15 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
562
562
|
color: var(--dt-btn-text, #fff);
|
|
563
563
|
cursor: pointer;
|
|
564
564
|
}
|
|
565
|
+
@media (pointer: coarse) {
|
|
566
|
+
.ag-allday-chip {
|
|
567
|
+
min-height: 44px;
|
|
568
|
+
}
|
|
569
|
+
.ag-q-done-item {
|
|
570
|
+
min-height: 44px;
|
|
571
|
+
align-items: center;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
565
574
|
.ag-allday-chip:hover,
|
|
566
575
|
.ag-allday-chip:active {
|
|
567
576
|
filter: brightness(1.08);
|
|
@@ -820,7 +829,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
820
829
|
}
|
|
821
830
|
.ag-q-done-toggle:focus-visible {
|
|
822
831
|
outline: none;
|
|
823
|
-
box-shadow: 0 0 0 2px var(--dt-
|
|
832
|
+
box-shadow: 0 0 0 2px var(--dt-text, #0f0f0f);
|
|
824
833
|
}
|
|
825
834
|
.ag-q-now-sub {
|
|
826
835
|
font: 400 12.5px/1.2 var(--dt-sans, system-ui, sans-serif);
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Answers: "What's coming up and when do I need to be ready?"
|
|
10
10
|
*/
|
|
11
11
|
import { createClock } from "../../core/clock.svelte.js";
|
|
12
|
-
import { sod, addDaysMs, startOfWeek,
|
|
13
|
-
import { weekdayLong, monthLong } from "../../core/locale.js";
|
|
12
|
+
import { sod, addDaysMs, startOfWeek, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
13
|
+
import { weekdayLong, monthLong, fmtDayNum } from "../../core/locale.js";
|
|
14
14
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
15
15
|
import EventContent from "../shared/EventContent.svelte";
|
|
16
16
|
import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from "../shared/format.js";
|
|
@@ -165,7 +165,7 @@ const weekDays = $derived.by(() => {
|
|
|
165
165
|
ms,
|
|
166
166
|
dayName: weekdayLong(ms, locale),
|
|
167
167
|
month: monthLong(ms, locale),
|
|
168
|
-
num:
|
|
168
|
+
num: fmtDayNum(ms, locale),
|
|
169
169
|
tier,
|
|
170
170
|
// A dateless weekly template (showDates=false) has no today to mark.
|
|
171
171
|
isToday: showDates && ms === todayMs,
|
|
@@ -796,9 +796,9 @@ const weekDays = $derived.by(() => {
|
|
|
796
796
|
}
|
|
797
797
|
.ag-wday-head--click:focus-visible,
|
|
798
798
|
.ag-wday-empty--click:focus-visible {
|
|
799
|
-
outline: 2px solid var(--dt-
|
|
799
|
+
outline: 2px solid var(--dt-text, #0f0f0f);
|
|
800
800
|
outline-offset: -2px;
|
|
801
|
-
border-radius:
|
|
801
|
+
border-radius: 0;
|
|
802
802
|
}
|
|
803
803
|
.ag-wday-head {
|
|
804
804
|
display: flex;
|
|
@@ -908,8 +908,8 @@ const weekDays = $derived.by(() => {
|
|
|
908
908
|
}
|
|
909
909
|
.ag-past-toggle:focus-visible {
|
|
910
910
|
outline: none;
|
|
911
|
-
box-shadow: 0 0 0 2px var(--dt-
|
|
912
|
-
border-radius:
|
|
911
|
+
box-shadow: 0 0 0 2px var(--dt-text, #0f0f0f);
|
|
912
|
+
border-radius: 0;
|
|
913
913
|
}
|
|
914
914
|
.ag-past-chevron {
|
|
915
915
|
transition: transform 120ms;
|
|
@@ -1081,7 +1081,7 @@ const weekDays = $derived.by(() => {
|
|
|
1081
1081
|
}
|
|
1082
1082
|
.ag-compact-more:focus-visible {
|
|
1083
1083
|
outline: none;
|
|
1084
|
-
box-shadow: 0 0 0 2px var(--dt-
|
|
1084
|
+
box-shadow: 0 0 0 2px var(--dt-text, #0f0f0f);
|
|
1085
1085
|
border-radius: 0;
|
|
1086
1086
|
}
|
|
1087
1087
|
|
|
@@ -793,7 +793,7 @@ $effect(() => {
|
|
|
793
793
|
}
|
|
794
794
|
.mb-allday-more:focus-visible {
|
|
795
795
|
outline: none;
|
|
796
|
-
box-shadow: 0 0 0 2px var(--dt-
|
|
796
|
+
box-shadow: 0 0 0 2px var(--dt-text, #0f0f0f);
|
|
797
797
|
}
|
|
798
798
|
|
|
799
799
|
/* ─── Grid ───────────────────────────────────────── */
|
|
@@ -811,7 +811,7 @@ $effect(() => {
|
|
|
811
811
|
.mb--auto .mb-grid { overflow-y: visible; }
|
|
812
812
|
.mb-grid:focus-visible {
|
|
813
813
|
outline: none;
|
|
814
|
-
box-shadow: inset 0 0 0 2px var(--dt-
|
|
814
|
+
box-shadow: inset 0 0 0 2px var(--dt-text, #0f0f0f);
|
|
815
815
|
}
|
|
816
816
|
|
|
817
817
|
.mb-grid-inner {
|
|
@@ -10,7 +10,7 @@ import EventContent from "../shared/EventContent.svelte";
|
|
|
10
10
|
import { createClock } from "../../core/clock.svelte.js";
|
|
11
11
|
import { sod, addDaysMs, isAllDay } from "../../core/time.js";
|
|
12
12
|
import { startOfWeek as sowFn } from "../../core/time.js";
|
|
13
|
-
import { fmtTime as _fmtTime, fmtWeekRange, weekdayShort } from "../../core/locale.js";
|
|
13
|
+
import { fmtTime as _fmtTime, fmtDayNum, fmtWeekRange, weekdayShort } from "../../core/locale.js";
|
|
14
14
|
import { dateLong } from "../shared/format.js";
|
|
15
15
|
import { createSwipe } from "./swipe.js";
|
|
16
16
|
let { mondayStart = true, locale, height = null, events = [], style = "", focusDate, oneventclick, selectedEventId = null } = $props();
|
|
@@ -207,7 +207,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
207
207
|
<div class="mw-date">
|
|
208
208
|
<span class="mw-day-name" class:mw-day-name--today={cell.isToday}>{cell.dayName}</span>
|
|
209
209
|
{#if showDates}
|
|
210
|
-
<span class="mw-day-num" class:mw-day-num--today={cell.isToday}>{cell.
|
|
210
|
+
<span class="mw-day-num" class:mw-day-num--today={cell.isToday}>{fmtDayNum(cell.ms, locale)}</span>
|
|
211
211
|
{/if}
|
|
212
212
|
</div>
|
|
213
213
|
|
|
@@ -231,7 +231,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
231
231
|
onclick={(e) => { e.stopPropagation(); oneventclick?.(ev); }}
|
|
232
232
|
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev); } }}
|
|
233
233
|
onpointerenter={() => oneventhover?.(ev)}
|
|
234
|
-
aria-label="{ev.title}{statusText(ev)}, {evTimeLabel(ev)}"
|
|
234
|
+
aria-label="{ev.title}{statusText(ev)}, {dateLong(cell.ms, locale)}, {evTimeLabel(ev)}"
|
|
235
235
|
>
|
|
236
236
|
<span class="mw-ev-stripe"></span>
|
|
237
237
|
<div class="mw-ev-body">
|
|
@@ -247,6 +247,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
247
247
|
type="button"
|
|
248
248
|
class="mw-ev-more"
|
|
249
249
|
aria-expanded={expandedDays.has(cell.ms)}
|
|
250
|
+
aria-label="{expandedDays.has(cell.ms) ? L.showLess : L.nMore(cell.totalCount - MAX_EVENTS)}, {dateLong(cell.ms, locale)}"
|
|
250
251
|
onclick={() => toggleExpand(cell.ms)}
|
|
251
252
|
>
|
|
252
253
|
{expandedDays.has(cell.ms) ? L.showLess : L.nMore(cell.totalCount - MAX_EVENTS)}
|
|
@@ -356,7 +357,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
356
357
|
}
|
|
357
358
|
.mw-row-target:focus-visible {
|
|
358
359
|
outline: none;
|
|
359
|
-
box-shadow: inset 0 0 0 2px var(--dt-
|
|
360
|
+
box-shadow: inset 0 0 0 2px var(--dt-text, #0f0f0f);
|
|
360
361
|
}
|
|
361
362
|
|
|
362
363
|
/* ─── Date column ────────────────────────────────── */
|
|
@@ -533,19 +534,15 @@ function handleDayKeydown(e, dayMs) {
|
|
|
533
534
|
pointer-events: auto;
|
|
534
535
|
-webkit-tap-highlight-color: transparent;
|
|
535
536
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
right: 0;
|
|
542
|
-
top: 50%;
|
|
543
|
-
transform: translateY(-50%);
|
|
544
|
-
height: 44px;
|
|
537
|
+
@media (pointer: coarse) {
|
|
538
|
+
.mw-ev-more {
|
|
539
|
+
min-height: 44px;
|
|
540
|
+
min-width: 44px;
|
|
541
|
+
}
|
|
545
542
|
}
|
|
546
543
|
.mw-ev-more:focus-visible {
|
|
547
544
|
outline: none;
|
|
548
|
-
box-shadow: 0 0 0 2px var(--dt-
|
|
545
|
+
box-shadow: 0 0 0 2px var(--dt-text, #0f0f0f);
|
|
549
546
|
}
|
|
550
547
|
|
|
551
548
|
/* ─── Chevron ────────────────────────────────────── */
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
<script lang="ts">import { useCalendarContext } from "../shared/context.svelte.js";
|
|
10
10
|
import { createClock } from "../../core/clock.svelte.js";
|
|
11
11
|
import { DAY_MS, sod, addDaysMs, isAllDay } from "../../core/time.js";
|
|
12
|
-
import { fmtTime, weekdayShort } from "../../core/locale.js";
|
|
12
|
+
import { fmtTime, fmtDayNum, weekdayShort } from "../../core/locale.js";
|
|
13
|
+
import { dateLong } from "../shared/format.js";
|
|
13
14
|
let { events = [], style = "", height = null, locale, focusDate, oneventclick, selectedEventId = null } = $props();
|
|
14
15
|
const ctx = useCalendarContext();
|
|
15
16
|
const L = $derived(ctx.labels);
|
|
@@ -248,7 +249,7 @@ function chipTime(e) {
|
|
|
248
249
|
onkeydown={(e) => cellKeydown(e, cell)}
|
|
249
250
|
>
|
|
250
251
|
<span class="mg-daynum" class:mg-daynum--today={cell.isToday}>
|
|
251
|
-
{cell.
|
|
252
|
+
{fmtDayNum(cell.ms, locale)}
|
|
252
253
|
</span>
|
|
253
254
|
<!-- Phones: dots are a picture of the day; the cell (its label counts
|
|
254
255
|
the events) is the one tap and tab target. -->
|
|
@@ -282,7 +283,7 @@ function chipTime(e) {
|
|
|
282
283
|
class:mg-chip--cancelled={ev.status === 'cancelled'}
|
|
283
284
|
style:--mg-chip-color={ev.color ?? (isAllDay(ev) ? 'var(--dt-accent)' : 'var(--dt-event, var(--dt-accent))')}
|
|
284
285
|
title={ev.title}
|
|
285
|
-
aria-label="{ev.title}{chipTime(ev) ? `, ${chipTime(ev)}` : ''}"
|
|
286
|
+
aria-label="{ev.title}, {dateLong(cell.ms, locale)}{chipTime(ev) ? `, ${chipTime(ev)}` : ''}"
|
|
286
287
|
onclick={(e) => {
|
|
287
288
|
e.stopPropagation();
|
|
288
289
|
oneventclick?.(ev, e.currentTarget.getBoundingClientRect());
|
|
@@ -304,7 +305,7 @@ function chipTime(e) {
|
|
|
304
305
|
type="button"
|
|
305
306
|
class="mg-more"
|
|
306
307
|
aria-expanded={ondayclick ? undefined : false}
|
|
307
|
-
aria-label={L.nMore(cell.overflow)}
|
|
308
|
+
aria-label="{L.nMore(cell.overflow)}, {dateLong(cell.ms, locale)}"
|
|
308
309
|
onclick={(e) => overflowClick(e, cell)}
|
|
309
310
|
>
|
|
310
311
|
{L.nMore(cell.overflow)}
|
|
@@ -417,7 +418,7 @@ function chipTime(e) {
|
|
|
417
418
|
background: var(--dt-hover, rgba(15, 15, 15, 0.04));
|
|
418
419
|
}
|
|
419
420
|
.mg-cell:focus-visible {
|
|
420
|
-
outline: 2px solid var(--dt-
|
|
421
|
+
outline: 2px solid var(--dt-text, #0f0f0f);
|
|
421
422
|
outline-offset: -2px;
|
|
422
423
|
}
|
|
423
424
|
|
|
@@ -515,7 +516,7 @@ function chipTime(e) {
|
|
|
515
516
|
color: var(--dt-text);
|
|
516
517
|
}
|
|
517
518
|
.mg-more:focus-visible {
|
|
518
|
-
box-shadow: 0 0 0 2px var(--dt-
|
|
519
|
+
box-shadow: 0 0 0 2px var(--dt-text, #0f0f0f);
|
|
519
520
|
outline: none;
|
|
520
521
|
}
|
|
521
522
|
|
|
@@ -22,7 +22,7 @@ import EventContent from "../shared/EventContent.svelte";
|
|
|
22
22
|
import { createClock } from "../../core/clock.svelte.js";
|
|
23
23
|
import { HOUR_MS, sod, addDaysMs, diffDays } from "../../core/time.js";
|
|
24
24
|
import { startOfWeek as sowFn, isAllDay, segmentForDay } from "../../core/time.js";
|
|
25
|
-
import { weekdayShort, monthLong, fmtTime as _fmtTime } from "../../core/locale.js";
|
|
25
|
+
import { weekdayShort, monthLong, fmtDayNum, fmtTime as _fmtTime } from "../../core/locale.js";
|
|
26
26
|
import { createChipFit } from "../shared/chip-fit.svelte.js";
|
|
27
27
|
let { mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, onexternaldrop, selectedEventId = null, readOnly = false, visibleHours } = $props();
|
|
28
28
|
const ctx = useCalendarContext();
|
|
@@ -873,7 +873,7 @@ function onCellKeydown(e, ms) {
|
|
|
873
873
|
<div class="wg-cell-hd" class:wg-cell-hd--today={day.isToday}>
|
|
874
874
|
<span class="wg-day-wd">{weekdayShort(day.ms, locale)}</span>
|
|
875
875
|
{#if showDates}
|
|
876
|
-
<span class="wg-day-num">{day.
|
|
876
|
+
<span class="wg-day-num">{fmtDayNum(day.ms, locale)}</span>
|
|
877
877
|
{/if}
|
|
878
878
|
</div>
|
|
879
879
|
|
|
@@ -1476,12 +1476,12 @@ function onCellKeydown(e, ms) {
|
|
|
1476
1476
|
|
|
1477
1477
|
.wg-ev-more:focus-visible {
|
|
1478
1478
|
outline: none;
|
|
1479
|
-
box-shadow: 0 0 0 2px var(--dt-
|
|
1479
|
+
box-shadow: 0 0 0 2px var(--dt-text, #0f0f0f);
|
|
1480
1480
|
}
|
|
1481
1481
|
|
|
1482
1482
|
/* ─── Focus-visible ──────────────────────────────── */
|
|
1483
1483
|
.wg-cell:focus-visible {
|
|
1484
|
-
outline: 2px solid var(--dt-
|
|
1484
|
+
outline: 2px solid var(--dt-text, #0f0f0f);
|
|
1485
1485
|
outline-offset: -2px;
|
|
1486
1486
|
}
|
|
1487
1487
|
|