panelui-native 0.80.0 → 0.82.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 (92) hide show
  1. package/lib/module/components/bottom-sheet/index.js +90 -17
  2. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  3. package/lib/module/components/button/index.js +13 -0
  4. package/lib/module/components/button/index.js.map +1 -1
  5. package/lib/module/components/button-group/index.js +3 -0
  6. package/lib/module/components/button-group/index.js.map +1 -1
  7. package/lib/module/components/image-generation/dot-field.js +210 -0
  8. package/lib/module/components/image-generation/dot-field.js.map +1 -0
  9. package/lib/module/components/image-generation/index.js +417 -0
  10. package/lib/module/components/image-generation/index.js.map +1 -0
  11. package/lib/module/components/kpi/index.js +2 -4
  12. package/lib/module/components/kpi/index.js.map +1 -1
  13. package/lib/module/components/menu/index.js +8 -16
  14. package/lib/module/components/menu/index.js.map +1 -1
  15. package/lib/module/components/panelside/index.js +340 -33
  16. package/lib/module/components/panelside/index.js.map +1 -1
  17. package/lib/module/components/planner/index.js +446 -32
  18. package/lib/module/components/planner/index.js.map +1 -1
  19. package/lib/module/components/planner/planner-entries.js +36 -0
  20. package/lib/module/components/planner/planner-entries.js.map +1 -1
  21. package/lib/module/components/planner/planner-grid-navigation.js +34 -2
  22. package/lib/module/components/planner/planner-grid-navigation.js.map +1 -1
  23. package/lib/module/components/planner/planner-weeks.js +95 -0
  24. package/lib/module/components/planner/planner-weeks.js.map +1 -0
  25. package/lib/module/components/select/index.js +127 -23
  26. package/lib/module/components/select/index.js.map +1 -1
  27. package/lib/module/components/slide-button/index.js +610 -0
  28. package/lib/module/components/slide-button/index.js.map +1 -0
  29. package/lib/module/components/slide-button/slide-button-track.js +98 -0
  30. package/lib/module/components/slide-button/slide-button-track.js.map +1 -0
  31. package/lib/module/icons/index.js +553 -1934
  32. package/lib/module/icons/index.js.map +1 -1
  33. package/lib/module/index.js +3 -1
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/primitives/portal.js +15 -1
  36. package/lib/module/primitives/portal.js.map +1 -1
  37. package/lib/module/utils/date.js +10 -3
  38. package/lib/module/utils/date.js.map +1 -1
  39. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  40. package/lib/typescript/src/components/button/index.d.ts +12 -0
  41. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  42. package/lib/typescript/src/components/button-group/index.d.ts +9 -0
  43. package/lib/typescript/src/components/button-group/index.d.ts.map +1 -1
  44. package/lib/typescript/src/components/image-generation/dot-field.d.ts +61 -0
  45. package/lib/typescript/src/components/image-generation/dot-field.d.ts.map +1 -0
  46. package/lib/typescript/src/components/image-generation/index.d.ts +126 -0
  47. package/lib/typescript/src/components/image-generation/index.d.ts.map +1 -0
  48. package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
  49. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  50. package/lib/typescript/src/components/panelside/index.d.ts +194 -10
  51. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  52. package/lib/typescript/src/components/planner/index.d.ts +65 -4
  53. package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
  54. package/lib/typescript/src/components/planner/planner-entries.d.ts +18 -0
  55. package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -1
  56. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts +7 -1
  57. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts.map +1 -1
  58. package/lib/typescript/src/components/planner/planner-weeks.d.ts +50 -0
  59. package/lib/typescript/src/components/planner/planner-weeks.d.ts.map +1 -0
  60. package/lib/typescript/src/components/select/index.d.ts +75 -2
  61. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  62. package/lib/typescript/src/components/slide-button/index.d.ts +317 -0
  63. package/lib/typescript/src/components/slide-button/index.d.ts.map +1 -0
  64. package/lib/typescript/src/components/slide-button/slide-button-track.d.ts +70 -0
  65. package/lib/typescript/src/components/slide-button/slide-button-track.d.ts.map +1 -0
  66. package/lib/typescript/src/icons/index.d.ts +136 -189
  67. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  68. package/lib/typescript/src/index.d.ts +5 -3
  69. package/lib/typescript/src/index.d.ts.map +1 -1
  70. package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
  71. package/lib/typescript/src/utils/date.d.ts +1 -1
  72. package/lib/typescript/src/utils/date.d.ts.map +1 -1
  73. package/package.json +3 -2
  74. package/src/components/bottom-sheet/index.tsx +97 -17
  75. package/src/components/button/index.tsx +10 -2
  76. package/src/components/button-group/index.tsx +1 -0
  77. package/src/components/image-generation/dot-field.ts +226 -0
  78. package/src/components/image-generation/index.tsx +488 -0
  79. package/src/components/kpi/index.tsx +2 -4
  80. package/src/components/menu/index.tsx +10 -15
  81. package/src/components/panelside/index.tsx +498 -33
  82. package/src/components/planner/index.tsx +516 -38
  83. package/src/components/planner/planner-entries.ts +33 -0
  84. package/src/components/planner/planner-grid-navigation.ts +37 -2
  85. package/src/components/planner/planner-weeks.ts +90 -0
  86. package/src/components/select/index.tsx +158 -31
  87. package/src/components/slide-button/index.tsx +681 -0
  88. package/src/components/slide-button/slide-button-track.ts +100 -0
  89. package/src/icons/index.tsx +338 -1327
  90. package/src/index.ts +30 -0
  91. package/src/primitives/portal.tsx +15 -1
  92. package/src/utils/date.ts +13 -3
