@zeyos/zx 4.2.0 → 4.3.4

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/README.md +5 -3
  3. package/dist/types/components/calendar/calendar-model.d.ts +22 -0
  4. package/dist/types/components/calendar/calendar.d.ts +19 -2
  5. package/dist/types/components/card-view/card-view.d.ts +1 -1
  6. package/dist/types/components/card-view/record-card.d.ts +47 -5
  7. package/dist/types/components/dialog/dialog.d.ts +6 -1
  8. package/dist/types/components/dropdown/dropdown.d.ts +5 -0
  9. package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
  10. package/dist/types/components/kanban-view/kanban-view.d.ts +669 -24
  11. package/dist/types/components/launcher/launcher.d.ts +5 -0
  12. package/dist/types/components/message/message.d.ts +9 -1
  13. package/dist/types/components/modal/modal.d.ts +44 -7
  14. package/dist/types/components/sheet/sheet.d.ts +22 -12
  15. package/dist/types/core/component.d.ts +6 -1
  16. package/dist/types/core/overlay-host.d.ts +12 -0
  17. package/dist/types/index.d.ts +2 -1
  18. package/dist/zx-zeyos.esm.js +173 -43
  19. package/dist/zx-zeyos.esm.min.js +1 -1
  20. package/dist/zx.css +876 -230
  21. package/dist/zx.esm.js +2053 -338
  22. package/dist/zx.esm.min.js +3 -3
  23. package/dist/zx.global.js +2053 -338
  24. package/dist/zx.global.min.js +3 -3
  25. package/dist/zx.min.css +1 -1
  26. package/docs/api.json +250 -2
  27. package/docs/llms.md +67 -48
  28. package/docs/llms.txt +1 -3
  29. package/package.json +1 -2
  30. package/src/components/account-menu/account-menu.css +1 -1
  31. package/src/components/app-rail/app-rail.css +1 -1
  32. package/src/components/calendar/calendar-model.js +45 -0
  33. package/src/components/calendar/calendar.css +96 -34
  34. package/src/components/calendar/calendar.js +122 -29
  35. package/src/components/card-view/card-view.css +211 -9
  36. package/src/components/card-view/card-view.js +7 -7
  37. package/src/components/card-view/record-card.js +92 -19
  38. package/src/components/context-menu/context-menu.css +2 -2
  39. package/src/components/context-menu/context-menu.js +5 -1
  40. package/src/components/date-picker/date-picker.css +18 -11
  41. package/src/components/datebox/date-range-box.css +4 -2
  42. package/src/components/datebox/datebox.css +4 -2
  43. package/src/components/dialog/dialog.css +4 -3
  44. package/src/components/dialog/dialog.js +2 -1
  45. package/src/components/dropdown/dropdown.css +4 -2
  46. package/src/components/dropdown/dropdown.js +7 -2
  47. package/src/components/kanban-view/kanban-policy.js +412 -0
  48. package/src/components/kanban-view/kanban-view.css +480 -36
  49. package/src/components/kanban-view/kanban-view.js +1514 -235
  50. package/src/components/launcher/launcher.css +10 -8
  51. package/src/components/launcher/launcher.js +9 -2
  52. package/src/components/menu-button/menu-button.css +2 -2
  53. package/src/components/message/message.css +15 -25
  54. package/src/components/message/message.js +17 -11
  55. package/src/components/modal/modal.css +6 -4
  56. package/src/components/modal/modal.js +99 -25
  57. package/src/components/select/select.css +9 -7
  58. package/src/components/sheet/sheet.css +25 -7
  59. package/src/components/sheet/sheet.js +43 -23
  60. package/src/components/table/table.css +5 -3
  61. package/src/components/tag-picker/tag-picker.css +5 -3
  62. package/src/components/tooltip/tooltip.css +4 -2
  63. package/src/components/tooltip/tooltip.js +4 -1
  64. package/src/core/component.js +8 -3
  65. package/src/core/overlay-host.js +23 -0
  66. package/src/index.js +8 -3
  67. package/styles/tokens/dark.css +4 -0
  68. package/styles/tokens/semantic.css +83 -8
  69. package/DESIGN-SYSTEM.md +0 -182
@@ -9,15 +9,20 @@
9
9
  min-inline-size: 0;
10
10
  overflow: hidden;
11
11
  border: 1px solid var(--zx-color-border);
12
- border-radius: var(--zx-radius-lg);
12
+ border-radius: 0;
13
13
  background: var(--zx-color-bg-surface);
