Compare commits
85 Commits
jazz-run@0
...
feat/inspe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e44caaebc | ||
|
|
c95f344c41 | ||
|
|
7320adc58d | ||
|
|
57a92f4aa0 | ||
|
|
c9b2b01928 | ||
|
|
5b97ac3b92 | ||
|
|
09f0a98eef | ||
|
|
3d8babdbb6 | ||
|
|
f10004c6bc | ||
|
|
33ecca3d10 | ||
|
|
bc601d809b | ||
|
|
cc8462f071 | ||
|
|
790d5dde40 | ||
|
|
7326a19373 | ||
|
|
f39c87b181 | ||
|
|
e19681a4a7 | ||
|
|
c5a3b29c61 | ||
|
|
79513379c8 | ||
|
|
1271a6f753 | ||
|
|
77f58ddcad | ||
|
|
7e945b5eac | ||
|
|
deea2222cb | ||
|
|
ff4a839dca | ||
|
|
fb053a0dd5 | ||
|
|
253b775bff | ||
|
|
831fce6d55 | ||
|
|
e3f85f997c | ||
|
|
44043991fb | ||
|
|
60af229bcc | ||
|
|
be6dafc36a | ||
|
|
cdf3ed898f | ||
|
|
d2379eacd7 | ||
|
|
0525d5c056 | ||
|
|
7803a7a85e | ||
|
|
e9d131cc9c | ||
|
|
4f2730fa00 | ||
|
|
b416136b12 | ||
|
|
56869c3593 | ||
|
|
bce7dade72 | ||
|
|
2c00f93141 | ||
|
|
1bfa9bb1de | ||
|
|
9106881d19 | ||
|
|
75319a6eaf | ||
|
|
01e54ddf4e | ||
|
|
8f6e16a899 | ||
|
|
a4e342a59b | ||
|
|
73b71df524 | ||
|
|
fcaa5dddda | ||
|
|
c27fadc4e0 | ||
|
|
56af3c7412 | ||
|
|
5e5ef10675 | ||
|
|
2efc55f9bf | ||
|
|
7101f10573 | ||
|
|
3849392272 | ||
|
|
ce1fc720c1 | ||
|
|
4eb634175d | ||
|
|
adc6343531 | ||
|
|
c745e099fe | ||
|
|
970c9f5c6c | ||
|
|
4e3986ae98 | ||
|
|
6c691bf641 | ||
|
|
d03ba7fdd0 | ||
|
|
35cb7d8988 | ||
|
|
0e40a9daab | ||
|
|
bd1996c458 | ||
|
|
373ebec04c | ||
|
|
7fedbeb8e4 | ||
|
|
4bfc23091c | ||
|
|
6e5ea6f19c | ||
|
|
bcd67cb23f | ||
|
|
27781ed778 | ||
|
|
eb097ecdb1 | ||
|
|
cdae2603ba | ||
|
|
2b3490cce2 | ||
|
|
4baba65cdd | ||
|
|
c6ef96d642 | ||
|
|
385ce9ba37 | ||
|
|
d128490da5 | ||
|
|
60f5b3ffeb | ||
|
|
3615b4871b | ||
|
|
65b2947c18 | ||
|
|
f9147dae85 | ||
|
|
21309953ee | ||
|
|
bc2f37df37 | ||
|
|
57ffac4432 |
18
.changeset/big-pens-pull.md
Normal file
18
.changeset/big-pens-pull.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
"jazz-tailwind-demo-auth-starter": patch
|
||||
"file-share-svelte": patch
|
||||
"jazz-password-manager": patch
|
||||
"version-history": patch
|
||||
"passkey-svelte": patch
|
||||
"chat-rn-clerk": patch
|
||||
"jazz-example-music-player": patch
|
||||
"passphrase": patch
|
||||
"multiauth": patch
|
||||
"reactions": patch
|
||||
"passkey": patch
|
||||
"clerk": patch
|
||||
"jazz-example-pets": patch
|
||||
"jazz-example-todo": patch
|
||||
---
|
||||
|
||||
Removed when="singedUp" from examples apps' Jazz providers. This is a really niche use-case option and can lead to broken-feeling experiences when anonymous users try to load something.
|
||||
5
.changeset/dull-tools-run.md
Normal file
5
.changeset/dull-tools-run.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-jazz-app": patch
|
||||
---
|
||||
|
||||
add directory param to create-jazz-app
|
||||
6
.changeset/eleven-moles-grow.md
Normal file
6
.changeset/eleven-moles-grow.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"jazz-inspector": patch
|
||||
"jazz-inspector-app": patch
|
||||
---
|
||||
|
||||
UI and JSON display improvements
|
||||
@@ -1,5 +1,14 @@
|
||||
# chat-rn-clerk
|
||||
|
||||
## 1.0.86
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.11.5
|
||||
- jazz-react-native-auth-clerk@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
- jazz-react-native-media-images@0.11.5
|
||||
|
||||
## 1.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chat-rn-clerk",
|
||||
"main": "index.js",
|
||||
"version": "1.0.85",
|
||||
"version": "1.0.86",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
|
||||
@@ -12,7 +12,6 @@ export function JazzAndAuth({ children }: PropsWithChildren) {
|
||||
storage="sqlite"
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp", // This makes the app work in local mode when the user is not authenticated
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# chat-rn
|
||||
|
||||
## 1.0.82
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 1.0.81
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.81",
|
||||
"version": "1.0.82",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# chat-vue
|
||||
|
||||
## 0.0.67
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
- jazz-vue@0.11.5
|
||||
|
||||
## 0.0.66
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -11,12 +11,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example chat-vue --project-name chat-vue
|
||||
npx create-jazz-app@latest chat-vue-app --example chat-vue
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd chat-vue
|
||||
cd chat-vue-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-vue",
|
||||
"version": "0.0.66",
|
||||
"version": "0.0.67",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.164
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
- jazz-browser-media-images@0.11.5
|
||||
|
||||
## 0.0.163
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example chat --project-name chat
|
||||
npx create-jazz-app@latest chat-app --example chat
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd chat
|
||||
cd chat-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.163",
|
||||
"version": "0.0.164",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,6 +16,7 @@
|
||||
"clsx": "^2.0.0",
|
||||
"hash-slash": "workspace:*",
|
||||
"jazz-browser-media-images": "workspace:*",
|
||||
"jazz-inspector": "workspace:*",
|
||||
"jazz-react": "workspace:*",
|
||||
"jazz-tools": "workspace:*",
|
||||
"lucide-react": "^0.274.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { apiKey } from "@/apiKey.ts";
|
||||
import { getRandomUsername, inIframe, onChatLoad } from "@/util.ts";
|
||||
import { useIframeHashRouter } from "hash-slash";
|
||||
import { JazzInspector } from "jazz-inspector";
|
||||
import { JazzProvider, useAccount } from "jazz-react";
|
||||
import { Group, ID } from "jazz-tools";
|
||||
import { StrictMode } from "react";
|
||||
@@ -61,6 +62,7 @@ createRoot(document.getElementById("root")!).render(
|
||||
defaultProfileName={defaultProfileName}
|
||||
>
|
||||
<App />
|
||||
<JazzInspector />
|
||||
</JazzProvider>
|
||||
</StrictMode>
|
||||
</ThemeProvider>,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# minimal-auth-clerk
|
||||
|
||||
## 0.0.63
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-react-auth-clerk@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 0.0.62
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -15,12 +15,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example clerk --project-name clerk
|
||||
npx create-jazz-app@latest clerk-app --example clerk
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd clerk
|
||||
cd clerk-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clerk",
|
||||
"private": true,
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.63",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@clerk/clerk-react": "^5.4.1",
|
||||
"jazz-react": "workspace:*",
|
||||
"jazz-react-auth-clerk": "workspace:0.11.4",
|
||||
"jazz-react-auth-clerk": "workspace:0.11.5",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
|
||||
@@ -21,7 +21,6 @@ function JazzProvider({ children }: { children: React.ReactNode }) {
|
||||
clerk={clerk}
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp", // This makes the app work in local mode when the user is not authenticated
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# file-share-svelte
|
||||
|
||||
## 0.0.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 0.0.46
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "file-share-svelte",
|
||||
"version": "0.0.46",
|
||||
"version": "0.0.47",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
AccountSchema={FileShareAccount}
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="File Share">
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# jazz-tailwind-demo-auth-starter
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "filestream",
|
||||
"private": true,
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# form
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
- jazz-browser-media-images@0.11.5
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -28,12 +28,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example form --project-name form
|
||||
npx create-jazz-app@latest form-app --example form
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd form
|
||||
cd form-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "form",
|
||||
"private": true,
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# image-upload
|
||||
|
||||
## 0.0.61
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
- jazz-browser-media-images@0.11.5
|
||||
|
||||
## 0.0.60
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -15,12 +15,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example image-upload --project-name image-upload
|
||||
npx create-jazz-app@latest image-upload-app --example image-upload
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd image-upload
|
||||
cd image-upload-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "image-upload",
|
||||
"private": true,
|
||||
"version": "0.0.60",
|
||||
"version": "0.0.61",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-example-inspector
|
||||
|
||||
## 0.0.114
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [60f5b3f]
|
||||
- cojson@0.11.5
|
||||
- cojson-transport-ws@0.11.5
|
||||
|
||||
## 0.0.113
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector-app",
|
||||
"private": true,
|
||||
"version": "0.0.113",
|
||||
"version": "0.0.114",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -11,27 +11,19 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"jazz-inspector": "workspace:*",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.11.4",
|
||||
"cojson-transport-ws": "workspace:0.11.4",
|
||||
"cojson": "workspace:0.11.5",
|
||||
"cojson-transport-ws": "workspace:0.11.5",
|
||||
"hash-slash": "workspace:0.2.2",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router": "^6.16.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-use": "^17.4.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uniqolor": "^1.1.0"
|
||||
"react-use": "^17.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qrcode": "^1.5.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
|
||||
@@ -1,92 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--primary: 24 9.8% 10%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
|
||||
--muted: 60 4.8% 95.9%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
|
||||
--accent: 60 4.8% 95.9%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 20 5.9% 90%;
|
||||
--input: 20 5.9% 90%;
|
||||
--ring: 20 14.3% 4.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
|
||||
--card: 20 14.3% 4.1%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--popover: 20 14.3% 4.1%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--primary: 60 9.1% 97.8%;
|
||||
--primary-foreground: 24 9.8% 10%;
|
||||
|
||||
--secondary: 12 6.5% 15.1%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--muted: 12 6.5% 15.1%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 12 6.5% 15.1%;
|
||||
--input: 12 6.5% 15.1%;
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateZ(400px) translateY(30px) scale(1.05);
|
||||
opacity: 0.4;
|
||||
}
|
||||
to {
|
||||
transform: translateZ(0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
export function LinkIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from "react";
|
||||
import { PageInfo } from "./types";
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
path: PageInfo[];
|
||||
onBreadcrumbClick: (index: number) => void;
|
||||
}
|
||||
|
||||
export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
|
||||
path,
|
||||
onBreadcrumbClick,
|
||||
}) => {
|
||||
return (
|
||||
<div className="z-20 relative bg-indigo-400/10 backdrop-blur-sm rounded-lg inline-flex px-2 py-1 whitespace-pre transition-all items-center space-x-1 min-h-10">
|
||||
<button
|
||||
onClick={() => onBreadcrumbClick(-1)}
|
||||
className="flex items-center justify-center p-1 rounded-sm hover:bg-indigo-500/10 transition-colors"
|
||||
aria-label="Go to home"
|
||||
>
|
||||
<img src="jazz-logo.png" alt="Jazz Logo" className="size-5" />
|
||||
</button>
|
||||
{path.map((page, index) => {
|
||||
return (
|
||||
<span key={index} className="inline-block first:pl-1 last:pr-1">
|
||||
{index === 0 ? null : (
|
||||
<span className="text-indigo-500/30">{" / "}</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onBreadcrumbClick(index)}
|
||||
className="text-indigo-700 hover:underline"
|
||||
>
|
||||
{index === 0 ? page.name || "Root" : page.name}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,344 +0,0 @@
|
||||
import {
|
||||
CoID,
|
||||
LocalNode,
|
||||
RawBinaryCoStream,
|
||||
RawCoStream,
|
||||
RawCoValue,
|
||||
} from "cojson";
|
||||
import { base64URLtoBytes } from "cojson";
|
||||
import { BinaryStreamItem, BinaryStreamStart, CoStreamItem } from "cojson";
|
||||
import { JsonObject, JsonValue } from "cojson";
|
||||
import { ArrowDownToLine } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PageInfo } from "./types";
|
||||
import { AccountOrGroupPreview } from "./value-renderer";
|
||||
|
||||
// typeguard for BinaryStreamStart
|
||||
function isBinaryStreamStart(item: unknown): item is BinaryStreamStart {
|
||||
return (
|
||||
typeof item === "object" &&
|
||||
item !== null &&
|
||||
"type" in item &&
|
||||
item.type === "start"
|
||||
);
|
||||
}
|
||||
|
||||
function detectCoStreamType(value: RawCoStream | RawBinaryCoStream) {
|
||||
const firstKey = Object.keys(value.items)[0];
|
||||
if (!firstKey)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
|
||||
const items = value.items[firstKey as never]?.map((v) => v.value);
|
||||
|
||||
if (!items)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
const firstItem = items[0];
|
||||
if (!firstItem)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
// This is a binary stream
|
||||
if (isBinaryStreamStart(firstItem)) {
|
||||
return {
|
||||
type: "binary",
|
||||
items: items as BinaryStreamItem[],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "coStream",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getBlobFromCoStream({
|
||||
items,
|
||||
onlyFirstChunk = false,
|
||||
}: {
|
||||
items: BinaryStreamItem[];
|
||||
onlyFirstChunk?: boolean;
|
||||
}) {
|
||||
if (onlyFirstChunk && items.length > 1) {
|
||||
items = items.slice(0, 2);
|
||||
}
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
|
||||
const binary_U_prefixLength = 8;
|
||||
|
||||
let lastProgressUpdate = Date.now();
|
||||
|
||||
for (const item of items.slice(1)) {
|
||||
if (item.type === "end") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (item.type !== "chunk") {
|
||||
console.error("Invalid binary stream chunk", item);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chunk = base64URLtoBytes(item.chunk.slice(binary_U_prefixLength));
|
||||
// totalLength += chunk.length;
|
||||
chunks.push(chunk);
|
||||
|
||||
if (Date.now() - lastProgressUpdate > 100) {
|
||||
lastProgressUpdate = Date.now();
|
||||
}
|
||||
}
|
||||
const defaultMime = "mimeType" in items[0] ? items[0].mimeType : null;
|
||||
|
||||
const blob = new Blob(chunks, defaultMime ? { type: defaultMime } : {});
|
||||
|
||||
const mimeType =
|
||||
defaultMime === "" ? await detectPDFMimeType(blob) : defaultMime;
|
||||
|
||||
return {
|
||||
blob,
|
||||
mimeType: mimeType as string,
|
||||
unfinishedChunks: items.length > 1,
|
||||
totalSize:
|
||||
"totalSizeBytes" in items[0]
|
||||
? (items[0].totalSizeBytes as number)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const detectPDFMimeType = async (blob: Blob): Promise<string> => {
|
||||
const arrayBuffer = await blob.slice(0, 4).arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
const header = uint8Array.reduce(
|
||||
(acc, byte) => acc + String.fromCharCode(byte),
|
||||
"",
|
||||
);
|
||||
|
||||
if (header === "%PDF") {
|
||||
return "application/pdf";
|
||||
}
|
||||
return "application/octet-stream";
|
||||
};
|
||||
|
||||
const BinaryDownloadButton = ({
|
||||
pdfBlob,
|
||||
fileName = "document",
|
||||
label,
|
||||
mimeType,
|
||||
}: {
|
||||
pdfBlob: Blob;
|
||||
mimeType?: string;
|
||||
fileName?: string;
|
||||
label: string;
|
||||
}) => {
|
||||
const downloadFile = () => {
|
||||
const url = URL.createObjectURL(
|
||||
new Blob([pdfBlob], mimeType ? { type: mimeType } : {}),
|
||||
);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download =
|
||||
mimeType === "application/pdf" ? `${fileName}.pdf` : fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="flex items-center gap-2 px-2 py-1 text-gray-900 border border-gray-900/10 bg-clip-border shadow-sm transition-colors rounded bg-gray-50 text-sm"
|
||||
onClick={downloadFile}
|
||||
>
|
||||
<ArrowDownToLine size={16} />
|
||||
{label}
|
||||
{/* Download {mimeType === "application/pdf" ? "PDF" : "File"} */}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const LabelContentPair = ({
|
||||
label,
|
||||
content,
|
||||
}: {
|
||||
label: string;
|
||||
content: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 ">
|
||||
<span className="uppercase text-xs font-medium text-gray-600 tracking-wide">
|
||||
{label}
|
||||
</span>
|
||||
<span>{content}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function RenderCoBinaryStream({
|
||||
value,
|
||||
items,
|
||||
}: {
|
||||
items: BinaryStreamItem[];
|
||||
value: RawBinaryCoStream;
|
||||
}) {
|
||||
const [file, setFile] = useState<
|
||||
| {
|
||||
blob: Blob;
|
||||
mimeType: string;
|
||||
unfinishedChunks: boolean;
|
||||
totalSize: number | undefined;
|
||||
}
|
||||
| undefined
|
||||
| null
|
||||
>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// load only the first chunk to get the mime type and size
|
||||
getBlobFromCoStream({
|
||||
items,
|
||||
onlyFirstChunk: true,
|
||||
})
|
||||
.then((v) => {
|
||||
if (v) {
|
||||
setFile(v);
|
||||
if (v.mimeType.includes("image")) {
|
||||
// If it's an image, load the full blob
|
||||
getBlobFromCoStream({
|
||||
items,
|
||||
}).then((s) => {
|
||||
if (s) setFile(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [items]);
|
||||
|
||||
if (!isLoading && !file) return <div>No blob</div>;
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (!file) return <div>No blob</div>;
|
||||
|
||||
const { blob, mimeType } = file;
|
||||
|
||||
const sizeInKB = (file.totalSize || 0) / 1024;
|
||||
|
||||
return (
|
||||
<div className="space-y-8 mt-4">
|
||||
<div className="grid grid-cols-3 gap-2 max-w-3xl">
|
||||
<LabelContentPair
|
||||
label="Mime Type"
|
||||
content={
|
||||
<span className="font-mono bg-gray-100 rounded px-2 py-1 text-sm">
|
||||
{mimeType || "No mime type"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<LabelContentPair
|
||||
label="Size"
|
||||
content={<span>{sizeInKB.toFixed(2)} KB</span>}
|
||||
/>
|
||||
<LabelContentPair
|
||||
label="Download"
|
||||
content={
|
||||
<BinaryDownloadButton
|
||||
fileName={value.id.toString()}
|
||||
pdfBlob={blob}
|
||||
mimeType={mimeType}
|
||||
label={
|
||||
mimeType === "application/pdf"
|
||||
? "Download PDF"
|
||||
: "Download File"
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{mimeType === "image/png" || mimeType === "image/jpeg" ? (
|
||||
<LabelContentPair
|
||||
label="Preview"
|
||||
content={
|
||||
<div className="bg-gray-50 p-3 rounded-sm">
|
||||
<RenderBlobImage blob={blob} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderCoStream({
|
||||
value,
|
||||
node,
|
||||
}: {
|
||||
value: RawCoStream;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const streamPerUser = Object.keys(value.items);
|
||||
const userCoIds = streamPerUser.map((stream) => stream.split("_session")[0]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{userCoIds.map((id, idx) => (
|
||||
<div
|
||||
className="bg-gray-100 p-3 rounded-lg transition-colors overflow-hidden bg-white border hover:bg-gray-100/5 cursor-pointer shadow-sm"
|
||||
key={id}
|
||||
>
|
||||
<AccountOrGroupPreview coId={id as CoID<RawCoValue>} node={node} />
|
||||
{/* @ts-expect-error - TODO: fix types */}
|
||||
{value.items[streamPerUser[idx]]?.map(
|
||||
(item: CoStreamItem<JsonValue>) => (
|
||||
<div>
|
||||
{new Date(item.madeAt).toLocaleString()}{" "}
|
||||
{JSON.stringify(item.value)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CoStreamView({
|
||||
value,
|
||||
node,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
node: LocalNode;
|
||||
value: RawCoStream;
|
||||
}) {
|
||||
// if (!value) return <div>No value</div>;
|
||||
|
||||
const streamType = detectCoStreamType(value);
|
||||
|
||||
if (streamType.type === "binary") {
|
||||
if (streamType.items === undefined) {
|
||||
return <div>No binary stream</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderCoBinaryStream
|
||||
value={value as RawBinaryCoStream}
|
||||
items={streamType.items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (streamType.type === "coStream") {
|
||||
return <RenderCoStream value={value} node={node} />;
|
||||
}
|
||||
|
||||
if (streamType.type === "unknown") return <div>Unknown stream type</div>;
|
||||
|
||||
return <div>Unknown stream type</div>;
|
||||
}
|
||||
|
||||
function RenderBlobImage({ blob }: { blob: Blob }) {
|
||||
const urlCreator = window.URL || window.webkitURL;
|
||||
return <img src={urlCreator.createObjectURL(blob)} />;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import { JsonObject } from "cojson";
|
||||
import { ResolveIcon } from "./type-icon";
|
||||
import { PageInfo, isCoId } from "./types";
|
||||
import { CoMapPreview, ValueRenderer } from "./value-renderer";
|
||||
|
||||
export function GridView({
|
||||
data,
|
||||
onNavigate,
|
||||
node,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const entries = Object.entries(data);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-2">
|
||||
{entries.map(([key, child], childIndex) => (
|
||||
<div
|
||||
key={childIndex}
|
||||
className={clsx(
|
||||
"bg-gray-100 p-3 rounded-lg transition-colors overflow-hidden",
|
||||
isCoId(child)
|
||||
? "bg-white border hover:bg-gray-100/5 cursor-pointer shadow-sm"
|
||||
: "bg-gray-50",
|
||||
)}
|
||||
onClick={() =>
|
||||
isCoId(child) &&
|
||||
onNavigate([{ coId: child as CoID<RawCoValue>, name: key }])
|
||||
}
|
||||
>
|
||||
<h3 className="truncate">
|
||||
{isCoId(child) ? (
|
||||
<span className="font-medium flex justify-between">
|
||||
{key}
|
||||
|
||||
<div className="px-2 py-1 text-xs bg-gray-100 rounded">
|
||||
<ResolveIcon coId={child as CoID<RawCoValue>} node={node} />
|
||||
</div>
|
||||
</span>
|
||||
) : (
|
||||
<span>{key}</span>
|
||||
)}
|
||||
</h3>
|
||||
<div className="mt-2 text-sm">
|
||||
{isCoId(child) ? (
|
||||
<CoMapPreview coId={child as CoID<RawCoValue>} node={node} />
|
||||
) : (
|
||||
<ValueRenderer
|
||||
json={child}
|
||||
onCoIDClick={(coId) => {
|
||||
onNavigate([{ coId, name: key }]);
|
||||
}}
|
||||
compact
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { LocalNode } from "cojson";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
import { PageStack } from "./page-stack";
|
||||
import { PageInfo } from "./types";
|
||||
import { Breadcrumbs, PageStack } from "jazz-inspector";
|
||||
import type { PageInfo } from "jazz-inspector";
|
||||
import { usePagePath } from "./use-page-path";
|
||||
|
||||
export default function CoJsonViewer({
|
||||
|
||||
@@ -9,10 +9,15 @@ import {
|
||||
} from "cojson";
|
||||
import { createWebSocketPeer } from "cojson-transport-ws";
|
||||
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Icon,
|
||||
Input,
|
||||
PageStack,
|
||||
Select,
|
||||
} from "jazz-inspector";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
import { PageStack } from "./page-stack";
|
||||
import { usePagePath } from "./use-page-path";
|
||||
import { resolveCoValue, useResolvedCoValue } from "./use-resolve-covalue";
|
||||
|
||||
@@ -121,15 +126,23 @@ export default function CoJsonViewerApp() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen bg-gray-100 p-4 overflow-hidden flex flex-col">
|
||||
<div className="flex items-center mb-4 gap-4">
|
||||
<div
|
||||
className={clsx(
|
||||
"h-screen overflow-hidden flex flex-col",
|
||||
" text-stone-700 bg-white",
|
||||
"dark:text-stone-300 dark:bg-stone-950",
|
||||
)}
|
||||
>
|
||||
<header className="flex items-center gap-4 p-3">
|
||||
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
|
||||
<div className="flex-1">
|
||||
<form onSubmit={handleCoValueIdSubmit}>
|
||||
{path.length !== 0 && (
|
||||
<input
|
||||
className="border p-2 rounded-lg min-w-[21rem] font-mono"
|
||||
<Input
|
||||
className="min-w-[21rem] font-mono"
|
||||
placeholder="co_z1234567890abcdef123456789"
|
||||
label="CoValue ID"
|
||||
hideLabel
|
||||
value={coValueId}
|
||||
onChange={(e) =>
|
||||
setCoValueId(e.target.value as CoID<RawCoValue>)
|
||||
@@ -145,7 +158,7 @@ export default function CoJsonViewerApp() {
|
||||
deleteCurrentAccount={deleteCurrentAccount}
|
||||
localNode={localNode}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<PageStack
|
||||
path={path}
|
||||
@@ -153,49 +166,39 @@ export default function CoJsonViewerApp() {
|
||||
goBack={goBack}
|
||||
addPages={addPages}
|
||||
>
|
||||
{!currentAccount ? (
|
||||
<AddAccountForm addAccount={addAccount} />
|
||||
) : (
|
||||
{!currentAccount && <AddAccountForm addAccount={addAccount} />}
|
||||
|
||||
{currentAccount && path.length <= 0 && (
|
||||
<form
|
||||
onSubmit={handleCoValueIdSubmit}
|
||||
aria-hidden={path.length !== 0}
|
||||
className={clsx(
|
||||
"flex flex-col justify-center items-center gap-2 h-full w-full mb-20 ",
|
||||
"transition-all duration-150",
|
||||
path.length > 0
|
||||
? "opacity-0 -translate-y-2 scale-95"
|
||||
: "opacity-100",
|
||||
)}
|
||||
className="flex flex-col relative -top-6 justify-center gap-2 h-full w-full max-w-sm mx-auto"
|
||||
>
|
||||
<fieldset className="flex flex-col gap-2 text-sm">
|
||||
<h2 className="text-3xl font-medium text-gray-950 text-center mb-4">
|
||||
Jazz CoValue Inspector
|
||||
</h2>
|
||||
<input
|
||||
className="border p-4 rounded-lg min-w-[21rem] font-mono"
|
||||
placeholder="co_z1234567890abcdef123456789"
|
||||
value={coValueId}
|
||||
onChange={(e) =>
|
||||
setCoValueId(e.target.value as CoID<RawCoValue>)
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-indigo-500 hover:bg-indigo-500/80 text-white px-4 py-2 rounded-md"
|
||||
>
|
||||
Inspect
|
||||
</button>
|
||||
<hr />
|
||||
<button
|
||||
type="button"
|
||||
className="border inline-block px-2 py-1.5 text-black rounded"
|
||||
onClick={() => {
|
||||
setPage(currentAccount.id);
|
||||
}}
|
||||
>
|
||||
Inspect My Account
|
||||
</button>
|
||||
</fieldset>
|
||||
<h1 className="text-lg text-center font-medium mb-4 text-stone-900 dark:text-white">
|
||||
Jazz CoValue Inspector
|
||||
</h1>
|
||||
<Input
|
||||
label="CoValue ID"
|
||||
className="font-mono"
|
||||
hideLabel
|
||||
placeholder="co_z1234567890abcdef123456789"
|
||||
value={coValueId}
|
||||
onChange={(e) => setCoValueId(e.target.value as CoID<RawCoValue>)}
|
||||
/>
|
||||
<Button type="submit" variant="primary">
|
||||
Inspect CoValue
|
||||
</Button>
|
||||
|
||||
<p className="text-center">or</p>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setPage(currentAccount.id);
|
||||
}}
|
||||
>
|
||||
Inspect my account
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</PageStack>
|
||||
@@ -217,8 +220,10 @@ function AccountSwitcher({
|
||||
localNode: LocalNode | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex items-center gap-1">
|
||||
<select
|
||||
<div className="relative flex items-stretch gap-1">
|
||||
<Select
|
||||
label="Account to inspect"
|
||||
className="label:sr-only max-w-96"
|
||||
value={currentAccount?.id || "add-account"}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "add-account") {
|
||||
@@ -228,7 +233,6 @@ function AccountSwitcher({
|
||||
setCurrentAccount(account || null);
|
||||
}
|
||||
}}
|
||||
className="p-2 px-4 bg-gray-100/50 border border-indigo-500/10 backdrop-blur-sm rounded-md text-indigo-700 appearance-none"
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
@@ -240,15 +244,16 @@ function AccountSwitcher({
|
||||
</option>
|
||||
))}
|
||||
<option value="add-account">Add account</option>
|
||||
</select>
|
||||
</Select>
|
||||
{currentAccount && (
|
||||
<button
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={deleteCurrentAccount}
|
||||
className="p-3 rounded hover:bg-gray-200 transition-colors"
|
||||
title="Delete Account"
|
||||
className="rounded-md p-2 ml-1"
|
||||
aria-label="Remove account"
|
||||
>
|
||||
<Trash2 size={16} className="text-gray-500" />
|
||||
</button>
|
||||
<Icon name="delete" className="text-gray-500" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -272,30 +277,34 @@ function AddAccountForm({
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col gap-2 max-w-md mx-auto h-full justify-center"
|
||||
className="flex flex-col gap-3 max-w-md mx-auto h-full justify-center"
|
||||
>
|
||||
<h2 className="text-2xl font-medium text-gray-900 mb-3">
|
||||
Add an Account to Inspect
|
||||
<h2 className="text-2xl font-medium text-gray-900 dark:text-white">
|
||||
Add an account to inspect
|
||||
</h2>
|
||||
<input
|
||||
className="border py-2 px-3 rounded-md"
|
||||
placeholder="Account ID"
|
||||
<p className="leading-relaxed mb-5">
|
||||
Use the{" "}
|
||||
<code className="whitespace-nowrap text-stone-900 dark:text-white font-semibold">
|
||||
jazz-logged-in-secret
|
||||
</code>{" "}
|
||||
local storage key from within your Jazz app for your account
|
||||
credentials.
|
||||
</p>
|
||||
<Input
|
||||
label="Account ID"
|
||||
value={id}
|
||||
placeholder="co_z1234567890abcdef123456789"
|
||||
onChange={(e) => setId(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
label="Account secret"
|
||||
type="password"
|
||||
className="border py-2 px-3 rounded-md"
|
||||
placeholder="Account Secret"
|
||||
value={secret}
|
||||
onChange={(e) => setSecret(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-indigo-500 text-white px-4 py-2 rounded-md"
|
||||
>
|
||||
Add Account
|
||||
</button>
|
||||
<Button className="mt-3" type="submit">
|
||||
Add account
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import { Page } from "./page"; // Assuming you have a Page component
|
||||
|
||||
// Define the structure of a page in the path
|
||||
interface PageInfo {
|
||||
coId: CoID<RawCoValue>;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// Props for the PageStack component
|
||||
interface PageStackProps {
|
||||
path: PageInfo[];
|
||||
node?: LocalNode | null;
|
||||
goBack: () => void;
|
||||
addPages: (pages: PageInfo[]) => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PageStack({
|
||||
path,
|
||||
node,
|
||||
goBack,
|
||||
addPages,
|
||||
children,
|
||||
}: PageStackProps) {
|
||||
return (
|
||||
<div className="relative mt-4 h-[calc(100vh-6rem)]">
|
||||
{children && <div className="absolute inset-0 pb-20">{children}</div>}
|
||||
{node &&
|
||||
path.map((page, index) => (
|
||||
<Page
|
||||
key={`${page.coId}-${index}`}
|
||||
coId={page.coId}
|
||||
node={node}
|
||||
name={page.name || page.coId}
|
||||
onHeaderClick={goBack}
|
||||
onNavigate={addPages}
|
||||
isTopLevel={index === path.length - 1}
|
||||
style={{
|
||||
transform: `translateZ(${(index - path.length + 1) * 200}px) scale(${
|
||||
1 - (path.length - index - 1) * 0.05
|
||||
}) translateY(${-(index - path.length + 1) * -4}%)`,
|
||||
opacity: 1 - (path.length - index - 1) * 0.05,
|
||||
zIndex: index,
|
||||
transitionProperty: "transform, opacity",
|
||||
transitionDuration: "0.3s",
|
||||
transitionTimingFunction: "ease-out",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import { CoID, LocalNode, RawCoStream, RawCoValue } from "cojson";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CoStreamView } from "./co-stream-view";
|
||||
import { GridView } from "./grid-view";
|
||||
import { TableView } from "./table-viewer";
|
||||
import { TypeIcon } from "./type-icon";
|
||||
import { PageInfo } from "./types";
|
||||
import { useResolvedCoValue } from "./use-resolve-covalue";
|
||||
import { AccountOrGroupPreview } from "./value-renderer";
|
||||
|
||||
type PageProps = {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
name: string;
|
||||
onNavigate: (newPages: PageInfo[]) => void;
|
||||
onHeaderClick?: () => void;
|
||||
isTopLevel?: boolean;
|
||||
style: React.CSSProperties;
|
||||
};
|
||||
|
||||
export function Page({
|
||||
coId,
|
||||
node,
|
||||
name,
|
||||
onNavigate,
|
||||
onHeaderClick,
|
||||
style,
|
||||
isTopLevel,
|
||||
}: PageProps) {
|
||||
const { value, snapshot, type, extendedType } = useResolvedCoValue(
|
||||
coId,
|
||||
node,
|
||||
);
|
||||
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
|
||||
|
||||
const supportsTableView = type === "colist" || extendedType === "record";
|
||||
|
||||
// Automatically switch to table view if the page is a CoMap record
|
||||
useEffect(() => {
|
||||
if (supportsTableView) {
|
||||
setViewMode("table");
|
||||
}
|
||||
}, [supportsTableView]);
|
||||
|
||||
if (snapshot === "unavailable") {
|
||||
return <div style={style}>Data unavailable</div>;
|
||||
}
|
||||
|
||||
if (!snapshot) {
|
||||
return <div style={style}></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={clsx(
|
||||
"absolute inset-0 border border-gray-900/5 bg-clip-padding bg-white rounded-xl shadow-lg p-6 animate-in",
|
||||
)}
|
||||
>
|
||||
{!isTopLevel && (
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 h-10"
|
||||
aria-label="Back"
|
||||
onClick={() => {
|
||||
onHeaderClick?.();
|
||||
}}
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
)}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-2xl font-bold flex items-start flex-col gap-1">
|
||||
<span>
|
||||
{name}
|
||||
{typeof snapshot === "object" && "name" in snapshot ? (
|
||||
<span className="text-gray-600 font-medium">
|
||||
{" "}
|
||||
{
|
||||
(
|
||||
snapshot as {
|
||||
name: string;
|
||||
}
|
||||
).name
|
||||
}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
|
||||
{type && <TypeIcon type={type} extendedType={extendedType} />}
|
||||
</span>
|
||||
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
|
||||
{coId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* {supportsTableView && (
|
||||
<button
|
||||
onClick={toggleViewMode}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
{viewMode === "grid" ? "Table View" : "Grid View"}
|
||||
</button>
|
||||
)} */}
|
||||
</div>
|
||||
<div className="overflow-auto max-h-[calc(100%-4rem)]">
|
||||
{type === "costream" ? (
|
||||
<CoStreamView
|
||||
data={snapshot}
|
||||
onNavigate={onNavigate}
|
||||
node={node}
|
||||
value={value as RawCoStream}
|
||||
/>
|
||||
) : viewMode === "grid" ? (
|
||||
<GridView data={snapshot} onNavigate={onNavigate} node={node} />
|
||||
) : (
|
||||
<TableView data={snapshot} node={node} onNavigate={onNavigate} />
|
||||
)}
|
||||
{/* --- */}
|
||||
{extendedType !== "account" && extendedType !== "group" && (
|
||||
<div className="text-xs text-gray-500 mt-4">
|
||||
Owned by{" "}
|
||||
<AccountOrGroupPreview
|
||||
coId={value.group.id}
|
||||
node={node}
|
||||
showId
|
||||
onClick={() => {
|
||||
onNavigate([{ coId: value.group.id, name: "owner" }]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import { JsonObject } from "cojson";
|
||||
import { useMemo, useState } from "react";
|
||||
import { LinkIcon } from "../link-icon";
|
||||
import { PageInfo } from "./types";
|
||||
import { useResolvedCoValues } from "./use-resolve-covalue";
|
||||
import { ValueRenderer } from "./value-renderer";
|
||||
|
||||
export function TableView({
|
||||
data,
|
||||
node,
|
||||
onNavigate,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
node: LocalNode;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
}) {
|
||||
const [visibleRowsCount, setVisibleRowsCount] = useState(10);
|
||||
const [coIdArray, visibleRows] = useMemo(() => {
|
||||
const coIdArray = Array.isArray(data)
|
||||
? data
|
||||
: Object.values(data).every(
|
||||
(k) => typeof k === "string" && k.startsWith("co_"),
|
||||
)
|
||||
? Object.values(data).map((k) => k as CoID<RawCoValue>)
|
||||
: [];
|
||||
|
||||
const visibleRows = coIdArray.slice(0, visibleRowsCount);
|
||||
|
||||
return [coIdArray, visibleRows];
|
||||
}, [data, visibleRowsCount]);
|
||||
const resolvedRows = useResolvedCoValues(visibleRows, node);
|
||||
|
||||
const hasMore = visibleRowsCount < coIdArray.length;
|
||||
|
||||
if (!coIdArray.length) {
|
||||
return <div>No data to display</div>;
|
||||
}
|
||||
|
||||
if (resolvedRows.length === 0) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const keys = Array.from(
|
||||
new Set(resolvedRows.flatMap((item) => Object.keys(item.snapshot || {}))),
|
||||
);
|
||||
|
||||
const loadMore = () => {
|
||||
setVisibleRowsCount((prevVisibleRows) => prevVisibleRows + 10);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="sticky top-0 border-b">
|
||||
<tr>
|
||||
{["", ...keys].map((key) => (
|
||||
<th
|
||||
key={key}
|
||||
className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 rounded"
|
||||
>
|
||||
{key}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{resolvedRows.slice(0, visibleRowsCount).map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-1 py-0">
|
||||
<button
|
||||
onClick={() =>
|
||||
onNavigate([
|
||||
{
|
||||
coId: item.value!.id,
|
||||
name: index.toString(),
|
||||
},
|
||||
])
|
||||
}
|
||||
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 hover:text-blue-500 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<LinkIcon />
|
||||
</button>
|
||||
</td>
|
||||
{keys.map((key) => (
|
||||
<td
|
||||
key={key}
|
||||
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500"
|
||||
>
|
||||
<ValueRenderer
|
||||
json={(item.snapshot as JsonObject)[key]}
|
||||
onCoIDClick={(coId) => {
|
||||
async function handleClick() {
|
||||
onNavigate([
|
||||
{
|
||||
coId: item.value!.id,
|
||||
name: index.toString(),
|
||||
},
|
||||
{
|
||||
coId: coId,
|
||||
name: key,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
handleClick();
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="py-4 text-gray-500 flex items-center justify-between gap-2">
|
||||
<span>
|
||||
Showing {Math.min(visibleRowsCount, coIdArray.length)} of{" "}
|
||||
{coIdArray.length}
|
||||
</span>
|
||||
{hasMore && (
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={loadMore}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import {
|
||||
CoJsonType,
|
||||
ExtendedCoJsonType,
|
||||
useResolvedCoValue,
|
||||
} from "./use-resolve-covalue";
|
||||
|
||||
export const TypeIcon = ({
|
||||
type,
|
||||
extendedType,
|
||||
}: {
|
||||
type: CoJsonType;
|
||||
extendedType?: ExtendedCoJsonType;
|
||||
}) => {
|
||||
const iconMap: Record<ExtendedCoJsonType | CoJsonType, string> = {
|
||||
record: "{} Record",
|
||||
image: "🖼️ Image",
|
||||
comap: "{} CoMap",
|
||||
costream: "≋ CoStream",
|
||||
colist: "☰ CoList",
|
||||
account: "👤 Account",
|
||||
group: "👥 Group",
|
||||
};
|
||||
|
||||
const iconKey = extendedType || type;
|
||||
const icon = iconMap[iconKey as keyof typeof iconMap];
|
||||
|
||||
return icon ? <span className="font-mono">{icon}</span> : null;
|
||||
};
|
||||
|
||||
export const ResolveIcon = ({
|
||||
coId,
|
||||
node,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
}) => {
|
||||
const { type, extendedType, snapshot } = useResolvedCoValue(coId, node);
|
||||
|
||||
if (snapshot === "unavailable" && !type) {
|
||||
return <div className="text-gray-600 font-medium">Unavailable</div>;
|
||||
}
|
||||
|
||||
if (!type) return <div className="whitespace-pre w-14 font-mono"> </div>;
|
||||
|
||||
return <TypeIcon type={type} extendedType={extendedType} />;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { CoID, RawCoValue } from "cojson";
|
||||
|
||||
export type PageInfo = {
|
||||
coId: CoID<RawCoValue>;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export const isCoId = (coId: unknown): coId is CoID<RawCoValue> =>
|
||||
typeof coId === "string" && coId.startsWith("co_");
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CoID, RawCoValue } from "cojson";
|
||||
import { PageInfo } from "jazz-inspector";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { PageInfo } from "./types";
|
||||
|
||||
export function usePagePath(defaultPath?: PageInfo[]) {
|
||||
const [path, setPath] = useState<PageInfo[]>(() => {
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import { CoID, JsonValue, LocalNode, RawCoValue } from "cojson";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { LinkIcon } from "../link-icon";
|
||||
import {
|
||||
isBrowserImage,
|
||||
resolveCoValue,
|
||||
useResolvedCoValue,
|
||||
} from "./use-resolve-covalue";
|
||||
|
||||
// Is there a chance we can pass the actual CoValue here?
|
||||
export function ValueRenderer({
|
||||
json,
|
||||
compact,
|
||||
onCoIDClick,
|
||||
}: {
|
||||
json: JsonValue | undefined;
|
||||
compact?: boolean;
|
||||
onCoIDClick?: (childNode: CoID<RawCoValue>) => void;
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (typeof json === "undefined" || json === undefined) {
|
||||
return <span className="text-gray-400">undefined</span>;
|
||||
}
|
||||
|
||||
if (json === null) {
|
||||
return <span className="text-gray-400">null</span>;
|
||||
}
|
||||
|
||||
if (typeof json === "string" && json.startsWith("co_")) {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"inline-flex gap-1 items-center",
|
||||
onCoIDClick && "text-blue-500 cursor-pointer hover:underline",
|
||||
)}
|
||||
onClick={() => {
|
||||
onCoIDClick?.(json as CoID<RawCoValue>);
|
||||
}}
|
||||
>
|
||||
{json}
|
||||
{onCoIDClick && <LinkIcon />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "string") {
|
||||
return (
|
||||
<span className="text-green-900 font-mono">
|
||||
{/* <span className="select-none opacity-70">{'"'}</span> */}
|
||||
{json}
|
||||
{/* <span className="select-none opacity-70">{'"'}</span> */}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "number") {
|
||||
return <span className="text-purple-500">{json}</span>;
|
||||
}
|
||||
|
||||
if (typeof json === "boolean") {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
json
|
||||
? "text-green-700 bg-green-700/5"
|
||||
: "text-amber-700 bg-amber-500/5",
|
||||
"font-mono",
|
||||
"inline-block px-1 py-0.5 rounded",
|
||||
)}
|
||||
>
|
||||
{json.toString()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(json)) {
|
||||
return (
|
||||
<span title={JSON.stringify(json)}>
|
||||
Array <span className="text-gray-500">({json.length})</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "object") {
|
||||
return (
|
||||
<span
|
||||
title={JSON.stringify(json, null, 2)}
|
||||
className="inline-block max-w-64"
|
||||
>
|
||||
{compact ? (
|
||||
<span>
|
||||
Object{" "}
|
||||
<span className="text-gray-500">({Object.keys(json).length})</span>
|
||||
<pre className="mt-1 text-sm whitespace-pre-wrap">
|
||||
{isExpanded
|
||||
? JSON.stringify(json, null, 2)
|
||||
: JSON.stringify(json, null, 2)
|
||||
.split("\n")
|
||||
.slice(0, 3)
|
||||
.join("\n") + (Object.keys(json).length > 2 ? "\n..." : "")}
|
||||
</pre>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-xs text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{isExpanded ? "Show less" : "Show more"}
|
||||
</button>
|
||||
</span>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{JSON.stringify(json, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{String(json)}</span>;
|
||||
}
|
||||
|
||||
export const CoMapPreview = ({
|
||||
coId,
|
||||
node,
|
||||
limit = 6,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const { value, snapshot, type, extendedType } = useResolvedCoValue(
|
||||
coId,
|
||||
node,
|
||||
);
|
||||
|
||||
if (!snapshot) {
|
||||
return (
|
||||
<div className="rounded bg-gray-100 animate-pulse whitespace-pre w-24">
|
||||
{" "}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot === "unavailable" && !value) {
|
||||
return <div className="text-gray-500">Unavailable</div>;
|
||||
}
|
||||
|
||||
if (extendedType === "image" && isBrowserImage(snapshot)) {
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
src={snapshot.placeholderDataURL}
|
||||
className="size-8 border-2 border-white drop-shadow-md my-2"
|
||||
/>
|
||||
<span className="text-gray-500 text-sm">
|
||||
{snapshot.originalSize[0]} x {snapshot.originalSize[1]}
|
||||
</span>
|
||||
|
||||
{/* <CoMapPreview coId={value[]} node={node} /> */}
|
||||
{/* <ProgressiveImg image={value}>
|
||||
{({ src }) => <img src={src} className={clsx("w-full")} />}
|
||||
</ProgressiveImg> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (extendedType === "record") {
|
||||
return (
|
||||
<div>
|
||||
Record{" "}
|
||||
<span className="text-gray-500">({Object.keys(snapshot).length})</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "colist") {
|
||||
return (
|
||||
<div>
|
||||
List{" "}
|
||||
<span className="text-gray-500">
|
||||
({(snapshot as unknown as []).length})
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-sm flex flex-col gap-2 items-start">
|
||||
<div className="grid grid-cols-[auto_1fr] gap-2">
|
||||
{Object.entries(snapshot)
|
||||
.slice(0, limit)
|
||||
.map(([key, value]) => (
|
||||
<React.Fragment key={key}>
|
||||
<span className="font-medium">{key}: </span>
|
||||
<span>
|
||||
<ValueRenderer json={value} />
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
{Object.entries(snapshot).length > limit && (
|
||||
<div className="text-left text-xs text-gray-500 mt-2">
|
||||
{Object.entries(snapshot).length - limit} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function AccountOrGroupPreview({
|
||||
coId,
|
||||
node,
|
||||
showId = false,
|
||||
onClick,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
showId?: boolean;
|
||||
onClick?: (name?: string) => void;
|
||||
}) {
|
||||
const { snapshot, extendedType } = useResolvedCoValue(coId, node);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (extendedType === "account") {
|
||||
resolveCoValue(
|
||||
(snapshot as unknown as { profile: CoID<RawCoValue> }).profile,
|
||||
node,
|
||||
).then(({ snapshot }) => {
|
||||
if (
|
||||
typeof snapshot === "object" &&
|
||||
"name" in snapshot &&
|
||||
typeof snapshot.name === "string"
|
||||
) {
|
||||
setName(snapshot.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [snapshot, node, extendedType]);
|
||||
|
||||
if (!snapshot) return <span>Loading...</span>;
|
||||
if (extendedType !== "account" && extendedType !== "group") {
|
||||
return <span>CoID is not an account or group</span>;
|
||||
}
|
||||
|
||||
const displayName = extendedType === "account" ? name || "Account" : "Group";
|
||||
const displayText = showId ? `${displayName} (${coId})` : displayName;
|
||||
|
||||
const props = onClick
|
||||
? {
|
||||
onClick: () => onClick(displayName),
|
||||
className: "text-blue-500 cursor-pointer hover:underline",
|
||||
}
|
||||
: {
|
||||
className: "text-gray-500",
|
||||
};
|
||||
|
||||
return <span {...props}>{displayText}</span>;
|
||||
}
|
||||
@@ -1,5 +1,30 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import animate from "tailwindcss-animate";
|
||||
import plugin from "tailwindcss/plugin";
|
||||
|
||||
const stonePalette = {
|
||||
50: "oklch(0.988281 0.002 75)",
|
||||
100: "oklch(0.980563 0.002 75)",
|
||||
200: "oklch(0.917969 0.002 75)",
|
||||
300: "oklch(0.853516 0.002 75)",
|
||||
400: "oklch(0.789063 0.002 75)",
|
||||
500: "oklch(0.726563 0.002 75)",
|
||||
600: "oklch(0.613281 0.002 75)",
|
||||
700: "oklch(0.523438 0.002 75)",
|
||||
800: "oklch(0.412109 0.002 75)",
|
||||
900: "oklch(0.302734 0.002 75)",
|
||||
925: "oklch(0.220000 0.002 75)",
|
||||
950: "oklch(0.193359 0.002 75)",
|
||||
};
|
||||
|
||||
const stonePaletteWithAlpha = { ...stonePalette };
|
||||
|
||||
Object.keys(stonePalette).forEach((key) => {
|
||||
// @ts-ignore
|
||||
stonePaletteWithAlpha[key] = stonePaletteWithAlpha[key].replace(
|
||||
")",
|
||||
"/ <alpha-value>)",
|
||||
);
|
||||
});
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
@@ -18,62 +43,26 @@ const config: Config = {
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
stone: stonePaletteWithAlpha,
|
||||
gray: stonePaletteWithAlpha,
|
||||
blue: {
|
||||
50: "#f5f7ff",
|
||||
100: "#ebf0fe",
|
||||
200: "#d6e0fd",
|
||||
300: "#b3c7fc",
|
||||
400: "#8aa6f9",
|
||||
500: "#5870F1",
|
||||
600: "#3651E7",
|
||||
700: "#3313F7",
|
||||
800: "#2A12BE",
|
||||
900: "#12046A",
|
||||
950: "#1e1b4b",
|
||||
DEFAULT: "#3313F7",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [animate],
|
||||
plugins: [plugin(({ addVariant }) => addVariant("label", "& :is(label)"))],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# multiauth
|
||||
|
||||
## 0.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-react-auth-clerk@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -13,11 +13,11 @@ To run this example, you may either:
|
||||
|
||||
1. Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example counter --project-name counter
|
||||
npx create-jazz-app@latest counter-app --example counter
|
||||
```
|
||||
2. Navigate to the new project and start the development server.
|
||||
```bash
|
||||
cd counter
|
||||
cd counter-app
|
||||
npm run dev
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "multiauth",
|
||||
"private": true,
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -19,7 +19,6 @@ createRoot(document.getElementById("root")!).render(
|
||||
<OmniAuth
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp", // This makes the app work in local mode when the user is not authenticated
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-example-musicplayer
|
||||
|
||||
## 0.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.11.5
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 0.0.84
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example music-player --project-name music-player
|
||||
npx create-jazz-app@latest music-player-app --example music-player
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd music-player
|
||||
cd music-player-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.84",
|
||||
"version": "0.0.85",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -22,8 +22,8 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-inspector": "workspace:*",
|
||||
"jazz-react": "workspace:0.11.4",
|
||||
"jazz-tools": "workspace:0.11.4",
|
||||
"jazz-react": "workspace:0.11.5",
|
||||
"jazz-tools": "workspace:0.11.5",
|
||||
"lucide-react": "^0.274.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -72,7 +72,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer,
|
||||
when: "signedUp", // This makes the app work in local mode when the user is anonymous
|
||||
}}
|
||||
storage="indexedDB"
|
||||
AccountSchema={MusicaAccount}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# organization
|
||||
|
||||
## 0.0.57
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 0.0.56
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -16,12 +16,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example organization --project-name organization
|
||||
npx create-jazz-app@latest organization-app --example organization
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd organization
|
||||
cd organization-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "organization",
|
||||
"private": true,
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.57",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.11.5
|
||||
|
||||
## 0.0.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -21,12 +21,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example passkey-svelte --project-name passkey-svelte
|
||||
npx create-jazz-app@latest passkey-svelte-app --example passkey-svelte
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd passkey-svelte
|
||||
cd passkey-svelte-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "passkey-svelte",
|
||||
"version": "0.0.50",
|
||||
"version": "0.0.51",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key={apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="minimal-svelte-auth-passkey">
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# minimal-auth-passkey
|
||||
|
||||
## 0.0.62
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 0.0.61
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -15,12 +15,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example passkey --project-name passkey
|
||||
npx create-jazz-app@latest passkey-app --example passkey
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd passkey
|
||||
cd passkey-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passkey",
|
||||
"private": true,
|
||||
"version": "0.0.61",
|
||||
"version": "0.0.62",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -10,7 +10,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="Jazz Minimal Auth Passkey Example">
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# passphrase
|
||||
|
||||
## 0.0.59
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 0.0.58
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -12,12 +12,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example passphrase --project-name passphrase
|
||||
npx create-jazz-app@latest passphrase-app --example passphrase
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd passphrase
|
||||
cd passphrase-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passphrase",
|
||||
"private": true,
|
||||
"version": "0.0.58",
|
||||
"version": "0.0.59",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -142,7 +142,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: "wss://cloud.jazz.tools/?key=minimal-auth-passphrase-example@garden.co",
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PassphraseAuthBasicUI
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# jazz-password-manager
|
||||
|
||||
## 0.0.83
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 0.0.82
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -15,12 +15,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example password-manager --project-name password-manager
|
||||
npx create-jazz-app@latest password-manager-app --example password-manager
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd password-manager
|
||||
cd password-manager-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-password-manager",
|
||||
"private": true,
|
||||
"version": "0.0.82",
|
||||
"version": "0.0.83",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,8 +12,8 @@
|
||||
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
|
||||
},
|
||||
"dependencies": {
|
||||
"jazz-react": "workspace:0.11.4",
|
||||
"jazz-tools": "workspace:0.11.4",
|
||||
"jazz-react": "workspace:0.11.5",
|
||||
"jazz-tools": "workspace:0.11.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.41.5",
|
||||
|
||||
@@ -12,7 +12,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
AccountSchema={PasswordManagerAccount}
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="Jazz Password Manager">
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 0.0.181
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
- jazz-browser-media-images@0.11.5
|
||||
|
||||
## 0.0.180
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example pets --project-name pets
|
||||
npx create-jazz-app@latest pets-app --example pets
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd pets
|
||||
cd pets-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.180",
|
||||
"version": "0.0.181",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -19,9 +19,9 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-browser-media-images": "workspace:0.11.4",
|
||||
"jazz-react": "workspace:0.11.4",
|
||||
"jazz-tools": "workspace:0.11.4",
|
||||
"jazz-browser-media-images": "workspace:0.11.5",
|
||||
"jazz-react": "workspace:0.11.5",
|
||||
"jazz-tools": "workspace:0.11.5",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"is-ci": "^3.0.1",
|
||||
"jazz-run": "workspace:0.11.4",
|
||||
"jazz-run": "workspace:0.11.5",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.2",
|
||||
|
||||
@@ -52,7 +52,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer,
|
||||
when: "signedUp",
|
||||
}}
|
||||
AccountSchema={PetAccount}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# reactions
|
||||
|
||||
## 0.0.61
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
- jazz-browser-media-images@0.11.5
|
||||
|
||||
## 0.0.60
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example reactions --project-name reactions
|
||||
npx create-jazz-app@latest reactions-app --example reactions
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd reactions
|
||||
cd reactions-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "reactions",
|
||||
"private": true,
|
||||
"version": "0.0.60",
|
||||
"version": "0.0.61",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -10,7 +10,6 @@ createRoot(document.getElementById("root")!).render(
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="Jazz Reactions Example">
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# todo-vue
|
||||
|
||||
## 0.0.65
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
- jazz-vue@0.11.5
|
||||
|
||||
## 0.0.64
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -11,12 +11,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example todo-vue --project-name todo-vue
|
||||
npx create-jazz-app@latest todo-vue-app --example todo-vue
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd todo-vue
|
||||
cd todo-vue-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "todo-vue",
|
||||
"version": "0.0.64",
|
||||
"version": "0.0.65",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 0.0.180
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 0.0.179
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example todo --project-name todo
|
||||
npx create-jazz-app@latest todo-app --example todo
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd todo
|
||||
cd todo-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.179",
|
||||
"version": "0.0.180",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,8 +16,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.11.4",
|
||||
"jazz-tools": "workspace:0.11.4",
|
||||
"jazz-react": "workspace:0.11.5",
|
||||
"jazz-tools": "workspace:0.11.5",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -42,7 +42,6 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
AccountSchema={TodoAccount}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# version-history
|
||||
|
||||
## 0.0.58
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.11.5
|
||||
- jazz-react@0.11.5
|
||||
- jazz-tools@0.11.5
|
||||
|
||||
## 0.0.57
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -13,12 +13,12 @@ You can either
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest --example version-history --project-name version-history
|
||||
npx create-jazz-app@latest version-history-app --example version-history
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd version-history
|
||||
cd version-history-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "version-history",
|
||||
"private": true,
|
||||
"version": "0.0.57",
|
||||
"version": "0.0.58",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -11,7 +11,6 @@ createRoot(document.getElementById("root")!).render(
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
when: "signedUp",
|
||||
}}
|
||||
>
|
||||
<DemoAuthBasicUI appName="Jazz Version History Example">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TocProvider } from "@/components/TocProvider";
|
||||
import { ApiNav } from "@/components/docs/ApiNav";
|
||||
import DocsLayout from "@/components/docs/DocsLayout";
|
||||
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
|
||||
@@ -8,8 +9,10 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<DocsLayout nav={<ApiNav />} navIcon="package" navName="API Ref">
|
||||
<Prose className="overflow-x-hidden lg:flex-1 py-10">{children}</Prose>
|
||||
</DocsLayout>
|
||||
<TocProvider>
|
||||
<DocsLayout nav={<ApiNav />} navIcon="package" navName="API Ref">
|
||||
<Prose className="overflow-x-hidden lg:flex-1 py-10">{children}</Prose>
|
||||
</DocsLayout>
|
||||
</TocProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import DocsLayout from "@/components/docs/DocsLayout";
|
||||
import { TocItemsSetter } from "@/components/docs/TocItemsSetter";
|
||||
import ComingSoonPage from "@/components/docs/coming-soon.mdx";
|
||||
import { DocNav } from "@/components/docs/nav";
|
||||
import { docNavigationItems } from "@/lib/docNavigationItems.js";
|
||||
import { Framework, frameworks } from "@/lib/framework";
|
||||
import type { Toc } from "@stefanprobst/rehype-extract-toc";
|
||||
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
|
||||
|
||||
async function getMdxSource(slugPath: string, framework: string) {
|
||||
// Try to import the framework-specific file first
|
||||
@@ -46,7 +44,6 @@ export default async function Page({
|
||||
}: { params: Promise<{ slug: string[]; framework: string }> }) {
|
||||
const { slug, framework } = await params;
|
||||
const slugPath = slug.join("/");
|
||||
const bodyClassName = "overflow-x-hidden lg:flex-1 py-10 max-w-3xl mx-auto";
|
||||
|
||||
try {
|
||||
const mdxSource = await getMdxSource(slugPath, framework);
|
||||
@@ -56,19 +53,17 @@ export default async function Page({
|
||||
const tocItems = (tableOfContents as Toc)?.[0]?.children;
|
||||
|
||||
return (
|
||||
<DocsLayout toc={tocItems} nav={<DocNav />}>
|
||||
<Prose className={bodyClassName}>
|
||||
<Content />
|
||||
</Prose>
|
||||
</DocsLayout>
|
||||
<>
|
||||
<TocItemsSetter items={tocItems} />
|
||||
<Content />
|
||||
</>
|
||||
);
|
||||
} catch (error) {
|
||||
return (
|
||||
<DocsLayout nav={<DocNav />}>
|
||||
<Prose className={bodyClassName}>
|
||||
<ComingSoonPage />
|
||||
</Prose>
|
||||
</DocsLayout>
|
||||
<>
|
||||
<TocItemsSetter items={[]} />
|
||||
<ComingSoonPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
export const metadata = { title: "FileStreams" };
|
||||
|
||||
# FileStreams
|
||||
|
||||
FileStreams handle binary data in Jazz applications - think documents, audio files, and other non-text content. They're essentially collaborative versions of `Blob`s that sync automatically across devices.
|
||||
|
||||
Use FileStreams when you need to:
|
||||
- Distribute documents across devices
|
||||
- Store audio or video files
|
||||
- Sync any binary data between users
|
||||
|
||||
**Note:** For images specifically, Jazz provides the higher-level `ImageDefinition` abstraction which manages multiple image resolutions - see the [ImageDefinition documentation](/docs/using-covalues/imagedef) for details.
|
||||
|
||||
FileStreams provide automatic chunking when using the `createFromBlob` method, track upload progress, and handle MIME types and metadata.
|
||||
|
||||
In your schema, reference FileStreams like any other CoValue:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { CoMap, FileStream, co } from "jazz-tools";
|
||||
|
||||
class Document extends CoMap {
|
||||
title = co.string;
|
||||
file = co.ref(FileStream); // Store a document file
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Creating FileStreams
|
||||
|
||||
There are two main ways to create FileStreams: creating empty ones for manual data population or creating directly from existing files or blobs.
|
||||
|
||||
### Creating from Blobs and Files
|
||||
|
||||
For files from input elements or drag-and-drop interfaces, use `createFromBlob`:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// From a file input
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
fileInput.addEventListener('change', async () => {
|
||||
const file = fileInput.files[0];
|
||||
if (file) {
|
||||
// Create FileStream from user-selected file
|
||||
const fileStream = await FileStream.createFromBlob(file);
|
||||
|
||||
// Or with progress tracking for better UX
|
||||
const fileWithProgress = await FileStream.createFromBlob(file, {
|
||||
onProgress: (progress) => {
|
||||
// progress is a value between 0 and 1
|
||||
const percent = Math.round(progress * 100);
|
||||
console.log(`Upload progress: ${percent}%`);
|
||||
progressBar.style.width = `${percent}%`;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Creating Empty FileStreams
|
||||
|
||||
Create an empty FileStream when you want to manually [add binary data in chunks](/docs/using-covalues/filestreams#writing-to-filestreams):
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { FileStream } from "jazz-tools";
|
||||
|
||||
// Create a new empty FileStream
|
||||
const fileStream = FileStream.create();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Reading from FileStreams
|
||||
|
||||
`FileStream`s provide several ways to access their binary content, from raw chunks to convenient Blob objects.
|
||||
|
||||
### Getting Raw Data Chunks
|
||||
|
||||
To access the raw binary data and metadata:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Get all chunks and metadata
|
||||
const fileData = fileStream.getChunks();
|
||||
|
||||
if (fileData) {
|
||||
console.log(`MIME type: ${fileData.mimeType}`);
|
||||
console.log(`Total size: ${fileData.totalSizeBytes} bytes`);
|
||||
console.log(`File name: ${fileData.fileName}`);
|
||||
console.log(`Is complete: ${fileData.finished}`);
|
||||
|
||||
// Access raw binary chunks
|
||||
for (const chunk of fileData.chunks) {
|
||||
// Each chunk is a Uint8Array
|
||||
console.log(`Chunk size: ${chunk.length} bytes`);
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
By default, `getChunks()` only returns data for completely synced `FileStream`s. To start using chunks from a `FileStream` that's currently still being synced use the `allowUnfinished` option:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Get data even if the stream isn't complete
|
||||
const partialData = fileStream.getChunks({ allowUnfinished: true });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Converting to Blobs
|
||||
|
||||
For easier integration with web APIs, convert to a `Blob`:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Convert to a Blob
|
||||
const blob = fileStream.toBlob();
|
||||
|
||||
if (blob) {
|
||||
// Use with URL.createObjectURL
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create a download link
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileData?.fileName || 'document.pdf';
|
||||
link.click();
|
||||
|
||||
// Clean up when done
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Loading FileStreams as Blobs
|
||||
|
||||
You can directly load a `FileStream` as a `Blob` when you only have its ID:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Load directly as a Blob when you have an ID
|
||||
const blob = await FileStream.loadAsBlob(fileStreamId);
|
||||
|
||||
// By default, waits for complete uploads
|
||||
// For in-progress uploads:
|
||||
const partialBlob = await FileStream.loadAsBlob(fileStreamId, {
|
||||
allowUnfinished: true
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Checking Completion Status
|
||||
|
||||
Check if a `FileStream` is fully synced:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
if (fileStream.isBinaryStreamEnded()) {
|
||||
console.log('File is completely synced');
|
||||
} else {
|
||||
console.log('File upload is still in progress');
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Writing to FileStreams
|
||||
|
||||
When creating a `FileStream` manually (not using `createFromBlob`), you need to manage the upload process yourself. This gives you more control over chunking and progress tracking.
|
||||
|
||||
### The Upload Lifecycle
|
||||
|
||||
`FileStream` uploads follow a three-stage process:
|
||||
|
||||
1. **Start** - Initialize with metadata
|
||||
2. **Push** - Send one or more chunks of data
|
||||
3. **End** - Mark the stream as complete
|
||||
|
||||
### Starting a `FileStream`
|
||||
|
||||
Begin by providing metadata about the file:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Create an empty FileStream
|
||||
const fileStream = FileStream.create();
|
||||
|
||||
// Initialize with metadata
|
||||
fileStream.start({
|
||||
mimeType: 'application/pdf', // MIME type (required)
|
||||
totalSizeBytes: 1024 * 1024 * 2, // Size in bytes (if known)
|
||||
fileName: 'document.pdf' // Original filename (optional)
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Pushing Data
|
||||
|
||||
Add binary data in chunks - this helps with large files and progress tracking:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Create a sample Uint8Array (in real apps, this would be file data)
|
||||
const data = new Uint8Array([...]);
|
||||
|
||||
// For large files, break into chunks (e.g., 100KB each)
|
||||
const chunkSize = 1024 * 100;
|
||||
for (let i = 0; i < data.length; i += chunkSize) {
|
||||
// Create a slice of the data
|
||||
const chunk = data.slice(i, i + chunkSize);
|
||||
|
||||
// Push chunk to the FileStream
|
||||
fileStream.push(chunk);
|
||||
|
||||
// Track progress
|
||||
const progress = Math.min(100, Math.round((i + chunk.length) * 100 / data.length));
|
||||
console.log(`Upload progress: ${progress}%`);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Completing the Upload
|
||||
|
||||
Once all chunks are pushed, mark the `FileStream` as complete:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Finalize the upload
|
||||
fileStream.end();
|
||||
|
||||
console.log('Upload complete!');
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Subscribing to `FileStream`s
|
||||
|
||||
Like other CoValues, you can subscribe to `FileStream`s to get notified of changes as they happen. This is especially useful for tracking upload progress when someone else is uploading a file.
|
||||
|
||||
### Loading by ID
|
||||
|
||||
Load a `FileStream` when you have its ID:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Load a FileStream by ID
|
||||
const fileStream = await FileStream.load(fileStreamId, []);
|
||||
|
||||
if (fileStream) {
|
||||
console.log('FileStream loaded successfully');
|
||||
|
||||
// Check if it's complete
|
||||
if (fileStream.isBinaryStreamEnded()) {
|
||||
// Process the completed file
|
||||
const blob = fileStream.toBlob();
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Subscribing to Changes
|
||||
|
||||
Subscribe to a `FileStream` to be notified when chunks are added or when the upload is complete:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Subscribe to a FileStream by ID
|
||||
const unsubscribe = FileStream.subscribe(fileStreamId, [], (fileStream) => {
|
||||
// Called whenever the FileStream changes
|
||||
console.log('FileStream updated');
|
||||
|
||||
// Get current status
|
||||
const chunks = fileStream.getChunks({ allowUnfinished: true });
|
||||
if (chunks) {
|
||||
const uploadedBytes = chunks.chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const totalBytes = chunks.totalSizeBytes || 1;
|
||||
const progress = Math.min(100, Math.round(uploadedBytes * 100 / totalBytes));
|
||||
|
||||
console.log(`Upload progress: ${progress}%`);
|
||||
|
||||
if (fileStream.isBinaryStreamEnded()) {
|
||||
console.log('Upload complete!');
|
||||
// Now safe to use the file
|
||||
const blob = fileStream.toBlob();
|
||||
|
||||
// Clean up the subscription if we're done
|
||||
unsubscribe();
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Waiting for Upload Completion
|
||||
|
||||
If you need to wait for a `FileStream` to be fully synchronized across devices:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Wait for the FileStream to be fully synced
|
||||
await fileStream.waitForSync({
|
||||
timeout: 5000 // Optional timeout in ms
|
||||
});
|
||||
|
||||
console.log('FileStream is now synced to all connected devices');
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
This is useful when you need to ensure that a file is available to other users before proceeding with an operation.
|
||||
@@ -0,0 +1,213 @@
|
||||
import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
export const metadata = { title: "ImageDefinition" };
|
||||
|
||||
# ImageDefinition
|
||||
|
||||
`ImageDefinition` is a specialized CoValue designed specifically for managing images in Jazz applications. It extends beyond basic file storage by supporting multiple resolutions of the same image and progressive loading patterns.
|
||||
|
||||
We also offer [`createImage()`](#creating-images), a higher-level function to create an `ImageDefinition` from a file.
|
||||
|
||||
If you're building with React, we recommend starting with our [React-specific image documentation](/docs/react/using-covalues/imagedef) which covers higher-level components and hooks for working with images.
|
||||
|
||||
The [Image Upload example](https://github.com/gardencmp/jazz/tree/main/examples/image-upload) demonstrates use of `ImageDefinition`.
|
||||
|
||||
## Creating Images
|
||||
|
||||
The easiest way to create and use images in your Jazz application is with the `createImage()` function:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { createImage } from "jazz-browser-media-images";
|
||||
|
||||
// Create an image from a file input
|
||||
async function handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
// Creates ImageDefinition with multiple resolutions automatically
|
||||
const image = await createImage(file, {
|
||||
owner: me.profile._owner,
|
||||
});
|
||||
|
||||
// Store the image in your application data
|
||||
me.profile.image = image;
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
> Note: `createImage()` requires a browser environment as it uses browser APIs to process images.
|
||||
|
||||
The `createImage()` function:
|
||||
- Creates an `ImageDefinition` with the right properties
|
||||
- Generates a small placeholder for immediate display
|
||||
- Creates multiple resolution variants of your image
|
||||
- Returns the ID of the created `ImageDefinition`
|
||||
|
||||
### Configuration Options
|
||||
|
||||
You can configure `createImage()` with additional options:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
// Configuration options
|
||||
const options = {
|
||||
owner: me, // Owner for access control
|
||||
maxSize: 1024 // Maximum resolution to generate
|
||||
};
|
||||
|
||||
// Setting maxSize controls which resolutions are generated:
|
||||
// 256: Only creates the smallest resolution (256px on longest side)
|
||||
// 1024: Creates 256px and 1024px resolutions
|
||||
// 2048: Creates 256px, 1024px, and 2048px resolutions
|
||||
// undefined: Creates all resolutions including the original size
|
||||
|
||||
const image = await createImage(file, options);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Creating ImageDefinitions
|
||||
|
||||
Create an `ImageDefinition` by specifying the original dimensions and an optional placeholder:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { ImageDefinition } from "jazz-tools";
|
||||
|
||||
// Create with original dimensions
|
||||
const image = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
});
|
||||
|
||||
// With a placeholder for immediate display
|
||||
const imageWithPlaceholder = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
placeholderDataURL: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAP...",
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Structure
|
||||
|
||||
`ImageDefinition` stores:
|
||||
- The original image dimensions (`originalSize`)
|
||||
- An optional placeholder (`placeholderDataURL`, typically a tiny base64-encoded preview)
|
||||
- Multiple resolution variants of the same image as [`FileStream`s](./using-covalues/filestream)
|
||||
|
||||
Each resolution is stored with a key in the format `"widthxheight"` (e.g., `"1920x1080"`, `"800x450"`).
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { CoMap, CoList, ImageDefinition, co } from "jazz-tools";
|
||||
|
||||
class ListOfImages extends CoList.Of(co.ref(ImageDefinition)) {}
|
||||
|
||||
class Gallery extends CoMap {
|
||||
title = co.string;
|
||||
images = co.ref(ListOfImages);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Adding Image Resolutions
|
||||
|
||||
Add multiple resolutions to an `ImageDefinition` by creating `FileStream`s for each size:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Create FileStreams for different resolutions
|
||||
const fullRes = await FileStream.createFromBlob(fullSizeBlob);
|
||||
const mediumRes = await FileStream.createFromBlob(mediumSizeBlob);
|
||||
const thumbnailRes = await FileStream.createFromBlob(thumbnailBlob);
|
||||
|
||||
// Add to the ImageDefinition with appropriate resolution keys
|
||||
image["1920x1080"] = fullRes;
|
||||
image["800x450"] = mediumRes;
|
||||
image["320x180"] = thumbnailRes;
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Retrieving Images
|
||||
|
||||
The `highestResAvailable` method helps select the best image resolution for the current context:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Get the highest resolution available
|
||||
const highestRes = image.highestResAvailable();
|
||||
if (highestRes) {
|
||||
console.log(`Found resolution: ${highestRes.res}`);
|
||||
|
||||
// Convert to a usable blob
|
||||
const blob = highestRes.stream.toBlob();
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
imageElement.src = url;
|
||||
|
||||
// Remember to revoke the URL when no longer needed
|
||||
imageElement.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Or constrain by maximum width
|
||||
const maxWidth = window.innerWidth;
|
||||
const appropriateRes = image.highestResAvailable({ maxWidth });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Fallback Behavior
|
||||
|
||||
`highestResAvailable` returns the largest resolution that fits your constraints. If a resolution has incomplete data, it falls back to the next available lower resolution.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const image = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
});
|
||||
|
||||
image["1920x1080"] = FileStream.create(); // Empty image upload
|
||||
image["800x450"] = await FileStream.createFromBlob(mediumSizeBlob);
|
||||
|
||||
const highestRes = image.highestResAvailable();
|
||||
console.log(highestRes.res); // 800x450
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Progressive Loading Patterns
|
||||
|
||||
`ImageDefinition` supports simple progressive loading with placeholders and resolution selection:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Start with placeholder for immediate display
|
||||
if (image.placeholderDataURL) {
|
||||
imageElement.src = image.placeholderDataURL;
|
||||
}
|
||||
|
||||
// Then load the best resolution for the current display
|
||||
const screenWidth = window.innerWidth;
|
||||
const bestRes = image.highestResAvailable({ maxWidth: screenWidth });
|
||||
|
||||
if (bestRes) {
|
||||
const blob = bestRes.stream.toBlob();
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
imageElement.src = url;
|
||||
|
||||
// Remember to revoke the URL when no longer needed
|
||||
imageElement.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
## Best Practices
|
||||
|
||||
- **Generate resolutions server-side** when possible for optimal quality
|
||||
- **Use placeholders** (like LQIP - Low Quality Image Placeholders) for instant rendering
|
||||
- **Prioritize loading** the resolution appropriate for the current viewport
|
||||
- **Consider device pixel ratio** (window.devicePixelRatio) for high-DPI displays
|
||||
- **Always call URL.revokeObjectURL** after the image loads to prevent memory leaks
|
||||
@@ -0,0 +1,233 @@
|
||||
import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
export const metadata = { title: "ImageDefinition" };
|
||||
|
||||
# ImageDefinition
|
||||
|
||||
`ImageDefinition` is a specialized CoValue designed specifically for managing images in Jazz. It extends beyond basic file storage by supporting multiple resolutions of the same image, optimized for mobile devices.
|
||||
|
||||
Jazz offers several tools to work with images in React Native:
|
||||
- [`createImage()`](#creating-images) - function to create an `ImageDefinition` from a base64 image data URI
|
||||
- [`ProgressiveImg`](#displaying-images-with-progressiveimg) - React component to display an image with progressive loading
|
||||
- [`useProgressiveImg`](#using-useprogressiveimg-hook) - React hook to load an image in your own component
|
||||
|
||||
For an example of use, see our [React Native Clerk Chat example](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-clerk).
|
||||
|
||||
## Creating Images
|
||||
|
||||
The easiest way to create and use images in your Jazz application is with the `createImage()` function:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
import { createImage } from "jazz-react-native-media-images";
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
|
||||
async function handleImagePicker() {
|
||||
try {
|
||||
// Launch the image picker
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
base64: true,
|
||||
quality: 1,
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
const base64Uri = `data:image/jpeg;base64,${result.assets[0].base64}`;
|
||||
|
||||
// Creates ImageDefinition with multiple resolutions automatically
|
||||
const image = await createImage(base64Uri, {
|
||||
owner: me.profile._owner,
|
||||
maxSize: 2048, // Optional: limit maximum resolution
|
||||
});
|
||||
|
||||
// Store the image
|
||||
me.profile.image = image;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating image:", error);
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The `createImage()` function:
|
||||
- Creates an `ImageDefinition` with the right properties
|
||||
- Generates a small placeholder for immediate display
|
||||
- Creates multiple resolution variants of your image
|
||||
- Returns the created `ImageDefinition`
|
||||
|
||||
### Configuration Options
|
||||
|
||||
You can configure `createImage()` with additional options:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
// Configuration options
|
||||
const options = {
|
||||
owner: me, // Owner for access control
|
||||
maxSize: 1024 // Maximum resolution to generate
|
||||
};
|
||||
|
||||
// Setting maxSize controls which resolutions are generated:
|
||||
// 256: Only creates the smallest resolution (256px on longest side)
|
||||
// 1024: Creates 256px and 1024px resolutions
|
||||
// 2048: Creates 256px, 1024px, and 2048px resolutions
|
||||
// undefined: Creates all resolutions including the original size
|
||||
|
||||
const image = await createImage(base64Uri, options);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Displaying Images with `ProgressiveImg`
|
||||
|
||||
For a complete progressive loading experience, use the `ProgressiveImg` component:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
import { ProgressiveImg } from "jazz-react-native";
|
||||
import { Image, StyleSheet } from "react-native";
|
||||
|
||||
function GalleryView({ image }) {
|
||||
return (
|
||||
<ProgressiveImg
|
||||
image={image} // The image definition to load
|
||||
maxWidth={800} // Limit to resolutions up to 800px wide
|
||||
>
|
||||
{({ src }) => (
|
||||
<Image
|
||||
source={{ uri: src }}
|
||||
style={styles.galleryImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
)}
|
||||
</ProgressiveImg>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
galleryImage: {
|
||||
width: '100%',
|
||||
height: 200,
|
||||
borderRadius: 8,
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The `ProgressiveImg` component handles:
|
||||
- Showing a placeholder while loading
|
||||
- Automatically selecting the appropriate resolution
|
||||
- Progressive enhancement as higher resolutions become available
|
||||
- Cleaning up resources when unmounted
|
||||
|
||||
## Using `useProgressiveImg` Hook
|
||||
|
||||
For more control over image loading, you can implement your own progressive image component:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
import { useProgressiveImg } from "jazz-react-native";
|
||||
import { Image, View, Text, ActivityIndicator } from "react-native";
|
||||
|
||||
function CustomImageComponent({ image }) {
|
||||
const {
|
||||
src, // Data URI containing the image data as a base64 string,
|
||||
// or a placeholder image URI
|
||||
res, // The current resolution
|
||||
originalSize // The original size of the image
|
||||
} = useProgressiveImg({
|
||||
image: image, // The image definition to load
|
||||
maxWidth: 800 // Limit to resolutions up to 800px wide
|
||||
});
|
||||
|
||||
// When image is not available yet
|
||||
if (!src) {
|
||||
return (
|
||||
<View style={{ height: 200, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f0f0f0' }}>
|
||||
<ActivityIndicator size="small" color="#0000ff" />
|
||||
<Text style={{ marginTop: 10 }}>Loading image...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// When using placeholder
|
||||
if (res === "placeholder") {
|
||||
return (
|
||||
<View style={{ position: 'relative' }}>
|
||||
<Image
|
||||
source={{ uri: src }}
|
||||
style={{ width: '100%', height: 200, opacity: 0.7 }}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<ActivityIndicator
|
||||
size="large"
|
||||
color="#ffffff"
|
||||
style={{ position: 'absolute', top: '50%', left: '50%', marginLeft: -20, marginTop: -20 }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Full image display with custom overlay
|
||||
return (
|
||||
<View style={{ position: 'relative', width: '100%', height: 200 }}>
|
||||
<Image
|
||||
source={{ uri: src }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<View style={{ position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'rgba(0,0,0,0.5)', padding: 8 }}>
|
||||
<Text style={{ color: 'white' }}>Resolution: {res}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Understanding ImageDefinition
|
||||
|
||||
Behind the scenes, `ImageDefinition` is a specialized CoValue that stores:
|
||||
|
||||
- The original image dimensions (`originalSize`)
|
||||
- An optional placeholder (`placeholderDataURL`) for immediate display
|
||||
- Multiple resolution variants of the same image as [`FileStream`s](../using-covalues/filestreams)
|
||||
|
||||
Each resolution is stored with a key in the format `"widthxheight"` (e.g., `"1920x1080"`, `"800x450"`).
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
// Structure of an ImageDefinition
|
||||
const image = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
placeholderDataURL: "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
|
||||
});
|
||||
|
||||
// Accessing the highest available resolution
|
||||
const highestRes = image.highestResAvailable();
|
||||
if (highestRes) {
|
||||
console.log(`Found resolution: ${highestRes.res}`);
|
||||
console.log(`Stream: ${highestRes.stream}`);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
For more details on using `ImageDefinition` directly, see the [VanillaJS docs](/docs/vanilla/using-covalues/imagedef).
|
||||
|
||||
### Fallback Behavior
|
||||
|
||||
`highestResAvailable` returns the largest resolution that fits your constraints. If a resolution has incomplete data, it falls back to the next available lower resolution.
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
const image = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
});
|
||||
|
||||
image["1920x1080"] = FileStream.create(); // Empty image upload
|
||||
image["800x450"] = await FileStream.createFromBlob(mediumSizeBlob);
|
||||
|
||||
const highestRes = image.highestResAvailable();
|
||||
console.log(highestRes.res); // 800x450
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -0,0 +1,196 @@
|
||||
import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
export const metadata = { title: "ImageDefinition" };
|
||||
|
||||
# ImageDefinition
|
||||
|
||||
`ImageDefinition` is a specialized CoValue designed specifically for managing images in Jazz applications. It extends beyond basic file storage by supporting multiple resolutions of the same image and progressive loading patterns.
|
||||
|
||||
Beyond [`ImageDefinition`](#understanding-imagedefinition), Jazz offers higher-level functions and components that make it easier to use images:
|
||||
- [`createImage()`](#creating-images) - function to create an `ImageDefinition` from a file
|
||||
- [`ProgressiveImg`](#displaying-images-with-progressiveimg) - React component to display an image with progressive loading
|
||||
- [`useProgressiveImg`](#using-useprogressiveimg-hook) - React hook to load an image in your own component
|
||||
|
||||
The [Image Upload example](https://github.com/gardencmp/jazz/tree/main/examples/image-upload) demonstrates use of `ProgressiveImg` and `ImageDefinition`.
|
||||
|
||||
## Creating Images
|
||||
|
||||
The easiest way to create and use images in your Jazz application is with the `createImage()` function:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { createImage } from "jazz-browser-media-images";
|
||||
|
||||
// Create an image from a file input
|
||||
async function handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
// Creates ImageDefinition with multiple resolutions automatically
|
||||
const image = await createImage(file, {
|
||||
owner: me.profile._owner,
|
||||
});
|
||||
|
||||
// Store the image in your application data
|
||||
me.profile.image = image;
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
> Note: `createImage()` requires a browser environment as it uses browser APIs to process images.
|
||||
|
||||
The `createImage()` function:
|
||||
- Creates an `ImageDefinition` with the right properties
|
||||
- Generates a small placeholder for immediate display
|
||||
- Creates multiple resolution variants of your image
|
||||
- Returns the created `ImageDefinition`
|
||||
|
||||
### Configuration Options
|
||||
|
||||
You can configure `createImage()` with additional options:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
// Configuration options
|
||||
const options = {
|
||||
owner: me, // Owner for access control
|
||||
maxSize: 1024 // Maximum resolution to generate
|
||||
};
|
||||
|
||||
// Setting maxSize controls which resolutions are generated:
|
||||
// 256: Only creates the smallest resolution (256px on longest side)
|
||||
// 1024: Creates 256px and 1024px resolutions
|
||||
// 2048: Creates 256px, 1024px, and 2048px resolutions
|
||||
// undefined: Creates all resolutions including the original size
|
||||
|
||||
const image = await createImage(file, options);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Displaying Images with `ProgressiveImg`
|
||||
|
||||
For a complete progressive loading experience, use the `ProgressiveImg` component:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
import { ProgressiveImg } from "jazz-react";
|
||||
|
||||
function GalleryView({ image }) {
|
||||
return (
|
||||
<div className="image-container">
|
||||
<ProgressiveImg
|
||||
image={image} // The image definition to load
|
||||
maxWidth={800} // Limit to resolutions up to 800px wide
|
||||
>
|
||||
{({ src }) => (
|
||||
<img
|
||||
src={src}
|
||||
alt="Gallery image"
|
||||
className="gallery-image"
|
||||
/>
|
||||
)}
|
||||
</ProgressiveImg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The `ProgressiveImg` component handles:
|
||||
- Showing a placeholder while loading
|
||||
- Automatically selecting the appropriate resolution
|
||||
- Progressive enhancement as higher resolutions become available
|
||||
- Cleaning up resources when unmounted
|
||||
|
||||
## Using `useProgressiveImg` Hook
|
||||
|
||||
For more control over image loading, you can implement your own progressive image component:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
import { useProgressiveImg } from "jazz-react";
|
||||
|
||||
function CustomImageComponent({ image }) {
|
||||
const {
|
||||
src, // Data URI containing the image data as a base64 string,
|
||||
// or a placeholder image URI
|
||||
res, // The current resolution
|
||||
originalSize // The original size of the image
|
||||
} = useProgressiveImg({
|
||||
image: image, // The image definition to load
|
||||
maxWidth: 800 // Limit to resolutions up to 800px wide
|
||||
});
|
||||
|
||||
// When image is not available yet
|
||||
if (!src) {
|
||||
return <div className="image-loading-fallback">Loading image...</div>;
|
||||
}
|
||||
|
||||
// When image is loading, show a placeholder
|
||||
if (res === "placeholder") {
|
||||
return <img src={src} alt="Loading..." className="blur-effect" />;
|
||||
}
|
||||
|
||||
// Full image display with custom overlay
|
||||
return (
|
||||
<div className="custom-image-wrapper">
|
||||
<img
|
||||
src={src}
|
||||
alt="Custom image"
|
||||
className="custom-image"
|
||||
/>
|
||||
<div className="image-overlay">
|
||||
<span className="image-caption">Resolution: {res}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Understanding ImageDefinition
|
||||
|
||||
Behind the scenes, `ImageDefinition` is a specialized CoValue that stores:
|
||||
|
||||
- The original image dimensions (`originalSize`)
|
||||
- An optional placeholder (`placeholderDataURL`) for immediate display
|
||||
- Multiple resolution variants of the same image as [`FileStream`s](../using-covalues/filestreams)
|
||||
|
||||
Each resolution is stored with a key in the format `"widthxheight"` (e.g., `"1920x1080"`, `"800x450"`).
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Structure of an ImageDefinition
|
||||
const image = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
placeholderDataURL: "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
|
||||
});
|
||||
|
||||
// Accessing the highest available resolution
|
||||
const highestRes = image.highestResAvailable();
|
||||
if (highestRes) {
|
||||
console.log(`Found resolution: ${highestRes.res}`);
|
||||
console.log(`Stream: ${highestRes.stream}`);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
For more details on using `ImageDefinition` directly, see the [VanillaJS docs](/docs/vanilla/using-covalues/imagedef).
|
||||
|
||||
### Fallback Behavior
|
||||
|
||||
`highestResAvailable` returns the largest resolution that fits your constraints. If a resolution has incomplete data, it falls back to the next available lower resolution.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const image = ImageDefinition.create({
|
||||
originalSize: [1920, 1080],
|
||||
});
|
||||
|
||||
image["1920x1080"] = FileStream.create(); // Empty image upload
|
||||
image["800x450"] = await FileStream.createFromBlob(mediumSizeBlob);
|
||||
|
||||
const highestRes = image.highestResAvailable();
|
||||
console.log(highestRes.res); // 800x450
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TocProvider } from "@/components/TocProvider";
|
||||
import DocsLayout from "@/components/docs/DocsLayout";
|
||||
import { DocNav } from "@/components/docs/nav";
|
||||
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
|
||||
@@ -8,8 +9,12 @@ export default function Layout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<DocsLayout nav={<DocNav />}>
|
||||
<Prose className="max-w-3xl mx-auto lg:flex-1 py-10">{children}</Prose>
|
||||
</DocsLayout>
|
||||
<TocProvider>
|
||||
<DocsLayout nav={<DocNav />}>
|
||||
<Prose className="overflow-x-hidden lg:flex-1 py-10 max-w-3xl mx-auto">
|
||||
{children}
|
||||
</Prose>
|
||||
</DocsLayout>
|
||||
</TocProvider>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user