ai-design-system 0.1.74 → 0.1.75
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/components/composites/AuthCard/AuthCard.stories.tsx +65 -0
- package/components/composites/AuthCard/AuthCard.tsx +360 -0
- package/components/composites/AuthCard/index.ts +2 -0
- package/components/composites/index.ts +4 -0
- package/components/features/AuthFeature/AuthFeature.behaviors.stories.tsx +80 -0
- package/components/features/AuthFeature/AuthFeature.mocks.ts +41 -0
- package/components/features/AuthFeature/AuthFeature.stories.tsx +102 -0
- package/components/features/AuthFeature/AuthFeature.tsx +186 -0
- package/components/features/AuthFeature/README.md +33 -0
- package/components/features/AuthFeature/index.ts +10 -0
- package/components/features/AuthFeature/useAuthFeature.d.ts +57 -0
- package/components/features/AuthFeature/useAuthFeature.mock.ts +113 -0
- package/components/features/index.ts +5 -0
- package/dist/index.cjs +458 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +34 -0
- package/dist/index.d.ts +192 -18
- package/dist/index.js +458 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { AuthCard } from "./AuthCard";
|
|
3
|
+
import { fn } from "@storybook/test";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof AuthCard> = {
|
|
6
|
+
title: "Composites/AuthCard",
|
|
7
|
+
component: AuthCard,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "centered",
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
appName: "Waypoint",
|
|
13
|
+
mode: "login",
|
|
14
|
+
email: "",
|
|
15
|
+
isLoading: false,
|
|
16
|
+
allowSso: true,
|
|
17
|
+
socialProviders: ["apple", "google"],
|
|
18
|
+
onEmailChange: fn(),
|
|
19
|
+
onNameChange: fn(),
|
|
20
|
+
onPasswordChange: fn(),
|
|
21
|
+
onConfirmPasswordChange: fn(),
|
|
22
|
+
onSubmit: fn(),
|
|
23
|
+
onSsoClick: fn(),
|
|
24
|
+
onSocialClick: fn(),
|
|
25
|
+
onToggleMode: fn(),
|
|
26
|
+
},
|
|
27
|
+
} satisfies Meta<typeof AuthCard>;
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
type Story = StoryObj<typeof AuthCard>;
|
|
31
|
+
|
|
32
|
+
export const LoginDefault: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
mode: "login",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const LoginFilled: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
mode: "login",
|
|
41
|
+
email: "alex@example.com",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const SignupDefault: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
mode: "signup",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const LoadingState: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
mode: "login",
|
|
54
|
+
email: "alex@example.com",
|
|
55
|
+
isLoading: true,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const WithError: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
mode: "login",
|
|
62
|
+
email: "alex@example.com",
|
|
63
|
+
error: "Invalid email or authentication session expired. Please try again.",
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from "@/components/primitives/Card";
|
|
11
|
+
import { Input } from "@/components/primitives/Input";
|
|
12
|
+
import { Label } from "@/components/primitives/Label";
|
|
13
|
+
import { Button } from "@/components/primitives/Button";
|
|
14
|
+
import { Icon } from "@/components/primitives/Icon";
|
|
15
|
+
import { cn } from "@/lib/utils";
|
|
16
|
+
|
|
17
|
+
export type AuthMode = "login" | "signup";
|
|
18
|
+
export type SocialProvider = "google" | "apple" | "github";
|
|
19
|
+
|
|
20
|
+
export interface AuthCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
21
|
+
/** Mode: login (email/SSO per login-05) or signup (full form per signup-01) */
|
|
22
|
+
mode?: AuthMode;
|
|
23
|
+
/** Application name displayed in header */
|
|
24
|
+
appName?: string;
|
|
25
|
+
/** Custom logo or icon component */
|
|
26
|
+
appLogo?: React.ReactNode;
|
|
27
|
+
/** Card title override */
|
|
28
|
+
title?: string;
|
|
29
|
+
/** Card description override */
|
|
30
|
+
description?: string;
|
|
31
|
+
/** Name value (signup mode) */
|
|
32
|
+
name?: string;
|
|
33
|
+
/** Email input value */
|
|
34
|
+
email?: string;
|
|
35
|
+
/** Password value (signup mode) */
|
|
36
|
+
password?: string;
|
|
37
|
+
/** Confirm password value (signup mode) */
|
|
38
|
+
confirmPassword?: string;
|
|
39
|
+
/** Optional error message to display */
|
|
40
|
+
error?: string | null;
|
|
41
|
+
/** Loading state indicator */
|
|
42
|
+
isLoading?: boolean;
|
|
43
|
+
/** Enable SSO button */
|
|
44
|
+
allowSso?: boolean;
|
|
45
|
+
/** Custom label for SSO button */
|
|
46
|
+
ssoButtonLabel?: string;
|
|
47
|
+
/** Social auth providers to show */
|
|
48
|
+
socialProviders?: SocialProvider[];
|
|
49
|
+
/** Terms of service URL */
|
|
50
|
+
termsUrl?: string;
|
|
51
|
+
/** Privacy policy URL */
|
|
52
|
+
privacyUrl?: string;
|
|
53
|
+
/** Callback when name input changes */
|
|
54
|
+
onNameChange?: (name: string) => void;
|
|
55
|
+
/** Callback when email input changes */
|
|
56
|
+
onEmailChange?: (email: string) => void;
|
|
57
|
+
/** Callback when password input changes */
|
|
58
|
+
onPasswordChange?: (password: string) => void;
|
|
59
|
+
/** Callback when confirm password input changes */
|
|
60
|
+
onConfirmPasswordChange?: (confirmPassword: string) => void;
|
|
61
|
+
/** Callback when the form is submitted */
|
|
62
|
+
onSubmit?: (e?: React.FormEvent) => void;
|
|
63
|
+
/** Callback when SSO button is clicked */
|
|
64
|
+
onSsoClick?: () => void;
|
|
65
|
+
/** Callback when a social provider button is clicked */
|
|
66
|
+
onSocialClick?: (provider: SocialProvider) => void;
|
|
67
|
+
/** Callback when switching between login and signup */
|
|
68
|
+
onToggleMode?: (nextMode: AuthMode) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const AuthCard = React.memo<AuthCardProps>(
|
|
72
|
+
({
|
|
73
|
+
mode = "login",
|
|
74
|
+
appName = "Waypoint",
|
|
75
|
+
appLogo,
|
|
76
|
+
title,
|
|
77
|
+
description,
|
|
78
|
+
name = "",
|
|
79
|
+
email = "",
|
|
80
|
+
password = "",
|
|
81
|
+
confirmPassword = "",
|
|
82
|
+
error,
|
|
83
|
+
isLoading = false,
|
|
84
|
+
allowSso = true,
|
|
85
|
+
ssoButtonLabel = "Sign in with Single Sign-On",
|
|
86
|
+
socialProviders = ["google", "apple"],
|
|
87
|
+
termsUrl = "#",
|
|
88
|
+
privacyUrl = "#",
|
|
89
|
+
onNameChange,
|
|
90
|
+
onEmailChange,
|
|
91
|
+
onPasswordChange,
|
|
92
|
+
onConfirmPasswordChange,
|
|
93
|
+
onSubmit,
|
|
94
|
+
onSsoClick,
|
|
95
|
+
onSocialClick,
|
|
96
|
+
onToggleMode,
|
|
97
|
+
className,
|
|
98
|
+
...props
|
|
99
|
+
}) => {
|
|
100
|
+
const isSignup = mode === "signup";
|
|
101
|
+
|
|
102
|
+
const defaultTitle = isSignup
|
|
103
|
+
? "Create an account"
|
|
104
|
+
: `Welcome to ${appName}`;
|
|
105
|
+
|
|
106
|
+
const defaultDescription = isSignup
|
|
107
|
+
? "Enter your information below to create your account"
|
|
108
|
+
: undefined;
|
|
109
|
+
|
|
110
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
onSubmit?.(e);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className={cn("w-full max-w-sm", className)} {...props}>
|
|
117
|
+
<Card className="border shadow-sm">
|
|
118
|
+
<CardHeader className="space-y-2 text-center">
|
|
119
|
+
<div className="flex flex-col items-center gap-2">
|
|
120
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
121
|
+
{appLogo ?? <Icon name="gallery-vertical-end" size="lg" aria-label={appName} />}
|
|
122
|
+
</div>
|
|
123
|
+
<CardTitle className="text-xl font-bold tracking-tight">
|
|
124
|
+
{title ?? defaultTitle}
|
|
125
|
+
</CardTitle>
|
|
126
|
+
{description ? (
|
|
127
|
+
<CardDescription>{description}</CardDescription>
|
|
128
|
+
) : isSignup ? (
|
|
129
|
+
<CardDescription>{defaultDescription}</CardDescription>
|
|
130
|
+
) : (
|
|
131
|
+
<CardDescription>
|
|
132
|
+
Don't have an account?{" "}
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
onClick={() => onToggleMode?.("signup")}
|
|
136
|
+
className="font-medium text-primary underline underline-offset-4 hover:opacity-80"
|
|
137
|
+
>
|
|
138
|
+
Sign up
|
|
139
|
+
</button>
|
|
140
|
+
</CardDescription>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
</CardHeader>
|
|
144
|
+
<CardContent className="space-y-4">
|
|
145
|
+
{error && (
|
|
146
|
+
<div
|
|
147
|
+
role="alert"
|
|
148
|
+
className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-xs text-destructive"
|
|
149
|
+
>
|
|
150
|
+
{error}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* SSO Action (per ADR-032 / platform auth) in login mode */}
|
|
155
|
+
{!isSignup && allowSso && (
|
|
156
|
+
<div className="space-y-3">
|
|
157
|
+
<Button
|
|
158
|
+
type="button"
|
|
159
|
+
variant="default"
|
|
160
|
+
className="w-full"
|
|
161
|
+
disabled={isLoading}
|
|
162
|
+
onClick={onSsoClick}
|
|
163
|
+
>
|
|
164
|
+
{isLoading ? (
|
|
165
|
+
<>
|
|
166
|
+
<Icon name="loader-2" size="sm" className="mr-2 animate-spin" />
|
|
167
|
+
Connecting…
|
|
168
|
+
</>
|
|
169
|
+
) : (
|
|
170
|
+
<>
|
|
171
|
+
<Icon name="lock" size="sm" className="mr-2" />
|
|
172
|
+
{ssoButtonLabel}
|
|
173
|
+
</>
|
|
174
|
+
)}
|
|
175
|
+
</Button>
|
|
176
|
+
<div className="relative flex items-center justify-center text-xs uppercase text-muted-foreground">
|
|
177
|
+
<div className="w-full border-t border-border" />
|
|
178
|
+
<span className="bg-card px-2">Or continue with email</span>
|
|
179
|
+
<div className="w-full border-t border-border" />
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
185
|
+
{isSignup && (
|
|
186
|
+
<div className="space-y-1.5 text-left">
|
|
187
|
+
<Label htmlFor="auth-name">Full Name</Label>
|
|
188
|
+
<Input
|
|
189
|
+
id="auth-name"
|
|
190
|
+
type="text"
|
|
191
|
+
placeholder="John Doe"
|
|
192
|
+
value={name}
|
|
193
|
+
onChange={(e) => onNameChange?.(e.target.value)}
|
|
194
|
+
disabled={isLoading}
|
|
195
|
+
required
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
<div className="space-y-1.5 text-left">
|
|
201
|
+
<Label htmlFor="auth-email">Email</Label>
|
|
202
|
+
<Input
|
|
203
|
+
id="auth-email"
|
|
204
|
+
type="email"
|
|
205
|
+
placeholder="m@example.com"
|
|
206
|
+
value={email}
|
|
207
|
+
onChange={(e) => onEmailChange?.(e.target.value)}
|
|
208
|
+
disabled={isLoading}
|
|
209
|
+
required
|
|
210
|
+
/>
|
|
211
|
+
{isSignup && (
|
|
212
|
+
<p className="text-[11px] text-muted-foreground">
|
|
213
|
+
We will use this to contact you.
|
|
214
|
+
</p>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{isSignup && (
|
|
219
|
+
<>
|
|
220
|
+
<div className="space-y-1.5 text-left">
|
|
221
|
+
<Label htmlFor="auth-password">Password</Label>
|
|
222
|
+
<Input
|
|
223
|
+
id="auth-password"
|
|
224
|
+
type="password"
|
|
225
|
+
placeholder="••••••••"
|
|
226
|
+
value={password}
|
|
227
|
+
onChange={(e) => onPasswordChange?.(e.target.value)}
|
|
228
|
+
disabled={isLoading}
|
|
229
|
+
required
|
|
230
|
+
/>
|
|
231
|
+
<p className="text-[11px] text-muted-foreground">
|
|
232
|
+
Must be at least 8 characters long.
|
|
233
|
+
</p>
|
|
234
|
+
</div>
|
|
235
|
+
<div className="space-y-1.5 text-left">
|
|
236
|
+
<Label htmlFor="auth-confirm-password">Confirm Password</Label>
|
|
237
|
+
<Input
|
|
238
|
+
id="auth-confirm-password"
|
|
239
|
+
type="password"
|
|
240
|
+
placeholder="••••••••"
|
|
241
|
+
value={confirmPassword}
|
|
242
|
+
onChange={(e) => onConfirmPasswordChange?.(e.target.value)}
|
|
243
|
+
disabled={isLoading}
|
|
244
|
+
required
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
</>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
<Button
|
|
251
|
+
type="submit"
|
|
252
|
+
variant={isSignup ? "default" : "secondary"}
|
|
253
|
+
className="w-full"
|
|
254
|
+
disabled={isLoading}
|
|
255
|
+
>
|
|
256
|
+
{isLoading ? (
|
|
257
|
+
<>
|
|
258
|
+
<Icon name="loader-2" size="sm" className="mr-2 animate-spin" />
|
|
259
|
+
Please wait…
|
|
260
|
+
</>
|
|
261
|
+
) : isSignup ? (
|
|
262
|
+
"Create Account"
|
|
263
|
+
) : (
|
|
264
|
+
"Login with Email"
|
|
265
|
+
)}
|
|
266
|
+
</Button>
|
|
267
|
+
</form>
|
|
268
|
+
|
|
269
|
+
{/* Social options */}
|
|
270
|
+
{socialProviders.length > 0 && (
|
|
271
|
+
<>
|
|
272
|
+
<div className="relative flex items-center justify-center text-xs uppercase text-muted-foreground">
|
|
273
|
+
<div className="w-full border-t border-border" />
|
|
274
|
+
<span className="bg-card px-2">Or</span>
|
|
275
|
+
<div className="w-full border-t border-border" />
|
|
276
|
+
</div>
|
|
277
|
+
<div
|
|
278
|
+
className={cn(
|
|
279
|
+
"grid gap-2",
|
|
280
|
+
socialProviders.length > 1 ? "grid-cols-2" : "grid-cols-1"
|
|
281
|
+
)}
|
|
282
|
+
>
|
|
283
|
+
{socialProviders.includes("apple") && (
|
|
284
|
+
<Button
|
|
285
|
+
variant="outline"
|
|
286
|
+
type="button"
|
|
287
|
+
disabled={isLoading}
|
|
288
|
+
onClick={() => onSocialClick?.("apple")}
|
|
289
|
+
className="w-full text-xs"
|
|
290
|
+
>
|
|
291
|
+
<Icon name="apple" size="sm" className="mr-1.5" />
|
|
292
|
+
Apple
|
|
293
|
+
</Button>
|
|
294
|
+
)}
|
|
295
|
+
{socialProviders.includes("google") && (
|
|
296
|
+
<Button
|
|
297
|
+
variant="outline"
|
|
298
|
+
type="button"
|
|
299
|
+
disabled={isLoading}
|
|
300
|
+
onClick={() => onSocialClick?.("google")}
|
|
301
|
+
className="w-full text-xs"
|
|
302
|
+
>
|
|
303
|
+
<Icon name="google" size="sm" className="mr-1.5" />
|
|
304
|
+
Google
|
|
305
|
+
</Button>
|
|
306
|
+
)}
|
|
307
|
+
{socialProviders.includes("github") && (
|
|
308
|
+
<Button
|
|
309
|
+
variant="outline"
|
|
310
|
+
type="button"
|
|
311
|
+
disabled={isLoading}
|
|
312
|
+
onClick={() => onSocialClick?.("github")}
|
|
313
|
+
className="w-full text-xs"
|
|
314
|
+
>
|
|
315
|
+
<Icon name="github" size="sm" className="mr-1.5" />
|
|
316
|
+
GitHub
|
|
317
|
+
</Button>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
</>
|
|
321
|
+
)}
|
|
322
|
+
|
|
323
|
+
{isSignup && (
|
|
324
|
+
<p className="text-center text-xs text-muted-foreground">
|
|
325
|
+
Already have an account?{" "}
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
onClick={() => onToggleMode?.("login")}
|
|
329
|
+
className="font-medium text-primary underline underline-offset-4 hover:opacity-80"
|
|
330
|
+
>
|
|
331
|
+
Sign in
|
|
332
|
+
</button>
|
|
333
|
+
</p>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
<p className="px-4 text-center text-[11px] text-muted-foreground">
|
|
337
|
+
By clicking continue, you agree to our{" "}
|
|
338
|
+
<a
|
|
339
|
+
href={termsUrl}
|
|
340
|
+
className="underline underline-offset-2 hover:text-foreground"
|
|
341
|
+
>
|
|
342
|
+
Terms of Service
|
|
343
|
+
</a>{" "}
|
|
344
|
+
and{" "}
|
|
345
|
+
<a
|
|
346
|
+
href={privacyUrl}
|
|
347
|
+
className="underline underline-offset-2 hover:text-foreground"
|
|
348
|
+
>
|
|
349
|
+
Privacy Policy
|
|
350
|
+
</a>
|
|
351
|
+
.
|
|
352
|
+
</p>
|
|
353
|
+
</CardContent>
|
|
354
|
+
</Card>
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
AuthCard.displayName = "AuthCard";
|
|
@@ -218,4 +218,8 @@ export type { AppBreadcrumbProps, BreadcrumbItemData } from './AppBreadcrumb'
|
|
|
218
218
|
export { IconButton } from './IconButton'
|
|
219
219
|
export type { IconButtonProps } from './IconButton'
|
|
220
220
|
|
|
221
|
+
// AuthCard Composite
|
|
222
|
+
export { AuthCard } from './AuthCard'
|
|
223
|
+
export type { AuthCardProps, AuthMode, SocialProvider } from './AuthCard'
|
|
224
|
+
|
|
221
225
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import { expect, fn, userEvent, within } from "@storybook/test";
|
|
3
|
+
import { AuthFeature } from "./AuthFeature";
|
|
4
|
+
import { mockLoginProps, mockSignupProps } from "./AuthFeature.mocks";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Features/AuthFeature/Behaviors",
|
|
8
|
+
component: AuthFeature,
|
|
9
|
+
tags: ["test"],
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: "fullscreen",
|
|
12
|
+
},
|
|
13
|
+
} satisfies Meta<typeof AuthFeature>;
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
export const SubmitLoginForm: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
...mockLoginProps,
|
|
21
|
+
onLogin: fn(),
|
|
22
|
+
onEmailChange: fn(),
|
|
23
|
+
},
|
|
24
|
+
play: async ({ canvasElement, args }) => {
|
|
25
|
+
const canvas = within(canvasElement);
|
|
26
|
+
const emailInput = canvas.getByLabelText("Email");
|
|
27
|
+
await userEvent.clear(emailInput);
|
|
28
|
+
await userEvent.type(emailInput, "user@example.com");
|
|
29
|
+
await expect(args.onEmailChange).toHaveBeenCalled();
|
|
30
|
+
|
|
31
|
+
const loginButton = canvas.getByRole("button", { name: /Login with Email/i });
|
|
32
|
+
await userEvent.click(loginButton);
|
|
33
|
+
await expect(args.onLogin).toHaveBeenCalled();
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const ClickSsoButton: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
...mockLoginProps,
|
|
40
|
+
onSsoLogin: fn(),
|
|
41
|
+
},
|
|
42
|
+
play: async ({ canvasElement, args }) => {
|
|
43
|
+
const canvas = within(canvasElement);
|
|
44
|
+
const ssoButton = canvas.getByRole("button", { name: /Sign in with Single Sign-On/i });
|
|
45
|
+
await userEvent.click(ssoButton);
|
|
46
|
+
await expect(args.onSsoLogin).toHaveBeenCalled();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const ClickSocialButton: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
...mockLoginProps,
|
|
53
|
+
onSocialLogin: fn(),
|
|
54
|
+
},
|
|
55
|
+
play: async ({ canvasElement, args }) => {
|
|
56
|
+
const canvas = within(canvasElement);
|
|
57
|
+
const googleButton = canvas.getByRole("button", { name: /Google/i });
|
|
58
|
+
await userEvent.click(googleButton);
|
|
59
|
+
await expect(args.onSocialLogin).toHaveBeenCalledWith("google");
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const SubmitSignupForm: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
...mockSignupProps,
|
|
66
|
+
onSignup: fn(),
|
|
67
|
+
onNameChange: fn(),
|
|
68
|
+
},
|
|
69
|
+
play: async ({ canvasElement, args }) => {
|
|
70
|
+
const canvas = within(canvasElement);
|
|
71
|
+
const nameInput = canvas.getByLabelText("Full Name");
|
|
72
|
+
await userEvent.clear(nameInput);
|
|
73
|
+
await userEvent.type(nameInput, "Jane Doe");
|
|
74
|
+
await expect(args.onNameChange).toHaveBeenCalled();
|
|
75
|
+
|
|
76
|
+
const createButton = canvas.getByRole("button", { name: /Create Account/i });
|
|
77
|
+
await userEvent.click(createButton);
|
|
78
|
+
await expect(args.onSignup).toHaveBeenCalled();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AuthFeatureProps } from "./AuthFeature";
|
|
2
|
+
|
|
3
|
+
export const mockLoginProps: AuthFeatureProps = {
|
|
4
|
+
mode: "login",
|
|
5
|
+
appName: "Waypoint",
|
|
6
|
+
email: "m@example.com",
|
|
7
|
+
isLoading: false,
|
|
8
|
+
allowSso: true,
|
|
9
|
+
socialProviders: ["apple", "google"],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const mockSignupProps: AuthFeatureProps = {
|
|
13
|
+
mode: "signup",
|
|
14
|
+
appName: "Waypoint",
|
|
15
|
+
name: "John Doe",
|
|
16
|
+
email: "john@example.com",
|
|
17
|
+
password: "password123",
|
|
18
|
+
confirmPassword: "password123",
|
|
19
|
+
isLoading: false,
|
|
20
|
+
allowSso: false,
|
|
21
|
+
socialProviders: ["google"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const mockLoadingProps: AuthFeatureProps = {
|
|
25
|
+
mode: "login",
|
|
26
|
+
appName: "Waypoint",
|
|
27
|
+
email: "m@example.com",
|
|
28
|
+
isLoading: true,
|
|
29
|
+
allowSso: true,
|
|
30
|
+
socialProviders: ["apple", "google"],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const mockErrorProps: AuthFeatureProps = {
|
|
34
|
+
mode: "login",
|
|
35
|
+
appName: "Waypoint",
|
|
36
|
+
email: "m@example.com",
|
|
37
|
+
error: "Invalid email or credentials. Please try again.",
|
|
38
|
+
isLoading: false,
|
|
39
|
+
allowSso: true,
|
|
40
|
+
socialProviders: ["apple", "google"],
|
|
41
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import { AuthFeature } from "./AuthFeature";
|
|
3
|
+
import { useAuthFeatureMock } from "./useAuthFeature.mock";
|
|
4
|
+
import {
|
|
5
|
+
mockLoginProps,
|
|
6
|
+
mockSignupProps,
|
|
7
|
+
mockLoadingProps,
|
|
8
|
+
mockErrorProps,
|
|
9
|
+
} from "./AuthFeature.mocks";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* AuthFeature Stories
|
|
13
|
+
*
|
|
14
|
+
* Full-page authentication feature supporting email-only/SSO login (per shadcn login-05)
|
|
15
|
+
* and full account creation (per shadcn signup-01).
|
|
16
|
+
*
|
|
17
|
+
* ## Features
|
|
18
|
+
* - Login mode with Email-only and Single Sign-On (SSO / OIDC)
|
|
19
|
+
* - Signup mode with name, email, password, and confirmation
|
|
20
|
+
* - Social login integration (Google, Apple, GitHub)
|
|
21
|
+
* - Built-in loading and error feedback
|
|
22
|
+
* - WCAG 2.1 Level AA compliant
|
|
23
|
+
*/
|
|
24
|
+
const meta = {
|
|
25
|
+
title: "Features/AuthFeature",
|
|
26
|
+
component: AuthFeature,
|
|
27
|
+
tags: ["autodocs"],
|
|
28
|
+
parameters: {
|
|
29
|
+
layout: "fullscreen",
|
|
30
|
+
docs: {
|
|
31
|
+
description: {
|
|
32
|
+
component:
|
|
33
|
+
"Complete authentication feature designed according to shadcn login-05 and signup-01 specifications.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
} satisfies Meta<typeof AuthFeature>;
|
|
38
|
+
|
|
39
|
+
export default meta;
|
|
40
|
+
type Story = StoryObj<typeof meta>;
|
|
41
|
+
|
|
42
|
+
export const Default: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
...mockLoginProps,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const SignupMode: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
...mockSignupProps,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Loading: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
...mockLoadingProps,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const WithError: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
...mockErrorProps,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const WithStateManagement: Story = {
|
|
67
|
+
render: () => {
|
|
68
|
+
const hook = useAuthFeatureMock();
|
|
69
|
+
return (
|
|
70
|
+
<AuthFeature
|
|
71
|
+
mode={hook.mode}
|
|
72
|
+
appName={hook.appName}
|
|
73
|
+
name={hook.name}
|
|
74
|
+
email={hook.email}
|
|
75
|
+
password={hook.password}
|
|
76
|
+
confirmPassword={hook.confirmPassword}
|
|
77
|
+
error={hook.error}
|
|
78
|
+
isLoading={hook.isLoading}
|
|
79
|
+
allowSso={hook.allowSso}
|
|
80
|
+
socialProviders={hook.socialProviders}
|
|
81
|
+
onNameChange={hook.setName}
|
|
82
|
+
onEmailChange={hook.setEmail}
|
|
83
|
+
onPasswordChange={hook.setPassword}
|
|
84
|
+
onConfirmPasswordChange={hook.setConfirmPassword}
|
|
85
|
+
onLogin={hook.handleSubmit}
|
|
86
|
+
onSignup={hook.handleSubmit}
|
|
87
|
+
onSsoLogin={hook.handleSsoClick}
|
|
88
|
+
onSocialLogin={hook.handleSocialClick}
|
|
89
|
+
onModeChange={hook.setMode}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const DarkMode: Story = {
|
|
96
|
+
globals: {
|
|
97
|
+
theme: "dark-neutral",
|
|
98
|
+
},
|
|
99
|
+
args: {
|
|
100
|
+
...mockLoginProps,
|
|
101
|
+
},
|
|
102
|
+
};
|