panelui-native 0.66.1 → 0.71.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 (196) hide show
  1. package/README.md +31 -1
  2. package/lib/module/components/accordion/index.js +28 -9
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/area-chart/index.js +18 -1
  5. package/lib/module/components/area-chart/index.js.map +1 -1
  6. package/lib/module/components/bar-chart/index.js +18 -1
  7. package/lib/module/components/bar-chart/index.js.map +1 -1
  8. package/lib/module/components/button/index.js +14 -7
  9. package/lib/module/components/button/index.js.map +1 -1
  10. package/lib/module/components/candlestick-chart/index.js +18 -1
  11. package/lib/module/components/candlestick-chart/index.js.map +1 -1
  12. package/lib/module/components/collapsible/index.js +280 -0
  13. package/lib/module/components/collapsible/index.js.map +1 -0
  14. package/lib/module/components/flow/index.js +66 -3
  15. package/lib/module/components/flow/index.js.map +1 -1
  16. package/lib/module/components/form/index.js +7 -5
  17. package/lib/module/components/form/index.js.map +1 -1
  18. package/lib/module/components/form/typed-form.js +11 -0
  19. package/lib/module/components/form/typed-form.js.map +1 -0
  20. package/lib/module/components/form/use-form.js +4 -0
  21. package/lib/module/components/form/use-form.js.map +1 -1
  22. package/lib/module/components/heatmap-chart/index.js +27 -1
  23. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  24. package/lib/module/components/line-chart/index.js +18 -1
  25. package/lib/module/components/line-chart/index.js.map +1 -1
  26. package/lib/module/components/live-line-chart/index.js +103 -9
  27. package/lib/module/components/live-line-chart/index.js.map +1 -1
  28. package/lib/module/components/live-line-chart/live-line-accessibility.js +47 -0
  29. package/lib/module/components/live-line-chart/live-line-accessibility.js.map +1 -0
  30. package/lib/module/components/live-line-chart/live-line-lifecycle.js +33 -0
  31. package/lib/module/components/live-line-chart/live-line-lifecycle.js.map +1 -0
  32. package/lib/module/components/marquee/index.js +194 -0
  33. package/lib/module/components/marquee/index.js.map +1 -0
  34. package/lib/module/components/message-scroller/index.js +172 -10
  35. package/lib/module/components/message-scroller/index.js.map +1 -1
  36. package/lib/module/components/message-scroller/message-scroller-math.js +23 -0
  37. package/lib/module/components/message-scroller/message-scroller-math.js.map +1 -1
  38. package/lib/module/components/meter/index.js +268 -0
  39. package/lib/module/components/meter/index.js.map +1 -0
  40. package/lib/module/components/meter/meter-scale.js +142 -0
  41. package/lib/module/components/meter/meter-scale.js.map +1 -0
  42. package/lib/module/components/planner/index.js +788 -0
  43. package/lib/module/components/planner/index.js.map +1 -0
  44. package/lib/module/components/planner/planner-announcement.js +30 -0
  45. package/lib/module/components/planner/planner-announcement.js.map +1 -0
  46. package/lib/module/components/planner/planner-entries.js +132 -0
  47. package/lib/module/components/planner/planner-entries.js.map +1 -0
  48. package/lib/module/components/planner/planner-grid-navigation.js +33 -0
  49. package/lib/module/components/planner/planner-grid-navigation.js.map +1 -0
  50. package/lib/module/components/planner/planner-lifecycle.js +45 -0
  51. package/lib/module/components/planner/planner-lifecycle.js.map +1 -0
  52. package/lib/module/components/popover/index.js +2 -2
  53. package/lib/module/components/popover/index.js.map +1 -1
  54. package/lib/module/components/radar-chart/index.js +18 -1
  55. package/lib/module/components/radar-chart/index.js.map +1 -1
  56. package/lib/module/components/scatter-chart/index.js +18 -1
  57. package/lib/module/components/scatter-chart/index.js.map +1 -1
  58. package/lib/module/components/skeleton/index.js +45 -7
  59. package/lib/module/components/skeleton/index.js.map +1 -1
  60. package/lib/module/components/spinner/index.js +63 -14
  61. package/lib/module/components/spinner/index.js.map +1 -1
  62. package/lib/module/components/time-picker/index.js +46 -21
  63. package/lib/module/components/time-picker/index.js.map +1 -1
  64. package/lib/module/components/time-picker/ruler-window.js +25 -0
  65. package/lib/module/components/time-picker/ruler-window.js.map +1 -0
  66. package/lib/module/hooks/breakpoint-contract.js +29 -0
  67. package/lib/module/hooks/breakpoint-contract.js.map +1 -0
  68. package/lib/module/hooks/index.js +1 -1
  69. package/lib/module/hooks/index.js.map +1 -1
  70. package/lib/module/hooks/use-breakpoint.js +46 -1
  71. package/lib/module/hooks/use-breakpoint.js.map +1 -1
  72. package/lib/module/index.js +5 -1
  73. package/lib/module/index.js.map +1 -1
  74. package/lib/module/primitives/chart-accessibility.js +68 -0
  75. package/lib/module/primitives/chart-accessibility.js.map +1 -0
  76. package/lib/module/primitives/focus-restoration-store.js +52 -0
  77. package/lib/module/primitives/focus-restoration-store.js.map +1 -0
  78. package/lib/module/primitives/portal.js +36 -2
  79. package/lib/module/primitives/portal.js.map +1 -1
  80. package/lib/typescript/src/components/accordion/index.d.ts +4 -0
  81. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  82. package/lib/typescript/src/components/area-chart/index.d.ts +2 -1
  83. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  84. package/lib/typescript/src/components/bar-chart/index.d.ts +2 -1
  85. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  87. package/lib/typescript/src/components/candlestick-chart/index.d.ts +2 -1
  88. package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
  89. package/lib/typescript/src/components/collapsible/index.d.ts +76 -0
  90. package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -0
  91. package/lib/typescript/src/components/flow/index.d.ts +7 -1
  92. package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/form/index.d.ts +18 -5
  94. package/lib/typescript/src/components/form/index.d.ts.map +1 -1
  95. package/lib/typescript/src/components/form/typed-form.d.ts +7 -0
  96. package/lib/typescript/src/components/form/typed-form.d.ts.map +1 -0
  97. package/lib/typescript/src/components/form/use-form.d.ts +4 -0
  98. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
  99. package/lib/typescript/src/components/heatmap-chart/index.d.ts +2 -1
  100. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  101. package/lib/typescript/src/components/line-chart/index.d.ts +2 -1
  102. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  103. package/lib/typescript/src/components/live-line-chart/index.d.ts +21 -7
  104. package/lib/typescript/src/components/live-line-chart/index.d.ts.map +1 -1
  105. package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts +22 -0
  106. package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts.map +1 -0
  107. package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts +23 -0
  108. package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts.map +1 -0
  109. package/lib/typescript/src/components/marquee/index.d.ts +73 -0
  110. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -0
  111. package/lib/typescript/src/components/message-scroller/index.d.ts +30 -2
  112. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
  113. package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts +10 -0
  114. package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts.map +1 -1
  115. package/lib/typescript/src/components/meter/index.d.ts +273 -0
  116. package/lib/typescript/src/components/meter/index.d.ts.map +1 -0
  117. package/lib/typescript/src/components/meter/meter-scale.d.ts +96 -0
  118. package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -0
  119. package/lib/typescript/src/components/planner/index.d.ts +274 -0
  120. package/lib/typescript/src/components/planner/index.d.ts.map +1 -0
  121. package/lib/typescript/src/components/planner/planner-announcement.d.ts +15 -0
  122. package/lib/typescript/src/components/planner/planner-announcement.d.ts.map +1 -0
  123. package/lib/typescript/src/components/planner/planner-entries.d.ts +75 -0
  124. package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -0
  125. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts +4 -0
  126. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts.map +1 -0
  127. package/lib/typescript/src/components/planner/planner-lifecycle.d.ts +20 -0
  128. package/lib/typescript/src/components/planner/planner-lifecycle.d.ts.map +1 -0
  129. package/lib/typescript/src/components/radar-chart/index.d.ts +4 -0
  130. package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
  131. package/lib/typescript/src/components/scatter-chart/index.d.ts +2 -1
  132. package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -1
  133. package/lib/typescript/src/components/skeleton/index.d.ts +25 -2
  134. package/lib/typescript/src/components/skeleton/index.d.ts.map +1 -1
  135. package/lib/typescript/src/components/spinner/index.d.ts +25 -2
  136. package/lib/typescript/src/components/spinner/index.d.ts.map +1 -1
  137. package/lib/typescript/src/components/time-picker/index.d.ts +5 -6
  138. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  139. package/lib/typescript/src/components/time-picker/ruler-window.d.ts +12 -0
  140. package/lib/typescript/src/components/time-picker/ruler-window.d.ts.map +1 -0
  141. package/lib/typescript/src/hooks/breakpoint-contract.d.ts +6 -0
  142. package/lib/typescript/src/hooks/breakpoint-contract.d.ts.map +1 -0
  143. package/lib/typescript/src/hooks/index.d.ts +1 -1
  144. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  145. package/lib/typescript/src/hooks/use-breakpoint.d.ts +22 -0
  146. package/lib/typescript/src/hooks/use-breakpoint.d.ts.map +1 -1
  147. package/lib/typescript/src/index.d.ts +5 -1
  148. package/lib/typescript/src/index.d.ts.map +1 -1
  149. package/lib/typescript/src/primitives/chart-accessibility.d.ts +33 -0
  150. package/lib/typescript/src/primitives/chart-accessibility.d.ts.map +1 -0
  151. package/lib/typescript/src/primitives/focus-restoration-store.d.ts +24 -0
  152. package/lib/typescript/src/primitives/focus-restoration-store.d.ts.map +1 -0
  153. package/lib/typescript/src/primitives/portal.d.ts +4 -0
  154. package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
  155. package/lib/typescript/types/form-api.type-test.d.ts +2 -0
  156. package/lib/typescript/types/form-api.type-test.d.ts.map +1 -0
  157. package/package.json +63 -4
  158. package/src/components/accordion/index.tsx +31 -8
  159. package/src/components/area-chart/index.tsx +27 -2
  160. package/src/components/bar-chart/index.tsx +27 -2
  161. package/src/components/button/index.tsx +11 -4
  162. package/src/components/candlestick-chart/index.tsx +30 -2
  163. package/src/components/collapsible/index.tsx +278 -0
  164. package/src/components/flow/index.tsx +93 -14
  165. package/src/components/form/index.tsx +32 -5
  166. package/src/components/form/typed-form.ts +13 -0
  167. package/src/components/form/use-form.ts +9 -0
  168. package/src/components/heatmap-chart/index.tsx +49 -3
  169. package/src/components/line-chart/index.tsx +27 -2
  170. package/src/components/live-line-chart/index.tsx +130 -13
  171. package/src/components/live-line-chart/live-line-accessibility.ts +61 -0
  172. package/src/components/live-line-chart/live-line-lifecycle.ts +63 -0
  173. package/src/components/marquee/index.tsx +232 -0
  174. package/src/components/message-scroller/index.tsx +234 -6
  175. package/src/components/message-scroller/message-scroller-math.ts +38 -0
  176. package/src/components/meter/index.tsx +341 -0
  177. package/src/components/meter/meter-scale.ts +185 -0
  178. package/src/components/planner/index.tsx +997 -0
  179. package/src/components/planner/planner-announcement.ts +37 -0
  180. package/src/components/planner/planner-entries.ts +155 -0
  181. package/src/components/planner/planner-grid-navigation.ts +45 -0
  182. package/src/components/planner/planner-lifecycle.ts +76 -0
  183. package/src/components/popover/index.tsx +3 -3
  184. package/src/components/radar-chart/index.tsx +24 -0
  185. package/src/components/scatter-chart/index.tsx +27 -2
  186. package/src/components/skeleton/index.tsx +59 -5
  187. package/src/components/spinner/index.tsx +80 -13
  188. package/src/components/time-picker/index.tsx +57 -29
  189. package/src/components/time-picker/ruler-window.ts +24 -0
  190. package/src/hooks/breakpoint-contract.ts +31 -0
  191. package/src/hooks/index.ts +4 -0
  192. package/src/hooks/use-breakpoint.ts +61 -10
  193. package/src/index.ts +43 -0
  194. package/src/primitives/chart-accessibility.ts +102 -0
  195. package/src/primitives/focus-restoration-store.ts +70 -0
  196. package/src/primitives/portal.tsx +34 -2
