panelui-native 0.28.1 → 0.30.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 (82) hide show
  1. package/README.md +18 -1
  2. package/lib/module/components/calendar/index.js +350 -115
  3. package/lib/module/components/calendar/index.js.map +1 -1
  4. package/lib/module/components/code-block/highlight.js +363 -0
  5. package/lib/module/components/code-block/highlight.js.map +1 -0
  6. package/lib/module/components/code-block/index.js +313 -0
  7. package/lib/module/components/code-block/index.js.map +1 -0
  8. package/lib/module/components/date-picker/index.js +8 -1
  9. package/lib/module/components/date-picker/index.js.map +1 -1
  10. package/lib/module/components/menu/index.js +714 -0
  11. package/lib/module/components/menu/index.js.map +1 -0
  12. package/lib/module/components/plan/index.js +296 -0
  13. package/lib/module/components/plan/index.js.map +1 -0
  14. package/lib/module/components/popover/index.js +35 -4
  15. package/lib/module/components/popover/index.js.map +1 -1
  16. package/lib/module/components/radio-group/index.js +13 -8
  17. package/lib/module/components/radio-group/index.js.map +1 -1
  18. package/lib/module/components/reasoning/index.js +244 -0
  19. package/lib/module/components/reasoning/index.js.map +1 -0
  20. package/lib/module/components/sources/index.js +221 -0
  21. package/lib/module/components/sources/index.js.map +1 -0
  22. package/lib/module/components/task/index.js +300 -0
  23. package/lib/module/components/task/index.js.map +1 -0
  24. package/lib/module/components/textarea/index.js +295 -0
  25. package/lib/module/components/textarea/index.js.map +1 -0
  26. package/lib/module/components/tooltip/index.js +179 -23
  27. package/lib/module/components/tooltip/index.js.map +1 -1
  28. package/lib/module/icons/index.js +139 -0
  29. package/lib/module/icons/index.js.map +1 -1
  30. package/lib/module/index.js +8 -0
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/primitives/collapse.js +112 -0
  33. package/lib/module/primitives/collapse.js.map +1 -0
  34. package/lib/typescript/src/components/calendar/index.d.ts +68 -3
  35. package/lib/typescript/src/components/calendar/index.d.ts.map +1 -1
  36. package/lib/typescript/src/components/code-block/highlight.d.ts +39 -0
  37. package/lib/typescript/src/components/code-block/highlight.d.ts.map +1 -0
  38. package/lib/typescript/src/components/code-block/index.d.ts +113 -0
  39. package/lib/typescript/src/components/code-block/index.d.ts.map +1 -0
  40. package/lib/typescript/src/components/date-picker/index.d.ts +7 -1
  41. package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -1
  42. package/lib/typescript/src/components/menu/index.d.ts +261 -0
  43. package/lib/typescript/src/components/menu/index.d.ts.map +1 -0
  44. package/lib/typescript/src/components/plan/index.d.ts +127 -0
  45. package/lib/typescript/src/components/plan/index.d.ts.map +1 -0
  46. package/lib/typescript/src/components/popover/index.d.ts +35 -1
  47. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  48. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  49. package/lib/typescript/src/components/reasoning/index.d.ts +97 -0
  50. package/lib/typescript/src/components/reasoning/index.d.ts.map +1 -0
  51. package/lib/typescript/src/components/sources/index.d.ts +85 -0
  52. package/lib/typescript/src/components/sources/index.d.ts.map +1 -0
  53. package/lib/typescript/src/components/task/index.d.ts +103 -0
  54. package/lib/typescript/src/components/task/index.d.ts.map +1 -0
  55. package/lib/typescript/src/components/textarea/index.d.ts +175 -0
  56. package/lib/typescript/src/components/textarea/index.d.ts.map +1 -0
  57. package/lib/typescript/src/components/tooltip/index.d.ts +63 -8
  58. package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -1
  59. package/lib/typescript/src/icons/index.d.ts +14 -0
  60. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  61. package/lib/typescript/src/index.d.ts +10 -2
  62. package/lib/typescript/src/index.d.ts.map +1 -1
  63. package/lib/typescript/src/primitives/collapse.d.ts +42 -0
  64. package/lib/typescript/src/primitives/collapse.d.ts.map +1 -0
  65. package/package.json +1 -1
  66. package/src/components/calendar/index.tsx +389 -122
  67. package/src/components/code-block/highlight.ts +278 -0
  68. package/src/components/code-block/index.tsx +341 -0
  69. package/src/components/date-picker/index.tsx +19 -1
  70. package/src/components/menu/index.tsx +812 -0
  71. package/src/components/plan/index.tsx +325 -0
  72. package/src/components/popover/index.tsx +69 -2
  73. package/src/components/radio-group/index.tsx +11 -2
  74. package/src/components/reasoning/index.tsx +297 -0
  75. package/src/components/sources/index.tsx +278 -0
  76. package/src/components/task/index.tsx +329 -0
  77. package/src/components/textarea/index.tsx +381 -0
  78. package/src/components/tooltip/index.tsx +203 -20
  79. package/src/icons/index.tsx +102 -0
  80. package/src/index.ts +71 -0
  81. package/src/primitives/collapse.tsx +128 -0
  82. package/theme.css +112 -0
