panelui-native 0.80.0 → 0.81.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.
Files changed (30) hide show
  1. package/lib/module/components/planner/index.js +446 -32
  2. package/lib/module/components/planner/index.js.map +1 -1
  3. package/lib/module/components/planner/planner-entries.js +36 -0
  4. package/lib/module/components/planner/planner-entries.js.map +1 -1
  5. package/lib/module/components/planner/planner-grid-navigation.js +34 -2
  6. package/lib/module/components/planner/planner-grid-navigation.js.map +1 -1
  7. package/lib/module/components/planner/planner-weeks.js +95 -0
  8. package/lib/module/components/planner/planner-weeks.js.map +1 -0
  9. package/lib/module/index.js.map +1 -1
  10. package/lib/module/utils/date.js +10 -3
  11. package/lib/module/utils/date.js.map +1 -1
  12. package/lib/typescript/src/components/planner/index.d.ts +65 -4
  13. package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
  14. package/lib/typescript/src/components/planner/planner-entries.d.ts +18 -0
  15. package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -1
  16. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts +7 -1
  17. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts.map +1 -1
  18. package/lib/typescript/src/components/planner/planner-weeks.d.ts +50 -0
  19. package/lib/typescript/src/components/planner/planner-weeks.d.ts.map +1 -0
  20. package/lib/typescript/src/index.d.ts +1 -1
  21. package/lib/typescript/src/index.d.ts.map +1 -1
  22. package/lib/typescript/src/utils/date.d.ts +1 -1
  23. package/lib/typescript/src/utils/date.d.ts.map +1 -1
  24. package/package.json +1 -1
  25. package/src/components/planner/index.tsx +516 -38
  26. package/src/components/planner/planner-entries.ts +33 -0
  27. package/src/components/planner/planner-grid-navigation.ts +37 -2
  28. package/src/components/planner/planner-weeks.ts +90 -0
  29. package/src/index.ts +2 -0
  30. package/src/utils/date.ts +13 -3
@@ -153,3 +153,36 @@ export function summariseMonth<T extends PlannerDatedEntry>(
153
153
  })),
154
154
  };
155
155
  }
156
+
157
+ /**
158
+ * Minutes since midnight, or `null` for an entry that lands exactly on it.
159
+ *
160
+ * The grid has only ever read an entry's day, so a date with no time set is
161
+ * midnight — and that is indistinguishable from something genuinely scheduled
162
+ * at 00:00. Reading it as all-day is the right way round: a set of entries
163
+ * built without times renders as a list of things happening that day, which is
164
+ * what it is, rather than as a column of midnights.
165
+ */
166
+ export function entryTimeMinutes(date: Date): number | null {
167
+ const minutes = date.getHours() * 60 + date.getMinutes();
168
+ return minutes === 0 ? null : minutes;
169
+ }
170
+
171
+ /**
172
+ * All-day first, then by time, keeping the caller's order within each.
173
+ *
174
+ * All-day leads because it frames the rest of the day rather than falling at a
175
+ * point in it, and a list that opens with "All day" reads as a heading for
176
+ * what follows.
177
+ */
178
+ export function sortByTime<T extends PlannerDatedEntry>(entries: readonly T[]): T[] {
179
+ return entries
180
+ .map((entry, index) => ({ entry, index, at: entryTimeMinutes(entry.date) }))
181
+ .sort((a, b) => {
182
+ if (a.at === null && b.at === null) return a.index - b.index;
183
+ if (a.at === null) return -1;
184
+ if (b.at === null) return 1;
185
+ return a.at === b.at ? a.index - b.index : a.at - b.at;
186
+ })
187
+ .map(({ entry }) => entry);
188
+ }
@@ -6,40 +6,75 @@ export type PlannerGridNavigationKey =
6
6
  | 'Home'
7
7
  | 'End';
8
8
 
9
+ /**
10
+ * Whether a cell can take focus. Some looks leave the days either side of the
11
+ * month blank, and a blank has nothing to focus — so the caller says which
12
+ * indices are real and movement steps over the rest.
13
+ */
14
+ export type PlannerGridNavigable = (index: number) => boolean;
15
+
9
16
  /** Returns the next visible cell for a bounded, row-major month grid. */
