Compare commits

..

151 Commits

Author SHA1 Message Date
Anselm
8b88ae25f1 Fix Highlight component 2025-03-25 11:55:30 +00:00
Anselm
f21089bd59 Fix darkmode diff colours 2025-03-25 11:43:54 +00:00
Anselm
4760b84828 Reintroduce Jazz colors 2025-03-25 11:41:02 +00:00
Anselm
e021068051 Typing in docs working 2025-03-24 12:55:21 +00:00
Anselm
16311977e9 First attempt to make twoslash work 2025-03-21 17:40:10 +00:00
Anselm
ab2970413e Upgrade shiki and use built-in transformers for diffs 2025-03-21 17:00:35 +00:00
Anselm
34ff647221 More details in deep loading 2025-03-21 12:41:44 +00:00
Anselm
ee01e2f059 Divide upgrade guide into breaking / new features 2025-03-21 10:58:35 +00:00
Anselm
525f24ffc1 Remove incorrect stuff 2025-03-21 10:50:27 +00:00
Anselm
82bb7733ff Clean up manual subscription & hooks 2025-03-21 10:48:32 +00:00
Anselm
e307ecc524 Include current version of history & time travel 2025-03-20 13:41:26 +00:00
Anselm
f0f43a8b85 "loading depth" -> "resolve query" 2025-03-20 13:34:30 +00:00
Anselm
77d2909999 Container-like -> collections 2025-03-20 13:25:52 +00:00
Trisha Lim
8c951fb08b move AI tools and Inspector in its own section 2025-03-20 13:21:36 +00:00
Trisha Lim
d5f43c5225 reduce spacing between docs nav items 2025-03-20 13:21:36 +00:00
Trisha Lim
4fe3d25adc remove "coming soon" legend on docs nav 2025-03-20 13:21:36 +00:00
Trisha Lim
bd84d87e94 install lucide-react on jazz-inspector 2025-03-20 13:21:36 +00:00
Trisha Lim
5e9b33fded remove covalue inspector form from dom if not shown 2025-03-20 13:21:35 +00:00
Trisha Lim
e2cbe62887 set input length 2025-03-20 13:21:35 +00:00
Trisha Lim
39734cb19f missing key 2025-03-20 13:21:35 +00:00
Trisha Lim
a1b1bab466 move ui components to separate dir 2025-03-20 13:21:35 +00:00
Trisha Lim
9383a0dd47 account selector UI improvement 2025-03-20 13:21:35 +00:00
Trisha Lim
1e7ca2d008 add changeset 2025-03-20 13:21:34 +00:00
Trisha Lim
65d1e94efc "add account" form improvement 2025-03-20 13:21:34 +00:00
Trisha Lim
fcd8e21bfb UI fixes 2025-03-20 13:21:34 +00:00
Trisha Lim
e0d171cf79 spacing and typography improvements 2025-03-20 13:21:34 +00:00
Trisha Lim
aa45457f44 show array contents in a co.json 2025-03-20 13:21:34 +00:00
James Vickery
7189a1ea17 fix lint 2025-03-20 13:21:33 +00:00
James Vickery
0ddf1c871e fix rss urls and set canonical url 2025-03-20 13:21:33 +00:00
James Vickery
abc74dd567 rss 2025-03-20 13:21:33 +00:00
Trisha Lim
69689289a1 update meta tags to align with new copy 2025-03-20 13:21:33 +00:00
Anselm Eickhoff
2e6ea42d21 bring back zero-config magic 2025-03-20 13:21:32 +00:00
Anselm Eickhoff
f1b8f4b5c0 more down-to-earth hero section 2025-03-20 13:21:32 +00:00
Trisha Lim
89f39406ba update homepage copy 2025-03-20 13:21:32 +00:00
Giordano Ricci
8cc79473db fix bar width on small screen to avoid horizontal scroll 2025-03-20 13:21:32 +00:00
Giordano Ricci
690d9e2587 fill missing data on the left 2025-03-20 13:21:31 +00:00
Giordano Ricci
04d1c18378 Fix status latency query, adjust probes 2025-03-20 13:21:31 +00:00
Trisha Lim
ac8081ff01 create-jazz-app: replace project name param with directory name (#1612) 2025-03-20 13:21:31 +00:00
Anselm Eickhoff
7339782684 Comment grammar fix 2025-03-20 13:21:31 +00:00
Guido D'Orsi
b8d64ba00a fix: align the profile migration in the passkey starter with the last best pratice 2025-03-20 13:21:31 +00:00
pax-k
71b2dc21a9 chore: changeset 2025-03-20 13:21:30 +00:00
pax-k
3f822c3cac fix(examples): removed when="singedUp" from examples apps' Jazz providers 2025-03-20 13:21:30 +00:00
Trisha Lim
789cdab504 set inspector widget height 2025-03-20 13:21:30 +00:00
Trisha Lim
22c3b2b92b fix button exports 2025-03-20 13:21:30 +00:00
Trisha Lim
cd14f30129 clean up 2025-03-20 13:21:29 +00:00
Trisha Lim
2423b3ff46 fit more data onscreen 2025-03-20 13:21:29 +00:00
Trisha Lim
91ada6fd2d set default cursor 2025-03-20 13:21:29 +00:00
Trisha Lim
2052c98094 use Input component for all inputs 2025-03-20 13:21:29 +00:00
Trisha Lim
daa8ddf890 use Button element for all buttons 2025-03-20 13:21:29 +00:00
Trisha Lim
760c90ccc0 remove unused packages and css 2025-03-20 13:21:28 +00:00
Trisha Lim
5003067f47 fix missing types when running dev 2025-03-20 13:21:28 +00:00
Trisha Lim
b36ef59aea fix account export to inspector app 2025-03-20 13:21:28 +00:00
Trisha Lim
2fe509774b reuse jazz-inspector components in inspector app 2025-03-20 13:21:28 +00:00
Trisha Lim
7e254cf9f5 reuse jazz-inspector components in inspector app 2025-03-20 13:21:27 +00:00
Benjamin S. Leveritt
dd25b5f926 Update filestreams.mdx 2025-03-20 13:21:27 +00:00
Benjamin S. Leveritt
1bb51398aa Adds CreateImg config, annotates other methods 2025-03-20 13:21:27 +00:00
Benjamin S. Leveritt
5deec8ddfd Adds React Native docs 2025-03-20 13:21:27 +00:00
Benjamin S. Leveritt
45ec2fabe3 Add Creating Images section to vanilla 2025-03-20 13:21:26 +00:00
Benjamin S. Leveritt
c7f9d9d263 Adds comment about knowness 2025-03-20 13:21:26 +00:00
Benjamin S. Leveritt
3285b85a90 Reword to synced 2025-03-20 13:21:26 +00:00
Benjamin S. Leveritt
64a88b7f54 Tweak presentation of FileStream 2025-03-20 13:21:26 +00:00
Benjamin S. Leveritt
dfc4b21fc8 Switches sections 2025-03-20 13:21:26 +00:00
Benjamin S. Leveritt
fdc86bcb9a Add link to Writing to FileStreams 2025-03-20 13:21:25 +00:00
Benjamin S. Leveritt
dca7edbf47 Add links to Vanilla docs 2025-03-20 13:21:25 +00:00
Benjamin S. Leveritt
56d8d87195 Add React docs 2025-03-20 13:21:25 +00:00
Benjamin S. Leveritt
b7731c8fca Adds revokeObjectURL references and best practices 2025-03-20 13:21:25 +00:00
Benjamin S. Leveritt
3e8afdafdc Fix note section 2025-03-20 13:21:24 +00:00
Benjamin S. Leveritt
b39dc35fae Adds example for fallback behaviour 2025-03-20 13:21:24 +00:00
Benjamin S. Leveritt
31d8831faa Adds imageDef docs 2025-03-20 13:21:24 +00:00
Benjamin S. Leveritt
673e194034 Removes mentions of images, pointing to ImageDef 2025-03-20 13:21:24 +00:00
Benjamin S. Leveritt
b579b32c69 Adds tests for imageDef 2025-03-20 13:21:23 +00:00
Benjamin S. Leveritt
c0e53dc8b8 Adds CodeGroups 2025-03-20 13:21:23 +00:00
Benjamin S. Leveritt
99ad5008eb Add FileStream progress test 2025-03-20 13:21:23 +00:00
Benjamin S. Leveritt
2c86de4176 Adds FileStream doc draft 2025-03-20 13:21:23 +00:00
Trisha Lim
ba3a0d4032 hide jazz-inspector if not dev 2025-03-20 13:21:23 +00:00
Trisha Lim
8f8d02e43e make dev generate app.js instead of jazz-inspector.js 2025-03-20 13:21:22 +00:00
Trisha Lim
2118c46efe add basic json viewer 2025-03-20 13:21:22 +00:00
Trisha Lim
0073ea4950 add covalue search to nav 2025-03-20 13:21:22 +00:00
Trisha Lim
744fcf53b2 bare minimum dark styles 2025-03-20 13:21:22 +00:00
Trisha Lim
37103fdff9 add inspector to chat app 2025-03-20 13:21:21 +00:00
Trisha Lim
7345bc6ca4 replace indigo with blue 2025-03-20 13:21:21 +00:00
Trisha Lim
042b407f2d remove javascript hover styles 2025-03-20 13:21:21 +00:00
Trisha Lim
ddcf91a014 match colors to jazz brand 2025-03-20 13:21:21 +00:00
Trisha Lim
366e1b1cc1 add button label 2025-03-20 13:21:21 +00:00
Trisha Lim
d8edad8041 rewrite styles to tailwind 2025-03-20 13:21:20 +00:00
Trisha Lim
20d823d6fd install twind 2025-03-20 13:21:20 +00:00
Trisha Lim
efb08caeae clean up 2025-03-20 13:21:20 +00:00
Trisha Lim
1a0e37eaed TOC fixes 2025-03-20 13:21:20 +00:00
Trisha Lim
86bc03ac4a fix coming soon TOC 2025-03-20 13:21:19 +00:00
Trisha Lim
748c7160ba set TOC items in context 2025-03-20 13:21:19 +00:00
Trisha Lim
c77c617aaa move docs layout 2025-03-20 13:21:19 +00:00
github-actions[bot]
9fb4961215 Version Packages 2025-03-20 13:21:19 +00:00
Guido D'Orsi
513ebd8fb9 fix: downgrade the WasmCrypto initialization error logging to a warning 2025-03-20 13:21:18 +00:00
github-actions[bot]
6b22fe9a93 Version Packages 2025-03-20 13:21:18 +00:00
Guido D'Orsi
3076a90a51 fix: use PureJSCrypto as fallback when WasmCrypto fails to initialize 2025-03-20 13:21:18 +00:00
Guido D'Orsi
7e6316b930 chore: move the cloudflare test to use WASM 2025-03-20 13:21:17 +00:00
Trisha Lim
f7cdfd5767 add changeset 2025-03-20 13:21:16 +00:00
Benjamin S. Leveritt
ca6bb1c006 Change to markdown + format
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-20 13:15:32 +00:00
Benjamin S. Leveritt
ba9f7eb487 Fix ‘for example’
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-20 12:18:12 +00:00
Anselm
3523ebf2bf Missing green line 2025-03-20 11:47:59 +00:00
Anselm
9131894f40 Upgrade guide improvments 2025-03-20 11:39:36 +00:00
Benjamin S. Leveritt
618be5d016 Fix date
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-20 09:45:29 +00:00
Benjamin S. Leveritt
8a18f30ba5 Update CoMap.Record resolve example
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-20 09:43:09 +00:00
Benjamin S. Leveritt
3128dda432 Remove auto-loading section
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-20 09:38:03 +00:00
Benjamin S. Leveritt
517662635e Add Vanilla examples
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-20 09:31:55 +00:00
Benjamin S. Leveritt
c119a613a7 Tweak copy
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-19 10:33:06 +00:00
Benjamin S. Leveritt
ddd4bc63c3 Fix permissions in lists comment
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-19 09:51:53 +00:00
Benjamin S. Leveritt
e802cabd0f Move Framework detail to just under the fold
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-19 09:49:27 +00:00
Benjamin S. Leveritt
dfabf2d295 Update Subs and Loading docs
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-18 10:20:58 +00:00
Benjamin S. Leveritt
834607f8bc Update subs doc
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-18 10:20:53 +00:00
Benjamin S. Leveritt
bda61d8440 Fix version and date
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-03-18 10:20:37 +00:00
Guido D'Orsi
3d882f0442 fix: update resolve in the new music player action 2025-03-14 15:25:23 +01:00
Guido D'Orsi
f61d568c9d Merge remote-tracking branch 'origin/main' into 0-12-0 2025-03-14 15:21:09 +01:00
Guido D'Orsi
6dd02d289c chore: fix a type error in the tests 2025-03-11 17:22:26 +01:00
Guido D'Orsi
33a4944ba3 Merge remote-tracking branch 'origin/jazz-581-add-docs' into 0-12-0 2025-03-11 17:17:26 +01:00
Guido D'Orsi
e367b6056d Merge remote-tracking branch 'origin/main' into jazz-581-rfc-new-deep-loading-api 2025-03-11 17:14:38 +01:00
Guido D'Orsi
f3f56b9be0 docs: clarifications 2025-03-06 14:42:30 +01:00
Guido D'Orsi
4cae6bad34 Merge 2025-03-06 14:38:01 +01:00
Guido D'Orsi
17f2ef57de docs: bump the upgrade version 2025-03-06 14:37:19 +01:00
Guido D'Orsi
3a4d111a37 Merge remote-tracking branch 'origin/main' into jazz-581-rfc-new-deep-loading-api 2025-03-06 14:33:18 +01:00
Guido D'Orsi
1e18c7f5fc Merge remote-tracking branch 'origin/0-11-0' into jazz-581-rfc-new-deep-loading-api 2025-02-28 14:58:32 +01:00
Guido D'Orsi
8c7a6b27ed docs: refine the examples and align the feature descripted with the implementation 2025-02-26 18:23:02 +01:00
Guido D'Orsi
91f96e1188 Merge branch 'jazz-581-rfc-new-deep-loading-api' into jazz-581-add-docs 2025-02-26 18:12:56 +01:00
Guido D'Orsi
28dac10723 Merge pull request #1517 from garden-co/fix/optional-fields-acl
feat(deepLoading): return undefined when an optional field is not accessible
2025-02-26 14:58:27 +01:00
Guido D'Orsi
9cb11e38dd feat(deepLoading): return undefined when an optional field is not accessible 2025-02-26 12:56:06 +01:00
Guido D'Orsi
f3e4bacb33 Merge remote-tracking branch 'origin/main' into jazz-581-rfc-new-deep-loading-api 2025-02-25 17:46:22 +01:00
Guido D'Orsi
626d43f07b Merge remote-tracking branch 'origin/main' into jazz-581-rfc-new-deep-loading-api 2025-02-21 18:27:14 +01:00
Benjamin S. Leveritt
1f5d073035 Update homepage/homepage/app/(docs)/docs/[framework]/[...slug]/upgrade/0-11-0.mdx
Co-authored-by: Guido D'Orsi <guido@float.com>
2025-02-13 17:34:20 +00:00
Benjamin S. Leveritt
a3b607e799 Update homepage/homepage/app/(docs)/docs/[framework]/[...slug]/upgrade/0-11-0.mdx
Co-authored-by: Guido D'Orsi <guido@float.com>
2025-02-13 17:34:06 +00:00
Benjamin S. Leveritt
8fb93502af Update homepage/homepage/app/(docs)/docs/[framework]/[...slug]/upgrade/0-11-0.mdx
Co-authored-by: Guido D'Orsi <guido@float.com>
2025-02-13 17:33:49 +00:00
Benjamin S. Leveritt
36774122e0 Updates react guide for new resolve API
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-13 16:08:52 +00:00
Benjamin S. Leveritt
a6923128c1 Adds upgrade guide
Signed-off-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-13 15:56:11 +00:00
Guido D'Orsi
706ca62feb Merge pull request #1363 from garden-co/unauthorized-deep-loading
fix: check CoValue permissions when loading/subscribing
2025-02-13 09:55:16 +01:00
Guido D'Orsi
01523dcca3 fix: check CoValue permissions when loading/subscribing 2025-02-13 09:19:19 +01:00
Guido D'Orsi
77f039b561 Merge remote-tracking branch 'origin/main' into jazz-581-rfc-new-deep-loading-api 2025-02-13 09:18:47 +01:00
Guido D'Orsi
d661ba77be chore: improve pre-release comment output 2025-02-12 11:32:05 +01:00
Guido D'Orsi
f8fbc59b6f Merge remote-tracking branch 'origin/main' into jazz-581-rfc-new-deep-loading-api 2025-02-11 16:26:12 +01:00
Guido D'Orsi
cce0d22007 fix: update resolve property 2025-02-11 15:15:44 +01:00
Guido D'Orsi
e3ff76e9cb Merge remote-tracking branch 'origin/authv2' into jazz-581-rfc-new-deep-loading-api 2025-02-11 15:12:51 +01:00
Guido D'Orsi
4cbf71bff7 Merge pull request #1338 from garden-co/new-deep-loading-extra-props-fix
fix: disallow extra props in the resolve type
2025-02-11 14:38:43 +01:00
Guido D'Orsi
ceb060243a fix: disallow extra props in the resolve type 2025-02-10 20:04:32 +01:00
Anselm
a70bebb96a Clean up new deep-loading API 2025-01-29 14:39:16 +00:00
Anselm
b3b2507c35 Merge branch 'main' into jazz-581-rfc-new-deep-loading-api 2025-01-27 11:36:08 +00:00
Anselm
6a8fa16b49 Fix form and chat-rn-clerk examples 2024-12-16 15:56:48 +00:00
Anselm
1f08807701 Merge branch 'main' into jazz-581-rfc-new-deep-loading-api 2024-12-16 15:37:39 +00:00
Anselm
ba4a7f6170 Remove temp vite config 2024-12-16 15:35:06 +00:00
Anselm
a2854e3602 Upgrade to minor version change 2024-12-16 11:25:00 +00:00
Anselm
4ea87dc494 Add changeset 2024-12-16 10:55:51 +00:00
Anselm
d8c87c5314 Use $each instead of each 2024-12-16 10:54:25 +00:00
Anselm
46f624a12e Replace 'items' with 'each' 2024-12-13 16:28:40 +00:00
Anselm
86ce770f38 Implement clearer syntax for deep loading 2024-12-13 15:12:42 +00:00
213 changed files with 5569 additions and 4579 deletions

View File

@@ -0,0 +1,18 @@
---
"jazz-tailwind-demo-auth-starter": patch
"file-share-svelte": patch
"jazz-password-manager": patch
"version-history": patch
"passkey-svelte": patch
"chat-rn-clerk": patch
"jazz-example-music-player": patch
"passphrase": patch
"multiauth": patch
"reactions": patch
"passkey": patch
"clerk": patch
"jazz-example-pets": patch
"jazz-example-todo": patch
---
Removed when="singedUp" from examples apps' Jazz providers. This is a really niche use-case option and can lead to broken-feeling experiences when anonymous users try to load something.

View File

@@ -0,0 +1,6 @@
---
"jazz-tools": minor
"cojson": minor
---
Check CoValue access permissions when loading

View File

@@ -0,0 +1,5 @@
---
"create-jazz-app": patch
---
add directory param to create-jazz-app

View File

@@ -0,0 +1,6 @@
---
"jazz-inspector": patch
"jazz-inspector-app": patch
---
UI and JSON display improvements

View File

@@ -0,0 +1,5 @@
---
"jazz-tools": minor
---
Implement new API for deep loading

View File

@@ -0,0 +1,5 @@
---
"cojson": minor
---
Return the EVERYONE role if the account is not direct a member of the group

View File

@@ -28,7 +28,7 @@ export default function Conversation() {
const { me } = useAccount();
const [chat, setChat] = useState<Chat>();
const [message, setMessage] = useState("");
const loadedChat = useCoState(Chat, chat?.id, [{}]);
const loadedChat = useCoState(Chat, chat?.id, { resolve: { $each: true } });
const navigation = useNavigation();
const [isUploading, setIsUploading] = useState(false);
@@ -71,7 +71,7 @@ export default function Conversation() {
const loadChat = async (chatId: ID<Chat>) => {
try {
const chat = await Chat.load(chatId, me, []);
const chat = await Chat.load(chatId, me);
setChat(chat);
} catch (error) {
console.log("Error loading chat", error);

View File

@@ -12,7 +12,6 @@ export function JazzAndAuth({ children }: PropsWithChildren) {
storage="sqlite"
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
when: "signedUp", // This makes the app work in local mode when the user is not authenticated
}}
>
{children}

View File

@@ -20,7 +20,7 @@ import { Chat, Message } from "./schema";
export default function ChatScreen({ navigation }: { navigation: any }) {
const { me, logOut } = useAccount();
const [chatId, setChatId] = useState<ID<Chat>>();
const loadedChat = useCoState(Chat, chatId, [{}]);
const loadedChat = useCoState(Chat, chatId, { resolve: { $each: true } });
const [message, setMessage] = useState("");
const profile = useCoState(Profile, me._refs.profile?.id, {});

View File

@@ -11,12 +11,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example chat-vue --project-name chat-vue
npx create-jazz-app@latest chat-vue-app --example chat-vue
```
Go to the new project directory.
```bash
cd chat-vue
cd chat-vue-app
```
Run the dev server.

View File

@@ -49,7 +49,7 @@ export default defineComponent({
},
},
setup(props) {
const chat = useCoState(Chat, props.chatId, [{}]);
const chat = useCoState(Chat, props.chatId, { resolve: { $each: true } });
const showNLastMessages = ref(30);
const displayedMessages = computed(() => {

View File

@@ -13,12 +13,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example chat --project-name chat
npx create-jazz-app@latest chat-app --example chat
```
Go to the new project directory.
```bash
cd chat
cd chat-app
```
Run the dev server.

View File

@@ -16,6 +16,7 @@
"clsx": "^2.0.0",
"hash-slash": "workspace:*",
"jazz-browser-media-images": "workspace:*",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",

View File

@@ -1,6 +1,7 @@
import { apiKey } from "@/apiKey.ts";
import { getRandomUsername, inIframe, onChatLoad } from "@/util.ts";
import { useIframeHashRouter } from "hash-slash";
import { JazzInspector } from "jazz-inspector";
import { JazzProvider, useAccount } from "jazz-react";
import { Group, ID } from "jazz-tools";
import { StrictMode } from "react";
@@ -61,6 +62,7 @@ createRoot(document.getElementById("root")!).render(
defaultProfileName={defaultProfileName}
>
<App />
<JazzInspector />
</JazzProvider>
</StrictMode>
</ThemeProvider>,

View File

@@ -17,8 +17,8 @@ import {
} from "./ui.tsx";
export function ChatScreen(props: { chatID: ID<Chat> }) {
const chat = useCoState(Chat, props.chatID, { resolve: { $each: true } });
const account = useAccount();
const chat = useCoState(Chat, props.chatID, [{}]);
const [showNLastMessages, setShowNLastMessages] = useState(30);
if (!chat)

View File

@@ -15,12 +15,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example clerk --project-name clerk
npx create-jazz-app@latest clerk-app --example clerk
```
Go to the new project directory.
```bash
cd clerk
cd clerk-app
```
Run the dev server.

View File

@@ -21,7 +21,6 @@ function JazzProvider({ children }: { children: React.ReactNode }) {
clerk={clerk}
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
when: "signedUp", // This makes the app work in local mode when the user is not authenticated
}}
>
{children}

View File

@@ -27,7 +27,6 @@
AccountSchema={FileShareAccount}
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
when: "signedUp",
}}
>
<PasskeyAuthBasicUI appName="File Share">

View File

@@ -28,12 +28,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example form --project-name form
npx create-jazz-app@latest form-app --example form
```
Go to the new project directory.
```bash
cd form
cd form-app
```
Run the dev server.

View File

@@ -12,7 +12,9 @@ import {
} from "./schema.ts";
export function CreateOrder() {
const { me } = useAccount({ root: { draft: {}, orders: [] } });
const { me } = useAccount({
resolve: { root: { draft: true, orders: true } },
});
const router = useIframeHashRouter();
const [errors, setErrors] = useState<string[]>([]);
@@ -60,7 +62,7 @@ function CreateOrderForm({
onSave: (draft: DraftBubbleTeaOrder) => void;
}) {
const draft = useCoState(DraftBubbleTeaOrder, id, {
addOns: [],
resolve: { addOns: true },
});
if (!draft) return;

View File

@@ -2,7 +2,7 @@ import { useAccount } from "jazz-react";
export function DraftIndicator() {
const { me } = useAccount({
root: { draft: {} },
resolve: { root: { draft: true } },
});
if (me?.root.draft?.hasChanges) {

View File

@@ -6,7 +6,7 @@ import { OrderThumbnail } from "./OrderThumbnail.tsx";
import { BubbleTeaOrder } from "./schema.ts";
export function EditOrder(props: { id: ID<BubbleTeaOrder> }) {
const order = useCoState(BubbleTeaOrder, props.id, []);
const order = useCoState(BubbleTeaOrder, props.id);
if (!order) return;

View File

@@ -4,7 +4,7 @@ import { OrderThumbnail } from "./OrderThumbnail.tsx";
export function Orders() {
const { me } = useAccount({
root: { orders: [] },
resolve: { root: { orders: true } },
});
return (

View File

@@ -15,12 +15,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example image-upload --project-name image-upload
npx create-jazz-app@latest image-upload-app --example image-upload
```
Go to the new project directory.
```bash
cd image-upload
cd image-upload-app
```
Run the dev server.

View File

@@ -11,27 +11,19 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"jazz-inspector": "workspace:*",
"clsx": "^2.0.0",
"cojson": "workspace:0.11.5",
"cojson-transport-ws": "workspace:0.11.5",
"hash-slash": "workspace:0.2.2",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-use": "^17.4.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uniqolor": "^1.1.0"
"react-use": "^17.4.0"
},
"devDependencies": {
"@types/qrcode": "^1.5.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react-swc": "^3.3.2",

View File

@@ -1,92 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--radius: 0.5rem;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 60 9.1% 97.8%;
--primary-foreground: 24 9.8% 10%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 24 5.7% 82.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
margin: 0;
padding: 0;
}
}
.animate-in {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateZ(400px) translateY(30px) scale(1.05);
opacity: 0.4;
}
to {
transform: translateZ(0) scale(1);
opacity: 1;
}
}

View File

@@ -1,18 +0,0 @@
export function LinkIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-3 h-3"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
/>
</svg>
);
}

View File

@@ -1,39 +0,0 @@
import React from "react";
import { PageInfo } from "./types";
interface BreadcrumbsProps {
path: PageInfo[];
onBreadcrumbClick: (index: number) => void;
}
export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
path,
onBreadcrumbClick,
}) => {
return (
<div className="z-20 relative bg-indigo-400/10 backdrop-blur-sm rounded-lg inline-flex px-2 py-1 whitespace-pre transition-all items-center space-x-1 min-h-10">
<button
onClick={() => onBreadcrumbClick(-1)}
className="flex items-center justify-center p-1 rounded-sm hover:bg-indigo-500/10 transition-colors"
aria-label="Go to home"
>
<img src="jazz-logo.png" alt="Jazz Logo" className="size-5" />
</button>
{path.map((page, index) => {
return (
<span key={index} className="inline-block first:pl-1 last:pr-1">
{index === 0 ? null : (
<span className="text-indigo-500/30">{" / "}</span>
)}
<button
onClick={() => onBreadcrumbClick(index)}
className="text-indigo-700 hover:underline"
>
{index === 0 ? page.name || "Root" : page.name}
</button>
</span>
);
})}
</div>
);
};

View File

@@ -1,344 +0,0 @@
import {
CoID,
LocalNode,
RawBinaryCoStream,
RawCoStream,
RawCoValue,
} from "cojson";
import { base64URLtoBytes } from "cojson";
import { BinaryStreamItem, BinaryStreamStart, CoStreamItem } from "cojson";
import { JsonObject, JsonValue } from "cojson";
import { ArrowDownToLine } from "lucide-react";
import { useEffect, useState } from "react";
import { PageInfo } from "./types";
import { AccountOrGroupPreview } from "./value-renderer";
// typeguard for BinaryStreamStart
function isBinaryStreamStart(item: unknown): item is BinaryStreamStart {
return (
typeof item === "object" &&
item !== null &&
"type" in item &&
item.type === "start"
);
}
function detectCoStreamType(value: RawCoStream | RawBinaryCoStream) {
const firstKey = Object.keys(value.items)[0];
if (!firstKey)
return {
type: "unknown",
};
const items = value.items[firstKey as never]?.map((v) => v.value);
if (!items)
return {
type: "unknown",
};
const firstItem = items[0];
if (!firstItem)
return {
type: "unknown",
};
// This is a binary stream
if (isBinaryStreamStart(firstItem)) {
return {
type: "binary",
items: items as BinaryStreamItem[],
};
} else {
return {
type: "coStream",
};
}
}
async function getBlobFromCoStream({
items,
onlyFirstChunk = false,
}: {
items: BinaryStreamItem[];
onlyFirstChunk?: boolean;
}) {
if (onlyFirstChunk && items.length > 1) {
items = items.slice(0, 2);
}
const chunks: Uint8Array[] = [];
const binary_U_prefixLength = 8;
let lastProgressUpdate = Date.now();
for (const item of items.slice(1)) {
if (item.type === "end") {
break;
}
if (item.type !== "chunk") {
console.error("Invalid binary stream chunk", item);
return undefined;
}
const chunk = base64URLtoBytes(item.chunk.slice(binary_U_prefixLength));
// totalLength += chunk.length;
chunks.push(chunk);
if (Date.now() - lastProgressUpdate > 100) {
lastProgressUpdate = Date.now();
}
}
const defaultMime = "mimeType" in items[0] ? items[0].mimeType : null;
const blob = new Blob(chunks, defaultMime ? { type: defaultMime } : {});
const mimeType =
defaultMime === "" ? await detectPDFMimeType(blob) : defaultMime;
return {
blob,
mimeType: mimeType as string,
unfinishedChunks: items.length > 1,
totalSize:
"totalSizeBytes" in items[0]
? (items[0].totalSizeBytes as number)
: undefined,
};
}
const detectPDFMimeType = async (blob: Blob): Promise<string> => {
const arrayBuffer = await blob.slice(0, 4).arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const header = uint8Array.reduce(
(acc, byte) => acc + String.fromCharCode(byte),
"",
);
if (header === "%PDF") {
return "application/pdf";
}
return "application/octet-stream";
};
const BinaryDownloadButton = ({
pdfBlob,
fileName = "document",
label,
mimeType,
}: {
pdfBlob: Blob;
mimeType?: string;
fileName?: string;
label: string;
}) => {
const downloadFile = () => {
const url = URL.createObjectURL(
new Blob([pdfBlob], mimeType ? { type: mimeType } : {}),
);
const link = document.createElement("a");
link.href = url;
link.download =
mimeType === "application/pdf" ? `${fileName}.pdf` : fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
return (
<button
className="flex items-center gap-2 px-2 py-1 text-gray-900 border border-gray-900/10 bg-clip-border shadow-sm transition-colors rounded bg-gray-50 text-sm"
onClick={downloadFile}
>
<ArrowDownToLine size={16} />
{label}
{/* Download {mimeType === "application/pdf" ? "PDF" : "File"} */}
</button>
);
};
const LabelContentPair = ({
label,
content,
}: {
label: string;
content: React.ReactNode;
}) => {
return (
<div className="flex flex-col gap-1.5 ">
<span className="uppercase text-xs font-medium text-gray-600 tracking-wide">
{label}
</span>
<span>{content}</span>
</div>
);
};
function RenderCoBinaryStream({
value,
items,
}: {
items: BinaryStreamItem[];
value: RawBinaryCoStream;
}) {
const [file, setFile] = useState<
| {
blob: Blob;
mimeType: string;
unfinishedChunks: boolean;
totalSize: number | undefined;
}
| undefined
| null
>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// load only the first chunk to get the mime type and size
getBlobFromCoStream({
items,
onlyFirstChunk: true,
})
.then((v) => {
if (v) {
setFile(v);
if (v.mimeType.includes("image")) {
// If it's an image, load the full blob
getBlobFromCoStream({
items,
}).then((s) => {
if (s) setFile(s);
});
}
}
})
.finally(() => setIsLoading(false));
}, [items]);
if (!isLoading && !file) return <div>No blob</div>;
if (isLoading) return <div>Loading...</div>;
if (!file) return <div>No blob</div>;
const { blob, mimeType } = file;
const sizeInKB = (file.totalSize || 0) / 1024;
return (
<div className="space-y-8 mt-4">
<div className="grid grid-cols-3 gap-2 max-w-3xl">
<LabelContentPair
label="Mime Type"
content={
<span className="font-mono bg-gray-100 rounded px-2 py-1 text-sm">
{mimeType || "No mime type"}
</span>
}
/>
<LabelContentPair
label="Size"
content={<span>{sizeInKB.toFixed(2)} KB</span>}
/>
<LabelContentPair
label="Download"
content={
<BinaryDownloadButton
fileName={value.id.toString()}
pdfBlob={blob}
mimeType={mimeType}
label={
mimeType === "application/pdf"
? "Download PDF"
: "Download File"
}
/>
}
/>
</div>
{mimeType === "image/png" || mimeType === "image/jpeg" ? (
<LabelContentPair
label="Preview"
content={
<div className="bg-gray-50 p-3 rounded-sm">
<RenderBlobImage blob={blob} />
</div>
}
/>
) : null}
</div>
);
}
function RenderCoStream({
value,
node,
}: {
value: RawCoStream;
node: LocalNode;
}) {
const streamPerUser = Object.keys(value.items);
const userCoIds = streamPerUser.map((stream) => stream.split("_session")[0]);
return (
<div className="grid grid-cols-3 gap-2">
{userCoIds.map((id, idx) => (
<div
className="bg-gray-100 p-3 rounded-lg transition-colors overflow-hidden bg-white border hover:bg-gray-100/5 cursor-pointer shadow-sm"
key={id}
>
<AccountOrGroupPreview coId={id as CoID<RawCoValue>} node={node} />
{/* @ts-expect-error - TODO: fix types */}
{value.items[streamPerUser[idx]]?.map(
(item: CoStreamItem<JsonValue>) => (
<div>
{new Date(item.madeAt).toLocaleString()}{" "}
{JSON.stringify(item.value)}
</div>
),
)}
</div>
))}
</div>
);
}
export function CoStreamView({
value,
node,
}: {
data: JsonObject;
onNavigate: (pages: PageInfo[]) => void;
node: LocalNode;
value: RawCoStream;
}) {
// if (!value) return <div>No value</div>;
const streamType = detectCoStreamType(value);
if (streamType.type === "binary") {
if (streamType.items === undefined) {
return <div>No binary stream</div>;
}
return (
<RenderCoBinaryStream
value={value as RawBinaryCoStream}
items={streamType.items}
/>
);
}
if (streamType.type === "coStream") {
return <RenderCoStream value={value} node={node} />;
}
if (streamType.type === "unknown") return <div>Unknown stream type</div>;
return <div>Unknown stream type</div>;
}
function RenderBlobImage({ blob }: { blob: Blob }) {
const urlCreator = window.URL || window.webkitURL;
return <img src={urlCreator.createObjectURL(blob)} />;
}

View File

@@ -1,65 +0,0 @@
import clsx from "clsx";
import { CoID, LocalNode, RawCoValue } from "cojson";
import { JsonObject } from "cojson";
import { ResolveIcon } from "./type-icon";
import { PageInfo, isCoId } from "./types";
import { CoMapPreview, ValueRenderer } from "./value-renderer";
export function GridView({
data,
onNavigate,
node,
}: {
data: JsonObject;
onNavigate: (pages: PageInfo[]) => void;
node: LocalNode;
}) {
const entries = Object.entries(data);
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-2">
{entries.map(([key, child], childIndex) => (
<div
key={childIndex}
className={clsx(
"bg-gray-100 p-3 rounded-lg transition-colors overflow-hidden",
isCoId(child)
? "bg-white border hover:bg-gray-100/5 cursor-pointer shadow-sm"
: "bg-gray-50",
)}
onClick={() =>
isCoId(child) &&
onNavigate([{ coId: child as CoID<RawCoValue>, name: key }])
}
>
<h3 className="truncate">
{isCoId(child) ? (
<span className="font-medium flex justify-between">
{key}
<div className="px-2 py-1 text-xs bg-gray-100 rounded">
<ResolveIcon coId={child as CoID<RawCoValue>} node={node} />
</div>
</span>
) : (
<span>{key}</span>
)}
</h3>
<div className="mt-2 text-sm">
{isCoId(child) ? (
<CoMapPreview coId={child as CoID<RawCoValue>} node={node} />
) : (
<ValueRenderer
json={child}
onCoIDClick={(coId) => {
onNavigate([{ coId, name: key }]);
}}
compact
/>
)}
</div>
</div>
))}
</div>
);
}

View File

@@ -1,7 +1,6 @@
import { LocalNode } from "cojson";
import { Breadcrumbs } from "./breadcrumbs";
import { PageStack } from "./page-stack";
import { PageInfo } from "./types";
import { Breadcrumbs, PageStack } from "jazz-inspector";
import type { PageInfo } from "jazz-inspector";
import { usePagePath } from "./use-page-path";
export default function CoJsonViewer({

View File

@@ -9,10 +9,15 @@ import {
} from "cojson";
import { createWebSocketPeer } from "cojson-transport-ws";
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import { Trash2 } from "lucide-react";
import {
Breadcrumbs,
Button,
Icon,
Input,
PageStack,
Select,
} from "jazz-inspector";
import React, { useState, useEffect } from "react";
import { Breadcrumbs } from "./breadcrumbs";
import { PageStack } from "./page-stack";
import { usePagePath } from "./use-page-path";
import { resolveCoValue, useResolvedCoValue } from "./use-resolve-covalue";
@@ -121,15 +126,23 @@ export default function CoJsonViewerApp() {
}
return (
<div className="w-full h-screen bg-gray-100 p-4 overflow-hidden flex flex-col">
<div className="flex items-center mb-4 gap-4">
<div
className={clsx(
"h-screen overflow-hidden flex flex-col",
" text-stone-700 bg-white",
"dark:text-stone-300 dark:bg-stone-950",
)}
>
<header className="flex items-center gap-4 p-3">
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
<div className="flex-1">
<form onSubmit={handleCoValueIdSubmit}>
{path.length !== 0 && (
<input
className="border p-2 rounded-lg min-w-[21rem] font-mono"
<Input
className="min-w-[21rem] font-mono"
placeholder="co_z1234567890abcdef123456789"
label="CoValue ID"
hideLabel
value={coValueId}
onChange={(e) =>
setCoValueId(e.target.value as CoID<RawCoValue>)
@@ -145,7 +158,7 @@ export default function CoJsonViewerApp() {
deleteCurrentAccount={deleteCurrentAccount}
localNode={localNode}
/>
</div>
</header>
<PageStack
path={path}
@@ -153,49 +166,39 @@ export default function CoJsonViewerApp() {
goBack={goBack}
addPages={addPages}
>
{!currentAccount ? (
<AddAccountForm addAccount={addAccount} />
) : (
{!currentAccount && <AddAccountForm addAccount={addAccount} />}
{currentAccount && path.length <= 0 && (
<form
onSubmit={handleCoValueIdSubmit}
aria-hidden={path.length !== 0}
className={clsx(
"flex flex-col justify-center items-center gap-2 h-full w-full mb-20 ",
"transition-all duration-150",
path.length > 0
? "opacity-0 -translate-y-2 scale-95"
: "opacity-100",
)}
className="flex flex-col relative -top-6 justify-center gap-2 h-full w-full max-w-sm mx-auto"
>
<fieldset className="flex flex-col gap-2 text-sm">
<h2 className="text-3xl font-medium text-gray-950 text-center mb-4">
Jazz CoValue Inspector
</h2>
<input
className="border p-4 rounded-lg min-w-[21rem] font-mono"
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
onChange={(e) =>
setCoValueId(e.target.value as CoID<RawCoValue>)
}
/>
<button
type="submit"
className="bg-indigo-500 hover:bg-indigo-500/80 text-white px-4 py-2 rounded-md"
>
Inspect
</button>
<hr />
<button
type="button"
className="border inline-block px-2 py-1.5 text-black rounded"
onClick={() => {
setPage(currentAccount.id);
}}
>
Inspect My Account
</button>
</fieldset>
<h1 className="text-lg text-center font-medium mb-4 text-stone-900 dark:text-white">
Jazz CoValue Inspector
</h1>
<Input
label="CoValue ID"
className="font-mono"
hideLabel
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
onChange={(e) => setCoValueId(e.target.value as CoID<RawCoValue>)}
/>
<Button type="submit" variant="primary">
Inspect CoValue
</Button>
<p className="text-center">or</p>
<Button
variant="secondary"
onClick={() => {
setPage(currentAccount.id);
}}
>
Inspect my account
</Button>
</form>
)}
</PageStack>
@@ -217,8 +220,10 @@ function AccountSwitcher({
localNode: LocalNode | null;
}) {
return (
<div className="relative flex items-center gap-1">
<select
<div className="relative flex items-stretch gap-1">
<Select
label="Account to inspect"
className="label:sr-only max-w-96"
value={currentAccount?.id || "add-account"}
onChange={(e) => {
if (e.target.value === "add-account") {
@@ -228,7 +233,6 @@ function AccountSwitcher({
setCurrentAccount(account || null);
}
}}
className="p-2 px-4 bg-gray-100/50 border border-indigo-500/10 backdrop-blur-sm rounded-md text-indigo-700 appearance-none"
>
{accounts.map((account) => (
<option key={account.id} value={account.id}>
@@ -240,15 +244,16 @@ function AccountSwitcher({
</option>
))}
<option value="add-account">Add account</option>
</select>
</Select>
{currentAccount && (
<button
<Button
variant="secondary"
onClick={deleteCurrentAccount}
className="p-3 rounded hover:bg-gray-200 transition-colors"
title="Delete Account"
className="rounded-md p-2 ml-1"
aria-label="Remove account"
>
<Trash2 size={16} className="text-gray-500" />
</button>
<Icon name="delete" className="text-gray-500" />
</Button>
)}
</div>
);
@@ -272,30 +277,34 @@ function AddAccountForm({
return (
<form
onSubmit={handleSubmit}
className="flex flex-col gap-2 max-w-md mx-auto h-full justify-center"
className="flex flex-col gap-3 max-w-md mx-auto h-full justify-center"
>
<h2 className="text-2xl font-medium text-gray-900 mb-3">
Add an Account to Inspect
<h2 className="text-2xl font-medium text-gray-900 dark:text-white">
Add an account to inspect
</h2>
<input
className="border py-2 px-3 rounded-md"
placeholder="Account ID"
<p className="leading-relaxed mb-5">
Use the{" "}
<code className="whitespace-nowrap text-stone-900 dark:text-white font-semibold">
jazz-logged-in-secret
</code>{" "}
local storage key from within your Jazz app for your account
credentials.
</p>
<Input
label="Account ID"
value={id}
placeholder="co_z1234567890abcdef123456789"
onChange={(e) => setId(e.target.value)}
/>
<input
<Input
label="Account secret"
type="password"
className="border py-2 px-3 rounded-md"
placeholder="Account Secret"
value={secret}
onChange={(e) => setSecret(e.target.value)}
/>
<button
type="submit"
className="bg-indigo-500 text-white px-4 py-2 rounded-md"
>
Add Account
</button>
<Button className="mt-3" type="submit">
Add account
</Button>
</form>
);
}

View File

@@ -1,53 +0,0 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import { Page } from "./page"; // Assuming you have a Page component
// Define the structure of a page in the path
interface PageInfo {
coId: CoID<RawCoValue>;
name?: string;
}
// Props for the PageStack component
interface PageStackProps {
path: PageInfo[];
node?: LocalNode | null;
goBack: () => void;
addPages: (pages: PageInfo[]) => void;
children?: React.ReactNode;
}
export function PageStack({
path,
node,
goBack,
addPages,
children,
}: PageStackProps) {
return (
<div className="relative mt-4 h-[calc(100vh-6rem)]">
{children && <div className="absolute inset-0 pb-20">{children}</div>}
{node &&
path.map((page, index) => (
<Page
key={`${page.coId}-${index}`}
coId={page.coId}
node={node}
name={page.name || page.coId}
onHeaderClick={goBack}
onNavigate={addPages}
isTopLevel={index === path.length - 1}
style={{
transform: `translateZ(${(index - path.length + 1) * 200}px) scale(${
1 - (path.length - index - 1) * 0.05
}) translateY(${-(index - path.length + 1) * -4}%)`,
opacity: 1 - (path.length - index - 1) * 0.05,
zIndex: index,
transitionProperty: "transform, opacity",
transitionDuration: "0.3s",
transitionTimingFunction: "ease-out",
}}
/>
))}
</div>
);
}

View File

@@ -1,138 +0,0 @@
import clsx from "clsx";
import { CoID, LocalNode, RawCoStream, RawCoValue } from "cojson";
import { useEffect, useState } from "react";
import { CoStreamView } from "./co-stream-view";
import { GridView } from "./grid-view";
import { TableView } from "./table-viewer";
import { TypeIcon } from "./type-icon";
import { PageInfo } from "./types";
import { useResolvedCoValue } from "./use-resolve-covalue";
import { AccountOrGroupPreview } from "./value-renderer";
type PageProps = {
coId: CoID<RawCoValue>;
node: LocalNode;
name: string;
onNavigate: (newPages: PageInfo[]) => void;
onHeaderClick?: () => void;
isTopLevel?: boolean;
style: React.CSSProperties;
};
export function Page({
coId,
node,
name,
onNavigate,
onHeaderClick,
style,
isTopLevel,
}: PageProps) {
const { value, snapshot, type, extendedType } = useResolvedCoValue(
coId,
node,
);
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
const supportsTableView = type === "colist" || extendedType === "record";
// Automatically switch to table view if the page is a CoMap record
useEffect(() => {
if (supportsTableView) {
setViewMode("table");
}
}, [supportsTableView]);
if (snapshot === "unavailable") {
return <div style={style}>Data unavailable</div>;
}
if (!snapshot) {
return <div style={style}></div>;
}
return (
<div
style={style}
className={clsx(
"absolute inset-0 border border-gray-900/5 bg-clip-padding bg-white rounded-xl shadow-lg p-6 animate-in",
)}
>
{!isTopLevel && (
<div
className="absolute inset-x-0 top-0 h-10"
aria-label="Back"
onClick={() => {
onHeaderClick?.();
}}
aria-hidden="true"
></div>
)}
<div className="flex justify-between items-center mb-4">
<div className="flex flex-col gap-2">
<h2 className="text-2xl font-bold flex items-start flex-col gap-1">
<span>
{name}
{typeof snapshot === "object" && "name" in snapshot ? (
<span className="text-gray-600 font-medium">
{" "}
{
(
snapshot as {
name: string;
}
).name
}
</span>
) : null}
</span>
</h2>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
{type && <TypeIcon type={type} extendedType={extendedType} />}
</span>
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
{coId}
</span>
</div>
</div>
{/* {supportsTableView && (
<button
onClick={toggleViewMode}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
{viewMode === "grid" ? "Table View" : "Grid View"}
</button>
)} */}
</div>
<div className="overflow-auto max-h-[calc(100%-4rem)]">
{type === "costream" ? (
<CoStreamView
data={snapshot}
onNavigate={onNavigate}
node={node}
value={value as RawCoStream}
/>
) : viewMode === "grid" ? (
<GridView data={snapshot} onNavigate={onNavigate} node={node} />
) : (
<TableView data={snapshot} node={node} onNavigate={onNavigate} />
)}
{/* --- */}
{extendedType !== "account" && extendedType !== "group" && (
<div className="text-xs text-gray-500 mt-4">
Owned by{" "}
<AccountOrGroupPreview
coId={value.group.id}
node={node}
showId
onClick={() => {
onNavigate([{ coId: value.group.id, name: "owner" }]);
}}
/>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,133 +0,0 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import { JsonObject } from "cojson";
import { useMemo, useState } from "react";
import { LinkIcon } from "../link-icon";
import { PageInfo } from "./types";
import { useResolvedCoValues } from "./use-resolve-covalue";
import { ValueRenderer } from "./value-renderer";
export function TableView({
data,
node,
onNavigate,
}: {
data: JsonObject;
node: LocalNode;
onNavigate: (pages: PageInfo[]) => void;
}) {
const [visibleRowsCount, setVisibleRowsCount] = useState(10);
const [coIdArray, visibleRows] = useMemo(() => {
const coIdArray = Array.isArray(data)
? data
: Object.values(data).every(
(k) => typeof k === "string" && k.startsWith("co_"),
)
? Object.values(data).map((k) => k as CoID<RawCoValue>)
: [];
const visibleRows = coIdArray.slice(0, visibleRowsCount);
return [coIdArray, visibleRows];
}, [data, visibleRowsCount]);
const resolvedRows = useResolvedCoValues(visibleRows, node);
const hasMore = visibleRowsCount < coIdArray.length;
if (!coIdArray.length) {
return <div>No data to display</div>;
}
if (resolvedRows.length === 0) {
return <div>Loading...</div>;
}
const keys = Array.from(
new Set(resolvedRows.flatMap((item) => Object.keys(item.snapshot || {}))),
);
const loadMore = () => {
setVisibleRowsCount((prevVisibleRows) => prevVisibleRows + 10);
};
return (
<div>
<table className="min-w-full divide-y divide-gray-200">
<thead className="sticky top-0 border-b">
<tr>
{["", ...keys].map((key) => (
<th
key={key}
className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 rounded"
>
{key}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{resolvedRows.slice(0, visibleRowsCount).map((item, index) => (
<tr key={index}>
<td className="px-1 py-0">
<button
onClick={() =>
onNavigate([
{
coId: item.value!.id,
name: index.toString(),
},
])
}
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 hover:text-blue-500 hover:bg-gray-100 rounded"
>
<LinkIcon />
</button>
</td>
{keys.map((key) => (
<td
key={key}
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500"
>
<ValueRenderer
json={(item.snapshot as JsonObject)[key]}
onCoIDClick={(coId) => {
async function handleClick() {
onNavigate([
{
coId: item.value!.id,
name: index.toString(),
},
{
coId: coId,
name: key,
},
]);
}
handleClick();
}}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
<div className="py-4 text-gray-500 flex items-center justify-between gap-2">
<span>
Showing {Math.min(visibleRowsCount, coIdArray.length)} of{" "}
{coIdArray.length}
</span>
{hasMore && (
<div className="text-center">
<button
onClick={loadMore}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Load More
</button>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,47 +0,0 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import {
CoJsonType,
ExtendedCoJsonType,
useResolvedCoValue,
} from "./use-resolve-covalue";
export const TypeIcon = ({
type,
extendedType,
}: {
type: CoJsonType;
extendedType?: ExtendedCoJsonType;
}) => {
const iconMap: Record<ExtendedCoJsonType | CoJsonType, string> = {
record: "{} Record",
image: "🖼️ Image",
comap: "{} CoMap",
costream: "≋ CoStream",
colist: "☰ CoList",
account: "👤 Account",
group: "👥 Group",
};
const iconKey = extendedType || type;
const icon = iconMap[iconKey as keyof typeof iconMap];
return icon ? <span className="font-mono">{icon}</span> : null;
};
export const ResolveIcon = ({
coId,
node,
}: {
coId: CoID<RawCoValue>;
node: LocalNode;
}) => {
const { type, extendedType, snapshot } = useResolvedCoValue(coId, node);
if (snapshot === "unavailable" && !type) {
return <div className="text-gray-600 font-medium">Unavailable</div>;
}
if (!type) return <div className="whitespace-pre w-14 font-mono"> </div>;
return <TypeIcon type={type} extendedType={extendedType} />;
};

View File

@@ -1,9 +0,0 @@
import { CoID, RawCoValue } from "cojson";
export type PageInfo = {
coId: CoID<RawCoValue>;
name?: string;
};
export const isCoId = (coId: unknown): coId is CoID<RawCoValue> =>
typeof coId === "string" && coId.startsWith("co_");

View File

@@ -1,6 +1,6 @@
import { CoID, RawCoValue } from "cojson";
import { PageInfo } from "jazz-inspector";
import { useCallback, useEffect, useState } from "react";
import { PageInfo } from "./types";
export function usePagePath(defaultPath?: PageInfo[]) {
const [path, setPath] = useState<PageInfo[]>(() => {

View File

@@ -1,260 +0,0 @@
import clsx from "clsx";
import { CoID, JsonValue, LocalNode, RawCoValue } from "cojson";
import React, { useEffect, useState } from "react";
import { LinkIcon } from "../link-icon";
import {
isBrowserImage,
resolveCoValue,
useResolvedCoValue,
} from "./use-resolve-covalue";
// Is there a chance we can pass the actual CoValue here?
export function ValueRenderer({
json,
compact,
onCoIDClick,
}: {
json: JsonValue | undefined;
compact?: boolean;
onCoIDClick?: (childNode: CoID<RawCoValue>) => void;
}) {
const [isExpanded, setIsExpanded] = useState(false);
if (typeof json === "undefined" || json === undefined) {
return <span className="text-gray-400">undefined</span>;
}
if (json === null) {
return <span className="text-gray-400">null</span>;
}
if (typeof json === "string" && json.startsWith("co_")) {
return (
<span
className={clsx(
"inline-flex gap-1 items-center",
onCoIDClick && "text-blue-500 cursor-pointer hover:underline",
)}
onClick={() => {
onCoIDClick?.(json as CoID<RawCoValue>);
}}
>
{json}
{onCoIDClick && <LinkIcon />}
</span>
);
}
if (typeof json === "string") {
return (
<span className="text-green-900 font-mono">
{/* <span className="select-none opacity-70">{'"'}</span> */}
{json}
{/* <span className="select-none opacity-70">{'"'}</span> */}
</span>
);
}
if (typeof json === "number") {
return <span className="text-purple-500">{json}</span>;
}
if (typeof json === "boolean") {
return (
<span
className={clsx(
json
? "text-green-700 bg-green-700/5"
: "text-amber-700 bg-amber-500/5",
"font-mono",
"inline-block px-1 py-0.5 rounded",
)}
>
{json.toString()}
</span>
);
}
if (Array.isArray(json)) {
return (
<span title={JSON.stringify(json)}>
Array <span className="text-gray-500">({json.length})</span>
</span>
);
}
if (typeof json === "object") {
return (
<span
title={JSON.stringify(json, null, 2)}
className="inline-block max-w-64"
>
{compact ? (
<span>
Object{" "}
<span className="text-gray-500">({Object.keys(json).length})</span>
<pre className="mt-1 text-sm whitespace-pre-wrap">
{isExpanded
? JSON.stringify(json, null, 2)
: JSON.stringify(json, null, 2)
.split("\n")
.slice(0, 3)
.join("\n") + (Object.keys(json).length > 2 ? "\n..." : "")}
</pre>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-xs text-gray-500 hover:text-gray-700"
>
{isExpanded ? "Show less" : "Show more"}
</button>
</span>
) : (
<pre className="whitespace-pre-wrap">
{JSON.stringify(json, null, 2)}
</pre>
)}
</span>
);
}
return <span>{String(json)}</span>;
}
export const CoMapPreview = ({
coId,
node,
limit = 6,
}: {
coId: CoID<RawCoValue>;
node: LocalNode;
limit?: number;
}) => {
const { value, snapshot, type, extendedType } = useResolvedCoValue(
coId,
node,
);
if (!snapshot) {
return (
<div className="rounded bg-gray-100 animate-pulse whitespace-pre w-24">
{" "}
</div>
);
}
if (snapshot === "unavailable" && !value) {
return <div className="text-gray-500">Unavailable</div>;
}
if (extendedType === "image" && isBrowserImage(snapshot)) {
return (
<div>
<img
src={snapshot.placeholderDataURL}
className="size-8 border-2 border-white drop-shadow-md my-2"
/>
<span className="text-gray-500 text-sm">
{snapshot.originalSize[0]} x {snapshot.originalSize[1]}
</span>
{/* <CoMapPreview coId={value[]} node={node} /> */}
{/* <ProgressiveImg image={value}>
{({ src }) => <img src={src} className={clsx("w-full")} />}
</ProgressiveImg> */}
</div>
);
}
if (extendedType === "record") {
return (
<div>
Record{" "}
<span className="text-gray-500">({Object.keys(snapshot).length})</span>
</div>
);
}
if (type === "colist") {
return (
<div>
List{" "}
<span className="text-gray-500">
({(snapshot as unknown as []).length})
</span>
</div>
);
}
return (
<div className="text-sm flex flex-col gap-2 items-start">
<div className="grid grid-cols-[auto_1fr] gap-2">
{Object.entries(snapshot)
.slice(0, limit)
.map(([key, value]) => (
<React.Fragment key={key}>
<span className="font-medium">{key}: </span>
<span>
<ValueRenderer json={value} />
</span>
</React.Fragment>
))}
</div>
{Object.entries(snapshot).length > limit && (
<div className="text-left text-xs text-gray-500 mt-2">
{Object.entries(snapshot).length - limit} more
</div>
)}
</div>
);
};
export function AccountOrGroupPreview({
coId,
node,
showId = false,
onClick,
}: {
coId: CoID<RawCoValue>;
node: LocalNode;
showId?: boolean;
onClick?: (name?: string) => void;
}) {
const { snapshot, extendedType } = useResolvedCoValue(coId, node);
const [name, setName] = useState<string | null>(null);
useEffect(() => {
if (extendedType === "account") {
resolveCoValue(
(snapshot as unknown as { profile: CoID<RawCoValue> }).profile,
node,
).then(({ snapshot }) => {
if (
typeof snapshot === "object" &&
"name" in snapshot &&
typeof snapshot.name === "string"
) {
setName(snapshot.name);
}
});
}
}, [snapshot, node, extendedType]);
if (!snapshot) return <span>Loading...</span>;
if (extendedType !== "account" && extendedType !== "group") {
return <span>CoID is not an account or group</span>;
}
const displayName = extendedType === "account" ? name || "Account" : "Group";
const displayText = showId ? `${displayName} (${coId})` : displayName;
const props = onClick
? {
onClick: () => onClick(displayName),
className: "text-blue-500 cursor-pointer hover:underline",
}
: {
className: "text-gray-500",
};
return <span {...props}>{displayText}</span>;
}

View File

@@ -1,5 +1,30 @@
import type { Config } from "tailwindcss";
import animate from "tailwindcss-animate";
import plugin from "tailwindcss/plugin";
const stonePalette = {
50: "oklch(0.988281 0.002 75)",
100: "oklch(0.980563 0.002 75)",
200: "oklch(0.917969 0.002 75)",
300: "oklch(0.853516 0.002 75)",
400: "oklch(0.789063 0.002 75)",
500: "oklch(0.726563 0.002 75)",
600: "oklch(0.613281 0.002 75)",
700: "oklch(0.523438 0.002 75)",
800: "oklch(0.412109 0.002 75)",
900: "oklch(0.302734 0.002 75)",
925: "oklch(0.220000 0.002 75)",
950: "oklch(0.193359 0.002 75)",
};
const stonePaletteWithAlpha = { ...stonePalette };
Object.keys(stonePalette).forEach((key) => {
// @ts-ignore
stonePaletteWithAlpha[key] = stonePaletteWithAlpha[key].replace(
")",
"/ <alpha-value>)",
);
});
const config: Config = {
content: [
@@ -18,62 +43,26 @@ const config: Config = {
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
stone: stonePaletteWithAlpha,
gray: stonePaletteWithAlpha,
blue: {
50: "#f5f7ff",
100: "#ebf0fe",
200: "#d6e0fd",
300: "#b3c7fc",
400: "#8aa6f9",
500: "#5870F1",
600: "#3651E7",
700: "#3313F7",
800: "#2A12BE",
900: "#12046A",
950: "#1e1b4b",
DEFAULT: "#3313F7",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [animate],
plugins: [plugin(({ addVariant }) => addVariant("label", "& :is(label)"))],
};
export default config;

View File

@@ -13,11 +13,11 @@ To run this example, you may either:
1. Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example counter --project-name counter
npx create-jazz-app@latest counter-app --example counter
```
2. Navigate to the new project and start the development server.
```bash
cd counter
cd counter-app
npm run dev
```

View File

@@ -19,7 +19,6 @@ createRoot(document.getElementById("root")!).render(
<OmniAuth
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
when: "signedUp", // This makes the app work in local mode when the user is not authenticated
}}
>
<App />

View File

@@ -13,12 +13,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example music-player --project-name music-player
npx create-jazz-app@latest music-player-app --example music-player
```
Go to the new project directory.
```bash
cd music-player
cd music-player-app
```
Run the dev server.

View File

@@ -72,7 +72,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<JazzProvider
sync={{
peer,
when: "signedUp", // This makes the app work in local mode when the user is anonymous
}}
storage="indexedDB"
AccountSchema={MusicaAccount}

View File

@@ -24,10 +24,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
* access rights to CoValues. We get it from the top-level provider `<WithJazz/>`.
*/
const { me } = useAccount({
root: {
rootPlaylist: {},
playlists: [],
},
resolve: { root: { rootPlaylist: true, playlists: true } },
});
const navigate = useNavigate();
@@ -51,8 +48,9 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
const params = useParams<{ playlistId: ID<Playlist> }>();
const playlistId = params.playlistId ?? me?.root._refs.rootPlaylist.id;
const playlist = useCoState(Playlist, playlistId, {
tracks: [],
resolve: { tracks: true },
});
const isRootPlaylist = !params.playlistId;

View File

@@ -27,9 +27,11 @@ export async function uploadMusicTracks(
isExampleTrack: boolean = false,
) {
const { root } = await MusicaAccount.getMe().ensureLoaded({
root: {
rootPlaylist: {
tracks: [],
resolve: {
root: {
rootPlaylist: {
tracks: true,
},
},
},
});
@@ -65,8 +67,10 @@ export async function uploadMusicTracks(
export async function createNewPlaylist() {
const { root } = await MusicaAccount.getMe().ensureLoaded({
root: {
playlists: [],
resolve: {
root: {
playlists: true,
},
},
});
@@ -149,9 +153,11 @@ export async function updateMusicTrackTitle(track: MusicTrack, title: string) {
export async function updateActivePlaylist(playlist?: Playlist) {
const { root } = await MusicaAccount.getMe().ensureLoaded({
root: {
activePlaylist: {},
rootPlaylist: {},
resolve: {
root: {
activePlaylist: true,
rootPlaylist: true,
},
},
});
@@ -160,7 +166,9 @@ export async function updateActivePlaylist(playlist?: Playlist) {
export async function updateActiveTrack(track: MusicTrack) {
const { root } = await MusicaAccount.getMe().ensureLoaded({
root: {},
resolve: {
root: {},
},
});
root.activeTrack = track;
@@ -170,17 +178,23 @@ export async function onAnonymousAccountDiscarded(
anonymousAccount: MusicaAccount,
) {
const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({
root: {
rootPlaylist: {
tracks: [{}],
resolve: {
root: {
rootPlaylist: {
tracks: {
$each: true,
},
},
},
},
});
const me = await MusicaAccount.getMe().ensureLoaded({
root: {
rootPlaylist: {
tracks: [],
resolve: {
root: {
rootPlaylist: {
tracks: true,
},
},
},
});
@@ -197,8 +211,10 @@ export async function onAnonymousAccountDiscarded(
export async function deletePlaylist(playlistId: string) {
const { root } = await MusicaAccount.getMe().ensureLoaded({
root: {
playlists: [],
resolve: {
root: {
playlists: true,
},
},
});

View File

@@ -9,7 +9,7 @@ import { getNextTrack, getPrevTrack } from "./lib/getters";
export function useMediaPlayer() {
const { me } = useAccount({
root: {},
resolve: { root: true },
});
const playState = usePlayState();

View File

@@ -16,8 +16,10 @@ export function InvitePage() {
const playlist = await Playlist.load(playlistId, {});
const me = await MusicaAccount.getMe().ensureLoaded({
root: {
playlists: [],
resolve: {
root: {
playlists: true,
},
},
});

View File

@@ -22,9 +22,13 @@ export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [error, setError] = useState<string | null>(null);
const { me } = useAccount({
root: {
rootPlaylist: {
tracks: [{}],
resolve: {
root: {
rootPlaylist: {
tracks: {
$each: true,
},
},
},
},
});

View File

@@ -30,9 +30,7 @@ export function MusicTrackRow({
const track = useCoState(MusicTrack, trackId);
const { me } = useAccount({
root: {
playlists: [{}],
},
resolve: { root: { playlists: { $each: true } } },
});
const playlists = me?.root.playlists ?? [];

View File

@@ -12,9 +12,7 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
const isPlaying = playState.value === "play";
const activePlaylist = useAccount({
root: {
activePlaylist: {},
},
resolve: { root: { activePlaylist: true } },
}).me?.root.activePlaylist;
useMediaEndListener(mediaPlayer.playNextTrack);
@@ -25,7 +23,7 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
});
const activeTrack = useCoState(MusicTrack, mediaPlayer.activeTrackId, {
waveform: {},
resolve: { waveform: true },
});
if (!activeTrack) return null;

View File

@@ -8,9 +8,7 @@ export function SidePanel() {
const { playlistId } = useParams();
const navigate = useNavigate();
const { me } = useAccount({
root: {
playlists: [{}],
},
resolve: { root: { playlists: { $each: true } } },
});
function handleAllTracksClick(evt: React.MouseEvent<HTMLAnchorElement>) {

View File

@@ -8,7 +8,6 @@ export function Waveform(props: { track: MusicTrack; height: number }) {
const waveformData = useCoState(
MusicTrackWaveform,
track._refs.waveform.id,
{},
)?.data;
const duration = track.duration;

View File

@@ -2,9 +2,11 @@ import { MusicaAccount } from "../1_schema";
export async function getNextTrack() {
const me = await MusicaAccount.getMe().ensureLoaded({
root: {
activePlaylist: {
tracks: [],
resolve: {
root: {
activePlaylist: {
tracks: true,
},
},
},
});
@@ -21,9 +23,11 @@ export async function getNextTrack() {
export async function getPrevTrack() {
const me = await MusicaAccount.getMe().ensureLoaded({
root: {
activePlaylist: {
tracks: [],
resolve: {
root: {
activePlaylist: {
tracks: true,
},
},
},
});

View File

@@ -13,7 +13,7 @@ export function useUploadExampleData() {
async function uploadOnboardingData() {
const me = await MusicaAccount.getMe().ensureLoaded({
root: {},
resolve: { root: true },
});
if (me.root.exampleDataLoaded) return;

View File

@@ -16,12 +16,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example organization --project-name organization
npx create-jazz-app@latest organization-app --example organization
```
Go to the new project directory.
```bash
cd organization
cd organization-app
```
Run the dev server.

View File

@@ -5,11 +5,11 @@ import { Organization } from "./schema.ts";
export function AcceptInvitePage() {
const navigate = useNavigate();
const { me } = useAccount({ root: { organizations: [] } });
const { me } = useAccount({ resolve: { root: { organizations: true } } });
const onAccept = (organizationId: ID<Organization>) => {
if (me?.root?.organizations) {
Organization.load(organizationId, me, []).then((organization) => {
Organization.load(organizationId).then((organization) => {
if (organization) {
// avoid duplicates
const ids = me.root.organizations.map(

View File

@@ -5,7 +5,7 @@ import { Heading } from "./components/Heading.tsx";
export function HomePage() {
const { me } = useAccount({
root: { organizations: [{}] },
resolve: { root: { organizations: true } },
});
if (!me?.root.organizations) return;

View File

@@ -3,7 +3,7 @@ import { UserIcon } from "lucide-react";
export function Layout({ children }: { children: React.ReactNode }) {
const { me, logOut } = useAccount({
root: { draftOrganization: {} },
resolve: { root: { draftOrganization: true } },
});
return (

View File

@@ -13,7 +13,7 @@ export function OrganizationPage() {
.organizationId;
const organization = useCoState(Organization, paramOrganizationId, {
projects: [],
resolve: { projects: true },
});
if (!organization) return <p>Loading organization...</p>;

View File

@@ -8,7 +8,7 @@ import { OrganizationForm } from "./OrganizationForm.tsx";
export function CreateOrganization() {
const { me } = useAccount({
root: { draftOrganization: {}, organizations: [] },
resolve: { root: { draftOrganization: true, organizations: true } },
});
const [errors, setErrors] = useState<string[]>([]);
const navigate = useNavigate();

View File

@@ -7,7 +7,7 @@ import { Organization } from "../schema.ts";
export function OrganizationSelector({ className }: { className?: string }) {
const { me } = useAccount({
root: { organizations: [{}] },
resolve: { root: { organizations: { $each: true } } },
});
const navigate = useNavigate();

View File

@@ -21,12 +21,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example passkey-svelte --project-name passkey-svelte
npx create-jazz-app@latest passkey-svelte-app --example passkey-svelte
```
Go to the new project directory.
```bash
cd passkey-svelte
cd passkey-svelte-app
```
Run the dev server.

View File

@@ -8,7 +8,6 @@
<JazzProvider
sync={{
peer: `wss://cloud.jazz.tools/?key={apiKey}`,
when: "signedUp",
}}
>
<PasskeyAuthBasicUI appName="minimal-svelte-auth-passkey">

View File

@@ -15,12 +15,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example passkey --project-name passkey
npx create-jazz-app@latest passkey-app --example passkey
```
Go to the new project directory.
```bash
cd passkey
cd passkey-app
```
Run the dev server.

View File

@@ -10,7 +10,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
<JazzProvider
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
when: "signedUp",
}}
>
<PasskeyAuthBasicUI appName="Jazz Minimal Auth Passkey Example">

View File

@@ -12,12 +12,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example passphrase --project-name passphrase
npx create-jazz-app@latest passphrase-app --example passphrase
```
Go to the new project directory.
```bash
cd passphrase
cd passphrase-app
```
Run the dev server.

View File

@@ -142,7 +142,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
<JazzProvider
sync={{
peer: "wss://cloud.jazz.tools/?key=minimal-auth-passphrase-example@garden.co",
when: "signedUp",
}}
>
<PassphraseAuthBasicUI

View File

@@ -15,12 +15,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example password-manager --project-name password-manager
npx create-jazz-app@latest password-manager-app --example password-manager
```
Go to the new project directory.
```bash
cd password-manager
cd password-manager-app
```
Run the dev server.

View File

@@ -12,7 +12,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
AccountSchema={PasswordManagerAccount}
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
when: "signedUp",
}}
>
<PasskeyAuthBasicUI appName="Jazz Password Manager">

View File

@@ -43,9 +43,11 @@ const VaultPage: React.FC = () => {
(item): item is Exclude<typeof item, null> => !!item,
) || [],
);
const folders = useCoState(FolderList, me.root?._refs.folders?.id, [
{ items: [{}] },
]);
const folders = useCoState(FolderList, me.root?._refs.folders?.id, {
resolve: {
$each: { items: { $each: true } },
},
});
const [selectedFolder, setSelectedFolder] = useState<Folder | undefined>();
const [isNewItemModalOpen, setIsNewItemModalOpen] = useState(false);

View File

@@ -60,11 +60,9 @@ export async function addSharedFolder(
me: PasswordManagerAccount,
) {
const [sharedFolder, account] = await Promise.all([
Folder.load(sharedFolderId, me, {}),
PasswordManagerAccount.load(me.id, me, {
root: {
folders: [],
},
Folder.load(sharedFolderId),
PasswordManagerAccount.load(me.id, {
resolve: { root: { folders: true } },
}),
]);

View File

@@ -13,12 +13,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example pets --project-name pets
npx create-jazz-app@latest pets-app --example pets
```
Go to the new project directory.
```bash
cd pets
cd pets-app
```
Run the dev server.

View File

@@ -52,7 +52,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<JazzProvider
sync={{
peer,
when: "signedUp",
}}
AccountSchema={PetAccount}
>

View File

@@ -13,12 +13,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example reactions --project-name reactions
npx create-jazz-app@latest reactions-app --example reactions
```
Go to the new project directory.
```bash
cd reactions
cd reactions-app
```
Run the dev server.

View File

@@ -14,7 +14,7 @@ const reactionEmojiMap: {
};
export function ReactionsScreen(props: { id: ID<Reactions> }) {
const reactions = useCoState(Reactions, props.id, []);
const reactions = useCoState(Reactions, props.id);
if (!reactions) return;

View File

@@ -10,7 +10,6 @@ createRoot(document.getElementById("root")!).render(
<JazzProvider
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
when: "signedUp",
}}
>
<PasskeyAuthBasicUI appName="Jazz Reactions Example">

View File

@@ -11,12 +11,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example todo-vue --project-name todo-vue
npx create-jazz-app@latest todo-vue-app --example todo-vue
```
Go to the new project directory.
```bash
cd todo-vue
cd todo-vue-app
```
Run the dev server.

View File

@@ -4,15 +4,15 @@
<div class="section-header">
<h2>Folders</h2>
<div class="new-folder">
<input
v-model="newFolderName"
placeholder="New folder name"
<input
v-model="newFolderName"
placeholder="New folder name"
class="input"
/>
<button class="btn btn-primary" @click="createFolder">Create</button>
</div>
</div>
<div class="folder-list">
<div
v-for="folder in folders"
@@ -32,9 +32,9 @@
<div class="section-header">
<h2>{{ selectedFolder?.name }}</h2>
<div class="new-todo">
<input
v-model="newTodoTitle"
placeholder="Add a new task"
<input
v-model="newTodoTitle"
placeholder="Add a new task"
class="input"
/>
<button class="btn btn-primary" @click="createTodo">Add</button>
@@ -72,7 +72,9 @@ import { Folder, FolderList, ToDoItem, ToDoList } from "../schema";
const { me } = useAccount();
const computedFoldersId = computed(() => me.value?.root?.folders?.id);
const folders = useCoState(FolderList, computedFoldersId, [{ items: [{}] }]);
const folders = useCoState(FolderList, computedFoldersId, {
resolve: { $each: { items: true } },
});
const selectedFolder = ref<Folder>();
const newFolderName = ref("");

View File

@@ -13,12 +13,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example todo --project-name todo
npx create-jazz-app@latest todo-app --example todo
```
Go to the new project directory.
```bash
cd todo
cd todo-app
```
Run the dev server.

View File

@@ -42,7 +42,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
<JazzProvider
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
when: "signedUp",
}}
AccountSchema={TodoAccount}
>
@@ -122,7 +121,7 @@ export default function App() {
function HomeScreen() {
const { me } = useAccount({
root: { projects: [{}] },
resolve: { root: { projects: { $each: true } } },
});
const navigate = useNavigate();

View File

@@ -13,12 +13,12 @@ You can either
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest --example version-history --project-name version-history
npx create-jazz-app@latest version-history-app --example version-history
```
Go to the new project directory.
```bash
cd version-history
cd version-history-app
```
Run the dev server.

View File

@@ -4,7 +4,9 @@ import { ID } from "jazz-tools";
import { IssueComponent } from "./Issue.tsx";
import { Issue, Project } from "./schema.ts";
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
const project = useCoState(Project, projectID, { issues: [{}] });
const project = useCoState(Project, projectID, {
resolve: { issues: { $each: true } },
});
if (!project) return;

View File

@@ -11,7 +11,6 @@ createRoot(document.getElementById("root")!).render(
<JazzProvider
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
when: "signedUp",
}}
>
<DemoAuthBasicUI appName="Jazz Version History Example">

View File

@@ -0,0 +1,37 @@
import { metaTags } from "@/app/layout";
import { posts } from "@/lib/posts";
import { Feed } from "feed";
import { NextResponse } from "next/server";
export async function GET() {
const feed = new Feed({
title: "Garden Computing Blog",
description: "News from Garden Computing",
id: metaTags.url,
link: `${metaTags.url}/news`,
language: "en",
image: `${metaTags.url}/social-image.png`,
favicon: `${metaTags.url}/favicon.ico`,
copyright: `${new Date().getFullYear()} Garden Computing, Inc.`,
});
posts.forEach((post) => {
feed.addItem({
title: post.meta.title,
description: post.meta.subtitle,
id: post.meta.slug,
link: `${metaTags.url}/news/${post.meta.slug}`,
date: new Date(post.meta.date),
author: [{ name: post.meta.author.name }],
guid: post.meta.slug,
image: `${metaTags.url}${post.meta.coverImage}`,
});
});
return new NextResponse(feed.rss2(), {
headers: {
"Content-Type": "application/xml",
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
},
});
}

View File

@@ -41,7 +41,7 @@ const commitMono = localFont({
display: "swap",
});
const metaTags = {
export const metaTags = {
title: "garden computing",
description:
"Computers are magic. So why do we put up with so much complexity? We believe just a few new ideas can make all the difference.",
@@ -70,6 +70,16 @@ export const metadata: Metadata = {
},
],
},
alternates: {
canonical: metaTags.url,
types: {
"application/rss+xml": `${
process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000"
}/api/rss`,
},
},
};
export default function RootLayout({

View File

@@ -49,7 +49,7 @@ function highlightPlugin() {
token.content.includes("// old"),
);
const isBinnedLine = line.some((token) =>
token.content.includes("// *bin*"),
token.content.includes("// [!code --]"),
);
const isHighlighted = line.some((token) =>
token.content.includes("// *highlight*"),
@@ -70,7 +70,7 @@ function highlightPlugin() {
line
.map((token) => {
let color = isHighlighted ? "currentColor" : token.color;
return `<span style="color: ${color};${isSubduedLine ? "opacity: 0.4;" : ""}">${escape(token.content.replace("// old", "").replace("// *bin*", "").replace("// *highlight*", ""))}</span>`;
return `<span style="color: ${color};${isSubduedLine ? "opacity: 0.4;" : ""}">${escape(token.content.replace("// old", "").replace("// [!code --]", "").replace("// *highlight*", ""))}</span>`;
})
.join("") +
"</span>"

View File

@@ -20,6 +20,7 @@
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",
"clsx": "^2.1.1",
"feed": "^4.2.2",
"gcmp-design-system": "workspace:*",
"lucide-react": "^0.436.0",
"mdast-util-from-markdown": "^2.0.0",

View File

@@ -1,3 +1,4 @@
import { TocProvider } from "@/components/TocProvider";
import { ApiNav } from "@/components/docs/ApiNav";
import DocsLayout from "@/components/docs/DocsLayout";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
@@ -8,8 +9,10 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<DocsLayout nav={<ApiNav />} navIcon="package" navName="API Ref">
<Prose className="overflow-x-hidden lg:flex-1 py-10">{children}</Prose>
</DocsLayout>
<TocProvider>
<DocsLayout nav={<ApiNav />} navIcon="package" navName="API Ref">
<Prose className="overflow-x-hidden lg:flex-1 py-10">{children}</Prose>
</DocsLayout>
</TocProvider>
);
}

View File

@@ -1,15 +0,0 @@
import DocsLayout from "@/components/docs/DocsLayout";
import { DocNav } from "@/components/docs/nav";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
export default function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<DocsLayout nav={<DocNav />}>
<Prose className="max-w-3xl mx-auto lg:flex-1 py-10">{children}</Prose>
</DocsLayout>
);
}

View File

@@ -331,7 +331,7 @@ This works because CoValues
<CodeGroup>
```ts
const unsub = issue.subscribe([], (updatedIssue) => console.log(updatedIssue));
const unsub = issue.subscribe({ resolve: true }, (updatedIssue) => console.log(updatedIssue));
```
</CodeGroup>
@@ -339,7 +339,7 @@ This works because CoValues
<CodeGroup>
```ts
const unsub = Issue.subscribe(issueID, me, [], (updatedIssue) => {
const unsub = Issue.subscribe(issueID, me, { resolve: true }, (updatedIssue) => {
console.log(updatedIssue);
});
```
@@ -358,7 +358,7 @@ This works because CoValues
function useCoState<V extends CoValue>(Schema: CoValueClass<V>, id?: ID<V>): V | undefined {
const [value, setValue] = useState<V>();
useEffect(() => Schema.subscribe(id, [], setValue), [id]);
useEffect(() => Schema.subscribe(id, { resolve: true }, setValue), [id]);
return value;
}
@@ -454,7 +454,7 @@ function App() { // old
const issue = useCoState(Issue, issueID); // old
// old
const createIssue = () => { // old
const group = Group.create({ owner: me });
const group = Group.create();
group.addMember("everyone", "writer");
// old
const newIssue = Issue.create( // old
@@ -543,7 +543,7 @@ function App() { // old
(window.location.search?.replace("?project=", "") || undefined) as ID<Project> | undefined
);
// old
const issue = useCoState(Issue, issueID); // *bin*
const issue = useCoState(Issue, issueID); // [!code --]
// old
const createProject = () => {
const group = Group.create();
@@ -605,6 +605,8 @@ export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
<button onClick={createAndAddIssue}>Create Issue</button>
</div>
</div>
) : project === null ? (
<div>Project not found or access denied</div>
) : (
<div>Loading project...</div>
);
@@ -620,7 +622,7 @@ Two things to note here:
- We only need to use `useCoState` on the Project, and the nested `ListOfIssues` and each `Issue` will be **automatically loaded and subscribed to when we access them.**
- However, because either the `Project`, `ListOfIssues`, or each `Issue` might not be loaded yet, we have to check for them being defined.
### Precise loading depths
### Precise resolve queries
The load-and-subscribe-on-access is a convenient way to have your rendering drive data loading (including in nested components!) and lets you quickly chuck UIs together without worrying too much about the shape of all data you'll need.
@@ -635,7 +637,7 @@ import { IssueComponent } from "./Issue.tsx"; // old
import { useCoState } from "jazz-react"; // old
// old
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {// old
const project = useCoState(Project, projectID, { issues: [{}] });
const project = useCoState(Project, projectID, { resolve: { issues: { $each: true } } });
const createAndAddIssue = () => {// old
project?.issues.push(Issue.create({
@@ -663,7 +665,7 @@ export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {//
```
</CodeGroup>
The loading-depth spec `{ issues: [{}] }` means "in `Project`, load `issues` and load each item in `issues` shallowly". (Since an `Issue` doesn't have any further references, "shallowly" actually means all its properties will be available).
The resolve query `{ resolve: { issues: { $each: true } } }` means "in `Project`, load `issues` and load each item in `issues` deeply". (Since an `Issue` doesn't have any further references, "deeply" actually means all its properties will be available).
- Now, we can get rid of a lot of conditional accesses because we know that once `project` is loaded, `project.issues` and each `Issue` in it will be loaded as well.
- This also results in only one rerender and visual update when everything is loaded, which is faster (especially for long lists) and gives you more control over the loading UX.
@@ -710,7 +712,7 @@ function App() { // old
// old
const createProject = () => { // old
const group = Group.create(); // old
group.addMember("everyone", "writer"); // *bin*
group.addMember("everyone", "writer"); // [!code --]
// old
const newProject = Project.create( // old
{ // old
@@ -747,7 +749,7 @@ import { createInviteLink } from "jazz-react";
// old
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {// old
const project = useCoState(Project, projectID, { issues: [{}] }); // old
const project = useCoState(Project, projectID, { resolve: { issues: { $each: true } } }); // old
const { me } = useAccount();

View File

@@ -1,10 +1,8 @@
import DocsLayout from "@/components/docs/DocsLayout";
import { TocItemsSetter } from "@/components/docs/TocItemsSetter";
import ComingSoonPage from "@/components/docs/coming-soon.mdx";
import { DocNav } from "@/components/docs/nav";
import { docNavigationItems } from "@/lib/docNavigationItems.js";
import { Framework, frameworks } from "@/lib/framework";
import type { Toc } from "@stefanprobst/rehype-extract-toc";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
async function getMdxSource(slugPath: string, framework: string) {
// Try to import the framework-specific file first
@@ -46,7 +44,6 @@ export default async function Page({
}: { params: Promise<{ slug: string[]; framework: string }> }) {
const { slug, framework } = await params;
const slugPath = slug.join("/");
const bodyClassName = "overflow-x-hidden lg:flex-1 py-10 max-w-3xl mx-auto";
try {
const mdxSource = await getMdxSource(slugPath, framework);
@@ -56,19 +53,17 @@ export default async function Page({
const tocItems = (tableOfContents as Toc)?.[0]?.children;
return (
<DocsLayout toc={tocItems} nav={<DocNav />}>
<Prose className={bodyClassName}>
<Content />
</Prose>
</DocsLayout>
<>
<TocItemsSetter items={tocItems} />
<Content />
</>
);
} catch (error) {
return (
<DocsLayout nav={<DocNav />}>
<Prose className={bodyClassName}>
<ComingSoonPage />
</Prose>
</DocsLayout>
<>
<TocItemsSetter items={[]} />
<ComingSoonPage />
</>
);
}
}

View File

@@ -112,7 +112,7 @@ declare module "jazz-react" {// old
## Resolving CoValues starting at `profile` or `root`
<ContentByFramework framework="react">
To use per-user data in your app, you typically use `useAccount` somewhere in a high-level component, specifying which references to resolve using a depth-spec (see [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)).
To use per-user data in your app, you typically use `useAccount` somewhere in a high-level component, specifying which references to resolve using a resolve query (see [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)).
<CodeGroup>
{/* prettier-ignore */}
@@ -163,12 +163,12 @@ export class MyAppAccount extends Account {
profile = co.ref(MyAppProfile);
async migrate(this: MyAppAccount, creationProps?: { name: string }) {
// we specifically need to check for undefined,
// we specifically need to check for undefined,
// because the root might simply be not loaded (`null`) yet
if (this.root === undefined) {
this.root = MyAppRoot.create({
// Using a group to set the owner is always a good idea.
// This way if in the future we want to share
// This way if in the future we want to share
// this coValue we can do so easily.
myChats: ListOfChats.create([], Group.create()),
myContacts: ListOfAccounts.create([], Group.create())
@@ -218,8 +218,8 @@ export class MyAppAccount extends Account {
root: {},
});
// we specifically need to check for undefined,
// because myBookmarks might simply be not loaded (`null`) yet
// we specifically need to check for undefined,
// because myBookmarks might simply be not loaded (`null`) yet
if (root.myBookmarks === undefined) {
root.myBookmarks = ListOfBookmarks.create([], Group.create());
}
@@ -228,9 +228,9 @@ export class MyAppAccount extends Account {
```
</CodeGroup>
{/*
TODO: Add best practice: only ever add fields
{/*
TODO: Add best practice: only ever add fields
Note: explain and reassure that there will be more guardrails in the future
Note: explain and reassure that there will be more guardrails in the future
https://github.com/garden-co/jazz/issues/1160
*/}

View File

@@ -29,9 +29,9 @@ Here is what's changed in this release:
{/* prettier-ignore */}
```tsx
<JazzProvider
auth={authMethod} // removed // *bin*
peer="wss://cloud.jazz.tools/?key=you@example.com" // moved into sync // *bin*
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} // *add*
auth={authMethod} // removed // [!code --]
peer="wss://cloud.jazz.tools/?key=you@example.com" // moved into sync // [!code --]
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} // [!code ++]
>
<App />
</JazzProvider>

View File

@@ -0,0 +1,170 @@
import { CodeGroup } from '@/components/forMdx'
export const metadata = { title: "Jazz 0.12.0 - Deeply resolved data" };
# Jazz 0.12.0 - Deeply resolved data
Jazz 0.12.0 makes it easier and safer to load nested data. You can now specify exactly which nested data you want to load, and Jazz will check permissions and handle missing data gracefully. This helps catch errors earlier during development and makes your code more reliable.
## What's new?
- New resolve API for a more type-safe deep loading
- A single, consistent load option for all loading methods
- Improved permission checks on deep loading
- Easier type safety with the `Resolved` type helper
## Breaking changes
### New Resolve API
We're introducing a new resolve API for deep loading, more friendly to TypeScript, IDE autocompletion and LLMs.
**Major changes:**
1. Functions and hooks for loading now take the resolve query as an explicit nested `resolve` prop
2. Shallowly loading a collection is now done with `true` instead of `[]` or `{}`
<CodeGroup>
```tsx twoslash
// @noErrors: 2451
import { CoMap, CoList, co, Account } from "jazz-tools";
import { useAccount } from "jazz-react";
class AccountRoot extends CoMap { friends = co.ref(ListOfAccounts); }
class ListOfAccounts extends CoList.Of(co.ref(Account)) {}
class MyAppAccount extends Account { root = co.ref(AccountRoot);}
declare module "jazz-react" { interface Register { Account: MyAppAccount; } }
// ---cut-before---
// Before
// @ts-expect-error
const { me } = useAccount({ root: { friends: [] } }); // [!code --]
// After
const { me } = useAccount({ // [!code ++]
resolve: { root: { friends: true } } // [!code ++]
}); // [!code ++]
```
</CodeGroup>
3. For collections, resolving items deeply is now done with a special `$each` key.
For a `CoList`:
<CodeGroup>
```tsx
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
// Before
const tasks = useCoState(ListOfTasks, id, [{}]); // [!code --]
// After
const tasks = useCoState(ListOfTasks, id, { resolve: { $each: true } }); // [!code ++]
```
</CodeGroup>
For a `CoMap.Record`:
<CodeGroup>
```tsx
class UsersByUsername extends CoMap.Record(co.ref(MyAppAccount)) {}
// Before
const usersByUsername = useCoState(UsersByUsername, id, [{}]); // [!code --]
// After
const usersByUsername = useCoState(UsersByUsername, id, { // [!code ++]
resolve: { $each: true } // [!code ++]
}); // [!code ++]
```
</CodeGroup>
Nested loading &mdash; note how it's now less terse, but more readable and auto-complete friendly:
<CodeGroup>
```tsx
// Before
const tasksWithAssigneesAndTheirOrgs = useCoState(ListOfTasks, id, [{ // [!code --]
assignees: [{ org: {}}]} // [!code --]
]); // [!code --]
// After
const tasksWithAssigneesAndTheirOrgs = useCoState(ListOfTasks, id, { // [!code ++]
resolve: { // [!code ++]
$each: { // [!code ++]
assignees: { // [!code ++]
$each: { org: true } // [!code ++]
} // [!code ++]
} // [!code ++]
} // [!code ++]
}); // [!code ++]
```
</CodeGroup>
### A single, consistent load option
The new API works across all loading methods, and separating out the resolve query means
other options with default values are easier to manage, for example: loading a value as a specific account instead of using the implicit current account:
<CodeGroup>
```ts
// Before
Playlist.load(id, otherAccount, { // [!code --]
tracks: [], // [!code --]
}); // [!code --]
// After
Playlist.load(id, { // [!code ++]
loadAs: otherAccount, // [!code ++]
resolve: { tracks: true } // [!code ++]
}); // [!code ++]
```
</CodeGroup>
### Improved permission checks on deep loading
Now `useCoState` will return `null` when the current user lacks permissions to load requested data.
Previously, `useCoState` would return `undefined` if the current user lacked permissions, making it hard to tell if the value is loading or if it's missing.
Now `undefined` means that the value is definitely loading, and `null` means that the value is temporarily missing.
We also have implemented a more granular permission checking, where if an *optional* CoValue cannot be accessed, `useCoState` will return the data stripped of that CoValue.
**Note:** The state handling around loading and error states will become more detailed and easy-to-handle in future releases, so this is just a small step towards consistency.
<CodeGroup>
{/* prettier-ignore */}
```tsx
class ListOfTracks extends CoList.Of(co.optional.ref(Track)) {}
// Before (ambiguous states)
const value = useCoState(ListOfTracks, id, [{}]); // [!code --]
if (value === undefined) return <div>Loading or access denied</div>; // [!code --]
if (value === null) return <div>Not found</div>; // [!code --]
// After
const value = useCoState(ListOfTracks, id, { resolve: { $each: true } }); // [!code ++]
if (value === undefined) return <div>Loading...</div>; // [!code ++]
if (value === null) return <div>Not found or access denied</div>; // [!code ++]
// This will only show tracks that we have access to and that are loaded.
return tracks.map(track => track && <MusicTrack track={track} />);
```
</CodeGroup>
## New Features
### The `Resolved` type helper
The new `Resolved` type can be used to define what kind of deeply loaded data you expect in your parameters, using the same resolve query syntax as the new loading APIs:
<CodeGroup>
```ts
type PlaylistResolved = Resolved<Playlist, {
tracks: { $each: true }
}>;
function TrackListComponent({ playlist }: { playlist: PlaylistResolved }) {
// Safe access to resolved tracks
return playlist.tracks.map(track => /* ... */);
}
```
</CodeGroup>

View File

@@ -25,9 +25,9 @@ So we decided to remove `createJazzRNApp` step and to provide the types through
import { JazzProvider, useDemoAuth, DemoAuthBasicUI } from "jazz-react-native";
import { MyAppAccount } from "./schema";
// Remove these lines // *bin*
const Jazz = createJazzRNApp({ AccountSchema: MyAppAccount }); // *bin*
export const { useAccount, useCoState } = Jazz; // *bin*
// Remove these lines // [!code --]
const Jazz = createJazzRNApp({ AccountSchema: MyAppAccount }); // [!code --]
export const { useAccount, useCoState } = Jazz; // [!code --]
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
const [auth, state] = useDemoAuth(); // old
@@ -66,8 +66,8 @@ This change improves IDE intellisense support and simplifies imports:
{/* prettier-ignore */}
```tsx
// Replace local imports with "jazz-react-native" imports
import { useAccount } from "./main"; // *bin*
import { useAccount } from "jazz-react-native"; // *add*
import { useAccount } from "./main"; // [!code --]
import { useAccount } from "jazz-react-native"; // [!code ++]
export function Hello() {
const { me } = useAccount();

View File

@@ -24,9 +24,9 @@ So we decided to remove `createJazzReactApp` step and to provide the types throu
import { JazzProvider, usePasskeyAuth, PasskeyAuthBasicUI } from "jazz-react";
import { MyAppAccount } from "./schema";
// Remove these lines // *bin*
const Jazz = createJazzReactApp({ AccountSchema: MyAppAccount }); // *bin*
export const { useAccount, useCoState } = Jazz; // *bin*
// Remove these lines // [!code --]
const Jazz = createJazzReactApp({ AccountSchema: MyAppAccount }); // [!code --]
export const { useAccount, useCoState } = Jazz; // [!code --]
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName }); // old
@@ -37,7 +37,7 @@ So we decided to remove `createJazzReactApp` step and to provide the types throu
<JazzProvider
auth={passkeyAuth} // old
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */} // *add*
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */} // [!code ++]
>
{children} // old
</JazzProvider>
@@ -47,11 +47,11 @@ So we decided to remove `createJazzReactApp` step and to provide the types throu
}
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-react" { // *add*
interface Register { // *add*
Account: MyAppAccount; // *add*
} // *add*
} // *add*
declare module "jazz-react" { // [!code ++]
interface Register { // [!code ++]
Account: MyAppAccount; // [!code ++]
} // [!code ++]
} // [!code ++]
```
</CodeGroup>
@@ -65,8 +65,8 @@ This change improves IDE intellisense support and simplifies imports:
{/* prettier-ignore */}
```tsx
// Replace local imports with "jazz-react" imports
import { useAccount } from "./main"; // *bin*
import { useAccount } from "jazz-react"; // *add*
import { useAccount } from "./main"; // [!code --]
import { useAccount } from "jazz-react"; // [!code ++]
export function Hello() {
const { me } = useAccount();

View File

@@ -32,7 +32,7 @@ So we decided to remove `createJazzApp` step and to provide the types through na
</script>
<script lang="ts">
import { Provider } from '$lib/jazz'; // *bin*
import { Provider } from '$lib/jazz'; // [!code --]
import { JazzProvider } from 'jazz-svelte';
// Example configuration for authentication and peer connection
@@ -62,8 +62,8 @@ So we decided to remove `createJazzApp` step and to provide the types through na
```svelte
<script lang="ts">
import { useAccount } from '$lib/jazz'; // *bin*
import { useAccount } from 'jazz-svelte'; // *add*
import { useAccount } from '$lib/jazz'; // [!code --]
import { useAccount } from 'jazz-svelte'; // [!code ++]
const { me } = useAccount();
</script>

View File

@@ -28,10 +28,10 @@ import App from "./App.vue"; // old
import router from "./router"; // old
import { ToDoAccount } from "./schema"; // old
// Remove these lines // *bin*
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount }); // *bin*
export const { useAccount, useCoState } = Jazz; // *bin*
const { JazzProvider } = Jazz; // *bin*
// Remove these lines // [!code --]
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount }); // [!code --]
export const { useAccount, useCoState } = Jazz; // [!code --]
const { JazzProvider } = Jazz; // [!code --]
const RootComponent = defineComponent({ // old
name: "RootComponent", // old
@@ -86,8 +86,8 @@ This change improves IDE intellisense support and simplifies imports:
<script setup lang="ts">
// Replace local imports with "jazz-vue" imports
import { useAccount } from "./main"; // *bin*
import { useAccount } from "jazz-vue"; // *add*
import { useAccount } from "./main"; // [!code --]
import { useAccount } from "jazz-vue"; // [!code ++]
const { me, logOut } = useAccount();
</script>

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