@remit/web-client 0.0.306 → 0.0.307

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.306",
3
+ "version": "0.0.307",
4
4
  "type": "module",
5
5
  "description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
6
6
  "exports": {
@@ -1,13 +1,9 @@
1
1
  import type { Density } from "@remit/ui";
2
2
  import type { Meta, StoryObj } from "@storybook/react-vite";
3
3
  import { expect, within } from "storybook/test";
4
+ import { instancesWithin } from "@/mocks/story-frame/calendar-window";
4
5
  import { AgendaView } from "./AgendaView";
5
- import {
6
- calendars,
7
- fortnight,
8
- instancesWithin,
9
- STORY_DATE,
10
- } from "./calendar-story-fixtures";
6
+ import { calendars, fortnight, STORY_DATE } from "./calendar-story-fixtures";
11
7
  import {
12
8
  type CalendarServer,
13
9
  CalendarStory,
@@ -1,10 +1,10 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
2
  import { expect, userEvent, within } from "storybook/test";
3
+ import { instancesWithin } from "@/mocks/story-frame/calendar-window";
3
4
  import { useOpenCalendarEvent } from "@/routing";
4
5
  import {
5
6
  calendars,
6
7
  fortnight,
7
- instancesWithin,
8
8
  ROADMAP_OBJECT,
9
9
  roadmapResource,
10
10
  STANDUP_OBJECT,
@@ -0,0 +1,119 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, userEvent, within } from "storybook/test";
3
+ import { AppStory } from "@/mocks/story-frame/AppStory";
4
+ import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
5
+ import { mailWorld } from "@/mocks/story-frame/mail-world";
6
+ import {
7
+ calendars,
8
+ fortnight,
9
+ ROADMAP_OBJECT,
10
+ roadmapResource,
11
+ STANDUP_OBJECT,
12
+ STANDUP_RECURRENCE,
13
+ STORY_DATE,
14
+ STORY_WEEK,
15
+ standupResource,
16
+ } from "./calendar-story-fixtures";
17
+
18
+ const world = mailWorld({
19
+ calendars,
20
+ calendarEvents: fortnight,
21
+ calendarResources: [roadmapResource, standupResource],
22
+ });
23
+ const desktop = { viewport: { value: "desktop", isRotated: false } };
24
+ const phone = { viewport: { value: "mobile", isRotated: false } };
25
+
26
+ const meta = {
27
+ title: "Playground/Shipped/Calendar/App",
28
+ component: AppStory,
29
+ args: { url: STORY_WEEK },
30
+ parameters: {
31
+ layout: "fullscreen",
32
+ msw: { handlers: mailHandlers(world) },
33
+ },
34
+ globals: desktop,
35
+ } satisfies Meta<typeof AppStory>;
36
+ export default meta;
37
+
38
+ type Story = StoryObj<typeof meta>;
39
+
40
+ const page = () => within(document.body);
41
+
42
+ export const Week: Story = {
43
+ play: async () => {
44
+ await expect(
45
+ (await page().findAllByText("Roadmap review"))[0],
46
+ ).toBeVisible();
47
+ await expect(
48
+ (await page().findAllByRole("link", { name: "Calendar" }))[0],
49
+ ).toBeVisible();
50
+ },
51
+ };
52
+
53
+ export const Empty: Story = {
54
+ parameters: { msw: { handlers: mailHandlers(mailWorld()) } },
55
+ play: async () => {
56
+ await expect(
57
+ (await page().findAllByRole("link", { name: "Calendar" }))[0],
58
+ ).toBeVisible();
59
+ await expect(page().queryByText("Roadmap review")).toBeNull();
60
+ },
61
+ };
62
+
63
+ export const OpenEvent: Story = {
64
+ args: { url: `${STORY_WEEK}/${ROADMAP_OBJECT}` },
65
+ play: async () => {
66
+ await expect(await page().findByText("Room Zuid")).toBeVisible();
67
+ await expect(
68
+ await page().findByRole("button", { name: "Edit" }),
69
+ ).toBeVisible();
70
+ },
71
+ };
72
+
73
+ export const RecurrenceScope: Story = {
74
+ args: { url: `${STORY_WEEK}/${STANDUP_OBJECT}/${STANDUP_RECURRENCE}` },
75
+ play: async () => {
76
+ await userEvent.click(await page().findByRole("button", { name: "Edit" }));
77
+ await expect(
78
+ await page().findByText("What should the change apply to?"),
79
+ ).toBeVisible();
80
+ },
81
+ };
82
+
83
+ export const NewEvent: Story = {
84
+ args: { url: `${STORY_WEEK}/new` },
85
+ play: async () => {
86
+ await expect(await page().findByLabelText("Date")).toHaveValue(STORY_DATE);
87
+ await expect(page().getByLabelText("Title")).toHaveValue("");
88
+ await expect(
89
+ await page().findByRole("radio", { name: "Northwind" }),
90
+ ).toBeChecked();
91
+ },
92
+ };
93
+
94
+ export const PhoneNewEvent: Story = {
95
+ args: { url: `/calendar/day/${STORY_DATE}/new` },
96
+ globals: phone,
97
+ play: async () => {
98
+ await expect(await page().findByLabelText("Date")).toHaveValue(STORY_DATE);
99
+ await expect(page().getByLabelText("Title")).toHaveValue("");
100
+ },
101
+ };
102
+
103
+ export const PhoneDay: Story = {
104
+ args: { url: `/calendar/day/${STORY_DATE}` },
105
+ globals: phone,
106
+ play: async () => {
107
+ await expect(
108
+ (await page().findAllByText("Roadmap review"))[0],
109
+ ).toBeVisible();
110
+ },
111
+ };
112
+
113
+ export const PhoneEvent: Story = {
114
+ args: { url: `/calendar/day/${STORY_DATE}/${ROADMAP_OBJECT}` },
115
+ globals: phone,
116
+ play: async () => {
117
+ await expect(await page().findByText("Room Zuid")).toBeVisible();
118
+ },
119
+ };
@@ -116,23 +116,3 @@ export const standupResource = {
116
116
  "END:VCALENDAR",
117
117
  ].join("\r\n"),
118
118
  } as RemitImapCalendarEventResponse;
119
-
120
- /**
121
- * The occurrences inside the window a request asked for.
122
- *
123
- * The panes read a week at a time and hold several weeks at once, so a server
124
- * that answered every window with the same list would draw each day's events
125
- * once per window it appears in.
126
- */
127
- export const instancesWithin = (
128
- url: URL,
129
- instances: readonly RemitImapCalendarEventInstance[],
130
- ): RemitImapCalendarEventInstance[] => {
131
- const from = Date.parse(url.searchParams.get("from") ?? "");
132
- const to = Date.parse(url.searchParams.get("to") ?? "");
133
- if (Number.isNaN(from) || Number.isNaN(to)) return [...instances];
134
- return instances.filter((instance) => {
135
- const start = Date.parse(instance.start);
136
- return start >= from && start < to;
137
- });
138
- };
@@ -32,6 +32,7 @@ import {
32
32
  ComfortableRowTextContent,
33
33
  cn,
34
34
  comfortableRowClass,
35
+ PaneHeader,
35
36
  type ThreadRowData,
36
37
  type ThreadSection,
37
38
  } from "@remit/ui";
@@ -194,18 +195,13 @@ export function DraftsView({
194
195
 
195
196
  return (
196
197
  <section className="flex h-full w-full flex-col bg-surface">
197
- {/* List datum bar */}
198
- <header className="flex h-pane-header shrink-0 items-center gap-2 border-b border-line px-row-inset">
199
- <NavMenuButton />
200
- <h1 className="min-w-0 flex-1 truncate text-sm font-semibold text-fg">
201
- {title}
202
- </h1>
198
+ <PaneHeader title={title} leading={<NavMenuButton />}>
203
199
  {(unreadCount ?? 0) > 0 && (
204
200
  <span className="shrink-0 text-2xs text-fg-subtle tabular-nums">
205
201
  {unreadCount} unread
206
202
  </span>
207
203
  )}
208
- </header>
204
+ </PaneHeader>
209
205
 
210
206
  {/* Scrollable body */}
211
207
  <div className="flex-1 overflow-y-auto">
@@ -26,6 +26,7 @@ import {
26
26
  cn,
27
27
  LIST_ROW_SELECTOR,
28
28
  OutboxRow,
29
+ PaneHeader,
29
30
  ReadingPaneEmpty,
30
31
  useRovingFocus,
31
32
  } from "@remit/ui";
@@ -458,18 +459,7 @@ function OutboxList() {
458
459
 
459
460
  return (
460
461
  <div className="h-full flex flex-col bg-canvas">
461
- {/* List datum bar (40px, the shared `--spacing-pane-header`): keeps
462
- the bottom hairline on the same y as the mailbox view so the grid
463
- line stays continuous across nav → outbox (no staircase, #422). */}
464
- <header className="flex h-pane-header shrink-0 items-center gap-2 border-b border-line px-row-inset">
465
- <NavMenuButton />
466
- <h1 className="min-w-0 flex-1 truncate text-sm font-semibold text-fg">
467
- Outbox
468
- </h1>
469
- <span className="shrink-0 text-2xs text-fg-subtle">
470
- {messages.length} {messages.length === 1 ? "message" : "messages"}
471
- </span>
472
- </header>
462
+ <OutboxListHeader count={messages.length} />
473
463
  <div ref={listRef} className="flex-1 overflow-y-auto">
474
464
  {messages.map((message) => (
475
465
  <OutboxMessageRow
@@ -484,6 +474,16 @@ function OutboxList() {
484
474
  );
485
475
  }
486
476
 
477
+ function OutboxListHeader({ count }: { count: number }) {
478
+ return (
479
+ <PaneHeader title="Outbox" leading={<NavMenuButton />}>
480
+ <span className="shrink-0 text-2xs text-fg-subtle">
481
+ {count} {count === 1 ? "message" : "messages"}
482
+ </span>
483
+ </PaneHeader>
484
+ );
485
+ }
486
+
487
487
  /**
488
488
  * Outbox reading pane. Mount in the `reading` slot of `AppShellSlotted`.
489
489
  * Only rendered ≥ 1024px.
@@ -496,7 +496,7 @@ function OutboxReading() {
496
496
  {/* Detail datum bar (40px): outbox has no message-action toolbar,
497
497
  but the datum bar must be present so its bottom hairline lines
498
498
  up with the list pane's, keeping the grid continuous (#422). */}
499
- <header className="flex h-pane-header shrink-0 items-center border-b border-line px-3" />
499
+ <PaneHeader />
500
500
  <div className="min-h-0 flex-1 overflow-hidden">
501
501
  {selectedMessage ? (
502
502
  <OutboxMessageDetail message={selectedMessage} />
@@ -556,15 +556,7 @@ function OutboxPhone() {
556
556
 
557
557
  return (
558
558
  <div className="h-full flex flex-col bg-canvas">
559
- <header className="flex h-pane-header shrink-0 items-center gap-2 border-b border-line px-row-inset">
560
- <NavMenuButton />
561
- <h1 className="min-w-0 flex-1 truncate text-sm font-semibold text-fg">
562
- Outbox
563
- </h1>
564
- <span className="shrink-0 text-2xs text-fg-subtle">
565
- {messages.length} {messages.length === 1 ? "message" : "messages"}
566
- </span>
567
- </header>
559
+ <OutboxListHeader count={messages.length} />
568
560
  <div ref={listRef} className="flex-1 overflow-y-auto">
569
561
  {messages.map((message) => (
570
562
  <OutboxMessageRow
@@ -0,0 +1,21 @@
1
+ import type { RemitImapCalendarEventInstance } from "@remit/api-http-client/types.gen.ts";
2
+
3
+ /**
4
+ * The occurrences inside the window a request asked for.
5
+ *
6
+ * The panes read a week at a time and hold several weeks at once, so a server
7
+ * that answered every window with the same list would draw each day's events
8
+ * once per window it appears in.
9
+ */
10
+ export const instancesWithin = (
11
+ url: URL,
12
+ instances: readonly RemitImapCalendarEventInstance[],
13
+ ): RemitImapCalendarEventInstance[] => {
14
+ const from = Date.parse(url.searchParams.get("from") ?? "");
15
+ const to = Date.parse(url.searchParams.get("to") ?? "");
16
+ if (Number.isNaN(from) || Number.isNaN(to)) return [...instances];
17
+ return instances.filter((instance) => {
18
+ const start = Date.parse(instance.start);
19
+ return start >= from && start < to;
20
+ });
21
+ };
@@ -2,6 +2,7 @@ import type {
2
2
  AccountDetailOperationsUpdateAccountResponse,
3
3
  AddressDetailOperationsUpdateAddressResponse,
4
4
  AddressOperationsSearchAddressesResponse,
5
+ CalendarEventDetailOperationsGetCalendarEventResponse,
5
6
  CalendarEventOperationsListCalendarEventsResponse,
6
7
  CalendarOperationsListCalendarsResponse,
7
8
  CalendarSuggestionOperationsListCalendarSuggestionsResponse,
@@ -35,6 +36,7 @@ import type {
35
36
  } from "@remit/api-http-client/types.gen.ts";
36
37
  import { type HttpHandler, HttpResponse, http } from "msw";
37
38
  import { makeConfig } from "@/test-support/fixtures";
39
+ import { instancesWithin } from "./calendar-window";
38
40
  import type { MailWorld } from "./mail-world";
39
41
 
40
42
  const API = "/api";
@@ -487,13 +489,29 @@ export const mailHandlers = (
487
489
  return new HttpResponse(null, { status: 204 });
488
490
  }),
489
491
  http.get(`${API}/calendars`, () =>
490
- HttpResponse.json<CalendarOperationsListCalendarsResponse>({ items: [] }),
492
+ HttpResponse.json<CalendarOperationsListCalendarsResponse>({
493
+ items: world.calendars,
494
+ }),
491
495
  ),
492
- http.get(`${API}/calendar-events`, () =>
496
+ http.get(`${API}/calendar-events`, ({ request }) =>
493
497
  HttpResponse.json<CalendarEventOperationsListCalendarEventsResponse>({
494
- items: [],
498
+ items: instancesWithin(new URL(request.url), world.calendarEvents),
495
499
  }),
496
500
  ),
501
+ http.get(`${API}/calendar-events/:calendarObjectId`, ({ params }) => {
502
+ const resource = world.calendarResources.find(
503
+ (candidate) =>
504
+ candidate.calendarObjectId === String(params.calendarObjectId),
505
+ );
506
+ if (!resource)
507
+ return HttpResponse.json(
508
+ { status: 404, message: "Calendar event not found" },
509
+ { status: 404 },
510
+ );
511
+ return HttpResponse.json<CalendarEventDetailOperationsGetCalendarEventResponse>(
512
+ resource,
513
+ );
514
+ }),
497
515
  http.get(`${API}/calendar-suggestions`, () =>
498
516
  HttpResponse.json<CalendarSuggestionOperationsListCalendarSuggestionsResponse>(
499
517
  { items: [] },
@@ -1,6 +1,9 @@
1
1
  import type {
2
2
  RemitImapAccountResponse,
3
3
  RemitImapAddressResponse,
4
+ RemitImapCalendarEventInstance,
5
+ RemitImapCalendarEventResponse,
6
+ RemitImapCalendarResponse,
4
7
  RemitImapCanonicalMailboxRole,
5
8
  RemitImapMailboxResponse,
6
9
  RemitImapMessageCategory,
@@ -27,6 +30,9 @@ export interface MailWorld {
27
30
  addresses: RemitImapAddressResponse[];
28
31
  systemUpdate: RemitImapSystemUpdateResponse;
29
32
  bodies: Record<string, string>;
33
+ calendars: RemitImapCalendarResponse[];
34
+ calendarEvents: RemitImapCalendarEventInstance[];
35
+ calendarResources: RemitImapCalendarEventResponse[];
30
36
  }
31
37
 
32
38
  export const MINUTE = 60_000;
@@ -429,5 +435,8 @@ export const mailWorld = (overrides: Partial<MailWorld> = {}): MailWorld => ({
429
435
  run: null,
430
436
  },
431
437
  bodies: {},
438
+ calendars: [],
439
+ calendarEvents: [],
440
+ calendarResources: [],
432
441
  ...overrides,
433
442
  });