Compare commits
192 Commits
create-jaz
...
create-jaz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a34c0675cd | ||
|
|
325a554bd1 | ||
|
|
7422943e83 | ||
|
|
23bfea5861 | ||
|
|
605a54eb11 | ||
|
|
a7aaee51e6 | ||
|
|
4b8983858a | ||
|
|
8a8c4d11e1 | ||
|
|
b774bb345d | ||
|
|
7fd891d7b9 | ||
|
|
27cac4a6d7 | ||
|
|
2b71ef1181 | ||
|
|
ae169c7b3a | ||
|
|
d888c99d9a | ||
|
|
0b54917f19 | ||
|
|
c87b215b75 | ||
|
|
e4ba23cbef | ||
|
|
98c005a6e0 | ||
|
|
8528db4de4 | ||
|
|
e0fe5a20b7 | ||
|
|
e16e4d53d1 | ||
|
|
d904fae506 | ||
|
|
f67c0b3db3 | ||
|
|
283d7c6bf0 | ||
|
|
0e7a7dbbc0 | ||
|
|
63c69b6b95 | ||
|
|
fd02627069 | ||
|
|
827adc991d | ||
|
|
651b69e5af | ||
|
|
277e4d49e8 | ||
|
|
cfa44f32eb | ||
|
|
ef920435e9 | ||
|
|
87d05404dd | ||
|
|
535c460f5a | ||
|
|
fa1b302474 | ||
|
|
45f73a774c | ||
|
|
2a9e271dc3 | ||
|
|
3d96d9c829 | ||
|
|
844051405d | ||
|
|
625eff2333 | ||
|
|
59e2871065 | ||
|
|
acdc88fb91 | ||
|
|
05eab4e2a9 | ||
|
|
efcd65ae38 | ||
|
|
ad60fa942a | ||
|
|
5272d3cd2a | ||
|
|
d837811813 | ||
|
|
2b4aba2d1b | ||
|
|
50b4da18d9 | ||
|
|
d18d09e002 | ||
|
|
d983f27bbe | ||
|
|
fcf83b0da4 | ||
|
|
9231e2c22f | ||
|
|
33157ee0ad | ||
|
|
4b964edcaf | ||
|
|
df22f2617e | ||
|
|
280495c533 | ||
|
|
d5c6fbdc3c | ||
|
|
57776a1400 | ||
|
|
156c45aa0e | ||
|
|
2d0dba6bbc | ||
|
|
7241d2ad95 | ||
|
|
4a9eeace00 | ||
|
|
4f9c91f6ff | ||
|
|
a8e1726797 | ||
|
|
a6eeada331 | ||
|
|
3b38a8241c | ||
|
|
c49330c308 | ||
|
|
0c4e27c18d | ||
|
|
d8d273821e | ||
|
|
def0ca81b4 | ||
|
|
7ff13a8f55 | ||
|
|
0e7e53238b | ||
|
|
fcc18e5212 | ||
|
|
5741d7f09c | ||
|
|
766d2c8846 | ||
|
|
70a43d0c39 | ||
|
|
cf44258848 | ||
|
|
4db5ec2dd8 | ||
|
|
4983e57e62 | ||
|
|
5b2fc70ca1 | ||
|
|
53af54570c | ||
|
|
e0b4626c22 | ||
|
|
a273a0db58 | ||
|
|
277104ebba | ||
|
|
434e59d5c4 | ||
|
|
bb20907774 | ||
|
|
282425575f | ||
|
|
75501a9051 | ||
|
|
c114cf4029 | ||
|
|
03da7dc994 | ||
|
|
12d5c68f98 | ||
|
|
ec1e359621 | ||
|
|
44fb13ddd7 | ||
|
|
4fd4c5bbed | ||
|
|
a1d31566ed | ||
|
|
dcde3aa811 | ||
|
|
a621141d76 | ||
|
|
0e1fbd7dfc | ||
|
|
27efdf9b8e | ||
|
|
83068b33e9 | ||
|
|
769d9b0517 | ||
|
|
b0ffe5ed7e | ||
|
|
c67a0cd3ce | ||
|
|
48e11a243f | ||
|
|
de904698d8 | ||
|
|
7c8180dcb4 | ||
|
|
da3e101c50 | ||
|
|
7605d228f2 | ||
|
|
3063d74ab9 | ||
|
|
2a4e3fc0cd | ||
|
|
7cc6d63d40 | ||
|
|
d574bbc521 | ||
|
|
6388a7272b | ||
|
|
cd358888d8 | ||
|
|
f427f2324b | ||
|
|
d0e2041b10 | ||
|
|
edce59d238 | ||
|
|
7f75d852c1 | ||
|
|
2683af7d28 | ||
|
|
4223720010 | ||
|
|
1b4508fea7 | ||
|
|
776fa09279 | ||
|
|
5c200aa60d | ||
|
|
9fc6c5f6c8 | ||
|
|
7e06cd4a77 | ||
|
|
c43191d97b | ||
|
|
5706b5eb81 | ||
|
|
5eed930997 | ||
|
|
35e5e50508 | ||
|
|
e88a3d0712 | ||
|
|
13b64af5b2 | ||
|
|
9804c6a729 | ||
|
|
937d415cc2 | ||
|
|
9196154207 | ||
|
|
c907b2aac2 | ||
|
|
b5a9dfa7ec | ||
|
|
113c77b416 | ||
|
|
cad5444400 | ||
|
|
b2b350e4d0 | ||
|
|
80d5d62852 | ||
|
|
75f8833c1a | ||
|
|
99d1f3f28b | ||
|
|
4066cfe011 | ||
|
|
e5f8b06af1 | ||
|
|
485b5a238d | ||
|
|
c42ee6d6e2 | ||
|
|
1fb2fd0f50 | ||
|
|
0fce4adfc5 | ||
|
|
b59086c808 | ||
|
|
fe91463652 | ||
|
|
b9065db109 | ||
|
|
d13da295ed | ||
|
|
0291389c3b | ||
|
|
f417f518cb | ||
|
|
f69f99a209 | ||
|
|
31c4fd7c07 | ||
|
|
613aadd775 | ||
|
|
0f969b4be4 | ||
|
|
f7ac1015b2 | ||
|
|
d7c5d4a03d | ||
|
|
8384c55cce | ||
|
|
be947d1086 | ||
|
|
3924bb2ede | ||
|
|
2e3003c2fc | ||
|
|
5ec15ba0a2 | ||
|
|
c7ccf5c5d7 | ||
|
|
9e8c81a1a6 | ||
|
|
c563e2547f | ||
|
|
00cb697bc7 | ||
|
|
a6a0560059 | ||
|
|
55fa5977c2 | ||
|
|
8b4bc1bc97 | ||
|
|
df403ccbc6 | ||
|
|
1f490d2aae | ||
|
|
1b766b6369 | ||
|
|
3f54da98f8 | ||
|
|
05b98c0f8b | ||
|
|
928e63620b | ||
|
|
ea9b1eb88e | ||
|
|
83a88252e2 | ||
|
|
7a61c19135 | ||
|
|
a2f87f304c | ||
|
|
f9f24d2ad2 | ||
|
|
ed3197a7fd | ||
|
|
772a88e98f | ||
|
|
e22a7f46ad | ||
|
|
f890f2f460 | ||
|
|
e9c17e12dc | ||
|
|
4b908e3024 | ||
|
|
90fd2b5da0 | ||
|
|
d7b4360f11 |
24
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
### What this Does
|
||||
Brief summary of the change, ideally framed in user or product terms.
|
||||
|
||||
### Why Are We Doing This?
|
||||
Link to the shaped pitch or explain what problem it solves.
|
||||
|
||||
### Scope / Boundaries
|
||||
Includes:
|
||||
- [x] Core feature functionality
|
||||
- [x] Tests or validation steps
|
||||
|
||||
Do NOT include:
|
||||
- [ ] Related stretch features or follow-ups
|
||||
|
||||
### Testing Instructions
|
||||
How a reviewer or QA can verify behavior, offer step-by-step instructions if possible. Screenshots or recordings are welcome.
|
||||
|
||||
### Known Issues / Open Questions (if any)
|
||||
- [ ] Note anything you’d like review on or decided async
|
||||
|
||||
### Related Links
|
||||
- GitHub issue
|
||||
- Linear pitch
|
||||
- Design links or references
|
||||
2
.github/workflows/e2e-rn-test.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
disable-animations: true
|
||||
working-directory: ./examples/chat-rn-expo/
|
||||
# killall due to this issue: https://github.com/ReactiveCircus/android-emulator-runner/issues/385
|
||||
script: ./test/e2e/run.sh && killall -INT crashpad_handler || true
|
||||
script: ./test/e2e/run.sh && ( killall -INT crashpad_handler || true )
|
||||
|
||||
- name: Copy Maestro Output
|
||||
if: steps.e2e_test.outcome != 'success'
|
||||
|
||||
@@ -63,7 +63,7 @@ You'll need Node.js 22.x installed (we're working on support for 23.x), and pnpm
|
||||
4. **Build the packages**:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm build:packages
|
||||
```
|
||||
|
||||
5. **Run tests** to verify everything is working:
|
||||
|
||||
23
examples/chat-rn-expo-clerk/.gitignore
vendored
@@ -1,23 +0,0 @@
|
||||
node_modules/
|
||||
.expo/
|
||||
dist/
|
||||
npm-debug.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
ios
|
||||
android
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
@@ -1,27 +0,0 @@
|
||||
# 🎷 Jazz + Expo + `expo-router` + Clerk Auth
|
||||
|
||||
## 🚀 How to Run
|
||||
|
||||
### 1. Inside the Workspace Root
|
||||
|
||||
First, install dependencies and build the project:
|
||||
|
||||
```bash
|
||||
pnpm i
|
||||
mv .env.example .env
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
Don't forget to update `VITE_CLERK_PUBLISHABLE_KEY` in `.env` with your [Publishable Key](https://clerk.com/docs/deployments/clerk-environment-variables#clerk-publishable-and-secret-keys) from Clerk.
|
||||
|
||||
### 2. Inside the `examples/chat-rn-expo-clerk` Directory
|
||||
|
||||
Next, navigate to the specific example project and run the following commands:
|
||||
|
||||
```bash
|
||||
pnpm expo prebuild
|
||||
pnpx pod-install
|
||||
pnpm expo run:ios
|
||||
```
|
||||
|
||||
This will set up and launch the app on iOS. For Android, you can replace the last command with `pnpm expo run:android`.
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "jazz-chat-rn-expo-clerk",
|
||||
"scheme": "jazz-chat-rn-expo-clerk",
|
||||
"slug": "jazz-chat-rn-expo-clerk",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"splash": {
|
||||
"image": "./assets/images/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.jazz.chatrnclerk"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"package": "com.jazz.chatrnclerk"
|
||||
},
|
||||
"newArchEnabled": true,
|
||||
"plugins": [
|
||||
"expo-secure-store",
|
||||
"expo-font",
|
||||
"expo-router",
|
||||
"expo-sqlite",
|
||||
[
|
||||
"expo-image-picker",
|
||||
{
|
||||
"photosPermission": "The app accesses your photos to let you share them with your friends."
|
||||
}
|
||||
]
|
||||
],
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "ca3d46e5-a10a-47ec-9d77-3b841e1c62d4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Redirect, Stack } from "expo-router";
|
||||
import { useIsAuthenticated } from "jazz-tools/expo";
|
||||
import React from "react";
|
||||
|
||||
export default function HomeLayout() {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Redirect href={"/chat"} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack screenOptions={{ headerShown: false, headerBackVisible: true }} />
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { SignedOut } from "@clerk/clerk-expo";
|
||||
import { Link } from "expo-router";
|
||||
import React from "react";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<View className="flex-1 justify-center items-center bg-gray-100 p-6">
|
||||
<SignedOut>
|
||||
<View className="bg-white p-6 rounded-lg shadow-lg w-11/12 max-w-md">
|
||||
<Text className="text-2xl font-bold text-center text-gray-900 mb-4">
|
||||
Jazz 🤝 Clerk 🤝 Expo
|
||||
</Text>
|
||||
<Link href="/sign-in" className="mb-4">
|
||||
<Text className="text-center text-blue-600 underline text-lg">
|
||||
Sign In
|
||||
</Text>
|
||||
</Link>
|
||||
<Link href="/sign-in-oauth" className="mb-4">
|
||||
<Text className="text-center text-blue-600 underline text-lg">
|
||||
Sign In OAuth
|
||||
</Text>
|
||||
</Link>
|
||||
<Link href="/sign-up">
|
||||
<Text className="text-center text-blue-600 underline text-lg">
|
||||
Sign Up
|
||||
</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</SignedOut>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Redirect, Stack } from "expo-router";
|
||||
import { useIsAuthenticated } from "jazz-tools/expo";
|
||||
|
||||
export default function UnAuthenticatedLayout() {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Redirect href={"/chat"} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: true,
|
||||
headerBackVisible: true,
|
||||
headerTitle: "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useOAuth } from "@clerk/clerk-expo";
|
||||
import * as Linking from "expo-linking";
|
||||
import { Link } from "expo-router";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
import React from "react";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
|
||||
export const useWarmUpBrowser = () => {
|
||||
React.useEffect(() => {
|
||||
// Warm up the android browser to improve UX
|
||||
// https://docs.expo.dev/guides/authentication/#improving-user-experience
|
||||
void WebBrowser.warmUpAsync();
|
||||
return () => {
|
||||
void WebBrowser.coolDownAsync();
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
WebBrowser.maybeCompleteAuthSession();
|
||||
|
||||
const SignInWithOAuth = () => {
|
||||
useWarmUpBrowser();
|
||||
|
||||
const { startOAuthFlow } = useOAuth({ strategy: "oauth_google" });
|
||||
|
||||
const onPress = React.useCallback(async () => {
|
||||
try {
|
||||
const { createdSessionId, signIn, signUp, setActive } =
|
||||
await startOAuthFlow({
|
||||
redirectUrl: Linking.createURL("/", {
|
||||
scheme: "jazz-chat-rn-expo-clerk",
|
||||
}),
|
||||
});
|
||||
|
||||
if (createdSessionId) {
|
||||
setActive!({ session: createdSessionId });
|
||||
} else {
|
||||
// Use signIn or signUp for next steps such as MFA
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("OAuth error", err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View className="flex-1 justify-center items-center bg-gray-50 p-6">
|
||||
<View className="bg-white w-11/12 max-w-md p-8 rounded-lg shadow-lg items-center">
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
className="w-full bg-red-500 py-3 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<Text className="text-white text-lg font-semibold">
|
||||
Sign in with Google
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Link href="/" className="mt-4">
|
||||
<Text className="text-blue-600 text-lg font-semibold underline mb-6">
|
||||
Back to Home
|
||||
</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default SignInWithOAuth;
|
||||
@@ -1,79 +0,0 @@
|
||||
import { useSignIn } from "@clerk/clerk-expo";
|
||||
import { Link } from "expo-router";
|
||||
import React from "react";
|
||||
import { Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
|
||||
export default function SignInPage() {
|
||||
const { signIn, setActive, isLoaded } = useSignIn();
|
||||
const [emailAddress, setEmailAddress] = React.useState("");
|
||||
const [password, setPassword] = React.useState("");
|
||||
const [errorMessage, setErrorMessage] = React.useState("");
|
||||
|
||||
const onSignInPress = React.useCallback(async () => {
|
||||
if (!isLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage("");
|
||||
|
||||
try {
|
||||
const signInAttempt = await signIn.create({
|
||||
identifier: emailAddress,
|
||||
password,
|
||||
});
|
||||
|
||||
if (signInAttempt.status === "complete") {
|
||||
await setActive({ session: signInAttempt.createdSessionId });
|
||||
} else {
|
||||
console.error(JSON.stringify(signInAttempt, null, 2));
|
||||
setErrorMessage("Invalid credentials. Please try again.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(JSON.stringify(err, null, 2));
|
||||
if (err.errors && err.errors[0]?.message) {
|
||||
setErrorMessage(err.errors[0].message);
|
||||
} else {
|
||||
setErrorMessage("An unexpected error occurred. Please try again.");
|
||||
}
|
||||
}
|
||||
}, [isLoaded, emailAddress, password]);
|
||||
|
||||
return (
|
||||
<View className="flex-1 justify-center items-center bg-gray-50 p-6">
|
||||
<View className="bg-white w-11/12 max-w-md p-8 rounded-lg shadow-md">
|
||||
<Text className="text-3xl font-bold text-center text-gray-800 mb-6">
|
||||
Sign In
|
||||
</Text>
|
||||
{errorMessage ? (
|
||||
<Text className="text-red-500 text-center mb-4">{errorMessage}</Text>
|
||||
) : null}
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
value={emailAddress}
|
||||
placeholder="Email..."
|
||||
onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
|
||||
className="w-full h-12 mb-4 px-4 bg-gray-100 border border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
<TextInput
|
||||
value={password}
|
||||
placeholder="Password..."
|
||||
secureTextEntry={true}
|
||||
onChangeText={(password) => setPassword(password)}
|
||||
className="w-full h-12 mb-6 px-4 bg-gray-100 border border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={onSignInPress}
|
||||
className="w-full h-12 bg-blue-600 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<Text className="text-white text-lg font-semibold">Sign In</Text>
|
||||
</TouchableOpacity>
|
||||
<View className="flex-row items-center justify-center mt-4">
|
||||
<Text className="text-gray-600">Don't have an account?</Text>
|
||||
<Link href="/sign-up">
|
||||
<Text className="text-blue-500 ml-2 font-semibold">Sign up</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import { useSignUp } from "@clerk/clerk-expo";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import * as React from "react";
|
||||
import { Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
|
||||
export default function SignUpPage() {
|
||||
const { isLoaded, signUp, setActive } = useSignUp();
|
||||
|
||||
const [emailAddress, setEmailAddress] = React.useState("");
|
||||
const [password, setPassword] = React.useState("");
|
||||
const [pendingVerification, setPendingVerification] = React.useState(false);
|
||||
const [code, setCode] = React.useState("");
|
||||
const [errorMessage, setErrorMessage] = React.useState("");
|
||||
const navigation = useNavigation();
|
||||
|
||||
const onSignUpPress = async () => {
|
||||
if (!isLoaded) return;
|
||||
|
||||
setErrorMessage("");
|
||||
|
||||
try {
|
||||
await signUp.create({
|
||||
emailAddress,
|
||||
password,
|
||||
});
|
||||
|
||||
await signUp.prepareEmailAddressVerification({
|
||||
strategy: "email_code",
|
||||
});
|
||||
|
||||
setPendingVerification(true);
|
||||
} catch (err: any) {
|
||||
console.error(JSON.stringify(err, null, 2));
|
||||
if (err.errors && err.errors[0]?.message) {
|
||||
setErrorMessage(err.errors[0].message);
|
||||
} else {
|
||||
setErrorMessage("An unexpected error occurred. Please try again.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onPressVerify = async () => {
|
||||
if (!isLoaded) return;
|
||||
|
||||
setErrorMessage("");
|
||||
|
||||
try {
|
||||
const completeSignUp = await signUp.attemptEmailAddressVerification({
|
||||
code,
|
||||
});
|
||||
|
||||
if (completeSignUp.status === "complete") {
|
||||
await setActive({ session: completeSignUp.createdSessionId });
|
||||
} else {
|
||||
console.error(JSON.stringify(completeSignUp, null, 2));
|
||||
setErrorMessage("Failed to verify. Please check your code.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(JSON.stringify(err, null, 2));
|
||||
setErrorMessage("Invalid verification code. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-1 justify-center items-center bg-gray-50 p-6">
|
||||
<View className="bg-white w-11/12 max-w-md p-8 rounded-lg shadow-lg">
|
||||
<Text className="text-3xl font-bold text-center text-gray-800 mb-6">
|
||||
{pendingVerification ? "Verify Email" : "Sign Up"}
|
||||
</Text>
|
||||
{errorMessage ? (
|
||||
<Text className="text-red-500 text-center mb-4">{errorMessage}</Text>
|
||||
) : null}
|
||||
|
||||
{!pendingVerification && (
|
||||
<>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
value={emailAddress}
|
||||
placeholder="Email..."
|
||||
onChangeText={(email) => setEmailAddress(email)}
|
||||
className="w-full h-12 mb-4 px-4 bg-gray-100 border border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
<TextInput
|
||||
value={password}
|
||||
placeholder="Password..."
|
||||
secureTextEntry={true}
|
||||
onChangeText={(password) => setPassword(password)}
|
||||
className="w-full h-12 mb-6 px-4 bg-gray-100 border border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={onSignUpPress}
|
||||
className="w-full h-12 bg-blue-600 rounded-lg flex justify-center items-center mb-4"
|
||||
>
|
||||
<Text className="text-white text-lg font-semibold">Sign Up</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
|
||||
{pendingVerification && (
|
||||
<>
|
||||
<TextInput
|
||||
value={code}
|
||||
placeholder="Verification Code..."
|
||||
onChangeText={(code) => setCode(code)}
|
||||
className="w-full h-12 mb-4 px-4 bg-gray-100 border border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={onPressVerify}
|
||||
className="w-full h-12 bg-green-600 rounded-lg flex justify-center items-center mb-4"
|
||||
>
|
||||
<Text className="text-white text-lg font-semibold">
|
||||
Verify Email
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ScrollViewStyleReset } from "expo-router/html";
|
||||
import { type PropsWithChildren } from "react";
|
||||
|
||||
/**
|
||||
* This file is web-only and used to configure the root HTML for every web page during static rendering.
|
||||
* The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs.
|
||||
*/
|
||||
export default function Root({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
|
||||
{/*
|
||||
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
|
||||
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
|
||||
*/}
|
||||
<ScrollViewStyleReset />
|
||||
|
||||
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
|
||||
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
|
||||
{/* Add any additional <head> elements that you want globally available on web... */}
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
const responsiveBackground = `
|
||||
body {
|
||||
background-color: #fff;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #000;
|
||||
}
|
||||
}`;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Link, Stack } from "expo-router";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: "Oops!" }} />
|
||||
<View style={styles.container}>
|
||||
<Text>This screen doesn't exist.</Text>
|
||||
<Link href="/" style={styles.link}>
|
||||
<Text>Go to home screen!</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 20,
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import "../global.css";
|
||||
import { ClerkLoaded, ClerkProvider } from "@clerk/clerk-expo";
|
||||
import { secureStore } from "@clerk/clerk-expo/secure-store";
|
||||
import { useFonts } from "expo-font";
|
||||
import { Slot, useRouter, useSegments } from "expo-router";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
import { useIsAuthenticated, useJazzContext } from "jazz-tools/expo";
|
||||
import React, { useEffect } from "react";
|
||||
import { tokenCache } from "../cache";
|
||||
import { JazzAndAuth } from "../src/auth-context";
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
function InitialLayout() {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const segments = useSegments();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const inAuthGroup = segments[0] === "(auth)";
|
||||
|
||||
if (isAuthenticated && inAuthGroup) {
|
||||
router.replace("/chat");
|
||||
} else if (!isAuthenticated && !inAuthGroup) {
|
||||
router.replace("/");
|
||||
}
|
||||
|
||||
SplashScreen.hideAsync();
|
||||
}, [isAuthenticated, segments, router]);
|
||||
|
||||
return <Slot />;
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
const [fontsLoaded] = useFonts({
|
||||
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
||||
});
|
||||
|
||||
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
||||
|
||||
if (!publishableKey) {
|
||||
throw new Error(
|
||||
"Missing Publishable Key. Please set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env",
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (fontsLoaded) {
|
||||
} else {
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
}
|
||||
}, [fontsLoaded]);
|
||||
|
||||
if (!fontsLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClerkProvider
|
||||
tokenCache={tokenCache}
|
||||
publishableKey={publishableKey}
|
||||
__experimental_resourceCache={secureStore}
|
||||
>
|
||||
<ClerkLoaded>
|
||||
<JazzAndAuth>
|
||||
<InitialLayout />
|
||||
</JazzAndAuth>
|
||||
</ClerkLoaded>
|
||||
</ClerkProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
import { Chat, Message } from "@/src/schema";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import clsx from "clsx";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { CoPlainText, Group, Loaded } from "jazz-tools";
|
||||
import { useAccount, useCoState } from "jazz-tools/expo";
|
||||
import { ProgressiveImgNative } from "jazz-tools/expo";
|
||||
import { createImageNative } from "jazz-tools/react-native-media-images";
|
||||
import { useEffect, useLayoutEffect, useState } from "react";
|
||||
import React, {
|
||||
SafeAreaView,
|
||||
View,
|
||||
Text,
|
||||
Alert,
|
||||
TouchableOpacity,
|
||||
FlatList,
|
||||
KeyboardAvoidingView,
|
||||
TextInput,
|
||||
Button,
|
||||
Image,
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
|
||||
export default function Conversation() {
|
||||
const { chatId } = useLocalSearchParams();
|
||||
const { me } = useAccount();
|
||||
const [chat, setChat] = useState<Loaded<typeof Chat>>();
|
||||
const [message, setMessage] = useState("");
|
||||
const loadedChat = useCoState(Chat, chat?.id, { resolve: { $each: true } });
|
||||
const navigation = useNavigation();
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (chat) return;
|
||||
if (chatId === "new") {
|
||||
createChat();
|
||||
} else {
|
||||
loadChat(chatId as string);
|
||||
}
|
||||
}, [chat]);
|
||||
|
||||
// Effect to dynamically set header options
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerTitle: "Chat",
|
||||
headerRight: () =>
|
||||
chat ? (
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (chat?.id) {
|
||||
Clipboard.setStringAsync(
|
||||
`https://chat.jazz.tools/#/chat/${chat.id}`,
|
||||
);
|
||||
Alert.alert("Copied to clipboard", `Chat ID: ${chat.id}`);
|
||||
}
|
||||
}}
|
||||
title="Share"
|
||||
/>
|
||||
) : null,
|
||||
});
|
||||
}, [navigation, chat]);
|
||||
|
||||
const createChat = () => {
|
||||
const group = Group.create({ owner: me });
|
||||
group.addMember("everyone", "writer");
|
||||
const chat = Chat.create([], { owner: group });
|
||||
setChat(chat);
|
||||
};
|
||||
|
||||
const loadChat = async (chatId: string) => {
|
||||
try {
|
||||
const chat = await Chat.load(chatId);
|
||||
if (chat) setChat(chat);
|
||||
} catch (error) {
|
||||
console.log("Error loading chat", error);
|
||||
Alert.alert("Error", `Error loading chat: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = () => {
|
||||
if (!chat) return;
|
||||
if (message.trim()) {
|
||||
chat.push(
|
||||
Message.create(
|
||||
{ text: CoPlainText.create(message, chat._owner) },
|
||||
chat._owner,
|
||||
),
|
||||
);
|
||||
setMessage("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageUpload = async () => {
|
||||
try {
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
base64: true,
|
||||
quality: 0.7,
|
||||
});
|
||||
|
||||
if (!result.canceled && result.assets[0].base64 && chat) {
|
||||
setIsUploading(true);
|
||||
const base64Uri = `data:image/jpeg;base64,${result.assets[0].base64}`;
|
||||
|
||||
const image = await createImageNative(base64Uri, {
|
||||
owner: chat._owner,
|
||||
maxSize: 2048,
|
||||
});
|
||||
|
||||
chat.push(
|
||||
Message.create(
|
||||
{ text: CoPlainText.create("", chat._owner), image },
|
||||
chat._owner,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert("Error", "Failed to upload image");
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderMessageItem = ({ item }: { item: Loaded<typeof Message> }) => {
|
||||
const isMe = item._edits.text?.by?.isMe;
|
||||
return (
|
||||
<View
|
||||
className={clsx(
|
||||
`rounded-xl px-3 py-2 max-w-[75%] my-1`,
|
||||
isMe ? `bg-blue-500 self-end` : `bg-gray-200 self-start`,
|
||||
)}
|
||||
>
|
||||
{!isMe ? (
|
||||
<Text
|
||||
className={clsx(
|
||||
`text-xs text-gray-500 mb-1`,
|
||||
isMe ? "text-right" : "text-left",
|
||||
)}
|
||||
>
|
||||
{item._edits.text?.by?.profile?.name}
|
||||
</Text>
|
||||
) : null}
|
||||
<View
|
||||
className={clsx(
|
||||
"flex relative items-end justify-between",
|
||||
isMe ? "flex-row" : "flex-row",
|
||||
)}
|
||||
>
|
||||
{item.image && (
|
||||
<ProgressiveImgNative
|
||||
image={item.image}
|
||||
maxWidth={1024}
|
||||
children={({ src }) => (
|
||||
<Image
|
||||
source={{ uri: src }}
|
||||
className="w-48 h-48 rounded-lg mb-2"
|
||||
resizeMode="cover"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{item.text && (
|
||||
<Text
|
||||
className={clsx(
|
||||
!isMe ? "text-black" : "text-gray-200",
|
||||
`text-md max-w-[85%]`,
|
||||
)}
|
||||
>
|
||||
{item.text}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
className={clsx(
|
||||
"text-[10px] text-right ml-2",
|
||||
!isMe ? "mt-2 text-gray-500" : "mt-1 text-gray-200",
|
||||
)}
|
||||
>
|
||||
{item._edits.text?.madeAt?.getHours().toString().padStart(2, "0")}:
|
||||
{item._edits.text?.madeAt?.getMinutes().toString().padStart(2, "0")}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-gray-50">
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 8,
|
||||
}}
|
||||
className="flex"
|
||||
data={loadedChat}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderMessageItem}
|
||||
/>
|
||||
<KeyboardAvoidingView
|
||||
keyboardVerticalOffset={110}
|
||||
behavior="padding"
|
||||
className="p-3 bg-white border-t border-gray-300"
|
||||
>
|
||||
<SafeAreaView className="flex-row items-center gap-2">
|
||||
<TouchableOpacity
|
||||
onPress={handleImageUpload}
|
||||
disabled={isUploading}
|
||||
className="h-10 w-10 items-center justify-center"
|
||||
>
|
||||
{isUploading ? (
|
||||
<ActivityIndicator size="small" color="#0000ff" />
|
||||
) : (
|
||||
<Text className="text-2xl">🖼️</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<TextInput
|
||||
className="flex-1 rounded-full h-10 px-4 bg-gray-100 border border-gray-300 focus:border-blue-500 focus:bg-white"
|
||||
value={message}
|
||||
onChangeText={setMessage}
|
||||
placeholder="Type a message..."
|
||||
textAlignVertical="center"
|
||||
onSubmitEditing={sendMessage}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={sendMessage}
|
||||
className="bg-blue-500 rounded-full h-10 w-10 items-center justify-center"
|
||||
>
|
||||
<Text className="text-white text-xl">↑</Text>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Stack } from "expo-router";
|
||||
import React from "react";
|
||||
|
||||
export default function ChatLayout() {
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: true,
|
||||
headerBackVisible: true,
|
||||
headerTitle: "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useRouter } from "expo-router";
|
||||
import { ID } from "jazz-tools";
|
||||
import { useLayoutEffect } from "react";
|
||||
import React, {
|
||||
Button,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
Alert,
|
||||
} from "react-native";
|
||||
|
||||
import { useUser } from "@clerk/clerk-expo";
|
||||
import { useAccount } from "jazz-tools/expo";
|
||||
import { Chat } from "../../src/schema";
|
||||
|
||||
export default function ChatScreen() {
|
||||
const { logOut } = useAccount();
|
||||
const router = useRouter();
|
||||
const navigation = useNavigation();
|
||||
const { user } = useUser();
|
||||
|
||||
function handleLogOut() {
|
||||
logOut();
|
||||
router.navigate("/");
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerTitle: "Chat",
|
||||
headerRight: () => <Button onPress={handleLogOut} title="Logout" />,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const loadChat = async (chatId: string | "new") => {
|
||||
router.navigate(`/chat/${chatId}`);
|
||||
};
|
||||
|
||||
const joinChat = () => {
|
||||
Alert.prompt(
|
||||
"Join Chat",
|
||||
"Enter the Chat ID (example: co_zBGEHYvRfGuT2YSBraY3njGjnde)",
|
||||
[
|
||||
{
|
||||
text: "Cancel",
|
||||
style: "cancel",
|
||||
},
|
||||
{
|
||||
text: "Join",
|
||||
onPress: (chatId) => {
|
||||
if (chatId) {
|
||||
loadChat(chatId);
|
||||
} else {
|
||||
Alert.alert("Error", "Chat ID cannot be empty.");
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
"plain-text",
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-gray-50">
|
||||
<View className="flex-1 justify-center items-center px-6">
|
||||
<View className="w-full max-w-sm bg-white p-8 rounded-lg shadow-lg">
|
||||
<Text className="text-xl font-semibold text-gray-800">
|
||||
Welcome, {user?.emailAddresses[0].emailAddress}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => loadChat("new")}
|
||||
className="w-full bg-blue-600 py-4 rounded-md mb-4 mt-4"
|
||||
>
|
||||
<Text className="text-white text-lg font-semibold text-center">
|
||||
Start New Chat
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={joinChat}
|
||||
className="w-full bg-green-500 py-4 rounded-md"
|
||||
>
|
||||
<Text className="text-white text-lg font-semibold text-center">
|
||||
Join Chat
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 46 KiB |
@@ -1,9 +0,0 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: [
|
||||
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
||||
"nativewind/babel",
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
export interface TokenCache {
|
||||
getToken: (key: string) => Promise<string | undefined | null>;
|
||||
saveToken: (key: string, token: string) => Promise<void>;
|
||||
clearToken: (key: string) => void;
|
||||
}
|
||||
|
||||
const createTokenCache = (): TokenCache => {
|
||||
return {
|
||||
getToken: async (key: string) => {
|
||||
try {
|
||||
const item = await SecureStore.getItemAsync(key);
|
||||
if (item) {
|
||||
console.log(`${key} was used 🔐 \n`);
|
||||
} else {
|
||||
console.log("No values stored under key: " + key);
|
||||
}
|
||||
return item;
|
||||
} catch (error) {
|
||||
console.error("secure store get item error: ", error);
|
||||
await SecureStore.deleteItemAsync(key);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
saveToken: (key: string, token: string) => {
|
||||
return SecureStore.setItemAsync(key, token);
|
||||
},
|
||||
clearToken: (key: string) => {
|
||||
return SecureStore.deleteItemAsync(key);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// SecureStore is not supported on the web
|
||||
// https://github.com/expo/expo/issues/7744#issuecomment-611093485
|
||||
export const tokenCache =
|
||||
Platform.OS !== "web" ? createTokenCache() : undefined;
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 12.5.1",
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"ios-simulator": {
|
||||
"extends": "development",
|
||||
"ios": {
|
||||
"simulator": true
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,2 +0,0 @@
|
||||
import "./polyfills";
|
||||
import "expo-router/entry";
|
||||
@@ -1,35 +0,0 @@
|
||||
// Learn more https://docs.expo.dev/guides/monorepos
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const { withNativeWind } = require("nativewind/metro");
|
||||
const { FileStore } = require("metro-cache");
|
||||
const path = require("path");
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const projectRoot = __dirname;
|
||||
const workspaceRoot = path.resolve(projectRoot, "../..");
|
||||
|
||||
const config = getDefaultConfig(projectRoot);
|
||||
|
||||
// Since we are using pnpm, we have to setup the monorepo manually for Metro
|
||||
// #1 - Watch all files in the monorepo
|
||||
config.watchFolders = [workspaceRoot];
|
||||
// #2 - Try resolving with project modules first, then workspace modules
|
||||
config.resolver.nodeModulesPaths = [
|
||||
path.resolve(projectRoot, "node_modules"),
|
||||
path.resolve(workspaceRoot, "node_modules"),
|
||||
];
|
||||
config.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"];
|
||||
config.resolver.requireCycleIgnorePatterns = [
|
||||
/(^|\/|\\)node_modules($|\/|\\)/,
|
||||
/(^|\/|\\)packages($|\/|\\)/,
|
||||
];
|
||||
|
||||
// Use turborepo to restore the cache when possible
|
||||
config.cacheStores = [
|
||||
new FileStore({
|
||||
root: path.join(projectRoot, "node_modules", ".cache", "metro"),
|
||||
}),
|
||||
];
|
||||
|
||||
// module.exports = config;
|
||||
module.exports = withNativeWind(config, { input: "./global.css" });
|
||||
@@ -1 +0,0 @@
|
||||
/// <reference types="nativewind/types" />
|
||||
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"name": "chat-rn-expo-clerk",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
"format-and-lint": "biome check .",
|
||||
"format-and-lint:fix": "biome check . --write",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo prebuild && pnpx pod-install && expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"run:ios": "pnpm expo prebuild && npx pod-install && pnpm expo run:ios"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/core-asynciterator-polyfill": "^1.0.2",
|
||||
"@bacons/text-decoder": "0.0.0",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
||||
"@clerk/clerk-expo": "^2.2.21",
|
||||
"@expo/vector-icons": "^14.1.0",
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"@react-navigation/native": "7.0.19",
|
||||
"@react-navigation/native-stack": "7.2.1",
|
||||
"clsx": "^2.0.0",
|
||||
"expo": "^53.0.8",
|
||||
"expo-build-properties": "~0.14.6",
|
||||
"expo-clipboard": "~7.1.4",
|
||||
"expo-constants": "~17.1.6",
|
||||
"expo-crypto": "~14.1.4",
|
||||
"expo-dev-client": "~5.1.8",
|
||||
"expo-file-system": "^18.1.9",
|
||||
"expo-font": "~13.3.1",
|
||||
"expo-image-picker": "~16.1.4",
|
||||
"expo-linking": "~7.1.4",
|
||||
"expo-router": "~5.0.6",
|
||||
"expo-secure-store": "~14.2.3",
|
||||
"expo-splash-screen": "~0.30.8",
|
||||
"expo-sqlite": "15.2.9",
|
||||
"expo-status-bar": "~2.2.3",
|
||||
"expo-web-browser": "~14.1.6",
|
||||
"jazz-tools": "workspace:*",
|
||||
"nativewind": "^4.1.21",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-gesture-handler": "~2.24.0",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"react-native-reanimated": "~3.17.5",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "4.10.0",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-web": "~0.20.0",
|
||||
"readable-stream": "4.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/react": "~19.0.14",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "chat-rn-expo-clerk-example-jazz@garden.co";
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useClerk } from "@clerk/clerk-expo";
|
||||
import { JazzExpoProviderWithClerk } from "jazz-tools/expo";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { apiKey } from "./apiKey";
|
||||
|
||||
export function JazzAndAuth({ children }: PropsWithChildren) {
|
||||
const clerk = useClerk();
|
||||
|
||||
return (
|
||||
<JazzExpoProviderWithClerk
|
||||
clerk={clerk}
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</JazzExpoProviderWithClerk>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { CoList, co, coField, z } from "jazz-tools";
|
||||
|
||||
export const Message = co.map({
|
||||
text: co.plainText(),
|
||||
image: z.optional(co.image()),
|
||||
});
|
||||
|
||||
export const Chat = co.list(Message);
|
||||
@@ -1,14 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
// NOTE: Update this to include the paths to all of your component files.
|
||||
content: [
|
||||
"./app/**/*.{js,jsx,ts,tsx}",
|
||||
"./components/**/*.{js,jsx,ts,tsx}",
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
presets: [require("nativewind/preset")],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "nativewind-env.d.ts"]
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
"expo-clipboard": "^7.1.4",
|
||||
"expo-secure-store": "~14.2.3",
|
||||
"expo-sqlite": "~15.2.10",
|
||||
"expo-status-bar": "~2.2.3",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.2",
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [23bfea5]
|
||||
- Updated dependencies [e4ba23c]
|
||||
- Updated dependencies [4b89838]
|
||||
- jazz-tools@0.15.5
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-tools@0.15.4
|
||||
|
||||
## 0.0.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [45f73a7]
|
||||
- jazz-tools@0.15.3
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [0e7e532]
|
||||
- jazz-tools@0.15.2
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-svelte",
|
||||
"version": "0.0.90",
|
||||
"version": "0.0.94",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
1
examples/clerk-expo/.env.test
Normal file
@@ -0,0 +1 @@
|
||||
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_ZXZpZGVudC1kYW5lLTg5LmNsZXJrLmFjY291bnRzLmRldiQ
|
||||
44
examples/clerk-expo/.gitignore
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
expo-env.d.ts
|
||||
|
||||
# Native
|
||||
.kotlin/
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
android/
|
||||
ios/
|
||||
# env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
32
examples/clerk-expo/app.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "rnexpoclerk",
|
||||
"slug": "rnexpoclerk",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "tools.jazz.rnexpoclerk"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"package": "tools.jazz.chatrnexpo"
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
},
|
||||
"plugins": ["expo-secure-store", "expo-sqlite", "expo-web-browser"]
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
BIN
examples/clerk-expo/assets/splash-icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
9
examples/clerk-expo/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { registerRootComponent } from "expo";
|
||||
import "./polyfills";
|
||||
|
||||
import App from "./src/App";
|
||||
|
||||
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||
// the environment is set up appropriately
|
||||
registerRootComponent(App);
|
||||
35
examples/clerk-expo/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "clerk-expo",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"build": "expo prebuild",
|
||||
"start": "expo start",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/core-asynciterator-polyfill": "^1.0.2",
|
||||
"@bacons/text-decoder": "^0.0.0",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
||||
"@clerk/clerk-expo": "^2.13.1",
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"expo": "~53.0.9",
|
||||
"expo-crypto": "~14.1.5",
|
||||
"expo-linking": "~7.1.5",
|
||||
"expo-secure-store": "~14.2.3",
|
||||
"expo-sqlite": "~15.2.10",
|
||||
"expo-web-browser": "~14.2.0",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"readable-stream": "^4.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/react": "~19.0.10",
|
||||
"typescript": "~5.8.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
48
examples/clerk-expo/src/App.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ClerkLoaded, ClerkProvider, useClerk } from "@clerk/clerk-expo";
|
||||
import { resourceCache } from "@clerk/clerk-expo/resource-cache";
|
||||
import { tokenCache } from "@clerk/clerk-expo/token-cache";
|
||||
import { JazzExpoProviderWithClerk } from "jazz-tools/expo";
|
||||
import { MainScreen } from "./MainScreen";
|
||||
import { apiKey } from "./apiKey";
|
||||
import { AuthScreen } from "./auth/AuthScreen";
|
||||
|
||||
function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
const clerk = useClerk();
|
||||
|
||||
return (
|
||||
<JazzExpoProviderWithClerk
|
||||
clerk={clerk}
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</JazzExpoProviderWithClerk>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
||||
|
||||
if (!publishableKey) {
|
||||
throw new Error(
|
||||
"Missing Publishable Key. Please set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env",
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ClerkProvider
|
||||
tokenCache={tokenCache}
|
||||
__experimental_resourceCache={resourceCache}
|
||||
publishableKey={publishableKey}
|
||||
>
|
||||
<ClerkLoaded>
|
||||
<JazzAndAuth>
|
||||
<AuthScreen>
|
||||
<MainScreen />
|
||||
</AuthScreen>
|
||||
</JazzAndAuth>
|
||||
</ClerkLoaded>
|
||||
</ClerkProvider>
|
||||
);
|
||||
}
|
||||
44
examples/clerk-expo/src/MainScreen.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useClerk } from "@clerk/clerk-expo";
|
||||
import { useAccount } from "jazz-tools/expo";
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||
|
||||
export function MainScreen() {
|
||||
const { me } = useAccount();
|
||||
const { signOut } = useClerk();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await signOut();
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>You're logged in</Text>
|
||||
<Text style={styles.subtitle}>Welcome back, {me?.profile?.name}</Text>
|
||||
<TouchableOpacity onPress={handleSignOut}>
|
||||
<Text>Sign out</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 20,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 16,
|
||||
textAlign: "center",
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
marginBottom: 24,
|
||||
textAlign: "center",
|
||||
color: "#666",
|
||||
},
|
||||
});
|
||||
1
examples/clerk-expo/src/apiKey.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const apiKey = "chat-rn-expo-example-jazz@garden.co";
|
||||
19
examples/clerk-expo/src/auth/AuthScreen.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useIsAuthenticated } from "jazz-tools/expo";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { SignInScreen } from "./SignInScreen";
|
||||
import { SignUpScreen } from "./SignUpScreen";
|
||||
|
||||
export function AuthScreen({ children }: { children: ReactNode }) {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const [page, setPage] = useState<"sign-in" | "sign-up">("sign-in");
|
||||
|
||||
if (isAuthenticated) {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (page === "sign-in") {
|
||||
return <SignInScreen setPage={setPage} />;
|
||||
}
|
||||
|
||||
return <SignUpScreen setPage={setPage} />;
|
||||
}
|
||||
127
examples/clerk-expo/src/auth/SignInScreen.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useSignIn } from "@clerk/clerk-expo";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
export function SignInScreen({
|
||||
setPage,
|
||||
}: { setPage: (page: "sign-in" | "sign-up") => void }) {
|
||||
const { signIn, setActive, isLoaded } = useSignIn();
|
||||
|
||||
const [emailAddress, setEmailAddress] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
// Handle the submission of the sign-in form
|
||||
const onSignInPress = async () => {
|
||||
if (!isLoaded) return;
|
||||
|
||||
// Start the sign-in process using the email and password provided
|
||||
try {
|
||||
const signInAttempt = await signIn.create({
|
||||
identifier: emailAddress,
|
||||
password,
|
||||
});
|
||||
|
||||
// If sign-in process is complete, set the created session as active
|
||||
// and redirect the user
|
||||
if (signInAttempt.status === "complete") {
|
||||
await setActive({ session: signInAttempt.createdSessionId });
|
||||
} else {
|
||||
// If the status isn't complete, check why. User might need to
|
||||
// complete further steps.
|
||||
console.error(JSON.stringify(signInAttempt, null, 2));
|
||||
}
|
||||
} catch (err) {
|
||||
// See https://clerk.com/docs/custom-flows/error-handling
|
||||
// for more info on error handling
|
||||
console.error(JSON.stringify(err, null, 2));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Sign in</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
autoCapitalize="none"
|
||||
value={emailAddress}
|
||||
placeholder="Enter email"
|
||||
onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={password}
|
||||
placeholder="Enter password"
|
||||
secureTextEntry={true}
|
||||
onChangeText={(password) => setPassword(password)}
|
||||
/>
|
||||
<TouchableOpacity style={styles.button} onPress={onSignInPress}>
|
||||
<Text style={styles.buttonText}>Continue</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Don't have an account?</Text>
|
||||
<TouchableOpacity onPress={() => setPage("sign-up")}>
|
||||
<Text style={styles.linkText}>Sign up</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#f5f5f5",
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
marginBottom: 30,
|
||||
color: "#333",
|
||||
},
|
||||
input: {
|
||||
backgroundColor: "white",
|
||||
borderWidth: 1,
|
||||
borderColor: "#ddd",
|
||||
borderRadius: 8,
|
||||
padding: 15,
|
||||
marginBottom: 15,
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: "#007AFF",
|
||||
borderRadius: 8,
|
||||
padding: 15,
|
||||
alignItems: "center",
|
||||
marginTop: 10,
|
||||
},
|
||||
buttonText: {
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
footer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginTop: 20,
|
||||
gap: 5,
|
||||
},
|
||||
footerText: {
|
||||
color: "#666",
|
||||
fontSize: 14,
|
||||
},
|
||||
linkText: {
|
||||
color: "#007AFF",
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
169
examples/clerk-expo/src/auth/SignUpScreen.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { useSignUp } from "@clerk/clerk-expo";
|
||||
import * as React from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
export function SignUpScreen({
|
||||
setPage,
|
||||
}: { setPage: (page: "sign-in" | "sign-up") => void }) {
|
||||
const { isLoaded, signUp, setActive } = useSignUp();
|
||||
|
||||
const [emailAddress, setEmailAddress] = React.useState("");
|
||||
const [password, setPassword] = React.useState("");
|
||||
const [pendingVerification, setPendingVerification] = React.useState(false);
|
||||
const [code, setCode] = React.useState("");
|
||||
|
||||
// Handle submission of sign-up form
|
||||
const onSignUpPress = async () => {
|
||||
if (!isLoaded) return;
|
||||
|
||||
console.log(emailAddress, password);
|
||||
|
||||
// Start sign-up process using email and password provided
|
||||
try {
|
||||
await signUp.create({
|
||||
emailAddress,
|
||||
password,
|
||||
});
|
||||
|
||||
// Send user an email with verification code
|
||||
await signUp.prepareEmailAddressVerification({ strategy: "email_code" });
|
||||
|
||||
// Set 'pendingVerification' to true to display second form
|
||||
// and capture OTP code
|
||||
setPendingVerification(true);
|
||||
} catch (err) {
|
||||
// See https://clerk.com/docs/custom-flows/error-handling
|
||||
// for more info on error handling
|
||||
console.error(JSON.stringify(err, null, 2));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle submission of verification form
|
||||
const onVerifyPress = async () => {
|
||||
if (!isLoaded) return;
|
||||
|
||||
try {
|
||||
// Use the code the user provided to attempt verification
|
||||
const signUpAttempt = await signUp.attemptEmailAddressVerification({
|
||||
code,
|
||||
});
|
||||
|
||||
// If verification was completed, set the session to active
|
||||
// and redirect the user
|
||||
if (signUpAttempt.status === "complete") {
|
||||
await setActive({ session: signUpAttempt.createdSessionId });
|
||||
} else {
|
||||
// If the status is not complete, check why. User may need to
|
||||
// complete further steps.
|
||||
console.error(JSON.stringify(signUpAttempt, null, 2));
|
||||
}
|
||||
} catch (err) {
|
||||
// See https://clerk.com/docs/custom-flows/error-handling
|
||||
// for more info on error handling
|
||||
console.error(JSON.stringify(err, null, 2));
|
||||
}
|
||||
};
|
||||
|
||||
if (pendingVerification) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Verify your email</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={code}
|
||||
placeholder="Enter your verification code"
|
||||
onChangeText={(code) => setCode(code)}
|
||||
/>
|
||||
<TouchableOpacity style={styles.button} onPress={onVerifyPress}>
|
||||
<Text style={styles.buttonText}>Verify</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Sign up</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
autoCapitalize="none"
|
||||
value={emailAddress}
|
||||
placeholder="Enter email"
|
||||
onChangeText={(email) => setEmailAddress(email)}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={password}
|
||||
placeholder="Enter password"
|
||||
secureTextEntry={true}
|
||||
onChangeText={(password) => setPassword(password)}
|
||||
/>
|
||||
<TouchableOpacity style={styles.button} onPress={onSignUpPress}>
|
||||
<Text style={styles.buttonText}>Continue</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.linkContainer}>
|
||||
<Text style={styles.linkText}>Already have an account? </Text>
|
||||
<TouchableOpacity onPress={() => setPage("sign-in")}>
|
||||
<Text style={styles.link}>Sign in</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
padding: 20,
|
||||
backgroundColor: "#f5f5f5",
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
marginBottom: 30,
|
||||
color: "#333",
|
||||
},
|
||||
input: {
|
||||
backgroundColor: "white",
|
||||
borderWidth: 1,
|
||||
borderColor: "#ddd",
|
||||
borderRadius: 8,
|
||||
padding: 15,
|
||||
marginBottom: 15,
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: "#007AFF",
|
||||
borderRadius: 8,
|
||||
padding: 15,
|
||||
alignItems: "center",
|
||||
marginBottom: 20,
|
||||
},
|
||||
buttonText: {
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
linkContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
linkText: {
|
||||
color: "#666",
|
||||
fontSize: 14,
|
||||
},
|
||||
link: {
|
||||
color: "#007AFF",
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
9
examples/clerk-expo/src/schema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
export const Message = co.map({
|
||||
text: z.string(),
|
||||
});
|
||||
export type Message = co.loaded<typeof Message>;
|
||||
|
||||
export const Chat = co.list(Message);
|
||||
export type Chat = co.loaded<typeof Chat>;
|
||||
6
examples/clerk-expo/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export const TodoAccount = co
|
||||
/** The account migration is run on account creation and on every log-in.
|
||||
* You can use it to set up the account root and any other initial CoValues you need.
|
||||
*/
|
||||
if (!account.root) {
|
||||
if (account.root === undefined) {
|
||||
account.root = TodoAccountRoot.create({
|
||||
projects: co.list(TodoProject).create([], { owner: account }),
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@ import React from "react";
|
||||
import { TodoAccount, TodoProject } from "./1_schema.ts";
|
||||
import { NewProjectForm } from "./3_NewProjectForm.tsx";
|
||||
import { ProjectTodoTable } from "./4_ProjectTodoTable.tsx";
|
||||
import { apiKey } from "./apiKey";
|
||||
import {
|
||||
Button,
|
||||
ThemeProvider,
|
||||
@@ -42,7 +41,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<JazzReactProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
peer: `ws://localhost:4200`,
|
||||
}}
|
||||
AccountSchema={TodoAccount}
|
||||
>
|
||||
@@ -53,6 +52,8 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
// http://localhost:5173/#/project/co_znUD6vciCQazKwAKi4hFwRodxEk
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<JazzAndAuth>
|
||||
|
||||
@@ -11,7 +11,9 @@ import { useNavigate } from "react-router";
|
||||
export function NewProjectForm() {
|
||||
// `me` represents the current user account, which will determine
|
||||
// access rights to CoValues. We get it from the top-level provider `<WithJazz/>`.
|
||||
const { me } = useAccount(TodoAccount);
|
||||
const { me } = useAccount(TodoAccount, {
|
||||
resolve: { root: { projects: { $each: { $onError: null } } } },
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
const createProject = useCallback(
|
||||
|
||||
@@ -5,6 +5,7 @@ export const COLORS = {
|
||||
GREEN: "#8BDA27",
|
||||
PINK: "#EF478E",
|
||||
PURPLE: "#B441EB",
|
||||
YELLOW: "#FBC400",
|
||||
YELLOW: "#FCAE00",
|
||||
RED: "#FF601C",
|
||||
ORANGE: "#FF601C",
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"radix-ui": "^1.4.2",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"resend": "^4.0.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
Before Width: | Height: | Size: 629 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,47 +1,46 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--color-primary: #146aff;
|
||||
--color-primary-dark: lch(
|
||||
from var(--color-primary) calc(l - 15) calc(c - 1) calc(h + 10)
|
||||
from var(--color-primary) calc(l - 10) calc(c - 1) calc(h + 5)
|
||||
);
|
||||
--color-primary-light: lch(
|
||||
from var(--color-primary) calc(l + 15) calc(c + 1) calc(h - 10)
|
||||
from var(--color-primary) calc(l + 10) calc(c + 1) calc(h - 5)
|
||||
);
|
||||
|
||||
--color-secondary: var(--color-primary-dark);
|
||||
--color-highlight: #2dc9c9;
|
||||
--color-success: #42bb69;
|
||||
--color-info: #fbc400;
|
||||
--color-info: #b441eb;
|
||||
--color-warning: #ff601c;
|
||||
--color-tip: #b441eb;
|
||||
--color-success: #8bda27;
|
||||
--color-alert: #fcae00;
|
||||
--color-tip: #2dc9c9;
|
||||
--color-pink: #ef478e;
|
||||
--color-danger: #ee494c;
|
||||
|
||||
--color-default: theme("colors.stone.700");
|
||||
--color-highlight: theme("colors.stone.900");
|
||||
--color-strong: theme("colors.stone.800");
|
||||
--color-muted: theme("colors.stone.300");
|
||||
|
||||
--color-transparent-white: rgba(255, 255, 255, 0.1);
|
||||
--color-transparent-primary: lch(from var(--color-primary-dark) l c h / 0.1);
|
||||
--color-transparent-primary: lch(from var(--color-primary) l c h / 0.1);
|
||||
|
||||
--color-border-default: theme("colors.stone.200");
|
||||
--color-background-highlight: lch(from var(--color-primary) l c h / 0.25);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-secondary: var(--color-primary-light);
|
||||
|
||||
--color-default: theme("colors.stone.400");
|
||||
--color-highlight: theme("colors.white");
|
||||
--color-strong: theme("colors.stone.100");
|
||||
--color-muted: theme("colors.stone.700");
|
||||
|
||||
--color-transparent-primary: lch(from var(--color-primary-light) l c h / 0.2);
|
||||
--color-transparent-primary: lch(from var(--color-primary) l c h / 0.3);
|
||||
|
||||
--color-border-default: theme("colors.stone.900");
|
||||
--color-background-highlight: lch(from var(--color-primary) l c h / 0.5);
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { ThemeProvider } from "../components/organisms/ThemeProvider";
|
||||
import { fontClasses } from "../fonts";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -13,7 +15,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="h-full">
|
||||
<html lang="en" className="h-full" suppressHydrationWarning>
|
||||
<body
|
||||
className={[
|
||||
...fontClasses,
|
||||
@@ -21,7 +23,15 @@ export default function RootLayout({
|
||||
"bg-white dark:bg-stone-950 text-default",
|
||||
].join(" ")}
|
||||
>
|
||||
{children}
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<Toaster position="bottom-right" />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,74 +1,14 @@
|
||||
import { Prose } from "@components/molecules/Prose";
|
||||
import { NewsletterForm } from "@components/organisms/NewsletterForm";
|
||||
import { ViewsLayout } from "./views/ViewsLayout";
|
||||
import Colors from "./views/colors/page";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="container flex flex-col gap-8 py-8 lg:py-16">
|
||||
<h1 className="text-2xl font-semibold font-display">
|
||||
Jazz Design System
|
||||
</h1>
|
||||
|
||||
<h2>Typography (Prose)</h2>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
Heading 1
|
||||
<Prose className="p-3 border">
|
||||
<h1>Ship top-tier apps at high tempo</h1>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Heading 2
|
||||
<Prose className="p-3 border">
|
||||
<h2>Ship top-tier apps at high tempo</h2>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Heading 3
|
||||
<Prose className="p-3 border">
|
||||
<h3>Ship top-tier apps at high tempo</h3>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Heading 4
|
||||
<Prose className="p-3 border">
|
||||
<h4>Ship top-tier apps at high tempo</h4>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Paragraph
|
||||
<Prose className="p-3 border">
|
||||
<p>
|
||||
<strong>Jazz is a framework for building local-first apps</strong>{" "}
|
||||
— an architecture that lets companies like Figma and Linear play
|
||||
in a league of their own.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Open source. Self-host or use Jazz Cloud for zero-config magic.
|
||||
</p>
|
||||
</Prose>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Link
|
||||
<Prose className="p-3 border">
|
||||
This is a <a href="https://jazz.tools">link</a>
|
||||
</Prose>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Code
|
||||
<Prose className="p-3 border">
|
||||
This is a one-line <code>piece of code</code>
|
||||
</Prose>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Newsletter Subscription Form</h2>
|
||||
|
||||
<div className="p-3 border">
|
||||
<NewsletterForm />
|
||||
<main>
|
||||
<div className="col-span-8 overflow-y-scroll">
|
||||
<ViewsLayout>
|
||||
<h1 className="text-2xl font-bold mt-4">Colors</h1>
|
||||
<Colors />
|
||||
</ViewsLayout>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
18
homepage/design-system/src/app/views/ViewsLayout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { ViewsSideMenu } from "./ViewsSideMenu";
|
||||
|
||||
export function ViewsLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="container py-8 lg:py-16 relative h-full overflow-hidden flex flex-row gap-2">
|
||||
<ViewsSideMenu />
|
||||
<div className="flex-1 overflow-y-scroll overflow-x-hidden pr-8">
|
||||
<h1 className="text-2xl font-semibold font-display">
|
||||
Jazz Design System
|
||||
</h1>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
homepage/design-system/src/app/views/ViewsSideMenu.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Button } from "../../components/atoms/Button";
|
||||
|
||||
const designSystemTopics = [
|
||||
"Colors",
|
||||
"Typography",
|
||||
"Buttons",
|
||||
"Components",
|
||||
"Inputs",
|
||||
"Icons",
|
||||
];
|
||||
|
||||
export function ViewsSideMenu() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className={clsx("sticky top-0", mobileMenuOpen ? "w-32" : "w-7")}>
|
||||
<Button
|
||||
intent="default"
|
||||
variant="link"
|
||||
icon={mobileMenuOpen ? "close" : "chevronRight"}
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
size="sm"
|
||||
/>
|
||||
{mobileMenuOpen && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{designSystemTopics.map((topic) => (
|
||||
<div key={topic}>
|
||||
<Button
|
||||
intent="default"
|
||||
variant="link"
|
||||
onClick={() => router.push(`/views/${topic.toLowerCase()}`)}
|
||||
>
|
||||
{topic}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
230
homepage/design-system/src/app/views/buttons/page.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/atoms/Button";
|
||||
import { Icon } from "@/components/atoms/Icon";
|
||||
import { Table } from "@/components/molecules/Table";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
} from "@/components/organisms/Dropdown";
|
||||
import { useState } from "react";
|
||||
import { Style } from "../../../utils/tailwindClassesMap";
|
||||
export default function ButtonsPage() {
|
||||
const variants = [
|
||||
"default",
|
||||
"primary",
|
||||
"tip",
|
||||
"info",
|
||||
"success",
|
||||
"warning",
|
||||
"alert",
|
||||
"danger",
|
||||
"muted",
|
||||
"strong",
|
||||
] as const;
|
||||
const [selectedVariant, setSelectedVariant] = useState<Style>("default");
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-lg mt-5 mb-2 font-bold">Variants</h3>
|
||||
<p className="mb-3">
|
||||
For compatibility the shadcn/ui variants are mapped to the design
|
||||
system.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button variant="default">default</Button>
|
||||
<Button variant="link">link</Button>
|
||||
<Button variant="ghost">ghost</Button>
|
||||
<Button variant="outline">outline</Button>
|
||||
<Button variant="secondary">secondary</Button>
|
||||
<Button variant="destructive">destructive</Button>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg mt-5 mb-2 font-bold">Intents</h3>
|
||||
<p>
|
||||
We have extended the shadcn/ui variants to include more styles via the
|
||||
intent prop.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* <Button intent="default">default</Button> */}
|
||||
<Button intent="primary">primary</Button>
|
||||
<Button intent="tip">tip</Button>
|
||||
<Button intent="info">info</Button>
|
||||
<Button intent="success">success</Button>
|
||||
<Button intent="warning">warning</Button>
|
||||
<Button intent="alert">alert</Button>
|
||||
<Button intent="danger">danger</Button>
|
||||
<Button intent="muted">muted</Button>
|
||||
<Button intent="strong">strong</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center w-48 mt-10">
|
||||
<h3 className="text-lg font-bold min-w-52">Variants & Intents</h3>
|
||||
<div className="max-w-xs ml-3">
|
||||
<Dropdown>
|
||||
<DropdownButton
|
||||
className="w-full justify-between"
|
||||
as={Button}
|
||||
intent="default"
|
||||
variant="inverted"
|
||||
>
|
||||
{selectedVariant}
|
||||
<Icon name="chevronDown" size="sm" />
|
||||
</DropdownButton>
|
||||
<DropdownMenu>
|
||||
{variants.map((variant) => (
|
||||
<DropdownItem
|
||||
key={variant}
|
||||
onClick={() => setSelectedVariant(variant)}
|
||||
>
|
||||
{variant}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm mt-2 mb-5">
|
||||
<strong>NB:</strong> Variants and styles are interchangeable. See the
|
||||
intent on each variant with the dropdown
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button intent={selectedVariant} variant="outline">
|
||||
outline
|
||||
</Button>
|
||||
<Button intent={selectedVariant} variant="inverted">
|
||||
inverted
|
||||
</Button>
|
||||
<Button intent={selectedVariant} variant="ghost">
|
||||
ghost
|
||||
</Button>
|
||||
<Button intent={selectedVariant} variant="link">
|
||||
link
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold mt-5">Icons</h3>
|
||||
|
||||
<p>Buttons can also contain an icon and text.</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
icon="delete"
|
||||
intent="danger"
|
||||
variant="link"
|
||||
iconPosition="right"
|
||||
className="col-span-2 md:col-span-1"
|
||||
>
|
||||
text danger with icon
|
||||
</Button>
|
||||
<Button
|
||||
icon="info"
|
||||
iconPosition="left"
|
||||
intent="info"
|
||||
variant="outline"
|
||||
className="col-span-2 md:col-span-1"
|
||||
>
|
||||
outline info with icon
|
||||
</Button>
|
||||
<p className="col-span-2">
|
||||
Or just use the icon prop with any of the button variants, style
|
||||
variants and colors.
|
||||
</p>
|
||||
<Button icon="newsletter" intent="tip" variant="inverted" />
|
||||
<Button icon="check" intent="success" />
|
||||
</div>
|
||||
<div className="overflow-auto">
|
||||
<h3 className="text-xl mt-5 mb-2 font-bold">Props Table</h3>
|
||||
<Table tableData={buttonPropsTableData} copyable={true} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const buttonPropsTableData = {
|
||||
headers: ["prop", "types", "default"],
|
||||
data: [
|
||||
{
|
||||
prop: "intent?",
|
||||
types: [
|
||||
"primary",
|
||||
"tip",
|
||||
"info",
|
||||
"success",
|
||||
"warning",
|
||||
"alert",
|
||||
"danger",
|
||||
"muted",
|
||||
"strong",
|
||||
],
|
||||
default: "default",
|
||||
},
|
||||
{
|
||||
prop: "variant?",
|
||||
types: [
|
||||
"default",
|
||||
"outline",
|
||||
"inverted",
|
||||
"ghost",
|
||||
"link",
|
||||
"secondary",
|
||||
"destructive",
|
||||
],
|
||||
default: "undefined",
|
||||
},
|
||||
{
|
||||
prop: "icon?",
|
||||
types: "Lucide icon name",
|
||||
default: "undefined",
|
||||
},
|
||||
{
|
||||
prop: "iconPosition?",
|
||||
types: ["left", "right"],
|
||||
default: "left",
|
||||
},
|
||||
{
|
||||
prop: "loading?",
|
||||
types: "boolean",
|
||||
default: "false",
|
||||
},
|
||||
{
|
||||
prop: "loadingText?",
|
||||
types: "string",
|
||||
default: "Loading...",
|
||||
},
|
||||
{
|
||||
prop: "disabled?",
|
||||
types: "boolean",
|
||||
default: "false",
|
||||
},
|
||||
{
|
||||
prop: "href?",
|
||||
types: "string",
|
||||
default: "undefined",
|
||||
},
|
||||
{
|
||||
prop: "newTab?",
|
||||
types: "boolean",
|
||||
default: "false",
|
||||
},
|
||||
{
|
||||
prop: "size?",
|
||||
types: ["sm", "md", "lg"],
|
||||
default: "md",
|
||||
},
|
||||
{
|
||||
prop: "className?",
|
||||
types: "string",
|
||||
default: "undefined",
|
||||
},
|
||||
{
|
||||
prop: "children?",
|
||||
types: "React.ReactNode",
|
||||
default: "undefined",
|
||||
},
|
||||
],
|
||||
};
|
||||
85
homepage/design-system/src/app/views/colors/page.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Colors() {
|
||||
return (
|
||||
<>
|
||||
<p className="p-1">
|
||||
Jazz uses a color palette which extends tailwind classes, with some
|
||||
modifications, see{" "}
|
||||
<Link
|
||||
href="https://tailwindcss.com/docs/colors#using-color-utilities"
|
||||
className="text-highlight"
|
||||
>
|
||||
Tailwind Color Utilities
|
||||
</Link>{" "}
|
||||
for more infomation on basic usage.
|
||||
</p>
|
||||
<p className="mt-1 p-1">
|
||||
Nearly all use cases are encapsulated by harnessing variables which have
|
||||
a baked in light & dark mode; meaning there are only a limited number of
|
||||
variables which are required for most development.
|
||||
<span className="italic">
|
||||
To see light/dark mode toggle your system settings.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<h3 id="color-variables" className="text-md mt-6 mb-1 font-bold">
|
||||
Color Variables
|
||||
</h3>
|
||||
|
||||
<p className="mb-2 p-1">
|
||||
The following variables are available and should be used as a preference
|
||||
to tailwind classes:
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 p-3">
|
||||
<div className="bg-primary text-white p-3 rounded-md">Primary</div>
|
||||
<div className="bg-highlight text-white p-3 rounded-md">Highlight</div>
|
||||
<div className="bg-tip text-white p-3 rounded-md">Tip</div>
|
||||
<div className="bg-info text-white p-3 rounded-md">Info</div>
|
||||
<div className="bg-success text-white p-3 rounded-md">Success</div>
|
||||
<div className="bg-warning text-white p-3 rounded-md">Warning</div>
|
||||
<div className="bg-alert text-white p-3 rounded-md">Alert</div>
|
||||
<div className="bg-danger text-white p-3 rounded-md">Danger</div>
|
||||
<div className="bg-muted text-white p-3 rounded-md">Muted</div>
|
||||
<div className="bg-strong text-white dark:text-stone-900 p-3 rounded-md">
|
||||
Strong
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs mt-1 mb-4">
|
||||
NB: These classes should be used across all apps as the primary an
|
||||
secondary colour are updated per app.
|
||||
<br />
|
||||
<br />
|
||||
For full custimisation, the default colours on tailwind are programmed
|
||||
to be the tailwind semantic colours, so you can achieve a transparent
|
||||
primary with `blue/20`.
|
||||
</p>
|
||||
|
||||
<h3 id="text-color-variables" className="text-md mt-6 font-bold">
|
||||
Text Color Variables
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 my-3 px-3">
|
||||
<ColorTypography />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ColorTypography = () => {
|
||||
return (
|
||||
<div className={clsx("text-default rounded-md")}>
|
||||
<div className="text-default mb-1">text-default</div>
|
||||
<div className="text-muted mb-1">text-muted</div>
|
||||
<div className="text-strong mb-1">text-strong</div>
|
||||
{/* <div>
|
||||
<span className="text-default bg-highlight">bg-highlight*</span>
|
||||
</div> */}
|
||||
<div className="text-strong my-1">
|
||||
<span className="bg-highlight">text-strong bg-highlight*</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
33
homepage/design-system/src/app/views/components/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { Switch } from "@/components/atoms/Switch";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function Components() {
|
||||
const [checked, setChecked] = useState({
|
||||
md: true,
|
||||
sm: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-3">
|
||||
<div className="pb-4 flex gap-6 flex-col md:flex-row">
|
||||
<h3 className="text-md font-semibold">Switches</h3>
|
||||
<Switch
|
||||
label="Switch default (md) (Primary)"
|
||||
id="switch-md"
|
||||
checked={checked.md}
|
||||
onChange={() => setChecked({ ...checked, md: !checked.md })}
|
||||
/>
|
||||
<Switch
|
||||
label="Switch (sm) success"
|
||||
id="switch-sm"
|
||||
checked={checked.sm}
|
||||
onChange={() => setChecked({ ...checked, sm: !checked.sm })}
|
||||
size="sm"
|
||||
intent="success"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
82
homepage/design-system/src/app/views/icons/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Icon } from "@/components/atoms/Icon";
|
||||
import { Table } from "@/components/molecules/Table";
|
||||
|
||||
export default function IconsView() {
|
||||
return (
|
||||
<div className="p-3">
|
||||
<div className="flex gap-2">
|
||||
<Icon name="search" size="xs" intent="primary" />
|
||||
<Icon name="zip" size="md" intent="info" />
|
||||
<Icon name="docs" size="lg" intent="success" />
|
||||
<Icon name="file" size="xl" intent="warning" />
|
||||
<Icon name="hash" size="2xl" intent="danger" />
|
||||
<Icon name="help" size="3xl" intent="alert" />
|
||||
<Icon name="image" size="4xl" intent="tip" />
|
||||
<Icon name="corecord" size="5xl" intent="default" />
|
||||
<Icon name="corecord" size="6xl" intent="muted" />
|
||||
<Icon name="corecord" size="7xl" intent="strong" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Icon name="search" size="xs" intent="primary" hasBackground />
|
||||
<Icon name="zip" size="md" intent="info" hasBackground />
|
||||
<Icon name="docs" size="lg" intent="success" hasBackground />
|
||||
<Icon name="file" size="xl" intent="warning" hasBackground />
|
||||
<Icon name="hash" size="2xl" intent="danger" hasBackground />
|
||||
<Icon name="help" size="3xl" intent="alert" hasBackground />
|
||||
<Icon name="image" size="4xl" intent="tip" hasBackground />
|
||||
<Icon name="corecord" size="5xl" intent="default" hasBackground />
|
||||
<Icon name="corecord" size="6xl" intent="muted" hasBackground />
|
||||
<Icon name="corecord" size="7xl" intent="strong" hasBackground />
|
||||
</div>
|
||||
<Table className="mt-6" tableData={iconPropsTable} copyable />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const iconPropsTable = {
|
||||
headers: ["prop", "types", "default"],
|
||||
data: [
|
||||
{
|
||||
prop: "name",
|
||||
types: "string",
|
||||
default: "undefined",
|
||||
},
|
||||
{
|
||||
prop: "icon",
|
||||
types: "LucideIcon",
|
||||
default: "undefined",
|
||||
},
|
||||
{
|
||||
prop: "size",
|
||||
types: ["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl"],
|
||||
default: "md",
|
||||
},
|
||||
{
|
||||
prop: "intent",
|
||||
types: [
|
||||
"default",
|
||||
"primary",
|
||||
"info",
|
||||
"success",
|
||||
"warning",
|
||||
"danger",
|
||||
"alert",
|
||||
"tip",
|
||||
"muted",
|
||||
"strong",
|
||||
"white",
|
||||
],
|
||||
default: "default",
|
||||
},
|
||||
{
|
||||
prop: "hasBackground",
|
||||
types: "boolean",
|
||||
default: "false",
|
||||
},
|
||||
{
|
||||
prop: "className",
|
||||
types: "string",
|
||||
default: "undefined",
|
||||
},
|
||||
],
|
||||
};
|
||||
146
homepage/design-system/src/app/views/inputs/page.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { Input } from "@/components/molecules/Input";
|
||||
import { InputWithButton } from "@/components/molecules/InputWithButton";
|
||||
import { Table } from "@/components/molecules/Table";
|
||||
import { NewsletterForm } from "@/components/organisms/NewsletterForm";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function InputsView() {
|
||||
const [checked, setChecked] = useState({
|
||||
md: true,
|
||||
sm: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-3">
|
||||
<p>
|
||||
Inputs consist of a combintion of atoms which can be used to create a
|
||||
variety of inputs. These atoms include:
|
||||
<br />
|
||||
<br />
|
||||
<code>Icon</code>, <code>Label</code> and <code>Button</code>, and also
|
||||
may be styled with the <code>variant</code> prop.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 mt-3">
|
||||
<h3 className="text-lg font-semibold my-2">Icons</h3>
|
||||
<Input
|
||||
icon="search"
|
||||
label="Search [label hidden]"
|
||||
iconPosition="left"
|
||||
placeholder="Search"
|
||||
labelHidden={true}
|
||||
/>
|
||||
<Input
|
||||
icon="check"
|
||||
label="Email"
|
||||
iconPosition="left"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<Input
|
||||
icon="file"
|
||||
label="Password"
|
||||
iconPosition="right"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<Input
|
||||
icon="eye"
|
||||
label="Password"
|
||||
iconPosition="right"
|
||||
labelPosition="row"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<h3 className="text-lg font-semibold my-2">Variants</h3>
|
||||
<Input label="Muted" placeholder="Muted" intent="muted" />
|
||||
<Input label="Strong" placeholder="Strong" intent="strong" />
|
||||
<Input label="Default" placeholder="Default" intent="default" />
|
||||
<h3 className="text-lg font-semibold my-2">Buttons</h3>
|
||||
<InputWithButton
|
||||
inputProps={{
|
||||
label: "Input with button [label visible]",
|
||||
labelHidden: false,
|
||||
placeholder: "Input with button",
|
||||
intent: "success",
|
||||
}}
|
||||
buttonProps={{
|
||||
children: "Let's go",
|
||||
intent: "success",
|
||||
variant: "inverted",
|
||||
icon: "check",
|
||||
iconPosition: "left",
|
||||
}}
|
||||
/>
|
||||
<InputWithButton
|
||||
inputProps={{
|
||||
label: "Input with button [label visible]",
|
||||
labelHidden: false,
|
||||
labelPosition: "row",
|
||||
placeholder: "Input with button",
|
||||
}}
|
||||
buttonProps={{
|
||||
children: "Learn more",
|
||||
intent: "tip",
|
||||
variant: "outline",
|
||||
icon: "corecord",
|
||||
iconPosition: "right",
|
||||
}}
|
||||
/>
|
||||
<NewsletterForm />
|
||||
</div>
|
||||
<Table className="mt-6" tableData={inputPropsTable} copyable />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const inputPropsTable = {
|
||||
headers: ["prop", "types", "default"],
|
||||
data: [
|
||||
{
|
||||
prop: "label",
|
||||
types: "string",
|
||||
default: "undefined",
|
||||
},
|
||||
{
|
||||
prop: "labelHidden?",
|
||||
types: "boolean",
|
||||
default: "false",
|
||||
},
|
||||
{
|
||||
prop: "labelPosition?",
|
||||
types: ["column", "row"],
|
||||
default: "column",
|
||||
},
|
||||
{
|
||||
prop: "icon?",
|
||||
types: ["LucideIcon"],
|
||||
default: "undefined",
|
||||
},
|
||||
{
|
||||
prop: "iconPosition?",
|
||||
types: ["left", "right"],
|
||||
default: "left",
|
||||
},
|
||||
{
|
||||
prop: "intent?",
|
||||
types: [
|
||||
"primary",
|
||||
"secondary",
|
||||
"info",
|
||||
"success",
|
||||
"warning",
|
||||
"danger",
|
||||
"alert",
|
||||
"tip",
|
||||
"muted",
|
||||
"strong",
|
||||
"default",
|
||||
],
|
||||
default: "default",
|
||||
},
|
||||
{
|
||||
prop: "buttonProps?",
|
||||
types: "see Button Props",
|
||||
default: "undefined",
|
||||
},
|
||||
],
|
||||
};
|
||||
16
homepage/design-system/src/app/views/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ViewsLayout } from "./ViewsLayout";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const viewName = pathname.split("/").pop();
|
||||
|
||||
return (
|
||||
<ViewsLayout>
|
||||
<h2 className="text-2xl font-bold capitalize my-3">{viewName}</h2>
|
||||
{children}
|
||||
</ViewsLayout>
|
||||
);
|
||||
}
|
||||
56
homepage/design-system/src/app/views/typography/page.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Prose } from "@/components/molecules/Prose";
|
||||
|
||||
export default function Typography() {
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
Heading 1
|
||||
<Prose className="p-3">
|
||||
<h1>Ship top-tier apps at high tempo</h1>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Heading 2
|
||||
<Prose className="p-3">
|
||||
<h2>Ship top-tier apps at high tempo</h2>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Heading 3
|
||||
<Prose className="p-3">
|
||||
<h3>Ship top-tier apps at high tempo</h3>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Heading 4
|
||||
<Prose className="p-3">
|
||||
<h4>Ship top-tier apps at high tempo</h4>
|
||||
</Prose>
|
||||
</div>
|
||||
<div>
|
||||
Paragraph
|
||||
<p className="text-xs text-highlight my-1">
|
||||
NB: That text can be styled with colour classes, including{" "}
|
||||
<code>text-muted</code> and <code>text-highlight</code>, see{" "}
|
||||
<a href="#text-color-variables">Text Color Variables</a>.
|
||||
</p>
|
||||
<Prose className="p-3">
|
||||
<p>
|
||||
<strong>Jazz is a framework for building local-first apps</strong> —
|
||||
an architecture that lets companies like Figma and Linear play in a
|
||||
league of their own.
|
||||
</p>
|
||||
|
||||
<p>Open source. Self-host or use Jazz Cloud for zero-config magic.</p>
|
||||
</Prose>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Code
|
||||
<Prose className="p-3">
|
||||
This is a one-line <code>piece of code</code>
|
||||
</Prose>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,42 @@
|
||||
import { clsx } from "clsx";
|
||||
import Link from "next/link";
|
||||
import { forwardRef } from "react";
|
||||
import {
|
||||
Style,
|
||||
Variant,
|
||||
VariantColor,
|
||||
colorToBgActiveMap25,
|
||||
colorToBgActiveMap50,
|
||||
colorToBgHoverMap10,
|
||||
colorToBgHoverMap30,
|
||||
colorToBgMap,
|
||||
shadowClassesBase,
|
||||
sizeClasses,
|
||||
styleToBgGradientColorMap,
|
||||
styleToBgGradientHoverMap,
|
||||
styleToBgTransparentActiveMap,
|
||||
styleToBorderMap,
|
||||
styleToButtonStateMap,
|
||||
styleToColorMap,
|
||||
styleToHoverShadowMap,
|
||||
styleToTextActiveMap,
|
||||
styleToTextHoverMap,
|
||||
styleToTextMap,
|
||||
} from "../../utils/tailwindClassesMap";
|
||||
import { Icon } from "./Icon";
|
||||
import type { IconName } from "./Icon";
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "tertiary" | "destructive" | "plain";
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
intent?: Style;
|
||||
variant?: Variant;
|
||||
state?: "hover" | "active" | "focus" | "disabled";
|
||||
size?: "sm" | "md" | "lg";
|
||||
href?: string;
|
||||
newTab?: boolean;
|
||||
icon?: IconName;
|
||||
iconPosition?: "left" | "right" | "center";
|
||||
loading?: boolean;
|
||||
loadingText?: string;
|
||||
children?: React.ReactNode;
|
||||
@@ -18,63 +44,44 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function ButtonIcon({ icon, loading }: ButtonProps) {
|
||||
if (!Icon) return null;
|
||||
|
||||
const className = "size-5";
|
||||
|
||||
if (loading) return <Spinner className={className} />;
|
||||
|
||||
if (icon) {
|
||||
return <Icon name={icon} className={className} />;
|
||||
}
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
size = "md",
|
||||
variant = "primary",
|
||||
intent = "default",
|
||||
variant,
|
||||
href,
|
||||
disabled,
|
||||
newTab,
|
||||
loading,
|
||||
loadingText,
|
||||
icon,
|
||||
iconPosition = "left",
|
||||
type = "button",
|
||||
...buttonProps
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const sizeClasses = {
|
||||
sm: "text-sm py-1 px-2",
|
||||
md: "py-1.5 px-3",
|
||||
lg: "md:text-lg py-2 px-3 md:px-8 md:py-3",
|
||||
const styleClass =
|
||||
styleClasses(intent, variant)[variant as keyof typeof styleClasses] || "";
|
||||
|
||||
const getClasses = ({ variant }: { variant: string | undefined }) => {
|
||||
return {
|
||||
[sizeClasses[size as keyof typeof sizeClasses]]: size,
|
||||
[variantClass(intent)]: !variant,
|
||||
[styleClass]: variant,
|
||||
};
|
||||
};
|
||||
|
||||
const variantClasses = {
|
||||
primary:
|
||||
"bg-primary border border-primary text-white font-medium hover:bg-highlight hover:border-primary hover:text-primary dark:hover:bg-highlight dark:hover:text-primary",
|
||||
secondary:
|
||||
"text-stone-900 border font-medium hover:border-primary hover:text-primary hover:bg-highlight hover:dark:border-primary dark:text-white dark:hover:text-primary",
|
||||
tertiary: "text-primary underline underline-offset-4",
|
||||
destructive:
|
||||
"bg-red-600 border-red-600 text-white font-medium hover:bg-red-700 hover:border-red-700",
|
||||
};
|
||||
|
||||
const classNames =
|
||||
variant === "plain"
|
||||
? className
|
||||
: clsx(
|
||||
className,
|
||||
"inline-flex items-center justify-center gap-2 rounded-lg text-center transition-colors",
|
||||
"disabled:pointer-events-none disabled:opacity-70",
|
||||
sizeClasses[size],
|
||||
variantClasses[variant],
|
||||
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
|
||||
);
|
||||
const classNames = clsx(
|
||||
"inline-flex items-center justify-center gap-2 rounded-lg text-center transition-colors w-fit text-nowrap",
|
||||
getClasses({ variant }),
|
||||
"disabled:pointer-events-none disabled:opacity-70",
|
||||
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
|
||||
className,
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
@@ -83,10 +90,17 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
target={newTab ? "_blank" : undefined}
|
||||
className={classNames}
|
||||
>
|
||||
<ButtonIcon icon={icon} loading={loading} />
|
||||
{icon && (
|
||||
<Icon
|
||||
name={icon}
|
||||
className={`size-5 ${iconPosition === "left" ? "mr-2" : iconPosition === "right" ? "ml-2" : ""}, ${iconVariant(intent, variant)}`}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
{newTab ? (
|
||||
<span className="inline-block text-muted relative -top-0.5 -left-2 -mr-2">
|
||||
<span
|
||||
className={`inline-block relative -top-0.5 -left-2 -mr-2 ${styleToTextMap[intent as keyof typeof styleToTextMap]}`}
|
||||
>
|
||||
⌝
|
||||
</span>
|
||||
) : (
|
||||
@@ -104,10 +118,49 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
className={classNames}
|
||||
type={type}
|
||||
>
|
||||
<ButtonIcon icon={icon} loading={loading} />
|
||||
|
||||
{loading ? (
|
||||
<Spinner className="size-5" />
|
||||
) : (
|
||||
icon &&
|
||||
iconPosition === "left" && (
|
||||
<Icon name={icon} intent={iconVariant(intent, variant)} />
|
||||
)
|
||||
)}
|
||||
{loading && loadingText ? loadingText : children}
|
||||
{icon && iconPosition === "right" && (
|
||||
<Icon
|
||||
name={icon}
|
||||
intent={iconVariant(intent, variant)}
|
||||
hasHover={true}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const iconVariant = (intent: Style, variant: Variant | undefined) => {
|
||||
return variant ? intent : intent === "default" ? "default" : "white";
|
||||
};
|
||||
const textColorVariant = (style: Style) => {
|
||||
return style === "default"
|
||||
? "text-stone-700 dark:text-white hover:text-stone-800 active:text-stone-700 dark:hover:text-stone-100 dark:active:text-stone-200"
|
||||
: style === "strong"
|
||||
? "text-stone-100 dark:text-stone-900"
|
||||
: "text-white";
|
||||
};
|
||||
|
||||
const variantClass = (intent: Style) =>
|
||||
`${styleToBgGradientColorMap[intent]} ${styleToBgGradientHoverMap[intent]} ${textColorVariant(intent)} ${styleToButtonStateMap[intent]} ${shadowClassesBase} shadow-stone-400/20`;
|
||||
|
||||
const styleClasses = (intent: Style, variant: Variant | undefined) => {
|
||||
return {
|
||||
outline: `border ${styleToBorderMap[intent]} ${styleToTextMap[intent]} ${styleToTextHoverMap[intent]} ${styleToHoverShadowMap[intent]} ${styleToBgTransparentActiveMap[intent]} shadow-[5px_0px]`,
|
||||
inverted: `${styleToTextMap[intent]} ${colorToBgHoverMap30[styleToColorMap[intent] as VariantColor]} ${colorToBgMap[styleToColorMap[intent] as VariantColor]} ${colorToBgActiveMap50[styleToColorMap[intent] as VariantColor]} ${shadowClassesBase}`,
|
||||
ghost: `bg-transparent ${styleToTextMap[intent]} ${colorToBgHoverMap10[styleToColorMap[intent] as VariantColor]} ${colorToBgActiveMap25[styleToColorMap[intent] as VariantColor]}`,
|
||||
link: `bg-transparent ${styleToTextMap[intent]} underline underline-offset-2 p-0 hover:bg-transparent ${styleToTextHoverMap[intent]} ${styleToTextActiveMap[intent]} active:underline-stone-500`,
|
||||
secondary: `bg-stone-300 ${styleToTextMap[intent]} hover:bg-stone-400/80 active:bg-stone-500/80`,
|
||||
destructive: `bg-danger text-white hover:bg-red/80 active:bg-red/70`,
|
||||
default: `${styleToBgGradientColorMap["default"]} ${styleToBgGradientHoverMap["default"]} ${textColorVariant("default")} ${styleToButtonStateMap["default"]} ${shadowClassesBase} shadow-stone-400/20`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ChevronRightIcon,
|
||||
ClipboardIcon,
|
||||
CodeIcon,
|
||||
Eye,
|
||||
FileLock2Icon,
|
||||
FileTextIcon,
|
||||
FingerprintIcon,
|
||||
@@ -46,9 +47,15 @@ import {
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
Style,
|
||||
styleToTextHoverMap,
|
||||
styleToTextMap,
|
||||
} from "../../utils/tailwindClassesMap";
|
||||
import { GcmpIcons } from "./icons";
|
||||
|
||||
const icons = {
|
||||
export const icons = {
|
||||
arrowDown: ArrowDownIcon,
|
||||
arrowRight: ArrowRightIcon,
|
||||
auth: UserIcon,
|
||||
@@ -100,6 +107,7 @@ const icons = {
|
||||
// text editor icons
|
||||
bold: BoldIcon,
|
||||
italic: ItalicIcon,
|
||||
eye: Eye,
|
||||
};
|
||||
|
||||
// copied from tailwind line height https://tailwindcss.com/docs/font-size
|
||||
@@ -107,8 +115,8 @@ const sizes = {
|
||||
"2xs": 14,
|
||||
xs: 16,
|
||||
sm: 20,
|
||||
md: 24,
|
||||
lg: 28,
|
||||
md: 22,
|
||||
lg: 26,
|
||||
xl: 28,
|
||||
"2xl": 32,
|
||||
"3xl": 36,
|
||||
@@ -143,13 +151,19 @@ export function Icon({
|
||||
name,
|
||||
icon,
|
||||
size = "md",
|
||||
intent = "default",
|
||||
hasBackground = false,
|
||||
className,
|
||||
hasHover = false,
|
||||
...svgProps
|
||||
}: {
|
||||
name?: IconName;
|
||||
icon?: LucideIcon;
|
||||
size?: keyof typeof sizes;
|
||||
intent?: Style | "white";
|
||||
hasBackground?: boolean;
|
||||
className?: string;
|
||||
hasHover?: boolean;
|
||||
} & React.SVGProps<SVGSVGElement>) {
|
||||
if (!icon && (!name || !icons.hasOwnProperty(name))) {
|
||||
throw new Error(`Icon not found: ${name}`);
|
||||
@@ -158,13 +172,52 @@ export function Icon({
|
||||
// @ts-ignore
|
||||
const IconComponent = icons?.hasOwnProperty(name) ? icons[name] : icon;
|
||||
|
||||
const iconClass = {
|
||||
...styleToTextMap,
|
||||
white: "text-white",
|
||||
};
|
||||
|
||||
const iconHoverClass = {
|
||||
...styleToTextHoverMap,
|
||||
white: "hover:text-white/90",
|
||||
};
|
||||
|
||||
const backgroundClasses = {
|
||||
default: "bg-stone-200/30 dark:bg-stone-900/30",
|
||||
primary: "bg-primary-transparent",
|
||||
secondary: "bg-secondary-transparent",
|
||||
info: "bg-info-transparent",
|
||||
success: "bg-success-transparent",
|
||||
warning: "bg-warning-transparent",
|
||||
danger: "bg-danger-transparent",
|
||||
alert: "bg-alert-transparent",
|
||||
tip: "bg-tip-transparent",
|
||||
muted: "bg-stone-300/30 dark:bg-stone-700/30",
|
||||
strong: "bg-stone-900/30 dark:bg-stone-100/30",
|
||||
};
|
||||
|
||||
const roundedClasses = {
|
||||
xs: "rounded-xs",
|
||||
sm: "rounded-sm",
|
||||
md: "rounded-md",
|
||||
lg: "rounded-lg",
|
||||
xl: "rounded-xl",
|
||||
};
|
||||
|
||||
return (
|
||||
<IconComponent
|
||||
aria-hidden="true"
|
||||
size={sizes[size]}
|
||||
strokeWidth={strokeWidths[size]}
|
||||
strokeLinecap="round"
|
||||
className={className}
|
||||
className={clsx(
|
||||
roundedClasses[size as keyof typeof roundedClasses] || "rounded-lg",
|
||||
iconClass[intent as keyof typeof iconClass],
|
||||
hasBackground &&
|
||||
backgroundClasses[intent as keyof typeof backgroundClasses],
|
||||
hasHover && iconHoverClass[intent as keyof typeof iconHoverClass],
|
||||
className,
|
||||
)}
|
||||
{...svgProps}
|
||||
/>
|
||||
);
|
||||
|
||||
13
homepage/design-system/src/components/atoms/Label.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Label as LabelRadix } from "radix-ui";
|
||||
|
||||
export function Label({
|
||||
label,
|
||||
htmlFor,
|
||||
className,
|
||||
}: { label: string; htmlFor: string; className?: string }) {
|
||||
return (
|
||||
<LabelRadix.Root className={className} htmlFor={htmlFor}>
|
||||
{label}
|
||||
</LabelRadix.Root>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export function Spinner({ className }: { className?: string }) {
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import clsx from "clsx";
|
||||
import { Switch as RadixSwitch } from "radix-ui";
|
||||
import { Style, styleToBgMap } from "../../utils/tailwindClassesMap";
|
||||
|
||||
export function Switch({
|
||||
id,
|
||||
size = "sm",
|
||||
size = "md",
|
||||
intent = "primary",
|
||||
checked,
|
||||
onChange,
|
||||
label,
|
||||
}: {
|
||||
id: string;
|
||||
size?: "sm" | "md";
|
||||
intent?: Style;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
label?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-content">
|
||||
<div className="flex items-center w-content">
|
||||
<label
|
||||
className={clsx("text-xs text-gray-500", labelSizeClass[size])}
|
||||
className={clsx("text-gray-500 mr-2", labelSizeClass[size])}
|
||||
htmlFor={id}
|
||||
>
|
||||
{label}
|
||||
@@ -25,18 +28,21 @@ export function Switch({
|
||||
<RadixSwitch.Root
|
||||
id={id}
|
||||
className={clsx(
|
||||
"min-w-10 h-6 rounded-full relative",
|
||||
size === "sm" && "min-w-6 h-3.5",
|
||||
checked ? "bg-primary" : "bg-stone-200",
|
||||
"rounded-full relative",
|
||||
size === "sm" ? "min-w-6 h-4" : "min-w-10 h-6",
|
||||
checked ? styleToBgMap[intent] : "bg-stone-200",
|
||||
)}
|
||||
checked={checked}
|
||||
onCheckedChange={onChange}
|
||||
>
|
||||
<RadixSwitch.Thumb
|
||||
className={clsx(
|
||||
"block w-4 h-4 bg-white rounded-full transition-transform duration-300 translate-x-0 ml-[0.06rem]",
|
||||
size === "sm" && "w-3 h-3",
|
||||
checked && "translate-x-[0.6rem]",
|
||||
"block bg-white rounded-full transition-transform duration-300 translate-x-0 ml-[0.1em]",
|
||||
size === "sm" ? "w-3 h-3" : "w-5 h-5",
|
||||
checked &&
|
||||
(size === "sm"
|
||||
? "translate-x-[0.5rem]"
|
||||
: "translate-x-[1.01rem]"),
|
||||
)}
|
||||
/>
|
||||
</RadixSwitch.Root>
|
||||
|
||||
@@ -21,7 +21,9 @@ export function FeatureCard({
|
||||
{icon && (
|
||||
<Icon
|
||||
name={icon}
|
||||
className="text-primary p-1.5 rounded-lg bg-blue-50 dark:bg-stone-900 mb-2.5"
|
||||
intent="primary"
|
||||
hasBackground
|
||||
className="p-1.5 rounded-lg mb-2.5"
|
||||
size="3xl"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,16 +1,47 @@
|
||||
import { clsx } from "clsx";
|
||||
import { forwardRef, useId } from "react";
|
||||
import { Style, styleToActiveBorderMap } from "../../utils/tailwindClassesMap";
|
||||
import { Button, ButtonProps } from "../atoms/Button";
|
||||
import { Icon, icons } from "../atoms/Icon";
|
||||
import { Label } from "../atoms/Label";
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
// label can be hidden with a "label:sr-only" className
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
className?: string;
|
||||
id?: string;
|
||||
placeholder?: string;
|
||||
icon?: keyof typeof icons;
|
||||
iconPosition?: "left" | "right";
|
||||
labelHidden?: boolean;
|
||||
labelPosition?: "column" | "row";
|
||||
button?: ButtonProps;
|
||||
intent?: Style;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, className, id: customId, ...inputProps }, ref) => {
|
||||
(
|
||||
{
|
||||
label,
|
||||
className,
|
||||
id: customId,
|
||||
placeholder,
|
||||
icon,
|
||||
iconPosition = "left",
|
||||
labelHidden,
|
||||
labelPosition,
|
||||
button,
|
||||
intent = "default",
|
||||
...inputProps
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const generatedId = useId();
|
||||
const id = customId || generatedId;
|
||||
const inputIconClassName =
|
||||
icon && iconPosition === "left"
|
||||
? "pl-9"
|
||||
: icon && iconPosition === "right";
|
||||
|
||||
const inputClassName = clsx(
|
||||
"w-full rounded-md border px-3.5 py-2 shadow-sm",
|
||||
@@ -18,15 +49,53 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
"dark:text-white dark:bg-stone-925",
|
||||
);
|
||||
|
||||
const containerClassName = clsx("grid gap-1", className);
|
||||
|
||||
return (
|
||||
<div className={containerClassName}>
|
||||
<label htmlFor={id} className="text-stone-600 dark:text-stone-300">
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<input ref={ref} {...inputProps} id={id} className={inputClassName} />
|
||||
<div
|
||||
className={clsx(
|
||||
"relative w-full",
|
||||
labelPosition === "row" ? "flex flex-row items-center" : "",
|
||||
)}
|
||||
>
|
||||
<Label
|
||||
label={label}
|
||||
htmlFor={id}
|
||||
className={clsx(
|
||||
labelPosition === "row" ? "mr-2" : "w-full",
|
||||
labelHidden ? "sr-only" : "",
|
||||
)}
|
||||
/>
|
||||
<div className={clsx("flex gap-2 w-full items-center")}>
|
||||
<input
|
||||
ref={ref}
|
||||
{...inputProps}
|
||||
id={id}
|
||||
className={clsx(
|
||||
inputClassName,
|
||||
inputIconClassName,
|
||||
className,
|
||||
"px-2",
|
||||
styleToActiveBorderMap[
|
||||
intent as keyof typeof styleToActiveBorderMap
|
||||
],
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{icon && (
|
||||
<Icon
|
||||
name={icon}
|
||||
className={clsx(
|
||||
"absolute",
|
||||
iconPosition === "left"
|
||||
? "left-2"
|
||||
: iconPosition === "right"
|
||||
? "right-2"
|
||||
: "",
|
||||
)}
|
||||
intent={intent}
|
||||
/>
|
||||
)}
|
||||
{button && <Button {...button} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ButtonProps } from "../atoms/Button";
|
||||
import { Input, InputProps } from "./Input";
|
||||
|
||||
export function InputWithButton({
|
||||
inputProps,
|
||||
buttonProps,
|
||||
}: {
|
||||
inputProps: InputProps;
|
||||
buttonProps: ButtonProps;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex gap-2 w-full">
|
||||
<Input {...inputProps} button={buttonProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
110
homepage/design-system/src/components/molecules/Table.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
import { clsx } from "clsx";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
export interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
|
||||
className?: string;
|
||||
tableData: {
|
||||
headers: string[];
|
||||
data: {
|
||||
[key: string]: string | string[];
|
||||
}[];
|
||||
};
|
||||
copyable?: boolean;
|
||||
}
|
||||
|
||||
export function Table({
|
||||
className,
|
||||
tableData,
|
||||
copyable,
|
||||
...tableProps
|
||||
}: TableProps) {
|
||||
return (
|
||||
<table
|
||||
className={clsx(
|
||||
"w-full border border-gray-200 rounded-lg overflow-hidden overflow-x-scroll",
|
||||
className,
|
||||
)}
|
||||
{...tableProps}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
{tableData.headers.map((header) => (
|
||||
<th key={header} className="text-left pl-1 capitalize">
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="border-t border-gray-200">
|
||||
{tableData.data.map((row, index) => (
|
||||
<tr
|
||||
key={`${row.id as string}-${index}=${tableData.headers.join("-")}`}
|
||||
className={clsx(
|
||||
index % 2 === 0
|
||||
? "bg-stone-200/20 dark:bg-stone-800/40 hover:bg-stone-200/70 dark:hover:bg-stone-800/90"
|
||||
: "hover:bg-stone-200/50 dark:hover:bg-stone-100/20",
|
||||
"border-b border-stone-200 text-stone-800 hover:text-black dark:text-stone-200 dark:hover:text-white",
|
||||
)}
|
||||
>
|
||||
{tableData.headers.map((header, index) => (
|
||||
<td
|
||||
key={header}
|
||||
className={clsx(
|
||||
index === 0 && "pl-1",
|
||||
typeof row[header] !== "string" && "flex",
|
||||
)}
|
||||
>
|
||||
{typeof row[header] !== "string" ? (
|
||||
<TableDataContainer>
|
||||
{row[header]?.map((item) => (
|
||||
<div
|
||||
className={clsx(
|
||||
"hover:underline",
|
||||
copyable && "cursor-pointer",
|
||||
)}
|
||||
key={item}
|
||||
onClick={() => {
|
||||
if (copyable) {
|
||||
navigator.clipboard.writeText(item.toString());
|
||||
toast.success("Copied to clipboard");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</TableDataContainer>
|
||||
) : (
|
||||
<TableDataContainer isCopyable={copyable}>
|
||||
{row[header as keyof typeof row]}
|
||||
</TableDataContainer>
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
const TableDataContainer = ({
|
||||
children,
|
||||
className,
|
||||
isCopyable,
|
||||
}: { children: React.ReactNode; className?: string; isCopyable?: boolean }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx("flex gap-2", className, isCopyable && "cursor-pointer")}
|
||||
onClick={() => {
|
||||
if (isCopyable && children) {
|
||||
navigator.clipboard.writeText(children.toString());
|
||||
toast.success("Copied to clipboard");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -170,6 +170,7 @@ export function MobileNav({
|
||||
{item.title}
|
||||
</NavLink>
|
||||
))}
|
||||
{cta}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
@@ -200,7 +201,6 @@ export function MobileNav({
|
||||
<NavLinkLogo prominent href="/" className="mr-auto">
|
||||
{mainLogo}
|
||||
</NavLinkLogo>
|
||||
{cta}
|
||||
<button
|
||||
className="flex gap-2 p-3 rounded-xl items-center"
|
||||
onClick={() => {
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
import { useState } from "react";
|
||||
import { ErrorResponse } from "resend";
|
||||
import { subscribe } from "../../actions/resend";
|
||||
import { Button } from "../atoms/Button";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
import { Input } from "../molecules/Input";
|
||||
import { InputWithButton } from "../molecules/InputWithButton";
|
||||
|
||||
export function NewsletterForm() {
|
||||
const [email, setEmail] = useState("");
|
||||
// const [subscribed, setSubscribed] = useState(false);
|
||||
const [error, setError] = useState<ErrorResponse | undefined>();
|
||||
|
||||
const [state, setState] = useState<"ready" | "loading" | "success" | "error">(
|
||||
@@ -41,32 +39,37 @@ export function NewsletterForm() {
|
||||
}
|
||||
|
||||
if (state === "error" && error?.message) {
|
||||
return <p className="text-red-700">Error: {error.message}</p>;
|
||||
return <p className="text-danger">Error: {error.message}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<form action="" onSubmit={submit} className="flex gap-x-4 w-120 max-w-md">
|
||||
<Input
|
||||
id="email-address"
|
||||
name="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
className="flex-1 label:sr-only"
|
||||
label="Email address"
|
||||
<InputWithButton
|
||||
inputProps={{
|
||||
id: "email-address",
|
||||
name: "email",
|
||||
type: "email",
|
||||
value: email,
|
||||
onChange: (e) => setEmail(e.target.value),
|
||||
required: true,
|
||||
placeholder: "Enter your email",
|
||||
autoComplete: "email",
|
||||
className: "flex-1",
|
||||
label: "Email address",
|
||||
labelHidden: true,
|
||||
intent: "primary",
|
||||
}}
|
||||
buttonProps={{
|
||||
type: "submit",
|
||||
intent: "primary",
|
||||
variant: "outline",
|
||||
loadingText: "Subscribing...",
|
||||
loading: state === "loading",
|
||||
icon: "newsletter",
|
||||
iconPosition: "right",
|
||||
children: "Subscribe",
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
loadingText="Subscribing..."
|
||||
loading={state === "loading"}
|
||||
icon="newsletter"
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
function ThemeWatcher() {
|
||||
let { resolvedTheme, setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
let media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
function onMediaChange() {
|
||||
let systemTheme = media.matches ? "dark" : "light";
|
||||
if (resolvedTheme === systemTheme) {
|
||||
setTheme("system");
|
||||
}
|
||||
}
|
||||
|
||||
onMediaChange();
|
||||
media.addEventListener("change", onMediaChange);
|
||||
|
||||
return () => {
|
||||
media.removeEventListener("change", onMediaChange);
|
||||
};
|
||||
}, [resolvedTheme, setTheme]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return (
|
||||
<NextThemesProvider {...props}>
|
||||
<ThemeWatcher />
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
13
homepage/design-system/src/covalues/schema.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
// Example CoMap class
|
||||
export const Person = co.map({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
height: z.number().optional(),
|
||||
weight: z.number().optional(),
|
||||
});
|
||||
|
||||
export const ListOfPeople = co.list(Person);
|
||||
|
||||
export const PersonFeed = co.feed(Person);
|
||||
308
homepage/design-system/src/utils/tailwindClassesMap.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
export type Variant =
|
||||
| "default"
|
||||
| "secondary"
|
||||
| "destructive"
|
||||
| "ghost"
|
||||
| "outline"
|
||||
| "link"
|
||||
| "inverted";
|
||||
|
||||
export type Style =
|
||||
| "default"
|
||||
| "primary"
|
||||
| "tip"
|
||||
| "info"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "alert"
|
||||
| "danger"
|
||||
| "muted"
|
||||
| "strong";
|
||||
|
||||
export const sizeClasses = {
|
||||
sm: "text-sm py-1 px-2",
|
||||
md: "py-1.5 px-3",
|
||||
lg: "md:text-lg py-2 px-3 md:px-8 md:py-3",
|
||||
};
|
||||
|
||||
export const styleToBorderMap = {
|
||||
primary: "border-primary",
|
||||
info: "border-info",
|
||||
success: "border-success",
|
||||
warning: "border-warning",
|
||||
danger: "border-danger",
|
||||
alert: "border-alert",
|
||||
tip: "border-tip",
|
||||
muted: "border-stone-200 dark:border-stone-700",
|
||||
strong: "border-stone-900 dark:border-stone-100",
|
||||
default: "border-stone-600 dark:border-stone-200",
|
||||
};
|
||||
|
||||
export const styleToActiveBorderMap = {
|
||||
primary: "active:border-primary-transparent focus:border-primary-transparent",
|
||||
info: "active:border-info-transparent focus:border-info-transparent",
|
||||
success: "active:border-success-transparent focus:border-success-transparent",
|
||||
warning: "active:border-warning-transparent focus:border-warning-transparent",
|
||||
danger: "active:border-danger-transparent focus:border-danger-transparent",
|
||||
alert: "active:border-alert-transparent focus:border-alert-transparent",
|
||||
tip: "active:border-tip-transparent focus:border-tip-transparent",
|
||||
muted:
|
||||
"active:border-stone-200/30 focus:border-stone-200/30 dark:active:border-stone-900/30 dark:focus:border-stone-900/30",
|
||||
strong:
|
||||
"active:border-stone-900/30 focus:border-stone-900/30 dark:active:border-stone-200/30 dark:focus:border-stone-200/30",
|
||||
default:
|
||||
"active:border-stone-600/30 dark:active:border-stone-100/30 focus:border-stone-600/30 dark:focus:border-stone-100/30",
|
||||
};
|
||||
|
||||
export const styleToBgMap = {
|
||||
primary: "bg-primary",
|
||||
info: "bg-info",
|
||||
success: "bg-success",
|
||||
warning: "bg-warning",
|
||||
danger: "bg-danger",
|
||||
alert: "bg-alert",
|
||||
tip: "bg-tip",
|
||||
muted: "bg-stone-200 dark:bg-stone-900",
|
||||
strong: "bg-stone-900 dark:bg-stone-200",
|
||||
default: "bg-stone-700 dark:bg-stone-100",
|
||||
};
|
||||
|
||||
export const styleToBgTransparentHoverMap = {
|
||||
primary: "hover:bg-primary-transparent",
|
||||
info: "hover:bg-info-transparent",
|
||||
success: "hover:bg-success-transparent",
|
||||
warning: "hover:bg-warning-transparent",
|
||||
danger: "hover:bg-danger-transparent",
|
||||
alert: "hover:bg-alert-transparent",
|
||||
tip: "hover:bg-tip-transparent",
|
||||
muted: "hover:bg-stone-100/20 dark:hover:bg-stone-900/20",
|
||||
strong: "hover:bg-stone-900/20 dark:hover:bg-stone-100/20",
|
||||
default: "hover:bg-stone-600/20 dark:hover:bg-stone-100/20",
|
||||
};
|
||||
|
||||
export const styleToBgTransparentActiveMap = {
|
||||
primary: "active:bg-blue/20",
|
||||
info: "active:bg-purple/20",
|
||||
success: "active:bg-green/20",
|
||||
warning: "active:bg-orange/20",
|
||||
danger: "active:bg-red/20",
|
||||
alert: "active:bg-yellow/20",
|
||||
tip: "active:bg-cyan/20",
|
||||
muted: "active:bg-stone-400/20",
|
||||
strong: "active:bg-stone-900/20",
|
||||
default: "active:bg-stone-600/20 dark:active:bg-stone-100/20",
|
||||
};
|
||||
|
||||
export const styleToTextMap = {
|
||||
primary: "text-primary",
|
||||
info: "text-info",
|
||||
success: "text-success",
|
||||
warning: "text-warning",
|
||||
danger: "text-danger",
|
||||
alert: "text-alert",
|
||||
tip: "text-tip",
|
||||
muted: "text-stone-500 dark:text-stone-400",
|
||||
strong: "text-stone-900 dark:text-white",
|
||||
default: "text-stone-700 dark:text-stone-100",
|
||||
};
|
||||
|
||||
export const styleToTextHoverMap = {
|
||||
primary: "hover:text-primary-light",
|
||||
info: "hover:text-info-light",
|
||||
success: "hover:text-success-light",
|
||||
warning: "hover:text-warning-light",
|
||||
danger: "hover:text-danger-light",
|
||||
alert: "hover:text-alert-light",
|
||||
tip: "hover:text-tip-light",
|
||||
muted: "hover:text-stone-400 dark:hover:text-stone-500",
|
||||
strong: "hover:text-stone-700 dark:hover:text-stone-300",
|
||||
default: "hover:text-stone-600 dark:hover:text-stone-200",
|
||||
};
|
||||
|
||||
export const styleToTextActiveMap = {
|
||||
primary: "active:text-primary-dark",
|
||||
info: "active:text-info-dark",
|
||||
success: "active:text-success-dark",
|
||||
warning: "active:text-warning-dark",
|
||||
danger: "active:text-danger-dark",
|
||||
alert: "active:text-alert-dark",
|
||||
tip: "active:text-tip-dark",
|
||||
muted: "active:text-stone-400 dark:active:text-stone-500",
|
||||
strong: "active:text-stone-700 dark:active:text-stone-300",
|
||||
default: "active:text-stone-800 dark:active:text-stone-400",
|
||||
};
|
||||
|
||||
export type VariantColor =
|
||||
| "blue"
|
||||
| "indigo"
|
||||
| "purple"
|
||||
| "green"
|
||||
| "orange"
|
||||
| "red"
|
||||
| "yellow"
|
||||
| "cyan"
|
||||
| "muted"
|
||||
| "strong"
|
||||
| "default";
|
||||
|
||||
export const styleToColorMap = {
|
||||
primary: "blue",
|
||||
info: "purple",
|
||||
success: "green",
|
||||
warning: "orange",
|
||||
danger: "red",
|
||||
alert: "yellow",
|
||||
tip: "cyan",
|
||||
muted: "muted",
|
||||
strong: "strong",
|
||||
default: "default",
|
||||
};
|
||||
|
||||
export const colorToBgMap = {
|
||||
blue: "bg-blue/20",
|
||||
indigo: "bg-indigo-500/20",
|
||||
purple: "bg-purple/20",
|
||||
green: "bg-green/20",
|
||||
orange: "bg-orange/20",
|
||||
red: "bg-red/20",
|
||||
yellow: "bg-yellow/20",
|
||||
cyan: "bg-cyan/20",
|
||||
muted: "bg-stone-200/20 dark:bg-stone-900/50",
|
||||
strong: "bg-stone-900/20 dark:bg-stone-100/50",
|
||||
default: "bg-stone-600/20 dark:bg-white/20",
|
||||
};
|
||||
|
||||
export const colorToBgHoverMap30 = {
|
||||
blue: "hover:bg-blue/30",
|
||||
indigo: "hover:bg-indigo-500/30",
|
||||
purple: "hover:bg-purple/30",
|
||||
green: "hover:bg-green/30",
|
||||
orange: "hover:bg-orange/30",
|
||||
red: "hover:bg-red/30",
|
||||
yellow: "hover:bg-yellow/30",
|
||||
cyan: "hover:bg-cyan/30",
|
||||
muted: "hover:bg-stone-200/30 dark:hover:bg-stone-900/30",
|
||||
strong: "hover:bg-stone-900/30 dark:hover:bg-stone-100/30",
|
||||
default: "hover:bg-stone-600/30 dark:hover:bg-white/30",
|
||||
};
|
||||
|
||||
export const colorToBgHoverMap10 = {
|
||||
blue: "hover:bg-blue/10",
|
||||
indigo: "hover:bg-indigo-500/10",
|
||||
purple: "hover:bg-purple/10",
|
||||
green: "hover:bg-green/10",
|
||||
orange: "hover:bg-orange/10",
|
||||
red: "hover:bg-red/10",
|
||||
yellow: "hover:bg-yellow/10",
|
||||
cyan: "hover:bg-cyan/10",
|
||||
muted: "hover:bg-stone-200/30 dark:hover:bg-stone-800/30",
|
||||
strong: "hover:bg-stone-900/10 dark:hover:bg-stone-100/10",
|
||||
default: "hover:bg-stone-600/10 dark:hover:bg-white/10",
|
||||
};
|
||||
|
||||
export const colorToBgActiveMap50 = {
|
||||
blue: "active:bg-blue/50",
|
||||
indigo: "active:bg-indigo-500/50",
|
||||
purple: "active:bg-purple/50",
|
||||
green: "active:bg-green/50",
|
||||
orange: "active:bg-orange/50",
|
||||
red: "active:bg-red/50",
|
||||
yellow: "active:bg-yellow/50",
|
||||
cyan: "active:bg-cyan/50",
|
||||
muted: "active:bg-stone-100/50 dark:active:bg-stone-900/50",
|
||||
strong: "active:bg-stone-800/40 dark:active:bg-stone-200/40",
|
||||
default: "active:bg-stone-900/40 dark:active:bg-white/50",
|
||||
};
|
||||
|
||||
export const colorToBgActiveMap25 = {
|
||||
blue: "active:bg-blue/25",
|
||||
indigo: "active:bg-indigo-500/25",
|
||||
purple: "active:bg-purple/25",
|
||||
green: "active:bg-green/25",
|
||||
orange: "active:bg-orange/25",
|
||||
red: "active:bg-red/25",
|
||||
yellow: "active:bg-yellow/25",
|
||||
cyan: "active:bg-cyan/25",
|
||||
muted: "active:bg-stone-100/25 dark:active:bg-stone-900/25",
|
||||
strong: "active:bg-stone-900/25 dark:active:bg-stone-100/25",
|
||||
default: "active:bg-black/25 dark:active:bg-white/25",
|
||||
};
|
||||
|
||||
const gradiantClassesBase = "bg-gradient-to-t from-7% via-50% to-95%";
|
||||
|
||||
export const styleToBgGradientColorMap = {
|
||||
primary: `from-primary-dark via-primary to-primary-light ${gradiantClassesBase}`,
|
||||
info: `from-info-dark via-info to-info-light ${gradiantClassesBase}`,
|
||||
success: `from-success-dark via-success to-success-light ${gradiantClassesBase}`,
|
||||
warning: `from-warning-dark via-warning to-warning-light ${gradiantClassesBase}`,
|
||||
danger: `from-danger-dark via-danger to-danger-light ${gradiantClassesBase}`,
|
||||
alert: `from-alert-dark via-alert to-alert-light ${gradiantClassesBase}`,
|
||||
tip: `from-tip-dark via-tip to-tip-light ${gradiantClassesBase}`,
|
||||
muted: `from-stone-200 via-stone-300 to-stone-400 ${gradiantClassesBase} dark:from-stone-900 dark:via-stone-900 dark:to-stone-800`,
|
||||
strong: `from-stone-700 via-stone-800 to-stone-900 ${gradiantClassesBase} dark:from-stone-100 dark:via-stone-200 dark:to-stone-300`,
|
||||
default: `from-stone-200/40 via-white to-stone-100 ${gradiantClassesBase} dark:from-stone-900 dark:via-black dark:to-stone-950`,
|
||||
};
|
||||
|
||||
export const styleToBgGradientHoverMap = {
|
||||
primary: `hover:from-primary-brightLight hover:to-primary-light ${gradiantClassesBase}`,
|
||||
info: `hover:from-info-brightLight hover:to-info-light ${gradiantClassesBase}`,
|
||||
success: `hover:from-success-brightLight hover:to-success-light ${gradiantClassesBase}`,
|
||||
warning: `hover:from-warning-brightLight hover:to-warning-light ${gradiantClassesBase}`,
|
||||
danger: `hover:from-danger-brightLight hover:to-danger-light ${gradiantClassesBase}`,
|
||||
alert: `hover:from-alert-brightLight hover:to-alert-light ${gradiantClassesBase}`,
|
||||
tip: `hover:from-tip-brightLight hover:to-tip-light ${gradiantClassesBase}`,
|
||||
muted: `hover:from-stone-200 hover:to-stone-300 ${gradiantClassesBase} dark:hover:from-stone-900 dark:hover:to-stone-700/70`,
|
||||
strong: `hover:from-stone-700 hover:to-stone-800 ${gradiantClassesBase} dark:hover:from-stone-100 dark:hover:to-stone-200`,
|
||||
default: `hover:from-stone-100/50 hover:to-stone-100/50 dark:hover:from-stone-950 dark:hover:to-stone-900 ${gradiantClassesBase} border border-stone-100 dark:border-stone-900`,
|
||||
};
|
||||
|
||||
export const styleToBgGradientActiveMap = {
|
||||
primary: `active:from-primary-brightDark active:to-primary-light ${gradiantClassesBase}`,
|
||||
info: `active:from-info-brightDark active:to-info-light ${gradiantClassesBase}`,
|
||||
success: `active:from-success-brightDark active:to-success-light ${gradiantClassesBase}`,
|
||||
warning: `active:from-warning-brightDark active:to-warning-light ${gradiantClassesBase}`,
|
||||
danger: `active:from-danger-brightDark active:to-danger-light ${gradiantClassesBase}`,
|
||||
alert: `active:from-alert-brightDark active:to-alert-light ${gradiantClassesBase}`,
|
||||
tip: `active:from-tip-brightDark active:to-tip-light ${gradiantClassesBase}`,
|
||||
muted: `active:from-stone-300 active:to-stone-300 ${gradiantClassesBase} dark:active:from-stone-900 dark:active:to-stone-800`,
|
||||
strong: `active:from-stone-950 active:to-stone-900 ${gradiantClassesBase} dark:active:from-stone-100 dark:active:to-stone-200`,
|
||||
default: `active:from-stone-200/50 active:to-stone-100/50 dark:active:from-stone-950 dark:active:to-black ${gradiantClassesBase}`,
|
||||
};
|
||||
|
||||
export const shadowClassesBase = "shadow-sm";
|
||||
|
||||
export const styleToHoverShadowMap = {
|
||||
primary: `${shadowClassesBase} shadow-blue/20 hover:shadow-blue/40`,
|
||||
info: `${shadowClassesBase} shadow-purple/20 hover:shadow-purple/30`,
|
||||
success: `${shadowClassesBase} shadow-green/20 hover:shadow-green/30`,
|
||||
warning: `${shadowClassesBase} shadow-orange/20 hover:shadow-orange/30`,
|
||||
danger: `${shadowClassesBase} shadow-red/20 hover:shadow-red/30`,
|
||||
alert: `${shadowClassesBase} shadow-yellow/20 hover:shadow-yellow/30`,
|
||||
tip: `${shadowClassesBase} shadow-cyan/20 hover:shadow-cyan/30`,
|
||||
muted: `${shadowClassesBase} shadow-stone-200/20 hover:shadow-stone-200/30 dark:shadow-stone-600/20 dark:hover:shadow-stone-600/30`,
|
||||
strong: `${shadowClassesBase} shadow-stone-900/20 hover:shadow-stone-900/30 dark:shadow-white/20 dark:hover:shadow-white/30`,
|
||||
default: `${shadowClassesBase} shadow-stone-600/20 hover:shadow-stone-600/30 dark:shadow-stone-200/20 dark:hover:shadow-stone-200/30`,
|
||||
};
|
||||
|
||||
const focusRingClassesBase =
|
||||
"focus:outline-none focus-visible:ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-opacity-10";
|
||||
|
||||
export const styleToButtonStateMap = {
|
||||
primary: `${styleToBgGradientActiveMap.primary} ${focusRingClassesBase} focus:ring-primary`,
|
||||
info: `${styleToBgGradientActiveMap.info} ${focusRingClassesBase} focus:ring-info`,
|
||||
success: `${styleToBgGradientActiveMap.success} ${focusRingClassesBase} focus:ring-success`,
|
||||
warning: `${styleToBgGradientActiveMap.warning} ${focusRingClassesBase} focus:ring-warning`,
|
||||
danger: `${styleToBgGradientActiveMap.danger} ${focusRingClassesBase} focus:ring-danger`,
|
||||
alert: `${styleToBgGradientActiveMap.alert} ${focusRingClassesBase} focus:ring-alert`,
|
||||
tip: `${styleToBgGradientActiveMap.tip} ${focusRingClassesBase} focus:ring-tip`,
|
||||
muted: `${styleToBgGradientActiveMap.muted} ${focusRingClassesBase} focus:ring-stone-200 dark:focus:ring-stone-900`,
|
||||
strong: `${styleToBgGradientActiveMap.strong} ${focusRingClassesBase} focus:ring-stone-800 dark:focus:ring-stone-200`,
|
||||
default: `${styleToBgGradientActiveMap.default} ${focusRingClassesBase} focus:ring-black dark:focus:ring-white`,
|
||||
};
|
||||
|
||||
export const variantStyleToButtonStateMap = {
|
||||
outline: `${focusRingClassesBase}`,
|
||||
inverted: `${focusRingClassesBase}`,
|
||||
ghost: `${focusRingClassesBase}`,
|
||||
text: `${focusRingClassesBase}`,
|
||||
};
|
||||
@@ -30,6 +30,36 @@ const jazzBlue = {
|
||||
DEFAULT: COLORS.BLUE,
|
||||
};
|
||||
|
||||
const green = {
|
||||
...colors.green,
|
||||
DEFAULT: COLORS.FOREST,
|
||||
};
|
||||
|
||||
const cyan = {
|
||||
...colors.cyan,
|
||||
DEFAULT: COLORS.TURQUOISE,
|
||||
};
|
||||
|
||||
const red = {
|
||||
...colors.red,
|
||||
DEFAULT: COLORS.RED,
|
||||
};
|
||||
|
||||
const yellow = {
|
||||
...colors.yellow,
|
||||
DEFAULT: COLORS.YELLOW,
|
||||
};
|
||||
|
||||
const orange = {
|
||||
...colors.orange,
|
||||
DEFAULT: COLORS.ORANGE,
|
||||
};
|
||||
|
||||
const purple = {
|
||||
...colors.purple,
|
||||
DEFAULT: COLORS.PURPLE,
|
||||
};
|
||||
|
||||
const stonePaletteWithAlpha = { ...stonePalette };
|
||||
|
||||
Object.keys(stonePalette).forEach((key) => {
|
||||
@@ -40,11 +70,12 @@ Object.keys(stonePalette).forEach((key) => {
|
||||
});
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const config = {
|
||||
export const preset = {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/utils/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
@@ -52,28 +83,103 @@ const config = {
|
||||
...harmonyPalette,
|
||||
stone: stonePaletteWithAlpha,
|
||||
blue: jazzBlue,
|
||||
primary: "var(--color-primary)",
|
||||
secondary: "var(--color-secondary)",
|
||||
highlight: "var(--color-transparent-primary)",
|
||||
success: "var(--color-success)",
|
||||
info: "var(--color-info)",
|
||||
warning: "var(--color-warning)",
|
||||
tip: "var(--color-tip)",
|
||||
green: {
|
||||
DEFAULT: "var(--color-green)",
|
||||
green,
|
||||
cyan,
|
||||
red,
|
||||
yellow,
|
||||
purple,
|
||||
orange,
|
||||
muted: "var(--color-muted)",
|
||||
strong: "var(--color-strong)",
|
||||
primary: {
|
||||
DEFAULT: "var(--color-primary)",
|
||||
transparent: "var(--color-transparent-primary)",
|
||||
dark: "var(--color-primary-dark)",
|
||||
light:
|
||||
"lch(from var(--color-primary) calc(l + 10) calc(c + 1) calc(h - 5))",
|
||||
brightLight:
|
||||
"lch(from var(--color-primary) calc(l - 1) calc(c + 20) calc(h + 5))",
|
||||
brightDark:
|
||||
"lch(from var(--color-primary) calc(l - 6) calc(c + 20) calc(h + 5))",
|
||||
},
|
||||
success: {
|
||||
DEFAULT: "var(--color-success)",
|
||||
transparent: "lch(from var(--color-success) l c h / 0.3)",
|
||||
dark: "lch(from var(--color-success) calc(l - 7) calc(c - 1) calc(h + 5))",
|
||||
light:
|
||||
"lch(from var(--color-success) calc(l + 4) calc(c + 1) calc(h - 5))",
|
||||
brightLight:
|
||||
"lch(from var(--color-success) calc(l - 1) calc(c + 20) calc(h + 10))",
|
||||
brightDark:
|
||||
"lch(from var(--color-success) calc(l - 6) calc(c + 20) calc(h + 10))",
|
||||
},
|
||||
info: {
|
||||
DEFAULT: "var(--color-info)",
|
||||
transparent: "lch(from var(--color-info) l c h / 0.3)",
|
||||
dark: "lch(from var(--color-info) calc(l - 7) calc(c - 1) calc(h + 5))",
|
||||
light:
|
||||
"lch(from var(--color-info) calc(l + 4) calc(c + 1) calc(h - 5))",
|
||||
brightLight:
|
||||
"lch(from var(--color-info) calc(l - 1) calc(c + 20) calc(h + 5))",
|
||||
brightDark:
|
||||
"lch(from var(--color-info) calc(l - 4) calc(c + 20) calc(h + 5))",
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: "var(--color-warning)",
|
||||
transparent: "lch(from var(--color-warning) l c h / 0.3)",
|
||||
dark: "lch(from var(--color-warning) calc(l - 7) calc(c - 1) calc(h + 5))",
|
||||
light:
|
||||
"lch(from var(--color-warning) calc(l + 4) calc(c + 1) calc(h - 5))",
|
||||
brightLight:
|
||||
"lch(from var(--color-warning) calc(l - 1) calc(c + 30) calc(h + 15))",
|
||||
brightDark:
|
||||
"lch(from var(--color-warning) calc(l - 4) calc(c + 30) calc(h + 15))",
|
||||
},
|
||||
danger: {
|
||||
DEFAULT: "var(--color-danger)",
|
||||
transparent: "lch(from var(--color-danger) l c h / 0.3)",
|
||||
dark: "lch(from var(--color-danger) calc(l - 7) calc(c - 1) calc(h + 5))",
|
||||
light:
|
||||
"lch(from var(--color-danger) calc(l + 4) calc(c + 1) calc(h - 5))",
|
||||
brightLight:
|
||||
"lch(from var(--color-danger) calc(l - 2) calc(c + 20) calc(h + 10))",
|
||||
brightDark:
|
||||
"lch(from var(--color-danger) calc(l - 6) calc(c + 10) calc(h + 10))",
|
||||
},
|
||||
tip: {
|
||||
DEFAULT: "var(--color-tip)",
|
||||
transparent: "lch(from var(--color-tip) l c h / 0.3)",
|
||||
dark: "lch(from var(--color-tip) calc(l - 7) calc(c - 1) calc(h + 5))",
|
||||
light:
|
||||
"lch(from var(--color-tip) calc(l + 4) calc(c + 1) calc(h - 5))",
|
||||
brightLight:
|
||||
"lch(from var(--color-tip) calc(l - 1) calc(c + 20) calc(h + 10))",
|
||||
brightDark:
|
||||
"lch(from var(--color-tip) calc(l - 4) calc(c + 20) calc(h + 10))",
|
||||
},
|
||||
alert: {
|
||||
DEFAULT: "var(--color-alert)",
|
||||
transparent: "lch(from var(--color-alert) l c h / 0.3)",
|
||||
dark: "lch(from var(--color-alert) calc(l - 7) calc(c - 1) calc(h + 5))",
|
||||
light:
|
||||
"lch(from var(--color-alert) calc(l + 4) calc(c + 1) calc(h - 5))",
|
||||
brightLight:
|
||||
"lch(from var(--color-alert) calc(l - 1) calc(c + 50) calc(h + 15))",
|
||||
brightDark:
|
||||
"lch(from var(--color-alert) calc(l - 5) calc(c + 50) calc(h + 15))",
|
||||
},
|
||||
},
|
||||
textColor: {
|
||||
default: "var(--color-default)",
|
||||
highlight: "var(--color-highlight)",
|
||||
strong: "var(--color-strong)",
|
||||
muted: "var(--color-muted)",
|
||||
},
|
||||
borderColor: {
|
||||
DEFAULT: "var(--color-border-default)",
|
||||
},
|
||||
backgroundColor: {
|
||||
highlight: "var(--color-transparent-primary)",
|
||||
muted: "var(--color-bg-muted)",
|
||||
highlight: "var(--color-background-highlight)",
|
||||
},
|
||||
fontFamily: {
|
||||
display: ["var(--font-manrope)"],
|
||||
@@ -195,4 +301,10 @@ const config = {
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
const config = {
|
||||
presets: [preset],
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -4,7 +4,12 @@ import { GcmpLogo } from "@garden-co/design-system/src/components/atoms/logos/Gc
|
||||
import { Nav } from "@garden-co/design-system/src/components/organisms/Nav";
|
||||
export function GcmpNav() {
|
||||
const cta = (
|
||||
<Button variant="secondary" className="ml-3" href="mailto:hello@garden.co">
|
||||
<Button
|
||||
intent="success"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
href="mailto:hello@garden.co"
|
||||
>
|
||||
Contact us
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -31,7 +31,11 @@ export default function Products() {
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<JazzLogo className="h-10 w-auto" />
|
||||
<div>
|
||||
<Button href="https://jazz.tools" variant="secondary">
|
||||
<Button
|
||||
href="https://jazz.tools"
|
||||
intent="primary"
|
||||
variant="outline"
|
||||
>
|
||||
Go to jazz.tools
|
||||
</Button>
|
||||
</div>
|
||||
|
||||