Compare commits

..

3 Commits

Author SHA1 Message Date
Trisha Lim
d73896b24c missing highlight 2025-04-01 16:28:09 +07:00
Trisha Lim
1ab581358e replace focus styles with green 2025-04-01 16:13:19 +07:00
Trisha Lim
9fca21a3f8 remove line highlight for old lines, add highlight for focus 2025-04-01 16:12:13 +07:00
126 changed files with 862 additions and 1781 deletions

View File

@@ -1,5 +0,0 @@
---
"cojson": patch
---
Correctly load CoValues after they are marked as unavailable and improve timeout management

View File

@@ -0,0 +1,5 @@
---
"multiauth": patch
---
Use the new Resolve API

4
.gitignore vendored
View File

@@ -24,6 +24,4 @@ test-results
.vscode/settings.json
.svelte-kit
.idea
.svelte-kit

View File

@@ -1,14 +1,5 @@
# chat-rn-clerk
## 1.0.92
### Patch Changes
- jazz-react-native@0.12.2
- jazz-react-native-auth-clerk@0.12.2
- jazz-tools@0.12.2
- jazz-react-native-media-images@0.12.2
## 1.0.91
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "chat-rn-clerk",
"main": "index.js",
"version": "1.0.92",
"version": "1.0.91",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",

View File

@@ -1,12 +1,5 @@
# chat-rn
## 1.0.88
### Patch Changes
- jazz-react-native@0.12.2
- jazz-tools@0.12.2
## 1.0.87
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn",
"version": "1.0.88",
"version": "1.0.87",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",

View File

