@vvedantb/native-ui 0.6.0 → 0.7.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/README.md +84 -9
- package/package.json +31 -2
- package/src/ExpoWebRoot.tsx +81 -0
- package/src/ai/code-block.tsx +1 -1
- package/src/auth/AuthProviders.tsx +55 -0
- package/src/auth/AuthScreen.tsx +97 -0
- package/src/auth/GoogleSignInButton.tsx +116 -0
- package/src/auth/SignInScreen.tsx +302 -0
- package/src/auth/authErrors.ts +19 -0
- package/src/auth/index.ts +22 -0
- package/src/brand/AppIcons.tsx +4 -4
- package/src/brand/AppMark.tsx +4 -4
- package/src/brand/GoogleMark.tsx +2 -2
- package/src/entries/auth.ts +5 -0
- package/src/entries/expo-web-root.ts +4 -0
- package/src/floating.ts +16 -2
- package/src/primitives/switch.tsx +2 -2
- package/src/tailwind-preset.cjs +248 -0
- package/src/tailwind-preset.d.cts +50 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import { Text, View } from "react-native";
|
|
3
|
+
import { useSignIn, useSignUp } from "@clerk/clerk-expo";
|
|
4
|
+
import { useRouter } from "expo-router";
|
|
5
|
+
import { Button } from "../Button";
|
|
6
|
+
import { textStyle } from "../controlStyles";
|
|
7
|
+
import { Input } from "../Input";
|
|
8
|
+
import { toast } from "../toastStore";
|
|
9
|
+
import { useScheme } from "../useScheme";
|
|
10
|
+
import { AuthScreen, FieldError, OrDivider } from "./AuthScreen";
|
|
11
|
+
import { authErrorMessage } from "./authErrors";
|
|
12
|
+
import { GoogleSignInButton, type AuthNavigationOptions } from "./GoogleSignInButton";
|
|
13
|
+
|
|
14
|
+
export type SignInMode = "sign-in" | "sign-up";
|
|
15
|
+
|
|
16
|
+
/** The messages the screen can show. Generic: none names a provider. */
|
|
17
|
+
export const AUTH_MESSAGES = {
|
|
18
|
+
missingFields: "Enter your email and password",
|
|
19
|
+
signInFailed: "Sign in failed",
|
|
20
|
+
invalidCredentials: "Invalid email or password",
|
|
21
|
+
signUpFailed: "Could not create account",
|
|
22
|
+
verifyFailed: "Invalid code",
|
|
23
|
+
missingCode: "Enter the code from the email",
|
|
24
|
+
needsAnotherStep: "This account needs another step to sign in. Finish it on the web.",
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
export type SignInScreenProps = AuthNavigationOptions & {
|
|
28
|
+
/** Default `"sign-in"`. `"sign-up"` creates the account and checks an emailed code. */
|
|
29
|
+
mode?: SignInMode;
|
|
30
|
+
/** Default "Welcome back", or "Create account" in `sign-up`. */
|
|
31
|
+
title?: string;
|
|
32
|
+
/** Default "Sign in to continue", or "Sign up to get started" in `sign-up`. */
|
|
33
|
+
subtitle?: string;
|
|
34
|
+
/** The app URL scheme, such as `"vibot"`, for the Google redirect `scheme://sso-callback`. */
|
|
35
|
+
scheme: string;
|
|
36
|
+
/** Default `"sso-callback"`. */
|
|
37
|
+
redirectPath?: string;
|
|
38
|
+
/** Default `true`. */
|
|
39
|
+
showGoogle?: boolean;
|
|
40
|
+
/** Default "Continue with Google". */
|
|
41
|
+
googleLabel?: string;
|
|
42
|
+
/** In `sign-in`: the "Sign up" link in the footer. No link without it. */
|
|
43
|
+
signUpHref?: string;
|
|
44
|
+
/** In `sign-up`: the "Sign in" link in the footer. No link without it. */
|
|
45
|
+
signInHref?: string;
|
|
46
|
+
/** Sign-up rejects a shorter password before sending it. Default: no check. */
|
|
47
|
+
minPasswordLength?: number;
|
|
48
|
+
/** Default `"toast"`. `"inline"` shows errors under the fields. */
|
|
49
|
+
errorDisplay?: "toast" | "inline";
|
|
50
|
+
/** Prefix for every `testID`: `sign-in-email`, `sign-in-password`, `sign-in-submit` and the rest. Default the mode. */
|
|
51
|
+
testIDPrefix?: string;
|
|
52
|
+
/** Above the title. App brand chrome goes here. */
|
|
53
|
+
header?: ReactNode;
|
|
54
|
+
/** Below the footer link row, such as an app credit. */
|
|
55
|
+
footer?: ReactNode;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Email and password, and Google, sign-in or sign-up. Sign-up sends an email
|
|
60
|
+
* code and checks it before the session starts. On success it calls
|
|
61
|
+
* `onSuccess`, or `router.replace(afterAuthHref, replaceOptions)`.
|
|
62
|
+
*
|
|
63
|
+
* Forgot-password is not here: an app that needs it keeps its own screen.
|
|
64
|
+
*/
|
|
65
|
+
export function SignInScreen({
|
|
66
|
+
mode = "sign-in",
|
|
67
|
+
title,
|
|
68
|
+
subtitle,
|
|
69
|
+
scheme,
|
|
70
|
+
redirectPath,
|
|
71
|
+
afterAuthHref = "/",
|
|
72
|
+
replaceOptions,
|
|
73
|
+
onSuccess,
|
|
74
|
+
showGoogle = true,
|
|
75
|
+
googleLabel,
|
|
76
|
+
signUpHref,
|
|
77
|
+
signInHref,
|
|
78
|
+
minPasswordLength,
|
|
79
|
+
errorDisplay = "toast",
|
|
80
|
+
testIDPrefix,
|
|
81
|
+
header,
|
|
82
|
+
footer,
|
|
83
|
+
}: SignInScreenProps) {
|
|
84
|
+
const signInState = useSignIn();
|
|
85
|
+
const signUpState = useSignUp();
|
|
86
|
+
const router = useRouter();
|
|
87
|
+
const colorScheme = useScheme();
|
|
88
|
+
const [email, setEmail] = useState("");
|
|
89
|
+
const [password, setPassword] = useState("");
|
|
90
|
+
const [code, setCode] = useState("");
|
|
91
|
+
const [pendingVerification, setPendingVerification] = useState(false);
|
|
92
|
+
const [error, setError] = useState("");
|
|
93
|
+
const [submitting, setSubmitting] = useState(false);
|
|
94
|
+
const signUp = mode === "sign-up";
|
|
95
|
+
const id = (name: string) => `${testIDPrefix ?? mode}-${name}`;
|
|
96
|
+
|
|
97
|
+
const fail = (heading: string, message: string) => {
|
|
98
|
+
if (errorDisplay === "inline") setError(message);
|
|
99
|
+
else toast.error(heading, message === heading ? undefined : { description: message });
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const done = () => {
|
|
103
|
+
if (onSuccess) onSuccess();
|
|
104
|
+
else router.replace(afterAuthHref, replaceOptions);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const onSignIn = async () => {
|
|
108
|
+
const { isLoaded, signIn, setActive } = signInState;
|
|
109
|
+
if (!isLoaded || !signIn || !setActive || submitting) return;
|
|
110
|
+
if (!email || !password) return fail(AUTH_MESSAGES.signInFailed, AUTH_MESSAGES.missingFields);
|
|
111
|
+
setSubmitting(true);
|
|
112
|
+
setError("");
|
|
113
|
+
try {
|
|
114
|
+
const attempt = await signIn.create({ identifier: email, password });
|
|
115
|
+
if (attempt.status === "complete" && attempt.createdSessionId) {
|
|
116
|
+
await setActive({ session: attempt.createdSessionId });
|
|
117
|
+
done();
|
|
118
|
+
} else {
|
|
119
|
+
fail(AUTH_MESSAGES.signInFailed, AUTH_MESSAGES.needsAnotherStep);
|
|
120
|
+
}
|
|
121
|
+
} catch (caught) {
|
|
122
|
+
fail(AUTH_MESSAGES.signInFailed, authErrorMessage(caught, AUTH_MESSAGES.invalidCredentials));
|
|
123
|
+
} finally {
|
|
124
|
+
setSubmitting(false);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const onSignUp = async () => {
|
|
129
|
+
const { isLoaded, signUp: attempt } = signUpState;
|
|
130
|
+
if (!isLoaded || !attempt || submitting) return;
|
|
131
|
+
if (!email || !password) return fail(AUTH_MESSAGES.signUpFailed, AUTH_MESSAGES.missingFields);
|
|
132
|
+
if (minPasswordLength && password.length < minPasswordLength) {
|
|
133
|
+
return fail(AUTH_MESSAGES.signUpFailed, `Use at least ${minPasswordLength} characters for the password`);
|
|
134
|
+
}
|
|
135
|
+
setSubmitting(true);
|
|
136
|
+
setError("");
|
|
137
|
+
try {
|
|
138
|
+
await attempt.create({ emailAddress: email, password });
|
|
139
|
+
await attempt.prepareEmailAddressVerification({ strategy: "email_code" });
|
|
140
|
+
setPendingVerification(true);
|
|
141
|
+
} catch (caught) {
|
|
142
|
+
fail(AUTH_MESSAGES.signUpFailed, authErrorMessage(caught, AUTH_MESSAGES.signUpFailed));
|
|
143
|
+
} finally {
|
|
144
|
+
setSubmitting(false);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const onVerify = async () => {
|
|
149
|
+
const { isLoaded, signUp: attempt, setActive } = signUpState;
|
|
150
|
+
if (!isLoaded || !attempt || !setActive || submitting) return;
|
|
151
|
+
if (!code) return fail(AUTH_MESSAGES.verifyFailed, AUTH_MESSAGES.missingCode);
|
|
152
|
+
setSubmitting(true);
|
|
153
|
+
setError("");
|
|
154
|
+
try {
|
|
155
|
+
const result = await attempt.attemptEmailAddressVerification({ code });
|
|
156
|
+
if (result.status === "complete" && result.createdSessionId) {
|
|
157
|
+
await setActive({ session: result.createdSessionId });
|
|
158
|
+
done();
|
|
159
|
+
} else {
|
|
160
|
+
fail(AUTH_MESSAGES.verifyFailed, AUTH_MESSAGES.needsAnotherStep);
|
|
161
|
+
}
|
|
162
|
+
} catch (caught) {
|
|
163
|
+
fail(AUTH_MESSAGES.verifyFailed, authErrorMessage(caught, AUTH_MESSAGES.verifyFailed));
|
|
164
|
+
} finally {
|
|
165
|
+
setSubmitting(false);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const linkHref = signUp ? signInHref : signUpHref;
|
|
170
|
+
const footerRow = (
|
|
171
|
+
<>
|
|
172
|
+
{linkHref && !pendingVerification ? (
|
|
173
|
+
<>
|
|
174
|
+
<Text className="font-instrument-sans text-sm" style={textStyle("muted", colorScheme)}>
|
|
175
|
+
{signUp ? "Already have an account?" : "Don't have an account?"}
|
|
176
|
+
</Text>
|
|
177
|
+
<Button
|
|
178
|
+
testID={id(signUp ? "sign-in-link" : "sign-up-link")}
|
|
179
|
+
variant="link"
|
|
180
|
+
size="sm"
|
|
181
|
+
fullWidth={false}
|
|
182
|
+
onPress={() => router.push(linkHref)}
|
|
183
|
+
>
|
|
184
|
+
{signUp ? "Sign in" : "Sign up"}
|
|
185
|
+
</Button>
|
|
186
|
+
</>
|
|
187
|
+
) : null}
|
|
188
|
+
{footer}
|
|
189
|
+
</>
|
|
190
|
+
);
|
|
191
|
+
const hasFooter = Boolean((linkHref && !pendingVerification) || footer);
|
|
192
|
+
|
|
193
|
+
if (pendingVerification) {
|
|
194
|
+
return (
|
|
195
|
+
<AuthScreen
|
|
196
|
+
testID={id("screen")}
|
|
197
|
+
title="Check your email"
|
|
198
|
+
subtitle="Enter the code we sent to your email."
|
|
199
|
+
header={header}
|
|
200
|
+
footer={hasFooter ? footerRow : undefined}
|
|
201
|
+
>
|
|
202
|
+
<Input
|
|
203
|
+
testID={id("code")}
|
|
204
|
+
accessibilityLabel="Verification code"
|
|
205
|
+
value={code}
|
|
206
|
+
placeholder="Verification code"
|
|
207
|
+
keyboardType="number-pad"
|
|
208
|
+
autoComplete="one-time-code"
|
|
209
|
+
textContentType="oneTimeCode"
|
|
210
|
+
autoFocus
|
|
211
|
+
returnKeyType="go"
|
|
212
|
+
onChangeText={setCode}
|
|
213
|
+
onSubmitEditing={() => void onVerify()}
|
|
214
|
+
/>
|
|
215
|
+
<FieldError testID={id("error")} message={error} />
|
|
216
|
+
<Button testID={id("verify")} loading={submitting} onPress={() => void onVerify()}>
|
|
217
|
+
Verify
|
|
218
|
+
</Button>
|
|
219
|
+
<Button
|
|
220
|
+
testID={id("back")}
|
|
221
|
+
variant="ghost"
|
|
222
|
+
onPress={() => {
|
|
223
|
+
setPendingVerification(false);
|
|
224
|
+
setCode("");
|
|
225
|
+
setError("");
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
Use a different email
|
|
229
|
+
</Button>
|
|
230
|
+
</AuthScreen>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<AuthScreen
|
|
236
|
+
testID={id("screen")}
|
|
237
|
+
title={title ?? (signUp ? "Create account" : "Welcome back")}
|
|
238
|
+
subtitle={subtitle ?? (signUp ? "Sign up to get started" : "Sign in to continue")}
|
|
239
|
+
header={header}
|
|
240
|
+
footer={hasFooter ? footerRow : undefined}
|
|
241
|
+
>
|
|
242
|
+
{showGoogle ? (
|
|
243
|
+
<>
|
|
244
|
+
<GoogleSignInButton
|
|
245
|
+
testID={id("google")}
|
|
246
|
+
scheme={scheme}
|
|
247
|
+
redirectPath={redirectPath}
|
|
248
|
+
afterAuthHref={afterAuthHref}
|
|
249
|
+
replaceOptions={replaceOptions}
|
|
250
|
+
onSuccess={onSuccess}
|
|
251
|
+
onError={(message) => fail(message, message)}
|
|
252
|
+
label={googleLabel}
|
|
253
|
+
disabled={submitting}
|
|
254
|
+
/>
|
|
255
|
+
<OrDivider />
|
|
256
|
+
</>
|
|
257
|
+
) : null}
|
|
258
|
+
<Input
|
|
259
|
+
testID={id("email")}
|
|
260
|
+
accessibilityLabel="Email"
|
|
261
|
+
autoCapitalize="none"
|
|
262
|
+
autoComplete="email"
|
|
263
|
+
textContentType="emailAddress"
|
|
264
|
+
keyboardType="email-address"
|
|
265
|
+
returnKeyType="next"
|
|
266
|
+
value={email}
|
|
267
|
+
placeholder="Email"
|
|
268
|
+
onChangeText={setEmail}
|
|
269
|
+
/>
|
|
270
|
+
<Input
|
|
271
|
+
testID={id("password")}
|
|
272
|
+
accessibilityLabel="Password"
|
|
273
|
+
autoCapitalize="none"
|
|
274
|
+
autoComplete={signUp ? "new-password" : "password"}
|
|
275
|
+
textContentType={signUp ? "newPassword" : "password"}
|
|
276
|
+
secureTextEntry
|
|
277
|
+
returnKeyType="go"
|
|
278
|
+
value={password}
|
|
279
|
+
placeholder="Password"
|
|
280
|
+
onChangeText={setPassword}
|
|
281
|
+
onSubmitEditing={() => void (signUp ? onSignUp() : onSignIn())}
|
|
282
|
+
/>
|
|
283
|
+
<FieldError testID={id("error")} message={error} />
|
|
284
|
+
<Button
|
|
285
|
+
testID={id("submit")}
|
|
286
|
+
loading={submitting}
|
|
287
|
+
onPress={() => void (signUp ? onSignUp() : onSignIn())}
|
|
288
|
+
>
|
|
289
|
+
{signUp ? "Create account" : "Sign in"}
|
|
290
|
+
</Button>
|
|
291
|
+
{/* Bot protection mounts its widget here on web sign-up. */}
|
|
292
|
+
{signUp ? <View nativeID="clerk-captcha" /> : null}
|
|
293
|
+
</AuthScreen>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export type SignUpScreenProps = Omit<SignInScreenProps, "mode">;
|
|
298
|
+
|
|
299
|
+
/** `SignInScreen` with `mode="sign-up"`. */
|
|
300
|
+
export function SignUpScreen(props: SignUpScreenProps) {
|
|
301
|
+
return <SignInScreen {...props} mode="sign-up" />;
|
|
302
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isClerkAPIResponseError } from "@clerk/clerk-expo";
|
|
2
|
+
|
|
3
|
+
/** Words that must not reach the user: the auth provider and stack names. */
|
|
4
|
+
const PRODUCT_NAMES = /\b(clerk|convex)\b/i;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A message to show for an auth error. An API error gives its first
|
|
8
|
+
* `longMessage` or `message` (such as "Password is incorrect."); anything
|
|
9
|
+
* else, or a message that names the provider, gives `fallback`. Never shows
|
|
10
|
+
* a raw exception message.
|
|
11
|
+
*/
|
|
12
|
+
export function authErrorMessage(error: unknown, fallback: string): string {
|
|
13
|
+
if (isClerkAPIResponseError(error)) {
|
|
14
|
+
const first = error.errors[0];
|
|
15
|
+
const message = first?.longMessage ?? first?.message;
|
|
16
|
+
if (message && !PRODUCT_NAMES.test(message)) return message;
|
|
17
|
+
}
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Clerk + Convex providers and the shared sign-in and sign-up screens. On
|
|
2
|
+
// `@vvedantb/native-ui/auth` only: they import `@clerk/clerk-expo`, `convex`,
|
|
3
|
+
// `expo-auth-session`, `expo-web-browser` and `expo-router`, and Metro does not
|
|
4
|
+
// tree-shake.
|
|
5
|
+
export { AuthProviders } from "./AuthProviders";
|
|
6
|
+
export type { AuthProvidersProps } from "./AuthProviders";
|
|
7
|
+
export { AuthScreen, OrDivider, FieldError } from "./AuthScreen";
|
|
8
|
+
export type { AuthScreenProps } from "./AuthScreen";
|
|
9
|
+
export { authErrorMessage } from "./authErrors";
|
|
10
|
+
export {
|
|
11
|
+
GoogleSignInButton,
|
|
12
|
+
useGoogleSignIn,
|
|
13
|
+
useWarmUpBrowser,
|
|
14
|
+
GOOGLE_ERROR_MESSAGE,
|
|
15
|
+
} from "./GoogleSignInButton";
|
|
16
|
+
export type {
|
|
17
|
+
AuthNavigationOptions,
|
|
18
|
+
GoogleSignInOptions,
|
|
19
|
+
GoogleSignInButtonProps,
|
|
20
|
+
} from "./GoogleSignInButton";
|
|
21
|
+
export { SignInScreen, SignUpScreen, AUTH_MESSAGES } from "./SignInScreen";
|
|
22
|
+
export type { SignInMode, SignInScreenProps, SignUpScreenProps } from "./SignInScreen";
|
package/src/brand/AppIcons.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Platform, type
|
|
2
|
-
import Svg, { G, Path } from "react-native-svg";
|
|
1
|
+
import { Platform, type ViewProps } from "react-native";
|
|
2
|
+
import Svg, { G, Path, type GProps } from "react-native-svg";
|
|
3
3
|
import { svgA11yProps } from "../svgA11y";
|
|
4
4
|
import { tokens } from "../tokens";
|
|
5
5
|
import { useScheme } from "../useScheme";
|
|
@@ -26,7 +26,7 @@ export type AppIconProps = {
|
|
|
26
26
|
align?: "center" | "start";
|
|
27
27
|
/** Set as `data-testid` on web, where `Svg` does not map `testID`. */
|
|
28
28
|
testID?: string;
|
|
29
|
-
style?:
|
|
29
|
+
style?: ViewProps["style"];
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
export type AppIconComponent = ((props: AppIconProps) => React.JSX.Element) & {
|
|
@@ -54,7 +54,7 @@ function createAppIcon(app: AppName, displayName: string): AppIconComponent {
|
|
|
54
54
|
stroke === undefined
|
|
55
55
|
? markStrokeWidth(app)
|
|
56
56
|
: stroke * (viewBoxSide(app) / size);
|
|
57
|
-
const glyphProps =
|
|
57
|
+
const glyphProps: GProps =
|
|
58
58
|
kind === "fill"
|
|
59
59
|
? { fill }
|
|
60
60
|
: {
|
package/src/brand/AppMark.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Platform, type
|
|
2
|
-
import Svg, { G, Path, Rect } from "react-native-svg";
|
|
1
|
+
import { Platform, type ViewProps } from "react-native";
|
|
2
|
+
import Svg, { G, Path, Rect, type GProps } from "react-native-svg";
|
|
3
3
|
import { svgA11yProps } from "../svgA11y";
|
|
4
4
|
import { APP_ICONS } from "./AppIcons";
|
|
5
5
|
import {
|
|
@@ -23,7 +23,7 @@ export type AppMarkProps = {
|
|
|
23
23
|
title?: string;
|
|
24
24
|
/** Set as `data-testid` on web, where `Svg` does not map `testID`. */
|
|
25
25
|
testID?: string;
|
|
26
|
-
style?:
|
|
26
|
+
style?: ViewProps["style"];
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const TILE_TONES = {
|
|
@@ -50,7 +50,7 @@ export function AppMark({
|
|
|
50
50
|
}
|
|
51
51
|
const { kind, paths } = APP_MARKS[app];
|
|
52
52
|
const tone = TILE_TONES[variant];
|
|
53
|
-
const glyphProps =
|
|
53
|
+
const glyphProps: GProps =
|
|
54
54
|
kind === "fill"
|
|
55
55
|
? { fill: tone.fg }
|
|
56
56
|
: {
|
package/src/brand/GoogleMark.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Platform, type
|
|
1
|
+
import { Platform, type ViewProps } from "react-native";
|
|
2
2
|
import Svg, { Path } from "react-native-svg";
|
|
3
3
|
import { svgA11yProps } from "../svgA11y";
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ export type GoogleMarkProps = {
|
|
|
9
9
|
title?: string;
|
|
10
10
|
/** Set as `data-testid` on web, where `Svg` does not map `testID`. */
|
|
11
11
|
testID?: string;
|
|
12
|
-
style?:
|
|
12
|
+
style?: ViewProps["style"];
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
/** The four paths of the Google "G" on the 24 grid, each in its brand colour. */
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// `@vvedantb/native-ui/auth`: `AuthProviders`, `SignInScreen`, `SignUpScreen`,
|
|
2
|
+
// `GoogleSignInButton` and the auth frame. Not on the root entry: it imports
|
|
3
|
+
// `@clerk/clerk-expo`, `convex`, `expo-auth-session`, `expo-web-browser` and
|
|
4
|
+
// `expo-router`, and Metro does not tree-shake.
|
|
5
|
+
export * from "../auth";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// `@vvedantb/native-ui/expo-web-root`: `ExpoWebRoot` for `app/+html.tsx`. Not on
|
|
2
|
+
// the root entry: it imports `expo-router/html`, and Metro does not tree-shake.
|
|
3
|
+
export { ExpoWebRoot, EXPO_WEB_VIEWPORT, expoWebBackgroundCss } from "../ExpoWebRoot";
|
|
4
|
+
export type { ExpoWebRootProps, ExpoWebRootViewport } from "../ExpoWebRoot";
|
package/src/floating.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Platform
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
2
|
import { tokens } from "./tokens";
|
|
3
3
|
|
|
4
4
|
export const FLOATING_INSET = tokens.spacing.unit;
|
|
@@ -14,7 +14,21 @@ export const FLOATING_PILL_RADIUS = tokens.radius.pill;
|
|
|
14
14
|
|
|
15
15
|
const FLOATING_BOX_SHADOW = "0px 8px 24px rgba(10, 10, 10, 0.12)";
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Only the shadow keys, not `ViewStyle`: an Expo-widened `ViewStyle`
|
|
19
|
+
* (`position: "fixed"`, `backgroundImage`) is not assignable to a React
|
|
20
|
+
* Native 0.88 View `style`. A plain inferred type would be `{}` and lose
|
|
21
|
+
* `floatingShadow.shadowColor` and the rest for callers.
|
|
22
|
+
*/
|
|
23
|
+
type FloatingShadowStyle = {
|
|
24
|
+
shadowColor?: string;
|
|
25
|
+
shadowOffset?: { width: number; height: number };
|
|
26
|
+
shadowOpacity?: number;
|
|
27
|
+
shadowRadius?: number;
|
|
28
|
+
boxShadow?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const floatingShadow: FloatingShadowStyle =
|
|
18
32
|
Platform.select({
|
|
19
33
|
ios: {
|
|
20
34
|
shadowColor: "#0a0a0a",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as SwitchPrimitives from "@rn-primitives/switch";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { Platform, type ViewStyle } from "react-native";
|
|
3
|
+
import { Platform, type ViewProps, type ViewStyle } from "react-native";
|
|
4
4
|
import { cn } from "../cn";
|
|
5
5
|
|
|
6
6
|
type SwitchProps = React.ComponentPropsWithoutRef<
|
|
@@ -13,7 +13,7 @@ type SwitchProps = React.ComponentPropsWithoutRef<
|
|
|
13
13
|
* which spreads `style`, so these are the only styles applied there.
|
|
14
14
|
*/
|
|
15
15
|
trackStyle?: ViewStyle;
|
|
16
|
-
thumbStyle?:
|
|
16
|
+
thumbStyle?: ViewProps["style"];
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
const Switch = React.forwardRef<
|