@nomideusz/svelte-calendar 0.21.0 → 1.0.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 +502 -269
- package/dist/adapters/composite.d.ts +12 -1
- package/dist/adapters/composite.js +109 -47
- package/dist/adapters/errors.d.ts +22 -0
- package/dist/adapters/errors.js +32 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/jmap.js +26 -17
- package/dist/adapters/mapped.d.ts +2 -2
- package/dist/adapters/mapped.js +18 -9
- package/dist/adapters/memory.d.ts +1 -1
- package/dist/adapters/memory.js +5 -3
- package/dist/adapters/recurring.d.ts +1 -1
- package/dist/adapters/recurring.js +37 -19
- package/dist/adapters/rest.d.ts +2 -2
- package/dist/adapters/rest.js +9 -4
- package/dist/calendar/Calendar.svelte +122 -78
- package/dist/calendar/Calendar.svelte.d.ts +69 -14
- package/dist/calendar/index.d.ts +2 -2
- package/dist/calendar/index.js +1 -1
- package/dist/core/clock.svelte.js +3 -2
- package/dist/core/locale.d.ts +11 -1
- package/dist/core/locale.js +12 -12
- package/dist/core/time.d.ts +9 -0
- package/dist/core/time.js +8 -0
- package/dist/core/timezone.d.ts +7 -3
- package/dist/core/timezone.js +10 -4
- package/dist/engine/event-store.svelte.js +93 -56
- package/dist/engine/view-state.svelte.d.ts +5 -1
- package/dist/engine/view-state.svelte.js +5 -3
- package/dist/headless/create-agenda.svelte.d.ts +8 -1
- package/dist/headless/create-agenda.svelte.js +32 -14
- package/dist/headless/create-calendar.svelte.js +53 -17
- package/dist/headless/create-range-agenda.svelte.d.ts +7 -0
- package/dist/headless/create-range-agenda.svelte.js +33 -12
- package/dist/headless/index.d.ts +1 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/primitives/DayHeader.svelte +3 -2
- package/dist/primitives/DayHeader.svelte.d.ts +1 -1
- package/dist/primitives/EmptySlot.svelte +5 -2
- package/dist/primitives/EventBlock.svelte +21 -14
- package/dist/primitives/FloatingPanel.svelte +110 -15
- package/dist/primitives/FloatingPanel.svelte.d.ts +7 -5
- package/dist/primitives/NowIndicator.svelte +11 -4
- package/dist/primitives/TimeGutter.svelte +10 -6
- package/dist/primitives/index.d.ts +1 -0
- package/dist/text-fit.d.ts +8 -2
- package/dist/views/agenda/AgendaDay.svelte +41 -33
- package/dist/views/agenda/AgendaWeek.svelte +71 -34
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +2 -0
- package/dist/views/mobile/MobileDay.svelte +98 -41
- package/dist/views/mobile/MobileWeek.svelte +29 -13
- package/dist/views/mobile/swipe.js +6 -1
- package/dist/views/month/MonthGrid.svelte +29 -17
- package/dist/views/month/MonthGrid.svelte.d.ts +1 -1
- package/dist/views/planner/PlannerScroll.svelte +183 -51
- package/dist/views/planner/PlannerWeek.svelte +148 -48
- package/dist/views/shared/context.svelte.d.ts +1 -1
- package/package.json +24 -9
- package/widget/widget.js +7983 -7688
- package/dist/assets/favicon.svg +0 -9
- package/dist/widget/CalendarWidget.svelte +0 -114
- package/dist/widget/CalendarWidget.svelte.d.ts +0 -37
- package/dist/widget/index.d.ts +0 -1
- package/dist/widget/index.js +0 -1
- package/dist/widget/widget.d.ts +0 -10
- package/dist/widget/widget.js +0 -96
|
@@ -32,29 +32,50 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { untrack } from 'svelte';
|
|
34
34
|
import { createEventStore } from '../engine/event-store.svelte.js';
|
|
35
|
-
import { sod, addDaysMs } from '../core/time.js';
|
|
35
|
+
import { sod, addDaysMs, overlapsRange } from '../core/time.js';
|
|
36
|
+
import { toZonedTime, nowInZone, wrapAdapterWithTimezone } from '../core/timezone.js';
|
|
36
37
|
import { fmtTime as _fmtTime, fmtDuration } from '../core/locale.js';
|
|
37
38
|
// ─── Implementation ─────────────────────────────────────
|
|
38
39
|
export function createRangeAgenda(options) {
|
|
39
|
-
const { initialDate, locale, days: dayCount = 7 } = options;
|
|
40
|
-
const
|
|
40
|
+
const { initialDate, locale, days: dayCount = 7, timezone } = options;
|
|
41
|
+
const rawAdapter = typeof options.adapter === 'function'
|
|
41
42
|
? options.adapter
|
|
42
43
|
: () => options.adapter;
|
|
43
|
-
const
|
|
44
|
+
const resolveAdapter = timezone
|
|
45
|
+
? () => wrapAdapterWithTimezone(rawAdapter(), timezone)
|
|
46
|
+
: rawAdapter;
|
|
47
|
+
/** An instant on the plane the days are drawn on (zoned wall-clock with `timezone`). */
|
|
48
|
+
const plane = (ms) => (timezone ? toZonedTime(ms, timezone).getTime() : ms);
|
|
49
|
+
const nowMs = () => (timezone ? nowInZone(timezone).getTime() : Date.now());
|
|
50
|
+
// One store for the agenda's life: a getter adapter that changes identity
|
|
51
|
+
// reloads it instead of rebuilding it empty (every row blinking out).
|
|
52
|
+
const store = createEventStore(resolveAdapter);
|
|
44
53
|
// ── Window start (reactive, writable) ──
|
|
45
|
-
let startMs = $state(sod(initialDate
|
|
54
|
+
let startMs = $state(sod(initialDate ? plane(initialDate.getTime()) : nowMs()));
|
|
46
55
|
const endMs = $derived(addDaysMs(startMs, dayCount));
|
|
47
56
|
// ── Load events for the window (re-runs on paging / adapter swap) ──
|
|
48
|
-
|
|
57
|
+
// Eager initial load for synchronous adapters (the server render holds the
|
|
58
|
+
// rows); the effect's first run then has nothing new to fetch — a seeded
|
|
59
|
+
// adapter would otherwise send its seed range straight to the network.
|
|
60
|
+
let eagerKey = untrack(() => {
|
|
61
|
+
if (!resolveAdapter().fetchEventsSync)
|
|
62
|
+
return '';
|
|
49
63
|
store.load({ start: new Date(startMs), end: new Date(endMs) });
|
|
64
|
+
return `${startMs}-${endMs}`;
|
|
50
65
|
});
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
$effect(() => {
|
|
67
|
+
rawAdapter(); // a new adapter reloads
|
|
68
|
+
const key = `${startMs}-${endMs}`;
|
|
69
|
+
if (key === eagerKey) {
|
|
70
|
+
eagerKey = '';
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
eagerKey = '';
|
|
53
74
|
store.load({ start: new Date(startMs), end: new Date(endMs) });
|
|
54
75
|
});
|
|
55
76
|
// ── Day derivations ──
|
|
56
77
|
const days = $derived.by(() => {
|
|
57
|
-
const todayMs = sod(
|
|
78
|
+
const todayMs = sod(nowMs());
|
|
58
79
|
const events = store.events;
|
|
59
80
|
return Array.from({ length: dayCount }, (_, i) => {
|
|
60
81
|
const ms = addDaysMs(startMs, i);
|
|
@@ -69,7 +90,7 @@ export function createRangeAgenda(options) {
|
|
|
69
90
|
isToday: ms === todayMs,
|
|
70
91
|
isPast: dayEnd <= todayMs,
|
|
71
92
|
events: events
|
|
72
|
-
.filter((ev) => ev
|
|
93
|
+
.filter((ev) => overlapsRange(ev, date, new Date(dayEnd)))
|
|
73
94
|
.sort((a, b) => a.start.getTime() - b.start.getTime()),
|
|
74
95
|
};
|
|
75
96
|
});
|
|
@@ -98,10 +119,10 @@ export function createRangeAgenda(options) {
|
|
|
98
119
|
startMs = addDaysMs(startMs, dayCount);
|
|
99
120
|
},
|
|
100
121
|
goToday() {
|
|
101
|
-
startMs = sod(
|
|
122
|
+
startMs = sod(nowMs());
|
|
102
123
|
},
|
|
103
124
|
setDate(date) {
|
|
104
|
-
startMs = sod(date.getTime());
|
|
125
|
+
startMs = sod(plane(date.getTime()));
|
|
105
126
|
},
|
|
106
127
|
refresh() {
|
|
107
128
|
void store.load({ start: new Date(startMs), end: new Date(endMs) });
|
package/dist/headless/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export type { AgendaOptions, HeadlessAgenda } from './create-agenda.svelte.js';
|
|
|
4
4
|
export { createRangeAgenda } from './create-range-agenda.svelte.js';
|
|
5
5
|
export type { RangeAgendaOptions, RangeAgendaDay, HeadlessRangeAgenda, } from './create-range-agenda.svelte.js';
|
|
6
6
|
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, } from './types.js';
|
|
7
|
+
export type { TimeSlot } from '../views/shared/format.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, FloatingPanel, } from './primitives/index.js';
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export
|
|
2
|
+
export type { FloatingPanelAnchor, FloatingPanelProps } from './primitives/index.js';
|
|
3
|
+
export { Calendar, defaultViews } from './calendar/index.js';
|
|
4
|
+
export type { CalendarView, CalendarViewProps, CalendarProps } from './calendar/index.js';
|
|
5
|
+
export { Planner, PlannerScroll, Agenda, Mobile, MonthGrid } from './views/index.js';
|
|
6
|
+
export { useCalendarContext } from './views/shared/context.svelte.js';
|
|
7
|
+
export type { CalendarContext } from './views/shared/context.svelte.js';
|
|
6
8
|
export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
|
|
7
9
|
export type { EventStore, ViewState, ViewStateOptions, CalendarViewId, BuiltInViewId, ViewMode, Selection as CalendarSelection, DragState, DragMode, DragPayload, } from './engine/index.js';
|
|
8
10
|
export { createMemoryAdapter, createRestAdapter, createRecurringAdapter, createMappedAdapter, createCompositeAdapter, createJmapAdapter, withInitialEvents } from './adapters/index.js';
|
|
11
|
+
export { CalendarReadOnlyError, EventNotFoundError, isReadOnlyError, isNotFoundError } from './adapters/index.js';
|
|
9
12
|
export type { CalendarAdapter, WritableCalendarAdapter, DateRange, MemoryAdapterOptions, RestAdapterOptions, RecurringEvent, RecurringAdapterOptions, FieldMapping, MappedAdapterOptions, MutationHandler, CompositeAdapterOptions, JmapClient, JmapCalendarAdapterOptions, } from './adapters/index.js';
|
|
10
|
-
export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
|
|
13
|
+
export { createClock, sod, startOfWeek, addDaysMs, diffDays, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
|
|
11
14
|
export type { Clock, TimelineEvent, BlockedSlot, DaySegment, CalendarLabels, EventStatus, } from './core/index.js';
|
|
12
15
|
export { auto, neutral, midnight, presets } from './theme/index.js';
|
|
13
16
|
export { probeHostTheme, observeHostTheme } from './theme/index.js';
|
|
14
17
|
export { wrapAdapterWithTimezone } from './core/timezone.js';
|
|
15
18
|
export type { PresetName, AutoThemeOptions } from './theme/index.js';
|
|
16
19
|
export { createCalendar, createAgenda, createRangeAgenda } from './headless/index.js';
|
|
17
|
-
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, AgendaOptions, HeadlessAgenda, RangeAgendaOptions, RangeAgendaDay, HeadlessRangeAgenda, } from './headless/index.js';
|
|
20
|
+
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, AgendaOptions, HeadlessAgenda, RangeAgendaOptions, RangeAgendaDay, HeadlessRangeAgenda, TimeSlot, } from './headless/index.js';
|
|
18
21
|
export { fits, lineCount, textHeight, textWidth, pickFit, fontOf, fitLabel, fitParts, breakLines, typeset } from './text-fit.js';
|
|
19
|
-
export type { ChipPart } from './text-fit.js';
|
|
22
|
+
export type { ChipPart, TextLayoutOptions } from './text-fit.js';
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
// ─── Primitives ─────────────────────────────────────────
|
|
2
2
|
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, FloatingPanel, } from './primitives/index.js';
|
|
3
3
|
// ─── Calendar shell ─────────────────────────────────────
|
|
4
|
-
export { Calendar } from './calendar/index.js';
|
|
5
|
-
// Raw view components —
|
|
6
|
-
|
|
7
|
-
export {
|
|
4
|
+
export { Calendar, defaultViews } from './calendar/index.js';
|
|
5
|
+
// Raw view components — register them under your own ids, or compose your
|
|
6
|
+
// own shell around the engine. They read the Calendar's context.
|
|
7
|
+
export { Planner, PlannerScroll, Agenda, Mobile, MonthGrid } from './views/index.js';
|
|
8
|
+
// For custom views: the running Calendar's engines, config and labels.
|
|
9
|
+
export { useCalendarContext } from './views/shared/context.svelte.js';
|
|
8
10
|
// ─── Engine (reactive state) ────────────────────────────
|
|
9
11
|
export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
|
|
10
12
|
// ─── Adapters ───────────────────────────────────────────
|
|
11
13
|
export { createMemoryAdapter, createRestAdapter, createRecurringAdapter, createMappedAdapter, createCompositeAdapter, createJmapAdapter, withInitialEvents } from './adapters/index.js';
|
|
14
|
+
export { CalendarReadOnlyError, EventNotFoundError, isReadOnlyError, isNotFoundError } from './adapters/index.js';
|
|
12
15
|
// ─── Core: clock, time, locale, types ───────────────────
|
|
13
|
-
export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
|
|
16
|
+
export { createClock, sod, startOfWeek, addDaysMs, diffDays, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
|
|
14
17
|
// ─── Themes ─────────────────────────────────────────────
|
|
15
18
|
export { auto, neutral, midnight, presets } from './theme/index.js';
|
|
16
19
|
export { probeHostTheme, observeHostTheme } from './theme/index.js';
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
- Short/long weekday + date number for week views
|
|
7
7
|
-->
|
|
8
8
|
<script lang="ts">import { weekdayShort, weekdayLong, monthShort, fmtDay } from "../core/locale.js";
|
|
9
|
-
|
|
9
|
+
import { sod } from "../core/time.js";
|
|
10
|
+
let { dayMs, todayMs = sod(Date.now()), format = "short", isToday = false, isPast = false } = $props();
|
|
10
11
|
const dayNum = $derived(new Date(dayMs).getDate());
|
|
11
12
|
</script>
|
|
12
13
|
|
|
@@ -16,7 +17,7 @@ const dayNum = $derived(new Date(dayMs).getDate());
|
|
|
16
17
|
class:dh-past={isPast}
|
|
17
18
|
>
|
|
18
19
|
{#if format === 'relative'}
|
|
19
|
-
<span class="dh-rel">{fmtDay(dayMs, todayMs)}</span>
|
|
20
|
+
<span class="dh-rel">{fmtDay(sod(dayMs), sod(todayMs))}</span>
|
|
20
21
|
{:else if format === 'long'}
|
|
21
22
|
<span class="dh-name dh-name-long">{weekdayLong(dayMs)}</span>
|
|
22
23
|
<span class="dh-date">{monthShort(dayMs)} {dayNum}</span>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
/** Timestamp (ms) of the day to label */
|
|
3
3
|
dayMs: number;
|
|
4
|
-
/**
|
|
4
|
+
/** Any timestamp (ms) on today, for relative labels — default: now */
|
|
5
5
|
todayMs?: number;
|
|
6
6
|
/** Display format */
|
|
7
7
|
format?: 'relative' | 'short' | 'long';
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
between events.
|
|
7
7
|
-->
|
|
8
8
|
<script lang="ts">import { fmtTime, fmtDuration, getLabels } from "../core/locale.js";
|
|
9
|
-
|
|
9
|
+
import { getContext } from "svelte";
|
|
10
|
+
// Inside a Calendar: its per-instance labels; standalone: the global ones.
|
|
11
|
+
const calendarCtx = getContext("calendar");
|
|
12
|
+
const L = $derived(calendarCtx?.labels ?? getLabels());
|
|
10
13
|
let { start, end, onclick, orientation = "vertical" } = $props();
|
|
11
14
|
const dur = $derived(`${fmtDuration(start, end)} ${L.free}`);
|
|
12
15
|
function handleKeydown(e) {
|
|
@@ -26,7 +29,7 @@ function handleKeydown(e) {
|
|
|
26
29
|
class:es-h={orientation === 'horizontal'}
|
|
27
30
|
role="button"
|
|
28
31
|
tabindex="0"
|
|
29
|
-
aria-label="{L.createEvent}, {fmtTime(start)}
|
|
32
|
+
aria-label="{L.createEvent}, {fmtTime(start)} – {fmtTime(end)}, {dur}"
|
|
30
33
|
onclick={() => onclick?.({ start, end })}
|
|
31
34
|
onkeydown={handleKeydown}
|
|
32
35
|
>
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
Emits: onclick, ondragstart, onresize (for future interaction wiring)
|
|
10
10
|
-->
|
|
11
11
|
<script lang="ts">import { fmtTime, fmtDuration, getLabels } from "../core/locale.js";
|
|
12
|
-
|
|
12
|
+
import { getContext } from "svelte";
|
|
13
|
+
// Inside a Calendar: its per-instance labels; standalone: the global ones.
|
|
14
|
+
const calendarCtx = getContext("calendar");
|
|
15
|
+
const L = $derived(calendarCtx?.labels ?? getLabels());
|
|
13
16
|
let { event, variant = "chip", active = false, past = false, showTime = false, showDuration = false, editable = false, onclick, children } = $props();
|
|
14
17
|
const accentColor = $derived(event.color || "var(--dt-accent, #2563eb)");
|
|
15
18
|
const isCancelled = $derived(event.status === "cancelled");
|
|
@@ -18,10 +21,11 @@ const isFull = $derived(event.status === "full");
|
|
|
18
21
|
const isLimited = $derived(event.status === "limited");
|
|
19
22
|
const ariaLabel = $derived.by(() => {
|
|
20
23
|
const t = event.title;
|
|
21
|
-
const time = `${fmtTime(event.start)}
|
|
24
|
+
const time = `${fmtTime(event.start)} – ${fmtTime(event.end)}`;
|
|
22
25
|
const dur = fmtDuration(event.start, event.end);
|
|
23
26
|
const loc = event.location ? `, ${event.location}` : "";
|
|
24
|
-
const
|
|
27
|
+
const status = isCancelled ? L.cancelled : isTentative ? L.tentative : isFull ? L.full : isLimited ? L.limited : "";
|
|
28
|
+
const statusStr = status ? `, ${status}` : "";
|
|
25
29
|
const activeStr = active ? `, ${L.happeningNow}` : past ? `, ${L.past}` : "";
|
|
26
30
|
return `${t}${loc}, ${time}, ${dur}${statusStr}${activeStr}`;
|
|
27
31
|
});
|
|
@@ -63,11 +67,7 @@ function handleKeydown(e) {
|
|
|
63
67
|
{/each}
|
|
64
68
|
</div>
|
|
65
69
|
{/if}
|
|
66
|
-
{
|
|
67
|
-
{#if isTentative}<span class="eb-status-badge eb-tentative-badge">Tentative</span>{/if}
|
|
68
|
-
{#if isFull}<span class="eb-status-badge eb-full-badge">Full</span>{/if}
|
|
69
|
-
{#if isLimited}<span class="eb-status-badge eb-limited-badge">Limited</span>{/if}
|
|
70
|
-
{#if active}<span class="eb-live-badge">{L.now}</span>{/if}
|
|
70
|
+
{@render badges()}
|
|
71
71
|
</div>
|
|
72
72
|
{:else}
|
|
73
73
|
<!-- row -->
|
|
@@ -90,13 +90,20 @@ function handleKeydown(e) {
|
|
|
90
90
|
{/each}
|
|
91
91
|
</span>
|
|
92
92
|
{/if}
|
|
93
|
-
{
|
|
94
|
-
{#if isFull}<span class="eb-status-badge eb-full-badge">Full</span>{/if}
|
|
95
|
-
{#if isLimited}<span class="eb-status-badge eb-limited-badge">Limited</span>{/if}
|
|
96
|
-
{#if active}<span class="eb-live-badge">{L.now}</span>{/if}
|
|
93
|
+
{@render badges()}
|
|
97
94
|
{/if}
|
|
98
95
|
{/snippet}
|
|
99
96
|
|
|
97
|
+
<!-- Status badges: the same set in card and row. The badge CSS uppercases
|
|
98
|
+
the (lower-case) status labels. -->
|
|
99
|
+
{#snippet badges()}
|
|
100
|
+
{#if isCancelled}<span class="eb-status-badge eb-cancelled-badge">{L.cancelled}</span>{/if}
|
|
101
|
+
{#if isTentative}<span class="eb-status-badge eb-tentative-badge">{L.tentative}</span>{/if}
|
|
102
|
+
{#if isFull}<span class="eb-status-badge eb-full-badge">{L.full}</span>{/if}
|
|
103
|
+
{#if isLimited}<span class="eb-status-badge eb-limited-badge">{L.limited}</span>{/if}
|
|
104
|
+
{#if active}<span class="eb-live-badge">{L.now}</span>{/if}
|
|
105
|
+
{/snippet}
|
|
106
|
+
|
|
100
107
|
{#if onclick}
|
|
101
108
|
<div
|
|
102
109
|
class="eb eb-{variant}"
|
|
@@ -331,8 +338,8 @@ function handleKeydown(e) {
|
|
|
331
338
|
background: color-mix(in srgb, var(--_color) 12%, transparent);
|
|
332
339
|
}
|
|
333
340
|
.eb-full-badge {
|
|
334
|
-
color: #2563eb;
|
|
335
|
-
background: color-mix(in srgb, #2563eb 15%, transparent);
|
|
341
|
+
color: var(--dt-accent, #2563eb);
|
|
342
|
+
background: color-mix(in srgb, var(--dt-accent, #2563eb) 15%, transparent);
|
|
336
343
|
}
|
|
337
344
|
.eb-limited-badge {
|
|
338
345
|
color: var(--_color);
|
|
@@ -1,9 +1,48 @@
|
|
|
1
1
|
<script module lang="ts">// Phone sheet scroll-lock: nested panels each take a lock; the page behind
|
|
2
2
|
// unlocks when the last one closes.
|
|
3
3
|
let sheetLocks = 0;
|
|
4
|
+
// Open panels, oldest first. Escape and the outside tap belong to the
|
|
5
|
+
// topmost one only, so a nested panel closes without its parent.
|
|
6
|
+
const openPanels = [];
|
|
7
|
+
// Events a panel already acted on — the next panel down must not act on
|
|
8
|
+
// the same keydown/pointerdown once the top one unmounts mid-dispatch.
|
|
9
|
+
const handled = new WeakSet();
|
|
10
|
+
/**
|
|
11
|
+
* Spend the click a closing tap synthesizes. `onclose` usually unmounts the
|
|
12
|
+
* panel before that click arrives, so the listener lives on `window`,
|
|
13
|
+
* outside the component: it eats the next click (capture phase, before any
|
|
14
|
+
* calendar handler) and removes itself then, on the next pointerdown, on a
|
|
15
|
+
* pointercancel (no click follows), or after a timeout.
|
|
16
|
+
*/
|
|
17
|
+
function swallowNextClick() {
|
|
18
|
+
const eat = (e) => {
|
|
19
|
+
e.stopPropagation();
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
done();
|
|
22
|
+
};
|
|
23
|
+
const done = () => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
window.removeEventListener("click", eat, true);
|
|
26
|
+
window.removeEventListener("pointerdown", done, true);
|
|
27
|
+
window.removeEventListener("pointercancel", done, true);
|
|
28
|
+
};
|
|
29
|
+
const timer = setTimeout(done, 1500);
|
|
30
|
+
window.addEventListener("click", eat, true);
|
|
31
|
+
window.addEventListener("pointercancel", done, true);
|
|
32
|
+
// Armed after this pointerdown has finished dispatching.
|
|
33
|
+
setTimeout(() => window.addEventListener("pointerdown", done, true), 0);
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
4
36
|
</script>
|
|
5
37
|
|
|
6
|
-
<script lang="ts"
|
|
38
|
+
<script lang="ts">// A non-modal window that floats over the calendar: opened beside the
|
|
39
|
+
// block that was clicked (right of it, left when the right edge is near,
|
|
40
|
+
// clamped to the viewport), dragged by its header, closed by Escape, its
|
|
41
|
+
// own button, or a click anywhere outside — that outside click is spent on
|
|
42
|
+
// closing, so a block under it is not opened by the same tap. Below 640px
|
|
43
|
+
// it is a bottom sheet instead.
|
|
44
|
+
import { untrack } from "svelte";
|
|
45
|
+
let { title = "", anchor = {
|
|
7
46
|
x: 24,
|
|
8
47
|
y: 24
|
|
9
48
|
}, width = 440, theme = "", closeLabel = "Close", closeOnOutside = true, onclose, children } = $props();
|
|
@@ -28,11 +67,66 @@ function beside(a) {
|
|
|
28
67
|
const px = right + w + EDGE <= window.innerWidth ? right : a.x - GAP - w >= EDGE ? a.x - GAP - w : a.x;
|
|
29
68
|
place(px, a.y);
|
|
30
69
|
}
|
|
70
|
+
// Where focus goes back to when the panel closes: whatever held it when
|
|
71
|
+
// the panel opened (or when a new anchor re-focused it).
|
|
72
|
+
let returnFocus = null;
|
|
73
|
+
// Set once the user drags the panel: from then on a size change re-clamps
|
|
74
|
+
// it where it is instead of jumping back beside the anchor.
|
|
75
|
+
let dragged = false;
|
|
31
76
|
// A new anchor (the next click) re-places the panel; each run focuses it.
|
|
32
77
|
$effect(() => {
|
|
78
|
+
dragged = false;
|
|
33
79
|
beside(anchor);
|
|
80
|
+
const active = document.activeElement;
|
|
81
|
+
if (active instanceof HTMLElement && active !== document.body && !el?.contains(active)) {
|
|
82
|
+
returnFocus = active;
|
|
83
|
+
}
|
|
34
84
|
el?.focus({ preventScroll: true });
|
|
35
85
|
});
|
|
86
|
+
// Content that grows (or shrinks) after opening — a form section, loaded
|
|
87
|
+
// data — must stay inside the viewport; so must a resized window.
|
|
88
|
+
$effect(() => {
|
|
89
|
+
if (!el) return;
|
|
90
|
+
const reclamp = () => {
|
|
91
|
+
if (dragged) place(left, top);
|
|
92
|
+
else beside(anchor);
|
|
93
|
+
};
|
|
94
|
+
const ro = new ResizeObserver(reclamp);
|
|
95
|
+
ro.observe(el);
|
|
96
|
+
window.addEventListener("resize", reclamp);
|
|
97
|
+
return () => {
|
|
98
|
+
ro.disconnect();
|
|
99
|
+
window.removeEventListener("resize", reclamp);
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
// Topmost-panel stack + focus restore on close.
|
|
103
|
+
const myId = Symbol("floating-panel");
|
|
104
|
+
const isTopmost = () => openPanels[openPanels.length - 1] === myId;
|
|
105
|
+
$effect(() => {
|
|
106
|
+
const id = myId;
|
|
107
|
+
openPanels.push(id);
|
|
108
|
+
const panel = untrack(() => el);
|
|
109
|
+
return () => {
|
|
110
|
+
const i = openPanels.indexOf(id);
|
|
111
|
+
if (i !== -1) openPanels.splice(i, 1);
|
|
112
|
+
// Only pull focus back when it would otherwise be lost (inside the
|
|
113
|
+
// closing panel, or dropped to <body>) — not from where the user
|
|
114
|
+
// has since moved it.
|
|
115
|
+
const active = document.activeElement;
|
|
116
|
+
const lost = !active || active === document.body || !!panel && panel.contains(active);
|
|
117
|
+
const target = returnFocus;
|
|
118
|
+
if (lost && target && target.isConnected) {
|
|
119
|
+
target.focus({ preventScroll: true });
|
|
120
|
+
} else if (lost && target) {
|
|
121
|
+
// The opener may be re-rendered by the close (a view refresh);
|
|
122
|
+
// try once more after the DOM settles.
|
|
123
|
+
queueMicrotask(() => {
|
|
124
|
+
const a = document.activeElement;
|
|
125
|
+
if ((!a || a === document.body) && target.isConnected) target.focus({ preventScroll: true });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
});
|
|
36
130
|
// In sheet mode the page behind must not scroll — drags on the sheet
|
|
37
131
|
// chrome, or reaching the end of the sheet body, would otherwise reach it.
|
|
38
132
|
$effect(() => {
|
|
@@ -59,35 +153,36 @@ function down(e) {
|
|
|
59
153
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
60
154
|
}
|
|
61
155
|
function move(e) {
|
|
62
|
-
if (grab)
|
|
156
|
+
if (!grab) return;
|
|
157
|
+
dragged = true;
|
|
158
|
+
place(e.clientX - grab.dx, e.clientY - grab.dy);
|
|
63
159
|
}
|
|
64
160
|
function up() {
|
|
65
161
|
grab = null;
|
|
66
162
|
}
|
|
67
163
|
// The outside pointerdown is swallowed here, before any calendar handler,
|
|
68
164
|
// and so is the click it would synthesize — one tap closes, the next acts.
|
|
69
|
-
|
|
165
|
+
// With nested panels only the topmost reacts: a tap outside it (even on
|
|
166
|
+
// the panel beneath) closes it alone.
|
|
70
167
|
function outsideDown(e) {
|
|
71
168
|
if (!closeOnOutside || !el || el.contains(e.target)) return;
|
|
169
|
+
if (handled.has(e) || !isTopmost()) return;
|
|
170
|
+
handled.add(e);
|
|
72
171
|
e.stopPropagation();
|
|
73
172
|
e.preventDefault();
|
|
74
|
-
|
|
173
|
+
swallowNextClick();
|
|
75
174
|
onclose?.();
|
|
76
175
|
}
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
e.
|
|
81
|
-
|
|
176
|
+
function onKeydown(e) {
|
|
177
|
+
// defaultPrevented: a control inside the panel (a combobox, a menu)
|
|
178
|
+
// already used this Escape to close itself.
|
|
179
|
+
if (e.key !== "Escape" || e.defaultPrevented || handled.has(e) || !isTopmost()) return;
|
|
180
|
+
handled.add(e);
|
|
181
|
+
onclose?.();
|
|
82
182
|
}
|
|
83
|
-
export {};
|
|
84
183
|
</script>
|
|
85
184
|
|
|
86
|
-
<svelte:window
|
|
87
|
-
onkeydown={(e) => e.key === 'Escape' && onclose?.()}
|
|
88
|
-
onpointerdowncapture={outsideDown}
|
|
89
|
-
onclickcapture={outsideClick}
|
|
90
|
-
/>
|
|
185
|
+
<svelte:window onkeydown={onKeydown} onpointerdowncapture={outsideDown} />
|
|
91
186
|
|
|
92
187
|
<div
|
|
93
188
|
class="fp"
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
|
|
2
|
+
/** What a FloatingPanel opens beside: a viewport rect (a clicked block's `getBoundingClientRect()`), or a bare point. */
|
|
3
|
+
export type FloatingPanelAnchor = {
|
|
3
4
|
x: number;
|
|
4
5
|
y: number;
|
|
5
6
|
width?: number;
|
|
6
7
|
height?: number;
|
|
7
8
|
};
|
|
8
|
-
|
|
9
|
+
export interface FloatingPanelProps {
|
|
9
10
|
title?: string;
|
|
10
11
|
/** What to open beside: the clicked block's viewport rect, or a bare point. */
|
|
11
|
-
anchor?:
|
|
12
|
+
anchor?: FloatingPanelAnchor;
|
|
13
|
+
/** Panel width in px (default 440). Below 640px viewport width it is a bottom sheet. */
|
|
12
14
|
width?: number;
|
|
13
15
|
/** The same `--dt-*` theme string a Calendar takes. */
|
|
14
16
|
theme?: string;
|
|
@@ -17,7 +19,7 @@ type $$ComponentProps = {
|
|
|
17
19
|
closeOnOutside?: boolean;
|
|
18
20
|
onclose?: () => void;
|
|
19
21
|
children: Snippet;
|
|
20
|
-
}
|
|
21
|
-
declare const FloatingPanel: import("svelte").Component
|
|
22
|
+
}
|
|
23
|
+
declare const FloatingPanel: import("svelte").Component<FloatingPanelProps, {}, "">;
|
|
22
24
|
type FloatingPanel = ReturnType<typeof FloatingPanel>;
|
|
23
25
|
export default FloatingPanel;
|
|
@@ -12,14 +12,19 @@
|
|
|
12
12
|
<NowIndicator mode="badge" />
|
|
13
13
|
-->
|
|
14
14
|
<script lang="ts">import { getLabels } from "../core/locale.js";
|
|
15
|
-
|
|
15
|
+
import { getContext } from "svelte";
|
|
16
|
+
// Inside a Calendar: its per-instance labels; standalone: the global ones.
|
|
17
|
+
const calendarCtx = getContext("calendar");
|
|
18
|
+
const L = $derived(calendarCtx?.labels ?? getLabels());
|
|
16
19
|
let { mode = "line", position = 0, orientation = "vertical", time = "", seconds = "", showLabel = true, color, children } = $props();
|
|
17
20
|
const posStyle = $derived(orientation === "vertical" ? `left: ${position}px` : `top: ${position}px`);
|
|
18
21
|
const colorVar = $derived(color ? `--ni-color: ${color}` : "");
|
|
19
22
|
</script>
|
|
20
23
|
|
|
21
24
|
{#if mode === 'badge'}
|
|
22
|
-
|
|
25
|
+
<!-- Plain text, not a live region: the time it carries changes every
|
|
26
|
+
second, and a status role would read each change aloud. -->
|
|
27
|
+
<span class="ni-badge" style={colorVar}>
|
|
23
28
|
{#if children}
|
|
24
29
|
{@render children()}
|
|
25
30
|
{:else}
|
|
@@ -27,7 +32,9 @@ const colorVar = $derived(color ? `--ni-color: ${color}` : "");
|
|
|
27
32
|
{/if}
|
|
28
33
|
</span>
|
|
29
34
|
{:else if mode === 'dot'}
|
|
30
|
-
|
|
35
|
+
<!-- Decorative: the visible time is the grid's own; hidden from AT so a
|
|
36
|
+
ticking label is never announced. -->
|
|
37
|
+
<div class="ni ni-dot {orientation}" style="{posStyle}; {colorVar}" aria-hidden="true">
|
|
31
38
|
<div class="ni-dot-circle"></div>
|
|
32
39
|
{#if showLabel && time}
|
|
33
40
|
<div class="ni-label">
|
|
@@ -37,7 +44,7 @@ const colorVar = $derived(color ? `--ni-color: ${color}` : "");
|
|
|
37
44
|
{/if}
|
|
38
45
|
</div>
|
|
39
46
|
{:else}
|
|
40
|
-
<div class="ni ni-line {orientation}" style="{posStyle}; {colorVar}"
|
|
47
|
+
<div class="ni ni-line {orientation}" style="{posStyle}; {colorVar}" aria-hidden="true">
|
|
41
48
|
<div class="ni-line-bar"></div>
|
|
42
49
|
{#if showLabel && time}
|
|
43
50
|
<div class="ni-label">
|
|
@@ -8,23 +8,26 @@
|
|
|
8
8
|
<script lang="ts">import { HOURS } from "../core/time.js";
|
|
9
9
|
import { fmtH } from "../core/locale.js";
|
|
10
10
|
let { orientation = "horizontal", hourSize = 120, halfHour = true, hours = HOURS, formatHour = fmtH } = $props();
|
|
11
|
+
// Labels sit relative to the first hour shown: hours [6…21] start at 0px,
|
|
12
|
+
// not 6 hours down.
|
|
13
|
+
const firstHour = $derived(hours[0] ?? 0);
|
|
11
14
|
</script>
|
|
12
15
|
|
|
13
16
|
{#if orientation === 'horizontal'}
|
|
14
|
-
<div class="tg tg-h" style="--tg-hour: {hourSize}px">
|
|
17
|
+
<div class="tg tg-h" style="--tg-hour: {hourSize}px; --tg-count: {hours.length}">
|
|
15
18
|
{#each hours as h}
|
|
16
|
-
<div class="tg-tick" style="left: {h * hourSize}px">
|
|
19
|
+
<div class="tg-tick" style="left: {(h - firstHour) * hourSize}px">
|
|
17
20
|
<span class="tg-label">{formatHour(h)}</span>
|
|
18
21
|
</div>
|
|
19
22
|
{#if halfHour}
|
|
20
|
-
<div class="tg-tick tg-tick-half" style="left: {(h + 0.5) * hourSize}px"></div>
|
|
23
|
+
<div class="tg-tick tg-tick-half" style="left: {(h - firstHour + 0.5) * hourSize}px"></div>
|
|
21
24
|
{/if}
|
|
22
25
|
{/each}
|
|
23
26
|
</div>
|
|
24
27
|
{:else}
|
|
25
|
-
<div class="tg tg-v" style="--tg-hour: {hourSize}px">
|
|
28
|
+
<div class="tg tg-v" style="--tg-hour: {hourSize}px; --tg-count: {hours.length}">
|
|
26
29
|
{#each hours as h}
|
|
27
|
-
<div class="tg-row" style="top: {h * hourSize}px; height: {hourSize}px">
|
|
30
|
+
<div class="tg-row" style="top: {(h - firstHour) * hourSize}px; height: {hourSize}px">
|
|
28
31
|
<span class="tg-label">{formatHour(h)}</span>
|
|
29
32
|
</div>
|
|
30
33
|
{/each}
|
|
@@ -41,7 +44,7 @@ let { orientation = "horizontal", hourSize = 120, halfHour = true, hours = HOURS
|
|
|
41
44
|
/* ── Horizontal ── */
|
|
42
45
|
.tg-h {
|
|
43
46
|
height: 100%;
|
|
44
|
-
width: calc(24 * var(--tg-hour));
|
|
47
|
+
width: calc(var(--tg-count, 24) * var(--tg-hour));
|
|
45
48
|
}
|
|
46
49
|
.tg-tick {
|
|
47
50
|
position: absolute;
|
|
@@ -66,6 +69,7 @@ let { orientation = "horizontal", hourSize = 120, halfHour = true, hours = HOURS
|
|
|
66
69
|
/* ── Vertical ── */
|
|
67
70
|
.tg-v {
|
|
68
71
|
width: 48px;
|
|
72
|
+
height: calc(var(--tg-count, 24) * var(--tg-hour));
|
|
69
73
|
flex-shrink: 0;
|
|
70
74
|
}
|
|
71
75
|
.tg-row {
|
|
@@ -4,3 +4,4 @@ export { default as TimeGutter } from './TimeGutter.svelte';
|
|
|
4
4
|
export { default as DayHeader } from './DayHeader.svelte';
|
|
5
5
|
export { default as EmptySlot } from './EmptySlot.svelte';
|
|
6
6
|
export { default as FloatingPanel } from './FloatingPanel.svelte';
|
|
7
|
+
export type { FloatingPanelAnchor, FloatingPanelProps } from './FloatingPanel.svelte';
|
package/dist/text-fit.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { type PrepareOptions } from '@chenglou/pretext';
|
|
2
1
|
import type { Attachment } from 'svelte/attachments';
|
|
3
|
-
|
|
2
|
+
/** How text wraps when measured — the CSS properties of the same names. */
|
|
3
|
+
export interface TextLayoutOptions {
|
|
4
|
+
whiteSpace?: 'normal' | 'pre-wrap';
|
|
5
|
+
wordBreak?: 'normal' | 'keep-all';
|
|
6
|
+
/** Extra px between characters */
|
|
7
|
+
letterSpacing?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function lineCount(text: string, font: string, width: number, options?: TextLayoutOptions): number;
|
|
4
10
|
export declare function fits(text: string, font: string, width: number, lines?: number): boolean;
|
|
5
11
|
/** Height the text will take at `width`, before it is in the DOM. */
|
|
6
12
|
export declare function textHeight(text: string, font: string, width: number, lineHeight: number): number;
|