Compare commits

...

162 Commits

Author SHA1 Message Date
Guido D'Orsi
cfa44f32eb Merge pull request #2578 from garden-co/changeset-release/main
Version Packages
2025-06-27 12:53:43 +02:00
github-actions[bot]
ef920435e9 Version Packages 2025-06-27 10:07:38 +00:00
Guido D'Orsi
87d05404dd Merge pull request #2581 from garden-co/fix/idbtransaction
fix: refresh the IndexedDB transaction when finished but not flagged as done
2025-06-27 12:03:19 +02:00
Guido D'Orsi
535c460f5a fix: refresh the IndexedDB transaction when finished but not flagged as done 2025-06-27 11:49:49 +02:00
Guido D'Orsi
fa1b302474 Merge pull request #2577 from garden-co/fix/GCO-600-image-original-size
Fix GCO-600: image original size
2025-06-26 14:35:27 +02:00
Matteo Manchi
45f73a774c chore: update changeset 2025-06-26 12:08:55 +02:00
Matteo Manchi
2a9e271dc3 chore(create-jazz-app/browser-media-images): small refactoring of function 2025-06-26 12:04:51 +02:00
Anselm Eickhoff
3d96d9c829 Merge pull request #2416 from garden-co/feat/design-system-improvements
Feat/design system improvements
2025-06-26 10:16:24 +01:00
Anselm
844051405d Smaller nav CTA & move CTA to menu on mobile 2025-06-26 10:07:24 +01:00
Anselm
625eff2333 Fix pricing page button 2025-06-26 09:54:07 +01:00
Anselm
59e2871065 Rename styleType to intent 2025-06-26 09:49:26 +01:00
Anselm
acdc88fb91 Tweak md icon size 2025-06-26 09:47:53 +01:00
Matteo Manchi
05eab4e2a9 fix(jazz-tools/browser-media-images): use coherent values for resized images - Fixes GCO-600 2025-06-25 23:43:13 +02:00
Guido D'Orsi
efcd65ae38 docs: improve the contributing 2025-06-25 15:42:39 +02:00
Meg Culotta
ad60fa942a Merge pull request #2571 from garden-co/closes-gco-581-add-pr-template-to-gh
Closes GCO-581 - Create pull_request_template.md
2025-06-25 08:05:52 -05:00
Sammii
5272d3cd2a update nav button on gcmp homepage 2025-06-24 16:10:04 +01:00
Sammii
d837811813 variant updates on fake get started buttons and example link components 2025-06-24 15:54:18 +01:00
Guido D'Orsi
2b4aba2d1b fix: fix todo schema migration 2025-06-24 16:53:13 +02:00
Sammii
50b4da18d9 refactoring everything to be more compatible with shadcn nomenclature, styleVariants now variants, variants now styleTyles, shad cn button variants mapped to design system, all buttons/icons etc required are updated monorepo wide 2025-06-24 15:41:41 +01:00
Guido D'Orsi
d18d09e002 Merge pull request #2574 from garden-co/changeset-release/main
Version Packages
2025-06-24 15:33:26 +02:00
github-actions[bot]
d983f27bbe Version Packages 2025-06-24 13:32:21 +00:00
Guido D'Orsi
fcf83b0da4 Merge pull request #2570 from joeinnes/post-0.15-docs-fixes
Post 0.15 docs fixes
2025-06-24 15:26:22 +02:00
Guido D'Orsi
9231e2c22f Merge pull request #2568 from garden-co/feat/cotext-display
feat(inspector): improve CoPlainText view
2025-06-24 15:24:54 +02:00
Guido D'Orsi
33157ee0ad Merge pull request #2573 from garden-co/fix/debug-transactions
fix: add debug code on parseJSON errors during the transactions parsing
2025-06-24 15:24:18 +02:00
Guido D'Orsi
4b964edcaf fix: add debug code on parseJSON errors during the transactions parsing 2025-06-24 15:16:10 +02:00
Sammii
df22f2617e amend secondary button style to default 2025-06-24 12:01:33 +01:00
Sammii
280495c533 updating styles on HelpLinks and NewsLetterForm 2025-06-24 11:40:46 +01:00
Sammii
d5c6fbdc3c removing secondary styles 2025-06-24 11:40:32 +01:00
Sammii
57776a1400 amending default button with icon styling 2025-06-24 11:40:24 +01:00
Sammii
156c45aa0e update button styles on design system side nav 2025-06-24 11:28:34 +01:00
Sammii
2d0dba6bbc amend button style on early adopter section 2025-06-24 11:23:44 +01:00
Sammii
7241d2ad95 refactor button highlight to strong 2025-06-24 11:22:46 +01:00
Sammii
4a9eeace00 refactor highlight to strong colours inputs and icons 2025-06-24 11:21:32 +01:00
Meg Culotta
4f9c91f6ff Closes GCO-581 - Create pull_request_template.md 2025-06-23 14:39:52 -05:00
Guido D'Orsi
a8e1726797 Merge pull request #2569 from garden-co/docs/type-aliases
docs: add type aliases to the docs examples
2025-06-23 21:25:43 +02:00
Joe Innes
a6eeada331 docs: ⚠️ update Node.js requirement to v20.0.0 or later
* Updated the minimum Node.js version requirement in the documentation to ensure compatibility with the latest features and improvements.
2025-06-23 20:56:59 +02:00
Joe Innes
3b38a8241c docs: ✏️ add Node.js v20 requirement alerts in documentation
* Added `<Alert>` components to inform users about the requirement of Node.js v20 across various setup documentation files.
* Updated installation instructions for clarity and improved user guidance.
2025-06-23 20:56:40 +02:00
Joe Innes
c49330c308 docs: ⚠️ add warning about custom AccountSchema requirement
* Added an `<Alert>` component to inform users that they need to pass their custom `AccountSchema` to the provider.
* This change aims to reduce confusion for new adopters regarding schema registration.
2025-06-23 20:20:12 +02:00
Guido D'Orsi
0c4e27c18d docs: add type aliases to the docs examples 2025-06-23 19:54:42 +02:00
Joe Innes
d8d273821e docs: ✏️ remove 'note' on docs inaccuracies
* Removed outdated note about Jazz 0.14.0 release.
* We've got links on the right now so docs issues can be reported, no need for an extra chore to bump the version number in this note every release.
2025-06-23 18:04:04 +02:00
Guido D'Orsi
def0ca81b4 Merge pull request #2567 from garden-co/fix/clerk-starter
fix: fix clerk-expo starter and make it more minimal
2025-06-23 18:00:08 +02:00
Sammii
7ff13a8f55 erfactor out secondary variant 2025-06-23 16:52:51 +01:00
Trisha Lim
0e7e53238b feat(inspector): improve CoPlainText view 2025-06-23 15:17:47 +01:00
Guido D'Orsi
fcc18e5212 fix: fix clerk-expo starter and make it more minimal 2025-06-23 15:45:43 +02:00
Sammii
5741d7f09c update input props table 2025-06-23 14:34:36 +01:00
Sammii
766d2c8846 update inputs view 2025-06-23 14:33:02 +01:00
Sammii
70a43d0c39 Icon refactor and style adjust 2025-06-23 14:26:39 +01:00
Sammii
cf44258848 variant fix 2025-06-23 11:41:30 +01:00
Sammii
4db5ec2dd8 variant update ViewsSideMenu 2025-06-23 11:39:12 +01:00
Sammii
4983e57e62 more styling updates 2025-06-23 11:21:22 +01:00
Sammii
5b2fc70ca1 refactorign buttons view page 2025-06-23 11:16:02 +01:00
Sammii
53af54570c refactoring button variants 2025-06-23 10:57:22 +01:00
Sammii
e0b4626c22 reactor outline button to have hover text map class 2025-06-23 10:04:07 +01:00
Sammii
a273a0db58 quick search style update 2025-06-23 09:39:20 +01:00
Sammii
277104ebba Input style refactor 2025-06-23 09:37:56 +01:00
Sammii
434e59d5c4 refactoring Inputs, improving styling and InputsView page & table props 2025-06-23 08:38:23 +01:00
Sammii
bb20907774 update button props on view table 2025-06-23 08:07:42 +01:00
Sammii
282425575f amend quick search design to ahere to design system 2025-06-23 08:05:52 +01:00
Sammii
75501a9051 amending styling for gcmp homepage 2025-06-20 15:36:55 +01:00
Sammii
c114cf4029 fresh pnpm lock 2025-06-20 11:08:14 +01:00
Sammii
03da7dc994 Merge branch 'main' into feat/design-system-improvements 2025-06-20 11:06:17 +01:00
Sammii
12d5c68f98 update colors view 2025-06-19 14:44:46 +01:00
Sammii
ec1e359621 help links responsive button styling amends 2025-06-19 13:49:15 +01:00
Sammii
44fb13ddd7 refactoring variants with default with black/white baked in on system mode, refactoring Buttons and Icons 2025-06-19 13:48:57 +01:00
Sammii
4fd4c5bbed amending styling on home page 2025-06-19 11:36:01 +01:00
Sammii
a1d31566ed adding views layout to landing page to look like views routes 2025-06-19 11:34:50 +01:00
Sammii
dcde3aa811 amending bg highlight on dark 2025-06-19 11:34:29 +01:00
Sammii
a621141d76 adding colour classes to all colour class maps and improving types 2025-06-18 17:19:07 +01:00
Sammii
0e1fbd7dfc type update 2025-06-18 16:37:31 +01:00
Sammii
27efdf9b8e button instance refactor to new variant colors, code tidy and Layout/view update 2025-06-18 10:59:14 +01:00
Sammii
83068b33e9 refactoring colors into variants 2025-06-18 10:58:23 +01:00
Sammii
769d9b0517 amending HelpLinks, deleting custom styling and refactoring to use button component 2025-06-18 10:36:22 +01:00
Sammii
b0ffe5ed7e creatnig buttong gradient styles with pure tailwind button MVP 2025-06-18 10:35:55 +01:00
Sammii
c67a0cd3ce create and add ThemeProvider component to design system 2025-06-17 17:24:21 +01:00
Sammii
48e11a243f Icon refactor 2025-06-17 17:24:05 +01:00
Sammii
de904698d8 amending Variant import in Icon 2025-06-17 17:01:14 +01:00
Sammii
7c8180dcb4 refactor pages for Layout view with dynamic header based on path 2025-06-17 16:35:01 +01:00
Sammii
da3e101c50 Button StyleVariant Type 2025-06-17 16:17:10 +01:00
Sammii
7605d228f2 page update inputs 2025-06-17 16:10:36 +01:00
Sammii
3063d74ab9 refactoring views, ading new input and icon views to page routes/pages 2025-06-17 16:09:59 +01:00
Sammii
2a4e3fc0cd refactoring components view and icons 2025-06-17 16:04:12 +01:00
Sammii
7cc6d63d40 Icons, styling an code tidy 2025-06-17 15:43:56 +01:00
Sammii
d574bbc521 Icon refactor, has background and refactor icon components on homepage 2025-06-17 15:40:44 +01:00
Sammii
6388a7272b add InputProps table and update ButtonsProps table 2025-06-17 15:23:07 +01:00
Sammii
cd358888d8 Table refactor 2025-06-17 15:22:42 +01:00
Sammii
f427f2324b incorporate icons into design system, with logic to colour button dependant on button variant and style 2025-06-17 15:21:55 +01:00
Sammii
d0e2041b10 styling adjustments 2025-06-13 16:44:16 +01:00
Sammii
edce59d238 making everything fully responsive 2025-06-13 16:35:12 +01:00
Sammii
7f75d852c1 tailwind config amends 2025-06-13 16:21:30 +01:00
Sammii
2683af7d28 ameding tailwind config 2025-06-13 16:14:31 +01:00
Sammii
4223720010 formatting 2025-06-13 16:09:43 +01:00
Sammii
1b4508fea7 making layout with side nav fully responsive 2025-06-13 16:06:21 +01:00
Sammii
776fa09279 refactoring entire design system view for routes base views, created layout and side menu 2025-06-13 15:10:43 +01:00
Sammii
5c200aa60d updating globals.css 2025-06-13 14:13:51 +01:00
Sammii
9fc6c5f6c8 updating Input imports 2025-06-13 14:12:40 +01:00
Sammii
7e06cd4a77 refactor Colors view 2025-06-13 14:12:30 +01:00
Sammii
c43191d97b amending taiwlind config 2025-06-13 14:08:05 +01:00
Sammii
5706b5eb81 Buttons prop table refactor 2025-06-13 14:06:46 +01:00
Sammii
5eed930997 button refactor for outline style variants 2025-06-13 12:35:48 +01:00
Sammii
35e5e50508 table refactor 2025-06-13 12:24:08 +01:00
Sammii
e88a3d0712 adding more variety to Component displays 2025-06-13 10:50:38 +01:00
Sammii
13b64af5b2 adding colour defaulty for dark mode 2025-06-13 10:41:15 +01:00
Sammii
9804c6a729 refactoring Inputs and resolving + showcasing all combinations of labels, positions, icons and buttons available 2025-06-13 10:40:26 +01:00
Sammii
937d415cc2 add home page pnpm lock 2025-06-12 14:59:41 +01:00
Sammii
9196154207 adding states for all button colors 2025-06-12 12:23:48 +01:00
Sammii
c907b2aac2 adding states for all button variants, styleVariants 2025-06-12 12:20:32 +01:00
Sammii
b5a9dfa7ec adding active and focus states to base variant button types 2025-06-12 10:15:23 +01:00
Sammii
113c77b416 removing padding from text buttons 2025-06-12 10:14:37 +01:00
Sammii
cad5444400 more color tweaks 2025-06-12 10:14:04 +01:00
Sammii
b2b350e4d0 adding light dark variables to all semeantic color vars 2025-06-12 10:13:47 +01:00
Sammii
80d5d62852 code tidy 2025-06-12 09:36:08 +01:00
Sammii
75f8833c1a add toaster to design system 2025-06-11 15:14:07 +01:00
Sammii
99d1f3f28b add toast refacvtor table with new isCopyable prop + functionality 2025-06-11 15:13:22 +01:00
Sammii
4066cfe011 adding rest of button props 2025-06-11 13:00:43 +01:00
Sammii
e5f8b06af1 resolve favicon 2025-06-11 13:00:26 +01:00
Sammii
485b5a238d styling Table component 2025-06-11 12:29:40 +01:00
Sammii
c42ee6d6e2 putting the favicon in the public folder 2025-06-11 12:29:23 +01:00
Sammii
1fb2fd0f50 creating button prop table 2025-06-11 12:14:50 +01:00
Sammii
0fce4adfc5 creating Table component 2025-06-11 12:14:37 +01:00
Sammii
b59086c808 doc amend 2025-06-11 11:51:35 +01:00
Sammii
fe91463652 adjusting examples 2025-06-11 11:50:34 +01:00
Sammii
b9065db109 adjusting button styling 2025-06-11 11:45:48 +01:00
Sammii
d13da295ed updating Components 2025-06-11 11:44:26 +01:00
Sammii
0291389c3b update buttons 2025-06-11 11:43:53 +01:00
Sammii
f417f518cb refactor NewsletterForm with new InputWithButton component 2025-06-11 11:43:35 +01:00
Sammii
f69f99a209 create InputWithButton 2025-06-11 11:42:17 +01:00
Sammii
31c4fd7c07 refactor Input 2025-06-11 11:41:35 +01:00
Sammii
613aadd775 create Label 2025-06-11 11:41:10 +01:00
Sammii
0f969b4be4 add search icon to Icon 2025-06-11 11:40:53 +01:00
Sammii
f7ac1015b2 refactor Button to use Icon and remove own ButtonIcon component, introduce icon position 2025-06-11 11:40:02 +01:00
Sammii
d7c5d4a03d styling homepage with design system 2025-06-06 17:49:03 +01:00
Sammii
8384c55cce amending muted tailwind config 2025-06-06 17:48:20 +01:00
Sammii
be947d1086 fixing more paths 2025-06-06 16:19:32 +01:00
Sammii
3924bb2ede making paths fixed 2025-06-06 16:03:15 +01:00
Sammii
2e3003c2fc styling and tweaking Switch and Components 2025-06-06 15:47:32 +01:00
Sammii
5ec15ba0a2 refactoring Forms to Components and adding Switch 2025-06-06 15:44:15 +01:00
Sammii
c7ccf5c5d7 switch refactor 2025-06-06 15:18:33 +01:00
Sammii
9e8c81a1a6 button refactor 2025-06-06 15:18:23 +01:00
Sammii
c563e2547f button refactor 2025-06-06 15:14:54 +01:00
Sammii
00cb697bc7 moving the component views to /src/views 2025-06-06 14:37:06 +01:00
Sammii
a6a0560059 Merge branch 'main' into feat/design-system-improvements 2025-06-06 14:34:38 +01:00
Sammii
55fa5977c2 formatting 2025-06-06 13:48:15 +01:00
Sammii
8b4bc1bc97 updating design system favicon 2025-06-06 13:46:27 +01:00
Sammii
df403ccbc6 styling button new tab icon 2025-06-05 18:41:24 +01:00
Sammii
1f490d2aae reordering style + variant buttons to match style order 2025-06-05 13:38:14 +01:00
Sammii
1b766b6369 UI view reorder 2025-06-05 11:45:33 +01:00
Sammii
3f54da98f8 fix outline button jump 2025-06-05 11:43:47 +01:00
Sammii
05b98c0f8b info update 2025-06-05 11:37:57 +01:00
Sammii
928e63620b renaming styleVariant back 2025-06-05 11:24:16 +01:00
Sammii
ea9b1eb88e renaming symbol styleVariant to style 2025-06-05 10:59:07 +01:00
Sammii
83a88252e2 fighting with tailwind, buttons MVP LGTM 2025-06-05 10:58:40 +01:00
Sammii
7a61c19135 buttons refactor 2025-06-04 17:43:14 +01:00
Sammii
a2f87f304c base structure for component library buttons 2025-06-04 17:00:06 +01:00
Sammii
f9f24d2ad2 styling 2025-06-03 16:48:16 +01:00
Sammii
ed3197a7fd updating schema 2025-06-03 16:47:59 +01:00
Sammii
772a88e98f finessing styling 2025-06-03 16:27:49 +01:00
Sammii
e22a7f46ad stylingg 2025-06-03 15:31:15 +01:00
Sammii
f890f2f460 refactor design system page.tsx to display new components from refactor 2025-06-03 15:16:58 +01:00
Sammii
e9c17e12dc create new Colors view component 2025-06-03 15:15:27 +01:00
Sammii
4b908e3024 refactor Forms view into own component 2025-06-03 15:15:03 +01:00
Sammii
90fd2b5da0 refactor Typography view into own component 2025-06-03 15:14:55 +01:00
Sammii
d7b4360f11 adding error color 2025-06-03 15:13:55 +01:00
171 changed files with 3511 additions and 3938 deletions