@@ -58,7 +58,7 @@
58
58
  * are the whole content of the grid for somebody who cannot see it.
59
59
  */
60
60
  import { Children, createContext, forwardRef, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
61
- import { AccessibilityInfo, AppState, Platform, Pressable, View } from 'react-native';
61
+ import { AccessibilityInfo, AppState, FlatList, Platform, Pressable, View } from 'react-native';
62
62
  import { tv } from 'tailwind-variants';
63
63
  import { useCSSVariable } from 'uniwind';
64
64
  import { ChevronLeftIcon, ChevronRightIcon } from "../../icons/index.js";
@@ -70,16 +70,51 @@ import { Frame } from "../frame/index.js";
70
70
  import { bucketByDay, dayAccessibilityLabel, entriesOn, summariseMonth, visibleEntries } from "./planner-entries.js";
71
71
  import { usePlannerMonthAnnouncement } from "./planner-announcement.js";
72
72
  import { plannerGridTarget } from "./planner-grid-navigation.js";
73
+ import { weekAnchor, weekDays, weekIndex, weekRange } from "./planner-weeks.js";
73
74
  import { usePlannerMonthLifecycle, usePlannerSelectionLifecycle } from "./planner-lifecycle.js";
74
75
  import { millisecondsUntilNextLocalDay } from "./planner-today.js";
75
76
 
76
- /** How many of a day's entries a cell draws before it stops and counts. */
77
+ /**
78
+ * How many of a day's entries a cell draws before it stops and counts.
79
+ *
80
+ * `calendar` gets one more because a named entry is a row, and a cell tall
81
+ * enough to name one is tall enough for three; `tiles` draws a single icon at
82
+ * the size of the tile, so the limit there is the look rather than a number.
83
+ */
77
84
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
78
- const DEFAULT_ENTRY_LIMIT = 2;
85
+ const DEFAULT_ENTRY_LIMIT = {
86
+ default: 2,
87
+ tiles: 1,
88
+ calendar: 3
89
+ };
79
90
 
80
91
  /** The palette a category takes its dot from when it does not name a colour. */
81
92
  const PALETTE_SIZE = 5;
82
93
 
94
+ /**
95
+ * How strongly a `tiles` day takes its category's colour.
96
+ *
97
+ * Low enough that the date in the corner and the icon over it both stay
98
+ * legible, high enough that a brand colour arrives as that brand rather than as
99
+ * a grey tile with a suggestion of one.
100
+ */
101
+ const TILE_TINT_OPACITY = 0.32;
102
+
103
+ /**
104
+ * The height a `tiles` grid reserves, and what it is made of.
105
+ *
106
+ * Six weeks' worth, always — but only the weeks the month actually spans are
107
+ * drawn, and they share the reserved height out between them. A five-week
108
+ * month therefore has slightly taller tiles than a six-week one and no band of
109
+ * empty space under the last row, while the panel itself never changes size as
110
+ * you page through the year. Reserving the height and leaving the sixth row
111
+ * blank, or dropping the row and letting the panel shrink, each fix one of
112
+ * those at the cost of the other.
113
+ */
114
+ const TILE_ROW_HEIGHT = 64;
115
+ const TILE_ROW_GAP = 4;
116
+ const TILE_GRID_HEIGHT = TILE_ROW_HEIGHT * 6 + TILE_ROW_GAP * 5;
117
+
83
118
  /** Stable wrapper keeps the native method's receiver and the hook dependency steady. */
84
119
  const announceMonth = label => {
85
120
  AccessibilityInfo.announceForAccessibility(label);
@@ -91,6 +126,19 @@ const plannerVariants = tv({
91
126
  heading: 'flex-1 text-center',
92
127
  legend: 'flex-row flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3',
93
128
  swatch: 'h-2 w-2 rounded-full'
129
+ },
130
+ variants: {
131
+ /*
132
+ * The weeks share out the height instead of standing at their own. The
133
+ * weekday row keeps its natural height — it is a label, and stretching it
134
+ * only moves the letters away from the column they name.
135
+ */
136
+ fill: {
137
+ true: {
138
+ grid: 'flex-1',
139
+ week: 'flex-1'
140
+ }
141
+ }
94
142
  }
95
143
  });
96
144
  const dayVariants = tv({
@@ -114,9 +162,32 @@ const dayVariants = tv({
114
162
  cell: 'mx-0.5 h-14 flex-1 rounded-xl border border-transparent px-1.5 pt-1.5',
115
163
  number: 'text-xs leading-none',
116
164
  marker: 'absolute right-1.5 top-1.5 h-1.5 w-1.5 rounded-full',
117
- body: 'flex-1 flex-row items-center justify-center gap-0.5 pb-1'
165
+ body: 'flex-1 flex-row items-center justify-center gap-0.5 pb-1',
166
+ /** The colour a day's entries give it, drawn under everything else. */
167
+ tint: 'absolute inset-0 rounded-xl',
168
+ /*
169
+ * The corners of a tile. These are Views rather than classes on the Text
170
+ * itself: a Text takes its typography from a class but not its position,
171
+ * so a number told to sit in a corner stayed where the layout put it —
172
+ * dead centre, under the icon.
173
+ */
174
+ corner: 'absolute bottom-1 right-1.5',
175
+ counter: 'absolute left-1.5 top-1',
176
+ /** One entry, named, in a cell with the height to name it. */
177
+ chip: 'w-full overflow-hidden rounded-sm bg-muted px-1 py-0.5',
178
+ overflow: 'leading-none'
118
179
  },
119
180
  variants: {
181
+ /*
182
+ * Cells stretch to their container rather than standing at a fixed height.
183
+ * The grid is still six weeks, so what changes is how tall a week is, not
184
+ * how many there are.
185
+ */
186
+ fill: {
187
+ true: {
188
+ cell: 'h-auto min-h-0 flex-1'
189
+ }
190
+ },
120
191
  /** Inside the month being shown, as opposed to the days either side. */
121
192
  inMonth: {
122
193
  true: {
@@ -148,6 +219,38 @@ const dayVariants = tv({
148
219
  true: {
149
220
  cell: 'opacity-40'
150
221
  }
222
+ },
223
+ /*
224
+ * What a cell draws. One axis, because these are three answers to the same
225
+ * question and a cell can only give one of them.
226
+ *
227
+ * Last, so a look can undo what the states above it set. `calendar` has no
228
+ * tile to ring or fill, so it has to be able to take the background and the
229
+ * border back off a cell that today or the open day just gave one to.
230
+ */
231
+ variant: {
232
+ default: {},
233
+ /*
234
+ * A tile per day, carrying the one thing on it at the size of a mark
235
+ * rather than a marker. The date moves out of the way into the corner:
236
+ * it is how you find the day you want, not what the cell is showing.
237
+ */
238
+ tiles: {
239
+ cell: 'h-full items-center justify-center px-0 pt-0',
240
+ overflow: 'leading-none'
241
+ },
242
+ /*
243
+ * Named entries stacked down an open cell. No tile, because a tile per
244
+ * day and a block per entry are two boxes saying the same thing and the
245
+ * entries are the ones carrying words — the weeks are ruled off instead,
246
+ * which separates them without enclosing anything.
247
+ */
248
+ calendar: {
249
+ cell: 'mx-0 h-full min-h-0 items-stretch gap-px rounded-none border-transparent bg-transparent px-1 pb-1 pt-1.5',
250
+ number: 'text-center',
251
+ body: 'flex-col items-stretch justify-start gap-px pb-0',
252
+ overflow: 'text-center leading-none'
253
+ }
151
254
  }
152
255
  },
153
256
  compoundVariants: [
@@ -164,7 +267,8 @@ const dayVariants = tv({
164
267
  }
165
268
  }],
166
269
  defaultVariants: {
167
- inMonth: true
270
+ inMonth: true,
271
+ variant: 'default'
168
272
  }
169
273
  });
170
274
 
@@ -172,6 +276,15 @@ const dayVariants = tv({
172
276
  * Data
173
277
  * ------------------------------------------------------------------ */
174
278
 
279
+ /**
280
+ * What a day cell draws.
281
+ *
282
+ * `default` is a number, a marker and small icons. `tiles` gives the day over
283
+ * to one large icon tinted with its category's colour, with the date in the
284
+ * corner. `calendar` names each entry in a block under a centred date, on an
285
+ * open grid ruled off by week.
286
+ */
287
+
175
288
  /** One thing that falls on a day. */
176
289
 
177
290
  /** A group of entries: the key to a colour, and a line in the legend. */
@@ -246,7 +359,9 @@ const PlannerRoot = /*#__PURE__*/forwardRef(({
246
359
  defaultSelected = null,
247
360
  onSelectedChange,
248
361
  onDayPress,
249
- entryLimit = DEFAULT_ENTRY_LIMIT,
362
+ variant = 'default',
363
+ fill = false,
364
+ entryLimit,
250
365
  weekStartsOn = 'auto',
251
366
  locale,
252
367
  calendar = 'gregory',
@@ -254,6 +369,7 @@ const PlannerRoot = /*#__PURE__*/forwardRef(({
254
369
  children,
255
370
  ...props
256
371
  }, ref) => {
372
+ const resolvedEntryLimit = entryLimit ?? DEFAULT_ENTRY_LIMIT[variant];
257
373
  const system = resolveCalendar(calendar, locale);
258
374
  const palette = usePalette();
259
375
  const today = useToday();
@@ -276,6 +392,15 @@ const PlannerRoot = /*#__PURE__*/forwardRef(({
276
392
  monthLabel: calendarMonthLabel(month, system, locale),
277
393
  announce: announceMonth
278
394
  });
395
+ /*
396
+ * Scrolling changes the month continuously, and announcing each one as it
397
+ * goes would talk over somebody reading the weeks. The announcement belongs
398
+ * to a deliberate move — the arrows, the Today pill — so a scroll reports
399
+ * the change and stays quiet about it.
400
+ */
401
+ const syncMonth = useCallback(next => {
402
+ requestMonth(next);
403
+ }, [requestMonth]);
279
404
  const setMonth = useCallback(next => {
280
405
  expectMonthAnnouncement(settleMonth(next));
281
406
  requestMonth(next);
@@ -305,6 +430,7 @@ const PlannerRoot = /*#__PURE__*/forwardRef(({
305
430
  const context = useMemo(() => ({
306
431
  month,
307
432
  setMonth,
433
+ syncMonth,
308
434
  today,
309
435
  selected,
310
436
  select,
@@ -313,14 +439,16 @@ const PlannerRoot = /*#__PURE__*/forwardRef(({
313
439
  categoryLabels,
314
440
  colorOf,
315
441
  summary,
316
- entryLimit,
442
+ entryLimit: resolvedEntryLimit,
443
+ variant,
444
+ fill,
317
445
  weekStartsOn: normalizedWeekStart,
318
446
  grid,
319
447
  locale,
320
448
  system,
321
449
  isInMonth,
322
450
  onDayPress
323
- }), [month, setMonth, today, selected, select, days, categories, categoryLabels, colorOf, summary, entryLimit, normalizedWeekStart, grid, locale, system, isInMonth, onDayPress]);
451
+ }), [month, setMonth, syncMonth, today, selected, select, days, categories, categoryLabels, colorOf, summary, resolvedEntryLimit, variant, fill, normalizedWeekStart, grid, locale, system, isInMonth, onDayPress]);
324
452
 
325
453
  /*
326
454
  * The header belongs in the Frame's strip and everything else in its
@@ -336,18 +464,20 @@ const PlannerRoot = /*#__PURE__*/forwardRef(({
336
464
  /*#__PURE__*/
337
465
  // Full width by default: the grid is seven equal columns, and a
338
466
  // container that sizes to its content collapses them to the width of
339
- // a two-digit number.
467
+ // a two-digit number. Under `fill` the panel has to flex too, or the
468
+ // grid inside it stretches against a container that does not.
340
469
  _jsxs(Frame, {
341
470
  ref: ref,
342
- className: cn('w-full', className),
471
+ className: cn('w-full', fill && 'flex-1', className),
343
472
  ...props,
344
473
  children: [header, /*#__PURE__*/_jsx(Frame.Panel, {
345
474
  dividers: false,
475
+ className: fill ? 'flex-1' : undefined,
346
476
  children: body
347
477
  })]
348
478
  }) : /*#__PURE__*/_jsxs(View, {
349
479
  ref: ref,
350
- className: cn('w-full', className),
480
+ className: cn('w-full', fill && 'flex-1', className),
351
481
  ...props,
352
482
  children: [header, body]
353
483
  })
@@ -377,10 +507,10 @@ function PlannerHeader({
377
507
  const lead = parts.filter(child => !trails(child));
378
508
  return /*#__PURE__*/_jsxs(Frame.Header, {
379
509
  children: [/*#__PURE__*/_jsx(View, {
380
- className: "min-w-0 flex-1 flex-row items-center gap-2",
510
+ className: "min-w-0 shrink flex-row items-center gap-2",
381
511
  children: lead
382
512
  }), trailing.length > 0 ? /*#__PURE__*/_jsx(View, {
383
- className: "shrink-0 flex-row items-center gap-1.5",
513
+ className: "ml-auto shrink-0 flex-row items-center gap-1.5",
384
514
  children: trailing
385
515
  }) : null]
386
516
  });
@@ -510,16 +640,35 @@ function PlannerGrid({
510
640
  locale,
511
641
  selected,
512
642
  today,
513
- isInMonth
643
+ isInMonth,
644
+ variant,
645
+ fill
514
646
  } = usePlanner('Planner.Grid');
515
647
  const {
516
648
  grid,
517
649
  week: weekRow,
518
650
  heading
519
- } = plannerVariants();
520
- const headings = useMemo(() => weekdayNames(locale, weekStartsOn), [locale, weekStartsOn]);
521
- const dates = useMemo(() => weeks.flat(), [weeks]);
651
+ } = plannerVariants({
652
+ fill
653
+ });
654
+ /*
655
+ * `tiles` leaves the days either side of the month blank, so its columns are
656
+ * told apart by position alone and a single letter is enough to head them.
657
+ */
658
+ const headings = useMemo(() => weekdayNames(locale, weekStartsOn, variant === 'tiles' ? 'narrow' : 'short'), [locale, weekStartsOn, variant]);
659
+ /*
660
+ * `tiles` draws nothing for the days either side of the month, so a week made
661
+ * only of those is a band of empty space rather than a week. It is dropped,
662
+ * and the grid keeps its height by letting the weeks that remain stretch.
663
+ */
664
+ const shownWeeks = useMemo(() => variant === 'tiles' ? weeks.filter(week => week.some(isInMonth)) : weeks, [weeks, variant, isInMonth]);
665
+ const dates = useMemo(() => shownWeeks.flat(), [shownWeeks]);
522
666
  const indexByDay = useMemo(() => new Map(dates.map((date, index) => [date.getTime(), index])), [dates]);
667
+ /*
668
+ * A blank cell has nothing to focus, so arrow keys step over it rather than
669
+ * landing on a day that is not drawn and losing focus altogether.
670
+ */
671
+ const navigable = useMemo(() => variant === 'tiles' ? index => isInMonth(dates[index] ?? today) : undefined, [variant, dates, isInMonth, today]);
523
672
  const initialDay = selected ?? today;
524
673
  const [activeDay, setActiveDay] = useState(initialDay.getTime());
525
674
  const storedIndex = indexByDay.get(activeDay);
@@ -534,12 +683,12 @@ function PlannerGrid({
534
683
  if (date) setActiveDay(date.getTime());
535
684
  }, [dates]);
536
685
  const focus = useCallback((index, event) => {
537
- const target = plannerGridTarget(event.nativeEvent.key ?? '', index, dates.length);
686
+ const target = plannerGridTarget(event.nativeEvent.key ?? '', index, dates.length, 7, navigable);
538
687
  if (target === null) return;
539
688
  event.preventDefault();
540
689
  makeActive(target);
541
690
  refs.current.get(target)?.focus();
542
- }, [dates.length, makeActive]);
691
+ }, [dates.length, makeActive, navigable]);
543
692
  const navigation = useMemo(() => ({
544
693
  activeIndex,
545
694
  indexByDay,
@@ -553,18 +702,21 @@ function PlannerGrid({
553
702
  className: grid({
554
703
  className
555
704
  }),
705
+ style: variant === 'tiles' && !fill ? {
706
+ height: TILE_GRID_HEIGHT
707
+ } : undefined,
556
708
  children: [/*#__PURE__*/_jsx(View, {
557
709
  className: weekRow(),
558
710
  accessibilityElementsHidden: true,
559
711
  importantForAccessibility: "no-hide-descendants",
560
- children: headings.map(label => /*#__PURE__*/_jsx(Text, {
712
+ children: headings.map((label, position) => /*#__PURE__*/_jsx(Text, {
561
713
  size: "xs",
562
714
  muted: true,
563
715
  className: heading(),
564
716
  children: label.toUpperCase()
565
- }, label))
566
- }), weeks.map((week, index) => /*#__PURE__*/_jsx(View, {
567
- className: weekRow(),
717
+ }, position))
718
+ }), shownWeeks.map((week, index) => /*#__PURE__*/_jsx(View, {
719
+ className: cn(weekRow(), variant === 'tiles' && 'flex-1'),
568
720
  children: week.map(date => /*#__PURE__*/_jsx(PlannerDay, {
569
721
  date: date,
570
722
  renderDay: renderDay
@@ -587,6 +739,8 @@ function PlannerDay({
587
739
  colorOf,
588
740
  categoryLabels,
589
741
  entryLimit,
742
+ variant,
743
+ fill,
590
744
  locale,
591
745
  system,
592
746
  isInMonth,
@@ -606,14 +760,30 @@ function PlannerDay({
606
760
  } = visibleEntries(entries, entryLimit);
607
761
  const drawable = shown.filter(entry => entry.icon);
608
762
  const styles = dayVariants({
763
+ variant,
764
+ fill,
609
765
  inMonth,
610
766
  today: isToday,
611
767
  selected: isSelected
612
768
  });
769
+ /*
770
+ * The colour the day's entries give it. The first entry naming a category the
771
+ * planner knows wins, and an uncategorised day gets nothing — the tint is a
772
+ * signal about what is on the day, so a day with no answer stays plain rather
773
+ * than being coloured for the sake of it.
774
+ */
775
+ const tint = markerColor(entries, colorOf);
613
776
  const label = dayAccessibilityLabel(calendarLongDate(date, system, locale), entries, categoryLabels);
777
+
778
+ /*
779
+ * Pressing the open day again closes it. Without this the mark is a one-way
780
+ * door: a planner with no `Details` bound has nothing to dismiss, so the day
781
+ * you pressed stays ringed until you press a different one and there is no
782
+ * way back to none selected.
783
+ */
614
784
  const press = () => {
615
785
  onDayPress?.(date, entries);
616
- select(date);
786
+ select(isSelected ? null : date);
617
787
  };
618
788
  const gridIndex = navigation?.indexByDay.get(date.getTime());
619
789
  const setRef = useCallback(node => {
@@ -643,6 +813,21 @@ function PlannerDay({
643
813
  })
644
814
  });
645
815
  }
816
+
817
+ /*
818
+ * `tiles` leaves the days either side of the month out altogether. The row
819
+ * still holds their column, so the grid is six weeks whatever month it is
820
+ * showing, but there is no tile, no number and nothing to press or to read
821
+ * out: a blank is the answer, and drawing a faded one only invites the press
822
+ * it is going to ignore.
823
+ */
824
+ if (variant === 'tiles' && !inMonth) {
825
+ return /*#__PURE__*/_jsx(View, {
826
+ className: cn('mx-0.5 flex-1', fill ? 'min-h-0' : 'h-16'),
827
+ accessibilityElementsHidden: true,
828
+ importantForAccessibility: "no-hide-descendants"
829
+ });
830
+ }
646
831
  return /*#__PURE__*/_jsxs(Pressable, {
647
832
  ...webGridProps,
648
833
  onPress: press,
@@ -653,28 +838,111 @@ function PlannerDay({
653
838
  selected: isSelected
654
839
  },
655
840
  className: styles.cell(),
656
- children: [/*#__PURE__*/_jsx(Text, {
841
+ children: [variant === 'tiles' && tint ? /*#__PURE__*/_jsx(View, {
842
+ pointerEvents: "none",
843
+ className: styles.tint(),
844
+ style: {
845
+ backgroundColor: tint,
846
+ opacity: TILE_TINT_OPACITY
847
+ }
848
+ }) : null, variant === 'tiles' ? /*#__PURE__*/_jsx(View, {
849
+ className: styles.corner(),
850
+ pointerEvents: "none",
851
+ children: /*#__PURE__*/_jsx(Text, {
852
+ size: "xs",
853
+ className: styles.number(),
854
+ children: calendarDayNumber(date, system, locale)
855
+ })
856
+ }) : variant === 'calendar' ?
857
+ /*#__PURE__*/
858
+ /*
859
+ * A disc around the date rather than a ring around the cell. There is
860
+ * no tile here to ring, and the date is the only thing in the cell that
861
+ * is always present — so it is the only place a mark can go and be
862
+ * found in the same spot on every day of the month.
863
+ */
864
+ _jsx(View, {
865
+ className: cn('h-6 w-6 items-center justify-center self-center rounded-full', isToday && 'bg-primary'),
866
+ children: /*#__PURE__*/_jsx(Text, {
867
+ size: "xs",
868
+ className: cn(styles.number(), isToday && 'text-primary-foreground'),
869
+ children: calendarDayNumber(date, system, locale)
870
+ })
871
+ }) : /*#__PURE__*/_jsx(Text, {
657
872
  className: styles.number(),
658
873
  children: calendarDayNumber(date, system, locale)
659
- }), entries.length > 0 ? /*#__PURE__*/_jsx(View, {
874
+ }), variant === 'default' && entries.length > 0 ? /*#__PURE__*/_jsx(View, {
660
875
  className: styles.marker(),
661
876
  style: {
662
- backgroundColor: markerColor(entries, colorOf)
877
+ backgroundColor: tint
663
878
  }
664
- }) : null, drawable.length > 0 ? /*#__PURE__*/_jsxs(View, {
879
+ }) : null, variant === 'calendar' ? /*#__PURE__*/_jsxs(View, {
880
+ className: styles.body(),
881
+ children: [shown.map(entry => /*#__PURE__*/_jsx(PlannerChip, {
882
+ entry: entry,
883
+ color: entry.color ?? colorOf(entry.category)
884
+ }, entry.id)), overflow > 0 ? /*#__PURE__*/_jsx(Text, {
885
+ size: "xs",
886
+ muted: true,
887
+ className: styles.overflow(),
888
+ numberOfLines: 1,
889
+ children: "\u2022\u2022\u2022"
890
+ }) : null]
891
+ }) : null, variant === 'tiles' && overflow > 0 ? /*#__PURE__*/_jsx(View, {
892
+ className: styles.counter(),
893
+ pointerEvents: "none",
894
+ children: /*#__PURE__*/_jsxs(Text, {
895
+ size: "xs",
896
+ muted: true,
897
+ className: styles.overflow(),
898
+ children: ["+", overflow]
899
+ })
900
+ }) : null, variant !== 'calendar' && drawable.length > 0 ? /*#__PURE__*/_jsxs(View, {
665
901
  className: styles.body(),
666
902
  children: [drawable.map(entry => /*#__PURE__*/_jsx(View, {
667
903
  children: entry.icon
668
- }, entry.id)), overflow > 0 ? /*#__PURE__*/_jsxs(Text, {
904
+ }, entry.id)), variant !== 'tiles' && overflow > 0 ? /*#__PURE__*/_jsxs(Text, {
669
905
  size: "xs",
670
906
  muted: true,
671
- className: "leading-none",
907
+ className: styles.overflow(),
672
908
  children: ["+", overflow]
673
909
  }) : null]
674
910
  }) : null]
675
911
  });
676
912
  }
677
- PlannerDay.displayName = 'Planner.Day';
913
+
914
+ /**
915
+ * One named entry inside a `calendar` cell.
916
+ *
917
+ * A solid block of the entry's colour with light text on it. That is a real
918
+ * constraint rather than a free choice: the colour is the caller's, so a pale
919
+ * one will not carry white well — but a wash with the label in the foreground
920
+ * colour makes every entry the same weight and loses the one thing the colour
921
+ * was for, which is telling them apart down a column at a glance.
922
+ */
923
+ function PlannerChip({
924
+ entry,
925
+ color
926
+ }) {
927
+ const {
928
+ chip
929
+ } = dayVariants();
930
+ return /*#__PURE__*/_jsx(View, {
931
+ className: chip(),
932
+ style: color ? {
933
+ backgroundColor: color
934
+ } : undefined,
935
+ children: /*#__PURE__*/_jsx(Text, {
936
+ size: "xs",
937
+ numberOfLines: 1,
938
+ className: cn('leading-none', !color && 'text-foreground'),
939
+ style: color ? {
940
+ color: '#ffffff'
941
+ } : undefined,
942
+ children: entry.label
943
+ })
944
+ });
945
+ }
678
946
 
679
947
  /**
680
948
  * The dot's colour: the first entry that names a category the planner knows.
@@ -685,7 +953,9 @@ PlannerDay.displayName = 'Planner.Day';
685
953
  */
686
954
  function markerColor(entries, colorOf) {
687
955
  for (const entry of entries) {
688
- const color = colorOf(entry.category);
956
+ // The entry's own colour first: a brand belongs to the thing, not to the
957
+ // group it was filed under.
958
+ const color = entry.color ?? colorOf(entry.category);
689
959
  if (color) return color;
690
960
  }
691
961
  return undefined;
@@ -807,6 +1077,149 @@ function PlannerDetails({
807
1077
  });
808
1078
  }
809
1079
  PlannerDetails.displayName = 'Planner.Details';
1080
+
1081
+ /* ------------------------------------------------------------------ *
1082
+ * Scroller
1083
+ * ------------------------------------------------------------------ */
1084
+
1085
+ /** How many weeks either side of the opening month a scroller reaches by default. */
1086
+ const DEFAULT_SCROLLER_WEEKS = 53;
1087
+
1088
+ /** The height of one week row, and enough of it for a date and three entries. */
1089
+ const DEFAULT_WEEK_HEIGHT = 96;
1090
+ /**
1091
+ * The weeks of the year, scrolled through rather than paged.
1092
+ *
1093
+ * A month grid answers "what does this month look like"; a scroller answers
1094
+ * "what is coming", which does not stop at the end of a month. The week
1095
+ * straddling the boundary is drawn once, in one piece, instead of appearing cut
1096
+ * in half at the bottom of one page and again at the top of the next.
1097
+ *
1098
+ * The range is bounded rather than endless. A scroller has to know its own
1099
+ * height to place a scrollbar and to jump to a month without rendering its way
1100
+ * there, and neither is possible over a list with no end.
1101
+ *
1102
+ * The month in the header follows the scroll: whichever month holds most of the
1103
+ * first week on screen is the one named, and the days either side of it grey
1104
+ * out. `Planner.Nav` and `Planner.Today` still work — they scroll the list
1105
+ * rather than replacing what is in it.
1106
+ */
1107
+ function PlannerScroller({
1108
+ className,
1109
+ weeks: span = DEFAULT_SCROLLER_WEEKS,
1110
+ rowHeight = DEFAULT_WEEK_HEIGHT,
1111
+ renderDay
1112
+ }) {
1113
+ const {
1114
+ month,
1115
+ syncMonth,
1116
+ weekStartsOn,
1117
+ locale,
1118
+ variant,
1119
+ system
1120
+ } = usePlanner('Planner.Scroller');
1121
+ const {
1122
+ week: weekRow,
1123
+ heading
1124
+ } = plannerVariants();
1125
+ const list = useRef(null);
1126
+
1127
+ /*
1128
+ * The range is anchored where the planner opened and never rebuilt. Rebuilding
1129
+ * it around the current month would move every row's index under the scroll
1130
+ * position each time the month changed, which is the one thing a list being
1131
+ * scrolled must not do.
1132
+ */
1133
+ const [anchor] = useState(() => month);
1134
+ const range = useMemo(() => weekRange(anchor, weekStartsOn, span, span), [anchor, weekStartsOn, span]);
1135
+ const headings = useMemo(() => weekdayNames(locale, weekStartsOn, 'narrow'), [locale, weekStartsOn]);
1136
+
1137
+ /*
1138
+ * Which month the scroll is showing, reported up but not acted on here. The
1139
+ * ref is what stops a scroll that stays inside one month from setting it over
1140
+ * and over, and what stops the scroll this component performs in answer to a
1141
+ * month change from reporting that same month straight back.
1142
+ */
1143
+ const reported = useRef(month.getTime());
1144
+ const onViewable = useRef(({
1145
+ viewableItems
1146
+ }) => {
1147
+ const first = viewableItems[0]?.item;
1148
+ if (!first) return;
1149
+ const next = startOfCalendarMonth(weekAnchor(first), system, locale);
1150
+ if (next.getTime() === reported.current) return;
1151
+ reported.current = next.getTime();
1152
+ syncMonth(next);
1153
+ });
1154
+
1155
+ /*
1156
+ * A month arriving from outside — the arrows, the Today pill, a controlled
1157
+ * prop — scrolls the list to it. A month the scroll itself just reported is
1158
+ * already there, and scrolling to it again would fight the finger.
1159
+ */
1160
+ useEffect(() => {
1161
+ if (month.getTime() === reported.current) return;
1162
+ reported.current = month.getTime();
1163
+ const index = weekIndex(range, month, weekStartsOn);
1164
+ if (index >= 0) list.current?.scrollToIndex({
1165
+ index,
1166
+ animated: true
1167
+ });
1168
+ }, [month, range, weekStartsOn]);
1169
+ const initialIndex = Math.max(0, weekIndex(range, month, weekStartsOn));
1170
+ return /*#__PURE__*/_jsxs(View, {
1171
+ className: cn('flex-1', className),
1172
+ children: [/*#__PURE__*/_jsx(View, {
1173
+ className: cn(weekRow(), 'px-1 pb-1'),
1174
+ accessibilityElementsHidden: true,
1175
+ importantForAccessibility: "no-hide-descendants",
1176
+ children: headings.map((label, position) => /*#__PURE__*/_jsx(Text, {
1177
+ size: "xs",
1178
+ muted: true,
1179
+ className: heading(),
1180
+ children: label.toUpperCase()
1181
+ }, position))
1182
+ }), /*#__PURE__*/_jsx(FlatList, {
1183
+ ref: list,
1184
+ data: range,
1185
+ keyExtractor: weekStart => String(weekStart.getTime()),
1186
+ initialScrollIndex: initialIndex
1187
+ // Every row is the same height, so the list can place any of them
1188
+ // without measuring — which is what makes jumping to a month cheap.
1189
+ ,
1190
+ getItemLayout: (_unused, index) => ({
1191
+ length: rowHeight,
1192
+ offset: rowHeight * index,
1193
+ index
1194
+ }),
1195
+ onViewableItemsChanged: onViewable.current,
1196
+ viewabilityConfig: VIEWABILITY,
1197
+ showsVerticalScrollIndicator: false,
1198
+ renderItem: ({
1199
+ item
1200
+ }) => /*#__PURE__*/_jsx(View, {
1201
+ className: cn(weekRow(), 'border-t border-border px-1', variant === 'calendar' && 'items-stretch'),
1202
+ style: {
1203
+ height: rowHeight
1204
+ },
1205
+ children: weekDays(item).map(date => /*#__PURE__*/_jsx(PlannerDay, {
1206
+ date: date,
1207
+ renderDay: renderDay
1208
+ }, date.getTime()))
1209
+ })
1210
+ })]
1211
+ });
1212
+ }
1213
+ PlannerScroller.displayName = 'Planner.Scroller';
1214
+
1215
+ /*
1216
+ * The first row more than half on screen is the one that names the month. A
1217
+ * lower threshold hands the month over while the week is still mostly below the
1218
+ * fold, which reads as the header changing early.
1219
+ */
1220
+ const VIEWABILITY = {
1221
+ itemVisiblePercentThreshold: 60
1222
+ };
810
1223
  export const Planner = Object.assign(PlannerRoot, {
811
1224
  Header: PlannerHeader,
812
1225
  Title: PlannerTitle,
@@ -814,6 +1227,7 @@ export const Planner = Object.assign(PlannerRoot, {
814
1227
  Nav: PlannerNav,
815
1228
  Action: PlannerAction,
816
1229
  Grid: PlannerGrid,
1230
+ Scroller: PlannerScroller,
817
1231
  Day: PlannerDay,
818
1232
  Legend: PlannerLegend,
819
1233
  Summary: PlannerSummary,