@remit/web-client 0.0.294 → 0.0.295

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/src/components/calendar/CalendarUnavailable.tsx +56 -0
  3. package/src/components/calendar/CalendarWaiting.tsx +99 -0
  4. package/src/components/calendar/CalendarWorkspace.stories.tsx +100 -0
  5. package/src/components/calendar/CalendarWorkspace.tsx +49 -40
  6. package/src/components/calendar/PendingSuggestions.tsx +70 -0
  7. package/src/components/calendar/pending-suggestions.render.test.ts +132 -0
  8. package/src/components/mail/DailyBrief.tsx +2 -1
  9. package/src/components/mail/IntelligencePane.tsx +5 -0
  10. package/src/components/mail/authenticity-warning.stories.tsx +6 -1
  11. package/src/components/mail/intelligence-drawer.stories.tsx +11 -2
  12. package/src/components/mail/intelligence-story-calendar.ts +19 -0
  13. package/src/hooks/calendar/agenda-window.test.ts +216 -132
  14. package/src/hooks/calendar/index.ts +8 -0
  15. package/src/hooks/calendar/useCalendarSuggestions.ts +180 -0
  16. package/src/hooks/calendar/useCalendars.ts +14 -2
  17. package/src/hooks/intelligence-calendar.render.test.ts +277 -0
  18. package/src/hooks/useBriefSections.ts +2 -1
  19. package/src/hooks/useIntelligenceCalendar.ts +347 -0
  20. package/src/lib/calendar-format.ts +41 -1
  21. package/src/lib/calendar-report.ts +11 -0
  22. package/src/lib/calendar-suggestion.test.ts +169 -0
  23. package/src/lib/calendar-suggestion.ts +251 -0
  24. package/src/lib/calendar-suggestion.west.test.ts +56 -0
  25. package/src/lib/starred-rows.ts +4 -2
  26. package/src/routes/calendar/$view.$date.tsx +2 -0
  27. package/src/routing/calendar.ts +29 -0
  28. package/src/routing/index.ts +1 -0
@@ -12,6 +12,7 @@ import { expect, userEvent, within } from "storybook/test";
12
12
  import { Drawer } from "@/components/layout/Drawer";
13
13
  import { MessageToolbar } from "@/components/mail/MessageToolbar";
14
14
  import { useIntelligenceDrawer } from "@/hooks/useIntelligenceDrawer";
15
+ import { quietCalendar } from "./intelligence-story-calendar";
15
16
 
16
17
  /**
17
18
  * The intelligence drawer below the desktop tier (#777, #778).
@@ -159,7 +160,11 @@ const PhoneReader = () => {
159
160
  ariaLabel="Message details"
160
161
  side="right"
161
162
  >
162
- <IntelligencePanel data={intelligence} hideCloseButton />
163
+ <IntelligencePanel
164
+ data={intelligence}
165
+ calendar={quietCalendar}
166
+ hideCloseButton
167
+ />
163
168
  </Drawer>
164
169
  </div>
165
170
  <p className="font-mono text-2xs text-fg-subtle">
@@ -291,7 +296,11 @@ const MidWidthReader = () => {
291
296
  ariaLabel="Message details"
292
297
  side="right"
293
298
  >
294
- <IntelligencePanel data={intelligence} hideCloseButton />
299
+ <IntelligencePanel
300
+ data={intelligence}
301
+ calendar={quietCalendar}
302
+ hideCloseButton
303
+ />
295
304
  </Drawer>
296
305
  </div>
297
306
  );
@@ -0,0 +1,19 @@
1
+ import type { IntelligenceCalendarSurface } from "@remit/ui";
2
+
3
+ /**
4
+ * The calendar half the shipping pane always carries, for a message that says
5
+ * nothing about time. Stories of the Sender half mount it too, so the tab strip
6
+ * they draw is the one the app draws.
7
+ */
8
+ export const quietCalendar: IntelligenceCalendarSurface = {
9
+ data: { suggestions: [], day: [], dayLabel: "" },
10
+ actions: {
11
+ onAddInvite: () => undefined,
12
+ onDeclineInvite: () => undefined,
13
+ onOfferOtherTimes: () => undefined,
14
+ onToggleSlot: () => undefined,
15
+ onAddSuggestion: () => undefined,
16
+ onDismissSuggestion: () => undefined,
17
+ onSelectEvent: () => undefined,
18
+ },
19
+ };
@@ -7,6 +7,11 @@
7
7
  * free-time half asserts that busy spans the strip did not draw still take the