24
.github/pull_request_template.md vendored Normal file
View 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 youd like review on or decided async
### Related Links
- GitHub issue
- Linear pitch
- Design links or references

View File

@@ -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:

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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`.

View File

@@ -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"
}
}
}
}

View File

@@ -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 }} />
);
}

View File

@@ -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>
);
}

View File

@@ -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: "",
}}
/>
);
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;
}
}`;

View File

@@ -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,
},
});

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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: "",
}}
/>
);
}

View File

@@ -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>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -1,9 +0,0 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};

View File

@@ -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;

View File

@@ -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": {}
}
}

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,2 +0,0 @@
import "./polyfills";
import "expo-router/entry";

View File

@@ -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" });

View File

@@ -1 +0,0 @@
/// <reference types="nativewind/types" />

View File

@@ -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
}

View File

@@ -1 +0,0 @@
export const apiKey = "chat-rn-expo-clerk-example-jazz@garden.co";

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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: [],
};

View File

@@ -1,11 +0,0 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"moduleResolution": "bundler",
"paths": {
"@/*": ["./*"]
}
},
"include": ["**/*.ts", "**/*.tsx", "nativewind-env.d.ts"]
}

View File

@@ -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",

View File

@@ -1,5 +1,19 @@
# passkey-svelte
## 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

