@exegia/corpora-ui 0.6.0 → 0.8.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.
- package/dist-lib/components/blocks/auth/__tests__/linked-accounts-block.test.d.ts +1 -0
- package/dist-lib/components/blocks/auth/__tests__/onboarding-block.test.d.ts +1 -0
- package/dist-lib/components/blocks/auth/__tests__/passkey-manager-block.test.d.ts +1 -0
- package/dist-lib/components/blocks/auth/__tests__/passkey-sign-in-block.test.d.ts +1 -0
- package/dist-lib/components/blocks/auth/__tests__/signup-block.test.d.ts +1 -0
- package/dist-lib/components/blocks/auth/__tests__/update-password-block.test.d.ts +1 -0
- package/dist-lib/components/blocks/auth/linked-accounts-block.d.ts +30 -0
- package/dist-lib/components/blocks/auth/onboarding/profile-step.d.ts +19 -0
- package/dist-lib/components/blocks/auth/onboarding-block.d.ts +69 -0
- package/dist-lib/components/blocks/auth/passkey-manager-block.d.ts +38 -0
- package/dist-lib/components/blocks/auth/passkey-sign-in-block.d.ts +27 -0
- package/dist-lib/components/blocks/auth/signup-block.d.ts +16 -1
- package/dist-lib/components/blocks/auth/update-password-block.d.ts +28 -0
- package/dist-lib/components/composed/social-providers.d.ts +7 -0
- package/dist-lib/index.d.ts +5 -0
- package/dist-lib/index.js +1122 -392
- package/dist-lib/index.js.map +1 -1
- package/package.json +5 -1
- package/src/components/blocks/auth/__tests__/linked-accounts-block.test.tsx +110 -0
- package/src/components/blocks/auth/__tests__/onboarding-block.test.tsx +156 -0
- package/src/components/blocks/auth/__tests__/passkey-manager-block.test.tsx +131 -0
- package/src/components/blocks/auth/__tests__/passkey-sign-in-block.test.tsx +59 -0
- package/src/components/blocks/auth/__tests__/signup-block.test.tsx +176 -0
- package/src/components/blocks/auth/__tests__/update-password-block.test.tsx +95 -0
- package/src/components/blocks/auth/linked-accounts-block.tsx +223 -0
- package/src/components/blocks/auth/onboarding/profile-step.tsx +227 -0
- package/src/components/blocks/auth/onboarding-block.tsx +240 -0
- package/src/components/blocks/auth/passkey-manager-block.tsx +334 -0
- package/src/components/blocks/auth/passkey-sign-in-block.tsx +81 -0
- package/src/components/blocks/auth/signup-block.tsx +43 -10
- package/src/components/blocks/auth/update-password-block.tsx +157 -0
- package/src/components/composed/social-providers.tsx +3 -2
- package/src/index.ts +5 -0
|
@@ -31,6 +31,11 @@ import {
|
|
|
31
31
|
export interface SignupBlockProps {
|
|
32
32
|
title?: string;
|
|
33
33
|
description?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Replaces the built-in "terms" link inside the consent label — pass your
|
|
36
|
+
* own dialog trigger to render it inline instead of wiring `onTerms`.
|
|
37
|
+
*/
|
|
38
|
+
termsComponent?: React.ReactNode;
|
|
34
39
|
/** Brand mark rendered above the title. Omit for no logo row at all. */
|
|
35
40
|
logo?: React.ReactNode;
|
|
36
41
|
/** Brand accent for the primary action. Omit to keep the default primary. */
|
|
@@ -39,6 +44,16 @@ export interface SignupBlockProps {
|
|
|
39
44
|
showNameField?: boolean;
|
|
40
45
|
/** Require the terms checkbox before submitting. */
|
|
41
46
|
showTerms?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Controls the terms checkbox. Pass it with `onTermsCheckedChange` when
|
|
49
|
+
* something outside the block has to tick the box — an "I agree" action in
|
|
50
|
+
* your own terms dialog, say. Omit to let the block own the state.
|
|
51
|
+
*/
|
|
52
|
+
termsChecked?: boolean;
|
|
53
|
+
/** Starting state of the terms checkbox while it is uncontrolled. */
|
|
54
|
+
defaultTermsChecked?: boolean;
|
|
55
|
+
/** Fires on every change, controlled or not. */
|
|
56
|
+
onTermsCheckedChange?: (checked: boolean) => void;
|
|
42
57
|
/** Block submission until every password requirement is met. */
|
|
43
58
|
enforceStrongPassword?: boolean;
|
|
44
59
|
onSubmit?: (data: {
|
|
@@ -59,21 +74,36 @@ export function SignupBlock({
|
|
|
59
74
|
providers = ["google", "apple", "github"],
|
|
60
75
|
showNameField = true,
|
|
61
76
|
showTerms = true,
|
|
77
|
+
termsChecked,
|
|
78
|
+
defaultTermsChecked = false,
|
|
79
|
+
onTermsCheckedChange,
|
|
62
80
|
enforceStrongPassword = true,
|
|
63
81
|
onSubmit,
|
|
82
|
+
termsComponent,
|
|
64
83
|
onProviderSelect,
|
|
65
84
|
onLogin,
|
|
66
85
|
onTerms,
|
|
67
86
|
}: SignupBlockProps) {
|
|
68
87
|
const nameId = React.useId();
|
|
69
88
|
const emailId = React.useId();
|
|
89
|
+
const passwordId = React.useId();
|
|
70
90
|
const termsId = React.useId();
|
|
71
91
|
const [status, setStatus] = React.useState<AuthStatus>("idle");
|
|
72
92
|
const [error, setError] = React.useState<string | null>(null);
|
|
73
93
|
const [loadingProvider, setLoadingProvider] =
|
|
74
94
|
React.useState<SocialProvider | null>(null);
|
|
75
95
|
const [password, setPassword] = React.useState("");
|
|
76
|
-
|
|
96
|
+
// Controlled when `termsChecked` is passed, uncontrolled otherwise. The
|
|
97
|
+
// internal state is kept either way, so a block that switches between the
|
|
98
|
+
// two mid-life does not lose the box, and the callback fires in both modes.
|
|
99
|
+
const [uncontrolledTerms, setUncontrolledTerms] =
|
|
100
|
+
React.useState(defaultTermsChecked);
|
|
101
|
+
const terms = termsChecked ?? uncontrolledTerms;
|
|
102
|
+
|
|
103
|
+
function handleTermsChange(checked: boolean) {
|
|
104
|
+
if (termsChecked === undefined) setUncontrolledTerms(checked);
|
|
105
|
+
onTermsCheckedChange?.(checked);
|
|
106
|
+
}
|
|
77
107
|
// Tracked from each input's own constraint validation rather than a regex.
|
|
78
108
|
// The values stay uncontrolled — only validity is needed, and the form reads
|
|
79
109
|
// values from FormData on submit.
|
|
@@ -198,6 +228,7 @@ export function SignupBlock({
|
|
|
198
228
|
id={emailId}
|
|
199
229
|
name="email"
|
|
200
230
|
type="email"
|
|
231
|
+
aria-label="Email"
|
|
201
232
|
autoComplete="email"
|
|
202
233
|
placeholder="you@example.com"
|
|
203
234
|
required
|
|
@@ -209,9 +240,11 @@ export function SignupBlock({
|
|
|
209
240
|
</Field>
|
|
210
241
|
<Reveal show={emailValid}>
|
|
211
242
|
<Field name="password">
|
|
212
|
-
<FieldLabel>Password</FieldLabel>
|
|
243
|
+
<FieldLabel htmlFor={passwordId}>Password</FieldLabel>
|
|
213
244
|
<PasswordInput
|
|
245
|
+
id={passwordId}
|
|
214
246
|
name="password"
|
|
247
|
+
aria-label="Password"
|
|
215
248
|
autoComplete="new-password"
|
|
216
249
|
placeholder="Create a password"
|
|
217
250
|
showStrength
|
|
@@ -227,18 +260,18 @@ export function SignupBlock({
|
|
|
227
260
|
<Checkbox
|
|
228
261
|
id={termsId}
|
|
229
262
|
checked={terms}
|
|
230
|
-
onCheckedChange={(checked) =>
|
|
263
|
+
onCheckedChange={(checked) =>
|
|
264
|
+
handleTermsChange(checked === true)
|
|
265
|
+
}
|
|
231
266
|
disabled={busy}
|
|
232
267
|
/>
|
|
233
268
|
<Label htmlFor={termsId} className="font-normal">
|
|
234
269
|
I agree to the{" "}
|
|
235
|
-
|
|
236
|
-
variant="link"
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
terms
|
|
241
|
-
</Button>
|
|
270
|
+
{termsComponent ?? (
|
|
271
|
+
<Button variant="link" type="button" onClick={onTerms}>
|
|
272
|
+
terms
|
|
273
|
+
</Button>
|
|
274
|
+
)}
|
|
242
275
|
</Label>
|
|
243
276
|
</div>
|
|
244
277
|
)}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getPasswordStrength,
|
|
7
|
+
PasswordInput,
|
|
8
|
+
} from "@/components/composed/password-input";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { Field, FieldLabel } from "@/components/ui/field";
|
|
11
|
+
import { type AuthAccent, authAccentActionStyles } from "@/lib/auth-accent";
|
|
12
|
+
import { cn } from "@/lib/utils";
|
|
13
|
+
import {
|
|
14
|
+
AuthCard,
|
|
15
|
+
AuthError,
|
|
16
|
+
AuthSuccess,
|
|
17
|
+
MorphStep,
|
|
18
|
+
Reveal,
|
|
19
|
+
type AuthStatus,
|
|
20
|
+
} from "./auth-shell";
|
|
21
|
+
|
|
22
|
+
export interface UpdatePasswordBlockProps {
|
|
23
|
+
title?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
/** Brand mark rendered above the title. Omit for no logo row at all. */
|
|
26
|
+
logo?: React.ReactNode;
|
|
27
|
+
/** Brand accent for the primary action. Omit to keep the default primary. */
|
|
28
|
+
accent?: AuthAccent;
|
|
29
|
+
/**
|
|
30
|
+
* Minimum strength (0-4, as scored by `getPasswordStrength`) the new
|
|
31
|
+
* password must reach before the confirm field is revealed. 0 disables the
|
|
32
|
+
* gate.
|
|
33
|
+
*/
|
|
34
|
+
minStrength?: number;
|
|
35
|
+
/** Reject (or throw) to show the error state with the error's message. */
|
|
36
|
+
onSubmit?: (data: { password: string }) => Promise<void> | void;
|
|
37
|
+
onDone?: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Password change form for an already-authenticated user: a strength-metered
|
|
42
|
+
* new password gates the confirm field, which in turn gates the submit
|
|
43
|
+
* button. Mismatch is caught client-side; everything else comes back as a
|
|
44
|
+
* rejection from `onSubmit`.
|
|
45
|
+
*/
|
|
46
|
+
export function UpdatePasswordBlock({
|
|
47
|
+
title = "Update your password",
|
|
48
|
+
description = "Choose a new password for your account",
|
|
49
|
+
logo,
|
|
50
|
+
accent,
|
|
51
|
+
minStrength = 4,
|
|
52
|
+
onSubmit,
|
|
53
|
+
onDone,
|
|
54
|
+
}: UpdatePasswordBlockProps) {
|
|
55
|
+
const passwordId = React.useId();
|
|
56
|
+
const confirmId = React.useId();
|
|
57
|
+
const [status, setStatus] = React.useState<AuthStatus>("idle");
|
|
58
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
59
|
+
const [password, setPassword] = React.useState("");
|
|
60
|
+
const [confirmPassword, setConfirmPassword] = React.useState("");
|
|
61
|
+
|
|
62
|
+
// Progressive disclosure, matching the signup block: the confirm field
|
|
63
|
+
// appears once the new password clears the policy, the submit button once
|
|
64
|
+
// the two match.
|
|
65
|
+
const strongEnough = getPasswordStrength(password) >= minStrength;
|
|
66
|
+
const matches = confirmPassword.length > 0 && confirmPassword === password;
|
|
67
|
+
const canSubmit = strongEnough && matches;
|
|
68
|
+
|
|
69
|
+
// Only flag a mismatch once the confirmation has diverged — while it is
|
|
70
|
+
// still a prefix of the new password the user is simply mid-keystroke, and
|
|
71
|
+
// nagging on every character would shake the field the whole way through.
|
|
72
|
+
const mismatch =
|
|
73
|
+
confirmPassword.length > 0 && !password.startsWith(confirmPassword);
|
|
74
|
+
|
|
75
|
+
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
// Enter in any field submits the form even while the button is hidden.
|
|
78
|
+
if (!canSubmit) return;
|
|
79
|
+
setError(null);
|
|
80
|
+
setStatus("loading");
|
|
81
|
+
try {
|
|
82
|
+
await onSubmit?.({ password });
|
|
83
|
+
setStatus("success");
|
|
84
|
+
} catch (cause) {
|
|
85
|
+
setStatus("idle");
|
|
86
|
+
setError(
|
|
87
|
+
cause instanceof Error ? cause.message : "Unable to update password.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<AuthCard
|
|
94
|
+
title={title}
|
|
95
|
+
description={description}
|
|
96
|
+
logo={logo}
|
|
97
|
+
accent={accent}
|
|
98
|
+
>
|
|
99
|
+
<MorphStep step={status === "success" ? "success" : "form"}>
|
|
100
|
+
{status === "success" ? (
|
|
101
|
+
<AuthSuccess
|
|
102
|
+
title="Password updated"
|
|
103
|
+
description="Your password has been changed successfully."
|
|
104
|
+
>
|
|
105
|
+
{onDone && (
|
|
106
|
+
<Button variant="link" onClick={onDone}>
|
|
107
|
+
Continue
|
|
108
|
+
</Button>
|
|
109
|
+
)}
|
|
110
|
+
</AuthSuccess>
|
|
111
|
+
) : (
|
|
112
|
+
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
|
113
|
+
<Field name="password">
|
|
114
|
+
<FieldLabel htmlFor={passwordId}>New password</FieldLabel>
|
|
115
|
+
<PasswordInput
|
|
116
|
+
id={passwordId}
|
|
117
|
+
name="password"
|
|
118
|
+
autoComplete="new-password"
|
|
119
|
+
required
|
|
120
|
+
showStrength
|
|
121
|
+
disabled={status === "loading"}
|
|
122
|
+
value={password}
|
|
123
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
124
|
+
/>
|
|
125
|
+
</Field>
|
|
126
|
+
<Reveal show={strongEnough}>
|
|
127
|
+
<Field name="confirmPassword">
|
|
128
|
+
<FieldLabel htmlFor={confirmId}>Confirm new password</FieldLabel>
|
|
129
|
+
<PasswordInput
|
|
130
|
+
id={confirmId}
|
|
131
|
+
name="confirmPassword"
|
|
132
|
+
autoComplete="new-password"
|
|
133
|
+
required
|
|
134
|
+
disabled={status === "loading"}
|
|
135
|
+
value={confirmPassword}
|
|
136
|
+
onChange={(event) => setConfirmPassword(event.target.value)}
|
|
137
|
+
/>
|
|
138
|
+
</Field>
|
|
139
|
+
</Reveal>
|
|
140
|
+
<AuthError
|
|
141
|
+
message={error ?? (mismatch ? "Passwords do not match." : null)}
|
|
142
|
+
/>
|
|
143
|
+
<Reveal show={canSubmit}>
|
|
144
|
+
<Button
|
|
145
|
+
className={cn("w-full", accent && authAccentActionStyles)}
|
|
146
|
+
type="submit"
|
|
147
|
+
loading={status === "loading"}
|
|
148
|
+
>
|
|
149
|
+
Update password
|
|
150
|
+
</Button>
|
|
151
|
+
</Reveal>
|
|
152
|
+
</form>
|
|
153
|
+
)}
|
|
154
|
+
</MorphStep>
|
|
155
|
+
</AuthCard>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -13,7 +13,8 @@ import { cn } from "@/lib/utils";
|
|
|
13
13
|
|
|
14
14
|
export type SocialProvider = "google" | "apple" | "github" | "x";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/** Label + brand mark per provider, shared with the auth blocks. */
|
|
17
|
+
export const SOCIAL_PROVIDERS: Record<
|
|
17
18
|
SocialProvider,
|
|
18
19
|
{ label: string; Icon: React.ComponentType<{ className?: string }> }
|
|
19
20
|
> = {
|
|
@@ -63,7 +64,7 @@ export function SocialProviders({
|
|
|
63
64
|
)}
|
|
64
65
|
>
|
|
65
66
|
{providers.map((provider) => {
|
|
66
|
-
const { label, Icon } =
|
|
67
|
+
const { label, Icon } = SOCIAL_PROVIDERS[provider];
|
|
67
68
|
return (
|
|
68
69
|
<Button
|
|
69
70
|
key={provider}
|
package/src/index.ts
CHANGED
|
@@ -28,8 +28,13 @@ export * from "./components/composed/social-providers"
|
|
|
28
28
|
export * from "./components/blocks/auth/auth-shell"
|
|
29
29
|
export * from "./components/blocks/auth/code-auth-block"
|
|
30
30
|
export * from "./components/blocks/auth/forgot-password-block"
|
|
31
|
+
export * from "./components/blocks/auth/linked-accounts-block"
|
|
31
32
|
export * from "./components/blocks/auth/login-block"
|
|
33
|
+
export * from "./components/blocks/auth/onboarding-block"
|
|
34
|
+
export * from "./components/blocks/auth/passkey-manager-block"
|
|
35
|
+
export * from "./components/blocks/auth/passkey-sign-in-block"
|
|
32
36
|
export * from "./components/blocks/auth/signup-block"
|
|
37
|
+
export * from "./components/blocks/auth/update-password-block"
|
|
33
38
|
|
|
34
39
|
// lib
|
|
35
40
|
export * from "./lib/auth-accent"
|