@@ -0,0 +1,788 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Planner — a month of days, each carrying what falls on it.
5
+ *
6
+ * ```tsx
7
+ * const [month, setMonth] = useState(new Date());
8
+ *
9
+ * <Planner
10
+ * month={month}
11
+ * onMonthChange={setMonth}
12
+ * entries={renewals}
13
+ * categories={[
14
+ * { id: 'monthly', label: 'Monthly' },
15
+ * { id: 'yearly', label: 'Yearly', colorIndex: 4 },
16
+ * ]}
17
+ * >
18
+ * <Planner.Header>
19
+ * <Planner.Title />
20
+ * <Planner.Today />
21
+ * <Planner.Nav />
22
+ * </Planner.Header>
23
+ * <Planner.Grid />
24
+ * <Planner.Legend />
25
+ * <Planner.Details>
26
+ * {(date, entries) => <Text>{entries.length} on {date.toDateString()}</Text>}
27
+ * </Planner.Details>
28
+ * </Planner>
29
+ * ```
30
+ *
31
+ * ## How it differs from Calendar
32
+ *
33
+ * `Calendar` picks a date and answers with one. This shows what is already on
34
+ * the days and answers with the day you asked about — the selection exists to
35
+ * open something, not to be submitted.
36
+ *
37
+ * ## Why it draws its own Frame
38
+ *
39
+ * A month at a glance is a widget: a boundary, a strip along the top carrying
40
+ * the month and the way through it, and a footer that holds still while the
41
+ * middle changes. That is `Frame`, so the root renders one instead of leaving
42
+ * every caller to assemble the same shell. Pass `frame={false}` to drop it,
43
+ * for a planner in a sheet or a card that already draws its own edge.
44
+ *
45
+ * ## Why the grid is always six weeks
46
+ *
47
+ * A month can span five weeks or six. Drawn at its natural height the panel
48
+ * jumps as you page through the year and the days appear to move under your
49
+ * thumb, so the grid is always six rows and the last one is sometimes all
50
+ * next month. `Calendar` fixes its height for the same reason.
51
+ *
52
+ * ## Why a day says more than its date
53
+ *
54
+ * The marker on a day is a coloured dot, and colour is a signal that does not
55
+ * reach everyone looking at it. So the legend prints its label beside every
56
+ * swatch, and a day is spoken as its date, how many entries it carries and
57
+ * which categories they belong to. Neither is decoration: between them they
58
+ * are the whole content of the grid for somebody who cannot see it.
59
+ */
60
+ import { Children, createContext, forwardRef, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
61
+ import { AccessibilityInfo, AppState, Platform, Pressable, View } from 'react-native';
62
+ import { tv } from 'tailwind-variants';
63
+ import { useCSSVariable } from 'uniwind';
64
+ import { ChevronLeftIcon, ChevronRightIcon } from "../../icons/index.js";
65
+ import { Text } from "../../primitives/text.js";
66
+ import { cn } from "../../utils/cn.js";
67
+ import { addCalendarMonths, calendarDayNumber, calendarLongDate, calendarMonthLabel, isSameCalendarMonth, isSameDay, localeWeekStart, monthGrid, normalizeWeekStart, resolveCalendar, startOfCalendarMonth, startOfDay, weekdayNames } from "../../utils/date.js";
68
+ import { Dialog } from "../dialog/index.js";
69
+ import { Frame } from "../frame/index.js";
70
+ import { bucketByDay, dayAccessibilityLabel, entriesOn, summariseMonth, visibleEntries } from "./planner-entries.js";
71
+ import { usePlannerMonthAnnouncement } from "./planner-announcement.js";
72
+ import { plannerGridTarget } from "./planner-grid-navigation.js";
73
+ import { usePlannerMonthLifecycle, usePlannerSelectionLifecycle } from "./planner-lifecycle.js";
74
+
75
+ /** How many of a day's entries a cell draws before it stops and counts. */
76
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
77
+ const DEFAULT_ENTRY_LIMIT = 2;
78
+
79
+ /** The palette a category takes its dot from when it does not name a colour. */
80
+ const PALETTE_SIZE = 5;
81
+
82
+ /** Stable wrapper keeps the native method's receiver and the hook dependency steady. */
83
+ const announceMonth = label => {
84
+ AccessibilityInfo.announceForAccessibility(label);
85
+ };
86
+ const plannerVariants = tv({
87
+ slots: {
88
+ grid: 'gap-1 px-3 pb-3 pt-1',
89
+ week: 'flex-row',
90
+ heading: 'flex-1 text-center',
91
+ legend: 'flex-row flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3',
92
+ swatch: 'h-2 w-2 rounded-full'
93
+ }
94
+ });
95
+ const dayVariants = tv({
96
+ slots: {
97
+ /*
98
+ * A fixed height, and a square-ish tile rather than a circle.
99
+ *
100
+ * Fixed because the cell has to be the same size whether the day carries
101
+ * anything or not — sized to its contents, a row with one icon in it is
102
+ * taller than the five around it and the grid stops being a grid.
103
+ *
104
+ * Square-ish because a day here holds a number, a marker and sometimes an
105
+ * icon, and a circle wastes the corners it needs for them.
106
+ */
107
+ cell: 'mx-0.5 h-14 flex-1 rounded-xl px-1.5 pt-1.5',
108
+ number: 'text-xs leading-none',
109
+ marker: 'absolute right-1.5 top-1.5 h-1.5 w-1.5 rounded-full',
110
+ body: 'flex-1 flex-row items-center justify-center gap-0.5 pb-1'
111
+ },
112
+ variants: {
113
+ /** Inside the month being shown, as opposed to the days either side. */
114
+ inMonth: {
115
+ true: {
116
+ cell: 'bg-muted/40',
117
+ number: 'text-foreground'
118
+ },
119
+ false: {
120
+ cell: 'bg-muted/15',
121
+ number: 'text-muted-foreground/40'
122
+ }
123
+ },
124
+ /*
125
+ * Today is the number, not the tile. A ring on today and a ring on the
126
+ * open day are two rings that mean different things and look the same.
127
+ */
128
+ today: {
129
+ true: {
130
+ number: 'text-primary font-semibold'
131
+ }
132
+ },
133
+ selected: {
134
+ true: {
135
+ cell: 'border border-primary/60 bg-primary/10'
136
+ }
137
+ },
138
+ disabled: {
139
+ true: {
140
+ cell: 'opacity-40'
141
+ }
142
+ }
143
+ },
144
+ defaultVariants: {
145
+ inMonth: true
146
+ }
147
+ });
148
+
149
+ /* ------------------------------------------------------------------ *
150
+ * Data
151
+ * ------------------------------------------------------------------ */
152
+
153
+ /** One thing that falls on a day. */
154
+
155
+ /** A group of entries: the key to a colour, and a line in the legend. */
156
+
157
+ const PlannerContext = /*#__PURE__*/createContext(null);
158
+ function usePlanner(component) {
159
+ const context = useContext(PlannerContext);
160
+ if (!context) {
161
+ throw new Error(`${component} must be used within a <Planner>`);
162
+ }
163
+ return context;
164
+ }
165
+
166
+ /** Today, recomputed when the app comes back rather than only at mount. */
167
+ function useToday() {
168
+ const [today, setToday] = useState(() => startOfDay(new Date()));
169
+ useEffect(() => {
170
+ const refresh = () => {
171
+ const now = startOfDay(new Date());
172
+ // A new object every resume would invalidate every memo below it for a
173
+ // date that has not changed.
174
+ setToday(current => current.getTime() === now.getTime() ? current : now);
175
+ };
176
+ const subscription = AppState.addEventListener('change', state => {
177
+ if (state === 'active') refresh();
178
+ });
179
+ return () => subscription.remove();
180
+ }, []);
181
+ return today;
182
+ }
183
+
184
+ /**
185
+ * The five series colours, read once.
186
+ *
187
+ * A fixed number of reads rather than one per category: a hook cannot run in a
188
+ * loop whose length is a prop.
189
+ */
190
+ function usePalette() {
191
+ const one = useCSSVariable('--color-chart-1');
192
+ const two = useCSSVariable('--color-chart-2');
193
+ const three = useCSSVariable('--color-chart-3');
194
+ const four = useCSSVariable('--color-chart-4');
195
+ const five = useCSSVariable('--color-chart-5');
196
+ return useMemo(() => [one, two, three, four, five].map(v => typeof v === 'string' ? v : undefined), [one, two, three, four, five]);
197
+ }
198
+
199
+ /* ------------------------------------------------------------------ *
200
+ * Root
201
+ * ------------------------------------------------------------------ */
202
+
203
+ const PlannerRoot = /*#__PURE__*/forwardRef(({
204
+ className,
205
+ month: monthProp,
206
+ defaultMonth,
207
+ onMonthChange,
208
+ entries = [],
209
+ categories = [],
210
+ selected: selectedProp,
211
+ defaultSelected = null,
212
+ onSelectedChange,
213
+ onDayPress,
214
+ entryLimit = DEFAULT_ENTRY_LIMIT,
215
+ weekStartsOn = 'auto',
216
+ locale,
217
+ calendar = 'gregory',
218
+ frame = true,
219
+ children,
220
+ ...props
221
+ }, ref) => {
222
+ const system = resolveCalendar(calendar, locale);
223
+ const palette = usePalette();
224
+ const today = useToday();
225
+ const settleMonth = useCallback(date => startOfCalendarMonth(date, system, locale), [system, locale]);
226
+ const [month, requestMonth] = usePlannerMonthLifecycle({
227
+ month: monthProp,
228
+ defaultMonth,
229
+ settleMonth,
230
+ onMonthChange
231
+ });
232
+ /*
233
+ * The announcement is registered against the month a press asks for, and
234
+ * spoken only once a commit arrives carrying it. That is what keeps a
235
+ * controlled parent's rejection silent, so it wraps the lifecycle's setter
236
+ * rather than living inside it — the hook owns which month is current, and
237
+ * this owns whether the change was the user's to hear about.
238
+ */
239
+ const expectMonthAnnouncement = usePlannerMonthAnnouncement({
240
+ monthKey: month.getTime(),
241
+ monthLabel: calendarMonthLabel(month, system, locale),
242
+ announce: announceMonth
243
+ });
244
+ const setMonth = useCallback(next => {
245
+ expectMonthAnnouncement(settleMonth(next));
246
+ requestMonth(next);
247
+ }, [expectMonthAnnouncement, requestMonth, settleMonth]);
248
+ const [selected, select] = usePlannerSelectionLifecycle({
249
+ selected: selectedProp,
250
+ defaultSelected,
251
+ onSelectedChange
252
+ });
253
+ const days = useMemo(() => bucketByDay(entries), [entries]);
254
+ const normalizedWeekStart = weekStartsOn === 'auto' ? localeWeekStart(locale) : normalizeWeekStart(weekStartsOn);
255
+ const grid = useMemo(() => monthGrid(month, normalizedWeekStart, system, locale), [month, normalizedWeekStart, system, locale]);
256
+ const isInMonth = useCallback(date => isSameCalendarMonth(date, month, system, locale), [month, system, locale]);
257
+ const categoryLabels = useMemo(() => new Map(categories.map(category => [category.id, category.label])), [categories]);
258
+ const categoryColors = useMemo(() => {
259
+ const colors = new Map();
260
+ categories.forEach((category, index) => {
261
+ // `findIndex` used to make the first duplicate id authoritative.
262
+ if (colors.has(category.id)) return;
263
+ const slot = (category.colorIndex ?? index + 1) - 1;
264
+ colors.set(category.id, category.color ?? palette[(slot % PALETTE_SIZE + PALETTE_SIZE) % PALETTE_SIZE]);
265
+ });
266
+ return colors;
267
+ }, [categories, palette]);
268
+ const colorOf = useCallback(id => id ? categoryColors.get(id) : undefined, [categoryColors]);
269
+ const summary = useMemo(() => summariseMonth(days, grid, categories, isInMonth), [days, grid, categories, isInMonth]);
270
+ const context = useMemo(() => ({
271
+ month,
272
+ setMonth,
273
+ today,
274
+ selected,
275
+ select,
276
+ days,
277
+ categories,
278
+ categoryLabels,
279
+ colorOf,
280
+ summary,
281
+ entryLimit,
282
+ weekStartsOn: normalizedWeekStart,
283
+ grid,
284
+ locale,
285
+ system,
286
+ isInMonth,
287
+ onDayPress
288
+ }), [month, setMonth, today, selected, select, days, categories, categoryLabels, colorOf, summary, entryLimit, normalizedWeekStart, grid, locale, system, isInMonth, onDayPress]);
289
+
290
+ /*
291
+ * The header belongs in the Frame's strip and everything else in its
292
+ * panel, so the root sorts its children rather than asking the caller to
293
+ * nest them in two places to get one widget.
294
+ */
295
+ const parts = Children.toArray(children);
296
+ const header = parts.find(child => /*#__PURE__*/isValidElement(child) && child.type === PlannerHeader);
297
+ const body = parts.filter(child => child !== header);
298
+ return /*#__PURE__*/_jsx(PlannerContext.Provider, {
299
+ value: context,
300
+ children: frame ?
301
+ /*#__PURE__*/
302
+ // Full width by default: the grid is seven equal columns, and a
303
+ // container that sizes to its content collapses them to the width of
304
+ // a two-digit number.
305
+ _jsxs(Frame, {
306
+ ref: ref,
307
+ className: cn('w-full', className),
308
+ ...props,
309
+ children: [header, /*#__PURE__*/_jsx(Frame.Panel, {
310
+ dividers: false,
311
+ children: body
312
+ })]
313
+ }) : /*#__PURE__*/_jsxs(View, {
314
+ ref: ref,
315
+ className: cn('w-full', className),
316
+ ...props,
317
+ children: [header, body]
318
+ })
319
+ });
320
+ });
321
+ PlannerRoot.displayName = 'Planner';
322
+
323
+ /* ------------------------------------------------------------------ *
324
+ * Header strip
325
+ * ------------------------------------------------------------------ */
326
+
327
+ /**
328
+ * The strip along the top of the frame. Put `Title`, `Today` and `Nav` in it.
329
+ *
330
+ * The strip has two ends rather than an even spread. What the month *is* —
331
+ * its name, and the way back to today — reads from the leading edge; what
332
+ * *moves* it sits at the trailing edge, under the thumb that reaches for it.
333
+ * Spaced evenly across a full-width strip they read as three unrelated
334
+ * controls instead of a label and a pair of buttons.
335
+ */
336
+ function PlannerHeader({
337
+ children
338
+ }) {
339
+ const parts = Children.toArray(children);
340
+ const trails = child => /*#__PURE__*/isValidElement(child) && (child.type === PlannerNav || child.type === PlannerAction);
341
+ const trailing = parts.filter(trails);
342
+ const lead = parts.filter(child => !trails(child));
343
+ return /*#__PURE__*/_jsxs(Frame.Header, {
344
+ children: [/*#__PURE__*/_jsx(View, {
345
+ className: "min-w-0 flex-1 flex-row items-center gap-2",
346
+ children: lead
347
+ }), trailing.length > 0 ? /*#__PURE__*/_jsx(View, {
348
+ className: "shrink-0 flex-row items-center gap-1.5",
349
+ children: trailing
350
+ }) : null]
351
+ });
352
+ }
353
+ PlannerHeader.displayName = 'Planner.Header';
354
+ /** The month on show, in the calendar system and locale the grid uses. */
355
+ function PlannerTitle({
356
+ children,
357
+ className,
358
+ ...props
359
+ }) {
360
+ const {
361
+ month,
362
+ system,
363
+ locale
364
+ } = usePlanner('Planner.Title');
365
+ return /*#__PURE__*/_jsx(Text, {
366
+ weight: "medium",
367
+ className: cn('text-base', className),
368
+ ...props,
369
+ children: children ?? calendarMonthLabel(month, system, locale)
370
+ });
371
+ }
372
+ PlannerTitle.displayName = 'Planner.Title';
373
+ /** Jumps back to the month today is in, and selects nothing. */
374
+ function PlannerToday({
375
+ children = 'Today'
376
+ }) {
377
+ const {
378
+ today,
379
+ setMonth,
380
+ month,
381
+ system,
382
+ locale
383
+ } = usePlanner('Planner.Today');
384
+ const alreadyHere = isSameCalendarMonth(today, month, system, locale);
385
+ return /*#__PURE__*/_jsx(Pressable, {
386
+ onPress: () => setMonth(today),
387
+ disabled: alreadyHere,
388
+ hitSlop: 6,
389
+ accessibilityRole: "button",
390
+ accessibilityLabel: "Go to this month",
391
+ accessibilityState: {
392
+ disabled: alreadyHere
393
+ },
394
+ className: cn('rounded-full border border-border px-3 py-1', alreadyHere ? 'opacity-40' : 'active:bg-accent'),
395
+ children: /*#__PURE__*/_jsx(Text, {
396
+ size: "xs",
397
+ muted: true,
398
+ children: children
399
+ })
400
+ });
401
+ }
402
+ PlannerToday.displayName = 'Planner.Today';
403
+ /** Back and forward a month. */
404
+ function PlannerNav({
405
+ className
406
+ }) {
407
+ const {
408
+ month,
409
+ setMonth,
410
+ system,
411
+ locale
412
+ } = usePlanner('Planner.Nav');
413
+ return /*#__PURE__*/_jsxs(View, {
414
+ className: cn('flex-row items-center gap-1', className),
415
+ children: [/*#__PURE__*/_jsx(PlannerArrow, {
416
+ direction: "previous",
417
+ onPress: () => setMonth(addCalendarMonths(month, -1, system, locale))
418
+ }), /*#__PURE__*/_jsx(PlannerArrow, {
419
+ direction: "next",
420
+ onPress: () => setMonth(addCalendarMonths(month, 1, system, locale))
421
+ })]
422
+ });
423
+ }
424
+ PlannerNav.displayName = 'Planner.Nav';
425
+ function PlannerArrow({
426
+ direction,
427
+ onPress
428
+ }) {
429
+ return /*#__PURE__*/_jsx(Pressable, {
430
+ onPress: onPress,
431
+ hitSlop: 8,
432
+ accessibilityRole: "button",
433
+ accessibilityLabel: direction === 'next' ? 'Next month' : 'Previous month',
434
+ className: "h-8 w-8 items-center justify-center rounded-lg active:bg-accent",
435
+ children: direction === 'next' ? /*#__PURE__*/_jsx(ChevronRightIcon, {
436
+ size: 18
437
+ }) : /*#__PURE__*/_jsx(ChevronLeftIcon, {
438
+ size: 18
439
+ })
440
+ });
441
+ }
442
+ /** The trailing end of the header strip, for a button of the caller's. */
443
+ function PlannerAction({
444
+ children
445
+ }) {
446
+ return /*#__PURE__*/_jsx(Frame.Action, {
447
+ children: children
448
+ });
449
+ }
450
+ PlannerAction.displayName = 'Planner.Action';
451
+
452
+ /* ------------------------------------------------------------------ *
453
+ * Grid
454
+ * ------------------------------------------------------------------ */
455
+
456
+ /** What a custom cell is handed. Everything the default cell draws from. */
457
+
458
+ const PlannerGridNavigationContext = /*#__PURE__*/createContext(null);
459
+
460
+ /**
461
+ * The weekday row and the six weeks below it.
462
+ *
463
+ * React Native has no per-cell grid vocabulary — no `gridcell`, no `row` — so
464
+ * a screen reader never hears "row three, column five" and cannot fall back on
465
+ * position for context. Every day therefore carries its own full date, and the
466
+ * weekday headings are hidden rather than read out 42 times over.
467
+ */
468
+ function PlannerGrid({
469
+ className,
470
+ renderDay
471
+ }) {
472
+ const {
473
+ grid: weeks,
474
+ weekStartsOn,
475
+ locale,
476
+ selected,
477
+ today,
478
+ isInMonth
479
+ } = usePlanner('Planner.Grid');
480
+ const {
481
+ grid,
482
+ week: weekRow,
483
+ heading
484
+ } = plannerVariants();
485
+ const headings = useMemo(() => weekdayNames(locale, weekStartsOn), [locale, weekStartsOn]);
486
+ const dates = useMemo(() => weeks.flat(), [weeks]);
487
+ const indexByDay = useMemo(() => new Map(dates.map((date, index) => [date.getTime(), index])), [dates]);
488
+ const initialDay = selected ?? today;
489
+ const [activeDay, setActiveDay] = useState(initialDay.getTime());
490
+ const storedIndex = indexByDay.get(activeDay);
491
+ const preferredIndex = (selected ? indexByDay.get(startOfDay(selected).getTime()) : undefined) ?? indexByDay.get(today.getTime()) ?? dates.findIndex(isInMonth);
492
+ const activeIndex = storedIndex ?? Math.max(0, preferredIndex);
493
+ const refs = useRef(new Map());
494
+ const register = useCallback((index, node) => {
495
+ if (node) refs.current.set(index, node);else refs.current.delete(index);
496
+ }, []);
497
+ const makeActive = useCallback(index => {
498
+ const date = dates[index];
499
+ if (date) setActiveDay(date.getTime());
500
+ }, [dates]);
501
+ const focus = useCallback((index, event) => {
502
+ const target = plannerGridTarget(event.nativeEvent.key ?? '', index, dates.length);
503
+ if (target === null) return;
504
+ event.preventDefault();
505
+ makeActive(target);
506
+ refs.current.get(target)?.focus();
507
+ }, [dates.length, makeActive]);
508
+ const navigation = useMemo(() => ({
509
+ activeIndex,
510
+ indexByDay,
511
+ register,
512
+ focus,
513
+ makeActive
514
+ }), [activeIndex, indexByDay, register, focus, makeActive]);
515
+ return /*#__PURE__*/_jsx(PlannerGridNavigationContext.Provider, {
516
+ value: renderDay ? null : navigation,
517
+ children: /*#__PURE__*/_jsxs(View, {
518
+ className: grid({
519
+ className
520
+ }),
521
+ children: [/*#__PURE__*/_jsx(View, {
522
+ className: weekRow(),
523
+ accessibilityElementsHidden: true,
524
+ importantForAccessibility: "no-hide-descendants",
525
+ children: headings.map(label => /*#__PURE__*/_jsx(Text, {
526
+ size: "xs",
527
+ muted: true,
528
+ className: heading(),
529
+ children: label.toUpperCase()
530
+ }, label))
531
+ }), weeks.map((week, index) => /*#__PURE__*/_jsx(View, {
532
+ className: weekRow(),
533
+ children: week.map(date => /*#__PURE__*/_jsx(PlannerDay, {
534
+ date: date,
535
+ renderDay: renderDay
536
+ }, date.getTime()))
537
+ }, index))]
538
+ })
539
+ });
540
+ }
541
+ PlannerGrid.displayName = 'Planner.Grid';
542
+ /** One cell. Pressing it selects the day and opens whatever is bound to it. */
543
+ function PlannerDay({
544
+ date,
545
+ renderDay
546
+ }) {
547
+ const {
548
+ days,
549
+ today,
550
+ selected,
551
+ select,
552
+ colorOf,
553
+ categoryLabels,
554
+ entryLimit,
555
+ locale,
556
+ system,
557
+ isInMonth,
558
+ onDayPress
559
+ } = usePlanner('Planner.Day');
560
+ const navigation = useContext(PlannerGridNavigationContext);
561
+ const registerGridCell = navigation?.register;
562
+ const focusGridCell = navigation?.focus;
563
+ const makeGridCellActive = navigation?.makeActive;
564
+ const entries = entriesOn(days, date);
565
+ const inMonth = isInMonth(date);
566
+ const isToday = isSameDay(date, today);
567
+ const isSelected = selected ? isSameDay(date, selected) : false;
568
+ const {
569
+ shown,
570
+ overflow
571
+ } = visibleEntries(entries, entryLimit);
572
+ const drawable = shown.filter(entry => entry.icon);
573
+ const styles = dayVariants({
574
+ inMonth,
575
+ today: isToday,
576
+ selected: isSelected
577
+ });
578
+ const label = dayAccessibilityLabel(calendarLongDate(date, system, locale), entries, categoryLabels);
579
+ const press = () => {
580
+ onDayPress?.(date, entries);
581
+ select(date);
582
+ };
583
+ const gridIndex = navigation?.indexByDay.get(date.getTime());
584
+ const setRef = useCallback(node => {
585
+ if (gridIndex !== undefined) registerGridCell?.(gridIndex, node);
586
+ }, [gridIndex, registerGridCell]);
587
+ const onFocus = useCallback(() => {
588
+ if (gridIndex !== undefined) makeGridCellActive?.(gridIndex);
589
+ }, [gridIndex, makeGridCellActive]);
590
+ const onKeyDown = useCallback(event => {
591
+ if (gridIndex !== undefined) focusGridCell?.(gridIndex, event);
592
+ }, [gridIndex, focusGridCell]);
593
+ const webGridProps = Platform.OS === 'web' && gridIndex !== undefined && navigation ? {
594
+ ref: setRef,
595
+ tabIndex: navigation.activeIndex === gridIndex ? 0 : -1,
596
+ onFocus,
597
+ onKeyDown
598
+ } : {};
599
+ if (renderDay) {
600
+ return /*#__PURE__*/_jsx(View, {
601
+ className: "flex-1",
602
+ children: renderDay({
603
+ date,
604
+ entries,
605
+ isToday,
606
+ isSelected,
607
+ isInMonth: inMonth
608
+ })
609
+ });
610
+ }
611
+ return /*#__PURE__*/_jsxs(Pressable, {
612
+ ...webGridProps,
613
+ onPress: press,
614
+ accessibilityRole: "button",
615
+ accessibilityLabel: label,
616
+ accessibilityHint: entries.length > 0 ? 'Opens what is on this day' : undefined,
617
+ accessibilityState: {
618
+ selected: isSelected
619
+ },
620
+ className: styles.cell(),
621
+ children: [/*#__PURE__*/_jsx(Text, {
622
+ className: styles.number(),
623
+ children: calendarDayNumber(date, system, locale)
624
+ }), entries.length > 0 ? /*#__PURE__*/_jsx(View, {
625
+ className: styles.marker(),
626
+ style: {
627
+ backgroundColor: markerColor(entries, colorOf)
628
+ }
629
+ }) : null, drawable.length > 0 ? /*#__PURE__*/_jsxs(View, {
630
+ className: styles.body(),
631
+ children: [drawable.map(entry => /*#__PURE__*/_jsx(View, {
632
+ children: entry.icon
633
+ }, entry.id)), overflow > 0 ? /*#__PURE__*/_jsxs(Text, {
634
+ size: "xs",
635
+ muted: true,
636
+ className: "leading-none",
637
+ children: ["+", overflow]
638
+ }) : null]
639
+ }) : null]
640
+ });
641
+ }
642
+ PlannerDay.displayName = 'Planner.Day';
643
+
644
+ /**
645
+ * The dot's colour: the first entry that names a category the planner knows.
646
+ *
647
+ * One dot rather than one per entry. A cell this size fits a row of dots or a
648
+ * day number, and the count is already spoken — so the dot answers "is there
649
+ * anything here, and roughly what kind", which is all it has room to answer.
650
+ */
651
+ function markerColor(entries, colorOf) {
652
+ for (const entry of entries) {
653
+ const color = colorOf(entry.category);
654
+ if (color) return color;
655
+ }
656
+ return undefined;
657
+ }
658
+
659
+ /* ------------------------------------------------------------------ *
660
+ * Legend and summary
661
+ * ------------------------------------------------------------------ */
662
+
663
+ /**
664
+ * The key to the dots.
665
+ *
666
+ * It prints the label beside every swatch, because a column of coloured dots
667
+ * with nothing to read them against is a quiz.
668
+ */
669
+ function PlannerLegend({
670
+ className,
671
+ counts = false,
672
+ children
673
+ }) {
674
+ const {
675
+ summary,
676
+ colorOf
677
+ } = usePlanner('Planner.Legend');
678
+ const {
679
+ legend,
680
+ swatch
681
+ } = plannerVariants();
682
+ return /*#__PURE__*/_jsxs(View, {
683
+ className: cn(legend(), 'justify-between border-t border-border', className),
684
+ children: [/*#__PURE__*/_jsx(View, {
685
+ className: "flex-row flex-wrap items-center gap-x-4 gap-y-2",
686
+ children: summary.categories.map(category => /*#__PURE__*/_jsxs(View, {
687
+ className: "flex-row items-center gap-1.5",
688
+ children: [/*#__PURE__*/_jsx(View, {
689
+ className: swatch(),
690
+ style: {
691
+ backgroundColor: colorOf(category.id)
692
+ }
693
+ }), /*#__PURE__*/_jsx(Text, {
694
+ size: "xs",
695
+ muted: true,
696
+ children: counts ? `${category.label} ${category.count}` : category.label
697
+ })]
698
+ }, category.id))
699
+ }), children]
700
+ });
701
+ }
702
+ PlannerLegend.displayName = 'Planner.Legend';
703
+ /** What the month adds up to. Counts this month only, never the days either side. */
704
+ function PlannerSummary({
705
+ children,
706
+ ...props
707
+ }) {
708
+ const {
709
+ summary
710
+ } = usePlanner('Planner.Summary');
711
+ return /*#__PURE__*/_jsx(Text, {
712
+ size: "xs",
713
+ muted: true,
714
+ ...props,
715
+ children: children ?? `${summary.total} ${summary.total === 1 ? 'entry' : 'entries'}`
716
+ });
717
+ }
718
+ PlannerSummary.displayName = 'Planner.Summary';
719
+ /** The strip along the bottom, for tools that act on the month. */
720
+ function PlannerFooter({
721
+ className,
722
+ children
723
+ }) {
724
+ return /*#__PURE__*/_jsx(View, {
725
+ className: cn('flex-row items-center justify-between gap-3 px-4 py-3', className),
726
+ children: children
727
+ });
728
+ }
729
+ PlannerFooter.displayName = 'Planner.Footer';
730
+
731
+ /* ------------------------------------------------------------------ *
732
+ * Details
733
+ * ------------------------------------------------------------------ */
734
+
735
+ /**
736
+ * A dialog bound to the open day.
737
+ *
738
+ * The planner owns the grid and the binding; what the dialog says is the
739
+ * application's, because the contents of a day are its data and not the
740
+ * component's. Leave it out and `onDayPress` is still called — a planner that
741
+ * pushes a screen instead of opening a dialog wants that and nothing else.
742
+ */
743
+ function PlannerDetails({
744
+ className,
745
+ title,
746
+ description,
747
+ children
748
+ }) {
749
+ const {
750
+ selected,
751
+ select,
752
+ days,
753
+ system,
754
+ locale
755
+ } = usePlanner('Planner.Details');
756
+ const entries = selected ? entriesOn(days, selected) : [];
757
+ const count = entries.length === 1 ? '1 entry' : `${entries.length} entries`;
758
+ return /*#__PURE__*/_jsx(Dialog, {
759
+ open: selected !== null,
760
+ onOpenChange: open => !open && select(null),
761
+ children: /*#__PURE__*/_jsxs(Dialog.Content, {
762
+ className: cn('gap-3', className),
763
+ children: [/*#__PURE__*/_jsxs(View, {
764
+ className: "gap-0.5",
765
+ children: [/*#__PURE__*/_jsx(Dialog.Title, {
766
+ children: title ?? (selected ? calendarLongDate(selected, system, locale) : '')
767
+ }), description === null ? null : /*#__PURE__*/_jsx(Dialog.Description, {
768
+ children: description ?? (entries.length === 0 ? 'Nothing planned' : count)
769
+ })]
770
+ }), selected ? children(selected, entries) : null]
771
+ })
772
+ });
773
+ }
774
+ PlannerDetails.displayName = 'Planner.Details';
775
+ export const Planner = Object.assign(PlannerRoot, {
776
+ Header: PlannerHeader,
777
+ Title: PlannerTitle,
778
+ Today: PlannerToday,
779
+ Nav: PlannerNav,
780
+ Action: PlannerAction,
781
+ Grid: PlannerGrid,
782
+ Day: PlannerDay,
783
+ Legend: PlannerLegend,
784
+ Summary: PlannerSummary,
785
+ Footer: PlannerFooter,
786
+ Details: PlannerDetails
787
+ });
788
+ //# sourceMappingURL=index.js.map