@nomideusz/svelte-calendar 0.9.0 → 0.10.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 +167 -59
- 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 +259 -110
- package/dist/views/agenda/AgendaWeek.svelte +190 -65
- package/dist/views/mobile/MobileDay.svelte +408 -213
- package/dist/views/mobile/MobileWeek.svelte +136 -74
- package/dist/views/mobile/swipe.d.ts +28 -0
- package/dist/views/mobile/swipe.js +64 -0
- package/dist/views/month/MonthGrid.svelte +166 -30
- 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 +3263 -2727
- 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,7 +96,9 @@ 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 ? MOBILE_BREAKPOINT - 1 : 0
|
|
101
|
+
);
|
|
99
102
|
const isMobileContainer = $derived(containerWidth > 0 && containerWidth < MOBILE_BREAKPOINT);
|
|
100
103
|
const useMobile = $derived(
|
|
101
104
|
mobileProp === "auto" ? isMobileContainer : Boolean(mobileProp)
|
|
@@ -233,7 +236,7 @@ setContext("calendar", {
|
|
|
233
236
|
return oneventhover;
|
|
234
237
|
},
|
|
235
238
|
get ondayclick() {
|
|
236
|
-
return ondayclick;
|
|
239
|
+
return ondayclick ?? defaultDayClick;
|
|
237
240
|
},
|
|
238
241
|
get timezone() {
|
|
239
242
|
return timezone;
|
|
@@ -287,6 +290,9 @@ setContext("calendar", {
|
|
|
287
290
|
get compact() {
|
|
288
291
|
return compact;
|
|
289
292
|
},
|
|
293
|
+
get labels() {
|
|
294
|
+
return mergedLabels;
|
|
295
|
+
},
|
|
290
296
|
// Load range (read/write)
|
|
291
297
|
get loadRange() {
|
|
292
298
|
return viewLoadRange;
|
|
@@ -347,6 +353,13 @@ const dateLabel = $derived.by(() => {
|
|
|
347
353
|
day: "numeric"
|
|
348
354
|
});
|
|
349
355
|
}
|
|
356
|
+
if (viewState.mode === "week") {
|
|
357
|
+
return fmtWeekRange(
|
|
358
|
+
viewState.range.start.getTime(),
|
|
359
|
+
locale,
|
|
360
|
+
viewState.range.end.getTime() - 1
|
|
361
|
+
);
|
|
362
|
+
}
|
|
350
363
|
return viewState.focusDate.toLocaleDateString(locale, {
|
|
351
364
|
month: "long",
|
|
352
365
|
year: "numeric"
|
|
@@ -356,19 +369,77 @@ const modes = $derived.by(() => {
|
|
|
356
369
|
const g = new Set(desktopViews.map((v) => v.mode));
|
|
357
370
|
return ["day", "week", "month"].filter((key) => g.has(key));
|
|
358
371
|
});
|
|
359
|
-
const
|
|
372
|
+
const mergedLabels = $derived(
|
|
373
|
+
labelsProp ? { ...getLabels(), ...labelsProp } : getLabels()
|
|
374
|
+
);
|
|
375
|
+
const L = $derived(mergedLabels);
|
|
376
|
+
let lastViewLabel = $state(void 0);
|
|
377
|
+
$effect(() => {
|
|
378
|
+
const current = views.find((v) => v.id === viewState.view);
|
|
379
|
+
if (current && current.mode !== "month") lastViewLabel = current.label;
|
|
380
|
+
});
|
|
360
381
|
function switchMode(g) {
|
|
361
|
-
const
|
|
362
|
-
const
|
|
382
|
+
const currentView = desktopViews.find((v) => v.id === viewState.view) ?? activeView;
|
|
383
|
+
const preferredLabel = currentView?.mode === "month" ? lastViewLabel ?? currentView?.label : currentView?.label;
|
|
384
|
+
const match = desktopViews.find((v) => v.mode === g && v.label === preferredLabel);
|
|
363
385
|
const fallback = desktopViews.find((v) => v.mode === g);
|
|
364
386
|
const target = match ?? fallback;
|
|
365
387
|
if (target) viewState.setView(target.id);
|
|
366
388
|
}
|
|
389
|
+
const labelsForMode = $derived.by(() => {
|
|
390
|
+
const seen = [];
|
|
391
|
+
for (const v of desktopViews) {
|
|
392
|
+
if (v.mode === viewState.mode && !seen.includes(v.label)) seen.push(v.label);
|
|
393
|
+
}
|
|
394
|
+
return seen;
|
|
395
|
+
});
|
|
396
|
+
function switchLabel(label) {
|
|
397
|
+
const target = desktopViews.find((v) => v.mode === viewState.mode && v.label === label);
|
|
398
|
+
if (target) viewState.setView(target.id);
|
|
399
|
+
}
|
|
400
|
+
const defaultDayClick = $derived.by(() => {
|
|
401
|
+
const target = desktopViews.find((v) => v.mode === "day" && v.label === lastViewLabel) ?? desktopViews.find((v) => v.mode === "day");
|
|
402
|
+
if (!target) return void 0;
|
|
403
|
+
return (date) => {
|
|
404
|
+
viewState.setFocusDate(date);
|
|
405
|
+
viewState.setView(target.id);
|
|
406
|
+
};
|
|
407
|
+
});
|
|
367
408
|
const viewIncludesToday = $derived.by(() => {
|
|
368
|
-
const now = Date
|
|
409
|
+
const now = /* @__PURE__ */ new Date();
|
|
410
|
+
if (viewState.mode === "month") {
|
|
411
|
+
const f = viewState.focusDate;
|
|
412
|
+
return f.getMonth() === now.getMonth() && f.getFullYear() === now.getFullYear();
|
|
413
|
+
}
|
|
369
414
|
const { start, end } = viewState.range;
|
|
370
|
-
return now >= start.getTime() && now < end.getTime();
|
|
415
|
+
return now.getTime() >= start.getTime() && now.getTime() < end.getTime();
|
|
371
416
|
});
|
|
417
|
+
const resolvedDir = $derived.by(() => {
|
|
418
|
+
if (dir) return dir;
|
|
419
|
+
if (!locale) return void 0;
|
|
420
|
+
try {
|
|
421
|
+
const info = new Intl.Locale(locale);
|
|
422
|
+
const direction = info.textInfo?.direction ?? info.getTextInfo?.().direction;
|
|
423
|
+
return direction === "rtl" ? "rtl" : void 0;
|
|
424
|
+
} catch {
|
|
425
|
+
return void 0;
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
function handleShortcuts(e) {
|
|
429
|
+
if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey) return;
|
|
430
|
+
const t = e.target;
|
|
431
|
+
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
|
|
432
|
+
if (e.key === "t" || e.key === "T") {
|
|
433
|
+
e.preventDefault();
|
|
434
|
+
viewState.goToday();
|
|
435
|
+
} else if (e.key === "ArrowLeft") {
|
|
436
|
+
e.preventDefault();
|
|
437
|
+
viewState.prev();
|
|
438
|
+
} else if (e.key === "ArrowRight") {
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
viewState.next();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
372
443
|
const headerCtx = $derived({
|
|
373
444
|
dateLabel,
|
|
374
445
|
mode: viewState.mode,
|
|
@@ -390,6 +461,7 @@ const navCtx = $derived({
|
|
|
390
461
|
});
|
|
391
462
|
</script>
|
|
392
463
|
|
|
464
|
+
<!-- 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
465
|
<div
|
|
394
466
|
class="cal"
|
|
395
467
|
bind:this={calEl}
|
|
@@ -397,8 +469,10 @@ const navCtx = $derived({
|
|
|
397
469
|
class:cal--auto={heightProp === 'auto'}
|
|
398
470
|
role="region"
|
|
399
471
|
aria-label={L.calendar}
|
|
400
|
-
|
|
472
|
+
aria-busy={store.loading || undefined}
|
|
473
|
+
dir={resolvedDir}
|
|
401
474
|
lang={locale}
|
|
475
|
+
onkeydown={handleShortcuts}
|
|
402
476
|
>
|
|
403
477
|
<!-- ─── Custom header snippet (replaces all chrome) ─── -->
|
|
404
478
|
{#if headerSnippet}
|
|
@@ -408,21 +482,15 @@ const navCtx = $derived({
|
|
|
408
482
|
{:else if useMobile && (showNavigation || (showModePills && modes.length > 1) || dateLabel)}
|
|
409
483
|
<div class="cal-m-hd">
|
|
410
484
|
<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
485
|
{#if showModePills && modes.length > 1}
|
|
420
|
-
<div class="cal-m-pills" role="
|
|
421
|
-
{#each modes as g}
|
|
486
|
+
<div class="cal-m-pills" role="radiogroup" aria-label={L.viewMode}>
|
|
487
|
+
{#each modes as g (g)}
|
|
422
488
|
<button
|
|
489
|
+
type="button"
|
|
423
490
|
class="cal-m-pill"
|
|
424
491
|
class:cal-m-pill--active={viewState.mode === g}
|
|
425
|
-
|
|
492
|
+
role="radio"
|
|
493
|
+
aria-checked={viewState.mode === g}
|
|
426
494
|
onclick={() => switchMode(g)}
|
|
427
495
|
>
|
|
428
496
|
{g === 'day' ? L.day : g === 'week' ? L.week : L.month}
|
|
@@ -432,26 +500,32 @@ const navCtx = $derived({
|
|
|
432
500
|
{/if}
|
|
433
501
|
</div>
|
|
434
502
|
|
|
435
|
-
<span class="cal-m-title">{dateLabel}</span>
|
|
503
|
+
<span class="cal-m-title" role="status" aria-live="polite" aria-atomic="true">{dateLabel}</span>
|
|
436
504
|
|
|
437
505
|
<div class="cal-m-right">
|
|
438
|
-
{#if
|
|
439
|
-
|
|
506
|
+
{#if navigationSnippet}
|
|
507
|
+
{@render navigationSnippet(navCtx)}
|
|
508
|
+
{:else if showNavigation}
|
|
509
|
+
<!-- Always rendered (disabled on today) so navigating never shifts layout -->
|
|
510
|
+
<button
|
|
511
|
+
type="button"
|
|
512
|
+
class="cal-m-today"
|
|
513
|
+
onclick={() => viewState.goToday()}
|
|
514
|
+
disabled={viewIncludesToday}
|
|
515
|
+
title={L.goToToday}
|
|
516
|
+
>
|
|
517
|
+
{L.today}
|
|
518
|
+
</button>
|
|
519
|
+
<button type="button" class="cal-m-nav" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : viewState.mode === 'month' ? L.previousMonth : L.previousWeek}>
|
|
520
|
+
<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>
|
|
521
|
+
</button>
|
|
522
|
+
<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
523
|
<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
524
|
</button>
|
|
442
525
|
{/if}
|
|
443
526
|
</div>
|
|
444
527
|
</div>
|
|
445
528
|
|
|
446
|
-
<!-- Today pill — floats below header, doesn't affect header flow -->
|
|
447
|
-
{#if !navigationSnippet && showNavigation && !viewIncludesToday}
|
|
448
|
-
<div class="cal-m-today-bar">
|
|
449
|
-
<button class="cal-m-today" onclick={() => viewState.goToday()}>
|
|
450
|
-
{L.today}
|
|
451
|
-
</button>
|
|
452
|
-
</div>
|
|
453
|
-
{/if}
|
|
454
|
-
|
|
455
529
|
<!-- ─── Desktop header ─── -->
|
|
456
530
|
{:else if showNavigation || (showModePills && modes.length > 1) || dateLabel}
|
|
457
531
|
<div class="cal-hd">
|
|
@@ -459,28 +533,52 @@ const navCtx = $derived({
|
|
|
459
533
|
{#if navigationSnippet}
|
|
460
534
|
{@render navigationSnippet(navCtx)}
|
|
461
535
|
{:else if showNavigation}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
536
|
+
<!-- Always rendered (disabled on today) so navigating never shifts the centered title -->
|
|
537
|
+
<button
|
|
538
|
+
type="button"
|
|
539
|
+
class="cal-hd-today"
|
|
540
|
+
onclick={() => viewState.goToday()}
|
|
541
|
+
disabled={viewIncludesToday}
|
|
542
|
+
title={L.goToToday}
|
|
543
|
+
>
|
|
544
|
+
{L.today}
|
|
545
|
+
</button>
|
|
546
|
+
<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
547
|
<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
548
|
</button>
|
|
470
|
-
<button class="cal-hd-btn" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : viewState.mode === 'month' ? L.nextMonth : L.nextWeek}>
|
|
549
|
+
<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
550
|
<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
551
|
</button>
|
|
473
552
|
{/if}
|
|
474
553
|
</div>
|
|
475
|
-
<span class="cal-hd-title">{dateLabel}</span>
|
|
554
|
+
<span class="cal-hd-title" role="status" aria-live="polite" aria-atomic="true">{dateLabel}</span>
|
|
476
555
|
<div class="cal-hd-side cal-hd-side--end">
|
|
556
|
+
{#if showModePills && labelsForMode.length > 1}
|
|
557
|
+
<!-- View-type switcher (e.g. Planner ↔ Agenda) for the current mode -->
|
|
558
|
+
<div class="cal-pills cal-pills--labels" role="radiogroup" aria-label={L.viewMode}>
|
|
559
|
+
{#each labelsForMode as label (label)}
|
|
560
|
+
<button
|
|
561
|
+
type="button"
|
|
562
|
+
class="cal-pill"
|
|
563
|
+
class:cal-pill--active={activeView?.label === label}
|
|
564
|
+
role="radio"
|
|
565
|
+
aria-checked={activeView?.label === label}
|
|
566
|
+
onclick={() => switchLabel(label)}
|
|
567
|
+
>
|
|
568
|
+
{label}
|
|
569
|
+
</button>
|
|
570
|
+
{/each}
|
|
571
|
+
</div>
|
|
572
|
+
{/if}
|
|
477
573
|
{#if showModePills && modes.length > 1}
|
|
478
|
-
<div class="cal-pills" role="
|
|
479
|
-
{#each modes as g}
|
|
574
|
+
<div class="cal-pills" role="radiogroup" aria-label={L.viewMode}>
|
|
575
|
+
{#each modes as g (g)}
|
|
480
576
|
<button
|
|
577
|
+
type="button"
|
|
481
578
|
class="cal-pill"
|
|
482
579
|
class:cal-pill--active={viewState.mode === g}
|
|
483
|
-
|
|
580
|
+
role="radio"
|
|
581
|
+
aria-checked={viewState.mode === g}
|
|
484
582
|
onclick={() => switchMode(g)}
|
|
485
583
|
>
|
|
486
584
|
{g === 'day' ? L.day : g === 'week' ? L.week : L.month}
|
|
@@ -543,6 +641,7 @@ const navCtx = $derived({
|
|
|
543
641
|
/* ── Desktop header ── */
|
|
544
642
|
.cal-hd {
|
|
545
643
|
display: flex;
|
|
644
|
+
flex-wrap: wrap;
|
|
546
645
|
align-items: center;
|
|
547
646
|
gap: 8px;
|
|
548
647
|
padding: 8px 12px;
|
|
@@ -611,10 +710,14 @@ const navCtx = $derived({
|
|
|
611
710
|
transition: background 120ms, color 120ms, border-color 120ms;
|
|
612
711
|
}
|
|
613
712
|
|
|
614
|
-
.cal-hd-today:hover {
|
|
713
|
+
.cal-hd-today:hover:not(:disabled) {
|
|
615
714
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
616
715
|
border-color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
617
716
|
}
|
|
717
|
+
.cal-hd-today:disabled {
|
|
718
|
+
opacity: 0.45;
|
|
719
|
+
cursor: default;
|
|
720
|
+
}
|
|
618
721
|
|
|
619
722
|
.cal-pills {
|
|
620
723
|
display: flex;
|
|
@@ -685,6 +788,13 @@ const navCtx = $derived({
|
|
|
685
788
|
100% { transform: translateX(100%); }
|
|
686
789
|
}
|
|
687
790
|
|
|
791
|
+
@media (prefers-reduced-motion: reduce) {
|
|
792
|
+
.cal-loading {
|
|
793
|
+
animation: none;
|
|
794
|
+
background: var(--dt-accent-dim, rgba(37, 99, 235, 0.12));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
688
798
|
/* ── Mobile header (flow layout) ── */
|
|
689
799
|
.cal-m-hd {
|
|
690
800
|
display: flex;
|
|
@@ -712,8 +822,8 @@ const navCtx = $derived({
|
|
|
712
822
|
display: flex;
|
|
713
823
|
align-items: center;
|
|
714
824
|
justify-content: center;
|
|
715
|
-
width:
|
|
716
|
-
height:
|
|
825
|
+
width: 40px;
|
|
826
|
+
height: 40px;
|
|
717
827
|
border: none;
|
|
718
828
|
background: transparent;
|
|
719
829
|
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
@@ -749,8 +859,8 @@ const navCtx = $derived({
|
|
|
749
859
|
background: transparent;
|
|
750
860
|
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
751
861
|
cursor: pointer;
|
|
752
|
-
font: 600
|
|
753
|
-
padding:
|
|
862
|
+
font: 600 12px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
863
|
+
padding: 9px 12px;
|
|
754
864
|
border-radius: 6px;
|
|
755
865
|
letter-spacing: 0.04em;
|
|
756
866
|
text-transform: uppercase;
|
|
@@ -776,19 +886,13 @@ const navCtx = $derived({
|
|
|
776
886
|
min-width: 0;
|
|
777
887
|
}
|
|
778
888
|
|
|
779
|
-
.cal-m-today-bar {
|
|
780
|
-
display: flex;
|
|
781
|
-
justify-content: center;
|
|
782
|
-
padding: 12px 8px 6px;
|
|
783
|
-
flex-shrink: 0;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
889
|
.cal-m-today {
|
|
787
|
-
font: 600
|
|
890
|
+
font: 600 12px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
788
891
|
color: var(--dt-accent, #2563eb);
|
|
789
892
|
background: color-mix(in srgb, var(--dt-accent, #2563eb) 10%, transparent);
|
|
790
893
|
border: none;
|
|
791
|
-
|
|
894
|
+
min-height: 40px;
|
|
895
|
+
padding: 5px 12px;
|
|
792
896
|
border-radius: 6px;
|
|
793
897
|
cursor: pointer;
|
|
794
898
|
white-space: nowrap;
|
|
@@ -798,12 +902,16 @@ const navCtx = $derived({
|
|
|
798
902
|
-webkit-tap-highlight-color: transparent;
|
|
799
903
|
flex-shrink: 0;
|
|
800
904
|
}
|
|
801
|
-
.cal-m-today:hover {
|
|
905
|
+
.cal-m-today:hover:not(:disabled) {
|
|
802
906
|
background: color-mix(in srgb, var(--dt-accent, #2563eb) 18%, transparent);
|
|
803
907
|
}
|
|
804
|
-
.cal-m-today:active {
|
|
908
|
+
.cal-m-today:active:not(:disabled) {
|
|
805
909
|
background: color-mix(in srgb, var(--dt-accent, #2563eb) 25%, transparent);
|
|
806
910
|
}
|
|
911
|
+
.cal-m-today:disabled {
|
|
912
|
+
opacity: 0.45;
|
|
913
|
+
cursor: default;
|
|
914
|
+
}
|
|
807
915
|
.cal-m-today:focus-visible {
|
|
808
916
|
outline: 2px solid color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
|
|
809
917
|
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
|
];
|
package/dist/theme/presets.d.ts
CHANGED
|
@@ -26,13 +26,13 @@ export declare const auto = "";
|
|
|
26
26
|
* Neutral — explicit light theme. White bg, blue accent, inherits host fonts.
|
|
27
27
|
* Use when embedding standalone without ancestor --dt-* vars.
|
|
28
28
|
*/
|
|
29
|
-
export declare const neutral = "\n\t--dt-stage-bg: #ffffff;\n\t--dt-bg: #ffffff;\n\t--dt-surface: #f9fafb;\n\t--dt-border: rgba(0, 0, 0, 0.08);\n\t--dt-border-day: rgba(0, 0, 0, 0.14);\n\t--dt-text: rgba(0, 0, 0, 0.87);\n\t--dt-text-2: rgba(0, 0, 0, 0.54);\n\t--dt-text-3: rgba(0, 0, 0, 0.38);\n\t--dt-accent: #2563eb;\n\t--dt-accent-dim:
|
|
29
|
+
export declare const neutral = "\n\t--dt-stage-bg: #ffffff;\n\t--dt-bg: #ffffff;\n\t--dt-surface: #f9fafb;\n\t--dt-border: rgba(0, 0, 0, 0.08);\n\t--dt-border-day: rgba(0, 0, 0, 0.14);\n\t--dt-text: rgba(0, 0, 0, 0.87);\n\t--dt-text-2: rgba(0, 0, 0, 0.54);\n\t--dt-text-3: rgba(0, 0, 0, 0.38);\n\t--dt-accent: var(--asini-accent, #2563eb);\n\t--dt-accent-dim: color-mix(in srgb, var(--dt-accent) 12%, transparent);\n\t--dt-glow: color-mix(in srgb, var(--dt-accent) 25%, transparent);\n\t--dt-today-bg: color-mix(in srgb, var(--dt-accent) 7%, transparent);\n\t--dt-btn-text: #fff;\n\t--dt-scrollbar: rgba(0, 0, 0, 0.1);\n\t--dt-success: rgba(22, 163, 74, 0.7);\n\t--dt-weekend-bg: rgba(0, 0, 0, 0.02);\n\t--dt-hover: rgba(0, 0, 0, 0.04);\n\t--dt-sans: inherit;\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
|
|
30
30
|
/** Midnight Industrial — dark charcoal + red accent, tech monitoring */
|
|
31
|
-
export declare const midnight = "\n\t--dt-stage-bg: #080a0f;\n\t--dt-bg: #0b0e14;\n\t--dt-surface: #10141c;\n\t--dt-border: rgba(148, 163, 184, 0.07);\n\t--dt-border-day: rgba(148, 163, 184, 0.14);\n\t--dt-text: rgba(226, 232, 240, 0.85);\n\t--dt-text-2: rgba(148, 163, 184, 0.55);\n\t--dt-text-3: rgba(100, 116, 139, 0.55);\n\t--dt-accent: #ef4444;\n\t--dt-accent-dim: rgba(239, 68, 68, 0.18);\n\t--dt-glow: rgba(239, 68, 68, 0.35);\n\t--dt-today-bg: rgba(239, 68, 68, 0.
|
|
31
|
+
export declare const midnight = "\n\t--dt-stage-bg: #080a0f;\n\t--dt-bg: #0b0e14;\n\t--dt-surface: #10141c;\n\t--dt-border: rgba(148, 163, 184, 0.07);\n\t--dt-border-day: rgba(148, 163, 184, 0.14);\n\t--dt-text: rgba(226, 232, 240, 0.85);\n\t--dt-text-2: rgba(148, 163, 184, 0.55);\n\t--dt-text-3: rgba(100, 116, 139, 0.55);\n\t--dt-accent: #ef4444;\n\t--dt-accent-dim: rgba(239, 68, 68, 0.18);\n\t--dt-glow: rgba(239, 68, 68, 0.35);\n\t--dt-today-bg: rgba(239, 68, 68, 0.07);\n\t--dt-btn-text: #fff;\n\t--dt-scrollbar: rgba(148, 163, 184, 0.12);\n\t--dt-success: rgba(74, 222, 128, 0.7);\n\t--dt-weekend-bg: rgba(148, 163, 184, 0.03);\n\t--dt-hover: rgba(148, 163, 184, 0.06);\n\t--dt-sans: inherit;\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
|
|
32
32
|
/** All available presets keyed by name */
|
|
33
33
|
export declare const presets: {
|
|
34
34
|
readonly auto: "";
|
|
35
|
-
readonly neutral: "\n\t--dt-stage-bg: #ffffff;\n\t--dt-bg: #ffffff;\n\t--dt-surface: #f9fafb;\n\t--dt-border: rgba(0, 0, 0, 0.08);\n\t--dt-border-day: rgba(0, 0, 0, 0.14);\n\t--dt-text: rgba(0, 0, 0, 0.87);\n\t--dt-text-2: rgba(0, 0, 0, 0.54);\n\t--dt-text-3: rgba(0, 0, 0, 0.38);\n\t--dt-accent: #2563eb;\n\t--dt-accent-dim:
|
|
36
|
-
readonly midnight: "\n\t--dt-stage-bg: #080a0f;\n\t--dt-bg: #0b0e14;\n\t--dt-surface: #10141c;\n\t--dt-border: rgba(148, 163, 184, 0.07);\n\t--dt-border-day: rgba(148, 163, 184, 0.14);\n\t--dt-text: rgba(226, 232, 240, 0.85);\n\t--dt-text-2: rgba(148, 163, 184, 0.55);\n\t--dt-text-3: rgba(100, 116, 139, 0.55);\n\t--dt-accent: #ef4444;\n\t--dt-accent-dim: rgba(239, 68, 68, 0.18);\n\t--dt-glow: rgba(239, 68, 68, 0.35);\n\t--dt-today-bg: rgba(239, 68, 68, 0.
|
|
35
|
+
readonly neutral: "\n\t--dt-stage-bg: #ffffff;\n\t--dt-bg: #ffffff;\n\t--dt-surface: #f9fafb;\n\t--dt-border: rgba(0, 0, 0, 0.08);\n\t--dt-border-day: rgba(0, 0, 0, 0.14);\n\t--dt-text: rgba(0, 0, 0, 0.87);\n\t--dt-text-2: rgba(0, 0, 0, 0.54);\n\t--dt-text-3: rgba(0, 0, 0, 0.38);\n\t--dt-accent: var(--asini-accent, #2563eb);\n\t--dt-accent-dim: color-mix(in srgb, var(--dt-accent) 12%, transparent);\n\t--dt-glow: color-mix(in srgb, var(--dt-accent) 25%, transparent);\n\t--dt-today-bg: color-mix(in srgb, var(--dt-accent) 7%, transparent);\n\t--dt-btn-text: #fff;\n\t--dt-scrollbar: rgba(0, 0, 0, 0.1);\n\t--dt-success: rgba(22, 163, 74, 0.7);\n\t--dt-weekend-bg: rgba(0, 0, 0, 0.02);\n\t--dt-hover: rgba(0, 0, 0, 0.04);\n\t--dt-sans: inherit;\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
|
|
36
|
+
readonly midnight: "\n\t--dt-stage-bg: #080a0f;\n\t--dt-bg: #0b0e14;\n\t--dt-surface: #10141c;\n\t--dt-border: rgba(148, 163, 184, 0.07);\n\t--dt-border-day: rgba(148, 163, 184, 0.14);\n\t--dt-text: rgba(226, 232, 240, 0.85);\n\t--dt-text-2: rgba(148, 163, 184, 0.55);\n\t--dt-text-3: rgba(100, 116, 139, 0.55);\n\t--dt-accent: #ef4444;\n\t--dt-accent-dim: rgba(239, 68, 68, 0.18);\n\t--dt-glow: rgba(239, 68, 68, 0.35);\n\t--dt-today-bg: rgba(239, 68, 68, 0.07);\n\t--dt-btn-text: #fff;\n\t--dt-scrollbar: rgba(148, 163, 184, 0.12);\n\t--dt-success: rgba(74, 222, 128, 0.7);\n\t--dt-weekend-bg: rgba(148, 163, 184, 0.03);\n\t--dt-hover: rgba(148, 163, 184, 0.06);\n\t--dt-sans: inherit;\n\t--dt-mono: ui-monospace, 'SFMono-Regular', monospace;\n";
|
|
37
37
|
};
|
|
38
38
|
export type PresetName = keyof typeof presets;
|