@exegia/corpora-ui 0.5.0

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 (71) hide show
  1. package/README.md +21 -0
  2. package/dist-lib/components/blocks/auth/auth-shell.d.ts +56 -0
  3. package/dist-lib/components/blocks/auth/code-auth-block.d.ts +22 -0
  4. package/dist-lib/components/blocks/auth/forgot-password-block.d.ts +15 -0
  5. package/dist-lib/components/blocks/auth/login-block.d.ts +25 -0
  6. package/dist-lib/components/blocks/auth/signup-block.d.ts +26 -0
  7. package/dist-lib/components/composed/password-input.d.ts +30 -0
  8. package/dist-lib/components/composed/social-providers.d.ts +22 -0
  9. package/dist-lib/components/docs/code-block.d.ts +4 -0
  10. package/dist-lib/components/docs/component-preview.d.ts +10 -0
  11. package/dist-lib/components/docs/demo-controls.d.ts +28 -0
  12. package/dist-lib/components/docs/props-table.d.ts +4 -0
  13. package/dist-lib/components/theme-provider.d.ts +15 -0
  14. package/dist-lib/components/ui/button.d.ts +28 -0
  15. package/dist-lib/components/ui/card.d.ts +16 -0
  16. package/dist-lib/components/ui/checkbox.d.ts +7 -0
  17. package/dist-lib/components/ui/field.d.ts +10 -0
  18. package/dist-lib/components/ui/frame.d.ts +7 -0
  19. package/dist-lib/components/ui/glasscn/glass-button.d.ts +6 -0
  20. package/dist-lib/components/ui/glasscn/liquid-glass.d.ts +31 -0
  21. package/dist-lib/components/ui/input-group.d.ts +13 -0
  22. package/dist-lib/components/ui/input.d.ts +9 -0
  23. package/dist-lib/components/ui/label.d.ts +11 -0
  24. package/dist-lib/components/ui/otp-field.d.ts +11 -0
  25. package/dist-lib/components/ui/separator.d.ts +4 -0
  26. package/dist-lib/components/ui/spinner.d.ts +3 -0
  27. package/dist-lib/components/ui/textarea.d.ts +8 -0
  28. package/dist-lib/index.d.ts +29 -0
  29. package/dist-lib/index.js +1617 -0
  30. package/dist-lib/index.js.map +1 -0
  31. package/dist-lib/lib/auth-accent.d.ts +33 -0
  32. package/dist-lib/lib/glass-variants.d.ts +6 -0
  33. package/dist-lib/lib/sound.d.ts +13 -0
  34. package/dist-lib/lib/utils.d.ts +2 -0
  35. package/dist-lib/lib/utils.test.d.ts +1 -0
  36. package/package.json +80 -0
  37. package/src/components/blocks/.gitkeep +0 -0
  38. package/src/components/blocks/auth/auth-shell.tsx +266 -0
  39. package/src/components/blocks/auth/code-auth-block.tsx +190 -0
  40. package/src/components/blocks/auth/forgot-password-block.tsx +123 -0
  41. package/src/components/blocks/auth/login-block.tsx +239 -0
  42. package/src/components/blocks/auth/signup-block.tsx +260 -0
  43. package/src/components/composed/.gitkeep +0 -0
  44. package/src/components/composed/password-input.tsx +179 -0
  45. package/src/components/composed/social-providers.tsx +89 -0
  46. package/src/components/docs/code-block.tsx +8 -0
  47. package/src/components/docs/component-preview.tsx +28 -0
  48. package/src/components/docs/demo-controls.tsx +118 -0
  49. package/src/components/docs/props-table.tsx +35 -0
  50. package/src/components/theme-provider.tsx +230 -0
  51. package/src/components/ui/button.tsx +199 -0
  52. package/src/components/ui/card.tsx +253 -0
  53. package/src/components/ui/checkbox.tsx +83 -0
  54. package/src/components/ui/field.tsx +89 -0
  55. package/src/components/ui/frame.tsx +87 -0
  56. package/src/components/ui/glasscn/glass-button.tsx +25 -0
  57. package/src/components/ui/glasscn/liquid-glass.tsx +276 -0
  58. package/src/components/ui/input-group.tsx +105 -0
  59. package/src/components/ui/input.tsx +70 -0
  60. package/src/components/ui/label.tsx +36 -0
  61. package/src/components/ui/otp-field.tsx +88 -0
  62. package/src/components/ui/separator.tsx +23 -0
  63. package/src/components/ui/spinner.tsx +17 -0
  64. package/src/components/ui/textarea.tsx +58 -0
  65. package/src/index.css +171 -0
  66. package/src/index.ts +37 -0
  67. package/src/lib/auth-accent.ts +49 -0
  68. package/src/lib/glass-variants.ts +57 -0
  69. package/src/lib/sound.ts +39 -0
  70. package/src/lib/utils.test.ts +22 -0
  71. package/src/lib/utils.ts +6 -0
