Compare commits

..

103 Commits

Author SHA1 Message Date
Anselm
fc88dbbe2d Clean up homepage structure, particularly around area-specific components 2025-08-07 12:09:01 -07:00
Guido D'Orsi
165a6170cd Merge pull request #2700 from garden-co/changeset-release/main
Version Packages
2025-08-04 21:13:54 +02:00
github-actions[bot]
5148419df9 Version Packages 2025-08-04 19:11:13 +00:00
Guido D'Orsi
fc0ecb0968 chore: changeset 2025-08-04 21:07:48 +02:00
Guido D'Orsi
802b5a3060 chore: changeset 2025-08-04 21:06:23 +02:00
Guido D'Orsi
e47af262b3 Merge pull request #2673 from garden-co/feat/storage-wal
fix: ensure that transactions are synced in the correct order
2025-08-04 20:54:53 +02:00
Guido D'Orsi
e98b610fd0 Merge pull request #2698 from garden-co/feat/comap-pick-and-partial
feat: Add `.pick()` and `.partial()` methods to CoMapSchema
2025-08-04 14:53:38 +02:00
Guido D'Orsi
b554983558 Merge pull request #2699 from garden-co/fix/extend-circular-check
fix: fixes error when extending a group without having child groups loaded
2025-08-04 14:53:15 +02:00
Guido D'Orsi
d95dcbe7db fix: align pick to the Zod API 2025-08-04 13:24:44 +02:00
Guido D'Orsi
f9d538f049 fix: fixes error when extending a group without having child groups loaded 2025-08-04 12:37:53 +02:00
Guido D'Orsi
93e68c62f5 docs: fix a missing type alias 2025-08-04 10:52:06 +02:00
Guido D'Orsi
dadee9dcc5 test: fix flaky test 2025-08-04 10:40:03 +02:00
Guido D'Orsi
6724c4bd83 feat: add docs, remove lodash-es dependency and add tests for recursive types with pick and partial 2025-08-04 10:34:43 +02:00
NicoR
1942bd5de4 Replace lodash with lodash-es 2025-08-04 01:44:27 -03:00
NicoR
16764f6365 Add changeset 2025-08-04 01:23:00 -03:00
NicoR
b56cfc2e1f Add TS docs 2025-08-04 01:21:32 -03:00
NicoR
7091bcf9c0 Add CoMapSchema.partial 2025-08-04 01:17:25 -03:00
NicoR
436cbfa095 Add CoMapSchema.pick 2025-08-04 01:00:57 -03:00
Guido D'Orsi
104e664bbb fix: fix build errors on music player 2025-08-03 17:20:15 +02:00
Guido D'Orsi
f199b451eb chore: use inline JSON when creating covalues 2025-08-03 17:09:02 +02:00
Guido D'Orsi
70bc48458e Merge pull request #2695 from garden-co/feat/music-player-refresh
docs: exclude upgrade guides from llm.txt
2025-08-02 14:37:56 +02:00
Guido D'Orsi
f28b2a6135 docs: exclude upgrade guides 2025-08-02 14:36:55 +02:00
Guido D'Orsi
55b770b7c9 Merge pull request #2694 from garden-co/feat/music-player-refresh
feat: improve the music player UI
2025-08-02 14:29:35 +02:00
Guido D'Orsi
e6838dfb98 feat: make the music-player a PWA 2025-08-02 14:22:36 +02:00
Guido D'Orsi
5e34061fdc feat: improve the music player UI 2025-08-02 14:19:24 +02:00
Guido D'Orsi
acecffaeb2 test: fix flaky tests on the created and update time 2025-08-01 19:57:33 +02:00
Nico Rainhart
0a98d6aaf2 Merge pull request #2691 from garden-co/changeset-release/main
Version Packages
2025-08-01 12:57:33 -03:00
github-actions[bot]
4ea1a63a0a Version Packages 2025-08-01 15:47:51 +00:00
Nico Rainhart
41a4c3bc95 Merge pull request #2683 from garden-co/feat/json-create-and-set
feat: create CoValues using plain JSON objects
2025-08-01 12:45:30 -03:00
Guido D'Orsi
60d0027f9d Merge pull request #2690 from garden-co/docs/useCoState-jsDoc
docs: adds jsDocs to useCoState and useAccount react hooks
2025-08-01 16:12:39 +02:00
Guido D'Orsi
748c2ff751 Merge pull request #2688 from garden-co/changeset-release/main
Version Packages
2025-08-01 15:46:41 +02:00
github-actions[bot]
70938b0ab3 Version Packages 2025-08-01 13:42:25 +00:00
Guido D'Orsi
f2f5b55dbf Update packages/jazz-tools/src/react-core/hooks.ts
Co-authored-by: Nico Rainhart <nmrainhart@gmail.com>
2025-08-01 15:42:24 +02:00
Guido D'Orsi
3c3acae803 Merge pull request #2689 from joeinnes/2687-fix-broken-link
Fix HTTP API link in inbox.mdx. Fixes #2687
2025-08-01 15:40:07 +02:00
Guido D'Orsi
896ee3460f docs: adds jsDocs to useCoState and useAccount react hooks 2025-08-01 15:37:02 +02:00
Joe Innes
9b9bf44e2b Fix HTTP API link in inbox.mdx. Fixes #2687 2025-08-01 15:13:33 +02:00
Guido D'Orsi
392aa88d95 Merge pull request #2655 from joeinnes/docs/optional-references
Docs/optional references
2025-08-01 13:15:20 +02:00
Joe Innes
7ce82cd934 Merge branch 'main' into docs/optional-references 2025-08-01 13:13:26 +02:00
Guido D'Orsi
0c8158b91c Merge pull request #2676 from Gabrola/fix/jazz-run-exports
fix: jazz-run package.json exports
2025-08-01 13:07:30 +02:00
Guido D'Orsi
5a48c9c44c chore: improve tests titles and add comments 2025-08-01 10:14:24 +02:00
Guido D'Orsi
25c56146f5 Merge pull request #2686 from garden-co/test/logout-state
test: logout integration tests on browser
2025-08-01 09:57:18 +02:00
NicoR
c564fbb02e test: add permission tests for creating nested CoValues from JSON 2025-07-31 15:03:25 -03:00
Guido D'Orsi
12481e14c2 test: logout integration tests on browser 2025-07-31 18:56:37 +02:00
NicoR
fd2d247ff5 docs: improve examples 2025-07-31 13:08:50 -03:00
NicoR
9e9ea029b2 fix: move alert out of CodeGroup 2025-07-30 17:32:56 -03:00
NicoR
a0da272dcd fix: add missing import in docs 2025-07-30 16:51:23 -03:00
NicoR
72fbcc3262 chore: remove unnecessary import from form example 2025-07-30 15:52:07 -03:00
NicoR
f4c8cc858b docs: add sections for creating CoValues from JSON and permissions 2025-07-30 15:48:55 -03:00
Anselm
0ab4d7a20d Update meta description 2025-07-30 11:34:54 -07:00
NicoR
4cbda689c4 refactor: update form example to use new API 2025-07-30 14:54:08 -03:00
NicoR
771b0ed914 fix: cannot create empty plain text when nested 2025-07-30 14:53:38 -03:00
NicoR
79913c3136 fix: simplify CoMapInit schema 2025-07-30 13:37:10 -03:00
NicoR
43d3511d15 Add changeset 2025-07-30 12:40:25 -03:00
NicoR
928ef14086 feat: support deeply nested optional primitive fields 2025-07-30 12:33:28 -03:00
NicoR
048dd7def0 feat: support deeply nested optional CoValue fields 2025-07-30 12:33:09 -03:00
Guido D'Orsi
51fcb8a44b test: improve the client subscription test 2025-07-30 17:29:01 +02:00
Guido D'Orsi
c5888c39f5 perf: update parent before updating children to favor batching 2025-07-30 17:14:45 +02:00
Guido D'Orsi
2defcfae67 test: mark retry unavailable states as flaky 2025-07-30 17:10:50 +02:00
NicoR
873b146d15 feat: create a child group for each created CoValue 2025-07-30 11:03:38 -03:00
Guido D'Orsi
213de11c3b feat: preserve transaction order on sync 2025-07-30 15:37:58 +02:00
Guido D'Orsi
af295d816a chore: add comments and rename CoValueSyncQueue in LocalTransactionsSyncQueue 2025-07-30 12:54:46 +02:00
Guido D'Orsi
fe8d3497c0 chore: fix the peer attribution on storage corrections tests 2025-07-30 12:37:19 +02:00
Guido D'Orsi
ba9ad295b6 fix: don't consider -1 as a valid signature checkpoint 2025-07-30 12:32:07 +02:00
NicoR
ab1798c7bd feat: make CoValue creation from JSON type safe 2025-07-29 16:41:48 -03:00
NicoR
26ae69a242 refactor: reuse TypeOfZodSchema 2025-07-29 12:44:42 -03:00
NicoR
21ad3767b9 refactor: avoid InstanceOrPrimitiveOfSchemaCoValuesNullable duplication 2025-07-29 11:48:46 -03:00
NicoR
a9383516c1 refactor: avoid InstanceOrPrimitiveOfSchema duplication 2025-07-29 11:38:09 -03:00
NicoR
bffc516c68 refactor: extract TypeOfZodSchema util 2025-07-29 11:16:14 -03:00
NicoR
9e7c0d9887 chore: clean up instantiateRefEncodedWithInit's implementation 2025-07-29 10:56:32 -03:00
NicoR
99b44d5780 feat: create CoMap with JSON discriminated union fields 2025-07-29 10:36:33 -03:00
NicoR
02db5f3b1d feat: create CoMap with JSON CoFeed fields 2025-07-29 09:13:01 -03:00
NicoR
1949a5fcd9 feat: create CoMap with JSON CoList fields 2025-07-28 17:15:43 -03:00
NicoR
dcd3b022cc feat: create CoMap with JSON plain and rich text fields 2025-07-28 16:56:43 -03:00
NicoR
a7b837c7e1 feat: create CoMap with JSON CoMap fields 2025-07-28 16:47:51 -03:00
Anselm Eickhoff
88ebcf58ab Merge pull request #2680 from garden-co/jazz-as-a-db
Jazz as a DB narrative MVP
2025-07-28 11:34:01 -07:00
Guido D'Orsi
b173e0884a feat: improve local transactions streaming calculation 2025-07-28 19:45:31 +02:00
Anselm
f379a168be Update garden co slogan 2025-07-28 10:30:18 -07:00
Anselm
bde6ac7d45 Jazz as a DB narrative MVP 2025-07-28 10:08:56 -07:00
Guido D'Orsi
231947c97a fix(sync): start a new content message when the size exceeds the recommended value 2025-07-28 18:44:13 +02:00
Guido D'Orsi
d1609cdd55 Merge pull request #2679 from garden-co/changeset-release/main
Version Packages
2025-07-28 18:12:46 +02:00
Guido D'Orsi
d5b57ad1fc fix: fix priority for content 2025-07-28 17:53:33 +02:00
github-actions[bot]
b71ab3168a Version Packages 2025-07-28 15:15:40 +00:00
Nico Rainhart
0c8f6e5039 Merge pull request #2677 from garden-co/feat/add-nullable-support
feat: Add support for nullable non-collaborative fields
2025-07-28 12:12:12 -03:00
Guido D'Orsi
0bf5c53bec fix: disable code coverage check on CI 2025-07-28 16:59:02 +02:00
Guido D'Orsi
e7b1550003 feat: perserve insert order when storing transactions on multiple covalues 2025-07-28 16:59:02 +02:00
NicoR
6a93a1b8a3 chore: add comment on why nullable date cofields are not supported 2025-07-28 11:52:44 -03:00
NicoR
9f654a2603 test: loading a map with a nullable field 2025-07-28 11:46:50 -03:00
NicoR
dbf735d9e1 Fix rebase error 2025-07-28 10:27:44 -03:00
NicoR
c62abefb66 Add changeset 2025-07-28 10:19:55 -03:00
NicoR
1453869a46 Add support for nullable non-collaborative fields 2025-07-28 10:18:39 -03:00
Youssef Gaber
239da90c9f chore: changeset 2025-07-28 17:06:01 +04:00
Youssef Gaber
972791e7a8 fix: correct jazz-run package.json exports 2025-07-28 17:05:45 +04:00
NicoR
0c0178764e chore: fix rebase errors 2025-07-28 09:34:24 -03:00
NicoR
928350b821 refactor: rename OptionalizeUndefinedKeys to PartialOnUndefined 2025-07-28 09:34:23 -03:00
NicoR
be3fd9c696 test: create CoMap with shallowly resolved CoValue 2025-07-28 09:34:23 -03:00
NicoR
269c028df0 test: add test for CoMapSchema + catchall 2025-07-28 09:34:23 -03:00
NicoR
e4df837138 refactor: rename CoMapInitZod to CoMapSchemaInit 2025-07-28 09:34:23 -03:00
NicoR
54fe6d93ba refactor: extract CoMapSchema.create's return type 2025-07-28 09:34:23 -03:00
NicoR
979689c6d8 refactor: improve CatchAll type handling in CoMapSchemas 2025-07-28 09:34:23 -03:00
NicoR
859a37868f refactor: simplify CoMapSchema.create's return type 2025-07-28 09:34:23 -03:00
NicoR
57bd32d77e refactor: simplify the type of CoMapSchema.create's init parameter 2025-07-28 09:34:23 -03:00
Joe Innes
6b662b0efe Type fixes for twoslash 2025-07-18 15:06:03 +02:00
Joe Innes
a8b3ec7bb0 Add more detail regarding optional references
As the boundary becomes more defined between CoValue schemas and Zod schemas, we need to ensure folks pick the right `.optional()` between `co.optional()` for CoValues and `z.optional()` for primitives.
2025-07-17 20:24:03 +02:00
189 changed files with 5734 additions and 1366 deletions

