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
|
@@ -72,9 +72,11 @@ import {
|
|
|
72
72
|
import {
|
|
73
73
|
AccessibilityInfo,
|
|
74
74
|
AppState,
|
|
75
|
+
FlatList,
|
|
75
76
|
Platform,
|
|
76
77
|
Pressable,
|
|
77
78
|
View,
|
|
79
|
+
type ViewToken,
|
|
78
80
|
type ViewProps,
|
|
79
81
|
} from 'react-native';
|
|
80
82
|
import { tv } from 'tailwind-variants';
|
|
@@ -111,18 +113,53 @@ import {
|
|
|
111
113
|
} from './planner-entries';
|
|
112
114
|
import { usePlannerMonthAnnouncement } from './planner-announcement';
|
|
113
115
|
import { plannerGridTarget } from './planner-grid-navigation';
|
|
116
|
+
import { weekAnchor, weekDays, weekIndex, weekRange } from './planner-weeks';
|
|
114
117
|
import {
|
|
115
118
|
usePlannerMonthLifecycle,
|
|
116
119
|
usePlannerSelectionLifecycle,
|
|
117
120
|
} from './planner-lifecycle';
|
|
118
121
|
import { millisecondsUntilNextLocalDay } from './planner-today';
|
|
119
122
|
|
|
120
|
-
/**
|
|
121
|
-
|
|
123
|
+
/**
|
|
124
|
+
* How many of a day's entries a cell draws before it stops and counts.
|
|
125
|
+
*
|
|
126
|
+
* `calendar` gets one more because a named entry is a row, and a cell tall
|
|
127
|
+
* enough to name one is tall enough for three; `tiles` draws a single icon at
|
|
128
|
+
* the size of the tile, so the limit there is the look rather than a number.
|
|
129
|
+
*/
|
|
130
|
+
const DEFAULT_ENTRY_LIMIT: Record<PlannerVariant, number> = {
|
|
131
|
+
default: 2,
|
|
132
|
+
tiles: 1,
|
|
133
|
+
calendar: 3,
|
|
134
|
+
};
|
|
122
135
|
|
|
123
136
|
/** The palette a category takes its dot from when it does not name a colour. */
|
|
124
137
|
const PALETTE_SIZE = 5;
|
|
125
138
|
|
|
139
|
+
/**
|
|
140
|
+
* How strongly a `tiles` day takes its category's colour.
|
|
141
|
+
*
|
|
142
|
+
* Low enough that the date in the corner and the icon over it both stay
|
|
143
|
+
* legible, high enough that a brand colour arrives as that brand rather than as
|
|
144
|
+
* a grey tile with a suggestion of one.
|
|
145
|
+
*/
|
|
146
|
+
const TILE_TINT_OPACITY = 0.32;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The height a `tiles` grid reserves, and what it is made of.
|
|
150
|
+
*
|
|
151
|
+
* Six weeks' worth, always — but only the weeks the month actually spans are
|
|
152
|
+
* drawn, and they share the reserved height out between them. A five-week
|
|
153
|
+
* month therefore has slightly taller tiles than a six-week one and no band of
|
|
154
|
+
* empty space under the last row, while the panel itself never changes size as
|
|
155
|
+
* you page through the year. Reserving the height and leaving the sixth row
|
|
156
|
+
* blank, or dropping the row and letting the panel shrink, each fix one of
|
|
157
|
+
* those at the cost of the other.
|
|
158
|
+
*/
|
|
159
|
+
const TILE_ROW_HEIGHT = 64;
|
|
160
|
+
const TILE_ROW_GAP = 4;
|
|
161
|
+
const TILE_GRID_HEIGHT = TILE_ROW_HEIGHT * 6 + TILE_ROW_GAP * 5;
|
|
162
|
+
|
|
126
163
|
/** Stable wrapper keeps the native method's receiver and the hook dependency steady. */
|
|
127
164
|
const announceMonth = (label: string) => {
|
|
128
165
|
AccessibilityInfo.announceForAccessibility(label);
|
|
@@ -136,6 +173,14 @@ const plannerVariants = tv({
|
|
|
136
173
|
legend: 'flex-row flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3',
|
|
137
174
|
swatch: 'h-2 w-2 rounded-full',
|
|
138
175
|
},
|
|
176
|
+
variants: {
|
|
177
|
+
/*
|
|
178
|
+
* The weeks share out the height instead of standing at their own. The
|
|
179
|
+
* weekday row keeps its natural height — it is a label, and stretching it
|
|
180
|
+
* only moves the letters away from the column they name.
|
|
181
|
+
*/
|
|
182
|
+
fill: { true: { grid: 'flex-1', week: 'flex-1' } },
|
|
183
|
+
},
|
|
139
184
|
});
|
|
140
185
|
|
|
141
186
|
const dayVariants = tv({
|
|
@@ -160,8 +205,27 @@ const dayVariants = tv({
|
|
|
160
205
|
number: 'text-xs leading-none',
|
|
161
206
|
marker: 'absolute right-1.5 top-1.5 h-1.5 w-1.5 rounded-full',
|
|
162
207
|
body: 'flex-1 flex-row items-center justify-center gap-0.5 pb-1',
|
|
208
|
+
/** The colour a day's entries give it, drawn under everything else. */
|
|
209
|
+
tint: 'absolute inset-0 rounded-xl',
|
|
210
|
+
/*
|
|
211
|
+
* The corners of a tile. These are Views rather than classes on the Text
|
|
212
|
+
* itself: a Text takes its typography from a class but not its position,
|
|
213
|
+
* so a number told to sit in a corner stayed where the layout put it —
|
|
214
|
+
* dead centre, under the icon.
|
|
215
|
+
*/
|
|
216
|
+
corner: 'absolute bottom-1 right-1.5',
|
|
217
|
+
counter: 'absolute left-1.5 top-1',
|
|
218
|
+
/** One entry, named, in a cell with the height to name it. */
|
|
219
|
+
chip: 'w-full overflow-hidden rounded-sm bg-muted px-1 py-0.5',
|
|
220
|
+
overflow: 'leading-none',
|
|
163
221
|
},
|
|
164
222
|
variants: {
|
|
223
|
+
/*
|
|
224
|
+
* Cells stretch to their container rather than standing at a fixed height.
|
|
225
|
+
* The grid is still six weeks, so what changes is how tall a week is, not
|
|
226
|
+
* how many there are.
|
|
227
|
+
*/
|
|
228
|
+
fill: { true: { cell: 'h-auto min-h-0 flex-1' } },
|
|
165
229
|
/** Inside the month being shown, as opposed to the days either side. */
|
|
166
230
|
inMonth: {
|
|
167
231
|
true: { cell: 'bg-muted/40', number: 'text-foreground' },
|
|
@@ -175,6 +239,38 @@ const dayVariants = tv({
|
|
|
175
239
|
today: { true: { cell: 'border-foreground', number: 'font-semibold' } },
|
|
176
240
|
selected: { true: { cell: 'border-primary/60 bg-primary/10' } },
|
|
177
241
|
disabled: { true: { cell: 'opacity-40' } },
|
|
242
|
+
/*
|
|
243
|
+
* What a cell draws. One axis, because these are three answers to the same
|
|
244
|
+
* question and a cell can only give one of them.
|
|
245
|
+
*
|
|
246
|
+
* Last, so a look can undo what the states above it set. `calendar` has no
|
|
247
|
+
* tile to ring or fill, so it has to be able to take the background and the
|
|
248
|
+
* border back off a cell that today or the open day just gave one to.
|
|
249
|
+
*/
|
|
250
|
+
variant: {
|
|
251
|
+
default: {},
|
|
252
|
+
/*
|
|
253
|
+
* A tile per day, carrying the one thing on it at the size of a mark
|
|
254
|
+
* rather than a marker. The date moves out of the way into the corner:
|
|
255
|
+
* it is how you find the day you want, not what the cell is showing.
|
|
256
|
+
*/
|
|
257
|
+
tiles: {
|
|
258
|
+
cell: 'h-full items-center justify-center px-0 pt-0',
|
|
259
|
+
overflow: 'leading-none',
|
|
260
|
+
},
|
|
261
|
+
/*
|
|
262
|
+
* Named entries stacked down an open cell. No tile, because a tile per
|
|
263
|
+
* day and a block per entry are two boxes saying the same thing and the
|
|
264
|
+
* entries are the ones carrying words — the weeks are ruled off instead,
|
|
265
|
+
* which separates them without enclosing anything.
|
|
266
|
+
*/
|
|
267
|
+
calendar: {
|
|
268
|
+
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',
|
|
269
|
+
number: 'text-center',
|
|
270
|
+
body: 'flex-col items-stretch justify-start gap-px pb-0',
|
|
271
|
+
overflow: 'text-center leading-none',
|
|
272
|
+
},
|
|
273
|
+
},
|
|
178
274
|
},
|
|
179
275
|
compoundVariants: [
|
|
180
276
|
/*
|
|
@@ -184,13 +280,23 @@ const dayVariants = tv({
|
|
|
184
280
|
*/
|
|
185
281
|
{ today: true, selected: true, class: { cell: 'border-foreground bg-primary/10' } },
|
|
186
282
|
],
|
|
187
|
-
defaultVariants: { inMonth: true },
|
|
283
|
+
defaultVariants: { inMonth: true, variant: 'default' },
|
|
188
284
|
});
|
|
189
285
|
|
|
190
286
|
/* ------------------------------------------------------------------ *
|
|
191
287
|
* Data
|
|
192
288
|
* ------------------------------------------------------------------ */
|
|
193
289
|
|
|
290
|
+
/**
|
|
291
|
+
* What a day cell draws.
|
|
292
|
+
*
|
|
293
|
+
* `default` is a number, a marker and small icons. `tiles` gives the day over
|
|
294
|
+
* to one large icon tinted with its category's colour, with the date in the
|
|
295
|
+
* corner. `calendar` names each entry in a block under a centred date, on an
|
|
296
|
+
* open grid ruled off by week.
|
|
297
|
+
*/
|
|
298
|
+
export type PlannerVariant = 'default' | 'tiles' | 'calendar';
|
|
299
|
+
|
|
194
300
|
/** One thing that falls on a day. */
|
|
195
301
|
export interface PlannerEntry {
|
|
196
302
|
/** Stable across renders; it keys the cell's contents. */
|
|
@@ -203,6 +309,12 @@ export interface PlannerEntry {
|
|
|
203
309
|
category?: string;
|
|
204
310
|
/** Drawn in the cell — a brand mark, an avatar, a glyph. */
|
|
205
311
|
icon?: ReactNode;
|
|
312
|
+
/**
|
|
313
|
+
* This entry's own colour, for a mark that belongs to the entry rather than
|
|
314
|
+
* to a group of them — a brand. It wins over the category's colour, and
|
|
315
|
+
* under `tiles` it is what tints the day.
|
|
316
|
+
*/
|
|
317
|
+
color?: string;
|
|
206
318
|
}
|
|
207
319
|
|
|
208
320
|
/** A group of entries: the key to a colour, and a line in the legend. */
|
|
@@ -222,6 +334,8 @@ export interface PlannerCategory {
|
|
|
222
334
|
interface PlannerContextValue {
|
|
223
335
|
month: Date;
|
|
224
336
|
setMonth: (month: Date) => void;
|
|
337
|
+
/** Moves the month without announcing it. For scrolling, which is continuous. */
|
|
338
|
+
syncMonth: (month: Date) => void;
|
|
225
339
|
today: Date;
|
|
226
340
|
selected: Date | null;
|
|
227
341
|
select: (date: Date | null) => void;
|
|
@@ -231,6 +345,8 @@ interface PlannerContextValue {
|
|
|
231
345
|
colorOf: (category: string | undefined) => string | undefined;
|
|
232
346
|
summary: { total: number; categories: PlannerCountedCategory[] };
|
|
233
347
|
entryLimit: number;
|
|
348
|
+
variant: PlannerVariant;
|
|
349
|
+
fill: boolean;
|
|
234
350
|
weekStartsOn: number;
|
|
235
351
|
grid: Date[][];
|
|
236
352
|
locale: DateLocale;
|
|
@@ -325,7 +441,22 @@ export interface PlannerProps extends Omit<ViewProps, 'children'> {
|
|
|
325
441
|
onSelectedChange?: (date: Date | null) => void;
|
|
326
442
|
/** Runs before the selection moves, whether or not `Details` is present. */
|
|
327
443
|
onDayPress?: (date: Date, entries: PlannerEntry[]) => void;
|
|
328
|
-
/**
|
|
444
|
+
/**
|
|
445
|
+
* What each day draws. `tiles` is one large icon per day, tinted by its
|
|
446
|
+
* category; `calendar` names every entry and needs the height to do it.
|
|
447
|
+
*/
|
|
448
|
+
variant?: PlannerVariant;
|
|
449
|
+
/**
|
|
450
|
+
* Stretch the grid to its container instead of standing at its own height.
|
|
451
|
+
* For a planner that owns a screen — the six weeks share out whatever is
|
|
452
|
+
* left after the header, the legend and anything below them.
|
|
453
|
+
*/
|
|
454
|
+
fill?: boolean;
|
|
455
|
+
/**
|
|
456
|
+
* How many entries a cell draws before it counts the rest. Default `2`, or
|
|
457
|
+
* `3` under `calendar`, which has the room; `tiles` draws one whatever you
|
|
458
|
+
* pass.
|
|
459
|
+
*/
|
|
329
460
|
entryLimit?: number;
|
|
330
461
|
/** First day of the week, 0 is Sunday. Defaults to the locale's. */
|
|
331
462
|
weekStartsOn?: number | 'auto';
|
|
@@ -349,7 +480,9 @@ const PlannerRoot = forwardRef<View, PlannerProps>(
|
|
|
349
480
|
defaultSelected = null,
|
|
350
481
|
onSelectedChange,
|
|
351
482
|
onDayPress,
|
|
352
|
-
|
|
483
|
+
variant = 'default',
|
|
484
|
+
fill = false,
|
|
485
|
+
entryLimit,
|
|
353
486
|
weekStartsOn = 'auto',
|
|
354
487
|
locale,
|
|
355
488
|
calendar = 'gregory',
|
|
@@ -359,6 +492,7 @@ const PlannerRoot = forwardRef<View, PlannerProps>(
|
|
|
359
492
|
},
|
|
360
493
|
ref
|
|
361
494
|
) => {
|
|
495
|
+
const resolvedEntryLimit = entryLimit ?? DEFAULT_ENTRY_LIMIT[variant];
|
|
362
496
|
const system = resolveCalendar(calendar, locale);
|
|
363
497
|
const palette = usePalette();
|
|
364
498
|
const today = useToday();
|
|
@@ -385,6 +519,18 @@ const PlannerRoot = forwardRef<View, PlannerProps>(
|
|
|
385
519
|
monthLabel: calendarMonthLabel(month, system, locale),
|
|
386
520
|
announce: announceMonth,
|
|
387
521
|
});
|
|
522
|
+
/*
|
|
523
|
+
* Scrolling changes the month continuously, and announcing each one as it
|
|
524
|
+
* goes would talk over somebody reading the weeks. The announcement belongs
|
|
525
|
+
* to a deliberate move — the arrows, the Today pill — so a scroll reports
|
|
526
|
+
* the change and stays quiet about it.
|
|
527
|
+
*/
|
|
528
|
+
const syncMonth = useCallback(
|
|
529
|
+
(next: Date) => {
|
|
530
|
+
requestMonth(next);
|
|
531
|
+
},
|
|
532
|
+
[requestMonth]
|
|
533
|
+
);
|
|
388
534
|
const setMonth = useCallback(
|
|
389
535
|
(next: Date) => {
|
|
390
536
|
expectMonthAnnouncement(settleMonth(next));
|
|
@@ -449,6 +595,7 @@ const PlannerRoot = forwardRef<View, PlannerProps>(
|
|
|
449
595
|
() => ({
|
|
450
596
|
month,
|
|
451
597
|
setMonth,
|
|
598
|
+
syncMonth,
|
|
452
599
|
today,
|
|
453
600
|
selected,
|
|
454
601
|
select,
|
|
@@ -457,7 +604,9 @@ const PlannerRoot = forwardRef<View, PlannerProps>(
|
|
|
457
604
|
categoryLabels,
|
|
458
605
|
colorOf,
|
|
459
606
|
summary,
|
|
460
|
-
entryLimit,
|
|
607
|
+
entryLimit: resolvedEntryLimit,
|
|
608
|
+
variant,
|
|
609
|
+
fill,
|
|
461
610
|
weekStartsOn: normalizedWeekStart,
|
|
462
611
|
grid,
|
|
463
612
|
locale,
|
|
@@ -466,9 +615,9 @@ const PlannerRoot = forwardRef<View, PlannerProps>(
|
|
|
466
615
|
onDayPress,
|
|
467
616
|
}),
|
|
468
617
|
[
|
|
469
|
-
month, setMonth, today, selected, select, days, categories, categoryLabels,
|
|
470
|
-
colorOf, summary,
|
|
471
|
-
isInMonth, onDayPress,
|
|
618
|
+
month, setMonth, syncMonth, today, selected, select, days, categories, categoryLabels,
|
|
619
|
+
colorOf, summary, resolvedEntryLimit, variant, fill, normalizedWeekStart,
|
|
620
|
+
grid, locale, system, isInMonth, onDayPress,
|
|
472
621
|
]
|
|
473
622
|
);
|
|
474
623
|
|
|
@@ -488,13 +637,16 @@ const PlannerRoot = forwardRef<View, PlannerProps>(
|
|
|
488
637
|
{frame ? (
|
|
489
638
|
// Full width by default: the grid is seven equal columns, and a
|
|
490
639
|
// container that sizes to its content collapses them to the width of
|
|
491
|
-
// a two-digit number.
|
|
492
|
-
|
|
640
|
+
// a two-digit number. Under `fill` the panel has to flex too, or the
|
|
641
|
+
// grid inside it stretches against a container that does not.
|
|
642
|
+
<Frame ref={ref} className={cn('w-full', fill && 'flex-1', className)} {...props}>
|
|
493
643
|
{header}
|
|
494
|
-
<Frame.Panel dividers={false}
|
|
644
|
+
<Frame.Panel dividers={false} className={fill ? 'flex-1' : undefined}>
|
|
645
|
+
{body}
|
|
646
|
+
</Frame.Panel>
|
|
495
647
|
</Frame>
|
|
496
648
|
) : (
|
|
497
|
-
<View ref={ref} className={cn('w-full', className)} {...props}>
|
|
649
|
+
<View ref={ref} className={cn('w-full', fill && 'flex-1', className)} {...props}>
|
|
498
650
|
{header}
|
|
499
651
|
{body}
|
|
500
652
|
</View>
|
|
@@ -532,9 +684,15 @@ function PlannerHeader({ children }: PlannerHeaderProps) {
|
|
|
532
684
|
|
|
533
685
|
return (
|
|
534
686
|
<Frame.Header>
|
|
535
|
-
<View className="min-w-0
|
|
687
|
+
<View className="min-w-0 shrink flex-row items-center gap-2">{lead}</View>
|
|
688
|
+
{/*
|
|
689
|
+
`ml-auto` rather than a flexible lead. The nav belongs against the right
|
|
690
|
+
edge whatever is to its left — one short title, a title and a pill, or
|
|
691
|
+
nothing at all — and pushing it there with a margin says that directly
|
|
692
|
+
instead of leaving it to whether the thing beside it happened to grow.
|
|
693
|
+
*/}
|
|
536
694
|
{trailing.length > 0 ? (
|
|
537
|
-
<View className="shrink-0 flex-row items-center gap-1.5">{trailing}</View>
|
|
695
|
+
<View className="ml-auto shrink-0 flex-row items-center gap-1.5">{trailing}</View>
|
|
538
696
|
) : null}
|
|
539
697
|
</Frame.Header>
|
|
540
698
|
);
|
|
@@ -686,18 +844,40 @@ const PlannerGridNavigationContext = createContext<PlannerGridNavigationContextV
|
|
|
686
844
|
* weekday headings are hidden rather than read out 42 times over.
|
|
687
845
|
*/
|
|
688
846
|
function PlannerGrid({ className, renderDay }: PlannerGridProps) {
|
|
689
|
-
const { grid: weeks, weekStartsOn, locale, selected, today, isInMonth } =
|
|
847
|
+
const { grid: weeks, weekStartsOn, locale, selected, today, isInMonth, variant, fill } =
|
|
690
848
|
usePlanner('Planner.Grid');
|
|
691
|
-
const { grid, week: weekRow, heading } = plannerVariants();
|
|
849
|
+
const { grid, week: weekRow, heading } = plannerVariants({ fill });
|
|
850
|
+
/*
|
|
851
|
+
* `tiles` leaves the days either side of the month blank, so its columns are
|
|
852
|
+
* told apart by position alone and a single letter is enough to head them.
|
|
853
|
+
*/
|
|
692
854
|
const headings = useMemo(
|
|
693
|
-
() => weekdayNames(locale, weekStartsOn),
|
|
694
|
-
[locale, weekStartsOn]
|
|
855
|
+
() => weekdayNames(locale, weekStartsOn, variant === 'tiles' ? 'narrow' : 'short'),
|
|
856
|
+
[locale, weekStartsOn, variant]
|
|
857
|
+
);
|
|
858
|
+
/*
|
|
859
|
+
* `tiles` draws nothing for the days either side of the month, so a week made
|
|
860
|
+
* only of those is a band of empty space rather than a week. It is dropped,
|
|
861
|
+
* and the grid keeps its height by letting the weeks that remain stretch.
|
|
862
|
+
*/
|
|
863
|
+
const shownWeeks = useMemo(
|
|
864
|
+
() => (variant === 'tiles' ? weeks.filter((week) => week.some(isInMonth)) : weeks),
|
|
865
|
+
[weeks, variant, isInMonth]
|
|
695
866
|
);
|
|
696
|
-
const dates = useMemo(() =>
|
|
867
|
+
const dates = useMemo(() => shownWeeks.flat(), [shownWeeks]);
|
|
697
868
|
const indexByDay = useMemo(
|
|
698
869
|
() => new Map(dates.map((date, index) => [date.getTime(), index])),
|
|
699
870
|
[dates]
|
|
700
871
|
);
|
|
872
|
+
/*
|
|
873
|
+
* A blank cell has nothing to focus, so arrow keys step over it rather than
|
|
874
|
+
* landing on a day that is not drawn and losing focus altogether.
|
|
875
|
+
*/
|
|
876
|
+
const navigable = useMemo(
|
|
877
|
+
() =>
|
|
878
|
+
variant === 'tiles' ? (index: number) => isInMonth(dates[index] ?? today) : undefined,
|
|
879
|
+
[variant, dates, isInMonth, today]
|
|
880
|
+
);
|
|
701
881
|
const initialDay = selected ?? today;
|
|
702
882
|
const [activeDay, setActiveDay] = useState(initialDay.getTime());
|
|
703
883
|
const storedIndex = indexByDay.get(activeDay);
|
|
@@ -720,13 +900,19 @@ function PlannerGrid({ className, renderDay }: PlannerGridProps) {
|
|
|
720
900
|
);
|
|
721
901
|
const focus = useCallback(
|
|
722
902
|
(index: number, event: PlannerGridKeyDownEvent) => {
|
|
723
|
-
const target = plannerGridTarget(
|
|
903
|
+
const target = plannerGridTarget(
|
|
904
|
+
event.nativeEvent.key ?? '',
|
|
905
|
+
index,
|
|
906
|
+
dates.length,
|
|
907
|
+
7,
|
|
908
|
+
navigable
|
|
909
|
+
);
|
|
724
910
|
if (target === null) return;
|
|
725
911
|
event.preventDefault();
|
|
726
912
|
makeActive(target);
|
|
727
913
|
refs.current.get(target)?.focus();
|
|
728
914
|
},
|
|
729
|
-
[dates.length, makeActive]
|
|
915
|
+
[dates.length, makeActive, navigable]
|
|
730
916
|
);
|
|
731
917
|
const navigation = useMemo<PlannerGridNavigationContextValue>(
|
|
732
918
|
() => ({ activeIndex, indexByDay, register, focus, makeActive }),
|
|
@@ -735,21 +921,29 @@ function PlannerGrid({ className, renderDay }: PlannerGridProps) {
|
|
|
735
921
|
|
|
736
922
|
return (
|
|
737
923
|
<PlannerGridNavigationContext.Provider value={renderDay ? null : navigation}>
|
|
738
|
-
<View
|
|
924
|
+
<View
|
|
925
|
+
className={grid({ className })}
|
|
926
|
+
style={variant === 'tiles' && !fill ? { height: TILE_GRID_HEIGHT } : undefined}
|
|
927
|
+
>
|
|
739
928
|
<View
|
|
740
929
|
className={weekRow()}
|
|
741
930
|
accessibilityElementsHidden
|
|
742
931
|
importantForAccessibility="no-hide-descendants"
|
|
743
932
|
>
|
|
744
|
-
{
|
|
745
|
-
|
|
933
|
+
{/*
|
|
934
|
+
Keyed by position rather than by the word: narrow names repeat in
|
|
935
|
+
plenty of locales — English has T twice and S twice — and a column
|
|
936
|
+
is identified by where it is, not by the letter over it.
|
|
937
|
+
*/}
|
|
938
|
+
{headings.map((label, position) => (
|
|
939
|
+
<Text key={position} size="xs" muted className={heading()}>
|
|
746
940
|
{label.toUpperCase()}
|
|
747
941
|
</Text>
|
|
748
942
|
))}
|
|
749
943
|
</View>
|
|
750
944
|
|
|
751
|
-
{
|
|
752
|
-
<View key={index} className={weekRow()}>
|
|
945
|
+
{shownWeeks.map((week, index) => (
|
|
946
|
+
<View key={index} className={cn(weekRow(), variant === 'tiles' && 'flex-1')}>
|
|
753
947
|
{week.map((date) => (
|
|
754
948
|
<PlannerDay key={date.getTime()} date={date} renderDay={renderDay} />
|
|
755
949
|
))}
|
|
@@ -770,7 +964,7 @@ export interface PlannerDayProps {
|
|
|
770
964
|
function PlannerDay({ date, renderDay }: PlannerDayProps) {
|
|
771
965
|
const {
|
|
772
966
|
days, today, selected, select, colorOf, categoryLabels,
|
|
773
|
-
entryLimit, locale, system, isInMonth, onDayPress,
|
|
967
|
+
entryLimit, variant, fill, locale, system, isInMonth, onDayPress,
|
|
774
968
|
} = usePlanner('Planner.Day');
|
|
775
969
|
const navigation = useContext(PlannerGridNavigationContext);
|
|
776
970
|
const registerGridCell = navigation?.register;
|
|
@@ -783,7 +977,14 @@ function PlannerDay({ date, renderDay }: PlannerDayProps) {
|
|
|
783
977
|
const isSelected = selected ? isSameDay(date, selected) : false;
|
|
784
978
|
const { shown, overflow } = visibleEntries(entries, entryLimit);
|
|
785
979
|
const drawable = shown.filter((entry) => entry.icon);
|
|
786
|
-
const styles = dayVariants({ inMonth, today: isToday, selected: isSelected });
|
|
980
|
+
const styles = dayVariants({ variant, fill, inMonth, today: isToday, selected: isSelected });
|
|
981
|
+
/*
|
|
982
|
+
* The colour the day's entries give it. The first entry naming a category the
|
|
983
|
+
* planner knows wins, and an uncategorised day gets nothing — the tint is a
|
|
984
|
+
* signal about what is on the day, so a day with no answer stays plain rather
|
|
985
|
+
* than being coloured for the sake of it.
|
|
986
|
+
*/
|
|
987
|
+
const tint = markerColor(entries, colorOf);
|
|
787
988
|
|
|
788
989
|
const label = dayAccessibilityLabel(
|
|
789
990
|
calendarLongDate(date, system, locale),
|
|
@@ -791,9 +992,15 @@ function PlannerDay({ date, renderDay }: PlannerDayProps) {
|
|
|
791
992
|
categoryLabels
|
|
792
993
|
);
|
|
793
994
|
|
|
995
|
+
/*
|
|
996
|
+
* Pressing the open day again closes it. Without this the mark is a one-way
|
|
997
|
+
* door: a planner with no `Details` bound has nothing to dismiss, so the day
|
|
998
|
+
* you pressed stays ringed until you press a different one and there is no
|
|
999
|
+
* way back to none selected.
|
|
1000
|
+
*/
|
|
794
1001
|
const press = () => {
|
|
795
1002
|
onDayPress?.(date, entries);
|
|
796
|
-
select(date);
|
|
1003
|
+
select(isSelected ? null : date);
|
|
797
1004
|
};
|
|
798
1005
|
const gridIndex = navigation?.indexByDay.get(date.getTime());
|
|
799
1006
|
const setRef = useCallback(
|
|
@@ -829,6 +1036,23 @@ function PlannerDay({ date, renderDay }: PlannerDayProps) {
|
|
|
829
1036
|
);
|
|
830
1037
|
}
|
|
831
1038
|
|
|
1039
|
+
/*
|
|
1040
|
+
* `tiles` leaves the days either side of the month out altogether. The row
|
|
1041
|
+
* still holds their column, so the grid is six weeks whatever month it is
|
|
1042
|
+
* showing, but there is no tile, no number and nothing to press or to read
|
|
1043
|
+
* out: a blank is the answer, and drawing a faded one only invites the press
|
|
1044
|
+
* it is going to ignore.
|
|
1045
|
+
*/
|
|
1046
|
+
if (variant === 'tiles' && !inMonth) {
|
|
1047
|
+
return (
|
|
1048
|
+
<View
|
|
1049
|
+
className={cn('mx-0.5 flex-1', fill ? 'min-h-0' : 'h-16')}
|
|
1050
|
+
accessibilityElementsHidden
|
|
1051
|
+
importantForAccessibility="no-hide-descendants"
|
|
1052
|
+
/>
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
832
1056
|
return (
|
|
833
1057
|
<Pressable
|
|
834
1058
|
{...(webGridProps as ViewProps)}
|
|
@@ -839,10 +1063,75 @@ function PlannerDay({ date, renderDay }: PlannerDayProps) {
|
|
|
839
1063
|
accessibilityState={{ selected: isSelected }}
|
|
840
1064
|
className={styles.cell()}
|
|
841
1065
|
>
|
|
842
|
-
|
|
1066
|
+
{/*
|
|
1067
|
+
The tint sits under the content rather than on the cell itself: the
|
|
1068
|
+
colour arrives as a runtime string from the theme, so it cannot go
|
|
1069
|
+
through a class with an opacity on it, and painting the cell at full
|
|
1070
|
+
strength would take the number down with it.
|
|
1071
|
+
*/}
|
|
1072
|
+
{variant === 'tiles' && tint ? (
|
|
1073
|
+
<View
|
|
1074
|
+
pointerEvents="none"
|
|
1075
|
+
className={styles.tint()}
|
|
1076
|
+
style={{ backgroundColor: tint, opacity: TILE_TINT_OPACITY }}
|
|
1077
|
+
/>
|
|
1078
|
+
) : null}
|
|
1079
|
+
|
|
1080
|
+
{variant === 'tiles' ? (
|
|
1081
|
+
<View className={styles.corner()} pointerEvents="none">
|
|
1082
|
+
<Text size="xs" className={styles.number()}>
|
|
1083
|
+
{calendarDayNumber(date, system, locale)}
|
|
1084
|
+
</Text>
|
|
1085
|
+
</View>
|
|
1086
|
+
) : variant === 'calendar' ? (
|
|
1087
|
+
/*
|
|
1088
|
+
* A disc around the date rather than a ring around the cell. There is
|
|
1089
|
+
* no tile here to ring, and the date is the only thing in the cell that
|
|
1090
|
+
* is always present — so it is the only place a mark can go and be
|
|
1091
|
+
* found in the same spot on every day of the month.
|
|
1092
|
+
*/
|
|
1093
|
+
<View
|
|
1094
|
+
className={cn(
|
|
1095
|
+
'h-6 w-6 items-center justify-center self-center rounded-full',
|
|
1096
|
+
isToday && 'bg-primary'
|
|
1097
|
+
)}
|
|
1098
|
+
>
|
|
1099
|
+
<Text
|
|
1100
|
+
size="xs"
|
|
1101
|
+
className={cn(styles.number(), isToday && 'text-primary-foreground')}
|
|
1102
|
+
>
|
|
1103
|
+
{calendarDayNumber(date, system, locale)}
|
|
1104
|
+
</Text>
|
|
1105
|
+
</View>
|
|
1106
|
+
) : (
|
|
1107
|
+
<Text className={styles.number()}>{calendarDayNumber(date, system, locale)}</Text>
|
|
1108
|
+
)}
|
|
1109
|
+
|
|
1110
|
+
{variant === 'default' && entries.length > 0 ? (
|
|
1111
|
+
<View className={styles.marker()} style={{ backgroundColor: tint }} />
|
|
1112
|
+
) : null}
|
|
843
1113
|
|
|
844
|
-
{
|
|
845
|
-
<View className={styles.
|
|
1114
|
+
{variant === 'calendar' ? (
|
|
1115
|
+
<View className={styles.body()}>
|
|
1116
|
+
{shown.map((entry) => (
|
|
1117
|
+
<PlannerChip
|
|
1118
|
+
key={entry.id}
|
|
1119
|
+
entry={entry}
|
|
1120
|
+
color={entry.color ?? colorOf(entry.category)}
|
|
1121
|
+
/>
|
|
1122
|
+
))}
|
|
1123
|
+
{/*
|
|
1124
|
+
Three dots, not a count. The cell has already run out of room for
|
|
1125
|
+
the entries themselves, and "+2 more" spends a whole row of that
|
|
1126
|
+
room on a number nobody acts on — the dots say there is more and
|
|
1127
|
+
leave the row for an entry.
|
|
1128
|
+
*/}
|
|
1129
|
+
{overflow > 0 ? (
|
|
1130
|
+
<Text size="xs" muted className={styles.overflow()} numberOfLines={1}>
|
|
1131
|
+
•••
|
|
1132
|
+
</Text>
|
|
1133
|
+
) : null}
|
|
1134
|
+
</View>
|
|
846
1135
|
) : null}
|
|
847
1136
|
|
|
848
1137
|
{/*
|
|
@@ -851,13 +1140,21 @@ function PlannerDay({ date, renderDay }: PlannerDayProps) {
|
|
|
851
1140
|
"something is here" with its marker; adding a bare "+3" under an empty
|
|
852
1141
|
row says it twice, in a way that looks like a stray number.
|
|
853
1142
|
*/}
|
|
854
|
-
{
|
|
1143
|
+
{variant === 'tiles' && overflow > 0 ? (
|
|
1144
|
+
<View className={styles.counter()} pointerEvents="none">
|
|
1145
|
+
<Text size="xs" muted className={styles.overflow()}>
|
|
1146
|
+
+{overflow}
|
|
1147
|
+
</Text>
|
|
1148
|
+
</View>
|
|
1149
|
+
) : null}
|
|
1150
|
+
|
|
1151
|
+
{variant !== 'calendar' && drawable.length > 0 ? (
|
|
855
1152
|
<View className={styles.body()}>
|
|
856
1153
|
{drawable.map((entry) => (
|
|
857
1154
|
<View key={entry.id}>{entry.icon}</View>
|
|
858
1155
|
))}
|
|
859
|
-
{overflow > 0 ? (
|
|
860
|
-
<Text size="xs" muted className=
|
|
1156
|
+
{variant !== 'tiles' && overflow > 0 ? (
|
|
1157
|
+
<Text size="xs" muted className={styles.overflow()}>
|
|
861
1158
|
+{overflow}
|
|
862
1159
|
</Text>
|
|
863
1160
|
) : null}
|
|
@@ -866,7 +1163,31 @@ function PlannerDay({ date, renderDay }: PlannerDayProps) {
|
|
|
866
1163
|
</Pressable>
|
|
867
1164
|
);
|
|
868
1165
|
}
|
|
869
|
-
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* One named entry inside a `calendar` cell.
|
|
1169
|
+
*
|
|
1170
|
+
* A solid block of the entry's colour with light text on it. That is a real
|
|
1171
|
+
* constraint rather than a free choice: the colour is the caller's, so a pale
|
|
1172
|
+
* one will not carry white well — but a wash with the label in the foreground
|
|
1173
|
+
* colour makes every entry the same weight and loses the one thing the colour
|
|
1174
|
+
* was for, which is telling them apart down a column at a glance.
|
|
1175
|
+
*/
|
|
1176
|
+
function PlannerChip({ entry, color }: { entry: PlannerEntry; color?: string }) {
|
|
1177
|
+
const { chip } = dayVariants();
|
|
1178
|
+
return (
|
|
1179
|
+
<View className={chip()} style={color ? { backgroundColor: color } : undefined}>
|
|
1180
|
+
<Text
|
|
1181
|
+
size="xs"
|
|
1182
|
+
numberOfLines={1}
|
|
1183
|
+
className={cn('leading-none', !color && 'text-foreground')}
|
|
1184
|
+
style={color ? { color: '#ffffff' } : undefined}
|
|
1185
|
+
>
|
|
1186
|
+
{entry.label}
|
|
1187
|
+
</Text>
|
|
1188
|
+
</View>
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
870
1191
|
|
|
871
1192
|
/**
|
|
872
1193
|
* The dot's colour: the first entry that names a category the planner knows.
|
|
@@ -880,7 +1201,9 @@ function markerColor(
|
|
|
880
1201
|
colorOf: (category: string | undefined) => string | undefined
|
|
881
1202
|
): string | undefined {
|
|
882
1203
|
for (const entry of entries) {
|
|
883
|
-
|
|
1204
|
+
// The entry's own colour first: a brand belongs to the thing, not to the
|
|
1205
|
+
// group it was filed under.
|
|
1206
|
+
const color = entry.color ?? colorOf(entry.category);
|
|
884
1207
|
if (color) return color;
|
|
885
1208
|
}
|
|
886
1209
|
return undefined;
|
|
@@ -1013,6 +1336,160 @@ function PlannerDetails({ className, title, description, children }: PlannerDeta
|
|
|
1013
1336
|
}
|
|
1014
1337
|
PlannerDetails.displayName = 'Planner.Details';
|
|
1015
1338
|
|
|
1339
|
+
/* ------------------------------------------------------------------ *
|
|
1340
|
+
* Scroller
|
|
1341
|
+
* ------------------------------------------------------------------ */
|
|
1342
|
+
|
|
1343
|
+
/** How many weeks either side of the opening month a scroller reaches by default. */
|
|
1344
|
+
const DEFAULT_SCROLLER_WEEKS = 53;
|
|
1345
|
+
|
|
1346
|
+
/** The height of one week row, and enough of it for a date and three entries. */
|
|
1347
|
+
const DEFAULT_WEEK_HEIGHT = 96;
|
|
1348
|
+
|
|
1349
|
+
export interface PlannerScrollerProps {
|
|
1350
|
+
className?: string;
|
|
1351
|
+
/**
|
|
1352
|
+
* How many weeks either side of the opening month can be reached. Default
|
|
1353
|
+
* `53`, about a year each way.
|
|
1354
|
+
*/
|
|
1355
|
+
weeks?: number;
|
|
1356
|
+
/** The height of one week row. Default `96`. */
|
|
1357
|
+
rowHeight?: number;
|
|
1358
|
+
/** Draws a cell yourself. It is handed the day and what falls on it. */
|
|
1359
|
+
renderDay?: PlannerDayRenderer;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* The weeks of the year, scrolled through rather than paged.
|
|
1364
|
+
*
|
|
1365
|
+
* A month grid answers "what does this month look like"; a scroller answers
|
|
1366
|
+
* "what is coming", which does not stop at the end of a month. The week
|
|
1367
|
+
* straddling the boundary is drawn once, in one piece, instead of appearing cut
|
|
1368
|
+
* in half at the bottom of one page and again at the top of the next.
|
|
1369
|
+
*
|
|
1370
|
+
* The range is bounded rather than endless. A scroller has to know its own
|
|
1371
|
+
* height to place a scrollbar and to jump to a month without rendering its way
|
|
1372
|
+
* there, and neither is possible over a list with no end.
|
|
1373
|
+
*
|
|
1374
|
+
* The month in the header follows the scroll: whichever month holds most of the
|
|
1375
|
+
* first week on screen is the one named, and the days either side of it grey
|
|
1376
|
+
* out. `Planner.Nav` and `Planner.Today` still work — they scroll the list
|
|
1377
|
+
* rather than replacing what is in it.
|
|
1378
|
+
*/
|
|
1379
|
+
function PlannerScroller({
|
|
1380
|
+
className,
|
|
1381
|
+
weeks: span = DEFAULT_SCROLLER_WEEKS,
|
|
1382
|
+
rowHeight = DEFAULT_WEEK_HEIGHT,
|
|
1383
|
+
renderDay,
|
|
1384
|
+
}: PlannerScrollerProps) {
|
|
1385
|
+
const { month, syncMonth, weekStartsOn, locale, variant, system } =
|
|
1386
|
+
usePlanner('Planner.Scroller');
|
|
1387
|
+
const { week: weekRow, heading } = plannerVariants();
|
|
1388
|
+
const list = useRef<FlatList<Date>>(null);
|
|
1389
|
+
|
|
1390
|
+
/*
|
|
1391
|
+
* The range is anchored where the planner opened and never rebuilt. Rebuilding
|
|
1392
|
+
* it around the current month would move every row's index under the scroll
|
|
1393
|
+
* position each time the month changed, which is the one thing a list being
|
|
1394
|
+
* scrolled must not do.
|
|
1395
|
+
*/
|
|
1396
|
+
const [anchor] = useState(() => month);
|
|
1397
|
+
const range = useMemo(
|
|
1398
|
+
() => weekRange(anchor, weekStartsOn, span, span),
|
|
1399
|
+
[anchor, weekStartsOn, span]
|
|
1400
|
+
);
|
|
1401
|
+
|
|
1402
|
+
const headings = useMemo(
|
|
1403
|
+
() => weekdayNames(locale, weekStartsOn, 'narrow'),
|
|
1404
|
+
[locale, weekStartsOn]
|
|
1405
|
+
);
|
|
1406
|
+
|
|
1407
|
+
/*
|
|
1408
|
+
* Which month the scroll is showing, reported up but not acted on here. The
|
|
1409
|
+
* ref is what stops a scroll that stays inside one month from setting it over
|
|
1410
|
+
* and over, and what stops the scroll this component performs in answer to a
|
|
1411
|
+
* month change from reporting that same month straight back.
|
|
1412
|
+
*/
|
|
1413
|
+
const reported = useRef(month.getTime());
|
|
1414
|
+
const onViewable = useRef(({ viewableItems }: { viewableItems: ViewToken[] }) => {
|
|
1415
|
+
const first = viewableItems[0]?.item as Date | undefined;
|
|
1416
|
+
if (!first) return;
|
|
1417
|
+
const next = startOfCalendarMonth(weekAnchor(first), system, locale);
|
|
1418
|
+
if (next.getTime() === reported.current) return;
|
|
1419
|
+
reported.current = next.getTime();
|
|
1420
|
+
syncMonth(next);
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
/*
|
|
1424
|
+
* A month arriving from outside — the arrows, the Today pill, a controlled
|
|
1425
|
+
* prop — scrolls the list to it. A month the scroll itself just reported is
|
|
1426
|
+
* already there, and scrolling to it again would fight the finger.
|
|
1427
|
+
*/
|
|
1428
|
+
useEffect(() => {
|
|
1429
|
+
if (month.getTime() === reported.current) return;
|
|
1430
|
+
reported.current = month.getTime();
|
|
1431
|
+
const index = weekIndex(range, month, weekStartsOn);
|
|
1432
|
+
if (index >= 0) list.current?.scrollToIndex({ index, animated: true });
|
|
1433
|
+
}, [month, range, weekStartsOn]);
|
|
1434
|
+
|
|
1435
|
+
const initialIndex = Math.max(0, weekIndex(range, month, weekStartsOn));
|
|
1436
|
+
|
|
1437
|
+
return (
|
|
1438
|
+
<View className={cn('flex-1', className)}>
|
|
1439
|
+
<View
|
|
1440
|
+
className={cn(weekRow(), 'px-1 pb-1')}
|
|
1441
|
+
accessibilityElementsHidden
|
|
1442
|
+
importantForAccessibility="no-hide-descendants"
|
|
1443
|
+
>
|
|
1444
|
+
{headings.map((label, position) => (
|
|
1445
|
+
<Text key={position} size="xs" muted className={heading()}>
|
|
1446
|
+
{label.toUpperCase()}
|
|
1447
|
+
</Text>
|
|
1448
|
+
))}
|
|
1449
|
+
</View>
|
|
1450
|
+
|
|
1451
|
+
<FlatList
|
|
1452
|
+
ref={list}
|
|
1453
|
+
data={range}
|
|
1454
|
+
keyExtractor={(weekStart) => String(weekStart.getTime())}
|
|
1455
|
+
initialScrollIndex={initialIndex}
|
|
1456
|
+
// Every row is the same height, so the list can place any of them
|
|
1457
|
+
// without measuring — which is what makes jumping to a month cheap.
|
|
1458
|
+
getItemLayout={(_unused, index) => ({
|
|
1459
|
+
length: rowHeight,
|
|
1460
|
+
offset: rowHeight * index,
|
|
1461
|
+
index,
|
|
1462
|
+
})}
|
|
1463
|
+
onViewableItemsChanged={onViewable.current}
|
|
1464
|
+
viewabilityConfig={VIEWABILITY}
|
|
1465
|
+
showsVerticalScrollIndicator={false}
|
|
1466
|
+
renderItem={({ item }) => (
|
|
1467
|
+
<View
|
|
1468
|
+
className={cn(
|
|
1469
|
+
weekRow(),
|
|
1470
|
+
'border-t border-border px-1',
|
|
1471
|
+
variant === 'calendar' && 'items-stretch'
|
|
1472
|
+
)}
|
|
1473
|
+
style={{ height: rowHeight }}
|
|
1474
|
+
>
|
|
1475
|
+
{weekDays(item).map((date) => (
|
|
1476
|
+
<PlannerDay key={date.getTime()} date={date} renderDay={renderDay} />
|
|
1477
|
+
))}
|
|
1478
|
+
</View>
|
|
1479
|
+
)}
|
|
1480
|
+
/>
|
|
1481
|
+
</View>
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
PlannerScroller.displayName = 'Planner.Scroller';
|
|
1485
|
+
|
|
1486
|
+
/*
|
|
1487
|
+
* The first row more than half on screen is the one that names the month. A
|
|
1488
|
+
* lower threshold hands the month over while the week is still mostly below the
|
|
1489
|
+
* fold, which reads as the header changing early.
|
|
1490
|
+
*/
|
|
1491
|
+
const VIEWABILITY = { itemVisiblePercentThreshold: 60 };
|
|
1492
|
+
|
|
1016
1493
|
export const Planner = Object.assign(PlannerRoot, {
|
|
1017
1494
|
Header: PlannerHeader,
|
|
1018
1495
|
Title: PlannerTitle,
|
|
@@ -1020,6 +1497,7 @@ export const Planner = Object.assign(PlannerRoot, {
|
|
|
1020
1497
|
Nav: PlannerNav,
|
|
1021
1498
|
Action: PlannerAction,
|
|
1022
1499
|
Grid: PlannerGrid,
|
|
1500
|
+
Scroller: PlannerScroller,
|
|
1023
1501
|
Day: PlannerDay,
|
|
1024
1502
|
Legend: PlannerLegend,
|
|
1025
1503
|
Summary: PlannerSummary,
|