@tasknotes/calendar 0.1.5 → 0.1.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.1.7
6
+
7
+ ### Added
8
+
9
+ - Hosts can read collision column counts and the unobscured timed content width, choose views responsively, fill an empty list through a content hook, and translate the stack label with the `appointments` text.
10
+
11
+ ### Fixed
12
+
13
+ - Collision stacks show appointment counts and open only the matching collision group, without unrelated all-day or timed items. Narrow time grids reveal the selected day on entry and view changes, not only after Today.
14
+ - Crowded time-grid days use readable collision stacks without widening the entire week; stack contents retain drag-and-drop, including after moving the calendar into another window.
15
+ - Today reveals the current day horizontally, with persistent day navigation when the grid is wider than its pane.
16
+ - Overflow dialogs clamp to the visible viewport, reposition on resize and scroll, keep Close reachable, and provide touch-sized rows and keyboard navigation.
17
+ - Collision stacks honour `eventMaxStack`, and overflow triggers stay stable across rerenders, so Escape returns focus to them.
18
+
19
+ ## 0.1.6
20
+
21
+ ### Changed
22
+
23
+ - Time grids give tracked (`lane: "actual"`) items their narrow strip only where they overlap planned items. Elsewhere, planned and tracked items use the full column width, as before the side lane existed. The strip's width is the `--tnc-actual-w` custom property (default 22%).
24
+ - Reported by @RumiaKitinari in [TaskNotes issue 2494](https://github.com/callumalpass/tasknotes/issues/2494) and @mgreen3 in [TaskNotes issue 2498](https://github.com/callumalpass/tasknotes/issues/2498).
25
+ - The toolbar's previous and next buttons draw their chevrons as icons, which are larger and clearer than the ‹ › glyphs.
26
+
3
27
  ## 0.1.5
4
28
 
5
29
  ### Added
package/README.md CHANGED
@@ -69,8 +69,15 @@ to 00:00–24:00 in 30-minute slots, initially scrolled to 08:00.
69
69
  `text` supplies translated labels; `itemContent` and related hooks supply
70
70
  custom DOM. The exported `CalendarOptions<D>` type documents the full API.
71
71
 
72
+ `toolbar.viewDisplay` defaults to `"auto"`: full view-name buttons become a
73
+ native select when the toolbar cannot fit them. Use `"buttons"` or `"select"`
74
+ to choose a fixed presentation. The select uses the same translated view names
75
+ and changes views through the same navigation lifecycle as the buttons.
76
+
72
77
  ### Month and list
73
78
 
79
+ - `listEmptyContent()` may return a host-owned DOM node for an empty list, such as an explanation and recovery action. The default translated empty message remains when omitted. The node is removed when the list gains items.
80
+
74
81
  - `monthDisplay: "dots"` draws every item in a month cell as a small dot (`--tnc-dot-size`, `--tnc-dot-gap`) that wraps across the cell, with "+N" when they do not fit; `dayMaxEvents` caps the dots (`false` lets cells grow). The day cell is the tap target: dots ignore the pointer, are `aria-hidden`, get no content hook, and the cell is labelled "<date>, N items" (`text.itemCount`). Items in dots have `variant: "indicator"`; colour them with `color` or `itemDidMount` setting `--tnc-c`.
75
82
  - **"+N more" height**: day rows (and the time grid's all-day row) make room for the link at its rendered height, so a host may give it a 44px touch target (`--tnc-more-h`, or any min-height) without overflowing the row.
76
83
  - `listStart: "date" | "week"`: the list view starts at the date (default; FullCalendar `duration: { days: n }` lists, as the TaskNotes plugin and app used) or at the start of its week (FullCalendar's stock `listWeek`); week-aligned lists step by whole weeks.
@@ -105,6 +112,30 @@ custom DOM. The exported `CalendarOptions<D>` type documents the full API.
105
112
  Set `--tnc-theme-*` variables on the host (they inherit into the calendar)
106
113
  or on `.tnc`. The stylesheet is scoped to `.tnc` and does not inject CSS.
107
114
 
115
+ Time grids collapse crowded collision groups into labelled, expandable stacks
116
+ instead of widening every day. `--tnc-event-min-width` sets the readable event
117
+ width in pixels (default 128). Keep the day minimum independent of collisions:
118
+
119
+ ```css
120
+ .my-calendar .tnc-tg {
121
+ --tnc-col-min: 128px;
122
+ --tnc-event-min-width: 128px;
123
+ }
124
+ ```
125
+
126
+ When the day minimums exceed a narrow viewport, persistent day buttons reveal
127
+ offscreen dates, and Today reveals the current day. Headers, all-day rows and
128
+ timed columns stay aligned. `--tnc-collision-columns` remains available as
129
+ diagnostic metadata (respecting `eventMaxStack`), not a week-wide width multiplier.
130
+ Overflow dialogs clamp to the visible calendar and viewport, scroll vertically,
131
+ and support Tab, arrow keys, Home, End and Escape. Timed items dragged from
132
+ overflow start at their actual time rather than the slot underneath the dialog.
133
+
134
+ Timed events also expose `--tnc-event-readable-width`, the percentage of their
135
+ width not covered by the next collision column. Hosts can use it as the
136
+ `max-width` of `.tnc-item-main` to keep text readable while preserving the user's
137
+ overlapping event backgrounds. It is `100%` for non-overlapping placements.
138
+
108
139
  ```css
109
140
  .my-calendar {
110
141
  --tnc-theme-bg: #181818;
package/dist/calendar.css CHANGED
@@ -23,6 +23,8 @@
23
23
  --tnc-off: color-mix(in srgb, var(--tnc-text) 3.5%, transparent);
24
24
  --tnc-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
25
25
  --tnc-radius: 6px;
26
+ /* Time-grid share for tracked ("actual") items where they overlap planned ones. */
27
+ --tnc-actual-w: 22%;
26
28
  --tnc-font-size: 12px;
27
29
  --tnc-small: 11px;
28
30
  --tnc-bar-h: 20px;
@@ -131,9 +133,21 @@
131
133
  }
132
134
  .tnc-btn-prev,
133
135
  .tnc-btn-next {
136
+ display: inline-flex;
137
+ align-items: center;
138
+ justify-content: center;
134
139
  min-width: 28px;
135
140
  padding: 3px 6px;
136
- font-size: 15px;
141
+ }
142
+ .tnc-btn-prev svg,
143
+ .tnc-btn-next svg {
144
+ width: 16px;
145
+ height: 16px;
146
+ fill: none;
147
+ stroke: currentColor;
148
+ stroke-width: 2;
149
+ stroke-linecap: round;
150
+ stroke-linejoin: round;
137
151
  }
138
152
  .tnc-btn:focus-visible,
139
153
  .tnc-navlink:focus-visible,
@@ -453,6 +467,35 @@
453
467
  overflow: auto;
454
468
  overscroll-behavior: contain;
455
469
  }
470
+ .tnc-tg-day-navigation {
471
+ display: flex;
472
+ flex-wrap: wrap;
473
+ gap: 2px;
474
+ flex: none;
475
+ }
476
+ .tnc-tg-day-navigation[hidden] {
477
+ display: none;
478
+ }
479
+ .tnc-tg-day-navigation .is-offscreen {
480
+ text-decoration: underline;
481
+ text-underline-offset: 3px;
482
+ }
483
+ .tnc-more--collision {
484
+ display: flex;
485
+ align-items: start;
486
+ gap: 4px;
487
+ min-height: 40px;
488
+ white-space: normal;
489
+ text-align: start;
490
+ }
491
+ .tnc-collision-title {
492
+ flex: 1;
493
+ min-width: 0;
494
+ display: -webkit-box;
495
+ -webkit-box-orient: vertical;
496
+ -webkit-line-clamp: 3;
497
+ overflow: hidden;
498
+ }
456
499
  .tnc--height-auto .tnc-tg-scroll {
457
500
  overflow: visible;
458
501
  }
@@ -612,31 +655,19 @@
612
655
  .tnc-col.is-today {
613
656
  background-color: color-mix(in srgb, var(--tnc-accent) 5%, transparent);
614
657
  }
658
+ /* Items position themselves within the column; see inLane in timegrid.ts. */
615
659
  .tnc-col-plan,
616
660
  .tnc-col-bg,
617
661
  .tnc-col-actual {
618
662
  position: absolute;
619
- top: 0;
620
- bottom: 0;
621
- }
622
- .tnc-col-bg {
623
- left: 0;
624
- right: 0;
625
- }
626
- .tnc-col-plan {
627
- left: 1px;
628
- right: 8px;
663
+ inset: 0;
629
664
  }
665
+ /* Above the plan layer, so it must not swallow clicks meant for plan items. */
630
666
  .tnc-col-actual {
631
- display: none;
632
- right: 2px;
633
- width: 22%;
634
- }
635
- .tnc-col.has-actual .tnc-col-plan {
636
- right: calc(22% + 4px);
667
+ pointer-events: none;
637
668
  }
638
- .tnc-col.has-actual .tnc-col-actual {
639
- display: block;
669
+ .tnc-col-actual > * {
670
+ pointer-events: auto;
640
671
  }
641
672
  .tnc-now {
642
673
  position: absolute;
@@ -1618,6 +1649,7 @@
1618
1649
  outline: none;
1619
1650
  }
1620
1651
  .tnc-popover-head {
1652
+ flex: none;
1621
1653
  display: flex;
1622
1654
  align-items: center;
1623
1655
  justify-content: space-between;
@@ -1627,6 +1659,7 @@
1627
1659
  font-weight: 650;
1628
1660
  }
1629
1661
  .tnc-popover-close {
1662
+ flex: none;
1630
1663
  min-width: 28px;
1631
1664
  min-height: 28px;
1632
1665
  border: 0;
@@ -1640,6 +1673,7 @@
1640
1673
  background: var(--tnc-surface-3);
1641
1674
  }
1642
1675
  .tnc-popover-body {
1676
+ min-height: 0;
1643
1677
  display: flex;
1644
1678
  flex-direction: column;
1645
1679
  gap: 2px;
@@ -1650,6 +1684,27 @@
1650
1684
  position: relative;
1651
1685
  flex: none;
1652
1686
  }
1687
+ .tnc-popover-title {
1688
+ min-width: 0;
1689
+ overflow: hidden;
1690
+ text-overflow: ellipsis;
1691
+ white-space: nowrap;
1692
+ }
1693
+ .tnc-popover-body .tnc-item {
1694
+ height: auto;
1695
+ min-height: 40px;
1696
+ }
1697
+ .tnc-popover-body .tnc-item-main {
1698
+ min-height: 40px;
1699
+ }
1700
+ .tnc-popover-body .tnc-item-title {
1701
+ white-space: normal;
1702
+ overflow-wrap: anywhere;
1703
+ }
1704
+ .tnc-popover-close:focus-visible {
1705
+ outline: 2px solid var(--tnc-accent);
1706
+ outline-offset: -2px;
1707
+ }
1653
1708
 
1654
1709
  @media (prefers-reduced-motion: reduce) {
1655
1710
  .tnc * {
package/dist/cjs/core.cjs CHANGED
@@ -520,7 +520,7 @@ function packTimed(segs, minMinutes, order = defaultOrder) {
520
520
  colEnds.push(0);
521
521
  }
522
522
  colEnds[col] = s.end;
523
- const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex };
523
+ const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex, split: false };
524
524
  cluster.push(p);
525
525
  out.push(p);
526
526
  clusterEnd = Math.max(clusterEnd, s.end);
@@ -540,7 +540,24 @@ function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTE
540
540
  else if (n.item.lane === "actual") actual.push(seg);
541
541
  else plan.push(seg);
542
542
  }
543
- return { day, plan: packTimed(plan, minMinutes, order), actual: packTimed(actual, minMinutes, order), background };
543
+ const planPlaced = packTimed(plan, minMinutes, order);
544
+ const actualPlaced = packTimed(actual, minMinutes, order);
545
+ markSplit(planPlaced, actualPlaced, minMinutes);
546
+ markSplit(actualPlaced, planPlaced, minMinutes);
547
+ return { day, plan: planPlaced, actual: actualPlaced, background };
548
+ }
549
+ function markSplit(lane, other, minMinutes) {
550
+ if (!lane.length || !other.length) return;
551
+ const end = (p) => Math.max(p.endMin, p.startMin + minMinutes);
552
+ const ranges = /* @__PURE__ */ new Map();
553
+ for (const p of lane) {
554
+ const r = ranges.get(p.cluster);
555
+ if (r) r[1] = Math.max(r[1], end(p));
556
+ else ranges.set(p.cluster, [p.startMin, end(p)]);
557
+ }
558
+ const split = /* @__PURE__ */ new Set();
559
+ for (const [cluster, [s, e]] of ranges) if (other.some((o) => o.startMin < e && s < end(o))) split.add(cluster);
560
+ for (const p of lane) p.split = split.has(p.cluster);
544
561
  }
545
562
  function layoutRow(items, days, order = defaultOrder) {
546
563
  const first = days[0];