14
14
  color: var(--zx-color-text);
15
15
  font-size: var(--zx-text-sm);
16
16
  }
17
17
 
18
+ /*
19
+ * The heading is the only elastic track. With `1fr auto 1fr` the end-justified commands group
20
+ * kept its min-content width inside a narrower `1fr` track and bled back over the centered
21
+ * heading; giving navigation and commands their natural width lets the heading ellipsize instead.
22
+ */
18
23
  .zx-calendar__toolbar {
19
24
  display: grid;
20
- grid-template-columns: 1fr auto 1fr;
25
+ grid-template-columns: auto minmax(0, 1fr) auto;
21
26
  align-items: center;
22
27
  gap: var(--zx-space-3);
23
28
  min-inline-size: 0;
@@ -35,7 +40,14 @@
35
40
 
36
41
  .zx-calendar__navigation { justify-self: start; gap: var(--zx-space-1); }
37
42
  .zx-calendar__commands { justify-self: end; gap: var(--zx-space-3); }
38
- .zx-calendar__views { overflow-x: auto; }
43
+ .zx-calendar__views {
44
+ block-size: var(--zx-control-height);
45
+ gap: 2px;
46
+ overflow-x: auto;
47
+ border-radius: 0;
48
+ background: var(--zx-color-bg-muted);
49
+ padding: 3px;
50
+ }
39
51
 
40
52
  .zx-calendar__toolbar-button,
41
53
  .zx-calendar__view-button,
@@ -59,15 +71,21 @@
59
71
  }
60
72
 
61
73
  .zx-calendar__toolbar-button:has(.zx-icon) { padding-inline: var(--zx-space-2); }
62
- .zx-calendar__view-button { border-inline-end-width: 0; padding-inline: var(--zx-space-3); }
63
- .zx-calendar__view-button:first-child { border-start-start-radius: var(--zx-control-radius); border-end-start-radius: var(--zx-control-radius); }
64
- .zx-calendar__view-button:last-child { border-inline-end-width: 1px; border-start-end-radius: var(--zx-control-radius); border-end-end-radius: var(--zx-control-radius); }
74
+ .zx-calendar__view-button {
75
+ block-size: 100%;
76
+ min-block-size: 0;
77
+ border-color: transparent;
78
+ border-radius: 0;
79
+ background: transparent;
80
+ padding-inline: var(--zx-space-3);
81
+ }
65
82
  .zx-calendar__view-button[aria-pressed="true"] {
66
83
  position: relative;
67
84
  z-index: 1;
68
- border-color: var(--zx-color-accent);
69
- background: var(--zx-color-bg-selected);
70
- color: var(--zx-color-accent);
85
+ border-color: var(--zx-color-border-control);
86
+ background: var(--zx-color-bg-control);
87
+ box-shadow: var(--zx-shadow-2);
88
+ color: var(--zx-color-text);
71
89
  }
72
90
 
73
91
  .zx-calendar__new {
@@ -81,7 +99,7 @@
81
99
  }
82
100
 
83
101
  .zx-calendar__toolbar-button:hover:not(:disabled),
84
- .zx-calendar__view-button:hover:not(:disabled) { background: var(--zx-color-bg-hover); }
102
+ .zx-calendar__view-button:hover:not(:disabled):not([aria-pressed="true"]) { background: var(--zx-color-bg-hover); }
85
103
  .zx-calendar__new:hover:not(:disabled) { background: var(--zx-color-accent-hover); }
86
104
  .zx-calendar button:disabled { cursor: default; opacity: .55; }
87
105
 
@@ -148,6 +166,7 @@
148
166
  min-block-size: calc(var(--zx-control-height) + var(--zx-space-2));
149
167
  place-content: center;
150
168
  border: 0;
169
+ border-radius: 0;
151
170
  border-inline-end: 1px solid var(--zx-color-border);
152
171
  background: var(--zx-color-bg-surface);
153
172
  color: var(--zx-color-text);
