@nomideusz/svelte-calendar 0.9.0 → 0.11.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/README.md +15 -7
- package/dist/calendar/Calendar.svelte +202 -60
- package/dist/calendar/Calendar.svelte.d.ts +7 -0
- package/dist/core/locale.d.ts +15 -1
- package/dist/core/locale.js +9 -2
- package/dist/theme/auto.js +27 -6
- package/dist/theme/presets.d.ts +4 -4
- package/dist/theme/presets.js +11 -7
- package/dist/views/agenda/AgendaDay.svelte +327 -126
- package/dist/views/agenda/AgendaWeek.svelte +345 -121
- package/dist/views/mobile/MobileDay.svelte +416 -213
- package/dist/views/mobile/MobileWeek.svelte +142 -74
- package/dist/views/mobile/swipe.d.ts +29 -0
- package/dist/views/mobile/swipe.js +73 -0
- package/dist/views/month/MonthGrid.svelte +200 -31
- package/dist/views/planner/PlannerDay.svelte +155 -80
- package/dist/views/planner/PlannerWeek.svelte +1155 -630
- package/dist/views/planner/PlannerWeek.svelte.d.ts +2 -0
- package/dist/views/shared/context.svelte.d.ts +3 -0
- package/dist/views/shared/context.svelte.js +13 -9
- package/dist/views/shared/format.d.ts +2 -1
- package/dist/views/shared/format.js +8 -5
- package/dist/widget/CalendarWidget.svelte +33 -5
- package/dist/widget/CalendarWidget.svelte.d.ts +16 -2
- package/dist/widget/widget.d.ts +9 -0
- package/dist/widget/widget.js +59 -13
- package/package.json +1 -1
- package/widget/widget.js +3364 -2783
- package/widget/svelte-calendar.css +0 -3054
package/README.md
CHANGED
|
@@ -657,19 +657,25 @@ The `locale` prop controls date/time formatting (BCP 47):
|
|
|
657
657
|
|
|
658
658
|
`setDefaultLocale('de-DE')` sets the locale globally instead of per component (`getDefaultLocale()` reads it back, `is24HourLocale(tag)` tells you how times will format).
|
|
659
659
|
|
|
660
|
-
Override UI labels
|
|
660
|
+
Override UI labels per calendar with the `labels` prop — it's reactive (swap it to switch language live) and instance-scoped (two calendars on one page can carry different languages):
|
|
661
661
|
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
setLabels({
|
|
662
|
+
```svelte
|
|
663
|
+
<Calendar {adapter} locale="de-DE" labels={{
|
|
666
664
|
today: 'Heute', day: 'Tag', week: 'Woche',
|
|
667
665
|
noEvents: 'Keine Termine',
|
|
668
666
|
nMore: (n) => `+${n} weitere`,
|
|
669
|
-
}
|
|
667
|
+
}} />
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
Or set labels globally before mount:
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
import { setLabels } from '@nomideusz/svelte-calendar';
|
|
674
|
+
|
|
675
|
+
setLabels({ today: 'Heute', day: 'Tag', week: 'Woche' });
|
|
670
676
|
```
|
|
671
677
|
|
|
672
|
-
Call `resetLabels()` to restore English defaults (`defaultLabels` exports them; `getLabels()` reads the active set).
|
|
678
|
+
The `labels` prop merges over the global set. Call `resetLabels()` to restore English defaults (`defaultLabels` exports them; `getLabels()` reads the active set). Note: global `setLabels()` calls after mount don't re-render already-mounted calendars — use the `labels` prop for dynamic language switching. Standalone primitives (`DayHeader`, `EventBlock`, …) always read the global set.
|
|
673
679
|
|
|
674
680
|
<details>
|
|
675
681
|
<summary>All label keys</summary>
|
|
@@ -784,6 +790,8 @@ Drop into any HTML page — no build tools needed. Registers a `<day-calendar>`
|
|
|
784
790
|
></day-calendar>
|
|
785
791
|
```
|
|
786
792
|
|
|
793
|
+
The widget renders inside shadow DOM: host-page CSS (resets, theme stylesheets) cannot affect the calendar and calendar styles never leak out, while the `auto` theme still probes the host page's colors and fonts across the shadow boundary.
|
|
794
|
+
|
|
787
795
|
| Attribute | Description |
|
|
788
796
|
|-----------|-------------|
|
|
789
797
|
| `api` | REST endpoint — fetched as `GET {api}?start=...&end=...` |
|
|
@@ -23,7 +23,7 @@ import { createViewState } from "../engine/view-state.svelte.js";
|
|
|
23
23
|
import { createSelection } from "../engine/selection.svelte.js";
|
|
24
24
|
import { createDragState } from "../engine/drag.svelte.js";
|
|
25
25
|
import { onMount } from "svelte";
|
|
26
|
-
import { getLabels } from "../core/locale.js";
|
|
26
|
+
import { getLabels, fmtWeekRange } from "../core/locale.js";
|
|
27
27
|
import { auto } from "../theme/presets.js";
|
|
28
28
|
import { probeHostTheme, observeHostTheme } from "../theme/auto.js";
|
|
29
29
|
import Planner from "../views/planner/Planner.svelte";
|
|
@@ -52,6 +52,7 @@ let {
|
|
|
52
52
|
borderRadius = 12,
|
|
53
53
|
dir,
|
|
54
54
|
locale,
|
|
55
|
+
labels: labelsProp,
|
|
55
56
|
readOnly = false,
|
|
56
57
|
visibleHours,
|
|
57
58
|
initialDate,
|
|
@@ -95,11 +96,17 @@ function handleEventClick(ev) {
|
|
|
95
96
|
selection.select(ev.id);
|
|
96
97
|
oneventclick?.(ev);
|
|
97
98
|
}
|
|
98
|
-
let containerWidth = $state(
|
|
99
|
+
let containerWidth = $state(
|
|
100
|
+
typeof window !== "undefined" && window.matchMedia?.(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches ? window.innerWidth : 0
|
|
101
|
+
);
|
|
99
102
|
const isMobileContainer = $derived(containerWidth > 0 && containerWidth < MOBILE_BREAKPOINT);
|
|
100
103
|
const useMobile = $derived(
|
|
101
104
|
mobileProp === "auto" ? isMobileContainer : Boolean(mobileProp)
|
|
102
105
|
);
|
|
106
|
+
const HEADER_STACK_BREAKPOINT = 520;
|
|
107
|
+
const stackHeader = $derived(
|
|
108
|
+
useMobile && containerWidth > 0 && containerWidth < HEADER_STACK_BREAKPOINT
|
|
109
|
+
);
|
|
103
110
|
let calEl = $state();
|
|
104
111
|
let probedTheme = $state("");
|
|
105
112
|
const needsProbe = $derived(theme === auto && autoTheme !== false);
|
|
@@ -233,7 +240,7 @@ setContext("calendar", {
|
|
|
233
240
|
return oneventhover;
|
|
234
241
|
},
|
|
235
242
|
get ondayclick() {
|
|
236
|
-
return ondayclick;
|
|
243
|
+
return ondayclick ?? defaultDayClick;
|
|
237
244
|
},
|
|
238
245
|
get timezone() {
|
|
239
246
|
return timezone;
|
|
@@ -287,6 +294,9 @@ setContext("calendar", {
|
|
|
287
294
|
get compact() {
|
|
288
295
|
return compact;
|
|
289
296
|
},
|
|
297
|
+
get labels() {
|
|
298
|
+
return mergedLabels;
|
|
299
|
+
},
|
|
290
300
|
// Load range (read/write)
|
|
291
301
|
get loadRange() {
|
|
292
302
|
return viewLoadRange;
|
|
@@ -347,6 +357,13 @@ const dateLabel = $derived.by(() => {
|
|
|
347
357
|
day: "numeric"
|
|
348
358
|
});
|
|
349
359
|
}
|
|
360
|
+
if (viewState.mode === "week") {
|
|
361
|
+
return fmtWeekRange(
|
|
362
|
+
viewState.range.start.getTime(),
|
|
363
|
+
locale,
|
|
364
|
+
viewState.range.end.getTime() - 1
|
|
365
|
+
);
|
|
366
|
+
}
|
|
350
367
|
return viewState.focusDate.toLocaleDateString(locale, {
|
|
351
368
|
month: "long",
|
|
352
369
|
year: "numeric"
|
|
@@ -356,19 +373,77 @@ const modes = $derived.by(() => {
|
|
|
356
373
|
const g = new Set(desktopViews.map((v) => v.mode));
|
|
357
374
|
return ["day", "week", "month"].filter((key) => g.has(key));
|
|
358
375
|
});
|
|
359
|
-
const
|
|
376
|
+
const mergedLabels = $derived(
|
|
377
|
+
labelsProp ? { ...getLabels(), ...labelsProp } : getLabels()
|
|
378
|
+
);
|
|
379
|
+
const L = $derived(mergedLabels);
|
|
380
|
+
let lastViewLabel = $state(void 0);
|
|
381
|
+
$effect(() => {
|
|
382
|
+
const current = views.find((v) => v.id === viewState.view);
|
|
383
|
+
if (current && current.mode !== "month") lastViewLabel = current.label;
|
|
384
|
+
});
|
|
360
385
|
function switchMode(g) {
|
|
361
|
-
const
|
|
362
|
-
const
|
|
386
|
+
const currentView = desktopViews.find((v) => v.id === viewState.view) ?? activeView;
|
|
387
|
+
const preferredLabel = currentView?.mode === "month" ? lastViewLabel ?? currentView?.label : currentView?.label;
|
|
388
|
+
const match = desktopViews.find((v) => v.mode === g && v.label === preferredLabel);
|
|
363
389
|
const fallback = desktopViews.find((v) => v.mode === g);
|
|
364
390
|
const target = match ?? fallback;
|
|
365
391
|
if (target) viewState.setView(target.id);
|
|
366
392
|
}
|
|
393
|
+
const labelsForMode = $derived.by(() => {
|
|
394
|
+
const seen = [];
|
|
395
|
+
for (const v of desktopViews) {
|
|
396
|
+
if (v.mode === viewState.mode && !seen.includes(v.label)) seen.push(v.label);
|
|
397
|
+
}
|
|
398
|
+
return seen;
|
|
399
|
+
});
|
|
400
|
+
function switchLabel(label) {
|
|
401
|
+
const target = desktopViews.find((v) => v.mode === viewState.mode && v.label === label);
|
|
402
|
+
if (target) viewState.setView(target.id);
|
|
403
|
+
}
|
|
404
|
+
const defaultDayClick = $derived.by(() => {
|
|
405
|
+
const target = desktopViews.find((v) => v.mode === "day" && v.label === lastViewLabel) ?? desktopViews.find((v) => v.mode === "day");
|
|
406
|
+
if (!target) return void 0;
|
|
407
|
+
return (date) => {
|
|
408
|
+
viewState.setFocusDate(date);
|
|
409
|
+
viewState.setView(target.id);
|
|
410
|
+
};
|
|
411
|
+
});
|
|
367
412
|
const viewIncludesToday = $derived.by(() => {
|
|
368
|
-
const now = Date
|
|
413
|
+
const now = /* @__PURE__ */ new Date();
|
|
414
|
+
if (viewState.mode === "month") {
|
|
415
|
+
const f = viewState.focusDate;
|
|
416
|
+
return f.getMonth() === now.getMonth() && f.getFullYear() === now.getFullYear();
|
|
417
|
+
}
|
|
369
418
|
const { start, end } = viewState.range;
|
|
370
|
-
return now >= start.getTime() && now < end.getTime();
|
|
419
|
+
return now.getTime() >= start.getTime() && now.getTime() < end.getTime();
|
|
420
|
+
});
|
|
421
|
+
const resolvedDir = $derived.by(() => {
|
|
422
|
+
if (dir) return dir;
|
|
423
|
+
if (!locale) return void 0;
|
|
424
|
+
try {
|
|
425
|
+
const info = new Intl.Locale(locale);
|
|
426
|
+
const direction = info.textInfo?.direction ?? info.getTextInfo?.().direction;
|
|
427
|
+
return direction === "rtl" ? "rtl" : void 0;
|
|
428
|
+
} catch {
|
|
429
|
+
return void 0;
|
|
430
|
+
}
|
|
371
431
|
});
|
|
432
|
+
function handleShortcuts(e) {
|
|
433
|
+
if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey) return;
|
|
434
|
+
const t = e.target;
|
|
435
|
+
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
|
|
436
|
+
if (e.key === "t" || e.key === "T") {
|
|
437
|
+
e.preventDefault();
|
|
438
|
+
viewState.goToday();
|
|
439
|
+
} else if (e.key === "ArrowLeft") {
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
viewState.prev();
|
|
442
|
+
} else if (e.key === "ArrowRight") {
|
|
443
|
+
e.preventDefault();
|
|
444
|
+
viewState.next();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
372
447
|
const headerCtx = $derived({
|
|
373
448
|
dateLabel,
|
|
374
449
|
mode: viewState.mode,
|
|
@@ -390,6 +465,7 @@ const navCtx = $derived({
|
|
|
390
465
|
});
|
|
391
466
|
</script>
|
|
392
467
|
|
|
468
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -- shortcut keys (t/←/→) act on events bubbling from the calendar's focusable controls; the region itself is not a tab stop -->
|
|
393
469
|
<div
|
|
394
470
|
class="cal"
|
|
395
471
|
bind:this={calEl}
|
|
@@ -397,8 +473,10 @@ const navCtx = $derived({
|
|
|
397
473
|
class:cal--auto={heightProp === 'auto'}
|
|
398
474
|
role="region"
|
|
399
475
|
aria-label={L.calendar}
|
|
400
|
-
|
|
476
|
+
aria-busy={store.loading || undefined}
|
|
477
|
+
dir={resolvedDir}
|
|
401
478
|
lang={locale}
|
|
479
|
+
onkeydown={handleShortcuts}
|
|
402
480
|
>
|
|
403
481
|
<!-- ─── Custom header snippet (replaces all chrome) ─── -->
|
|
404
482
|
{#if headerSnippet}
|
|
@@ -406,23 +484,18 @@ const navCtx = $derived({
|
|
|
406
484
|
|
|
407
485
|
<!-- ─── Mobile header (flow layout, no absolute) ─── -->
|
|
408
486
|
{:else if useMobile && (showNavigation || (showModePills && modes.length > 1) || dateLabel)}
|
|
409
|
-
|
|
487
|
+
{@const titleBelow = stackHeader && !!dateLabel}
|
|
488
|
+
<div class="cal-m-hd" class:cal-m-hd--stack={stackHeader} class:cal-m-hd--titled={titleBelow}>
|
|
410
489
|
<div class="cal-m-left">
|
|
411
|
-
{#if navigationSnippet}
|
|
412
|
-
{@render navigationSnippet(navCtx)}
|
|
413
|
-
{:else if showNavigation}
|
|
414
|
-
<button class="cal-m-nav" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : viewState.mode === 'month' ? L.previousMonth : L.previousWeek}>
|
|
415
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M10 3 5 8l5 5"/></svg>
|
|
416
|
-
</button>
|
|
417
|
-
{/if}
|
|
418
|
-
|
|
419
490
|
{#if showModePills && modes.length > 1}
|
|
420
|
-
<div class="cal-m-pills" role="
|
|
421
|
-
{#each modes as g}
|
|
491
|
+
<div class="cal-m-pills" role="radiogroup" aria-label={L.viewMode}>
|
|
492
|
+
{#each modes as g (g)}
|
|
422
493
|
<button
|
|
494
|
+
type="button"
|
|
423
495
|
class="cal-m-pill"
|
|
424
496
|
class:cal-m-pill--active={viewState.mode === g}
|
|
425
|
-
|
|
497
|
+
role="radio"
|
|
498
|
+
aria-checked={viewState.mode === g}
|
|
426
499
|
onclick={() => switchMode(g)}
|
|
427
500
|
>
|
|
428
501
|
{g === 'day' ? L.day : g === 'week' ? L.week : L.month}
|
|
@@ -432,23 +505,36 @@ const navCtx = $derived({
|
|
|
432
505
|
{/if}
|
|
433
506
|
</div>
|
|
434
507
|
|
|
435
|
-
|
|
508
|
+
{#if !titleBelow}
|
|
509
|
+
<span class="cal-m-title" role="status" aria-live="polite" aria-atomic="true">{dateLabel}</span>
|
|
510
|
+
{/if}
|
|
436
511
|
|
|
437
512
|
<div class="cal-m-right">
|
|
438
|
-
{#if
|
|
439
|
-
|
|
513
|
+
{#if navigationSnippet}
|
|
514
|
+
{@render navigationSnippet(navCtx)}
|
|
515
|
+
{:else if showNavigation}
|
|
516
|
+
<!-- Always rendered (disabled on today) so navigating never shifts layout -->
|
|
517
|
+
<button
|
|
518
|
+
type="button"
|
|
519
|
+
class="cal-m-today"
|
|
520
|
+
onclick={() => viewState.goToday()}
|
|
521
|
+
disabled={viewIncludesToday}
|
|
522
|
+
title={L.goToToday}
|
|
523
|
+
>
|
|
524
|
+
{L.today}
|
|
525
|
+
</button>
|
|
526
|
+
<button type="button" class="cal-m-nav" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : viewState.mode === 'month' ? L.previousMonth : L.previousWeek}>
|
|
527
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M10 3 5 8l5 5"/></svg>
|
|
528
|
+
</button>
|
|
529
|
+
<button type="button" class="cal-m-nav" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : viewState.mode === 'month' ? L.nextMonth : L.nextWeek}>
|
|
440
530
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
|
|
441
531
|
</button>
|
|
442
532
|
{/if}
|
|
443
533
|
</div>
|
|
444
534
|
</div>
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
<div class="cal-m-today-bar">
|
|
449
|
-
<button class="cal-m-today" onclick={() => viewState.goToday()}>
|
|
450
|
-
{L.today}
|
|
451
|
-
</button>
|
|
535
|
+
{#if titleBelow}
|
|
536
|
+
<div class="cal-m-titlebar">
|
|
537
|
+
<span class="cal-m-title" role="status" aria-live="polite" aria-atomic="true">{dateLabel}</span>
|
|
452
538
|
</div>
|
|
453
539
|
{/if}
|
|
454
540
|
|
|
@@ -459,28 +545,52 @@ const navCtx = $derived({
|
|
|
459
545
|
{#if navigationSnippet}
|
|
460
546
|
{@render navigationSnippet(navCtx)}
|
|
461
547
|
{:else if showNavigation}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
548
|
+
<!-- Always rendered (disabled on today) so navigating never shifts the centered title -->
|
|
549
|
+
<button
|
|
550
|
+
type="button"
|
|
551
|
+
class="cal-hd-today"
|
|
552
|
+
onclick={() => viewState.goToday()}
|
|
553
|
+
disabled={viewIncludesToday}
|
|
554
|
+
title={L.goToToday}
|
|
555
|
+
>
|
|
556
|
+
{L.today}
|
|
557
|
+
</button>
|
|
558
|
+
<button type="button" class="cal-hd-btn" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : viewState.mode === 'month' ? L.previousMonth : L.previousWeek}>
|
|
468
559
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M10 3 5 8l5 5"/></svg>
|
|
469
560
|
</button>
|
|
470
|
-
<button class="cal-hd-btn" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : viewState.mode === 'month' ? L.nextMonth : L.nextWeek}>
|
|
561
|
+
<button type="button" class="cal-hd-btn" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : viewState.mode === 'month' ? L.nextMonth : L.nextWeek}>
|
|
471
562
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
|
|
472
563
|
</button>
|
|
473
564
|
{/if}
|
|
474
565
|
</div>
|
|
475
|
-
<span class="cal-hd-title">{dateLabel}</span>
|
|
566
|
+
<span class="cal-hd-title" role="status" aria-live="polite" aria-atomic="true">{dateLabel}</span>
|
|
476
567
|
<div class="cal-hd-side cal-hd-side--end">
|
|
568
|
+
{#if showModePills && labelsForMode.length > 1}
|
|
569
|
+
<!-- View-type switcher (e.g. Planner ↔ Agenda) for the current mode -->
|
|
570
|
+
<div class="cal-pills cal-pills--labels" role="radiogroup" aria-label={L.viewMode}>
|
|
571
|
+
{#each labelsForMode as label (label)}
|
|
572
|
+
<button
|
|
573
|
+
type="button"
|
|
574
|
+
class="cal-pill"
|
|
575
|
+
class:cal-pill--active={activeView?.label === label}
|
|
576
|
+
role="radio"
|
|
577
|
+
aria-checked={activeView?.label === label}
|
|
578
|
+
onclick={() => switchLabel(label)}
|
|
579
|
+
>
|
|
580
|
+
{label}
|
|
581
|
+
</button>
|
|
582
|
+
{/each}
|
|
583
|
+
</div>
|
|
584
|
+
{/if}
|
|
477
585
|
{#if showModePills && modes.length > 1}
|
|
478
|
-
<div class="cal-pills" role="
|
|
479
|
-
{#each modes as g}
|
|
586
|
+
<div class="cal-pills" role="radiogroup" aria-label={L.viewMode}>
|
|
587
|
+
{#each modes as g (g)}
|
|
480
588
|
<button
|
|
589
|
+
type="button"
|
|
481
590
|
class="cal-pill"
|
|
482
591
|
class:cal-pill--active={viewState.mode === g}
|
|
483
|
-
|
|
592
|
+
role="radio"
|
|
593
|
+
aria-checked={viewState.mode === g}
|
|
484
594
|
onclick={() => switchMode(g)}
|
|
485
595
|
>
|
|
486
596
|
{g === 'day' ? L.day : g === 'week' ? L.week : L.month}
|
|
@@ -543,6 +653,7 @@ const navCtx = $derived({
|
|
|
543
653
|
/* ── Desktop header ── */
|
|
544
654
|
.cal-hd {
|
|
545
655
|
display: flex;
|
|
656
|
+
flex-wrap: wrap;
|
|
546
657
|
align-items: center;
|
|
547
658
|
gap: 8px;
|
|
548
659
|
padding: 8px 12px;
|
|
@@ -611,10 +722,14 @@ const navCtx = $derived({
|
|
|
611
722
|
transition: background 120ms, color 120ms, border-color 120ms;
|
|
612
723
|
}
|
|
613
724
|
|
|
614
|
-
.cal-hd-today:hover {
|
|
725
|
+
.cal-hd-today:hover:not(:disabled) {
|
|
615
726
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
616
727
|
border-color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
617
728
|
}
|
|
729
|
+
.cal-hd-today:disabled {
|
|
730
|
+
opacity: 0.45;
|
|
731
|
+
cursor: default;
|
|
732
|
+
}
|
|
618
733
|
|
|
619
734
|
.cal-pills {
|
|
620
735
|
display: flex;
|
|
@@ -637,7 +752,9 @@ const navCtx = $derived({
|
|
|
637
752
|
transition: background 100ms, color 100ms;
|
|
638
753
|
}
|
|
639
754
|
|
|
640
|
-
|
|
755
|
+
/* :not(--active) — the hover rule otherwise outranks the active color,
|
|
756
|
+
and iOS keeps :hover stuck after a tap (dark text on the accent). */
|
|
757
|
+
.cal-pill:hover:not(.cal-pill--active) {
|
|
641
758
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
642
759
|
}
|
|
643
760
|
|
|
@@ -685,6 +802,13 @@ const navCtx = $derived({
|
|
|
685
802
|
100% { transform: translateX(100%); }
|
|
686
803
|
}
|
|
687
804
|
|
|
805
|
+
@media (prefers-reduced-motion: reduce) {
|
|
806
|
+
.cal-loading {
|
|
807
|
+
animation: none;
|
|
808
|
+
background: var(--dt-accent-dim, rgba(37, 99, 235, 0.12));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
688
812
|
/* ── Mobile header (flow layout) ── */
|
|
689
813
|
.cal-m-hd {
|
|
690
814
|
display: flex;
|
|
@@ -696,6 +820,26 @@ const navCtx = $derived({
|
|
|
696
820
|
min-height: 44px;
|
|
697
821
|
}
|
|
698
822
|
|
|
823
|
+
/* Narrow containers: the date label moves to its own row (.cal-m-titlebar),
|
|
824
|
+
so the controls row spreads pills and nav to the edges. */
|
|
825
|
+
.cal-m-hd--stack {
|
|
826
|
+
justify-content: space-between;
|
|
827
|
+
}
|
|
828
|
+
.cal-m-hd--titled {
|
|
829
|
+
border-bottom: none;
|
|
830
|
+
padding-bottom: 2px;
|
|
831
|
+
}
|
|
832
|
+
.cal-m-titlebar {
|
|
833
|
+
display: flex;
|
|
834
|
+
justify-content: center;
|
|
835
|
+
padding: 0 8px 8px;
|
|
836
|
+
border-bottom: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
837
|
+
flex-shrink: 0;
|
|
838
|
+
}
|
|
839
|
+
.cal-m-titlebar .cal-m-title {
|
|
840
|
+
flex: 0 1 auto;
|
|
841
|
+
}
|
|
842
|
+
|
|
699
843
|
.cal-m-left,
|
|
700
844
|
.cal-m-right {
|
|
701
845
|
display: flex;
|
|
@@ -712,8 +856,8 @@ const navCtx = $derived({
|
|
|
712
856
|
display: flex;
|
|
713
857
|
align-items: center;
|
|
714
858
|
justify-content: center;
|
|
715
|
-
width:
|
|
716
|
-
height:
|
|
859
|
+
width: 40px;
|
|
860
|
+
height: 40px;
|
|
717
861
|
border: none;
|
|
718
862
|
background: transparent;
|
|
719
863
|
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
@@ -749,15 +893,15 @@ const navCtx = $derived({
|
|
|
749
893
|
background: transparent;
|
|
750
894
|
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
751
895
|
cursor: pointer;
|
|
752
|
-
font: 600
|
|
753
|
-
padding:
|
|
896
|
+
font: 600 12px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
897
|
+
padding: 9px 12px;
|
|
754
898
|
border-radius: 6px;
|
|
755
899
|
letter-spacing: 0.04em;
|
|
756
900
|
text-transform: uppercase;
|
|
757
901
|
transition: background 100ms, color 100ms;
|
|
758
902
|
-webkit-tap-highlight-color: transparent;
|
|
759
903
|
}
|
|
760
|
-
.cal-m-pill:hover {
|
|
904
|
+
.cal-m-pill:hover:not(.cal-m-pill--active) {
|
|
761
905
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
762
906
|
}
|
|
763
907
|
.cal-m-pill--active {
|
|
@@ -776,19 +920,13 @@ const navCtx = $derived({
|
|
|
776
920
|
min-width: 0;
|
|
777
921
|
}
|
|
778
922
|
|
|
779
|
-
.cal-m-today-bar {
|
|
780
|
-
display: flex;
|
|
781
|
-
justify-content: center;
|
|
782
|
-
padding: 12px 8px 6px;
|
|
783
|
-
flex-shrink: 0;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
923
|
.cal-m-today {
|
|
787
|
-
font: 600
|
|
924
|
+
font: 600 12px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
788
925
|
color: var(--dt-accent, #2563eb);
|
|
789
926
|
background: color-mix(in srgb, var(--dt-accent, #2563eb) 10%, transparent);
|
|
790
927
|
border: none;
|
|
791
|
-
|
|
928
|
+
min-height: 40px;
|
|
929
|
+
padding: 5px 12px;
|
|
792
930
|
border-radius: 6px;
|
|
793
931
|
cursor: pointer;
|
|
794
932
|
white-space: nowrap;
|
|
@@ -798,12 +936,16 @@ const navCtx = $derived({
|
|
|
798
936
|
-webkit-tap-highlight-color: transparent;
|
|
799
937
|
flex-shrink: 0;
|
|
800
938
|
}
|
|
801
|
-
.cal-m-today:hover {
|
|
939
|
+
.cal-m-today:hover:not(:disabled) {
|
|
802
940
|
background: color-mix(in srgb, var(--dt-accent, #2563eb) 18%, transparent);
|
|
803
941
|
}
|
|
804
|
-
.cal-m-today:active {
|
|
942
|
+
.cal-m-today:active:not(:disabled) {
|
|
805
943
|
background: color-mix(in srgb, var(--dt-accent, #2563eb) 25%, transparent);
|
|
806
944
|
}
|
|
945
|
+
.cal-m-today:disabled {
|
|
946
|
+
opacity: 0.45;
|
|
947
|
+
cursor: default;
|
|
948
|
+
}
|
|
807
949
|
.cal-m-today:focus-visible {
|
|
808
950
|
outline: 2px solid color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
|
|
809
951
|
outline-offset: 2px;
|
|
@@ -2,6 +2,7 @@ import { type Component, type Snippet } from 'svelte';
|
|
|
2
2
|
import type { CalendarAdapter } from '../adapters/types.js';
|
|
3
3
|
import type { CalendarViewId } from '../engine/view-state.svelte.js';
|
|
4
4
|
import type { TimelineEvent, BlockedSlot } from '../core/types.js';
|
|
5
|
+
import { type CalendarLabels } from '../core/locale.js';
|
|
5
6
|
import type { AutoThemeOptions } from '../theme/auto.js';
|
|
6
7
|
/** One view registration */
|
|
7
8
|
export interface CalendarView {
|
|
@@ -42,6 +43,12 @@ interface Props {
|
|
|
42
43
|
dir?: 'ltr' | 'rtl' | 'auto';
|
|
43
44
|
/** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
|
|
44
45
|
locale?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Per-instance UI label overrides (merged over the global `setLabels()` set).
|
|
48
|
+
* Reactive: changing the prop re-renders all label text, and two calendars
|
|
49
|
+
* on one page can carry different languages.
|
|
50
|
+
*/
|
|
51
|
+
labels?: Partial<CalendarLabels>;
|
|
45
52
|
/** Read-only mode: disables drag, resize, empty-slot creation */
|
|
46
53
|
readOnly?: boolean;
|
|
47
54
|
/** Visible hour range: [startHour, endHour). Crops the grid to these hours. */
|
package/dist/core/locale.d.ts
CHANGED
|
@@ -40,6 +40,14 @@ export interface CalendarLabels {
|
|
|
40
40
|
nextMonth: string;
|
|
41
41
|
calendar: string;
|
|
42
42
|
viewMode: string;
|
|
43
|
+
/** Event status: cancelled */
|
|
44
|
+
cancelled: string;
|
|
45
|
+
/** Event status: tentative */
|
|
46
|
+
tentative: string;
|
|
47
|
+
/** Event status: fully booked */
|
|
48
|
+
full: string;
|
|
49
|
+
/** Event status: limited availability */
|
|
50
|
+
limited: string;
|
|
43
51
|
dayNavigation: string;
|
|
44
52
|
weekNavigation: string;
|
|
45
53
|
dayPlanner: string;
|
|
@@ -64,6 +72,12 @@ export interface CalendarLabels {
|
|
|
64
72
|
dayNOfTotal: (current: number, total: number) => string;
|
|
65
73
|
/** e.g. "75% complete" */
|
|
66
74
|
percentComplete: (pct: number) => string;
|
|
75
|
+
/** Relative countdown, e.g. "in 45m" */
|
|
76
|
+
inMinutes: (mins: number) => string;
|
|
77
|
+
/** Relative countdown, e.g. "in 2h 15m" / "in 2h" */
|
|
78
|
+
inHours: (hours: number, mins: number) => string;
|
|
79
|
+
/** Relative countdown, e.g. "in 3d" */
|
|
80
|
+
inDays: (days: number) => string;
|
|
67
81
|
}
|
|
68
82
|
/** English defaults — used unless overridden via `setLabels()`. */
|
|
69
83
|
export declare const defaultLabels: CalendarLabels;
|
|
@@ -102,7 +116,7 @@ export declare function fmtDay(ms: number, todayMs: number, opts?: {
|
|
|
102
116
|
/**
|
|
103
117
|
* Format a week range label: "Feb 17 – 23, 2026" or "Jan 27 – Feb 2, 2026"
|
|
104
118
|
*/
|
|
105
|
-
export declare function fmtWeekRange(weekStartMs: number, locale?: string): string;
|
|
119
|
+
export declare function fmtWeekRange(weekStartMs: number, locale?: string, weekEndMs?: number): string;
|
|
106
120
|
/**
|
|
107
121
|
* Format a Date as a compact time string.
|
|
108
122
|
*
|
package/dist/core/locale.js
CHANGED
|
@@ -38,6 +38,10 @@ export const defaultLabels = {
|
|
|
38
38
|
nextMonth: 'Next month',
|
|
39
39
|
calendar: 'Calendar',
|
|
40
40
|
viewMode: 'View mode',
|
|
41
|
+
cancelled: 'cancelled',
|
|
42
|
+
tentative: 'tentative',
|
|
43
|
+
full: 'full',
|
|
44
|
+
limited: 'limited',
|
|
41
45
|
dayNavigation: 'Day navigation',
|
|
42
46
|
weekNavigation: 'Week navigation',
|
|
43
47
|
dayPlanner: 'Day planner',
|
|
@@ -57,6 +61,9 @@ export const defaultLabels = {
|
|
|
57
61
|
showLess: 'Show less',
|
|
58
62
|
dayNOfTotal: (current, total) => `day ${current} of ${total}`,
|
|
59
63
|
percentComplete: (pct) => `${pct}% complete`,
|
|
64
|
+
inMinutes: (mins) => `in ${mins}m`,
|
|
65
|
+
inHours: (hours, mins) => (mins > 0 ? `in ${hours}h ${mins}m` : `in ${hours}h`),
|
|
66
|
+
inDays: (days) => `in ${days}d`,
|
|
60
67
|
};
|
|
61
68
|
let _labels = { ...defaultLabels };
|
|
62
69
|
/** Replace one or more UI labels. Merges with current labels. */
|
|
@@ -160,10 +167,10 @@ export function fmtDay(ms, todayMs, opts, locale) {
|
|
|
160
167
|
/**
|
|
161
168
|
* Format a week range label: "Feb 17 – 23, 2026" or "Jan 27 – Feb 2, 2026"
|
|
162
169
|
*/
|
|
163
|
-
export function fmtWeekRange(weekStartMs, locale) {
|
|
170
|
+
export function fmtWeekRange(weekStartMs, locale, weekEndMs) {
|
|
164
171
|
const loc = locale ?? defaultLocale;
|
|
165
172
|
const s = new Date(weekStartMs);
|
|
166
|
-
const e = new Date(weekStartMs + 6 * DAY_MS);
|
|
173
|
+
const e = new Date(weekEndMs ?? weekStartMs + 6 * DAY_MS);
|
|
167
174
|
const sm = s.toLocaleDateString(loc, { month: 'short' });
|
|
168
175
|
const em = e.toLocaleDateString(loc, { month: 'short' });
|
|
169
176
|
const sy = s.getFullYear();
|
package/dist/theme/auto.js
CHANGED
|
@@ -99,6 +99,23 @@ function mix(c1, c2, t) {
|
|
|
99
99
|
Math.round(c1[2] + (c2[2] - c1[2]) * t),
|
|
100
100
|
];
|
|
101
101
|
}
|
|
102
|
+
// ── Shadow-aware DOM walking ────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Parent element that hops shadow boundaries. When a node is a direct child
|
|
105
|
+
* of a ShadowRoot, `parentElement` is null — continue the walk from the
|
|
106
|
+
* shadow host so probes can still see the host page (the embeddable widget
|
|
107
|
+
* mounts the calendar inside a shadow root).
|
|
108
|
+
*/
|
|
109
|
+
function parentAcrossShadow(node) {
|
|
110
|
+
if (node.parentElement)
|
|
111
|
+
return node.parentElement;
|
|
112
|
+
const root = node.getRootNode();
|
|
113
|
+
return typeof ShadowRoot !== 'undefined' &&
|
|
114
|
+
root instanceof ShadowRoot &&
|
|
115
|
+
root.host instanceof HTMLElement
|
|
116
|
+
? root.host
|
|
117
|
+
: null;
|
|
118
|
+
}
|
|
102
119
|
// ── Text color detection ────────────────────────────────
|
|
103
120
|
/**
|
|
104
121
|
* Common CSS variable names for text / foreground color used by popular frameworks.
|
|
@@ -164,7 +181,7 @@ function probeTextColor(el, bg) {
|
|
|
164
181
|
break;
|
|
165
182
|
}
|
|
166
183
|
}
|
|
167
|
-
node = node
|
|
184
|
+
node = parentAcrossShadow(node);
|
|
168
185
|
}
|
|
169
186
|
// 3. Walk up the DOM reading *computed* color
|
|
170
187
|
node = el;
|
|
@@ -178,7 +195,7 @@ function probeTextColor(el, bg) {
|
|
|
178
195
|
}
|
|
179
196
|
}
|
|
180
197
|
catch { /* ignore */ }
|
|
181
|
-
node = node
|
|
198
|
+
node = parentAcrossShadow(node);
|
|
182
199
|
}
|
|
183
200
|
// Pick the first candidate with adequate contrast against the background.
|
|
184
201
|
// WCAG AA large-text minimum is 3:1.
|
|
@@ -367,7 +384,7 @@ function probeBackground(el) {
|
|
|
367
384
|
if (rgb)
|
|
368
385
|
return result(rgb);
|
|
369
386
|
}
|
|
370
|
-
node = node
|
|
387
|
+
node = parentAcrossShadow(node);
|
|
371
388
|
}
|
|
372
389
|
// 3. Fall back to computed backgroundColor (may be mid-transition, but
|
|
373
390
|
// still correct when no transition is active).
|
|
@@ -382,7 +399,7 @@ function probeBackground(el) {
|
|
|
382
399
|
catch {
|
|
383
400
|
// getComputedStyle may fail in test environments
|
|
384
401
|
}
|
|
385
|
-
node = node
|
|
402
|
+
node = parentAcrossShadow(node);
|
|
386
403
|
}
|
|
387
404
|
// 4. Ultimate fallback: check color-scheme preference
|
|
388
405
|
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
@@ -400,7 +417,9 @@ function probeBackground(el) {
|
|
|
400
417
|
export function probeHostTheme(el, options = {}) {
|
|
401
418
|
// Start probing from the parent — `el` itself is the calendar root, which
|
|
402
419
|
// has its own --dt-bg fallback in CSS. We need the *host page's* context.
|
|
403
|
-
|
|
420
|
+
// (Shadow-aware: inside a shadow root the parent hop lands on the shadow
|
|
421
|
+
// host, i.e. the <day-calendar> element in the light DOM.)
|
|
422
|
+
const host = parentAcrossShadow(el) ?? el;
|
|
404
423
|
const htmlRoot = (host.closest('body') ?? host) instanceof HTMLElement
|
|
405
424
|
? (host.closest('body') ?? host)
|
|
406
425
|
: document.body;
|
|
@@ -445,7 +464,7 @@ export function probeHostTheme(el, options = {}) {
|
|
|
445
464
|
// ── Accent derivatives ──
|
|
446
465
|
const accentDim = isDark ? 0.15 : 0.12;
|
|
447
466
|
const glow = isDark ? 0.30 : 0.25;
|
|
448
|
-
const todayBg = isDark ? 0.
|
|
467
|
+
const todayBg = isDark ? 0.07 : 0.07;
|
|
449
468
|
// Ensure accent is readable on the background — adjust lightness if needed
|
|
450
469
|
const accentL = isDark
|
|
451
470
|
? Math.max(aL, 0.45) // bright enough on dark
|
|
@@ -475,6 +494,8 @@ export function probeHostTheme(el, options = {}) {
|
|
|
475
494
|
`--dt-btn-text: ${btnText}`,
|
|
476
495
|
`--dt-scrollbar: ${rgba(...borderRgb, scrollAlpha)}`,
|
|
477
496
|
`--dt-success: ${rgba(...successRgb, 0.7)}`,
|
|
497
|
+
`--dt-weekend-bg: ${rgba(...borderRgb, isDark ? 0.03 : 0.02)}`,
|
|
498
|
+
`--dt-hover: ${rgba(...borderRgb, isDark ? 0.06 : 0.04)}`,
|
|
478
499
|
`--dt-sans: ${fonts.sans}`,
|
|
479
500
|
`--dt-mono: ${fonts.mono}`,
|
|
480
501
|
];
|