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