Compare commits

..

36 Commits

Author SHA1 Message Date
Guido D'Orsi
867cb6b7a5 Merge pull request #2659 from garden-co/changeset-release/main
Version Packages
2025-07-23 18:30:51 +02:00
github-actions[bot]
0401fcf2a8 Version Packages 2025-07-23 12:43:56 +00:00
Nico Rainhart
139a649279 Merge pull request #2663 from garden-co/fix/export-WithHelpers-type
fix: Export `WithHelpers` type
2025-07-23 09:41:20 -03:00
NicoR
9acccb5df2 Add changeset 2025-07-23 09:23:12 -03:00
NicoR
fd90cdb49a fix: Export WithHelpers type 2025-07-23 09:15:09 -03:00
Giordano Ricci
df487d5335 Merge pull request #2658 from garden-co/gio/quint 2025-07-23 12:35:06 +01:00
Giordano Ricci
1efe84c691 Merge branch 'main' into gio/quint 2025-07-23 12:17:02 +01:00
Guido D'Orsi
063553090e docs: fix type errors on co.profile 2025-07-22 18:29:18 +02:00
Guido D'Orsi
6dffe73bd2 chore: rename jazz-paper-scissors in server-worker-inbox 2025-07-22 16:32:08 +02:00
Guido D'Orsi
68cb357a94 Merge pull request #2660 from garden-co/feat/persistent-peers
feat: introduce persistent peers
2025-07-22 16:18:56 +02:00
Guido D'Orsi
4f7bc91502 chore: apply suggestions from code review
Co-authored-by: Nico Rainhart <nmrainhart@gmail.com>
2025-07-22 14:40:45 +02:00
Giordano Ricci
f61a120560 changeset 2025-07-22 12:22:22 +01:00
Giordano Ricci
2f1307a0ba rename package 2025-07-22 12:21:19 +01:00
Giordano Ricci
fa15ea56d1 build & dev workflows 2025-07-22 12:03:21 +01:00
Guido D'Orsi
1e58ecb3ac test: integration tests for browser and workers on offline loading & sync 2025-07-22 12:42:49 +02:00
Giordano Ricci
ceeabfaf89 strip down PR 2025-07-22 11:07:52 +01:00
Guido D'Orsi
70ce7c5736 feat: introduce persistent peers 2025-07-22 11:12:48 +02:00
Nico Rainhart
6afdb16739 Merge pull request #2656 from garden-co/feat/prevent-resolving-discriminated-union-fields
feat: Prevent resolving discriminated union fields
2025-07-21 14:45:51 -03:00
Giordano Ricci
b0b2b85a6f fix lint issues 2025-07-21 18:13:18 +01:00
Giordano Ricci
28c19c134f Merge branch 'main' into gio/quint 2025-07-21 18:09:42 +01:00
Giordano Ricci
0924c9baaa revert changes to example app 2025-07-21 18:04:17 +01:00
Giordano Ricci
b2712e18a2 fix imports 2025-07-21 17:46:15 +01:00
Giordano Ricci
66894b63d7 more cleanup 2025-07-21 17:43:11 +01:00
Giordano Ricci
b1a05143e3 cleanup 2025-07-21 15:26:49 +01:00
Giordano Ricci
fb761ce66d cleanup 2025-07-21 15:25:09 +01:00
Giordano Ricci
07a6c340dc some cleanup 2025-07-21 15:23:02 +01:00
Giordano Ricci
0fea904dd0 tailwind class source 2025-07-21 14:04:39 +01:00
Giordano Ricci
373aef313f wip: quint 2025-07-21 13:55:21 +01:00
NicoR
a584590ed8 Add changeset 2025-07-18 18:00:50 -03:00
NicoR
0a830e29a9 Prevent resolving discriminated union fields 2025-07-18 17:57:07 -03:00
Guido D'Orsi
efff4d0f4f Merge pull request #2654 from garden-co/fix/vitest-type-tests
fix: restore type tests on Vitest and upgrade Vitest to v3.2.4
2025-07-18 14:13:01 +02:00
Guido D'Orsi
ea2b01d8a2 fix: restore type tests on Vitest and upgrade Vitest to v3.2.4 2025-07-18 12:51:02 +02:00
Guido D'Orsi
55cb83e6e0 Merge pull request #2652 from garden-co/changeset-release/main
Version Packages
2025-07-17 15:26:20 +02:00
github-actions[bot]
6290088fec Version Packages 2025-07-17 13:06:49 +00:00
Guido D'Orsi
b9c17b37db Merge pull request #2651 from garden-co/fix/optional-load
fix: load failures when loading a missing ref declared with z.optional and Schema.optional
2025-07-17 15:04:36 +02:00
Guido D'Orsi
6c76ff8fbf fix: load of missing z.optional and Schema.optional doesn't fail 2025-07-17 14:54:33 +02:00
109 changed files with 2740 additions and 865 deletions

View File

@@ -14,7 +14,7 @@
"tests/jazz-svelte/src/**",
"examples/*svelte*/**",
"starters/*svelte*/**",
"examples/jazz-paper-scissors/src/routeTree.gen.ts",
"examples/server-worker-inbox/src/routeTree.gen.ts",
"homepage/homepage/**",
"**/package.json"
]

View File

@@ -1,5 +1,20 @@
# passkey-svelte
## 0.0.103
### Patch Changes
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- jazz-tools@0.15.14
## 0.0.102
### Patch Changes
- Updated dependencies [6c76ff8]
- jazz-tools@0.15.13
## 0.0.101
### Patch Changes

