@remit/web-client 0.0.232 → 0.0.234
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/package.json +1 -1
- package/src/components/calendar/AgendaStrip.stories.tsx +263 -0
- package/src/components/calendar/AgendaStrip.tsx +162 -0
- package/src/components/calendar/AgendaView.tsx +241 -0
- package/src/components/calendar/CalendarViewPlaceholder.tsx +3 -4
- package/src/components/calendar/CalendarWorkspace.stories.tsx +12 -1
- package/src/components/calendar/CalendarWorkspace.tsx +56 -38
- package/src/components/calendar/agenda-address.render.test.ts +493 -0
- package/src/components/calendar/agenda-strip.render.test.ts +241 -0
- package/src/components/calendar/calendar-workspace.render.test.ts +13 -6
- package/src/hooks/calendar/agenda-window.test.ts +308 -0
- package/src/hooks/calendar/agenda-window.ts +223 -0
- package/src/hooks/calendar/index.ts +34 -0
- package/src/hooks/calendar/selection.ts +107 -0
- package/src/hooks/calendar/useCalendarEvents.ts +74 -3
- package/src/hooks/calendar/useCalendarFreeBusy.ts +80 -0
- package/src/hooks/calendar/window.ts +8 -3
- package/src/hooks/useCalendarData.ts +27 -53
- package/src/lib/calendar-density.ts +13 -1
- package/src/lib/calendar-route.test.ts +20 -1
- package/src/lib/calendar-route.ts +14 -4
- package/src/routes/calendar/$view.$date.tsx +9 -1
- package/src/routing/calendar.ts +17 -0
|
@@ -11,16 +11,13 @@ import type {
|
|
|
11
11
|
CalendarEventData,
|
|
12
12
|
CalendarViewId,
|
|
13
13
|
} from "@remit/ui";
|
|
14
|
-
import { useMemo } from "react";
|
|
15
14
|
import {
|
|
16
15
|
type CalendarInstanceRef,
|
|
17
16
|
calendarWindow,
|
|
18
|
-
isDrawnInstance,
|
|
19
17
|
readCalendarInstanceId,
|
|
20
|
-
toCalendarEventData,
|
|
21
|
-
UNZONED_CALENDAR,
|
|
22
18
|
useCalendarEventWindow,
|
|
23
|
-
|
|
19
|
+
useCalendarSelection,
|
|
20
|
+
useDrawnEvents,
|
|
24
21
|
usePrefetchAdjacentWindows,
|
|
25
22
|
} from "@/hooks/calendar";
|
|
26
23
|
|
|
@@ -30,6 +27,13 @@ export interface CalendarDataRequest {
|
|
|
30
27
|
date: string;
|
|
31
28
|
/** The calendars the address has ticked; empty means every one of them. */
|
|
32
29
|
calendarIds: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* False where the zoom on screen draws nothing from this window. The agenda
|
|
32
|
+
* rolls its own range a week at a time, so asking for this view's week as
|
|
33
|
+
* well would fetch a window nothing renders — and the address rewriting on
|
|
34
|
+
* every scroll would ask again, with its neighbours, all the way down.
|
|
35
|
+
*/
|
|
36
|
+
enabled?: boolean;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
export interface CalendarData {
|
|
@@ -46,66 +50,36 @@ export interface CalendarData {
|
|
|
46
50
|
instanceOf: (eventId: string) => CalendarInstanceRef;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
/**
|
|
50
|
-
* The calendars a request is asking about. An empty tick list is every
|
|
51
|
-
* calendar rather than none: a reader who has ticked nothing off is looking at
|
|
52
|
-
* all of them, and a URL naming a calendar that no longer exists shows the
|
|
53
|
-
* rest instead of an empty week.
|
|
54
|
-
*/
|
|
55
|
-
export function selectCalendarIds(
|
|
56
|
-
calendars: readonly CalendarDescriptor[],
|
|
57
|
-
ticked: readonly string[],
|
|
58
|
-
): string[] {
|
|
59
|
-
const known = calendars.map((calendar) => calendar.id);
|
|
60
|
-
if (ticked.length === 0) return known;
|
|
61
|
-
const wanted = new Set(ticked);
|
|
62
|
-
const shown = known.filter((id) => wanted.has(id));
|
|
63
|
-
return shown.length === 0 ? known : shown;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
53
|
export function useCalendarData({
|
|
67
54
|
view,
|
|
68
55
|
date,
|
|
69
56
|
calendarIds,
|
|
57
|
+
enabled = true,
|
|
70
58
|
}: CalendarDataRequest): CalendarData {
|
|
71
|
-
const {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// narrowed address waits for them rather than asking about ids it cannot
|
|
81
|
-
// know are real.
|
|
82
|
-
const resolved = calendarIds.length === 0 || calendars.length > 0;
|
|
59
|
+
const {
|
|
60
|
+
calendars,
|
|
61
|
+
colorByCalendarId,
|
|
62
|
+
timeZoneByCalendarId,
|
|
63
|
+
shown,
|
|
64
|
+
narrowed,
|
|
65
|
+
resolved,
|
|
66
|
+
isLoading,
|
|
67
|
+
} = useCalendarSelection(calendarIds);
|
|
83
68
|
|
|
84
69
|
const window = calendarWindow(view, date);
|
|
70
|
+
const active = enabled && resolved;
|
|
85
71
|
const events = useCalendarEventWindow({
|
|
86
72
|
...window,
|
|
87
73
|
calendarIds: narrowed,
|
|
88
|
-
enabled:
|
|
74
|
+
enabled: active,
|
|
89
75
|
});
|
|
90
|
-
usePrefetchAdjacentWindows(view, date, narrowed,
|
|
76
|
+
usePrefetchAdjacentWindows(view, date, narrowed, active);
|
|
91
77
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return instances
|
|
98
|
-
.filter(
|
|
99
|
-
(instance) =>
|
|
100
|
-
isDrawnInstance(instance) && drawn.has(instance.calendarId),
|
|
101
|
-
)
|
|
102
|
-
.map((instance) =>
|
|
103
|
-
toCalendarEventData(
|
|
104
|
-
instance,
|
|
105
|
-
timeZoneByCalendarId[instance.calendarId] ?? UNZONED_CALENDAR,
|
|
106
|
-
),
|
|
107
|
-
);
|
|
108
|
-
}, [instances, timeZoneByCalendarId, shownKey]);
|
|
78
|
+
const eventData = useDrawnEvents(
|
|
79
|
+
events.instances,
|
|
80
|
+
timeZoneByCalendarId,
|
|
81
|
+
shown,
|
|
82
|
+
);
|
|
109
83
|
|
|
110
84
|
return {
|
|
111
85
|
calendars,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* way the intelligence rail's preference is (`lib/intelligence-pref.ts`), and
|
|
9
9
|
* a storage failure falls back to the default rather than crashing.
|
|
10
10
|
*/
|
|
11
|
-
import type { Density } from "@remit/ui";
|
|
11
|
+
import type { AgendaDensity, Density } from "@remit/ui";
|
|
12
12
|
|
|
13
13
|
export const CALENDAR_DENSITY_KEY = "remit:calendar-density";
|
|
14
14
|
|
|
@@ -32,3 +32,15 @@ export function writeCalendarDensity(density: Density): void {
|
|
|
32
32
|
// Storage unavailable — the in-memory default stands.
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The same preference, as the strip reads it.
|
|
38
|
+
*
|
|
39
|
+
* The strip has three readings and the preference has two, so the third — a
|
|
40
|
+
* month of coloured dots with no titles on it — is not reachable from Glance.
|
|
41
|
+
* That is deliberate: the preference says how much of a day this screen shows,
|
|
42
|
+
* not whether to say what is on it, and one calendar-wide answer beats a second
|
|
43
|
+
* control that means something different at every zoom.
|
|
44
|
+
*/
|
|
45
|
+
export const agendaDensityOf = (density: Density): AgendaDensity =>
|
|
46
|
+
density === "compact" ? "pills" : "detail";
|
|
@@ -8,6 +8,7 @@ import { describe, it } from "node:test";
|
|
|
8
8
|
import { CALENDAR_DENSITY_KEY } from "./calendar-density.js";
|
|
9
9
|
import {
|
|
10
10
|
calendarSearchSchema,
|
|
11
|
+
calendarViewMountsAgenda,
|
|
11
12
|
calendarViewMountsGrid,
|
|
12
13
|
calendarViews,
|
|
13
14
|
canonicalCalendarParams,
|
|
@@ -89,13 +90,31 @@ describe("calendarViewMountsGrid", () => {
|
|
|
89
90
|
assert.equal(calendarViewMountsGrid("day"), true);
|
|
90
91
|
});
|
|
91
92
|
|
|
92
|
-
it("
|
|
93
|
+
it("leaves the agenda to the strip and the other two to a later stage", () => {
|
|
93
94
|
assert.equal(calendarViewMountsGrid("year"), false);
|
|
94
95
|
assert.equal(calendarViewMountsGrid("month"), false);
|
|
95
96
|
assert.equal(calendarViewMountsGrid("agenda"), false);
|
|
96
97
|
});
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
describe("calendarViewMountsAgenda", () => {
|
|
101
|
+
it("draws the strip at one zoom and at no other", () => {
|
|
102
|
+
for (const view of calendarViews) {
|
|
103
|
+
assert.equal(calendarViewMountsAgenda(view), view === "agenda");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("never draws the same zoom as the grid", () => {
|
|
108
|
+
for (const view of calendarViews) {
|
|
109
|
+
assert.equal(
|
|
110
|
+
calendarViewMountsAgenda(view) && calendarViewMountsGrid(view),
|
|
111
|
+
false,
|
|
112
|
+
`${view} claimed two surfaces`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
99
118
|
describe("stepCalendarDate", () => {
|
|
100
119
|
it("steps a screenful in the view's own unit", () => {
|
|
101
120
|
assert.equal(stepCalendarDate("2026-06-10", "day", 1), "2026-06-11");
|
|
@@ -32,15 +32,24 @@ const calendarViewSchema = z.enum(calendarViews);
|
|
|
32
32
|
export const DEFAULT_CALENDAR_VIEW: CalendarViewId = "week";
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* The views the grid draws
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* The views the grid draws. Year and month are named surfaces that arrive in
|
|
36
|
+
* later stages, and they say so on screen rather than matching a route that
|
|
37
|
+
* renders nothing.
|
|
38
38
|
*/
|
|
39
39
|
const GRID_VIEWS = new Set<CalendarViewId>(["week", "day"]);
|
|
40
40
|
|
|
41
41
|
export const calendarViewMountsGrid = (view: CalendarViewId): boolean =>
|
|
42
42
|
GRID_VIEWS.has(view);
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The one view the strip draws. It is a zoom of the same ladder rather than a
|
|
46
|
+
* different screen: the grid is what a reader drops into for a day that has
|
|
47
|
+
* earned it, and the strip is what they come back out to, with the day and the
|
|
48
|
+
* ticked calendars they left with.
|
|
49
|
+
*/
|
|
50
|
+
export const calendarViewMountsAgenda = (view: CalendarViewId): boolean =>
|
|
51
|
+
view === "agenda";
|
|
52
|
+
|
|
44
53
|
/** The view a segment names, or `undefined` where it names none. */
|
|
45
54
|
export function parseCalendarView(segment: string): CalendarViewId | undefined {
|
|
46
55
|
const parsed = calendarViewSchema.safeParse(segment);
|
|
@@ -101,7 +110,8 @@ export function canonicalCalendarParams(
|
|
|
101
110
|
/**
|
|
102
111
|
* The day the previous or next screenful starts on. A step is measured in the
|
|
103
112
|
* view's own unit, which is what makes back-and-forward land where the reader
|
|
104
|
-
* came from at every zoom. Agenda steps a week,
|
|
113
|
+
* came from at every zoom. Agenda steps a week, which is far enough to be worth
|
|
114
|
+
* a press and near enough to stay inside the days it is already holding.
|
|
105
115
|
*/
|
|
106
116
|
export function stepCalendarDate(
|
|
107
117
|
date: string,
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import type { CalendarSlotPick } from "@remit/ui";
|
|
17
17
|
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
|
|
18
18
|
import { useCallback, useState } from "react";
|
|
19
|
+
import { AgendaView } from "@/components/calendar/AgendaView";
|
|
19
20
|
import { CalendarComposeSeedProvider } from "@/components/calendar/CalendarComposeSeed";
|
|
20
21
|
import { CalendarShell } from "@/components/calendar/CalendarShell";
|
|
21
22
|
import { CalendarWorkspace } from "@/components/calendar/CalendarWorkspace";
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
} from "@/lib/calendar-density";
|
|
28
29
|
import {
|
|
29
30
|
calendarSearchSchema,
|
|
31
|
+
calendarViewMountsAgenda,
|
|
30
32
|
canonicalCalendarParams,
|
|
31
33
|
isoDate,
|
|
32
34
|
} from "@/lib/calendar-route";
|
|
@@ -55,8 +57,13 @@ export const Route = createFileRoute("/calendar/$view/$date")({
|
|
|
55
57
|
|
|
56
58
|
function CalendarViewLayout() {
|
|
57
59
|
const { view, date, calendarIds } = useCalendarAddress();
|
|
60
|
+
// The strip fetches the days it holds a week at a time and draws none of
|
|
61
|
+
// this, so at that zoom the layout asks for nothing: the address rewrites on
|
|
62
|
+
// every scroll, and each rewrite would otherwise fetch a week nobody renders
|
|
63
|
+
// along with the two beside it.
|
|
64
|
+
const drawsGrid = !calendarViewMountsAgenda(view);
|
|
58
65
|
const { events, colorByCalendarId, isLoading, error, retry, instanceOf } =
|
|
59
|
-
useCalendarData({ view, date, calendarIds });
|
|
66
|
+
useCalendarData({ view, date, calendarIds, enabled: drawsGrid });
|
|
60
67
|
const { goToView, goToToday, step, openEvent, openComposer } =
|
|
61
68
|
useCalendarNavigation();
|
|
62
69
|
const openedEvent = useOpenCalendarEvent();
|
|
@@ -93,6 +100,7 @@ function CalendarViewLayout() {
|
|
|
93
100
|
date={date}
|
|
94
101
|
events={events}
|
|
95
102
|
colorByCalendarId={colorByCalendarId}
|
|
103
|
+
agenda={<AgendaView density={density} onPickSlot={pickSlot} />}
|
|
96
104
|
isLoading={isLoading}
|
|
97
105
|
error={error}
|
|
98
106
|
onRetry={retry}
|
package/src/routing/calendar.ts
CHANGED
|
@@ -82,6 +82,14 @@ export interface CalendarNavigation {
|
|
|
82
82
|
step: (direction: -1 | 1) => void;
|
|
83
83
|
/** Home, from every view and every distance away. */
|
|
84
84
|
goToToday: () => void;
|
|
85
|
+
/**
|
|
86
|
+
* The day the strip has scrolled to, written by `replace`. Scrolling is not
|
|
87
|
+
* somewhere the reader went — Back belongs to the screen they arrived from,
|
|
88
|
+
* not to every row they passed on the way down.
|
|
89
|
+
*/
|
|
90
|
+
setAnchorDate: (date: string) => void;
|
|
91
|
+
/** Out of the list and into the grid for one day. */
|
|
92
|
+
zoomToDay: (date: string) => void;
|
|
85
93
|
/**
|
|
86
94
|
* One occurrence names the series it belongs to and the instance under it;
|
|
87
95
|
* a resource that does not recur names only itself.
|
|
@@ -122,6 +130,15 @@ export function useCalendarNavigation(): CalendarNavigation {
|
|
|
122
130
|
step: (direction: -1 | 1) =>
|
|
123
131
|
goTo({ view, date: stepCalendarDate(date, view, direction) }),
|
|
124
132
|
goToToday: () => goTo({ view, date: isoDate(new Date()) }),
|
|
133
|
+
setAnchorDate: (next: string) =>
|
|
134
|
+
navigate({
|
|
135
|
+
to: VIEW_ROUTE,
|
|
136
|
+
params: { view, date: next },
|
|
137
|
+
search: true,
|
|
138
|
+
hash: retainPanels,
|
|
139
|
+
replace: true,
|
|
140
|
+
}),
|
|
141
|
+
zoomToDay: (next: string) => goTo({ view: "day", date: next }),
|
|
125
142
|
openEvent: (calendarObjectId: string, recurrenceId?: string) =>
|
|
126
143
|
recurrenceId === undefined || recurrenceId === ""
|
|
127
144
|
? navigate({
|