View File

@@ -1,6 +1,6 @@
{
"name": "chat-svelte",
"version": "0.0.90",
"version": "0.0.92",
"type": "module",
"private": true,
"scripts": {

View File

@@ -0,0 +1 @@
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_ZXZpZGVudC1kYW5lLTg5LmNsZXJrLmFjY291bnRzLmRldiQ

44
examples/clerk-expo/.gitignore vendored Normal file
View 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

View 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"]
}
}

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View 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);

View 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
}

View 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>
);
}

View 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",
},
});

View File

@@ -0,0 +1 @@
export const apiKey = "chat-rn-expo-example-jazz@garden.co";

View 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} />;
}

View 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",
},
});

View 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",
},
});

View 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>;

View File

@@ -0,0 +1,6 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
}

View File

@@ -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 }),
});

View File

@@ -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>

View File

@@ -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(

View File

@@ -5,6 +5,7 @@ export const COLORS = {
GREEN: "#8BDA27",
PINK: "#EF478E",
PURPLE: "#B441EB",
YELLOW: "#FBC400",
YELLOW: "#FCAE00",
RED: "#FF601C",
ORANGE: "#FF601C",
};

View File

@@ -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",

View File

@@ -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

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -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;

View File

@@ -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>
);

View File

@@ -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>
);

View 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>
);
}

View 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>
);
}

View 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",
},
],
};

View 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>
);
};

View 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>
);
}

View 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",
},
],
};

View 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",
},
],
};

View 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>
);
}

View 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>
);
}

View File

@@ -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`,
};
};

View File

@@ -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}
/>
);

View 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>
);
}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"
/>
)}

View File

@@ -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>
);
},

View File

@@ -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>
);
}

View 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>
);
};

View File

@@ -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={() => {

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View 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);

View 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}`,
};

View File

@@ -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;

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -1,7 +1,7 @@
import type { Config } from "tailwindcss";
const config: Config = {
presets: [require("@garden-co/design-system/tailwind.config.js")],
presets: [require("@garden-co/design-system/tailwind.config.js").preset],
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",

Some files were not shown because too many files have changed in this diff Show More