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.
@@ -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&apos;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";
@@ -0,0 +1,2 @@
1
+ export { AuthCard } from "./AuthCard";
2
+ export type { AuthCardProps, AuthMode, SocialProvider } from "./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
+ };