View File

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

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -94,7 +94,7 @@ const MusicaAccountRoot = co.map({
const MusicaAccount = co.account({
root: MusicaAccountRoot,
profile: co.profile({}),
profile: co.profile(),
});
type MusicaAccount = co.loaded<typeof MusicaAccount>

View File

@@ -67,7 +67,7 @@ export const JazzAccountRoot = co.map({
export const JazzAccount = co
.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
})
.withMigration((account) => {
if (account.root === undefined) {
@@ -130,7 +130,7 @@ const JazzAccountRoot = co.map({
const JazzAccount = co.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
});
// ---cut---

View File

@@ -11,7 +11,7 @@ The main detail to understand when using Jazz server-side is that Server Workers
This lets you share CoValues with Server Workers, having precise access control by adding the Worker to `Groups` with specific roles just like you would with other users.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/jazz-paper-scissors)
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/server-worker-inbox)
<Alert variant="info" className="mt-4 flex gap-2 items-center">Requires at least Node.js v20.</Alert>

View File

@@ -1,5 +1,18 @@
# cojson-storage-indexeddb
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- cojson@0.15.14
## 0.15.13
### Patch Changes
- cojson@0.15.13
## 0.15.12
### Patch Changes

View File

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

View File

@@ -1,5 +1,18 @@
# cojson-storage-sqlite
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- cojson@0.15.14
## 0.15.13
### Patch Changes
- cojson@0.15.13
## 0.15.12
### Patch Changes

View File

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

View File

@@ -1,5 +1,19 @@
# cojson-transport-nodejs-ws
## 0.15.14
### Patch Changes
- 70ce7c5: Introduce the persistent peers. Used to mark the WebSocket connections to the server as persistent, and wait for reconnection before failing load.
- Updated dependencies [70ce7c5]
- cojson@0.15.14
## 0.15.13
### Patch Changes
- cojson@0.15.13
## 0.15.12
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "cojson-transport-ws",
"type": "module",
"version": "0.15.12",
"version": "0.15.14",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",

View File

@@ -165,6 +165,6 @@ export function createWebSocketPeer({
incoming,
outgoing,
role,
deletePeerStateOnClose,
persistent: !deletePeerStateOnClose,
};
}

View File

@@ -1,5 +1,13 @@
# cojson
## 0.15.14
### Patch Changes
- 70ce7c5: Introduce the persistent peers. Used to mark the WebSocket connections to the server as persistent, and wait for reconnection before failing load.
## 0.15.13
## 0.15.12
## 0.15.11

View File

@@ -25,7 +25,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.15.12",
"version": "0.15.14",
"devDependencies": {
"@opentelemetry/sdk-metrics": "^2.0.0",
"libsql": "^0.5.13",

View File

@@ -97,6 +97,10 @@ export class PeerState {
return this.peer.incoming;
}
get persistent() {
return this.peer.persistent;
}
pushOutgoingMessage(msg: SyncMessage) {
this.peer.outgoing.push(msg);
}

View File

@@ -1056,37 +1056,33 @@ export class CoValueCore {
return;
}
const peersToActuallyLoadFrom = [] as PeerState[];
for (const peer of peers) {
const currentState = this.peers.get(peer.id)?.type;
const currentState = this.peers.get(peer.id)?.type ?? "unknown";
if (
!currentState ||
currentState === "unknown" ||
currentState === "unavailable"
) {
peersToActuallyLoadFrom.push(peer);
if (currentState === "unknown" || currentState === "unavailable") {
this.markPending(peer.id);
this.internalLoadFromPeer(peer);
}
}
for (const peer of peersToActuallyLoadFrom) {
this.internalLoadFromPeer(peer);
}
}
internalLoadFromPeer(peer: PeerState) {
if (peer.closed) {
if (peer.closed && !peer.persistent) {
this.markNotFoundInPeer(peer.id);
return;
}
peer.pushOutgoingMessage({
action: "load",
...this.knownState(),
});
peer.trackLoadRequestSent(this.id);
/**
* On reconnection persistent peers will automatically fire the load request
* as part of the reconnection process.
*/
if (!peer.closed) {
peer.pushOutgoingMessage({
action: "load",
...this.knownState(),
});
peer.trackLoadRequestSent(this.id);
}
return new Promise<void>((resolve) => {
const markNotFound = () => {
@@ -1100,7 +1096,9 @@ export class CoValueCore {
};
const timeout = setTimeout(markNotFound, CO_VALUE_LOADING_CONFIG.TIMEOUT);
const removeCloseListener = peer.addCloseListener(markNotFound);
const removeCloseListener = peer.persistent
? undefined
: peer.addCloseListener(markNotFound);
const listener = (state: CoValueCore) => {
const peerState = state.peers.get(peer.id);
@@ -1111,7 +1109,7 @@ export class CoValueCore {
peerState?.type === "unavailable"
) {
this.listeners.delete(listener);
removeCloseListener();
removeCloseListener?.();
clearTimeout(timeout);
resolve();
}

View File

@@ -13,6 +13,14 @@ export const CO_VALUE_LOADING_CONFIG = {
RETRY_DELAY: 3000,
};
export function setCoValueLoadingMaxRetries(maxRetries: number) {
CO_VALUE_LOADING_CONFIG.MAX_RETRIES = maxRetries;
}
export function setCoValueLoadingTimeout(timeout: number) {
CO_VALUE_LOADING_CONFIG.TIMEOUT = timeout;
}
export function setCoValueLoadingRetryDelay(delay: number) {
CO_VALUE_LOADING_CONFIG.RETRY_DELAY = delay;
}

View File

@@ -13,9 +13,11 @@ export function connectedPeers(
{
peer1role = "client",
peer2role = "client",
persistent = false,
}: {
peer1role?: Peer["role"];
peer2role?: Peer["role"];
persistent?: boolean;
} = {},
): [Peer, Peer] {
const from1to2 = new ConnectedPeerChannel();
@@ -26,6 +28,7 @@ export function connectedPeers(
incoming: from2to1,
outgoing: from1to2,
role: peer2role,
persistent,
};
const peer1AsPeer: Peer = {
@@ -33,6 +36,7 @@ export function connectedPeers(
incoming: from1to2,
outgoing: from2to1,
role: peer1role,
persistent,
};
return [peer1AsPeer, peer2AsPeer];

View File

@@ -88,7 +88,7 @@ export interface Peer {
outgoing: OutgoingPeerChannel;
role: "server" | "client";
priority?: number;
deletePeerStateOnClose?: boolean;
persistent?: boolean;
}
export function combinedKnownStates(
@@ -157,8 +157,7 @@ export class SyncManager {
getServerPeers(excludePeerId?: PeerID): PeerState[] {
return this.getPeers().filter(
(peer) =>
peer.role === "server" && peer.id !== excludePeerId && !peer.closed,
(peer) => peer.role === "server" && peer.id !== excludePeerId,
);
}
@@ -353,7 +352,7 @@ export class SyncManager {
unsubscribeFromKnownStatesUpdates();
this.peersCounter.add(-1, { role: peer.role });
if (peer.deletePeerStateOnClose && this.peers[peer.id] === peerState) {
if (!peer.persistent && this.peers[peer.id] === peerState) {
delete this.peers[peer.id];
}
});
@@ -794,8 +793,8 @@ export class SyncManager {
const peerState = this.peers[peerId];
// The peer has been closed, so it isn't possible to sync
if (!peerState || peerState.closed) {
// The peer has been closed and is not persistent, so it isn't possible to sync
if (!peerState) {
return;
}
@@ -830,7 +829,7 @@ export class SyncManager {
return this.local.storage?.waitForSync(id, this.local.getCoValue(id));
}
waitForSync(id: RawCoID, timeout = 30_000) {
waitForSync(id: RawCoID, timeout = 60_000) {
const peers = this.getPeers();
return Promise.all(

View File

@@ -251,9 +251,11 @@ describe("SyncStateManager", () => {
).toEqual({ uploaded: true });
});
test("should skip closed peers", async () => {
test("should skip non-persistent closed peers", async () => {
const client = setupTestNode();
const { peerState } = client.connectToSyncServer();
const { peerState } = client.connectToSyncServer({
persistent: false,
});
peerState.gracefulShutdown();
@@ -263,6 +265,42 @@ describe("SyncStateManager", () => {
await map.core.waitForSync();
});
test("should wait for persistent closed peers to reconnect", async () => {
const client = setupTestNode();
const { peerState } = client.connectToSyncServer({
persistent: true,
});
peerState.gracefulShutdown();
const group = client.node.createGroup();
const map = group.createMap();
const promise = map.core.waitForSync().then(() => "waitForSync");
const result = await Promise.race([
promise,
new Promise((resolve) => {
setTimeout(() => resolve("timeout"), 10);
}),
]);
expect(result).toBe("timeout");
client.connectToSyncServer({
persistent: true,
});
const result2 = await Promise.race([
promise,
new Promise((resolve) => {
setTimeout(() => resolve("timeout"), 10);
}),
]);
expect(result2).toBe("waitForSync");
});
test("should skip client peers that are not subscribed to the coValue", async () => {
const server = setupTestNode({ isSyncServer: true });
const client = setupTestNode();

View File

@@ -1,6 +1,9 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { expectMap } from "../coValue";
import { setCoValueLoadingRetryDelay } from "../config";
import {
CO_VALUE_LOADING_CONFIG,
setCoValueLoadingRetryDelay,
} from "../config";
import { RawCoMap } from "../exports";
import {
SyncMessagesLog,
@@ -59,31 +62,36 @@ describe("loading coValues from server", () => {
connected: true,
});
expect(async () => await node.load("test" as any)).rejects.toThrow(
await expect(async () => await node.load("test" as any)).rejects.toThrow(
"Trying to load CoValue with invalid id test",
);
expect(async () => await node.load(null as any)).rejects.toThrow(
await expect(async () => await node.load(null as any)).rejects.toThrow(
"Trying to load CoValue with invalid id null",
);
expect(async () => await node.load(undefined as any)).rejects.toThrow(
await expect(async () => await node.load(undefined as any)).rejects.toThrow(
"Trying to load CoValue with invalid id undefined",
);
expect(async () => await node.load(1 as any)).rejects.toThrow(
await expect(async () => await node.load(1 as any)).rejects.toThrow(
"Trying to load CoValue with invalid id 1",
);
expect(async () => await node.load({} as any)).rejects.toThrow(
await expect(async () => await node.load({} as any)).rejects.toThrow(
"Trying to load CoValue with invalid id [object Object]",
);
expect(async () => await node.load([] as any)).rejects.toThrow(
await expect(async () => await node.load([] as any)).rejects.toThrow(
"Trying to load CoValue with invalid id []",
);
expect(async () => await node.load(["test"] as any)).rejects.toThrow(
await expect(async () => await node.load(["test"] as any)).rejects.toThrow(
'Trying to load CoValue with invalid id ["test"]',
);
expect(async () => await node.load((() => {}) as any)).rejects.toThrow(
"Trying to load CoValue with invalid id () => {\n }",
);
expect(async () => await node.load(new Date() as any)).rejects.toThrow();
await expect(
async () => await node.load((() => {}) as any),
).rejects.toMatchInlineSnapshot(`
[TypeError: Trying to load CoValue with invalid id () => {
}]
`);
await expect(
async () => await node.load(new Date() as any),
).rejects.toThrow();
});
test("unavailable coValue retry with skipRetry set to true", async () => {
@@ -502,9 +510,12 @@ describe("loading coValues from server", () => {
`);
});
test("should mark the coValue as unavailable if the peer is closed", async () => {
test("should wait for a persistent peer to reconnect before marking the coValue as unavailable", async () => {
const client = setupTestNode();
const { peerState } = client.connectToSyncServer();
const connection1 = client.connectToSyncServer({
persistent: true,
});
connection1.peerState.gracefulShutdown();
const group = jazzCloud.node.createGroup();
group.addMember("everyone", "writer");
@@ -512,13 +523,15 @@ describe("loading coValues from server", () => {
const map = group.createMap({
test: "value",
});
const promise = client.node.load(map.id);
// Close the peer connection
peerState.gracefulShutdown();
await new Promise((resolve) => setTimeout(resolve, 10));
expect(await promise).toEqual("unavailable");
client.connectToSyncServer();
const coValue = await promise;
expect(coValue).not.toBe("unavailable");
expect(
SyncMessagesLog.getMessages({
@@ -528,6 +541,60 @@ describe("loading coValues from server", () => {
).toMatchInlineSnapshot(`
[
"client -> server | LOAD Map sessions: empty",
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
"client -> server | KNOWN Group sessions: header/5",
"client -> server | KNOWN Map sessions: header/1",
]
`);
});
test("should handle reconnections in the middle of a load with a persistent peer", async () => {
const client = setupTestNode();
const connection1 = client.connectToSyncServer({
persistent: true,
});
const group = jazzCloud.node.createGroup();
group.addMember("everyone", "writer");
const map = group.createMap({
test: "value",
});
blockMessageTypeOnOutgoingPeer(connection1.peerOnServer, "content", {
id: map.id,
once: true,
});
const promise = client.node.load(map.id);
await new Promise((resolve) => setTimeout(resolve, 10));
// Close the peer connection
connection1.peerState.gracefulShutdown();
client.connectToSyncServer();
const coValue = await promise;
expect(coValue).not.toBe("unavailable");
expect(
SyncMessagesLog.getMessages({
Group: group.core,
Map: map.core,
}),
).toMatchInlineSnapshot(`
[
"client -> server | LOAD Map sessions: empty",
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
"client -> server | KNOWN Group sessions: header/5",
"client -> server | LOAD Map sessions: empty",
"client -> server | LOAD Group sessions: header/5",
"server -> client | KNOWN Group sessions: header/5",
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
"client -> server | KNOWN Map sessions: header/1",
]
`);
});
@@ -904,4 +971,62 @@ describe("loading coValues from server", () => {
]
`);
});
test("should retry loading from a closed persistent peer after a timeout", async () => {
vi.useFakeTimers();
const client = setupTestNode();
const connection1 = client.connectToSyncServer({
persistent: true,
});
// Close the peer connection
connection1.peerState.gracefulShutdown();
const group = jazzCloud.node.createGroup();
group.addMember("everyone", "reader");
const map = group.createMap();
map.set("hello", "world");
const promise = loadCoValueOrFail(client.node, map.id);
await vi.advanceTimersByTimeAsync(
CO_VALUE_LOADING_CONFIG.TIMEOUT +
CO_VALUE_LOADING_CONFIG.RETRY_DELAY +
10,
);
client.connectToSyncServer({
persistent: true,
});
await vi.advanceTimersByTimeAsync(
CO_VALUE_LOADING_CONFIG.TIMEOUT +
CO_VALUE_LOADING_CONFIG.RETRY_DELAY +
10,
);
const coValue = await promise;
expect(coValue).not.toBe("unavailable");
expect(
SyncMessagesLog.getMessages({
Group: group.core,
Map: map.core,
}),
).toMatchInlineSnapshot(`
[
"client -> server | LOAD Map sessions: empty",
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
"client -> server | KNOWN Group sessions: header/5",
"client -> server | KNOWN Map sessions: header/1",
]
`);
vi.useRealTimers();
});
});

View File

@@ -26,6 +26,7 @@ function setupMesh() {
ourName: "edge-italy",
syncServerName: "core",
syncServer: coreServer.node,
persistent: true,
});
edgeItaly.addStorage({
ourName: "edge-italy",
@@ -36,6 +37,7 @@ function setupMesh() {
ourName: "edge-france",
syncServerName: "core",
syncServer: coreServer.node,
persistent: true,
});
edgeFrance.addStorage({
ourName: "edge-france",

View File

@@ -111,10 +111,12 @@ test("Can sync a coValue with private transactions through a server to another c
expect(mapOnClient2.get("hello")).toEqual("world");
});
test("should keep the peer state when the peer closes", async () => {
test("should keep the peer state when the peer closes if persistent is true", async () => {
const client = setupTestNode();
const { peer, peerState, peerOnServer } = client.connectToSyncServer();
const { peer, peerState, peerOnServer } = client.connectToSyncServer({
persistent: true,
});
const group = jazzCloud.node.createGroup();
const map = group.createMap();
@@ -133,12 +135,12 @@ test("should keep the peer state when the peer closes", async () => {
expect(syncManager.peers[peer.id]).not.toBeUndefined();
});
test("should delete the peer state when the peer closes if deletePeerStateOnClose is true", async () => {
test("should delete the peer state when the peer closes if persistent is false", async () => {
const client = setupTestNode();
const { peer, peerState, peerOnServer } = client.connectToSyncServer();
peer.deletePeerStateOnClose = true;
const { peer, peerState, peerOnServer } = client.connectToSyncServer({
persistent: false,
});
const group = jazzCloud.node.createGroup();
const map = group.createMap();

View File

@@ -429,6 +429,7 @@ export function getSyncServerConnectedPeer(opts: {
ourName?: string;
syncServer?: LocalNode;
peerId: string;
persistent?: boolean;
}) {
const currentSyncServer = opts?.syncServer ?? syncServer.current;
@@ -451,6 +452,7 @@ export function getSyncServerConnectedPeer(opts: {
role: "client",
name: opts.ourName,
},
persistent: opts?.persistent,
});
currentSyncServer.syncManager.addPeer(peer2);
@@ -483,6 +485,7 @@ export function setupTestNode(
syncServerName?: string;
ourName?: string;
syncServer?: LocalNode;
persistent?: boolean;
}) {
const { peer, peerStateOnServer, peerOnServer } =
getSyncServerConnectedPeer({
@@ -490,6 +493,7 @@ export function setupTestNode(
syncServerName: opts?.syncServerName,
ourName: opts?.ourName,
syncServer: opts?.syncServer,
persistent: opts?.persistent,
});
node.syncManager.addPeer(peer);
@@ -646,12 +650,22 @@ export type SyncTestMessage = {
export function connectedPeersWithMessagesTracking(opts: {
peer1: { id: string; role: Peer["role"]; name?: string };
peer2: { id: string; role: Peer["role"]; name?: string };
persistent?: boolean;
}) {
const [peer1, peer2] = connectedPeers(opts.peer1.id, opts.peer2.id, {
peer1role: opts.peer1.role,
peer2role: opts.peer2.role,
persistent: opts.persistent,
});
// If the persistent option is not provided, we default to true for the server and false for the client
// Trying to mimic the real world behavior of the sync server
if (opts.persistent === undefined) {
peer1.persistent = opts.peer1.role === "server";
peer2.persistent = opts.peer2.role === "server";
}
const peer1Push = peer1.outgoing.push;
peer1.outgoing.push = (msg) => {
if (typeof msg !== "string") {

View File

@@ -7890,7 +7890,7 @@ export const JazzAccountRoot = co.map({
export const JazzAccount = co
.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
})
.withMigration((account) => {
if (account.root === undefined) {
@@ -7950,7 +7950,7 @@ const JazzAccountRoot = co.map({
const JazzAccount = co.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
});
// ---cut---

View File

@@ -1,5 +1,25 @@
# jazz-auth-betterauth
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson@0.15.14
- jazz-tools@0.15.14
- jazz-betterauth-client-plugin@0.15.14
## 0.15.13
### Patch Changes
- Updated dependencies [6c76ff8]
- jazz-tools@0.15.13
- jazz-betterauth-client-plugin@0.15.13
- cojson@0.15.13
## 0.15.12
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-auth-betterauth",
"version": "0.15.12",
"version": "0.15.14",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,17 @@
# jazz-betterauth-client-plugin
## 0.15.14
### Patch Changes
- jazz-betterauth-server-plugin@0.15.14
## 0.15.13
### Patch Changes
- jazz-betterauth-server-plugin@0.15.13
## 0.15.12
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-betterauth-client-plugin",
"version": "0.15.12",
"version": "0.15.14",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,23 @@
# jazz-betterauth-server-plugin
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson@0.15.14
- jazz-tools@0.15.14
## 0.15.13
### Patch Changes
- Updated dependencies [6c76ff8]
- jazz-tools@0.15.13
- cojson@0.15.13
## 0.15.12
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-betterauth-server-plugin",
"version": "0.15.12",
"version": "0.15.14",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,27 @@
# jazz-react-auth-betterauth
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson@0.15.14
- jazz-tools@0.15.14
- jazz-auth-betterauth@0.15.14
- jazz-betterauth-client-plugin@0.15.14
## 0.15.13
### Patch Changes
- Updated dependencies [6c76ff8]
- jazz-tools@0.15.13
- jazz-auth-betterauth@0.15.13
- jazz-betterauth-client-plugin@0.15.13
- cojson@0.15.13
## 0.15.12
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-react-auth-betterauth",
"version": "0.15.12",
"version": "0.15.14",
"type": "module",
"main": "dist/index.js",
"types": "src/index.tsx",

View File

@@ -1,5 +1,27 @@
# jazz-run
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson-transport-ws@0.15.14
- cojson@0.15.14
- jazz-tools@0.15.14
- cojson-storage-sqlite@0.15.14
## 0.15.13
### Patch Changes
- Updated dependencies [6c76ff8]
- jazz-tools@0.15.13
- cojson@0.15.13
- cojson-storage-sqlite@0.15.13
- cojson-transport-ws@0.15.13
## 0.15.12
### Patch Changes

View File

@@ -3,7 +3,7 @@
"bin": "./dist/index.js",
"type": "module",
"license": "MIT",
"version": "0.15.12",
"version": "0.15.14",
"exports": {
"./startSyncServer": {
"import": "./dist/startSyncServer.js",
@@ -28,11 +28,11 @@
"@effect/printer-ansi": "^0.34.5",
"@effect/schema": "^0.71.1",
"@effect/typeclass": "^0.25.5",
"cojson": "workspace:0.15.12",
"cojson-storage-sqlite": "workspace:0.15.12",
"cojson-transport-ws": "workspace:0.15.12",
"cojson": "workspace:0.15.14",
"cojson-storage-sqlite": "workspace:0.15.14",
"cojson-transport-ws": "workspace:0.15.14",
"effect": "^3.6.5",
"jazz-tools": "workspace:0.15.12",
"jazz-tools": "workspace:0.15.14",
"ws": "^8.14.2"
},
"devDependencies": {

View File

@@ -230,8 +230,7 @@ describe("startWorker integration", () => {
await worker2.done();
});
// Flaky test, fails randomly on CI
test.skip("worker reconnects when sync server is closed and reopened", async () => {
test("worker reconnects when sync server is closed and reopened", async () => {
const worker1 = await setup();
const worker2 = await setupWorker(worker1.syncServer);
@@ -267,10 +266,6 @@ describe("startWorker integration", () => {
db: "",
});
// Wait for reconnection
await worker1.waitForConnection();
await worker2.waitForConnection();
await worker1.worker.waitForAllCoValuesSync();
// Verify both old and new values are synced

View File

@@ -1,5 +1,25 @@
# jazz-tools
## 0.15.14
### Patch Changes
- a584590: Prevent resolving discriminated union fields
- 9acccb5: Export `WithHelpers` type used in CoValue schemas
- Updated dependencies [70ce7c5]
- cojson-transport-ws@0.15.14
- cojson@0.15.14
- cojson-storage-indexeddb@0.15.14
## 0.15.13
### Patch Changes
- 6c76ff8: Fix load failures when loading a missing ref declared with z.optional and Schema.optional
- cojson@0.15.13
- cojson-storage-indexeddb@0.15.13
- cojson-transport-ws@0.15.13
## 0.15.12
### Patch Changes

View File

@@ -139,7 +139,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.15.12",
"version": "0.15.14",
"dependencies": {
"@manuscripts/prosemirror-recreate-steps": "^0.1.4",
"@scure/base": "1.2.1",

View File

@@ -2,11 +2,23 @@ import { SessionID } from "cojson";
import { ItemsSym } from "../internal.js";
import { type Account } from "./account.js";
import { CoFeedEntry } from "./coFeed.js";
import { type CoKeys, type CoMap } from "./coMap.js";
import { type CoKeys } from "./coMap.js";
import { type CoValue, type ID } from "./interfaces.js";
type NotNull<T> = Exclude<T, null>;
/**
* Used to check if T is a union type.
*
* If T is a union type, the left hand side of the extends becomes a union of function types.
* The right hand side is always a single function type.
*/
type IsUnion<T, U = T> = (T extends any ? (x: T) => void : never) extends (
x: U,
) => void
? false
: true;
export type RefsToResolve<
V,
DepthLimit extends number = 10,
@@ -16,56 +28,58 @@ export type RefsToResolve<
| (DepthLimit extends CurrentDepth["length"]
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
any
: // Basically V extends CoList - but if we used that we'd introduce circularity into the definition of CoList itself
V extends Array<infer Item>
?
| {
$each: RefsToResolve<
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
| boolean
: // Basically V extends CoMap | Group | Account - but if we used that we'd introduce circularity into the definition of CoMap itself
V extends { _type: "CoMap" | "Group" | "Account" }
: IsUnion<NonNullable<V>> extends true
? true
: // Basically V extends CoList - but if we used that we'd introduce circularity into the definition of CoList itself
V extends Array<infer Item>
?
| ({
[Key in CoKeys<V> as NonNullable<V[Key]> extends CoValue
? Key
: never]?: RefsToResolve<
NonNullable<V[Key]>,
| {
$each: RefsToResolve<
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
} & { $onError?: null })
| (ItemsSym extends keyof V
? {
$onError?: null;
}
| boolean
: // Basically V extends CoMap | Group | Account - but if we used that we'd introduce circularity into the definition of CoMap itself
V extends { _type: "CoMap" | "Group" | "Account" }
?
| ({
[Key in CoKeys<V> as NonNullable<V[Key]> extends CoValue
? Key
: never]?: RefsToResolve<
NonNullable<V[Key]>,
DepthLimit,
[0, ...CurrentDepth]
>;
} & { $onError?: null })
| (ItemsSym extends keyof V
? {
$each: RefsToResolve<
NonNullable<V[ItemsSym]>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
: never)
| boolean
: V extends {
_type: "CoStream";
byMe: CoFeedEntry<infer Item> | undefined;
}
?
| {
$each: RefsToResolve<
NonNullable<V[ItemsSym]>,
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
: never)
| boolean
: V extends {
_type: "CoStream";
byMe: CoFeedEntry<infer Item> | undefined;
}
?
| {
$each: RefsToResolve<
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
| boolean
: boolean);
| boolean
: boolean);
export type RefsToResolveStrict<T, V> = V extends RefsToResolve<T>
? RefsToResolve<T>

View File

@@ -35,6 +35,7 @@ export type {
TextPos,
AccountClass,
AccountCreationProps,
WithHelpers,
} from "./internal.js";
export {

View File

@@ -43,27 +43,35 @@ type SchemaField =
| z.core.$ZodCatch<z.core.$ZodType>
| (z.core.$ZodCustom<any, any> & { builtin: any });
export function schemaFieldToCoFieldDef(schema: SchemaField) {
export function schemaFieldToCoFieldDef(
schema: SchemaField,
isOptional = false,
) {
if (isCoValueClass(schema)) {
return coField.ref(schema);
if (isOptional) {
return coField.ref(schema, { optional: true });
} else {
return coField.ref(schema);
}
} else if (isCoValueSchema(schema)) {
if (isAnyCoOptionalSchema(schema)) {
return coField.ref(schema.getCoValueClass(), {
optional: true,
});
}
return coField.ref(schema.getCoValueClass());
if (isOptional) {
return coField.ref(schema.getCoValueClass(), { optional: true });
} else {
return coField.ref(schema.getCoValueClass());
}
} else {
if ("_zod" in schema) {
if (schema._zod.def.type === "optional") {
const inner = zodSchemaToCoSchemaOrKeepPrimitive(
schema._zod.def.innerType,
);
if (isCoValueClass(inner)) {
return coField.ref(inner, { optional: true });
} else {
return schemaFieldToCoFieldDef(inner);
}
return schemaFieldToCoFieldDef(inner, true);
} else if (schema._zod.def.type === "string") {
return coField.string;
} else if (schema._zod.def.type === "number") {
@@ -77,6 +85,7 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
} else if (schema._zod.def.type === "readonly") {
return schemaFieldToCoFieldDef(
(schema as unknown as ZodReadonly).def.innerType as SchemaField,
isOptional,
);
} else if (schema._zod.def.type === "date") {
return coField.optional.Date;
@@ -86,6 +95,7 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
// Mostly to support z.json()
return schemaFieldToCoFieldDef(
(schema as unknown as ZodLazy).unwrap() as SchemaField,
isOptional,
);
} else if (
schema._zod.def.type === "default" ||
@@ -98,6 +108,7 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
return schemaFieldToCoFieldDef(
(schema as unknown as ZodDefault | ZodCatch).def
.innerType as SchemaField,
isOptional,
);
} else if (schema._zod.def.type === "literal") {
if (
@@ -129,7 +140,7 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
return coField.json();
} else if (schema._zod.def.type === "custom") {
if ("builtin" in schema) {
return schemaFieldToCoFieldDef(schema.builtin);
return schemaFieldToCoFieldDef(schema.builtin, isOptional);
} else {
throw new Error(`Unsupported custom zod type`);
}
@@ -137,9 +148,13 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
if (isUnionOfPrimitivesDeeply(schema)) {
return coField.json();
} else if (isUnionOfCoMapsDeeply(schema)) {
return coField.ref<CoValueClass<CoMap>>(
schemaUnionDiscriminatorFor(schema),
);
const result = schemaUnionDiscriminatorFor(schema);
if (isOptional) {
return coField.ref<CoValueClass<CoMap>>(result, { optional: true });
} else {
return coField.ref<CoValueClass<CoMap>>(result);
}
} else {
throw new Error(
"z.union()/z.discriminatedUnion() of mixed collaborative and non-collaborative types is not supported",

View File

@@ -381,6 +381,64 @@ test("The resolve type doesn't accept extra keys", async () => {
}
});
test("The resolve type accepts keys from optional fields", async () => {
const Person = co.map({
name: z.string(),
});
const Dog = co.map({
type: z.literal("dog"),
owner: Person.optional(),
});
const Pets = co.list(Dog);
const pets = await Pets.create([
Dog.create({ type: "dog", owner: Person.create({ name: "Rex" }) }),
]);
await pets.ensureLoaded({
resolve: {
$each: { owner: true },
},
});
expect(pets[0]?.owner?.name).toEqual("Rex");
});
test("The resolve type doesn't accept keys from discriminated unions", async () => {
const Person = co.map({
name: z.string(),
});
const Dog = co.map({
type: z.literal("dog"),
owner: Person,
});
const Cat = co.map({
type: z.literal("cat"),
});
const Pet = co.discriminatedUnion("type", [Dog, Cat]);
const Pets = co.list(Pet);
const pets = await Pets.create([
Dog.create({ type: "dog", owner: Person.create({ name: "Rex" }) }),
]);
await pets.ensureLoaded({
resolve: {
$each: true,
},
});
await pets.ensureLoaded({
// @ts-expect-error cannot resolve owner
resolve: { $each: { owner: true } },
});
expect(pets).toBeTruthy();
if (pets?.[0]?.type === "dog") {
expect(pets[0].owner?.name).toEqual("Rex");
}
});
describe("Deep loading with unauthorized account", async () => {
const bob = await createJazzTestAccount({
creationProps: { name: "Bob" },

View File

@@ -42,6 +42,120 @@ test("return null if id is invalid", async () => {
expect(john).toBeNull();
});
test("load a missing optional value (co.optional)", async () => {
const Dog = co.map({
name: z.string(),
});
const Person = co.map({
name: z.string(),
dog: co.optional(Dog),
});
const group = Group.create();
const map = Person.create({ name: "John" }, group);
group.addMember("everyone", "reader");
const alice = await createJazzTestAccount();
const john = await Person.load(map.id, {
loadAs: alice,
resolve: { dog: true },
});
assert(john);
expect(john.name).toBe("John");
expect(john.dog).toBeUndefined();
});
test("load a missing optional value (z.optional)", async () => {
const Dog = co.map({
name: z.string(),
});
const Person = co.map({
name: z.string(),
dog: z.optional(Dog),
});
const group = Group.create();
const map = Person.create({ name: "John" }, group);
group.addMember("everyone", "reader");
const alice = await createJazzTestAccount();
const john = await Person.load(map.id, {
loadAs: alice,
resolve: { dog: true },
});
assert(john);
expect(john.name).toBe("John");
expect(john.dog).toBeUndefined();
});
test("load a missing optional value (Schema.optional)", async () => {
const Dog = co.map({
name: z.string(),
});
const Person = co.map({
name: z.string(),
dog: Dog.optional(),
});
const group = Group.create();
const map = Person.create({ name: "John" }, group);
group.addMember("everyone", "reader");
const alice = await createJazzTestAccount();
const john = await Person.load(map.id, {
loadAs: alice,
resolve: { dog: true },
});
assert(john);
expect(john.name).toBe("John");
expect(john.dog).toBeUndefined();
});
test("load a missing optional value (optional discrminatedUnion)", async () => {
const Dog = co.map({
type: z.literal("dog"),
name: z.string(),
});
const Cat = co.map({
type: z.literal("cat"),
name: z.string(),
});
const Person = co.map({
name: z.string(),
pet: co.discriminatedUnion("type", [Dog, Cat]).optional(),
});
const group = Group.create();
const map = Person.create({ name: "John" }, group);
group.addMember("everyone", "reader");
const alice = await createJazzTestAccount();
const john = await Person.load(map.id, {
loadAs: alice,
resolve: { pet: true },
});
assert(john);
expect(john.name).toBe("John");
expect(john.pet).toBeUndefined();
});
test("retry an unavailable value", async () => {
const Person = co.map({
name: z.string(),

View File

@@ -1,22 +1,16 @@
import { defineConfig } from "vitest/config";
import { defineProject } from "vitest/config";
export default defineConfig({
export default defineProject({
test: {
workspace: [
{
test: {
typecheck: {
enabled: true,
checker: "tsc",
},
include: ["src/**/*.test.ts"],
name: "unit",
},
},
name: "jazz-tools",
typecheck: {
enabled: true,
checker: "tsc",
},
projects: [
{
test: {
include: ["src/**/*.test.browser.ts"],
name: "browser",
browser: {
enabled: true,
provider: "playwright",
@@ -24,6 +18,13 @@ export default defineConfig({
screenshotFailures: false,
instances: [{ browser: "chromium" }],
},
name: "browser",
},
},
{
test: {
include: ["src/**/*.test.{js,ts,svelte}"],
name: "unit",
},
},
],

1
packages/quint-ui/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
next-env.d.ts

View File

@@ -0,0 +1,7 @@
# quint-ui
## 0.0.1
### Patch Changes
- f61a120: Initial Quint-UI release

View File

@@ -0,0 +1,69 @@
# Quint UI
## Installation
```sh
pnpm add quint-ui tailwindcss
```
then in your tailwind styles (ie. `styles.css`) add:
```css
@import "quint-ui/styles.css";
@source "../node_modules/quint-ui";
```
> [!TIP]
> There's no need to do
> ```css
> @import "tailwindcss";
> ```
> Quint does this for you.
### Usage
```tsx
import { Button } from "quint-ui";
<Button>Click me</Button>
```
## Development workflow
### In Quint
In this directory, run:
```sh
pnpm dev:docs
```
This starts a nextjs app you can use to write docs and/or test components.
### In example apps
In this directory, run:
```sh
pnpm dev
```
This starts typescript in watch mode.
> [!IMPORTANT]
> Make sure quint-ui is installed in the example via
> ```json
> "dependencies": {
> "quint-ui": "workspace:*"
> }
> ```
## Building
### Docs website
```sh
pnpm build:docs
```
### Package
```sh
pnpm build
```

View File

@@ -0,0 +1,25 @@
import { Button } from "@/src/components/button";
export default function ButtonDocsPage() {
return (
<div>
<h1 className="text-2xl font-bold">Button</h1>
<div className="flex gap-2">
<Button intent="primary">Default</Button>
<Button intent="primary" variant="outline">
Outline
</Button>
<Button intent="primary" variant="ghost">
Ghost
</Button>
<Button intent="primary" variant="link">
Link
</Button>
<Button intent="primary" variant="inverted">
Inverted
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import Link from "next/link";
export default function DocsPage() {
return (
<div>
<h1 className="text-2xl font-bold">Components</h1>
<ul>
<li>
<Link href="/docs/button">Button</Link>
</li>
</ul>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import "@/src/globals.css";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<title>Quint UI</title>
</head>
<body>
<div className="container mx-auto">{children}</div>
</body>
</html>
);
}

View File

@@ -0,0 +1,22 @@
import { Button } from "@/src/components/button";
import { BookIcon } from "lucide-react";
import Link from "next/link";
export default function Home() {
return (
<main className="flex flex-col gap-8">
<h1 className="text-3xl font-bold tracking-tight">Quint UI</h1>
{/* Example usage of the button component with an icon and rendered as a nextjs link */}
<Button
variant="default"
intent="primary"
size="lg"
render={<Link href="/docs" />}
>
<BookIcon />
Read the docs!
</Button>
</main>
);
}

View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
export default {
typescript: {
ignoreBuildErrors: true,
},
};

View File

@@ -0,0 +1,44 @@
{
"name": "quint-ui",
"version": "0.0.1",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./styles.css": "./src/globals.css"
},
"scripts": {
"dev": "pnpm clean && tsc --project tsconfig.build.json --watch",
"dev:website": "next dev --turbopack",
"build": "pnpm clean && tsc --project tsconfig.build.json",
"build:website": "next build",
"clean": "rm -rf ./dist",
"start": "next start"
},
"dependencies": {
"@base-ui-components/react": "1.0.0-beta.1",
"clsx": "^2.1.1",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^1.0.0",
"tw-animate-css": "^1.3.5"
},
"peerDependencies": {
"react": ">=19.0.0",
"react-dom": ">=19.0.0",
"tailwindcss": ">=4.0.0"
},
"devDependencies": {
"next": "15.4.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"@tailwindcss/postcss": "^4.1.11",
"@types/node": "^22.16.5",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"lucide-react": "^0.525.0",
"tailwindcss": "^4.1.11",
"typescript": "^5.8.3"
}
}

View File

@@ -0,0 +1,6 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -0,0 +1,160 @@
import { mergeProps } from "@base-ui-components/react/merge-props";
import { useRender } from "@base-ui-components/react/use-render";
import { type VariantProps, tv } from "tailwind-variants";
const button = tv({
base: "inline-flex items-center justify-center gap-2 rounded-sm text-center transition-colors w-fit text-nowrap disabled:pointer-events-none disabled:opacity-70 disabled:cursor-not-allowed disabled:pointer-events-none font-medium",
variants: {
variant: {
default:
"focus:outline-none focus-visible:ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-opacity-10",
ghost: "",
outline: "shadow-sm border",
link: "",
inverted: "",
},
intent: {
default: "border-secondary",
primary: "border-primary",
success: "border-success",
danger: "border-danger",
},
size: {
sm: "text-sm py-1 px-2 [&>svg]:size-4",
md: "py-1.5 px-3 h-[36px] [&>svg]:size-4",
lg: "py-2 px-5 md:px-6 md:py-2.5 [&>svg]:size-5",
icon: "p-2 [&>svg]:size-4",
},
},
compoundVariants: [
// Default Variants
{
variant: "default",
intent: ["primary", "success", "danger", "default"],
className: "bg-gradient-to-t from-7% via-50% to-95%",
},
{
variant: "default",
intent: "primary",
className: [
"text-primary-foreground",
"from-primary-dark via-primary to-primary-light",
"hover:from-primary-brightLight hover:to-primary-light",
"active:from-primary-brightDark active:to-primary-light active:border-primary-transparent ",
"focus:ring-primary focus:border-primary-transparent",
],
},
{
variant: "default",
intent: "success",
className: [
"text-success-foreground",
"from-success-dark via-success to-success-light",
"hover:from-success-brightLight hover:to-success-light",
"active:from-success-brightDark active:to-success-light",
"focus:ring-success",
],
},
{
variant: "default",
intent: "danger",
className: [
"text-danger-foreground",
"from-danger-dark via-danger to-danger-light",
"hover:from-danger-brightLight hover:to-danger-light",
"active:from-danger-brightDark active:to-danger-light",
"focus:ring-danger",
],
},
// Outline Variants
{
variant: "outline",
intent: "primary",
className: [
"text-primary",
"active:bg-primary/30",
"hover:text-primary-light hover:border-primary-light",
],
},
{
variant: "outline",
intent: "danger",
className: [
"text-danger",
"active:bg-danger/30",
"hover:text-danger-light hover:border-danger-light",
],
},
{
variant: "outline",
intent: "success",
className: [
"text-success-dark",
"active:bg-success/30",
"hover:text-success-light hover:border-success-light",
],
},
// Ghost Variants
{
variant: "ghost",
intent: "primary",
className: [
"text-primary",
"hover:bg-primary/10",
"active:bg-primary/30",
],
},
{
variant: "ghost",
intent: "primary",
className: [
"text-primary",
"hover:bg-primary/10",
"active:bg-primary/30",
],
},
{
variant: "ghost",
intent: "danger",
className: ["text-danger", "hover:bg-danger/10", "active:bg-danger/30"],
},
{
variant: "ghost",
intent: "success",
className: [
"text-success",
"hover:bg-success/10",
"active:bg-success/30",
],
},
],
defaultVariants: {
size: "md",
variant: "default",
intent: "default",
},
});
type ButtonVariants = VariantProps<typeof button>;
interface ButtonProps
extends Omit<useRender.ComponentProps<"button">, keyof ButtonVariants>,
ButtonVariants {}
export function Button({ render = <button />, ...props }: ButtonProps) {
const element = useRender({
render,
props: mergeProps<"button">(
{
className: button({
size: props.size,
variant: props.variant,
intent: props.intent,
}),
},
props,
),
});
return element;
}

View File

@@ -0,0 +1,152 @@
@import "tailwindcss";
@import "tw-animate-css";
/* Dark mode. If you want to use dark mode, you can add the dark class to the body/head element. */
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-primary-transparent: var(--primary-transparent);
--color-primary-dark: var(--primary-dark);
--color-primary-light: var(--primary-light);
--color-primary-brightLight: var(--primary-brightLight);
--color-primary-brightDark: var(--primary-brightDark);
--color-alert: var(--alert);
--color-alert-foreground: var(--alert-foreground);
--color-alert-transparent: var(--alert-transparent);
--color-alert-dark: var(--alert-dark);
--color-alert-light: var(--alert-light);
--color-alert-brightLight: var(--alert-brightLight);
--color-alert-brightDark: var(--alert-brightDark);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-success-transparent: var(--success-transparent);
--color-success-dark: var(--success-dark);
--color-success-light: var(--success-light);
--color-success-brightLight: var(--success-brightLight);
--color-success-brightDark: var(--success-brightDark);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-info-transparent: var(--info-transparent);
--color-info-dark: var(--info-dark);
--color-info-light: var(--info-light);
--color-info-brightLight: var(--info-brightLight);
--color-info-brightDark: var(--info-brightDark);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-warning-transparent: var(--warning-transparent);
--color-warning-light: var(--warning-light);
--color-warning-dark: var(--warning-dark);
--color-warning-brightLight: var(--warning-brightLight);
--color-warning-brightDark: var(--warning-brightDark);
--color-tip: var(--tip);
--color-tip-foreground: var(--tip-foreground);
--color-tip-transparent: var(--tip-transparent);
--color-tip-dark: var(--tip-dark);
--color-tip-light: var(--tip-light);
--color-tip-brightLight: var(--tip-brightLight);
--color-tip-brightDark: var(--tip-brightDark);
--color-danger: var(--danger);
--color-danger-foreground: var(--danger-foreground);
--color-danger-transparent: var(--danger-transparent);
--color-danger-dark: var(--danger-dark);
--color-danger-light: var(--danger-light);
--color-danger-brightLight: var(--danger-brightLight);
--color-danger-brightDark: var(--danger-brightDark);
}
:root {
--primary: oklch(57.29% 0.235 261.2);
--primary-transparent: lch(from var(--color-primary) l c h / 0.1);
--primary-dark: lch(
from var(--color-primary) calc(l - 10) calc(c - 1) calc(h + 5)
);
--primary-light: lch(
from var(--color-primary) calc(l + 10) calc(c + 1) calc(h - 5)
);
--primary-brightLight: lch(
from var(--color-primary) calc(l - 1) calc(c + 20) calc(h + 5)
);
--primary-brightDark: lch(
from var(--color-primary) calc(l - 6) calc(c + 20) calc(h + 5)
);
--primary-foreground: oklch(0.985 0.045 264.32);
--alert: oklch(80.54% 0.169 76.45);
--alert-transparent: oklch(0.843 0.222 85.04 / 0.3);
--alert-dark: oklch(0.773 0.212 90.04);
--alert-light: oklch(0.883 0.232 80.04);
--alert-brightLight: oklch(0.833 0.522 100.04);
--alert-brightDark: oklch(0.803 0.522 100.04);
--alert-foreground: oklch(0.28 0.07 46);
--success: oklch(70.49% 0.16 150.62);
--success-transparent: lch(from var(--color-success) l c h / 0.3);
--success-dark: lch(
from var(--color-success) calc(l - 7) calc(c - 1) calc(h + 5)
);
--success-light: lch(
from var(--color-success) calc(l + 4) calc(c + 1) calc(h - 5)
);
--success-brightLight: lch(
from var(--color-success) calc(l - 1) calc(c + 20) calc(h + 10)
);
--success-brightDark: lch(
from var(--color-success) calc(l - 6) calc(c + 20) calc(h + 10)
);
--success-foreground: oklch(0.28 0.07 46);
--info: oklch(61.13% 0.248 312.2);
--info-transparent: oklch(0.514 0.247 291.21 / 0.3);
--info-dark: oklch(0.444 0.237 296.21);
--info-light: oklch(0.554 0.257 286.21);
--info-brightLight: oklch(0.504 0.447 296.21);
--info-brightDark: oklch(0.474 0.447 296.21);
--info-foreground: oklch(0.28 0.07 46);
--warning: oklch(68.92% 0.206 39.71);
--warning-transparent: oklch(0.661 0.372 75.45 / 0.3);
--warning-dark: oklch(0.591 0.362 80.45);
--warning-light: oklch(0.701 0.382 70.45);
--warning-brightLight: oklch(0.651 0.672 90.45);
--warning-brightDark: oklch(0.621 0.672 90.45);
--warning-foreground: oklch(0.28 0.07 46);
--tip: oklch(76.08% 0.122 194.9);
--tip-transparent: oklch(0.595 0.317 176.3 / 0.3);
--tip-dark: oklch(0.525 0.307 181.3);
--tip-light: oklch(0.635 0.327 171.3);
--tip-brightLight: oklch(0.585 0.617 186.3);
--tip-brightDark: oklch(0.555 0.617 186.3);
--tip-foreground: oklch(0.28 0.07 46);
--danger: oklch(64.11% 0.202 23.95);
--danger-transparent: lch(from var(--color-danger) l c h / 0.3);
--danger-dark: lch(
from var(--color-danger) calc(l - 7) calc(c - 1) calc(h + 5)
);
--danger-light: lch(
from var(--color-danger) calc(l + 4) calc(c + 1) calc(h - 5)
);
--danger-brightLight: lch(
from var(--color-danger) calc(l - 2) calc(c + 20) calc(h + 10)
);
--danger-brightDark: lch(
from var(--color-danger) calc(l - 6) calc(c + 10) calc(h + 10)
);
--danger-foreground: oklch(0.28 0.07 46);
}
.dark {
/*
* Dark mode styles.
* Here you can override the css variables above for dark mode.
*/
}

View File

@@ -0,0 +1,5 @@
// COMPONENTS
export { Button } from "./components/button.js";
// UTILS
export { cn } from "./lib/utils.js";

View File

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

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"jsx": "react-jsx",
"outDir": "./dist"
},
"include": ["src"]
}

View File

@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"react": ["./node_modules/@types/react"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next.config.mjs"
],
"exclude": ["node_modules"]
}

1960
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ catalog:
"@biomejs/biome": 1.9.4
typescript: 5.6.2
vite: 6.3.5
vitest: 3.1.3
"@vitest/browser": 3.1.3
"@vitest/coverage-istanbul": 3.1.3
"@vitest/coverage-v8": 3.1.3
"@vitest/ui": 3.1.3
vitest: 3.2.4
"@vitest/browser": 3.2.4
"@vitest/coverage-istanbul": 3.2.4
"@vitest/coverage-v8": 3.2.4
"@vitest/ui": 3.2.4

View File

@@ -1,5 +1,20 @@
# jazz-react-tailwind-starter
## 0.0.134
### Patch Changes
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- jazz-tools@0.15.14
## 0.0.133
### Patch Changes
- Updated dependencies [6c76ff8]
- jazz-tools@0.15.13
## 0.0.132
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-react-passkey-auth-starter",
"private": true,
"version": "0.0.132",
"version": "0.0.134",
"type": "module",
"scripts": {
"dev": "vite",

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