@terreno/ui 0.1.0 → 0.3.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/AIRequestExplorer.d.ts +31 -0
- package/dist/AIRequestExplorer.js +44 -0
- package/dist/AIRequestExplorer.js.map +1 -0
- package/dist/AttachmentPreview.d.ts +8 -0
- package/dist/AttachmentPreview.js +16 -0
- package/dist/AttachmentPreview.js.map +1 -0
- package/dist/Common.d.ts +44 -0
- package/dist/FilePickerButton.d.ts +13 -0
- package/dist/FilePickerButton.js +50 -0
- package/dist/FilePickerButton.js.map +1 -0
- package/dist/GPTChat.d.ts +66 -0
- package/dist/GPTChat.js +112 -0
- package/dist/GPTChat.js.map +1 -0
- package/dist/GPTMemoryModal.d.ts +8 -0
- package/dist/GPTMemoryModal.js +14 -0
- package/dist/GPTMemoryModal.js.map +1 -0
- package/dist/SocialLoginButton.d.ts +19 -0
- package/dist/SocialLoginButton.js +119 -0
- package/dist/SocialLoginButton.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/login/LoginScreen.d.ts +25 -0
- package/dist/login/LoginScreen.js +55 -0
- package/dist/login/LoginScreen.js.map +1 -0
- package/dist/login/index.d.ts +2 -0
- package/dist/login/index.js +2 -0
- package/dist/login/index.js.map +1 -0
- package/dist/login/loginTypes.d.ts +48 -0
- package/dist/login/loginTypes.js +2 -0
- package/dist/login/loginTypes.js.map +1 -0
- package/dist/signUp/OAuthButtons.d.ts +18 -0
- package/dist/signUp/OAuthButtons.js +15 -0
- package/dist/signUp/OAuthButtons.js.map +1 -0
- package/dist/signUp/PasswordRequirements.d.ts +15 -0
- package/dist/signUp/PasswordRequirements.js +14 -0
- package/dist/signUp/PasswordRequirements.js.map +1 -0
- package/dist/signUp/SignUpScreen.d.ts +26 -0
- package/dist/signUp/SignUpScreen.js +64 -0
- package/dist/signUp/SignUpScreen.js.map +1 -0
- package/dist/signUp/Swiper.d.ts +13 -0
- package/dist/signUp/Swiper.js +16 -0
- package/dist/signUp/Swiper.js.map +1 -0
- package/dist/signUp/index.d.ts +6 -0
- package/dist/signUp/index.js +6 -0
- package/dist/signUp/index.js.map +1 -0
- package/dist/signUp/passwordPresets.d.ts +9 -0
- package/dist/signUp/passwordPresets.js +41 -0
- package/dist/signUp/passwordPresets.js.map +1 -0
- package/dist/signUp/signUpTypes.d.ts +90 -0
- package/dist/signUp/signUpTypes.js +2 -0
- package/dist/signUp/signUpTypes.js.map +1 -0
- package/package.json +4 -2
- package/src/AIRequestExplorer.tsx +147 -0
- package/src/AttachmentPreview.tsx +63 -0
- package/src/Common.ts +52 -0
- package/src/FilePickerButton.tsx +88 -0
- package/src/GPTChat.tsx +551 -0
- package/src/GPTMemoryModal.tsx +50 -0
- package/src/SocialLoginButton.test.tsx +158 -0
- package/src/SocialLoginButton.tsx +182 -0
- package/src/__snapshots__/SocialLoginButton.test.tsx.snap +277 -0
- package/src/index.tsx +9 -0
- package/src/login/LoginScreen.test.tsx +148 -0
- package/src/login/LoginScreen.tsx +159 -0
- package/src/login/__snapshots__/LoginScreen.test.tsx.snap +630 -0
- package/src/login/index.ts +2 -0
- package/src/login/loginTypes.ts +51 -0
- package/src/signUp/OAuthButtons.test.tsx +45 -0
- package/src/signUp/OAuthButtons.tsx +52 -0
- package/src/signUp/PasswordRequirements.test.tsx +41 -0
- package/src/signUp/PasswordRequirements.tsx +49 -0
- package/src/signUp/SignUpScreen.test.tsx +134 -0
- package/src/signUp/SignUpScreen.tsx +172 -0
- package/src/signUp/Swiper.test.tsx +46 -0
- package/src/signUp/Swiper.tsx +59 -0
- package/src/signUp/__snapshots__/OAuthButtons.test.tsx.snap +272 -0
- package/src/signUp/__snapshots__/PasswordRequirements.test.tsx.snap +427 -0
- package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +851 -0
- package/src/signUp/__snapshots__/Swiper.test.tsx.snap +249 -0
- package/src/signUp/index.ts +13 -0
- package/src/signUp/passwordPresets.test.ts +57 -0
- package/src/signUp/passwordPresets.ts +43 -0
- package/src/signUp/signUpTypes.ts +94 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PasswordRequirement } from "./signUpTypes";
|
|
2
|
+
/**
|
|
3
|
+
* Default password requirements with strong validation rules.
|
|
4
|
+
*/
|
|
5
|
+
export declare const defaultPasswordRequirements: PasswordRequirement[];
|
|
6
|
+
/**
|
|
7
|
+
* Simple password requirements with minimal validation.
|
|
8
|
+
*/
|
|
9
|
+
export declare const simplePasswordRequirements: PasswordRequirement[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default password requirements with strong validation rules.
|
|
3
|
+
*/
|
|
4
|
+
export const defaultPasswordRequirements = [
|
|
5
|
+
{
|
|
6
|
+
key: "minLength",
|
|
7
|
+
label: "At least 8 characters",
|
|
8
|
+
validate: (password) => password.length >= 8,
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
key: "uppercase",
|
|
12
|
+
label: "At least one uppercase letter",
|
|
13
|
+
validate: (password) => /[A-Z]/.test(password),
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: "lowercase",
|
|
17
|
+
label: "At least one lowercase letter",
|
|
18
|
+
validate: (password) => /[a-z]/.test(password),
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: "number",
|
|
22
|
+
label: "At least one number",
|
|
23
|
+
validate: (password) => /\d/.test(password),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: "special",
|
|
27
|
+
label: "At least one special character",
|
|
28
|
+
validate: (password) => /[!@#$%^&*(),.?":{}|<>]/.test(password),
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Simple password requirements with minimal validation.
|
|
33
|
+
*/
|
|
34
|
+
export const simplePasswordRequirements = [
|
|
35
|
+
{
|
|
36
|
+
key: "minLength",
|
|
37
|
+
label: "At least 6 characters",
|
|
38
|
+
validate: (password) => password.length >= 6,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
//# sourceMappingURL=passwordPresets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passwordPresets.js","sourceRoot":"","sources":["../../src/signUp/passwordPresets.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAA0B;IAChE;QACE,GAAG,EAAE,WAAW;QAChB,KAAK,EAAE,uBAAuB;QAC9B,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC;KACrD;IACD;QACE,GAAG,EAAE,WAAW;QAChB,KAAK,EAAE,+BAA+B;QACtC,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;KACvD;IACD;QACE,GAAG,EAAE,WAAW;QAChB,KAAK,EAAE,+BAA+B;QACtC,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;KACvD;IACD;QACE,GAAG,EAAE,QAAQ;QACb,KAAK,EAAE,qBAAqB;QAC5B,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;KACpD;IACD;QACE,GAAG,EAAE,SAAS;QACd,KAAK,EAAE,gCAAgC;QACvC,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,wBAAwB,CAAC,IAAI,CAAC,QAAQ,CAAC;KACxE;CACF,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAA0B;IAC/D;QACE,GAAG,EAAE,WAAW;QAChB,KAAK,EAAE,uBAAuB;QAC9B,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC;KACrD;CACF,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Supported OAuth providers for social login buttons.
|
|
4
|
+
*/
|
|
5
|
+
export type OAuthProvider = "google" | "github" | "apple";
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for an OAuth provider button.
|
|
8
|
+
*/
|
|
9
|
+
export interface OAuthProviderConfig {
|
|
10
|
+
/** The OAuth provider identifier. */
|
|
11
|
+
provider: OAuthProvider;
|
|
12
|
+
/** Callback triggered when the provider button is pressed. */
|
|
13
|
+
onPress: () => Promise<void>;
|
|
14
|
+
/** Whether the button is in a loading state. */
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
/** Whether the button is disabled. */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Configuration for a sign-up form field.
|
|
21
|
+
*/
|
|
22
|
+
export interface SignUpFieldConfig {
|
|
23
|
+
/** Unique field name used as the key in form state. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** Display label for the field. */
|
|
26
|
+
label: string;
|
|
27
|
+
/** Placeholder text shown when the field is empty. */
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
/** Input type for the field. */
|
|
30
|
+
type?: "text" | "email" | "password";
|
|
31
|
+
/** Whether the field is required. */
|
|
32
|
+
required?: boolean;
|
|
33
|
+
/** Auto-complete hint for the field. */
|
|
34
|
+
autoComplete?: "current-password" | "on" | "off" | "username";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A single password requirement with a label and validation function.
|
|
38
|
+
*/
|
|
39
|
+
export interface PasswordRequirement {
|
|
40
|
+
/** Unique key for the requirement. */
|
|
41
|
+
key: string;
|
|
42
|
+
/** Display label for the requirement. */
|
|
43
|
+
label: string;
|
|
44
|
+
/** Returns true if the password meets this requirement. */
|
|
45
|
+
validate: (password: string) => boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Configuration for a single onboarding page in the swiper.
|
|
49
|
+
*/
|
|
50
|
+
export interface OnboardingPage {
|
|
51
|
+
/** Title text displayed on the page. */
|
|
52
|
+
title: string;
|
|
53
|
+
/** Subtitle or description text. */
|
|
54
|
+
subtitle?: string;
|
|
55
|
+
/** Custom content to render on the page. */
|
|
56
|
+
content?: ReactNode;
|
|
57
|
+
/** Image source for the page. */
|
|
58
|
+
image?: number | {
|
|
59
|
+
uri: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Props for the SignUpScreen component.
|
|
64
|
+
*/
|
|
65
|
+
export interface SignUpScreenProps {
|
|
66
|
+
/** Form field configurations. */
|
|
67
|
+
fields: SignUpFieldConfig[];
|
|
68
|
+
/** Callback triggered on form submission. Receives field values as a record. */
|
|
69
|
+
onSubmit: (values: Record<string, string>) => Promise<void>;
|
|
70
|
+
/** Optional OAuth provider configurations for social login buttons. */
|
|
71
|
+
oauthProviders?: OAuthProviderConfig[];
|
|
72
|
+
/** Password requirements to validate against. */
|
|
73
|
+
passwordRequirements?: PasswordRequirement[];
|
|
74
|
+
/** Onboarding pages to display before the sign-up form. */
|
|
75
|
+
onboardingPages?: OnboardingPage[];
|
|
76
|
+
/** Custom logo or banner to display above the form. */
|
|
77
|
+
logo?: ReactNode;
|
|
78
|
+
/** Title text for the sign-up form. */
|
|
79
|
+
title?: string;
|
|
80
|
+
/** Whether the form is in a loading state. */
|
|
81
|
+
loading?: boolean;
|
|
82
|
+
/** Error message to display. */
|
|
83
|
+
error?: string;
|
|
84
|
+
/** Text for the link to navigate to login. */
|
|
85
|
+
loginLinkText?: string;
|
|
86
|
+
/** Callback triggered when the login link is pressed. */
|
|
87
|
+
onLoginPress?: () => void;
|
|
88
|
+
/** Test ID for the root element. */
|
|
89
|
+
testID?: string;
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signUpTypes.js","sourceRoot":"","sources":["../../src/signUp/signUpTypes.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@react-native-picker/picker": "2.11.4",
|
|
28
28
|
"emoji-datasource": "^16.0.0",
|
|
29
29
|
"expo-clipboard": "~8.0.8",
|
|
30
|
+
"expo-document-picker": "~14.0.8",
|
|
30
31
|
"expo-font": "~14.0.10",
|
|
31
32
|
"expo-haptics": "~15.0.8",
|
|
32
33
|
"expo-image-manipulator": "~14.0.8",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"expo-web-browser": "~15.0.10",
|
|
42
43
|
"libphonenumber-js": "^1.12.36",
|
|
43
44
|
"linkify-it": "^5.0.0",
|
|
44
|
-
"lodash": "^4.17.
|
|
45
|
+
"lodash": "^4.17.23",
|
|
45
46
|
"luxon": "^3.7.2",
|
|
46
47
|
"react-app-polyfill": "^3.0.0",
|
|
47
48
|
"react-date-picker": "^12.0.1",
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
"react": "19.1.0",
|
|
94
95
|
"react-router": "^6.26.2",
|
|
95
96
|
"react-router-dom": "^6.30.1",
|
|
97
|
+
"react-test-renderer": "19.1.0",
|
|
96
98
|
"tsc-watch": "^7.1.1",
|
|
97
99
|
"typescript": "~5.9.2"
|
|
98
100
|
},
|
|
@@ -129,5 +131,5 @@
|
|
|
129
131
|
"test:ci": "TZ=America/New_York bun test",
|
|
130
132
|
"types": "bunx typedoc"
|
|
131
133
|
},
|
|
132
|
-
"version": "0.
|
|
134
|
+
"version": "0.3.0"
|
|
133
135
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import {Box} from "./Box";
|
|
4
|
+
import type {DataTableColumn} from "./Common";
|
|
5
|
+
import {DataTable} from "./DataTable";
|
|
6
|
+
import {DateTimeField} from "./DateTimeField";
|
|
7
|
+
import {Heading} from "./Heading";
|
|
8
|
+
import {MultiselectField} from "./MultiselectField";
|
|
9
|
+
import {Pagination} from "./Pagination";
|
|
10
|
+
import {Spinner} from "./Spinner";
|
|
11
|
+
import {Text} from "./Text";
|
|
12
|
+
|
|
13
|
+
export interface AIRequestExplorerData {
|
|
14
|
+
aiModel: string;
|
|
15
|
+
created: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
prompt: string;
|
|
18
|
+
requestType: string;
|
|
19
|
+
response?: string;
|
|
20
|
+
responseTime?: number;
|
|
21
|
+
tokensUsed?: number;
|
|
22
|
+
user?: {email?: string; name?: string};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AIRequestExplorerProps {
|
|
26
|
+
data: AIRequestExplorerData[];
|
|
27
|
+
endDate?: string;
|
|
28
|
+
isLoading?: boolean;
|
|
29
|
+
onEndDateChange?: (date: string) => void;
|
|
30
|
+
onPageChange: (page: number) => void;
|
|
31
|
+
onRequestTypeFilterChange?: (types: string[]) => void;
|
|
32
|
+
onStartDateChange?: (date: string) => void;
|
|
33
|
+
page: number;
|
|
34
|
+
requestTypeFilter?: string[];
|
|
35
|
+
startDate?: string;
|
|
36
|
+
testID?: string;
|
|
37
|
+
totalCount: number;
|
|
38
|
+
totalPages: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const REQUEST_TYPE_OPTIONS = [
|
|
42
|
+
{label: "General", value: "general"},
|
|
43
|
+
{label: "Remix", value: "remix"},
|
|
44
|
+
{label: "Summarization", value: "summarization"},
|
|
45
|
+
{label: "Translation", value: "translation"},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const COLUMNS: DataTableColumn[] = [
|
|
49
|
+
{columnType: "text", title: "Type", width: 120},
|
|
50
|
+
{columnType: "text", title: "Model", width: 150},
|
|
51
|
+
{columnType: "text", title: "User", width: 150},
|
|
52
|
+
{columnType: "text", title: "Prompt", width: 250},
|
|
53
|
+
{columnType: "text", title: "Response", width: 250},
|
|
54
|
+
{columnType: "number", title: "Tokens", width: 80},
|
|
55
|
+
{columnType: "text", title: "Time (ms)", width: 100},
|
|
56
|
+
{columnType: "text", title: "Created", width: 180},
|
|
57
|
+
{columnType: "text", title: "Error", width: 150},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const formatRow = (item: AIRequestExplorerData) => {
|
|
61
|
+
return [
|
|
62
|
+
{value: item.requestType},
|
|
63
|
+
{value: item.aiModel},
|
|
64
|
+
{value: item.user?.name ?? item.user?.email ?? "-"},
|
|
65
|
+
{value: item.prompt ?? "-"},
|
|
66
|
+
{value: item.response ?? "-"},
|
|
67
|
+
{value: item.tokensUsed?.toString() ?? "-"},
|
|
68
|
+
{value: item.responseTime != null ? `${item.responseTime}ms` : "-"},
|
|
69
|
+
{value: item.created ? new Date(item.created).toLocaleString() : "-"},
|
|
70
|
+
{value: item.error ?? ""},
|
|
71
|
+
];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const AIRequestExplorer = ({
|
|
75
|
+
data,
|
|
76
|
+
endDate,
|
|
77
|
+
isLoading = false,
|
|
78
|
+
onEndDateChange,
|
|
79
|
+
onPageChange,
|
|
80
|
+
onRequestTypeFilterChange,
|
|
81
|
+
onStartDateChange,
|
|
82
|
+
page,
|
|
83
|
+
requestTypeFilter,
|
|
84
|
+
startDate,
|
|
85
|
+
testID,
|
|
86
|
+
totalCount,
|
|
87
|
+
totalPages,
|
|
88
|
+
}: AIRequestExplorerProps): React.ReactElement => {
|
|
89
|
+
return (
|
|
90
|
+
<Box direction="column" flex="grow" gap={4} padding={4} testID={testID}>
|
|
91
|
+
<Heading size="lg">AI Request Explorer</Heading>
|
|
92
|
+
<Text color="secondaryDark" size="sm">
|
|
93
|
+
{totalCount} total requests
|
|
94
|
+
</Text>
|
|
95
|
+
|
|
96
|
+
{/* Filters */}
|
|
97
|
+
<Box direction="row" gap={3} wrap>
|
|
98
|
+
{onRequestTypeFilterChange ? (
|
|
99
|
+
<Box minWidth={200}>
|
|
100
|
+
<MultiselectField
|
|
101
|
+
onChange={onRequestTypeFilterChange}
|
|
102
|
+
options={REQUEST_TYPE_OPTIONS}
|
|
103
|
+
title="Request Type"
|
|
104
|
+
value={requestTypeFilter ?? []}
|
|
105
|
+
/>
|
|
106
|
+
</Box>
|
|
107
|
+
) : null}
|
|
108
|
+
{onStartDateChange ? (
|
|
109
|
+
<Box minWidth={200}>
|
|
110
|
+
<DateTimeField
|
|
111
|
+
onChange={onStartDateChange}
|
|
112
|
+
title="Start Date"
|
|
113
|
+
type="datetime"
|
|
114
|
+
value={startDate ?? ""}
|
|
115
|
+
/>
|
|
116
|
+
</Box>
|
|
117
|
+
) : null}
|
|
118
|
+
{onEndDateChange ? (
|
|
119
|
+
<Box minWidth={200}>
|
|
120
|
+
<DateTimeField
|
|
121
|
+
onChange={onEndDateChange}
|
|
122
|
+
title="End Date"
|
|
123
|
+
type="datetime"
|
|
124
|
+
value={endDate ?? ""}
|
|
125
|
+
/>
|
|
126
|
+
</Box>
|
|
127
|
+
) : null}
|
|
128
|
+
</Box>
|
|
129
|
+
|
|
130
|
+
{/* Table */}
|
|
131
|
+
{isLoading ? (
|
|
132
|
+
<Box alignItems="center" padding={6}>
|
|
133
|
+
<Spinner />
|
|
134
|
+
</Box>
|
|
135
|
+
) : (
|
|
136
|
+
<DataTable alternateRowBackground columns={COLUMNS} data={data.map(formatRow)} />
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* Pagination */}
|
|
140
|
+
{totalPages > 1 ? (
|
|
141
|
+
<Box alignItems="center">
|
|
142
|
+
<Pagination page={page} setPage={onPageChange} totalPages={totalPages} />
|
|
143
|
+
</Box>
|
|
144
|
+
) : null}
|
|
145
|
+
</Box>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {Image as RNImage} from "react-native";
|
|
3
|
+
|
|
4
|
+
import {Box} from "./Box";
|
|
5
|
+
import {DismissButton} from "./DismissButton";
|
|
6
|
+
import type {SelectedFile} from "./FilePickerButton";
|
|
7
|
+
import {Icon} from "./Icon";
|
|
8
|
+
import {Text} from "./Text";
|
|
9
|
+
|
|
10
|
+
export interface AttachmentPreviewProps {
|
|
11
|
+
attachments: SelectedFile[];
|
|
12
|
+
onRemove: (index: number) => void;
|
|
13
|
+
testID?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const isImageMimeType = (mimeType: string): boolean => {
|
|
17
|
+
return mimeType.startsWith("image/");
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const AttachmentPreview = ({
|
|
21
|
+
attachments,
|
|
22
|
+
onRemove,
|
|
23
|
+
testID,
|
|
24
|
+
}: AttachmentPreviewProps): React.ReactElement | null => {
|
|
25
|
+
if (attachments.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Box direction="row" gap={2} padding={2} testID={testID ?? "attachment-preview"} wrap>
|
|
31
|
+
{attachments.map((attachment, index) => (
|
|
32
|
+
<Box
|
|
33
|
+
alignItems="center"
|
|
34
|
+
border="default"
|
|
35
|
+
direction="row"
|
|
36
|
+
gap={1}
|
|
37
|
+
key={`attachment-${index}`}
|
|
38
|
+
padding={1}
|
|
39
|
+
rounding="md"
|
|
40
|
+
>
|
|
41
|
+
{isImageMimeType(attachment.mimeType) ? (
|
|
42
|
+
<RNImage
|
|
43
|
+
source={{uri: attachment.uri}}
|
|
44
|
+
style={{borderRadius: 4, height: 40, width: 40}}
|
|
45
|
+
/>
|
|
46
|
+
) : (
|
|
47
|
+
<Box alignItems="center" justifyContent="center" padding={1}>
|
|
48
|
+
<Icon iconName="file" size="sm" />
|
|
49
|
+
</Box>
|
|
50
|
+
)}
|
|
51
|
+
<Text size="sm" truncate>
|
|
52
|
+
{attachment.name}
|
|
53
|
+
</Text>
|
|
54
|
+
<DismissButton
|
|
55
|
+
accessibilityHint="Removes this attachment"
|
|
56
|
+
accessibilityLabel={`Remove ${attachment.name}`}
|
|
57
|
+
onClick={() => onRemove(index)}
|
|
58
|
+
/>
|
|
59
|
+
</Box>
|
|
60
|
+
))}
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
};
|
package/src/Common.ts
CHANGED
|
@@ -1540,6 +1540,58 @@ export interface ButtonProps {
|
|
|
1540
1540
|
onClick: () => void | Promise<void>;
|
|
1541
1541
|
}
|
|
1542
1542
|
|
|
1543
|
+
/**
|
|
1544
|
+
* Props for the SocialLoginButton component.
|
|
1545
|
+
* Used for OAuth social login buttons (Google, GitHub, Apple).
|
|
1546
|
+
*/
|
|
1547
|
+
export interface SocialLoginButtonProps {
|
|
1548
|
+
/**
|
|
1549
|
+
* The OAuth provider for the social login.
|
|
1550
|
+
*/
|
|
1551
|
+
provider: "google" | "github" | "apple";
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* The function to call when the button is pressed.
|
|
1555
|
+
* Should initiate the OAuth flow.
|
|
1556
|
+
*/
|
|
1557
|
+
onPress: () => Promise<void>;
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* If true, a loading spinner will be shown in the button.
|
|
1561
|
+
*/
|
|
1562
|
+
loading?: boolean;
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* The visual variant of the button.
|
|
1566
|
+
* - "primary": Uses the provider's brand colors
|
|
1567
|
+
* - "outline": Uses an outline style with neutral colors
|
|
1568
|
+
* @default "primary"
|
|
1569
|
+
*/
|
|
1570
|
+
variant?: "primary" | "outline";
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* If true, the button will be disabled.
|
|
1574
|
+
* @default false
|
|
1575
|
+
*/
|
|
1576
|
+
disabled?: boolean;
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* If true, the button will take the full width of its container.
|
|
1580
|
+
* @default true
|
|
1581
|
+
*/
|
|
1582
|
+
fullWidth?: boolean;
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* Custom text for the button. Defaults to "Continue with {Provider}".
|
|
1586
|
+
*/
|
|
1587
|
+
text?: string;
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Test ID for testing purposes.
|
|
1591
|
+
*/
|
|
1592
|
+
testID?: string;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1543
1595
|
export interface CustomSelectFieldProps {
|
|
1544
1596
|
/**
|
|
1545
1597
|
* The current value of the custom select field.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, {useCallback, useState} from "react";
|
|
2
|
+
|
|
3
|
+
import {Box} from "./Box";
|
|
4
|
+
import {Button} from "./Button";
|
|
5
|
+
import {IconButton} from "./IconButton";
|
|
6
|
+
import {Modal} from "./Modal";
|
|
7
|
+
|
|
8
|
+
export interface SelectedFile {
|
|
9
|
+
mimeType: string;
|
|
10
|
+
name: string;
|
|
11
|
+
uri: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FilePickerButtonProps {
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
multiple?: boolean;
|
|
17
|
+
onFilesSelected: (files: SelectedFile[]) => void;
|
|
18
|
+
testID?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const FilePickerButton = ({
|
|
22
|
+
disabled = false,
|
|
23
|
+
multiple = false,
|
|
24
|
+
onFilesSelected,
|
|
25
|
+
testID,
|
|
26
|
+
}: FilePickerButtonProps): React.ReactElement => {
|
|
27
|
+
const [showModal, setShowModal] = useState(false);
|
|
28
|
+
|
|
29
|
+
const handlePickImage = useCallback(async () => {
|
|
30
|
+
setShowModal(false);
|
|
31
|
+
const ImagePicker = await import("expo-image-picker");
|
|
32
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
33
|
+
allowsMultipleSelection: multiple,
|
|
34
|
+
mediaTypes: ["images"],
|
|
35
|
+
quality: 0.8,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!result.canceled && result.assets.length > 0) {
|
|
39
|
+
const files: SelectedFile[] = result.assets.map((asset) => ({
|
|
40
|
+
mimeType: asset.mimeType ?? "image/jpeg",
|
|
41
|
+
name: asset.fileName ?? `image-${Date.now()}.jpg`,
|
|
42
|
+
uri: asset.uri,
|
|
43
|
+
}));
|
|
44
|
+
onFilesSelected(files);
|
|
45
|
+
}
|
|
46
|
+
}, [multiple, onFilesSelected]);
|
|
47
|
+
|
|
48
|
+
const handlePickDocument = useCallback(async () => {
|
|
49
|
+
setShowModal(false);
|
|
50
|
+
const DocumentPicker = await import("expo-document-picker");
|
|
51
|
+
const result = await DocumentPicker.getDocumentAsync({
|
|
52
|
+
multiple,
|
|
53
|
+
type: ["application/pdf", "text/plain", "text/csv", "application/json"],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!result.canceled && result.assets.length > 0) {
|
|
57
|
+
const files: SelectedFile[] = result.assets.map((asset) => ({
|
|
58
|
+
mimeType: asset.mimeType ?? "application/octet-stream",
|
|
59
|
+
name: asset.name,
|
|
60
|
+
uri: asset.uri,
|
|
61
|
+
}));
|
|
62
|
+
onFilesSelected(files);
|
|
63
|
+
}
|
|
64
|
+
}, [multiple, onFilesSelected]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<IconButton
|
|
69
|
+
accessibilityLabel="Attach file"
|
|
70
|
+
disabled={disabled}
|
|
71
|
+
iconName="paperclip"
|
|
72
|
+
onClick={() => setShowModal(true)}
|
|
73
|
+
testID={testID ?? "file-picker-button"}
|
|
74
|
+
/>
|
|
75
|
+
<Modal onDismiss={() => setShowModal(false)} size="sm" title="Attach" visible={showModal}>
|
|
76
|
+
<Box gap={2} padding={3}>
|
|
77
|
+
<Button
|
|
78
|
+
iconName="image"
|
|
79
|
+
onClick={handlePickImage}
|
|
80
|
+
text="Photo Library"
|
|
81
|
+
variant="outline"
|
|
82
|
+
/>
|
|
83
|
+
<Button iconName="file" onClick={handlePickDocument} text="Document" variant="outline" />
|
|
84
|
+
</Box>
|
|
85
|
+
</Modal>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
};
|