View File

@@ -1,5 +1,32 @@
# passkey-svelte
## 0.0.110
### Patch Changes
- Updated dependencies [16764f6]
- jazz-tools@0.16.4
## 0.0.109
### Patch Changes
- Updated dependencies [43d3511]
- jazz-tools@0.16.3
## 0.0.108
### Patch Changes
- jazz-tools@0.16.2
## 0.0.107
### Patch Changes
- Updated dependencies [c62abef]
- jazz-tools@0.16.1
## 0.0.106
### Patch Changes

View File

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

View File

@@ -1,5 +1,4 @@
import { useIframeHashRouter } from "hash-slash";
import { Loaded } from "jazz-tools";
import { useAccount, useCoState } from "jazz-tools/react";
import { useState } from "react";
import { Errors } from "./Errors.tsx";
@@ -9,7 +8,6 @@ import {
BubbleTeaOrder,
DraftBubbleTeaOrder,
JazzAccount,
ListOfBubbleTeaAddOns,
validateDraftOrder,
} from "./schema.ts";
@@ -22,7 +20,7 @@ export function CreateOrder() {
if (!me?.root) return;
const onSave = (draft: Loaded<typeof DraftBubbleTeaOrder>) => {
const onSave = (draft: DraftBubbleTeaOrder) => {
const validation = validateDraftOrder(draft);
setErrors(validation.errors);
if (validation.errors.length > 0) {
@@ -30,11 +28,11 @@ export function CreateOrder() {
}
// turn the draft into a real order
me.root.orders.push(draft as Loaded<typeof BubbleTeaOrder>);
me.root.orders.push(draft as BubbleTeaOrder);
// reset the draft
me.root.draft = DraftBubbleTeaOrder.create({
addOns: ListOfBubbleTeaAddOns.create([]),
addOns: [],
});
router.navigate("/");
@@ -60,7 +58,7 @@ function CreateOrderForm({
onSave,
}: {
id: string;
onSave: (draft: Loaded<typeof DraftBubbleTeaOrder>) => void;
onSave: (draft: DraftBubbleTeaOrder) => void;
}) {
const draft = useCoState(DraftBubbleTeaOrder, id, {
resolve: { addOns: true, instructions: true },

View File

@@ -1,4 +1,4 @@
import { CoPlainText, Loaded } from "jazz-tools";
import { CoPlainText } from "jazz-tools";
import {
BubbleTeaAddOnTypes,
BubbleTeaBaseTeaTypes,
@@ -10,7 +10,7 @@ export function OrderForm({
order,
onSave,
}: {
order: Loaded<typeof BubbleTeaOrder> | Loaded<typeof DraftBubbleTeaOrder>;
order: BubbleTeaOrder | DraftBubbleTeaOrder;
onSave?: (e: React.FormEvent<HTMLFormElement>) => void;
}) {
// Handles updates to the instructions field of the order.

View File

@@ -1,10 +1,9 @@
import { Loaded } from "jazz-tools";
import { BubbleTeaOrder } from "./schema.ts";
export function OrderThumbnail({
order,
}: {
order: Loaded<typeof BubbleTeaOrder>;
order: BubbleTeaOrder;
}) {
const { id, baseTea, addOns, instructions, deliveryDate, withMilk } = order;
const date = deliveryDate.toLocaleDateString();

View File

@@ -1,4 +1,4 @@
import { Loaded, co, z } from "jazz-tools";
import { co, z } from "jazz-tools";
export const BubbleTeaAddOnTypes = [
"Pearl",
@@ -18,8 +18,9 @@ export const BubbleTeaBaseTeaTypes = [
export const ListOfBubbleTeaAddOns = co.list(
z.literal([...BubbleTeaAddOnTypes]),
);
export type ListOfBubbleTeaAddOns = co.loaded<typeof ListOfBubbleTeaAddOns>;
function hasAddOnsChanges(list?: Loaded<typeof ListOfBubbleTeaAddOns> | null) {
function hasAddOnsChanges(list?: ListOfBubbleTeaAddOns | null) {
return list && Object.entries(list._raw.insertions).length > 0;
}
@@ -30,16 +31,12 @@ export const BubbleTeaOrder = co.map({
withMilk: z.boolean(),
instructions: co.optional(co.plainText()),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export const DraftBubbleTeaOrder = co.map({
baseTea: z.optional(z.literal([...BubbleTeaBaseTeaTypes])),
addOns: co.optional(ListOfBubbleTeaAddOns),
deliveryDate: z.optional(z.date()),
withMilk: z.optional(z.boolean()),
instructions: co.optional(co.plainText()),
});
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
export function validateDraftOrder(order: Loaded<typeof DraftBubbleTeaOrder>) {
export function validateDraftOrder(order: DraftBubbleTeaOrder) {
const errors: string[] = [];
if (!order.baseTea) {
@@ -52,7 +49,7 @@ export function validateDraftOrder(order: Loaded<typeof DraftBubbleTeaOrder>) {
return { errors };
}
export function hasChanges(order?: Loaded<typeof DraftBubbleTeaOrder> | null) {
export function hasChanges(order?: DraftBubbleTeaOrder | null) {
return (
!!order &&
(Object.keys(order._edits).length > 1 || hasAddOnsChanges(order.addOns))
@@ -73,15 +70,9 @@ export const JazzAccount = co
})
.withMigration((account) => {
if (!account.root) {
const orders = co.list(BubbleTeaOrder).create([], account);
const draft = DraftBubbleTeaOrder.create(
{
addOns: ListOfBubbleTeaAddOns.create([], account),
instructions: co.plainText().create("", account),
},
account.root = AccountRoot.create(
{ draft: { addOns: [], instructions: "" }, orders: [] },
account,
);
account.root = AccountRoot.create({ draft, orders }, account);
}
});

View File

@@ -38,6 +38,7 @@
"postcss": "^8.4.27",
"tailwindcss": "^4.1.10",
"typescript": "5.6.2",
"vite": "^6.3.5"
"vite": "^6.3.5",
"vite-plugin-pwa": "^1.0.2"
}
}

View File

@@ -84,15 +84,14 @@ export const MusicaAccount = co
* You can use it to set up the account root and any other initial CoValues you need.
*/
if (account.root === undefined) {
const tracks = co.list(MusicTrack).create([]);
const rootPlaylist = Playlist.create({
tracks,
tracks: [],
title: "",
});
account.root = MusicaAccountRoot.create({
rootPlaylist,
playlists: co.list(Playlist).create([]),
playlists: [],
activeTrack: undefined,
activePlaylist: rootPlaylist,
exampleDataLoaded: false,

View File

@@ -11,6 +11,7 @@ import { uploadMusicTracks } from "./4_actions";
import { MediaPlayer } from "./5_useMediaPlayer";
import { FileUploadButton } from "./components/FileUploadButton";
import { MusicTrackRow } from "./components/MusicTrackRow";
import { PlayerControls } from "./components/PlayerControls";
import { PlaylistTitleInput } from "./components/PlaylistTitleInput";
import { SidePanel } from "./components/SidePanel";
import { Button } from "./components/ui/button";
@@ -42,7 +43,11 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
const playlistId = params.playlistId ?? me?.root._refs.rootPlaylist.id;
const playlist = useCoState(Playlist, playlistId, {
resolve: { tracks: true },
resolve: {
tracks: {
$each: true,
},
},
});
const isRootPlaylist = !params.playlistId;
@@ -66,8 +71,8 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
return (
<SidebarInset className="flex flex-col h-screen text-gray-800 bg-blue-50">
<div className="flex flex-1 overflow-hidden">
<SidePanel mediaPlayer={mediaPlayer} />
<main className="flex-1 p-6 overflow-y-auto overflow-x-hidden">
<SidePanel />
<main className="flex-1 p-6 overflow-y-auto overflow-x-hidden relative">
<SidebarTrigger />
<div className="flex items-center justify-between mb-6">
{isRootPlaylist ? (
@@ -106,12 +111,12 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
onClick={() => {
mediaPlayer.setActiveTrack(track, playlist);
}}
showAddToPlaylist={isRootPlaylist}
/>
),
)}
</ul>
</main>
<PlayerControls mediaPlayer={mediaPlayer} />
</div>
</SidebarInset>
);

View File

@@ -1,11 +1,6 @@
import { getAudioFileData } from "@/lib/audio/getAudioFileData";
import { FileStream, Group, co } from "jazz-tools";
import {
MusicTrack,
MusicTrackWaveform,
MusicaAccount,
Playlist,
} from "./1_schema";
import { FileStream, Group } from "jazz-tools";
import { MusicTrack, MusicaAccount, Playlist } from "./1_schema";
/**
* Walkthrough: Actions
@@ -51,7 +46,7 @@ export async function uploadMusicTracks(
{
file: fileStream,
duration: data.duration,
waveform: MusicTrackWaveform.create({ data: data.waveform }, group),
waveform: { data: data.waveform },
title: file.name,
isExampleTrack,
},
@@ -73,18 +68,10 @@ export async function createNewPlaylist() {
},
});
// Since playlists are meant to be shared we associate them
// to a group which will contain the keys required to get
// access to the "owned" values
const playlistGroup = Group.create();
const playlist = Playlist.create(
{
title: "New Playlist",
tracks: co.list(MusicTrack).create([], playlistGroup),
},
playlistGroup,
);
const playlist = Playlist.create({
title: "New Playlist",
tracks: [],
});
// Again, we associate the new playlist to the
// user by pushing it into the playlists CoList

View File

@@ -0,0 +1,59 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
interface ConfirmDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: string;
confirmText?: string;
cancelText?: string;
onConfirm: () => void;
variant?: "default" | "destructive";
}
export function ConfirmDialog({
isOpen,
onOpenChange,
title,
description,
confirmText = "Confirm",
cancelText = "Cancel",
onConfirm,
variant = "destructive",
}: ConfirmDialogProps) {
function handleConfirm() {
onConfirm();
onOpenChange(false);
}
function handleCancel() {
onOpenChange(false);
}
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
{cancelText}
</Button>
<Button variant={variant} onClick={handleConfirm}>
{confirmText}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -10,27 +10,34 @@ import { cn } from "@/lib/utils";
import { Loaded } from "jazz-tools";
import { useAccount, useCoState } from "jazz-tools/react";
import { MoreHorizontal } from "lucide-react";
import { Fragment } from "react/jsx-runtime";
import { MusicTrackTitleInput } from "./MusicTrackTitleInput";
import { Fragment, useCallback, useState } from "react";
import { EditTrackDialog } from "./RenameTrackDialog";
import { Button } from "./ui/button";
function isPartOfThePlaylist(
trackId: string,
playlist: Loaded<typeof Playlist, { tracks: true }>,
) {
return Array.from(playlist.tracks._refs).some((t) => t.id === trackId);
}
export function MusicTrackRow({
trackId,
isLoading,
isPlaying,
onClick,
showAddToPlaylist,
}: {
trackId: string;
isLoading: boolean;
isPlaying: boolean;
onClick: (track: Loaded<typeof MusicTrack>) => void;
showAddToPlaylist: boolean;
}) {
const track = useCoState(MusicTrack, trackId);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { me } = useAccount(MusicaAccount, {
resolve: { root: { playlists: { $each: true } } },
resolve: { root: { playlists: { $each: { tracks: true } } } },
});
const playlists = me?.root.playlists ?? [];
@@ -60,10 +67,18 @@ export function MusicTrackRow({
}
}
function handleEdit() {
setIsEditDialogOpen(true);
}
const handleContextMenu = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsDropdownOpen(true);
}, []);
return (
<li
className={"flex gap-1 hover:bg-slate-200 group py-2 px-2 cursor-pointer"}
onClick={handleTrackClick}
>
<button
className={cn(
@@ -81,50 +96,57 @@ export function MusicTrackRow({
"▶️"
)}
</button>
<MusicTrackTitleInput trackId={trackId} />
<button
onContextMenu={handleContextMenu}
onClick={handleTrackClick}
className="w-full flex items-center overflow-hidden text-ellipsis whitespace-nowrap"
>
{track?.title}
</button>
<div onClick={(evt) => evt.stopPropagation()}>
{showAddToPlaylist && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
aria-label={`Open ${track?.title} menu`}
>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
key={`delete`}
onSelect={async () => {
if (!track) return;
deleteTrack();
}}
>
Delete
</DropdownMenuItem>
{playlists.map((playlist, index) => (
<Fragment key={index}>
<DropdownMenuItem
key={`add-${index}`}
onSelect={() => handleAddToPlaylist(playlist)}
>
Add to {playlist.title}
</DropdownMenuItem>
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
aria-label={`Open ${track?.title} menu`}
>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={handleEdit}>Edit</DropdownMenuItem>
{playlists.map((playlist, index) => (
<Fragment key={index}>
{isPartOfThePlaylist(trackId, playlist) ? (
<DropdownMenuItem
key={`remove-${index}`}
onSelect={() => handleRemoveFromPlaylist(playlist)}
>
Remove from {playlist.title}
</DropdownMenuItem>
</Fragment>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
) : (
<DropdownMenuItem
key={`add-${index}`}
onSelect={() => handleAddToPlaylist(playlist)}
>
Add to {playlist.title}
</DropdownMenuItem>
)}
</Fragment>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
{track && isEditDialogOpen && (
<EditTrackDialog
track={track}
isOpen={isEditDialogOpen}
onOpenChange={setIsEditDialogOpen}
onDelete={deleteTrack}
/>
)}
</li>
);
}

View File

@@ -24,25 +24,25 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
const activeTrackTitle = activeTrack.title;
return (
<footer className="flex items-center justify-between p-4 gap-4 bg-white border-t border-gray-200 fixed bottom-0 left-0 right-0 w-full">
<div className="flex justify-center items-center space-x-2">
<div className="flex items-center space-x-4">
<footer className="flex items-center justify-between p-2 sm:p-4 gap-2 sm:gap-4 bg-white border-t border-gray-200 absolute bottom-0 left-0 right-0 w-full z-50">
<div className="flex justify-center items-center space-x-1 sm:space-x-2 flex-shrink-0">
<div className="flex items-center space-x-2 sm:space-x-4">
<button
onClick={mediaPlayer.playPrevTrack}
className="text-blue-600 hover:text-blue-800"
aria-label="Previous track"
>
<SkipBack size={20} />
<SkipBack size={16} className="sm:w-5 sm:h-5" />
</button>
<button
onClick={playState.toggle}
className="w-[42px] h-[42px] flex items-center justify-center bg-blue-600 rounded-full text-white hover:bg-blue-700"
className="w-8 h-8 sm:w-[42px] sm:h-[42px] flex items-center justify-center bg-blue-600 rounded-full text-white hover:bg-blue-700"
aria-label={isPlaying ? "Pause active track" : "Play active track"}
>
{isPlaying ? (
<Pause size={24} fill="currentColor" />
<Pause size={16} className="sm:w-6 sm:h-6" fill="currentColor" />
) : (
<Play size={24} fill="currentColor" />
<Play size={16} className="sm:w-6 sm:h-6" fill="currentColor" />
)}
</button>
<button
@@ -50,16 +50,22 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
className="text-blue-600 hover:text-blue-800"
aria-label="Next track"
>
<SkipForward size={20} />
<SkipForward size={16} className="sm:w-5 sm:h-5" />
</button>
</div>
</div>
<div className=" sm:hidden md:flex flex-col flex-shrink-1 items-center w-[75%]">
<Waveform track={activeTrack} height={30} />
<div className="md:hidden sm:hidden lg:flex flex-1 justify-center items-center min-w-0 px-2">
<Waveform
track={activeTrack}
height={30}
className="h-5 sm:h-6 md:h-8 lg:h-10"
/>
</div>
<div className="flex flex-col items-end gap-1 text-right min-w-fit w-[25%]">
<h4 className="font-medium text-blue-800">{activeTrackTitle}</h4>
<p className="text-sm text-gray-600">
<div className="flex flex-col items-end gap-1 text-right min-w-fit flex-shrink-0">
<h4 className="font-medium text-blue-800 text-sm sm:text-base truncate max-w-32 sm:max-w-80">
{activeTrackTitle}
</h4>
<p className="text-xs sm:text-sm text-gray-600 truncate max-w-32 sm:max-w-80">
{activePlaylist?.title || "All tracks"}
</p>
</div>

View File

@@ -0,0 +1,108 @@
import { MusicTrack } from "@/1_schema";
import { updateMusicTrackTitle } from "@/4_actions";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Loaded } from "jazz-tools";
import { useState } from "react";
import { ConfirmDialog } from "./ConfirmDialog";
interface EditTrackDialogProps {
track: Loaded<typeof MusicTrack>;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onDelete: () => void;
}
export function EditTrackDialog({
track,
isOpen,
onOpenChange,
onDelete,
}: EditTrackDialogProps) {
const [newTitle, setNewTitle] = useState(track.title);
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
function handleSave() {
if (track && newTitle.trim()) {
updateMusicTrackTitle(track, newTitle.trim());
onOpenChange(false);
}
}
function handleCancel() {
setNewTitle(track?.title || "");
onOpenChange(false);
}
function handleDeleteClick() {
setIsDeleteConfirmOpen(true);
}
function handleDeleteConfirm() {
onDelete();
onOpenChange(false);
}
function handleKeyDown(event: React.KeyboardEvent) {
if (event.key === "Enter") {
handleSave();
} else if (event.key === "Escape") {
handleCancel();
}
}
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Track</DialogTitle>
<DialogDescription>Edit "{track?.title}".</DialogDescription>
</DialogHeader>
<form className="py-4" onSubmit={handleSave}>
<Input
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter track name..."
autoFocus
/>
</form>
<DialogFooter className="flex justify-between">
<Button
variant="destructive"
onClick={handleDeleteClick}
className="mr-auto"
>
Delete Track
</Button>
<div className="flex gap-2">
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleSave} disabled={!newTitle.trim()}>
Save
</Button>
</div>
</DialogFooter>
</DialogContent>
<ConfirmDialog
isOpen={isDeleteConfirmOpen}
onOpenChange={setIsDeleteConfirmOpen}
title="Delete Track"
description={`Are you sure you want to delete "${track.title}"? This action cannot be undone.`}
confirmText="Delete"
cancelText="Cancel"
onConfirm={handleDeleteConfirm}
variant="destructive"
/>
</Dialog>
);
}

View File

@@ -1,10 +1,8 @@
import { MusicTrack, MusicaAccount } from "@/1_schema";
import { MusicaAccount } from "@/1_schema";
import { createNewPlaylist, deletePlaylist } from "@/4_actions";
import { MediaPlayer } from "@/5_useMediaPlayer";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
@@ -14,22 +12,18 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { usePlayState } from "@/lib/audio/usePlayState";
import { useAccount, useCoState } from "jazz-tools/react";
import { Home, Music, Pause, Play, Plus, Trash2 } from "lucide-react";
import { useAccount } from "jazz-tools/react";
import { Home, Music, Plus, Trash2 } from "lucide-react";
import { useNavigate, useParams } from "react-router";
import { AuthButton } from "./AuthButton";
export function SidePanel({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
export function SidePanel() {
const { playlistId } = useParams();
const navigate = useNavigate();
const { me } = useAccount(MusicaAccount, {
resolve: { root: { playlists: { $each: true } } },
});
const playState = usePlayState();
const isPlaying = playState.value === "play";
function handleAllTracksClick() {
navigate(`/`);
}
@@ -50,12 +44,6 @@ export function SidePanel({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
navigate(`/playlist/${playlist.id}`);
}
const activeTrack = useCoState(MusicTrack, mediaPlayer.activeTrackId, {
resolve: { waveform: true },
});
const activeTrackTitle = activeTrack?.title;
return (
<Sidebar>
<SidebarHeader>
@@ -137,29 +125,6 @@ export function SidePanel({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
{activeTrack && (
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem className="flex justify-end">
<SidebarMenuButton
onClick={playState.toggle}
aria-label={
isPlaying ? "Pause active track" : "Play active track"
}
>
<div className="w-[28px] h-[28px] flex items-center justify-center bg-blue-600 rounded-full text-white hover:bg-blue-700">
{isPlaying ? (
<Pause size={16} fill="currentColor" />
) : (
<Play size={16} fill="currentColor" />
)}
</div>
<span>{activeTrackTitle}</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
)}
</Sidebar>
);
}

View File

@@ -7,6 +7,7 @@ import { useCoState } from "jazz-tools/react";
export function Waveform(props: {
track: Loaded<typeof MusicTrack>;
height: number;
className?: string;
}) {
const { track, height } = props;
const waveformData = useCoState(
@@ -36,7 +37,7 @@ export function Waveform(props: {
return (
<div
className="flex justify-center items-end w-full"
className={cn("flex justify-center items-end w-full", props.className)}
style={{
height,
gap: 1,

View File

@@ -2,6 +2,12 @@
@custom-variant dark (&:is(.dark *));
html {
overflow: hidden;
max-width: 1200px;
position: relative;
}
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(20 14.3% 4.1%);

View File

@@ -55,10 +55,20 @@ export class HomePage {
async editTrackTitle(trackTitle: string, newTitle: string) {
await this.page
.getByRole("textbox", {
name: `Edit track title: ${trackTitle}`,
.getByRole("button", {
name: `Open ${trackTitle} menu`,
})
.fill(newTitle);
.click();
await this.page
.getByRole("menuitem", {
name: `Edit`,
})
.click();
await this.page.getByPlaceholder("Enter track name...").fill(newTitle);
await this.page.getByRole("button", { name: "Save" }).click();
}
async createPlaylist() {

View File

@@ -1,10 +1,21 @@
import path from "path";
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [
react(),
VitePWA({
registerType: "autoUpdate",
strategies: "generateSW",
workbox: {
globPatterns: ["**/*.{js,css,html,ico,png,svg}"],
maximumFileSizeToCacheInBytes: 1024 * 1024 * 5,
},
}),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),

View File

@@ -41,7 +41,9 @@ export function Footer({
</Link>
</div>
<p className="col-span-full sm:col-span-6 md:col-span-4 text-sm sm:text-base">
Playful software for serious problems.
Computers are magic.
<br />
Time to make them less complex.
</p>
</div>
<div className="grid gap-y-8 grid-cols-12">

View File

@@ -1,3 +0,0 @@
- Collaborative plain-text & rich-text
- Gracefully prevents most editing conflicts
- Rendered as markdown, HTML, JSX, etc.

View File

@@ -53,7 +53,7 @@ export function LocalFirstFeaturesSection() {
return (
<div>
<SectionHeader
title="The best of all worlds"
title="Local-first state with global sync"
slogan={
<>
<p>

View File

@@ -1,15 +1,15 @@
import { DiagramAfterJazz } from "@/components/DiagramAfterJazz";
import { DiagramBeforeJazz } from "@/components/DiagramBeforeJazz";
import { DiagramAfterJazz } from "./DiagramAfterJazz";
import { DiagramBeforeJazz } from "./DiagramBeforeJazz";
import { Icon } from "@garden-co/design-system/src/components/atoms/Icon";
import { Prose } from "@garden-co/design-system/src/components/molecules/Prose";
import { SectionHeader } from "@garden-co/design-system/src/components/molecules/SectionHeader";
export default function ProblemStatementSection() {
export default function BeforeAndAfterSection() {
return (
<div className="grid gap-4 lg:gap-8">
<SectionHeader
className="sm:text-center sm:mx-auto"
title={"Powered by the first “flat stack”"}
title={"A database that does what's actually needed"}
slogan="A perspective shift worth 10,000 hours"
/>
@@ -41,8 +41,7 @@ export default function ProblemStatementSection() {
<Prose>
<p>
For each new app you tackle a{" "}
<strong>mess of moving parts and infra worries.</strong> Or, you
haven't even tried because "you're not full-stack".
<strong>mess of moving parts and infra worries.</strong> Your backend is responsible for shuffling data around in a myriad of ways.
</p>
<p>
Want to build a <strong>modern app</strong> with multiplayer or
@@ -68,7 +67,7 @@ export default function ProblemStatementSection() {
<strong>With users &amp; permissions built-in.</strong>
</p>
<p>
With completely <strong>app-independent infra,</strong> you get to
With a <strong>DB and infra made for modern apps</strong> you get to
focus on <strong>building the app your users want.</strong> You'll
notice that <strong>90% of the work is now the UI.</strong>
</p>

View File

@@ -1,4 +1,4 @@
import { ServerWorkersDiagram } from "@/components/home/ServerWorkersDiagram";
import { ServerWorkersDiagram } from "@/app/(others)/(home)/components/everythingElse/ServerWorkersDiagram";
import { ClerkLogo } from "@/components/icons/ClerkLogo";
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
import { Card } from "@garden-co/design-system/src/components/atoms/Card";
@@ -89,7 +89,7 @@ const features = [
},
];
export function FeaturesSection() {
export function EverythingElseSection() {
return (
<div>
<SectionHeader

View File

@@ -83,7 +83,7 @@ export function HowJazzWorksSection() {
<div className="grid gap-3">
<Kicker>How it works</Kicker>
<H2>Build entire apps using only client-side code</H2>
<H2>Build entire apps with collaborative state</H2>
</div>
<GappedGrid>
<Step

View File

@@ -2,12 +2,12 @@
import { Framework } from "@/content/framework";
import { useFramework } from "@/lib/use-framework";
import NpxCreateJazzApp from "@/components/home/NpxCreateJazzApp.mdx";
import NpxCreateJazzApp from "./NpxCreateJazzApp.mdx";
import { CopyButton } from "@garden-co/design-system/src/components/molecules/CodeGroup";
import { useState } from "react";
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
import Link from "next/link";
import { FrameworkSelect } from "../docs/FrameworkSelect";
import { FrameworkSelect } from "@/components/docs/FrameworkSelect";
import clsx from "clsx";
import { track } from "@vercel/analytics";
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";

View File

@@ -0,0 +1,25 @@
"use client";
import { heroCopy } from "@/content/homepage";
import { H1 } from "@garden-co/design-system/src/components/atoms/Headings";
import { Kicker } from "@garden-co/design-system/src/components/atoms/Kicker";
import { Prose } from "@garden-co/design-system/src/components/molecules/Prose";
export function HeroSection() {
return (
<div className="container grid items-center gap-x-8 gap-y-12 mt-12 md:mt-16 lg:mt-24 mb-12 lg:gap-x-10 lg:grid-cols-12">
<div className="flex flex-col justify-center gap-5 lg:col-span-11 lg:gap-8">
<Kicker>{heroCopy.kicker}</Kicker>
<H1>
<span className="inline-block text-highlight">
{heroCopy.headline}
</span>
</H1>
<Prose size="lg" className="text-pretty max-w-2xl dark:text-stone-200 prose-p:leading-normal">
{heroCopy.descriptionLong}
</Prose>
</div>
</div>
);
}

View File

@@ -1,5 +1,6 @@
import { BunLogo } from "@/components/icons/BunLogo";
import { CloudflareWorkerLogo } from "@/components/icons/CloudflareWorkerLogo";
import { VercelLogo } from "@/components/icons/VercelLogo";
import { ExpoLogo } from "@/components/icons/ExpoLogo";
import { JavascriptLogo } from "@/components/icons/JavascriptLogo";
import { NodejsLogo } from "@/components/icons/NodejsLogo";
@@ -44,14 +45,18 @@ const serverWorkers = [
icon: NodejsLogo,
href: "/docs/react/server-workers",
},
{
name: "Cloudflare Workers",
icon: CloudflareWorkerLogo,
},
{
name: "Bun",
icon: BunLogo,
},
{
name: "Vercel",
icon: VercelLogo,
},
{
name: "CF Workers",
icon: CloudflareWorkerLogo,
}
];
export function SupportedEnvironmentsSection() {

View File

@@ -1,22 +1,20 @@
import { ChatDemoSection } from "@/components/home/ChatDemoSection";
import { CollaborationFeaturesSection } from "@/components/home/CollaborationFeaturesSection";
import { ComingSoonSection } from "@/components/home/ComingSoonSection";
import { EarlyAdopterSection } from "@/components/home/EarlyAdopterSection";
import { EncryptionSection } from "@/components/home/EncryptionSection";
import { FeaturesSection } from "@/components/home/FeaturesSection";
import { GetStartedSnippetSelect } from "@/components/home/GetStartedSnippetSelect";
import { HeroSection } from "@/components/home/HeroSection";
import { HowJazzWorksSection } from "@/components/home/HowJazzWorksSection";
import { LocalFirstFeaturesSection } from "@/components/home/LocalFirstFeaturesSection";
import ProblemStatementSection from "@/components/home/ProblemStatementSection";
import { SupportedEnvironmentsSection } from "@/components/home/SupportedEnvironmentsSection";
import { ChatDemoSection } from "./components/ChatDemoSection";
import { CollaborationFeaturesSection } from "./components/collaborationFeatures/CollaborationFeaturesSection";
import { EarlyAdopterSection } from "./components/EarlyAdopterSection";
import { EncryptionSection } from "./components/EncryptionSection";
import { EverythingElseSection } from "./components/everythingElse/EverythingElseSection";
import { HeroSection } from "./components/hero/HeroSection";
import { GetStartedSnippetSelect } from "./components/hero/GetStartedSnippetSelect";
import { HowJazzWorksSection } from "./components/fourSteps/HowJazzWorksSection";
import { LocalFirstFeaturesSection } from "./components/LocalFirstFeaturesSection";
import BeforeAndAfterSection from "./components/beforeAndAfter/BeforeAndAfterSection";
import { SupportedEnvironmentsSection } from "./components/hero/SupportedEnvironmentsSection";
import { Testimonial } from "@garden-co/design-system/src/components/molecules/Testimonial";
export default function Home() {
return (
<>
<HeroSection />
<div className="container flex flex-col gap-12 lg:gap-20">
<GetStartedSnippetSelect />
<SupportedEnvironmentsSection />
@@ -35,7 +33,7 @@ export default function Home() {
<ChatDemoSection />
<ProblemStatementSection />
<BeforeAndAfterSection />
<LocalFirstFeaturesSection />
@@ -52,9 +50,7 @@ export default function Home() {
</span>
</Testimonial>
<FeaturesSection />
<ComingSoonSection />
<EverythingElseSection />
<EarlyAdopterSection />
</div>

View File

@@ -1,4 +0,0 @@
- 2D canvas cursors
- Text carets
- Element-based focus-presence
- Scroll-based / out-of-bounds helpers

View File

@@ -1,3 +0,0 @@
- Prisma
- Drizzle
- PostgreSQL introspection

View File

@@ -1,2 +0,0 @@
- Automatic WebRTC connections between `Group` members
- Audio/video recording into `BinaryCoStreams`

View File

@@ -7,7 +7,7 @@ import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./tooltip";
} from "@/components/tooltip";
const ranges = [
{

View File

@@ -1,5 +1,5 @@
import { Pricing } from "@/components/Pricing";
import { LatencyMap } from "@/components/cloud/latencyMap";
import { Pricing } from "./components/Pricing";
import { LatencyMap } from "@/app/(others)/cloud/components/latencyMap";
import { GridCard } from "@garden-co/design-system/src/components/atoms/GridCard";
import { H2, H3 } from "@garden-co/design-system/src/components/atoms/Headings";
import { P } from "@garden-co/design-system/src/components/atoms/Paragraph";

View File

@@ -1,5 +1,5 @@
import { ExampleLinks } from "@/components/examples/ExampleLinks";
import { ExampleTags } from "@/components/examples/ExampleTags";
import { ExampleLinks } from "@/app/(others)/examples/components/ExampleLinks";
import { ExampleTags } from "@/app/(others)/examples/components/ExampleTags";
import { Example } from "@/content/example";
import { clsx } from "clsx";

View File

@@ -1,4 +1,4 @@
import { ExampleCard } from "@/components/examples/ExampleCard";
import { ExampleCard } from "@/app/(others)/examples/components/ExampleCard";
import { ClerkFullLogo } from "@/components/icons/ClerkFullLogo";
import { ReactLogo } from "@/components/icons/ReactLogo";
import { ReactNativeLogo } from "@/components/icons/ReactNativeLogo";

View File

@@ -1,4 +1,4 @@
import LatencyChart from "@/components/LatencyChart";
import LatencyChart from "@/app/(others)/cloud/components/LatencyChart";
import { HeroHeader } from "@garden-co/design-system/src/components/molecules/HeroHeader";
import { clsx } from "clsx";
import type { Metadata } from "next";

View File

@@ -1,11 +1,11 @@
import * as turf from "@turf/turf";
import type { FeatureCollection, Point, Position } from "geojson";
import { type NextRequest } from "next/server";
import land from "../../../components/cloud/ne_110m_land.json";
import land from "@/app/(others)/cloud/components/ne_110m_land.json";
import { pingColorThresholds } from "../../../components/cloud/pingColorThresholds";
import { pingColorThresholds } from "../../(others)/cloud/components/pingColorThresholds";
// generated with: globalping ping cloud.jazz.tools from world --limit 500 --packets 16 --json | jq "del(.results[].result.rawOutput)" > pings.json
import pings from "../../../components/cloud/pings.json";
import pings from "@/app/(others)/cloud/components/pings.json";
export const revalidate = 7200; // 2 hours

View File

@@ -2,15 +2,15 @@ import "./globals.css";
import { ThemeProvider } from "@/components/ThemeProvider";
import { JazzFooter } from "@/components/footer";
import { PagefindSearch } from "@/components/pagefind";
import { marketingCopy } from "@/content/marketingCopy";
import { heroCopy } from "@/content/homepage";
import { fontClasses } from "@garden-co/design-system/src/fonts";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import type { Metadata } from "next";
const metaTags = {
title: `Jazz - ${marketingCopy.headline}`,
description: marketingCopy.description,
title: `Jazz - ${heroCopy.headline}`,
description: heroCopy.description,
url: "https://jazz.tools",
};

View File

@@ -1,14 +1,14 @@
import { marketingCopy } from "@/content/marketingCopy";
import { heroCopy } from "@/content/homepage";
import {
OpenGraphImage,
imageContentType,
imageSize,
} from "@garden-co/design-system/src/components/organisms/OpenGraphImage";
export const title = marketingCopy.headline;
export const title = heroCopy.headline;
export const size = imageSize;
export const contentType = imageContentType;
export const alt = marketingCopy.headline;
export const alt = heroCopy.headline;
export default async function Image() {
return OpenGraphImage({ title });

View File

@@ -1,54 +0,0 @@
import CoPlainTextDescription from "@/app/(others)/(home)/coValueDescriptions/coPlainTextDescription.mdx";
import CursorsAndCaretsDescription from "@/app/(others)/(home)/toolkit/cursorsAndCarets.mdx";
import TwoWaySyncDescription from "@/app/(others)/(home)/toolkit/twoWaySync.mdx";
import VideoPresenceCallsDescription from "@/app/(others)/(home)/toolkit/videoPresenceCalls.mdx";
import { CodeRef } from "@garden-co/design-system/src/components/atoms/CodeRef";
import { P } from "@garden-co/design-system/src/components/atoms/Paragraph";
import { FeatureCard } from "@garden-co/design-system/src/components/molecules/FeatureCard";
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";
import { Prose } from "@garden-co/design-system/src/components/molecules/Prose";
import { SectionHeader } from "@garden-co/design-system/src/components/molecules/SectionHeader";
export function ComingSoonSection() {
return (
<div>
<SectionHeader title="More features coming soon" />
<GappedGrid cols={4}>
<FeatureCard className="p-4" label={<h3>Cursors & carets</h3>}>
<P>Ready-made spatial presence.</P>
<Prose size="sm">
<CursorsAndCaretsDescription />
</Prose>
</FeatureCard>
<FeatureCard className="p-4" label={<h3>Two-way sync to your DB</h3>}>
<P>Add Jazz to an existing app.</P>
<Prose size="sm">
<TwoWaySyncDescription />
</Prose>
</FeatureCard>
<FeatureCard className="p-4" label={<h3>Video presence & calls</h3>}>
<P>Stream and record audio & video.</P>
<Prose size="sm">
<VideoPresenceCallsDescription />
</Prose>
</FeatureCard>
<FeatureCard
className="p-4"
label={
<h3>
<CodeRef>CoPlainText</CodeRef> & <CodeRef>CoRichText</CodeRef>
</h3>
}
>
<Prose size="sm">
<CoPlainTextDescription />
</Prose>
</FeatureCard>
</GappedGrid>
</div>
);
}

View File

@@ -1,7 +0,0 @@
```sh
npx create-jazz-app@latest
> Framework: react
> Auth: passkey
> Project name: _
```

View File

@@ -1,97 +0,0 @@
"use client";
import { marketingCopy } from "@/content/marketingCopy";
import { H1 } from "@garden-co/design-system/src/components/atoms/Headings";
import {
Icon,
type IconName,
} from "@garden-co/design-system/src/components/atoms/Icon";
import { Kicker } from "@garden-co/design-system/src/components/atoms/Kicker";
import { Prose } from "@garden-co/design-system/src/components/molecules/Prose";
import Link from "next/link";
import { GetStartedSnippetSelect } from "./GetStartedSnippetSelect";
const features: Array<{
title: string;
icon: IconName;
}> = [
{
title: "Instant updates",
icon: "instant",
},
{
title: "Real-time sync",
icon: "devices",
},
{
title: "Multiplayer",
icon: "spatialPresence",
},
{
title: "File uploads",
icon: "upload",
},
{
title: "Social features",
icon: "social",
},
{
title: "Permissions",
icon: "permissions",
},
{
title: "E2E encryption",
icon: "encryption",
},
{
title: "Authentication",
icon: "auth",
},
];
export function HeroSection() {
return (
<div className="container grid items-center gap-x-8 gap-y-12 my-12 md:my-16 lg:my-24 lg:gap-x-10 lg:grid-cols-12">
<div className="flex flex-col justify-center gap-5 lg:col-span-11 lg:gap-8">
<Kicker>Toolkit for backendless apps</Kicker>
<H1>
<span className="inline-block text-highlight">
{marketingCopy.headline}
</span>
</H1>
<Prose size="lg" className="text-pretty max-w-2xl dark:text-stone-200">
<p>
Jazz gives you data without needing a database plus auth,
permissions, files and multiplayer without needing a backend.
</p>
<p>
Do everything right from the frontend and ship better apps, faster.
</p>
<p>
Open source. Self-host or use{" "}
<Link className="text-reset" href="/cloud">
Jazz Cloud
</Link>{" "}
for zero-config magic.
</p>
</Prose>
<div className="grid grid-cols-2 gap-2 max-w-3xl sm:grid-cols-4 sm:gap-4">
{features.map(({ title, icon }) => (
<div
key={title}
className="flex text-xs sm:text-sm gap-2 items-center"
>
<span className="p-1.5 rounded-lg bg-primary-transparent">
<Icon size="xs" name={icon} intent="primary" />
</span>
<p>{title}</p>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import React from "react";
import type { SVGProps } from "react";
export function VercelLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
width="1.5em"
height="1.5em"
viewBox="0 0 76 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" fill="currentColor" />
</svg>
);
}

View File

@@ -54,7 +54,7 @@ function AuthStateIndicator() {
const isGuest = agent._type !== "Account"
// Anonymous authentication: has an account but not fully authenticated
const isAnonymous = agent._type === "Account" && !isAuthenticated;
const isAnonymous = agent._type === "Account" && !isAuthenticated;
return (
<div>
{isGuest && <span>Guest Mode</span>}

View File

@@ -54,10 +54,10 @@ import { co, z, CoMap } from "jazz-tools";
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
```
</CodeGroup>
@@ -73,17 +73,17 @@ import { co, z } from "jazz-tools";
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
// ---cut---
// OrderForm.tsx
export function OrderForm({
order,
onSave,
}: {
order: co.loaded<typeof BubbleTeaOrder> | co.loaded<typeof DraftBubbleTeaOrder>;
order: BubbleTeaOrder | DraftBubbleTeaOrder;
onSave?: (e: React.FormEvent<HTMLFormElement>) => void;
}) {
return (
@@ -118,16 +118,16 @@ import * as React from "react";
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
export function OrderForm({
order,
onSave,
}: {
order: co.loaded<typeof BubbleTeaOrder> | co.loaded<typeof DraftBubbleTeaOrder>;
order: BubbleTeaOrder | DraftBubbleTeaOrder;
onSave?: (e: React.FormEvent<HTMLFormElement>) => void;
}) {
return (
@@ -177,10 +177,10 @@ import { useState, useEffect } from "react";
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
export const AccountRoot = co.map({
draft: DraftBubbleTeaOrder,
@@ -218,7 +218,7 @@ export function OrderForm({
// CreateOrder.tsx
export function CreateOrder() {
const { me } = useAccount();
const [draft, setDraft] = useState<co.loaded<typeof DraftBubbleTeaOrder>>();
const [draft, setDraft] = useState<DraftBubbleTeaOrder>();
useEffect(() => {
setDraft(DraftBubbleTeaOrder.create({}));
@@ -228,7 +228,7 @@ export function CreateOrder() {
e.preventDefault();
if (!draft || !draft.name) return;
const order = draft as co.loaded<typeof BubbleTeaOrder>; // TODO: this should narrow correctly
const order = draft as BubbleTeaOrder; // TODO: this should narrow correctly
console.log("Order created:", order);
};
@@ -251,11 +251,15 @@ Update the schema to include a `validateDraftOrder` helper.
import { co, z } from "jazz-tools";
// ---cut---
// schema.ts
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>) { // [!code ++:9]
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
export function validateDraftOrder(draft: DraftBubbleTeaOrder) { // [!code ++:9]
const errors: string[] = [];
if (!draft.name) {
@@ -279,12 +283,12 @@ import { useState, useEffect } from "react";
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>) {
export function validateDraftOrder(draft: DraftBubbleTeaOrder) {
const errors: string[] = [];
if (!draft.name) {
@@ -307,7 +311,7 @@ export function OrderForm({
order,
onSave,
}: {
order: co.loaded<typeof BubbleTeaOrder> | co.loaded<typeof DraftBubbleTeaOrder>;
order: BubbleTeaOrder | DraftBubbleTeaOrder;
onSave?: (e: React.FormEvent<HTMLFormElement>) => void;
}) {
return (
@@ -330,7 +334,7 @@ export function OrderForm({
// CreateOrder.tsx
export function CreateOrder() {
const { me } = useAccount();
const [draft, setDraft] = useState<co.loaded<typeof DraftBubbleTeaOrder>>();
const [draft, setDraft] = useState<DraftBubbleTeaOrder>();
useEffect(() => {
setDraft(DraftBubbleTeaOrder.create({}));
@@ -346,7 +350,7 @@ export function CreateOrder() {
return;
}
const order = draft as co.loaded<typeof BubbleTeaOrder>;
const order = draft as BubbleTeaOrder;
console.log("Order created:", order);
};
@@ -372,10 +376,10 @@ import { co, z } from "jazz-tools";
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
export const AccountRoot = co.map({ // [!code ++:15]
draft: DraftBubbleTeaOrder,
@@ -403,10 +407,10 @@ import { co, z } from "jazz-tools";
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
export const AccountRoot = co.map({
draft: DraftBubbleTeaOrder,
@@ -452,12 +456,12 @@ import { co, z } from "jazz-tools";
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>) {
export function validateDraftOrder(draft: DraftBubbleTeaOrder) {
const errors: string[] = [];
if (!draft.name) {
@@ -492,7 +496,7 @@ export function OrderForm({
order,
onSave,
}: {
order: co.loaded<typeof BubbleTeaOrder> | co.loaded<typeof DraftBubbleTeaOrder>;
order: BubbleTeaOrder | DraftBubbleTeaOrder;
onSave?: (e: React.FormEvent<HTMLFormElement>) => void;
}) {
return (
@@ -533,7 +537,7 @@ export function CreateOrder() {
return;
}
const order = draft as co.loaded<typeof BubbleTeaOrder>;
const order = draft as BubbleTeaOrder;
console.log("Order created:", order);
// create a new empty draft
@@ -577,11 +581,15 @@ Simply add a `hasChanges` helper to your schema.
import { co, z } from "jazz-tools";
// ---cut---
// schema.ts
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>) {
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
export function validateDraftOrder(draft: DraftBubbleTeaOrder) {
const errors: string[] = [];
if (!draft.name) {
@@ -591,7 +599,7 @@ export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>)
return { errors };
};
export function hasChanges(draft?: co.loaded<typeof DraftBubbleTeaOrder>) { // [!code ++:3]
export function hasChanges(draft?: DraftBubbleTeaOrder) { // [!code ++:3]
return draft ? Object.keys(draft._edits).length : false;
};
```
@@ -608,12 +616,12 @@ import * as React from "react";
export const BubbleTeaOrder = co.map({
name: z.string(),
});
export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>;
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
});
export const DraftBubbleTeaOrder = BubbleTeaOrder.partial();
export type DraftBubbleTeaOrder = co.loaded<typeof DraftBubbleTeaOrder>;
export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>) {
export function validateDraftOrder(draft: DraftBubbleTeaOrder) {
const errors: string[] = [];
if (!draft.name) {
@@ -623,7 +631,7 @@ export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>)
return { errors };
};
export function hasChanges(draft?: co.loaded<typeof DraftBubbleTeaOrder>) {
export function hasChanges(draft?: DraftBubbleTeaOrder) {
return draft ? Object.keys(draft._edits).length : false;
};

View File

@@ -31,7 +31,7 @@ export const Organization = co.map({
name: z.string(),
// shared data between users of each organization
projects: co.list(Project),
projects: co.list(Project),
});
export const ListOfOrganizations = co.list(Organization);
@@ -115,7 +115,7 @@ import * as React from "react";
import { useAcceptInvite, useAccount } from "jazz-tools/react";
import { co, z } from "jazz-tools";
const Project = z.object({
const Project = co.map({
name: z.string(),
});

View File

@@ -205,6 +205,101 @@ console.log(containingGroup.getParentGroups()); // [addedGroup]
```
</CodeGroup>
## Group hierarchy on CoValue creation
When creating CoValues that contain other CoValues using plain JSON objects, Jazz not only creates
the necessary CoValues automatically but it will also manage their group ownership.
<CodeGroup>
```ts twoslash
import { co, z } from "jazz-tools";
// ---cut---
const Task = co.plainText();
const Column = co.list(Task);
const Board = co.map({
title: z.string(),
columns: co.list(Column),
});
const board = Board.create({
title: "My board",
columns: [
["Task 1.1", "Task 1.2"],
["Task 2.1", "Task 2.2"],
],
});
```
</CodeGroup>
For each created column and task CoValue, Jazz also creates a new group as its owner and
adds the referencing CoValue's owner as a member of that group. This means permissions for nested CoValues
are inherited from the CoValue that references them, but can also be modified independently for each CoValue
if needed.
<CodeGroup>
```ts twoslash
import { co, z, Group, Account } from "jazz-tools";
const alice = {} as unknown as Account;
const bob = {} as unknown as Account;
const Task = co.plainText();
const Column = co.list(Task);
const Board = co.map({
title: z.string(),
columns: co.list(Column),
});
// ---cut---
const writeAccess = Group.create();
writeAccess.addMember(bob, "writer");
// Give Bob write access to the board, columns and tasks
const board = Board.create({
title: "My board",
columns: [
["Task 1.1", "Task 1.2"],
["Task 2.1", "Task 2.2"],
],
}, writeAccess);
// Give Alice read access to one specific task
const task = board.columns[0][0];
const taskGroup = task._owner.castAs(Group);
taskGroup.addMember(alice, "reader");
```
</CodeGroup>
If you prefer to manage permissions differently, you can always create CoValues explicitly:
<CodeGroup>
```ts twoslash
import { co, Group, z, Account } from "jazz-tools";
const bob = {} as unknown as Account;
const Task = co.plainText();
const Column = co.list(Task);
const Board = co.map({
title: z.string(),
columns: co.list(Column),
});
// ---cut---
const writeAccess = Group.create();
writeAccess.addMember(bob, "writer");
const readAccess = Group.create();
readAccess.addMember(bob, "reader");
// Give Bob read access to the board and write access to the columns and tasks
const board = Board.create({
title: "My board",
columns: co.list(Column).create([
["Task 1.1", "Task 1.2"],
["Task 2.1", "Task 2.2"],
], writeAccess),
}, readAccess);
```
</CodeGroup>
## Example: Team Hierarchy
Here's a practical example of using group inheritance for team permissions:

View File

@@ -7,9 +7,11 @@ export const metadata = {
# Learn some <span className="sr-only">Jazz</span> <JazzLogo className="h-[41px] -ml-0.5 -mt-[3px] inline" />
**Jazz is a toolkit for building backendless apps**. You get data without needing a database — plus auth, permissions, files and multiplayer without needing a backend. Jazz lets you do everything right from the frontend and you'll ship better apps, faster.
**Jazz is a new kind of database** that's **distributed** across your frontend, containers, serverless functions and its own storage cloud.
Instead of wrestling with databases, APIs, and server infrastructure, you work with **CoValues** ("collaborative values") — your new cloud-synced building blocks that feel like local state but automatically sync across all devices and users in real-time.
It syncs structured data, files and LLM streams instantly, and looks like local reactive JSON state.
It also provides auth, orgs & teams, real-time multiplayer, edit histories, permissions, E2E encryption and offline-support out of the box.
---
@@ -19,7 +21,7 @@ You can use [`create-jazz-app`](/docs/tools/create-jazz-app) to create a new Jaz
<CodeGroup>
```sh
npx create-jazz-app@latest --api-key you@example.com
npx create-jazz-app@latest --api-key you@example.com
```
</CodeGroup>
@@ -30,21 +32,10 @@ Or you can follow this [React step-by-step guide](/docs/react/guide) where we wa
</ContentByFramework> */}
## Why Jazz is different
Most apps rebuild the same thing: shared state that syncs between users and devices. Jazz starts from that shared state, giving you:
- **No backend required** — Focus on building features, not infrastructure
- **Real-time sync** — Changes appear everywhere immediately
- **Multiplayer by default** — Collaboration just works
- **Local-first** — Your app works offline and feels instant
Think Figma, Notion, or Linear — but you don't need years to build a custom stack.
## How it works
1. **Define your data** with CoValues schemas
2. **Connect to sync infrastructure** (Jazz Cloud or self-hosted)
2. **Connect to storage infrastructure** (Jazz Cloud or self-hosted)
3. **Create and edit CoValues** like normal objects
4. **Get automatic sync and persistence** across all devices and users

View File

@@ -3,6 +3,7 @@ export const metadata = {
};
import { CodeGroup, ComingSoon } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# Defining schemas: CoValues
@@ -80,6 +81,40 @@ const project = TodoProject.create(
</CodeGroup>
When creating CoValues that contain other CoValues, you can pass in a plain JSON object.
Jazz will automatically create the CoValues for you.
<CodeGroup>
```ts twoslash
// @filename: schema.ts
import { co, z, CoMap, CoList } from "jazz-tools";
export const ListOfTasks = co.list(z.string());
export const TodoProject = co.map({
title: z.string(),
tasks: ListOfTasks,
});
// @filename: app.ts
// ---cut---
// app.ts
import { Group } from "jazz-tools";
import { TodoProject, ListOfTasks } from "./schema";
const group = Group.create().makePublic();
const project = TodoProject.create({
title: "New Project",
tasks: [], // Permissions are inherited, so the tasks list will also be public
}, group);
```
</CodeGroup>
<Alert variant="info" className="flex gap-2 items-center my-4">
To learn more about how permissions work when creating nested CoValues with plain JSON objects,
refer to [Group hierarchy on CoValue creation](/docs/groups/inheritance#group-hierarchy-on-covalue-creation).
</Alert>
## Types of CoValues
### `CoMap` (declaration)
@@ -320,6 +355,10 @@ const Company = co.map({
</CodeGroup>
#### Optional References
You can make schema fields optional using either `z.optional()` or `co.optional()`, depending on the type of value:
- Use `z.optional()` for primitive Zod values like `z.string()`, `z.number()`, or `z.boolean()`
- Use `co.optional()` for CoValues like `co.map()`, `co.list()`, or `co.record()`
You can make references optional with `co.optional()`:
@@ -331,7 +370,8 @@ const Pet = co.map({
});
// ---cut---
const Person = co.map({
pet: co.optional(Pet),
age: z.optional(z.number()), // primitive
pet: co.optional(Pet), // CoValue
});
```
</CodeGroup>

View File

@@ -7,7 +7,7 @@ import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# Inbox API with Server Workers
The Inbox API provides a message-based communication system for Server Workers in Jazz.
The Inbox API provides a message-based communication system for Server Workers in Jazz.
It works on top of the Jazz APIs and uses sync to transfer messages between the client and the server.
@@ -154,8 +154,8 @@ function EventComponent({ event }: { event: Event }) {
```
</CodeGroup>
The `sendInboxMessage` API returns a Promise that waits for the message to be handled by a Worker.
A message is considered to be handled when the Promise returned by `inbox.subscribe` resolves.
The `sendInboxMessage` API returns a Promise that waits for the message to be handled by a Worker.
A message is considered to be handled when the Promise returned by `inbox.subscribe` resolves.
The value returned will be the id of the CoValue returned in the `inbox.subscribe` resolved promise.
@@ -163,4 +163,4 @@ The value returned will be the id of the CoValue returned in the `inbox.subscrib
Multi-region deployments are not supported when using the Inbox API.
If you need to split the workload across multiple regions, you can use the [HTTP API](./http-requests.mdx) instead.
If you need to split the workload across multiple regions, you can use the [HTTP API](./http-requests) instead.

View File

@@ -228,6 +228,54 @@ export type Project = co.loaded<typeof Project>;
```
</CodeGroup>
### Partial
For convenience Jazz provies a dedicated API for making all the properties of a CoMap optional:
<CodeGroup>
```ts twoslash
import { co, z } from "jazz-tools";
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
});
const ProjectDraft = Project.partial();
// The fields are all optional now
const project = ProjectDraft.create({});
```
</CodeGroup>
### Pick
You can also pick specific fields from a CoMap:
<CodeGroup>
```ts twoslash
import { co, z } from "jazz-tools";
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
});
const ProjectStep1 = Project.pick({
name: true,
startDate: true,
});
// We don't provide the status field
const project = ProjectStep1.create({
name: "My project",
startDate: new Date("2025-04-01"),
});
```
</CodeGroup>
### Working with Record CoMaps
For record-type CoMaps, you can access values using bracket notation:

View File

@@ -0,0 +1,38 @@
import Link from "next/link";
export const heroCopy = {
kicker: "Reactive, distributed, secure",
headline: "Smooth database.",
description:
"Jazz is a database that's distributed across your frontend, containers and functions. It syncs structured data, files and LLM streams instantly and looks like local reactive JSON state.",
descriptionLong: (
<>
<p>
Jazz is a new kind of database that's distributed across your frontend,
containers, serverless functions and its own storage cloud.
</p>
<p>
It syncs structured data, files and LLM streams instantly.
<br />
It looks like local reactive JSON state.
</p>
<p>
And you get auth, orgs & teams, real-time multiplayer, edit histories,
permissions, E2E encryption and offline-support out of the box.
</p>
<p>
This lets you get rid of 90% of the traditional backend, and most of
your frontend state juggling. You&apos;ll ship better apps, faster.
</p>
<p className="text-base">
Self-host or use{" "}
<Link className="text-reset" href="/cloud">
Jazz Cloud
</Link>{" "}
for a zero-deploy globally-scaled DB.
<br />
Open source (MIT)
</p>
</>
),
};

View File

@@ -1,5 +0,0 @@
export const marketingCopy = {
headline: "Whip up an app",
description:
"Jazz gives you data without needing a database — plus auth, permissions, files and multiplayer without needing a backend. Do everything right from the frontend and ship better apps, faster.",
};

View File

@@ -5,6 +5,8 @@ import { readFile, readdir } from "fs/promises";
import { DOC_SECTIONS } from "./utils/config.mjs";
import { writeDocsFile } from "./utils/index.mjs";
const exclude = [/\/upgrade\//];
async function readMdxContent(url) {
try {
// Special case for the introduction
@@ -31,12 +33,17 @@ async function readMdxContent(url) {
// If it's a directory, try to read all framework variants
const fullPath = path.join(baseDir, relativePath);
if (exclude.some((pattern) => pattern.test(fullPath))) {
return null;
}
try {
const stats = await fs.stat(fullPath);
if (stats.isDirectory()) {
// Read all MDX files in the directory
const files = await fs.readdir(fullPath);
const mdxFiles = files.filter((f) => f.endsWith(".mdx"));
const mdxFiles = files.filter((f) => f.endsWith(".mdx")).filter((f) => !exclude.some((pattern) => pattern.test(f)));
if (mdxFiles.length === 0) return null;

View File

@@ -1,5 +1,5 @@
import type { Config } from "tailwindcss";
import { pingColorMap } from "./components/cloud/pingColorThresholds";
import { pingColorMap } from "./app/(others)/cloud/components/pingColorThresholds";
export const colourSafelist = Object.values(pingColorMap).flatMap((value) => {
const { light, dark } = value as { light: string; dark: string };

View File

@@ -31,7 +31,7 @@
"build:packages": "turbo build --filter='./packages/*'",
"lint": "turbo lint && cd homepage/homepage && pnpm run lint",
"test": "vitest",
"test:ci": "vitest run --watch=false --coverage.enabled=true",
"test:ci": "vitest run --watch=false",
"test:coverage": "vitest --ui --coverage.enabled=true",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",

View File

@@ -1,5 +1,31 @@
# cojson-storage-indexeddb
## 0.16.4
### Patch Changes
- Updated dependencies [f9d538f]
- Updated dependencies [802b5a3]
- cojson@0.16.4
## 0.16.3
### Patch Changes
- cojson@0.16.3
## 0.16.2
### Patch Changes
- cojson@0.16.2
## 0.16.1
### Patch Changes
- cojson@0.16.1
## 0.16.0
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.16.0",
"version": "0.16.4",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",

View File

@@ -179,8 +179,9 @@ test("should load dependencies correctly (group inheritance)", async () => {
),
).toMatchInlineSnapshot(`
[
"client -> CONTENT Group header: true new: After: 0 New: 5",
"client -> CONTENT Group header: true new: After: 0 New: 3",
"client -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"client -> CONTENT Group header: false new: After: 3 New: 2",
"client -> CONTENT Map header: true new: After: 0 New: 1",
]
`);
@@ -561,9 +562,10 @@ test("should sync and load accounts from storage", async () => {
),
).toMatchInlineSnapshot(`
[
"client -> CONTENT Account header: true new: After: 0 New: 4",
"client -> CONTENT Account header: true new: After: 0 New: 3",
"client -> CONTENT ProfileGroup header: true new: After: 0 New: 5",
"client -> CONTENT Profile header: true new: After: 0 New: 1",
"client -> CONTENT Account header: false new: After: 3 New: 1",
]
`);

View File

@@ -36,12 +36,11 @@ export function trackMessages() {
};
StorageApiAsync.prototype.store = async function (data, correctionCallback) {
for (const msg of data) {
messages.push({
from: "client",
msg,
});
}
messages.push({
from: "client",
msg: data,
});
return originalStore.call(this, data, (msg) => {
messages.push({
from: "storage",
@@ -51,7 +50,18 @@ export function trackMessages() {
...msg,
},
});
correctionCallback(msg);
const correctionMessages = correctionCallback(msg);
if (correctionMessages) {
for (const msg of correctionMessages) {
messages.push({
from: "client",
msg,
});
}
}
return correctionMessages;
});
};

View File

@@ -1,5 +1,31 @@
# cojson-storage-sqlite
## 0.16.4
### Patch Changes
- Updated dependencies [f9d538f]
- Updated dependencies [802b5a3]
- cojson@0.16.4
## 0.16.3
### Patch Changes
- cojson@0.16.3
## 0.16.2
### Patch Changes
- cojson@0.16.2
## 0.16.1
### Patch Changes
- cojson@0.16.1
## 0.16.0
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.16.0",
"version": "0.16.4",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",

View File

@@ -211,8 +211,9 @@ test("should load dependencies correctly (group inheritance)", async () => {
),
).toMatchInlineSnapshot(`
[
"client -> CONTENT Group header: true new: After: 0 New: 5",
"client -> CONTENT Group header: true new: After: 0 New: 3",
"client -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"client -> CONTENT Group header: false new: After: 3 New: 2",
"client -> CONTENT Map header: true new: After: 0 New: 1",
]
`);
@@ -374,6 +375,8 @@ test("should recover from data loss", async () => {
[
"client -> CONTENT Group header: true new: After: 0 New: 3",
"client -> CONTENT Map header: true new: After: 0 New: 1",
"client -> CONTENT Map header: false new: After: 3 New: 1",
"storage -> KNOWN CORRECTION Map sessions: header/4",
"client -> CONTENT Map header: false new: After: 1 New: 3",
]
`);
@@ -455,10 +458,7 @@ test("should recover missing dependencies from storage", async () => {
data,
correctionCallback,
) {
if (
data[0]?.id &&
[group.core.id, account.core.id as string].includes(data[0].id)
) {
if ([group.core.id, account.core.id as string].includes(data.id)) {
return false;
}

View File

@@ -36,12 +36,11 @@ export function trackMessages() {
};
StorageApiSync.prototype.store = function (data, correctionCallback) {
for (const msg of data) {
messages.push({
from: "client",
msg,
});
}
messages.push({
from: "client",
msg: data,
});
return originalStore.call(this, data, (msg) => {
messages.push({
from: "storage",
@@ -51,7 +50,19 @@ export function trackMessages() {
...msg,
},
});
correctionCallback(msg);
const correctionMessages = correctionCallback(msg);
if (correctionMessages) {
for (const msg of correctionMessages) {
messages.push({
from: "client",
msg,
});
}
}
return correctionMessages;
});
};

View File

@@ -1,5 +1,31 @@
# cojson-transport-nodejs-ws
## 0.16.4
### Patch Changes
- Updated dependencies [f9d538f]
- Updated dependencies [802b5a3]
- cojson@0.16.4
## 0.16.3
### Patch Changes
- cojson@0.16.3
## 0.16.2
### Patch Changes
- cojson@0.16.2
## 0.16.1
### Patch Changes
- cojson@0.16.1
## 0.16.0
### Patch Changes

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