Compare commits

..

2 Commits

Author SHA1 Message Date
Guido D'Orsi
ff44f51a14 feat: make the PeerKnownState subscribable 2024-10-20 01:24:59 +02:00
Guido D'Orsi
a9d2c82557 chore: refactor peer known states management 2024-10-19 16:02:22 +02:00
168 changed files with 1504 additions and 5143 deletions

View File

@@ -1,51 +0,0 @@
name: Jazz Run Tests
on:
push:
branches: ["main"]
pull_request:
types: [opened, synchronize, reopened]
jobs:
test:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Enable corepack
run: corepack enable
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build jazz-run
run: pnpm exec turbo build && chmod +x dist/index.js;
working-directory: ./packages/jazz-run
- name: Run create account
run: ./dist/index.js account create --name "Jazz Run CI test"
working-directory: ./packages/jazz-run

View File

@@ -1,51 +0,0 @@
name: Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
- name: Enable corepack
run: corepack enable
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
version: pnpm changeset-version
publish: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -13,13 +13,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Enable corepack
run: corepack enable
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -29,7 +29,7 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}

View File

@@ -1,131 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official email address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to [the community leaders responsible for enforcement](mailto:hello@gcmp.io).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,73 +0,0 @@
# Contribution Guide
Thank you for considering contributing to Jazz! Jazz is an open-source framework for building local-first apps. We value your time and effort and are excited to collaborate with you. This guide will help you get started with contributing.
## How to Contribute
### 1. Reporting Bugs
If you find a bug, please [open an issue with as much detail as possible](https://github.com/gardencmp/jazz/issues). Include:
- A clear and descriptive title.
- Steps to reproduce the issue.
- What you expected to happen.
- What actually happened.
### 2. Suggesting Enhancements
We welcome all ideas! If you have suggestions, feel free to open an issue marked with the "enhancement" label. Please provide context on why the enhancement would be beneficial and how it could be implemented.
### 3. Pull Requests
1. **Fork the repository** and create your feature branch (see [GitHub's guide on forking a repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo) if you're unfamiliar with the process):
2. **Make your changes**, ensuring that you follow our coding standards (`pnpm format` (prettier) and `pnpm lint` (eslint) will automatically let you know there are issues).
3. **Commit your changes** with a descriptive commit message.
4. **Push to your fork** and submit a pull request.
5. **Describe your pull request**, explaining the problem it solves or the enhancement it adds.
### 4. Code Style Guidelines
- We use [Prettier](https://prettier.io/) for formatting. Please ensure your code is formatted before submitting.
- Write descriptive comments where necessary.
### 5. Local Setup
1. **Clone the repository**:
```bash
git clone https://github.com/gardencmp/jazz.git
```
2. **Install dependencies**:
```bash
pnpm install
```
3. **Run tests** to verify everything is working:
```bash
pnpm test
```
### 6. Testing
Please write tests for any new features or bug fixes. We use Vitest for unit tests. Make sure all tests pass before submitting a pull request.
### 7. Communication
- If you're unsure about anything, feel free to ask questions by opening a discussion, reaching out via issues, or on our [Discord](https://discord.gg/utDMjHYg42).
- Be respectful and constructive, this is a welcoming community for everyone.
- Please be mindful of GitHubs [Community Guidelines](https://docs.github.com/en/site-policy/github-terms/github-community-guidelines), which include being kind, avoiding disruptive behavior, and respecting others.
## Code of Conduct
Please read and adhere to our [Code of Conduct](./CODE_OF_CONDUCT.md) to ensure a positive experience for all contributors.
---
Thank you again for your interest in contributing to Jazz. Your help makes this project better for everyone!
If you have any questions, don't hesitate to reach out. Let's make something great together!

View File

@@ -1,14 +1,5 @@
# @jazz-e2e/binarycostream
## 0.0.89
### Patch Changes
- Updated dependencies [1ed4ab5]
- cojson@0.8.11
- jazz-react@0.8.11
- jazz-tools@0.8.11
## 0.0.88
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@jazz-e2e/binarycostream",
"private": true,
"version": "0.0.89",
"version": "0.0.88",
"type": "module",
"scripts": {
"dev": "vite",
@@ -14,11 +14,11 @@
"*.{js,jsx,mdx,json}": "prettier --write"
},
"dependencies": {
"cojson": "workspace:0.8.11",
"cojson": "workspace:0.8.5",
"hash-slash": "workspace:0.2.1",
"is-ci": "^3.0.1",
"jazz-react": "workspace:0.8.11",
"jazz-tools": "workspace:0.8.11",
"jazz-react": "workspace:0.8.7",
"jazz-tools": "workspace:0.8.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},

View File

@@ -1,14 +1,5 @@
# @jazz-e2e/covalues
## 0.0.88
### Patch Changes
- Updated dependencies [1ed4ab5]
- cojson@0.8.11
- jazz-react@0.8.11
- jazz-tools@0.8.11
## 0.0.87
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@jazz-e2e/covalues",
"private": true,
"version": "0.0.88",
"version": "0.0.87",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -27,11 +27,10 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:5173/",
baseURL: isCI ? "http://localhost:4173/" : "http://localhost:5173",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
permissions: ["clipboard-read", "clipboard-write"],
},
/* Configure projects for major browsers */
@@ -43,11 +42,8 @@ export default defineConfig({
],
/* Run your local dev server before starting the tests */
webServer: [
{
command: "pnpm preview --port 5173",
url: "http://localhost:5173/",
reuseExistingServer: !isCI,
},
],
webServer: isCI ? {
command: "pnpm preview",
url: "http://localhost:4173/",
} : undefined,
});

View File

@@ -2,20 +2,15 @@ import React from "react";
import ReactDOM from "react-dom/client";
import { AuthAndJazz } from "./jazz";
import { TestInput } from "./pages/TestInput";
import { ResumeSyncState } from "./pages/ResumeSyncState";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { RouterProvider, createHashRouter } from "react-router-dom";
const router = createBrowserRouter([
const router = createHashRouter([
{
path: "/test-input",
path: "/",
element: <TestInput />,
},
{
path: "/resume-sync",
element: <ResumeSyncState />,
},
{
path: "/",
path: "/test-input",
element: <TestInput />,
},
]);

View File

@@ -1,43 +1,25 @@
import { createJazzReactApp, useDemoAuth } from "jazz-react";
import { useEffect, useRef } from "react";
import { createJazzReactApp } from "jazz-react";
import { ephemeralCredentialsAuth } from "jazz-tools";
import { useState } from "react";
const key = `test-comap@jazz.tools`;
const url = new URL(window.location.href);
const peer =
(url.searchParams.get("peer") as `ws://${string}`) ??
`wss://cloud.jazz.tools/`;
const localSync = new URLSearchParams(location.search).has("localSync");
const Jazz = createJazzReactApp();
export const { useAccount, useCoState } = Jazz;
function getUserInfo() {
return url.searchParams.get("userName") ?? "Mister X";
}
export function AuthAndJazz({ children }: { children: React.ReactNode }) {
const [auth, state] = useDemoAuth();
const [ephemeralAuth] = useState(ephemeralCredentialsAuth())
const signedUp = useRef(false);
useEffect(() => {
if (state.state === "ready" && !signedUp.current) {
const userName = getUserInfo();
if (state.existingUsers.includes(userName)) {
state.logInAs(userName);
} else {
state.signUp(userName);
}
signedUp.current = true;
}
}, [state.state]);
return (
<Jazz.Provider auth={auth} peer={`${peer}?key=${key}`}>
{children}
</Jazz.Provider>
);
return (
<Jazz.Provider auth={ephemeralAuth} peer={
localSync
? `ws://localhost:4200?key=${key}`
: `wss://cloud.jazz.tools/?key=${key}`
}>
{children}
</Jazz.Provider>
);
}

View File

@@ -1,53 +0,0 @@
import { co, CoMap, Group, ID } from "jazz-tools";
import { useAccount, useCoState } from "../jazz";
import { useEffect, useState } from "react";
export class ResumeSyncCoMap extends CoMap {
value = co.string;
}
function getIdParam() {
const url = new URL(window.location.href);
return (url.searchParams.get("id") as ID<ResumeSyncCoMap>) ?? undefined;
}
export function ResumeSyncState() {
const [id, setId] = useState(getIdParam);
const coMap = useCoState(ResumeSyncCoMap, id);
const { me } = useAccount();
useEffect(() => {
if (id) {
const url = new URL(window.location.href);
url.searchParams.set("id", id);
history.pushState({}, "", url.toString());
}
}, [id])
useEffect(() => {
if (!me || id) return;
const group = Group.create({ owner: me });
group.addMember("everyone", "writer");
setId(ResumeSyncCoMap.create({ value: "" }, { owner: group }).id);
}, [me]);
if (!coMap) return null;
return (
<div>
<h1>Resume Sync State</h1>
<p data-testid="id">{coMap.id}</p>
<label htmlFor="value">Value</label>
<input
id="value"
value={coMap.value ?? ""}
onChange={(e) => {
coMap.value = e.target.value;
}}
/>
</div>
);
}

View File

@@ -1,47 +0,0 @@
import { test, expect } from "@playwright/test";
import { setTimeout } from "node:timers/promises";
test.describe("ResumeSyncState", () => {
test.skip("should resume the sync even after a page reload", async ({ page, browser }) => {
const context = page.context();
await page.goto("/resume-sync?userName=SuperMario");
const id = await page.getByTestId("id").textContent();
// Sync an initial value
await page.getByRole("textbox", { name: "Value" }).fill("Let's go!");
await setTimeout(1000);
await context.setOffline(true);
// Change the value while offline
await page.getByRole("textbox", { name: "Value" }).fill("Mammamia!");
// Navigate away from the page
await page.goto(`about:blank`);
await setTimeout(1000);
await context.setOffline(false);
// Reload the page but without loading the coValue
// await page.goto(`/resume-sync?userName=SuperMario`);
await page.goto(`/resume-sync?userName=SuperMario`);
await setTimeout(1000);
await expect(page.getByTestId("id")).toBeInViewport();
// Create a new incognito instance and try to load the coValue
const newUserPage = await (await browser.newContext()).newPage();
await newUserPage.goto(`/resume-sync?userName=Luigi&id=${id}`);
await expect(newUserPage.getByTestId("id")).toBeInViewport();
// The initial user should have synced the value even if the coValue was not loaded
// when the user is back online
await expect(newUserPage.getByRole("textbox", { name: "Value" })).toHaveValue("Mammamia!", {
timeout: 20_000
});
});
});

View File

@@ -1,13 +1,5 @@
# jazz-example-book-shelf
## 0.1.4
### Patch Changes
- jazz-react@0.8.11
- jazz-tools@0.8.11
- jazz-browser-media-images@0.8.11
## 0.1.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-example-book-shelf",
"version": "0.1.4",
"version": "0.1.3",
"private": true,
"scripts": {
"dev": "next dev",
@@ -11,9 +11,9 @@
},
"dependencies": {
"clsx": "^2.0.0",
"jazz-browser-media-images": "workspace:0.8.11",
"jazz-react": "workspace:0.8.11",
"jazz-tools": "workspace:0.8.11",
"jazz-browser-media-images": "workspace:0.8.7",
"jazz-react": "workspace:0.8.7",
"jazz-tools": "workspace:0.8.5",
"next": "14.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"

View File

@@ -1,15 +1,5 @@
# jazz-example-chat
## 0.0.88
### Patch Changes
- Updated dependencies [1ed4ab5]
- cojson@0.8.11
- jazz-react@0.8.11
- jazz-react-auth-clerk@0.8.11
- jazz-tools@0.8.11
## 0.0.87
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat-clerk",
"private": true,
"version": "0.0.88",
"version": "0.0.87",
"type": "module",
"scripts": {
"dev": "vite",
@@ -21,11 +21,11 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:0.8.11",
"cojson": "workspace:0.8.5",
"hash-slash": "workspace:0.2.1",
"jazz-react": "workspace:0.8.11",
"jazz-react-auth-clerk": "workspace:0.8.11",
"jazz-tools": "workspace:0.8.11",
"jazz-react": "workspace:0.8.7",
"jazz-react-auth-clerk": "workspace:0.8.7",
"jazz-tools": "workspace:0.8.5",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -1,14 +1,5 @@
# chat-rn-clerk
## 1.0.4
### Patch Changes
- jazz-react-auth-clerk@0.8.11
- jazz-react-native@0.8.11
- jazz-tools@0.8.11
- jazz-react-native-media-images@0.8.7
## 1.0.3
### Patch Changes

View File

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

View File

@@ -1,12 +1,5 @@
# chat-rn
## 1.0.6
### Patch Changes
- jazz-react-native@0.8.11
- jazz-tools@0.8.11
## 1.0.5
### Patch Changes

View File

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

View File

@@ -1,14 +1,5 @@
# jazz-example-chat
## 0.0.90
### Patch Changes
- Updated dependencies [1ed4ab5]
- cojson@0.8.11
- jazz-react@0.8.11
- jazz-tools@0.8.11
## 0.0.89
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.90",
"version": "0.0.89",
"type": "module",
"scripts": {
"dev": "vite",
@@ -22,10 +22,10 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:0.8.11",
"cojson": "workspace:0.8.5",
"hash-slash": "workspace:0.2.1",
"jazz-react": "workspace:0.8.11",
"jazz-tools": "workspace:0.8.11",
"jazz-react": "workspace:0.8.7",
"jazz-tools": "workspace:0.8.5",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -1,13 +1,5 @@
# jazz-example-inspector
## 0.0.66
### Patch Changes
- Updated dependencies [1ed4ab5]
- cojson@0.8.11
- cojson-transport-ws@0.8.11
## 0.0.65
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-inspector",
"private": true,
"version": "0.0.66",
"version": "0.0.65",
"type": "module",
"scripts": {
"dev": "vite",
@@ -15,8 +15,8 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:0.8.11",
"cojson-transport-ws": "workspace:0.8.11",
"cojson": "workspace:0.8.5",
"cojson-transport-ws": "workspace:0.8.7",
"hash-slash": "workspace:0.2.1",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",

View File

@@ -1,12 +1,5 @@
# jazz-example-musicplayer
## 0.0.10
### Patch Changes
- jazz-react@0.8.11
- jazz-tools@0.8.11
## 0.0.9
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-music-player",
"private": true,
"version": "0.0.10",
"version": "0.0.9",
"type": "module",
"scripts": {
"dev": "vite",
@@ -22,8 +22,8 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "workspace:0.8.11",
"jazz-tools": "workspace:0.8.11",
"jazz-react": "workspace:0.8.7",
"jazz-tools": "workspace:0.8.5",
"lucide-react": "^0.274.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@@ -1,12 +1,5 @@
# jazz-password-manager
## 0.0.9
### Patch Changes
- jazz-react@0.8.11
- jazz-tools@0.8.11
## 0.0.8
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-password-manager",
"private": true,
"version": "0.0.9",
"version": "0.0.8",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,8 +11,8 @@
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
},
"dependencies": {
"jazz-react": "workspace:0.8.11",
"jazz-tools": "workspace:0.8.11",
"jazz-react": "workspace:0.8.7",
"jazz-tools": "workspace:0.8.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.41.5",

View File

@@ -1,13 +1,5 @@
# jazz-example-pets
## 0.0.107
### Patch Changes
- jazz-react@0.8.11
- jazz-tools@0.8.11
- jazz-browser-media-images@0.8.11
## 0.0.106
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.107",
"version": "0.0.106",
"type": "module",
"scripts": {
"dev": "vite",
@@ -23,9 +23,9 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-browser-media-images": "workspace:0.8.11",
"jazz-react": "workspace:0.8.11",
"jazz-tools": "workspace:0.8.11",
"jazz-browser-media-images": "workspace:0.8.7",
"jazz-react": "workspace:0.8.7",
"jazz-tools": "workspace:0.8.5",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",
@@ -50,7 +50,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"is-ci": "^3.0.1",
"jazz-run": "workspace:0.8.11",
"jazz-run": "workspace:0.8.7",
"postcss": "^8.4.27",
"tailwindcss": "3.3.2",
"typescript": "^5.3.3",

View File

@@ -1,12 +1,5 @@
# jazz-example-todo
## 0.0.106
### Patch Changes
- jazz-react@0.8.11
- jazz-tools@0.8.11
## 0.0.105
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.106",
"version": "0.0.105",
"type": "module",
"scripts": {
"dev": "vite",
@@ -20,8 +20,8 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "workspace:0.8.11",
"jazz-tools": "workspace:0.8.11",
"jazz-react": "workspace:0.8.7",
"jazz-tools": "workspace:0.8.5",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -15,7 +15,6 @@
"clsx": "^2.1.1",
"lucide-react": "^0.436.0",
"next": "14.2.7",
"next-themes": "^0.2.1",
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^1.14.0",

View File

@@ -28,7 +28,7 @@ export function Button(props: ButtonProps) {
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",
"text-stone-900 border border-stone-200 dark:border-stone-800 font-medium hover:border-stone-300 hover:dark:border-stone-700 dark:text-white",
};
const classNames = clsx(

View File

@@ -6,10 +6,12 @@ export function CodeRef({ children }: { children: React.ReactNode }) {
className={clsx(
"font-mono",
"text-[0.9em]",
"px-2 py-1",
"rounded",
"text-stone-900 dark:text-stone-200",
"px-1 py-0.5",
"rounded-sm",
"border",
"text-stone-800 dark:text-stone-200",
"bg-stone-100 dark:bg-stone-800",
"border-stone-200 dark:border-stone-900",
)}
>
{children}

View File

@@ -6,7 +6,7 @@ export function GridCard(props: { children: ReactNode; className?: string }) {
<div
className={clsx(
"col-span-2 p-4 [&>h4]:mt-0 [&>h3]:mt-0 [&>:last-child]:mb-0",
"border rounded-xl shadow-sm",
"border border-stone-200 dark:border-stone-900 rounded-xl shadow-sm",
props.className,
)}
>

View File

@@ -30,7 +30,7 @@ export function H2({ children, className }: HeadingProps) {
className,
"font-display",
"text-stone-950 dark:text-white",
"text-2xl md:text-4xl",
"text-2xl",
"mb-2",
"font-semibold",
"tracking-tight",
@@ -48,7 +48,7 @@ export function H3({ children, className }: HeadingProps) {
className,
"font-display",
"text-stone-950 dark:text-white",
"text-xl md:text-2xl",
"text-xl",
"mb-2",
"font-semibold",
"tracking-tight",

View File

@@ -1,5 +1,3 @@
import clsx from "clsx";
export function P({ children, className }: { children: React.ReactNode, className?: string }) {
return <p className={clsx(className, "mb-4")}>{children}</p>;
export function P({ children }: { children: React.ReactNode }) {
return <p className="mb-4">{children}</p>;
}

View File

@@ -15,7 +15,7 @@ export function Input(props: Props) {
const inputClassName = clsx(
"w-full rounded-md border px-3.5 py-2 shadow-sm",
"font-medium text-stone-900",
"dark:text-white",
"dark:border-stone-900 dark:text-white",
);
const containerClassName = clsx("grid gap-1", className);

View File

@@ -5,20 +5,17 @@ export function LabelledFeatureIcon({
label,
icon: Icon,
explanation,
className,
}: {
label: string;
icon: LucideIcon;
explanation: React.ReactNode;
className?: string;
}) {
return (
<div
className={clsx(
className,
"p-4 flex flex-col gap-3",
"not-prose text-base",
"border rounded-xl",
"border border-stone-200 dark:border-stone-900 rounded-xl",
)}
>
<div>

View File

@@ -1,32 +1,23 @@
import clsx from "clsx";
import { ReactNode } from "react";
export function Prose({
children,
className,
size = "md",
}: {
children: ReactNode;
className?: string;
size?: "sm" | "md" | "lg";
}) {
const sizeClassName = {
sm: "prose-sm",
md: "",
lg: "prose-xl",
}[size];
export function Prose(props: { children: ReactNode; className?: string }) {
return (
<div
className={clsx(
className,
"prose",
sizeClassName,
"dark:prose-invert",
"prose-code:dark:bg-stone-900",
"max-w-4xl prose-stone dark:prose-invert",
"prose-headings:font-display",
"lg:prose-h1:text-5xl prose-h1:font-medium prose-h1:tracking-tight",
"lg:prose-h2:text-3xl prose-h2:font-medium prose-h2:tracking-tight",
"prose-p:leading-snug",
"prose-strong:font-medium",
"prose-code:font-normal prose-code:before:content-none prose-code:after:content-none prose-code:bg-stone-100 prose-code:dark:bg-stone-900 prose-code:p-1 prose-code:rounded",
"prose-pre:border prose-pre:p-0 prose-pre:bg-stone-50 prose-pre:dark:bg-stone-900 dark:prose-pre:border-stone-800",
"prose-pre:text-black dark:prose-pre:text-white",
props.className || "prose",
)}
>
{children}
{props.children}
</div>
);
}

View File

@@ -9,7 +9,7 @@ function H2Sub({ children }: { children: React.ReactNode }) {
"text-lg lg:text-xl",
"leading-snug",
"tracking-tight",
"max-w-2xl",
"max-w-4xl",
"text-stone-700 dark:text-stone-500",
)}
>
@@ -21,14 +21,12 @@ function H2Sub({ children }: { children: React.ReactNode }) {
export function SectionHeader({
title,
slogan,
className,
}: {
title: ReactNode;
slogan: ReactNode;
className?: string;
}) {
return (
<hgroup className={clsx(className, "mb-5")}>
<hgroup className="mb-5">
<H2>{title}</H2>
<H2Sub>{slogan}</H2Sub>
</hgroup>

View File

@@ -1,35 +0,0 @@
export function Testimonial({
children,
name,
role,
}: {
children: React.ReactNode;
name: string;
role: string;
}) {
return (
<figure className="max-w-2xl">
<svg
className="size-8 text-blue"
fill="currentColor"
viewBox="0 0 32 32"
aria-hidden="true"
>
<path d="M9.352 4C4.456 7.456 1 13.12 1 19.36c0 5.088 3.072 8.064 6.624 8.064 3.36 0 5.856-2.688 5.856-5.856 0-3.168-2.208-5.472-5.088-5.472-.576 0-1.344.096-1.536.192.48-3.264 3.552-7.104 6.624-9.024L9.352 4zm16.512 0c-4.8 3.456-8.256 9.12-8.256 15.36 0 5.088 3.072 8.064 6.624 8.064 3.264 0 5.856-2.688 5.856-5.856 0-3.168-2.304-5.472-5.184-5.472-.576 0-1.248.096-1.44.192.48-3.264 3.456-7.104 6.528-9.024L25.864 4z"></path>
</svg>
<blockquote className="mt-6 text-balance font-medium leading-8 text-stone-900 sm:text-lg dark:text-white">
{children}
</blockquote>
<figcaption className="mt-6 flex items-center gap-x-6">
<div className="text-sm leading-6 sm:text-base">
<div className="font-semibold text-stone-900 dark:text-white">
{name}
</div>
<div className="mt-0.5 text-stone-600 dark:text-stone-500">
{role}
</div>
</div>
</figcaption>
</figure>
);
}

View File

@@ -1,39 +0,0 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
import { useEffect } from "react";
function ThemeWatcher() {
let { resolvedTheme, setTheme } = useTheme();
useEffect(() => {
let media = window.matchMedia("(prefers-color-scheme: dark)");
function onMediaChange() {
let systemTheme = media.matches ? "dark" : "light";
if (resolvedTheme === systemTheme) {
setTheme("system");
}
}
onMediaChange();
media.addEventListener("change", onMediaChange);
return () => {
media.removeEventListener("change", onMediaChange);
};
}, [resolvedTheme, setTheme]);
return null;
}
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider {...props}>
<ThemeWatcher />
{children}
</NextThemesProvider>
);
}

View File

@@ -1,44 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { MoonIcon, SunIcon } from "lucide-react";
import clsx from "clsx";
export function ThemeToggle({ className }: { className?: string }) {
let { resolvedTheme, setTheme } = useTheme();
let otherTheme = resolvedTheme === "dark" ? "light" : "dark";
let [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<button
type="button"
className={clsx(
className,
"md:p-2 md:rounded-full md:border",
"text-stone-400 hover:text-stone-900 dark:text-stone-400 dark:hover:text-white",
"md:hover:bg-stone-200 md:dark:hover:bg-stone-900",
"transition-colors"
)}
aria-label={
mounted ? `Switch to ${otherTheme} theme` : "Toggle theme"
}
onClick={() => setTheme(otherTheme)}
>
<MoonIcon
size={24}
strokeWidth={1.5}
className="size-5 md:size-6 stroke-stone-900 dark:hidden"
/>
<SunIcon
size={24}
strokeWidth={1.5}
className="size-5 md:size-6 hidden stroke-white dark:block"
/>
</button>
);
}

View File

@@ -4,7 +4,6 @@ import clsx from "clsx";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ReactNode } from "react";
import { ThemeToggle } from "../molecules/ThemeToggle";
type FooterSection = {
title: string;
@@ -21,38 +20,20 @@ type FooterProps = {
sections: FooterSection[];
};
function Copyright({
className,
companyName,
}: {
companyName: string;
className?: string;
}) {
return (
<p className={clsx(className, "text-sm")}>
© {new Date().getFullYear()} {companyName}
</p>
);
}
export function Footer({ logo, companyName, sections }: FooterProps) {
return (
<footer className="w-full border-t bg-stone-100 dark:bg-stone-925">
<div className="container py-8 md:py-16 grid gap-y-8 grid-cols-12">
<div className="flex flex-col justify-between col-span-full md:col-span-4">
<footer className="flex z-10 mt-10 min-h-[15rem] border-t bg-stone-100 dark:bg-stone-925 text-stone-600 dark:text-stone-400 w-full justify-center dark:border-stone-900">
<div className="p-8 container w-full grid grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-8 max-sm:mb-12">
<div className="col-span-full md:col-span-1 sm:row-start-4 md:row-start-auto lg:col-span-2 md:row-span-2 md:flex-1 flex flex-row md:flex-col max-sm:mt-4 justify-between max-sm:items-start gap-2 text-sm min-w-[10rem]">
{logo}
<Copyright
className="hidden md:block"
companyName={companyName}
/>
<p className="max-sm:text-right">
© {new Date().getFullYear()}
<br />
{companyName}
</p>
</div>
{sections.map((section, index) => (
<div
key={index}
className="flex flex-col gap-2 text-sm col-span-4 sm:col-span-4 md:col-span-2"
>
<div key={index} className="flex flex-col gap-2 text-sm">
<h2 className="font-medium">{section.title}</h2>
{section.links.map((link, linkIndex) => (
<FooterLink
@@ -65,15 +46,6 @@ export function Footer({ logo, companyName, sections }: FooterProps) {
))}
</div>
))}
<div className="hidden md:flex justify-end items-end md:col-span-2">
<ThemeToggle />
</div>
<Copyright
className="col-span-full md:hidden"
companyName={companyName}
/>
</div>
</footer>
);

View File

@@ -6,7 +6,6 @@ import { ReactNode, useLayoutEffect, useRef, useState } from "react";
import { BreadCrumb } from "../molecules/Breadcrumb";
import clsx from "clsx";
import Link from "next/link";
import { ThemeToggle } from "../molecules/ThemeToggle";
export function Nav({
mainLogo,
@@ -40,9 +39,11 @@ export function Nav({
<nav
className={[
clsx(
"hidden md:flex sticky left-0 right-0 top-0 w-full justify-center",
"bg-white dark:bg-stone-950 border-b",
"hidden md:flex sticky left-0 right-0 top-0 max-sm:bottom-0 w-full justify-center",
"bg-white dark:bg-stone-950 border-b max-sm:border-t border-stone-200 dark:border-stone-900",
"max-h-none overflow-hidden transition[max-height] duration-300 ease-in-out",
"z-50",
menuOpen ? "h-[100dvh]" : "h-16",
),
].join(" ")}
>
@@ -89,7 +90,6 @@ export function Nav({
setMenuOpen((o) => !o);
setSearchOpen(false);
}}
aria-label="Open menu"
>
<MenuIcon />
<BreadCrumb items={items} />
@@ -108,7 +108,7 @@ export function Nav({
<nav
className={clsx(
"md:hidden fixed flex flex-col bottom-4 right-4 z-50",
"bg-stone-50 dark:bg-stone-925 border rounded-lg shadow-lg",
"bg-stone-50 dark:bg-stone-925 border border-stone-100 dark:border-stone-900 dark:outline dark:outline-1 dark:outline-black/60 rounded-lg shadow-lg",
menuOpen || searchOpen ? "left-4" : "",
)}
>
@@ -118,7 +118,7 @@ export function Nav({
" px-2 pb-2",
)}
>
<div className="flex items-center w-full border-b">
<div className="flex items-center w-full border-b border-stone-100 dark:border-stone-900">
<NavLinkLogo
prominent
href="/"
@@ -141,7 +141,7 @@ export function Nav({
</div>
{pathname.startsWith("/docs") && docNav && (
<div className="max-h-[calc(100dvh-15rem)] p-4 border-b overflow-x-auto">
<div className="max-h-[calc(100dvh-15rem)] p-4 border-b border-stone-100 dark:border-stone-900 overflow-x-auto">
{docNav}
</div>
)}
@@ -162,7 +162,7 @@ export function Nav({
))}
</div>
<div className="flex gap-4 justify-end border-b">
<div className="flex gap-4 justify-end border-b border-stone-100 dark:border-stone-900">
{items
.filter((item) => !("icon" in item))
.slice(3)
@@ -179,12 +179,12 @@ export function Nav({
))}
</div>
</div>
<div className="flex items-center self-stretch justify-between">
<div className="flex items-center self-stretch justify-end">
{/* <input
type="text"
className={clsx(
menuOpen || searchOpen ? "" : "hidden",
"ml-2 border px-2 py-1 rounded w-full"
"ml-2 border border-stone-200 dark:border-stone-900 px-2 py-1 rounded w-full"
)}
placeholder="Search docs..."
ref={searchRef}
@@ -202,16 +202,12 @@ export function Nav({
>
<SearchIcon className="" />
</button> */}
{(menuOpen || searchOpen) && (
<ThemeToggle className="p-3" />
)}
<button
className="flex gap-2 p-3 rounded-xl items-center"
onMouseDown={() => {
setMenuOpen((o) => !o);
setSearchOpen(false);
}}
aria-label="Close menu"
>
{menuOpen || searchOpen ? (
<XIcon />

View File

@@ -1,3 +1,45 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* .manrope-bold {
font-family: "Manrope", sans-serif;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
} */
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
font-family: "Manrope", sans-serif;
font-optical-sizing: auto;
font-weight: 300;
font-style: normal;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

View File

@@ -51,7 +51,6 @@ export default function RootLayout({
commitMono.variable,
inter.className,
"h-full",
"bg-white text-stone-700 dark:text-stone-400 dark:bg-stone-950",
].join(" ")}
>
{children}

View File

@@ -1,72 +1,9 @@
import { Prose } from "@components/molecules/Prose";
export default function Home() {
return (
<main className="container flex flex-col gap-8 py-8 lg:py-16">
<main className="container h-full flex flex-col gap-8 py-8 lg:py-16">
<h1 className="text-2xl font-semibold font-display">
Jazz Design System
</h1>
<h2>Typography (Prose)</h2>
<div className="grid gap-4">
<div>
Heading 1
<Prose className="p-3 border">
<h1>Ship top-tier apps at high tempo</h1>
</Prose>
</div>
<div>
Heading 2
<Prose className="p-3 border">
<h2>Ship top-tier apps at high tempo</h2>
</Prose>
</div>
<div>
Heading 3
<Prose className="p-3 border">
<h3>Ship top-tier apps at high tempo</h3>
</Prose>
</div>
<div>
Heading 4
<Prose className="p-3 border">
<h4>Ship top-tier apps at high tempo</h4>
</Prose>
</div>
<div>
Paragraph
<Prose className="p-3 border">
<p>
<strong>
Jazz is a framework for building local-first
apps
</strong>{" "}
an architecture that lets companies like Figma and
Linear play in a league of their own.
</p>
<p>
Open source. Self-host or use Jazz Cloud for
zero-config magic.
</p>
</Prose>
</div>
<div>
Link
<Prose className="p-3 border">
This is a <a href="https://jazz.tools">link</a>
</Prose>
</div>
<div>
Code
<Prose className="p-3 border">
This is a one-line <code>piece of code</code>
</Prose>
</div>
</div>
</main>
);
}

View File

@@ -4,28 +4,6 @@ import tailwindCSSAnimate from "tailwindcss-animate";
const colors = require("tailwindcss/colors")
const plugin = require("tailwindcss/plugin")
const stonePalette = {
"50": "oklch(0.988281 0.002 75)",
"75": "oklch(0.980563 0.002 75)",
"100": "oklch(0.964844 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 => {
stonePaletteWithAlpha[key] = stonePaletteWithAlpha[key].replace(")", "/ <alpha-value>)")
})
/** @type {import('tailwindcss').Config} */
const config = {
content: [
@@ -36,7 +14,21 @@ const config = {
theme: {
colors: {
...harmonyPalette,
stone: stonePaletteWithAlpha,
stone: {
"50": "oklch(0.988281 0.002 75 / <alpha-value>)",
"75": "oklch(0.980563 0.002 75 / <alpha-value>)",
"100": "oklch(0.964844 0.002 75 / <alpha-value>)",
"200": "oklch(0.917969 0.002 75 / <alpha-value>)",
"300": "oklch(0.853516 0.002 75 / <alpha-value>)",
"400": "oklch(0.789063 0.002 75 / <alpha-value>)",
"500": "oklch(0.726563 0.002 75 / <alpha-value>)",
"600": "oklch(0.613281 0.002 75 / <alpha-value>)",
"700": "oklch(0.523438 0.002 75 / <alpha-value>)",
"800": "oklch(0.412109 0.002 75 / <alpha-value>)",
"900": "oklch(0.302734 0.002 75 / <alpha-value>)",
"925": "oklch(0.220000 0.002 75 / <alpha-value>)",
"950": "oklch(0.193359 0.002 75 / <alpha-value>)",
},
blue: {
...colors.indigo,
"500": "#5870F1",
@@ -46,15 +38,15 @@ const config = {
"900": "#12046A",
DEFAULT: "#3313F7",
},
green: colors.green,
red: colors.red,
},
extend: {
fontFamily: {
display: ["var(--font-manrope)"],
mono: ["var(--font-commit-mono)"],
handwritten: ["var(--font-sriracha)"],
},
fontSize: {
'2xs': ['0.75rem', { lineHeight: '1.25rem' }],
},
// shadcn-ui
@@ -93,6 +85,11 @@ const config = {
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
@@ -122,107 +119,12 @@ const config = {
md: "960px",
lg: "1276px",
},
typography: (theme) => ({
DEFAULT: {
css: {
"--tw-prose-body": stonePalette[700],
"--tw-prose-headings": stonePalette[900],
"--tw-prose-bold": stonePalette[900],
"--tw-prose-invert-bold": theme("colors.white"),
"--tw-prose-invert-body": stonePalette[400],
"--tw-prose-invert-headings": theme("colors.white"),
"--tw-prose-code": stonePalette[900],
"--tw-prose-invert-code": stonePalette[50],
"--tw-prose-links": theme("colors.blue.DEFAULT"),
"--tw-prose-invert-links": theme("colors.blue.500"),
maxWidth: theme("screens.4xl"),
strong: {
color: "var(--tw-prose-bold)",
fontWeight: theme("fontWeight.medium"),
},
b: {
color: "var(--tw-prose-bold)",
fontWeight: theme("fontWeight.medium"),
},
a: {
fontWeight: theme("fontWeight.normal"),
textUnderlineOffset: "4px",
},
h1: {
fontFamily: theme("fontFamily.display"),
letterSpacing: theme("letterSpacing.tight"),
fontWeight: theme("fontWeight.semibold"),
fontSize: theme("fontSize.4xl"),
},
h2: {
fontFamily: theme("fontFamily.display"),
letterSpacing: theme("letterSpacing.tight"),
fontWeight: theme("fontWeight.semibold"),
fontSize: theme("fontSize.3xl"),
},
h3: {
fontFamily: theme("fontFamily.display"),
letterSpacing: theme("letterSpacing.tight"),
fontWeight: theme("fontWeight.semibold"),
fontSize: theme("fontSize.2xl"),
},
h4: {
textTransform: "uppercase",
letterSpacing: theme("letterSpacing.widest"),
fontWeight: theme("fontWeight.medium"),
fontSize: theme("fontSize.sm"),
},
'code::before': {
content: 'none',
},
'code::after': {
content: 'none',
},
code: {
backgroundColor: stonePalette[100],
padding: "0.15rem 0.25rem",
borderRadius: "2px",
whiteSpace: "nowrap",
},
p: {
marginBottom: theme("spacing.3"),
marginTop: theme("spacing.3"),
}
}
},
xl: {
css: {
p: {
marginBottom: theme("spacing.3"),
marginTop: theme("spacing.3"),
}
},
}
}),
},
},
plugins: [
tailwindCSSAnimate,
typography(),
plugin(({ addVariant }) => addVariant("label", "& :is(label)")),
plugin(({ addUtilities }) => addUtilities({
".text-reset, .text-reset:hover, .text-reset:focus": {
color: "inherit",
textDecoration: "none",
},
})),
plugin(({ addBase }) => addBase({
":root": {
"--gcmp-border-color": stonePalette[200],
"--gcmp-invert-border-color": stonePalette[900],
},
"*": {
borderColor: "var(--gcmp-border-color)",
},
".dark *": {
borderColor: "var(--gcmp-invert-border-color)",
},
}))
],
};
export default config;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -65,6 +65,10 @@
--ring: 24 5.7% 82.9%;
}
* {
@apply border-border;
}
.overlay-close {
@apply bg-black;
}

View File

@@ -1,14 +1,10 @@
import "./globals.css";
import type { Metadata } from "next";
import { ThemeProvider } from "gcmp-design-system/src/app/components/molecules/ThemeProvider";
import { ThemeProvider } from "@/components/themeProvider";
import { Inter, Manrope } from "next/font/google";
import localFont from "next/font/local";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Analytics } from "@vercel/analytics/react";
import { GcmpNav } from "@/components/Nav";
import { ThemeToggle } from "gcmp-design-system/src/app/components/molecules/ThemeToggle";
// If loading a variable font, you don't need to specify the font weight
const manrope = Manrope({
@@ -23,23 +19,6 @@ const inter = Inter({
display: "swap",
});
const commitMono = localFont({
src: [
{
path: "../../design-system/fonts/CommitMono-Regular.woff2",
weight: "400",
style: "normal",
},
{
path: "../../design-system/fonts/CommitMono-Regular.woff",
weight: "400",
style: "normal",
},
],
variable: "--font-commit-mono",
display: "swap",
});
const metaTags = {
title: "garden computing",
description:
@@ -77,13 +56,12 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en" className="h-full">
<html lang="en">
<body
className={[
manrope.variable,
commitMono.variable,
inter.className,
"h-full flex flex-col items-center",
"flex min-h-screen flex-col items-center",
"bg-white text-stone-700 dark:text-stone-400 dark:bg-stone-950",
].join(" ")}
>
@@ -99,10 +77,8 @@ export default function RootLayout({
<main className="flex flex-1 flex-col w-full">
{children}
</main>
<footer className="py-8 md:py-16 text-sm flex justify-between gap-3 w-full container">
<footer className="py-8 md:py-16 text-sm">
<p>©2024 Garden Computing, Inc.</p>
<ThemeToggle className="hidden md:block"/>
</footer>
</ThemeProvider>
</body>

View File

@@ -1,90 +0,0 @@
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getPostBySlug, posts } from "@/lib/posts";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import { H1 } from "gcmp-design-system/src/app/components/atoms/Headings";
import PostCoverImage from "@/components/blog/PostCoverImage";
import Image from "next/image";
import { FormattedDate } from "@/components/FormattedDate";
import { PostJsonLd } from "@/components/blog/PostJsonLd";
export default async function Post({ params }: Params) {
const post = getPostBySlug(params.slug);
if (!post) {
return notFound();
}
const { title, coverImage, date, author, excerpt } = post.meta;
const content = post.default({});
return (
<>
<PostJsonLd
title={title}
image={coverImage}
author={author.name}
datePublished={date}
description={excerpt}
/>
<article className="container max-w-3xl flex flex-col gap-8 py-8 lg:py-16 lg:gap-12">
<div className="flex flex-col gap-2">
<H1>{title}</H1>
<div className="flex items-center gap-3">
<Image
width={100}
height={100}
src={author.image}
className="size-12 rounded-full"
alt=""
/>
<div>
<p className="text-stone-900 dark:text-white">
{author.name}
</p>
<p className="text-sm text-stone-600 dark:text-stone-400">
<FormattedDate date={date} />
</p>
</div>
</div>
</div>
<PostCoverImage src={coverImage} title={title} />
<Prose>{content}</Prose>
</article>
</>
);
}
type Params = {
params: {
slug: string;
};
};
export function generateMetadata({ params }: Params): Metadata {
const post = getPostBySlug(params.slug);
if (!post) {
return notFound();
}
const { title, excerpt, coverImage } = post.meta;
return {
title,
description: excerpt,
openGraph: {
title,
images: [coverImage],
},
};
}
export async function generateStaticParams() {
return posts.map((post) => ({
slug: post.meta.slug,
}));
}

View File

@@ -2,7 +2,6 @@ import Link from "next/link";
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import { NewsletterForm } from "@/components/NewsletterForm";
import { Posts } from "@/components/blog/Posts";
export const metadata = {
title: "Blog",
@@ -34,8 +33,6 @@ export default function NewsPage() {
!
</p>
</Prose>
<Posts />
</div>
);
}

View File

@@ -53,7 +53,7 @@ pre.twoslash data-lsp::before {
position: absolute;
transform: translate(0, 1.2rem);
max-width: 30rem;
@apply text-xs px-1.5 py-1 rounded border shadow-lg overflow-hidden whitespace-pre-wrap text-stone-700 bg-stone-50 dark:text-stone-200 dark:bg-stone-950;
@apply text-xs px-1.5 py-1 rounded border border-stone-200 dark:border-stone-900 shadow-lg overflow-hidden whitespace-pre-wrap text-stone-700 bg-stone-50 dark:text-stone-200 dark:bg-stone-950;
text-align: left;
z-index: 100;
opacity: 0;
@@ -79,7 +79,7 @@ pre.twoslash data-lsp:hover::before {
}
pre .code-container {
@apply overflow-auto p-2 pl-0 bg-white dark:bg-stone-925 rounded-b-xl text-xs h-full;
@apply overflow-auto p-2 pl-0 bg-white dark:bg-stone-925 rounded-b-xl text-xs h-full dark:border-stone-900;
}
/* The try button */
pre .code-container > a {

View File

@@ -1,187 +1,42 @@
import { GlobeIcon, LucideIcon } from "lucide-react";
import { H1, H2 } from "gcmp-design-system/src/app/components/atoms/Headings";
import { MailIcon } from "lucide-react";
import Link from "next/link";
import {
SiBluesky,
SiGithub,
SiGitlab,
SiLinkedin,
SiX,
} from "@icons-pack/react-simple-icons";
import { SiGithub, SiTwitter } from "@icons-pack/react-simple-icons";
import { SectionHeader } from "gcmp-design-system/src/app/components/molecules/SectionHeader";
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
interface TeamMember {
name: string;
titles: string[];
image?: string;
location: string;
x?: string;
github?: string;
gitlab?: string;
website?: string;
linkedin?: string;
bluesky?: string;
}
const team: Array<TeamMember> = [
{
name: "Anselm Eickhoff",
titles: ["Founder"],
image: "anselm.jpg",
location: "Canterbury, UK ",
x: "anselm_io",
github: "aeplay",
website: "http://anselm.io",
bluesky: "anselm-io",
},
{
name: "Andrei Popa",
titles: ["Full-Stack Dev", "Infra"],
image: "andrei.jpeg",
location: "Bucharest, Romania ",
x: "elitepax",
github: "pax-k",
},
{
name: "Guido D'Orsi",
titles: ["Frontend Dev", "React Performance"],
image: "guido.jpeg",
location: "Piano di Sorrento, Italy ",
github: "gdorsi",
},
{
name: "Trisha Lim",
titles: ["Frontend Dev", "Design", "Marketing"],
image: "trisha.png",
location: "Lisbon, Portugal ",
github: "trishalim",
website: "https://trishalim.com",
},
{
name: "Benjamin Leveritt",
titles: ["Full-Stack Dev"],
image: "benjamin.jpg",
location: "Portsmouth, UK ",
github: "bensleveritt",
},
{
name: "Marina Orlova",
titles: ["Full-Stack Dev"],
location: "Tarragona, Spain ",
gitlab: "marinaorlova",
linkedin: "marina-orlova-52a34394",
// github: "marinoska",
image: "marina.jpeg",
},
];
function SocialLink({
link,
label,
icon: Icon,
}: {
label: string;
link: string;
icon: LucideIcon;
}) {
return (
<Link href={link} className="p-1 -m-1">
<Icon size={16} />
<span className="sr-only">{label}</span>
</Link>
);
}
function Person({ person }: { person: TeamMember }) {
const imageClassName = "size-24 shadow rounded-md bg-stone-100 sm:size-28 ";
return (
<div className="flex items-center gap-5">
{person.image ? (
<img src={`/team/${person.image}`} className={imageClassName} />
) : (
<span className={imageClassName}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-full pt-5 h-full text-stone-300 dark:text-stone-700"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M4 22a8 8 0 1 1 16 0zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6"
/>
</svg>
</span>
)}
<div className="flex flex-col gap-2.5">
<h3 className="text-lg leading-none font-semibold tracking-tight text-stone-900 dark:text-white">
{person.name}
</h3>
<p className="text-sm leading-none text-gray-600 dark:text-stone-400">
{person.titles.join(", ")}
</p>
<p className="text-sm leading-none text-gray-600 dark:text-stone-400">
{person.location}
</p>
<div className="flex gap-2 mt-0.5">
{person.website && (
<SocialLink
link={person.website}
icon={GlobeIcon}
label="Website"
/>
)}
{person.x && (
<SocialLink
link={`https://x.com/${person.x}`}
icon={SiX}
label="X profile"
/>
)}
{person.bluesky && (
<SocialLink
link={`https://bsky.app/profile/${person.bluesky}.bsky.social`}
icon={SiBluesky}
label="Bluesky profile"
/>
)}
{person.gitlab && (
<SocialLink
link={`https://gitlab.com/${person.gitlab}`}
icon={SiGitlab}
label="Gitlab profile"
/>
)}
{person.linkedin && (
<SocialLink
link={`https://www.linkedin.com/in/${person.linkedin}`}
icon={SiLinkedin}
label="Linkedin profile"
/>
)}
{person.github && (
<SocialLink
link={`https://github.com/${person.github}`}
label="Github profile"
icon={SiGithub}
/>
)}
</div>
</div>
</div>
);
}
export const metadata = {
title: "Team",
};
export default function TeamPage() {
return (
<div className="container">
<HeroHeader title="Meet the team" slogan="" />
<HeroHeader title="Team" slogan="" />
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 lg:gap-10">
{team.map((person) => (
<Person key={person.name} person={person} />
))}
<div className="grid gap-1">
<H2>Anselm Eickhoff (Founder)</H2>
<Link
href="mailto:anselm@gcmp.io"
className="flex gap-1 items-center"
aria-label="Email address"
>
<MailIcon height="1em" /> anselm@gcmp.io
</Link>
<Link
href="https://x.com/anselm_io"
className="flex gap-1 items-center"
aria-label="Twitter link"
>
<SiTwitter height="1em" /> anselm_io
</Link>
<Link
href="https://github.com/aeplay"
className="flex gap-1 items-center"
aria-label="GitHub link"
>
<SiGithub height="1em" /> aeplay
</Link>
</div>
</div>
);

View File

@@ -1,7 +0,0 @@
import { formatDate } from "@/lib/date";
export function FormattedDate({ date }: { date: string }) {
const formattedDate = formatDate(new Date(date));
return <>{formattedDate}</>;
}

View File

@@ -1,44 +0,0 @@
import Link from "next/link";
import Image from "next/image";
import { clsx } from "clsx";
const PostCoverImage = ({
src,
title,
slug,
alt = "",
className,
}: {
src: string;
title: string;
slug?: string;
alt?: string;
className?: string;
}) => {
const image = (
<Image
alt={alt}
src={src}
className={clsx(className, "w-full")}
width={1300}
height={630}
/>
);
return (
<div className={className}>
{slug ? (
<Link
className={className}
href={`/news/${slug}`}
aria-label={title}
>
{image}
</Link>
) : (
image
)}
</div>
);
};
export default PostCoverImage;

View File

@@ -1,37 +0,0 @@
export function PostJsonLd({
title,
image,
author,
datePublished,
description,
}: {
title: string;
image: string;
author: string;
datePublished: string;
description: string;
}) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: title,
image,
author: {
"@type": "Person",
name: author,
},
publisher: {
"@type": "Organization",
name: "Garden Computing",
},
datePublished,
description,
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}

View File

@@ -1,34 +0,0 @@
import { posts } from "@/lib/posts";
import { FormattedDate } from "@/components/FormattedDate";
import PostCoverImage from "@/components/blog/PostCoverImage";
import Link from "next/link";
export function Posts() {
return (
<div className="grid grid-cols-3 gap-8">
{posts.map((post) => (
<div className="flex flex-col gap-2" key={post.meta.slug}>
<PostCoverImage
src={post.meta.coverImage}
title={post.meta.title}
slug={post.meta.slug}
className="mb-1.5 rounded-lg"
/>
<Link
href={`/news/${post.meta.slug}`}
className="text-lg font-medium text-display text-stone-900 dark:text-white"
>
{post.meta.title}
</Link>
<p className="line-clamp-3 leading-relaxed text-ellipsis text-sm text-stone-900 dark:text-stone-400">
{post.meta.excerpt}
</p>
<div className="flex text-sm items-center">
{post.meta.author.name} {" "}
<FormattedDate date={post.meta.date} />
</div>
</div>
))}
</div>
);
}

View File

@@ -1,22 +0,0 @@
export const meta = {
slug: "test",
title: "Test",
date: "2023-01-01",
coverImage: "/social-image.png",
author: {
name: "Anselm Eickhoff",
picture: "/social-image.png",
},
excerpt:
"Jazz is a framework for local-first data/permissions. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut gravida vel urna sit amet lacinia. Morbi euismod mi ac lacus feugiat, vel sollicitudin urna faucibus. ",
};
Sed viverra in justo sit amet imperdiet. Proin posuere euismod ante, eu bibendum arcu lobortis non. Ut vitae placerat tellus. Phasellus quis tortor sollicitudin nisi scelerisque bibendum nec ac massa. Donec hendrerit felis non est elementum, at rutrum lorem sodales. Nam ligula urna, varius eu sem non, mollis dictum leo. Duis facilisis id sapien et feugiat.
Proin ultrices sit amet magna et euismod. Duis sed porta ipsum, eu iaculis eros. Proin at purus sollicitudin, consequat velit vel, fermentum turpis. Integer ut dapibus augue. Ut ultrices viverra justo a lobortis. In mattis leo vel condimentum facilisis. Nullam eu nulla et dolor cursus laoreet. Aenean vehicula sollicitudin nibh, placerat semper lorem finibus eu. Curabitur commodo ligula eu metus porta, id facilisis purus mollis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
Maecenas turpis elit, vulputate at bibendum ut, scelerisque eu arcu. Ut porta dolor elit, eget venenatis arcu pharetra at. Duis facilisis faucibus luctus. Morbi at mi turpis. Aliquam euismod semper purus sed dignissim. Aenean ultricies sollicitudin nisl, in dapibus libero malesuada nec. Quisque pulvinar enim risus, et egestas elit congue quis. Nulla gravida nibh vitae neque malesuada molestie.
## Heading 2
Sed viverra in justo sit amet imperdiet. Proin posuere euismod ante, eu bibendum arcu lobortis non. Ut vitae placerat tellus. Phasellus quis tortor sollicitudin nisi scelerisque bibendum nec ac massa. Donec hendrerit felis non est elementum, at rutrum lorem sodales. Nam ligula urna, varius eu sem non, mollis dictum leo. Duis facilisis id sapien et feugiat.

View File

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

View File

@@ -1,8 +0,0 @@
export function formatDate(date: Date) {
const day = date.getDate().toString();
date.getMonth();
const year = date.getFullYear();
const month = date.toLocaleString("en", { month: "short" });
return `${month} ${day}, ${year}`;
}

View File

@@ -1,7 +0,0 @@
import * as TestPost from "@/components/blog/posts/test.mdx";
export const posts: (typeof TestPost)[] = [];
export const getPostBySlug = (slug: string) => {
return posts.find((post) => post.meta.slug === slug);
};

View File

@@ -1,13 +0,0 @@
declare module "*.mdx" {
export const meta: {
slug: string;
title: string;
date: string;
coverImage: string;
author: {
name: string;
image: string;
};
excerpt: string;
};
}

View File

@@ -30,6 +30,7 @@
"mdast-util-mdx": "^3.0.0",
"micromark-extension-mdxjs": "^3.0.0",
"next": "14.2.15",
"next-themes": "^0.2.1",
"react": "^18",
"react-dom": "^18",
"resend": "^4.0.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

View File

@@ -22,12 +22,6 @@
"@/*": ["./*"]
}
},
"include": [
"mdx.d.ts",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,6 @@
Based on CRDTs and public-key cryptography, CoValues...
- Can be read & edited like simple local JSON state
- Can be created anywhere, are automatically synced & persisted
- Always keep full edit history & author metadata
- Automatically resolve most conflicts

View File

@@ -1,9 +1,11 @@
import { CodeExampleTabs, ResponsiveIframe } from "@/components/forMdx";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import {
Prose,
SmallProse,
} from "gcmp-design-system/src/app/components/molecules/Prose";
import { SectionHeader } from "gcmp-design-system/src/app/components/molecules/SectionHeader";
import { GappedGrid } from "gcmp-design-system/src/app/components/molecules/GappedGrid";
import { LabelledFeatureIcon } from "gcmp-design-system/src/app/components/molecules/LabelledFeatureIcon";
import { Testimonial } from "gcmp-design-system/src/app/components/molecules/Testimonial";
import { GridCard } from "gcmp-design-system/src/app/components/atoms/GridCard";
import { CodeRef } from "gcmp-design-system/src/app/components/atoms/CodeRef";
import { ComingSoonBadge } from "gcmp-design-system/src/app/components/atoms/ComingSoonBadge";
@@ -18,7 +20,6 @@ import {
HardDriveDownloadIcon,
KeyRoundIcon,
MousePointerSquareDashedIcon,
UserIcon,
} from "lucide-react";
import {
@@ -29,6 +30,7 @@ import {
Ui_tsx,
} from "@/codeSamples/examples/chat/src";
import CoValuesIntro from "./coValuesIntro.mdx";
import CoMapDescription from "./coValueDescriptions/coMapDescription.mdx";
import CoListDescription from "./coValueDescriptions/coListDescription.mdx";
import CoPlainTextDescription from "./coValueDescriptions/coPlainTextDescription.mdx";
@@ -39,37 +41,67 @@ import GroupDescription from "./coValueDescriptions/groupDescription.mdx";
import AccountDescription from "./coValueDescriptions/accountDescription.mdx";
import AutoSubDescription from "./toolkit/autoSub.mdx";
import CursorsAndCaretsDescription from "./toolkit/cursorsAndCarets.mdx";
import AuthProvidersDescription from "./toolkit/authProviders.mdx";
import TwoWaySyncDescription from "./toolkit/twoWaySync.mdx";
import FileUploadDownloadDescription from "./toolkit/fileUploadDownload.mdx";
import VideoPresenceCallsDescription from "./toolkit/videoPresenceCalls.mdx";
import CloudIntro from "./cloudIntro.mdx";
import { H2, H3 } from "gcmp-design-system/src/app/components/atoms/Headings";
import { H3 } from "gcmp-design-system/src/app/components/atoms/Headings";
import Link from "next/link";
import { DiagramBeforeJazz } from "@/components/DiagramBeforeJazz";
import { DiagramAfterJazz } from "@/components/DiagramAfterJazz";
import { SupportedEnvironments } from "@/components/home/SupportedEnvironments";
import { HairlineBleedGrid } from "gcmp-design-system/src/app/components/molecules/HairlineGrid";
import { Hero } from "@/components/home/Hero";
import { HowItWorks } from "@/components/home/HowItWorks";
import BeforeAfterJazz from "@/components/home/BeforeAfterJazz";
import { P } from "gcmp-design-system/src/app/components/atoms/Paragraph";
const ArrowDoodle = ({ className }: { className?: string }) => (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
width="107"
height="85"
viewBox="0 0 107 85"
fill="none"
>
<path
d="M3.87338 33.7809C32.7867 45.4747 65.5543 47.9975 91.7667 37.4141"
stroke="currentColor"
strokeWidth="6"
strokeLinecap="round"
/>
<path
d="M74.1719 24.958C83.1201 33.0289 92.3253 37.1887 98.5899 34.6593"
stroke="currentColor"
strokeWidth="6"
strokeLinecap="round"
/>
<path
d="M87.2448 58.8003C88.3842 46.6564 92.3253 37.1887 98.5899 34.6593"
stroke="currentColor"
strokeWidth="6"
strokeLinecap="round"
/>
</svg>
);
export default function Home() {
const localFirst = {
title: "Local-first",
icon: HardDriveDownloadIcon,
description: (
<>
<p>
All data you load or create is persisted locally, so your
users can work offline.
</p>
<p>
When youre back online, your local changes are synced to
the server.
</p>
</>
),
};
const features = [
{
title: "Local-first",
icon: HardDriveDownloadIcon,
description: (
<>
<p>
All data you load or create is persisted locally, so
your users can work offline.
</p>
<p>
When youre back online, your local changes are synced
to the server.
</p>
</>
),
},
{
title: "Multiplayer",
icon: MousePointerSquareDashedIcon,
@@ -138,7 +170,7 @@ export default function Home() {
),
},
{
title: "Real-time sync",
title: "Sync",
icon: MonitorSmartphoneIcon,
description: (
<>
@@ -154,7 +186,7 @@ export default function Home() {
),
},
{
title: "E2E encryption",
title: "E2EE",
icon: KeyRoundIcon,
description: (
<>
@@ -185,64 +217,113 @@ export default function Home() {
</>
),
},
{
title: "Authentication",
icon: UserIcon,
description: (
<>
<p>Plug and play different kinds of auth.</p>
<ul className="pl-4 list-disc">
<li>DemoAuth (for quick multi-user demos)</li>
<li>WebAuthN (TouchID/FaceID)</li>
<li>Clerk</li>
<li>
Auth0, Okta, NextAuth <ComingSoonBadge />
</li>
</ul>
</>
),
},
];
return (
<>
<Hero features={features} />
<BeforeAfterJazz />
<div className="bg-stone-100 border-y dark:bg-stone-925 py-8 lg:py-16 dark:border-stone-900">
<div className="container grid gap-8 lg:gap-12">
<h2 className="font-display md:text-center text-stone-950 dark:text-white text-2xl md:text-3xl font-semibold tracking-tight">
Hard things are easy now.
</h2>
<div className="grid gap-8 md:grid-cols-11 lg:gap-5">
<div className="md:col-span-5 flex flex-col justify-between">
<div className="text-pretty leading-relaxed text-lg max-w-2xl space-y-2 md:text-xl md:space-y-4 md:leading-relaxed">
<p>
Ever notice how every stack just{" "}
<span className="font-semibold text-stone-900 dark:text-white">
reinvents shared state between users and
machines
</span>
?
</p>
<p>
And far from the simple client-server model,
you routinely tackle{" "}
<span className="font-semibold text-stone-900 dark:text-white">
a mess of moving parts
</span>
, tech choices and deployment questions.
</p>
<p>
And{" "}
<span className="font-semibold text-stone-900 dark:text-white">
your apps code is all over the place.
</span>
</p>
</div>
<div className="w-full mt-8 lg:mt-12">
<div className="p-4 sm:p-8 rounded-xl bg-white shadow-sm dark:bg-stone-900">
<DiagramBeforeJazz className="w-full h-auto max-w-[30rem] mx-auto dark:text-white"></DiagramBeforeJazz>
</div>
</div>
</div>
<div className="container flex flex-col gap-12 py-12 lg:gap-20 lg:py-20">
<HowItWorks />
<div className="hidden md:block relative pr-3 top-24">
<ArrowDoodle className="w-full h-auto text-stone-300 dark:text-stone-800" />
</div>
<Testimonial name="Serious Adopter #4" role="Technical Founder">
<p>
You don&apos;t have to think about deploying a database,
SQL schemas, relations, and writing queries Basically,{" "}
<span className="bg-blue-50 px-1 dark:bg-transparent">
if you know TypeScript, you know Jazz
</span>
, and you can ship an app. It&apos;s just so nice!
</p>
</Testimonial>
<div className="md:col-span-5 flex flex-col justify-between">
<div className="text-pretty leading-relaxed text-lg max-w-2xl space-y-2 md:text-xl md:space-y-4 md:leading-relaxed">
<p>
Jazz provides a single new abstraction to do
the whole job.
</p>
<p>
It turns the data flow around and gives you{" "}
<span className="font-semibold text-stone-900 dark:text-white">
mutable local state
</span>
, solving{" "}
<span className="font-semibold text-stone-900 dark:text-white">
sync
</span>
,{" "}
<span className="font-semibold text-stone-900 dark:text-white">
concurrent editing
</span>{" "}
and{" "}
<span className="font-semibold text-stone-900 dark:text-white">
permissions
</span>{" "}
under the hood.
</p>
<p>
<span className="font-semibold text-stone-900 dark:text-white">
All thats left?
</span>{" "}
What makes your app your app.
</p>
</div>
<div className="w-full mt-8 lg:mt-12">
<div className="p-4 sm:p-8 rounded-xl bg-white shadow-sm dark:bg-stone-900">
<DiagramAfterJazz className="w-full h-auto max-w-[30rem] mx-auto dark:text-white"></DiagramAfterJazz>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="container flex flex-col gap-8 py-8 lg:gap-20 lg:py-20">
<div className="flex flex-col gap-4 md:gap-6">
<SectionHeader
title="Everything you need to ship top-tier apps quickly."
slogan="Features that used to take months to build now work out-of-the-box."
/>
<GappedGrid>
{[localFirst, ...features].map(
({ title, icon: Icon, description }) => (
<LabelledFeatureIcon
className="col-span-2"
key={title}
label={title}
icon={Icon}
explanation={description}
/>
),
)}
</GappedGrid>
<HairlineBleedGrid>
{features.map(({ title, icon: Icon, description }) => (
<LabelledFeatureIcon
key={title}
label={title}
icon={Icon}
explanation={description}
/>
))}
</HairlineBleedGrid>
</div>
<div>
@@ -251,7 +332,7 @@ export default function Home() {
slogan="A chat app in 174 lines of code."
/>
<div className="flex flex-col md:grid md:grid-cols-2 md:divide-x border rounded-sm overflow-hidden shadow-sm dark:divide-stone-900">
<div className="flex flex-col md:grid md:grid-cols-2 md:divide-x border rounded-sm overflow-hidden shadow-sm dark:border-stone-900 dark:divide-stone-900">
<CodeExampleTabs
tabs={[
{
@@ -277,14 +358,14 @@ export default function Home() {
]}
/>
<div className="border-b order-first md:order-last flex flex-col md:border-b-0">
<div className="flex border-b overflow-x-auto overflow-y-hidden bg-white dark:bg-stone-900">
<div className="flex border-b overflow-x-auto overflow-y-hidden bg-white dark:border-stone-900 dark:bg-stone-900">
<p className="items-center -mb-px transition-colors px-3 pb-1.5 pt-2 block text-xs border-b-2 border-blue-700 text-stone-700 dark:bg-stone-925 dark:text-blue-500 dark:border-blue-500">
result
</p>
</div>
<ResponsiveIframe
src="https://chat.jazz.tools"
localsrc="http://localhost:5173"
localSrc="http://localhost:5173"
/>
</div>
</div>
@@ -307,6 +388,17 @@ export default function Home() {
</div>
</div>
<div>
<SectionHeader
title="Collaborative Values"
slogan="Your new building blocks."
/>
<Prose>
<CoValuesIntro />
</Prose>
</div>
<GappedGrid
title="Bread-and-butter datastructures"
className="grid-cols-2 lg:grid-cols-4"
@@ -315,18 +407,18 @@ export default function Home() {
<H3>
<CodeRef>CoMap</CodeRef>
</H3>
<Prose size="sm">
<SmallProse>
<CoMapDescription />
</Prose>
</SmallProse>
</GridCard>
<GridCard>
<H3>
<CodeRef>CoList</CodeRef>
</H3>
<Prose size="sm">
<SmallProse>
<CoListDescription />
</Prose>
</SmallProse>
</GridCard>
<GridCard>
@@ -334,18 +426,18 @@ export default function Home() {
<CodeRef>CoPlainText</CodeRef> &{" "}
<CodeRef>CoRichText</CodeRef> <ComingSoonBadge />
</H3>
<Prose size="sm">
<SmallProse>
<CoPlainTextDescription />
</Prose>
</SmallProse>
</GridCard>
<GridCard>
<H3>
<CodeRef>CoStream</CodeRef>
</H3>
<Prose size="sm">
<SmallProse>
<CoStreamDescription />
</Prose>
</SmallProse>
</GridCard>
</GappedGrid>
@@ -357,18 +449,18 @@ export default function Home() {
<H3>
<CodeRef>BinaryCoStream</CodeRef>
</H3>
<Prose size="sm">
<SmallProse>
<BinaryCoStreamDescription />
</Prose>
</SmallProse>
</GridCard>
<GridCard>
<H3>
<CodeRef>ImageDefinition</CodeRef>
</H3>
<Prose size="sm">
<SmallProse>
<ImageDefinitionDescription />
</Prose>
</SmallProse>
</GridCard>
</GappedGrid>
@@ -380,82 +472,98 @@ export default function Home() {
<H3>
<CodeRef>Group</CodeRef>
</H3>
<Prose size="sm">
<SmallProse>
<GroupDescription />
</Prose>
</SmallProse>
</GridCard>
<GridCard>
<H3>
<CodeRef>Account</CodeRef>
</H3>
<Prose size="sm">
<SmallProse>
<AccountDescription />
</Prose>
</SmallProse>
</GridCard>
</GappedGrid>
<SupportedEnvironments />
<h2 className="sr-only">More features</h2>
<GappedGrid>
<GridCard>
<H3>Auto-sub</H3>
<P className="text-lg">
Let your UI drive data-syncing.
</P>
<Prose size="sm">
<SectionHeader
title="Auto-sub"
slogan="Let your UI drive data-syncing."
/>
<SmallProse>
<AutoSubDescription />
</Prose>
</SmallProse>
</GridCard>
<GridCard>
<H3>Cursors & carets</H3>
<P className="text-lg">Ready-made spatial presence.</P>
<Prose size="sm">
<SectionHeader
title="Cursors & carets"
slogan="Ready-made spatial presence."
/>
<SmallProse>
<CursorsAndCaretsDescription />
</Prose>
</SmallProse>
</GridCard>
<GridCard>
<H3>Two-way sync to your DB</H3>
<P className="text-lg">Add Jazz to an existing app.</P>
<Prose size="sm">
<SectionHeader
title="Auth Providers"
slogan="Plug and play different kinds of auth."
/>
<SmallProse>
<AuthProvidersDescription />
</SmallProse>
</GridCard>
<GridCard>
<SectionHeader
title="Two-way sync to your DB"
slogan="Add Jazz to an existing app."
/>
<SmallProse>
<TwoWaySyncDescription />
</Prose>
</SmallProse>
</GridCard>
<GridCard>
<H3>File upload & download</H3>
<P className="text-lg">
Just use <CodeRef>{`<input type='file'/>`}</CodeRef>
.
</P>
<Prose size="sm">
<SectionHeader
title="File upload & download"
slogan={
<>
Just use{" "}
<CodeRef>{`<input type='file'/>`}</CodeRef>.
</>
}
/>
<SmallProse>
<FileUploadDownloadDescription />
</Prose>
</SmallProse>
</GridCard>
<GridCard>
<H3>Video presence & calls</H3>
<P className="text-lg">
Stream and record audio & video.
</P>
<Prose size="sm">
<SectionHeader
title="Video presence & calls"
slogan="Stream and record audio & video."
/>
<SmallProse>
<VideoPresenceCallsDescription />
</Prose>
</SmallProse>
</GridCard>
</GappedGrid>
<div className="border rounded-xl shadow-sm p-4 md:py-16">
<div className="lg:max-w-3xl md:text-center mx-auto space-y-6">
<p className="uppercase text-blue tracking-widest text-sm font-medium dark:text-stone-400">
<div className="border border-stone-200 dark:border-stone-900 rounded-xl shadow-sm p-4 md:py-16">
<div className="flex flex-col lg:max-w-3xl md:text-center mx-auto justify-between gap-6">
<p className="uppercase text-blue tracking-wide text-sm font-medium dark:text-stone-400">
Become an early adopter
</p>
<H2>
<h3 className="font-display md:text-center text-stone-950 dark:text-white text-2xl md:text-3xl font-semibold tracking-tight">
We&apos;ll help you build your next app with Jazz
</H2>
<Prose className="md:text-balance mx-auto">
</h3>
<div className="space-y-2 md:text-balance leading-relaxed">
<p>
It&apos;s early days, but we work hard every day
to make Jazz a great tool for our users.
@@ -466,7 +574,7 @@ export default function Home() {
We&apos;ll prioritize features that you need to
succeed.
</p>
</Prose>
</div>
<div className="flex md:justify-center gap-3">
<Button
href="https://discord.gg/utDMjHYg42"

View File

@@ -0,0 +1,7 @@
import { ComingSoonBadge } from "gcmp-design-system/src/app/components/atoms/ComingSoonBadge";
- DemoAuth (for quick multi-user demos)
- WebAuthN (TouchID/FaceID)
- Auth0, Clerk & Okta <ComingSoonBadge/>
- NextAuth <ComingSoonBadge/>

View File

@@ -1,13 +0,0 @@
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
export default function DocsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<Prose className="container w-full max-w-full col-span-12 md:col-span-8 lg:col-span-9">
{children}
</Prose>
);
}

View File

@@ -1,29 +0,0 @@
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
export default function Page() {
return (
<Prose>
<h1>Welcome to the Jazz documentation.</h1>
<p>
The Jazz docs are currently heavily work in progress, sorry about that!
</p>
<p>The best ways to get started are:</p>
<ul>
<li>Quickstart (incomplete)<ol>
<li><a href="/docs/sync-and-storage">Sync & Storage Setup</a></li>
<li><a href="/docs/project-setup/react">React Project Setup</a></li>
<li><a href="/docs/schemas/covalues">CoValue Basics & Schema Definition</a></li>
<li><span className="opacity-50">Creating Covalues</span></li>
<li><span className="opacity-50">Using Covalues</span></li>
</ol></li>
<li>The step-by-step <a href="/docs/guide">Guide</a> (incomplete)</li>
</ul>
<p>Also make sure to:</p>
<ul>
<li>Find an <a href="/docs/examples">example app with code</a> most similar to what you want to build</li>
<li>Check out the <a href="/docs/api-reference">API Reference</a> (incomplete)</li>
</ul>
<p>And the best way to get help is to join the <a href="https://discord.gg/utDMjHYg42">Discord</a>!</p>
</Prose>
);
}

View File

@@ -63,7 +63,7 @@ export default function Page() {
href={`/docs/api-reference/${name}`}
key={name}
>
<Card className="border shadow-sm">
<Card className="border border-stone-200 dark:border-stone-900 shadow-sm">
<PackageIcon
size={25}
strokeWidth={1.5}
@@ -88,11 +88,11 @@ export default function Page() {
</CardHeading>
<CardBody>
Get help from our{" "}
<Link href="https://discord.gg/utDMjHYg42" className="underline">
<Link href="https://discord.gg/utDMjHYg42">
Discord
</Link>
, or open an issue on{" "}
<Link href="https://github.com/gardencmp/jazz" className="underline">
<Link href="https://github.com/gardencmp/jazz">
GitHub
</Link>
.

View File

@@ -236,7 +236,7 @@ export default App; // old
<div className="text-xs uppercase text-stone-400 dark:text-stone-600 tracking-wider -mb-3">
Preview
</div>
<div className="p-3 md:-mx-3 rounded border border-stone-100 bg-white dark:bg-black not-prose">
<div className="p-3 md:-mx-3 rounded border border-stone-100 dark:border-stone-900 bg-white dark:bg-black not-prose">
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
<h2>Buy terrarium</h2>
<p className="col-span-3">Make sure it's big enough for 10 snails.</p>
@@ -349,7 +349,7 @@ export function IssueComponent({ issue }: { issue: Issue }) { // old
<div className="text-xs uppercase text-stone-400 dark:text-stone-600 tracking-wider -mb-3">
Preview
</div>
<div className="p-3 md:-mx-3 rounded border border-stone-100 bg-white dark:bg-black not-prose">
<div className="p-3 md:-mx-3 rounded border border-stone-100 dark:border-stone-900 bg-white dark:bg-black not-prose">
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
<input type="text" value={"Buy terrarium"} />
<input

View File

@@ -1,89 +0,0 @@
import Guide from "./guide.mdx";
import { TableOfContents } from "@/components/docs/TableOfContents";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import { clsx } from "clsx";
const navItems = [
{
name: "Project Setup",
href: "/docs/guide#project-setup",
},
{
name: "Intro to CoValues",
href: "/docs/guide#intro-to-covalues",
items: [
{
name: "Declaration",
href: "/docs/guide#declaring-covalues",
},
{
name: "Reading",
href: "/docs/guide#reading-covalues",
},
{
name: "Creation",
href: "/docs/guide#creating-covalues",
},
{
name: "Editing & Subscription",
href: "/docs/guide#editing-and-subscription",
},
{
name: "Persistence",
href: "/docs/guide#persistence",
},
{
name: "Remote Sync",
href: "/docs/guide#remote-sync",
},
{
name: "Simple Public Sharing",
href: "/docs/guide#simple-public-sharing",
},
],
},
{
name: "Refs & Auto-Subscribe",
href: "/docs/guide#refs-and-on-demand-subscribe",
items: [
{
name: "Precise Loading Depths",
href: "/docs/guide#loading-depth",
},
],
},
{
name: "Groups & Permissions",
href: "/docs/guide#groups-and-permissions",
items: [
{
name: "Groups/Accounts as Scopes",
href: "/docs/guide#groups-accounts-as-scopes",
},
{
name: "Creating Invites",
href: "/docs/guide#creating-invites",
},
{
name: "Consuming Invites",
href: "/docs/guide#consuming-invites",
},
],
},
];
export default function Page() {
return (
<div
className={clsx(
"col-span-12 md:col-span-8 lg:col-span-9",
"lg:flex lg:gap-5",
)}
>
<Prose className="overflow-x-hidden lg:flex-1">
<Guide />
</Prose>
<TableOfContents className="w-48 shrink-0" items={navItems} />
</div>
);
}

View File

@@ -1,4 +1,6 @@
import { DocNav } from "@/components/docs/nav";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import { cn } from "@/lib/utils";
export const metadata = {
title: "Docs",
@@ -11,9 +13,17 @@ export default function DocsLayout({
children: React.ReactNode;
}) {
return (
<div className="container relative grid grid-cols-12 gap-5 py-8">
<DocNav />
{children}
<div className="container relative grid grid-cols-12 gap-x-8 py-8">
<DocNav
className={cn(
"md:col-span-4 lg:col-span-3",
"sticky align-start top-[4.75rem] h-[calc(100vh-8rem)] overflow-y-auto overflow-x-hidden",
"hidden md:block",
)}
/>
<div className="col-span-12 md:col-span-8 lg:col-span-9">
<Prose>{children}</Prose>
</div>
</div>
);
}

View File

@@ -0,0 +1,5 @@
import Guide from "./guide.mdx";
export default function Page() {
return <Guide />;
}

View File

@@ -1,50 +0,0 @@
import ReactGuide from "./react.mdx"
import { TableOfContents } from "@/components/docs/TableOfContents";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import { clsx } from "clsx";
const navItems = [
{
name: "React",
href: "/docs/project-setup/react#react",
},
{
name: "Next.JS",
href: "/docs/project-setup/react#next",
items: [
{
name: "Client-side only",
href: "/docs/project-setup/react#next-csr",
},
{
name: "SSR use 🧪",
href: "/docs/project-setup/react#next-ssr",
},
{
name: "SSR + client-side 🧪",
href: "/docs/project-setup/react#next-ssr-plus-csr",
},
]
},
{
name: "React Native",
href: "/docs/project-setup/react#react-native"
}
]
export default function Page() {
return (
<div
className={clsx(
"col-span-12 md:col-span-8 lg:col-span-9",
"lg:flex lg:gap-5",
)}
>
<Prose className="overflow-x-hidden lg:flex-1">
<ReactGuide />
</Prose>
<TableOfContents className="w-48 shrink-0" items={navItems} />
</div>
);
}

View File

@@ -1,161 +0,0 @@
import { CodeGroup } from "@/components/forMdx";
# React
Currently, the recommended pattern to set up a React app with Jazz is to create a separate file (for example, called `jazz.tsx`) in which:
1. You create a new Jazz React app context, extracting and exporting Jazz hooks.
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { createJazzReactApp } from "jazz-react";
const Jazz = createJazzReactApp();
export const { useAccount, useCoState } = Jazz;
```
</CodeGroup>
- You extract hooks here so they can be aware of a custom `AccountSchema` for your app once you start using one (see [Accounts & Migrations](/docs/schemas/accounts-and-migrations)).
This way, accounts returned by the hooks will be correctly typed throughout your app. Simply import them from `jazz.tsx` wherever you need them.
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { createJazzReactApp } from "jazz-react"; // old
const Jazz = createJazzReactApp({ AccountSchema: MyAppAccount });
export const { useAccount, useCoState } = Jazz; // old
```
</CodeGroup>
2. You define a context providing component (typically called `JazzAndAuth`) that uses
- the context provider of the Jazz React app context you just created
- the hooks and default/custom UI of one of the [Auth Methods](/docs/auth/auth-methods).
This is also where you specify the sync & storage server to connect to (see [Sync and storage](/docs/sync-and-storage)).
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { createJazzReactApp } from "jazz-react";// old
const Jazz = createJazzReactApp();// old
export const { useAccount, useCoState } = Jazz;// old
import { PasskeyAuthBasicUI, usePasskeyAuth } from "jazz-react";
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName });
return (
<>
<Jazz.Provider
auth={passkeyAuth}
peer="wss://mesh.jazz.tools/?key=you@example.com"
>
{children}
</Jazz.Provider>
<PasskeyAuthBasicUI state={passKeyState} />
</>
);
}
```
</CodeGroup>
With `JazzAndAuth` defined, you can wrap your app in it and then use the extracted and exported hooks within your App.
<CodeGroup>
{/* prettier-ignore */}
```tsx
ReactDOM.createRoot(document.getElementById("root")!).render( // old
<React.StrictMode> // old
<JazzAndAuth>
<App />
</JazzAndAuth>
</React.StrictMode>// old
);// old
```
</CodeGroup>
# Next.JS
## Client-side only
The easiest way to use Jazz with Next.JS is to only use it on the client side. You can ensure this by:
- marking the `jazz.tsx` file as `"use client"`
<CodeGroup>
{/* prettier-ignore */}
```tsx
"use client"
import { createJazzReactApp } from "jazz-react";// old
const Jazz = createJazzReactApp();// old
export const { useAccount, useCoState } = Jazz;// old
import { PasskeyAuthBasicUI, usePasskeyAuth } from "jazz-react";// old
function JazzAndAuth({ children }: { children: React.ReactNode }) {// old
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName });// old
return (// old
<>// old
<Jazz.Provider// old
auth={passkeyAuth}// old
peer="wss://mesh.jazz.tools/?key=you@example.com"// old
>// old
{children}// old
</Jazz.Provider>// old
<PasskeyAuthBasicUI state={passKeyState} />// old
</>// old
);// old
}// old
```
</CodeGroup>
- marking any file with components where you use Jazz hooks (such as `useAccount` or `useCoState`) as `"use client"`
## SSR use (experimental)
Pure SSR use of Jazz is basically just using jazz-nodejs (see [Node.JS / Server Workers](/docs/project-setup/server-side)) inside Server Components.
Instead of using hooks as you would on the client, you await promises returned by `CoValue.load(...)` inside your Server Components.
TODO: code example
This should work well for cases like rendering publicly-readable information, since the worker account will be able to load them.
In the future, it will be possible to use trusted auth methods (such as Clerk, Auth0, etc.) that let you act as the same Jazz user both on the client and on the server, letting you use SSR even for data private to that user.
## SSR + client-side (experimental)
You can combine the two approaches by creating
1. A pure "rendering" component that renders an already-loaded CoValue (in JSON-ified form)
TODO: code example
2. A "hydrating" component (with `"use client"`) that
- expects a pre-loaded CoValue as a prop (in JSON-ified form)
- uses one of the client-side Jazz hooks (such as `useAccount` or `useCoState`) to subscribe to that same CoValue
- passing the client-side subscribed state to the "rendering" component, with the pre-loaded CoValue as a fallback until the client receives the first subscribed state
TODO: code example
3. A "pre-loading" Server Component that
- pre-loads the CoValue by awaiting it's `load(...)` method (as described above)
- renders the "hydrating" component, passing the pre-loaded CoValue as a prop
TODO: code example
# React Native
TODO

View File

@@ -1,40 +0,0 @@
import ServerGuide from "./server-side.mdx"
import { TableOfContents } from "@/components/docs/TableOfContents";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import { clsx } from "clsx";
const navItems = [
{
name: "Generating Credentials",
href: "/docs/project-setup/server-side#generating-credentials",
},
{
name: "Storing and Providing Credentials",
href: "/docs/project-setup/server-side#storing-credentials",
},
{
name: "Starting a Server Worker",
href: "/docs/project-setup/server-side#starting",
},
{
name: "Using CoValues instead of Requests",
href: "/docs/project-setup/server-side#covalues-instead-of-requests",
}
]
export default function Page() {
return (
<div
className={clsx(
"col-span-12 md:col-span-8 lg:col-span-9",
"lg:flex lg:gap-5",
)}
>
<Prose className="overflow-x-hidden lg:flex-1">
<ServerGuide />
</Prose>
<TableOfContents className="w-48 shrink-0" items={navItems} />
</div>
);
}

View File

@@ -1,65 +0,0 @@
import { CodeGroup } from "@/components/forMdx";
# Node.JS / Server Workers
The main detail to understand when using Jazz server-side is that Server Workers have Jazz `Accounts`, just like normal users do.
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.
## Generating Credentials
Server Workers typically have static credentials, consisting of a public Account ID and a private Account Secret.
To generate new credentials for a Server Worker, you can run:
<CodeGroup>
{/* prettier-ignore */}
```sh
npx jazz-run account create --name "My Server Worker"
```
</CodeGroup>
The name will be put in the public profile of the Server Worker's `Account`, which can be helpful when inspecting metadata of CoValue edits that the Server Worker has done.
## Storing & Providing Credentials
Server Worker credentials are typically stored and provided as environmental variables.
**Take extra care with the Account Secret &mdash; handle it like any other secret environment variable such as a DB password.**
## Starting a Server Worker
You can use `startWorker` from `jazz-nodejs` to start a Server Worker. Similarly to setting up a client-side Jazz context, it:
- takes a custom `AccountSchema` if you have one (for example, because the worker needs to store information in it's private account root)
- takes a URL for a sync & storage server
`startWorker` expects credentials in the `JAZZ_WORKER_ACCOUNT` and `JAZZ_WORKER_SECRET` environment variables by default (as printed by `npx account create ...`), but you can also pass them manually as `accountID` and `accountSecret` parameters if you get them from elsewhere.
<CodeGroup>
{/* prettier-ignore */}
```ts
import { startWorker } from 'jazz-nodejs';
const { worker } = await startWorker({
AccountSchema: MyWorkerAccount,
syncServer: 'wss://cloud.jazz.tools/?key=you@example.com',
});
```
</CodeGroup>
`worker` acts like `me` (as returned by `useAccount` on the client) - you can use it to:
- load/subscribe to CoValues: `MyCoValue.subscribe(id, worker, {...})`
- create CoValues & Groups `const val = MyCoValue.create({...}, { owner: worker })`
## Using CoValues instead of Requests
Just like traditional backend functions, you can use Server Workers to do useful stuff (computations, calls to third-party APIs etc.) and put the results back into CoValues, which subscribed clients automatically get notified about.
What's less clear is how you can trigger this work to happen.
- One option is to define traditional HTTP API handlers that use the Jazz Worker internally. This is helpful if you need to mutate Jazz state in response to HTTP requests such as for webhooks or non-Jazz API clients
- The other option is to have the Jazz Worker subscribe to CoValues which they will then collaborate on with clients.
- A common pattern is to implement a state machine represented by a CoValue, where the client will do some state transitions (such as `draft -> ready`), which the worker will notice and then do some work in response, feeding the result back in a further state transition (such as `ready -> success & data`, or `ready -> failure & error details`).
- This way, client and worker don't have to explicitly know about each other or communicate directly, but can rely on Jazz as a communication mechanism - with computation progressing in a distributed manner wherever and whenever possible.

View File

@@ -1,65 +0,0 @@
import { CodeGroup } from "@/components/forMdx";
# CoValues
**CoValues ("Collaborative Values") are the core abstraction of Jazz.** Think of them as bread-and-butter datastructures that you can use to represent everything you need in your app.
As their name suggests, they are inherently collaborative, meaning **multiple users and devices can edit them at the same time.**
- CoValues allow for concurrent edits by always keeping their full edit histories, deriving the "current state" based on the currently locally available history.
- **Think of CoValues as "super-fast Git for lots of tiny data".**
- (The fact that this happens in an eventually-consistent way makes them [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type))
- Having the full history around also means that you often don't need explicit timestamps and author info - you get this for free, just by having different accounts edit a value and then looking at its [edit metadata](/docs/covalues/metadata).
CoValues mostly model JSON with CoMaps and CoLists, but also offer CoStreams for simple per-user value streams, and also let you represent binary data with BinaryCoStreams.
## Schemas as Your App's First Step
Under the hood, CoValues are as dynamic and flexible as JSON itself, but the way you use them in Jazz is by defining fixed schemas to describe the shape of data in your app.
- This helps correctness and development speed in general, but is particularly important
- when you evolve your app and need migrations
- when different clients and server workers collaborate on CoValues and need to make compatible changes
- Luckily, thinking about the shape of your data first is also a really good way to model your app. Even before you know the details of how your app will work, you'll probably know which kinds of objects it will deal with, and how they relate to each other.
Jazz makes it really quick to define and update schemas, since they are simple TypeScript classes:
<CodeGroup>
{/* prettier-ignore */}
```ts
export class TodoProject extends CoMap {
title = co.string;
tasks = co.ref(ListOfTasks);
}
```
</CodeGroup>
Here you can see how we use the `co` definer for declaring collaboratively editable fields, which ensures that schema info is correct at the type level and at runtime.
Classes might look a bit old-fashioned to some, but one nice property they have is that they are both types and values in TypeScript, so we can use them as both (with a single definition & import).
<CodeGroup>
{/* prettier-ignore */}
```ts
import { TodoProject } from "./schema";
const project: TodoProject = TodoProject.create(
{
title: "New Project",
tasks: ListOfTasks.create([], { owner: me }),
},
{ owner: me }
);
```
</CodeGroup>
## CoValue field types
Before we look at the different types of CoValues, let's learn what we can put *into* them:
### Primitive fields
### Refs
### Computed fields, methods & constructors

View File

@@ -1,76 +0,0 @@
import CoValuesGuide from "./covalues.mdx"
import { TableOfContents } from "@/components/docs/TableOfContents";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import { clsx } from "clsx";
const navItems = [
{
name: "CoValues",
href: "/docs/schemas/covalues",
},
{
name: "Schemas as Your App's First Step",
href: "/docs/schemas/covalues#schemas-as-first-step",
},
{
name: "CoValue field types",
href: "/docs/schemas/covalues#field-types",
items: [
{
name: "Primitive Fields",
href: "/docs/schemas/covalues#primitive-fields",
},
{
name: "Refs",
href: "/docs/schemas/covalues#refs",
},
{
name: "Computed Fields, Methods & Constructors ",
href: "/docs/schemas/covalues#custom-fields"
}
]
},
{
name: "CoMaps",
href: "/docs/schemas/covalues#comaps",
items: [
{
name: "Struct-like CoMaps",
href: "/docs/schemas/covalues#comaps-struct-like",
},
{
name: "Dict/Record-like CoMaps",
href: "/docs/schemas/covalues#comaps-dict-like",
},
]
},
{
name: "CoLists",
href: "/docs/schemas/covalues#colists",
},
{
name: "CoStreams",
href: "/docs/schemas/covalues#costreams",
},
{
name: "BinaryCoStreams",
href: "/docs/schemas/covalues#binarycostreams",
}
]
export default function Page() {
return (
<div
className={clsx(
"col-span-12 md:col-span-8 lg:col-span-9",
"lg:flex lg:gap-5",
)}
>
<Prose className="overflow-x-hidden lg:flex-1">
<CoValuesGuide />
</Prose>
<TableOfContents className="w-48 shrink-0" items={navItems} />
</div>
);
}

View File

@@ -1,48 +0,0 @@
import SyncAndStorage from "./sync-and-storage.mdx"
import { TableOfContents } from "@/components/docs/TableOfContents";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import { clsx } from "clsx";
const navItems = [
{
name: "Using Jazz Cloud",
href: "/docs/sync-and-storage#using-jazz-cloud",
items: [
{
name: "Free Public Alpha",
href: "/docs/sync-and-storage#free-public-alpha",
},
]
},
{
name: "Running your own sync server",
href: "/docs/sync-and-storage#running-your-own",
items: [
{
name: "Command line options",
href: "/docs/sync-and-storage#command-line-options",
},
{
name: "Source code",
href: "/docs/sync-and-storage#source-code",
},
]
}
]
export default function Page() {
return (
<div
className={clsx(
"col-span-12 md:col-span-8 lg:col-span-9",
"lg:flex lg:gap-5",
)}
>
<Prose className="overflow-x-hidden lg:flex-1">
<SyncAndStorage />
</Prose>
<TableOfContents className="w-48 shrink-0" items={navItems} />
</div>
);
}

View File

@@ -1,47 +0,0 @@
# Sync and Storage
## Using Jazz Cloud
Simply use `wss://cloud.jazz.tools/?key=...` as the sync server URL.
Jazz Cloud will
- sync CoValues in real-time between users and devices
- safely persist CoValues on redundant storage nodes with additional backups
- make use of geographically distributed cache nodes for low latency
### Free Public Alpha <a id="free-public-alpha"/>
- Jazz Cloud is free during the public alpha, with no strict usage limits
- We plan to keep a free tier, so you'll always be able to get started with zero setup
- See [Jazz Cloud Pricing](https://jazz.tools/cloud#pricing) for more details
- ⚠️ Please use a valid email address as your API key.
Your full sync server URL should look something like
```wss://cloud.jazz.tools/?key=you@example.com```
Once we support per-app API keys, we'll email you an API key you can use instead.
## Running your own sync server <a id="running-your-own"/>
You can run your own sync server using:
```sh
npx jazz-run sync
```
And then use `ws://localhost:4200` as the sync server URL.
You can also run this simple sync server behind a proxy that supports WebSockets, for example to provide TLS.
In this case, provide the WebSocket endpoint your proxy exposes as the sync server URL.
### Command line options: <a id="command-line-options"/>
- `--port` / `-p` - the port to run the sync server on. Defaults to 4200.
- `--in-memory` - keep CoValues in-memory only and do sync only, no persistence. Persistence is enabled by default.
- `--db` - the path to the file where to store the data (SQLite). Defaults to `sync-db/storage.db`.
### Source code <a id="source-code"/>
The implementation of this simple sync server is available open-source [on GitHub](https://github.com/gardencmp/jazz/blob/main/packages/jazz-run/src/startSync.ts).

View File

@@ -2,6 +2,78 @@
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--radius: 0.5rem;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 60 9.1% 97.8%;
--primary-foreground: 24 9.8% 10%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 24 5.7% 82.9%;
}
* {
@apply border-border;
}
.overlay-close {
@apply bg-black;
}
}
@layer components {
@import "shiki.css";
}

View File

@@ -1,14 +1,14 @@
import "./globals.css";
import type { Metadata } from "next";
import { ThemeProvider } from "@/components/themeProvider";
import { Inter, Manrope } from "next/font/google";
import { Inter, Manrope, Sriracha } from "next/font/google";
import localFont from "next/font/local";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Analytics } from "@vercel/analytics/react";
import { JazzNav } from "@/components/nav";
import { JazzFooter } from "@/components/footer";
import { ThemeProvider } from "gcmp-design-system/src/app/components/molecules/ThemeProvider";
// If loading a variable font, you don't need to specify the font weight
const manrope = Manrope({
@@ -23,6 +23,13 @@ const inter = Inter({
display: "swap",
});
const sriracha = Sriracha({
weight: "400",
subsets: ["latin"],
variable: "--font-sriracha",
display: "swap",
});
const commitMono = localFont({
src: [
{
@@ -41,7 +48,7 @@ const commitMono = localFont({
});
const metaTags = {
title: "Jazz - Build local-first apps",
title: "jazz - Build your next app with sync",
description:
"Jazz is an open-source framework for building local-first apps, removing 90% of the backend and infrastructure complexity.",
url: "https://jazz.tools",
@@ -51,16 +58,16 @@ export const metadata: Metadata = {
// metadataBase is a convenience option to set a base URL prefix for metadata fields that require a fully qualified URL.
metadataBase: new URL(metaTags.url),
title: {
template: "%s | Jazz",
template: "%s | jazz",
default: metaTags.title,
},
applicationName: "Jazz",
applicationName: "jazz",
description: metaTags.description,
openGraph: {
title: metaTags.title,
description: metaTags.description,
url: metaTags.url,
siteName: "Jazz",
siteName: "jazz",
images: [
{
url: "/social-image.png",
@@ -77,13 +84,14 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en" className="h-full" suppressHydrationWarning>
<html lang="en">
<body
className={[
manrope.variable,
commitMono.variable,
inter.className,
"h-full flex flex-col items-center [&_*]:scroll-mt-[5rem]",
sriracha.variable,
"flex flex-col items-center [&_*]:scroll-mt-[5rem]",
"bg-white text-stone-700 dark:text-stone-400 dark:bg-stone-950",
].join(" ")}
>

View File

@@ -53,7 +53,7 @@ pre.twoslash data-lsp::before {
position: absolute;
transform: translate(0, 1.2rem);
max-width: 30rem;
@apply text-xs px-1.5 py-1 rounded border shadow-lg overflow-hidden whitespace-pre-wrap text-stone-700 bg-stone-50 dark:text-stone-200 dark:bg-stone-950;
@apply text-xs px-1.5 py-1 rounded border border-stone-200 dark:border-stone-900 shadow-lg overflow-hidden whitespace-pre-wrap text-stone-700 bg-stone-50 dark:text-stone-200 dark:bg-stone-950;
text-align: left;
z-index: 100;
opacity: 0;
@@ -79,7 +79,7 @@ pre.twoslash data-lsp:hover::before {
}
pre .code-container {
@apply overflow-auto p-2 pl-0 bg-white dark:bg-stone-925 rounded-b-xl text-xs h-full;
@apply overflow-auto p-2 pl-0 bg-white dark:bg-stone-925 rounded-b-xl text-xs h-full dark:border-stone-900;
}
/* The try button */
pre .code-container > a {

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