@nomideusz/svelte-calendar 0.6.3 → 0.6.5
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 +192 -10
- package/dist/adapters/composite.d.ts +15 -0
- package/dist/adapters/composite.js +65 -0
- package/dist/adapters/index.d.ts +5 -1
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/mapped.d.ts +170 -0
- package/dist/adapters/mapped.js +343 -0
- package/dist/adapters/recurring.d.ts +4 -0
- package/dist/adapters/recurring.js +5 -10
- package/dist/adapters/types.d.ts +11 -2
- package/dist/assets/favicon.svg +9 -1
- package/dist/calendar/Calendar.svelte +161 -90
- package/dist/calendar/Calendar.svelte.d.ts +12 -0
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +2 -0
- package/dist/core/measure.d.ts +104 -0
- package/dist/core/measure.js +195 -0
- package/dist/core/time.js +2 -2
- package/dist/core/types.d.ts +32 -0
- package/dist/engine/event-store.svelte.js +22 -7
- package/dist/headless/create-agenda.svelte.d.ts +67 -0
- package/dist/headless/create-agenda.svelte.js +139 -0
- package/dist/headless/create-calendar.svelte.d.ts +2 -0
- package/dist/headless/create-calendar.svelte.js +380 -0
- package/dist/headless/index.d.ts +4 -0
- package/dist/headless/index.js +2 -0
- package/dist/headless/types.d.ts +219 -0
- package/dist/headless/types.js +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -2
- package/dist/primitives/DayHeader.svelte +6 -6
- package/dist/primitives/EmptySlot.svelte +3 -3
- package/dist/primitives/EventBlock.svelte +99 -14
- package/dist/primitives/NowIndicator.svelte +5 -5
- package/dist/views/agenda/AgendaDay.svelte +215 -116
- package/dist/views/agenda/AgendaWeek.svelte +166 -113
- package/dist/views/mobile/MobileDay.svelte +45 -21
- package/dist/views/mobile/MobileWeek.svelte +85 -35
- package/dist/views/planner/PlannerDay.svelte +116 -60
- package/dist/views/planner/PlannerWeek.svelte +228 -166
- package/dist/views/shared/context.svelte.d.ts +47 -0
- package/dist/views/shared/context.svelte.js +47 -0
- package/dist/views/shared/format.d.ts +19 -0
- package/dist/views/shared/format.js +46 -0
- package/dist/views/shared/index.d.ts +4 -0
- package/dist/views/shared/index.js +2 -0
- package/package.json +9 -2
|
@@ -108,6 +108,8 @@
|
|
|
108
108
|
maxDuration?: number;
|
|
109
109
|
/** Specific dates to disable (greyed-out, no event creation). */
|
|
110
110
|
disabledDates?: Date[];
|
|
111
|
+
/** Compact mode: use minimal text-row rendering in Agenda views (dot + time + title). */
|
|
112
|
+
compact?: boolean;
|
|
111
113
|
/**
|
|
112
114
|
* Mobile mode.
|
|
113
115
|
* - `'auto'` (default): detect via viewport width (< 768 px)
|
|
@@ -123,6 +125,16 @@
|
|
|
123
125
|
empty?: Snippet;
|
|
124
126
|
/** Custom day header snippet. Receives { date, isToday, dayName }. */
|
|
125
127
|
dayHeader?: Snippet<[{ date: Date; isToday: boolean; dayName: string }]>;
|
|
128
|
+
/**
|
|
129
|
+
* Replace the entire header chrome (date label + mode pills + nav arrows).
|
|
130
|
+
* Receives context: { dateLabel, mode, modes, switchMode, prev, next, goToday, isViewOnToday, focusDate }.
|
|
131
|
+
*/
|
|
132
|
+
header?: Snippet<[import('../headless/types.js').HeaderContext]>;
|
|
133
|
+
/**
|
|
134
|
+
* Replace just the navigation controls (arrows + today button).
|
|
135
|
+
* Receives context: { prev, next, goToday, isViewOnToday, focusDate, mode }.
|
|
136
|
+
*/
|
|
137
|
+
navigation?: Snippet<[import('../headless/types.js').NavigationContext]>;
|
|
126
138
|
|
|
127
139
|
// ── Callbacks ──
|
|
128
140
|
oneventclick?: (event: TimelineEvent) => void;
|
|
@@ -171,10 +183,13 @@
|
|
|
171
183
|
minDuration,
|
|
172
184
|
maxDuration,
|
|
173
185
|
disabledDates,
|
|
186
|
+
compact = false,
|
|
174
187
|
mobile: mobileProp = 'auto',
|
|
175
188
|
event: eventSnippet,
|
|
176
189
|
empty: emptySnippet,
|
|
177
190
|
dayHeader: dayHeaderSnippet,
|
|
191
|
+
header: headerSnippet,
|
|
192
|
+
navigation: navigationSnippet,
|
|
178
193
|
oneventclick,
|
|
179
194
|
oneventcreate,
|
|
180
195
|
oneventmove,
|
|
@@ -187,20 +202,12 @@
|
|
|
187
202
|
const effectiveCreate = $derived(readOnly ? undefined : oneventcreate);
|
|
188
203
|
const effectiveMove = $derived(readOnly ? undefined : oneventmove);
|
|
189
204
|
|
|
190
|
-
// ── Mobile detection ──
|
|
191
|
-
let
|
|
192
|
-
|
|
193
|
-
onMount(() => {
|
|
194
|
-
if (typeof window === 'undefined') return;
|
|
195
|
-
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
196
|
-
isMobileViewport = mql.matches;
|
|
197
|
-
function onChange(e: MediaQueryListEvent) { isMobileViewport = e.matches; }
|
|
198
|
-
mql.addEventListener('change', onChange);
|
|
199
|
-
return () => mql.removeEventListener('change', onChange);
|
|
200
|
-
});
|
|
205
|
+
// ── Mobile detection (container-based, not viewport) ──
|
|
206
|
+
let containerWidth = $state(0);
|
|
207
|
+
const isMobileContainer = $derived(containerWidth > 0 && containerWidth < MOBILE_BREAKPOINT);
|
|
201
208
|
|
|
202
209
|
const useMobile = $derived(
|
|
203
|
-
mobileProp === 'auto' ?
|
|
210
|
+
mobileProp === 'auto' ? isMobileContainer : Boolean(mobileProp)
|
|
204
211
|
);
|
|
205
212
|
|
|
206
213
|
// ── Smart auto-theme ──
|
|
@@ -208,15 +215,27 @@
|
|
|
208
215
|
// probe the host page on mount and reactively watch for host theme changes.
|
|
209
216
|
let calEl: HTMLElement | undefined = $state();
|
|
210
217
|
let probedTheme = $state('');
|
|
218
|
+
// Only delay rendering when auto-probe is actually needed
|
|
219
|
+
const needsProbe = $derived(theme === auto && autoTheme !== false);
|
|
211
220
|
|
|
212
221
|
onMount(() => {
|
|
213
222
|
if (!calEl) return;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
223
|
+
|
|
224
|
+
// Measure container width for mobile detection
|
|
225
|
+
containerWidth = calEl.clientWidth;
|
|
226
|
+
const ro = new ResizeObserver((entries) => {
|
|
227
|
+
containerWidth = Math.round(entries[0].contentRect.width);
|
|
228
|
+
});
|
|
229
|
+
ro.observe(calEl);
|
|
230
|
+
|
|
231
|
+
// Only probe theme when using the auto preset
|
|
232
|
+
if (!needsProbe) return () => ro.disconnect();
|
|
217
233
|
|
|
218
234
|
const opts: AutoThemeOptions = typeof autoTheme === 'object' ? autoTheme : {};
|
|
219
|
-
|
|
235
|
+
const stopTheme = observeHostTheme(calEl, (vars) => {
|
|
236
|
+
probedTheme = vars;
|
|
237
|
+
}, opts);
|
|
238
|
+
return () => { ro.disconnect(); stopTheme?.(); };
|
|
220
239
|
});
|
|
221
240
|
|
|
222
241
|
/** Effective theme: user-provided takes priority, otherwise probed auto. */
|
|
@@ -236,7 +255,7 @@
|
|
|
236
255
|
|
|
237
256
|
// ── Drag commit handler ──
|
|
238
257
|
// Views call this on pointer-up to process drag results.
|
|
239
|
-
function commitDrag(): void {
|
|
258
|
+
async function commitDrag(): Promise<void> {
|
|
240
259
|
if (readOnly) { drag.cancel(); return; }
|
|
241
260
|
const mode = drag.mode;
|
|
242
261
|
const payload = drag.commit();
|
|
@@ -288,55 +307,68 @@
|
|
|
288
307
|
}
|
|
289
308
|
|
|
290
309
|
if ((mode === 'move' || mode === 'resize-start' || mode === 'resize-end') && payload.eventId) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
310
|
+
try {
|
|
311
|
+
await store.move(payload.eventId, start, end);
|
|
312
|
+
const ev = store.byId(payload.eventId);
|
|
313
|
+
if (ev) effectiveMove?.(ev, start, end);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
// Silently handle read-only / missing event errors.
|
|
316
|
+
// The optimistic update in store.move() already reverted.
|
|
317
|
+
const msg = e instanceof Error ? e.message : '';
|
|
318
|
+
if (!msg.includes('read-only') && !msg.includes('not found')) {
|
|
319
|
+
console.warn('[calendar] drag commit failed:', e);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
294
322
|
} else if (mode === 'create') {
|
|
295
323
|
effectiveCreate?.({ start, end });
|
|
296
324
|
}
|
|
297
325
|
}
|
|
298
326
|
|
|
299
|
-
// Context is set once at mount; engine objects hold $state internally
|
|
300
|
-
// so consumers reading from them stay reactive automatically.
|
|
301
|
-
// Wrap store in a getter since it's $derived and may change when adapter changes.
|
|
302
|
-
setContext('calendar:store', { get current() { return store; } });
|
|
303
|
-
setContext('calendar:viewState', viewState);
|
|
304
|
-
setContext('calendar:selection', selection);
|
|
305
|
-
setContext('calendar:drag', drag);
|
|
306
|
-
setContext('calendar:commitDrag', commitDrag);
|
|
307
|
-
// Callbacks: provide a getter-based object so context consumers
|
|
308
|
-
// always see the latest callback references.
|
|
309
|
-
setContext('calendar:callbacks', {
|
|
310
|
-
get oneventclick() { return oneventclick; },
|
|
311
|
-
get oneventcreate() { return effectiveCreate; },
|
|
312
|
-
get oneventmove() { return effectiveMove; },
|
|
313
|
-
get oneventhover() { return oneventhover; },
|
|
314
|
-
});
|
|
315
|
-
setContext('calendar:readOnly', { get current() { return readOnly; } });
|
|
316
|
-
setContext('calendar:visibleHours', { get current() { return visibleHours; } });
|
|
317
|
-
setContext('calendar:snapInterval', { get current() { return snapInterval; } });
|
|
318
|
-
setContext('calendar:eventSnippet', { get current() { return eventSnippet; } });
|
|
319
|
-
setContext('calendar:emptySnippet', { get current() { return emptySnippet; } });
|
|
320
|
-
setContext('calendar:showNavigation', { get current() { return showNavigation; } });
|
|
321
|
-
setContext('calendar:equalDays', { get current() { return equalDays; } });
|
|
322
|
-
setContext('calendar:showDates', { get current() { return showDates; } });
|
|
323
|
-
setContext('calendar:hideDays', { get current() { return hideDays; } });
|
|
324
|
-
setContext('calendar:blockedSlots', { get current() { return blockedSlots; } });
|
|
325
|
-
setContext('calendar:dayHeaderSnippet', { get current() { return dayHeaderSnippet; } });
|
|
326
|
-
setContext('calendar:minDuration', { get current() { return minDuration; } });
|
|
327
|
-
setContext('calendar:maxDuration', { get current() { return maxDuration; } });
|
|
328
|
-
setContext('calendar:disabledDates', { get current() { return disabledDates; } });
|
|
329
|
-
setContext('calendar:mobile', { get current() { return useMobile; } });
|
|
330
|
-
setContext('calendar:autoHeight', { get current() { return heightProp === 'auto'; } });
|
|
331
|
-
|
|
332
327
|
// ── Load range signal ──
|
|
333
328
|
// Views can write a wider range here to override the default viewState.range.
|
|
334
329
|
// This lets infinite-scroll views (PlannerWeek, PlannerDay) declare their
|
|
335
330
|
// buffer needs without directly calling store.load().
|
|
336
331
|
let viewLoadRange = $state<{ start: Date; end: Date } | null>(null);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
332
|
+
|
|
333
|
+
// ── Single context object ──
|
|
334
|
+
// All view state is exposed through one context key with reactive getters.
|
|
335
|
+
// Views read this via useCalendarContext() from views/shared/context.svelte.ts.
|
|
336
|
+
setContext('calendar', {
|
|
337
|
+
// Engine objects (hold $state internally)
|
|
338
|
+
get store() { return store; },
|
|
339
|
+
viewState,
|
|
340
|
+
selection,
|
|
341
|
+
drag,
|
|
342
|
+
commitDrag,
|
|
343
|
+
|
|
344
|
+
// Callbacks
|
|
345
|
+
get oneventclick() { return oneventclick; },
|
|
346
|
+
get oneventcreate() { return effectiveCreate; },
|
|
347
|
+
get oneventmove() { return effectiveMove; },
|
|
348
|
+
get oneventhover() { return oneventhover; },
|
|
349
|
+
|
|
350
|
+
// Config (reactive via getters)
|
|
351
|
+
get readOnly() { return readOnly; },
|
|
352
|
+
get visibleHours() { return visibleHours; },
|
|
353
|
+
get snapInterval() { return snapInterval; },
|
|
354
|
+
get eventSnippet() { return eventSnippet; },
|
|
355
|
+
get emptySnippet() { return emptySnippet; },
|
|
356
|
+
get showNavigation() { return showNavigation; },
|
|
357
|
+
get equalDays() { return equalDays; },
|
|
358
|
+
get showDates() { return showDates; },
|
|
359
|
+
get hideDays() { return hideDays; },
|
|
360
|
+
get blockedSlots() { return blockedSlots; },
|
|
361
|
+
get dayHeaderSnippet() { return dayHeaderSnippet; },
|
|
362
|
+
get minDuration() { return minDuration; },
|
|
363
|
+
get maxDuration() { return maxDuration; },
|
|
364
|
+
get disabledDates() { return disabledDates; },
|
|
365
|
+
get mobile() { return useMobile; },
|
|
366
|
+
get autoHeight() { return heightProp === 'auto'; },
|
|
367
|
+
get compact() { return compact; },
|
|
368
|
+
|
|
369
|
+
// Load range (read/write)
|
|
370
|
+
get loadRange() { return viewLoadRange; },
|
|
371
|
+
setLoadRange(range: { start: Date; end: Date } | null) { viewLoadRange = range; },
|
|
340
372
|
});
|
|
341
373
|
|
|
342
374
|
// ── Load events when effective range changes ──
|
|
@@ -344,6 +376,9 @@
|
|
|
344
376
|
const range = viewLoadRange ?? viewState.range;
|
|
345
377
|
store.load({ start: range.start, end: range.end });
|
|
346
378
|
});
|
|
379
|
+
// Eager initial load — $effect runs after paint, but for sync adapters
|
|
380
|
+
// (memory, recurring) this resolves in the same microtask.
|
|
381
|
+
untrack(() => store.load({ start: viewState.range.start, end: viewState.range.end }));
|
|
347
382
|
|
|
348
383
|
// Keep active view in sync when external view prop changes after mount.
|
|
349
384
|
$effect(() => {
|
|
@@ -378,6 +413,7 @@
|
|
|
378
413
|
onviewchange?.(viewState.view);
|
|
379
414
|
});
|
|
380
415
|
|
|
416
|
+
|
|
381
417
|
// ── Resolve active view ──
|
|
382
418
|
// When mobile is active, Planner views get remapped to Mobile variants.
|
|
383
419
|
// Agenda views stay as Agenda — they're already list-based and will adapt
|
|
@@ -448,6 +484,29 @@
|
|
|
448
484
|
const { start, end } = viewState.range;
|
|
449
485
|
return now >= start.getTime() && now < end.getTime();
|
|
450
486
|
});
|
|
487
|
+
|
|
488
|
+
/** Header context for custom header snippet */
|
|
489
|
+
const headerCtx = $derived({
|
|
490
|
+
dateLabel,
|
|
491
|
+
mode: viewState.mode,
|
|
492
|
+
modes,
|
|
493
|
+
switchMode,
|
|
494
|
+
prev: () => viewState.prev(),
|
|
495
|
+
next: () => viewState.next(),
|
|
496
|
+
goToday: () => viewState.goToday(),
|
|
497
|
+
isViewOnToday: viewIncludesToday,
|
|
498
|
+
focusDate: viewState.focusDate,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
/** Navigation context for custom navigation snippet */
|
|
502
|
+
const navCtx = $derived({
|
|
503
|
+
prev: () => viewState.prev(),
|
|
504
|
+
next: () => viewState.next(),
|
|
505
|
+
goToday: () => viewState.goToday(),
|
|
506
|
+
isViewOnToday: viewIncludesToday,
|
|
507
|
+
focusDate: viewState.focusDate,
|
|
508
|
+
mode: viewState.mode,
|
|
509
|
+
});
|
|
451
510
|
</script>
|
|
452
511
|
|
|
453
512
|
<div
|
|
@@ -460,11 +519,17 @@
|
|
|
460
519
|
dir={dir}
|
|
461
520
|
lang={locale}
|
|
462
521
|
>
|
|
522
|
+
<!-- ─── Custom header snippet (replaces all chrome) ─── -->
|
|
523
|
+
{#if headerSnippet}
|
|
524
|
+
{@render headerSnippet(headerCtx)}
|
|
525
|
+
|
|
463
526
|
<!-- ─── Mobile header (flow layout, no absolute) ─── -->
|
|
464
|
-
{
|
|
527
|
+
{:else if useMobile}
|
|
465
528
|
<div class="cal-m-hd">
|
|
466
529
|
<div class="cal-m-left">
|
|
467
|
-
{#if
|
|
530
|
+
{#if navigationSnippet}
|
|
531
|
+
{@render navigationSnippet(navCtx)}
|
|
532
|
+
{:else if showNavigation}
|
|
468
533
|
<button class="cal-m-nav" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : L.previousWeek}>
|
|
469
534
|
<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>
|
|
470
535
|
</button>
|
|
@@ -489,7 +554,7 @@
|
|
|
489
554
|
<span class="cal-m-title">{dateLabel}</span>
|
|
490
555
|
|
|
491
556
|
<div class="cal-m-right">
|
|
492
|
-
{#if showNavigation}
|
|
557
|
+
{#if !navigationSnippet && showNavigation}
|
|
493
558
|
<button class="cal-m-nav" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : L.nextWeek}>
|
|
494
559
|
<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>
|
|
495
560
|
</button>
|
|
@@ -498,29 +563,31 @@
|
|
|
498
563
|
</div>
|
|
499
564
|
|
|
500
565
|
<!-- Today pill — floats below header, doesn't affect header flow -->
|
|
501
|
-
{#if showNavigation && !viewIncludesToday}
|
|
566
|
+
{#if !navigationSnippet && showNavigation && !viewIncludesToday}
|
|
502
567
|
<div class="cal-m-today-bar">
|
|
503
568
|
<button class="cal-m-today" onclick={() => viewState.goToday()}>
|
|
504
569
|
{L.today}
|
|
505
570
|
</button>
|
|
506
571
|
</div>
|
|
507
572
|
{/if}
|
|
508
|
-
{/if}
|
|
509
573
|
|
|
510
|
-
<!--
|
|
511
|
-
{
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
574
|
+
<!-- ─── Desktop chrome ─── -->
|
|
575
|
+
{:else}
|
|
576
|
+
<!-- Floating mode pills (hidden for Agenda views) -->
|
|
577
|
+
{#if showModePills && modes.length > 1 && activeView?.label !== 'Agenda'}
|
|
578
|
+
<div class="cal-pills" role="group" aria-label={L.viewMode}>
|
|
579
|
+
{#each modes as g}
|
|
580
|
+
<button
|
|
581
|
+
class="cal-pill"
|
|
582
|
+
class:cal-pill--active={viewState.mode === g}
|
|
583
|
+
aria-pressed={viewState.mode === g}
|
|
584
|
+
onclick={() => switchMode(g)}
|
|
585
|
+
>
|
|
586
|
+
{g === 'day' ? L.day : L.week}
|
|
587
|
+
</button>
|
|
588
|
+
{/each}
|
|
589
|
+
</div>
|
|
590
|
+
{/if}
|
|
524
591
|
{/if}
|
|
525
592
|
|
|
526
593
|
<div class="cal-body">
|
|
@@ -554,19 +621,23 @@
|
|
|
554
621
|
<style>
|
|
555
622
|
.cal {
|
|
556
623
|
position: relative;
|
|
624
|
+
width: 100%;
|
|
625
|
+
min-width: 0;
|
|
557
626
|
height: var(--cal-h, 600px);
|
|
558
|
-
background: var(--dt-bg,
|
|
627
|
+
background: var(--dt-bg, inherit);
|
|
559
628
|
border-radius: var(--cal-r, 12px);
|
|
560
629
|
overflow: clip;
|
|
561
630
|
display: flex;
|
|
562
631
|
flex-direction: column;
|
|
563
632
|
border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
633
|
+
box-sizing: border-box;
|
|
564
634
|
}
|
|
565
635
|
.cal--auto {
|
|
566
636
|
height: auto;
|
|
567
637
|
overflow: visible;
|
|
568
638
|
}
|
|
569
639
|
|
|
640
|
+
|
|
570
641
|
/* ── Floating pills ── */
|
|
571
642
|
.cal-pills {
|
|
572
643
|
position: absolute;
|
|
@@ -576,7 +647,7 @@
|
|
|
576
647
|
z-index: 20;
|
|
577
648
|
display: flex;
|
|
578
649
|
gap: 2px;
|
|
579
|
-
background: color-mix(in srgb, var(--dt-surface, #
|
|
650
|
+
background: color-mix(in srgb, var(--dt-surface, var(--dt-bg, #ffffff)) 85%, transparent);
|
|
580
651
|
backdrop-filter: blur(6px);
|
|
581
652
|
-webkit-backdrop-filter: blur(6px);
|
|
582
653
|
border-radius: 8px;
|
|
@@ -589,7 +660,7 @@
|
|
|
589
660
|
background: transparent;
|
|
590
661
|
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
591
662
|
cursor: pointer;
|
|
592
|
-
font: 600 11px / 1 var(--dt-sans,
|
|
663
|
+
font: 600 11px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
593
664
|
padding: 6px 12px;
|
|
594
665
|
border-radius: 6px;
|
|
595
666
|
letter-spacing: 0.04em;
|
|
@@ -602,12 +673,12 @@
|
|
|
602
673
|
}
|
|
603
674
|
|
|
604
675
|
.cal-pill--active {
|
|
605
|
-
background: var(--dt-accent, #
|
|
676
|
+
background: var(--dt-accent, #2563eb);
|
|
606
677
|
color: var(--dt-btn-text, #fff);
|
|
607
678
|
}
|
|
608
679
|
|
|
609
680
|
.cal-pill:focus-visible {
|
|
610
|
-
outline: 2px solid color-mix(in srgb, var(--dt-accent, #
|
|
681
|
+
outline: 2px solid color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
|
|
611
682
|
outline-offset: 2px;
|
|
612
683
|
}
|
|
613
684
|
|
|
@@ -626,7 +697,7 @@
|
|
|
626
697
|
align-items: center;
|
|
627
698
|
justify-content: center;
|
|
628
699
|
height: 100%;
|
|
629
|
-
font: 400 13px / 1 var(--dt-sans,
|
|
700
|
+
font: 400 13px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
630
701
|
color: var(--dt-text-3, rgba(100, 116, 139, 0.55));
|
|
631
702
|
}
|
|
632
703
|
|
|
@@ -639,7 +710,7 @@
|
|
|
639
710
|
background: linear-gradient(
|
|
640
711
|
90deg,
|
|
641
712
|
transparent 0%,
|
|
642
|
-
var(--dt-accent, #
|
|
713
|
+
var(--dt-accent, #2563eb) 50%,
|
|
643
714
|
transparent 100%
|
|
644
715
|
);
|
|
645
716
|
animation: cal-slide 1.2s ease-in-out infinite;
|
|
@@ -696,14 +767,14 @@
|
|
|
696
767
|
background: var(--dt-accent-dim, rgba(239, 68, 68, 0.12));
|
|
697
768
|
}
|
|
698
769
|
.cal-m-nav:focus-visible {
|
|
699
|
-
outline: 2px solid color-mix(in srgb, var(--dt-accent, #
|
|
770
|
+
outline: 2px solid color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
|
|
700
771
|
outline-offset: 2px;
|
|
701
772
|
}
|
|
702
773
|
|
|
703
774
|
.cal-m-pills {
|
|
704
775
|
display: flex;
|
|
705
776
|
gap: 2px;
|
|
706
|
-
background: color-mix(in srgb, var(--dt-surface, #
|
|
777
|
+
background: color-mix(in srgb, var(--dt-surface, var(--dt-bg, #ffffff)) 85%, transparent);
|
|
707
778
|
border-radius: 8px;
|
|
708
779
|
padding: 2px;
|
|
709
780
|
border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
@@ -726,7 +797,7 @@
|
|
|
726
797
|
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
727
798
|
}
|
|
728
799
|
.cal-m-pill--active {
|
|
729
|
-
background: var(--dt-accent, #
|
|
800
|
+
background: var(--dt-accent, #2563eb);
|
|
730
801
|
color: var(--dt-btn-text, #fff);
|
|
731
802
|
}
|
|
732
803
|
|
|
@@ -750,8 +821,8 @@
|
|
|
750
821
|
|
|
751
822
|
.cal-m-today {
|
|
752
823
|
font: 600 11px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
753
|
-
color: var(--dt-accent, #
|
|
754
|
-
background: color-mix(in srgb, var(--dt-accent, #
|
|
824
|
+
color: var(--dt-accent, #2563eb);
|
|
825
|
+
background: color-mix(in srgb, var(--dt-accent, #2563eb) 10%, transparent);
|
|
755
826
|
border: none;
|
|
756
827
|
padding: 5px 10px;
|
|
757
828
|
border-radius: 6px;
|
|
@@ -764,13 +835,13 @@
|
|
|
764
835
|
flex-shrink: 0;
|
|
765
836
|
}
|
|
766
837
|
.cal-m-today:hover {
|
|
767
|
-
background: color-mix(in srgb, var(--dt-accent, #
|
|
838
|
+
background: color-mix(in srgb, var(--dt-accent, #2563eb) 18%, transparent);
|
|
768
839
|
}
|
|
769
840
|
.cal-m-today:active {
|
|
770
|
-
background: color-mix(in srgb, var(--dt-accent, #
|
|
841
|
+
background: color-mix(in srgb, var(--dt-accent, #2563eb) 25%, transparent);
|
|
771
842
|
}
|
|
772
843
|
.cal-m-today:focus-visible {
|
|
773
|
-
outline: 2px solid color-mix(in srgb, var(--dt-accent, #
|
|
844
|
+
outline: 2px solid color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
|
|
774
845
|
outline-offset: 2px;
|
|
775
846
|
}
|
|
776
847
|
</style>
|
|
@@ -72,6 +72,8 @@ interface Props {
|
|
|
72
72
|
maxDuration?: number;
|
|
73
73
|
/** Specific dates to disable (greyed-out, no event creation). */
|
|
74
74
|
disabledDates?: Date[];
|
|
75
|
+
/** Compact mode: use minimal text-row rendering in Agenda views (dot + time + title). */
|
|
76
|
+
compact?: boolean;
|
|
75
77
|
/**
|
|
76
78
|
* Mobile mode.
|
|
77
79
|
* - `'auto'` (default): detect via viewport width (< 768 px)
|
|
@@ -89,6 +91,16 @@ interface Props {
|
|
|
89
91
|
isToday: boolean;
|
|
90
92
|
dayName: string;
|
|
91
93
|
}]>;
|
|
94
|
+
/**
|
|
95
|
+
* Replace the entire header chrome (date label + mode pills + nav arrows).
|
|
96
|
+
* Receives context: { dateLabel, mode, modes, switchMode, prev, next, goToday, isViewOnToday, focusDate }.
|
|
97
|
+
*/
|
|
98
|
+
header?: Snippet<[import('../headless/types.js').HeaderContext]>;
|
|
99
|
+
/**
|
|
100
|
+
* Replace just the navigation controls (arrows + today button).
|
|
101
|
+
* Receives context: { prev, next, goToday, isViewOnToday, focusDate, mode }.
|
|
102
|
+
*/
|
|
103
|
+
navigation?: Snippet<[import('../headless/types.js').NavigationContext]>;
|
|
92
104
|
oneventclick?: (event: TimelineEvent) => void;
|
|
93
105
|
oneventcreate?: (range: {
|
|
94
106
|
start: Date;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -5,5 +5,7 @@ export type { DaySegment } from './time.js';
|
|
|
5
5
|
export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, defaultLabels, setLabels, resetLabels, getLabels, } from './locale.js';
|
|
6
6
|
export type { CalendarLabels } from './locale.js';
|
|
7
7
|
export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
|
|
8
|
-
export type { TimelineEvent, BlockedSlot, } from './types.js';
|
|
8
|
+
export type { TimelineEvent, BlockedSlot, EventStatus, } from './types.js';
|
|
9
9
|
export { generatePalette, extractAccent, VIVID_PALETTE } from './palette.js';
|
|
10
|
+
export { createTextMeasure, initTextMeasure } from './measure.js';
|
|
11
|
+
export type { TextMeasure, TextMeasureOptions, ContentFit } from './measure.js';
|
package/dist/core/index.js
CHANGED
|
@@ -9,3 +9,5 @@ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, fmtTime, fmtD
|
|
|
9
9
|
export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
|
|
10
10
|
// Palette
|
|
11
11
|
export { generatePalette, extractAccent, VIVID_PALETTE } from './palette.js';
|
|
12
|
+
// Text measurement (optional Pretext integration)
|
|
13
|
+
export { createTextMeasure, initTextMeasure } from './measure.js';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text measurement utilities powered by Pretext.
|
|
3
|
+
*
|
|
4
|
+
* Provides DOM-free text measurement for calendar event blocks.
|
|
5
|
+
* Pretext is an optional peer dependency — if not installed,
|
|
6
|
+
* these functions degrade gracefully to simple heuristics.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const m = createTextMeasure({ font: '500 12px Inter' });
|
|
10
|
+
* const fits = m.fits('Meeting with team', 120, 16); // width, lineHeight
|
|
11
|
+
* const { height, lineCount } = m.measure('Long event title...', 120, 16);
|
|
12
|
+
* m.clear(); // release caches
|
|
13
|
+
*/
|
|
14
|
+
export interface TextMeasure {
|
|
15
|
+
/**
|
|
16
|
+
* Measure text height at a given width.
|
|
17
|
+
* Returns { height, lineCount }.
|
|
18
|
+
*/
|
|
19
|
+
measure(text: string, maxWidth: number, lineHeight: number): {
|
|
20
|
+
height: number;
|
|
21
|
+
lineCount: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Check if text fits in a single line at the given width.
|
|
25
|
+
*/
|
|
26
|
+
fits(text: string, maxWidth: number, lineHeight: number): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Measure multiple texts and return their combined height.
|
|
29
|
+
* Useful for checking if title + subtitle + tags fit in a block.
|
|
30
|
+
*/
|
|
31
|
+
measureStack(items: Array<{
|
|
32
|
+
text: string;
|
|
33
|
+
font?: string;
|
|
34
|
+
lineHeight?: number;
|
|
35
|
+
}>, maxWidth: number, gap?: number): {
|
|
36
|
+
height: number;
|
|
37
|
+
breakdown: Array<{
|
|
38
|
+
height: number;
|
|
39
|
+
lineCount: number;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Given available height and width, decide which content layers fit.
|
|
44
|
+
* Returns a visibility map for common event fields.
|
|
45
|
+
*/
|
|
46
|
+
fitContent(opts: {
|
|
47
|
+
title: string;
|
|
48
|
+
subtitle?: string;
|
|
49
|
+
location?: string;
|
|
50
|
+
time?: string;
|
|
51
|
+
tags?: string[];
|
|
52
|
+
maxWidth: number;
|
|
53
|
+
maxHeight: number;
|
|
54
|
+
}): ContentFit;
|
|
55
|
+
/** Release internal caches. */
|
|
56
|
+
clear(): void;
|
|
57
|
+
/** Whether Pretext is available (true) or using heuristic fallback (false). */
|
|
58
|
+
readonly available: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface ContentFit {
|
|
61
|
+
/** Title always shown */
|
|
62
|
+
title: true;
|
|
63
|
+
/** Number of lines the title takes */
|
|
64
|
+
titleLines: number;
|
|
65
|
+
/** Whether subtitle fits */
|
|
66
|
+
subtitle: boolean;
|
|
67
|
+
/** Whether location fits */
|
|
68
|
+
location: boolean;
|
|
69
|
+
/** Whether time range fits */
|
|
70
|
+
time: boolean;
|
|
71
|
+
/** Whether tags fit */
|
|
72
|
+
tags: boolean;
|
|
73
|
+
/** Total height of all visible content */
|
|
74
|
+
totalHeight: number;
|
|
75
|
+
}
|
|
76
|
+
export interface TextMeasureOptions {
|
|
77
|
+
/** CSS font shorthand for titles (e.g. '500 12px Inter') */
|
|
78
|
+
titleFont?: string;
|
|
79
|
+
/** CSS font shorthand for secondary text (subtitle, location, time) */
|
|
80
|
+
secondaryFont?: string;
|
|
81
|
+
/** CSS font shorthand for tags */
|
|
82
|
+
tagFont?: string;
|
|
83
|
+
/** Default line height for titles */
|
|
84
|
+
titleLineHeight?: number;
|
|
85
|
+
/** Default line height for secondary text */
|
|
86
|
+
secondaryLineHeight?: number;
|
|
87
|
+
/** Gap between content rows in px */
|
|
88
|
+
contentGap?: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create a text measure instance.
|
|
92
|
+
* Lazily loads Pretext on first use. Falls back to char-width heuristics if unavailable.
|
|
93
|
+
*/
|
|
94
|
+
export declare function createTextMeasure(opts?: TextMeasureOptions): TextMeasure;
|
|
95
|
+
/**
|
|
96
|
+
* Initialize Pretext for text measurement.
|
|
97
|
+
* Call this once in your app's entry point (e.g., +layout.svelte onMount).
|
|
98
|
+
* If not called, measurement falls back to character-width heuristics.
|
|
99
|
+
*
|
|
100
|
+
* Usage:
|
|
101
|
+
* import { initTextMeasure } from '@nomideusz/svelte-calendar';
|
|
102
|
+
* onMount(() => initTextMeasure());
|
|
103
|
+
*/
|
|
104
|
+
export declare function initTextMeasure(): Promise<boolean>;
|