@@ -17,24 +17,34 @@
17
17
  * between months. So the grid is six rows always, and the spare cells hold the
18
18
  * neighbouring months' days.
19
19
  *
20
- * ## Why a range is drawn in three pieces
20
+ * ## How a range is drawn
21
21
  *
22
22
  * On the web the band under a selected range is one background with the ends
23
23
  * rounded by `:first-child` and `:last-child`. There are no pseudo-classes
24
- * here, so each day works out for itself whether it is a start, a middle or an
25
- * end — and, separately, whether it is at the edge of its own row, because a
26
- * range that runs over a weekend has to close off on Saturday and open again
27
- * on Sunday rather than trailing into the gap.
24
+ * here, so each cell draws its own piece of the band as a view *behind* the
25
+ * number, and works out for itself where that piece has to stop.
28
26
  *
29
- * Every cell therefore draws its band as a view *behind* the number, sized and
30
- * rounded from those flags, rather than as a background on the cell itself.
27
+ * Two questions, and they are not the same one:
28
+ *
29
+ * - **How wide.** A day between the two ends fills its cell, so the pieces meet
30
+ * and the band reads as continuous. An end fills half its cell, from the
31
+ * centre outwards towards the middle of the range — the half that is not
32
+ * covered by the disc, so the disc appears to sit *on* the band rather than
33
+ * beside it. That is the join the old three-piece version never made, which
34
+ * is why a range used to read as two discs with a gap and a stripe.
35
+ * - **Where it is rounded.** Only where it genuinely stops: at an end of the
36
+ * range, or at the edge of a row, because a range running over a weekend has
37
+ * to close off on Saturday and open again on Sunday rather than trailing into
38
+ * the gap between the two.
31
39
  */
32
- import { createContext, useCallback, useContext, useMemo, useState } from 'react';
33
- import { Pressable, View } from 'react-native';
40
+ import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
41
+ import { Pressable, ScrollView, View } from 'react-native';
34
42
  import { tv } from 'tailwind-variants';
