@ankhorage/zora 0.5.3 → 0.6.1

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 (83) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/form/Form.d.ts +4 -0
  3. package/dist/components/form/Form.d.ts.map +1 -0
  4. package/dist/components/form/Form.js +27 -0
  5. package/dist/components/form/Form.js.map +1 -0
  6. package/dist/components/form/FormActions.d.ts +4 -0
  7. package/dist/components/form/FormActions.d.ts.map +1 -0
  8. package/dist/components/form/FormActions.js +12 -0
  9. package/dist/components/form/FormActions.js.map +1 -0
  10. package/dist/components/form/FormError.d.ts +4 -0
  11. package/dist/components/form/FormError.d.ts.map +1 -0
  12. package/dist/components/form/FormError.js +14 -0
  13. package/dist/components/form/FormError.js.map +1 -0
  14. package/dist/components/form/FormField.d.ts +4 -0
  15. package/dist/components/form/FormField.d.ts.map +1 -0
  16. package/dist/components/form/FormField.js +74 -0
  17. package/dist/components/form/FormField.js.map +1 -0
  18. package/dist/components/form/index.d.ts +8 -0
  19. package/dist/components/form/index.d.ts.map +1 -0
  20. package/dist/components/form/index.js +7 -0
  21. package/dist/components/form/index.js.map +1 -0
  22. package/dist/components/form/types.d.ts +107 -0
  23. package/dist/components/form/types.d.ts.map +1 -0
  24. package/dist/components/form/types.js +2 -0
  25. package/dist/components/form/types.js.map +1 -0
  26. package/dist/components/form/useFormController.d.ts +3 -0
  27. package/dist/components/form/useFormController.d.ts.map +1 -0
  28. package/dist/components/form/useFormController.js +62 -0
  29. package/dist/components/form/useFormController.js.map +1 -0
  30. package/dist/components/form/validation.d.ts +6 -0
  31. package/dist/components/form/validation.d.ts.map +1 -0
  32. package/dist/components/form/validation.js +52 -0
  33. package/dist/components/form/validation.js.map +1 -0
  34. package/dist/index.d.ts +4 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/patterns/auth/ForgotPasswordForm.d.ts +4 -0
  39. package/dist/patterns/auth/ForgotPasswordForm.d.ts.map +1 -0
  40. package/dist/patterns/auth/ForgotPasswordForm.js +31 -0
  41. package/dist/patterns/auth/ForgotPasswordForm.js.map +1 -0
  42. package/dist/patterns/auth/OtpForm.d.ts +4 -0
  43. package/dist/patterns/auth/OtpForm.d.ts.map +1 -0
  44. package/dist/patterns/auth/OtpForm.js +30 -0
  45. package/dist/patterns/auth/OtpForm.js.map +1 -0
  46. package/dist/patterns/auth/SignInForm.d.ts +4 -0
  47. package/dist/patterns/auth/SignInForm.d.ts.map +1 -0
  48. package/dist/patterns/auth/SignInForm.js +45 -0
  49. package/dist/patterns/auth/SignInForm.js.map +1 -0
  50. package/dist/patterns/auth/SignUpForm.d.ts +4 -0
  51. package/dist/patterns/auth/SignUpForm.d.ts.map +1 -0
  52. package/dist/patterns/auth/SignUpForm.js +37 -0
  53. package/dist/patterns/auth/SignUpForm.js.map +1 -0
  54. package/dist/patterns/auth/index.d.ts +6 -0
  55. package/dist/patterns/auth/index.d.ts.map +1 -0
  56. package/dist/patterns/auth/index.js +5 -0
  57. package/dist/patterns/auth/index.js.map +1 -0
  58. package/dist/patterns/auth/types.d.ts +57 -0
  59. package/dist/patterns/auth/types.d.ts.map +1 -0
  60. package/dist/patterns/auth/types.js +2 -0
  61. package/dist/patterns/auth/types.js.map +1 -0
  62. package/dist/patterns/auth/utils.d.ts +8 -0
  63. package/dist/patterns/auth/utils.d.ts.map +1 -0
  64. package/dist/patterns/auth/utils.js +51 -0
  65. package/dist/patterns/auth/utils.js.map +1 -0
  66. package/package.json +2 -2
  67. package/src/components/form/Form.tsx +61 -0
  68. package/src/components/form/FormActions.tsx +23 -0
  69. package/src/components/form/FormError.tsx +20 -0
  70. package/src/components/form/FormField.tsx +128 -0
  71. package/src/components/form/index.ts +24 -0
  72. package/src/components/form/types.ts +115 -0
  73. package/src/components/form/useFormController.ts +105 -0
  74. package/src/components/form/validation.test.ts +79 -0
  75. package/src/components/form/validation.ts +83 -0
  76. package/src/index.ts +43 -2
  77. package/src/patterns/auth/ForgotPasswordForm.tsx +84 -0
  78. package/src/patterns/auth/OtpForm.tsx +80 -0
  79. package/src/patterns/auth/SignInForm.tsx +111 -0
  80. package/src/patterns/auth/SignUpForm.tsx +76 -0
  81. package/src/patterns/auth/index.ts +17 -0
  82. package/src/patterns/auth/types.ts +67 -0
  83. package/src/patterns/auth/utils.ts +80 -0
