@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.
Files changed (33) hide show
  1. package/dist-lib/components/blocks/auth/__tests__/linked-accounts-block.test.d.ts +1 -0
  2. package/dist-lib/components/blocks/auth/__tests__/onboarding-block.test.d.ts +1 -0
  3. package/dist-lib/components/blocks/auth/__tests__/passkey-manager-block.test.d.ts +1 -0
  4. package/dist-lib/components/blocks/auth/__tests__/passkey-sign-in-block.test.d.ts +1 -0
  5. package/dist-lib/components/blocks/auth/__tests__/signup-block.test.d.ts +1 -0
  6. package/dist-lib/components/blocks/auth/__tests__/update-password-block.test.d.ts +1 -0
  7. package/dist-lib/components/blocks/auth/linked-accounts-block.d.ts +30 -0
  8. package/dist-lib/components/blocks/auth/onboarding/profile-step.d.ts +19 -0
  9. package/dist-lib/components/blocks/auth/onboarding-block.d.ts +69 -0
  10. package/dist-lib/components/blocks/auth/passkey-manager-block.d.ts +38 -0
  11. package/dist-lib/components/blocks/auth/passkey-sign-in-block.d.ts +27 -0
  12. package/dist-lib/components/blocks/auth/signup-block.d.ts +16 -1
  13. package/dist-lib/components/blocks/auth/update-password-block.d.ts +28 -0
  14. package/dist-lib/components/composed/social-providers.d.ts +7 -0
  15. package/dist-lib/index.d.ts +5 -0
  16. package/dist-lib/index.js +1122 -392
  17. package/dist-lib/index.js.map +1 -1
  18. package/package.json +5 -1
  19. package/src/components/blocks/auth/__tests__/linked-accounts-block.test.tsx +110 -0
  20. package/src/components/blocks/auth/__tests__/onboarding-block.test.tsx +156 -0
  21. package/src/components/blocks/auth/__tests__/passkey-manager-block.test.tsx +131 -0
  22. package/src/components/blocks/auth/__tests__/passkey-sign-in-block.test.tsx +59 -0
  23. package/src/components/blocks/auth/__tests__/signup-block.test.tsx +176 -0
  24. package/src/components/blocks/auth/__tests__/update-password-block.test.tsx +95 -0
  25. package/src/components/blocks/auth/linked-accounts-block.tsx +223 -0
  26. package/src/components/blocks/auth/onboarding/profile-step.tsx +227 -0
  27. package/src/components/blocks/auth/onboarding-block.tsx +240 -0
  28. package/src/components/blocks/auth/passkey-manager-block.tsx +334 -0
  29. package/src/components/blocks/auth/passkey-sign-in-block.tsx +81 -0
  30. package/src/components/blocks/auth/signup-block.tsx +43 -10
  31. package/src/components/blocks/auth/update-password-block.tsx +157 -0
  32. package/src/components/composed/social-providers.tsx +3 -2
  33. 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
- const [terms, setTerms] = React.useState(false);
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) => setTerms(checked === true)}
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
- <Button
236
- variant="link"
237
- type="button"
238
- onClick={onTerms}
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
- const PROVIDERS: Record<
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 } = PROVIDERS[provider];
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"