8
8
  * hours they cover — the rule itself is `@remit/ui`'s, and this only decides
9
9
  * which spans a day gets.
10
+ *
11
+ * The window a day names is a civil date, and the clock time carried on it is
12
+ * the device's own — so every case here is asserted in more than one zone. A
13
+ * window boundary asserted only in UTC would miss the hour a DST change hides
14
+ * from anything that measures it as elapsed time instead of a calendar date.
10
15
  */
11
16
  import assert from "node:assert/strict";
12
17
  import { describe, it } from "node:test";
@@ -48,41 +53,81 @@ const clocks = (
48
53
  `${Math.floor(stretch.startMinute / 60)}–${Math.floor(stretch.endMinute / 60)}`,
49
54
  );
50
55
 
56
+ /** The days between two civil dates, ignoring whatever offset each one carries. */
57
+ const daysBetween = (from: string, to: string): number =>
58
+ (Date.parse(to.slice(0, 10)) - Date.parse(from.slice(0, 10))) / 86_400_000;
59
+
60
+ /** The zones a reader's device might be on, asserted so a body cannot pass silently. */
61
+ const ZONES = [
62
+ { name: "UTC", hourAtNoonUtc: 12 },
63
+ { name: "Europe/Amsterdam", hourAtNoonUtc: 14 },
64
+ { name: "America/New_York", hourAtNoonUtc: 8 },
65
+ ];
66
+
67
+ function inEveryZone(body: () => void): void {
68
+ const before = process.env.TZ;
69
+ try {
70
+ for (const zone of ZONES) {
71
+ process.env.TZ = zone.name;
72
+ assert.equal(
73
+ new Date("2026-06-11T12:00:00Z").getHours(),
74
+ zone.hourAtNoonUtc,
75
+ `the machine never moved to ${zone.name}`,
76
+ );
77
+ body();
78
+ }
79
+ } finally {
80
+ if (before === undefined) delete process.env.TZ;
81
+ else process.env.TZ = before;
82
+ }
83
+ }
84
+
51
85
  describe("the days the strip holds", () => {
52
86
  it("opens further ahead than behind, because a diary is read forwards", () => {
53
- const range = rangeAround(DATE);
54
- assert.equal(range.from, "2026-05-31");
55
- assert.equal(range.to, "2026-07-04");
56
- assert.equal(datesInRange(range).length, LEAD_IN + LEAD_OUT + 1);
87
+ inEveryZone(() => {
88
+ const range = rangeAround(DATE);
89
+ assert.equal(range.from, "2026-05-31");
90
+ assert.equal(range.to, "2026-07-04");
91
+ assert.equal(datesInRange(range).length, LEAD_IN + LEAD_OUT + 1);
92
+ });
57
93
  });
58
94
 
59
95
  it("grows the run at whichever end was reached, keeping the other", () => {
60
- const range = rangeAround(DATE);
61
- assert.deepEqual(extendRangeStart(range), { ...range, from: "2026-05-24" });
62
- assert.deepEqual(extendRangeEnd(range), { ...range, to: "2026-07-11" });
63
- assert.equal(
64
- datesInRange(extendRangeEnd(range)).length,
65
- datesInRange(range).length + PAGE,
66
- );
96
+ inEveryZone(() => {
97
+ const range = rangeAround(DATE);
98
+ assert.deepEqual(extendRangeStart(range), {
99
+ ...range,
100
+ from: "2026-05-24",
101
+ });
102
+ assert.deepEqual(extendRangeEnd(range), { ...range, to: "2026-07-11" });
103
+ assert.equal(
104
+ datesInRange(extendRangeEnd(range)).length,
105
+ datesInRange(range).length + PAGE,
106
+ );
107
+ });
67
108
  });
68
109
 
69
110
  it("keeps the run a day is already in, so a scroll refetches nothing", () => {
70
- const range = rangeAround(DATE);
71
- for (const day of ["2026-05-31", DATE, "2026-06-28", "2026-07-04"]) {
72
- assert.equal(rangeCovering(range, day), range, `${day} moved the run`);
73
- }
111
+ inEveryZone(() => {
112
+ const range = rangeAround(DATE);
113
+ for (const day of ["2026-05-31", DATE, "2026-06-28", "2026-07-04"]) {
114
+ assert.equal(rangeCovering(range, day), range, `${day} moved the run`);
115
+ }
116
+ });
74
117
  });
75
118
 
76
119
  it("opens a window where a jump landed rather than stretching to it", () => {
77
- const range = rangeAround(DATE);
78
- assert.deepEqual(
79
- rangeCovering(range, "2026-11-02"),
80
- rangeAround("2026-11-02"),
81
- );
82
- assert.deepEqual(
83
- rangeCovering(range, "2026-01-05"),
84
- rangeAround("2026-01-05"),
85
- );
120
+ inEveryZone(() => {
121
+ const range = rangeAround(DATE);
122
+ assert.deepEqual(
123
+ rangeCovering(range, "2026-11-02"),
124
+ rangeAround("2026-11-02"),
125
+ );
126
+ assert.deepEqual(
127
+ rangeCovering(range, "2026-01-05"),
128
+ rangeAround("2026-01-05"),
129
+ );
130
+ });
86
131
  });
87
132
  });
