@nomideusz/svelte-calendar 1.3.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 +164 -91
- package/dist/adapters/mapped.d.ts +7 -7
- package/dist/adapters/mapped.js +5 -2
- package/dist/adapters/memory.d.ts +7 -6
- package/dist/adapters/memory.js +3 -4
- package/dist/adapters/recurring.d.ts +6 -6
- package/dist/adapters/recurring.js +12 -14
- package/dist/calendar/Calendar.svelte +65 -18
- package/dist/calendar/Calendar.svelte.d.ts +1 -1
- package/dist/core/locale.d.ts +5 -0
- package/dist/core/locale.js +68 -17
- package/dist/primitives/DayHeader.svelte +9 -9
- package/dist/primitives/EmptySlot.svelte +6 -6
- package/dist/primitives/EventBlock.svelte +19 -19
- package/dist/primitives/FloatingPanel.svelte +4 -4
- package/dist/primitives/NowIndicator.svelte +5 -5
- package/dist/primitives/TimeGutter.svelte +4 -4
- package/dist/theme/auto.d.ts +5 -0
- package/dist/theme/auto.js +32 -4
- package/dist/views/agenda/AgendaDay.svelte +83 -74
- package/dist/views/agenda/AgendaWeek.svelte +53 -52
- package/dist/views/mobile/MobileDay.svelte +34 -34
- package/dist/views/mobile/MobileWeek.svelte +41 -41
- package/dist/views/month/MonthGrid.svelte +49 -23
- package/dist/views/planner/PlannerScroll.svelte +47 -47
- package/dist/views/planner/PlannerWeek.svelte +101 -73
- package/dist/views/shared/context.svelte.d.ts +2 -0
- package/dist/views/shared/context.svelte.js +1 -0
- package/package.json +1 -1
- package/widget/widget.js +376 -223
|
@@ -95,16 +95,16 @@ export interface MappedAdapterOptions<T = Record<string, unknown>> {
|
|
|
95
95
|
*/
|
|
96
96
|
includeData?: string[] | '*';
|
|
97
97
|
/**
|
|
98
|
-
* When true
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* @default true
|
|
98
|
+
* When true, ignore source colors and auto-assign from the palette
|
|
99
|
+
* (`VIVID_PALETTE` unless `palette` is given) grouped by category / title.
|
|
100
|
+
* By default source colors are kept, and events without one draw in
|
|
101
|
+
* `--dt-event` (ink) unless a `palette` is given to fill them.
|
|
102
|
+
* @default false (since 1.4; was true)
|
|
103
103
|
*/
|
|
104
104
|
autoColor?: boolean;
|
|
105
105
|
/**
|
|
106
|
-
* Color palette used
|
|
107
|
-
*
|
|
106
|
+
* Color palette used to fill events without a source color
|
|
107
|
+
* (or every event when `autoColor: true`). Off by default.
|
|
108
108
|
*/
|
|
109
109
|
palette?: readonly string[];
|
|
110
110
|
/**
|
package/dist/adapters/mapped.js
CHANGED
|
@@ -139,15 +139,18 @@ function collectData(raw, mappedKeys, include) {
|
|
|
139
139
|
* ```
|
|
140
140
|
*/
|
|
141
141
|
export function createMappedAdapter(sourceData, options = {}) {
|
|
142
|
-
const { fields, mapEvent: customMapper, includeData = '*', autoColor =
|
|
142
|
+
const { fields, mapEvent: customMapper, includeData = '*', autoColor = false, palette = autoColor ? VIVID_PALETTE : undefined, readOnly = true, } = options;
|
|
143
143
|
// Build the internal event list
|
|
144
144
|
const events = [];
|
|
145
145
|
const colorAssignments = new Map();
|
|
146
146
|
let colorIndex = 0;
|
|
147
147
|
function resolveColor(ev) {
|
|
148
|
-
//
|
|
148
|
+
// autoColor: always assign from the palette (ignore source color).
|
|
149
|
+
// Otherwise keep the source color; fill only when a palette is given.
|
|
149
150
|
if (!autoColor && ev.color)
|
|
150
151
|
return ev.color;
|
|
152
|
+
if (!palette?.length)
|
|
153
|
+
return ev.color;
|
|
151
154
|
const key = ev.category ?? ev.title;
|
|
152
155
|
if (!colorAssignments.has(key)) {
|
|
153
156
|
colorAssignments.set(key, palette[colorIndex % palette.length]);
|
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
* In-memory adapter — the default for demos and testing.
|
|
3
3
|
*
|
|
4
4
|
* Events are stored in a plain array. No persistence across page loads.
|
|
5
|
-
* Events without a `color`
|
|
6
|
-
*
|
|
5
|
+
* Events without a `color` stay uncoloured and draw in `--dt-event` (ink).
|
|
6
|
+
* Pass `palette` to auto-assign colours instead, grouped by `category` or
|
|
7
|
+
* `title` so related events share one.
|
|
7
8
|
*
|
|
8
9
|
* Usage:
|
|
9
10
|
* import { createMemoryAdapter } from './';
|
|
10
11
|
* const adapter = createMemoryAdapter(initialEvents);
|
|
11
|
-
* const adapter = createMemoryAdapter(initialEvents, { palette:
|
|
12
|
+
* const adapter = createMemoryAdapter(initialEvents, { palette: VIVID_PALETTE });
|
|
12
13
|
*/
|
|
13
14
|
import type { TimelineEvent } from '../core/types.js';
|
|
14
15
|
import type { CalendarAdapter } from './types.js';
|
|
15
16
|
export interface MemoryAdapterOptions {
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Colour palette for auto-colouring events that have no `color`.
|
|
19
|
+
* Off by default (events draw in `--dt-event`). Pass `VIVID_PALETTE`
|
|
20
|
+
* for the pre-1.4 look, or `generatePalette(accent)` for theme-harmonious colours.
|
|
20
21
|
*/
|
|
21
22
|
palette?: readonly string[];
|
|
22
23
|
}
|
package/dist/adapters/memory.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { VIVID_PALETTE } from '../core/palette.js';
|
|
2
1
|
import { overlapsRange } from '../core/time.js';
|
|
3
2
|
import { EventNotFoundError } from './errors.js';
|
|
4
3
|
let counter = 0;
|
|
@@ -7,12 +6,12 @@ function uid() {
|
|
|
7
6
|
}
|
|
8
7
|
export function createMemoryAdapter(initial = [], options) {
|
|
9
8
|
const events = [...initial];
|
|
10
|
-
const palette = options?.palette
|
|
11
|
-
//
|
|
9
|
+
const palette = options?.palette;
|
|
10
|
+
// Opt-in auto-color: assign from the palette by category/title
|
|
12
11
|
const colorAssignments = new Map();
|
|
13
12
|
let colorIndex = 0;
|
|
14
13
|
function resolveColor(ev) {
|
|
15
|
-
if (ev.color)
|
|
14
|
+
if (ev.color || !palette?.length)
|
|
16
15
|
return ev.color;
|
|
17
16
|
const key = ev.category ?? ev.title;
|
|
18
17
|
if (!colorAssignments.has(key)) {
|
|
@@ -74,10 +74,10 @@ export interface RecurringAdapterOptions {
|
|
|
74
74
|
/** Start weeks on Monday (default: true) */
|
|
75
75
|
mondayStart?: boolean;
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* Colour palette for auto-colouring events that have no `color`.
|
|
78
|
+
* Off by default (events draw in `--dt-event`). Pass `VIVID_PALETTE`
|
|
79
|
+
* for the pre-1.4 look, `generatePalette('#yourAccent')` for
|
|
80
|
+
* theme-harmonious colours, or your own array of colours.
|
|
81
81
|
*/
|
|
82
82
|
palette?: readonly string[];
|
|
83
83
|
/**
|
|
@@ -95,8 +95,8 @@ export interface RecurringAdapterOptions {
|
|
|
95
95
|
* Create a CalendarAdapter that projects recurring events onto concrete
|
|
96
96
|
* dates for whatever range the calendar requests.
|
|
97
97
|
*
|
|
98
|
-
* Events without a `color`
|
|
99
|
-
* grouped by `category` or `title
|
|
98
|
+
* Events without a `color` draw in `--dt-event` (ink) unless `palette` is
|
|
99
|
+
* given; then they are auto-assigned one, grouped by `category` or `title`.
|
|
100
100
|
*
|
|
101
101
|
* Read-only by default — create/update/delete throw.
|
|
102
102
|
*/
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { startOfWeek, addDaysMs, diffDays, DAY_MS, overlapsRange } from '../core/time.js';
|
|
2
|
-
import { VIVID_PALETTE } from '../core/palette.js';
|
|
3
2
|
import { CalendarReadOnlyError, EventNotFoundError } from './errors.js';
|
|
4
3
|
// ── Helpers ─────────────────────────────────────────────
|
|
5
4
|
/** Parse "HH:MM" into [hours, minutes] */
|
|
@@ -234,32 +233,31 @@ function projectMonthly(rec, range, startDate, effectiveUntil, out) {
|
|
|
234
233
|
}
|
|
235
234
|
}
|
|
236
235
|
// ── Adapter factory ─────────────────────────────────────
|
|
237
|
-
/** Default palette for auto-coloring */
|
|
238
|
-
const PALETTE = VIVID_PALETTE;
|
|
239
236
|
/**
|
|
240
237
|
* Create a CalendarAdapter that projects recurring events onto concrete
|
|
241
238
|
* dates for whatever range the calendar requests.
|
|
242
239
|
*
|
|
243
|
-
* Events without a `color`
|
|
244
|
-
* grouped by `category` or `title
|
|
240
|
+
* Events without a `color` draw in `--dt-event` (ink) unless `palette` is
|
|
241
|
+
* given; then they are auto-assigned one, grouped by `category` or `title`.
|
|
245
242
|
*
|
|
246
243
|
* Read-only by default — create/update/delete throw.
|
|
247
244
|
*/
|
|
248
245
|
export function createRecurringAdapter(schedule, options = {}) {
|
|
249
246
|
const { mondayStart = true, palette, movable = false } = options;
|
|
250
|
-
const colors = palette?.length ? palette :
|
|
247
|
+
const colors = palette?.length ? palette : undefined;
|
|
251
248
|
// Auto-color: assign from palette by category/title
|
|
252
249
|
const colorAssignments = new Map();
|
|
253
250
|
let colorIndex = 0;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
colorAssignments.
|
|
260
|
-
|
|
251
|
+
if (colors)
|
|
252
|
+
for (const rec of schedule) {
|
|
253
|
+
if (rec.color)
|
|
254
|
+
continue;
|
|
255
|
+
const key = rec.category ?? rec.title;
|
|
256
|
+
if (!colorAssignments.has(key)) {
|
|
257
|
+
colorAssignments.set(key, colors[colorIndex % colors.length]);
|
|
258
|
+
colorIndex++;
|
|
259
|
+
}
|
|
261
260
|
}
|
|
262
|
-
}
|
|
263
261
|
function resolveColor(rec) {
|
|
264
262
|
if (rec.color)
|
|
265
263
|
return rec.color;
|
|
@@ -376,6 +376,7 @@ setContext("calendar", {
|
|
|
376
376
|
get labels() {
|
|
377
377
|
return mergedLabels;
|
|
378
378
|
},
|
|
379
|
+
announce,
|
|
379
380
|
// Load range (read/write)
|
|
380
381
|
get loadRange() {
|
|
381
382
|
return viewLoadRange;
|
|
@@ -479,6 +480,17 @@ const dateLabel = $derived.by(() => {
|
|
|
479
480
|
year: "numeric"
|
|
480
481
|
});
|
|
481
482
|
});
|
|
483
|
+
// ── Polite announcements (views say why an action did nothing) ──
|
|
484
|
+
// Shares the header's status line; a note lasts until the range changes.
|
|
485
|
+
let note = $state(null);
|
|
486
|
+
function announce(text) {
|
|
487
|
+
// A repeated message must change the text node to be read again.
|
|
488
|
+
note = {
|
|
489
|
+
text: note?.text === text ? `${text}\u00a0` : text,
|
|
490
|
+
label: dateLabel
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const noteText = $derived(note && note.label === dateLabel ? note.text : "");
|
|
482
494
|
// ── Period title (the built-in header's big line) ──
|
|
483
495
|
// Names the month the user is looking at: "October 2026"; a week across two
|
|
484
496
|
// months reads "Oct – Nov 2026"; a day gives its date. `dateLabel` keeps the
|
|
@@ -611,6 +623,28 @@ function handleShortcuts(e) {
|
|
|
611
623
|
else viewState.prev();
|
|
612
624
|
}
|
|
613
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
|
+
}
|
|
614
648
|
/** Header context for custom header snippet */
|
|
615
649
|
const headerCtx = $derived({
|
|
616
650
|
dateLabel,
|
|
@@ -656,10 +690,12 @@ const navCtx = $derived({
|
|
|
656
690
|
<div class="cal-hd" class:cal-hd--mobile={useMobile} class:cal-hd--stack={stackHeader}>
|
|
657
691
|
<!-- The visible title names the month; the status line announces the exact range -->
|
|
658
692
|
<span class="cal-hd-title" aria-hidden="true">{periodTitle}</span>
|
|
659
|
-
<span class="cal-sr" role="status" aria-live="polite" aria-atomic="true">{dateLabel}</span>
|
|
693
|
+
<span class="cal-sr" role="status" aria-live="polite" aria-atomic="true">{noteText || dateLabel}</span>
|
|
660
694
|
{#if showModePills && !useMobile && labelsForMode.length > 1}
|
|
661
695
|
<!-- View-type switcher (e.g. Planner ↔ Agenda) for the current mode -->
|
|
662
|
-
|
|
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}>
|
|
663
699
|
{#each labelsForMode as label (label)}
|
|
664
700
|
<button
|
|
665
701
|
type="button"
|
|
@@ -667,6 +703,7 @@ const navCtx = $derived({
|
|
|
667
703
|
class:cal-pill--active={activeView?.label === label}
|
|
668
704
|
role="radio"
|
|
669
705
|
aria-checked={activeView?.label === label}
|
|
706
|
+
tabindex={label === activeLabel ? 0 : -1}
|
|
670
707
|
onclick={() => switchLabel(label)}
|
|
671
708
|
>
|
|
672
709
|
{viewLabel(label)}
|
|
@@ -675,7 +712,9 @@ const navCtx = $derived({
|
|
|
675
712
|
</div>
|
|
676
713
|
{/if}
|
|
677
714
|
{#if showModePills && modes.length > 1}
|
|
678
|
-
|
|
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}>
|
|
679
718
|
{#each modes as g (g)}
|
|
680
719
|
<button
|
|
681
720
|
type="button"
|
|
@@ -683,6 +722,7 @@ const navCtx = $derived({
|
|
|
683
722
|
class:cal-pill--active={viewState.mode === g}
|
|
684
723
|
role="radio"
|
|
685
724
|
aria-checked={viewState.mode === g}
|
|
725
|
+
tabindex={g === activeMode ? 0 : -1}
|
|
686
726
|
onclick={() => switchMode(g)}
|
|
687
727
|
>
|
|
688
728
|
{g === 'day' ? L.day : g === 'week' ? L.week : L.month}
|
|
@@ -715,6 +755,10 @@ const navCtx = $derived({
|
|
|
715
755
|
</div>
|
|
716
756
|
{/if}
|
|
717
757
|
|
|
758
|
+
{#if headerSnippet || !(showNavigation || (showModePills && modes.length > 1) || dateLabel)}
|
|
759
|
+
<span class="cal-sr" role="status" aria-live="polite" aria-atomic="true">{noteText}</span>
|
|
760
|
+
{/if}
|
|
761
|
+
|
|
718
762
|
<div class="cal-body">
|
|
719
763
|
{#if activeView}
|
|
720
764
|
{@const Comp = activeView.component}
|
|
@@ -755,7 +799,7 @@ const navCtx = $derived({
|
|
|
755
799
|
overflow: clip;
|
|
756
800
|
display: flex;
|
|
757
801
|
flex-direction: column;
|
|
758
|
-
border: 1px solid var(--dt-border, rgba(
|
|
802
|
+
border: 1px solid var(--dt-border, rgba(15, 15, 15, 0.08));
|
|
759
803
|
box-sizing: border-box;
|
|
760
804
|
}
|
|
761
805
|
.cal--auto {
|
|
@@ -775,11 +819,11 @@ const navCtx = $derived({
|
|
|
775
819
|
align-items: center;
|
|
776
820
|
gap: 10px 24px;
|
|
777
821
|
padding: 14px 16px 12px;
|
|
778
|
-
border-bottom: 1px solid var(--dt-border, rgba(
|
|
822
|
+
border-bottom: 1px solid var(--dt-border, rgba(15, 15, 15, 0.08));
|
|
779
823
|
flex-shrink: 0;
|
|
780
824
|
box-sizing: border-box;
|
|
781
825
|
font-family: var(--dt-sans, system-ui, sans-serif);
|
|
782
|
-
color: var(--dt-text, rgba(
|
|
826
|
+
color: var(--dt-text, rgba(15, 15, 15, 0.87));
|
|
783
827
|
}
|
|
784
828
|
|
|
785
829
|
.cal-hd-title {
|
|
@@ -819,10 +863,10 @@ const navCtx = $derived({
|
|
|
819
863
|
.cal-hd-btn {
|
|
820
864
|
box-sizing: border-box;
|
|
821
865
|
height: var(--cal-ctl-h);
|
|
822
|
-
border: 2px solid var(--dt-text, rgba(
|
|
866
|
+
border: 2px solid var(--dt-text, rgba(15, 15, 15, 0.87));
|
|
823
867
|
border-radius: var(--cal-ctl-r);
|
|
824
868
|
background: transparent;
|
|
825
|
-
color: var(--dt-text, rgba(
|
|
869
|
+
color: var(--dt-text, rgba(15, 15, 15, 0.87));
|
|
826
870
|
cursor: pointer;
|
|
827
871
|
transition: background-color 120ms, opacity 120ms;
|
|
828
872
|
-webkit-tap-highlight-color: transparent;
|
|
@@ -848,7 +892,7 @@ const navCtx = $derived({
|
|
|
848
892
|
}
|
|
849
893
|
.cal-hd-today:active:not(:disabled),
|
|
850
894
|
.cal-hd-btn:active {
|
|
851
|
-
background: color-mix(in srgb, var(--dt-text, #
|
|
895
|
+
background: color-mix(in srgb, var(--dt-text, #0f0f0f) 12%, transparent);
|
|
852
896
|
}
|
|
853
897
|
/* Today is already in view: the frame stays, quietly */
|
|
854
898
|
.cal-hd-today:disabled {
|
|
@@ -859,7 +903,7 @@ const navCtx = $derived({
|
|
|
859
903
|
.cal-hd-today:focus-visible,
|
|
860
904
|
.cal-hd-btn:focus-visible,
|
|
861
905
|
.cal-pill:focus-visible {
|
|
862
|
-
outline: 2px solid var(--dt-text, rgba(
|
|
906
|
+
outline: 2px solid var(--dt-text, rgba(15, 15, 15, 0.87));
|
|
863
907
|
outline-offset: 2px;
|
|
864
908
|
}
|
|
865
909
|
|
|
@@ -875,12 +919,12 @@ const navCtx = $derived({
|
|
|
875
919
|
align-self: stretch;
|
|
876
920
|
align-items: center;
|
|
877
921
|
padding-inline-start: 24px;
|
|
878
|
-
border-inline-start: 1px solid var(--dt-border, rgba(
|
|
922
|
+
border-inline-start: 1px solid var(--dt-border, rgba(15, 15, 15, 0.12));
|
|
879
923
|
}
|
|
880
924
|
.cal-pill {
|
|
881
925
|
border: none;
|
|
882
926
|
background: transparent;
|
|
883
|
-
color: var(--dt-text-2, rgba(
|
|
927
|
+
color: var(--dt-text-2, rgba(15, 15, 15, 0.54));
|
|
884
928
|
cursor: pointer;
|
|
885
929
|
font: 400 15px/1 var(--dt-sans, system-ui, sans-serif);
|
|
886
930
|
min-height: var(--cal-ctl-h);
|
|
@@ -896,11 +940,11 @@ const navCtx = $derived({
|
|
|
896
940
|
}
|
|
897
941
|
/* :not(--active) — iOS keeps :hover stuck after a tap */
|
|
898
942
|
.cal-pill:hover:not(.cal-pill--active) {
|
|
899
|
-
color: var(--dt-text, rgba(
|
|
900
|
-
text-decoration-color: var(--dt-border-day, var(--dt-border, rgba(
|
|
943
|
+
color: var(--dt-text, rgba(15, 15, 15, 0.87));
|
|
944
|
+
text-decoration-color: var(--dt-border-day, var(--dt-border, rgba(15, 15, 15, 0.3)));
|
|
901
945
|
}
|
|
902
946
|
.cal-pill--active {
|
|
903
|
-
color: var(--dt-text, rgba(
|
|
947
|
+
color: var(--dt-text, rgba(15, 15, 15, 0.87));
|
|
904
948
|
text-decoration-color: currentColor;
|
|
905
949
|
}
|
|
906
950
|
|
|
@@ -936,6 +980,9 @@ const navCtx = $derived({
|
|
|
936
980
|
.cal-hd {
|
|
937
981
|
--cal-ctl-h: 44px;
|
|
938
982
|
}
|
|
983
|
+
.cal-pill {
|
|
984
|
+
min-width: 44px;
|
|
985
|
+
}
|
|
939
986
|
}
|
|
940
987
|
|
|
941
988
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -962,7 +1009,7 @@ const navCtx = $derived({
|
|
|
962
1009
|
justify-content: center;
|
|
963
1010
|
height: 100%;
|
|
964
1011
|
font: 400 13px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
965
|
-
color: var(--dt-text-3, rgba(
|
|
1012
|
+
color: var(--dt-text-3, rgba(15, 15, 15, 0.38));
|
|
966
1013
|
}
|
|
967
1014
|
|
|
968
1015
|
.cal-loading {
|
|
@@ -974,7 +1021,7 @@ const navCtx = $derived({
|
|
|
974
1021
|
background: linear-gradient(
|
|
975
1022
|
90deg,
|
|
976
1023
|
transparent 0%,
|
|
977
|
-
var(--dt-accent, #
|
|
1024
|
+
var(--dt-accent, #1d4fd7) 50%,
|
|
978
1025
|
transparent 100%
|
|
979
1026
|
);
|
|
980
1027
|
animation: cal-slide 1.2s ease-in-out infinite;
|
|
@@ -988,7 +1035,7 @@ const navCtx = $derived({
|
|
|
988
1035
|
@media (prefers-reduced-motion: reduce) {
|
|
989
1036
|
.cal-loading {
|
|
990
1037
|
animation: none;
|
|
991
|
-
background: var(--dt-accent-dim, rgba(
|
|
1038
|
+
background: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
|
|
992
1039
|
}
|
|
993
1040
|
}
|
|
994
1041
|
|
|
@@ -69,7 +69,7 @@ export interface CalendarProps {
|
|
|
69
69
|
mondayStart?: boolean;
|
|
70
70
|
/** Total height. Use `'auto'` to let content determine height (ideal for Agenda views). */
|
|
71
71
|
height?: number | 'auto';
|
|
72
|
-
/** Border radius in px (default:
|
|
72
|
+
/** Border radius in px (default: 0, square-cut). */
|
|
73
73
|
borderRadius?: number;
|
|
74
74
|
/** Text direction: 'ltr' (default), 'rtl', or 'auto' */
|
|
75
75
|
dir?: 'ltr' | 'rtl' | 'auto';
|
package/dist/core/locale.d.ts
CHANGED
|
@@ -98,6 +98,10 @@ export declare function setDefaultLocale(tag: string): void;
|
|
|
98
98
|
/** Get the current default locale */
|
|
99
99
|
export declare function getDefaultLocale(): string;
|
|
100
100
|
export declare function is24HourLocale(locale?: string): boolean;
|
|
101
|
+
/** A number in the locale's own digits ("٧" in ar-SA), optionally zero-padded. */
|
|
102
|
+
export declare function fmtNum(n: number, locale?: string, minDigits?: number): string;
|
|
103
|
+
/** Day-of-month in the locale's digits: "19", "١٩". */
|
|
104
|
+
export declare function fmtDayNum(ms: number, locale?: string): string;
|
|
101
105
|
/** Format hour index (0-23) as compact label: 12h ("12a", "1p") or 24h ("0", "13") */
|
|
102
106
|
export declare function fmtH(h: number, locale?: string): string;
|
|
103
107
|
/**
|
|
@@ -137,6 +141,7 @@ export declare function fmtWeekRange(weekStartMs: number, locale?: string, weekE
|
|
|
137
141
|
*
|
|
138
142
|
* 12-hour locales → "9a", "12:30p"
|
|
139
143
|
* 24-hour locales → "9:00", "14:30"
|
|
144
|
+
* other 12-hour locales → Intl's own, in the locale's digits ("٧:٠٠ ص")
|
|
140
145
|
*/
|
|
141
146
|
export declare function fmtTime(d: Date, locale?: string): string;
|
|
142
147
|
/**
|
package/dist/core/locale.js
CHANGED
|
@@ -106,16 +106,61 @@ export function is24HourLocale(locale) {
|
|
|
106
106
|
hourCycleCache.set(loc, is24);
|
|
107
107
|
return is24;
|
|
108
108
|
}
|
|
109
|
+
// ─── Numerals ───────────────────────────────────────────
|
|
110
|
+
const numCache = new Map();
|
|
111
|
+
/** A number in the locale's own digits ("٧" in ar-SA), optionally zero-padded. */
|
|
112
|
+
export function fmtNum(n, locale, minDigits = 1) {
|
|
113
|
+
const loc = locale ?? defaultLocale;
|
|
114
|
+
const key = `${loc}|${minDigits}`;
|
|
115
|
+
let f = numCache.get(key);
|
|
116
|
+
if (!f) {
|
|
117
|
+
f = new Intl.NumberFormat(loc, { useGrouping: false, minimumIntegerDigits: minDigits });
|
|
118
|
+
numCache.set(key, f);
|
|
119
|
+
}
|
|
120
|
+
return f.format(n);
|
|
121
|
+
}
|
|
122
|
+
/** Day-of-month in the locale's digits: "19", "١٩". */
|
|
123
|
+
export function fmtDayNum(ms, locale) {
|
|
124
|
+
return fmtNum(new Date(ms).getDate(), locale);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Whether the compact "9a" / "12:30p" style fits the locale: a 12-hour clock
|
|
128
|
+
* whose day periods are Latin AM/PM. Other 12-hour locales (ar "ص"/"م")
|
|
129
|
+
* get Intl's own day period instead.
|
|
130
|
+
*/
|
|
131
|
+
const compactCache = new Map();
|
|
132
|
+
function isCompact12h(locale) {
|
|
133
|
+
const loc = locale ?? defaultLocale;
|
|
134
|
+
if (is24HourLocale(loc))
|
|
135
|
+
return false;
|
|
136
|
+
let c = compactCache.get(loc);
|
|
137
|
+
if (c === undefined) {
|
|
138
|
+
const period = new Intl.DateTimeFormat(loc, { hour: 'numeric', hour12: true })
|
|
139
|
+
.formatToParts(new Date(2000, 0, 1, 9))
|
|
140
|
+
.find((p) => p.type === 'dayPeriod')?.value ?? '';
|
|
141
|
+
c = /^a\W*m\W*$/i.test(period);
|
|
142
|
+
compactCache.set(loc, c);
|
|
143
|
+
}
|
|
144
|
+
return c;
|
|
145
|
+
}
|
|
146
|
+
const dtfCache = new Map();
|
|
147
|
+
function dtf(locale, withMinutes) {
|
|
148
|
+
const loc = locale ?? defaultLocale;
|
|
149
|
+
const key = `${loc}|${withMinutes}`;
|
|
150
|
+
let f = dtfCache.get(key);
|
|
151
|
+
if (!f) {
|
|
152
|
+
f = new Intl.DateTimeFormat(loc, withMinutes ? { hour: 'numeric', minute: '2-digit' } : { hour: 'numeric' });
|
|
153
|
+
dtfCache.set(key, f);
|
|
154
|
+
}
|
|
155
|
+
return f;
|
|
156
|
+
}
|
|
109
157
|
/** Format hour index (0-23) as compact label: 12h ("12a", "1p") or 24h ("0", "13") */
|
|
110
158
|
export function fmtH(h, locale) {
|
|
111
|
-
if (is24HourLocale(locale))
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (h === 12)
|
|
117
|
-
return '12p';
|
|
118
|
-
return h < 12 ? h + 'a' : h - 12 + 'p';
|
|
159
|
+
if (is24HourLocale(locale))
|
|
160
|
+
return fmtNum(h, locale);
|
|
161
|
+
if (!isCompact12h(locale))
|
|
162
|
+
return dtf(locale, false).format(new Date(2000, 0, 1, h));
|
|
163
|
+
return fmtNum(h % 12 || 12, locale) + (h < 12 ? 'a' : 'p');
|
|
119
164
|
}
|
|
120
165
|
/**
|
|
121
166
|
* Hour index (0-23) split for a timetable gutter: the numeral and the small
|
|
@@ -123,8 +168,15 @@ export function fmtH(h, locale) {
|
|
|
123
168
|
*/
|
|
124
169
|
export function fmtHParts(h, locale) {
|
|
125
170
|
if (is24HourLocale(locale))
|
|
126
|
-
return [
|
|
127
|
-
|
|
171
|
+
return [fmtNum(h, locale), fmtNum(0, locale, 2)];
|
|
172
|
+
if (!isCompact12h(locale)) {
|
|
173
|
+
const parts = dtf(locale, false).formatToParts(new Date(2000, 0, 1, h));
|
|
174
|
+
return [
|
|
175
|
+
parts.find((p) => p.type === 'hour')?.value ?? fmtNum(h % 12 || 12, locale),
|
|
176
|
+
parts.find((p) => p.type === 'dayPeriod')?.value ?? '',
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
return [fmtNum(h % 12 || 12, locale), h < 12 ? 'am' : 'pm'];
|
|
128
180
|
}
|
|
129
181
|
/** Short weekday name for a timestamp: "Mon", "Tue", etc. */
|
|
130
182
|
export function weekdayShort(ms, locale) {
|
|
@@ -198,18 +250,17 @@ export function fmtWeekRange(weekStartMs, locale, weekEndMs) {
|
|
|
198
250
|
*
|
|
199
251
|
* 12-hour locales → "9a", "12:30p"
|
|
200
252
|
* 24-hour locales → "9:00", "14:30"
|
|
253
|
+
* other 12-hour locales → Intl's own, in the locale's digits ("٧:٠٠ ص")
|
|
201
254
|
*/
|
|
202
255
|
export function fmtTime(d, locale) {
|
|
203
|
-
if (is24HourLocale(locale)) {
|
|
204
|
-
const h = d.getHours();
|
|
205
|
-
const m = d.getMinutes();
|
|
206
|
-
return `${h}:${String(m).padStart(2, '0')}`;
|
|
207
|
-
}
|
|
208
256
|
const h = d.getHours();
|
|
209
257
|
const m = d.getMinutes();
|
|
258
|
+
if (is24HourLocale(locale))
|
|
259
|
+
return `${fmtNum(h, locale)}:${fmtNum(m, locale, 2)}`;
|
|
260
|
+
if (!isCompact12h(locale))
|
|
261
|
+
return dtf(locale, true).format(d);
|
|
210
262
|
const suffix = h >= 12 ? 'p' : 'a';
|
|
211
|
-
|
|
212
|
-
return `${h12}:${String(m).padStart(2, '0')}${suffix}`;
|
|
263
|
+
return `${fmtNum(h % 12 || 12, locale)}:${fmtNum(m, locale, 2)}${suffix}`;
|
|
213
264
|
}
|
|
214
265
|
/**
|
|
215
266
|
* Format the duration between two Dates as a compact string.
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
- Relative labels ("Today", "Tomorrow") for day views via fmtDay
|
|
6
6
|
- Short/long weekday + date number for week views
|
|
7
7
|
-->
|
|
8
|
-
<script lang="ts">import { weekdayShort, weekdayLong, monthShort, fmtDay } from "../core/locale.js";
|
|
8
|
+
<script lang="ts">import { weekdayShort, weekdayLong, monthShort, fmtDay, fmtDayNum } from "../core/locale.js";
|
|
9
9
|
import { sod } from "../core/time.js";
|
|
10
10
|
let { dayMs, todayMs = sod(Date.now()), format = "short", isToday = false, isPast = false } = $props();
|
|
11
|
-
const dayNum = $derived(
|
|
11
|
+
const dayNum = $derived(fmtDayNum(dayMs));
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
14
|
<div
|
|
@@ -43,18 +43,18 @@ const dayNum = $derived(new Date(dayMs).getDate());
|
|
|
43
43
|
font: 500 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
44
44
|
letter-spacing: 0.04em;
|
|
45
45
|
text-transform: uppercase;
|
|
46
|
-
color: var(--dt-text-2, rgba(
|
|
46
|
+
color: var(--dt-text-2, rgba(15, 15, 15, 0.54));
|
|
47
47
|
}
|
|
48
48
|
.dh-name-long {
|
|
49
49
|
font-size: 12px;
|
|
50
50
|
text-transform: none;
|
|
51
51
|
letter-spacing: 0.01em;
|
|
52
|
-
color: var(--dt-text, rgba(
|
|
52
|
+
color: var(--dt-text, rgba(15, 15, 15, 0.87));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
.dh-num {
|
|
56
56
|
font: 600 14px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
57
|
-
color: var(--dt-text, rgba(
|
|
57
|
+
color: var(--dt-text, rgba(15, 15, 15, 0.87));
|
|
58
58
|
width: 28px;
|
|
59
59
|
height: 28px;
|
|
60
60
|
display: flex;
|
|
@@ -63,20 +63,20 @@ const dayNum = $derived(new Date(dayMs).getDate());
|
|
|
63
63
|
border-radius: 50%;
|
|
64
64
|
}
|
|
65
65
|
.dh-num-today {
|
|
66
|
-
background: var(--dt-accent, #
|
|
66
|
+
background: var(--dt-accent, #1d4fd7);
|
|
67
67
|
color: var(--dt-btn-text, #fff);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
.dh-date {
|
|
71
71
|
font: 400 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
72
|
-
color: var(--dt-text-2, rgba(
|
|
72
|
+
color: var(--dt-text-2, rgba(15, 15, 15, 0.54));
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
.dh-rel {
|
|
76
76
|
font: 500 12px / 1.3 var(--dt-sans, system-ui, sans-serif);
|
|
77
|
-
color: var(--dt-text, rgba(
|
|
77
|
+
color: var(--dt-text, rgba(15, 15, 15, 0.87));
|
|
78
78
|
}
|
|
79
79
|
.dh-today .dh-rel {
|
|
80
|
-
color: var(--dt-accent, #
|
|
80
|
+
color: var(--dt-accent, #1d4fd7);
|
|
81
81
|
}
|
|
82
82
|
</style>
|
|
@@ -49,13 +49,13 @@ function handleKeydown(e) {
|
|
|
49
49
|
min-height: 24px;
|
|
50
50
|
}
|
|
51
51
|
.es:hover {
|
|
52
|
-
border-color: var(--dt-accent-dim, rgba(
|
|
53
|
-
background: var(--dt-accent-dim, rgba(
|
|
52
|
+
border-color: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
|
|
53
|
+
background: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
|
|
54
54
|
}
|
|
55
55
|
.es:focus-visible {
|
|
56
|
-
outline: 2px solid var(--dt-
|
|
56
|
+
outline: 2px solid var(--dt-text, #0f0f0f);
|
|
57
57
|
outline-offset: 2px;
|
|
58
|
-
border-color: var(--dt-accent-dim, rgba(
|
|
58
|
+
border-color: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
.es-hint {
|
|
@@ -74,12 +74,12 @@ function handleKeydown(e) {
|
|
|
74
74
|
|
|
75
75
|
.es-plus {
|
|
76
76
|
font: 500 14px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
77
|
-
color: var(--dt-accent, #
|
|
77
|
+
color: var(--dt-accent, #1d4fd7);
|
|
78
78
|
}
|
|
79
79
|
.es-range {
|
|
80
80
|
font: 400 10px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
81
81
|
font-variant-numeric: tabular-nums;
|
|
82
|
-
color: var(--dt-text-2, rgba(
|
|
82
|
+
color: var(--dt-text-2, rgba(15, 15, 15, 0.54));
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
.es-v {
|