Compare commits

...

424 Commits

Author SHA1 Message Date
Anselm
1e2f6d8f14 Release 2024-05-26 20:44:05 +01:00
Anselm Eickhoff
7e5b176930 Merge pull request #199 from tobiaslins/implement-loading-ui
Implement basic `loading` component that is rendered when migrations …
2024-05-26 20:40:00 +01:00
Anselm
b420eab503 Fix type for Provider 2024-05-26 20:37:55 +01:00
Tobias Lins
b370e2e14e Merge branch 'main' into implement-loading-ui 2024-05-26 20:48:38 +02:00
Tobias Lins
1fabee297d Implement basic loading component that is rendered when migrations are running or jazz not ready yet 2024-05-26 20:46:52 +02:00
Anselm
484dc460c5 Lots of doc progress 2024-05-26 17:39:16 +01:00
Anselm
0cb8756124 Fix lint error 2024-05-26 16:16:25 +01:00
Anselm
95d0f0221b Release 2024-05-26 16:15:18 +01:00
Anselm
0c9c0fcd60 Clean up loading & subscription API 2024-05-26 16:14:01 +01:00
Anselm
8be0dd133c Doc improvements 2024-05-26 16:13:25 +01:00
Anselm
e68e0ada0d Chage hero grid 2024-05-25 21:27:20 +01:00
Anselm
49a7349e4d Update copyright year 2024-05-25 21:27:11 +01:00
Anselm
979c7241a4 Guide progress, licenses 2024-05-25 20:54:53 +01:00
Anselm
e011e4a049 Add local-first conf badge 2024-05-25 19:46:36 +01:00
Anselm
92bccf5974 Release 2024-05-25 17:08:37 +01:00
Anselm
2c1d6dcb8f Fix type signature / depth of useCoState 2024-05-25 17:07:41 +01:00
Anselm Eickhoff
75f45ec0b2 Merge pull request #163 from gardencmp/jazz-schema
New unified API
2024-05-25 16:35:54 +01:00
Anselm
c0ebcadda9 Push tags on publish 2024-05-25 16:28:52 +01:00
Anselm
109e9b6a5b Use changeset for publishing, format 2024-05-25 16:28:10 +01:00
Anselm
d0c3d08e42 Add static homepage build output to standalone server 2024-05-25 16:15:05 +01:00
Anselm
7514830edb Increase homepage memory 2024-05-25 16:06:02 +01:00
Anselm
6f27128b87 Fix homepage port 2024-05-25 15:47:10 +01:00
Anselm
376518d4ef Provide correct nomad template 2024-05-25 15:27:58 +01:00
Anselm
272fc85c13 Fix homepage build 2024-05-25 15:21:57 +01:00
Anselm
579e4f93ee Fix homepage build some more 2024-05-25 15:20:29 +01:00
Anselm
a9accdc3a8 Fix homepage build 2024-05-25 15:19:32 +01:00
Anselm
b403db51d4 Remove typedoc output again 2024-05-25 15:03:45 +01:00
Anselm
108cae037f Homepage build 2024-05-25 14:46:00 +01:00
Anselm
c51897ab9e Release 2024-05-25 14:43:57 +01:00
Anselm
67171cb07a Add runtime option for optional refs 2024-05-25 14:42:33 +01:00
Anselm
6ae3bf6ac9 Release 0.7.0 2024-05-25 11:58:43 +01:00
Anselm
b64b15877a Homepage fixes 2024-05-25 11:53:01 +01:00
Anselm
19f52b7361 Alpha release 2024-05-25 10:32:57 +01:00
Anselm
9dcd15dbc8 Fixed bug with newRandomSessionID being called before crypto was ready 2024-05-25 10:32:33 +01:00
Anselm
a423eeea3b Alpha release 2024-05-24 22:52:34 +01:00
Anselm Eickhoff
99be6a3566 Merge pull request #193 from markmelville/ws_close
ignore error on ws close, fixing "Invalid state: Controller is already closed"
2024-05-24 22:51:33 +01:00
Anselm
e97f730c0f Alpha release 2024-05-24 22:37:22 +01:00
Anselm
f5642335ff Fix useAccount 2024-05-24 22:36:53 +01:00
Anselm
bf0f8ec824 Alpha release 2024-05-24 22:28:50 +01:00
Anselm
fb9ef4ea20 Fix noble curves dep 2024-05-24 22:28:13 +01:00
Anselm
86363197cd Alpha release 2024-05-24 22:11:47 +01:00
Anselm
e93187c971 Homepage improvements 2024-05-24 22:11:32 +01:00
Anselm
1a86e13cf1 Implement deep loading, simplify API 2024-05-24 22:10:46 +01:00
Anselm
b863c1d20a Deep loading based on removing nulls 2024-05-23 17:32:04 +01:00
Anselm
4c8d658c25 Only load each image resolution once 2024-05-21 16:48:14 +01:00
Anselm
69d37437ef Fix infinite recursion in subscriptionScope 2024-05-21 16:47:27 +01:00
Anselm
220bdbae62 Fix type of init param for CoMap.create 2024-05-21 16:46:44 +01:00
Anselm
b23b556b79 Fix readme typo 2024-05-21 14:14:47 +01:00
Anselm
ce721cf3d1 Update main readme 2024-05-21 13:47:48 +01:00
Anselm
41363415fe Add lint-staged 2024-05-21 13:44:26 +01:00
Anselm
b91d0769d5 Add husky pre-commit hook 2024-05-21 13:27:45 +01:00
Anselm
ad16690826 Add minimal package READMEs 2024-05-21 13:24:54 +01:00
Anselm
ca7b81c36a Add eslint to the homepage 2024-05-21 13:09:37 +01:00
Anselm
a632ce1477 Apply prettier and make everything testable from root 2024-05-21 12:10:06 +01:00
Anselm
ca7011e9af Apply eslint to all repos 2024-05-21 10:31:48 +01:00
Anselm
34df432ee8 Apply eslint & prettier to cojson-storage-indexeddb 2024-05-21 09:59:29 +01:00
Anselm
dfa7178041 Set up prettier for cojson 2024-05-21 08:27:09 +01:00
Anselm
a3e9a3b686 Extend eslint rules for cojson 2024-05-21 08:22:17 +01:00
Anselm
599db049f2 Remove dotenv 2024-05-21 07:55:48 +01:00
Anselm
7cba6dd690 Factor out crypto impl and provide pure js one 2024-05-20 20:37:43 +01:00
Anselm
2d406c9d58 Fix minor errors 2024-05-18 16:55:14 +01:00
Anselm
d8fe2b10f1 Alpha release 2024-05-18 16:51:28 +01:00
Anselm
c8343626ba Expose experimental OPFS storage 2024-05-18 14:38:59 +01:00
Anselm
5b188ec093 Alpha release 2024-05-16 18:29:44 +01:00
Anselm
7ebbd80049 Ensure accounts are synced after creation 2024-05-16 18:29:13 +01:00
Anselm
74c9e5d36d More LSM/OPFS progress 2024-05-16 18:16:44 +01:00
Anselm
1a3530747f Add changesets 2024-05-16 10:29:13 +01:00
Anselm
79899b9b18 Update lockfile 2024-05-16 10:29:04 +01:00
Anselm
6ef6a2f507 Implement first devtools formatters 2024-05-16 10:28:50 +01:00
Anselm
ab6d15c9f7 Use fresh subscribe context for useProgressiveImg 2024-05-16 10:28:09 +01:00
Anselm
a0dc9139a2 Add ability to declare minimum required data in subscribe & Improve property access tracing 2024-05-16 10:27:27 +01:00
Anselm
220fa319d5 Use lsm storage from cojson & update effect 2024-05-16 10:25:22 +01:00
Anselm
183d505a5e Remove old cojson-storage-lsm module 2024-05-16 10:24:43 +01:00
Anselm
f960e7e736 Optimizations for incoming sync messages 2024-05-16 10:24:20 +01:00
Anselm
d68ac84e03 WIP working-ish version of LSM storage 2024-05-16 10:22:52 +01:00
Mark Melville
30fbe2b6d7 ignore error on ws close 2024-05-14 15:27:34 -06:00
Anselm
e3a00570e1 Refactor 2024-05-13 15:46:23 +01:00
Anselm
499e02685a Merge branch 'lsm-storage' into jazz-schema 2024-05-13 14:32:37 +01:00
Anselm
6b0418f772 Fix image resolution loading 2024-05-13 14:06:15 +01:00
Anselm
1200aae47d Alpha release 2024-05-12 17:27:56 +01:00
Anselm
20fdb09b33 New cache for covalue proxies 2024-05-12 17:27:08 +01:00
Anselm
522b12dc42 CoJSON performance improvement 2024-05-12 17:24:53 +01:00
Anselm
a7cd0dcce5 Pricing update 2024-05-12 17:24:24 +01:00
Anselm
fd86c11336 Alpha release 2024-05-10 21:02:39 +01:00
Anselm
4fc414d744 Extract jazz cli into jazz-run package 2024-05-10 21:00:20 +01:00
Anselm
6d49e9b06c Alpha release 2024-05-10 17:37:40 +01:00
Anselm
8b866e288b Lots more doc improvements 2024-05-10 17:37:00 +01:00
Anselm
bf22588b0e Small homepage fixes 2024-05-10 14:41:24 +01:00
Anselm
60d5ca2811 Alpha release 2024-05-10 14:21:03 +01:00
Anselm
9e5dcdfa69 Update examples 2024-05-10 14:20:50 +01:00
Anselm
3cc39dd5ed Introduce jazz-tools CLI 2024-05-10 14:20:24 +01:00
Anselm
6878060346 Simplify jazz-nodejs 2024-05-10 14:19:57 +01:00
Anselm
9840061137 Clean up exports 2024-05-10 14:19:35 +01:00
Anselm
6f84e00463 homepage/docs improvements 2024-05-10 14:18:48 +01:00
Anselm
1e82d0d34e Allow fall-through of routes 2024-05-10 14:17:46 +01:00
Anselm
f35bc468b3 Separate pnpm workspace for homepage 2024-05-09 11:44:27 +01:00
Anselm
719071c286 Docs & guide progress 2024-05-09 10:46:33 +01:00
Anselm
c4b439e2e6 Lots of doc improvements 2024-05-07 13:16:27 +01:00
Anselm
77c2b56ceb Alpha release 2024-05-03 15:55:58 +01:00
Anselm
0b17b7ad5a Get rid of self generics, new create syntax 2024-05-03 15:55:32 +01:00
Anselm
db3011a1c9 Homepage improvements 2024-05-03 14:27:07 +01:00
Anselm
b47c695b97 Refactor cojson tests 2024-05-03 13:39:04 +01:00
Anselm
55c1c893ba Homepage improvements 2024-05-02 17:37:02 +01:00
Anselm
bde684fe30 Alpha release 2024-05-02 09:24:42 +01:00
Anselm
89b6c9004b CoValue casting & auto-subbing _owner 2024-05-02 09:23:49 +01:00
Anselm Eickhoff
97cdfbddaf Merge pull request #184 from tobiaslins/addmember-chaining 2024-05-02 07:20:44 +01:00
Tobias Lins
584ee2d136 Return this 2024-05-02 07:54:01 +02:00
Anselm
21771c4725 Alpha release 2024-05-01 21:36:51 +01:00
Anselm
7e8f1bed15 Merge branch 'main' into jazz-schema 2024-05-01 21:35:31 +01:00
Anselm
226ae03603 Better variable naming in test 2024-05-01 15:49:14 +01:00
Anselm
96c494f5ee Alpha release 2024-05-01 15:43:04 +01:00
Anselm
23ba00422f Implement group-based profiles & migrations 2024-05-01 15:42:14 +01:00
Anselm
52675c9c68 Alpha release 2024-05-01 10:36:39 +01:00
Anselm
1b113e0114 Fix CoList.splice / RawCoList.append 2024-05-01 10:36:04 +01:00
Anselm
0a930f5eeb Fix Costream[...].all 2024-05-01 10:35:38 +01:00
Anselm
84f5a83648 Homepage improvements 2024-05-01 10:35:11 +01:00
Anselm
5fa277c254 Alpha release 2024-04-29 13:56:11 +01:00
Anselm
d49c7f2dd4 Fix CoMap.Record.toJSON() 2024-04-29 13:55:23 +01:00
Anselm
a78f1688d9 Alpha release 2024-04-29 13:29:45 +01:00
Anselm
cd37a846d8 Make Account -> Profile a lazy ref schema 2024-04-29 13:28:38 +01:00
Anselm
63374ccb6d Alpha release 2024-04-28 17:27:47 +01:00
Anselm
efe2d91fb3 Make sure delete on CoMaps deletes keys 2024-04-28 17:26:47 +01:00
Anselm
234b2a019b Force cojson tests to pass... 2024-04-28 17:12:12 +01:00
Anselm
4bbcd366bc Relax types of CoMap._schema 2024-04-28 16:59:29 +01:00
Anselm
5724f8747a Fix schema of Account & Group 2024-04-28 16:23:17 +01:00
Anselm
38d44103d1 Alpha release 2024-04-27 18:22:11 +01:00
Anselm
96a7ff68e7 Homepage improvements 2024-04-27 18:20:12 +01:00
Anselm
fb78a55f76 Fix getOwnPropertyDescriptor in _refs proxy 2024-04-27 18:19:57 +01:00
Anselm
fdc7fc7bcf Introduce CoMap.Record 2024-04-27 18:13:48 +01:00
Anselm
ed5643aaf1 Alpha release 2024-04-25 09:35:02 +01:00
Anselm
ac431ef9ef Fix CoMap _refs for co.items 2024-04-25 09:34:08 +01:00
Anselm
0940508637 Homepage improvements 2024-04-25 09:33:21 +01:00
Anselm
704af7d04c Alpha release 2024-04-24 14:07:42 +01:00
Anselm
e4e476a834 Add maxWidth option for loading images 2024-04-24 14:07:18 +01:00
Anselm
ece35b3c6f Alpha release 2024-04-24 13:50:50 +01:00
Anselm
b26eab50b3 Include fast-check as dependency to help dev time resolution 2024-04-24 13:50:00 +01:00
Anselm
b42313a285 Fix subscription to account in useAccount 2024-04-24 13:49:15 +01:00
Anselm
129e2c1668 Alpha release 2024-04-23 21:45:30 +01:00
Anselm
87ddb81562 More precise imports from @effect/schema 2024-04-23 21:45:08 +01:00
Anselm
daee49cd9d Alpha release 2024-04-23 21:25:26 +01:00
Anselm
3aaf773b0a Add missing @scure/bip39 dep 2024-04-23 21:24:31 +01:00
Anselm
460478fc65 Alpha release 2024-04-23 21:17:36 +01:00
Anselm
fe8b5f45b9 Use effect 3.0 2024-04-23 21:17:02 +01:00
Anselm
01ac646c8e Alpha release 2024-04-23 21:10:53 +01:00
Anselm
d4b9fbcc60 Make CoMaps even more subclassable 2024-04-23 21:10:35 +01:00
Anselm
1cfa279543 Alpha release 2024-04-23 20:52:08 +01:00
Anselm
e35be73bcc More superclass-compatible CoMaps 2024-04-23 20:51:36 +01:00
Anselm
f8a5c46e18 Alpha release 2024-04-23 20:27:41 +01:00
Anselm
1c7d85ce76 Fix CoStream types 2024-04-23 20:27:18 +01:00
Anselm
19004b4c36 Alpha release 2024-04-23 20:23:50 +01:00
Anselm
930fa689a7 Add .all to CoStreamEntry 2024-04-23 20:22:50 +01:00
Anselm
18a7b2d6b4 Making progress with docs 2024-04-23 15:16:32 +01:00
Anselm
d2e03ff9d3 Alpha release 2024-04-22 19:30:02 +01:00
Anselm
77a9c8395e Fix variance of ID.__type 2024-04-22 19:29:21 +01:00
Anselm
c4151fcb95 Alpha release 2024-04-22 16:16:37 +01:00
Anselm
4c5c21bba2 Work with stricter TS lints 2024-04-22 16:16:02 +01:00
Anselm
f0f6f1b71c Alpha release 2024-04-22 16:02:21 +01:00
Anselm
a9d6d5a1db Reintroduce jazz-nodejs 2024-04-22 15:58:50 +01:00
Anselm
7849ce6de7 Clean up API even more 2024-04-22 15:47:50 +01:00
Anselm
354bdcdbfb Alpha release 2024-04-19 16:40:18 +01:00
Anselm
8ecd3e88c8 Even friendlier for subclassing CoMap 2024-04-19 16:39:51 +01:00
Anselm
85d2b627f1 Alpha release 2024-04-19 16:28:47 +01:00
Anselm
88fd92e4dc More subclass-friendly types in CoMap 2024-04-19 16:27:48 +01:00
Anselm
952982e7ea Alpha release 2024-04-19 15:28:03 +01:00
Anselm
22e7c27af7 Consistent proxy based API 2024-04-19 15:25:05 +01:00
Anselm
59c18c34de Alpha release 2024-04-17 13:26:56 +01:00
Anselm
6acbaede44 Fix keys 2024-04-17 13:26:23 +01:00
Anselm
1a44f875b3 Alpha release 2024-04-17 13:24:09 +01:00
Anselm
9d935fe1d0 Refactors & homepage improvements 2024-04-17 13:23:31 +01:00
Anselm
e5eed5b9b7 Alpha release 2024-04-16 14:01:57 +01:00
Anselm
05a549f04f Makre refs on list more precise 2024-04-16 14:01:32 +01:00
Anselm
a5e68a4fae Alpha release 2024-04-16 13:58:35 +01:00
Anselm
016a9e342a Move unported packages & make refs type more precise 2024-04-16 13:57:41 +01:00
Anselm
627d8950ae Alpha release 2024-04-16 13:21:46 +01:00
Anselm
770ce08c10 Get rid of Co namespace 2024-04-16 13:17:09 +01:00
Anselm
69ac514b3b New alpha 2024-04-15 16:54:39 +01:00
Anselm
b1481748f9 Boom! 2024-04-15 16:50:14 +01:00
Anselm
49944e323f Just before circular references... 2024-04-08 15:42:47 +01:00
Anselm
15310db389 Fix image loading and CoMap proxy issues 2024-04-05 17:22:46 +01:00
Anselm
ea5c5a2604 Rename jazz-js to jazz-tools 2024-04-05 15:43:23 +01:00
Anselm
e461dd1355 Fix test 2024-04-05 15:32:44 +01:00
Anselm
e299c3e9d8 Alpha 0 prerelease 2024-04-05 15:30:49 +01:00
Anselm
406c47271f Finesse types and fix auth impls 2024-04-05 14:54:03 +01:00
Anselm
05c7efea85 More feature parity for demo apps 2024-04-05 11:45:33 +01:00
Anselm
ce7ddf7055 More stuff to make chat app work 2024-04-04 15:19:50 +01:00
Anselm
beb40b5db6 Mostly make chat example work 2024-03-27 18:21:41 +00:00
Anselm
2def752cc4 Flatten CoValue metadata and add edits for CoList 2024-03-26 15:21:28 +00:00
Anselm
bacf3ae86a Change order of Self generic argument again 2024-03-26 10:52:33 +00:00
Anselm
0fef382f2e Docs & homepage improvements 2024-03-25 14:38:39 +00:00
Anselm
95523d8538 Doc organization & type improvements 2024-03-25 12:20:55 +00:00
Anselm
71f7220bfd Start adding doc comments 2024-03-22 15:25:55 +00:00
Anselm
2212c6deac Fix lots of type issues, implement Account & Group 2024-03-22 14:50:41 +00:00
Anselm
fb3efe4cfd Implement tests for BinaryCoStream 2024-03-21 17:33:15 +00:00
Anselm
e66ac6a7d0 Apply strict mode to tests, finish tests for CoStream 2024-03-21 12:06:05 +00:00
Anselm
7ab3908848 Type cleanup, more CoStream implementation 2024-03-19 17:41:36 +00:00
Anselm
921f1fbfe8 Starting test for CoStream 2024-03-18 11:51:01 +00:00
Anselm
2ac455f8b5 Sketch out more in bindings etc 2024-03-18 11:25:18 +00:00
Anselm
1ce881aed2 jazz-js-efs becomes jazz-js 2024-03-15 14:35:51 +00:00
Anselm
b1b5140951 Sketch out coStream interfaces 2024-03-15 14:34:38 +00:00
Anselm
b109c23233 Change "meta" to "co" 2024-03-15 13:58:49 +00:00
Anselm
a7a34a0b6e Tests for CoList subscription and auto-resolution 2024-03-15 10:44:03 +00:00
Anselm
4bf63934e1 Simple CoList resolution 2024-03-14 16:42:41 +00:00
Anselm
16f572282f Kind of implement splice 2024-03-14 16:15:07 +00:00
Anselm
44380c3700 Implement CoList with primitive items 2024-03-14 15:57:31 +00:00
Anselm
dc46cb1386 Move tests to vitest, fix some 2024-03-14 15:38:27 +00:00
Anselm
3ccb1e8ad7 Sketch imageDef 2024-03-13 14:56:17 +00:00
Anselm
d973c5f48b Pull out common constructor methods 2024-03-13 14:30:46 +00:00
Anselm
f4af78c834 Mostly make index signatures work 2024-03-13 14:16:38 +00:00
Anselm
e6d323fd30 Use correct subclasses at runtime, tests passing 2024-03-12 11:39:25 +00:00
Anselm
e6ab56aeb5 Solve lots of type issues 2024-03-12 11:13:43 +00:00
Anselm
779765b649 Fix a bunch of type issues 2024-03-08 14:45:51 +00:00
Anselm
6da730779a Better CoMapInit 2024-03-08 10:16:00 +00:00
Anselm
a3e77edc57 Support optional references 2024-03-07 16:24:22 +00:00
Anselm
ed00308986 Make CoMap subscription work 2024-03-07 12:29:52 +01:00
Anselm
89e9092e0f Make loading work 2024-03-01 14:27:11 +00:00
Anselm
f8b11754c8 Sketch ValueRefs and loading 2024-02-29 17:39:06 +00:00
Anselm
4b38d0793c Test encoding on assignment 2024-02-29 11:29:16 +00:00
Anselm
b2156f8154 Test with more complicated schema in key 2024-02-29 11:25:17 +00:00
Anselm
3a5422e635 Make CoMaps work with effect schema 2024-02-29 11:23:31 +00:00
Anselm
54d3d76868 move pre-effect-schema implementation of jazz-js away 2024-02-29 11:20:34 +00:00
Anselm Eickhoff
f4dc0ec1b7 Merge pull request #177 from tobiaslins/main 2024-02-27 22:51:51 +00:00
Tobias Lins
f500db2dd3 Implement tree view with nested resolving 2024-02-27 21:56:25 +01:00
Anselm
95f64f9934 Actually deploy inspector 2024-02-27 14:45:54 +00:00
Anselm
cccb0e1a21 First draft of inspector 2024-02-27 14:43:08 +00:00
Anselm
b434a4227f Off by 10x 2024-02-27 11:55:40 +00:00
Anselm
6ba4dc1f04 New pricing 2024-02-27 11:49:49 +00:00
Anselm
2fe4c81d1e Start of EffectTS schema based jazz-js 2024-02-27 11:42:41 +00:00
Anselm
5c00264184 test for enumarting props in CoMap 2024-02-24 11:47:35 +01:00
Anselm
c744849c9b Pull out constructor types from schemas 2024-02-23 14:37:25 +01:00
Anselm
f59b278f00 Make simple coValue resolution in extra keys work 2024-02-22 17:45:36 +01:00
Anselm
b26c155d5f Fix effect types and others 2024-02-22 16:35:43 +01:00
Anselm
6da79b8745 Move owner of coValue to first param of constructor 2024-02-22 16:14:51 +01:00
Anselm
0b92591b17 Sketch first test for comap resolution in extra keys 2024-02-22 15:55:58 +01:00
Anselm
974456db54 Support for primitive extra keys in CoMaps 2024-02-22 14:58:27 +01:00
Anselm
a1326a80fe Completely get rid of immutable map for now 2024-02-21 14:45:19 +01:00
Anselm
00d6946b24 Release 2024-02-14 15:41:37 +00:00
Anselm
c4ffde93c0 Simple reconnect logic for jazz-nodejs 2024-02-13 20:19:51 +00:00
Anselm
37bfe967ea Refactor coValue files into parts 2024-02-09 18:16:44 +00:00
Anselm
9abbbfd6fb Release 2024-02-09 11:16:25 +00:00
Anselm
155cd08e39 Mute most annoying log messages 2024-02-09 11:13:18 +00:00
Anselm
e2e6bdf3bd Sketch for new ImageDefinition 2024-02-09 11:09:26 +00:00
Anselm
810c42c743 Implement BinaryCoStream subscription 2024-02-08 17:04:12 +00:00
Anselm
99e4c1301e Lots of fixes to subscription logic and CoValues 2024-02-08 15:54:03 +00:00
Anselm
8c86a831fc Move coValues into their own folder 2024-02-07 10:02:41 +00:00
Anselm
5e976416a4 Implement and test basic CoStream functionality #171 2024-02-06 16:52:11 +00:00
Anselm
0339e14260 Implementation of CoStreams (WIP) 2024-02-04 17:05:10 +00:00
Anselm
4b94fcebf1 Split up tests 2024-01-18 10:56:25 +00:00
Anselm
ddd2a79f37 Subscription & auto-resolution for CoLists 2024-01-18 10:46:00 +00:00
Anselm
01a8f2dab3 Make nested subscriptions work incl edge cases 2024-01-12 22:14:17 +00:00
Anselm
801629d2c1 Fix type errors 2024-01-09 14:43:33 +00:00
Anselm
87d62c941f Nested coValue loading & introduce EffectTS 2024-01-09 14:37:18 +00:00
Anselm
7e6e0fdcc5 Lots of doc improvements 2024-01-05 16:25:27 +00:00
Anselm
a73b07424c Lots of docs improvements 2024-01-04 17:40:57 +00:00
Anselm
0f9b983132 Very first basic tests 2024-01-04 16:48:51 +00:00
Anselm
43e25902d3 Merge branch 'main' into jazz-schema 2024-01-04 14:37:09 +00:00
Anselm
2c27c8517f Add own lockfile to homepage 2024-01-04 13:45:28 +00:00
Anselm Eickhoff
b496058a0e Merge pull request #154 from gardencmp/pnpm-and-turbo
Move to pnpm and turborepo (deploy)
2024-01-04 13:27:05 +00:00
Anselm Eickhoff
4313663bd1 Merge branch 'main' into pnpm-and-turbo 2024-01-04 13:26:52 +00:00
Anselm Eickhoff
dbdbfbd07a Merge pull request #156 from mirshko/patch-1
Fix loading prop always being false
2024-01-04 13:24:14 +00:00
Anselm
184b23d61f Publish 2024-01-03 11:03:57 +00:00
Anselm
5c03b4f668 Parametrize BrowserImage for maxWidth 2024-01-03 11:01:07 +00:00
Anselm
bdbe777d68 Fix ordering bugs with IndexedDB 2024-01-03 10:59:36 +00:00
Anselm
a838a18647 Publish 2024-01-02 18:37:41 +00:00
Anselm
dd8dba63ea Fix migration changes being lost on loaded account 2024-01-02 18:36:18 +00:00
Jeff Reiner
3f5a664ee7 Fix loading prop always being false 2023-12-22 17:37:38 +01:00
Anselm Eickhoff
707292e1ff Merge pull request #152 from tobiaslins/pnpm-and-turbo
Move to pnpm & turborepo
2023-12-21 16:08:49 +00:00
Anselm
9a81b63943 Use pnpm install 2023-12-21 15:51:25 +00:00
Anselm
30216b7b80 Try GITHUB_TOKEN again 2023-12-21 15:49:47 +00:00
Anselm
b2fc91c2ce Use personal access token for ghcr push 2023-12-21 15:42:10 +00:00
Anselm
ef0328833c Try edited event for PRs 2023-12-21 15:30:35 +00:00
Anselm
6a93f17a4a Try to allow ghcr pushes from contributor PRs 2023-12-21 15:24:56 +00:00
Anselm
01bd07ac66 Use pnpm ci 2023-12-21 14:56:58 +00:00
Anselm
88859cfeca Actually add pnpm to CI 2023-12-21 10:23:58 +00:00
Anselm
dfe563e2bc Use pnpm & turbo for CI build 2023-12-21 10:21:53 +00:00
Anselm
7fc0ff981d Update instructions on "ejecting" examples 2023-12-21 10:15:37 +00:00
Anselm
1a9132102d Get rid of lerna 2023-12-21 09:32:08 +00:00
Anselm Eickhoff
d39638282f Merge branch 'main' into pnpm-and-turbo 2023-12-21 09:21:55 +00:00
Anselm Eickhoff
219071654d Merge pull request #153 from tobiaslins/move-to-localStorage 2023-12-21 07:43:36 +00:00
Tobias Lins
7c415db7bd Move to auth to use localStorage instead of sessionStorage 2023-12-21 08:28:03 +01:00
Tobias Lins
4354c340fc Add more dev exmaples 2023-12-21 08:20:43 +01:00
Anselm
a4b484fa36 Publish 2023-12-20 21:49:54 +00:00
Anselm
3757d12dc4 Fix loading of accounts 2023-12-20 21:48:01 +00:00
Anselm
c3a97b29a9 First test skeleton 2023-12-20 21:15:21 +00:00
Tobias Lins
b65e30ec70 Add changeset & publish commands 2023-12-20 22:04:21 +01:00
Tobias Lins
23a1e0266a Everything is building again 2023-12-20 21:49:31 +01:00
Tobias Lins
76acecfe50 further fixes 2023-12-20 21:18:27 +01:00
Tobias Lins
5031c77afb Update index.tsx 2023-12-20 21:05:33 +01:00
Tobias Lins
af90b8c989 Update vite.config.ts 2023-12-20 21:04:20 +01:00
Tobias Lins
d06b4adad0 Missing dependencies 2023-12-20 21:02:43 +01:00
Tobias Lins
b961cde946 Revert changed files 2023-12-20 20:54:06 +01:00
Tobias Lins
8cbbe2f312 Move to pnpm & turborepo 2023-12-20 20:44:01 +01:00
Anselm
c15a49d82d Merge branch 'main' into jazz-schema 2023-12-20 11:36:09 +00:00
Anselm
fc5b670c73 Publish 2023-12-20 11:33:39 +00:00
Anselm
c8adcc4c47 Fix unused var 2023-12-20 11:32:40 +00:00
Anselm
41a755fe41 publish 2023-12-20 11:17:25 +00:00
Anselm
8def1bb29e IndexedDB & timer perf improvements 2023-12-20 11:04:45 +00:00
Anselm
d379b04e33 Publish 2023-12-18 15:15:27 +00:00
Anselm
17a30e054e Add passphrase based auth + example 2023-12-18 15:13:37 +00:00
Anselm
93809911de more WIP 2023-12-16 16:45:46 +00:00
Anselm
edeb2ca9f4 More WIP 2023-12-13 16:47:56 +00:00
Anselm
01662fc3b8 Merge branch 'main' into jazz-schema 2023-12-06 11:51:28 +00:00
Anselm
7d8f4b4c00 Publish jazz-browser-auth0 2023-12-05 19:12:16 +00:00
Anselm
e2a3896bf0 v0.0.0 2023-12-05 19:11:00 +00:00
Anselm
446de8e0ff Fix for not receiving user_metadata 2023-12-05 19:10:15 +00:00
Anselm
5ae6c95878 Use authorization params everywhere 2023-12-05 17:07:18 +00:00
Anselm
7cde349a50 Actually deploy auth0 example 2023-12-05 16:45:41 +00:00
Anselm
61e640f574 Fix auth0 example dependencies 2023-12-05 16:43:18 +00:00
Anselm
ed122d9d8e Publish 2023-12-05 16:41:12 +00:00
Anselm
34817f4536 Auth0 integration and example 2023-12-05 16:38:37 +00:00
Anselm
134d2f0fda work in progress 2023-12-05 12:06:39 +00:00
Anselm
142973827c Merge branch 'main' into jazz-schema 2023-11-28 08:32:52 +00:00
Anselm
0998a0eabf Correct homepage path 2023-11-24 17:09:34 +00:00
Anselm
a96108478b Homepage updates 2023-11-24 17:04:03 +00:00
Anselm
47444888c3 more progress 2023-11-24 16:54:12 +00:00
Anselm
a4769058f4 Merge branch 'main' into jazz-schema 2023-11-08 11:42:49 +00:00
Anselm
a4cf4c40d4 publish 2023-11-08 11:42:36 +00:00
Anselm Eickhoff
934fe4d29b Merge pull request #127 from KyleAMathews/main
fix(jazz-nodejs): also set peersToLoadFrom on newly created accounts
2023-11-07 17:18:02 +00:00
Anselm Eickhoff
408012f2e5 remove redundant peer add 2023-11-07 17:17:41 +00:00
Kyle Mathews
d0078b830e fix(jazz-nodejs): also set peersToLoadFrom on newly created accounts 2023-11-03 14:58:43 -07:00
Anselm
f7f091e18c Simplify CoValue cards 2023-11-02 14:14:58 +00:00
Anselm
a969430247 move homepage to new folder 2023-11-02 12:13:15 +00:00
Anselm Eickhoff
e52948b2b7 Merge pull request #125 from gardencmp/jazz-nodejs
jazz-nodejs MVP
2023-11-02 12:11:56 +00:00
Anselm
53bb1b230b Fix interpolation 2023-11-02 11:53:19 +00:00
Anselm
54e83aeaaa use NOMAD_ADDR secret 2023-11-02 11:43:32 +00:00
Anselm
aa3129cab5 Provide localNode to AccountMigrations 2023-10-27 14:48:36 +01:00
Anselm
90520dddd7 Make addMember and removeMember take loaded Accounts instead of just IDs 2023-10-27 14:30:55 +01:00
Anselm
03eb77070a Allow account migrations to be async 2023-10-27 11:18:41 +01:00
Anselm
4ba5c255b6 Fewer assumptions in jazz-nodejs 2023-10-27 11:00:19 +01:00
Anselm
01817db873 Add docs for jazz-nodejs 2023-10-25 14:31:27 +01:00
Anselm
46fcbd6c01 Implement first version of jazz-nodejs 2023-10-25 14:27:55 +01:00
Anselm
aa3e3de09e Update docs 2023-10-20 11:09:19 +01:00
Anselm
af3d48764d Reset allTweets 2023-10-20 10:50:29 +01:00
Anselm
091f36b736 Update all tweets 2023-10-20 10:38:56 +01:00
Anselm
7107f79f42 Update all tweets 2023-10-20 10:32:30 +01:00
Anselm
9922db2336 Cosmetic fixes 2023-10-20 10:23:40 +01:00
Anselm
75db570198 Use DemoAuth in Twit example 2023-10-20 10:18:14 +01:00
Anselm
28a09f377b Fix weird TypeScript error 2023-10-20 10:11:31 +01:00
Anselm
fd2e0855bb Deploy twit and chat 2023-10-20 09:56:09 +01:00
Anselm
82e1d57bd6 Fix new accounts not synced 2023-10-20 09:51:12 +01:00
Anselm
a2fbb0b0c8 new allTweets list 2023-10-19 13:38:55 +01:00
Anselm
8feddf9932 Fix sqlite type dependency 2023-10-19 13:38:43 +01:00
Anselm Eickhoff
feed34b1cf Merge pull request #122 from gardencmp/react-advanced
Performance improvements & Twit example improvements
2023-10-19 10:57:37 +01:00
Anselm
662c980cf2 First changeset 2023-10-19 00:52:47 +01:00
Anselm
f5ae530890 Add and use mapDefered to ResolvedCoList 2023-10-19 00:38:35 +01:00
Anselm
46bf7dd3ce A ton of performance and twit example improvements 2023-10-18 23:16:39 +01:00
Anselm
5d4eb38204 A bunch of perf improvements and sync fixes 2023-10-18 00:37:41 +01:00
Anselm
66da658075 Twit example improvements & initial stress test 2023-10-17 21:22:04 +01:00
Anselm
3477b74573 Lots of sync improvements, basic peer priority 2023-10-17 21:21:39 +01:00
Anselm
f3de4906b7 Prepare stress test and fix #83 2023-10-17 16:34:17 +01:00
Anselm
caded3f189 Fix unknown signer bug for incoming transactions 2023-10-17 14:39:13 +01:00
Anselm
5196395495 Wording 2023-10-17 12:03:48 +01:00
Anselm
8089a7ed9f Add report to parent frame again 2023-10-17 11:51:41 +01:00
Anselm
99230d31d2 Add plausible script 2023-10-17 11:49:30 +01:00
Anselm Eickhoff
94bca03f59 Merge pull request #121 from gardencmp/new-hp
New homepage PR 2
2023-10-17 11:39:04 +01:00
Anselm
49719b6e6d Fix example deploy 2023-10-17 11:28:10 +01:00
Anselm
1bdb781452 Fix iframe and metadata 2023-10-17 11:19:00 +01:00
Anselm
c336f69a6b Build and deploy homepage 2023-10-17 11:03:59 +01:00
Anselm
c8cb1ce208 Rename font 2023-10-17 09:55:45 +01:00
Anselm
814a6a80cd Lots of homepage improvements 2023-10-16 23:47:51 +01:00
Anselm Eickhoff
5fdfe18b32 Merge pull request #119 from gardencmp/new-hp
Chat demo & start of new homepage
2023-10-13 11:44:40 +01:00
Anselm
7b7a74778b Reduce number of chat example deployments 2023-10-13 11:40:36 +01:00
Anselm
39dbd46556 Publish
- jazz-example-chat@0.0.45
 - jazz-example-file-drop@0.0.62
 - jazz-example-pets@0.0.62
 - jazz-example-todo@0.0.62
 - jazz-example-twit@0.0.62
 - hash-slash@0.1.3
 - jazz-browser@0.4.15
 - jazz-browser-auth-local@0.4.15
 - jazz-browser-media-images@0.4.15
 - jazz-react@0.4.15
 - jazz-react-auth-local@0.4.15