88
133
 
@@ -113,41 +158,51 @@ describe("how far the run grows before it asks", () => {
113
158
  };
114
159
 
115
160
  it("stops a year either way, however often an end is reached", () => {
116
- const range = reachEnd(reachStart());
117
- assert.equal(range.from, addDays(DATE, -CAP_DAYS));
118
- assert.equal(range.to, addDays(DATE, CAP_DAYS));
119
- assert.equal(rangeAtFloor(range), true);
120
- assert.equal(rangeAtCeiling(range), true);
161
+ inEveryZone(() => {
162
+ const range = reachEnd(reachStart());
163
+ assert.equal(range.from, addDays(DATE, -CAP_DAYS));
164
+ assert.equal(range.to, addDays(DATE, CAP_DAYS));
165
+ assert.equal(rangeAtFloor(range), true);
166
+ assert.equal(rangeAtCeiling(range), true);
167
+ });
121
168
  });
122
169
 
123
170
  it("hands back the run it was given at the cap, so nothing refetches", () => {
124
- const capped = reachEnd();
125
- assert.equal(extendRangeEnd(capped), capped);
126
- const behind = reachStart();
127
- assert.equal(extendRangeStart(behind), behind);
171
+ inEveryZone(() => {
172
+ const capped = reachEnd();
173
+ assert.equal(extendRangeEnd(capped), capped);
174
+ const behind = reachStart();
175
+ assert.equal(extendRangeStart(behind), behind);
176
+ });
128
177
  });
129
178
 
130
179
  it("opens the next year where the reader asked, keeping the days held", () => {
131
- const capped = reachEnd();
132
- const more = liftRangeCeiling(capped);
133
- assert.equal(more.from, capped.from);
134
- assert.equal(more.to, addDays(capped.to, PAGE));
135
- assert.equal(rangeAtCeiling(more), false);
136
- assert.equal(rangeAtFloor(more), false);
180
+ inEveryZone(() => {
181
+ const capped = reachEnd();
182
+ const more = liftRangeCeiling(capped);
183
+ assert.equal(more.from, capped.from);
184
+ assert.equal(more.to, addDays(capped.to, PAGE));
185
+ assert.equal(rangeAtCeiling(more), false);
186
+ assert.equal(rangeAtFloor(more), false);
187
+ });
137
188
  });
138
189
 
139
190
  it("does the same behind, without moving the end ahead", () => {
140
- const capped = reachStart();
141
- const more = liftRangeFloor(capped);
142
- assert.equal(more.to, capped.to);
143
- assert.equal(more.from, addDays(capped.from, -PAGE));
144
- assert.equal(rangeAtFloor(more), false);
191
+ inEveryZone(() => {
192
+ const capped = reachStart();
193
+ const more = liftRangeFloor(capped);
194
+ assert.equal(more.to, capped.to);
195
+ assert.equal(more.from, addDays(capped.from, -PAGE));
196
+ assert.equal(rangeAtFloor(more), false);
197
+ });
145
198
  });
146
199
 
147
200
  it("gives a jump its own year, measured from where it landed", () => {
148
- const jumped = rangeCovering(reachEnd(), "2028-03-01");
149
- assert.deepEqual(jumped, rangeAround("2028-03-01"));
150
- assert.equal(rangeAtCeiling(jumped), false);
201
+ inEveryZone(() => {
202
+ const jumped = rangeCovering(reachEnd(), "2028-03-01");
203
+ assert.deepEqual(jumped, rangeAround("2028-03-01"));
204
+ assert.equal(rangeAtCeiling(jumped), false);
205
+ });
151
206
  });
152
207
  });
