@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.
@@ -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"></span>
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 p.ev.location && p.height > 56}
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
- .tw-gutter-now {
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: -3px;
1143
- width: 6px;
1144
- height: 6px;
1145
- border-radius: 50%;
1146
- background: var(--dt-accent, #2563eb);
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)) 2.5%, transparent);
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: 2px;
1239
- background: var(--dt-accent, #2563eb);
1240
- box-shadow: 0 0 6px var(--dt-glow, rgba(37, 99, 235, 0.25));
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomideusz/svelte-calendar",
3
- "version": "0.16.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.3",
49
- "@sveltejs/adapter-vercel": "7.0.0-next.6",
50
- "@sveltejs/kit": "3.0.0-next.23",
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.56.9",
57
+ "svelte": "^5.57.0",
58
58
  "svelte-check": "^4.7.1",
59
59
  "typescript": "^6.0.3",
60
- "vite": "^8.2.1",
60
+ "vite": "^8.3.0",
61
61
  "vitest": "^4.1.10"
62
62
  },
63
63
  "dependencies": {