@@ -1,14 +1,5 @@
# chat-vue
## 0.0.73
### Patch Changes
- Updated dependencies [cc684eb]
- jazz-browser@0.12.2
- jazz-vue@0.12.2
- jazz-tools@0.12.2
## 0.0.72
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-vue",
"version": "0.0.73",
"version": "0.0.72",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,14 +1,5 @@
# jazz-example-chat
## 0.0.170
### Patch Changes
- Updated dependencies [8a71835]
- jazz-inspector@0.12.2
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.169
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.170",
"version": "0.0.169",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,13 +1,5 @@
# minimal-auth-clerk
## 0.0.69
### Patch Changes
- jazz-react@0.12.2
- jazz-react-auth-clerk@0.12.2
- jazz-tools@0.12.2
## 0.0.68
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "clerk",
"private": true,
"version": "0.0.69",
"version": "0.0.68",
"type": "module",
"scripts": {
"dev": "vite",
@@ -13,7 +13,7 @@
"dependencies": {
"@clerk/clerk-react": "^5.4.1",
"jazz-react": "workspace:*",
"jazz-react-auth-clerk": "workspace:0.12.2",
"jazz-react-auth-clerk": "workspace:0.12.1",
"jazz-tools": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1"

View File

@@ -1,12 +1,5 @@
# file-share-svelte
## 0.0.53
### Patch Changes
- jazz-svelte@0.12.2
- jazz-tools@0.12.2
## 0.0.52
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "file-share-svelte",
"version": "0.0.53",
"version": "0.0.52",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,12 +1,5 @@
# jazz-tailwind-demo-auth-starter
## 0.0.9
### Patch Changes
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.8
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "filestream",
"private": true,
"version": "0.0.9",
"version": "0.0.8",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,12 +1,5 @@
# form
## 0.1.11
### Patch Changes
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.1.10
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "form",
"private": true,
"version": "0.1.11",
"version": "0.1.10",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,12 +1,5 @@
# image-upload
## 0.0.67
### Patch Changes
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.66
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "image-upload",
"private": true,
"version": "0.0.67",
"version": "0.0.66",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,16 +1,5 @@
# jazz-example-inspector
## 0.0.120
### Patch Changes
- 8a71835: use goober for css
- Updated dependencies [8a71835]
- Updated dependencies [c2f4827]
- jazz-inspector@0.12.2
- cojson@0.12.2
- cojson-transport-ws@0.12.2
## 0.0.119
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-inspector-app",
"private": true,
"version": "0.0.120",
"version": "0.0.119",
"type": "module",
"scripts": {
"dev": "vite",
@@ -13,8 +13,8 @@
"dependencies": {
"jazz-inspector": "workspace:*",
"clsx": "^2.0.0",
"cojson": "workspace:0.12.2",
"cojson-transport-ws": "workspace:0.12.2",
"cojson": "workspace:0.12.1",
"cojson-transport-ws": "workspace:0.12.1",
"hash-slash": "workspace:0.2.2",
"lucide-react": "^0.274.0",
"react": "^18.3.1",

View File

@@ -12,7 +12,6 @@ import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import {
Breadcrumbs,
Button,
GlobalStyles,
Icon,
Input,
PageStack,
@@ -127,7 +126,7 @@ export default function CoJsonViewerApp() {
}
return (
<GlobalStyles
<div
className={clsx(
"h-screen overflow-hidden flex flex-col",
" text-stone-700 bg-white",
@@ -203,7 +202,7 @@ export default function CoJsonViewerApp() {
</form>
)}
</PageStack>
</GlobalStyles>
</div>
);
}

View File

@@ -1,12 +1,5 @@
# multi-cursors
## 0.0.63
### Patch Changes
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.62
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "multi-cursors",
"private": true,
"version": "0.0.63",
"version": "0.0.62",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,14 +1,5 @@
# multiauth
## 0.0.10
### Patch Changes
- b108c61: Use the new Resolve API
- jazz-react@0.12.2
- jazz-react-auth-clerk@0.12.2
- jazz-tools@0.12.2
## 0.0.9
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "multiauth",
"private": true,
"version": "0.0.10",
"version": "0.0.9",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,14 +1,5 @@
# jazz-example-musicplayer
## 0.0.91
### Patch Changes
- Updated dependencies [8a71835]
- jazz-inspector@0.12.2
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.90
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-music-player",
"private": true,
"version": "0.0.91",
"version": "0.0.90",
"type": "module",
"scripts": {
"dev": "vite",
@@ -22,8 +22,8 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:0.12.2",
"jazz-tools": "workspace:0.12.2",
"jazz-react": "workspace:0.12.1",
"jazz-tools": "workspace:0.12.1",
"lucide-react": "^0.274.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",

View File

@@ -1,12 +1,5 @@
# organization
## 0.0.63
### Patch Changes
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.62
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "organization",
"private": true,
"version": "0.0.63",
"version": "0.0.62",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,11 +1,5 @@
# passkey-svelte
## 0.0.57
### Patch Changes
- jazz-svelte@0.12.2
## 0.0.56
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "passkey-svelte",
"version": "0.0.57",
"version": "0.0.56",
"type": "module",
"private": true,
"scripts": {

View File

@@ -1,12 +1,5 @@
# minimal-auth-passkey
## 0.0.68
### Patch Changes
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.67
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "passkey",
"private": true,
"version": "0.0.68",
"version": "0.0.67",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,12 +1,5 @@
# passphrase
## 0.0.65
### Patch Changes
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.64
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "passphrase",
"private": true,
"version": "0.0.65",
"version": "0.0.64",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,12 +1,5 @@
# jazz-password-manager
## 0.0.89
### Patch Changes
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.88
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-password-manager",
"private": true,
"version": "0.0.89",
"version": "0.0.88",
"type": "module",
"scripts": {
"dev": "vite",
@@ -12,8 +12,8 @@
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
},
"dependencies": {
"jazz-react": "workspace:0.12.2",
"jazz-tools": "workspace:0.12.2",
"jazz-react": "workspace:0.12.1",
"jazz-tools": "workspace:0.12.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.41.5",

View File

@@ -1,12 +1,5 @@
# jazz-example-pets
## 0.0.187
### Patch Changes
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.186
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.187",
"version": "0.0.186",
"type": "module",
"scripts": {
"dev": "vite",
@@ -19,8 +19,8 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "workspace:0.12.2",
"jazz-tools": "workspace:0.12.2",
"jazz-react": "workspace:0.12.1",
"jazz-tools": "workspace:0.12.1",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.3.1",
@@ -40,7 +40,7 @@
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.20",
"is-ci": "^3.0.1",
"jazz-run": "workspace:0.12.2",
"jazz-run": "workspace:0.12.1",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",

View File

@@ -1,12 +1,5 @@
# reactions
## 0.0.67
### Patch Changes
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.66
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "reactions",
"private": true,
"version": "0.0.67",
"version": "0.0.66",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,14 +1,5 @@
# todo-vue
## 0.0.71
### Patch Changes
- Updated dependencies [cc684eb]
- jazz-browser@0.12.2
- jazz-vue@0.12.2
- jazz-tools@0.12.2
## 0.0.70
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "todo-vue",
"version": "0.0.71",
"version": "0.0.70",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,12 +1,5 @@
# jazz-example-todo
## 0.0.186
### Patch Changes
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.185
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.186",
"version": "0.0.185",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,8 +16,8 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "workspace:0.12.2",
"jazz-tools": "workspace:0.12.2",
"jazz-react": "workspace:0.12.1",
"jazz-tools": "workspace:0.12.1",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.3.1",

View File

@@ -1,14 +1,5 @@
# version-history
## 0.0.64
### Patch Changes
- Updated dependencies [8a71835]
- jazz-inspector@0.12.2
- jazz-react@0.12.2
- jazz-tools@0.12.2
## 0.0.63
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "version-history",
"private": true,
"version": "0.0.64",
"version": "0.0.63",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,13 +1,5 @@
# cojson-storage-indexeddb
## 0.12.2
### Patch Changes
- Updated dependencies [c2f4827]
- cojson@0.12.2
- cojson-storage@0.12.2
## 0.12.1
### Patch Changes

View File

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

View File

@@ -1,13 +1,5 @@
# cojson-storage-sqlite
## 0.12.2
### Patch Changes
- Updated dependencies [c2f4827]
- cojson@0.12.2
- cojson-storage@0.12.2
## 0.12.1
### Patch Changes

View File

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

View File

@@ -1,13 +1,5 @@
# cojson-storage-sqlite
## 0.12.2
### Patch Changes
- Updated dependencies [c2f4827]
- cojson@0.12.2
- cojson-storage@0.12.2
## 0.12.1
### Patch Changes

View File

@@ -1,13 +1,13 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.12.2",
"version": "0.12.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^11.7.0",
"cojson": "workspace:0.12.2",
"cojson": "workspace:0.12.1",
"cojson-storage": "workspace:*"
},
"devDependencies": {

View File

@@ -1,12 +1,5 @@
# cojson-storage
## 0.12.2
### Patch Changes
- Updated dependencies [c2f4827]
- cojson@0.12.2
## 0.12.1
### Patch Changes

View File

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

View File

@@ -1,12 +1,5 @@
# cojson-transport-nodejs-ws
## 0.12.2
### Patch Changes
- Updated dependencies [c2f4827]
- cojson@0.12.2
## 0.12.1
### Patch Changes

View File

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

View File

@@ -7,7 +7,7 @@ import {
logger,
} from "cojson";
import { BatchedOutgoingMessages } from "./BatchedOutgoingMessages.js";
import { deserializeMessages } from "./serialization.js";
import { deserializeMessages, getErrorMessage } from "./serialization.js";
import type { AnyWebSocket } from "./types.js";
export const BUFFER_LIMIT = 100_000;
@@ -20,16 +20,11 @@ export type CreateWebSocketPeerOpts = {
expectPings?: boolean;
batchingByDefault?: boolean;
deletePeerStateOnClose?: boolean;
pingTimeout?: number;
onClose?: () => void;
onSuccess?: () => void;
};
function createPingTimeoutListener(
enabled: boolean,
timeout: number,
callback: () => void,
) {
function createPingTimeoutListener(enabled: boolean, callback: () => void) {
if (!enabled) {
return {
reset() {},
@@ -44,7 +39,7 @@ function createPingTimeoutListener(
pingTimeout && clearTimeout(pingTimeout);
pingTimeout = setTimeout(() => {
callback();
}, timeout);
}, 10_000);
},
clear() {
pingTimeout && clearTimeout(pingTimeout);
@@ -133,7 +128,6 @@ export function createWebSocketPeer({
expectPings = true,
batchingByDefault = true,
deletePeerStateOnClose = false,
pingTimeout = 10_000,
onSuccess,
onClose,
}: CreateWebSocketPeerOpts): Peer {
@@ -162,18 +156,14 @@ export function createWebSocketPeer({
handleClose();
});
const pingTimeoutListener = createPingTimeoutListener(
expectPings,
pingTimeout,
() => {
incoming
.push("PingTimeout")
.catch((e) =>
logger.error("Error while pushing ping timeout", { err: e }),
);
emitClosedEvent();
},
);
const pingTimeout = createPingTimeoutListener(expectPings, () => {
incoming
.push("PingTimeout")
.catch((e) =>
logger.error("Error while pushing ping timeout", { err: e }),
);
emitClosedEvent();
});
const outgoingMessages = createOutgoingMessagesManager(
websocket,
@@ -182,8 +172,6 @@ export function createWebSocketPeer({
let isFirstMessage = true;
function handleIncomingMsg(event: { data: unknown }) {
pingTimeoutListener.reset();
if (event.data === "") {
return;
}
@@ -209,6 +197,8 @@ export function createWebSocketPeer({
outgoingMessages.setBatchingEnabled(true);
}
pingTimeout.reset();
for (const msg of messages) {
if (msg && "action" in msg) {
incoming
@@ -232,7 +222,7 @@ export function createWebSocketPeer({
websocket.removeEventListener("message", handleIncomingMsg);
websocket.removeEventListener("close", handleClose);
pingTimeoutListener.clear();
pingTimeout.clear();
emitClosedEvent();
if (websocket.readyState === 0) {

View File

@@ -4,7 +4,6 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { WebSocket } from "ws";
import { createWebSocketPeer } from "../createWebSocketPeer";
import { startSyncServer } from "./syncServer";
import { waitFor } from "./utils";
describe("WebSocket Peer Integration", () => {
let server: any;
@@ -130,35 +129,4 @@ describe("WebSocket Peer Integration", () => {
expect(disconnectCalled).toBe(true);
expect(ws.readyState).toBe(WebSocket.CLOSED);
});
test("should trigger a timeout if the server does not respond", async () => {
const clientAgent = crypto.newRandomAgentSecret();
const clientNode = new LocalNode(
new ControlledAgent(clientAgent, crypto),
crypto.newRandomSessionID(crypto.getAgentID(clientAgent)),
crypto,
);
const ws = new WebSocket(syncServerUrl);
let disconnectCalled = false;
const peer = createWebSocketPeer({
id: "test-client",
websocket: ws,
role: "server",
pingTimeout: 5,
onClose: () => {
disconnectCalled = true;
},
});
clientNode.syncManager.addPeer(peer);
// Wait for connection to establish and the timeout to kick in
await waitFor(() => {
expect(disconnectCalled).toBe(true);
});
expect(ws.readyState).toBe(WebSocket.CLOSED);
});
});

View File

@@ -1,11 +1,5 @@
# cojson
## 0.12.2
### Patch Changes
- c2f4827: StreamingHash: Remove redundant clone and skip double hash generation when creating a local transaction
## 0.12.1
### Patch Changes

View File

@@ -25,7 +25,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.12.2",
"version": "0.12.1",
"devDependencies": {
"@opentelemetry/sdk-metrics": "^2.0.0",
"typescript": "~5.6.2",

View File

@@ -199,7 +199,6 @@ export class CoValueCore {
givenExpectedNewHash: Hash | undefined,
newSignature: Signature,
skipVerify: boolean = false,
givenNewStreamingHash?: StreamingHash,
): Result<true, TryAddTransactionsError> {
return this.node
.resolveAccountAgent(
@@ -209,55 +208,42 @@ export class CoValueCore {
.andThen((agent) => {
const signerID = this.crypto.getAgentSignerID(agent);
if (
skipVerify === true &&
givenNewStreamingHash &&
givenExpectedNewHash
) {
this.doAddTransactions(
sessionID,
newTransactions,
newSignature,
givenExpectedNewHash,
givenNewStreamingHash,
"immediate",
);
} else {
const { expectedNewHash, newStreamingHash } =
this.expectedNewHashAfter(sessionID, newTransactions);
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
sessionID,
newTransactions,
);
if (
givenExpectedNewHash &&
givenExpectedNewHash !== expectedNewHash
) {
return err({
type: "InvalidHash",
id: this.id,
expectedNewHash,
givenExpectedNewHash,
} satisfies InvalidHashError);
}
if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
return err({
type: "InvalidSignature",
id: this.id,
newSignature,
sessionID,
signerID,
} satisfies InvalidSignatureError);
}
this.doAddTransactions(
sessionID,
newTransactions,
newSignature,
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
return err({
type: "InvalidHash",
id: this.id,
expectedNewHash,
newStreamingHash,
"immediate",
);
givenExpectedNewHash,
} satisfies InvalidHashError);
}
if (
skipVerify !== true &&
!this.crypto.verify(newSignature, expectedNewHash, signerID)
) {
return err({
type: "InvalidSignature",
id: this.id,
newSignature,
sessionID,
signerID,
} satisfies InvalidSignatureError);
}
this.doAddTransactions(
sessionID,
newTransactions,
newSignature,
expectedNewHash,
newStreamingHash,
"immediate",
);
return ok(true as const);
});
}
@@ -384,14 +370,40 @@ export class CoValueCore {
const streamingHash =
this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
new StreamingHash(this.crypto);
for (const transaction of newTransactions) {
streamingHash.update(transaction);
}
const newStreamingHash = streamingHash.clone();
return {
expectedNewHash: streamingHash.digest(),
newStreamingHash: streamingHash,
newStreamingHash,
};
}
async expectedNewHashAfterAsync(
sessionID: SessionID,
newTransactions: Transaction[],
): Promise<{ expectedNewHash: Hash; newStreamingHash: StreamingHash }> {
const streamingHash =
this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
new StreamingHash(this.crypto);
let before = performance.now();
for (const transaction of newTransactions) {
streamingHash.update(transaction);
const after = performance.now();
if (after - before > 1) {
await new Promise((resolve) => setTimeout(resolve, 0));
before = performance.now();
}
}
const newStreamingHash = streamingHash.clone();
return {
expectedNewHash: streamingHash.digest(),
newStreamingHash,
};
}
@@ -440,10 +452,9 @@ export class CoValueCore {
) as SessionID)
: this.node.currentSessionID;
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
sessionID,
[transaction],
);
const { expectedNewHash } = this.expectedNewHashAfter(sessionID, [
transaction,
]);
const signature = this.crypto.sign(
this.node.account.currentSignerSecret(),
@@ -456,7 +467,6 @@ export class CoValueCore {
expectedNewHash,
signature,
true,
newStreamingHash,
)._unsafeUnwrap({ withStackTrace: true });
if (success) {

View File

@@ -176,12 +176,11 @@ export class CoValueState {
async loadFromPeers(peers: PeerState[]) {
const state = this.state;
if (state.type === "loading" || state.type === "available") {
if (state.type !== "unknown" && state.type !== "unavailable") {
return;
}
if (peers.length === 0) {
this.moveToState(new CoValueUnavailableState());
return;
}
@@ -193,11 +192,7 @@ export class CoValueState {
// If we are in the loading state we move to a new loading state
// to reset all the loading promises
if (
this.state.type === "loading" ||
this.state.type === "unknown" ||
this.state.type === "unavailable"
) {
if (this.state.type === "loading" || this.state.type === "unknown") {
this.moveToState(
new CoValueLoadingState(peersWithoutErrors.map((p) => p.id)),
);
@@ -313,19 +308,6 @@ async function loadCoValueFromPeers(
}
if (coValueEntry.state.type === "loading") {
const { promise, resolve } = createResolvablePromise<void>();
/**
* Use a very long timeout for storage peers, because under pressure
* they may take a long time to consume the messages queue
*
* TODO: Track errors on storage and do not rely on timeout
*/
const timeoutDuration =
peer.role === "storage"
? CO_VALUE_LOADING_CONFIG.TIMEOUT * 10
: CO_VALUE_LOADING_CONFIG.TIMEOUT;
const timeout = setTimeout(() => {
if (coValueEntry.state.type === "loading") {
logger.warn("Failed to load coValue from peer", {
@@ -337,10 +319,9 @@ async function loadCoValueFromPeers(
type: "not-found-in-peer",
peerId: peer.id,
});
resolve();
}
}, timeoutDuration);
await Promise.race([promise, coValueEntry.state.waitForPeer(peer.id)]);
}, CO_VALUE_LOADING_CONFIG.TIMEOUT);
await coValueEntry.state.waitForPeer(peer.id);
clearTimeout(timeout);
}
}

View File

@@ -23,6 +23,10 @@ export function emptyKnownState(id: RawCoID): CoValueKnownState {
};
}
function getErrorMessage(e: unknown) {
return e instanceof Error ? e.message : "Unknown error";
}
export type SyncMessage =
| LoadMessage
| KnownStateMessage
@@ -411,20 +415,7 @@ export class SyncManager {
entry.loadFromPeers([peer]).catch((e) => {
logger.error("Error loading coValue in handleLoad", { err: e });
});
} else {
// We don't have any eligible peers to load the coValue from
// so we send a known state back to the sender to let it know
// that the coValue is unavailable
this.trySendToPeer(peer, {
action: "known",
id: msg.id,
header: false,
sessions: {},
}).catch((e) => {
logger.error("Error sending known state back", { err: e });
});
}
return;
} else {
this.local.loadCoValueCore(msg.id, peer.id).catch((e) => {
@@ -469,13 +460,6 @@ export class SyncManager {
err: e,
});
});
} else if (entry.state.type === "unavailable") {
this.trySendToPeer(peer, {
action: "known",
id: msg.id,
header: false,
sessions: {},
});
}
if (entry.state.type === "available") {
@@ -620,12 +604,39 @@ export class SyncManager {
continue;
}
const before = performance.now();
// eslint-disable-next-line neverthrow/must-use-result
const result = coValue.tryAddTransactions(
sessionID,
newTransactions,
undefined,
newContentForSession.lastSignature,
);
const after = performance.now();
if (after - before > 80) {
const totalTxLength = newTransactions
.map((t) =>
t.privacy === "private"
? t.encryptedChanges.length
: t.changes.length,
)
.reduce((a, b) => a + b, 0);
logger.debug(
`Adding incoming transactions took ${(after - before).toFixed(
2,
)}ms for ${totalTxLength} bytes = bandwidth: ${(
(1000 * totalTxLength) / (after - before) / (1024 * 1024)
).toFixed(2)} MB/s`,
);
}
// const theirTotalnTxs = Object.values(
// peer.optimisticKnownStates[msg.id]?.sessions || {},
// ).reduce((sum, nTxs) => sum + nTxs, 0);
// const ourTotalnTxs = [...coValue.sessionLogs.values()].reduce(
// (sum, session) => sum + session.transactions.length,
// 0,
// );
if (result.isErr()) {
logger.error("Failed to add transactions", {
@@ -724,10 +735,16 @@ export class SyncManager {
}
async actuallySyncCoValue(coValue: CoValueCore) {
// let blockingSince = performance.now();
for (const peer of this.peersInPriorityOrder()) {
if (peer.closed) continue;
if (peer.erroredCoValues.has(coValue.id)) continue;
// if (performance.now() - blockingSince > 5) {
// await new Promise<void>((resolve) => {
// setTimeout(resolve, 0);
// });
// blockingSince = performance.now();
// }
if (peer.optimisticKnownStates.has(coValue.id)) {
await this.tellUntoldKnownStateIncludingDependencies(coValue.id, peer);
await this.sendNewContentIncludingDependencies(coValue.id, peer);

View File

@@ -956,6 +956,27 @@ test.skip("When a peer's incoming/readable stream closes, we remove the peer", a
*/
});
test("If we start loading a coValue before connecting to a peer that has it, it will load it once we connect", async () => {
const { node: node1 } = await createConnectedTestNode();
const group = node1.createGroup();
const map = group.createMap();
map.set("hello", "world", "trusting");
const node2 = createTestNode();
const mapOnNode2Promise = loadCoValueOrFail(node2, map.id);
expect(node2.coValuesStore.get(map.core.id).state.type).toEqual("unknown");
connectNodeToSyncServer(node2);
const mapOnNode2 = await mapOnNode2Promise;
expect(mapOnNode2.get("hello")).toEqual("world");
});
test("should keep the peer state when the peer closes", async () => {
const client = createTestNode();
@@ -1705,45 +1726,6 @@ describe("loadCoValueCore with retry", () => {
await expect(promise1).resolves.not.toBe("unavailable");
await expect(promise2).resolves.not.toBe("unavailable");
});
test("should load unavailable coValues after they are synced", async () => {
const bob = createTestNode();
const alice = createTestNode();
// Create a group and map on anotherClient
const group = alice.createGroup();
const map = group.createMap();
map.set("key1", "value1", "trusting");
// Start loading before syncing
const result = await bob.loadCoValueCore(map.id);
expect(result).toBe("unavailable");
connectTwoPeers(alice, bob, "server", "server");
const result2 = await bob.loadCoValueCore(map.id);
expect(result2).not.toBe("unavailable");
});
test("should successfully mark a coValue as unavailable if the server does not have it", async () => {
const bob = createTestNode();
const alice = createTestNode();
const charlie = createTestNode();
connectTwoPeers(bob, charlie, "client", "server");
// Create a group and map on anotherClient
const group = alice.createGroup();
const map = group.createMap();
map.set("key1", "value1", "trusting");
// Start loading before syncing
const result = await bob.loadCoValueCore(map.id);
expect(result).toBe("unavailable");
});
});
describe("waitForSyncWithPeer", () => {
@@ -1912,8 +1894,6 @@ describe("sync protocol", () => {
const map = group.createMap();
map.set("hello", "world", "trusting");
await map.core.waitForSync();
const mapOnJazzCloud = await loadCoValueOrFail(jazzCloud, map.id);
expect(mapOnJazzCloud.get("hello")).toEqual("world");
@@ -2058,29 +2038,6 @@ describe("sync protocol", () => {
},
},
},
{
from: "server",
msg: {
action: "known",
header: true,
id: map.id,
sessions: {
[client.currentSessionID]: 1,
},
},
},
{
from: "server",
msg: {
action: "known",
asDependencyOf: undefined,
header: true,
id: map.id,
sessions: {
[client.currentSessionID]: 1,
},
},
},
]);
});
});

View File

@@ -1,15 +1,5 @@
# jazz-browser-media-images
## 0.12.2
### Patch Changes
- Updated dependencies [cc684eb]
- Updated dependencies [c2f4827]
- jazz-browser@0.12.2
- cojson@0.12.2
- jazz-tools@0.12.2
## 0.12.1
### Patch Changes

View File

@@ -1,14 +1,14 @@
{
"name": "jazz-auth-clerk",
"version": "0.12.2",
"version": "0.12.1",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:0.12.2",
"jazz-browser": "workspace:0.12.2",
"jazz-tools": "workspace:0.12.2"
"cojson": "workspace:0.12.1",
"jazz-browser": "workspace:0.12.1",
"jazz-tools": "workspace:0.12.1"
},
"scripts": {
"format-and-lint": "biome check .",

View File

@@ -1,13 +1,5 @@
# jazz-browser-media-images
## 0.12.2
### Patch Changes
- Updated dependencies [cc684eb]
- jazz-browser@0.12.2
- jazz-tools@0.12.2
## 0.12.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-browser-media-images",
"version": "0.12.2",
"version": "0.12.1",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -8,8 +8,8 @@
"dependencies": {
"@types/image-blob-reduce": "^4.1.1",
"image-blob-reduce": "^4.1.0",
"jazz-browser": "workspace:0.12.2",
"jazz-tools": "workspace:0.12.2",
"jazz-browser": "workspace:0.12.1",
"jazz-tools": "workspace:0.12.1",
"pica": "^9.0.1",
"typescript": "~5.6.2"
},

View File

@@ -1,16 +1,5 @@
# jazz-browser
## 0.12.2
### Patch Changes
- cc684eb: PasskeyAuth: Change the private attributes visibility to protected
- Updated dependencies [c2f4827]
- cojson@0.12.2
- cojson-storage-indexeddb@0.12.2
- cojson-transport-ws@0.12.2
- jazz-tools@0.12.2
## 0.12.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-browser",
"version": "0.12.2",
"version": "0.12.1",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -19,9 +19,9 @@ import {
*/
export class BrowserPasskeyAuth {
constructor(
protected crypto: CryptoProvider,
protected authenticate: AuthenticateAccountFunction,
protected authSecretStorage: AuthSecretStorage,
private crypto: CryptoProvider,
private authenticate: AuthenticateAccountFunction,
private authSecretStorage: AuthSecretStorage,
public appName: string,
public appHostname: string = window.location.hostname,
) {}

View File

@@ -1,15 +1,5 @@
# jazz-inspector
## 0.12.2
### Patch Changes
- 8a71835: use goober for css
- Updated dependencies [c2f4827]
- cojson@0.12.2
- jazz-react-core@0.12.2
- jazz-tools@0.12.2
## 0.12.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-inspector",
"version": "0.12.2",
"version": "0.12.1",
"type": "module",
"main": "./dist/app.js",
"types": "./dist/app.d.ts",
@@ -16,9 +16,11 @@
"preview": "vite preview"
},
"dependencies": {
"@twind/core": "^1.1.3",
"@twind/preset-autoprefix": "^1.0.7",
"@twind/preset-tailwind": "^1.1.4",
"clsx": "^2.0.0",
"cojson": "workspace:*",
"goober": "^2.1.16",
"jazz-react-core": "workspace:*",
"jazz-tools": "workspace:*"
},

View File

@@ -1,4 +1,5 @@
import React from "react";
// Import Twind setup
import "./twind.js";
export { JazzInspector } from "./viewer/new-app.js";
export { PageStack } from "./viewer/page-stack.js";
@@ -8,7 +9,6 @@ export { Button } from "./ui/button.js";
export { Input } from "./ui/input.js";
export { Select } from "./ui/select.js";
export { Icon } from "./ui/icon.js";
export { GlobalStyles } from "./ui/global-styles.js";
export {
resolveCoValue,
@@ -16,7 +16,3 @@ export {
} from "./viewer/use-resolve-covalue.js";
export type { PageInfo } from "./viewer/types.js";
import { setup } from "goober";
setup(React.createElement);

View File

@@ -0,0 +1,55 @@
import { defineConfig } from "@twind/core";
import presetAutoprefix from "@twind/preset-autoprefix";
import presetTailwind from "@twind/preset-tailwind";
const stonePalette = {
50: "oklch(0.988281 0.002 75)",
100: "oklch(0.980563 0.002 75)",
200: "oklch(0.917969 0.002 75)",
300: "oklch(0.853516 0.002 75)",
400: "oklch(0.789063 0.002 75)",
500: "oklch(0.726563 0.002 75)",
600: "oklch(0.613281 0.002 75)",
700: "oklch(0.523438 0.002 75)",
800: "oklch(0.412109 0.002 75)",
900: "oklch(0.302734 0.002 75)",
925: "oklch(0.220000 0.002 75)",
950: "oklch(0.193359 0.002 75)",
};
const stonePaletteWithAlpha = { ...stonePalette };
Object.keys(stonePalette).forEach((key) => {
// @ts-ignore
stonePaletteWithAlpha[key] = stonePaletteWithAlpha[key].replace(
")",
"/ <alpha-value>)",
);
});
export default defineConfig({
hash: true,
presets: [presetAutoprefix(), presetTailwind()],
theme: {
extend: {
colors: {
stone: stonePaletteWithAlpha,
gray: stonePaletteWithAlpha,
blue: {
50: "#f5f7ff",
100: "#ebf0fe",
200: "#d6e0fd",
300: "#b3c7fc",
400: "#8aa6f9",
500: "#5870F1",
600: "#3651E7",
700: "#3313F7",
800: "#2A12BE",
900: "#12046A",
950: "#1e1b4b",
DEFAULT: "#3313F7",
},
},
},
},
});

View File

@@ -0,0 +1,8 @@
import { setup } from "@twind/core";
import config from "./twind.config";
export const tw = setup(
config,
undefined,
document.getElementById("__jazz_inspector")!,
);

View File

@@ -1,24 +0,0 @@
import { styled } from "goober";
const StyledBadge = styled("span")<{ className?: string }>`
font-size: 0.875rem;
font-weight: 500;
padding: 0.125rem 0.25rem;
margin-left: -0.125rem;
border-radius: var(--j-radius-sm);
background-color: var(--j-neutral-200);
display: inline-block;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
color: var(--j-text-color-strong);
@media (prefers-color-scheme: dark) {
background-color: var(--j-neutral-900);
}
`;
export function Badge({
children,
className,
}: React.PropsWithChildren<{ className?: string }>) {
return <StyledBadge className={className}>{children}</StyledBadge>;
}

View File

@@ -1,63 +1,20 @@
import { styled } from "goober";
import { forwardRef } from "react";
import { classNames } from "../utils.js";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "link" | "plain";
variant?: "primary" | "secondary" | "tertiary" | "destructive" | "plain";
size?: "sm" | "md" | "lg";
children?: React.ReactNode;
className?: string;
disabled?: boolean;
}
const StyledButton = styled("button")<{ variant: string; disabled?: boolean }>`
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
text-align: center;
transition: colors 0.2s;
border-radius: var(--j-radius-lg);
pointer-events: ${(props) => (props.disabled ? "none" : "auto")};
opacity: ${(props) => (props.disabled ? 0.5 : 1)};
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
${(props) => {
switch (props.variant) {
case "primary":
return `
padding: 0.375rem 0.75rem;
background-color: var(--j-primary-color);
border-color: var(--j-primary-color);
color: white;
font-weight: 500;
`;
case "secondary":
return `
padding: 0.375rem 0.75rem;
color: var(--j-text-color-strong);
border: 1px solid var(--j-border-color);
font-weight: 500;
&:hover {
border-color: var(--j-border-color-hover);
}
`;
case "link":
return `
color: var(--j-link-color);
&:hover {
text-decoration: underline;
}
`;
default:
return "";
}
}}
`;
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
children,
size = "md",
variant = "primary",
disabled,
type = "button",
@@ -65,17 +22,44 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
},
ref,
) => {
const sizeClasses = {
sm: "text-sm py-1 px-2",
md: "py-1.5 px-3",
lg: "md:text-lg py-2 px-3 md:px-8 md:py-3",
};
const variantClasses = {
primary:
"bg-blue border-blue text-white font-medium bg-blue hover:bg-blue-800 hover:border-blue-800",
secondary:
"text-stone-900 border font-medium hover:border-stone-300 hover:dark:border-stone-700 dark:text-white",
tertiary: "text-blue underline underline-offset-4",
destructive:
"bg-red-600 border-red-600 text-white font-medium hover:bg-red-700 hover:border-red-700",
};
const classes =
variant === "plain"
? className
: classNames(
className,
"inline-flex items-center justify-center gap-2 rounded-lg text-center transition-colors",
"disabled:pointer-events-none disabled:opacity-70",
sizeClasses[size],
variantClasses[variant],
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
);
return (
<StyledButton
<button
ref={ref}
{...buttonProps}
disabled={disabled}
className={className}
className={classes}
type={type}
variant={variant}
>
{children}
</StyledButton>
</button>
);
},
);

View File

@@ -1,71 +0,0 @@
import { styled } from "goober";
export const GlobalStyles = styled("div")`
/* Colors */
--j-primary-color: #3313F7;
--j-link-color: var(--j-primary-color);
/* Neutral Colors */
--j-neutral-100: #faf8f8;
--j-neutral-200: #e5e3e4;
--j-neutral-300: #d0cecf;
--j-neutral-400: #bbbaba;
--j-neutral-500: #a8a6a6;
--j-neutral-600: #858484;
--j-neutral-700: #6b696a;
--j-neutral-900: #2f2e2e;
--j-neutral-925: #1b1a1a;
--j-neutral-950: #151414;
/* Text Colors */
--j-text-color: var(--j-neutral-700);
--j-text-color-strong: var(--j-neutral-900);
/* Border Colors */
--j-border-color: var(--j-neutral-200);
--j-border-color-hover: var(--j-neutral-300);
--j-border-dark: var(--j-neutral-900);
--j-border-focus: var(--j-primary-color);
/* Background Colors */
--j-background: #FFFFFF;
--j-foreground: var(--j-neutral-100);
/* Border Radius */
--j-radius-sm: 0.25rem;
--j-radius-md: 0.375rem;
--j-radius-lg: 0.5rem;
/* Shadows */
--j-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
@media (prefers-color-scheme: dark) {
--j-text-color: var(--j-neutral-400);
--j-link-color: #5870f1;
--j-border-color: var(--j-neutral-900);
--j-background: var(--j-neutral-950);
--j-foreground: var(--j-neutral-925);
--j-border-color-hover: var(--j-neutral-700);
--j-text-color-strong: var(--j-neutral-100);
}
*:focus {
outline: none;
}
*:focus-visible {
box-shadow: 0 0 0 2px var(--j-link-color);
}
.j-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
`;

View File

@@ -1,15 +0,0 @@
import { styled } from "goober";
const StyledHeading = styled("h1")<{ className?: string }>`
font-size: 1.125rem;
text-align: center;
font-weight: 500;
color: var(--j-text-color-strong);
`;
export function Heading({
children,
className,
}: React.PropsWithChildren<{ className?: string }>) {
return <StyledHeading className={className}>{children}</StyledHeading>;
}

View File

@@ -1,3 +1,4 @@
import { classNames } from "../utils.js";
import { ChevronDownIcon } from "./icons/chevron-down-icon.js";
import { DeleteIcon } from "./icons/delete-icon.js";
import { LinkIcon } from "./icons/link-icon.js";
@@ -51,6 +52,7 @@ export function Icon({
}: {
name?: keyof typeof icons;
size?: keyof typeof sizes;
className?: string;
} & React.SVGProps<SVGSVGElement>) {
if (!name || !icons.hasOwnProperty(name)) {
throw new Error(`Icon not found: ${name}`);
@@ -65,6 +67,7 @@ export function Icon({
size={sizes[size]}
strokeWidth={strokeWidths[size]}
strokeLinecap="round"
className={classNames(className)}
{...svgProps}
/>
);

View File

@@ -1,4 +1,6 @@
export function LinkIcon(props: React.SVGProps<SVGSVGElement>) {
import { classNames } from "../../utils.js";
export function LinkIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -6,7 +8,7 @@ export function LinkIcon(props: React.SVGProps<SVGSVGElement>) {
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
{...props}
className={classNames("w-3 h-3")}
>
<path
strokeLinecap="round"

View File

@@ -1,51 +1,40 @@
import { styled } from "goober";
import { forwardRef, useId } from "react";
interface LabelProps {
hideLabel?: boolean;
}
interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement>,
LabelProps {
import { classNames } from "../utils.js";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
// label can be hidden with a "label:sr-only" className
label: string;
className?: string;
id?: string;
hideLabel?: boolean;
}
const Container = styled("div")`
display: grid;
gap: 0.25rem;
`;
const StyledInput = styled("input")`
width: 100%;
border-radius: var(--j-radius-md);
border: 1px solid var(--j-border-color);
padding: 0.5rem 0.875rem;
box-shadow: var(--j-shadow-sm);
font-weight: 500;
background-color: white;
color: var(--text-color-strong);
@media (prefers-color-scheme: dark) {
background-color: var(--j-foreground);
}
`;
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, className, hideLabel, id: customId, ...inputProps }, ref) => {
const generatedId = useId();
const id = customId || generatedId;
const inputClassName = classNames(
"w-full rounded-md border px-3.5 py-2 shadow-sm",
"font-medium text-stone-900",
"dark:text-white dark:bg-stone-925",
);
const containerClassName = classNames("grid gap-1", className);
return (
<Container className={className}>
<label htmlFor={id} className={hideLabel ? "j-sr-only" : ""}>
<div className={containerClassName}>
<label
htmlFor={id}
className={classNames(
"text-stone-600 dark:text-stone-300",
hideLabel && "sr-only",
)}
>
{label}
</label>
<StyledInput ref={ref} {...inputProps} id={id} />
</Container>
<input ref={ref} {...inputProps} id={id} className={inputClassName} />
</div>
);
},
);

View File

@@ -1,48 +1,7 @@
import { styled } from "goober";
import { useId } from "react";
import { classNames } from "../utils.js";
import { Icon } from "./icon.js";
const SelectContainer = styled("div")<{ className?: string }>`
display: grid;
gap: 0.25rem;
`;
const SelectWrapper = styled("div")`
position: relative;
display: flex;
align-items: center;
`;
const StyledSelect = styled("select")`
width: 100%;
border-radius: var(--j-radius-md);
border: 1px solid var(--j-border-color);
padding: 0.5rem 0.875rem 0.5rem 0.875rem;
padding-right: 2rem;
box-shadow: var(--j-shadow-sm);
font-weight: 500;
color: var(--j-text-color-strong);
appearance: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@media (prefers-color-scheme: dark) {
background-color: var(--j-foreground);
}
`;
const SelectIcon = styled("span")`
position: absolute;
right: 0.5em;
color: var(--j-neutral-400);
pointer-events: none;
@media (prefers-color-scheme: dark) {
color: var(--j-neutral-900);
}
`;
export function Select(
props: React.SelectHTMLAttributes<HTMLSelectElement> & {
label: string;
@@ -53,21 +12,40 @@ export function Select(
const generatedId = useId();
const id = customId || generatedId;
const containerClassName = classNames("grid gap-1", className);
const selectClassName = classNames(
"w-full rounded-md border pl-3.5 py-2 pr-8 shadow-sm",
"font-medium text-stone-900",
"dark:text-white dark:bg-stone-925",
"appearance-none",
"truncate",
);
return (
<SelectContainer className={className}>
<label htmlFor={id} className={hideLabel ? "j-sr-only" : ""}>
<div className={classNames(containerClassName)}>
<label
htmlFor={id}
className={classNames("text-stone-600 dark:text-stone-300", {
"sr-only": hideLabel,
})}
>
{label}
</label>
<SelectWrapper>
<StyledSelect {...selectProps} id={id}>
<div className={classNames("relative flex items-center")}>
<select {...selectProps} id={id} className={selectClassName}>
{props.children}
</StyledSelect>
</select>
<SelectIcon>
<Icon name="chevronDown" size="sm" />
</SelectIcon>
</SelectWrapper>
</SelectContainer>
<Icon
name="chevronDown"
className={classNames(
"absolute right-[0.5em] text-stone-400 dark:text-stone-600",
)}
size="sm"
/>
</div>
</div>
);
}

View File

@@ -1,59 +0,0 @@
import { styled } from "goober";
const StyledTable = styled("table")`
width: 100%;
`;
const StyledThead = styled("thead")`
text-align: left;
border-bottom: 1px solid var(--j-border-color);
background-color: var(--j-neutral-100);
@media (prefers-color-scheme: dark) {
background-color: var(--j-neutral-925);
}
`;
const StyledTbody = styled("tbody")`
tr {
border-bottom: 1px solid var(--j-border-color);
&:last-child {
border-bottom: none;
}
}
`;
const StyledTh = styled("th")`
font-weight: 500;
padding: 0.5rem 0.75rem;
color: var(--j-text-color-strong);
`;
const StyledTd = styled("td")`
padding: 0.5rem 0.75rem;
`;
export function Table({ children }: React.PropsWithChildren<{}>) {
return <StyledTable>{children}</StyledTable>;
}
export function TableHead({ children }: React.PropsWithChildren<{}>) {
return <StyledThead>{children}</StyledThead>;
}
export function TableBody({ children }: React.PropsWithChildren<{}>) {
return <StyledTbody>{children}</StyledTbody>;
}
export function TableRow({ children }: React.PropsWithChildren<{}>) {
return <tr>{children}</tr>;
}
export function TableHeader({ children }: React.PropsWithChildren<{}>) {
return <StyledTh>{children}</StyledTh>;
}
export function TableCell({ children }: React.PropsWithChildren<{}>) {
return <StyledTd>{children}</StyledTd>;
}

View File

@@ -1,67 +0,0 @@
import { styled } from "goober";
import React from "react";
interface TextProps extends React.HTMLAttributes<HTMLParagraphElement> {
muted?: boolean;
strong?: boolean;
small?: boolean;
inline?: boolean;
}
const BaseText = React.forwardRef<HTMLParagraphElement, TextProps>(
({ muted, strong, small, inline, ...rest }, ref) => <p ref={ref} {...rest} />,
);
const StyledText = styled(BaseText)<TextProps>`
${(props) =>
props.muted &&
`
color: var(--j-neutral-500);
`}
${(props) =>
props.strong &&
`
font-weight: 500;
color: var(--j-text-color-strong);
`}
${(props) =>
props.small &&
`
font-size: 0.875rem;
`}
${(props) =>
props.inline &&
`
display: inline;
`}
`;
export function Text({
children,
className,
muted,
strong,
inline,
small,
}: React.PropsWithChildren<{
className?: string;
muted?: boolean;
strong?: boolean;
inline?: boolean;
small?: boolean;
}>) {
return (
<StyledText
className={className}
muted={muted}
strong={strong}
inline={inline}
small={small}
>
{children}
</StyledText>
);
}

View File

@@ -0,0 +1,6 @@
import { ClassValue, clsx } from "clsx";
import { tw } from "./twind.js";
export const classNames = (...inputs: ClassValue[]) => {
return tw(clsx(inputs));
};

View File

@@ -1,19 +1,8 @@
import { styled } from "goober";
import React from "react";
import { Button } from "../ui/button.js";
import { PageInfo } from "./types.js";
const BreadcrumbsContainer = styled("div")`
position: relative;
z-index: 20;
flex: 1;
display: flex;
align-items: center;
`;
const Separator = styled("span")`
padding: 0 0.125rem;
`;
import { classNames } from "../utils.js";
interface BreadcrumbsProps {
path: PageInfo[];
@@ -25,10 +14,10 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
onBreadcrumbClick,
}) => {
return (
<BreadcrumbsContainer>
<div className={classNames("relative z-20 flex-1 flex items-center")}>
<Button
variant="link"
style={{ padding: "0 0.25rem" }}
variant="plain"
className={classNames("text-blue px-1 dark:text-blue-400")}
onClick={() => onBreadcrumbClick(-1)}
>
Home
@@ -36,10 +25,17 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
{path.map((page, index) => {
return (
<React.Fragment key={page.coId}>
<Separator aria-hidden>/</Separator>
<span
aria-hidden
className={classNames(
"text-stone-400 dark:text-stone-600 px-0.5",
)}
>
/
</span>
<Button
variant="link"
style={{ padding: "0 0.25rem" }}
variant="plain"
className={classNames("text-blue px-1 dark:text-blue-400")}
onClick={() => onBreadcrumbClick(index)}
>
{index === 0 ? page.name || "Root" : page.name}
@@ -47,6 +43,6 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
</React.Fragment>
);
})}
</BreadcrumbsContainer>
</div>
);
};

View File

@@ -8,13 +8,13 @@ import {
import { base64URLtoBytes } from "cojson";
import { BinaryStreamItem, BinaryStreamStart, CoStreamItem } from "cojson";
import type { JsonObject, JsonValue } from "cojson";
import { styled } from "goober";
import { useEffect, useState } from "react";
import { Badge } from "../ui/badge.js";
import { Button } from "../ui/button.js";
import { PageInfo } from "./types.js";
import { AccountOrGroupPreview } from "./value-renderer.js";
import { classNames } from "../utils.js";
// typeguard for BinaryStreamStart
function isBinaryStreamStart(item: unknown): item is BinaryStreamStart {
return (
@@ -156,53 +156,6 @@ const BinaryDownloadButton = ({
);
};
const LabelContentPairContainer = styled("div")`
display: flex;
flex-direction: column;
gap: 0.375rem;
`;
const BinaryStreamContainer = styled("div")`
margin-top: 2rem;
display: flex;
flex-direction: column;
gap: 2rem;
`;
const BinaryStreamGrid = styled("div")`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
max-width: 48rem;
`;
const ImagePreviewContainer = styled("div")`
background-color: rgb(249 250 251);
padding: 0.75rem;
border-radius: var(--j-radius-md);
@media (prefers-color-scheme: dark) {
background-color: rgb(28 25 23);
}
`;
const CoStreamGrid = styled("div")`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
`;
const CoStreamItemContainer = styled("div")`
padding: 0.75rem;
border-radius: var(--j-radius-lg);
overflow: hidden;
border: 1px solid rgb(229 231 235);
cursor: pointer;
box-shadow: var(--j-shadow-sm);
&:hover {
background-color: rgb(243 244 246 / 0.05);
}
`;
const LabelContentPair = ({
label,
content,
@@ -211,10 +164,10 @@ const LabelContentPair = ({
content: React.ReactNode;
}) => {
return (
<LabelContentPairContainer>
<div className={classNames("flex flex-col gap-1.5")}>
<span>{label}</span>
<span>{content}</span>
</LabelContentPairContainer>
</div>
);
};
@@ -269,11 +222,19 @@ function RenderCoBinaryStream({
const sizeInKB = (file.totalSize || 0) / 1024;
return (
<BinaryStreamContainer>
<BinaryStreamGrid>
<div className={classNames("mt-8 flex flex-col gap-8")}>
<div className={classNames("grid grid-cols-3 gap-2 max-w-3xl")}>
<LabelContentPair
label="Mime Type"
content={<Badge>{mimeType || "No mime type"}</Badge>}
content={
<span
className={classNames(
"font-mono bg-gray-100 rounded px-2 py-1 text-sm dark:bg-stone-900",
)}
>
{mimeType || "No mime type"}
</span>
}
/>
<LabelContentPair
label="Size"
@@ -294,18 +255,20 @@ function RenderCoBinaryStream({
/>
}
/>
</BinaryStreamGrid>
</div>
{mimeType === "image/png" || mimeType === "image/jpeg" ? (
<LabelContentPair
label="Preview"
content={
<ImagePreviewContainer>
<div
className={classNames("bg-gray-50 dark:bg-gray-925 p-3 rounded")}
>
<RenderBlobImage blob={blob} />
</ImagePreviewContainer>
</div>
}
/>
) : null}
</BinaryStreamContainer>
</div>
);
}
@@ -320,9 +283,14 @@ function RenderCoStream({
const userCoIds = streamPerUser.map((stream) => stream.split("_session")[0]);
return (
<CoStreamGrid>
<div className={classNames("grid grid-cols-3 gap-2")}>
{userCoIds.map((id, idx) => (
<CoStreamItemContainer key={id}>
<div
className={classNames(
"p-3 rounded-lg overflow-hidden border border-gray-200 cursor-pointer shadow-sm hover:bg-gray-100/5",
)}
key={id}
>
<AccountOrGroupPreview coId={id as CoID<RawCoValue>} node={node} />
{/* @ts-expect-error - TODO: fix types */}
{value.items[streamPerUser[idx]]?.map(
@@ -333,9 +301,9 @@ function RenderCoStream({
</div>
),
)}
</CoStreamItemContainer>
</div>
))}
</CoStreamGrid>
</div>
);
}

View File

@@ -1,66 +1,11 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import { JsonObject } from "cojson";
import { styled } from "goober";
import { Button } from "../ui/button.js";
import { ResolveIcon } from "./type-icon.js";
import { PageInfo, isCoId } from "./types.js";
import { CoMapPreview, ValueRenderer } from "./value-renderer.js";
import { Badge } from "../ui/badge.js";
import { Text } from "../ui/text.js";
const GridContainer = styled("div")`
display: grid;
grid-template-columns: repeat(1, 1fr);
gap: 1rem;
@media (min-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 1280px) {
grid-template-columns: repeat(3, 1fr);
}
`;
const GridItem = styled(
({
isCoId,
...rest
}: { isCoId: boolean } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button {...rest} />
),
)`
padding: 0.75rem;
text-align: left;
border-radius: var(--j-radius-lg);
overflow: hidden;
transition: background-color 0.2s;
cursor: ${(props) => (props.isCoId ? "pointer" : "default")};
${(props) =>
props.isCoId
? `
border: 1px solid var(--j-border-color);
box-shadow: var(--j-shadow-sm);
`
: `
background-color: var(--j-foreground);
`}
`;
const TitleContainer = styled("h3")`
display: flex;
justify-content: space-between;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--j-text-color-strong);
`;
const ContentContainer = styled("div")`
margin-top: 0.5rem;
font-size: 0.875rem;
`;
import { classNames } from "../utils.js";
export function GridView({
data,
@@ -74,29 +19,49 @@ export function GridView({
const entries = Object.entries(data);
return (
<GridContainer>
<div
className={classNames(
"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4",
)}
>
{entries.map(([key, child], childIndex) => (
<GridItem
<Button
variant="plain"
key={childIndex}
isCoId={isCoId(child)}
className={classNames(
`p-3 text-left rounded-lg overflow-hidden transition-colors ${
isCoId(child)
? "border border-gray-200 shadow-sm hover:bg-gray-100/5"
: "bg-gray-50 dark:bg-gray-925 cursor-default"
}`,
)}
onClick={() =>
isCoId(child) &&
onNavigate([{ coId: child as CoID<RawCoValue>, name: key }])
}
>
<TitleContainer>
{isCoId(child) ? (
<>
<Text strong>{key}</Text>
<Badge>
<ResolveIcon coId={child as CoID<RawCoValue>} node={node} />
</Badge>
</>
) : (
<Text strong>{key}</Text>
<h3
className={classNames(
"overflow-hidden text-ellipsis whitespace-nowrap",
)}
</TitleContainer>
<ContentContainer>
>
{isCoId(child) ? (
<span className={classNames("font-medium flex justify-between")}>
{key}
<div
className={classNames(
"py-1 px-2 text-sm bg-gray-100 rounded dark:bg-gray-900",
)}
>
<ResolveIcon coId={child as CoID<RawCoValue>} node={node} />
</div>
</span>
) : (
<span>{key}</span>
)}
</h3>
<div className={classNames("mt-2 text-sm")}>
{isCoId(child) ? (
<CoMapPreview coId={child as CoID<RawCoValue>} node={node} />
) : (
@@ -107,9 +72,9 @@ export function GridView({
}}
/>
)}
</ContentContainer>
</GridItem>
</div>
</Button>
))}
</GridContainer>
</div>
);
}

View File

@@ -1,88 +0,0 @@
import { styled } from "goober";
import React from "react";
export type Position =
| "bottom right"
| "bottom left"
| "top right"
| "top left"
| "right"
| "left";
const StyledInspectorButton = styled("button")<{ position: Position }>`
position: fixed;
width: 2.5rem;
height: 2.5rem;
background-color: white;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
margin: 1rem;
padding: 0.5rem !important;
border: 1px solid #e5e3e4;
border-radius: 0.375rem;
${(props) => {
switch (props.position) {
case "bottom right":
return "bottom: 0; right: 0;";
case "bottom left":
return "bottom: 0; left: 0;";
case "top right":
return "top: 0; right: 0;";
case "top left":
return "top: 0; left: 0;";
case "right":
return "right: 0; top: 50%; transform: translateY(-50%);";
case "left":
return "left: 0; top: 50%; transform: translateY(-50%);";
default:
return "";
}
}}
`;
const JazzIcon = styled("svg")`
width: 100%;
height: auto;
position: relative;
left: -1px;
color: #3313F7;
`;
export function InspectorButton({
position = "right",
...buttonProps
}: React.ComponentPropsWithoutRef<"button"> & { position?: Position }) {
return (
<StyledInspectorButton position={position} {...buttonProps}>
<JazzIcon
xmlns="http://www.w3.org/2000/svg"
width="119"
height="115"
viewBox="0 0 119 115"
fill="none"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M118.179 23.8277V0.167999C99.931 7.5527 79.9854 11.6192 59.0897 11.6192C47.1466 11.6192 35.5138 10.2908 24.331 7.7737V30.4076V60.1508C23.2955 59.4385 22.1568 58.8458 20.9405 58.3915C18.1732 57.358 15.128 57.0876 12.1902 57.6145C9.2524 58.1414 6.5539 59.4419 4.4358 61.3516C2.3178 63.2613 0.875401 65.6944 0.291001 68.3433C-0.293399 70.9921 0.00659978 73.7377 1.1528 76.2329C2.2991 78.728 4.2403 80.861 6.7308 82.361C9.2214 83.862 12.1495 84.662 15.1448 84.662C15.6054 84.662 15.8365 84.662 16.0314 84.659C26.5583 84.449 35.042 75.9656 35.2513 65.4386C35.2534 65.3306 35.2544 65.2116 35.2548 65.0486L35.2552 64.7149V64.5521V61.0762V32.1993C43.0533 33.2324 51.0092 33.7656 59.0897 33.7656C59.6696 33.7656 60.2489 33.7629 60.8276 33.7574V89.696C59.792 88.983 58.6533 88.391 57.437 87.936C54.6697 86.903 51.6246 86.632 48.6867 87.159C45.7489 87.686 43.0504 88.987 40.9323 90.896C38.8143 92.806 37.3719 95.239 36.7875 97.888C36.2032 100.537 36.5031 103.283 37.6494 105.778C38.7956 108.273 40.7368 110.405 43.2273 111.906C45.7179 113.406 48.646 114.207 51.6414 114.207C52.1024 114.207 52.3329 114.207 52.5279 114.203C63.0548 113.994 71.5385 105.51 71.7478 94.983C71.7517 94.788 71.7517 94.558 71.7517 94.097V90.621V33.3266C83.962 32.4768 95.837 30.4075 107.255 27.2397V59.9017C106.219 59.1894 105.081 58.5966 103.864 58.1424C101.097 57.1089 98.052 56.8384 95.114 57.3653C92.176 57.8922 89.478 59.1927 87.36 61.1025C85.242 63.0122 83.799 65.4453 83.215 68.0941C82.631 70.743 82.931 73.4886 84.077 75.9837C85.223 78.4789 87.164 80.612 89.655 82.112C92.145 83.612 95.073 84.413 98.069 84.413C98.53 84.413 98.76 84.413 98.955 84.409C109.482 84.2 117.966 75.7164 118.175 65.1895C118.179 64.9945 118.179 64.764 118.179 64.3029V60.8271V23.8277Z"
fill="currentColor"
/>
</JazzIcon>
<span
style={{
position: "absolute",
width: "1px",
height: "1px",
padding: "0",
margin: "-1px",
overflow: "hidden",
clip: "rect(0, 0, 0, 0)",
whiteSpace: "nowrap",
border: "0",
}}
>
Open Jazz Inspector
</span>
</StyledInspectorButton>
);
}

View File

@@ -1,5 +1,4 @@
import { CoID, RawCoValue } from "cojson";
import { styled } from "goober";
import { useAccount } from "jazz-react-core";
import React, { useState } from "react";
import { Button } from "../ui/button.js";
@@ -8,55 +7,15 @@ import { Breadcrumbs } from "./breadcrumbs.js";
import { PageStack } from "./page-stack.js";
import { usePagePath } from "./use-page-path.js";
import { GlobalStyles } from "../ui/global-styles.js";
import { Heading } from "../ui/heading.js";
import { InspectorButton, type Position } from "./inpsector-button.js";
import { classNames } from "../utils.js";
const InspectorContainer = styled("div")`
position: fixed;
height: calc(100% - 12rem);
display: flex;
flex-direction: column;
bottom: 0;
left: 0;
width: 100%;
background-color: white;
border-top: 1px solid var(--j-border-color);
color: var(--j-text-color);
@media (prefers-color-scheme: dark) {
background-color: var(--j-background);
}
`;
const HeaderContainer = styled("div")`
display: flex;
align-items: center;
gap: 1rem;
padding: 0 0.75rem;
margin: 0.75rem 0;
`;
const Form = styled("form")`
width: 24rem;
`;
const InitialForm = styled("form")`
display: flex;
flex-direction: column;
position: relative;
top: -1.5rem;
justify-content: center;
gap: 0.5rem;
height: 100%;
width: 100%;
max-width: 24rem;
margin: 0 auto;
`;
const OrText = styled("p")`
text-align: center;
`;
type Position =
| "bottom right"
| "bottom left"
| "top right"
| "top left"
| "right"
| "left";
export function JazzInspector({ position = "right" }: { position?: Position }) {
const [open, setOpen] = useState(false);
@@ -76,32 +35,73 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
setCoValueId("");
};
const positionClasses = {
"bottom right": "bottom-0 right-0",
"bottom left": "bottom-0 left-0",
"top right": "top-0 right-0",
"top left": "top-0 left-0",
right: "right-0 top-1/2 -translate-y-1/2",
left: "left-0 top-1/2 -translate-y-1/2",
};
if (!open) {
// not sure if this will work, probably is better to use inline styles for the button, but please check.
return (
<InspectorButton position={position} onClick={() => setOpen(true)} />
<Button
id="__jazz_inspector"
variant="secondary"
size="sm"
onClick={() => setOpen(true)}
className={classNames(
`fixed w-10 h-10 bg-white shadow-sm bottom-0 right-0 m-4 p-1.5 ${positionClasses[position]}`,
)}
>
<svg
className={classNames("w-full h-auto relative -left-px text-blue")}
xmlns="http://www.w3.org/2000/svg"
width="119"
height="115"
viewBox="0 0 119 115"
fill="none"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M118.179 23.8277V0.167999C99.931 7.5527 79.9854 11.6192 59.0897 11.6192C47.1466 11.6192 35.5138 10.2908 24.331 7.7737V30.4076V60.1508C23.2955 59.4385 22.1568 58.8458 20.9405 58.3915C18.1732 57.358 15.128 57.0876 12.1902 57.6145C9.2524 58.1414 6.5539 59.4419 4.4358 61.3516C2.3178 63.2613 0.875401 65.6944 0.291001 68.3433C-0.293399 70.9921 0.00659978 73.7377 1.1528 76.2329C2.2991 78.728 4.2403 80.861 6.7308 82.361C9.2214 83.862 12.1495 84.662 15.1448 84.662C15.6054 84.662 15.8365 84.662 16.0314 84.659C26.5583 84.449 35.042 75.9656 35.2513 65.4386C35.2534 65.3306 35.2544 65.2116 35.2548 65.0486L35.2552 64.7149V64.5521V61.0762V32.1993C43.0533 33.2324 51.0092 33.7656 59.0897 33.7656C59.6696 33.7656 60.2489 33.7629 60.8276 33.7574V89.696C59.792 88.983 58.6533 88.391 57.437 87.936C54.6697 86.903 51.6246 86.632 48.6867 87.159C45.7489 87.686 43.0504 88.987 40.9323 90.896C38.8143 92.806 37.3719 95.239 36.7875 97.888C36.2032 100.537 36.5031 103.283 37.6494 105.778C38.7956 108.273 40.7368 110.405 43.2273 111.906C45.7179 113.406 48.646 114.207 51.6414 114.207C52.1024 114.207 52.3329 114.207 52.5279 114.203C63.0548 113.994 71.5385 105.51 71.7478 94.983C71.7517 94.788 71.7517 94.558 71.7517 94.097V90.621V33.3266C83.962 32.4768 95.837 30.4075 107.255 27.2397V59.9017C106.219 59.1894 105.081 58.5966 103.864 58.1424C101.097 57.1089 98.052 56.8384 95.114 57.3653C92.176 57.8922 89.478 59.1927 87.36 61.1025C85.242 63.0122 83.799 65.4453 83.215 68.0941C82.631 70.743 82.931 73.4886 84.077 75.9837C85.223 78.4789 87.164 80.612 89.655 82.112C92.145 83.612 95.073 84.413 98.069 84.413C98.53 84.413 98.76 84.413 98.955 84.409C109.482 84.2 117.966 75.7164 118.175 65.1895C118.179 64.9945 118.179 64.764 118.179 64.3029V60.8271V23.8277Z"
fill="currentColor"
/>
</svg>
<span className={classNames("sr-only")}>Open Jazz Inspector</span>
</Button>
);
}
return (
<InspectorContainer as={GlobalStyles} id="__jazz_inspector">
<HeaderContainer>
<div
className={classNames(
"fixed h-[calc(100%-12rem)] flex flex-col bottom-0 left-0 w-full bg-white border-t border-gray-200 dark:border-stone-900 dark:bg-stone-925",
)}
id="__jazz_inspector"
>
<div className={classNames("flex items-center gap-4 px-3 my-3")}>
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
<Form onSubmit={handleCoValueIdSubmit}>
<form onSubmit={handleCoValueIdSubmit} className={classNames("w-96")}>
{path.length !== 0 && (
<Input
label="CoValue ID"
style={{ fontFamily: "monospace" }}
className={classNames("font-mono")}
hideLabel
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
onChange={(e) => setCoValueId(e.target.value as CoID<RawCoValue>)}
/>
)}
</Form>
</form>
<Button variant="plain" type="button" onClick={() => setOpen(false)}>
Close
</Button>
</HeaderContainer>
</div>
<PageStack
path={path}
@@ -110,14 +110,23 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
addPages={addPages}
>
{path.length <= 0 && (
<InitialForm
<form
onSubmit={handleCoValueIdSubmit}
aria-hidden={path.length !== 0}
className={classNames(
"flex flex-col relative -top-6 justify-center gap-2 h-full w-full max-w-sm mx-auto",
)}
>
<Heading>Jazz CoValue Inspector</Heading>
<h2
className={classNames(
"text-lg text-center font-medium mb-4 text-stone-900 dark:text-white",
)}
>
Jazz CoValue Inspector
</h2>
<Input
label="CoValue ID"
style={{ minWidth: "21rem", fontFamily: "monospace" }}
className={classNames("min-w-[21rem] font-mono")}
hideLabel
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
@@ -127,7 +136,7 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
Inspect CoValue
</Button>
<OrText>or</OrText>
<p className={classNames("text-center")}>or</p>
<Button
variant="secondary"
@@ -138,9 +147,9 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
>
Inspect my account
</Button>
</InitialForm>
</form>
)}
</PageStack>
</InspectorContainer>
</div>
);
}

View File

@@ -1,7 +1,7 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import { styled } from "goober";
import { Page } from "./page.js";
import { Page } from "./page.js"; // Assuming you have a Page component
import { classNames } from "../utils.js";
// Define the structure of a page in the path
interface PageInfo {
coId: CoID<RawCoValue>;
@@ -17,15 +17,6 @@ interface PageStackProps {
children?: React.ReactNode;
}
const PageStackContainer = styled("div")`
position: relative;
padding: 0 0.75rem;
overflow-y: auto;
flex: 1;
color: var(--j-text-color);
font-size: 16px;
`;
export function PageStack({
path,
node,
@@ -37,20 +28,22 @@ export function PageStack({
const index = path.length - 1;
return (
<>
<PageStackContainer>
{children}
{node && page && (
<Page
coId={page.coId}
node={node}
name={page.name || page.coId}
onHeaderClick={goBack}
onNavigate={addPages}
isTopLevel={index === path.length - 1}
/>
)}
</PageStackContainer>
</>
<div
className={classNames(
"relative px-3 overflow-y-auto flex-1 text-stone-700 dark:text-stone-400",
)}
>
{children}
{node && page && (
<Page
coId={page.coId}
node={node}
name={page.name || page.coId}
onHeaderClick={goBack}
onNavigate={addPages}
isTopLevel={index === path.length - 1}
/>
)}
</div>
);
}

View File

@@ -1,10 +1,6 @@
import { CoID, LocalNode, RawCoStream, RawCoValue } from "cojson";
import { styled } from "goober";
import { useMemo } from "react";
import React from "react";
import { Badge } from "../ui/badge.js";
import { Heading } from "../ui/heading.js";
import { Text } from "../ui/text.js";
import { classNames } from "../utils.js";
import { CoStreamView } from "./co-stream-view.js";
import { GridView } from "./grid-view.js";
import { TableView } from "./table-viewer.js";
@@ -13,65 +9,6 @@ import { PageInfo } from "./types.js";
import { useResolvedCoValue } from "./use-resolve-covalue.js";
import { AccountOrGroupPreview } from "./value-renderer.js";
interface PageContainerProps extends React.HTMLAttributes<HTMLDivElement> {
isTopLevel?: boolean;
}
const BasePageContainer = React.forwardRef<HTMLDivElement, PageContainerProps>(
({ isTopLevel, ...rest }, ref) => <div ref={ref} {...rest} />,
);
const PageContainer = styled(BasePageContainer)<PageContainerProps>`
position: absolute;
z-index: 10;
inset: 0;
width: 100%;
height: 100%;
padding: 0 0.75rem;
`;
const BackButton = styled("div")`
position: absolute;
left: 0;
right: 0;
top: 0;
height: 2.5rem;
`;
const HeaderContainer = styled("div")`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
`;
const TitleContainer = styled("div")`
display: flex;
align-items: center;
gap: 0.75rem;
`;
const Title = styled(Heading)`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
`;
const BadgeContainer = styled("div")`
display: flex;
align-items: center;
gap: 0.75rem;
`;
const ContentContainer = styled("div")`
overflow: auto;
`;
const OwnerText = styled(Text)`
margin-top: 1rem;
`;
type PageProps = {
coId: CoID<RawCoValue>;
node: LocalNode;
@@ -115,38 +52,54 @@ export function Page({
}
return (
<PageContainer style={style} className={className} isTopLevel={isTopLevel}>
<div
style={style}
className={className + "absolute z-10 inset-0 w-full h-full px-3"}
>
{!isTopLevel && (
<BackButton
<div
className={classNames("absolute left-0 right-0 top-0 h-10")}
aria-label="Back"
onClick={() => {
onHeaderClick?.();
}}
aria-hidden="true"
></BackButton>
></div>
)}
<HeaderContainer>
<TitleContainer>
<Title>
<div className={classNames("flex justify-between items-center mb-4")}>
<div className={classNames("flex items-center gap-3")}>
<h2
className={classNames(
"text-lg font-medium flex flex-col items-start gap-1 text-stone-900 dark:text-white",
)}
>
<span>
{name}
{typeof snapshot === "object" && "name" in snapshot ? (
<span style={{ color: "#57534e", fontWeight: 500 }}>
<span className={classNames("text-gray-600 font-medium")}>
{" "}
{(snapshot as { name: string }).name}
</span>
) : null}
</span>
</Title>
<BadgeContainer>
<Badge>
{type && <TypeIcon type={type} extendedType={extendedType} />}
</Badge>
<Badge>{coId}</Badge>
</BadgeContainer>
</TitleContainer>
</HeaderContainer>
<ContentContainer>
</h2>
<span
className={classNames(
"text-sm text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono",
)}
>
{type && <TypeIcon type={type} extendedType={extendedType} />}
</span>
<span
className={classNames(
"text-sm text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono",
)}
>
{coId}
</span>
</div>
</div>
<div className={classNames("overflow-auto")}>
{type === "costream" ? (
<CoStreamView
data={snapshot}
@@ -160,7 +113,7 @@ export function Page({
<TableView data={snapshot} node={node} onNavigate={onNavigate} />
)}
{extendedType !== "account" && extendedType !== "group" && (
<OwnerText muted>
<div className={classNames("text-sm text-gray-500 mt-4")}>
Owned by{" "}
<AccountOrGroupPreview
coId={value.group.id}
@@ -170,9 +123,9 @@ export function Page({
onNavigate([{ coId: value.group.id, name: "owner" }]);
}}
/>
</OwnerText>
</div>
)}
</ContentContainer>
</PageContainer>
</div>
</div>
);
}

View File

@@ -1,34 +1,13 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import type { JsonObject } from "cojson";
import { styled } from "goober";
import { useMemo, useState } from "react";
import { Button } from "../ui/button.js";
import { Icon } from "../ui/icon.js";
import { classNames } from "../utils.js";
import { PageInfo } from "./types.js";
import { useResolvedCoValues } from "./use-resolve-covalue.js";
import { ValueRenderer } from "./value-renderer.js";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../ui/table.js";
import { Text } from "../ui/text.js";
const TableContainer = styled("div")`
margin-top: 2rem;
`;
const PaginationContainer = styled("div")`
padding: 1rem 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
`;
export function TableView({
data,
node,
@@ -73,20 +52,46 @@ export function TableView({
};
return (
<TableContainer>
<Table>
<TableHead>
<TableRow>
{[...keys, "Action"].map((key) => (
<TableHeader key={key}>{key}</TableHeader>
<div>
<table
className={classNames(
"min-w-full text-sm border-spacing-0 border-collapse",
)}
>
<thead className={classNames("sticky top-0 border-b border-gray-200")}>
<tr>
{["", ...keys].map((key) => (
<th
key={key}
className={classNames(
"p-3 bg-gray-50 dark:bg-gray-925 text-left font-medium rounded",
)}
>
{key}
</th>
))}
</TableRow>
</TableHead>
<TableBody>
</tr>
</thead>
<tbody className={classNames(" border-t border-gray-200")}>
{resolvedRows.slice(0, visibleRowsCount).map((item, index) => (
<TableRow key={index}>
<tr key={index}>
<td className={classNames("p-1")}>
<Button
variant="tertiary"
onClick={() =>
onNavigate([
{
coId: item.value!.id,
name: index.toString(),
},
])
}
>
<Icon name="link" />
</Button>
</td>
{keys.map((key) => (
<TableCell key={key}>
<td key={key} className={classNames("p-4 whitespace-nowrap")}>
<ValueRenderer
json={(item.snapshot as JsonObject)[key]}
onCoIDClick={(coId) => {
@@ -106,39 +111,35 @@ export function TableView({
handleClick();
}}
/>
</TableCell>
</td>
))}
<TableCell>
<Button
variant="secondary"
onClick={() =>
onNavigate([
{
coId: item.value!.id,
name: index.toString(),
},
])
}
>
View
</Button>
</TableCell>
</TableRow>
</tr>
))}
</TableBody>
</Table>
<PaginationContainer>
<Text muted small>
</tbody>
</table>
<div
className={classNames(
"py-4 text-gray-500 flex items-center justify-between gap-2",
)}
>
<span>
Showing {Math.min(visibleRowsCount, coIdArray.length)} of{" "}
{coIdArray.length}
</Text>
</span>
{hasMore && (
<Button variant="secondary" onClick={loadMore}>
Load more
</Button>
<div className={classNames("text-center")}>
<Button
variant="plain"
onClick={loadMore}
className={classNames(
"px-4 py-2 bg-blue text-white rounded hover:bg-blue-800",
)}
>
Load more
</Button>
</div>
)}
</PaginationContainer>
</TableContainer>
</div>
</div>
);
}

View File

@@ -1,24 +1,11 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import { styled } from "goober";
import {
CoJsonType,
ExtendedCoJsonType,
useResolvedCoValue,
} from "./use-resolve-covalue.js";
const IconText = styled("span")`
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
`;
const UnavailableText = styled("div")`
font-weight: 500;
`;
const EmptySpace = styled("div")`
white-space: pre;
width: 3.5rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
`;
import { classNames } from "../utils.js";
export const TypeIcon = ({
type,
@@ -41,7 +28,7 @@ export const TypeIcon = ({
const iconKey = extendedType || type;
const icon = iconMap[iconKey as keyof typeof iconMap];
return icon ? <IconText>{icon}</IconText> : null;
return icon ? <span className={classNames("font-mono")}>{icon}</span> : null;
};
export const ResolveIcon = ({
@@ -54,10 +41,13 @@ export const ResolveIcon = ({
const { type, extendedType, snapshot } = useResolvedCoValue(coId, node);
if (snapshot === "unavailable" && !type) {
return <UnavailableText>Unavailable</UnavailableText>;
return (
<div className={classNames("text-gray-600 font-medium")}>Unavailable</div>
);
}
if (!type) return <EmptySpace> </EmptySpace>;
if (!type)
return <div className={classNames("whitespace-pre w-14 font-mono")}> </div>;
return <TypeIcon type={type} extendedType={extendedType} />;
};

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