153
208
 
@@ -159,14 +214,16 @@ describe("how far the run grows before it asks", () => {
159
214
  */
160
215
  describe("the weeks a range is fetched as", () => {
161
216
  it("is the grid's own window for each week, once each", () => {
162
- const windows = weekWindowsOver(datesInRange(rangeAround(DATE)));
163
- for (const window of windows) {
164
- assert.deepEqual(
165
- window,
166
- calendarWindow("week", window.from.slice(0, 10)),
167
- );
168
- }
169
- assert.equal(new Set(windows.map((w) => w.from)).size, windows.length);
217
+ inEveryZone(() => {
218
+ const windows = weekWindowsOver(datesInRange(rangeAround(DATE)));
219
+ for (const window of windows) {
220
+ assert.deepEqual(
221
+ window,
222
+ calendarWindow("week", window.from.slice(0, 10)),
223
+ );
224
+ }
225
+ assert.equal(new Set(windows.map((w) => w.from)).size, windows.length);
226
+ });
170
227
  });
171
228
 
172
229
  /**
@@ -175,49 +232,58 @@ describe("the weeks a range is fetched as", () => {
175
232
  * scrolled to.
176
233
  */
177
234
  it("is the six weeks the opening run falls in, and no more", () => {
178
- assert.equal(weekWindowsOver(datesInRange(rangeAround(DATE))).length, 6);
235
+ inEveryZone(() => {
236
+ assert.equal(weekWindowsOver(datesInRange(rangeAround(DATE))).length, 6);
237
+ });
179
238
  });
180
239
 
181
240
  it("covers every day the strip holds and nothing before or after", () => {
182
- const dates = datesInRange(rangeAround(DATE));
183
- const held = new Set(weekWindowsOver(dates).map((window) => window.from));
184
- for (const date of dates) assert.ok(held.has(weekKeyOf(date)));
241
+ inEveryZone(() => {
242
+ const dates = datesInRange(rangeAround(DATE));
243
+ const held = new Set(weekWindowsOver(dates).map((window) => window.from));
244
+ for (const date of dates) assert.ok(held.has(weekKeyOf(date)));
245
+ });
185
246
  });
186
247
 
187
248
  it("names the week a day belongs to, Monday to Monday", () => {
188
- // 2026-06-10 is a Wednesday; its week opens on the 8th.
189
- assert.equal(weekKeyOf("2026-06-10"), startOfDay("2026-06-08"));
190
- assert.equal(weekKeyOf("2026-06-08"), startOfDay("2026-06-08"));
191
- assert.equal(weekKeyOf("2026-06-14"), startOfDay("2026-06-08"));
192
- assert.equal(weekKeyOf("2026-06-15"), startOfDay("2026-06-15"));
249
+ inEveryZone(() => {
250
+ // 2026-06-10 is a Wednesday; its week opens on the 8th.
251
+ assert.equal(weekKeyOf("2026-06-10"), startOfDay("2026-06-08"));
252
+ assert.equal(weekKeyOf("2026-06-08"), startOfDay("2026-06-08"));
253
+ assert.equal(weekKeyOf("2026-06-14"), startOfDay("2026-06-08"));
254
+ assert.equal(weekKeyOf("2026-06-15"), startOfDay("2026-06-15"));
255
+ });
193
256
  });
194
257
 
195
258
  it("asks for one week however far the reader scrolls", () => {
196
- // Past the server's 366-day ceiling in both directions.
197
- let range = rangeAround(DATE);
198
- for (let reach = 0; reach < 30; reach += 1) {
199
- range = extendRangeEnd(extendRangeStart(range));
200
- }
201
- const dates = datesInRange(range);
202
- assert.ok(dates.length > 366, "the range never grew past the ceiling");
203
-
204
- for (const window of weekWindowsOver(dates)) {
205
- const days =
206
- (Date.parse(window.to) - Date.parse(window.from)) / 86_400_000;
207
- assert.equal(days, 7, `a window covered ${days} days`);
208
- }
259
+ inEveryZone(() => {
260
+ // Past the server's 366-day ceiling in both directions.
261
+ let range = rangeAround(DATE);
262
+ for (let reach = 0; reach < 30; reach += 1) {
263
+ range = extendRangeEnd(extendRangeStart(range));
264
+ }
265
+ const dates = datesInRange(range);
266
+ assert.ok(dates.length > 366, "the range never grew past the ceiling");
267
+
268
+ for (const window of weekWindowsOver(dates)) {
269
+ const days = daysBetween(window.from, window.to);
270
+ assert.equal(days, 7, `a window covered ${days} days`);
271
+ }
272
+ });
209
273
  });
210
274
 
211
275
  it("grows by adding weeks rather than by widening one", () => {
212
- const opening = weekWindowsOver(datesInRange(rangeAround(DATE)));
213
- const wider = weekWindowsOver(
214
- datesInRange(extendRangeStart(rangeAround(DATE))),
215
- );
216
- const kept = new Set(wider.map((window) => window.from));
217
- for (const window of opening) {
218
- assert.ok(kept.has(window.from), `${window.from} was given up`);
219
- }
220
- assert.ok(wider.length > opening.length);
276
+ inEveryZone(() => {
277
+ const opening = weekWindowsOver(datesInRange(rangeAround(DATE)));
278
+ const wider = weekWindowsOver(
279
+ datesInRange(extendRangeStart(rangeAround(DATE))),
280
+ );
281
+ const kept = new Set(wider.map((window) => window.from));
282
+ for (const window of opening) {
283
+ assert.ok(kept.has(window.from), `${window.from} was given up`);
284
+ }
285
+ assert.ok(wider.length > opening.length);
286
+ });
221
287
  });
222
288
  });
223
289
 
@@ -225,50 +291,62 @@ describe("busy time, split into the days it covers", () => {
225
291
  const dates = ["2026-06-10", "2026-06-11", "2026-06-12"];
226
292
 
227
293
  it("reads a span as minutes of the day it falls on", () => {
228
- const byDate = busySpansByDate(dates, [
229
- { start: at("2026-06-10", "09:00"), end: at("2026-06-10", "10:30") },
230
- ]);
231
- assert.deepEqual(byDate.get("2026-06-10"), [{ from: 540, to: 630 }]);
232
- assert.equal(byDate.get("2026-06-11"), undefined);
294
+ inEveryZone(() => {
295
+ const byDate = busySpansByDate(dates, [
296
+ { start: at("2026-06-10", "09:00"), end: at("2026-06-10", "10:30") },
297
+ ]);
298
+ assert.deepEqual(byDate.get("2026-06-10"), [{ from: 540, to: 630 }]);
299
+ assert.equal(byDate.get("2026-06-11"), undefined);
300
+ });
233
301
  });
234
302
 
235
303
  it("gives a span crossing midnight one entry per day it touches", () => {
236
- const byDate = busySpansByDate(dates, [
237
- { start: at("2026-06-10", "22:00"), end: at("2026-06-11", "02:00") },
238
- ]);
239
- assert.deepEqual(byDate.get("2026-06-10"), [{ from: 1320, to: 1440 }]);
240
- assert.deepEqual(byDate.get("2026-06-11"), [{ from: 0, to: 120 }]);
304
+ inEveryZone(() => {
305
+ const byDate = busySpansByDate(dates, [
306
+ { start: at("2026-06-10", "22:00"), end: at("2026-06-11", "02:00") },
307
+ ]);
308
+ assert.deepEqual(byDate.get("2026-06-10"), [{ from: 1320, to: 1440 }]);
309
+ assert.deepEqual(byDate.get("2026-06-11"), [{ from: 0, to: 120 }]);
310
+ });
241
311
  });
242
312
 
243
313
  it("leaves no entry on the day a span merely ends at midnight of", () => {
244
- const byDate = busySpansByDate(dates, [
245
- { start: at("2026-06-10", "22:00"), end: at("2026-06-11", "00:00") },
246
- ]);
247
- assert.deepEqual(byDate.get("2026-06-10"), [{ from: 1320, to: 1440 }]);
248
- assert.equal(byDate.get("2026-06-11"), undefined);
314
+ inEveryZone(() => {
315
+ const byDate = busySpansByDate(dates, [
316
+ { start: at("2026-06-10", "22:00"), end: at("2026-06-11", "00:00") },
317
+ ]);
318
+ assert.deepEqual(byDate.get("2026-06-10"), [{ from: 1320, to: 1440 }]);
319
+ assert.equal(byDate.get("2026-06-11"), undefined);
320
+ });
249
321
  });
250
322
 
251
323
  it("merges spans that run into each other into the hours they cover", () => {
252
- const byDate = busySpansByDate(dates, [
253
- { start: at("2026-06-10", "11:00"), end: at("2026-06-10", "12:00") },
254
- { start: at("2026-06-10", "09:00"), end: at("2026-06-10", "11:30") },
255
- ]);
256
- assert.deepEqual(byDate.get("2026-06-10"), [{ from: 540, to: 720 }]);
324
+ inEveryZone(() => {
325
+ const byDate = busySpansByDate(dates, [
326
+ { start: at("2026-06-10", "11:00"), end: at("2026-06-10", "12:00") },
327
+ { start: at("2026-06-10", "09:00"), end: at("2026-06-10", "11:30") },
328
+ ]);
329
+ assert.deepEqual(byDate.get("2026-06-10"), [{ from: 540, to: 720 }]);
330
+ });
257
331
  });
258
332
 
259
333
  it("drops a span that is not a stretch of time at all", () => {
260
- const byDate = busySpansByDate(dates, [
261
- { start: at("2026-06-10", "11:00"), end: at("2026-06-10", "11:00") },
262
- { start: "not a date", end: at("2026-06-10", "12:00") },
263
- ]);
264
- assert.equal(byDate.size, 0);
334
+ inEveryZone(() => {
335
+ const byDate = busySpansByDate(dates, [
336
+ { start: at("2026-06-10", "11:00"), end: at("2026-06-10", "11:00") },
337
+ { start: "not a date", end: at("2026-06-10", "12:00") },
338
+ ]);
339
+ assert.equal(byDate.size, 0);
340
+ });
265
341
  });
266
342
 
267
343
  it("ignores busy time outside the days on screen", () => {
268
- const byDate = busySpansByDate(dates, [
269
- { start: at("2026-07-01", "09:00"), end: at("2026-07-01", "10:00") },
270
- ]);
271
- assert.equal(byDate.size, 0);
344
+ inEveryZone(() => {
345
+ const byDate = busySpansByDate(dates, [
346
+ { start: at("2026-07-01", "09:00"), end: at("2026-07-01", "10:00") },
347
+ ]);
348
+ assert.equal(byDate.size, 0);
349
+ });
272
350
  });
273
351
  });
274
352
 
@@ -276,9 +354,11 @@ describe("free time, off the spans the server merged", () => {
276
354
  const dates = ["2026-06-10", "2026-06-11"];
277
355
 
278
356
  it("answers every day the strip holds, including the untouched ones", () => {
279
- const free = freeStretchesByDate(dates, []);
280
- assert.deepEqual([...free.keys()], dates);
281
- assert.equal(free.get("2026-06-11")?.[0].wholeDay, true);
357
+ inEveryZone(() => {
358
+ const free = freeStretchesByDate(dates, []);
359
+ assert.deepEqual([...free.keys()], dates);
360
+ assert.equal(free.get("2026-06-11")?.[0].wholeDay, true);
361
+ });
282
362
  });
283
363
 
284
364
  /**
@@ -287,22 +367,26 @@ describe("free time, off the spans the server merged", () => {
287
367
  * this view must never say.
288
368
  */
289
369
  it("takes hours out of a day the strip is drawing nothing on", () => {
290
- const free = freeStretchesByDate(dates, [
291
- { start: at("2026-06-10", "09:00"), end: at("2026-06-10", "12:00") },
292
- { start: at("2026-06-10", "13:00"), end: at("2026-06-10", "16:00") },
293
- ]);
294
- assert.deepEqual(clocks(free.get("2026-06-10") ?? []), ["16–22"]);
295
- assert.equal(free.get("2026-06-10")?.[0].wholeDay, false);
370
+ inEveryZone(() => {
371
+ const free = freeStretchesByDate(dates, [
372
+ { start: at("2026-06-10", "09:00"), end: at("2026-06-10", "12:00") },
373
+ { start: at("2026-06-10", "13:00"), end: at("2026-06-10", "16:00") },
374
+ ]);
375
+ assert.deepEqual(clocks(free.get("2026-06-10") ?? []), ["16–22"]);
376
+ assert.equal(free.get("2026-06-10")?.[0].wholeDay, false);
377
+ });
296
378
  });
297
379
 
298
380
  it("is the rule `@remit/ui` states, applied to those spans", () => {
299
- const spans = [{ from: 9 * 60, to: 12 * 60 }];
300
- const free = freeStretchesByDate(dates, [
301
- { start: at("2026-06-10", "09:00"), end: at("2026-06-10", "12:00") },
302
- ]);
303
- assert.deepEqual(
304
- free.get("2026-06-10"),
305
- freeStretchesFromSpans("2026-06-10", spans),
306
- );
381
+ inEveryZone(() => {
382
+ const spans = [{ from: 9 * 60, to: 12 * 60 }];
383
+ const free = freeStretchesByDate(dates, [
384
+ { start: at("2026-06-10", "09:00"), end: at("2026-06-10", "12:00") },
385
+ ]);
386
+ assert.deepEqual(
387
+ free.get("2026-06-10"),
388
+ freeStretchesFromSpans("2026-06-10", spans),
389
+ );
390
+ });
307
391
  });
308
392
  });
@@ -79,6 +79,14 @@ export {
79
79
  type CalendarFreeBusyResult,
80
80
  useCalendarFreeBusyWeeks,
81
81
  } from "./useCalendarFreeBusy";
82
+ export {
83
+ type CalendarSuggestionAnswers,
84
+ type CalendarSuggestionsResult,
85
+ type SuggestionAnswer,
86
+ useCalendarSuggestionAnswers,
87
+ useMessageCalendarSuggestions,
88
+ usePendingCalendarSuggestions,
89
+ } from "./useCalendarSuggestions";
82
90
  export { type CalendarsResult, useCalendars } from "./useCalendars";
83
91
  export {
84
92
  type CalendarWriteOutcome,