2023-10-13 11:36:13 +01:00
Anselm
1db4a14be4 Update docs 2023-10-13 11:35:53 +01:00
Anselm
4a4ea4e196 Fix issues with packages 2023-10-13 11:35:35 +01:00
Anselm
e0724441eb Deploy chat example 2023-10-13 11:28:08 +01:00
Anselm
5d47895515 Rename hashroute to hash-slash 2023-10-13 11:25:46 +01:00
Anselm
c1dfac7260 Publish
- jazz-example-chat@0.0.44
 - jazz-example-file-drop@0.0.61
 - jazz-example-pets@0.0.61
 - jazz-example-todo@0.0.61
 - jazz-example-twit@0.0.61
 - hashroute@0.1.2
 - jazz-browser@0.4.14
 - jazz-browser-auth-local@0.4.14
 - jazz-browser-media-images@0.4.14
 - jazz-react@0.4.14
 - jazz-react-auth-local@0.4.14
2023-10-13 11:20:40 +01:00
Anselm
bf29cb3bae Make stuff mergeable 2023-10-13 11:20:07 +01:00
Anselm
a0a9b3f851 Merge branch 'main' into new-hp 2023-10-13 11:15:24 +01:00
Anselm
4c4deb22c9 Publish
- jazz-example-chat@0.0.43
 - jazz-example-pets@0.0.19
 - jazz-example-todo@0.0.43
 - jazz-example-twit@0.0.6
 - hashroute@0.1.1
 - jazz-browser@0.4.6
 - jazz-browser-auth-local@0.4.6
 - jazz-browser-media-images@0.4.7
 - jazz-react@0.4.6
 - jazz-react-auth-local@0.4.6
2023-10-13 11:13:24 +01:00
Anselm
a42c497055 Small imrovements to chat example 2023-10-13 11:12:32 +01:00
Anselm
f1dcdb20bc lots of new hp improvements 2023-10-13 11:11:37 +01:00
Anselm Eickhoff
46330ae201 Merge pull request #117 from gardencmp:data-throughput
Improve data throughput of BinaryCoStreams
2023-10-09 11:26:56 +01:00
Anselm
bfe3595b4c Fix errors in file-drop example 2023-10-09 11:23:15 +01:00
Anselm
34c39e6a55 Scale down other examples and add file-drop 2023-10-09 11:12:17 +01:00
Anselm
5a85501919 Publish
- jazz-example-file-drop@0.0.60
 - jazz-example-pets@0.0.60
 - jazz-example-todo@0.0.60
 - jazz-example-twit@0.0.60
 - cojson@0.4.13
 - cojson-simple-sync@0.4.13
 - cojson-storage-indexeddb@0.4.13
 - cojson-storage-sqlite@0.4.13
 - jazz-autosub@0.4.13
 - jazz-browser@0.4.13
 - jazz-browser-auth-local@0.4.13
 - jazz-browser-media-images@0.4.13
 - jazz-react@0.4.13
 - jazz-react-auth-local@0.4.13
2023-10-06 17:31:14 +01:00
Anselm
97a4282e5e Merge branch 'backend-support' into data-throughput 2023-10-06 17:23:13 +01:00
Anselm
39c13b50a3 Update docs 2023-10-06 17:18:30 +01:00
Anselm
ad304e321b Lots of improvements for BinaryCoStreams & file-drop example 2023-10-06 17:09:58 +01:00
Anselm
8c0b2da461 Start custom solution with nextjs & shiki-twoslash 2023-10-06 09:43:16 +01:00
Anselm
72fce45b2b Publish
- jazz-example-pets@0.0.21
 - jazz-example-todo@0.0.45
 - jazz-example-twit@0.0.8
 - cojson@0.4.8
 - cojson-simple-sync@0.4.8
 - cojson-storage-indexeddb@0.4.8
 - cojson-storage-sqlite@0.4.8
 - jazz-autosub@0.4.8
 - jazz-browser@0.4.8
 - jazz-browser-auth-local@0.4.8
 - jazz-browser-media-images@0.4.9
 - jazz-react@0.4.8
 - jazz-react-auth-local@0.4.8
2023-10-04 22:01:28 +01:00
Anselm
1f49d7fda6 Actually fix circular issues for esbuild/vite 2023-10-04 22:00:38 +01:00
Anselm
eec8ee7027 Publish
- jazz-example-pets@0.0.20
 - jazz-example-todo@0.0.44
 - jazz-example-twit@0.0.7
 - cojson@0.4.7
 - cojson-simple-sync@0.4.7
 - cojson-storage-indexeddb@0.4.7
 - cojson-storage-sqlite@0.4.7
 - jazz-autosub@0.4.7
 - jazz-browser@0.4.7
 - jazz-browser-auth-local@0.4.7
 - jazz-browser-media-images@0.4.8
 - jazz-react@0.4.7
 - jazz-react-auth-local@0.4.7
2023-10-04 21:23:52 +01:00
Anselm
188eb2e1e3 Update docs 2023-10-04 21:23:28 +01:00
Anselm
62867b32d9 Get rid of more cyclic imports 2023-10-04 21:22:52 +01:00
Anselm
ccebd2447d Publish
- jazz-example-pets@0.0.19
 - jazz-example-todo@0.0.43
 - jazz-example-twit@0.0.6
 - cojson@0.4.6
 - cojson-simple-sync@0.4.6
 - cojson-storage-indexeddb@0.4.6
 - cojson-storage-sqlite@0.4.6
 - jazz-autosub@0.4.6
 - jazz-browser@0.4.6
 - jazz-browser-auth-local@0.4.6
 - jazz-browser-media-images@0.4.7
 - jazz-react@0.4.6
 - jazz-react-auth-local@0.4.6
2023-10-04 21:01:44 +01:00
Anselm
08dca75789 Address some circular deps in cojson typescript 2023-10-04 21:00:59 +01:00
Anselm
16b3e1381b Sketch of new homepage with nextra + chat example 2023-10-04 19:59:32 +01:00
Anselm
f1cd639a09 Add project list to todo example 2023-10-03 13:59:00 +01:00
Anselm Eickhoff
be18e4de14 Merge pull request #111 from gardencmp/autosub-api
Autosub API
2023-10-01 13:18:40 +01:00
Anselm
7e62c91d44 Publish
- jazz-example-pets@0.0.18
 - jazz-example-twit@0.0.5
 - jazz-browser-media-images@0.4.6
2023-10-01 12:27:10 +01:00
Anselm
b2d5a103b5 move image-blob-reduce types to proper deps 2023-10-01 12:26:53 +01:00
Anselm
4ee2cad39e Publish
- jazz-example-pets@0.0.17
 - jazz-example-todo@0.0.42
 - jazz-example-twit@0.0.4
 - cojson@0.4.5
 - cojson-simple-sync@0.4.5
 - cojson-storage-indexeddb@0.4.5
 - cojson-storage-sqlite@0.4.5
 - jazz-autosub@0.4.5
 - jazz-browser@0.4.5
 - jazz-browser-auth-local@0.4.5
 - jazz-browser-media-images@0.4.5
 - jazz-react@0.4.5
 - jazz-react-auth-local@0.4.5
2023-10-01 12:18:35 +01:00
Anselm
b7c8a0038b Rename syncedQueries -> autosub and clean up a lot of APIs 2023-10-01 12:16:56 +01:00
Anselm
8c27e8c379 doc fixes 2023-09-28 23:05:59 +01:00
Anselm Eickhoff
0133aa47ff Merge pull request #105 from gardencmp/example-twit
Twitter example
2023-09-28 11:51:38 +01:00
Anselm
5659c925a2 Change Twit html title 2023-09-28 11:44:44 +01:00
Anselm
27779ac792 Publish
- jazz-example-pets@0.0.16
 - jazz-example-todo@0.0.41
 - jazz-example-twit@0.0.3
 - cojson@0.4.1
 - cojson-simple-sync@0.4.1
 - cojson-storage-indexeddb@0.4.1
 - cojson-storage-sqlite@0.4.1
 - jazz-browser@0.4.1
 - jazz-browser-auth-local@0.4.1
 - jazz-browser-media-images@0.4.1
 - jazz-react@0.4.1
 - jazz-react-auth-local@0.4.1
2023-09-28 11:25:36 +01:00
Anselm
3f1bfa4629 Improve twit example 2023-09-28 11:25:09 +01:00
Anselm
15a693c3ed Simplify QueriedCoStream 2023-09-28 11:23:23 +01:00
Anselm
b1d620e145 Update docs 2023-09-28 11:23:06 +01:00
Anselm
478fbd0aa9 Bigger inputs on mobile 2023-09-27 22:21:19 +01:00
Anselm
ee906b7351 Add QR code to own profile 2023-09-27 22:13:55 +01:00
Anselm
dd15f21ccb Fix follow button 2023-09-27 21:51:20 +01:00
Anselm
d7cd5fda7c Actually deploy twit example 2023-09-27 21:43:07 +01:00
Anselm
174300b00f Deploy twit example 2023-09-27 21:39:30 +01:00
Anselm
b2c8d8c855 Publish
- jazz-example-pets@0.0.15
 - jazz-example-todo@0.0.40
 - jazz-example-twit@0.0.2
 - cojson@0.4.0
 - cojson-simple-sync@0.4.0
 - cojson-storage-indexeddb@0.4.0
 - cojson-storage-sqlite@0.4.0
 - jazz-browser@0.4.0
 - jazz-browser-auth-local@0.4.0
 - jazz-browser-media-images@0.4.0
 - jazz-react@0.4.0
 - jazz-react-auth-local@0.4.0
2023-09-27 21:37:56 +01:00
Anselm
2bad2b6bfe Update docs 2023-09-27 21:37:22 +01:00
Anselm
880d0ff855 Fix last lint issues 2023-09-27 21:37:04 +01:00
Anselm
e66cbee6cd Implement Twitter example 2023-09-27 21:27:49 +01:00
Anselm
03e470721e AAAAAAAAAA 2023-09-27 15:08:09 +01:00
Anselm
ecf73bcfa7 Basic account initialization, fixes #103 2023-09-26 18:07:14 +01:00
Anselm
2c3a500286 Add root to queried group 2023-09-26 17:47:31 +01:00
Anselm
8b83061cf4 Update docs 2023-09-26 17:42:48 +01:00
Anselm
e75c3207d6 Make Groups and Accounts behave like proper CoValues, fixes #101 2023-09-26 17:42:28 +01:00
Anselm
41d4b5ba0b Ability to add/remove the public as readers & writers #99 2023-09-26 11:19:39 +01:00
Anselm
21fa1b168b First sketch of twit example 2023-09-26 09:50:08 +01:00
Anselm
91e5e7f2ab v0.3.7 2023-09-24 20:25:23 +01:00
Anselm
e3f7e2f1bd Actually use delayOnerror 2023-09-24 20:24:58 +01:00
Anselm
084cf80c60 v0.3.6 2023-09-24 20:16:25 +01:00
Anselm
632e3bbb08 Add option for delay on error when handling peer messages 2023-09-24 20:15:10 +01:00
Anselm
17d17833b2 Publish
- jazz-example-pets@0.0.14
 - jazz-example-todo@0.0.39
 - cojson@0.3.5
 - cojson-simple-sync@0.3.7
 - cojson-storage-indexeddb@0.3.5
 - cojson-storage-sqlite@0.3.7
 - jazz-browser@0.3.5
 - jazz-browser-auth-local@0.3.5
 - jazz-browser-media-images@0.3.5
 - jazz-react@0.3.5
 - jazz-react-auth-local@0.3.5
 - jazz-react-media-images@0.3.5
2023-09-22 15:18:21 +01:00
Anselm
8e22bd9c1e Lint fix 2023-09-22 15:17:44 +01:00
Anselm
98213743f3 deploy bump 2023-09-22 15:15:09 +01:00
455 changed files with 49612 additions and 36742 deletions

8
.changeset/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

24
.changeset/config.json Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [
[
"cojson",
"jazz-tools",
"jazz-browser",
"jazz-browser-media-images",
"jazz-react",
"jazz-nodejs",
"jazz-run",
"cojson-transport-nodejs-ws",
"cojson-storage-indexeddb",
"cojson-storage-sqlite"
]
],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

View File

@@ -7,22 +7,26 @@ on:
branches: [ "main" ]
jobs:
build:
build-examples:
runs-on: ubuntu-latest
strategy:
matrix:
example: ["todo", "pets"]
example: ["chat", "pets", "todo"]
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
steps:
- uses: actions/checkout@v3
with:
submodules: true
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
cache-dependency-path: yarn.lock
cache: 'pnpm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
@@ -34,14 +38,10 @@ jobs:
username: gardencmp
password: ${{ secrets.GITHUB_TOKEN }}
- name: Nuke Workspace
- name: Pnpm Build
run: |
rm package.json yarn.lock;
- name: Yarn Build
run: |
yarn install --frozen-lockfile;
yarn build;
pnpm install
pnpm turbo build;
working-directory: ./examples/${{ matrix.example }}
- name: Docker Build & Push
@@ -53,12 +53,60 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
build-homepage:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v3
with:
submodules: true
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'pnpm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: gardencmp
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pnpm Install (root)
run: |
pnpm install
working-directory: .
- name: Pnpm Install & Build (homepage)
run: |
pnpm install
pnpm build;
working-directory: ./homepage/homepage
- name: Docker Build & Push
uses: docker/build-push-action@v4
with:
context: ./homepage
push: true
tags: ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-examples:
runs-on: ubuntu-latest
needs: build-examples
strategy:
matrix:
example: ["todo", "pets"]
example: ["chat", "pets", "todo"]
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
steps:
- uses: actions/checkout@v3
@@ -86,5 +134,38 @@ jobs:
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
cat job-instance.nomad;
NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
working-directory: ./examples/${{ matrix.example }}
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
working-directory: ./examples/${{ matrix.example }}
deploy-homepage:
runs-on: ubuntu-latest
needs: build-homepage
steps:
- uses: actions/checkout@v3
with:
submodules: true
- uses: gacts/install-nomad@v1
- name: Tailscale
uses: tailscale/github-action@v1
with:
authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
- name: Deploy on Nomad
run: |
if [ "${{github.ref_name}}" == "main" ]; then
export BRANCH_SUFFIX="";
export BRANCH_SUBDOMAIN="";
else
export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
fi
export DOCKER_USER=gardencmp;
export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
cat job-instance.nomad;
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
working-directory: ./homepage

4
.gitignore vendored
View File

@@ -1,4 +1,6 @@
node_modules
yarn-error.log
lerna-debug.log
docsTmp
docsTmp
.DS_Store
.turbo

2
.husky/pre-commit Normal file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"

View File

@@ -1,3 +0,0 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

7724
DOCS.md

File diff suppressed because it is too large Load Diff

19
LICENSE.txt Normal file
View File

@@ -0,0 +1,19 @@
Copyright 2024, Garden Computing, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

118
README.md
View File