35
- import { ChevronLeftIcon, ChevronRightIcon } from "../../icons/index.js";
43
+ import { useCSSVariable } from 'uniwind';
44
+ import { CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "../../icons/index.js";
36
45
  import { Text } from "../../primitives/text.js";
37
46
  import { cn } from "../../utils/cn.js";
47
+ import { Popover } from "../popover/index.js";
38
48
  import { addCalendarMonths, addDays, calendarDayNumber, calendarLongDate, calendarMonthLabel, calendarMonthNames, calendarParts, isAfter, isBefore, isSameCalendarMonth, isSameDay, isWithin, resolveCalendar, startOfCalendarMonth, startOfDay, weekdayNames } from "../../utils/date.js";
39
49
 
40
50
  /** A range under construction: `to` is undefined between the two taps. */
@@ -49,15 +59,46 @@ import { addCalendarMonths, addDays, calendarDayNumber, calendarLongDate, calend
49
59
  * "no weekends" and "not in the past" are the two most common cases and
50
60
  * neither can be written as a list that stays right tomorrow.
51
61
  */
52
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
62
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
63
+ /**
64
+ * The corner radius of a day, and so of the band that runs between days.
65
+ *
66
+ * A number rather than a class because the band's four corners are set
67
+ * independently from the flags below, which no single `rounded-*` can express.
68
+ */
69
+ const DAY_RADIUS = 8;
70
+ const calendarVariants = tv({
71
+ slots: {
72
+ root: 'gap-3',
73
+ header: 'h-10 flex-row items-center justify-between gap-2',
74
+ nav: 'h-8 w-8 items-center justify-center rounded-full',
75
+ caption: 'flex-1 flex-row items-center justify-center gap-1'
76
+ },
77
+ variants: {
78
+ /** Draw the calendar's own panel, for one standing on the page. */
79
+ bordered: {
80
+ true: {
81
+ root: 'rounded-3xl border border-border bg-card p-3'
82
+ }
83
+ }
84
+ },
85
+ defaultVariants: {
86
+ bordered: true
87
+ }
88
+ });
53
89
  const dayVariants = tv({
54
90
  slots: {
55
- cell: 'flex-1 items-center justify-center',
91
+ cell: 'h-10 flex-1 items-center justify-center',
56
92
  /** The range band, drawn behind the number so it can run cell to cell. */
57
- band: 'absolute inset-y-1',
58
- /** The circle on a selected day, or the ring on today. */
59
- disc: 'h-9 w-9 items-center justify-center rounded-full',
60
- label: ''
93
+ band: 'absolute inset-y-0.5 bg-accent',
94
+ /**
95
+ * The fill on a selected day, or the ring on today. Square-ish rather than
96
+ * round: a circle has one point of contact with the band beside it, so a
97
+ * range reads as beads on a string. A rounded rectangle meets the band
98
+ * along its whole edge and the two become one shape.
99
+ */
100
+ disc: 'h-9 w-9 items-center justify-center rounded-lg',
101
+ label: 'text-sm'
61
102
  },
62
103
  variants: {
63
104
  selected: {
@@ -82,14 +123,15 @@ const dayVariants = tv({
82
123
  label: 'text-muted-foreground/30'
83
124
  }
84
125
  },
85
- inRange: {
126
+ /** A ruled-out day inside a range still sits on it, but faintly. */
127
+ fadedBand: {
86
128
  true: {
87
- band: 'bg-accent'
129
+ band: 'opacity-40'
88
130
  }
89
131
  }
90
132
  },
91
133
  compoundVariants: [
92
- // A selected day inside a range keeps the solid disc; the band passes
134
+ // A selected day inside a range keeps the solid fill; the band passes
93
135
  // under it rather than the disc losing to it.
94
136
  {
95
137
  selected: true,
@@ -110,8 +152,14 @@ function useCalendar(component) {
110
152
 
111
153
  /** Whether a rule, a list or a span rules this day out. */
112
154
  function isDisabled(date, disabled, minDate, maxDate) {
113
- if (minDate && isBefore(date, minDate)) return true;
114
- if (maxDate && isAfter(date, maxDate)) return true;
155
+ /*
156
+ * Both bounds are compared at local midnight, because they are about days
157
+ * and the caller almost always hands over a moment. `minDate={new Date()}`
158
+ * means "from today", and against a raw timestamp that would rule out every
159
+ * hour of today that has already passed — which is to say, today.
160
+ */
161
+ if (minDate && isBefore(date, startOfDay(minDate))) return true;
162
+ if (maxDate && isAfter(date, startOfDay(maxDate))) return true;
115
163
  if (!disabled) return false;
116
164
  if (typeof disabled === 'function') return disabled(date);
117
165
  if (Array.isArray(disabled)) return disabled.some(day => isSameDay(day, date));
@@ -131,8 +179,12 @@ function CalendarRoot({
131
179
  disabled,
132
180
  minDate,
133
181
  maxDate,
182
+ startMonth,
183
+ endMonth,
134
184
  weekStartsOn = 0,
135
185
  showOutsideDays = true,
186
+ selectOutsideDays = false,
187
+ bordered = true,
136
188
  locale,
137
189
  calendar = 'gregory',
138
190
  ...props
@@ -207,20 +259,28 @@ function CalendarRoot({
207
259
  system,
208
260
  minDate,
209
261
  maxDate,
262
+ startMonth,
263
+ endMonth,
210
264
  captionLayout
211
- }), [month, setMonth, locale, system, minDate, maxDate, captionLayout]);
265
+ }), [month, setMonth, locale, system, minDate, maxDate, startMonth, endMonth, captionLayout]);
266
+ const {
267
+ root
268
+ } = calendarVariants({
269
+ bordered
270
+ });
271
+ const months = Math.max(1, numberOfMonths);
212
272
  return /*#__PURE__*/_jsx(CalendarContext.Provider, {
213
273
  value: context,
214
274
  children: /*#__PURE__*/_jsx(View, {
215
275
  ...props,
216
- className: cn('gap-4', className),
276
+ className: cn(root(), className),
217
277
  children: Array.from({
218
- length: Math.max(1, numberOfMonths)
278
+ length: months
219
279
  }, (_unused, offset) => /*#__PURE__*/_jsxs(View, {
220
280
  className: "gap-2",
221
- children: [/*#__PURE__*/_jsx(CalendarCaption, {
281
+ children: [/*#__PURE__*/_jsx(CalendarHeader, {
222
282
  offset: offset,
223
- lastOffset: numberOfMonths - 1
283
+ lastOffset: months - 1
224
284
  }), /*#__PURE__*/_jsx(CalendarGrid, {
225
285
  month: addCalendarMonths(month, offset, system, locale),
226
286
  mode: mode,
@@ -231,6 +291,7 @@ function CalendarRoot({
231
291
  maxDate: maxDate,
232
292
  weekStartsOn: weekStartsOn,
233
293
  showOutsideDays: showOutsideDays,
294
+ selectOutsideDays: selectOutsideDays,
234
295
  locale: locale,
235
296
  system: system
236
297
  })]
@@ -245,15 +306,20 @@ CalendarRoot.displayName = 'Calendar';
245
306
  /* -------------------------------------------------------------------------- */
246
307
 
247
308
  /**
248
- * The month name and the arrows, or the month and year pickers.
309
+ * The row over a month: an arrow at each edge and the month's name between
310
+ * them.
249
311
  *
250
- * With several months shown the arrows are split across the row — back on the
251
- * first, forward on the last — because both sets on both captions would page
252
- * the whole run and give two ways to do one thing.
312
+ * The arrows are pinned to the edges of the calendar rather than packed around
313
+ * the label, so the two targets are as far apart as the panel allows and the
314
+ * name sits centred over its own grid. With several months shown they are split
315
+ * across the run — back on the first, forward on the last — because both sets
316
+ * on both headers would page the whole run and give two ways to do one thing.
317
+ * The month that owns neither still lays out a spacer, or its name would slide
318
+ * off centre.
253
319
  */
254
- function CalendarCaption({
255
- offset,
256
- lastOffset
320
+ function CalendarHeader({
321
+ offset = 0,
322
+ lastOffset = 0
257
323
  }) {
258
324
  const {
259
325
  month,
@@ -263,38 +329,56 @@ function CalendarCaption({
263
329
  minDate,
264
330
  maxDate,
265
331
  captionLayout
266
- } = useCalendar('Calendar.Caption');
332
+ } = useCalendar('Calendar.Header');
333
+ const {
334
+ header,
335
+ caption
336
+ } = calendarVariants();
267
337
  const shown = addCalendarMonths(month, offset, system, locale);
268
338
  const canGoBack = !minDate || isBefore(startOfCalendarMonth(minDate, system, locale), month);
269
339
  const canGoForward = !maxDate || isAfter(startOfCalendarMonth(maxDate, system, locale), shown);
270
340
  return /*#__PURE__*/_jsxs(View, {
271
- className: "h-9 flex-row items-center justify-between",
341
+ className: header(),
272
342
  children: [offset === 0 ? /*#__PURE__*/_jsx(CalendarNav, {
273
343
  direction: "previous",
274
344
  disabled: !canGoBack,
275
345
  onPress: () => setMonth(addCalendarMonths(month, -1, system, locale))
276
346
  }) : /*#__PURE__*/_jsx(View, {
277
- className: "h-7 w-7"
278
- }), captionLayout === 'dropdown' ? /*#__PURE__*/_jsx(CalendarDropdowns, {
279
- month: shown,
280
- offset: offset
281
- }) : /*#__PURE__*/_jsx(Text, {
282
- weight: "semibold",
283
- children: calendarMonthLabel(shown, system, locale)
347
+ className: "h-8 w-8"
348
+ }), /*#__PURE__*/_jsx(View, {
349
+ className: caption(),
350
+ children: captionLayout === 'dropdown' ? /*#__PURE__*/_jsx(CalendarDropdowns, {
351
+ month: shown,
352
+ offset: offset
353
+ }) : /*#__PURE__*/_jsx(Text, {
354
+ weight: "semibold",
355
+ className: "text-base",
356
+ children: calendarMonthLabel(shown, system, locale)
357
+ })
284
358
  }), offset === lastOffset ? /*#__PURE__*/_jsx(CalendarNav, {
285
359
  direction: "next",
286
360
  disabled: !canGoForward,
287
361
  onPress: () => setMonth(addCalendarMonths(month, 1, system, locale))
288
362
  }) : /*#__PURE__*/_jsx(View, {
289
- className: "h-7 w-7"
363
+ className: "h-8 w-8"
290
364
  })]
291
365
  });
292
366
  }
367
+ /**
368
+ * One paging arrow.
369
+ *
370
+ * Borderless: two bordered squares at the ends of a bordered panel read as a
371
+ * row of boxes rather than as controls, and the chevron is legible enough on
372
+ * its own. The target is still 32px with another 8 of slop around it.
373
+ */
293
374
  function CalendarNav({
294
375
  direction,
295
- disabled,
376
+ disabled = false,
296
377
  onPress
297
378
  }) {
379
+ const {
380
+ nav
381
+ } = calendarVariants();
298
382
  return /*#__PURE__*/_jsx(Pressable, {
299
383
  disabled: disabled,
300
384
  onPress: onPress,
@@ -304,21 +388,26 @@ function CalendarNav({
304
388
  accessibilityState: {
305
389
  disabled
306
390
  },
307
- className: cn('h-7 w-7 items-center justify-center rounded-md border border-border', disabled ? 'opacity-40' : 'active:bg-accent'),
391
+ className: cn(nav(), disabled ? 'opacity-30' : 'active:bg-accent'),
308
392
  children: direction === 'next' ? /*#__PURE__*/_jsx(ChevronRightIcon, {
309
- size: 16
393
+ size: 18
310
394
  }) : /*#__PURE__*/_jsx(ChevronLeftIcon, {
311
- size: 16
395
+ size: 18
312
396
  })
313
397
  });
314
398
  }
315
399
 
316
400
  /**
317
- * Month and year as two rows of chips rather than as native pickers.
401
+ * Month and year as chips that open a panel of options.
402
+ *
403
+ * The panel floats rather than opening in place, and the caption is the reason:
404
+ * it is a fixed-height row, so a list opened inside it has nowhere to go and
405
+ * spills over the grid below. A popover is laid out against the chip and
406
+ * clamped to the screen, which is the behaviour this needs and already has.
318
407
  *
319
- * A picker is a platform overlay, and putting one on top of a calendar that is
320
- * itself already inside a popover is two floating layers deep for choosing a
321
- * number. Chips stay on the surface the choice is being made on.
408
+ * Native pickers are still the wrong tool — a platform overlay on top of a
409
+ * calendar that is itself already in a popover is two floating layers deep for
410
+ * choosing a number, and it cannot show a Hijri year at all.
322
411
  */
323
412
  function CalendarDropdowns({
324
413
  month,
@@ -329,9 +418,10 @@ function CalendarDropdowns({
329
418
  locale,
330
419
  system,
331
420
  minDate,
332
- maxDate
421
+ maxDate,
422
+ startMonth,
423
+ endMonth
333
424
  } = useCalendar('Calendar.Caption');
334
- const [open, setOpen] = useState(null);
335
425
 
336
426
  /*
337
427
  * Everything here counts in the calendar on screen, not in the platform's.
@@ -341,10 +431,26 @@ function CalendarDropdowns({
341
431
  */
342
432
  const months = useMemo(() => calendarMonthNames(month, system, locale, 'short'), [month, system, locale]);
343
433
  const current = calendarParts(month, system, locale);
434
+ /*
435
+ * `startMonth`/`endMonth` bound the lists, falling back to the selectable
436
+ * bounds and then to a century back and a decade on. They are separate props
437
+ * because the two questions are different: a birthday picker wants a hundred
438
+ * years on offer and no bound at all on the days inside them.
439
+ */
440
+ const first = startMonth ?? minDate;
441
+ const last = endMonth ?? maxDate;
344
442
  const bounds = {
345
- from: minDate ? calendarParts(minDate, system, locale).year : current.year - 100,
346
- to: maxDate ? calendarParts(maxDate, system, locale).year : current.year + 10
443
+ from: first ? calendarParts(first, system, locale).year : current.year - 100,
444
+ to: last ? calendarParts(last, system, locale).year : current.year + 10
347
445
  };
446
+ /*
447
+ * Oldest first, the way a year runs. The list is long — a century by default
448
+ * — so it is opened scrolled to the year already in the caption rather than
449
+ * to either end, which is what makes the natural order affordable. Counting
450
+ * backwards was a way of putting the likely years at the top of a list that
451
+ * always opened at the top; it is not needed once the list opens in the right
452
+ * place, and it made every other use of the picker read upside down.
453
+ */
348
454
  const years = Array.from({
349
455
  length: Math.max(1, bounds.to - bounds.from + 1)
350
456
  }, (_unused, index) => bounds.from + index);
@@ -356,58 +462,154 @@ function CalendarDropdowns({
356
462
  */
357
463
  const choose = months_ => {
358
464
  setMonth(addCalendarMonths(month, months_ - offset, system, locale));
359
- setOpen(null);
360
465
  };
361
- return /*#__PURE__*/_jsxs(View, {
362
- className: "flex-1 items-center",
363
- children: [/*#__PURE__*/_jsxs(View, {
364
- className: "flex-row items-center gap-1",
365
- children: [/*#__PURE__*/_jsx(CaptionChip, {
366
- label: months[current.month - 1] ?? String(current.month),
367
- expanded: open === 'month',
368
- onPress: () => setOpen(open === 'month' ? null : 'month')
369
- }), /*#__PURE__*/_jsx(CaptionChip, {
370
- label: String(current.year),
371
- expanded: open === 'year',
372
- onPress: () => setOpen(open === 'year' ? null : 'year')
373
- })]
374
- }), open ? /*#__PURE__*/_jsx(View, {
375
- className: "mt-2 max-h-40 w-full flex-row flex-wrap justify-center gap-1 rounded-lg border border-border bg-popover p-2",
376
- children: (open === 'month' ? months : years.map(String)).map((option, index) => {
377
- const active = open === 'month' ? index === current.month - 1 : years[index] === current.year;
378
- return /*#__PURE__*/_jsx(Pressable, {
379
- onPress: () => choose(open === 'month' ? index - (current.month - 1) : (years[index] - current.year) * 12),
380
- accessibilityRole: "button",
381
- accessibilityState: {
382
- selected: active
383
- },
384
- className: cn('rounded-md px-2.5 py-1.5', active ? 'bg-primary' : 'active:bg-accent'),
385
- children: /*#__PURE__*/_jsx(Text, {
386
- size: "sm",
387
- className: active ? 'text-primary-foreground' : undefined,
388
- children: option
389
- })
390
- }, option);
466
+ return /*#__PURE__*/_jsxs(_Fragment, {
467
+ children: [/*#__PURE__*/_jsx(CaptionDropdown, {
468
+ label: months[current.month - 1] ?? String(current.month),
469
+ accessibilityLabel: "Month",
470
+ options: months,
471
+ active: current.month - 1,
472
+ width: MONTH_LIST_WIDTH,
473
+ onSelect: index => choose(index - (current.month - 1))
474
+ }), /*#__PURE__*/_jsx(CaptionDropdown, {
475
+ label: String(current.year),
476
+ accessibilityLabel: "Year",
477
+ options: years.map(String),
478
+ active: years.indexOf(current.year),
479
+ width: YEAR_LIST_WIDTH,
480
+ onSelect: index => choose((years[index] - current.year) * 12)
481
+ })]
482
+ });
483
+ }
484
+
485
+ /** Panel widths. A month name needs the room; a four-digit year does not. */
486
+ const MONTH_LIST_WIDTH = 176;
487
+ const YEAR_LIST_WIDTH = 132;
488
+
489
+ /**
490
+ * Height of one option row, and the tallest the list is allowed to be.
491
+ *
492
+ * The row height is a constant rather than a measurement because it is what
493
+ * turns "the twenty-ninth option" into a scroll offset, and that has to be
494
+ * known before the list has laid out — the whole point is that it opens in the
495
+ * right place rather than scrolling there afterwards.
496
+ */
497
+ const OPTION_HEIGHT = 40;
498
+ const LIST_MAX_HEIGHT = OPTION_HEIGHT * 6.5;
499
+
500
+ /**
501
+ * One chip and the list of options it opens.
502
+ *
503
+ * A list, not a grid of chips. Twelve months tile into a panel well enough, but
504
+ * the same treatment applied to a century of years is a wall of four-digit
505
+ * numbers with no order the eye can follow — and it opened at whichever end
506
+ * happened to be first. Rows in a column read as a list, take a checkmark, and
507
+ * can be scrolled to a particular one.
508
+ */
509
+ function CaptionDropdown({
510
+ label,
511
+ accessibilityLabel,
512
+ options,
513
+ active,
514
+ width,
515
+ onSelect
516
+ }) {
517
+ const [open, setOpen] = useState(false);
518
+ const list = useRef(null);
519
+ /*
520
+ * The check is given a colour rather than left to its own fallback, which is
521
+ * white — right inside a filled control, invisible on this panel.
522
+ */
523
+ const rawTick = useCSSVariable('--color-foreground');
524
+ const tick = typeof rawTick === 'string' ? rawTick : undefined;
525
+
526
+ /*
527
+ * Opened on the option already in the caption, centred in the panel rather
528
+ * than pinned to an edge so the years either side of it are in view too. A
529
+ * hundred-item list that opens at the top is showing the one year nobody came
530
+ * to pick, and asks for a flick through a century to reach the one they did.
531
+ */
532
+ const scrollToActive = useCallback(() => {
533
+ if (active < 0) return;
534
+ const centred = active * OPTION_HEIGHT - LIST_MAX_HEIGHT / 2 + OPTION_HEIGHT / 2;
535
+ list.current?.scrollTo({
536
+ y: Math.max(0, centred),
537
+ animated: false
538
+ });
539
+ }, [active]);
540
+ return /*#__PURE__*/_jsxs(Popover, {
541
+ open: open,
542
+ onOpenChange: setOpen,
543
+ children: [/*#__PURE__*/_jsx(Popover.Trigger, {
544
+ children: /*#__PURE__*/_jsx(CaptionChip, {
545
+ label: label,
546
+ expanded: open,
547
+ accessibilityLabel: accessibilityLabel
548
+ })
549
+ }), /*#__PURE__*/_jsx(Popover.Content, {
550
+ align: "center",
551
+ width: width,
552
+ className: "p-1.5",
553
+ children: /*#__PURE__*/_jsx(ScrollView, {
554
+ ref: list,
555
+ bounces: false,
556
+ showsVerticalScrollIndicator: false,
557
+ style: {
558
+ maxHeight: LIST_MAX_HEIGHT
559
+ }
560
+ // Fires once the rows exist, which is the first moment there is
561
+ // anything to scroll. Doing it on mount would scroll an empty list.
562
+ ,
563
+ onContentSizeChange: scrollToActive,
564
+ children: options.map((option, index) => {
565
+ const selected = index === active;
566
+ return /*#__PURE__*/_jsxs(Pressable, {
567
+ onPress: () => {
568
+ onSelect(index);
569
+ setOpen(false);
570
+ },
571
+ accessibilityRole: "button",
572
+ accessibilityState: {
573
+ selected
574
+ },
575
+ style: {
576
+ height: OPTION_HEIGHT
577
+ },
578
+ className: cn('flex-row items-center justify-between gap-2 rounded-xl px-3', selected ? 'bg-accent' : 'active:bg-accent'),
579
+ children: [/*#__PURE__*/_jsx(Text, {
580
+ size: "sm",
581
+ weight: selected ? 'semibold' : 'normal',
582
+ children: option
583
+ }), selected ? /*#__PURE__*/_jsx(CheckIcon, {
584
+ size: 16,
585
+ color: tick
586
+ }) : null]
587
+ }, option);
588
+ })
391
589
  })
392
- }) : null]
590
+ })]
393
591
  });
394
592
  }
395
593
  function CaptionChip({
396
594
  label,
397
595
  expanded,
596
+ accessibilityLabel,
398
597
  onPress
399
598
  }) {
400
- return /*#__PURE__*/_jsx(Pressable, {
599
+ return /*#__PURE__*/_jsxs(Pressable, {
401
600
  onPress: onPress,
402
601
  accessibilityRole: "button",
602
+ accessibilityLabel: accessibilityLabel,
403
603
  accessibilityState: {
404
604
  expanded
405
605
  },
406
- className: "flex-row items-center gap-1 rounded-md px-2 py-1 active:bg-accent",
407
- children: /*#__PURE__*/_jsx(Text, {
606
+ className: "flex-row items-center gap-1 rounded-lg px-2 py-1.5 active:bg-accent",
607
+ children: [/*#__PURE__*/_jsx(Text, {
408
608
  weight: "semibold",
409
609
  children: label
410
- })
610
+ }), /*#__PURE__*/_jsx(ChevronDownIcon, {
611
+ size: 14
612
+ })]
411
613
  });
412
614
  }
413
615
 
@@ -425,6 +627,7 @@ function CalendarGrid({
425
627
  maxDate,
426
628
  weekStartsOn,
427
629
  showOutsideDays,
630
+ selectOutsideDays = false,
428
631
  locale,
429
632
  system
430
633
  }) {
@@ -475,22 +678,41 @@ function CalendarGrid({
475
678
  className: "h-10 flex-1"
476
679
  }, dayIndex);
477
680
  }
478
- const selected = mode === 'range' ? isSameDay(date, range?.from) || isSameDay(date, range?.to) : mode === 'multiple' ? multiple.some(picked => isSameDay(picked, date)) : isSameDay(date, single);
681
+ const isStart = mode === 'range' && isSameDay(date, range?.from);
682
+ const isEnd = mode === 'range' && isSameDay(date, range?.to);
683
+ const selected = mode === 'range' ? isStart || isEnd : mode === 'multiple' ? multiple.some(picked => isSameDay(picked, date)) : isSameDay(date, single);
684
+
685
+ /*
686
+ * Inclusive of both ends, unlike the old middles-only rule. An end
687
+ * has to carry its half of the band or there is a gap between the
688
+ * disc and the run beside it — which is precisely what made a range
689
+ * read as two discs with a stripe floating between them.
690
+ */
691
+ const inRange = !!range?.to && isWithin(date, range.from, range.to);
692
+
693
+ /*
694
+ * Whether the band carries on past this cell's edge into the one
695
+ * beside it. It stops at an end of the range, and at the edge of a
696
+ * row — a range over a weekend closes on Saturday and opens again
697
+ * on Sunday rather than trailing into the gap between the rows.
698
+ */
699
+ const openStart = inRange && !isStart && dayIndex > 0;
700
+ const openEnd = inRange && !isEnd && dayIndex < 6;
479
701
 
480
- // Between the ends, but not an end itself — the band, not the disc.
481
- const inRange = !!range?.to && isWithin(date, range.from, range.to) && !selected;
702
+ // A neighbouring month's day is drawn, not offered. Reaching into
703
+ // July from June is what the arrows are for.
704
+ const ruledOut = isDisabled(date, disabled, minDate, maxDate) || outside && !selectOutsideDays;
482
705
  return /*#__PURE__*/_jsx(CalendarDay, {
483
706
  date: date,
484
707
  outside: outside,
485
708
  selected: selected,
486
709
  inRange: inRange,
710
+ rangeStart: isStart,
711
+ rangeEnd: isEnd,
487
712
  today: isSameDay(date, today),
488
- disabled: isDisabled(date, disabled, minDate, maxDate)
489
- // The band closes off at the edges of a row rather than
490
- // trailing into the gap between weeks.
491
- ,
492
- openStart: inRange && dayIndex > 0,
493
- openEnd: inRange && dayIndex < 6,
713
+ disabled: ruledOut,
714
+ openStart: openStart,
715
+ openEnd: openEnd,
494
716
  locale: locale,
495
717
  system: system,
496
718
  onPress: () => onSelect(date)
@@ -505,6 +727,8 @@ function CalendarDay({
505
727
  outside,
506
728
  selected,
507
729
  inRange,
730
+ rangeStart = false,
731
+ rangeEnd = false,
508
732
  today,
509
733
  disabled,
510
734
  openStart,
@@ -518,8 +742,25 @@ function CalendarDay({
518
742
  today: today && !selected,
519
743
  outside,
520
744
  disabled,
521
- inRange
745
+ fadedBand: disabled && inRange
522
746
  });
747
+
748
+ /*
749
+ * An end of the range fills half its cell, from the centre outwards towards
750
+ * the rest of the range; every other day in it fills the whole cell. Half a
751
+ * cell is what puts the band underneath the disc rather than beside it — the
752
+ * disc is centred, so a band starting at the centre emerges from behind it
753
+ * and the two read as one shape. A range of one day gets both halves and so
754
+ * no band at all, which is right: there is nothing to span.
755
+ */
756
+ const bandStyle = inRange ? {
757
+ left: rangeStart ? '50%' : 0,
758
+ right: rangeEnd ? '50%' : 0,
759
+ borderTopLeftRadius: openStart ? 0 : DAY_RADIUS,
760
+ borderBottomLeftRadius: openStart ? 0 : DAY_RADIUS,
761
+ borderTopRightRadius: openEnd ? 0 : DAY_RADIUS,
762
+ borderBottomRightRadius: openEnd ? 0 : DAY_RADIUS
763
+ } : null;
523
764
  return /*#__PURE__*/_jsxs(Pressable, {
524
765
  disabled: disabled,
525
766
  onPress: onPress,
@@ -529,28 +770,22 @@ function CalendarDay({
529
770
  selected,
530
771
  disabled
531
772
  },
532
- className: cn(styles.cell(), 'h-10'),
533
- children: [inRange ? /*#__PURE__*/_jsx(View, {
534
- className: cn(styles.band()),
535
- style: {
536
- left: openStart ? 0 : '10%',
537
- right: openEnd ? 0 : '10%',
538
- borderTopLeftRadius: openStart ? 0 : 999,
539
- borderBottomLeftRadius: openStart ? 0 : 999,
540
- borderTopRightRadius: openEnd ? 0 : 999,
541
- borderBottomRightRadius: openEnd ? 0 : 999
542
- }
773
+ className: styles.cell(),
774
+ children: [bandStyle ? /*#__PURE__*/_jsx(View, {
775
+ className: styles.band(),
776
+ style: bandStyle
543
777
  }) : null, /*#__PURE__*/_jsx(View, {
544
- className: cn(styles.disc()),
778
+ className: styles.disc(),
545
779
  children: /*#__PURE__*/_jsx(Text, {
546
- size: "sm",
547
- className: cn(styles.label()),
780
+ className: styles.label(),
548
781
  children: calendarDayNumber(date, system, locale)
549
782
  })
550
783
  })]
551
784
  });
552
785
  }
553
786
  export const Calendar = Object.assign(CalendarRoot, {
787
+ Header: CalendarHeader,
788
+ Nav: CalendarNav,
554
789
  Grid: CalendarGrid,
555
790
  Day: CalendarDay
556
791
  });