Compare commits
36 Commits
fix/missin
...
fix/code-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d73896b24c | ||
|
|
1ab581358e | ||
|
|
9fca21a3f8 | ||
|
|
a11f531d4b | ||
|
|
9dd717bf0e | ||
|
|
42551bb4fd | ||
|
|
5c5de61cb6 | ||
|
|
7fdfc7fddb | ||
|
|
b108c6166e | ||
|
|
e0bc9a7f67 | ||
|
|
f900495f8d | ||
|
|
4188c7a18d | ||
|
|
7315960477 | ||
|
|
815f485ee5 | ||
|
|
402008a08f | ||
|
|
bc1576cb92 | ||
|
|
acbe66ed60 | ||
|
|
ec1fd2aaa2 | ||
|
|
bd796555f2 | ||
|
|
153f6ec245 | ||
|
|
42d007da13 | ||
|
|
c764eeff56 | ||
|
|
a7a00e6a7c | ||
|
|
bc65695eee | ||
|
|
153231aecb | ||
|
|
d814899d71 | ||
|
|
0b6c35c08a | ||
|
|
e62ea5a8ac | ||
|
|
a5bffd7312 | ||
|
|
9aa91ec525 | ||
|
|
9b8c299ba5 | ||
|
|
7486ca768d | ||
|
|
fa4d501eb0 | ||
|
|
a126d5dbf8 | ||
|
|
d697cc5713 | ||
|
|
d95c8cc302 |
5
.changeset/good-parrots-rhyme.md
Normal file
5
.changeset/good-parrots-rhyme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"multiauth": patch
|
||||
---
|
||||
|
||||
Use the new Resolve API
|
||||
@@ -1,3 +1,3 @@
|
||||
VITE_CURSOR_FEED_ID=example-cursor-feed-id
|
||||
VITE_GROUP_ID=co_example-group-id
|
||||
VITE_CURSOR_FEED_ID=multi-cursors-250425-1708
|
||||
VITE_GROUP_ID=co_zXE8C8sd9QxEbxnt3neRvFRPFUc
|
||||
VITE_OLD_CURSOR_AGE_SECONDS=5
|
||||
@@ -7,10 +7,9 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"format-and-lint": "biome check .",
|
||||
"format-and-lint:fix": "biome check . --write"
|
||||
"format-and-lint:fix": "biome check . --write",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-spring/web": "^9.7.5",
|
||||
@@ -30,6 +29,7 @@
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "3.0.5"
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ function App() {
|
||||
const [cursorFeedID, setCursorFeedID] = useState<ID<CursorFeed> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Loading cursor feed...", me.id);
|
||||
if (!me?.id) return;
|
||||
const loadCursorFeed = async () => {
|
||||
const id = await loadCursorContainer(
|
||||
|
||||
16
examples/multi-cursors/src/components/Boundary.tsx
Normal file
16
examples/multi-cursors/src/components/Boundary.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ViewBox } from "../types";
|
||||
|
||||
export function Boundary({ bounds }: { bounds: ViewBox }) {
|
||||
return (
|
||||
<>
|
||||
<rect
|
||||
x={bounds.x}
|
||||
y={bounds.y}
|
||||
width={bounds.width}
|
||||
height={bounds.height}
|
||||
stroke="red"
|
||||
fill="none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useAccount } from "jazz-react";
|
||||
import { CoFeedEntry, co } from "jazz-tools";
|
||||
import { CursorMoveEvent, useCanvas } from "../hooks/useCanvas";
|
||||
import { Cursor as CursorType } from "../types";
|
||||
import { Cursor as CursorType, ViewBox } from "../types";
|
||||
import { centerOfBounds } from "../utils/centerOfBounds";
|
||||
import { getColor } from "../utils/getColor";
|
||||
import { getName } from "../utils/getName";
|
||||
import { Boundary } from "./Boundary";
|
||||
import { CanvasBackground } from "./CanvasBackground";
|
||||
import { CanvasDemoContent } from "./CanvasDemoContent";
|
||||
import { Cursor } from "./Cursor";
|
||||
@@ -12,6 +14,16 @@ const OLD_CURSOR_AGE_SECONDS = Number(
|
||||
import.meta.env.VITE_OLD_CURSOR_AGE_SECONDS,
|
||||
);
|
||||
|
||||
const DEBUG = import.meta.env.VITE_DEBUG === "true";
|
||||
|
||||
// For debugging purposes, we can set a fixed bounds
|
||||
const debugBounds: ViewBox = {
|
||||
x: 320,
|
||||
y: 320,
|
||||
width: 640,
|
||||
height: 640,
|
||||
};
|
||||
|
||||
interface CanvasProps {
|
||||
remoteCursors: CoFeedEntry<co<CursorType>>[];
|
||||
onCursorMove: (move: CursorMoveEvent) => void;
|
||||
@@ -28,8 +40,12 @@ function Canvas({ remoteCursors, onCursorMove, name }: CanvasProps) {
|
||||
mousePosition,
|
||||
bgPosition,
|
||||
dottedGridSize,
|
||||
viewBox,
|
||||
} = useCanvas({ onCursorMove });
|
||||
|
||||
const bounds = DEBUG ? debugBounds : viewBox;
|
||||
const center = centerOfBounds(bounds);
|
||||
|
||||
return (
|
||||
<svg width="100%" height="100%" {...svgProps}>
|
||||
<CanvasBackground
|
||||
@@ -38,6 +54,7 @@ function Canvas({ remoteCursors, onCursorMove, name }: CanvasProps) {
|
||||
/>
|
||||
|
||||
<CanvasDemoContent />
|
||||
{DEBUG && <Boundary bounds={bounds} />}
|
||||
|
||||
{remoteCursors.map((entry) => {
|
||||
if (
|
||||
@@ -48,14 +65,21 @@ function Canvas({ remoteCursors, onCursorMove, name }: CanvasProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = getName(entry.by?.profile?.name, entry.tx.sessionID);
|
||||
const color = getColor(entry.tx.sessionID);
|
||||
const age = new Date().getTime() - new Date(entry.madeAt).getTime();
|
||||
|
||||
return (
|
||||
<Cursor
|
||||
key={entry.tx.sessionID}
|
||||
position={entry.value.position}
|
||||
color={getColor(entry.tx.sessionID)}
|
||||
color={color}
|
||||
isDragging={false}
|
||||
isRemote={true}
|
||||
name={getName(entry.by?.profile?.name, entry.tx.sessionID)}
|
||||
name={name}
|
||||
age={age}
|
||||
centerOfBounds={center}
|
||||
bounds={bounds}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -67,6 +91,8 @@ function Canvas({ remoteCursors, onCursorMove, name }: CanvasProps) {
|
||||
isDragging={isDragging}
|
||||
isRemote={false}
|
||||
name={name}
|
||||
centerOfBounds={center}
|
||||
bounds={bounds}
|
||||
/>
|
||||
) : null}
|
||||
</svg>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { animated, to, useSpring } from "@react-spring/web";
|
||||
import { Vec2, ViewBox } from "../types";
|
||||
import { calculateBoundaryIntersection } from "../utils/boundaryIntersection";
|
||||
import { isOutOfBounds } from "../utils/isOutOfBounds";
|
||||
import { CursorLabel } from "./CursorLabel";
|
||||
|
||||
interface CursorProps {
|
||||
position: { x: number; y: number };
|
||||
@@ -6,18 +10,56 @@ interface CursorProps {
|
||||
isDragging: boolean;
|
||||
isRemote: boolean;
|
||||
name: string;
|
||||
age?: number;
|
||||
centerOfBounds: Vec2;
|
||||
bounds?: ViewBox;
|
||||
}
|
||||
|
||||
const LABEL_BOUNDS_PADDING = 32;
|
||||
const CURSOR_VISIBILITY_OFFSET = 20;
|
||||
|
||||
export function Cursor({
|
||||
position,
|
||||
color,
|
||||
isDragging,
|
||||
isRemote,
|
||||
name,
|
||||
age = 0,
|
||||
centerOfBounds,
|
||||
bounds,
|
||||
}: CursorProps) {
|
||||
if (!bounds) return null;
|
||||
|
||||
const intersectionPoint = calculateBoundaryIntersection(
|
||||
centerOfBounds,
|
||||
position,
|
||||
bounds,
|
||||
);
|
||||
|
||||
const labelBounds = {
|
||||
x: bounds.x + LABEL_BOUNDS_PADDING / 2,
|
||||
y: bounds.y + LABEL_BOUNDS_PADDING / 2,
|
||||
width: bounds.width - LABEL_BOUNDS_PADDING,
|
||||
height: bounds.height - LABEL_BOUNDS_PADDING,
|
||||
};
|
||||
|
||||
const cursorIntersectionPoint = calculateBoundaryIntersection(
|
||||
centerOfBounds,
|
||||
position,
|
||||
labelBounds,
|
||||
);
|
||||
|
||||
const isStrictlyOutOfBounds = isOutOfBounds(position, bounds);
|
||||
const shouldHideCursor = isOutOfBounds(
|
||||
position,
|
||||
bounds,
|
||||
CURSOR_VISIBILITY_OFFSET,
|
||||
);
|
||||
|
||||
const springs = useSpring({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
opacity: age > 60000 ? 0 : 1,
|
||||
immediate: !isRemote,
|
||||
config: {
|
||||
tension: 170,
|
||||
@@ -25,40 +67,75 @@ export function Cursor({
|
||||
},
|
||||
});
|
||||
|
||||
const intersectionSprings = useSpring({
|
||||
x: intersectionPoint.x,
|
||||
y: intersectionPoint.y,
|
||||
config: {
|
||||
tension: 170,
|
||||
friction: 26,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<animated.g
|
||||
transform={to(
|
||||
[springs.x, springs.y],
|
||||
(x: number, y: number) => `translate(${x}, ${y})`,
|
||||
)}
|
||||
>
|
||||
<polygon
|
||||
points="0,0 0,20 14.3,14.3"
|
||||
fill={
|
||||
isDragging ? color : `color-mix(in oklch, ${color}, transparent 56%)`
|
||||
}
|
||||
stroke={color}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<text
|
||||
x="10"
|
||||
y="25"
|
||||
fill={color}
|
||||
stroke="white"
|
||||
strokeWidth="3"
|
||||
strokeLinejoin="round"
|
||||
paintOrder="stroke"
|
||||
fontSize="14"
|
||||
dominantBaseline="hanging"
|
||||
style={{
|
||||
fontFamily: "Inter, Manrope, system-ui, sans-serif",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
<>
|
||||
<animated.g
|
||||
transform={to(
|
||||
[springs.x, springs.y],
|
||||
(x: number, y: number) => `translate(${x}, ${y})`,
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</text>
|
||||
</animated.g>
|
||||
{isStrictlyOutOfBounds ? (
|
||||
<circle cx={0} cy={0} r={4} fill={color} />
|
||||
) : null}
|
||||
{!shouldHideCursor ? (
|
||||
<polygon
|
||||
points="0,0 0,20 14.3,14.3"
|
||||
fill={
|
||||
isDragging
|
||||
? color
|
||||
: `color-mix(in oklch, ${color}, transparent 56%)`
|
||||
}
|
||||
stroke={color}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
) : null}
|
||||
</animated.g>
|
||||
|
||||
{isRemote ? (
|
||||
<>
|
||||
<CursorLabel
|
||||
name={name}
|
||||
color={color}
|
||||
position={cursorIntersectionPoint}
|
||||
bounds={bounds}
|
||||
isOutOfBounds={isStrictlyOutOfBounds}
|
||||
/>
|
||||
{isStrictlyOutOfBounds ? (
|
||||
<animated.g
|
||||
transform={to(
|
||||
[intersectionSprings.x, intersectionSprings.y],
|
||||
(x: number, y: number) => {
|
||||
const angle =
|
||||
Math.atan2(centerOfBounds.y - y, centerOfBounds.x - x) *
|
||||
(180 / Math.PI);
|
||||
return `translate(${x}, ${y}) rotate(${angle})`;
|
||||
},
|
||||
)}
|
||||
>
|
||||
<path
|
||||
d="M 8,-4 L 2,0 L 8,4"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</animated.g>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
93
examples/multi-cursors/src/components/CursorLabel.tsx
Normal file
93
examples/multi-cursors/src/components/CursorLabel.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { animated, to, useSpring } from "@react-spring/web";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Vec2, ViewBox } from "../types";
|
||||
import { getLabelPosition } from "../utils/getLabelPosition";
|
||||
|
||||
const DEBUG = import.meta.env.VITE_DEBUG === "true";
|
||||
|
||||
interface CursorLabelProps {
|
||||
name: string;
|
||||
color: string;
|
||||
position: Vec2;
|
||||
bounds?: ViewBox;
|
||||
isOutOfBounds?: boolean;
|
||||
}
|
||||
|
||||
interface TextDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export function CursorLabel({
|
||||
name,
|
||||
color,
|
||||
position,
|
||||
bounds,
|
||||
isOutOfBounds,
|
||||
}: CursorLabelProps) {
|
||||
const textRef = useRef<SVGTextElement>(null);
|
||||
const [dimensions, setDimensions] = useState<TextDimensions>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const bbox = textRef.current?.getBBox();
|
||||
setDimensions({ width: bbox?.width ?? 0, height: bbox?.height ?? 0 });
|
||||
}, [name]);
|
||||
|
||||
const labelPosition = getLabelPosition(
|
||||
position,
|
||||
dimensions,
|
||||
bounds,
|
||||
isOutOfBounds,
|
||||
);
|
||||
const labelSprings = useSpring<Vec2>({
|
||||
...labelPosition,
|
||||
config: {
|
||||
tension: 170,
|
||||
friction: 26,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<animated.text
|
||||
ref={textRef}
|
||||
x={to([labelSprings.x], (x) => x)}
|
||||
y={to([labelSprings.y], (y) => y)}
|
||||
fill={color}
|
||||
stroke="white"
|
||||
strokeWidth="3"
|
||||
strokeLinejoin="round"
|
||||
paintOrder="stroke"
|
||||
fontSize="14"
|
||||
dominantBaseline="hanging"
|
||||
textAnchor="start"
|
||||
>
|
||||
{name}
|
||||
</animated.text>
|
||||
{DEBUG ? (
|
||||
<>
|
||||
<text x={position.x} y={position.y} fill="red" fontSize="8">
|
||||
{position.x}, {position.y}
|
||||
</text>
|
||||
<text x={labelPosition.x} y={labelPosition.y} fill="red" fontSize="8">
|
||||
{bounds
|
||||
? `${bounds.x - labelPosition.x}, ${bounds.y - labelPosition.y}`
|
||||
: "no bounds"}
|
||||
</text>
|
||||
<line
|
||||
x1={position.x}
|
||||
y1={position.y}
|
||||
x2={labelPosition.x}
|
||||
y2={labelPosition.y}
|
||||
stroke="red"
|
||||
strokeWidth="1"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { ViewBox } from "../types";
|
||||
import { throttleTime } from "../utils/throttleTime";
|
||||
|
||||
export interface CursorMoveEvent {
|
||||
@@ -13,7 +14,7 @@ export function useCanvas({
|
||||
onCursorMove: (event: CursorMoveEvent) => void;
|
||||
throttleMs?: number;
|
||||
}) {
|
||||
const [viewBox, setViewBox] = useState({
|
||||
const [viewBox, setViewBox] = useState<ViewBox>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: window.innerWidth,
|
||||
@@ -134,5 +135,6 @@ export function useCanvas({
|
||||
mousePosition,
|
||||
bgPosition,
|
||||
dottedGridSize,
|
||||
viewBox,
|
||||
};
|
||||
}
|
||||
|
||||
7
examples/multi-cursors/src/types.d.ts
vendored
7
examples/multi-cursors/src/types.d.ts
vendored
@@ -18,3 +18,10 @@ export type RemoteCursor = Cursor & {
|
||||
isRemote: true;
|
||||
isDragging: boolean;
|
||||
};
|
||||
|
||||
export type ViewBox = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { calculateBoundaryIntersection } from "../boundaryIntersection";
|
||||
|
||||
describe("calculateBoundaryIntersection", () => {
|
||||
const bounds = { x: 0, y: 0, width: 100, height: 100 };
|
||||
|
||||
it("should handle vertical lines (dx = 0)", () => {
|
||||
const center = { x: 50, y: 50 };
|
||||
const point = { x: 50, y: 150 }; // Straight up from center
|
||||
|
||||
const intersection = calculateBoundaryIntersection(center, point, bounds);
|
||||
|
||||
expect(intersection).toEqual({ x: 50, y: 100 }); // Should intersect at bottom boundary
|
||||
});
|
||||
|
||||
it("should handle horizontal lines (dy = 0)", () => {
|
||||
const center = { x: 50, y: 50 };
|
||||
const point = { x: 150, y: 50 }; // Straight right from center
|
||||
|
||||
const intersection = calculateBoundaryIntersection(center, point, bounds);
|
||||
|
||||
expect(intersection).toEqual({ x: 100, y: 50 }); // Should intersect at right boundary
|
||||
});
|
||||
|
||||
it("should handle vertical lines at boundaries", () => {
|
||||
const center = { x: 0, y: 50 };
|
||||
const point = { x: 0, y: 150 }; // Vertical line at x=0
|
||||
|
||||
const intersection = calculateBoundaryIntersection(center, point, bounds);
|
||||
|
||||
expect(intersection).toEqual({ x: 0, y: 100 }); // Should intersect at bottom boundary
|
||||
});
|
||||
|
||||
it("should handle horizontal lines at boundaries", () => {
|
||||
const center = { x: 50, y: 0 };
|
||||
const point = { x: 150, y: 0 }; // Horizontal line at y=0
|
||||
|
||||
const intersection = calculateBoundaryIntersection(center, point, bounds);
|
||||
|
||||
expect(intersection).toEqual({ x: 100, y: 0 }); // Should intersect at right boundary
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getLabelPosition } from "../getLabelPosition";
|
||||
|
||||
describe("getLabelPosition", () => {
|
||||
const dimensions = { width: 100, height: 20 };
|
||||
const bounds = { x: 0, y: 0, width: 1000, height: 1000 };
|
||||
|
||||
it("should position label with default offset when cursor is in bounds", () => {
|
||||
const position = { x: 500, y: 500 };
|
||||
const result = getLabelPosition(position, dimensions, bounds, false);
|
||||
|
||||
expect(result).toEqual({
|
||||
x: position.x + 15,
|
||||
y: position.y + 25,
|
||||
});
|
||||
});
|
||||
|
||||
it("should position label with default offset when bounds are undefined", () => {
|
||||
const position = { x: 500, y: 500 };
|
||||
const result = getLabelPosition(position, dimensions, undefined, true);
|
||||
|
||||
expect(result).toEqual({
|
||||
x: position.x + 15,
|
||||
y: position.y + 25,
|
||||
});
|
||||
});
|
||||
|
||||
it("should adjust label position based on cursor position when out of bounds", () => {
|
||||
const position = { x: 800, y: 600 };
|
||||
const result = getLabelPosition(position, dimensions, bounds, true);
|
||||
|
||||
// At x=800, percentageH = 0.8, so x offset should be -80 (0.8 * width)
|
||||
// At y=600, percentageV = 0.6, so y offset should be -12 (0.6 * height)
|
||||
expect(result).toEqual({
|
||||
x: position.x - 0.8 * dimensions.width,
|
||||
y: position.y - 0.6 * dimensions.height,
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle cursor at bounds edges", () => {
|
||||
const position = { x: 1000, y: 1000 }; // Bottom-right corner
|
||||
const result = getLabelPosition(position, dimensions, bounds, true);
|
||||
|
||||
// At the edges, percentages should be 1, so full dimension should be subtracted
|
||||
expect(result).toEqual({
|
||||
x: position.x - dimensions.width,
|
||||
y: position.y - dimensions.height,
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle cursor at bounds origin", () => {
|
||||
const position = { x: 0, y: 0 }; // Top-left corner
|
||||
const result = getLabelPosition(position, dimensions, bounds, true);
|
||||
|
||||
// At origin, percentages should be 0, so no offset from position
|
||||
expect(result).toEqual({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isOutOfBounds } from "../isOutOfBounds";
|
||||
|
||||
describe("isOutOfBounds", () => {
|
||||
it("should return true if the position is out of bounds", () => {
|
||||
expect(
|
||||
isOutOfBounds(
|
||||
{ x: 101, y: 101 },
|
||||
{ x: 0, y: 0, width: 100, height: 100 },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the position is within bounds", () => {
|
||||
expect(
|
||||
isOutOfBounds({ x: 50, y: 50 }, { x: 0, y: 0, width: 100, height: 100 }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if the position is inside the grace area", () => {
|
||||
expect(
|
||||
isOutOfBounds(
|
||||
{ x: 110, y: 110 },
|
||||
{ x: 0, y: 0, width: 100, height: 100 },
|
||||
20,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
77
examples/multi-cursors/src/utils/boundaryIntersection.ts
Normal file
77
examples/multi-cursors/src/utils/boundaryIntersection.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Vec2, ViewBox } from "../types";
|
||||
|
||||
/**
|
||||
* Calculate the intersection point of a line and a boundary.
|
||||
* @param center - The origin of the line.
|
||||
* @param point - The end of the line to calculate the intersection for.
|
||||
* @param bounds - The bounds of the boundary.
|
||||
* @returns The intersection point.
|
||||
*/
|
||||
export function calculateBoundaryIntersection(
|
||||
center: Vec2,
|
||||
point: Vec2,
|
||||
bounds: ViewBox,
|
||||
): Vec2 {
|
||||
// Calculate direction vector
|
||||
const dx = point.x - center.x;
|
||||
const dy = point.y - center.y;
|
||||
|
||||
// Calculate all possible intersections
|
||||
let horizontalIntersection: Vec2 | null = null;
|
||||
let verticalIntersection: Vec2 | null = null;
|
||||
|
||||
// Check horizontal bounds
|
||||
if (dx !== 0) {
|
||||
// Skip horizontal bounds check if line is vertical
|
||||
if (point.x < bounds.x) {
|
||||
const y = center.y + (dy * (bounds.x - center.x)) / dx;
|
||||
if (y >= bounds.y && y <= bounds.y + bounds.height) {
|
||||
horizontalIntersection = { x: bounds.x, y };
|
||||
}
|
||||
} else if (point.x > bounds.x + bounds.width) {
|
||||
const y = center.y + (dy * (bounds.x + bounds.width - center.x)) / dx;
|
||||
if (y >= bounds.y && y <= bounds.y + bounds.height) {
|
||||
horizontalIntersection = { x: bounds.x + bounds.width, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check vertical bounds
|
||||
if (dy !== 0) {
|
||||
// Skip vertical bounds check if line is horizontal
|
||||
if (point.y < bounds.y) {
|
||||
const x = center.x + (dx * (bounds.y - center.y)) / dy;
|
||||
if (x >= bounds.x && x <= bounds.x + bounds.width) {
|
||||
verticalIntersection = { x, y: bounds.y };
|
||||
}
|
||||
} else if (point.y > bounds.y + bounds.height) {
|
||||
const x = center.x + (dx * (bounds.y + bounds.height - center.y)) / dy;
|
||||
if (x >= bounds.x && x <= bounds.x + bounds.width) {
|
||||
verticalIntersection = { x, y: bounds.y + bounds.height };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Choose the intersection point that's closest to the actual point
|
||||
if (horizontalIntersection && verticalIntersection) {
|
||||
const horizontalDist = Math.hypot(
|
||||
point.x - horizontalIntersection.x,
|
||||
point.y - horizontalIntersection.y,
|
||||
);
|
||||
const verticalDist = Math.hypot(
|
||||
point.x - verticalIntersection.x,
|
||||
point.y - verticalIntersection.y,
|
||||
);
|
||||
return horizontalDist < verticalDist
|
||||
? horizontalIntersection
|
||||
: verticalIntersection;
|
||||
}
|
||||
|
||||
return (
|
||||
horizontalIntersection ||
|
||||
verticalIntersection || {
|
||||
x: Math.max(bounds.x, Math.min(bounds.x + bounds.width, point.x)),
|
||||
y: Math.max(bounds.y, Math.min(bounds.y + bounds.height, point.y)),
|
||||
}
|
||||
);
|
||||
}
|
||||
17
examples/multi-cursors/src/utils/centerOfBounds.ts
Normal file
17
examples/multi-cursors/src/utils/centerOfBounds.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Vec2, ViewBox } from "../types";
|
||||
|
||||
/**
|
||||
* Get the center of a bounds.
|
||||
* @param bounds - The bounds to get the center of.
|
||||
* @returns The center of the bounds.
|
||||
*/
|
||||
export function centerOfBounds(bounds?: ViewBox): Vec2 {
|
||||
if (!bounds) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
x: bounds.x + bounds.width / 2,
|
||||
y: bounds.y + bounds.height / 2,
|
||||
};
|
||||
}
|
||||
40
examples/multi-cursors/src/utils/getLabelPosition.ts
Normal file
40
examples/multi-cursors/src/utils/getLabelPosition.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Vec2, ViewBox } from "../types";
|
||||
|
||||
interface TextDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface LabelPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the position of a cursor label based on cursor position, label dimensions, and bounds
|
||||
* Such that the label is always on the same side of the bounds as the cursor
|
||||
* @param position - The cursor position
|
||||
* @param dimensions - The dimensions of the label text
|
||||
* @param bounds - The viewport bounds
|
||||
* @param isOutOfBounds - Whether the cursor is outside the bounds
|
||||
* @returns The calculated label position
|
||||
*/
|
||||
export function getLabelPosition(
|
||||
position: Vec2,
|
||||
dimensions: TextDimensions,
|
||||
bounds?: ViewBox,
|
||||
isOutOfBounds?: boolean,
|
||||
): LabelPosition {
|
||||
if (!isOutOfBounds || !bounds) {
|
||||
return { x: position.x + 15, y: position.y + 25 };
|
||||
}
|
||||
|
||||
// Calculate the percentage of the bounds that the intersection point is from the left
|
||||
const percentageH = (position.x - bounds.x) / bounds.width;
|
||||
const percentageV = (position.y - bounds.y) / bounds.height;
|
||||
|
||||
return {
|
||||
x: position.x - percentageH * dimensions.width,
|
||||
y: position.y - percentageV * dimensions.height,
|
||||
};
|
||||
}
|
||||
21
examples/multi-cursors/src/utils/isOutOfBounds.ts
Normal file
21
examples/multi-cursors/src/utils/isOutOfBounds.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Vec2, ViewBox } from "../types";
|
||||
|
||||
/**
|
||||
* Check if a position is out of bounds of a view box.
|
||||
* @param position - The position to check.
|
||||
* @param bounds - The bounds of the view box.
|
||||
* @param grace - The grace distance to allow for the position to be out of bounds.
|
||||
* @returns True if the position is out of bounds, false otherwise.
|
||||
*/
|
||||
export function isOutOfBounds(
|
||||
position: Vec2,
|
||||
bounds: ViewBox,
|
||||
grace: number = 0,
|
||||
): boolean {
|
||||
return (
|
||||
position.x < bounds.x - grace ||
|
||||
position.x > bounds.x + bounds.width + grace ||
|
||||
position.y < bounds.y - grace ||
|
||||
position.y > bounds.y + bounds.height + grace
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,30 @@
|
||||
import { Account, Group, type ID } from "jazz-tools";
|
||||
import { CursorContainer, CursorFeed } from "../schema";
|
||||
|
||||
/**
|
||||
* Creates a new group to own the cursor container.
|
||||
* @param me - The account of the current user.
|
||||
* @returns The group.
|
||||
*/
|
||||
function createGroup(me: Account) {
|
||||
const group = Group.create({
|
||||
owner: me,
|
||||
});
|
||||
group.addMember("everyone", "writer");
|
||||
console.log("Created group");
|
||||
console.log(`Add "VITE_GROUP_ID=${group.id}" to your .env file`);
|
||||
return group;
|
||||
}
|
||||
|
||||
export async function loadGroup(me: Account, groupID: ID<Group>) {
|
||||
if (groupID === undefined) {
|
||||
console.log("No group ID found in .env, creating group...");
|
||||
return createGroup(me);
|
||||
}
|
||||
const group = await Group.load(groupID, {});
|
||||
if (group === null) {
|
||||
const group = Group.create({
|
||||
owner: me,
|
||||
});
|
||||
group.addMember("everyone", "writer");
|
||||
console.log("Created group:", group.id);
|
||||
return group;
|
||||
if (group === null || group === undefined) {
|
||||
console.log("Group not found, creating group...");
|
||||
return createGroup(me);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
@@ -24,23 +39,25 @@ export async function loadGroup(me: Account, groupID: ID<Group>) {
|
||||
*/
|
||||
export async function loadCursorContainer(
|
||||
me: Account,
|
||||
cursorFeedID: ID<CursorFeed>,
|
||||
cursorFeedID = "cursor-feed",
|
||||
groupID: ID<Group>,
|
||||
): Promise<ID<CursorFeed> | undefined> {
|
||||
if (!me) return;
|
||||
console.log("Loading group...");
|
||||
const group = await loadGroup(me, groupID);
|
||||
|
||||
const cursorContainerID = CursorContainer.findUnique(
|
||||
cursorFeedID,
|
||||
group?.id as ID<Group>,
|
||||
);
|
||||
console.log("Loading cursor container:", cursorContainerID);
|
||||
const cursorContainer = await CursorContainer.load(cursorContainerID, {
|
||||
resolve: {
|
||||
cursorFeed: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (cursorContainer === null) {
|
||||
if (cursorContainer === null || cursorContainer === undefined) {
|
||||
console.log("Global cursors does not exist, creating...");
|
||||
const cursorContainer = CursorContainer.create(
|
||||
{
|
||||
@@ -61,6 +78,6 @@ export async function loadCursorContainer(
|
||||
"Global cursors already exists, loading...",
|
||||
cursorContainer.id,
|
||||
);
|
||||
return cursorContainer.cursorFeed.id;
|
||||
return cursorContainer.cursorFeed?.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,12 @@ import { defineConfig } from "vite";
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
minify: "esbuild",
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAccount, useIsAuthenticated } from "jazz-react";
|
||||
|
||||
export function Home() {
|
||||
const { me, logOut } = useAccount({ root: {} });
|
||||
const { me, logOut } = useAccount({ resolve: { root: true } });
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
if (!me) return;
|
||||
|
||||
@@ -153,10 +153,10 @@ Update the schema to include a `validate` method.
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// schema.ts
|
||||
export class DraftBubbleTeaOrder extends CoMap { // old
|
||||
name = co.optional.string; // old
|
||||
export class DraftBubbleTeaOrder extends CoMap {
|
||||
name = co.optional.string;
|
||||
|
||||
validate() {
|
||||
validate() { // [!code ++:9]
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!this.name) {
|
||||
@@ -165,7 +165,7 @@ export class DraftBubbleTeaOrder extends CoMap { // old
|
||||
|
||||
return { errors };
|
||||
}
|
||||
} // old
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -174,33 +174,33 @@ Then perform the validation on submit.
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
// CreateOrder.tsx
|
||||
export function CreateOrder() { // old
|
||||
const { me } = useAccount(); // old
|
||||
const [draft, setDraft] = useState<DraftBubbleTeaOrder>(); // old
|
||||
export function CreateOrder() {
|
||||
const { me } = useAccount();
|
||||
const [draft, setDraft] = useState<DraftBubbleTeaOrder>();
|
||||
|
||||
useEffect(() => { // old
|
||||
setDraft(DraftBubbleTeaOrder.create({})); // old
|
||||
}, [me?.id]); // old
|
||||
useEffect(() => {
|
||||
setDraft(DraftBubbleTeaOrder.create({}));
|
||||
}, [me?.id]);
|
||||
|
||||
const onSave = (e: React.FormEvent<HTMLFormElement>) => { // old
|
||||
e.preventDefault(); // old
|
||||
if (!draft) return; // old
|
||||
const onSave = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!draft) return;
|
||||
|
||||
const validation = draft.validate();
|
||||
const validation = draft.validate(); // [!code ++:5]
|
||||
if (validation.errors.length > 0) {
|
||||
console.log(validation.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
const order = draft as BubbleTeaOrder; // old
|
||||
const order = draft as BubbleTeaOrder;
|
||||
|
||||
console.log("Order created:", order); // old
|
||||
}; // old
|
||||
console.log("Order created:", order);
|
||||
};
|
||||
|
||||
if (!draft) return; // old
|
||||
if (!draft) return;
|
||||
|
||||
return <OrderForm order={draft} onSave={onSave} />; // old
|
||||
} // old
|
||||
return <OrderForm order={draft} onSave={onSave} />;
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -213,15 +213,15 @@ By storing the draft in the user's account, they can come back to it anytime wit
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// schema.ts
|
||||
export class BubbleTeaOrder extends CoMap { // old
|
||||
name = co.string; // old
|
||||
} // old
|
||||
export class BubbleTeaOrder extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
|
||||
export class DraftBubbleTeaOrder extends CoMap { // old
|
||||
name = co.optional.string; // old
|
||||
} // old
|
||||
export class DraftBubbleTeaOrder extends CoMap {
|
||||
name = co.optional.string;
|
||||
}
|
||||
|
||||
export class AccountRoot extends CoMap {
|
||||
export class AccountRoot extends CoMap { // [!code ++:15]
|
||||
draft = co.ref(DraftBubbleTeaOrder);
|
||||
}
|
||||
|
||||
@@ -243,22 +243,22 @@ Let's not forget to update the `AccountSchema`.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { JazzProvider } from "jazz-react"; // old
|
||||
import { JazzAccount } from "./schema";
|
||||
import { JazzProvider } from "jazz-react";
|
||||
import { JazzAccount } from "./schema"; // [!code ++]
|
||||
|
||||
export function MyJazzProvider({ children }: { children: React.ReactNode }) { // old
|
||||
return ( // old
|
||||
<JazzProvider // old
|
||||
export function MyJazzProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<JazzProvider
|
||||
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }}
|
||||
AccountSchema={JazzAccount}
|
||||
>// old
|
||||
{children} // old
|
||||
</JazzProvider> // old
|
||||
); // old
|
||||
} // old
|
||||
AccountSchema={JazzAccount} // [!code ++]
|
||||
>
|
||||
{children}
|
||||
</JazzProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Register the Account schema so `useAccount` returns our custom `JazzAccount`
|
||||
declare module "jazz-react" {
|
||||
declare module "jazz-react" { // [!code ++:5]
|
||||
interface Register {
|
||||
Account: JazzAccount;
|
||||
}
|
||||
@@ -271,36 +271,36 @@ Instead of creating a new draft every time we use the create form, let's use the
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
// CreateOrder.tsx
|
||||
export function CreateOrder() {// old
|
||||
const { me } = useAccount({ root: { draft: {} } });
|
||||
export function CreateOrder() {
|
||||
const { me } = useAccount({ root: { draft: {} } }); // [!code ++:3]
|
||||
|
||||
if (!me?.root) return;
|
||||
|
||||
const onSave = (e: React.FormEvent<HTMLFormElement>) => {// old
|
||||
e.preventDefault();// old
|
||||
const onSave = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const draft = me.root.draft;
|
||||
const draft = me.root.draft; // [!code ++:2]
|
||||
if (!draft) return;
|
||||
|
||||
const validation = draft.validate();// old
|
||||
if (validation.errors.length > 0) {// old
|
||||
console.log(validation.errors);// old
|
||||
return;// old
|
||||
}// old
|
||||
const validation = draft.validate();
|
||||
if (validation.errors.length > 0) {
|
||||
console.log(validation.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
const order = draft as BubbleTeaOrder;// old
|
||||
console.log("Order created:", order);// old
|
||||
const order = draft as BubbleTeaOrder;
|
||||
console.log("Order created:", order);
|
||||
|
||||
// create a new empty draft
|
||||
me.root.draft = DraftBubbleTeaOrder.create(
|
||||
me.root.draft = DraftBubbleTeaOrder.create( // [!code ++:3]
|
||||
{},
|
||||
);
|
||||
};// old
|
||||
};
|
||||
|
||||
return <CreateOrderForm id={me.root.draft.id} onSave={onSave} />
|
||||
} // old
|
||||
}
|
||||
|
||||
function CreateOrderForm({
|
||||
function CreateOrderForm({ // [!code ++:13]
|
||||
id,
|
||||
onSave,
|
||||
}: {
|
||||
@@ -330,23 +330,23 @@ Simply add a `hasChanges` checker to your schema.
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// schema.ts
|
||||
export class DraftBubbleTeaOrder extends CoMap { // old
|
||||
name = co.optional.string; // old
|
||||
export class DraftBubbleTeaOrder extends CoMap {
|
||||
name = co.optional.string;
|
||||
|
||||
validate() { // old
|
||||
const errors: string[] = []; // old
|
||||
validate() {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!this.name) { // old
|
||||
errors.push("Plese enter a name."); // old
|
||||
} // old
|
||||
if (!this.name) {
|
||||
errors.push("Plese enter a name.");
|
||||
}
|
||||
|
||||
return { errors }; // old
|
||||
} // old
|
||||
return { errors };
|
||||
}
|
||||
|
||||
get hasChanges() {
|
||||
get hasChanges() { // [!code ++:3]
|
||||
return Object.keys(this._edits).length;
|
||||
}
|
||||
} // old
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
@@ -46,22 +46,22 @@ Collaborative Values (CoValues), build a UI and subscribe to changes, set permis
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import React from "react"; // old
|
||||
import ReactDOM from "react-dom/client"; // old
|
||||
import App from "./App.tsx"; // old
|
||||
import "./index.css"; // old
|
||||
import { JazzProvider } from "jazz-react"; // old
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
import { JazzProvider } from "jazz-react"; // [!code ++]
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render( // old
|
||||
<React.StrictMode> // old
|
||||
<JazzProvider
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<JazzProvider // [!code ++:6]
|
||||
// replace `you@example.com` with your email as a temporary API key
|
||||
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }}
|
||||
>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
</React.StrictMode>// old
|
||||
); // old
|
||||
</React.StrictMode>
|
||||
);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -143,18 +143,18 @@ Let's modify `src/App.tsx` to prepare for creating an Issue and then rendering i
|
||||
import { useState } from "react";
|
||||
import { Issue } from "./schema";
|
||||
import { IssueComponent } from "./components/Issue.tsx";
|
||||
// old
|
||||
function App() {// old
|
||||
|
||||
function App() {
|
||||
const [issue, setIssue] = useState<Issue>();
|
||||
// old
|
||||
|
||||
if (issue) {
|
||||
return <IssueComponent issue={issue} />;
|
||||
} else {
|
||||
return <button>Create Issue</button>;
|
||||
}
|
||||
} // old
|
||||
// old
|
||||
export default App; // old
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -163,14 +163,14 @@ Now, finally, let's implement creating an issue:
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { useState } from "react"; // old
|
||||
import { Issue } from "./schema"; // old
|
||||
import { IssueComponent } from "./components/Issue.tsx"; // old
|
||||
// old
|
||||
function App() {// old
|
||||
const [issue, setIssue] = useState<Issue>(); // old
|
||||
// old
|
||||
const createIssue = () => {
|
||||
import { useState } from "react";
|
||||
import { Issue } from "./schema";
|
||||
import { IssueComponent } from "./components/Issue.tsx";
|
||||
|
||||
function App() {
|
||||
const [issue, setIssue] = useState<Issue>();
|
||||
|
||||
const createIssue = () => { // [!code ++:11]
|
||||
const newIssue = Issue.create(
|
||||
{
|
||||
title: "Buy terrarium",
|
||||
@@ -181,15 +181,15 @@ function App() {// old
|
||||
);
|
||||
setIssue(newIssue);
|
||||
};
|
||||
// old
|
||||
if (issue) {// old
|
||||
return <IssueComponent issue={issue} />; // old
|
||||
} else { // old
|
||||
|
||||
if (issue) {
|
||||
return <IssueComponent issue={issue} />;
|
||||
} else {
|
||||
return <button onClick={createIssue}>Create Issue</button>;
|
||||
} // old
|
||||
} // old
|
||||
// old
|
||||
export default App; // old
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -234,37 +234,38 @@ Let's modify `src/App.tsx`:
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { useState } from "react"; // old
|
||||
import { Issue } from "./schema"; // old
|
||||
import { IssueComponent } from "./components/Issue.tsx"; // old
|
||||
import { useCoState } from "jazz-react";
|
||||
import { ID } from "jazz-tools"
|
||||
// old
|
||||
function App() { // old
|
||||
const [issueID, setIssueID] = useState<ID<Issue>>();
|
||||
// old
|
||||
const issue = useCoState(Issue, issueID);
|
||||
// old
|
||||
const createIssue = () => {// old
|
||||
const newIssue = Issue.create(// old
|
||||
{ // old
|
||||
title: "Buy terrarium", // old
|
||||
description: "Make sure it's big enough for 10 snails.", // old
|
||||
estimate: 5, // old
|
||||
status: "backlog", // old
|
||||
}, // old
|
||||
); // old
|
||||
import { useState } from "react";
|
||||
import { Issue } from "./schema";
|
||||
import { IssueComponent } from "./components/Issue.tsx";
|
||||
import { useCoState } from "jazz-react"; // [!code ++]
|
||||
import { ID } from "jazz-tools" // [!code ++]
|
||||
|
||||
function App() {
|
||||
const [issue, setIssue] = useState<Issue>(); // [!code --]
|
||||
const [issueID, setIssueID] = useState<ID<Issue>>(); // [!code ++]
|
||||
|
||||
const issue = useCoState(Issue, issueID); // [!code ++]
|
||||
|
||||
const createIssue = () => {
|
||||
const newIssue = Issue.create(
|
||||
{
|
||||
title: "Buy terrarium",
|
||||
description: "Make sure it's big enough for 10 snails.",
|
||||
estimate: 5,
|
||||
status: "backlog",
|
||||
},
|
||||
);
|
||||
setIssueID(newIssue.id);
|
||||
}; // old
|
||||
// old
|
||||
if (issue) { // old
|
||||
return <IssueComponent issue={issue} />; // old
|
||||
} else { // old
|
||||
return <button onClick={createIssue}>Create Issue</button>; // old
|
||||
} // old
|
||||
} // old
|
||||
// old
|
||||
export default App; // old
|
||||
};
|
||||
|
||||
if (issue) {
|
||||
return <IssueComponent issue={issue} />;
|
||||
} else {
|
||||
return <button onClick={createIssue}>Create Issue</button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -273,12 +274,12 @@ And now for the exciting part! Let's make `src/components/Issue.tsx` an editing
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { Issue } from "../schema"; // old
|
||||
// old
|
||||
export function IssueComponent({ issue }: { issue: Issue }) { // old
|
||||
return ( // old
|
||||
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t"> // old
|
||||
<input type="text"
|
||||
import { Issue } from "../schema";
|
||||
|
||||
export function IssueComponent({ issue }: { issue: Issue }) {
|
||||
return (
|
||||
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
|
||||
<input type="text" // [!code ++:22]
|
||||
value={issue.title}
|
||||
onChange={(event) => { issue.title = event.target.value }}/>
|
||||
<textarea className="col-span-3"
|
||||
@@ -300,9 +301,9 @@ export function IssueComponent({ issue }: { issue: Issue }) { // old
|
||||
<option value="in progress">In Progress</option>
|
||||
<option value="done">Done</option>
|
||||
</select>
|
||||
</div> // old
|
||||
); // old
|
||||
} // old
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -380,40 +381,40 @@ So let's store the ID in the browser's URL and make sure our useState is in sync
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { useState } from "react"; // old
|
||||
import { Issue } from "./schema"; // old
|
||||
import { IssueComponent } from "./components/Issue.tsx"; // old
|
||||
import { useCoState } from "jazz-react"; // old
|
||||
import { ID } from "jazz-tools" // old
|
||||
// old
|
||||
function App() { // old
|
||||
const [issueID, setIssueID] = useState<ID<Issue> | undefined>(
|
||||
import { useState } from "react";
|
||||
import { Issue } from "./schema";
|
||||
import { IssueComponent } from "./components/Issue.tsx";
|
||||
import { useCoState } from "jazz-react";
|
||||
import { ID } from "jazz-tools"
|
||||
|
||||
function App() {
|
||||
const [issueID, setIssueID] = useState<ID<Issue> | undefined>( // [!code ++:3]
|
||||
(window.location.search?.replace("?issue=", "") || undefined) as ID<Issue> | undefined,
|
||||
);
|
||||
// old
|
||||
const issue = useCoState(Issue, issueID); // old
|
||||
// old
|
||||
const createIssue = () => {// old
|
||||
const newIssue = Issue.create(// old
|
||||
{ // old
|
||||
title: "Buy terrarium", // old
|
||||
description: "Make sure it's big enough for 10 snails.", // old
|
||||
estimate: 5, // old
|
||||
status: "backlog", // old
|
||||
}, // old
|
||||
); // old
|
||||
setIssueID(newIssue.id); // old
|
||||
window.history.pushState({}, "", `?issue=${newIssue.id}`);
|
||||
}; // old
|
||||
// old
|
||||
if (issue) { // old
|
||||
return <IssueComponent issue={issue} />; // old
|
||||
} else { // old
|
||||
return <button onClick={createIssue}>Create Issue</button>; // old
|
||||
} // old
|
||||
} // old
|
||||
// old
|
||||
export default App; // old
|
||||
|
||||
const issue = useCoState(Issue, issueID);
|
||||
|
||||
const createIssue = () => {
|
||||
const newIssue = Issue.create(
|
||||
{
|
||||
title: "Buy terrarium",
|
||||
description: "Make sure it's big enough for 10 snails.",
|
||||
estimate: 5,
|
||||
status: "backlog",
|
||||
},
|
||||
);
|
||||
setIssueID(newIssue.id);
|
||||
window.history.pushState({}, "", `?issue=${newIssue.id}`); // [!code ++]
|
||||
};
|
||||
|
||||
if (issue) {
|
||||
return <IssueComponent issue={issue} />;
|
||||
} else {
|
||||
return <button onClick={createIssue}>Create Issue</button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -439,45 +440,45 @@ All we have to do is create a new group to own each new issue and add "everyone"
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { useState } from "react"; // old
|
||||
import { Issue } from "./schema"; // old
|
||||
import { IssueComponent } from "./components/Issue.tsx"; // old
|
||||
import { useAccount, useCoState } from "jazz-react";
|
||||
import { useState } from "react";
|
||||
import { Issue } from "./schema";
|
||||
import { IssueComponent } from "./components/Issue.tsx";
|
||||
import { useAccount, useCoState } from "jazz-react"; // [!code ++:2]
|
||||
import { ID, Group } from "jazz-tools"
|
||||
// old
|
||||
function App() { // old
|
||||
const { me } = useAccount();
|
||||
const [issueID, setIssueID] = useState<ID<Issue> | undefined>(// old
|
||||
(window.location.search?.replace("?issue=", "") || undefined) as ID<Issue> | undefined,// old
|
||||
); // old
|
||||
// old
|
||||
const issue = useCoState(Issue, issueID); // old
|
||||
// old
|
||||
const createIssue = () => { // old
|
||||
const group = Group.create();
|
||||
|
||||
function App() {
|
||||
const { me } = useAccount(); // [!code ++]
|
||||
const [issueID, setIssueID] = useState<ID<Issue> | undefined>(
|
||||
(window.location.search?.replace("?issue=", "") || undefined) as ID<Issue> | undefined,
|
||||
);
|
||||
|
||||
const issue = useCoState(Issue, issueID);
|
||||
|
||||
const createIssue = () => {
|
||||
const group = Group.create(); // [!code ++:2]
|
||||
group.addMember("everyone", "writer");
|
||||
// old
|
||||
const newIssue = Issue.create( // old
|
||||
{ // old
|
||||
title: "Buy terrarium", // old
|
||||
description: "Make sure it's big enough for 10 snails.", // old
|
||||
estimate: 5, // old
|
||||
status: "backlog", // old
|
||||
}, // old
|
||||
{ owner: group },
|
||||
); // old
|
||||
setIssueID(newIssue.id); // old
|
||||
window.history.pushState({}, "", `?issue=${newIssue.id}`); // old
|
||||
}; // old
|
||||
// old
|
||||
if (issue) { // old
|
||||
return <IssueComponent issue={issue} />; // old
|
||||
} else { // old
|
||||
return <button onClick={createIssue}>Create Issue</button>; // old
|
||||
} // old
|
||||
} // old
|
||||
// old
|
||||
export default App; // old
|
||||
|
||||
const newIssue = Issue.create(
|
||||
{
|
||||
title: "Buy terrarium",
|
||||
description: "Make sure it's big enough for 10 snails.",
|
||||
estimate: 5,
|
||||
status: "backlog",
|
||||
},
|
||||
{ owner: group }, // [!code ++]
|
||||
);
|
||||
setIssueID(newIssue.id);
|
||||
window.history.pushState({}, "", `?issue=${newIssue.id}`);
|
||||
};
|
||||
|
||||
if (issue) {
|
||||
return <IssueComponent issue={issue} />;
|
||||
} else {
|
||||
return <button onClick={createIssue}>Create Issue</button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -508,16 +509,16 @@ Add the following to `src/schema.ts`:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
import { CoMap, CoList, co } from "jazz-tools";
|
||||
// old
|
||||
export class Issue extends CoMap { // old
|
||||
title = co.string; // old
|
||||
description = co.string; // old
|
||||
estimate = co.number; // old
|
||||
status? = co.optional.literal("backlog", "in progress", "done"); // old
|
||||
} // old
|
||||
// old
|
||||
export class ListOfIssues extends CoList.Of(co.ref(Issue)) {}
|
||||
import { CoMap, CoList, co } from "jazz-tools"; // [!code ++]
|
||||
|
||||
export class Issue extends CoMap {
|
||||
title = co.string;
|
||||
description = co.string;
|
||||
estimate = co.number;
|
||||
status? = co.optional.literal("backlog", "in progress", "done");
|
||||
}
|
||||
|
||||
export class ListOfIssues extends CoList.Of(co.ref(Issue)) {} // [!code ++:6]
|
||||
|
||||
export class Project extends CoMap {
|
||||
name = co.string;
|
||||
@@ -533,19 +534,19 @@ First, we'll change `App.tsx` to create and render `Project`s instead of `Issue`
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { useState } from "react"; // old
|
||||
import { Project, ListOfIssues } from "./schema";
|
||||
import { useState } from "react";
|
||||
import { Project, ListOfIssues } from "./schema"; // [!code ++:3]
|
||||
import { ProjectComponent } from "./components/Project.tsx";
|
||||
import { ID, Group } from "jazz-tools"
|
||||
// old
|
||||
function App() { // old
|
||||
const [projectID, setProjectID] = useState<ID<Project> | undefined>(
|
||||
|
||||
function App() {
|
||||
const [projectID, setProjectID] = useState<ID<Project> | undefined>( // [!code ++:3]
|
||||
(window.location.search?.replace("?project=", "") || undefined) as ID<Project> | undefined
|
||||
);
|
||||
// old
|
||||
|
||||
const issue = useCoState(Issue, issueID); // [!code --]
|
||||
// old
|
||||
const createProject = () => {
|
||||
|
||||
const createProject = () => { // [!code ++:14]
|
||||
const group = Group.create();
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
@@ -559,15 +560,15 @@ function App() { // old
|
||||
setProjectID(newProject.id);
|
||||
window.history.pushState({}, "", `?project=${newProject.id}`);
|
||||
};
|
||||
// old
|
||||
if (projectID) {
|
||||
|
||||
if (projectID) { // [!code ++:4]
|
||||
return <ProjectComponent projectID={projectID} />;
|
||||
} else {
|
||||
return <button onClick={createProject}>Create Project</button>;
|
||||
}
|
||||
} // old
|
||||
// old
|
||||
export default App; // old
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -631,37 +632,41 @@ But you can also take more precise control over loading by defining a minimum-de
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { ID } from "jazz-tools";// old
|
||||
import { Project, Issue } from "../schema"; // old
|
||||
import { IssueComponent } from "./Issue.tsx"; // old
|
||||
import { useCoState } from "jazz-react"; // old
|
||||
// old
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {// old
|
||||
const project = useCoState(Project, projectID, { resolve: { issues: { $each: true } } });
|
||||
import { ID } from "jazz-tools";
|
||||
import { Project, Issue } from "../schema";
|
||||
import { IssueComponent } from "./Issue.tsx";
|
||||
import { useCoState } from "jazz-react";
|
||||
|
||||
const createAndAddIssue = () => {// old
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
|
||||
const project = useCoState(
|
||||
Project,
|
||||
projectID,
|
||||
{ resolve: { issues: { $each: true } } } // [!code ++]
|
||||
);
|
||||
|
||||
const createAndAddIssue = () => {
|
||||
project?.issues.push(Issue.create({
|
||||
title: "",// old
|
||||
description: "",// old
|
||||
estimate: 0,// old
|
||||
status: "backlog",// old
|
||||
}, project._owner));// old
|
||||
};// old
|
||||
// old
|
||||
return project ? (// old
|
||||
<div>// old
|
||||
<h1>{project.name}</h1>// old
|
||||
<div className="border-r border-b">// old
|
||||
title: "",
|
||||
description: "",
|
||||
estimate: 0,
|
||||
status: "backlog",
|
||||
}, project._owner));
|
||||
};
|
||||
|
||||
return project ? (
|
||||
<div>
|
||||
<h1>{project.name}</h1>
|
||||
<div className="border-r border-b">
|
||||
{project.issues.map((issue) => (
|
||||
<IssueComponent key={issue.id} issue={issue} />
|
||||
))}// old
|
||||
<button onClick={createAndAddIssue}>Create Issue</button>// old
|
||||
</div>// old
|
||||
</div>// old
|
||||
) : (// old
|
||||
<div>Loading project...</div>// old
|
||||
);// old
|
||||
}// old
|
||||
))}
|
||||
<button onClick={createAndAddIssue}>Create Issue</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>Loading project...</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -700,39 +705,39 @@ Turns out, we're already mostly there! First, let's remove making the Project pu
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { useState } from "react"; // old
|
||||
import { Project, ListOfIssues } from "./schema"; // old
|
||||
import { ProjectComponent } from "./components/Project.tsx"; // old
|
||||
import { ID, Group } from "jazz-tools" // old
|
||||
// old
|
||||
function App() { // old
|
||||
const [projectID, setProjectID] = useState<ID<Project> | undefined>( // old
|
||||
(window.location.search?.replace("?project=", "") || undefined) as ID<Project> | undefined, // old
|
||||
); // old
|
||||
// old
|
||||
const createProject = () => { // old
|
||||
const group = Group.create(); // old
|
||||
import { useState } from "react";
|
||||
import { Project, ListOfIssues } from "./schema";
|
||||
import { ProjectComponent } from "./components/Project.tsx";
|
||||
import { ID, Group } from "jazz-tools"
|
||||
|
||||
function App() {
|
||||
const [projectID, setProjectID] = useState<ID<Project> | undefined>(
|
||||
(window.location.search?.replace("?project=", "") || undefined) as ID<Project> | undefined,
|
||||
);
|
||||
|
||||
const createProject = () => {
|
||||
const group = Group.create();
|
||||
group.addMember("everyone", "writer"); // [!code --]
|
||||
// old
|
||||
const newProject = Project.create( // old
|
||||
{ // old
|
||||
name: "New Project", // old
|
||||
issues: ListOfIssues.create([], { owner: group }) // old
|
||||
}, // old
|
||||
group, // old
|
||||
); // old
|
||||
setProjectID(newProject.id); // old
|
||||
window.history.pushState({}, "", `?project=${newProject.id}`); // old
|
||||
}; // old
|
||||
// old
|
||||
if (projectID) { // old
|
||||
return <ProjectComponent projectID={projectID} />; // old
|
||||
} else { // old
|
||||
return <button onClick={createProject}>Create Project</button>; // old
|
||||
} // old
|
||||
} // old
|
||||
// old
|
||||
export default App; // old
|
||||
|
||||
const newProject = Project.create(
|
||||
{
|
||||
name: "New Project",
|
||||
issues: ListOfIssues.create([], { owner: group })
|
||||
},
|
||||
group,
|
||||
);
|
||||
setProjectID(newProject.id);
|
||||
window.history.pushState({}, "", `?project=${newProject.id}`);
|
||||
};
|
||||
|
||||
if (projectID) {
|
||||
return <ProjectComponent projectID={projectID} />;
|
||||
} else {
|
||||
return <button onClick={createProject}>Create Project</button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -741,52 +746,52 @@ Now, inside ProjectComponent, let's add a button to invite guests (read-only) or
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
import { ID } from "jazz-tools"; // old
|
||||
import { Project, Issue } from "../schema"; // old
|
||||
import { IssueComponent } from "./Issue.tsx"; // old
|
||||
import { useCoState } from "jazz-react"; // old
|
||||
import { createInviteLink } from "jazz-react";
|
||||
// old
|
||||
import { ID } from "jazz-tools";
|
||||
import { Project, Issue } from "../schema";
|
||||
import { IssueComponent } from "./Issue.tsx";
|
||||
import { useCoState } from "jazz-react";
|
||||
import { createInviteLink } from "jazz-react"; // [!code ++]
|
||||
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {// old
|
||||
const project = useCoState(Project, projectID, { resolve: { issues: { $each: true } } }); // old
|
||||
|
||||
const { me } = useAccount();
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
|
||||
const project = useCoState(Project, projectID, { resolve: { issues: { $each: true } } });
|
||||
|
||||
const { me } = useAccount(); // [!code ++:6]
|
||||
|
||||
const invite = (role: "reader" | "writer") => {
|
||||
const link = createInviteLink(project, role, { valueHint: "project" });
|
||||
navigator.clipboard.writeText(link);
|
||||
};
|
||||
|
||||
const createAndAddIssue = () => {// old
|
||||
project?.issues.push(Issue.create({ // old
|
||||
title: "",// old
|
||||
description: "",// old
|
||||
estimate: 0,// old
|
||||
status: "backlog",// old
|
||||
}, project._owner));// old
|
||||
};// old
|
||||
// old
|
||||
return project ? (// old
|
||||
<div>// old
|
||||
<h1>{project.name}</h1>// old
|
||||
{me.canAdmin(project) && (
|
||||
const createAndAddIssue = () => {
|
||||
project?.issues.push(Issue.create({
|
||||
title: "",
|
||||
description: "",
|
||||
estimate: 0,
|
||||
status: "backlog",
|
||||
}, project._owner));
|
||||
};
|
||||
|
||||
return project ? (
|
||||
<div>
|
||||
<h1>{project.name}</h1>
|
||||
{me.canAdmin(project) && ( // [!code ++:6]
|
||||
<>
|
||||
<button onClick={() => invite("reader")}>Invite Guest</button>
|
||||
<button onClick={() => invite("writer")}>Invite Member</button>
|
||||
</>
|
||||
)}
|
||||
<div className="border-r border-b">// old
|
||||
{project.issues.map((issue) => ( // old
|
||||
<IssueComponent key={issue.id} issue={issue} /> // old
|
||||
))}// old
|
||||
<button onClick={createAndAddIssue}>Create Issue</button>// old
|
||||
</div>// old
|
||||
</div>// old
|
||||
) : (// old
|
||||
<div>Loading project...</div>// old
|
||||
);// old
|
||||
}// old
|
||||
<div className="border-r border-b">
|
||||
{project.issues.map((issue) => (
|
||||
<IssueComponent key={issue.id} issue={issue} />
|
||||
))}
|
||||
<button onClick={createAndAddIssue}>Create Issue</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>Loading project...</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
@@ -31,9 +31,10 @@ Render the component within your `JazzProvider`.
|
||||
```tsx
|
||||
import { JazzInspector } from "jazz-inspector";
|
||||
|
||||
<JazzProvider> // old
|
||||
<JazzProvider>
|
||||
// [!code ++]
|
||||
<JazzInspector />
|
||||
</JazzProvider> // old
|
||||
</JazzProvider>
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ async function getMdxSource(framework: string, slugPath?: string) {
|
||||
// Try to import the framework-specific file first
|
||||
try {
|
||||
if (!slugPath) {
|
||||
return await import("./index.mdx")
|
||||
return await import("./index.mdx");
|
||||
}
|
||||
return await import(`./${slugPath}/${framework}.mdx`);
|
||||
} catch (error) {
|
||||
@@ -85,7 +85,7 @@ export async function generateStaticParams() {
|
||||
paths.push({
|
||||
framework,
|
||||
slug: [],
|
||||
})
|
||||
});
|
||||
for (const heading of docNavigationItems) {
|
||||
for (const item of heading?.items) {
|
||||
if (item.href && item.href.startsWith("/docs")) {
|
||||
|
||||
@@ -12,15 +12,16 @@ Wrap your application with `<JazzProvider />`, this is where you specify the syn
|
||||
import { JazzProvider } from "jazz-react";
|
||||
import { MyAppAccount } from "./schema";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render( // old
|
||||
<JazzProvider
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<JazzProvider // [!code ++:6]
|
||||
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }}
|
||||
AccountSchema={MyAppAccount}
|
||||
>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
);// old
|
||||
);
|
||||
|
||||
// [!code ++:6]
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module "jazz-react" {
|
||||
interface Register {
|
||||
@@ -42,21 +43,21 @@ The easiest way to use Jazz with Next.JS is to only use it on the client side. Y
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
"use client"
|
||||
import { JazzProvider } from "jazz-react"; // old
|
||||
import { MyAppAccount } from "./schema"; // old
|
||||
```tsx
|
||||
"use client" // [!code ++]
|
||||
import { JazzProvider } from "jazz-react";
|
||||
import { MyAppAccount } from "./schema";
|
||||
|
||||
export function MyJazzProvider(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<JazzProvider
|
||||
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }}
|
||||
AccountSchema={MyAppAccount}
|
||||
>
|
||||
{props.children}
|
||||
</JazzProvider>
|
||||
);
|
||||
}
|
||||
export function MyJazzProvider(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<JazzProvider
|
||||
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }}
|
||||
AccountSchema={MyAppAccount}
|
||||
>
|
||||
{props.children}
|
||||
</JazzProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
@@ -83,29 +83,29 @@ If you want to extend the `profile` to contain additional fields (such as an ava
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
import { Account, Profile, ImageDefinition } from "jazz-tools";
|
||||
import { Account, Profile, ImageDefinition } from "jazz-tools"; // [!code ++]
|
||||
|
||||
export class MyAppAccount extends Account {
|
||||
profile = co.ref(MyAppProfile);
|
||||
root = co.ref(MyAppRoot);// old
|
||||
profile = co.ref(MyAppProfile); // [!code ++]
|
||||
root = co.ref(MyAppRoot);
|
||||
}
|
||||
|
||||
export class MyAppRoot extends CoMap {// old
|
||||
myChats = co.ref(ListOfChats);// old
|
||||
myContacts = co.ref(ListOfAccounts);// old
|
||||
}// old
|
||||
export class MyAppRoot extends CoMap {
|
||||
myChats = co.ref(ListOfChats);
|
||||
myContacts = co.ref(ListOfAccounts);
|
||||
}
|
||||
|
||||
export class MyAppProfile extends Profile {
|
||||
export class MyAppProfile extends Profile { // [!code ++:4]
|
||||
name = co.string; // compatible with default Profile schema
|
||||
avatar = co.optional.ref(ImageDefinition);
|
||||
}
|
||||
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount` // old
|
||||
declare module "jazz-react" {// old
|
||||
interface Register {// old
|
||||
Account: MyAppAccount;// old
|
||||
}// old
|
||||
}// old
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module "jazz-react" {
|
||||
interface Register {
|
||||
Account: MyAppAccount;
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -203,24 +203,24 @@ Now let's say we want to add a `myBookmarks` field to the `root` schema:
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
export class MyAppAccount extends Account {
|
||||
root = co.ref(MyAppRoot);// old
|
||||
root = co.ref(MyAppRoot);
|
||||
|
||||
async migrate(this: MyAppAccount) {
|
||||
if (this.root === undefined) { // old
|
||||
this.root = MyAppRoot.create({ // old
|
||||
myChats: ListOfChats.create([], Group.create()), // old
|
||||
myContacts: ListOfAccounts.create([], Group.create()) // old
|
||||
}); // old
|
||||
} // old
|
||||
if (this.root === undefined) {
|
||||
this.root = MyAppRoot.create({
|
||||
myChats: ListOfChats.create([], Group.create()),
|
||||
myContacts: ListOfAccounts.create([], Group.create())
|
||||
});
|
||||
}
|
||||
|
||||
// We need to load the root field to check for the myContacts field
|
||||
const { root } = await this.ensureLoaded({
|
||||
const { root } = await this.ensureLoaded({ // [!code ++:3]
|
||||
root: {},
|
||||
});
|
||||
|
||||
// we specifically need to check for undefined,
|
||||
// because myBookmarks might simply be not loaded (`null`) yet
|
||||
if (root.myBookmarks === undefined) {
|
||||
if (root.myBookmarks === undefined) { // [!code ++:3]
|
||||
root.myBookmarks = ListOfBookmarks.create([], Group.create());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
|
||||
pre.shiki {
|
||||
height: 100%;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
padding: 0.65em 0;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.twoslash-popup-code pre.shiki {
|
||||
@@ -51,7 +51,7 @@ pre.shiki .line {
|
||||
|
||||
html.dark .shiki {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
background-color: transparent !important;
|
||||
/* Optional, if you also want font styles */
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
@@ -74,4 +74,9 @@ html.dark .shiki span {
|
||||
background-color: rgba(0, 255, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
@import "@shikijs/twoslash/style-rich.css";
|
||||
html.dark {
|
||||
--twoslash-popup-bg: #1b1a1a;
|
||||
--twoslash-border-color: #2f2e2e;
|
||||
}
|
||||
|
||||
@import "@shikijs/twoslash/style-rich.css";
|
||||
|
||||
@@ -2,16 +2,26 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export function IssueTrackerPreview() {
|
||||
const [title, setTitle] = useState("Buy terrarium");
|
||||
const [description, setDescription] = useState(
|
||||
"Make sure it's big enough for 10 snails.",
|
||||
);
|
||||
const [estimate, setEstimate] = useState(5);
|
||||
const [backlog, setBacklog] = useState("backlog");
|
||||
|
||||
return (
|
||||
<div className="p-3 md:-mx-3 rounded border border-stone-100 bg-white dark:bg-black not-prose">
|
||||
<div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
|
||||
<input type="text" value={"Buy terrarium"} />
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="col-span-3"
|
||||
value={"Make sure it's big enough for 10 snails."}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
<label className="flex">
|
||||
Estimate:{" "}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import { Fragment } from "react";
|
||||
import {
|
||||
CommentDisplayPart,
|
||||
DeclarationReflection,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
PropCategory,
|
||||
PropDecl,
|
||||
} from "./tags";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export async function PackageDocs({
|
||||
package: packageName,
|
||||
@@ -37,17 +37,18 @@ export async function PackageDocs({
|
||||
return (
|
||||
<section key={category.title}>
|
||||
<h2>{category.title}</h2>
|
||||
{category.children.map((child) => (
|
||||
// Ability to link external documents has been added. Turning it off for now
|
||||
// https://typedoc.org/documents/External_Documents.html
|
||||
child.variant !== "document" && (
|
||||
<RenderPackageChild
|
||||
child={child}
|
||||
key={child.id}
|
||||
inPackage={packageName}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
{category.children.map(
|
||||
(child) =>
|
||||
// Ability to link external documents has been added. Turning it off for now
|
||||
// https://typedoc.org/documents/External_Documents.html
|
||||
child.variant !== "document" && (
|
||||
<RenderPackageChild
|
||||
child={child}
|
||||
key={child.id}
|
||||
inPackage={packageName}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
@@ -194,11 +195,16 @@ function RenderClassOrInterface({
|
||||
),
|
||||
)}
|
||||
/>
|
||||
{category.children.map((prop) => (
|
||||
prop.variant !== "document" && (
|
||||
<RenderProp prop={prop} klass={classOrInterface} key={prop.id} />
|
||||
)
|
||||
))}
|
||||
{category.children.map(
|
||||
(prop) =>
|
||||
prop.variant !== "document" && (
|
||||
<RenderProp
|
||||
prop={prop}
|
||||
klass={classOrInterface}
|
||||
key={prop.id}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</ClassOrInterface>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Deserializer, FileRegistry, JSONOutput, ProjectReflection } from "typedoc";
|
||||
import {
|
||||
Deserializer,
|
||||
FileRegistry,
|
||||
JSONOutput,
|
||||
ProjectReflection,
|
||||
} from "typedoc";
|
||||
|
||||
import JazzBrowserMediaImagesDocs from "../../typedoc/jazz-browser-media-images.json";
|
||||
import JazzBrowserDocs from "../../typedoc/jazz-browser.json";
|
||||
@@ -22,5 +27,5 @@ export async function requestProject(
|
||||
return deserializer.reviveProject(packageName, docs[packageName], {
|
||||
projectRoot: "/",
|
||||
registry: new FileRegistry(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import Link from "next/link";
|
||||
import { ReactNode } from "react";
|
||||
import { createHighlighter } from "shiki";
|
||||
import { jazzLight } from "../../themes/jazzLight.mjs";
|
||||
import { jazzDark } from "../../themes/jazzDark.mjs";
|
||||
import { jazzLight } from "../../themes/jazzLight.mjs";
|
||||
|
||||
const highlighterPromise = createHighlighter({
|
||||
langs: ["typescript", "bash", "tsx", "json", "svelte", "vue"],
|
||||
@@ -32,17 +32,14 @@ export async function Highlight({
|
||||
lang?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const html = (await highlighterPromise).codeToHtml(
|
||||
children,
|
||||
{
|
||||
lang,
|
||||
structure: "inline",
|
||||
themes: {
|
||||
light: "jazz-light",
|
||||
dark: "jazz-dark",
|
||||
},
|
||||
const html = (await highlighterPromise).codeToHtml(children, {
|
||||
lang,
|
||||
structure: "inline",
|
||||
themes: {
|
||||
light: "jazz-light",
|
||||
dark: "jazz-dark",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<code className={className} dangerouslySetInnerHTML={{ __html: html }} />
|
||||
|
||||
@@ -208,7 +208,7 @@ async function readMdxContent(url) {
|
||||
if (url === "/docs") {
|
||||
const introPath = path.join(
|
||||
process.cwd(),
|
||||
"components/docs/docs-intro.mdx",
|
||||
"app/(docs)/docs/[framework]/[[...slug]]/index.mdx",
|
||||
);
|
||||
try {
|
||||
const content = await fs.readFile(introPath, "utf8");
|
||||
@@ -229,7 +229,7 @@ async function readMdxContent(url) {
|
||||
// Base directory for docs
|
||||
const baseDir = path.join(
|
||||
process.cwd(),
|
||||
"app/(docs)/docs/[framework]/[...slug]",
|
||||
"app/(docs)/docs/[framework]/[[...slug]]",
|
||||
);
|
||||
|
||||
// If it's a directory, try to read all framework variants
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import createMDX from "@next/mdx";
|
||||
import { transformerNotationDiff } from "@shikijs/transformers";
|
||||
import { transformerTwoslash } from "@shikijs/twoslash";
|
||||
import withToc from "@stefanprobst/rehype-extract-toc";
|
||||
import withTocExport from "@stefanprobst/rehype-extract-toc/mdx";
|
||||
import rehypeSlug from "rehype-slug";
|
||||
import { createHighlighter } from "shiki";
|
||||
import { transformerNotationDiff, transformerRemoveLineBreak } from '@shikijs/transformers'
|
||||
import { transformerTwoslash } from "@shikijs/twoslash";
|
||||
import { SKIP, visit } from "unist-util-visit";
|
||||
import { jazzLight } from "./themes/jazzLight.mjs";
|
||||
import { jazzDark } from "./themes/jazzDark.mjs";
|
||||
import { jazzLight } from "./themes/jazzLight.mjs";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
@@ -63,10 +63,13 @@ function highlightPlugin() {
|
||||
transformerTwoslash({
|
||||
explicitTrigger: true,
|
||||
throws: process.env.NODE_ENV === "production",
|
||||
onTwoslashError: process.env.NODE_ENV !== "production" ? (e) => {
|
||||
console.error(e);
|
||||
error = e;
|
||||
} : undefined,
|
||||
onTwoslashError:
|
||||
process.env.NODE_ENV !== "production"
|
||||
? (e) => {
|
||||
console.error(e);
|
||||
error = e;
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
transformerNotationDiff(),
|
||||
],
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -819,6 +819,9 @@ importers:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.6.1)
|
||||
vitest:
|
||||
specifier: 3.0.5
|
||||
version: 3.0.5(@types/node@22.10.2)(@vitest/browser@3.0.5)(@vitest/ui@3.0.5)(happy-dom@16.8.1)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.1)(msw@2.7.0(@types/node@22.10.2)(typescript@5.6.3))(terser@5.37.0)(yaml@2.6.1)
|
||||
|
||||
examples/multiauth:
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user