@nomideusz/svelte-calendar 0.16.0 → 0.20.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 +7 -2
- package/dist/adapters/composite.js +7 -2
- package/dist/adapters/recurring.d.ts +16 -0
- package/dist/adapters/recurring.js +21 -5
- package/dist/calendar/Calendar.svelte +13 -1
- package/dist/calendar/Calendar.svelte.d.ts +7 -0
- package/dist/engine/view-state.svelte.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/text-fit.d.ts +30 -0
- package/dist/text-fit.js +65 -8
- package/dist/views/planner/PlannerScroll.svelte +1217 -0
- package/dist/views/planner/PlannerScroll.svelte.d.ts +28 -0
- package/dist/views/planner/PlannerWeek.svelte +98 -13
- package/dist/views/planner/PlannerWeek.svelte.d.ts +7 -0
- package/dist/views/shared/chip-fit.svelte.d.ts +27 -0
- package/dist/views/shared/chip-fit.svelte.js +0 -0
- package/package.json +6 -6
- package/widget/widget.js +13816 -6235
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TimelineEvent } from '../../core/types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
mondayStart?: boolean;
|
|
4
|
+
locale?: string;
|
|
5
|
+
height?: number | null;
|
|
6
|
+
events?: TimelineEvent[];
|
|
7
|
+
style?: string;
|
|
8
|
+
focusDate?: Date;
|
|
9
|
+
/** `anchor` is the chip's viewport rect — a host positions a panel beside it. */
|
|
10
|
+
oneventclick?: (event: TimelineEvent, anchor?: DOMRect) => void;
|
|
11
|
+
oneventcreate?: (range: {
|
|
12
|
+
start: Date;
|
|
13
|
+
end: Date;
|
|
14
|
+
}) => void;
|
|
15
|
+
/** An HTML5 drag from outside the calendar dropped on a day. */
|
|
16
|
+
onexternaldrop?: (info: {
|
|
17
|
+
start: Date;
|
|
18
|
+
dataTransfer: DataTransfer;
|
|
19
|
+
}) => void;
|
|
20
|
+
selectedEventId?: string | null;
|
|
21
|
+
readOnly?: boolean;
|
|
22
|
+
/** Visible hour range [startHour, endHour) — start hour seeds empty-cell creation */
|
|
23
|
+
visibleHours?: [number, number];
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
declare const PlannerScroll: import("svelte").Component<Props, {}, "">;
|
|
27
|
+
type PlannerScroll = ReturnType<typeof PlannerScroll>;
|
|
28
|
+
export default PlannerScroll;
|
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
19
19
|
import EventContent from "../shared/EventContent.svelte";
|
|
20
20
|
import { createClock } from "../../core/clock.svelte.js";
|
|
21
|
+
import { fitParts } from "../../text-fit.js";
|
|
21
22
|
import { DAY_MS, HOUR_MS, sod, addDaysMs } from "../../core/time.js";
|
|
22
23
|
import { startOfWeek as sowFn, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
23
24
|
import { fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong } from "../../core/locale.js";
|
|
24
|
-
let { mode = "week", mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, selectedEventId = null, readOnly = false, visibleHours } = $props();
|
|
25
|
+
let { mode = "week", mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, onexternaldrop, selectedEventId = null, readOnly = false, visibleHours } = $props();
|
|
25
26
|
// ── Context ────────────────────────────────────────
|
|
26
27
|
const ctx = useCalendarContext();
|
|
27
28
|
const L = $derived(ctx.labels);
|
|
@@ -326,6 +327,40 @@ function blockedRangeLabel(dayMs, slotStart, slotEnd) {
|
|
|
326
327
|
return `${fmtTime(new Date(dayMs + slotStart * HOUR_MS), locale)} – ${fmtTime(new Date(dayMs + slotEnd * HOUR_MS), locale)}`;
|
|
327
328
|
}
|
|
328
329
|
// ── Status label (aria) ────────────────────────────
|
|
330
|
+
// ─── Block parts ────────────────────────────────
|
|
331
|
+
// A block stacks time, title and room, and a short one cannot hold all
|
|
332
|
+
// three. The same ladder the scroller runs on width runs here on height:
|
|
333
|
+
// time and title are anchors, the room is given up first. The line costs
|
|
334
|
+
// are .tw-ev-body's — 3px padding each side, 1px gaps, the parts' own
|
|
335
|
+
// line-heights — so the threshold is derived rather than picked. ROOM_AIR
|
|
336
|
+
// keeps the old breathing room: a block that fits the room to the pixel
|
|
337
|
+
// looked crammed, and the hand-set cutoff it replaces (56px) allowed for it.
|
|
338
|
+
const BODY_PAD_Y = 6;
|
|
339
|
+
const LINE_GAP = 1;
|
|
340
|
+
const TIME_LINE = 11 * 1.1;
|
|
341
|
+
const TITLE_LINE = 12 * 1.2;
|
|
342
|
+
const ROOM_LINE = 10 * 1.2;
|
|
343
|
+
const ROOM_AIR = 9;
|
|
344
|
+
function blockParts(ev, height) {
|
|
345
|
+
return fitParts([
|
|
346
|
+
{
|
|
347
|
+
key: "time",
|
|
348
|
+
size: TIME_LINE,
|
|
349
|
+
priority: 0
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
key: "title",
|
|
353
|
+
size: TITLE_LINE + LINE_GAP,
|
|
354
|
+
priority: 0
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
key: "room",
|
|
358
|
+
text: ev.location ?? "",
|
|
359
|
+
size: ROOM_LINE + LINE_GAP + ROOM_AIR,
|
|
360
|
+
priority: 1
|
|
361
|
+
}
|
|
362
|
+
], height - BODY_PAD_Y);
|
|
363
|
+
}
|
|
329
364
|
function statusText(ev) {
|
|
330
365
|
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
331
366
|
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
@@ -354,6 +389,13 @@ function ghostForDay(dayMs) {
|
|
|
354
389
|
showTime: cs === Math.max(s, dayMs)
|
|
355
390
|
};
|
|
356
391
|
}
|
|
392
|
+
/** The drag as the time axis sees it: the segment holding its start. */
|
|
393
|
+
const axisGhost = $derived.by(() => {
|
|
394
|
+
if (!drag?.active || !drag.payload) return null;
|
|
395
|
+
const s = drag.payload.start.getTime();
|
|
396
|
+
const day = dayCols.find((d) => s >= d.ms && s < d.ms + 24 * HOUR_MS);
|
|
397
|
+
return day ? ghostForDay(day.ms) : null;
|
|
398
|
+
});
|
|
357
399
|
// ── Drag-to-create (mouse: sweep; touch: long-press) ──
|
|
358
400
|
// Mouse movement below CREATE_THRESHOLD stays a plain click. On touch a
|
|
359
401
|
// vertical drag must remain a scroll, so create only arms after a ~350ms
|
|
@@ -392,6 +434,26 @@ function startColsCreate() {
|
|
|
392
434
|
drag.beginCreate(new Date(crAnchorMs), new Date(crAnchorMs + SNAP_MS));
|
|
393
435
|
addTouchScrollBlock();
|
|
394
436
|
}
|
|
437
|
+
// ── External drop (HTML5 DnD) ──────────────────────
|
|
438
|
+
function onColsDragOver(e) {
|
|
439
|
+
if (!onexternaldrop || readOnly) return;
|
|
440
|
+
const day = dayCols[pointerDayIndex(e.clientX)];
|
|
441
|
+
if (!day || day.isDisabled) return;
|
|
442
|
+
e.preventDefault();
|
|
443
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
444
|
+
}
|
|
445
|
+
function onColsDrop(e) {
|
|
446
|
+
if (!onexternaldrop || readOnly || !e.dataTransfer) return;
|
|
447
|
+
const day = dayCols[pointerDayIndex(e.clientX)];
|
|
448
|
+
if (!day || day.isDisabled) return;
|
|
449
|
+
e.preventDefault();
|
|
450
|
+
const raw = clampToDayBand(day.ms + pointerHour(e.clientY) * HOUR_MS, day.ms);
|
|
451
|
+
const snapped = day.ms + Math.floor((raw - day.ms) / SNAP_MS) * SNAP_MS;
|
|
452
|
+
onexternaldrop({
|
|
453
|
+
start: new Date(snapped),
|
|
454
|
+
dataTransfer: e.dataTransfer
|
|
455
|
+
});
|
|
456
|
+
}
|
|
395
457
|
function onColsPointerDown(e) {
|
|
396
458
|
if (e.button !== 0 || !drag || !oneventcreate || readOnly) return;
|
|
397
459
|
if (e.target.closest(".tw-ev, .tw-ghost")) return;
|
|
@@ -736,8 +798,13 @@ function onWindowKeydown(e) {
|
|
|
736
798
|
<span class="tw-gutter-lb" style:top="{i * HOUR_H}px">{fmtH(startHour + i, locale)}</span>
|
|
737
799
|
{/if}
|
|
738
800
|
{/each}
|
|
801
|
+
{#if axisGhost}
|
|
802
|
+
<!-- The axis reads the drag to the minute, at both edges. -->
|
|
803
|
+
<span class="tw-gutter-drag" style:top="{axisGhost.top}px">{fmtTime(axisGhost.start, locale)}</span>
|
|
804
|
+
<span class="tw-gutter-drag" style:top="{axisGhost.top + axisGhost.height}px">{fmtTime(axisGhost.end, locale)}</span>
|
|
805
|
+
{/if}
|
|
739
806
|
{#if nowY !== null && weekHasToday}
|
|
740
|
-
<span class="tw-gutter-now" style:top="{nowY}px"
|
|
807
|
+
<span class="tw-gutter-now" style:top="{nowY}px">{fmtTime(new Date(clock.tick), locale)}</span>
|
|
741
808
|
{/if}
|
|
742
809
|
</div>
|
|
743
810
|
|
|
@@ -748,6 +815,8 @@ function onWindowKeydown(e) {
|
|
|
748
815
|
bind:this={colsEl}
|
|
749
816
|
onclick={handleColsClick}
|
|
750
817
|
onpointerdown={onColsPointerDown}
|
|
818
|
+
ondragover={onColsDragOver}
|
|
819
|
+
ondrop={onColsDrop}
|
|
751
820
|
oncontextmenu={onColsContextMenu}
|
|
752
821
|
role="presentation"
|
|
753
822
|
>
|
|
@@ -808,6 +877,7 @@ function onWindowKeydown(e) {
|
|
|
808
877
|
class:tw-ev--tentative={p.ev.status === 'tentative'}
|
|
809
878
|
class:tw-ev--full={p.ev.status === 'full'}
|
|
810
879
|
class:tw-ev--limited={p.ev.status === 'limited'}
|
|
880
|
+
class:tw-ev--past={p.ev.end.getTime() < clock.tick}
|
|
811
881
|
class:tw-ev--short={p.height < 44}
|
|
812
882
|
class:tw-ev--compact={p.height < 34}
|
|
813
883
|
style:top="{p.top}px"
|
|
@@ -827,9 +897,10 @@ function onWindowKeydown(e) {
|
|
|
827
897
|
<div class="tw-ev-stripe" aria-hidden="true"></div>
|
|
828
898
|
<div class="tw-ev-body">
|
|
829
899
|
<EventContent event={p.ev}>
|
|
900
|
+
{@const parts = blockParts(p.ev, p.height)}
|
|
830
901
|
<span class="tw-ev-time">{fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}</span>
|
|
831
902
|
<span class="tw-ev-title">{p.ev.title}</span>
|
|
832
|
-
{#if
|
|
903
|
+
{#if parts.room}
|
|
833
904
|
<span class="tw-ev-loc">{p.ev.location}</span>
|
|
834
905
|
{/if}
|
|
835
906
|
</EventContent>
|
|
@@ -1137,13 +1208,23 @@ function onWindowKeydown(e) {
|
|
|
1137
1208
|
white-space: nowrap;
|
|
1138
1209
|
}
|
|
1139
1210
|
|
|
1140
|
-
|
|
1211
|
+
/* The clock beside the now line, and the drag's edges while it lasts —
|
|
1212
|
+
both sit on the surface so they cover the hour label beneath. */
|
|
1213
|
+
.tw-gutter-now,
|
|
1214
|
+
.tw-gutter-drag {
|
|
1141
1215
|
position: absolute;
|
|
1142
|
-
right:
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
border-radius:
|
|
1146
|
-
background: var(--dt-
|
|
1216
|
+
right: 3px;
|
|
1217
|
+
transform: translateY(-50%);
|
|
1218
|
+
padding: 1px 3px;
|
|
1219
|
+
border-radius: 3px;
|
|
1220
|
+
background: var(--dt-surface, var(--dt-bg, #fff));
|
|
1221
|
+
font: 600 10px/1.2 var(--dt-mono, ui-monospace, monospace);
|
|
1222
|
+
color: var(--dt-accent, #2563eb);
|
|
1223
|
+
white-space: nowrap;
|
|
1224
|
+
z-index: 2;
|
|
1225
|
+
}
|
|
1226
|
+
.tw-gutter-now {
|
|
1227
|
+
opacity: 0.75;
|
|
1147
1228
|
transform: translateY(-50%);
|
|
1148
1229
|
z-index: 2;
|
|
1149
1230
|
}
|
|
@@ -1187,7 +1268,11 @@ function onWindowKeydown(e) {
|
|
|
1187
1268
|
.tw-col--today { background: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
|
|
1188
1269
|
/* Dim past days with a wash, never a subtree opacity (event contrast) */
|
|
1189
1270
|
.tw-col--past {
|
|
1190
|
-
background: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87))
|
|
1271
|
+
background: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent);
|
|
1272
|
+
}
|
|
1273
|
+
/* What has already happened steps back; a cancelled block keeps its own look. */
|
|
1274
|
+
.tw-ev--past:not(.tw-ev--cancelled) {
|
|
1275
|
+
opacity: 0.6;
|
|
1191
1276
|
}
|
|
1192
1277
|
.tw-col--weekend:not(.tw-col--today):not(.tw-col--past) {
|
|
1193
1278
|
background: var(--dt-weekend-bg, rgba(0, 0, 0, 0.012));
|
|
@@ -1235,9 +1320,9 @@ function onWindowKeydown(e) {
|
|
|
1235
1320
|
position: absolute;
|
|
1236
1321
|
left: 0;
|
|
1237
1322
|
right: 0;
|
|
1238
|
-
height:
|
|
1239
|
-
|
|
1240
|
-
|
|
1323
|
+
height: 0;
|
|
1324
|
+
border-top: 1px dotted var(--dt-accent, #2563eb);
|
|
1325
|
+
opacity: 0.7;
|
|
1241
1326
|
z-index: 12;
|
|
1242
1327
|
pointer-events: none;
|
|
1243
1328
|
transform: translateY(-1px);
|
|
@@ -13,6 +13,13 @@ interface Props {
|
|
|
13
13
|
start: Date;
|
|
14
14
|
end: Date;
|
|
15
15
|
}) => void;
|
|
16
|
+
/** Something dragged in from OUTSIDE the calendar (HTML5 drag and drop —
|
|
17
|
+
* a class chip, a template) dropped on the grid: `start` is the pointer's
|
|
18
|
+
* time snapped to the grid, `dataTransfer` whatever the source set. */
|
|
19
|
+
onexternaldrop?: (info: {
|
|
20
|
+
start: Date;
|
|
21
|
+
dataTransfer: DataTransfer;
|
|
22
|
+
}) => void;
|
|
16
23
|
selectedEventId?: string | null;
|
|
17
24
|
readOnly?: boolean;
|
|
18
25
|
/** Visible hour range [startHour, endHour) */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The measuring half of chip labels: one ResizeObserver for a whole view, the
|
|
3
|
+
* fonts read from a hidden probe so they are known before the first chip of a
|
|
4
|
+
* kind renders, and a width cache cleared when webfonts settle.
|
|
5
|
+
*
|
|
6
|
+
* The deciding half is `fitParts` (pure, in text-fit.ts). A view owns the
|
|
7
|
+
* ladder — which parts it could show, in which order it gives them up.
|
|
8
|
+
*/
|
|
9
|
+
import { type ChipPart } from '../../text-fit.js';
|
|
10
|
+
export interface ChipFitOptions {
|
|
11
|
+
/** Selector for the box one chip gets; its content width is the budget. */
|
|
12
|
+
slot: string;
|
|
13
|
+
/** Font key → selector. Point these at a hidden probe, not at a live chip:
|
|
14
|
+
* a chip that is currently dropped cannot tell anyone what font it wanted. */
|
|
15
|
+
fonts: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
export interface ChipFit {
|
|
18
|
+
/** Content width of one slot, px. 0 until measured (SSR, first paint). */
|
|
19
|
+
readonly width: number;
|
|
20
|
+
/** The probed fonts, as canvas font strings, by key. */
|
|
21
|
+
readonly fonts: Record<string, string>;
|
|
22
|
+
/** `{@attach fit.watch}` on the element whose resize resizes the chips. */
|
|
23
|
+
watch(root: HTMLElement): () => void;
|
|
24
|
+
/** Which parts fit; `reserve` is what the chip spends on itself (padding). */
|
|
25
|
+
parts(list: readonly ChipPart[], reserve?: number): Record<string, boolean>;
|
|
26
|
+
}
|
|
27
|
+
export declare function createChipFit(options: ChipFitOptions): ChipFit;
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nomideusz/svelte-calendar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "A themeable Svelte 5 calendar with Day and Week views — Planner and Agenda.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -45,19 +45,19 @@
|
|
|
45
45
|
"svelte": "^5.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@sveltejs/adapter-auto": "8.0.0-next.
|
|
49
|
-
"@sveltejs/adapter-vercel": "7.0.0-next.
|
|
50
|
-
"@sveltejs/kit": "3.0.0-next.
|
|
48
|
+
"@sveltejs/adapter-auto": "8.0.0-next.4",
|
|
49
|
+
"@sveltejs/adapter-vercel": "7.0.0-next.9",
|
|
50
|
+
"@sveltejs/kit": "3.0.0-next.27",
|
|
51
51
|
"@sveltejs/package": "3.0.0-next.7",
|
|
52
52
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
53
53
|
"@types/node": "^22.20.0",
|
|
54
54
|
"@vercel/analytics": "^1.6.1",
|
|
55
55
|
"jsdom": "^29.1.1",
|
|
56
56
|
"marked": "^17.0.6",
|
|
57
|
-
"svelte": "^5.
|
|
57
|
+
"svelte": "^5.57.0",
|
|
58
58
|
"svelte-check": "^4.7.1",
|
|
59
59
|
"typescript": "^6.0.3",
|
|
60
|
-
"vite": "^8.
|
|
60
|
+
"vite": "^8.3.0",
|
|
61
61
|
"vitest": "^4.1.10"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|