@remit/ui 0.0.162 → 0.0.164
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,5 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
3
4
|
import { CalendarList } from "./calendar-list.js";
|
|
4
5
|
import type { CalendarDescriptor } from "./calendar-types.js";
|
|
5
6
|
|
|
@@ -127,4 +128,81 @@ export const AllHidden: Story = {
|
|
|
127
128
|
onToggle={() => {}}
|
|
128
129
|
/>
|
|
129
130
|
),
|
|
131
|
+
play: async ({ canvasElement }) => {
|
|
132
|
+
const canvas = within(canvasElement);
|
|
133
|
+
for (const box of canvas.getAllByRole("checkbox")) {
|
|
134
|
+
await expect(box).not.toBeChecked();
|
|
135
|
+
}
|
|
136
|
+
await expect(canvas.getByText("Travel")).toBeVisible();
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* One account with one calendar in it, which is what a new install looks like.
|
|
142
|
+
* The control is still the legend, so it stays on screen: a grid whose single
|
|
143
|
+
* colour is unexplained is no more readable than one with six.
|
|
144
|
+
*/
|
|
145
|
+
export const SingleCalendar: Story = {
|
|
146
|
+
render: () => {
|
|
147
|
+
const only = calendars.slice(0, 1);
|
|
148
|
+
const [visible, setVisible] = useState(new Set([only[0].id]));
|
|
149
|
+
return (
|
|
150
|
+
<CalendarList
|
|
151
|
+
calendars={only}
|
|
152
|
+
visible={visible}
|
|
153
|
+
onToggle={(id) =>
|
|
154
|
+
setVisible((prev) => {
|
|
155
|
+
const next = new Set(prev);
|
|
156
|
+
if (!next.delete(id)) next.add(id);
|
|
157
|
+
return next;
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
play: async ({ canvasElement }) => {
|
|
164
|
+
const canvas = within(canvasElement);
|
|
165
|
+
const northwind = canvas.getByRole("checkbox", { name: "Northwind" });
|
|
166
|
+
await expect(northwind).toBeChecked();
|
|
167
|
+
await userEvent.click(canvas.getByText("Northwind"));
|
|
168
|
+
await expect(northwind).not.toBeChecked();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The same control on a surface with no room for a rail: a scrolling row of
|
|
174
|
+
* chips at thumb size. It is laid out differently and it is not a popover —
|
|
175
|
+
* turning a calendar off stays one press away from the grid.
|
|
176
|
+
*/
|
|
177
|
+
export const AsAStrip: Story = {
|
|
178
|
+
render: () => {
|
|
179
|
+
const [visible, setVisible] = useState(new Set(calendars.map((c) => c.id)));
|
|
180
|
+
return (
|
|
181
|
+
<CalendarList
|
|
182
|
+
calendars={calendars}
|
|
183
|
+
layout="strip"
|
|
184
|
+
touch
|
|
185
|
+
visible={visible}
|
|
186
|
+
onToggle={(id) =>
|
|
187
|
+
setVisible((prev) => {
|
|
188
|
+
const next = new Set(prev);
|
|
189
|
+
if (!next.delete(id)) next.add(id);
|
|
190
|
+
return next;
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
},
|
|
196
|
+
play: async ({ canvasElement }) => {
|
|
197
|
+
const canvas = within(canvasElement);
|
|
198
|
+
await expect(canvas.getAllByRole("checkbox")).toHaveLength(
|
|
199
|
+
calendars.length,
|
|
200
|
+
);
|
|
201
|
+
const onCall = canvas.getByRole("checkbox", { name: "On-call" });
|
|
202
|
+
await userEvent.click(canvas.getByText("On-call"));
|
|
203
|
+
await expect(onCall).not.toBeChecked();
|
|
204
|
+
await expect(
|
|
205
|
+
canvas.getByRole("checkbox", { name: "Northwind" }),
|
|
206
|
+
).toBeChecked();
|
|
207
|
+
},
|
|
130
208
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
3
4
|
import { CalendarDateNav, CalendarViewSwitch } from "./calendar-toolbar.js";
|
|
4
5
|
import type { CalendarViewId } from "./calendar-types.js";
|
|
5
6
|
|
|
@@ -52,4 +53,76 @@ export const DateNav: Story = {
|
|
|
52
53
|
</div>
|
|
53
54
|
);
|
|
54
55
|
},
|
|
56
|
+
play: async ({ canvasElement }) => {
|
|
57
|
+
const canvas = within(canvasElement);
|
|
58
|
+
for (const name of ["Previous", "Next", "Today"]) {
|
|
59
|
+
await expect(canvas.getByRole("button", { name })).toBeVisible();
|
|
60
|
+
}
|
|
61
|
+
await userEvent.click(canvas.getByRole("radio", { name: "Day" }));
|
|
62
|
+
await expect(canvas.getByRole("radio", { name: "Day" })).toBeChecked();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The bar at a phone's width, where the whole ladder does not fit and a year
|
|
68
|
+
* grid would be unreadable anyway. Back, forward and Today stay: they are how
|
|
69
|
+
* the calendar is moved, and a toolbar that dropped them to make room would
|
|
70
|
+
* leave the reader stuck on whatever week they opened on.
|
|
71
|
+
*/
|
|
72
|
+
export const OnAPhone: Story = {
|
|
73
|
+
render: () => {
|
|
74
|
+
const [view, setView] = useState<CalendarViewId>("day");
|
|
75
|
+
return (
|
|
76
|
+
<div className="w-[390px] rounded-lg border border-line bg-surface p-2">
|
|
77
|
+
<CalendarDateNav
|
|
78
|
+
title="Wed 10 June 2026"
|
|
79
|
+
onPrev={() => {}}
|
|
80
|
+
onNext={() => {}}
|
|
81
|
+
onToday={() => {}}
|
|
82
|
+
touch
|
|
83
|
+
>
|
|
84
|
+
<CalendarViewSwitch
|
|
85
|
+
value={view}
|
|
86
|
+
onChange={setView}
|
|
87
|
+
views={["day", "agenda"]}
|
|
88
|
+
touch
|
|
89
|
+
/>
|
|
90
|
+
</CalendarDateNav>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
play: async ({ canvasElement }) => {
|
|
95
|
+
const canvas = within(canvasElement);
|
|
96
|
+
for (const name of ["Previous", "Next", "Today"]) {
|
|
97
|
+
await expect(canvas.getByRole("button", { name })).toBeVisible();
|
|
98
|
+
}
|
|
99
|
+
await expect(canvas.queryByRole("radio", { name: "Y" })).toBeNull();
|
|
100
|
+
await userEvent.click(canvas.getByRole("radio", { name: "List" }));
|
|
101
|
+
await expect(canvas.getByRole("radio", { name: "List" })).toBeChecked();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A range whose name is longer than the room it has. The title gives way, not
|
|
107
|
+
* the controls: it is the one thing on the bar that can be read off the grid
|
|
108
|
+
* underneath it.
|
|
109
|
+
*/
|
|
110
|
+
export const LongRangeTitle: Story = {
|
|
111
|
+
render: () => (
|
|
112
|
+
<div className="w-[420px] rounded-lg border border-line bg-surface p-2">
|
|
113
|
+
<CalendarDateNav
|
|
114
|
+
title="29 December 2025 – 4 January 2026, week 1"
|
|
115
|
+
onPrev={() => {}}
|
|
116
|
+
onNext={() => {}}
|
|
117
|
+
onToday={() => {}}
|
|
118
|
+
>
|
|
119
|
+
<CalendarViewSwitch value="week" onChange={() => {}} />
|
|
120
|
+
</CalendarDateNav>
|
|
121
|
+
</div>
|
|
122
|
+
),
|
|
123
|
+
play: async ({ canvasElement }) => {
|
|
124
|
+
const canvas = within(canvasElement);
|
|
125
|
+
await expect(canvas.getByRole("radio", { name: "Week" })).toBeChecked();
|
|
126
|
+
await expect(canvas.getByRole("button", { name: "Today" })).toBeVisible();
|
|
127
|
+
},
|
|
55
128
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { AtSign, Inbox, Server } from "lucide-react";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import { Button } from "./button.js";
|
|
4
5
|
import type { ServerSecurity } from "./security-select.js";
|
|
5
|
-
import { ConnectorTile, ServerFields } from "./wizard.js";
|
|
6
|
+
import { ConnectorTile, ServerFields, WizardShell } from "./wizard.js";
|
|
6
7
|
|
|
7
8
|
const meta: Meta = {
|
|
8
9
|
title: "Components/Wizard",
|
|
@@ -101,3 +102,53 @@ export const ServerFieldsPhone: Story = {
|
|
|
101
102
|
globals: { viewport: { value: "mobile" } },
|
|
102
103
|
render: () => <ServerFieldsDemo />,
|
|
103
104
|
};
|
|
105
|
+
|
|
106
|
+
const longStepRows = Array.from(
|
|
107
|
+
{ length: 60 },
|
|
108
|
+
(_, index) => `Row ${index + 1} of a report nobody sized the box for`,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
function LongStep() {
|
|
112
|
+
return (
|
|
113
|
+
<WizardShell
|
|
114
|
+
steps={["File", "Review", "Credentials", "Folders"]}
|
|
115
|
+
activeStep={1}
|
|
116
|
+
title="What this file will change"
|
|
117
|
+
subtitle="A step whose content runs well past the bottom of the screen."
|
|
118
|
+
footer={
|
|
119
|
+
<>
|
|
120
|
+
<Button variant="ghost">Back</Button>
|
|
121
|
+
<Button variant="primary">Continue</Button>
|
|
122
|
+
</>
|
|
123
|
+
}
|
|
124
|
+
>
|
|
125
|
+
<ul className="divide-y divide-line">
|
|
126
|
+
{longStepRows.map((row) => (
|
|
127
|
+
<li key={row} className="py-2 text-sm text-fg">
|
|
128
|
+
{row}
|
|
129
|
+
</li>
|
|
130
|
+
))}
|
|
131
|
+
</ul>
|
|
132
|
+
</WizardShell>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A step taller than the viewport, on the 1512×827 laptop the config import
|
|
138
|
+
* wizard was found unusable on (#1021). The rows scroll inside the card; the
|
|
139
|
+
* title and the Continue button stay on screen.
|
|
140
|
+
*/
|
|
141
|
+
export const ShellLongStepShortViewport: Story = {
|
|
142
|
+
name: "WizardShell — long step, short viewport",
|
|
143
|
+
parameters: { layout: "fullscreen" },
|
|
144
|
+
globals: { viewport: { value: "laptopShort" } },
|
|
145
|
+
render: () => <LongStep />,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** The same step on a phone with the address bar and system nav showing. */
|
|
149
|
+
export const ShellLongStepPhoneShort: Story = {
|
|
150
|
+
name: "WizardShell — long step, short phone",
|
|
151
|
+
parameters: { layout: "fullscreen" },
|
|
152
|
+
globals: { viewport: { value: "mobileShort" } },
|
|
153
|
+
render: () => <LongStep />,
|
|
154
|
+
};
|
|
@@ -76,25 +76,40 @@ export function WizardShell({
|
|
|
76
76
|
hideSteps,
|
|
77
77
|
}: WizardShellProps) {
|
|
78
78
|
return (
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
79
|
+
// The shell is exactly the viewport, never more: the step's own content
|
|
80
|
+
// is what scrolls, and the title and the footer actions are pinned
|
|
81
|
+
// either side of it. A dry-run report or a list of accounts can run to
|
|
82
|
+
// any length and the primary action stays where the reader can press it
|
|
83
|
+
// (#1021).
|
|
84
|
+
//
|
|
85
|
+
// First-run is still a focal moment, so on larger screens the column
|
|
86
|
+
// sits with its top edge about 30% down the viewport (optical centre).
|
|
87
|
+
// That offset is a flex spacer rather than top padding because padding
|
|
88
|
+
// cannot yield: the spacer collapses first and the card only gives up
|
|
89
|
+
// height once the offset is gone.
|
|
90
|
+
<div className="flex h-dvh w-full flex-col items-center overflow-hidden bg-canvas pb-[max(2rem,env(safe-area-inset-bottom,0px))] pl-[max(2rem,env(safe-area-inset-left,0px))] pr-[max(2rem,env(safe-area-inset-right,0px))] pt-[max(2rem,env(safe-area-inset-top,0px))] font-sans text-fg">
|
|
91
|
+
<div className="hidden shrink-[9999] basis-[30vh] sm:block" />
|
|
85
92
|
{!hideSteps && (
|
|
86
|
-
<div className="mb-5 w-full max-w-xl">
|
|
93
|
+
<div className="mb-5 w-full max-w-xl shrink-0">
|
|
87
94
|
<StepRail steps={steps} activeStep={activeStep} />
|
|
88
95
|
</div>
|
|
89
96
|
)}
|
|
90
|
-
<Card raised className="w-full max-w-xl">
|
|
91
|
-
<div className="px-5 pt-5">
|
|
97
|
+
<Card raised className="flex w-full min-h-0 max-w-xl flex-col">
|
|
98
|
+
<div className="shrink-0 px-5 pt-5">
|
|
92
99
|
<h1 className="text-xl font-semibold text-fg">{title}</h1>
|
|
93
100
|
{subtitle && <p className="mt-1 text-sm text-fg-muted">{subtitle}</p>}
|
|
94
101
|
</div>
|
|
95
|
-
<div
|
|
102
|
+
<div
|
|
103
|
+
data-testid="wizard-body"
|
|
104
|
+
className="min-h-0 overflow-y-auto px-5 py-5"
|
|
105
|
+
>
|
|
106
|
+
{children}
|
|
107
|
+
</div>
|
|
96
108
|
{footer && (
|
|
97
|
-
<div
|
|
109
|
+
<div
|
|
110
|
+
data-testid="wizard-footer"
|
|
111
|
+
className="flex shrink-0 items-center justify-between gap-3 border-t border-line px-5 py-3"
|
|
112
|
+
>
|
|
98
113
|
{footer}
|
|
99
114
|
</div>
|
|
100
115
|
)}
|