@@ -0,0 +1,266 @@
1
+ "use client";
2
+
3
+ import { AnimatePresence, MotionConfig, motion } from "motion/react";
4
+ import * as React from "react";
5
+
6
+ import {
7
+ Card,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardPanel,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Frame, FrameFooter } from "@/components/ui/frame";
14
+ import { Separator } from "@/components/ui/separator";
15
+ import { type AuthAccent, authAccentVars } from "@/lib/auth-accent";
16
+ import { cn } from "@/lib/utils";
17
+
18
+ /** transitions.dev motion scale, shared by all auth blocks. */
19
+ export const EASE = [0.22, 1, 0.36, 1] as const;
20
+
21
+ export type AuthStatus = "idle" | "loading" | "success" | "error";
22
+
23
+ /**
24
+ * Frame shell shared by the auth blocks (coss particle p-card-8): a raised
25
+ * Card inside a muted Frame, with the footer in the frame strip below the
26
+ * card. Wraps content in a MotionConfig that honors prefers-reduced-motion
27
+ * and layout-animates height changes so step swaps morph instead of jumping.
28
+ */
29
+ export function AuthCard({
30
+ title,
31
+ description,
32
+ children,
33
+ footer,
34
+ className,
35
+ logo,
36
+ accent,
37
+ }: {
38
+ title: React.ReactNode;
39
+ description?: React.ReactNode;
40
+ children: React.ReactNode;
41
+ footer?: React.ReactNode;
42
+ className?: string;
43
+ /** Brand mark rendered above the title. Omit for no logo row at all. */
44
+ logo?: React.ReactNode;
45
+ /** Brand accent for the card's primary action. Omit to keep `bg-primary`. */
46
+ accent?: AuthAccent;
47
+ }) {
48
+ return (
49
+ <MotionConfig reducedMotion="user">
50
+ <Frame
51
+ className={cn("w-full max-w-sm", className)}
52
+ data-accent={accent}
53
+ style={
54
+ accent ? (authAccentVars[accent] as React.CSSProperties) : undefined
55
+ }
56
+ >
57
+ <motion.div
58
+ layout
59
+ transition={{ duration: 0.3, ease: EASE }}
60
+ className="flex flex-col"
61
+ >
62
+ <Card>
63
+ <CardHeader>
64
+ {logo && (
65
+ // Sized like the button's icon rule: a bare img/svg gets a
66
+ // default height, an explicitly sized one keeps its own.
67
+ <div
68
+ className="mb-1.5 flex items-center [&_img:not([class*='h-'])]:h-8 [&_img]:w-auto [&_svg:not([class*='size-'])]:size-8"
69
+ data-slot="auth-logo"
70
+ >
71
+ {logo}
72
+ </div>
73
+ )}
74
+ <CardTitle>{title}</CardTitle>
75
+ {description && <CardDescription>{description}</CardDescription>}
76
+ </CardHeader>
77
+ <CardPanel className="overflow-hidden">{children}</CardPanel>
78
+ </Card>
79
+ {footer && (
80
+ <FrameFooter className="text-center text-sm text-muted-foreground">
81
+ {footer}
82
+ </FrameFooter>
83
+ )}
84
+ </motion.div>
85
+ </Frame>
86
+ </MotionConfig>
87
+ );
88
+ }
89
+
90
+ /** Cross-morphs between steps (form → success, …) keyed by `step`. */
91
+ export function MorphStep({
92
+ step,
93
+ children,
94
+ className,
95
+ }: {
96
+ step: string;
97
+ children: React.ReactNode;
98
+ className?: string;
99
+ }) {
100
+ return (
101
+ <AnimatePresence mode="popLayout" initial={false}>
102
+ <motion.div
103
+ key={step}
104
+ className={className}
105
+ initial={{ opacity: 0, y: 8, filter: "blur(2px)" }}
106
+ animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
107
+ exit={{ opacity: 0, y: -8, filter: "blur(2px)" }}
108
+ transition={{ duration: 0.25, ease: EASE }}
109
+ >
110
+ {children}
111
+ </motion.div>
112
+ </AnimatePresence>
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Progressive-disclosure wrapper: collapses/expands a step as `show` flips.
118
+ *
119
+ * `overflow-hidden` is only worn while the height is in flight — leaving it on
120
+ * would clip the focus ring of any full-width control inside (the ring sits
121
+ * 3px outside the input's border box, so the left/right edges would be cut).
122
+ */
123
+ export function Reveal({
124
+ show,
125
+ children,
126
+ className,
127
+ }: {
128
+ show: boolean;
129
+ children: React.ReactNode;
130
+ className?: string;
131
+ }) {
132
+ // Reset during render (not in an effect) when `show` flips, matching
133
+ // useCountdown below.
134
+ const [state, setState] = React.useState({ show, settled: show });
135
+ if (state.show !== show) setState({ show, settled: false });
136
+
137
+ return (
138
+ <AnimatePresence initial={false}>
139
+ {show && (
140
+ <motion.div
141
+ className={cn(!state.settled && "overflow-hidden", className)}
142
+ initial={{ height: 0, opacity: 0, y: -4, filter: "blur(2px)" }}
143
+ animate={{ height: "auto", opacity: 1, y: 0, filter: "blur(0px)" }}
144
+ exit={{ height: 0, opacity: 0, y: -4, filter: "blur(2px)" }}
145
+ transition={{ duration: 0.25, ease: EASE }}
146
+ onAnimationComplete={() =>
147
+ setState((current) => ({ ...current, settled: true }))
148
+ }
149
+ >
150
+ {children}
151
+ </motion.div>
152
+ )}
153
+ </AnimatePresence>
154
+ );
155
+ }
156
+
157
+ /** Inline error message that shakes each time `message` changes. */
158
+ export function AuthError({ message }: { message: string | null }) {
159
+ return (
160
+ <AnimatePresence initial={false}>
161
+ {message && (
162
+ <motion.p
163
+ key={message}
164
+ role="alert"
165
+ className="text-sm text-destructive-foreground"
166
+ initial={{ opacity: 0, height: 0 }}
167
+ animate={{
168
+ opacity: 1,
169
+ height: "auto",
170
+ x: [0, -6, 6, -4, 4, 0],
171
+ }}
172
+ exit={{ opacity: 0, height: 0 }}
173
+ transition={{
174
+ duration: 0.25,
175
+ ease: EASE,
176
+ x: { duration: 0.4, ease: "easeInOut" },
177
+ }}
178
+ >
179
+ {message}
180
+ </motion.p>
181
+ )}
182
+ </AnimatePresence>
183
+ );
184
+ }
185
+
186
+ /** Success panel with a stroke-drawn check. */
187
+ export function AuthSuccess({
188
+ title,
189
+ description,
190
+ children,
191
+ }: {
192
+ title: React.ReactNode;
193
+ description?: React.ReactNode;
194
+ children?: React.ReactNode;
195
+ }) {
196
+ return (
197
+ <div className="flex flex-col items-center gap-3 py-4 text-center">
198
+ <motion.span
199
+ className="flex size-12 items-center justify-center rounded-full bg-emerald-500/12 text-emerald-600"
200
+ initial={{ scale: 0.6, opacity: 0 }}
201
+ animate={{ scale: 1, opacity: 1 }}
202
+ transition={{ duration: 0.4, ease: EASE }}
203
+ >
204
+ <svg
205
+ aria-hidden="true"
206
+ viewBox="0 0 24 24"
207
+ className="size-6"
208
+ fill="none"
209
+ stroke="currentColor"
210
+ strokeWidth="2.5"
211
+ strokeLinecap="round"
212
+ strokeLinejoin="round"
213
+ >
214
+ <motion.path
215
+ d="M4 12.5l5 5L20 6.5"
216
+ initial={{ pathLength: 0 }}
217
+ animate={{ pathLength: 1 }}
218
+ transition={{ duration: 0.4, delay: 0.15, ease: EASE }}
219
+ />
220
+ </svg>
221
+ </motion.span>
222
+ <div className="flex flex-col gap-1">
223
+ <p className="font-medium">{title}</p>
224
+ {description && (
225
+ <p className="text-sm text-muted-foreground">{description}</p>
226
+ )}
227
+ </div>
228
+ {children}
229
+ </div>
230
+ );
231
+ }
232
+
233
+ /** "Or continue with" style divider. */
234
+ export function AuthSeparator({ label = "Or" }: { label?: string }) {
235
+ return (
236
+ <div className="flex items-center gap-3">
237
+ <Separator className="flex-1" />
238
+ <span className="text-xs text-muted-foreground uppercase">{label}</span>
239
+ <Separator className="flex-1" />
240
+ </div>
241
+ );
242
+ }
243
+
244
+ /** Countdown for resend links; restarts when `restartKey` changes. */
245
+ export function useCountdown(seconds: number, restartKey: number) {
246
+ const [state, setState] = React.useState({ restartKey, seconds, remaining: seconds });
247
+
248
+ // Reset during render (not in an effect) when the inputs change.
249
+ if (state.restartKey !== restartKey || state.seconds !== seconds) {
250
+ setState({ restartKey, seconds, remaining: seconds });
251
+ }
252
+
253
+ React.useEffect(() => {
254
+ if (seconds <= 0) return;
255
+ const interval = window.setInterval(() => {
256
+ setState((current) =>
257
+ current.remaining <= 0
258
+ ? current
259
+ : { ...current, remaining: current.remaining - 1 },
260
+ );
261
+ }, 1000);
262
+ return () => window.clearInterval(interval);
263
+ }, [seconds, restartKey]);
264
+
265
+ return state.remaining;
266
+ }
@@ -0,0 +1,190 @@
1
+ "use client";
2
+
3
+ import { MailIcon, SmartphoneIcon } from "lucide-react";
4
+ import { motion } from "motion/react";
5
+ import * as React from "react";
6
+
7
+ import { Button } from "@/components/ui/button";
8
+ import { OTPField, OTPFieldInput } from "@/components/ui/otp-field";
9
+ import { type AuthAccent, authAccentActionStyles } from "@/lib/auth-accent";
10
+ import { cn } from "@/lib/utils";
11
+ import {
12
+ AuthCard,
13
+ AuthError,
14
+ AuthSuccess,
15
+ EASE,
16
+ MorphStep,
17
+ useCountdown,
18
+ type AuthStatus,
19
+ } from "./auth-shell";
20
+
21
+ export interface CodeAuthBlockProps {
22
+ /** Brand mark rendered above the title. Omit for no logo row at all. */
23
+ logo?: React.ReactNode;
24
+ /** Brand accent for the primary action. Omit to keep the default primary. */
25
+ accent?: AuthAccent;
26
+ /** Where the code was sent; drives copy and icon. */
27
+ channel?: "email" | "sms";
28
+ /** Masked destination shown in the description, e.g. "y•••@example.com". */
29
+ destination?: string;
30
+ length?: number;
31
+ /** Submit automatically once all digits are entered. */
32
+ autoSubmit?: boolean;
33
+ /** Seconds before "Resend code" becomes available. 0 disables the wait. */
34
+ resendSeconds?: number;
35
+ /** Reject (or throw) to show the error shake and clear the code. */
36
+ onVerify?: (code: string) => Promise<void> | void;
37
+ onResend?: () => Promise<void> | void;
38
+ onBack?: () => void;
39
+ }
40
+
41
+ export function CodeAuthBlock({
42
+ channel = "email",
43
+ logo,
44
+ accent,
45
+ destination,
46
+ length = 6,
47
+ autoSubmit = true,
48
+ resendSeconds = 30,
49
+ onVerify,
50
+ onResend,
51
+ onBack,
52
+ }: CodeAuthBlockProps) {
53
+ const [status, setStatus] = React.useState<AuthStatus>("idle");
54
+ const [error, setError] = React.useState<string | null>(null);
55
+ const [code, setCode] = React.useState("");
56
+ const [resendCount, setResendCount] = React.useState(0);
57
+ const remaining = useCountdown(resendSeconds, resendCount);
58
+
59
+ const ChannelIcon = channel === "sms" ? SmartphoneIcon : MailIcon;
60
+ const channelLabel = channel === "sms" ? "phone" : "email";
61
+
62
+ const verify = React.useCallback(
63
+ async (value: string) => {
64
+ setError(null);
65
+ setStatus("loading");
66
+ try {
67
+ await onVerify?.(value);
68
+ setStatus("success");
69
+ } catch (cause) {
70
+ setStatus("idle");
71
+ setCode("");
72
+ setError(cause instanceof Error ? cause.message : "Invalid code.");
73
+ }
74
+ },
75
+ [onVerify],
76
+ );
77
+
78
+ function handleChange(value: string) {
79
+ setCode(value);
80
+ if (error) setError(null);
81
+ if (autoSubmit && value.length === length && status === "idle") {
82
+ void verify(value);
83
+ }
84
+ }
85
+
86
+ async function handleResend() {
87
+ setResendCount((count) => count + 1);
88
+ setCode("");
89
+ setError(null);
90
+ await onResend?.();
91
+ }
92
+
93
+ return (
94
+ <AuthCard
95
+ title="Enter verification code"
96
+ logo={logo}
97
+ accent={accent}
98
+ description={
99
+ <span className="inline-flex items-center gap-1.5">
100
+ <ChannelIcon className="size-3.5 shrink-0" />
101
+ <span>
102
+ We sent a {length}-digit code to your {channelLabel}
103
+ {destination ? (
104
+ <>
105
+ {" "}
106
+ <span className="font-medium text-foreground">
107
+ {destination}
108
+ </span>
109
+ </>
110
+ ) : null}
111
+ .
112
+ </span>
113
+ </span>
114
+ }
115
+ footer={
116
+ status !== "success" && (
117
+ <Button variant="link" onClick={onBack}>
118
+ Use a different {channelLabel}
119
+ </Button>
120
+ )
121
+ }
122
+ >
123
+ <MorphStep step={status === "success" ? "success" : "form"}>
124
+ {status === "success" ? (
125
+ <AuthSuccess
126
+ title="Verified"
127
+ description="Your identity has been confirmed."
128
+ />
129
+ ) : (
130
+ <div className="flex flex-col items-center gap-4">
131
+ <motion.div
132
+ // Shake the whole field on error; keying by message replays it.
133
+ key={error ?? "steady"}
134
+ animate={error ? { x: [0, -8, 8, -5, 5, 0] } : { x: 0 }}
135
+ transition={{ duration: 0.4, ease: "easeInOut" }}
136
+ >
137
+ <OTPField
138
+ length={length}
139
+ value={code}
140
+ onValueChange={handleChange}
141
+ disabled={status === "loading"}
142
+ >
143
+ {Array.from({ length }, (_, index) => (
144
+ <OTPFieldInput
145
+ key={`slot-${index}`}
146
+ aria-invalid={error ? true : undefined}
147
+ aria-label={`Digit ${index + 1} of ${length}`}
148
+ />
149
+ ))}
150
+ </OTPField>
151
+ </motion.div>
152
+ <AuthError message={error} />
153
+ {!autoSubmit && (
154
+ <Button
155
+ className={cn("w-full", accent && authAccentActionStyles)}
156
+ loading={status === "loading"}
157
+ disabled={code.length !== length}
158
+ onClick={() => void verify(code)}
159
+ >
160
+ Verify
161
+ </Button>
162
+ )}
163
+ <motion.p
164
+ layout
165
+ transition={{ duration: 0.25, ease: EASE }}
166
+ className="text-center text-sm text-muted-foreground"
167
+ >
168
+ {remaining > 0 ? (
169
+ <>
170
+ Resend code in{" "}
171
+ <span className="font-medium tabular-nums text-foreground">
172
+ {remaining}s
173
+ </span>
174
+ </>
175
+ ) : (
176
+ <Button
177
+ variant="link"
178
+ disabled={status === "loading"}
179
+ onClick={() => void handleResend()}
180
+ >
181
+ Resend code
182
+ </Button>
183
+ )}
184
+ </motion.p>
185
+ </div>
186
+ )}
187
+ </MorphStep>
188
+ </AuthCard>
189
+ );
190
+ }
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ import { ArrowLeftIcon } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import { Field, FieldLabel } from "@/components/ui/field";
8
+ import { Input } from "@/components/ui/input";
9
+ import { type AuthAccent, authAccentActionStyles } from "@/lib/auth-accent";
10
+ import { cn } from "@/lib/utils";
11
+ import {
12
+ AuthCard,
13
+ AuthError,
14
+ AuthSuccess,
15
+ MorphStep,
16
+ type AuthStatus,
17
+ } from "./auth-shell";
18
+
19
+ export interface ForgotPasswordBlockProps {
20
+ title?: string;
21
+ description?: string;
22
+ /** Brand mark rendered above the title. Omit for no logo row at all. */
23
+ logo?: React.ReactNode;
24
+ /** Brand accent for the primary action. Omit to keep the default primary. */
25
+ accent?: AuthAccent;
26
+ onSubmit?: (data: { email: string }) => Promise<void> | void;
27
+ onBackToLogin?: () => void;
28
+ }
29
+
30
+ export function ForgotPasswordBlock({
31
+ title = "Reset your password",
32
+ logo,
33
+ accent,
34
+ description = "Enter your email and we will send you a reset link",
35
+ onSubmit,
36
+ onBackToLogin,
37
+ }: ForgotPasswordBlockProps) {
38
+ const emailId = React.useId();
39
+ const [status, setStatus] = React.useState<AuthStatus>("idle");
40
+ const [error, setError] = React.useState<string | null>(null);
41
+ const [email, setEmail] = React.useState("");
42
+
43
+ async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
44
+ event.preventDefault();
45
+ setError(null);
46
+ setStatus("loading");
47
+ try {
48
+ await onSubmit?.({ email });
49
+ setStatus("success");
50
+ } catch (cause) {
51
+ setStatus("idle");
52
+ setError(
53
+ cause instanceof Error ? cause.message : "Unable to send reset link.",
54
+ );
55
+ }
56
+ }
57
+
58
+ return (
59
+ <AuthCard
60
+ title={title}
61
+ description={description}
62
+ logo={logo}
63
+ accent={accent}
64
+ footer={
65
+ <Button
66
+ variant="link"
67
+ className="gap-1"
68
+ onClick={onBackToLogin}
69
+ >
70
+ <ArrowLeftIcon className="size-3.5" />
71
+ Back to login
72
+ </Button>
73
+ }
74
+ >
75
+ <MorphStep step={status === "success" ? "success" : "form"}>
76
+ {status === "success" ? (
77
+ <AuthSuccess
78
+ title="Check your inbox"
79
+ description={
80
+ <>
81
+ We sent a password reset link to{" "}
82
+ <span className="font-medium text-foreground">{email}</span>.
83
+ </>
84
+ }
85
+ >
86
+ <Button
87
+ variant="link"
88
+ className="text-xs sm:text-xs"
89
+ onClick={() => setStatus("idle")}
90
+ >
91
+ Use a different email
92
+ </Button>
93
+ </AuthSuccess>
94
+ ) : (
95
+ <form className="flex flex-col gap-4" onSubmit={handleSubmit}>
96
+ <Field name="email">
97
+ <FieldLabel htmlFor={emailId}>Email</FieldLabel>
98
+ <Input
99
+ id={emailId}
100
+ name="email"
101
+ type="email"
102
+ autoComplete="email"
103
+ placeholder="you@example.com"
104
+ required
105
+ disabled={status === "loading"}
106
+ value={email}
107
+ onChange={(event) => setEmail(event.target.value)}
108
+ />
109
+ </Field>
110
+ <AuthError message={error} />
111
+ <Button
112
+ className={cn("w-full", accent && authAccentActionStyles)}
113
+ type="submit"
114
+ loading={status === "loading"}
115
+ >
116
+ Send reset link
117
+ </Button>
118
+ </form>
119
+ )}
120
+ </MorphStep>
121
+ </AuthCard>
122
+ );
123
+ }