10
17
  export function plannerGridTarget(
11
18
  key: string,
12
19
  index: number,
13
20
  count: number,
14
- columns = 7
21
+ columns = 7,
22
+ navigable?: PlannerGridNavigable
15
23
  ): number | null {
16
24
  if (!Number.isInteger(index) || index < 0 || index >= count || columns < 1) {
17
25
  return null;
18
26
  }
19
27
 
20
28
  let target: number;
29
+ /*
30
+ * Which way to keep going when the first landing is blank. The arrows carry
31
+ * on away from where you were; Home and End start at the edge of the row and
32
+ * come back inward, because the cell they want is the outermost real one and
33
+ * anything past it is off the row entirely.
34
+ */
35
+ let step: number;
21
36
  switch (key as PlannerGridNavigationKey) {
22
37
  case 'ArrowLeft':
23
38
  target = index - 1;
39
+ step = -1;
24
40
  break;
25
41
  case 'ArrowRight':
26
42
  target = index + 1;
43
+ step = 1;
27
44
  break;
28
45
  case 'ArrowUp':
29
46
  target = index - columns;
47
+ step = -columns;
30
48
  break;
31
49
  case 'ArrowDown':
32
50
  target = index + columns;
51
+ step = columns;
33
52
  break;
34
53
  case 'Home':
35
54
  target = index - (index % columns);
55
+ step = 1;
36
56
  break;
37
57
  case 'End':
38
58
  target = index + (columns - 1 - (index % columns));
59
+ step = -1;
39
60
  break;
40
61
  default:
41
62
  return null;
42
63
  }
43
64
 
44
- return target >= 0 && target < count ? target : index;
65
+ if (target < 0 || target >= count) return index;
66
+ if (!navigable) return target;
67
+
68
+ /*
69
+ * Stepping stops at the cell you started from rather than running past it.
70
+ * That covers both directions at once: an arrow that finds nothing before
71
+ * the grid ends stays put, and Home or End on a row whose edge is blank
72
+ * walks in until it reaches a real day or arrives back where it began.
73
+ */
74
+ while (target !== index && !navigable(target)) {
75
+ target += step;
76
+ if (target < 0 || target >= count) return index;
77
+ }
78
+
79
+ return target;
45
80
  }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The arithmetic behind a calendar that scrolls instead of paging: which weeks