@@ -227,6 +246,7 @@
227
246
  .zx-calendar__slot {
228
247
  min-inline-size: 0;
229
248
  border: 0;
249
+ border-radius: 0;
230
250
  border-block-start: 1px solid var(--zx-color-border);
231
251
  border-inline-end: 1px solid var(--zx-color-border);
232
252
  background: var(--zx-color-bg-surface);
@@ -261,6 +281,7 @@
261
281
  inline-size: 100%;
262
282
  block-size: 100%;
263
283
  min-inline-size: 0;
284
+ min-block-size: 0;
264
285
  overflow: hidden;
265
286
  border: 1px solid color-mix(in srgb, var(--zx-calendar-event-color) 65%, var(--zx-color-border));
266
287
  border-inline-start: 3px solid var(--zx-calendar-event-color);
@@ -310,8 +331,11 @@
310
331
  z-index: 3;
311
332
  display: grid;
312
333
  block-size: 8px;
334
+ min-block-size: 0;
313
335
  place-items: center;
336
+ appearance: none;
314
337
  border: 0;
338
+ border-radius: 0;
315
339
  background: transparent;
316
340
  padding: 0;
317
341
  cursor: ns-resize;
@@ -329,6 +353,7 @@
329
353
  .zx-calendar__event[data-variant="span"] .zx-calendar__event-action,
330
354
  .zx-calendar__event[data-variant="month"] .zx-calendar__event-action {
331
355
  display: flex;
356
+ min-block-size: 0;
332
357
  align-items: center;
333
358
  gap: var(--zx-space-1);
334
359
  padding-block: 2px;
@@ -358,10 +383,17 @@
358
383
  }
359
384
 
360
385
  .zx-calendar__agenda { display: grid; }
361
- .zx-calendar__agenda-day { display: grid; grid-template-columns: minmax(10rem, 22%) minmax(0, 1fr); border-block-end: 1px solid var(--zx-color-border); }
386
+ .zx-calendar__agenda-day {
387
+ display: grid;
388
+ grid-template-columns: minmax(10rem, 22%) minmax(0, 1fr);
389
+ border-block-end: 1px solid var(--zx-color-border);
390
+ }
362
391
  .zx-calendar__agenda-heading {
363
- align-self: start;
392
+ align-self: stretch;
393
+ appearance: none;
364
394
  border: 0;
395
+ border-inline-end: 1px solid var(--zx-color-border);
396
+ border-radius: 0;
365
397
  background: var(--zx-color-bg-muted);
366
398
  color: var(--zx-color-text);
367
399
  padding: var(--zx-space-3);
@@ -371,9 +403,13 @@
371
403
  cursor: pointer;
372
404
  }
373
405
  .zx-calendar__agenda-heading[aria-current="date"] { color: var(--zx-color-accent); }
406
+ .zx-calendar__agenda-events {
407
+ display: grid;
408
+ min-inline-size: 0;
409
+ }
374
410
  .zx-calendar__agenda-empty { margin: 0; color: var(--zx-color-text-muted); padding: var(--zx-space-3); }
375
- .zx-calendar__agenda-day > .zx-calendar__event { border-block-end: 1px solid var(--zx-color-border); }
376
- .zx-calendar__agenda-day > .zx-calendar__event:last-child { border-block-end: 0; }
411
+ .zx-calendar__agenda-events > .zx-calendar__event { border-block-end: 1px solid var(--zx-color-border); }
412
+ .zx-calendar__agenda-events > .zx-calendar__event:last-child { border-block-end: 0; }
377
413
  .zx-calendar__event[data-variant="agenda"] .zx-calendar__event-action {
378
414
  grid-template-columns: minmax(6rem, 18%) minmax(0, 1fr);
379
415
  grid-template-areas: "time title" "time location";
@@ -418,26 +454,13 @@
418
454
  text-transform: uppercase;
419
455
  letter-spacing: .04em;
420
456
  }
421
- .zx-calendar__month-weekdays .zx-calendar__month-weekday { grid-row: 1; }
422
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(7) { grid-column: 2; }
423
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(6) { grid-column: 3; }
424
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(5) { grid-column: 4; }
425
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(4) { grid-column: 5; }
426
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(3) { grid-column: 6; }
427
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(2) { grid-column: 7; }
428
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(1) { grid-column: 8; }
429
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(7) { grid-column: 1; }
430
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(6) { grid-column: 2; }
431
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(5) { grid-column: 3; }
432
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(4) { grid-column: 4; }
433
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(3) { grid-column: 5; }
434
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(2) { grid-column: 6; }
435
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(1) { grid-column: 7; }
436
457
  .zx-calendar__month-week { min-block-size: 7.5rem; border-block-end: 1px solid var(--zx-color-border); }
437
458
  .zx-calendar__month-week-number {
438
- grid-row: 1 / span 2;
459
+ grid-row: 1;
460
+ appearance: none;
439
461
  border: 0;
440
462
  border-inline-end: 1px solid var(--zx-color-border);
463
+ border-radius: 0;
441
464
  background: var(--zx-color-bg-muted);
442
465
  color: var(--zx-color-text-muted);
443
466
  font: inherit;
@@ -452,11 +475,21 @@
452
475
  }
453
476
  .zx-calendar__month-week:not(:has(.zx-calendar__month-week-number)) .zx-calendar__month-days,
454
477
  .zx-calendar__month-week:not(:has(.zx-calendar__month-week-number)) .zx-calendar__month-events { grid-column: 1; }
455
- .zx-calendar__month-days { grid-row: 1; }
478
+ .zx-calendar__month-days {
479
+ grid-row: 1;
480
+ min-block-size: 7.5rem;
481
+ align-items: stretch;
482
+ }
456
483
  .zx-calendar__month-day {
484
+ display: flex;
485
+ block-size: 100%;
486
+ appearance: none;
487
+ align-items: flex-start;
488
+ justify-content: flex-end;
457
489
  min-block-size: var(--zx-control-height);
458
490
  border: 0;
459
491
  border-inline-end: 1px solid var(--zx-color-border);
492
+ border-radius: 0;
460
493
  background: var(--zx-color-bg-surface);
461
494
  color: var(--zx-color-text);
462
495
  padding: var(--zx-space-2);
@@ -464,12 +497,34 @@
464
497
  text-align: end;
465
498
  cursor: pointer;
466
499
  }
500
+ .zx-calendar__month-day:last-child { border-inline-end: 0; }
467
501
  .zx-calendar__month-day[data-other-month="true"] { color: var(--zx-color-text-muted); background: var(--zx-color-bg-muted); }
468
- .zx-calendar__month-day[aria-current="date"] { color: var(--zx-color-accent); font-weight: 750; box-shadow: inset 0 2px var(--zx-color-accent); }
502
+ .zx-calendar__month-day[aria-current="date"] {
503
+ color: var(--zx-color-accent);
504
+ font-weight: 750;
505
+ }
469
506
  .zx-calendar__month-day:hover:not(:disabled) { background: var(--zx-color-bg-hover); }
470
- .zx-calendar__month-events { grid-row: 2; grid-auto-rows: calc(var(--zx-control-height) - var(--zx-space-2)); align-content: start; gap: 2px 0; padding-block: var(--zx-space-1); }
507
+ .zx-calendar__month-events {
508
+ grid-row: 1;
509
+ z-index: 2;
510
+ grid-auto-rows: calc(var(--zx-control-height) - var(--zx-space-2));
511
+ align-content: start;
512
+ gap: 2px 0;
513
+ padding-block: calc(var(--zx-control-height) + var(--zx-space-1)) var(--zx-space-1);
514
+ pointer-events: none;
515
+ }
516
+ .zx-calendar__month-events > .zx-calendar__event,
517
+ .zx-calendar__month-events > .zx-calendar__more { pointer-events: auto; }
518
+ .zx-calendar__event[data-variant="month"] .zx-calendar__event-action {
519
+ border-color: color-mix(in srgb, var(--zx-calendar-event-color) 55%, var(--zx-color-border));
520
+ border-inline-start-width: 1px;
521
+ border-radius: 0;
522
+ background: color-mix(in srgb, var(--zx-calendar-event-color) 13%, var(--zx-color-bg-surface));
523
+ }
524
+ .zx-calendar__event[data-variant="month"][data-all-day="true"] .zx-calendar__event-time { display: none; }
471
525
  .zx-calendar__more {
472
526
  align-self: center;
527
+ min-block-size: 0;
473
528
  overflow: hidden;
474
529
  border: 0;
475
530
  background: transparent;
@@ -546,7 +601,14 @@
546
601
  .zx-calendar__view-button { flex: 1 0 auto; }
547
602
  .zx-calendar__new span { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
548
603
  .zx-calendar__agenda-day { grid-template-columns: 1fr; }
549
- .zx-calendar__agenda-heading { position: sticky; inset-block-start: 0; z-index: 3; border-block-end: 1px solid var(--zx-color-border); }
604
+ .zx-calendar__agenda-heading {
605
+ position: sticky;
606
+ inset-block-start: 0;
607
+ z-index: 3;
608
+ border-inline-end: 0;
609
+ border-block-end: 1px solid var(--zx-color-border);
610
+ }
611
+ .zx-calendar__agenda-events { grid-column: 1; }
550
612
  .zx-calendar__event[data-variant="agenda"] .zx-calendar__event-action { grid-template-columns: 5.5rem minmax(0, 1fr); }
551
613
  .zx-calendar__year { grid-template-columns: 1fr; padding: var(--zx-space-3); }
552
614
  }
@@ -3,8 +3,8 @@ import { h } from '../../core/dom.js';
3
3
  import { icon } from '../../core/icons.js';
4
4
  import {
5
5
  CALENDAR_VIEWS, addCalendarDays, calendarDayDifference, calendarDayKey,
6
- calendarEventIntersects, calendarEventSpansDays, calendarRange, cloneCalendarEvent,
7
- layoutCalendarSpans, layoutTimedCalendarEvents, normalizeCalendarEvents,
6
+ calendarEventIntersects, calendarEventSpansDays, calendarPageDate, calendarRange,
7
+ cloneCalendarEvent, layoutCalendarSpans, layoutTimedCalendarEvents, normalizeCalendarEvents,
8
8
  shiftCalendarEvent, startOfCalendarDay
9
9
  } from './calendar-model.js';
10
10
 
@@ -289,7 +289,9 @@ export class Calendar extends Component {
289
289
  ? changes.durationEditable : current.durationEditable,
290
290
  data: changes.data ?? current.data
291
291
  };
292
- const [next] = normalizeCalendarEvents([source]);
292
+ // `source` is assembled from canonical keys, so the configured readers must not be reapplied
293
+ // here — but `dateUnit` must, or a numeric change is read in the wrong unit.
294
+ const [next] = normalizeCalendarEvents([source], { dateUnit: this.options.dateUnit });
293
295
  next.data = source.data;
294
296
  this._events = this._events.map((event, eventIndex) => eventIndex === index ? next : event);
295
297
  this._mutationVersions.delete(String(id));
@@ -452,7 +454,11 @@ export class Calendar extends Component {
452
454
  if (!calendarEventIntersects(event, day, next)) return false;
453
455
  return event.start >= day || index === 0;
454
456
  }).sort(compareEvents);
455
- const row = h('section', { class: 'zx-calendar__agenda-day', role: 'row' },
457
+ const row = h('section', {
458
+ class: 'zx-calendar__agenda-day',
459
+ role: 'row',
460
+ dataset: { date: calendarDayKey(day) }
461
+ },
456
462
  h('button', {
457
463
  type: 'button',
458
464
  class: 'zx-calendar__agenda-heading',
@@ -461,11 +467,13 @@ export class Calendar extends Component {
461
467
  ariaCurrent: sameCalendarDay(day, this._now()) ? 'date' : null,
462
468
  disabled: this.options.disabled
463
469
  }, this._format(day, { weekday: 'long', day: 'numeric', month: 'long' })));
470
+ const entries = h('div', { class: 'zx-calendar__agenda-events', role: 'gridcell' });
464
471
  if (!events.length) {
465
- row.append(h('p', { class: 'zx-calendar__agenda-empty', role: 'gridcell' }, this.msg('noEvents')));
472
+ entries.append(h('p', { class: 'zx-calendar__agenda-empty' }, this.msg('noEvents')));
466
473
  } else {
467
- for (const event of events) row.append(this._eventElement(event, { variant: 'agenda', day }));
474
+ for (const event of events) entries.append(this._eventElement(event, { variant: 'agenda', day }));
468
475
  }
476
+ row.append(entries);
469
477
  grid.append(row);
470
478
  }
471
479
  return grid;
@@ -721,6 +729,7 @@ export class Calendar extends Component {
721
729
  dataset: {
722
730
  eventId: key,
723
731
  variant: context.variant,
732
+ allDay: event.allDay ? 'true' : null,
724
733
  draggable: this._canMove(event) ? 'true' : null
725
734
  },
726
735
  style: event.color ? { '--zx-calendar-event-color': event.color } : null
@@ -989,7 +998,9 @@ export class Calendar extends Component {
989
998
  const item = this._eventById(control.dataset.eventId);
990
999
  const action = control.dataset.eventAction === 'resize' ? 'resize' : 'move';
991
1000
  if (!item || (action === 'move' ? !this._canMove(item) : !this._canResize(item))) return;
992
- control.setPointerCapture?.(event.pointerId);
1001
+ event.preventDefault();
1002
+ this._capturePointer(event.pointerId);
1003
+ const offset = action === 'move' ? this._dragOffsetForEvent(item, event.clientX, event.clientY) : null;
993
1004
  this._pointer = {
994
1005
  type: 'event',
995
1006
  pointerId: event.pointerId,
@@ -999,6 +1010,8 @@ export class Calendar extends Component {
999
1010
  event: cloneCalendarEvent(item),
1000
1011
  action,
1001
1012
  control,
1013
+ timeOffsetMinutes: offset?.minutes ?? 0,
1014
+ dayOffset: offset?.days ?? 0,
1002
1015
  moved: false
1003
1016
  };
1004
1017
  return;
@@ -1007,7 +1020,8 @@ export class Calendar extends Component {
1007
1020
  if (!slot || !this.options.selectable) return;
1008
1021
  const date = this._dateFromTarget(slot);
1009
1022
  if (!date) return;
1010
- slot.setPointerCapture?.(event.pointerId);
1023
+ event.preventDefault();
1024
+ this._capturePointer(event.pointerId);
1011
1025
  this._pointer = {
1012
1026
  type: 'selection',
1013
1027
  pointerId: event.pointerId,
@@ -1041,7 +1055,13 @@ export class Calendar extends Component {
1041
1055
  const wrapper = pointer.control.closest('.zx-calendar__event');
1042
1056
  if (wrapper) {
1043
1057
  wrapper.dataset.state = 'dragging';
1044
- wrapper.style.transform = `translate(${event.clientX - pointer.startX}px, ${event.clientY - pointer.startY}px)`;
1058
+ if (pointer.action === 'resize') {
1059
+ wrapper.style.transform = '';
1060
+ const height = this._timedPreviewHeight(proposed);
1061
+ if (height) wrapper.style.blockSize = height;
1062
+ } else {
1063
+ wrapper.style.transform = `translate(${event.clientX - pointer.startX}px, ${event.clientY - pointer.startY}px)`;
1064
+ }
1045
1065
  wrapper.dataset.preview = this._eventTime(proposed, proposed.start);
1046
1066
  }
1047
1067
  this._announce(this.msg('editPreview', proposed.title,
@@ -1053,6 +1073,7 @@ export class Calendar extends Component {
1053
1073
  const pointer = this._pointer;
1054
1074
  if (!pointer || pointer.pointerId !== event.pointerId) return;
1055
1075
  this._pointer = null;
1076
+ this._releasePointerCapture(pointer.pointerId);
1056
1077
  this._clearSelectionPreview();
1057
1078
  if (!pointer.moved) return;
1058
1079
  event.preventDefault();
@@ -1071,6 +1092,7 @@ export class Calendar extends Component {
1071
1092
  _cancelPointer(event) {
1072
1093
  if (!this._pointer || this._pointer.pointerId !== event.pointerId) return;
1073
1094
  const control = this._pointer.control;
1095
+ this._releasePointerCapture(this._pointer.pointerId);
1074
1096
  this._pointer = null;
1075
1097
  this._clearSelectionPreview();
1076
1098
  control.closest?.('.zx-calendar__event')?.removeAttribute('data-state');
@@ -1079,13 +1101,14 @@ export class Calendar extends Component {
1079
1101
 
1080
1102
  /** @param {CalendarEvent} event @param {'move'|'resize'} action @param {number} x @param {number} y @returns {CalendarEvent|null} */
1081
1103
  _proposalAtPoint(event, action, x, y) {
1082
- const timed = this._timeAtPoint(x, y);
1104
+ const resizeTimed = action === 'resize' && !calendarEventSpansDays(event);
1105
+ const timed = this._timeAtPoint(x, y, resizeTimed ? event.start : null);
1083
1106
  if (timed && !calendarEventSpansDays(event)) {
1084
1107
  if (action === 'move') {
1085
1108
  const duration = event.end.getTime() - event.start.getTime();
1086
1109
  const next = cloneCalendarEvent(event);
1087
- next.start = timed;
1088
- next.end = new Date(timed.getTime() + duration);
1110
+ next.start = addMinutes(timed, -Number(this._pointer?.timeOffsetMinutes ?? 0));
1111
+ next.end = new Date(next.start.getTime() + duration);
1089
1112
  return next;
1090
1113
  }
1091
1114
  const next = cloneCalendarEvent(event);
@@ -1094,7 +1117,8 @@ export class Calendar extends Component {
1094
1117
  }
1095
1118
  const day = this._dayAtPoint(x, y);
1096
1119
  if (!day) return null;
1097
- const difference = calendarDayDifference(startOfCalendarDay(event.start), day);
1120
+ const targetDay = addCalendarDays(day, -Number(this._pointer?.dayOffset ?? 0));
1121
+ const difference = calendarDayDifference(startOfCalendarDay(event.start), targetDay);
1098
1122
  if (action === 'move') return shiftCalendarEvent(event, { days: difference, minutes: 0 });
1099
1123
  const next = cloneCalendarEvent(event);
1100
1124
  const endDay = addCalendarDays(day, 1);
@@ -1102,15 +1126,23 @@ export class Calendar extends Component {
1102
1126
  return next.end > next.start ? next : null;
1103
1127
  }
1104
1128
 
1105
- /** @param {number} x @param {number} y @returns {Date|null} */
1106
- _timeAtPoint(x, y) {
1129
+ /** @param {number} x @param {number} y @param {Date|null} [fixedDay=null] @returns {Date|null} */
1130
+ _timeAtPoint(x, y, fixedDay = null) {
1107
1131
  const slots = this.refs.body.querySelector('.zx-calendar__slots');
1108
1132
  if (!slots) return null;
1109
1133
  const rect = slots.getBoundingClientRect();
1110
- if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom || rect.width <= 0 || rect.height <= 0) return null;
1134
+ const scroller = slots.closest('.zx-calendar__time-scroller');
1135
+ const viewport = scroller?.getBoundingClientRect();
1136
+ const left = viewport ? Math.max(rect.left, viewport.left) : rect.left;
1137
+ const right = viewport ? Math.min(rect.right, viewport.right) : rect.right;
1138
+ const top = viewport ? Math.max(rect.top, viewport.top) : rect.top;
1139
+ const bottom = viewport ? Math.min(rect.bottom, viewport.bottom) : rect.bottom;
1140
+ if (x < left || x > right || y < top || y > bottom || rect.width <= 0 || rect.height <= 0) return null;
1111
1141
  const columns = Number(slots.closest('[role="grid"]')?.getAttribute('aria-colcount')) || 1;
1112
1142
  const rows = Number(slots.closest('[role="grid"]')?.getAttribute('aria-rowcount')) || 1;
1113
- const column = Math.max(0, Math.min(columns - 1, Math.floor((x - rect.left) / rect.width * columns)));
1143
+ const fixedColumn = fixedDay ? this._timeGridDayIndex(fixedDay) : -1;
1144
+ const column = fixedColumn >= 0 ? fixedColumn
1145
+ : Math.max(0, Math.min(columns - 1, Math.floor((x - rect.left) / rect.width * columns)));
1114
1146
  const row = Math.max(0, Math.min(rows - 1, Math.floor((y - rect.top) / rect.height * rows)));
1115
1147
  const target = slots.querySelector(`[data-row-index="${row}"][data-day-index="${column}"]`);
1116
1148
  return target ? this._dateFromTarget(target) : null;
@@ -1118,11 +1150,6 @@ export class Calendar extends Component {
1118
1150
 
1119
1151
  /** @param {number} x @param {number} y @returns {Date|null} */
1120
1152
  _dayAtPoint(x, y) {
1121
- const elements = typeof document.elementsFromPoint === 'function' ? document.elementsFromPoint(x, y) : [];
1122
- for (const element of elements) {
1123
- const date = element instanceof HTMLElement ? element.dataset.date : null;
1124
- if (date) return parseDayKey(date);
1125
- }
1126
1153
  const allDay = this.refs.body.querySelector('.zx-calendar__all-day-grid');
1127
1154
  if (allDay) {
1128
1155
  const rect = allDay.getBoundingClientRect();
@@ -1132,9 +1159,79 @@ export class Calendar extends Component {
1132
1159
  return parseDayKey(cells[index]?.dataset.date);
1133
1160
  }
1134
1161
  }
1162
+ for (const monthEvents of this.refs.body.querySelectorAll('.zx-calendar__month-events')) {
1163
+ const rect = monthEvents.getBoundingClientRect();
1164
+ if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom || rect.width <= 0) continue;
1165
+ const start = parseDayKey(monthEvents.closest('.zx-calendar__month-week')?.dataset.weekStart);
1166
+ if (!start) continue;
1167
+ const index = Math.max(0, Math.min(6, Math.floor((x - rect.left) / rect.width * 7)));
1168
+ return addCalendarDays(start, index);
1169
+ }
1170
+ for (const agendaDay of this.refs.body.querySelectorAll('.zx-calendar__agenda-day')) {
1171
+ const rect = agendaDay.getBoundingClientRect();
1172
+ if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) continue;
1173
+ return parseDayKey(agendaDay.dataset.date);
1174
+ }
1175
+ const elements = typeof document.elementsFromPoint === 'function' ? document.elementsFromPoint(x, y) : [];
1176
+ for (const element of elements) {
1177
+ const dateTarget = element instanceof HTMLElement ? element.closest('[data-date]') : null;
1178
+ const date = dateTarget instanceof HTMLElement ? dateTarget.dataset.date : null;
1179
+ if (date) return parseDayKey(date);
1180
+ }
1135
1181
  return null;
1136
1182
  }
1137
1183
 
1184
+ /** @param {CalendarEvent} event @param {number} x @param {number} y @returns {{minutes: number, days: number}} */
1185
+ _dragOffsetForEvent(event, x, y) {
1186
+ if (!calendarEventSpansDays(event)) {
1187
+ const timed = this._timeAtPoint(x, y);
1188
+ if (timed) {
1189
+ const slot = normalizeSlot(this.options.slotDuration);
1190
+ const duration = Math.max(0, Math.round((event.end.getTime() - event.start.getTime()) / 60_000));
1191
+ const maxOffset = Math.max(0, duration - slot);
1192
+ const minutes = Math.round((timed.getTime() - event.start.getTime()) / 60_000);
1193
+ return { minutes: Math.max(0, Math.min(maxOffset, minutes)), days: 0 };
1194
+ }
1195
+ }
1196
+ const day = this._dayAtPoint(x, y);
1197
+ const days = day ? calendarDayDifference(startOfCalendarDay(event.start), day) : 0;
1198
+ return { minutes: 0, days: Math.max(0, days) };
1199
+ }
1200
+
1201
+ /** @param {Date} day @returns {number} */
1202
+ _timeGridDayIndex(day) {
1203
+ const key = calendarDayKey(day);
1204
+ const headings = [...this.refs.body.querySelectorAll('.zx-calendar__day-heading')];
1205
+ return headings.findIndex((heading) => heading.dataset.date === key);
1206
+ }
1207
+
1208
+ /** @param {CalendarEvent} event @returns {string|null} */
1209
+ _timedPreviewHeight(event) {
1210
+ const grid = this.refs.body.querySelector('.zx-calendar__time-grid');
1211
+ if (!grid || calendarEventSpansDays(event)) return null;
1212
+ const min = normalizeMinute(this.options.slotMinTime, 0);
1213
+ const max = normalizeMinute(this.options.slotMaxTime, 1440, true);
1214
+ const slot = normalizeSlot(this.options.slotDuration);
1215
+ const start = event.start.getHours() * 60 + event.start.getMinutes();
1216
+ const end = event.end.getHours() * 60 + event.end.getMinutes();
1217
+ const minutes = Math.max(slot, Math.min(max, end) - Math.max(min, start));
1218
+ return `${(minutes / (max - min)) * 100}%`;
1219
+ }
1220
+
1221
+ /** @param {number} pointerId @returns {void} */
1222
+ _capturePointer(pointerId) {
1223
+ try {
1224
+ this.el.setPointerCapture?.(pointerId);
1225
+ } catch {
1226
+ // Synthetic PointerEvents used by smoke tests do not create an active pointer capture target.
1227
+ }
1228
+ }
1229
+
1230
+ /** @param {number} pointerId @returns {void} */
1231
+ _releasePointerCapture(pointerId) {
1232
+ if (this.el.hasPointerCapture?.(pointerId)) this.el.releasePointerCapture?.(pointerId);
1233
+ }
1234
+
1138
1235
  /** @param {Date} start @param {Date} end @returns {void} */
1139
1236
  _previewSelection(start, end) {
1140
1237
  const low = Math.min(start.getTime(), end.getTime());
@@ -1240,13 +1337,9 @@ export class Calendar extends Component {
1240
1337
 
1241
1338
  /** @param {number} direction @returns {this} */
1242
1339
  _page(direction) {
1243
- const date = new Date(this._date.getTime());
1244
- if (this._view === 'day') date.setDate(date.getDate() + direction);
1245
- else if (this._view === 'agenda') date.setDate(date.getDate() + direction * this.options.agendaDays);
1246
- else if (this._view === 'week') date.setDate(date.getDate() + direction * 7);
1247
- else if (this._view === 'month') date.setMonth(date.getMonth() + direction);
1248
- else date.setFullYear(date.getFullYear() + direction);
1249
- return this.setDate(date);
1340
+ return this.setDate(calendarPageDate(this._view, this._date, direction, {
1341
+ agendaDays: this.options.agendaDays
1342
+ }));
1250
1343
  }
1251
1344
 
1252
1345
  /** @param {{start: Date, end: Date}} range @returns {string} */