@@ -1,116 +1,12 @@
# Jazz - instant sync
<sub>Homepage: [jazz.tools](https://jazz.tools) &mdash; Docs: [DOCS.md](./DOCS.md) &mdash; Community & support: [Discord](https://discord.gg/utDMjHYg42) &mdash; Updates: [Twitter](https://twitter.com/jazz_tools) & [Email](https://gcmp.io/news)</sub>
**Jazz is an open-source toolkit for building apps with *secure sync.***
Quickly build and ship apps with:
- **Cross-device sync**
- **Collaborative features** (incl. real-time multiplayer)
- **Instantly reacting UIs**
- Local-first storage & offline support
- File upload and real-time media streaming
# What is *secure sync*?
**Sync** means that, *instead of making API requests*, you:
- **Read and write data as if it was local** &mdash; from anywhere in your app.
- **Always have data synced to wherever it's needed, instantly:** to other devices of the same user, to other users, to your backend, to your local machine for debugging, etc.
**Secure** means that, *instead of relying on your API or DB for access control*, you:
- **Set fine-grained, role-based permissions in `Group`s** that are **synced along with your data**.
- **Permissions *verifiably enforced* everywhere,** using encryption & signatures under the hood.
- **Change roles dynamically** for evolving teams, expiring invite links and more.
# What's special about Jazz?
Compared to other libraries and frameworks for local-first, sync-based or real-time apps, these are some of the things that make Jazz unique:
- **Jazz is a *batteries-included,* vertically integrated toolkit,** offering everything you need to build an app, including auth, permissions, data model, sync, conflict resolution, blob storage, file uploads, real-time media streaming and more.
- **Jazz has a *small API surface* of only a few abstractions to learn,** which combine in powerful ways to implement a broad set of features.
- **Jazz *granularly* loads and caches *only the data that is needed*,** combining *local-first* instant UI reactivity and offline support with the on-demand data efficiency of conventional APIs
- **Jazz supports end-to-end encryption, but doesn't require it,** allowing you to either manage your user's secret keys for them (based on existing auth flows) or letting your users
- **Jazz is based on CoJSON, a soon-to-be *open standard,*** which means that there will be a whole ecosystem of compatible libraries and frameworks in a variety of environments &mdash; and it will be easy to achieve (secure) interop between Jazz/CoJSON-based apps and services.
# Jazz Global Mesh
Jazz is open source and you can run your own sync & storage server, but to really provide you with everything you need, we're also running
**[Jazz Global Mesh](https://jazz.tools/mesh)**, a globally distributed mesh of servers optimized for:
- **Ultra-low-latency sync** (with geo-aware edge caching and optimal routing)
- **Low-cost, reliable storage**
# Jazz - Instant sync
**Jazz Global Mesh is free for small volumes of data** and it's the **default syncing peer,** so you can **start building multi-user Jazz apps with persistent data in minutes,** using only frontend code!
# Getting started
**Jazz is an open-source toolkit for building apps with *distributed state.***
## Example App Walkthrough
- Homepage: [jazz.tools](https://jazz.tools)
- Docs: [jazz.tools/docs](https://jazz.tools/docs)
- Community & support: [Discord](https://discord.gg/utDMjHYg42)
- Updates: [Twitter](https://twitter.com/jazz_tools) & [Email](https://gcmp.io/news)
**For now the best tutorial is the walkthrough of the [Todo List Example App](#todo-list).**
## General Scenarios
### Building a new, entirely sync-based React app
1. Define your data model with [cojson Collaborative Values (CoValues)](./DOCS.md/#covalue).
2. Implement permission logic using [cojson Groups](./DOCS.md/#group).
3. Build a user interface with [jazz-react](./DOCS.md/#jazz-react)'s reactive [synced queries](./DOCS.md/#usesyncedqueryid).
### Gradually adding sync to an existing React app
Gradually migrate app features to use sync:
1. Define data model for small aspect of your app with [cojson Collaborative Values (CoValues)](./DOCS.md/#covalue).
- Schema adapters/importers for Prisma/Drizzle/PostgreSQL introspection coming soon.
2. Map existing permission logic with [cojson Groups](./DOCS.md/#group) & integrate existing auth.
- Auth integrations coming soon.
3. Replace some of the React state and API requests in your UI with [jazz-react](./DOCS.md/#jazz-react)'s reactive [synced queries](./DOCS.md/#usesyncedqueryid).
# Example Apps
## Todo List
**A simple collaborative todo list app.**
Live version: https://example-todo.jazz.tools
Source code & walkthrough: [`./examples/todo`](./examples/todo)
Demonstrates:
- Defining a data model with `CoMap`s and `CoList`s
- Creating data and setting permissions with `Group`s
- Fetching, rendering & editing data from nested `CoValue`s with reactive synced queries
## Rate-My-Pet
**A simple social polling app.**
Live version: https://example-pets.jazz.tools
Source code (walkthrough coming soon): [`./examples/pets`](./examples/pets)
Demonstrates:
- Implementing per-account data streams (reactions) with `CoStream`s
- Implementing image upload and progressive image streaming using helpers from `jazz-react-media-images` (on top of CoJSON's `BinaryCoStreams` & `ImageDefinition` convention)
# Documentation & API Reference
For now, docs are hosted in a single well-structured markdown file: [`./DOCS.md`](./DOCS.md).
- [Package Overview](./DOCS.md/#overview)
- [`jazz-react` API](./DOCS.md/#jazz-react)
- [`cojson` API](./DOCS.md/#cojson)
- [`jazz-react-media-images` API](./DOCS.md/#jazz-react-media-images)
In the future we'll build a dedicated docs page on the Jazz homepage.
----
Copyright 2023: Garden Computing, Inc.
Copyright 2024 &mdash; Garden Computing, Inc.

View File

@@ -0,0 +1,13 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {},
}

24
examples/chat/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

480
examples/chat/CHANGELOG.md Normal file
View File

@@ -0,0 +1,480 @@
# jazz-example-chat
## 0.0.51
### Patch Changes
- Updated dependencies
- jazz-react@0.7.4
## 0.0.50
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.3
- jazz-react@0.7.3
## 0.0.49
### Patch Changes
- Updated dependencies
- jazz-react@0.7.2
## 0.0.48
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.1
- jazz-react@0.7.1
## 0.0.47
### Patch Changes
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [96c494f]
- Updated dependencies [59c18c3]
- Updated dependencies [19f52b7]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [d8fe2b1]
- Updated dependencies [19004b4]
- Updated dependencies [a78f168]
- Updated dependencies [1200aae]
- Updated dependencies [60d5ca2]
- Updated dependencies [52675c9]
- Updated dependencies [129e2c1]
- Updated dependencies [6d49e9b]
- Updated dependencies [1cfa279]
- Updated dependencies [704af7d]
- Updated dependencies [e97f730]
- Updated dependencies [1a35307]
- Updated dependencies [460478f]
- Updated dependencies [6b0418f]
- Updated dependencies [e299c3e]
- Updated dependencies [ed5643a]
- Updated dependencies [bde684f]
- Updated dependencies [bf0f8ec]
- Updated dependencies [c4151fc]
- Updated dependencies [63374cc]
- Updated dependencies [8636319]
- Updated dependencies [01ac646]
- Updated dependencies [a5e68a4]
- Updated dependencies [8636319]
- Updated dependencies [952982e]
- Updated dependencies [1a35307]
- Updated dependencies [5fa277c]
- Updated dependencies [60d5ca2]
- Updated dependencies [21771c4]
- Updated dependencies [77c2b56]
- Updated dependencies [63374cc]
- Updated dependencies [d2e03ff]
- Updated dependencies [354bdcd]
- Updated dependencies [ece35b3]
- Updated dependencies [60d5ca2]
- Updated dependencies [69ac514]
- Updated dependencies [f8a5c46]
- Updated dependencies [f0f6f1b]
- Updated dependencies [e5eed5b]
- Updated dependencies [1a44f87]
- Updated dependencies [627d895]
- Updated dependencies [1200aae]
- Updated dependencies [63374cc]
- Updated dependencies [ece35b3]
- Updated dependencies [38d4410]
- Updated dependencies [85d2b62]
- Updated dependencies [fd86c11]
- Updated dependencies [52675c9]
- jazz-tools@0.7.0
- cojson@0.7.0
- jazz-react@0.7.0
- hash-slash@0.2.0
## 0.0.47-alpha.42
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.42
- cojson@0.7.0-alpha.42
- jazz-react@0.7.0-alpha.42
## 0.0.47-alpha.41
### Patch Changes
- jazz-tools@0.7.0-alpha.41
- jazz-react@0.7.0-alpha.41
## 0.0.47-alpha.40
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.40
## 0.0.47-alpha.39
### Patch Changes
- Updated dependencies
- cojson@0.7.0-alpha.39
- jazz-react@0.7.0-alpha.39
- jazz-tools@0.7.0-alpha.39
## 0.0.47-alpha.38
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.38
- jazz-react@0.7.0-alpha.38
- cojson@0.7.0-alpha.38
## 0.0.47-alpha.37
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.37
- cojson@0.7.0-alpha.37
- jazz-tools@0.7.0-alpha.37
## 0.0.47-alpha.36
### Patch Changes
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [6b0418f]
- Updated dependencies [1a35307]
- cojson@0.7.0-alpha.36
- jazz-tools@0.7.0-alpha.36
- jazz-react@0.7.0-alpha.36
## 0.0.47-alpha.35
### Patch Changes
- Updated dependencies
- Updated dependencies
- cojson@0.7.0-alpha.35
- jazz-tools@0.7.0-alpha.35
- jazz-react@0.7.0-alpha.35
## 0.0.47-alpha.34
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.34
- jazz-react@0.7.0-alpha.34
## 0.0.47-alpha.33
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.33
## 0.0.47-alpha.32
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- hash-slash@0.2.0-alpha.3
- jazz-tools@0.7.0-alpha.32
- jazz-react@0.7.0-alpha.32
## 0.0.47-alpha.31
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.31
- jazz-react@0.7.0-alpha.31
## 0.0.47-alpha.30
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.30
- jazz-react@0.7.0-alpha.30
## 0.0.47-alpha.29
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.29
- cojson@0.7.0-alpha.29
- jazz-react@0.7.0-alpha.29
## 0.0.47-alpha.28
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.28
- cojson@0.7.0-alpha.28
- jazz-react@0.7.0-alpha.28
## 0.0.47-alpha.27
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.27
- cojson@0.7.0-alpha.27
- jazz-react@0.7.0-alpha.27
## 0.0.47-alpha.26
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.26
- jazz-react@0.7.0-alpha.26
## 0.0.47-alpha.25
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.25
- jazz-react@0.7.0-alpha.25
## 0.0.47-alpha.24
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.24
- cojson@0.7.0-alpha.24
- jazz-react@0.7.0-alpha.24
## 0.0.47-alpha.23
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.23
- jazz-react@0.7.0-alpha.23
## 0.0.47-alpha.22
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.22
- jazz-react@0.7.0-alpha.22
## 0.0.47-alpha.21
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.21
- jazz-tools@0.7.0-alpha.21
## 0.0.47-alpha.20
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-react@0.7.0-alpha.20
- jazz-tools@0.7.0-alpha.20
## 0.0.47-alpha.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.19
- jazz-react@0.7.0-alpha.19
## 0.0.47-alpha.18
### Patch Changes
- jazz-react@0.7.0-alpha.18
## 0.0.47-alpha.17
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.17
- jazz-react@0.7.0-alpha.17
## 0.0.47-alpha.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.16
- jazz-react@0.7.0-alpha.16
## 0.0.47-alpha.15
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.15
- jazz-react@0.7.0-alpha.15
## 0.0.47-alpha.14
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.14
- jazz-react@0.7.0-alpha.14
## 0.0.47-alpha.13
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.13
- jazz-react@0.7.0-alpha.13
## 0.0.47-alpha.12
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.12
- jazz-tools@0.7.0-alpha.12
## 0.0.47-alpha.11
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.11
- jazz-tools@0.7.0-alpha.11
- cojson@0.7.0-alpha.11
## 0.0.47-alpha.10
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.10
- jazz-tools@0.7.0-alpha.10
- cojson@0.7.0-alpha.10
## 0.0.47-alpha.9
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.9
- jazz-tools@0.7.0-alpha.9
## 0.0.47-alpha.8
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.8
- jazz-tools@0.7.0-alpha.8
## 0.0.47-alpha.7
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.7
- jazz-tools@0.7.0-alpha.7
- cojson@0.7.0-alpha.7
## 0.0.47-alpha.6
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.6
- jazz-tools@0.7.0-alpha.6
## 0.0.47-alpha.5
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.5
- jazz-tools@0.7.0-alpha.5
- cojson@0.7.0-alpha.5
## 0.0.47-alpha.4
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.4
- jazz-react@0.7.0-alpha.4
## 0.0.47-alpha.3
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.3
- jazz-react@0.7.0-alpha.3
## 0.0.47-alpha.2
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.2
- jazz-react@0.7.0-alpha.2
- jazz-tools@0.7.0-alpha.2
## 0.0.47-alpha.1
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.1
- jazz-react@0.7.0-alpha.1
- jazz-tools@0.7.0-alpha.1
- cojson@0.7.0-alpha.1
## 0.0.47-alpha.0
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.0
- jazz-react@0.7.0-alpha.0
- jazz-tools@0.7.0-alpha.0
- cojson@0.7.0-alpha.0
## 0.0.46
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

4
examples/chat/Dockerfile Normal file
View File

@@ -0,0 +1,4 @@
FROM caddy:2.7.3-alpine
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
COPY ./dist /usr/share/caddy/

42
examples/chat/README.md Normal file
View File

@@ -0,0 +1,42 @@
# Jazz Chat Example
Live version: https://example-chat.jazz.tools
## Installing & running the example locally
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
Start by checking out `jazz`
```bash
git clone https://github.com/gardencmp/jazz.git
cd jazz/examples/chat
pnpm pack --pack-destination /tmp
mkdir -p ~/jazz-examples/chat # or any other directory
tar -xf /tmp/jazz-example-chat-* --strip-components 1 -C ~/jazz-examples/chat
cd ~/jazz-examples/chat
```
This ensures that you have the example app without git history and independent of the Jazz multi-package monorepo.
Install dependencies:
```bash
pnpm install
```
Start the dev server:
```bash
pnpm dev
```
## Questions / problems / feedback
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
## Configuration: sync server
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<Jazz.Provider>` provider component in [./src/2_main.tsx](./src/2_main.tsx).

14
examples/chat/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/jazz-logo.png" />
<link rel="stylesheet" href="/src/index.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jazz Chat Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,56 @@
job "chat$BRANCH_SUFFIX" {
region = "global"
datacenters = ["*"]
group "static" {
count = 4
network {
port "http" {
to = 80
}
}
constraint {
attribute = "${node.class}"
operator = "="
value = "mesh"
}
spread {
attribute = "${node.datacenter}"
weight = 100
}
constraint {
distinct_hosts = true
}
task "server" {
driver = "docker"
config {
image = "$DOCKER_TAG"
ports = ["http"]
auth = {
username = "$DOCKER_USER"
password = "$DOCKER_PASSWORD"
}
}
service {
tags = ["public"]
name = "chat$BRANCH_SUFFIX"
port = "http"
provider = "consul"
}
resources {
cpu = 50 # MHz
memory = 50 # MB
}
}
}
}
# deploy bump 4

View File

@@ -0,0 +1,54 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.51",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "echo 'chat example is codegolfed'",
"preview": "vite preview"
},
"lint-staged": {
"*.{ts,tsx}": "eslint --fix",
"*.{js,jsx,mdx,json}": "prettier --write"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.4",
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"hash-slash": "workspace:*",
"jazz-tools": "workspace:*",
"jazz-react": "workspace:*",
"cojson": "workspace:*",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"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"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^5.0.10"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

42
examples/chat/src/app.tsx Normal file
View File

@@ -0,0 +1,42 @@
import { CoMap, CoList, co, Group, ID } from "jazz-tools";
import { createJazzReactContext, DemoAuth } from "jazz-react";
import { createRoot } from "react-dom/client";
import { useIframeHashRouter } from "hash-slash";
import { ChatScreen } from "./chatScreen.tsx";
export class Message extends CoMap {
text = co.string;
}
export class Chat extends CoList.Of(co.ref(Message)) {}
const Jazz = createJazzReactContext({
auth: DemoAuth({ appName: "Jazz Chat" }),
peer: `wss://mesh.jazz.tools/?key=you@example.com`
});
export const { useAccount, useCoState } = Jazz;
function App() {
const { me, logOut } = useAccount();
const createChat = () => {
const group = Group.create({ owner: me });
group.addMember("everyone", "writer");
const chat = Chat.create([], { owner: group });
location.hash = "/chat/" + chat.id;
};
return <div className="flex flex-col items-center justify-between w-screen h-screen p-2 dark:bg-black dark:text-white">
<div className="rounded mb-5 px-2 py-1 text-sm self-end">
{me.profile?.name} · <button onClick={logOut}>Log Out</button>
</div>
{useIframeHashRouter().route({
'/': () => createChat() as never,
'/chat/:id': (id) => <ChatScreen chatID={id as ID<Chat>} />
})}
</div>;
}
createRoot(document.getElementById("root")!)
.render(<Jazz.Provider><App/></Jazz.Provider>);

View File

@@ -0,0 +1,42 @@
import { ID } from 'jazz-tools';
import { Chat, Message, useCoState } from './app.tsx';
export function ChatScreen(props: { chatID: ID<Chat> }) {
const chat = useCoState(Chat, props.chatID, [{}]);
return chat ? <div className='w-full max-w-xl h-full flex flex-col items-stretch'>
{chat.length > 0
? chat.map((msg) => <ChatBubble msg={msg} key={msg.id} />)
: <div className='m-auto text-sm'>(Empty chat)</div>}
<ChatInput onSubmit={(text) => {
chat.push(
Message.create({ text }, { owner: chat._owner })
);
}} />
</div> : <div>Loading...</div>;
}
function ChatBubble(props: { msg: Message }) {
const lastEdit = props.msg._edits.text;
const align = lastEdit.by?.isMe ? 'items-end' : 'items-start';
return <div className={`${align} flex flex-col`}>
<div className='rounded-xl bg-stone-100 dark:bg-stone-700 dark:text-white py-2 px-4 mt-2 min-w-[5rem]'>
{ props.msg.text }
</div>
<div className='text-xs text-neutral-500 ml-2'>
{ lastEdit.by?.profile?.name }{' '}
{ lastEdit.madeAt?.toLocaleTimeString() }
</div>
</div>;
}
function ChatInput(props: { onSubmit: (text: string) => void }) {
return <input className='rounded p-2 border mt-auto dark:bg-black dark:text-white border-stone-300 dark:border-stone-700'
placeholder='Type a message and press Enter'
onKeyDown={({ key, currentTarget: input }) => {
if (key !== 'Enter' || !input.value) return;
props.onSubmit(input.value);
input.value = '';
}} />;
}

View File

@@ -0,0 +1,78 @@
@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;
}
}

1
examples/chat/src/vite-env.d.ts vendored Normal file
View File

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

View File

@@ -0,0 +1,75 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
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))",
},
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: [require("tailwindcss-animate")],
}

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
minify: false
}
})

View File

@@ -5,6 +5,7 @@ module.exports = {
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',

View File

@@ -0,0 +1,9 @@
/** @type {import("prettier").Config} */
const config = {
trailingComma: "all",
tabWidth: 4,
semi: true,
singleQuote: false,
};
export default config;

498
examples/pets/CHANGELOG.md Normal file
View File

@@ -0,0 +1,498 @@
# jazz-example-pets
## 0.0.69
### Patch Changes
- Updated dependencies
- jazz-react@0.7.4
## 0.0.68
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.3
- jazz-browser-media-images@0.7.3
- jazz-react@0.7.3
## 0.0.67
### Patch Changes
- Updated dependencies
- jazz-react@0.7.2
## 0.0.66
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.1
- jazz-browser-media-images@0.7.1
- jazz-react@0.7.1
## 0.0.65
### Patch Changes
- Updated dependencies [8636319]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [96c494f]
- Updated dependencies [59c18c3]
- Updated dependencies [19f52b7]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [d8fe2b1]
- Updated dependencies [19004b4]
- Updated dependencies [a78f168]
- Updated dependencies [52675c9]
- Updated dependencies [129e2c1]
- Updated dependencies [6d49e9b]
- Updated dependencies [1cfa279]
- Updated dependencies [704af7d]
- Updated dependencies [e97f730]
- Updated dependencies [460478f]
- Updated dependencies [6b0418f]
- Updated dependencies [e299c3e]
- Updated dependencies [ed5643a]
- Updated dependencies [bde684f]
- Updated dependencies [c4151fc]
- Updated dependencies [63374cc]
- Updated dependencies [01ac646]
- Updated dependencies [a5e68a4]
- Updated dependencies [8636319]
- Updated dependencies [952982e]
- Updated dependencies [1a35307]
- Updated dependencies [5fa277c]
- Updated dependencies [60d5ca2]
- Updated dependencies [21771c4]
- Updated dependencies [77c2b56]
- Updated dependencies [63374cc]
- Updated dependencies [d2e03ff]
- Updated dependencies [354bdcd]
- Updated dependencies [ece35b3]
- Updated dependencies [60d5ca2]
- Updated dependencies [69ac514]
- Updated dependencies [f8a5c46]
- Updated dependencies [f0f6f1b]
- Updated dependencies [e5eed5b]
- Updated dependencies [1a44f87]
- Updated dependencies [627d895]
- Updated dependencies [1200aae]
- Updated dependencies [63374cc]
- Updated dependencies [ece35b3]
- Updated dependencies [38d4410]
- Updated dependencies [85d2b62]
- Updated dependencies [fd86c11]
- Updated dependencies [52675c9]
- jazz-tools@0.7.0
- jazz-browser-media-images@0.7.0
- jazz-react@0.7.0
## 0.0.65-alpha.42
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.42
- jazz-browser-media-images@0.7.0-alpha.40
- jazz-react@0.7.0-alpha.42
## 0.0.65-alpha.41
### Patch Changes
- jazz-tools@0.7.0-alpha.41
- jazz-browser-media-images@0.7.0-alpha.39
- jazz-react@0.7.0-alpha.41
## 0.0.65-alpha.40
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.40
## 0.0.65-alpha.39
### Patch Changes
- jazz-react@0.7.0-alpha.39
- jazz-tools@0.7.0-alpha.39
- jazz-browser-media-images@0.7.0-alpha.38
## 0.0.65-alpha.38
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.38
- jazz-react@0.7.0-alpha.38
- jazz-browser-media-images@0.7.0-alpha.37
## 0.0.65-alpha.37
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.37
- jazz-browser-media-images@0.7.0-alpha.36
- jazz-tools@0.7.0-alpha.37
## 0.0.65-alpha.36
### Patch Changes
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [6b0418f]
- Updated dependencies [1a35307]
- jazz-tools@0.7.0-alpha.36
- jazz-react@0.7.0-alpha.36
- jazz-browser-media-images@0.7.0-alpha.35
## 0.0.65-alpha.35
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.35
- jazz-react@0.7.0-alpha.35
- jazz-browser-media-images@0.7.0-alpha.34
## 0.0.65-alpha.34
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.34
- jazz-browser-media-images@0.7.0-alpha.33
- jazz-react@0.7.0-alpha.34
## 0.0.65-alpha.33
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.33
## 0.0.65-alpha.32
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.32
- jazz-react@0.7.0-alpha.32
- jazz-browser-media-images@0.7.0-alpha.32
## 0.0.65-alpha.31
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.31
- jazz-browser-media-images@0.7.0-alpha.31
- jazz-react@0.7.0-alpha.31
## 0.0.65-alpha.30
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.30
- jazz-browser-media-images@0.7.0-alpha.30
- jazz-react@0.7.0-alpha.30
## 0.0.65-alpha.29
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.29
- jazz-browser-media-images@0.7.0-alpha.29
- jazz-react@0.7.0-alpha.29
## 0.0.65-alpha.28
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.28
- jazz-browser-media-images@0.7.0-alpha.28
- jazz-react@0.7.0-alpha.28
## 0.0.65-alpha.27
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.27
- jazz-browser-media-images@0.7.0-alpha.27
- jazz-react@0.7.0-alpha.27
## 0.0.65-alpha.26
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.26
- jazz-browser-media-images@0.7.0-alpha.26
- jazz-react@0.7.0-alpha.26
## 0.0.65-alpha.25
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.25
- jazz-browser-media-images@0.7.0-alpha.25
- jazz-react@0.7.0-alpha.25
## 0.0.65-alpha.24
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.24
- jazz-browser-media-images@0.7.0-alpha.24
- jazz-react@0.7.0-alpha.24
## 0.0.65-alpha.23
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.23
- jazz-browser-media-images@0.7.0-alpha.23
- jazz-react@0.7.0-alpha.23
## 0.0.65-alpha.22
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.22
- jazz-browser-media-images@0.7.0-alpha.22
- jazz-react@0.7.0-alpha.22
## 0.0.65-alpha.21
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.21
- jazz-tools@0.7.0-alpha.21
- jazz-browser-media-images@0.7.0-alpha.21
## 0.0.65-alpha.20
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-react@0.7.0-alpha.20
- jazz-tools@0.7.0-alpha.20
- jazz-browser-media-images@0.7.0-alpha.20
## 0.0.65-alpha.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.19
- jazz-browser-media-images@0.7.0-alpha.19
- jazz-react@0.7.0-alpha.19
## 0.0.65-alpha.18
### Patch Changes
- jazz-browser-media-images@0.7.0-alpha.18
- jazz-react@0.7.0-alpha.18
## 0.0.65-alpha.17
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.17
- jazz-browser-media-images@0.7.0-alpha.17
- jazz-react@0.7.0-alpha.17
## 0.0.65-alpha.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.16
- jazz-browser-media-images@0.7.0-alpha.16
- jazz-react@0.7.0-alpha.16
## 0.0.65-alpha.15
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.15
- jazz-browser-media-images@0.7.0-alpha.15
- jazz-react@0.7.0-alpha.15
## 0.0.65-alpha.14
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.14
- jazz-browser-media-images@0.7.0-alpha.14
- jazz-react@0.7.0-alpha.14
## 0.0.65-alpha.13
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.13
- jazz-browser-media-images@0.7.0-alpha.13
- jazz-react@0.7.0-alpha.13
## 0.0.65-alpha.12
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.12
- jazz-react@0.7.0-alpha.12
- jazz-tools@0.7.0-alpha.12
## 0.0.65-alpha.11
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.11
- jazz-react@0.7.0-alpha.11
- jazz-tools@0.7.0-alpha.11
## 0.0.65-alpha.10
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.10
- jazz-react@0.7.0-alpha.10
- jazz-tools@0.7.0-alpha.10
## 0.0.65-alpha.9
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.9
- jazz-react@0.7.0-alpha.9
- jazz-tools@0.7.0-alpha.9
## 0.0.65-alpha.8
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.8
- jazz-react@0.7.0-alpha.8
- jazz-tools@0.7.0-alpha.8
## 0.0.65-alpha.7
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.7
- jazz-react@0.7.0-alpha.7
- jazz-tools@0.7.0-alpha.7
## 0.0.65-alpha.6
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.6
- jazz-react@0.7.0-alpha.6
- jazz-tools@0.7.0-alpha.6
## 0.0.65-alpha.5
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.5
- jazz-react@0.7.0-alpha.5
- jazz-tools@0.7.0-alpha.5
## 0.0.65-alpha.4
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.4
- jazz-browser-media-images@0.7.0-alpha.4
- jazz-react@0.7.0-alpha.4
## 0.0.65-alpha.3
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.3
- jazz-browser-media-images@0.7.0-alpha.3
- jazz-react@0.7.0-alpha.3
## 0.0.65-alpha.2
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.2
- jazz-react@0.7.0-alpha.2
- jazz-tools@0.7.0-alpha.2
## 0.0.65-alpha.1
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.1
- jazz-react@0.7.0-alpha.1
- jazz-tools@0.7.0-alpha.1
## 0.0.65-alpha.0
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.7.0-alpha.0
- jazz-react@0.7.0-alpha.0
- jazz-tools@0.7.0-alpha.0
## 0.0.64
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.6.0
## 0.0.63
### Patch Changes
- Updated dependencies
- jazz-browser-media-images@0.5.0
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -4,41 +4,32 @@ Live version: https://example-pets.jazz.tools
## Installing & running the example locally
Start by checking out just the example app to a folder:
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
Start by checking out `jazz`
```bash
npx degit gardencmp/jazz/examples/pets jazz-example-pets
cd jazz-example-pets
git clone https://github.com/gardencmp/jazz.git
cd jazz/examples/pets
pnpm pack --pack-destination /tmp
mkdir -p ~/jazz-examples/pets # or any other directory
tar -xf /tmp/jazz-example-pets-* --strip-components 1 -C ~/jazz-examples/pets
cd ~/jazz-examples/pets
```
(This ensures that you have the example app without git history or our multi-package monorepo)
This ensures that you have the example app without git history and independent of the Jazz multi-package monorepo.
Install dependencies:
```bash
npm install
pnpm install
```
Start the dev server:
```bash
npm run dev
pnpm dev
```
## Structure
TODO
## Walkthrough
### Main parts
TODO
### Helpers
TODO
## Questions / problems / feedback
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
@@ -48,4 +39,4 @@ If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<WithJazz>` provider component in [./src/0_main.tsx](./src/0_main.tsx).
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<Jazz.Provider>` provider component in [./src/2_main.tsx](./src/2_main.tsx).

View File

@@ -3,7 +3,7 @@ job "example-pets$BRANCH_SUFFIX" {
datacenters = ["*"]
group "static" {
count = 8
count = 4
network {
port "http" {

View File

@@ -1,14 +1,19 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.13",
"version": "0.0.69",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write './src/**/*.{ts,tsx}'",
"preview": "vite preview"
},
"lint-staged": {
"*.{ts,tsx}": "eslint --fix",
"*.{js,jsx,mdx,json}": "prettier --write"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
@@ -16,9 +21,9 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "^0.3.4",
"jazz-react-auth-local": "^0.3.4",
"jazz-react-media-images": "^0.3.4",
"jazz-browser-media-images": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -0,0 +1,59 @@
import {
Account,
CoList,
CoMap,
CoStream,
ImageDefinition,
Profile,
co,
} from "jazz-tools";
/** Walkthrough: Defining the data model with CoJSON
*
* Here, we define our main data model of TODO
*
* TODO
**/
export const ReactionTypes = [
"aww",
"love",
"haha",
"wow",
"tiny",
"chonkers",
] as const;
export type ReactionType = (typeof ReactionTypes)[number];
export class PetReactions extends CoStream.Of(co.json<ReactionType>()) {}
export class PetPost extends CoMap {
name = co.string;
image = co.ref(ImageDefinition);
reactions = co.ref(PetReactions);
}
export class ListOfPosts extends CoList.Of(co.ref(PetPost)) {}
export class PetAccountRoot extends CoMap {
posts = co.ref(ListOfPosts);
}
export class PetAccount extends Account {
profile = co.ref(Profile);
root = co.ref(PetAccountRoot);
migrate(this: PetAccount, creationProps?: { name: string }) {
super.migrate(creationProps);
if (!this._refs.root) {
this.root = PetAccountRoot.create(
{
posts: ListOfPosts.create([], { owner: this }),
},
{ owner: this },
);
}
}
}
/** Walkthrough: Continue with ./2_App.tsx */

View File

@@ -1,29 +0,0 @@
import { CoMap, CoStream, Media } from "cojson";
/** Walkthrough: Defining the data model with CoJSON
*
* Here, we define our main data model of TODO
*
* TODO
**/
export type PetPost = CoMap<{
name: string;
image: Media.ImageDefinition;
reactions: PetReactions;
}>;
export const REACTION_TYPES = [
"aww",
"love",
"haha",
"wow",
"tiny",
"chonkers",
] as const;
export type ReactionType = (typeof REACTION_TYPES)[number];
export type PetReactions = CoStream<ReactionType>;
/** Walkthrough: Continue with ./2_App.tsx */

View File

@@ -1,10 +1,9 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createHashRouter } from "react-router-dom";
import { Link, RouterProvider, createHashRouter } from "react-router-dom";
import "./index.css";
import { WithJazz, useJazz, useAcceptInvite } from "jazz-react";
import { LocalAuth } from "jazz-react-auth-local";
import { createJazzReactContext, PasskeyAuth } from "jazz-react";
import {
Button,
@@ -14,6 +13,7 @@ import {
import { PrettyAuthUI } from "./components/Auth.tsx";
import { NewPetPostForm } from "./3_NewPetPostForm.tsx";
import { RatePetPostUI } from "./4_RatePetPostUI.tsx";
import { PetAccount, PetPost } from "./1_schema.ts";
/** Walkthrough: The top-level provider `<WithJazz/>`
*
@@ -24,17 +24,30 @@ import { RatePetPostUI } from "./4_RatePetPostUI.tsx";
const appName = "Jazz Rate My Pet Example";
const auth = LocalAuth({
const auth = PasskeyAuth<PetAccount>({
appName,
Component: PrettyAuthUI,
accountSchema: PetAccount,
});
const Jazz = createJazzReactContext({
auth,
peer: "wss://mesh.jazz.tools/?key=you@example.com",
});
// eslint-disable-next-line react-refresh/only-export-components
export const { useAccount, useCoState, useAcceptInvite } = Jazz;
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<WithJazz auth={auth}>
<App />
</WithJazz>
</React.StrictMode>
<ThemeProvider>
<TitleAndLogo name={appName} />
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
<Jazz.Provider loading={<div>Loading</div>}>
<App />
</Jazz.Provider>
</div>
</ThemeProvider>
</React.StrictMode>,
);
/** Walkthrough: Creating pet posts & routing in `<App/>`
@@ -45,11 +58,15 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
*/
export default function App() {
const { logOut } = useJazz();
const { logOut } = useAccount();
const router = createHashRouter([
{
path: "/",
element: <PostOverview />,
},
{
path: "/new",
element: <NewPetPostForm />,
},
{
@@ -62,25 +79,46 @@ export default function App() {
},
]);
useAcceptInvite((petPostID) => router.navigate("/pet/" + petPostID));
useAcceptInvite({
invitedObjectSchema: PetPost,
onAccept: (petPostID) => router.navigate("/pet/" + petPostID),
});
return (
<ThemeProvider>
<TitleAndLogo name={appName} />
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
<RouterProvider router={router} />
<>
<RouterProvider router={router} />
<Button
onClick={() => router.navigate("/").then(logOut)}
variant="outline"
>
Log Out
</Button>
</div>
</ThemeProvider>
<Button
onClick={() => router.navigate("/").then(logOut)}
variant="outline"
>
Log Out
</Button>
</>
);
}
/** Walkthrough: continue with ./3_CreatePetPostForm.tsx */
export function PostOverview() {
const { me } = useAccount();
/** Walkthrough: Continue with ./1_types.ts */
const myPosts = me.root?.posts;
return (
<>
{myPosts?.length ? (
<>
<h1>My posts</h1>
{myPosts.map(
(post) =>
post && (
<Link key={post.id} to={"/pet/" + post.id}>
{post.name}
</Link>
),
)}
</>
) : undefined}
<Link to="/new">New post</Link>
</>
);
}

View File

@@ -1,66 +1,77 @@
import { ChangeEvent, useCallback, useState } from "react";
import { useNavigate } from "react-router";
import { CoID, CoMap, Media } from "cojson";
import { useJazz, useSyncedQuery } from "jazz-react";
import { createImage } from "jazz-browser-media-images";
import { PetReactions } from "./1_types";
import { PetPost, PetReactions } from "./1_schema";
import { Input, Button } from "./basicComponents";
import { useLoadImage } from "jazz-react-media-images";
import { useAccount, useCoState } from "./2_main";
import { CoMap, Group, ID, ImageDefinition, co } from "jazz-tools";
import { ProgressiveImg } from "jazz-react";
/** Walkthrough: TODO
*/
type PartialPetPost = CoMap<{
name: string;
image?: Media.ImageDefinition;
reactions: PetReactions;
}>;
class PartialPetPost extends CoMap {
name = co.string;
image = co.ref(ImageDefinition, { optional: true });
reactions = co.ref(PetReactions);
}
export function NewPetPostForm() {
const { localNode } = useJazz();
const { me } = useAccount();
const navigate = useNavigate();
const [newPostId, setNewPostId] = useState<
CoID<PartialPetPost> | undefined
>(undefined);
const [newPostId, setNewPostId] = useState<ID<PartialPetPost> | undefined>(
undefined,
);
const newPetPost = useSyncedQuery(newPostId);
const newPetPost = useCoState(PartialPetPost, newPostId);
const onChangeName = useCallback(
(name: string) => {
if (newPetPost) {
newPetPost.set({ name });
newPetPost.name = name;
} else {
const petPostGroup = localNode.createGroup();
const petPost = petPostGroup.createMap<PartialPetPost>({
name,
reactions: petPostGroup.createStream<PetReactions>(),
});
const petPostGroup = Group.create({ owner: me });
const petPost = PartialPetPost.create(
{
name,
reactions: PetReactions.create([], { owner: me }),
},
{ owner: petPostGroup },
);
setNewPostId(petPost.id);
}
},
[localNode, newPetPost]
[me, newPetPost],
);
const onImageSelected = useCallback(
async (event: ChangeEvent<HTMLInputElement>) => {
if (!newPetPost || !event.target.files) return;
const image = await createImage(
event.target.files[0],
newPetPost.group
);
const image = await createImage(event.target.files[0], {
owner: newPetPost._owner,
});
newPetPost.set({ image });
newPetPost.image = image;
},
[newPetPost]
[newPetPost],
);
const petImage = useLoadImage(newPetPost?.image?.id);
const onSubmit = useCallback(() => {
if (!newPetPost) return;
const myPosts = me.root?.posts;
if (!myPosts) {
throw new Error("No posts list found");
}
myPosts.push(newPetPost as PetPost);
navigate("/pet/" + newPetPost.id);
}, [me.root?.posts, newPetPost, navigate]);
return (
<div className="flex flex-col gap-10">
@@ -73,11 +84,12 @@ export function NewPetPostForm() {
value={newPetPost?.name || ""}
/>
{petImage ? (
<img
className="w-80 max-w-full rounded"
src={petImage.highestResSrc || petImage.placeholderDataURL}
/>
{newPetPost?.image ? (
<ProgressiveImg image={newPetPost.image}>
{({ src }) => (
<img className="w-80 max-w-full rounded" src={src} />
)}
</ProgressiveImg>
) : (
<Input
type="file"
@@ -87,13 +99,7 @@ export function NewPetPostForm() {
)}
{newPetPost?.name && newPetPost?.image && (
<Button
onClick={() => {
navigate("/pet/" + newPetPost.id);
}}
>
Submit Post
</Button>
<Button onClick={onSubmit}>Submit Post</Button>
)}
</div>
);

View File

@@ -1,18 +1,20 @@
import { useParams } from "react-router";
import { CoID, Queried } from "cojson";
import { useSyncedQuery } from "jazz-react";
import { PetPost, ReactionType, REACTION_TYPES, PetReactions } from "./1_types";
import { PetPost, PetReactions, ReactionTypes } from "./1_schema";
import { ShareButton } from "./components/ShareButton";
import { Button, Skeleton } from "./basicComponents";
import { useLoadImage } from "jazz-react-media-images";
import uniqolor from "uniqolor";
import { ID } from "jazz-tools";
import { useCoState } from "./2_main";
import { ProgressiveImg } from "jazz-react";
/** Walkthrough: TODO
*/
const reactionEmojiMap: { [reaction in ReactionType]: string } = {
const reactionEmojiMap: {
[reaction in (typeof ReactionTypes)[number]]: string;
} = {
aww: "😍",
love: "❤️",
haha: "😂",
@@ -22,10 +24,9 @@ const reactionEmojiMap: { [reaction in ReactionType]: string } = {
};
export function RatePetPostUI() {
const petPostID = useParams<{ petPostId: CoID<PetPost> }>().petPostId;
const petPostID = useParams<{ petPostId: ID<PetPost> }>().petPostId;
const petPost = useSyncedQuery(petPostID);
const petImage = useLoadImage(petPost?.image);
const petPost = useCoState(PetPost, petPostID);
return (
<div className="flex flex-col gap-8">
@@ -34,19 +35,18 @@ export function RatePetPostUI() {
<ShareButton petPost={petPost} />
</div>
{petImage && (
<img
className="w-80 max-w-full rounded"
src={petImage.highestResSrc || petImage.placeholderDataURL}
/>
)}
<ProgressiveImg image={petPost?.image}>
{({ src }) => (
<img className="w-80 max-w-full rounded" src={src} />
)}
</ProgressiveImg>
<div className="flex justify-between max-w-xs flex-wrap">
{REACTION_TYPES.map((reactionType) => (
{ReactionTypes.map((reactionType) => (
<Button
key={reactionType}
variant={
petPost?.reactions?.me?.last === reactionType
petPost?.reactions?.byMe?.value === reactionType
? "default"
: "outline"
}
@@ -61,26 +61,22 @@ export function RatePetPostUI() {
))}
</div>
{petPost?.group.myRole() === "admin" && petPost.reactions && (
{petPost?._owner.myRole() === "admin" && petPost.reactions && (
<ReactionOverview petReactions={petPost.reactions} />
)}
</div>
);
}
function ReactionOverview({
petReactions,
}: {
petReactions: Queried<PetReactions>;
}) {
function ReactionOverview({ petReactions }: { petReactions: PetReactions }) {
return (
<div>
<h2>Reactions</h2>
<div className="flex flex-col gap-1">
{REACTION_TYPES.map((reactionType) => {
{ReactionTypes.map((reactionType) => {
const reactionsOfThisType = Object.values(
petReactions.perAccount
).filter(({ last }) => last === reactionType);
petReactions,
).filter((entry) => entry.value === reactionType);
if (reactionsOfThisType.length === 0) return null;
@@ -104,7 +100,7 @@ function ReactionOverview({
className="mt-1 w-[50px] h-[1em] rounded-full"
key={idx}
/>
)
),
)}
</div>
);

View File

@@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

View File

@@ -25,7 +25,7 @@ export function ThemeProvider({
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState(
() => localStorage.getItem(storageKey) || defaultTheme
() => localStorage.getItem(storageKey) || defaultTheme,
);
useEffect(() => {
@@ -35,7 +35,7 @@ export function ThemeProvider({
if (theme === "system") {
const systemTheme = window.matchMedia(
"(prefers-color-scheme: dark)"
"(prefers-color-scheme: dark)",
).matches
? "dark"
: "light";
@@ -62,6 +62,7 @@ export function ThemeProvider({
);
}
// eslint-disable-next-line react-refresh/only-export-components
export const useTheme = () => {
const context = useContext(ThemeProviderContext);

View File

@@ -1,56 +1,58 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants };

View File

@@ -1,25 +1,25 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input }
export { Input };

View File

@@ -1,15 +1,15 @@
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
function Skeleton({
className,
...props
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
}
export { Skeleton }
export { Skeleton };

View File

@@ -1,127 +1,127 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const ToastProvider = ToastPrimitives.Provider
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
defaultVariants: {
variant: "default",
},
}
)
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View File

@@ -1,33 +1,41 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/basicComponents/ui/toast"
import { useToast } from "@/basicComponents/ui/use-toast"
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/basicComponents/ui/toast";
import { useToast } from "@/basicComponents/ui/use-toast";
export function Toaster() {
const { toasts } = useToast()
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
return (
<ToastProvider>
{toasts.map(function ({
id,
title,
description,
action,
...props
}) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>
{description}
</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View File

@@ -1,192 +1,193 @@
// Inspired by react-hot-toast library
import * as React from "react"
import * as React from "react";
import type {
ToastActionElement,
ToastProps,
} from "@/basicComponents/ui/toast"
ToastActionElement,
ToastProps,
} from "@/basicComponents/ui/toast";
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
count = (count + 1) % Number.MAX_VALUE;
return count.toString();
}
type ActionType = typeof actionTypes
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[]
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout)
}
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
case "DISMISS_TOAST": {
const { toastId } = action
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = []
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] }
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId()
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
return {
...state,
toast,
dismiss: (toastId?: string) =>
dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast }
export { useToast, toast };

View File

@@ -1,10 +1,10 @@
import { useState } from "react";
import { LocalAuthComponent } from "jazz-react-auth-local";
import { PasskeyAuth } from "jazz-react";
import { Input, Button } from "../basicComponents";
export const PrettyAuthUI: LocalAuthComponent = ({
export const PrettyAuthUI: PasskeyAuth.Component = ({
loading,
logIn,
signUp,

View File

@@ -1,19 +1,18 @@
import { useState } from "react";
import { PetPost } from "../1_types";
import { PetPost } from "../1_schema";
import { createInviteLink } from "jazz-react";
import QRCode from "qrcode";
import { useToast, Button } from "../basicComponents";
import { Queried } from "cojson";
export function ShareButton({ petPost }: { petPost?: Queried<PetPost> }) {
export function ShareButton({ petPost }: { petPost?: PetPost }) {
const [existingInviteLink, setExistingInviteLink] = useState<string>();
const { toast } = useToast();
return (
petPost?.group.myRole() === "admin" && (
petPost?._owner.myRole() === "admin" && (
<Button
size="sm"
className="py-0"
@@ -35,7 +34,7 @@ export function ShareButton({ petPost }: { petPost?: Queried<PetPost> }) {
description: (
<img src={qr} className="w-20 h-20" />
),
})
}),
);
}
}}

View File

@@ -5,6 +5,7 @@ module.exports = {
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',

View File

@@ -0,0 +1,9 @@
/** @type {import("prettier").Config} */
const config = {
trailingComma: "all",
tabWidth: 4,
semi: true,
singleQuote: false,
};
export default config;

446
examples/todo/CHANGELOG.md Normal file
View File

@@ -0,0 +1,446 @@
# jazz-example-todo
## 0.0.68
### Patch Changes
- Updated dependencies
- jazz-react@0.7.4
## 0.0.67
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.3
- jazz-react@0.7.3
## 0.0.66
### Patch Changes
- Updated dependencies
- jazz-react@0.7.2
## 0.0.65
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.1
- jazz-react@0.7.1
## 0.0.64
### Patch Changes
- Updated dependencies [8636319]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [96c494f]
- Updated dependencies [59c18c3]
- Updated dependencies [19f52b7]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [d8fe2b1]
- Updated dependencies [19004b4]
- Updated dependencies [a78f168]
- Updated dependencies [52675c9]
- Updated dependencies [129e2c1]
- Updated dependencies [6d49e9b]
- Updated dependencies [1cfa279]
- Updated dependencies [704af7d]
- Updated dependencies [e97f730]
- Updated dependencies [460478f]
- Updated dependencies [6b0418f]
- Updated dependencies [e299c3e]
- Updated dependencies [ed5643a]
- Updated dependencies [bde684f]
- Updated dependencies [c4151fc]
- Updated dependencies [63374cc]
- Updated dependencies [01ac646]
- Updated dependencies [a5e68a4]
- Updated dependencies [8636319]
- Updated dependencies [952982e]
- Updated dependencies [1a35307]
- Updated dependencies [5fa277c]
- Updated dependencies [60d5ca2]
- Updated dependencies [21771c4]
- Updated dependencies [77c2b56]
- Updated dependencies [63374cc]
- Updated dependencies [d2e03ff]
- Updated dependencies [354bdcd]
- Updated dependencies [ece35b3]
- Updated dependencies [60d5ca2]
- Updated dependencies [69ac514]
- Updated dependencies [f8a5c46]
- Updated dependencies [f0f6f1b]
- Updated dependencies [e5eed5b]
- Updated dependencies [1a44f87]
- Updated dependencies [627d895]
- Updated dependencies [1200aae]
- Updated dependencies [63374cc]
- Updated dependencies [ece35b3]
- Updated dependencies [38d4410]
- Updated dependencies [85d2b62]
- Updated dependencies [fd86c11]
- Updated dependencies [52675c9]
- jazz-tools@0.7.0
- jazz-react@0.7.0
## 0.0.64-alpha.42
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.42
- jazz-react@0.7.0-alpha.42
## 0.0.64-alpha.41
### Patch Changes
- jazz-tools@0.7.0-alpha.41
- jazz-react@0.7.0-alpha.41
## 0.0.64-alpha.40
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.40
## 0.0.64-alpha.39
### Patch Changes
- jazz-react@0.7.0-alpha.39
- jazz-tools@0.7.0-alpha.39
## 0.0.64-alpha.38
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.38
- jazz-react@0.7.0-alpha.38
## 0.0.64-alpha.37
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.37
- jazz-tools@0.7.0-alpha.37
## 0.0.64-alpha.36
### Patch Changes
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [6b0418f]
- Updated dependencies [1a35307]
- jazz-tools@0.7.0-alpha.36
- jazz-react@0.7.0-alpha.36
## 0.0.64-alpha.35
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.35
- jazz-react@0.7.0-alpha.35
## 0.0.64-alpha.34
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.34
- jazz-react@0.7.0-alpha.34
## 0.0.64-alpha.33
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.33
## 0.0.64-alpha.32
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.32
- jazz-react@0.7.0-alpha.32
## 0.0.64-alpha.31
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.31
- jazz-react@0.7.0-alpha.31
## 0.0.64-alpha.30
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.30
- jazz-react@0.7.0-alpha.30
## 0.0.64-alpha.29
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.29
- jazz-react@0.7.0-alpha.29
## 0.0.64-alpha.28
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.28
- jazz-react@0.7.0-alpha.28
## 0.0.64-alpha.27
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.27
- jazz-react@0.7.0-alpha.27
## 0.0.64-alpha.26
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.26
- jazz-react@0.7.0-alpha.26
## 0.0.64-alpha.25
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.25
- jazz-react@0.7.0-alpha.25
## 0.0.64-alpha.24
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.24
- jazz-react@0.7.0-alpha.24
## 0.0.64-alpha.23
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.23
- jazz-react@0.7.0-alpha.23
## 0.0.64-alpha.22
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.22
- jazz-react@0.7.0-alpha.22
## 0.0.64-alpha.21
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.21
- jazz-tools@0.7.0-alpha.21
## 0.0.64-alpha.20
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-react@0.7.0-alpha.20
- jazz-tools@0.7.0-alpha.20
## 0.0.64-alpha.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.19
- jazz-react@0.7.0-alpha.19
## 0.0.64-alpha.18
### Patch Changes
- jazz-react@0.7.0-alpha.18
## 0.0.64-alpha.17
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.17
- jazz-react@0.7.0-alpha.17
## 0.0.64-alpha.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.16
- jazz-react@0.7.0-alpha.16
## 0.0.64-alpha.15
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.15
- jazz-react@0.7.0-alpha.15
## 0.0.64-alpha.14
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.14
- jazz-react@0.7.0-alpha.14
## 0.0.64-alpha.13
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.13
- jazz-react@0.7.0-alpha.13
## 0.0.64-alpha.12
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.12
- jazz-tools@0.7.0-alpha.12
## 0.0.64-alpha.11
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.11
- jazz-tools@0.7.0-alpha.11
## 0.0.64-alpha.10
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.10
- jazz-tools@0.7.0-alpha.10
## 0.0.64-alpha.9
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.9
- jazz-tools@0.7.0-alpha.9
## 0.0.64-alpha.8
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.8
- jazz-tools@0.7.0-alpha.8
## 0.0.64-alpha.7
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.7
- jazz-tools@0.7.0-alpha.7
## 0.0.64-alpha.6
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.6
- jazz-tools@0.7.0-alpha.6
## 0.0.64-alpha.5
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.5
- jazz-tools@0.7.0-alpha.5
## 0.0.64-alpha.4
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.4
- jazz-react@0.7.0-alpha.4
## 0.0.64-alpha.3
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.3
- jazz-react@0.7.0-alpha.3
## 0.0.64-alpha.2
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.2
- jazz-tools@0.7.0-alpha.2
## 0.0.64-alpha.1
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.1
- jazz-tools@0.7.0-alpha.1
## 0.0.64-alpha.0
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.0
- cojson@0.7.0-alpha.0
## 0.0.63
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -4,32 +4,37 @@ Live version: https://example-todo.jazz.tools
## Installing & running the example locally
Start by checking out just the example app to a folder:
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
Start by checking out `jazz`
```bash
npx degit gardencmp/jazz/examples/todo jazz-example-todo
cd jazz-example-todo
git clone https://github.com/gardencmp/jazz.git
cd jazz/examples/todo
pnpm pack --pack-destination /tmp
mkdir -p ~/jazz-examples/todo # or any other directory
tar -xf /tmp/jazz-example-todo-* --strip-components 1 -C ~/jazz-examples/todo
cd ~/jazz-examples/todo
```
(This ensures that you have the example app without git history or our multi-package monorepo)
This ensures that you have the example app without git history and independent of the Jazz multi-package monorepo.
Install dependencies:
```bash
npm install
pnpm install
```
Start the dev server:
```bash
npm run dev
pnpm dev
```
## Structure
- [`src/basicComponents`](./src/basicComponents): simple components to build the UI, unrelated to Jazz (uses [shadcn/ui](https://ui.shadcn.com))
- [`src/components`](./src/components/): helper components that do contain Jazz-specific logic, but aren't very relevant to understand the basics of Jazz and CoJSON
- [`src/1_types.ts`](./src/1_types.ts),
- [`src/1_schema.ts`](./src/1_schema.ts),
[`src/2_main.tsx`](./src/2_main.tsx),
[`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx),
[`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx): the main files for this example, see the walkthrough below
@@ -38,7 +43,7 @@ npm run dev
### Main parts
1. Defining the data model with CoJSON: [`src/1_types.ts`](./src/1_types.ts)
1. Defining the data model with CoJSON: [`src/1_schema.ts`](./src/1_schema.ts)
2. The top-level provider `<WithJazz/>` and routing: [`src/2_main.tsx`](./src/2_main.tsx)
@@ -48,7 +53,7 @@ npm run dev
### Helpers
- (not yet explained) Creating invite links/QR codes with `<InviteButton/>`: [`src/components/InviteButton.tsx`](./src/components/InviteButton.tsx)
- (not yet explained) Creating Invite Links/QR codes with `<InviteButton/>`: [`src/components/InviteButton.tsx`](./src/components/InviteButton.tsx)
This is the whole Todo List app!
@@ -61,4 +66,4 @@ If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<WithJazz>` provider component in [./src/2_main.tsx](./src/2_main.tsx).
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<Jazz.Provider>` provider component in [./src/2_main.tsx](./src/2_main.tsx).

View File

@@ -3,7 +3,7 @@ job "example-todo$BRANCH_SUFFIX" {
datacenters = ["*"]
group "static" {
count = 8
count = 4
network {
port "http" {

View File

@@ -1,14 +1,19 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.38",
"version": "0.0.68",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write './src/**/*.{ts,tsx}'",
"preview": "vite preview"
},
"lint-staged": {
"*.{ts,tsx}": "eslint --fix",
"*.{js,jsx,mdx,json}": "prettier --write"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
@@ -16,8 +21,8 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "^0.3.4",
"jazz-react-auth-local": "^0.3.4",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",
@@ -36,6 +41,7 @@
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.27",

View File

@@ -0,0 +1,55 @@
import { Account, CoList, CoMap, Profile, co } from "jazz-tools";
/** Walkthrough: Defining the data model with CoJSON
*
* Here, we define our main data model of tasks, lists of tasks and projects
* using CoJSON's collaborative map and list types, CoMap & CoList.
*
* CoMap values and CoLists items can contain:
* - arbitrary immutable JSON
* - other CoValues
**/
/** An individual task which collaborators can tick or rename */
export class Task extends CoMap {
done = co.boolean;
text = co.string;
}
export class ListOfTasks extends CoList.Of(co.ref(Task)) {}
/** Our top level object: a project with a title, referencing a list of tasks */
export class TodoProject extends CoMap {
title = co.string;
tasks = co.ref(ListOfTasks);
}
export class ListOfProjects extends CoList.Of(co.ref(TodoProject)) {}
/** The account root is an app-specific per-user private `CoMap`
* where you can store top-level objects for that user */
export class TodoAccountRoot extends CoMap {
projects = co.ref(ListOfProjects);
}
export class TodoAccount extends Account {
profile = co.ref(Profile);
root = co.ref(TodoAccountRoot);
/** The account migration is run on account creation and on every log-in.
* You can use it to set up the account root and any other initial CoValues you need.
*/
migrate(this: TodoAccount, creationProps?: { name: string }) {
super.migrate(creationProps);
if (!this._refs.root) {
this.root = TodoAccountRoot.create(
{
projects: ListOfProjects.create([], { owner: this }),
},
{ owner: this },
);
}
}
}
/** Walkthrough: Continue with ./2_main.tsx */

View File

@@ -1,23 +0,0 @@
import { CoMap, CoList } from "cojson";
/** Walkthrough: Defining the data model with CoJSON
*
* Here, we define our main data model of tasks, lists of tasks and projects
* using CoJSON's collaborative map and list types, CoMap & CoList.
*
* CoMap values and CoLists items can contain:
* - arbitrary immutable JSON
* - references to other CoValues (internally stored by their CoID)
**/
/** An individual task which collaborators can tick or rename */
export type Task = CoMap<{ done: boolean; text: string; }>;
/** Our top level object: a project with a title, referencing a list of tasks */
export type TodoProject = CoMap<{
title: string;
/** A collaborative, ordered list of tasks */
tasks: CoList<Task>;
}>;
/** Walkthrough: Continue with ./2_main.tsx */

View File

@@ -1,10 +1,12 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createHashRouter } from "react-router-dom";
import {
RouterProvider,
createHashRouter,
useNavigate,
} from "react-router-dom";
import "./index.css";
import { WithJazz, useJazz, useAcceptInvite } from "jazz-react";
import { LocalAuth } from "jazz-react-auth-local";
import { createJazzReactContext, PasskeyAuth } from "jazz-react";
import {
Button,
@@ -14,29 +16,44 @@ import {
import { PrettyAuthUI } from "./components/Auth.tsx";
import { NewProjectForm } from "./3_NewProjectForm.tsx";
import { ProjectTodoTable } from "./4_ProjectTodoTable.tsx";
import { TodoAccount, TodoProject } from "./1_schema.ts";
/**
* Walkthrough: The top-level provider `<WithJazz/>`
* Walkthrough: The top-level provider `<Jazz.Provider/>`
*
* This shows how to use the top-level provider `<WithJazz/>`,
* which provides the rest of the app with a `LocalNode` (used through `useJazz` later),
* based on `LocalAuth` that uses Passkeys (aka WebAuthn) to store a user's account secret
* This shows how to use the top-level provider `<Jazz.Provider/>`,
* which provides the rest of the app with a controlled account (used through `useAccount` later).
* Here we use `PasskeyAuth`, which uses Passkeys (aka WebAuthn) to store a user's account secret
* - no backend needed.
*
* `<Jazz.Provider/>` also runs our account migration
*/
const appName = "Jazz Todo List Example";
const auth = LocalAuth({
const auth = PasskeyAuth<TodoAccount>({
appName,
Component: PrettyAuthUI,
accountSchema: TodoAccount,
});
const Jazz = createJazzReactContext<TodoAccount>({
auth,
peer: "wss://mesh.jazz.tools/?key=you@example.com",
});
// eslint-disable-next-line react-refresh/only-export-components
export const { useAccount, useCoState, useAcceptInvite } = Jazz;
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<WithJazz auth={auth}>
<App />
</WithJazz>
</React.StrictMode>
// <React.StrictMode>
<ThemeProvider>
<TitleAndLogo name={appName} />
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
<Jazz.Provider>
<App />
</Jazz.Provider>
</div>
</ThemeProvider>,
// </React.StrictMode>
);
/**
@@ -46,15 +63,14 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
* on the CoValue ID (CoID) of our TodoProject, stored in the URL hash
* - which can also contain invite links.
*/
function App() {
// logOut logs out the AuthProvider passed to `<WithJazz/>` above.
const { logOut } = useJazz();
export default function App() {
// logOut logs out the AuthProvider passed to `<Jazz.Provider/>` above.
const { logOut } = useAccount();
const router = createHashRouter([
{
path: "/",
element: <NewProjectForm />,
element: <HomeScreen />,
},
{
path: "/project/:projectId",
@@ -62,29 +78,54 @@ function App() {
},
{
path: "/invite/*",
element: <p>Accepting invite...</p>
}
element: <p>Accepting invite...</p>,
},
]);
// `useAcceptInvite()` is a hook that accepts an invite link from the URL hash,
// and on success calls our callback where we navigate to the project that we were just invited to.
useAcceptInvite((projectID) => router.navigate("/project/" + projectID));
useAcceptInvite({
invitedObjectSchema: TodoProject,
forValueHint: "project",
onAccept: (projectID) => router.navigate("/project/" + projectID),
});
return (
<ThemeProvider>
<TitleAndLogo name={appName} />
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
<>
<RouterProvider router={router} />
<RouterProvider router={router} />
<Button
onClick={() => router.navigate("/").then(logOut)}
variant="outline"
>
Log Out
</Button>
</>
);
}
<Button
onClick={() => router.navigate("/").then(logOut)}
variant="outline"
>
Log Out
</Button>
</div>
</ThemeProvider>
function HomeScreen() {
const { me } = useAccount({
root: { projects: [{}] },
});
const navigate = useNavigate();
return (
<>
{me?.root.projects.length ? <h1>My Projects</h1> : null}
{me?.root.projects.map((project) => {
return (
<Button
key={project.id}
onClick={() => navigate("/project/" + project?.id)}
variant="ghost"
>
{project.title}
</Button>
);
})}
<NewProjectForm />
</>
);
}

View File

@@ -1,19 +1,17 @@
import { useCallback } from "react";
import { useJazz } from "jazz-react";
import { Task, TodoProject } from "./1_types";
import { ListOfTasks, TodoProject } from "./1_schema";
import { SubmittableInput } from "./basicComponents";
import { CoList } from "cojson";
import { useNavigate } from "react-router";
import { useAccount } from "./2_main";
import { Group } from "jazz-tools";
export function NewProjectForm() {
// A `LocalNode` represents a local view of loaded & created CoValues.
// It is associated with a current user account, which will determine
// `me` represents the current user account, which will determine
// access rights to CoValues. We get it from the top-level provider `<WithJazz/>`.
const { localNode } = useJazz();
const { me } = useAccount();
const navigate = useNavigate();
const createProject = useCallback(
@@ -23,17 +21,22 @@ export function NewProjectForm() {
// To create a new todo project, we first create a `Group`,
// which is a scope for defining access rights (reader/writer/admin)
// of its members, which will apply to all CoValues owned by that group.
const projectGroup = localNode.createGroup();
const projectGroup = Group.create({ owner: me });
// Then we create an empty todo project within that group
const project = projectGroup.createMap<TodoProject>({
title,
tasks: projectGroup.createList<CoList<Task>>(),
});
const project = TodoProject.create(
{
title,
tasks: ListOfTasks.create([], { owner: projectGroup }),
},
{ owner: projectGroup },
);
me.root?.projects?.push(project);
navigate("/project/" + project.id);
},
[localNode, navigate]
[me, navigate],
);
return (

View File

@@ -1,9 +1,6 @@
import { useCallback } from "react";
import { CoID, Queried } from "cojson";
import { useSyncedQuery } from "jazz-react";
import { TodoProject, Task } from "./1_types";
import { TodoProject, Task } from "./1_schema";
import {
Checkbox,
@@ -20,23 +17,25 @@ import {
import { InviteButton } from "./components/InviteButton";
import uniqolor from "uniqolor";
import { useParams } from "react-router";
import { ID } from "jazz-tools";
import { useCoState } from "./2_main";
/** Walkthrough: Reactively rendering a todo project as a table,
* adding and editing tasks
*
* Here in `<TodoTable/>`, we use `useSyncedQuery()` for the first time,
* Here in `<TodoTable/>`, we use `useAutoSub()` for the first time,
* in this case to load the CoValue for our `TodoProject` as well as
* the `ListOfTasks` referenced in it.
*/
export function ProjectTodoTable() {
const projectId = useParams<{ projectId: CoID<TodoProject> }>().projectId;
const projectId = useParams<{ projectId: ID<TodoProject> }>().projectId;
// `useSyncedQuery()` reactively subscribes to updates to a CoValue's
// `useAutoSub()` reactively subscribes to updates to a CoValue's
// content - whether we create edits locally, load persisted data, or receive
// sync updates from other devices or participants!
// It also recursively resolves and subsribes to all referenced CoValues.
const project = useSyncedQuery(projectId);
const project = useCoState(TodoProject, projectId);
// `createTask` is similar to `createProject` we saw earlier, creating a new CoMap
// for a new task (in the same group as the project), and then
@@ -44,17 +43,18 @@ export function ProjectTodoTable() {
const createTask = useCallback(
(text: string) => {
if (!project?.tasks || !text) return;
const task = project.group.createMap<Task>({
done: false,
text,
});
const task = Task.create(
{
done: false,
text,
},
{ owner: project._owner },
);
// project.tasks is immutable, but `append` will create an edit
// that will cause useSyncedQuery to rerender this component
// - here and on other devices!
project.tasks.append(task);
// push will cause useCoState to rerender this component, both here and on other devices
project.tasks.push(task);
},
[project?.tasks, project?.group]
[project?.tasks, project?._owner],
);
return (
@@ -74,7 +74,7 @@ export function ProjectTodoTable() {
)
}
</h1>
<InviteButton value={project} />
<InviteButton value={project} valueHint="project" />
</div>
<Table>
<TableHeader>
@@ -85,7 +85,7 @@ export function ProjectTodoTable() {
</TableHeader>
<TableBody>
{project?.tasks?.map(
(task) => task && <TaskRow key={task.id} task={task} />
(task) => task && <TaskRow key={task.id} task={task} />,
)}
<NewTaskInputRow
createTask={createTask}
@@ -97,7 +97,7 @@ export function ProjectTodoTable() {
);
}
export function TaskRow({ task }: { task: Queried<Task> | undefined }) {
export function TaskRow({ task }: { task: Task | undefined }) {
return (
<TableRow>
<TableCell>
@@ -108,7 +108,7 @@ export function TaskRow({ task }: { task: Queried<Task> | undefined }) {
// Tick or untick the task
// Task is also immutable, but this will update all queries
// that include this task as a reference
task?.set({ done: !!checked });
if (task) task.done = !!checked;
}}
/>
</TableCell>
@@ -124,12 +124,12 @@ export function TaskRow({ task }: { task: Queried<Task> | undefined }) {
{
// Here we see for the first time how we can access edit history
// for a CoValue, and use it to display who created the task.
task?.edits.text?.by?.profile?.name ? (
task?._edits.text?.by?.profile?.name ? (
<span
className="rounded-full py-0.5 px-2 text-xs"
style={uniqueColoring(task.edits.text.by.id)}
style={uniqueColoring(task._edits.text.by.id)}
>
{task.edits.text.by.profile.name}
{task._edits.text.by.profile.name}
</span>
) : (
<Skeleton className="mt-1 w-[50px] h-[1em] rounded-full" />

View File

@@ -18,7 +18,7 @@ export function SubmittableInput({
onSubmit={(e) => {
e.preventDefault();
const textEl = e.currentTarget.elements.namedItem(
"text"
"text",
) as HTMLInputElement;
onSubmit(textEl.value);
textEl.value = "";
@@ -31,7 +31,11 @@ export function SubmittableInput({
autoComplete="off"
disabled={disabled}
/>
<Button asChild type="submit" className="flex-shrink flex-1 cursor-pointer">
<Button
asChild
type="submit"
className="flex-shrink flex-1 cursor-pointer"
>
<Input type="submit" value={label} disabled={disabled} />
</Button>
</form>

View File

@@ -1,10 +1,12 @@
import { Toaster } from ".";
export function TitleAndLogo({name}: {name: string}) {
return <>
<div className="flex items-center gap-2 justify-center mt-5">
export function TitleAndLogo({ name }: { name: string }) {
return (
<>
<div className="flex items-center gap-2 justify-center mt-5">
<img src="jazz-logo.png" className="h-5" /> {name}
</div>
<Toaster />
</>
}
</>
);
}

View File

@@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

View File

@@ -25,7 +25,7 @@ export function ThemeProvider({
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState(
() => localStorage.getItem(storageKey) || defaultTheme
() => localStorage.getItem(storageKey) || defaultTheme,
);
useEffect(() => {
@@ -35,7 +35,7 @@ export function ThemeProvider({
if (theme === "system") {
const systemTheme = window.matchMedia(
"(prefers-color-scheme: dark)"
"(prefers-color-scheme: dark)",
).matches
? "dark"
: "light";
@@ -62,6 +62,7 @@ export function ThemeProvider({
);
}
// eslint-disable-next-line react-refresh/only-export-components
export const useTheme = () => {
const context = useContext(ThemeProviderContext);

View File

@@ -1,56 +1,58 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants };

View File

@@ -1,28 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox }
export { Checkbox };

View File

@@ -1,25 +1,25 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input }
export { Input };

View File

@@ -1,15 +1,15 @@
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
function Skeleton({
className,
...props
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
}
export { Skeleton }
export { Skeleton };

View File

@@ -1,114 +1,120 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
<div className="w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("bg-primary font-medium text-primary-foreground", className)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
<tfoot
ref={ref}
className={cn(
"bg-primary font-medium text-primary-foreground",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
<td
ref={ref}
className={cn(
"p-4 align-middle [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@@ -1,127 +1,127 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/basicComponents/lib/utils"
import { cn } from "@/basicComponents/lib/utils";
const ToastProvider = ToastPrimitives.Provider
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
defaultVariants: {
variant: "default",
},
}
)
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View File

@@ -1,33 +1,41 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/basicComponents/ui/toast"
import { useToast } from "@/basicComponents/ui/use-toast"
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/basicComponents/ui/toast";
import { useToast } from "@/basicComponents/ui/use-toast";
export function Toaster() {
const { toasts } = useToast()
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
return (
<ToastProvider>
{toasts.map(function ({
id,
title,
description,
action,
...props
}) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>
{description}
</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View File

@@ -1,192 +1,193 @@
// Inspired by react-hot-toast library
import * as React from "react"
import * as React from "react";
import type {
ToastActionElement,
ToastProps,
} from "@/basicComponents/ui/toast"
ToastActionElement,
ToastProps,
} from "@/basicComponents/ui/toast";
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
count = (count + 1) % Number.MAX_VALUE;
return count.toString();
}
type ActionType = typeof actionTypes
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[]
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout)
}
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
case "DISMISS_TOAST": {
const { toastId } = action
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = []
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] }
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId()
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
return {
...state,
toast,
dismiss: (toastId?: string) =>
dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast }
export { useToast, toast };

View File

@@ -1,10 +1,10 @@
import { useState } from "react";
import { LocalAuthComponent } from "jazz-react-auth-local";
import { PasskeyAuth } from "jazz-react";
import { Input, Button } from "../basicComponents";
export const PrettyAuthUI: LocalAuthComponent = ({
export const PrettyAuthUI: PasskeyAuth.Component = ({
loading,
logIn,
signUp,

View File

@@ -1,26 +1,34 @@
import { useState } from "react";
import { createInviteLink } from "jazz-react";
import QRCode from "qrcode";
import { useToast, Button } from "../basicComponents";
import { CoValue, Queried } from "cojson";
import { createInviteLink } from "jazz-react";
import { CoValue } from "jazz-tools";
export function InviteButton<T extends CoValue>({ value }: { value: T | Queried<T> | undefined }) {
export function InviteButton<T extends CoValue>({
value,
valueHint,
}: {
value?: T;
valueHint?: string;
}) {
const [existingInviteLink, setExistingInviteLink] = useState<string>();
const { toast } = useToast();
return (
value?.group?.myRole() === "admin" && (
value?._owner?.myRole() === "admin" && (
<Button
size="sm"
className="py-0"
disabled={!value.group || !value.id}
disabled={!value._owner || !value.id}
variant="outline"
onClick={async () => {
let inviteLink = existingInviteLink;
if (value.group && value.id && !inviteLink) {
inviteLink = createInviteLink(value, "writer");
if (value._owner && value.id && !inviteLink) {
inviteLink = createInviteLink(value, "writer", {
valueHint,
});
setExistingInviteLink(inviteLink);
}
if (inviteLink) {
@@ -33,7 +41,7 @@ export function InviteButton<T extends CoValue>({ value }: { value: T | Queried<
description: (
<img src={qr} className="w-20 h-20" />
),
})
}),
);
}
}}

View File

@@ -13,4 +13,4 @@ export default defineConfig({
build: {
minify: false
}
})
})

View File

@@ -1,787 +0,0 @@
import { readFile, writeFile } from "fs/promises";
import { Application, JSONOutput, ReflectionKind } from "typedoc";
const manuallyIgnore = new Set(["CojsonInternalTypes"]);
async function main() {
// Application.bootstrap also exists, which will not load plugins
// Also accepts an array of option readers if you want to disable
// TypeDoc's tsconfig.json/package.json/typedoc.json option readers
const packageDocs = Object.entries({
"jazz-react": "index.tsx",
cojson: "index.ts",
"jazz-react-media-images": "index.tsx",
"jazz-browser": "index.ts",
"jazz-browser-media-images": "index.ts",
}).map(async ([packageName, entryPoint]) => {
const app = await Application.bootstrapWithPlugins({
entryPoints: [`packages/${packageName}/src/${entryPoint}`],
tsconfig: `packages/${packageName}/tsconfig.json`,
sort: ["required-first"],
groupOrder: ["Functions", "Classes", "TypeAliases", "Namespaces"],
categorizeByGroup: false
});
const project = await app.convert();
if (!project) {
throw new Error("Failed to convert project" + packageName);
}
// Alternatively generate JSON output
await app.generateJson(project, `docsTmp/${packageName}.json`);
const docs = JSON.parse(
await readFile(`docsTmp/${packageName}.json`, "utf8")
) as JSONOutput.ProjectReflection;
return (
`# ${packageName}\n\n` +
docs
.groups!.map((group) => {
return group.children
?.flatMap((childId) => {
const child = docs.children!.find(
(child) => child.id === childId
)!;
if (
manuallyIgnore.has(child.name) ||
child.comment?.blockTags?.some(
(tag) =>
tag.tag === "@deprecated" ||
tag.tag === "@internal" ||
tag.tag === "@ignore"
) ||
child.signatures?.every((signature) =>
signature.comment?.blockTags?.some(
(tag) =>
tag.tag === "@deprecated" ||
tag.tag === "@internal" ||
tag.tag === "@ignore"
)
)
) {
return [];
}
return (
`## \`${renderChildName(
child
)}\`\n\n<sup>(${group.title
.toLowerCase()
.replace("bles", "ble")
.replace("ces", "ce")
.replace(/es$/, "")
.replace(
"ns",
"n"
)} in \`${packageName}\`)</sup>\n\n` +
renderChildType(child) +
(child.kind === ReflectionKind.Class ||
child.kind === ReflectionKind.Interface ||
child.kind === ReflectionKind.Namespace
? renderSummary(child.comment) +
renderExamples(child.comment) +
(child.categories || child.groups)
?.map((category) =>
renderChildCategory(child, category)
)
.join("<br/>\n\n")
: child.kind === ReflectionKind.Function
? renderSummary(
child.signatures?.[0].comment
) +
renderParamComments(
child.signatures?.[0].parameters || []
) +
renderExamples(
child.signatures?.[0].comment
) +
"\n\n"
: "TODO: doc generator not implemented yet " +
child.kind)
);
})
.join("\n\n----\n\n");
})
.join("\n\n----\n\n")
);
function renderSummary(comment?: JSONOutput.Comment): string {
if (comment) {
return (
comment.summary
.map((token) =>
token.kind === "text" || token.kind === "code"
? token.text
: ""
)
.join("") +
"\n\n" +
"\n\n"
);
} else {
return "TODO: document\n\n";
}
}
function renderExamples(comment?: JSONOutput.Comment): string {
return (comment?.blockTags || [])
.map((blockTag) =>
blockTag.tag === "@example"
? "##### Example:\n\n" +
blockTag.content
.map((token) =>
token.kind === "text" || token.kind === "code"
? token.text
: ""
)
.join("") +
"\n\n"
: ""
)
.join("");
}
function renderParamComments(params: JSONOutput.ParameterReflection[]) {
const paramDocs = params.flatMap((param) => {
if (param.type?.type === "reflection") {
return param.type.declaration.children?.flatMap((child) => {
if (
child.name === "children" &&
child.type?.type === "reference" &&
child.type?.name === "ReactNode"
) {
return [];
}
return (
`| \`${param.name}.${child.name}${
child.flags.isOptional || child.defaultValue
? "?"
: ""
}\` | ` +
(child.comment
? child.comment.summary
.map((token) =>
token.kind === "text" ||
token.kind === "code"
? token.text
: ""
)
.join("")
: "TODO: document") +
" |"
);
});
} else {
const comment = param.comment;
return [
`| \`${param.name}${
param.flags.isOptional || param.defaultValue
? "?"
: ""
}\` | ` +
(comment
? comment.summary
.map((token) =>
token.kind === "text" ||
token.kind === "code"
? token.text
: ""
)
.join("")
: "TODO: document ") +
" |",
];
}
});
if (paramDocs.length) {
return `### Parameters:\n\n| name | description |\n| ----: | ---- |\n${paramDocs.join(
"\n"
)}\n\n`;
}
}
function renderChildName(child: JSONOutput.DeclarationReflection) {
if (child.signatures) {
if (
child.signatures[0].type?.type === "reference" &&
child.signatures[0].type.qualifiedName ===
"React.JSX.Element"
) {
return `<${child.name}/>`;
} else {
return (
child.name +
`(${(child.signatures[0].parameters || [])
.map(renderParamSimple)
.join(", ")})`
);
}
} else {
return child.name;
}
}
function renderChildType(
child: JSONOutput.DeclarationReflection
): string {
const isClass = child.kind === ReflectionKind.Class;
const isTypeAlias = child.kind === ReflectionKind.TypeAlias;
const isInterface = child.kind === ReflectionKind.Interface;
const isNamespace = child.kind === ReflectionKind.Namespace;
const isFunction = !!child.signatures;
const kind = isClass
? "class"
: isTypeAlias
? "type"
: isFunction
? "function"
: isInterface
? "interface"
: isNamespace
? "namespace"
: "";
return (
"```typescript\n" +
`export ${kind} ${child.name}` +
((child.typeParameters || child.signatures?.[0].typeParameter)
? "<" +
(child.typeParameters || child.signatures?.[0].typeParameter || []).map(renderTypeParam).join(", ") +
">"
: "") +
(child.extendedTypes
? " extends " +
child.extendedTypes.map(renderType).join(", ")
: "") +
(child.implementedTypes
? " implements " +
child.implementedTypes.map(renderType).join(", ")
: "") +
(isClass || isInterface || isNamespace
? " {...}"
: isTypeAlias
? ` = ${renderType(child.type)}`
: child.signatures
? `(${(child.signatures[0].parameters || [])
.map(renderParam)
.join(", ")}): ${renderType(
child.signatures[0].type
)}`
: "") +
"\n```\n"
);
}
function renderChildCategory(
child: JSONOutput.DeclarationReflection,
category: JSONOutput.ReflectionGroup
): string {
return (
`### \`${child.name}\`: ${category.title.replace(/[^d]+\./, "")}\n\n` +
category.children
?.map((memberId) => {
const member = child.children!.find(
(member) => member.id === memberId
)!;
if (member.kind === 2048 || member.kind === 512) {
if (
member.signatures?.every(
(sig) =>
sig.comment?.modifierTags?.includes(
"@internal"
) ||
sig.comment?.modifierTags?.includes(
"@deprecated"
)
)
) {
return "";
} else {
return documentConstructorOrMethod(
member,
child
);
}
} else if (
member.kind === 1024 ||
member.kind === 262144
) {
if (
member.comment?.modifierTags?.includes(
"@internal"
) ||
member.comment?.modifierTags?.includes(
"@deprecated"
)
) {
return "";
} else {
return documentProperty(member, child);
}
} else if (member.kind === 2097152) {
if (
member.comment?.modifierTags?.includes(
"@internal"
) ||
member.comment?.modifierTags?.includes(
"@deprecated"
)
) {
return "";
} else {
return documentProperty(
{ ...member, flags: { isStatic: true } },
child
);
}
} else {
return "Unknown member kind " + member.kind;
}
})
.join("\n\n")
);
}
function renderType(t?: JSONOutput.SomeType): string {
if (!t) return "";
if (t.type === "reference") {
return (
t.name +
(t.typeArguments
? "<" + t.typeArguments.map(renderType).join(", ") + ">"
: "")
);
} else if (t.type === "intrinsic") {
return t.name;
} else if (t.type === "literal") {
return JSON.stringify(t.value);
} else if (t.type === "union") {
const seen = new Set<string>();
return t.types
.flatMap((t) => {
const rendered =
t.type === "intersection" || t.type === "union"
? `(${renderType(t)})`
: renderType(t);
if (seen.has(rendered)) {
return [];
} else {
seen.add(rendered);
return [rendered];
}
})
.join(" | ");
} else if (t.type === "intersection") {
const seen = new Set<string>();
return t.types
.flatMap((t) => {
const rendered =
t.type === "intersection" || t.type === "union"
? `(${renderType(t)})`
: renderType(t);
if (seen.has(rendered)) {
return [];
} else {
seen.add(rendered);
return [rendered];
}
})
.join(" & ");
} else if (t.type === "indexedAccess") {
return (
renderType(t.objectType) +
"[" +
renderType(t.indexType) +
"]"
);
} else if (t.type === "reflection") {
if (t.declaration.indexSignature) {
return (
`{${
t.declaration.children
? t.declaration.children
.map(
(child) =>
` ${child.name}${
child.flags.isOptional
? "?"
: ""
}: ${indentEnd(
renderType(child.type)
)},`
)
.join("\n")
: ""
}\n [` +
t.declaration.indexSignature?.parameters?.[0].name +
": " +
renderType(
t.declaration.indexSignature?.parameters?.[0].type
) +
"]: " +
indentEnd(
renderType(t.declaration.indexSignature?.type)
) +
" }"
);
} else if (t.declaration.children) {
return `{\n${t.declaration.children
.map((child) =>
child.signatures
? child.signatures
.map(
(signature) =>
` ${child.name}(${
signature.parameters
? "\n " +
indent(
signature.parameters
.map((p) =>
indentEnd(
renderParam(
p
)
)
)
.join(",\n ")
) +
"\n )"
: "()"
}: ${indentEnd(
renderType(signature.type)
)}`
)
.join("\n") + ",\n"
: ` ${child.name}${
child.flags.isOptional ? "?" : ""
}: ${indentEnd(renderType(child.type))},\n`
)
.join("")}}`;
} else if (t.declaration.signatures) {
return t.declaration.signatures
.map(
(signature) =>
`(${(signature.parameters || [])
.map(renderParam)
.join(", ")}) => ${renderType(
signature.type
)}`
)
.join("\n");
} else {
return "COMPLEX_TYPE_REFLECTION";
}
} else if (t.type === "array") {
return renderType(t.elementType) + "[]";
} else if (t.type === "tuple") {
return `[${t.elements?.map(renderType).join(", ")}]`;
} else if (t.type === "templateLiteral") {
const matchingNamedType = docs.children?.find(
(child) =>
child.variant === "declaration" &&
child.type?.type === "templateLiteral" &&
child.type.head === t.head &&
child.type.tail.every(
(piece, i) => piece[1] === t.tail[i][1]
)
);
if (matchingNamedType) {
return matchingNamedType.name;
} else {
if (
t.head === "sealerSecret_z" &&
t.tail[0][1] === "/signerSecret_z"
) {
return "AgentSecret";
} else if (
t.head === "sealer_z" &&
t.tail[0][1] === "/signer_z"
) {
if (t.tail[1] && t.tail[1][1] === "_session_z") {
return "SessionID";
} else {
return "AgentID";
}
} else {
return (
"`" +
t.head +
t.tail
.map(
(bit) =>
"${" + renderType(bit[0]) + "}" + bit[1]
)
.join("") +
"`"
);
}
}
} else if (t.type === "conditional") {
const trueRendered = renderType(t.trueType);
const falseRendered = renderType(t.falseType);
if (
trueRendered.includes("\n") ||
falseRendered.includes("\n")
) {
return (
renderType(t.checkType) +
" extends " +
renderType(t.extendsType) +
"\n ? " +
indentEnd(renderType(t.trueType)) +
"\n : " +
indentEnd(renderType(t.falseType))
);
} else {
return (
renderType(t.checkType) +
" extends " +
renderType(t.extendsType) +
" ? " +
renderType(t.trueType) +
" : " +
renderType(t.falseType)
);
}
} else if (t.type === "inferred") {
return "infer " + t.name;
} else if (t.type === "typeOperator") {
return t.operator + " " + renderType(t.target);
} else if (t.type === "mapped") {
return `{\n [${t.parameter} in ${renderType(
t.parameterType
)}]: ${indentEnd(renderType(t.templateType))}\n}`;
} else {
return "COMPLEX_TYPE_" + t.type;
}
}
// function renderTemplateLiteral(tempLit: JSONOutput.TemplateLiteralType) {
// return tempLit.head + tempLit.tail.map((piece) => piece[0] + piece[1]).join("");
// }
// function resolveTemplateLiteralPieceType(t: SomeType): string {
// if (t.type === "string") {
// return "${string}"
// }
// if (t.type === "reference") {
// const referencedType = docs.children?.find(
// (child) => child.name === t.name
// );
// }
// }
function renderTypeParam(
t?: JSONOutput.TypeParameterReflection
): string {
if (!t) return "";
return t.name + (t.type ? " extends " + renderType(t.type) : "");
}
function renderParam(param: JSONOutput.ParameterReflection) {
return param.name === "__namedParameters"
? renderType(param.type)
: `${param.name}: ${renderType(param.type)}`;
}
function renderParamSimple(param: JSONOutput.ParameterReflection) {
return param.name === "__namedParameters" &&
param.type?.type === "reflection"
? `{${param.type?.declaration.children
?.map(
(child) =>
child.name + (child.flags.isOptional ? "?" : "")
)
.join(", ")}}${
param.flags.isOptional || param.defaultValue ? "?" : ""
}`
: param.name +
(param.flags.isOptional || param.defaultValue ? "?" : "");
}
function documentConstructorOrMethod(
member: JSONOutput.DeclarationReflection,
child: JSONOutput.DeclarationReflection
) {
const isInClass = child.kind === 128;
const isInTypeDef = child.kind === 2097152;
const isInInterface = child.kind === 256;
const isInNamespace = child.kind === 4;
const isInFunction = !!child.signatures;
const inKind = isInClass
? "class"
: isInTypeDef
? "type"
: isInFunction
? "function"
: isInInterface
? "interface"
: isInNamespace
? "namespace"
: "";
const stem =
member.name === "constructor"
? "new " + child.name + "</code></b>"
: (member.flags.isStatic ? child.name : "") +
"." +
member.name +
"";
return member.signatures
?.map((signature) => {
return (
`<details>\n<summary><b><code>${stem}(${(
signature?.parameters?.map(renderParamSimple) || []
).join(", ")})</code></b> ${
member.inheritedFrom
? "<sub><sup>from <code>" +
member.inheritedFrom.name.split(".")[0] +
"</code></sup></sub> "
: ""
} ${
signature?.comment
? ""
: "<sub><sup>(undocumented)</sup></sub>"
}</summary>\n\n` +
("```typescript\n" +
`${inKind} ${child.name}${
child.typeParameters
? `<${child.typeParameters
.map((t) => t.name)
.join(", ")}>`
: ""
} {\n\n${indent(
`${member.name}${
signature.typeParameter
? `<${signature.typeParameter
.map(renderTypeParam)
.join(", ")}>`
: ""
}(${
(
signature.parameters?.map(
(param) =>
`\n ${param.name}${
param.flags.isOptional ||
param.defaultValue
? "?"
: ""
}: ${indentEnd(
renderType(param.type)
)}${
param.defaultValue
? ` = ${param.defaultValue}`
: ""
}`
) || []
).join(",") +
(signature.parameters?.length ? "\n" : "")
}): ${renderType(signature.type)} {...}`
)}\n\n}\n` +
"```\n" +
renderSummary(signature.comment)) +
renderParamComments(signature.parameters || []) +
renderExamples(signature.comment) +
"</details>\n\n"
);
})
.join("\n\n");
}
function documentProperty(
member: JSONOutput.DeclarationReflection,
child: JSONOutput.DeclarationReflection
) {
const isInClass = child.kind === 128;
const isInTypeDef = child.kind === 2097152;
const isInInterface = child.kind === 256;
const isInNamespace = child.kind === 4;
const isInFunction = !!child.signatures;
const inKind = isInClass
? "class"
: isInTypeDef
? "type"
: isInFunction
? "function"
: isInInterface
? "interface"
: isInNamespace
? "namespace"
: "";
const stem = member.flags.isStatic ? child.name : "";
return (
`<details>\n<summary><b><code>${stem}.${
member.name
}</code></b> ${
member.inheritedFrom
? "<sub><sup>from <code>" +
member.inheritedFrom.name.split(".")[0] +
"</code></sup></sub> "
: ""
} ${
member.comment ? "" : "<sub><sup>(undocumented)</sup></sub>"
}</summary>\n\n` +
"```typescript\n" +
`${inKind} ${child.name}${
child.typeParameters
? `<${child.typeParameters
.map((t) => t.name)
.join(", ")}>`
: ""
} {\n\n${indent(
`${member.getSignature ? "get " : ""}${member.name}${
member.getSignature ? "()" : ""
}: ${renderType(member.type || member.getSignature?.type)}${
member.getSignature ? " {...}" : ""
}`
)}` +
"\n\n}\n```\n" +
renderSummary(member.comment) +
renderExamples(member.comment) +
"</details>\n\n"
);
}
});
const docsContent = await readFile("./DOCS.md", "utf8");
await writeFile(
"./DOCS.md",
docsContent.slice(
0,
docsContent.indexOf("<!-- AUTOGENERATED DOCS AFTER THIS POINT -->")
) +
"<!-- AUTOGENERATED DOCS AFTER THIS POINT -->\n" +
(await Promise.all(packageDocs)).join("\n\n\n")
);
}
function indent(text: string): string {
return text
.split("\n")
.map((line) => " " + line)
.join("\n");
}
function indentEnd(text: string): string {
return text
.split("\n")
.map((line, i) => (i === 0 ? line : " " + line))
.join("\n");
}
main().catch(console.error);

11
homepage/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM node:18-slim
COPY node_modules ./node_modules
COPY homepage/.next/standalone ./homepage
COPY homepage/.next/static ./homepage/.next/static
EXPOSE 3001
ENV PORT 3001
CMD ["node", "homepage/server.js"]

View File

@@ -0,0 +1,7 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git

View File

@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "prettier"]
}

38
homepage/homepage/.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
typedoc
codeSamples

View File

@@ -0,0 +1,9 @@
/** @type {import("prettier").Config} */
const config = {
trailingComma: "all",
tabWidth: 4,
semi: true,
singleQuote: false,
};
export default config;

View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -0,0 +1,426 @@
import { Slogan } from "@/components/forMdx";
import { JazzLogo } from "@/components/logos";
<h1 id="guide">Learn some Jazz.</h1>
<Slogan>Build an issue tracking app with distributed state.</Slogan>
Our issues app will be quite simple, but it will have team collaboration. <span className="text-nowrap">**Let's call it... &ldquo;Circular.&rdquo;**</span>
We'll build everything **step-by-step,** in typical, immediately usable stages. We'll explore many important things Jazz does &mdash; so **follow along** or **just pick things out.**
<h2 id="setup">Project Setup</h2>
1. Create a project from a generic Vite starter template:
```bash
npx degit gardencmp/vite-ts-react-tailwind circular
cd circular
npm install
npm run dev
```
You should now have an empty app running, typically at [localhost:5173](http://localhost:5173).<br/>
<small>
(If you make changes to the code, the app will automatically refresh.)
</small>
2. Install `jazz-tools` and `jazz-react`<br/>
<small>(in a new terminal window):</small>
```bash
cd circular
npm install jazz-tools jazz-react
```
3. Set up a Jazz context, by modifying `src/main.tsx`:
```tsx subtle=1,2,3,4,13,15,16,17,19
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { JazzReact } from "jazz-react";
const Jazz = createJazzReactContext({
auth: DemoAuth({ appName: "Circular" }),
peer: "wss://mesh.jazz.tools/?key=you@example.com", // <- put your email here to receive a proper API key for later
});
export const { useAccount, useCoState } = Jazz;
ReactDOM.createRoot(document.getElementById("root")!).render(
<Jazz.Provider>
<React.StrictMode>
<App />
</React.StrictMode>
</Jazz.Provider>,
);
```
This sets Jazz up, extracts app-specific hooks for later, and wraps our app in the provider.
TODO: explain Auth
<h2 id="intro-to-covalues">Intro to CoValues</h2>
Let's learn about the **central idea** behind Jazz: **Collaborative Values.**
What if we could **treat distributed state like local state?** That's what CoValues do.
We can
- **create** CoValues, anywhere
- **load** CoValues by `ID`, from anywhere else
- **edit** CoValues, from anywhere, by mutating them like local state
- **subscribe to edits** in CoValues, whether they're local or remote
<h3 id="first-schema">Declaring our own CoValues</h3>
To make our own CoValues, we first need to declare a schema for them. Think of a schema as a combination of TypeScript types and runtime type information.
Let's start by defining a schema for our most central entity in Circular: an **Issue.**
Create a new file `src/schema.ts` and add the following:
```ts
import { CoMap, co } from "jazz-tools";
export class Issue extends CoMap {
title = co.string;
description = co.string;
estimate = co.number;
status? = co.literal("backlog", "in progress", "done");
}
```
TODO: explain what's happening
<h3>Reading from CoValues</h3>
CoValues are designed to be read like simple local JSON state. Let's see how we can read from an Issue by building a component to render one.
Create a new file `src/components/Issue.tsx` and add the following:
```tsx
import { Issue } from "../schema";
export function IssueComponent({ issue }, { issue: Issue }) {
return (
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
<h2>{issue.title}</h2>
<p className="col-span-3">{issue.description}</p>
<p>Estimate: {issue.estimate}</p>
<p>Status: {issue.status}</p>
</div>
);
}
```
Simple enough!
<h3>Creating CoValues</h3>
To actually see an Issue, we have to create one. This is where things start to get interesting...
Let's modify `src/App.tsx` to prepare for creating an Issue and then rendering it:
```tsx subtle=5,13,14,15
import { useState, useCallback } from "react";
import { Issue } from "./schema";
import { IssueComponent } from "./components/Issue";
function App() {
const [issue, setIssue] = useState<Issue>();
if (issue) {
return <IssueComponent issue={issue} />;
} else {
return <button>Create Issue</button>;
}
}
export default App;
```
Now, finally, let's implement creating an issue:
```tsx subtle=1,2,3,5,6,8,23,24,25,27,28,29,30
import { useState } from "react";
import { Issue } from "./schema";
import { IssueComponent } from "./components/Issue";
import { useAccount } from "./main";
function App() {
const { me } = useAccount();
const [issue, setIssue] = useState<Issue>();
const createIssue = () => {
const newIssue = Issue.create(
{
title: "Buy terrarium",
description: "Make sure it's big enough for 10 snails.",
estimate: 5,
status: "backlog",
},
{ owner: me },
);
setIssue(newIssue);
};
if (issue) {
return <IssueComponent issue={issue} />;
} else {
return <button onClick={createIssue}>Create Issue</button>;
}
}
export default App;
```
Now you should be able to create a new issue by clicking the button and then see it rendered.
<div className="text-xs uppercase text-stone-400 dark:text-stone-600 tracking-wider">
Preview
</div>
<div className="p-3 md:-mx-3 rounded border border-stone-100 dark:border-stone-900 bg-white dark:bg-black not-prose">
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
<h2>Buy terrarium</h2>
<p className="col-span-3">Make sure it's big enough for 10 snails.</p>
<p>Estimate: 5</p>
<p>Status: backlog</p>
</div>
</div>
We'll already notice one interesting thing here:
- We have to create every CoValue with an `owner`!
- this will determine access rights on the CoValue, which we'll learn about in "Groups & Permissions"
- here we set `owner` to the current user `me`, which we get from the Jazz context / `useAccount`
**Behind the scenes, Jazz not only creates the Issue in memory but also automatically syncs an encrypted version to the cloud and persists it locally. The Issue also has a globally unique ID.**
We'll make use of both of these facts in a bit, but for now let's start with local editing and subscribing.
<h3>Editing CoValues and subscribing to edits</h3>
Since we're the owner of the CoValue, we should be able to edit it, right?
And since this is a React app, it would be nice to subscribe to edits of the CoValue and reactively re-render the UI, like we can with local state.
This is exactly what the `useCoState` hook is for!
- Note that `useCoState` doesn't take a CoValue directly, but rather a CoValue's schema, plus its `ID`.
- So we'll slightly adapt our `useState` to only keep track of an issue ID...
- ...and then use `useCoState` to get the actual issue
Let's modify `src/App.tsx`:
```tsx subtle=1,2,3,4,5,6,7,12,13,14,15,16,17,18,19,20,21,23,25,26,27,28,29,30,32
import { useState } from "react";
import { Issue } from "./schema";
import { IssueComponent } from "./components/Issue";
import { useAccount } from "./main";
function App() {
const { me } = useAccount();
const [issueID, setIssueID] = useState<ID<Issue>>();
const issue = useCoState(Issue, issueID);
const createIssue = () => {
const newIssue = Issue.create(
{
title: "Buy terrarium",
description: "Make sure it's big enough for 10 snails.",
estimate: 5,
status: "backlog",
},
{ owner: me },
);
setIssueID(newIssue.id);
};
if (issue) {
return <IssueComponent issue={issue} />;
} else {
return <button onClick={createIssue}>Create Issue</button>;
}
}
export default App;
```
And now for the exciting part! Let's make `src/components/Issue.tsx` an editing component.
```tsx subtle=1,3,4,5,28,29,30,31
import { Issue } from "../schema";
export function IssueComponent({ issue }, { issue: Issue }) {
return (
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
<input type="text"
value={issue.title}
onChange={(event) => { issue.title = event.target.value }}/>
<textarea className="col-span-3"
value={issue.description}
onChange={(event) => { issue.description = event.target.value }}/>
<label className="flex">
Estimate:
<input type="number" className="text-right min-w-0"
value={issue.estimate}
onChange={(event) => { issue.estimate = Number(event.target.value) }}/>
</label>
<select
value={issue.status}
onChange={(event) => {
issue.status = event.target.value as "backlog" | "in progress" | "done"
}}
>
<option value="backlog">Backlog</option>
<option value="in progress">In Progress</option>
<option value="done">Done</options>
</select>
</div>
);
}
```
<div className="text-xs uppercase text-stone-400 dark:text-stone-600 tracking-wider">
Preview
</div>
<div className="p-3 md:-mx-3 rounded border border-stone-100 dark:border-stone-900 bg-white dark:bg-black not-prose">
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
<input type="text" value={"Buy terrarium"} />
<input
type="text"
className="col-span-3"
value={"Make sure it's big enough for 10 snails."}
/>
<label className="flex">
Estimate:{" "}
<input type="number" value={5} className="text-right min-w-0" />
</label>
<select value={"backlog"}>
<option value="backlog">Backlog</option>
<option value="in progress">In Progress</option>
<option value="done">Done</option>
</select>
</div>
</div>
You'll immediately notice that we're doing something non-idiomatic for React: we mutate the issue directly, by assigning to its properties.
This works because CoValues intercept these edits, update their local view accordingly (React doesn't really care after rendering) and then notify subscribers of the change, who will receive a fresh, updated view of the CoValue.
<aside className="text-sm border border-stone-300 dark:border-stone-700 rounded px-4 my-4 max-w-3xl [&_pre]:mx-0">
<h4 className="not-prose text-base py-2 mb-3 -mx-4 px-4 border-b border-stone-300 dark:border-stone-700">💡 A Quick Overview of Subscribing to CoValues</h4>
There are three main ways to subscribe to a CoValue:
1. Directly on an instance:
```ts
const unsub = issue.subscribe((updatedIssue) => console.log(updatedIssue));
```
2. If you only have an ID (this will load the issue if needed):
```ts
const unsub = Issue.subscribe(issueID, { as: me }, (updatedIssue) => {
console.log(updatedIssue);
});
```
3. If you're in a React component, to re-render reactively:
`tsx
const issue = useCoState(Issue, issueID);
`
By the way, `useCoState` is basically just an optimized version of
```ts
function useCoState<V extends CoValue>(Schema: CoValueClass<V>, id?: ID<V>): V | undefined {
const { me } = useAccount();
const [value, setValue] = useState<V>();
useEffect(() => Schema.subscribe(id, { as: me }, setValue), [id]);
return value;
}
```
</aside>
We have one subscriber on our Issue, with `useCoState` in `src/App.tsx`, which will cause the `App` component and its children **to** re-render whenever the Issue changes.
<h3>Automatic local & cloud persistence</h3>
So far our Issue CoValues just looked like ephemeral local state. We'll now start exploring the first main feature that makes CoValues special: **automatic persistence.**
Actually, all the Issue CoValues we've created so far **have already been automatically persisted** to the cloud and locally - but we loose track of their ID after a reload.
So let's store the ID in URL state and make sure our useState is in sync with that.
```tsx subtle=1,2,3,4,5,6,7,12,13,14,15,16,17,18,19,20,21,22,23,24,26,27,28,29,30,31,32,33,34,35
import { useState } from "react";
import { Issue } from "./schema";
import { IssueComponent } from "./components/Issue";
import { useAccount } from "./main";
function App() {
const { me } = useAccount();
const [issueID, setIssueID] = useState<ID<Issue>>(
window.location.search?.replace("?issue=", "") || undefined
);
const issue = useCoState(Issue, issueID);
const createIssue = () => {
const newIssue = Issue.create(
{
title: "Buy terrarium",
description: "Make sure it's big enough for 10 snails.",
estimate: 5,
status: "backlog",
},
{ owner: me },
);
setIssueID(newIssue.id);
window.history.pushState({}, "", `?issue=${newIssue.id}`);
};
if (issue) {
return <IssueComponent issue={issue} />;
} else {
return <button onClick={createIssue}>Create Issue</button>;
}
}
export default App;
```
Now you should be able to create an issue, reload the page, and still see the same issue.
<h3>Remote sync</h3>
But even better, you should be able to:
- copy the URL to a new tab and see the same issue
- edit the issue and see the changes reflected in the other tab
<div className="text-amber-500 mt-52">
🚧 OH NO - This is as far as we've written the Guide. 🚧
</div>
{" -> "}
<a href="https://github.com/gardencmp/jazz/issues/186">Complain on GitHub</a>
<h2 id="refs-and-on-demand-subscribe">Refs & Auto-Subscribe</h2>
<h2 id="groups-and-permissions">Groups & Permissions</h2>
<h2 id="accounts-and-migrations">Accounts & Migrations</h2>
<h2 id="backend-workers">Backend Workers</h2>

View File

@@ -0,0 +1,55 @@
import { DocNav } from "@/components/docs/nav";
import { PackageDocs } from "@/components/docs/packageDocs";
import Guide from "./guide.mdx";
export const metadata = {
title: "jazz - Docs",
description: "Jazz Guide, FAQ & Docs.",
};
export default function Page() {
return (
<>
<div className="hidden md:block bg-stone-100 dark:bg-stone-900 p-4 rounded-xl sticky overflow-y-scroll overscroll-contain w-[16rem] h-[calc(100dvh-8rem)] -mb-[calc(100dvh-8rem)] top-[6rem] mr-10 prose-sm prose-ul:pl-1 prose-ul:ml-1 prose-li:my-2 prose-li:leading-tight prose-ul:list-['-']">
<DocNav />
</div>
<div className="md:ml-[20rem]">
<Guide />
<h1 id="faq">FAQ</h1>
<p>
<span className="text-amber-500">
🚧 OH NO - We don&apos;t have any FAQ yet. 🚧
</span>{" "}
{"->"}{" "}
<a href="https://github.com/gardencmp/jazz/issues/187">
Complain on GitHub
</a>
</p>
<div className="xl:-mr-[calc(50vw-40rem)]">
<h1>API Reference</h1>
<p>
<span className="text-amber-500">
🚧 OH NO - These docs are still highly
work-in-progress. 🚧
</span>{" "}
{"->"}{" "}
<a href="https://github.com/gardencmp/jazz/issues/188">
Complain on GitHub
</a>
</p>
<PackageDocs package="jazz-tools" />
<PackageDocs package="jazz-react" />
<PackageDocs package="jazz-browser" />
<PackageDocs package="jazz-browser-media-images" />
<PackageDocs package="jazz-nodejs" />
</div>
</div>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,418 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base, shiki;
@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;
}
.overlay-close {
background-color: "black";
}
}
pre.shiki {
overflow: hidden;
}
pre.shiki:hover .dim {
opacity: 1;
}
pre.shiki div.dim {
opacity: 0.5;
}
pre.shiki div.dim,
pre.shiki div.highlight {
margin: 0;
padding: 0;
}
pre.shiki div.highlight {
opacity: 1;
background-color: #f1f8ff;
}
pre.shiki div.line {
/* min-height: 1rem; */
counter-increment: lineNumber 1;
}
pre.shiki div.line::before {
content: counter(lineNumber);
display: inline-block;
vertical-align: middle;
width: 1.3rem;
padding-right: 0.3rem;
text-align: right;
transition: color 0.3s;
@apply text-stone-200/50 dark:text-stone-900 text-[0.65rem];
}
pre.shiki div.line:hover::before {
@apply text-stone-400 dark:text-stone-600;
}
/** Don't show the language identifiers */
pre.shiki .language-id {
display: none;
}
/** When you mouse over the pre, show the underlines */
pre.twoslash:hover data-lsp {
@apply border-dotted border-b border-stone-300 dark:border-stone-700;
}
/** The tooltip-like which provides the LSP response */
pre.twoslash data-lsp::before {
content: attr(lsp);
position: absolute;
transform: translate(0, 1.2rem);
max-width: 30rem;
@apply text-xs px-1.5 py-1 rounded border border-stone-200 dark:border-stone-800 shadow-lg overflow-hidden whitespace-pre-wrap text-stone-700 bg-stone-50 dark:text-stone-200 dark:bg-stone-950;
text-align: left;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
display: none;
}
pre.twoslash data-lsp:hover::before {
display: block;
opacity: 1;
pointer-events: visible;
width: auto;
height: auto;
}
.shiki-outer {
@apply shadow-sm rounded-xl;
}
.shiki-filename {
@apply px-3 py-2 bg-stone-100 text-stone-700 dark:bg-stone-900 dark:text-stone-300 rounded-t-xl text-xs;
}
pre .code-container {
overflow: scroll;
@apply p-2 pl-0 bg-stone-75 dark:bg-stone-925 rounded-b-xl text-[0.8rem] leading-tight border border-stone-100 dark:border-stone-900;
}
/* The try button */
pre .code-container > a {
position: absolute;
right: 8px;
bottom: 8px;
border-radius: 4px;
border: 1px solid #719af4;
padding: 0 8px;
color: #719af4;
text-decoration: none;
opacity: 0;
transition-timing-function: ease;
transition: opacity 0.3s;
}
/* Respect no animations */
@media (prefers-reduced-motion: reduce) {
pre .code-container > a {
transition: none;
}
}
pre .code-container > a:hover {
color: white;
background-color: #719af4;
}
pre .code-container:hover a {
opacity: 1;
}
pre code {
white-space: pre;
}
pre code a {
text-decoration: none;
}
pre data-err {
/* Extracted from VS Code */
background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
repeat-x bottom left;
padding-bottom: 3px;
}
pre .query {
margin-bottom: 10px;
color: #137998;
display: inline-block;
}
/* In order to have the 'popped out' style design and to not break the layout
/* we need to place a fake and un-selectable copy of the error which _isn't_ broken out
/* behind the actual error message.
/* This sections keeps both of those two in in sync */
pre .error,
pre .error-behind {
margin-left: -14px;
margin-top: 8px;
margin-bottom: 4px;
padding: 6px;
padding-left: 14px;
width: calc(100% - 20px);
white-space: pre-wrap;
display: block;
}
pre .error {
position: absolute;
background-color: #fee;
border-left: 2px solid #bf1818;
/* Give the space to the error code */
display: flex;
align-items: center;
color: black;
}
pre .error .code {
display: none;
}
pre .error-behind {
user-select: none;
visibility: transparent;
color: #fee;
}
/* Queries */
pre .arrow {
/* Transparent background */
background-color: #eee;
position: relative;
top: -7px;
margin-left: 0.1rem;
/* Edges */
border-left: 1px solid #eee;
border-top: 1px solid #eee;
transform: translateY(25%) rotate(45deg);
/* Size */
height: 8px;
width: 8px;
}
pre .popover {
margin-bottom: 10px;
background-color: #eee;
display: inline-block;
padding: 0 0.5rem 0.3rem;
margin-top: 10px;
border-radius: 3px;
}
/* Completion */
pre .inline-completions ul.dropdown {
display: inline-block;
position: absolute;
width: 240px;
background-color: gainsboro;
color: grey;
padding-top: 4px;
font-family: var(--code-font);
font-size: 0.8rem;
margin: 0;
padding: 0;
border-left: 4px solid #4b9edd;
}
pre .inline-completions ul.dropdown::before {
background-color: #4b9edd;
width: 2px;
position: absolute;
top: -1.2rem;
left: -3px;
content: " ";
}
pre .inline-completions ul.dropdown li {
overflow-x: hidden;
padding-left: 4px;
margin-bottom: 4px;
}
pre .inline-completions ul.dropdown li.deprecated {
text-decoration: line-through;
}
pre .inline-completions ul.dropdown li span.result-found {
color: #4b9edd;
}
pre .inline-completions ul.dropdown li span.result {
width: 100px;
color: black;
display: inline-block;
}
.dark-theme .markdown pre {
background-color: #d8d8d8;
border-color: #ddd;
filter: invert(98%) hue-rotate(180deg);
}
data-lsp {
/* Ensures there's no 1px jump when the hover happens */
border-bottom: 1px dotted transparent;
/* Fades in unobtrusively */
transition-timing-function: ease;
transition: border-color 0.3s;
}
/* Respect people's wishes to not have animations */
@media (prefers-reduced-motion: reduce) {
data-lsp {
transition: none;
}
}
/** Annotations support, providing a tool for meta commentary */
.tag-container {
position: relative;
}
.tag-container .twoslash-annotation {
position: absolute;
font-family:
"JetBrains Mono",
Menlo,
Monaco,
Consolas,
Courier New,
monospace;
right: -10px;
/** Default annotation text to 200px */
width: 200px;
color: #187abf;
background-color: #fcf3d9 bb;
}
.tag-container .twoslash-annotation p {
text-align: left;
font-size: 0.8rem;
line-height: 0.9rem;
}
.tag-container .twoslash-annotation svg {
float: left;
margin-left: -44px;
}
.tag-container .twoslash-annotation.left {
right: auto;
left: -200px;
}
.tag-container .twoslash-annotation.left svg {
float: right;
margin-right: -5px;
}
/** Support for showing console log/warn/errors inline */
pre .logger {
display: flex;
align-items: center;
color: black;
padding: 6px;
padding-left: 8px;
width: calc(100% - 19px);
white-space: pre-wrap;
}
pre .logger svg {
margin-right: 9px;
}
pre .logger.error-log {
background-color: #fee;
border-left: 2px solid #bf1818;
}
pre .logger.warn-log {
background-color: #ffe;
border-left: 2px solid #eae662;
}
pre .logger.log-log {
background-color: #e9e9e9;
border-left: 2px solid #ababab;
}
pre .logger.log-log svg {
margin-left: 6px;
margin-right: 9px;
}
body {
--shiki-color-text: #606060;
--shiki-color-background: transparent;
--shiki-token-constant: #00a5a5;
--shiki-token-string: #1aa245;
--shiki-token-comment: #aaa;
--shiki-token-keyword: #7b8bff;
--shiki-token-parameter: #ff9800;
--shiki-token-function: #445dd7;
--shiki-token-string-expression: #1aa245;
--shiki-token-punctuation: #969696;
--shiki-token-link: #1aa245;
}
.dark body {
--shiki-color-text: #d1d1d1;
--shiki-token-constant: #2dc9c9;
--shiki-token-string: #ffab70;
--shiki-token-comment: #6b737c;
--shiki-token-keyword: #7b8bff;
--shiki-token-parameter: #ff9800;
--shiki-token-function: #9babff;
--shiki-token-string-expression: #42bb69;
--shiki-token-punctuation: #bbb;
--shiki-token-link: #ffab70;
}

View File

@@ -0,0 +1,219 @@
import "./globals.css";
import type { Metadata } from "next";
import { ThemeProvider } from "@/components/themeProvider";
import { Manrope } from "next/font/google";
import { Inter } from "next/font/google";
import localFont from "next/font/local";
import { GcmpLogo, JazzLogo } from "@/components/logos";
import { SiGithub, SiDiscord, SiTwitter } from "@icons-pack/react-simple-icons";
import { Nav, NavLink, Newsletter, NewsletterButton } from "@/components/nav";
import { MailIcon } from "lucide-react";
import { DocNav } from "@/components/docs/nav";
// If loading a variable font, you don't need to specify the font weight
const manrope = Manrope({
subsets: ["latin"],
variable: "--font-manrope",
display: "swap",
});
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
display: "swap",
});
const pragmata = localFont({
src: "../fonts/ppr_0829.woff2",
variable: "--font-ppr",
});
export const metadata: Metadata = {
title: "jazz - Instant sync",
description: "Go beyond request/response - ship modern apps with sync.",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body
className={[
manrope.variable,
pragmata.variable,
inter.className,
"flex flex-col items-center bg-stone-50 dark:bg-stone-950 overflow-x-hidden",
].join(" ")}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Nav
mainLogo={<JazzLogo className="w-24 -ml-2" />}
items={[
{ title: "Home", href: "/" },
{ title: "Sync & Storage Mesh", href: "/mesh" },
{
title: "Docs",
href: "/docs",
},
{
title: "Blog",
href: "https://gcmp.io/news",
firstOnRight: true,
newTab: true,
},
{
title: "Releases",
href: "https://github.com/gardencmp/jazz/releases",
newTab: true,
},
{
title: "Roadmap",
href: "https://github.com/orgs/gardencmp/projects/4/views/3",
newTab: true,
},
{
title: "GitHub",
href: "https://github.com/gardencmp/jazz",
newTab: true,
icon: <SiGithub className="w-5" />,
},
{
title: "Discord",
href: "https://discord.gg/utDMjHYg42",
newTab: true,
icon: <SiDiscord className="w-5" />,
},
{
title: "X",
href: "https://x.com/jazz_tools",
newTab: true,
icon: <SiTwitter className="w-5" />,
},
]}
docNav={<DocNav />}
/>
<main className="flex min-h-screen flex-col p-8 max-w-[80rem] w-full [&_*]:scroll-mt-[6rem]">
<article
className={[
"prose lg:prose-lg max-w-none prose-stone dark:prose-invert",
"prose-headings:font-display",
"prose-h1:text-5xl lg:prose-h1:text-6xl prose-h1:font-medium prose-h1:tracking-tighter",
"prose-h2:text-2xl lg:prose-h2:text-3xl prose-h2:font-medium prose-h2:tracking-tight",
"prose-p:max-w-3xl prose-p:leading-snug",
"prose-strong:font-medium",
"prose-code:font-normal prose-code:leading-tight prose-code:before:content-none prose-code:after:content-none prose-code:bg-stone-100 prose-code:dark:bg-stone-900 prose-code:p-1 prose-code:-my-1 prose-code:rounded",
"prose-pre:max-w-3xl prose-pre:text-[0.8em] prose-pre:leading-[1.3] prose-pre:-mt-4 prose-pre:my-4 prose-pre:px-3 prose-pre:py-2 md:prose-pre:-mx-3 prose-pre:bg-stone-100 dark:prose-pre:bg-stone-900",
"prose-inner-code:font-normal prose-inner-code:text-[1em]",
].join(" ")}
>
{children}
</article>
</main>
<footer className="flex z-10 mt-10 min-h-[15rem] -mb-20 bg-stone-100 dark:bg-stone-900 text-stone-600 dark:text-stone-400 w-full justify-center">
<div className="p-8 max-w-[80rem] w-full grid grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-8 max-sm:mb-12">
<div className="col-span-full md:col-span-1 sm:row-start-4 md:row-start-auto lg:col-span-2 md:row-span-2 md:flex-1 flex flex-row md:flex-col max-sm:mt-4 justify-between max-sm:items-start gap-2 text-sm min-w-[10rem]">
<GcmpLogo monochrome className="w-32" />
<p className="max-sm:text-right">
© 2023
<br />
Garden Computing, Inc.
</p>
</div>
<div className="flex flex-col gap-2 text-sm">
<h1 className="font-medium">Resources</h1>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="/"
>
Toolkit
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="/mesh"
>
Sync & Storage Mesh
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="/docs"
>
Docs
</NavLink>
</div>
{/* <div className="flex flex-col gap-2 text-sm">
<h1 className="font-medium">Legal</h1>
</div> */}
<div className="flex flex-col gap-2 text-sm">
<h1 className="font-medium">Community</h1>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://github.com/gardencmp/jazz"
newTab
>
GitHub
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://discord.gg/utDMjHYg42"
newTab
>
Discord
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://x.com/jazz_tools"
newTab
>
Twitter
</NavLink>
</div>
<div className="flex flex-col gap-2 text-sm">
<h1 className="font-medium">News</h1>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://gcmp.io/news"
newTab
>
Blog
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://github.com/gardencmp/jazz/releases"
newTab
>
Releases
</NavLink>
<NavLink
className="py-0.5 max-sm:px-0 md:px-0 lg:px-0"
href="https://github.com/orgs/gardencmp/projects/4/views/3"
newTab
>
Roadmap
</NavLink>
</div>
<div className="col-span-3 md:col-start-2 lg:col-start-auto flex flex-col gap-2 text-sm">
Sign up for updates:
<Newsletter />
</div>
</div>
</footer>
</ThemeProvider>
<script
defer
data-api="/api/event"
data-domain="jazz.tools"
src="/js/script.js"
></script>
</body>
</html>
);
}

View File

@@ -0,0 +1,253 @@
import {
Slogan,
Grid,
GridCard,
GridItem,
ComingSoonBadge,
} from "@/components/forMdx";
export const metadata = {
title: "jazz - Jazz Mesh",
description: "Serverless sync & storage for Jazz apps.",
};
<div className="md:pt-20" />
# Sync & Storage Mesh
<Slogan>The first Collaboration Delivery Network.</Slogan>
Real-time sync and storage infrastructure that scales up to millions of users.<br/>
Pricing that scales down to zero.
<Grid>
<GridCard>
#### Optimal mesh routing.
Get ultra-low latency between any group of users with our decentralized mesh interconnect.
</GridCard>
<GridCard>
#### Smart caching.
Give users instant load times, with their latest data state always cached close to them.
</GridCard>
<GridCard>
#### Blob storage & media streaming.
Store files and media streams as idiomatic `CoValues` without S3.
</GridCard>
</Grid>
## Pricing
<Slogan small></Slogan>
<Grid>
<GridCard>
### Mesh Free
<span className="text-2xl">$0</span>
- Best-effort sync
- 3,000 sync-minutes/mo
- 1 GB storage
{/* - Egress/mo: 5 million ops <span className="text-xs">or 50GB blobs</span> */}
{/* - Storage: 2.5 million ops <span className="text-xs">or 25GB blobs</span> */}
</GridCard>
<GridCard>
### Mesh Starter <ComingSoonBadge/>
<span className="text-2xl">$9</span>/mo
- Base-priority sync
- 6,000 sync-minutes/mo
- 100 GB storage
<div className="text-xs">
Extra usage:
- $9 per additional 6,000 sync-minutes
- $9 per additional 1TB storage/mo
</div>
</GridCard>
<GridCard>
### Mesh Pro <ComingSoonBadge/>
<span className="text-2xl">$79</span>/mo
- High-priority sync
- 30,000 sync-minutes/mo
- 1 TB storage
- Offer sync.yourdomain.com
<div className="text-xs">
Extra usage:
- $15 per additional 6,000 sync-minutes
- $15 per additional 1TB storage/mo
</div>
</GridCard>
{/*<GridCard>
### Mesh Enterprise <ComingSoonBadge/>
<span className="text-2xl">Custom</span>
- Custom SLA
- Custom cloud deployment
- Dedicated support
- Audit logs
</GridCard>*/}
</Grid>
### FAQ:
<div className="text-sm">
#### How are sync-minutes counted?
Sync-minutes are counted on a **per-connected-device, per-minute basis.**<br/>
A device is considered syncing **only when it's actively sending or receiving data.**
#### How can I estimate my usage?
The best way to estimate your usage is to **guess how many minutes per month each user will spend actively using your app.**
Storage is mostly determined by large binary blobs (like images or videos) that you store in Jazz.
#### What happens if I exceed my plan's limits?
All limits are initially soft limits, so don't worry if you suddenly get lots of users or traffic!
Sync beyond the limit is still served, but at a lower priority.
Data beyond the storage limit is still stored and backed up, but may be significantly slower to access.
If you exceed your plan's limits consistently, we'll reach out to discuss upgrading your plan.
</div>
{/* *### Examples:* */}
<div className="text-sm">
</div>
## Global Footprint
We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage:
<Grid className="grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<GridItem>
<div className="text-sm">
**Under 50ms RTT**
- Frankfurt
- New York
- Newark
- North California
- North Virginia
- San Francisco
- Singapore
- Toronto
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 100ms RTT**
- Amsterdam
- Atlanta
- London
- Ohio
- Paris
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 200ms RTT**
- Bangalore
- Dallas
- Mumbai
- Oregon
**Under 300ms RTT**
- Seoul
- Tokyo
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 400ms RTT**
- Sao Paulo
- Sydney
**Under 500ms RTT**
- Cape Town
</div>
</GridItem>
</Grid>
### Enterprise
Custom deployment in the cloud, your private cloud, on-premises or hybrids?
SLAs and dedicated support? White-glove integration services?
Let's talk: <a href="mailto:hello@gcmp.io">hello@gcmp.io</a>
## Custom Deployment Scenarios
<Slogan small>You can rely on Jazz Mesh. But you don't have to.</Slogan>
<p>
Because Jazz is open-source, you can optionally run your own sync nodes
&mdash; in a variety of setups.
</p>
<Grid>
<GridCard>
#### Jazz Mesh + Data Backup Node.
<p className="no-prose text-base">
Connect your users to Jazz Mesh for all its benefits, but also run and
connect your own data backup node (just in case.)
</p>
<div className="text-sm">
Extra costs:
- Instance costs for the backup node.
- Moderate self-hosted storage costs.
- Your backup node is counted as a continously connected device.
</div>
</GridCard>
<GridCard>
#### Jazz Mesh + DIY Mesh.
<p className="no-prose text-base">
Connect your users to Jazz Mesh, or your own nodes as a lower-performance
fallback. The two networks stay in constant sync.
</p>
<div className="text-sm">
Extra costs:
- N × instance cost for your sync nodes.
- Typically moderate self-hosted egress costs.
- High self-hosted storage costs.
- Any of your sync nodes connected to Jazz Mesh is counted as a continously connected device.
</div>
</GridCard>
<GridCard>
#### Completely DIY Mesh.
<p className="no-prose text-base">
Build your own network of sync and storage nodes. Handle networking,
security and backups yourself.
</p>
<div className="text-sm">
Costs:
- N × instance cost for your sync nodes.
- Very high self-hosted egress costs.
- High self-hosted storage costs.
</div>
</GridCard>
</Grid>

View File

@@ -0,0 +1,278 @@
import {
Slogan,
Grid,
GridItem,
GridFeature,
GridCard,
MultiplayerIcon,
ResponsiveIframe,
ComingSoonBadge,
} from "@/components/forMdx";
import { JazzLogo, LocalFirstConfLogo } from "@/components/logos";
import {
WorkflowIcon,
UploadCloudIcon,
PlaneIcon,
MonitorSmartphoneIcon,
GaugeIcon,
UsersIcon,
FileLock2Icon,
HardDriveDownloadIcon,
KeyRoundIcon
} from "lucide-react";
import { App_tsx, ChatScreen_tsx } from "@/codeSamples/examples/chat/src";
import Link from "next/link";
<div className="md:pt-20" />
<a href="https://app.localfirstconf.com/schedule/conference/every-app-secretly-wants-to-be-local-first" className="-mt-8 md:-mt-20 float-right top-[5rem] right-4 border border-stone-700 dark:border-stone-300 rounded flex gap-3 items-center px-4 py-2 mb-4 rotate-2 md:rotate-6 no-underline hover:scale-105 transition-transform">
<div className="text-sm font-bold uppercase">See you in Berlin<br/>May 30-31!</div>
<LocalFirstConfLogo className="w-24"/>
</a>
# Instant sync.
<Slogan>A new way to build apps with distributed state.</Slogan>
<Grid className="gap-[1px] -mx-4 md:-mx-6 rounded-xl overflow-hidden bg-stone-50 dark:bg-stone-950 [&>*]:rounded-none [&>*]:border-none [&>*]:bg-stone-100 [&>*]:dark:bg-stone-900">
<GridFeature icon={<MonitorSmartphoneIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Cross-device sync</GridFeature>
<GridFeature icon={<MultiplayerIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Real-time multiplayer</GridFeature>
<GridFeature icon={<UsersIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Team/social features</GridFeature>
<GridFeature icon={<FileLock2Icon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Built-in permissions</GridFeature>
<GridFeature icon={<UploadCloudIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Cloud sync & storage</GridFeature>
<GridFeature icon={<HardDriveDownloadIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>On-device storage</GridFeature>
<GridFeature icon={<GaugeIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>Instant UI updates</GridFeature>
<GridFeature icon={<KeyRoundIcon strokeWidth={1} strokeLinecap="butt" size={40}/>}>E2EE & signatures</GridFeature>
<div className="col-start-1 row-start-1 row-span-2 col-span-2 px-4 md:px-6 text-base">
<h2 className="not-prose text-2xl font-medium tracking-tight mt-3 md:mt-5 mb-6">Hard things are easy now.</h2>
Jazz is an **open-source toolkit** that replaces APIs, databases and message queues with **a&nbsp;single new abstraction:**
**&ldquo;Collaborative Values&rdquo;** &mdash; **distributed state** with **secure permissions built-in.**
Features that used to take months to build now work out-of-the-box.
</div>
</Grid>
<div className="-mx-[calc(min(0,(100vw-95rem)/2))]">
### First impressions&hellip;
<Slogan small>A chat app in 84 lines of code.</Slogan>
<Grid className="mt-0 -mx-4 md:-mx-6">
<GridItem className="md:col-start-1 col-span-2">
<App_tsx/>
</GridItem>
<GridItem className="md:col-start-3 col-span-2">
<ChatScreen_tsx/>
</GridItem>
<ResponsiveIframe src="https://chat.jazz.tools" localSrc="http://localhost:5173" className="lg:col-start-5 col-span-2 rounded-xl overflow-hidden min-h-[50vh]"/>
</Grid>
</div>
## Collaborative Values
<Slogan small>Your new building blocks.</Slogan>
<div className='text-base'>
Based on CRDTs and public-key cryptography, CoValues...
- Can be read & edited like simple local JSON state
- Can be created anywhere, are automatically synced & persisted
- Always keep full edit history & author metadata
- Automatically resolve most conflicts
</div>
### Bread-and-butter datastructures
<Grid className="lg:gap-y-8 grid-cols-2 lg:grid-cols-4">
<GridCard>
#### `CoMap`
<div className="text-sm">
- Collaborative key-value map - Possible values: - Immutable JSON &
other CoValues
</div>
</GridCard>
<GridCard>
#### `CoList`
<div className="text-sm">
- Collaborative ordered list - Possible items: - Immutable JSON & other
CoValues
</div>
</GridCard>
<GridCard>
#### `CoPlainText` & `CoRichText` <ComingSoonBadge />
<div className="text-sm">
- Collaborative plain-text & rich-text - Gracefully prevents most
editing conflicts - Rendered as markdown, HTML, JSX, etc.
</div>
</GridCard>
<GridCard>
#### `CoStream`
<div className="text-sm">
- Collection of independent per-user items streams:
- Immutable JSON & other CoValues
- Great for presence, reactions, polls, replies etc.
</div>
</GridCard>
</Grid>
### First-class files & binary data
<Grid className="lg:gap-y-8 grid-cols-2 lg:grid-cols-4">
<GridCard>
#### `BinaryCoStream`
<div className="text-sm">
- Represents a file or live binary stream - Can be referenced and synced
like any other CoValue - Can easily be converted from/to browser `Blob`s
- <code>{`<input type="file"/>`}</code> -> `BinaryCoStream` -> `Blob` ->
`BlobURL`
</div>
</GridCard>
<GridCard>
#### `ImageDefinition`
<div className="text-sm">
- Represents multiple resolutions of the same image - Can be
progressively loaded, including super fast blur preview & image size
info
</div>
</GridCard>
</Grid>
### Secure permissions, authorship & teams
<Grid className="lg:gap-y-8 grid-cols-2 lg:grid-cols-4">
<GridCard>
#### `Group`
<div className="text-sm">
- A scope where specified accounts have roles (`reader`/`writer`/`admin`).
- A `Group` owns `CoValues`, with access right determined by group roles.
- Accounts can be added to groups directly or using shareable invite secrets.
</div>
</GridCard>
<GridCard>
#### `Account`
<div className="text-sm">
- Represents a single user and their signing/encryption keys.
- Has a private account root and a public profile
- Can contain arbitrary app-specific data
</div>
</GridCard>
</Grid>
## The Jazz Toolkit
<Slogan small>A high-level toolkit for building apps around CoValues.</Slogan>
Supported environments:
<div className="text-sm">
- Browser (sync via WebSockets, IndexedDB persistence)
- React
- Vanilla JS / framework agnostic base
- React Native <ComingSoonBadge/>
- NodeJS (sync via WebSockets, SQLite persistence) <ComingSoonBadge/>
- Swift, Kotlin, Rust <ComingSoonBadge when="later"/>
</div>
<Grid>
<GridCard>
### Auto-sub
<Slogan small>Let your UI drive data-syncing.</Slogan>
<div className="text-sm">
- Load and auto-subscribe to deeply nested `CoValues` with a reactive
hook (or callback). - Access properties & metadata as plain JSON. - Make
granular changes with simple mutators. - No queries needed, everything
loads on-demand: <br />
`profile?.tweets?.map(tweet => tweet?.text)`
</div>
</GridCard>
<GridCard>
### Cursors & carets
<Slogan small>Ready-made spatial presence.</Slogan>
<div className="text-sm">
- 2D canvas cursors <ComingSoonBadge />
- Text carets <ComingSoonBadge />
- Element-based focus-presence <ComingSoonBadge />
- Scroll-based / out-of-bounds helpers <ComingSoonBadge />
</div>
</GridCard>
<GridCard>
### Auth Providers
<Slogan small>Plug and play different kinds of auth.</Slogan>
<div className="text-sm">
- DemoAuth (for quick multi-user demos)
- WebAuthN (TouchID/FaceID)
- Auth0, Clerk & Okta <ComingSoonBadge/>
- NextAuth <ComingSoonBadge/>
</div>
</GridCard>
<GridCard>
### Two-way sync to your DB
<Slogan small>Add Jazz to an existing app.</Slogan>
<div className="text-sm">
- Prisma <ComingSoonBadge />
- Drizzle <ComingSoonBadge />
- PostgreSQL introspection <ComingSoonBadge />
</div>
</GridCard>
<GridCard>
### File upload & download
<Slogan small>Just use `<input type="file"/>`.</Slogan>
<div className="text-sm">
- Easily convert from and to Browser `Blob`s
- Super simple progressive image loading
</div>
</GridCard>
<GridCard>
### Video presence & calls
<Slogan small>Stream and record audio & video.</Slogan>
<div className="text-sm">
- Automatic WebRTC connections between `Group` members <ComingSoonBadge/>
- Audio/video recording into `BinaryCoStreams` <ComingSoonBadge/>
</div>
</GridCard>
</Grid>
## Jazz Mesh
<Slogan small>Serverless sync & storage for Jazz apps</Slogan>
To give you sync and secure collaborative data instantly on a global scale, we're running Jazz Mesh. It works with any Jazz-based app, requires no setup and has straightforward, scale-to-zero pricing.
Jazz Mesh is currently free &mdash; and it's set up as the default sync & storage peer in Jazz, letting you start building multi-user apps with persistence right away, no backend needed.
<Link href="/mesh" target="_blank">
Learn more about Jazz Mesh
</Link>
## Get Started
- <Link href="/docs" target="_blank">
Read the docs
</Link>
- <Link href="https://discord.gg/utDMjHYg42" target="_blank">
Join our Discord
</Link>

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "stone",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@@ -0,0 +1,82 @@
"use client";
import { useLayoutEffect, useState, useRef, IframeHTMLAttributes } from "react";
export function ResponsiveIframe(
props: IframeHTMLAttributes<HTMLIFrameElement> & { localSrc: string },
) {
const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [url, setUrl] = useState<string | undefined>();
const [src, setSrc] = useState<string | undefined>();
useLayoutEffect(() => {
const listener = (e: MessageEvent) => {
console.log(e);
if (e.data.type === "navigate" && src?.startsWith(e.origin)) {
setUrl(e.data.url);
}
};
window.addEventListener("message", listener);
return () => {
window.removeEventListener("message", listener);
};
}, [src]);
useLayoutEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver(() => {
if (!containerRef.current) return;
setDimensions({
width: containerRef.current.offsetWidth,
height: containerRef.current.offsetHeight,
});
});
observer.observe(containerRef.current);
return () => {
observer.disconnect();
};
}, [containerRef]);
useLayoutEffect(() => {
setSrc(
window.location.hostname === "localhost"
? props.localSrc
: props.src,
);
setUrl(
window.location.hostname === "localhost"
? props.localSrc
: props.src,
);
}, [props.src, props.localSrc]);
return (
<div
className={
"w-full h-full flex flex-col items-stretch border border-stone-100 dark:border-stone-900 " +
props.className
}
>
<div className="rounded-t-xl bg-stone-100 dark:bg-stone-900 py-1.5 px-10 flex">
<input
className="text-xs px-1 py-0.5 bg-stone-100 dark:bg-stone-900 outline outline-1 outline-stone-200 dark:outline-stone-800 w-full rounded text-center"
value={url?.replace("http://", "").replace("https://", "")}
onClick={(e) => e.currentTarget.select()}
onBlur={(e) => e.currentTarget.setSelectionRange(0, 0)}
readOnly
/>
</div>
<div className="flex-grow" ref={containerRef}>
<iframe
{...props}
src={src}
className="dark:bg-black"
{...dimensions}
allowFullScreen
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
"use client";
import { usePathname } from "next/navigation";
export function BreadCrumb({
items,
}: {
items: { title: string; href: string }[];
}) {
const pathName = usePathname();
return (
<span className="text-sm font-bold">
{items.find((item) => item.href === pathName)?.title}
</span>
);
}

View File

@@ -0,0 +1,158 @@
import { ReactNode } from "react";
import { ClassRef, PropRef } from "./tags";
import { requestProject } from "./requestProject";
import { PackageIcon } from "lucide-react";
export function DocNav() {
return (
<>
<p className="mt-0 not-prose font-medium">
<DocNavLink href="#guide">Guide</DocNavLink>
</p>
<ul>
<li>
<DocNavLink href="#guide-setup">Project Setup</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Intro to CoValues
</DocNavLink>
<ul>
<li>
<DocNavLink href="#intro-to-covalues">
Declaration
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Reading
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Creation
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Editing & Subscription
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Persistence
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Remote Sync
</DocNavLink>
</li>
<li>
<DocNavLink href="#intro-to-covalues">
Public Sharing
</DocNavLink>
</li>
</ul>
</li>
<li>
<DocNavLink href="#refs-and-on-demand-subscribe">
Refs & Auto-Subscribe
</DocNavLink>
</li>
<li>
<DocNavLink href="#groups-and-permissions">
Groups & Permissions
</DocNavLink>
</li>
<li>
<DocNavLink href="#accounts-and-migrations">
Accounts & Migrations
</DocNavLink>
</li>
<li>
<DocNavLink href="#backend-workers">
Backend Workers
</DocNavLink>
</li>
</ul>
<p className="font-medium border-t -mx-4 px-4 pt-4 border-stone-200 dark:border-stone-800">
<DocNavLink href="#faq">FAQ</DocNavLink>
</p>
<NavPackage package="jazz-tools" />
<NavPackage package="jazz-react" />
<NavPackage package="jazz-browser" />
<NavPackage package="jazz-browser-media-images" />
<NavPackage package="jazz-nodejs" />
</>
);
}
export async function NavPackage({
package: packageName,
}: {
package: string;
}) {
let project = await requestProject(packageName as any);
return (
<>
<h2 className="text-sm not-prose mt-4 flex gap-1 items-center -mx-4 px-4 pt-4 border-t border-stone-200 dark:border-stone-800 ">
<code className="font-bold">{packageName}</code>{" "}
<PackageIcon size={15} strokeWidth={1.5} />
</h2>
{project.categories?.map((category) => {
return (
<details
key={category.title}
open={category.title !== "Other"}
className="[&:not([open])_summary]:after:content-['...']"
>
<summary className="block text-xs mt-2 cursor-pointer">
{category.title}
</summary>
<div className="text-sm -ml-0.5 max-w-full text-balance">
{category.children.map(
(child, i, children) =>
(i == 0 ||
child.name !==
children[i - 1]!.name) && (
<>
<a
key={child.id}
className="inline-block not-prose px-1 m-0.5 bg-stone-200 dark:bg-stone-800 rounded opacity-70 hover:opacity-100 cursor-pointer"
href={`#${packageName}/${child.name}`}
>
<code>{child.name}</code>
</a>
{"\u200B"}
</>
),
)}
</div>
</details>
);
})}
</>
);
}
export function DocNavLink({
href,
children,
}: {
href: string;
children: ReactNode;
}) {
return (
<a
href={href}
className="not-prose hover:text-black dark:hover:text-white"
>
{children}
</a>
);
}

View File

@@ -0,0 +1,438 @@
import {
CommentDisplayPart,
DeclarationReflection,
ReflectionKind,
SignatureReflection,
SomeType,
TypeContext,
TypeParameterReflection,
} from "typedoc";
import {
ClassOrInterface,
DocComment,
FnDecl,
Highlight,
PropCategory,
PropDecl,
} from "./tags";
import { requestProject } from "./requestProject";
import { PackageIcon, Type } from "lucide-react";
export async function PackageDocs({
package: packageName,
}: {
package: string;
}) {
let project = await requestProject(packageName as any);
// console.dir(project, {depth: 10});
return (
<>
<h2 className="flex items-center gap-2">
<code>{packageName}</code> <PackageIcon />
</h2>
{project.categories?.map((category) => {
return (
<section key={category.title}>
<h3>{category.title}</h3>
{category.children.map((child) => (
<RenderPackageChild
child={child}
key={child.id}
inPackage={packageName}
/>
))}
</section>
);
})}
</>
);
}
function RenderPackageChild({
child,
inPackage,
}: {
child: DeclarationReflection;
inPackage: string;
}) {
if (
child.kind === ReflectionKind.Class ||
child.kind === ReflectionKind.Interface
) {
return (
<RenderClassOrInterface
classOrInterface={child}
inPackage={inPackage}
/>
);
} else if (child.kind === ReflectionKind.TypeAlias) {
return <RenderTypeAlias inPackage={inPackage} child={child} />;
} else if (child.kind === ReflectionKind.Function) {
return child.getAllSignatures().map((signature, i) => {
const paramTypes = printParamsWithTypes(signature);
return (
<div
key={i}
id={inPackage + "/" + child.name}
className="not-prose mt-4"
>
{
<Highlight hide={[0, 2]}>
{`function \n${printSimpleSignature(child, signature) + ":"}\n {}`}
</Highlight>
}{" "}
<span className="opacity-75 text-xs pl-1">
<Highlight>{printType(signature.type)}</Highlight>
</span>
<div className="ml-4 mt-0 text-xs opacity-75 flex">
{paramTypes.length > 0 && (
<div>
<Highlight
hide={[0, 1 + paramTypes.length]}
>{`function fn(...args: [\n${paramTypes.join(
",\n",
)}\n]) {}`}</Highlight>
</div>
)}
</div>
</div>
);
});
} else {
return (
<h4 id={inPackage + "/" + child.name}>
{child.name} {child.type?.type}
</h4>
);
}
}
function RenderTypeAlias({
inPackage,
child,
}: {
inPackage: string;
child: DeclarationReflection;
}) {
const typeParameters = child.typeParameters?.map(
(tParam) =>
tParam.name +
(tParam.type ? ` extends ${printType(tParam.type)}` : ""),
);
return (
<div className="mt-4">
<h4 className="not-prose" id={inPackage + "/" + child.name}>
<Highlight>{`type ${child.name}`}</Highlight>
</h4>
<p className="not-prose text-sm ml-4">
<Highlight>{`type ${child.name}${typeParameters?.length && `<${typeParameters?.join(", ")}>`} = ${printType(
child.type,
)}`}</Highlight>
</p>
<div className="ml-4 mt-2 flex-[3]">
<DocComment>
{child.comment
? renderSummary(child.comment.summary)
: "⚠️ undocumented"}
</DocComment>
</div>
</div>
);
}
function RenderClassOrInterface({
inPackage,
classOrInterface: classOrInterface,
}: {
inPackage: string;
classOrInterface: DeclarationReflection;
}) {
const commentSummary = classOrInterface.comment?.summary;
const typeParamsWithConstraints = printTypeParamsWithConstraints(
classOrInterface.typeParameters,
);
return (
<ClassOrInterface
inPackage={inPackage}
name={classOrInterface.name}
doc={renderSummary(commentSummary)}
isInterface={classOrInterface.kind === ReflectionKind.Interface}
typeParameters={
classOrInterface.typeParameters?.length
? "<" +
classOrInterface.typeParameters
.map((tParam) => tParam.name)
.join(", ") +
">"
: ""
}
>
{typeParamsWithConstraints.length > 0 && (
<div className="text-sm -mt-4">
<Highlight
hide={[0, 1 + typeParamsWithConstraints.length]}
>{`class Thing<\n${typeParamsWithConstraints.join(
",\n",
)}\n]> {}`}</Highlight>
</div>
)}
{classOrInterface.categories?.map((category) => (
<div key={category.title}>
<PropCategory
name={category.title}
description={renderSummary(
category.description?.filter(
(p) =>
p.kind !== "code" ||
!p.text.startsWith("```"),
),
)}
example={renderSummary(
category.description?.filter(
(p) =>
p.kind === "code" &&
p.text.startsWith("```"),
),
)}
/>
{category.children.map((prop) => (
<RenderProp
prop={prop}
klass={classOrInterface}
key={prop.id}
/>
))}
</div>
))}
</ClassOrInterface>
);
}
function renderSummary(commentSummary: CommentDisplayPart[] | undefined) {
return commentSummary?.map((part, idx) =>
part.kind === "text" ? (
<span key={idx}>{part.text.split("\n").map((line, i, lines) => <>
{line}
{i !== lines.length - 1 && <br />}
</>)}</span>
) : part.kind === "inline-tag" ? (
<code key={idx}>
{part.tag} {part.text}
</code>
) : part.text.startsWith("```") ? (
<pre key={idx} className="text-xs mt-4">
<code>
<Highlight>
{part.text.split("\n").slice(1, -1).join("\n")}
</Highlight>
</code>
</pre>
) : (
<code key={idx}>
<Highlight>{part.text.slice(1, -1)}</Highlight>
</code>
),
);
}
function RenderProp({
prop,
klass,
}: {
prop: DeclarationReflection;
klass: DeclarationReflection;
}) {
const propOrGetSig = prop.getSignature ? prop.getSignature : prop;
return prop.kind & ReflectionKind.FunctionOrMethod ? (
prop
.getAllSignatures()
.map((signature) => (
<FnDecl
key={signature.id}
signature={printSimplePropSignature(prop, klass, signature)}
typeParams={printTypeParamsWithConstraints(
signature.typeParameters,
)}
paramTypes={printParamsWithTypes(signature)}
returnType={printType(signature.type)}
doc={renderSummary(signature.comment?.summary)}
example={renderSummary(
signature.comment?.getTag("@example")?.content,
)}
/>
))
) : (
<PropDecl
name={
(prop.flags.isStatic ? klass.name : "") +
(prop.name.startsWith("[") ? "" : ".") +
prop.name
}
type={printType(propOrGetSig.type)}
doc={
propOrGetSig.comment &&
renderSummary(propOrGetSig.comment.summary)
}
example={renderSummary(
propOrGetSig.comment?.getTag("@example")?.content,
)}
/>
);
}
function printSimplePropSignature(
prop: DeclarationReflection,
klass: DeclarationReflection,
signature: SignatureReflection,
): string {
return (
`${prop.flags.isStatic ? klass.name : ""}.` +
printSimpleSignature(prop, signature)
);
}
function printSimpleSignature(
item: DeclarationReflection,
signature: SignatureReflection,
) {
return `${item.name}${
signature.typeParameters?.length
? "<" +
signature.typeParameters.map((tParam) => tParam.name).join(", ") +
">"
: ""
}(${printParams(signature)?.join(", ")})`;
}
function printParams(signature: SignatureReflection) {
return (
signature.parameters?.flatMap((param) =>
param.name === "this"
? []
: [
param.name === "__namedParameters" &&
param.type?.type === "reflection"
? "{ " +
param.type.declaration.children
?.map(
(child) =>
child.name +
(child.flags.isOptional ? "?" : ""),
)
.join(", ") +
" }"
: param.name + (param.defaultValue ? "?" : ""),
],
) || []
);
}
function printParamsWithTypes(signature: SignatureReflection) {
return (
signature.parameters?.map(
(param) =>
(param.name === "__namedParameters"
? ""
: param.name + (param.defaultValue ? "?" : "") + ": ") +
printType(param.type),
) || []
);
}
function printTypeParamsWithConstraints(
typeParams: TypeParameterReflection[] | undefined,
): string[] {
return (
typeParams?.flatMap((tParam) =>
tParam.type
? [`${tParam.name} extends ${printType(tParam.type)}`]
: [],
) || []
);
}
function printType(type: SomeType | undefined): string {
if (!type) return "NO TYPE";
if (type.type === "reflection") {
if (type.declaration.kind === ReflectionKind.TypeLiteral) {
if (type.declaration.signatures?.length) {
return (
type.declaration.signatures
?.map(
(sig) =>
`(${printParamsWithTypes(sig).join(
", ",
)}) => ${printType(sig.type)}`,
)
.join(" | ") || ""
);
} else {
return (
"{ " +
type.declaration.children
?.map(
(child) =>
`${child.name}: ${printType(child.type)}`,
)
.join(", ") +
" }"
);
}
}
return "TODO reflection type " + type.declaration.kind;
} else if (type.type === "reference") {
return (
type.name +
(type.typeArguments?.length
? "<" + type.typeArguments.map(printType).join(", ") + ">"
: "")
);
} else if (type.type === "intersection") {
return (
type.types
?.map((part) =>
part.needsParenthesis(TypeContext["intersectionElement"])
? `(${printType(part)})`
: printType(part),
)
.join(" & ") || "NO TYPES"
);
} else if (type.type === "union") {
return (
type.types
.sort((a, b) => (a.type === "intrinsic" ? 1 : -1))
?.map((part) =>
part.needsParenthesis(TypeContext["unionElement"])
? `(${printType(part)})`
: printType(part),
)
.join(" | ") || "NO TYPES"
);
} else if (type.type === "tuple") {
return `[${type.elements.map(printType).join(", ")}]`;
} else if (type.type === "array") {
if (type.needsParenthesis()) {
return `(${printType(type.elementType)})[]`;
} else {
return printType(type.elementType) + "[]";
}
} else if (type.type === "mapped") {
return `{[${type.parameter} in ${printType(
type.parameterType,
)}]: ${printType(type.templateType)}}`;
} else if (type.type === "indexedAccess") {
return `${printType(type.objectType)}[${printType(type.indexType)}]`;
} else if (type.type === "intrinsic") {
return type.name;
} else if (type.type === "predicate") {
return `${type.name} is ${printType(type.targetType)}`;
} else if (type.type === "query") {
return printType(type.queryType);
} else if (type.type === "literal") {
return JSON.stringify(type.value);
} else {
return "TODO type " + type.type;
}
}

View File

@@ -0,0 +1,24 @@
import { readFile } from "fs/promises";
import { ProjectReflection, Deserializer, JSONOutput } from "typedoc";
import JazzToolsDocs from "../../typedoc/jazz-tools.json";
import JazzReactDocs from "../../typedoc/jazz-react.json";
import JazzBrowserDocs from "../../typedoc/jazz-browser.json";
import JazzBrowserMediaImagesDocs from "../../typedoc/jazz-browser-media-images.json";
import JazzNodejsDocs from "../../typedoc/jazz-nodejs.json";
const docs = {
"jazz-tools": JazzToolsDocs as JSONOutput.ProjectReflection,
"jazz-react": JazzReactDocs as JSONOutput.ProjectReflection,
"jazz-browser": JazzBrowserDocs as JSONOutput.ProjectReflection,
"jazz-browser-media-images":
JazzBrowserMediaImagesDocs as JSONOutput.ProjectReflection,
"jazz-nodejs": JazzNodejsDocs as JSONOutput.ProjectReflection,
};
export async function requestProject(
packageName: keyof typeof docs,
): Promise<ProjectReflection> {
const deserializer = new Deserializer({} as any);
return deserializer.reviveProject(docs[packageName], packageName);
}

View File

@@ -0,0 +1,240 @@
import { LinkIcon } from "lucide-react";
import { ReactNode } from "react";
import { getHighlighter } from "shiki";
const highlighter = getHighlighter({
langs: ["typescript", "bash"],
theme: "css-variables", // use the theme
});
export async function Highlight({
children,
hide,
lang = "typescript",
}: {
children: string;
hide?: number[];
lang?: string;
}) {
const lines = (await highlighter).codeToThemedTokens(
children,
lang,
"css-variables",
);
return (
<code>
{lines
.filter((_, i) => !hide?.includes(i))
.map((line, i, all) => (
<>
{line.map((token, i) => (
<span key={i} style={{ color: token.color }}>
{token.content}
</span>
))}
{i !== all.length - 1 && <br />}
</>
))}
</code>
);
}
export function ClassOrInterface({
inPackage,
name,
typeParameters,
children,
doc,
isInterface,
}: {
inPackage: string;
name: string;
typeParameters?: string;
children: ReactNode;
doc: ReactNode;
isInterface?: boolean;
}) {
return (
<div className="relative not-prose">
<a
id={inPackage + "/" + name}
href={"#" + inPackage + "/" + name}
className="absolute md:top-[3.5rem] -ml-6 w-4 flex items-center justify-center opacity-0 peer-group-hover:opacity-100 target:opacity-100"
tabIndex={-1}
>
<LinkIcon size={15} />
</a>
<h4 className="peer sticky top-0 pt-[0.5rem] md:top-[2.5rem] md:pt-[3rem] bg-stone-50 dark:bg-stone-950 z-20">
<a href={"#" + inPackage + "/" + name}>
<Highlight>
{(isInterface ? "interface " : "class ") +
name +
typeParameters}
</Highlight>
</a>
</h4>
<div className="pl-2">
<div className=" mt-4 text-sm">{doc}</div>
<div className="">{children}</div>
</div>
</div>
);
}
export function ClassRef({ name }: { name: string }) {
return <Highlight hide={[0]}>{`class\n${name}`}</Highlight>;
}
export function PropRef({ on, prop }: { on?: string; prop: string }) {
return on ? (
<Highlight>{`${on}.${prop}`}</Highlight>
) : (
<Highlight>{prop}</Highlight>
);
}
export function PropDecl({
name,
type,
doc,
example,
}: {
name?: string;
type?: string;
doc: ReactNode;
example?: ReactNode;
}) {
return (
<div className="py-2 border-t border-stone-200 dark:border-stone-800 mt-4 text-sm">
<div>
{name && <Highlight>{name + ":"}</Highlight>}
{" "}
{type && (
<span className="opacity-75 text-xs pl-1">
<Highlight
hide={[0, 1, 2 + type.split("\n").length]}
>{`class X {\nprop:\n${type}`}</Highlight>
</span>
)}
</div>
<div className="lg:flex gap-2">
<div className="ml-4 mt-2 flex-[3]">
<DocComment>{doc || "⚠️ undocumented"}</DocComment>
</div>
{example && (
<div className="ml-4 lg:ml-0 lg:mt-0 flex-[1] relative w-full overflow-x-scroll col-span-2 pl-4 md:pl-0 md:mt-0 text-xs opacity-60 group-hover:opacity-100">
<div className="opacity-30 text-xs -mb-4">Example:</div>
{example}
</div>
)}
</div>
</div>
);
}
export function FnDecl({
signature,
typeParams,
paramTypes,
returnType,
doc,
example,
}: {
signature: string;
typeParams: string[];
paramTypes: string[];
returnType: string;
doc: ReactNode;
example: ReactNode;
}) {
return (
<div className="py-2 border-t border-stone-200 dark:border-stone-800 mt-4 text-sm">
<div>
{<Highlight>{signature + ":"}</Highlight>}{" "}
<span className="opacity-75 text-xs pl-1">
<Highlight>{returnType}</Highlight>
</span>
</div>
<div className="ml-4 mt-2 text-xs opacity-75 flex flex-col gap-2">
{typeParams.length > 0 && (
<div>
<Highlight
hide={[0, 1 + typeParams.length]}
>{`class Thing<\n${typeParams.join(
",\n",
)}\n]> {}`}</Highlight>
</div>
)}
{paramTypes.length > 0 && (
<div>
<Highlight
hide={[0, 1 + paramTypes.length]}
>{`function fn(...args: [\n${paramTypes.join(
",\n",
)}\n]) {}`}</Highlight>
</div>
)}
</div>
<div className="lg:flex gap-2">
<div className="ml-4 mt-2 flex-[3]">
<DocComment>{doc || "⚠️ undocumented"}</DocComment>
</div>
{example && (
<div className="flex-[1] relative w-full overflow-x-scroll col-span-2 pl-4 md:pl-0 mt-6 md:mt-0 text-xs opacity-60 group-hover:opacity-100">
<div className="opacity-30 text-xs -mb-4">Example:</div>
{example}
</div>
)}
</div>
</div>
);
}
export function PropCategory({
name,
description,
example,
}: {
name: string;
description?: ReactNode;
example?: ReactNode;
}) {
return (
<>
<div className="col-span-6 mt-8 -mb-4 text-[0.7em] uppercase font-medium tracking-widest opacity-50">
{name}
</div>
{description && <PropDecl doc={description} example={example} />}
</>
);
}
export function DocComment({ children }: { children: ReactNode }) {
return (
<div className="prose-inner-sm max-w-xl leading-snug">{children}</div>
);
}
export function NewCoValueExplainer({ type }: { type: string }) {
return (
<>
Creates a new <ClassRef name={type} /> with the given initial
values. The <ClassRef name={type} /> is immediately persisted
locally and synced to connected peers. Access rights are determined
by roles in the owner <ClassRef name="Group" /> or directly by the
owner <ClassRef name="Account" />.
</>
);
}
export function RefValueExplainer({ propName }: { propName: string }) {
return (
<>
Note that even non-optional <PropRef on="co" prop="ref(...)" />{" "}
{propName} might be <Highlight>null</Highlight> if the referenced
value isn&apos;t loaded yet. Accessing one will cause it to be
loaded if done from inside a <i>Subscription Context</i>.
</>
);
}

View File

@@ -0,0 +1,120 @@
export function Slogan(props: { children: ReactNode; small?: boolean }) {
return (
<div
className={[
"leading-snug tracking-tight mb-5 max-w-4xl text-stone-700 dark:text-stone-500",
props.small
? "text-lg lg:text-xl -mt-2"
: "text-3xl lg:text-4xl -mt-5",
].join(" ")}
>
{props.children}
</div>
);
}
export function Grid({
className,
children,
}: {
className?: string;
children: ReactNode;
}) {
return (
<div
className={cn(
"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4",
"mt-10 items-stretch",
className,
)}
>
{children}
</div>
);
}
export function GridItem(props: { children: ReactNode; className?: string }) {
return <div className={props.className || ""}>{props.children}</div>;
}
export function GridFeature(props: {
icon: ReactNode;
children: ReactNode;
className?: string;
}) {
return (
<div
className={[
"p-4 flex flex-col items-center justify-center gap-2",
"not-prose text-base",
"border border-stone-200 dark:border-stone-800 rounded-xl",
props.className || "",
].join(" ")}
>
<div className="text-stone-500 mr-2">{props.icon}</div>
<div className="text-stone-700 dark:text-stone-300">
{props.children}
</div>
</div>
);
}
export function GridCard(props: { children: ReactNode; className?: string }) {
return (
<div
className={[
"col-span-2 p-4 [&>h4]:mt-0 [&>h3]:mt-0 [&>:last-child]:mb-0",
"border border-stone-200 dark:border-stone-800 rounded-xl shadow-sm",
props.className,
].join(" ")}
>
{props.children}
</div>
);
}
export function MultiplayerIcon({
color,
strokeWidth,
size,
}: {
color?: string;
strokeWidth?: number;
size: number;
}) {
return (
<div className="relative z-0" style={{ width: size, height: size }}>
<MousePointer2Icon
size={0.6 * size}
strokeWidth={(strokeWidth || 1) / 0.6}
color={color}
className="absolute top-0 right-0"
/>
<MousePointer2Icon
size={0.5 * size}
strokeWidth={(strokeWidth || 1) / 0.5}
color={color}
className="absolute bottom-0 left-0 -scale-x-100"
/>
</div>
);
}
export function ComingSoonBadge({ when = "soon" }: { when?: string }) {
return (
<span className="bg-stone-100 dark:bg-stone-900 text-stone-500 dark:text-stone-400 border border-stone-300 dark:border-stone-700 text-[0.6rem] px-1 py-0.5 rounded-xl align-text-top">
Coming&nbsp;{when}
</span>
);
}
import { IframeHTMLAttributes, ReactNode } from "react";
import { ResponsiveIframe as ResponsiveIframeClient } from "./ResponsiveIframe";
import { HandIcon, MousePointer2Icon, TextCursorIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export function ResponsiveIframe(
props: IframeHTMLAttributes<HTMLIFrameElement> & { localSrc: string },
) {
return <ResponsiveIframeClient {...props} />;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,412 @@
"use client";
import { cn } from "@/lib/utils";
import { MailIcon, MenuIcon, SearchIcon, XIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ReactNode, useLayoutEffect, useRef, useState } from "react";
import { BreadCrumb } from "./breadcrumb";
import { DocNav } from "./docs/nav";
export function Nav({
mainLogo,
items,
docNav,
}: {
mainLogo: ReactNode;
items: {
href: string;
icon?: ReactNode;
title: string;
firstOnRight?: boolean;
newTab?: boolean;
}[];
docNav: ReactNode;
}) {
const [menuOpen, setMenuOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);
const searchRef = useRef<HTMLInputElement>(null);
useLayoutEffect(() => {
searchOpen && searchRef.current?.focus();
}, [searchOpen]);
const pathname = usePathname();
return (
<>
<nav
className={[
"hidden md:flex sticky left-0 right-0 top-0 max-sm:bottom-0 w-full justify-center",
"bg-stone-50 dark:bg-stone-950 border-b max-sm:border-t border-stone-50 dark:border-b-stone-950",
"max-h-none overflow-hidden transition[max-height] duration-300 ease-in-out",
"z-50",
menuOpen ? "h-[100dvh]" : "h-16",
].join(" ")}
>
<div className="flex flex-wrap px-8 items-center max-sm:justify-between lg:gap-2 max-w-[80rem] w-full">
<div className="flex items-center flex-shrink">
<NavLinkLogo prominent href="/" className="-ml-2">
{mainLogo}
</NavLinkLogo>
</div>
{items.map((item, i) =>
"icon" in item ? (
<NavLinkLogo
key={i}
href={item.href}
newTab={item.newTab}
>
{item.icon}
</NavLinkLogo>
) : (
<NavLink
key={i}
href={item.href}
newTab={item.newTab}
className={cn(
"max-sm:w-full",
item.firstOnRight ? "md:ml-auto" : "",
)}
>
{item.title}
</NavLink>
),
)}
</div>
</nav>
<div className="md:hidden px-4 flex items-center self-stretch dark:text-white">
<NavLinkLogo prominent href="/" className="mr-auto">
{mainLogo}
</NavLinkLogo>
<button
className="flex p-3 rounded-xl items-center"
onMouseDown={() => {
setMenuOpen((o) => !o);
setSearchOpen(false);
}}
>
<MenuIcon className="mr-2" />
<BreadCrumb items={items} />
</button>
</div>
<div
onClick={() => {
setMenuOpen(false);
setSearchOpen(false);
}}
className={cn(
menuOpen || searchOpen ? "block" : "hidden",
"fixed top-0 bottom-0 left-0 right-0 bg-stone-200/80 dark:bg-black/80 w-full h-full z-20",
)}
></div>
<nav
className={cn(
"md:hidden fixed flex flex-col bottom-4 right-4 z-50",
"bg-stone-50 dark:bg-stone-925 dark:text-white border border-stone-100 dark:border-stone-900 dark:outline dark:outline-1 dark:outline-black/60 rounded-lg shadow-lg",
menuOpen || searchOpen ? "left-4" : "",
)}
>
<div
className={cn(menuOpen ? "block" : "hidden", " px-2 pb-2")}
>
<div className="flex items-center w-full border-b border-stone-100 dark:border-stone-900">
<NavLinkLogo
prominent
href="/"
className="mr-auto"
onClick={() => setMenuOpen(false)}
>
{mainLogo}
</NavLinkLogo>
{items
.filter((item) => "icon" in item)
.map((item, i) => (
<NavLinkLogo
key={i}
href={item.href}
newTab={item.newTab}
>
{item.icon}
</NavLinkLogo>
))}
</div>
{pathname === "/docs" && (
<div className="max-h-[calc(100dvh-15rem)] p-4 border-b border-stone-100 dark:border-stone-900 overflow-x-auto prose-sm prose-ul:pl-1 prose-ul:ml-1 prose-li:my-2 prose-li:leading-tight prose-ul:list-['-']">
{docNav}
</div>
)}
<div className="flex gap-4 justify-end -mb-2">
{items
.filter((item) => !("icon" in item))
.slice(0, 3)
.map((item, i) => (
<>
<NavLink
key={i}
href={item.href}
onClick={() => setMenuOpen(false)}
newTab={item.newTab}
>
{item.title}
</NavLink>
</>
))}
</div>
<div className="flex gap-4 justify-end border-b border-stone-100 dark:border-stone-900">
{items
.filter((item) => !("icon" in item))
.slice(3)
.map((item, i) => (
<NavLink
key={i}
href={item.href}
onClick={() => setMenuOpen(false)}
newTab={item.newTab}
className={cn("")}
>
{item.title}
</NavLink>
))}
</div>
</div>
<div className="flex items-center self-stretch justify-end">
{/* <input
type="text"
className={cn(
menuOpen || searchOpen ? "" : "hidden",
"ml-2 border border-stone-200 dark:border-stone-900 px-2 py-1 rounded w-full"
)}
placeholder="Search docs..."
ref={searchRef}
/> */}
{/* <button
className="flex p-3 rounded-xl"
onClick={() => {
setSearchOpen(true);
}}
onBlur={(e) => {
if (!e.currentTarget.value) {
setSearchOpen(false);
}
}}
>
<SearchIcon className="" />
</button> */}
<button
className="flex p-3 rounded-xl items-center"
onMouseDown={() => {
setMenuOpen((o) => !o);
setSearchOpen(false);
}}
>
{menuOpen || searchOpen ? (
<XIcon />
) : (
<>
<MenuIcon className="mr-2" />
<BreadCrumb items={items} />
</>
)}
</button>
</div>
</nav>
</>
);
}
export function NavLink({
href,
className,
children,
onClick,
newTab,
}: {
href: string;
className?: string;
children: ReactNode;
onClick?: () => void;
newTab?: boolean;
}) {
const path = usePathname();
return (
<Link
href={href}
className={cn(
"px-2 lg:px-4 py-3 text-sm",
className,
path === href
? "font-medium text-black dark:text-white cursor-default"
: "text-stone-600 dark:text-stone-400 hover:text-black dark:hover:text-white transition-colors hover:transition-none",
)}
onClick={onClick}
target={newTab ? "_blank" : undefined}
>
{children}
{newTab ? (
<span className="inline-block text-stone-300 dark:text-stone-700 relative -top-0.5 -left-0.5 -mr-2">
</span>
) : (
""
)}
</Link>
);
}
export function NavLinkLogo({
href,
className,
children,
prominent,
onClick,
newTab,
}: {
href: string;
className?: string;
children: ReactNode;
prominent?: boolean;
onClick?: () => void;
newTab?: boolean;
}) {
const path = usePathname();
return (
<Link
href={href}
className={cn(
"max-sm:px-4 px-2 lg:px-3 py-3 transition-opacity hover:transition-none",
path === href
? "cursor-default"
: prominent
? "hover:opacity-50"
: "opacity-60 hover:opacity-100",
"text-black dark:text-white",
className,
)}
onClick={onClick}
target={newTab ? "_blank" : undefined}
>
{children}
</Link>
);
}
export function NewsletterButton() {
return (
<button
onClick={() =>
(window as any).ml_account(
"webforms",
"5744530",
"p5o0j8",
"show",
)
}
className="flex px-2 py-1 rounded gap-2 items-center bg-stone-300 hover:bg-stone-200 dark:bg-stone-950 dark:hover:bg-stone-800 text-black dark:text-white"
>
<MailIcon className="" size="14" /> Subscribe
</button>
);
}
{
/* <input
type="email"
autoComplete="email"
placeholder="you@example.com"
className="max-w-[14rem] border border-stone-200 dark:border-stone-900 px-2 py-1 rounded w-full"
/> */
}
export function Newsletter() {
return (
<>
<div
id="mlb2-5744530"
className="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-5744530"
>
<form
className="flex gap-2"
action="https://static.mailerlite.com/webforms/submit/p5o0j8"
data-code="p5o0j8"
method="post"
target="_blank"
>
<input
aria-label="email"
aria-required="true"
type="email"
className="text-base form-control max-w-[18rem] border border-stone-300 dark:border-transparent shadow-sm dark:bg-stone-925 px-2 py-1 rounded w-full"
data-inputmask=""
name="fields[email]"
placeholder="Email"
autoComplete="email"
/>
<input
type="checkbox"
className="hidden"
name="groups[]"
value="112132481"
checked
readOnly
/>
<input
type="checkbox"
className="hidden"
name="groups[]"
value="111453104"
/>
<input type="hidden" name="ml-submit" value="1" />
<button
type="submit"
className="flex px-3 py-1 rounded gap-2 items-center shadow-sm bg-stone-925 dark:bg-black hover:bg-stone-800 text-white"
>
<MailIcon className="" size="14" /> Subscribe
</button>
<input type="hidden" name="anticsrf" value="true" />
</form>
<div
className="ml-form-successBody row-success"
style={{ display: "none" }}
>
<div className="ml-form-successContent">
<p>You&apos;re subscribed 🎉</p>
</div>
</div>
</div>
<script
dangerouslySetInnerHTML={{
__html: `
function ml_webform_success_5744530(){var r=ml_jQuery||jQuery;r(".ml-subscribe-form-5744530 .row-success").show(),r(".ml-subscribe-form-5744530 .row-form").hide()}
`,
}}
/>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="https://track.mailerlite.com/webforms/o/5744530/p5o0j8?v1697487427"
width="1"
height="1"
style={{
maxWidth: "1px",
maxHeight: "1px",
visibility: "hidden",
padding: 0,
margin: 0,
display: "block",
}}
alt="."
/>
<script
src="https://static.mailerlite.com/js/w/webforms.min.js?vd4de52e171e8eb9c47c0c20caf367ddf"
type="text/javascript"
defer
></script>
</>
);
}

View File

@@ -0,0 +1,9 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

Binary file not shown.

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