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