@@ -0,0 +1,80 @@
1
+ import { Stack } from '@ankhorage/surface';
2
+ import React from 'react';
3
+
4
+ import { Button } from '../../components/button';
5
+ import { Form, type FormFieldConfig, type FormValues } from '../../components/form';
6
+ import type { OtpFormProps } from './types';
7
+
8
+ type OtpFieldName = 'otp';
9
+
10
+ export function OtpForm({
11
+ length = 6,
12
+ otpLabel = 'Code',
13
+ resendLabel = 'Resend code',
14
+ resendDisabled = false,
15
+ resendLoading = false,
16
+ loading = false,
17
+ disabled = false,
18
+ error,
19
+ submitLabel = 'Verify code',
20
+ onSubmit,
21
+ onResend,
22
+ testID,
23
+ }: OtpFormProps) {
24
+ const resolvedLength = Math.max(1, length);
25
+ const [values, setValues] = React.useState<FormValues<OtpFieldName>>({
26
+ otp: '',
27
+ });
28
+ const fields = React.useMemo<readonly FormFieldConfig<OtpFieldName>[]>(
29
+ () => [
30
+ {
31
+ name: 'otp',
32
+ label: otpLabel,
33
+ type: 'otp',
34
+ maxLength: resolvedLength,
35
+ rules: [{ kind: 'required' }, { kind: 'minLength', value: resolvedLength }],
36
+ },
37
+ ],
38
+ [otpLabel, resolvedLength],
39
+ );
40
+
41
+ const handleSubmit = React.useCallback(
42
+ (formValues: FormValues<OtpFieldName>) =>
43
+ onSubmit({
44
+ otp: formValues.otp.trim(),
45
+ }),
46
+ [onSubmit],
47
+ );
48
+
49
+ return (
50
+ <Form
51
+ actions={
52
+ onResend ? (
53
+ <Stack direction="row" gap="s" wrap="wrap">
54
+ <Button
55
+ disabled={disabled || loading || resendDisabled}
56
+ emphasis="ghost"
57
+ loading={resendLoading}
58
+ onPress={() => {
59
+ void onResend();
60
+ }}
61
+ size="s"
62
+ tone="neutral"
63
+ >
64
+ {resendLabel}
65
+ </Button>
66
+ </Stack>
67
+ ) : undefined
68
+ }
69
+ disabled={disabled}
70
+ error={error}
71
+ fields={fields}
72
+ loading={loading}
73
+ onChange={setValues}
74
+ onSubmit={handleSubmit}
75
+ submitLabel={submitLabel}
76
+ testID={testID}
77
+ values={values}
78
+ />
79
+ );
80
+ }
@@ -0,0 +1,111 @@
1
+ import { Stack } from '@ankhorage/surface';
2
+ import React from 'react';
3
+
4
+ import { Button } from '../../components/button';
5
+ import { Form, type FormFieldConfig, type FormValues } from '../../components/form';
6
+ import type { SignInFormProps } from './types';
7
+ import {
8
+ defaultIdentifiers,
9
+ normalizeIdentifierKind,
10
+ resolveIdentifierLabel,
11
+ resolveIdentifierRules,
12
+ resolveIdentifierType,
13
+ } from './utils';
14
+
15
+ type SignInFieldName = 'identifier' | 'secret';
16
+
17
+ export function SignInForm({
18
+ identifiers = defaultIdentifiers,
19
+ identifierLabel,
20
+ secretLabel = 'Password',
21
+ forgotPasswordLabel = 'Forgot password',
22
+ signUpLabel = 'Sign up',
23
+ loading = false,
24
+ disabled = false,
25
+ error,
26
+ submitLabel = 'Sign in',
27
+ onSubmit,
28
+ onForgotPassword,
29
+ onSignUp,
30
+ testID,
31
+ }: SignInFormProps) {
32
+ const [values, setValues] = React.useState<FormValues<SignInFieldName>>({
33
+ identifier: '',
34
+ secret: '',
35
+ });
36
+ const hasActions = Boolean(onForgotPassword ?? onSignUp);
37
+ const fields = React.useMemo<readonly FormFieldConfig<SignInFieldName>[]>(
38
+ () => [
39
+ {
40
+ name: 'identifier',
41
+ label: identifierLabel ?? resolveIdentifierLabel(identifiers),
42
+ type: resolveIdentifierType(identifiers),
43
+ autoCapitalize: 'none',
44
+ rules: resolveIdentifierRules(identifiers),
45
+ },
46
+ {
47
+ name: 'secret',
48
+ label: secretLabel,
49
+ type: 'password',
50
+ rules: [{ kind: 'required' }],
51
+ },
52
+ ],
53
+ [identifierLabel, identifiers, secretLabel],
54
+ );
55
+
56
+ const handleSubmit = React.useCallback(
57
+ (formValues: FormValues<SignInFieldName>) =>
58
+ onSubmit({
59
+ identifier: formValues.identifier.trim(),
60
+ identifierKind: normalizeIdentifierKind(formValues.identifier, identifiers),
61
+ secret: formValues.secret,
62
+ }),
63
+ [identifiers, onSubmit],
64
+ );
65
+
66
+ return (
67
+ <Form
68
+ actions={
69
+ hasActions ? (
70
+ <Stack direction="row" gap="s" wrap="wrap">
71
+ {onForgotPassword ? (
72
+ <Button
73
+ disabled={disabled || loading}
74
+ emphasis="ghost"
75
+ onPress={() => {
76
+ void onForgotPassword();
77
+ }}
78
+ size="s"
79
+ tone="neutral"
80
+ >
81
+ {forgotPasswordLabel}
82
+ </Button>
83
+ ) : null}
84
+ {onSignUp ? (
85
+ <Button
86
+ disabled={disabled || loading}
87
+ emphasis="ghost"
88
+ onPress={() => {
89
+ void onSignUp();
90
+ }}
91
+ size="s"
92
+ tone="neutral"
93
+ >
94
+ {signUpLabel}
95
+ </Button>
96
+ ) : null}
97
+ </Stack>
98
+ ) : undefined
99
+ }
100
+ disabled={disabled}
101
+ error={error}
102
+ fields={fields}
103
+ loading={loading}
104
+ onChange={setValues}
105
+ onSubmit={handleSubmit}
106
+ submitLabel={submitLabel}
107
+ testID={testID}
108
+ values={values}
109
+ />
110
+ );
111
+ }
@@ -0,0 +1,76 @@
1
+ import { Stack } from '@ankhorage/surface';
2
+ import React from 'react';
3
+
4
+ import { Button } from '../../components/button';
5
+ import { Form, type FormFieldConfig, type FormValues } from '../../components/form';
6
+ import type { SignUpFormProps } from './types';
7
+
8
+ const defaultSignUpFields = [
9
+ {
10
+ name: 'email',
11
+ label: 'Email',
12
+ type: 'email',
13
+ autoCapitalize: 'none',
14
+ rules: [{ kind: 'required' }, { kind: 'email' }],
15
+ },
16
+ {
17
+ name: 'password',
18
+ label: 'Password',
19
+ type: 'password',
20
+ rules: [{ kind: 'required' }, { kind: 'minLength', value: 8 }],
21
+ },
22
+ ] as const satisfies readonly FormFieldConfig[];
23
+
24
+ function createValues(fields: readonly FormFieldConfig[]): FormValues {
25
+ const values = fields.reduce<Record<string, string>>((nextValues, field) => {
26
+ nextValues[field.name] = '';
27
+ return nextValues;
28
+ }, {});
29
+
30
+ return values;
31
+ }
32
+
33
+ export function SignUpForm({
34
+ fields = defaultSignUpFields,
35
+ signInLabel = 'Sign in',
36
+ loading = false,
37
+ disabled = false,
38
+ error,
39
+ submitLabel = 'Sign up',
40
+ onSubmit,
41
+ onSignIn,
42
+ testID,
43
+ }: SignUpFormProps) {
44
+ const [values, setValues] = React.useState<FormValues>(() => createValues(fields));
45
+
46
+ return (
47
+ <Form
48
+ actions={
49
+ onSignIn ? (
50
+ <Stack direction="row" gap="s" wrap="wrap">
51
+ <Button
52
+ disabled={disabled || loading}
53
+ emphasis="ghost"
54
+ onPress={() => {
55
+ void onSignIn();
56
+ }}
57
+ size="s"
58
+ tone="neutral"
59
+ >
60
+ {signInLabel}
61
+ </Button>
62
+ </Stack>
63
+ ) : undefined
64
+ }
65
+ disabled={disabled}
66
+ error={error}
67
+ fields={fields}
68
+ loading={loading}
69
+ onChange={setValues}
70
+ onSubmit={onSubmit}
71
+ submitLabel={submitLabel}
72
+ testID={testID}
73
+ values={values}
74
+ />
75
+ );
76
+ }
@@ -0,0 +1,17 @@
1
+ export { ForgotPasswordForm } from './ForgotPasswordForm';
2
+ export { OtpForm } from './OtpForm';
3
+ export { SignInForm } from './SignInForm';
4
+ export { SignUpForm } from './SignUpForm';
5
+ export type {
6
+ AuthFormBaseProps,
7
+ AuthIdentifierKind,
8
+ ForgotPasswordFormProps,
9
+ ForgotPasswordFormValues,
10
+ OtpFormProps,
11
+ OtpFormValues,
12
+ SignInFormProps,
13
+ SignInFormValues,
14
+ SignUpFormField,
15
+ SignUpFormProps,
16
+ SignUpFormValues,
17
+ } from './types';
@@ -0,0 +1,67 @@
1
+ import type React from 'react';
2
+
3
+ import type { FormFieldConfig, FormValues } from '../../components/form';
4
+
5
+ export type AuthIdentifierKind = 'email' | 'phone' | 'username';
6
+
7
+ export interface AuthFormBaseProps {
8
+ loading?: boolean;
9
+ disabled?: boolean;
10
+ error?: React.ReactNode;
11
+ submitLabel?: React.ReactNode;
12
+ testID?: string;
13
+ }
14
+
15
+ export interface SignInFormValues {
16
+ identifier: string;
17
+ identifierKind: AuthIdentifierKind;
18
+ secret: string;
19
+ }
20
+
21
+ export interface SignInFormProps extends AuthFormBaseProps {
22
+ identifiers?: readonly AuthIdentifierKind[];
23
+ identifierLabel?: React.ReactNode;
24
+ secretLabel?: React.ReactNode;
25
+ forgotPasswordLabel?: React.ReactNode;
26
+ signUpLabel?: React.ReactNode;
27
+ onSubmit: (values: SignInFormValues) => void | Promise<void>;
28
+ onForgotPassword?: () => void | Promise<void>;
29
+ onSignUp?: () => void | Promise<void>;
30
+ }
31
+
32
+ export type SignUpFormValues = FormValues;
33
+ export type SignUpFormField = FormFieldConfig;
34
+
35
+ export interface SignUpFormProps extends AuthFormBaseProps {
36
+ fields?: readonly SignUpFormField[];
37
+ signInLabel?: React.ReactNode;
38
+ onSubmit: (values: SignUpFormValues) => void | Promise<void>;
39
+ onSignIn?: () => void | Promise<void>;
40
+ }
41
+
42
+ export interface ForgotPasswordFormValues {
43
+ identifier: string;
44
+ identifierKind: AuthIdentifierKind;
45
+ }
46
+
47
+ export interface ForgotPasswordFormProps extends AuthFormBaseProps {
48
+ identifiers?: readonly AuthIdentifierKind[];
49
+ identifierLabel?: React.ReactNode;
50
+ signInLabel?: React.ReactNode;
51
+ onSubmit: (values: ForgotPasswordFormValues) => void | Promise<void>;
52
+ onSignIn?: () => void | Promise<void>;
53
+ }
54
+
55
+ export interface OtpFormValues {
56
+ otp: string;
57
+ }
58
+
59
+ export interface OtpFormProps extends AuthFormBaseProps {
60
+ length?: number;
61
+ otpLabel?: React.ReactNode;
62
+ resendLabel?: React.ReactNode;
63
+ resendDisabled?: boolean;
64
+ resendLoading?: boolean;
65
+ onSubmit: (values: OtpFormValues) => void | Promise<void>;
66
+ onResend?: () => void | Promise<void>;
67
+ }
@@ -0,0 +1,80 @@
1
+ import type { FormFieldConfig, ValidationRule } from '../../components/form';
2
+ import type { AuthIdentifierKind } from './types';
3
+
4
+ export const defaultIdentifiers = ['email'] as const satisfies readonly AuthIdentifierKind[];
5
+
6
+ const identifierLabels: Record<AuthIdentifierKind, string> = {
7
+ email: 'Email',
8
+ phone: 'Phone',
9
+ username: 'Username',
10
+ };
11
+
12
+ function includesIdentifier(
13
+ identifiers: readonly AuthIdentifierKind[],
14
+ identifier: AuthIdentifierKind,
15
+ ): boolean {
16
+ return identifiers.includes(identifier);
17
+ }
18
+
19
+ export function resolveIdentifierLabel(identifiers: readonly AuthIdentifierKind[]): string {
20
+ if (identifiers.length === 1) {
21
+ return identifierLabels[identifiers[0] ?? 'email'];
22
+ }
23
+
24
+ return identifiers.map((identifier) => identifierLabels[identifier]).join(', or ');
25
+ }
26
+
27
+ export function resolveIdentifierType(
28
+ identifiers: readonly AuthIdentifierKind[],
29
+ ): FormFieldConfig['type'] {
30
+ if (identifiers.length !== 1) {
31
+ return 'text';
32
+ }
33
+
34
+ const [identifier] = identifiers;
35
+
36
+ if (identifier === 'email') {
37
+ return 'email';
38
+ }
39
+
40
+ if (identifier === 'phone') {
41
+ return 'tel';
42
+ }
43
+
44
+ return 'text';
45
+ }
46
+
47
+ export function resolveIdentifierRules(
48
+ identifiers: readonly AuthIdentifierKind[],
49
+ ): readonly ValidationRule[] {
50
+ if (identifiers.length === 1 && identifiers[0] === 'email') {
51
+ return [{ kind: 'required' }, { kind: 'email' }];
52
+ }
53
+
54
+ return [{ kind: 'required' }];
55
+ }
56
+
57
+ export function normalizeIdentifierKind(
58
+ identifier: string,
59
+ identifiers: readonly AuthIdentifierKind[],
60
+ ): AuthIdentifierKind {
61
+ const normalizedIdentifier = identifier.trim();
62
+
63
+ if (identifiers.length === 1) {
64
+ return identifiers[0] ?? 'email';
65
+ }
66
+
67
+ if (includesIdentifier(identifiers, 'email') && normalizedIdentifier.includes('@')) {
68
+ return 'email';
69
+ }
70
+
71
+ if (includesIdentifier(identifiers, 'phone') && /^[+()\d\s.-]+$/.test(normalizedIdentifier)) {
72
+ return 'phone';
73
+ }
74
+
75
+ if (includesIdentifier(identifiers, 'username')) {
76
+ return 'username';
77
+ }
78
+
79
+ return identifiers[0] ?? 'email';
80
+ }