@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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://badgen.net/npm/v/@nomideusz/svelte-calendar)](https://www.npmjs.com/package/@nomideusz/svelte-calendar) [![license](https://badgen.net/badge/license/MIT/blue)](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-and-drop, timezones, a headless API, an embeddable web component, and a smart auto-theme that adapts to any page.
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
- const adapter = createMemoryAdapter([
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 it: eight built-in views and drag-and-drop with live previews, out of the box. 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).
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. Opens when `view` is unset (the first registered view). The examples here, the demo, the widget and `createCalendar` open on `week-planner`. |
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` | Currently always `null`: size to the container |
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. Existing events stay visible and clickable, which suits holidays or fully booked 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
- <div class="cal-pills cal-pills--labels" role="radiogroup" aria-label={L.viewMode}>
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
- <div class="cal-pills" role="radiogroup" aria-label={L.viewMode}>
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) {
@@ -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
  /**
@@ -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 String(h);
113
- }
114
- if (h === 0)
115
- return '12a';
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 [String(h), '00'];
127
- return [String(h % 12 || 12), h < 12 ? 'am' : 'pm'];
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
- const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
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(new Date(dayMs).getDate());
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-accent, #1d4fd7);
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
  }
@@ -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. Re-probe once the document settles. A probe during initial load can
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, dayNum, isAllDay } from "../../core/time.js";
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">{dayNum(dayMs)}</span>
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-accent, #1d4fd7);
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, dayNum, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
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: dayNum(ms),
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-accent, #1d4fd7);
799
+ outline: 2px solid var(--dt-text, #0f0f0f);
800
800
  outline-offset: -2px;
801
- border-radius: 4px;
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-accent, #1d4fd7);
912
- border-radius: 4px;
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-accent, #1d4fd7);
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-accent, #1d4fd7);
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-accent, #1d4fd7);
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.dayNum}</span>
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-accent, #1d4fd7);
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
- /* Hit-slop: 44px effective touch target */
537
- .mw-ev-more::before {
538
- content: '';
539
- position: absolute;
540
- left: 0;
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-accent, #1d4fd7);
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.dayNum}
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-accent, #1d4fd7);
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-accent, #1d4fd7);
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.dayNum}</span>
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-accent, #1d4fd7);
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-accent, #1d4fd7);
1484
+ outline: 2px solid var(--dt-text, #0f0f0f);
1485
1485
  outline-offset: -2px;
1486
1486
  }
1487
1487