3
+ * are reachable, where a week starts, and which month a week belongs to.
4
+ *
5
+ * Nothing here imports anything, for the same reason as `planner-entries`: the
6
+ * tests run this file through `node --test` with type stripping rather than a
7
+ * bundler, so an extensionless import of a sibling does not resolve.
8
+ *
9
+ * A week is seven days whatever calendar is in force, so all of this is plain
10
+ * day arithmetic. Naming the month a week falls in is not, and is left to the
11
+ * caller, which has the calendar system.
12
+ */
13
+
14
+ const DAY_MS = 24 * 60 * 60 * 1000;
15
+
16
+ /** Local midnight, so two dates on the same day compare equal. */
17
+ function atMidnight(date: Date): Date {
18
+ const copy = new Date(date);
19
+ copy.setHours(0, 0, 0, 0);
20
+ return copy;
21
+ }
22
+
23
+ /**
24
+ * Days added by the calendar rather than by milliseconds.
25
+ *
26
+ * A day is not always 24 hours — the ones a daylight-saving change falls on are
27
+ * 23 or 25 — so stepping by `DAY_MS` drifts across a transition and lands a
28
+ * week row an hour before or after midnight, which then reads as the wrong day.
29
+ */
30
+ export function addDaysLocal(date: Date, days: number): Date {
31
+ const copy = new Date(date);
32
+ copy.setDate(copy.getDate() + days);
33
+ return atMidnight(copy);
34
+ }
35
+
36
+ /** The first day of the week `date` falls in, for a week starting on `weekStartsOn`. */
37
+ export function startOfWeek(date: Date, weekStartsOn: number): Date {
38
+ const start = ((weekStartsOn % 7) + 7) % 7;
39
+ const midnight = atMidnight(date);
40
+ const shift = (midnight.getDay() - start + 7) % 7;
41
+ return addDaysLocal(midnight, -shift);
42
+ }
43
+
44
+ /**
45
+ * The day a week is named after: its midpoint.
46
+ *
47
+ * A week straddling a month boundary belongs to whichever month holds most of
48
+ * it, and the fourth day is the cheapest way to ask that — it is on the heavier
49
+ * side by definition. Naming the week after its first day would put the last
50
+ * week of January under December for as little as one day's overlap.
51
+ */
52
+ export function weekAnchor(weekStart: Date): Date {
53
+ return addDaysLocal(weekStart, 3);
54
+ }
55
+
56
+ /** The seven days of a week, in order. */
57
+ export function weekDays(weekStart: Date): Date[] {
58
+ return Array.from({ length: 7 }, (_unused, day) => addDaysLocal(weekStart, day));
59
+ }
60
+
61
+ /**
62
+ * Every week start from `weeksBefore` before the anchor's week to `weeksAfter`
63
+ * after it, in order.
64
+ *
65
+ * A bounded range rather than an endless one: a scroller needs to know how tall
66
+ * it is to place a scrollbar and to jump to a month without rendering its way
67
+ * there, and neither is possible over an infinite list.
68
+ */
69
+ export function weekRange(anchor: Date, weekStartsOn: number, weeksBefore: number, weeksAfter: number): Date[] {
70
+ const first = startOfWeek(anchor, weekStartsOn);
71
+ const before = Math.max(0, Math.trunc(weeksBefore));
72
+ const after = Math.max(0, Math.trunc(weeksAfter));
73
+ return Array.from({ length: before + after + 1 }, (_unused, step) =>
74
+ addDaysLocal(first, (step - before) * 7)
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Where `date`'s week sits in a range, or `-1`.
80
+ *
81
+ * Computed from the span rather than searched for, so jumping to a month costs
82
+ * the same whether it is one screen away or a year.
83
+ */
84
+ export function weekIndex(weeks: readonly Date[], date: Date, weekStartsOn: number): number {
85
+ const first = weeks[0];
86
+ if (!first) return -1;
87
+ const target = startOfWeek(date, weekStartsOn);
88
+ const index = Math.round((target.getTime() - first.getTime()) / (7 * DAY_MS));
89
+ return index >= 0 && index < weeks.length ? index : -1;
90
+ }
package/src/index.ts CHANGED
@@ -833,6 +833,8 @@ export {
833
833
  type PlannerNavProps,
834
834
  type PlannerActionProps,
835
835
  type PlannerGridProps,
836
+ type PlannerScrollerProps,
837
+ type PlannerVariant,
836
838
  type PlannerDayProps,
837
839
  type PlannerDayState,
838
840
  type PlannerDayRenderer,
package/src/utils/date.ts CHANGED
@@ -535,13 +535,23 @@ export function monthNames(locale: DateLocale, style: 'long' | 'short' = 'long')
535
535
  }
536
536
 
537
537
  /** Column headings, rotated so the first one is `weekStartsOn`. */
538
- export function weekdayNames(locale: DateLocale, weekStartsOn: number): string[] {
538
+ export function weekdayNames(
539
+ locale: DateLocale,
540
+ weekStartsOn: number,
541
+ /*
542
+ * `narrow` is a single letter in most locales, and several of them repeat —
543
+ * English has T twice and S twice. That is only safe where the column it
544
+ * heads is already unambiguous by position, which is why it is a caller's
545
+ * choice rather than the default.
546
+ */
547
+ width: 'short' | 'narrow' = 'short'
548
+ ): string[] {
539
549
  const first = normalizeWeekStart(weekStartsOn);
540
550
  // 2021-08-01 was a Sunday, so adding the index lands on each weekday in turn.
541
551
  return Array.from({ length: 7 }, (_unused, day) => {
542
552
  const index = (day + first) % 7;
543
- const formatted = formatGregory(new Date(2021, 7, 1 + index), locale, { weekday: 'short' });
544
- return formatted ?? WEEKDAYS_EN[index]!;
553
+ const formatted = formatGregory(new Date(2021, 7, 1 + index), locale, { weekday: width });
554
+ return formatted ?? (width === 'narrow' ? WEEKDAYS_EN[index]![0]! : WEEKDAYS_EN[index]!);
545
555
  });
546
556
  }
547
557