+
+
0.5 ? GREEN_COLOR : YELLOW_COLOR,
+ opacity: 0.08 + optimizationProgress * 0.05,
+ }}
+ />
+
+
+
+
+
+
+ REACT DOCTOR × 3D
+ Your 3D scene deserves a performance review.
+ THREE.JS / REACT THREE FIBER
+
+
+
+
+
01 / FRAME LOOP
+
Pretty can still be expensive.
+
+
+
+
+
+ !
+ NEW OBJECT, EVERY FRAME
+
+
+
+
+
+
+
02 / REACT DOCTOR
+
It reads the scene graph.
+
React Three Fiber detected · performance rules enabled
+
+
+ {THREE_FINDINGS.map((finding, findingIndex) => (
+
+ ))}
+
+
+
+
+
+
+ 03 / OPTIMIZED
+
+
Reuse. Batch. Move by delta.
+
+
+
+
+
+ {THREE_FINDINGS.slice(0, 3).map((finding, findingIndex) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ REACT DOCTOR FOR 3D
+
+
Ship smoother 3D.
+
npx react-doctor@latest
+
+
+
+
+ RD / 3D
+
+
+ );
+};
diff --git a/apps/video/src/constants.ts b/apps/video/src/constants.ts
new file mode 100644
index 000000000..df1ce0f01
--- /dev/null
+++ b/apps/video/src/constants.ts
@@ -0,0 +1,8 @@
+export const VIDEO_WIDTH_PX = 1920;
+export const VIDEO_HEIGHT_PX = 1080;
+export const VIDEO_FPS = 30;
+export const GREEN_COLOR = "#4ade80";
+export const RED_COLOR = "#f87171";
+export const WHITE_COLOR = "#ffffff";
+export const YELLOW_COLOR = "#eab308";
+export const FONT_FAMILY = '"IBM Plex Mono", monospace';
diff --git a/apps/video/src/main.tsx b/apps/video/src/main.tsx
new file mode 100644
index 000000000..58af42a8c
--- /dev/null
+++ b/apps/video/src/main.tsx
@@ -0,0 +1,19 @@
+import "@fontsource/ibm-plex-mono/400.css";
+import "@fontsource/ibm-plex-mono/500.css";
+import "@fontsource/ibm-plex-mono/700.css";
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { VideoWorkspace } from "./video-workspace";
+import "./style.css";
+
+const rootElement = document.getElementById("root");
+
+if (!rootElement) {
+ throw new Error("The video workspace root element is missing.");
+}
+
+createRoot(rootElement).render(
+
+
+ ,
+);
diff --git a/apps/video/src/style.css b/apps/video/src/style.css
new file mode 100644
index 000000000..7ab988094
--- /dev/null
+++ b/apps/video/src/style.css
@@ -0,0 +1,464 @@
+:root {
+ color: #d4d4d8;
+ background: #000;
+ font-family: "IBM Plex Mono", monospace;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body,
+#root {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+}
+
+body {
+ min-width: 320px;
+ overflow: hidden;
+ background: #000;
+}
+
+button,
+input {
+ font: inherit;
+}
+
+.video-workspace {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ background: #000;
+}
+
+.video-frame {
+ position: relative;
+ flex: none;
+}
+
+.video-stage {
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+ transform-origin: top left;
+ background: #0a0a0a;
+}
+
+.scene {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ background: #0a0a0a;
+}
+
+.three-scene {
+ color: #d4d4d8;
+ background:
+ radial-gradient(circle at 70% 45%, rgba(255, 255, 255, 0.035), transparent 34%), #0a0a0a;
+}
+
+.three-grid {
+ position: absolute;
+ inset: 0;
+ opacity: 0.16;
+ background-image:
+ linear-gradient(rgba(255, 255, 255, 0.06) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255, 255, 255, 0.06) 1px, transparent 1px);
+ background-size: 72px 72px;
+ mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.5), transparent 80%);
+}
+
+.three-ambient-glow {
+ position: absolute;
+ width: 720px;
+ height: 720px;
+ border-radius: 999px;
+ filter: blur(180px);
+ right: 70px;
+ top: 150px;
+}
+
+.three-donut-wrap {
+ position: absolute;
+ z-index: 2;
+ top: 105px;
+ right: 20px;
+ width: 940px;
+ height: 850px;
+ transform-origin: center;
+}
+
+.three-donut-stage,
+.three-donut-stage canvas {
+ width: 100% !important;
+ height: 100% !important;
+}
+
+.three-copy {
+ position: absolute;
+ z-index: 4;
+ left: 112px;
+ width: 820px;
+}
+
+.three-intro-copy {
+ top: 262px;
+}
+
+.three-kicker,
+.three-step-label {
+ color: #eab308;
+ font-size: 25px;
+ font-weight: 700;
+ letter-spacing: 0.18em;
+}
+
+.three-copy h2,
+.three-scan-heading h2,
+.three-final-layout h2 {
+ margin: 26px 0 24px;
+ color: #fff;
+ font-size: 78px;
+ font-weight: 500;
+ letter-spacing: -0.055em;
+ line-height: 1.06;
+}
+
+.three-intro-copy h2 {
+ width: 840px;
+ font-size: 92px;
+}
+
+.three-copy > p,
+.three-scan-heading > p,
+.three-final-layout p {
+ margin: 0;
+ color: #737373;
+ font-size: 27px;
+ letter-spacing: 0.08em;
+}
+
+.three-problem-layout,
+.three-scan-layout,
+.three-optimized-layout,
+.three-final-layout {
+ position: absolute;
+ z-index: 5;
+ inset: 0;
+}
+
+.three-problem-copy,
+.three-optimized-copy {
+ top: 92px;
+}
+
+.three-problem-copy h2,
+.three-optimized-copy h2 {
+ width: 730px;
+ margin-top: 18px;
+ font-size: 64px;
+}
+
+.three-code-card {
+ width: 780px;
+ margin-top: 38px;
+ overflow: hidden;
+ border: 1px solid;
+ border-radius: 18px;
+ background: rgba(10, 10, 10, 0.82);
+ box-shadow: 0 30px 90px rgba(0, 0, 0, 0.45);
+}
+
+.three-code-card-header,
+.three-meter-heading,
+.three-meter-stats {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.three-code-card-header {
+ padding: 18px 24px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
+ color: #a3a3a3;
+ font-size: 20px;
+ letter-spacing: 0.08em;
+}
+
+.three-code-card pre {
+ display: flex;
+ padding: 22px 0 26px;
+ margin: 0;
+ flex-direction: column;
+ color: #e4e4e7;
+ font-family: inherit;
+ font-size: 24px;
+ line-height: 1.72;
+}
+
+.three-code-card pre span {
+ display: block;
+ padding: 0 26px;
+ white-space: pre;
+}
+
+.three-code-card pre i {
+ display: inline-block;
+ width: 58px;
+ color: #525252;
+ font-style: normal;
+ user-select: none;
+}
+
+.three-problem-hud,
+.three-optimized-hud {
+ position: absolute;
+ z-index: 5;
+ right: 98px;
+ bottom: 88px;
+ width: 710px;
+}
+
+.three-performance-meter {
+ padding: 24px 26px 22px;
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ border-radius: 16px;
+ background: rgba(10, 10, 10, 0.84);
+ backdrop-filter: blur(18px);
+}
+
+.three-meter-heading {
+ color: #a3a3a3;
+ font-size: 22px;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+}
+
+.three-meter-graph {
+ display: flex;
+ height: 84px;
+ margin: 24px 0 16px;
+ align-items: flex-end;
+ gap: 8px;
+}
+
+.three-meter-graph span {
+ flex: 1;
+ border-radius: 2px 2px 0 0;
+}
+
+.three-meter-stats {
+ color: #737373;
+ font-size: 17px;
+}
+
+.three-warning-pill {
+ display: flex;
+ width: max-content;
+ padding: 13px 18px;
+ margin: 18px 0 0 auto;
+ align-items: center;
+ gap: 14px;
+ border: 1px solid rgba(248, 113, 113, 0.3);
+ border-radius: 999px;
+ color: #fca5a5;
+ background: rgba(69, 10, 10, 0.72);
+ font-size: 18px;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+}
+
+.three-scan-heading {
+ position: absolute;
+ top: 70px;
+ left: 100px;
+ width: 920px;
+}
+
+.three-scan-heading h2 {
+ margin: 16px 0 12px;
+ font-size: 62px;
+}
+
+.three-findings {
+ position: absolute;
+ top: 315px;
+ left: 100px;
+ display: flex;
+ width: 960px;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.three-finding-row {
+ display: flex;
+ min-height: 112px;
+ padding: 18px 20px;
+ align-items: center;
+ gap: 20px;
+ border: 1px solid;
+ border-radius: 14px;
+ background: rgba(10, 10, 10, 0.82);
+ backdrop-filter: blur(18px);
+}
+
+.three-finding-status {
+ display: grid;
+ width: 42px;
+ height: 42px;
+ flex: none;
+ place-items: center;
+ border-radius: 8px;
+ font-size: 24px;
+ font-weight: 700;
+}
+
+.three-finding-copy {
+ min-width: 0;
+ flex: 1;
+}
+
+.three-finding-title {
+ color: #fff;
+ font-size: 23px;
+ font-weight: 500;
+}
+
+.three-finding-detail,
+.three-finding-file {
+ color: #737373;
+ font-size: 17px;
+}
+
+.three-finding-detail {
+ margin-top: 6px;
+}
+
+.three-finding-file {
+ flex: none;
+}
+
+.three-optimized-hud {
+ bottom: 68px;
+}
+
+.three-fixed-list {
+ display: flex;
+ margin-top: 14px;
+ flex-direction: column;
+ gap: 9px;
+}
+
+.three-fixed-list .three-finding-row {
+ min-height: 78px;
+ padding: 12px 15px;
+}
+
+.three-fixed-list .three-finding-detail {
+ display: none;
+}
+
+.three-fixed-list .three-finding-file {
+ font-size: 14px;
+}
+
+.three-final-layout {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 52px;
+ background: rgba(10, 10, 10, 0.8);
+ backdrop-filter: blur(26px);
+ transform-origin: center;
+}
+
+.three-final-layout h2 {
+ margin: 10px 0 16px;
+ font-size: 92px;
+}
+
+.three-final-layout p {
+ color: #fff;
+ font-size: 31px;
+ letter-spacing: 0;
+}
+
+.three-corner-mark {
+ position: absolute;
+ z-index: 8;
+ right: 54px;
+ bottom: 40px;
+ opacity: 0.34;
+ font-size: 18px;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+}
+
+.playback-controls {
+ position: fixed;
+ z-index: 50;
+ top: 24px;
+ left: 24px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ color: rgba(255, 255, 255, 0.62);
+ font-size: 20px;
+ line-height: 1;
+}
+
+.playback-controls button {
+ display: grid;
+ width: 34px;
+ height: 34px;
+ padding: 0;
+ place-items: center;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ border-radius: 999px;
+ color: white;
+ background: rgba(10, 10, 10, 0.8);
+ cursor: pointer;
+}
+
+.playback-controls button:hover {
+ border-color: rgba(255, 255, 255, 0.42);
+}
+
+.playback-controls button:focus-visible,
+.playback-controls input:focus-visible {
+ outline: 2px solid white;
+ outline-offset: 3px;
+}
+
+.playback-controls input {
+ width: 192px;
+ height: 20px;
+ cursor: ew-resize;
+ accent-color: white;
+}
+
+.playback-time {
+ min-width: 86px;
+ font-variant-numeric: tabular-nums;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
diff --git a/apps/video/src/three-constants.ts b/apps/video/src/three-constants.ts
new file mode 100644
index 000000000..afd81ac91
--- /dev/null
+++ b/apps/video/src/three-constants.ts
@@ -0,0 +1,96 @@
+export interface ThreeFinding {
+ detail: string;
+ file: string;
+ title: string;
+}
+
+export interface ThreeCodeLine {
+ id: string;
+ text: string;
+}
+
+export const THREE_INTRO_END_FRAME = 78;
+export const THREE_PROBLEM_END_FRAME = 190;
+export const THREE_SCAN_END_FRAME = 300;
+export const THREE_TOTAL_DURATION_FRAMES = 390;
+export const THREE_TRANSITION_DURATION_FRAMES = 18;
+export const THREE_FINDING_INTERVAL_FRAMES = 13;
+export const THREE_FINDING_FADE_DURATION_FRAMES = 8;
+export const THREE_FIX_INTERVAL_FRAMES = 9;
+export const THREE_DONUT_RADIUS = 1.52;
+export const THREE_DONUT_TUBE_RADIUS = 0.48;
+export const THREE_DONUT_RADIAL_SEGMENTS = 48;
+export const THREE_DONUT_TUBULAR_SEGMENTS = 144;
+export const THREE_CAMERA_Z = 6.6;
+export const THREE_CAMERA_FOV_DEGREES = 34;
+export const THREE_BAD_FRAME_STEP = 3;
+export const THREE_TARGET_FPS = 60;
+export const THREE_BAD_FPS = 24;
+export const THREE_INITIAL_ALLOCATIONS_PER_SECOND = 60;
+export const THREE_GRAPH_BAR_HEIGHT_PX = 74;
+export const THREE_PROBLEM_REVEAL_FRAME = 118;
+
+export const THREE_FINDINGS: ThreeFinding[] = [
+ {
+ title: "Allocation inside useFrame",
+ detail: "Reuse vectors instead of allocating every frame",
+ file: "Donut.tsx:18",
+ },
+ {
+ title: "Unbounded device pixel ratio",
+ detail: "Cap DPR before dense displays multiply the work",
+ file: "Scene.tsx:9",
+ },
+ {
+ title: "Repeated meshes use separate draw calls",
+ detail: "Batch shared geometry with instancedMesh",
+ file: "Sprinkles.tsx:31",
+ },
+ {
+ title: "Frame update ignores delta",
+ detail: "Make motion independent of refresh rate",
+ file: "Donut.tsx:17",
+ },
+];
+
+export const THREE_GRAPH_BAR_IDS = [
+ "01",
+ "02",
+ "03",
+ "04",
+ "05",
+ "06",
+ "07",
+ "08",
+ "09",
+ "10",
+ "11",
+ "12",
+ "13",
+ "14",
+ "15",
+ "16",
+ "17",
+ "18",
+ "19",
+ "20",
+ "21",
+ "22",
+];
+
+export const THREE_BAD_CODE_LINES: ThreeCodeLine[] = [
+ { id: "bad-frame-start", text: "useFrame(() => {" },
+ { id: "bad-frame-allocation", text: " const target = new THREE.Vector3()" },
+ { id: "bad-frame-lerp", text: " mesh.current.position.lerp(target, 0.1)" },
+ { id: "bad-frame-rotation", text: " mesh.current.rotation.y += 0.02" },
+ { id: "bad-frame-end", text: "})" },
+];
+
+export const THREE_GOOD_CODE_LINES: ThreeCodeLine[] = [
+ { id: "good-stable-target", text: "const target = useMemo(() => new THREE.Vector3(), [])" },
+ { id: "good-spacer", text: "" },
+ { id: "good-frame-start", text: "useFrame((_, delta) => {" },
+ { id: "good-frame-lerp", text: " mesh.current.position.lerp(target, delta * 6)" },
+ { id: "good-frame-rotation", text: " mesh.current.rotation.y += delta" },
+ { id: "good-frame-end", text: "})" },
+];
diff --git a/apps/video/src/utils/ease-in-out-cubic.ts b/apps/video/src/utils/ease-in-out-cubic.ts
new file mode 100644
index 000000000..bf931b0de
--- /dev/null
+++ b/apps/video/src/utils/ease-in-out-cubic.ts
@@ -0,0 +1,2 @@
+export const easeInOutCubic = (progress: number) =>
+ progress < 0.5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2;
diff --git a/apps/video/src/utils/ease-out-cubic.ts b/apps/video/src/utils/ease-out-cubic.ts
new file mode 100644
index 000000000..9cd1144a4
--- /dev/null
+++ b/apps/video/src/utils/ease-out-cubic.ts
@@ -0,0 +1 @@
+export const easeOutCubic = (progress: number) => 1 - (1 - progress) ** 3;
diff --git a/apps/video/src/utils/format-playback-time.ts b/apps/video/src/utils/format-playback-time.ts
new file mode 100644
index 000000000..1b86c67b6
--- /dev/null
+++ b/apps/video/src/utils/format-playback-time.ts
@@ -0,0 +1,6 @@
+export const formatPlaybackTime = (seconds: number) => {
+ const tenths = Math.floor(seconds * 10);
+ const minutes = Math.floor(tenths / 600);
+ const wholeSeconds = Math.floor((tenths % 600) / 10);
+ return `${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${tenths % 10}`;
+};
diff --git a/apps/video/src/utils/interpolate-number.ts b/apps/video/src/utils/interpolate-number.ts
new file mode 100644
index 000000000..d95163dc4
--- /dev/null
+++ b/apps/video/src/utils/interpolate-number.ts
@@ -0,0 +1,22 @@
+export interface InterpolateNumberInput {
+ easing?: (progress: number) => number;
+ inputEnd: number;
+ inputStart: number;
+ outputEnd: number;
+ outputStart: number;
+ value: number;
+}
+
+export const interpolateNumber = ({
+ easing = (progress) => progress,
+ inputEnd,
+ inputStart,
+ outputEnd,
+ outputStart,
+ value,
+}: InterpolateNumberInput) => {
+ const duration = inputEnd - inputStart;
+ const rawProgress = duration === 0 ? 1 : (value - inputStart) / duration;
+ const progress = Math.min(1, Math.max(0, rawProgress));
+ return outputStart + (outputEnd - outputStart) * easing(progress);
+};
diff --git a/apps/video/src/video-workspace.tsx b/apps/video/src/video-workspace.tsx
new file mode 100644
index 000000000..ebfbaf101
--- /dev/null
+++ b/apps/video/src/video-workspace.tsx
@@ -0,0 +1,123 @@
+import {
+ animate,
+ domAnimation,
+ LazyMotion,
+ useMotionValue,
+ useMotionValueEvent,
+ useReducedMotion,
+} from "framer-motion";
+import { useEffect, useRef, useState, useSyncExternalStore } from "react";
+import { PlaybackControls, type TimelinePlaybackControls } from "./components/playback-controls";
+import { ThreeTimeline } from "./components/three-timeline";
+import { VIDEO_FPS, VIDEO_HEIGHT_PX, VIDEO_WIDTH_PX } from "./constants";
+import { THREE_TOTAL_DURATION_FRAMES } from "./three-constants";
+
+const subscribeViewport = (onChange: () => void) => {
+ window.addEventListener("resize", onChange);
+ return () => window.removeEventListener("resize", onChange);
+};
+
+const getViewportWidth = () => window.innerWidth;
+const getViewportHeight = () => window.innerHeight;
+const getServerViewportWidth = () => VIDEO_WIDTH_PX;
+const getServerViewportHeight = () => VIDEO_HEIGHT_PX;
+
+export const VideoWorkspace = () => {
+ const searchParameters = new URLSearchParams(window.location.search);
+ const isCaptureMode = searchParameters.get("real") === "true";
+ const totalDurationFrames = THREE_TOTAL_DURATION_FRAMES;
+ const requestedFrameParameter = searchParameters.get("frame");
+ const requestedFrame = Number(requestedFrameParameter);
+ const hasRequestedFrame =
+ requestedFrameParameter !== null && Number.isFinite(requestedFrame) && requestedFrame >= 0;
+ const isManualFrame = searchParameters.get("manual") === "true";
+ const isStillFrame = isManualFrame || hasRequestedFrame;
+ const initialFrame = isStillFrame ? Math.min(requestedFrame, totalDurationFrames) : 0;
+ const playhead = useMotionValue(initialFrame / VIDEO_FPS);
+ const [currentFrame, setCurrentFrame] = useState(initialFrame);
+ const playbackRef = useRef
(null);
+ const shouldReduceMotion = Boolean(useReducedMotion());
+ const totalDurationSeconds = totalDurationFrames / VIDEO_FPS;
+ const canAutoplay = isCaptureMode || !shouldReduceMotion;
+ const viewportWidth = useSyncExternalStore(
+ subscribeViewport,
+ getViewportWidth,
+ getServerViewportWidth,
+ );
+ const viewportHeight = useSyncExternalStore(
+ subscribeViewport,
+ getViewportHeight,
+ getServerViewportHeight,
+ );
+ const canvasScale = Math.min(viewportWidth / VIDEO_WIDTH_PX, viewportHeight / VIDEO_HEIGHT_PX);
+
+ useMotionValueEvent(playhead, "change", (seconds) => {
+ setCurrentFrame(Math.min(totalDurationFrames, seconds * VIDEO_FPS));
+ });
+
+ useEffect(() => {
+ if (!isManualFrame) return;
+ const setManualFrame = (event: Event) => {
+ if (!(event instanceof CustomEvent) || typeof event.detail !== "number") return;
+ setCurrentFrame(Math.max(0, Math.min(totalDurationFrames, event.detail)));
+ };
+ window.addEventListener("react-doctor:set-frame", setManualFrame);
+ return () => window.removeEventListener("react-doctor:set-frame", setManualFrame);
+ }, [isManualFrame, totalDurationFrames]);
+
+ useEffect(() => {
+ if (isStillFrame) return;
+
+ const playback = animate(playhead, totalDurationSeconds, {
+ duration: totalDurationSeconds,
+ ease: "linear",
+ });
+ playbackRef.current = playback;
+
+ if (!canAutoplay) playback.pause();
+
+ return () => {
+ playbackRef.current = null;
+ playback.stop();
+ };
+ }, [canAutoplay, isStillFrame, playhead, totalDurationSeconds]);
+
+ const currentSeconds = currentFrame / VIDEO_FPS;
+
+ return (
+
+
+ React Doctor for Three.js performance animation
+
+ {!isCaptureMode && (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/apps/video/tsconfig.json b/apps/video/tsconfig.json
new file mode 100644
index 000000000..d3396793a
--- /dev/null
+++ b/apps/video/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "types": ["vite/client"],
+ "jsx": "react-jsx"
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/apps/video/vite.config.ts b/apps/video/vite.config.ts
new file mode 100644
index 000000000..58676f788
--- /dev/null
+++ b/apps/video/vite.config.ts
@@ -0,0 +1,6 @@
+import react from "@vitejs/plugin-react";
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ plugins: [react()],
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8a5f8e459..d8b841604 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -52,6 +52,49 @@ importers:
specifier: ^0.1.15
version: 0.1.20(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.0)(tsx@4.22.4)(typescript@6.0.3)(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0)
+ apps/video:
+ dependencies:
+ '@fontsource/ibm-plex-mono':
+ specifier: ^5.2.7
+ version: 5.3.0
+ '@react-three/fiber':
+ specifier: ^9.7.0
+ version: 9.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(three@0.185.1)
+ framer-motion:
+ specifier: ^12.23.24
+ version: 12.43.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react:
+ specifier: 19.2.5
+ version: 19.2.5
+ react-dom:
+ specifier: 19.2.5
+ version: 19.2.5(react@19.2.5)
+ three:
+ specifier: ^0.185.1
+ version: 0.185.1
+ devDependencies:
+ '@types/react':
+ specifier: ^19.2.14
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.4(@types/react@19.2.14)
+ '@types/three':
+ specifier: ^0.185.3
+ version: 0.185.3
+ '@vitejs/plugin-react':
+ specifier: ^5.1.1
+ version: 5.2.0(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0))
+ playwright-core:
+ specifier: ^1.62.1
+ version: 1.62.1
+ typescript:
+ specifier: ^6.0.3
+ version: 6.0.3
+ vite:
+ specifier: ^7.3.1
+ version: 7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0)
+
packages/api:
dependencies:
'@react-doctor/core':
@@ -511,6 +554,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
+ '@babel/helper-plugin-utils@7.29.7':
+ resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
@@ -545,6 +592,18 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
+ '@babel/plugin-transform-react-jsx-self@7.29.7':
+ resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.29.7':
+ resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/runtime@7.29.2':
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
engines: {node: '>=6.9.0'}
@@ -635,6 +694,9 @@ packages:
'@daytona/toolbox-api-client@0.196.0':
resolution: {integrity: sha512-QjLGLr7NzD8+3SLwUGpIhroi8rdhP6VlUHYUD+mlR09xHHWlZA9QidbaoDyGqhLlH04PthXNB3nuX5UjXNAf4g==}
+ '@dimforge/rapier3d-compat@0.12.0':
+ resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
+
'@effect/platform-node-shared@4.0.0-beta.70':
resolution: {integrity: sha512-3VXuL63IDmq13We+ApRKn2JW3Rb9g5gj1YEmfb8u2b73norur1VsIJ/pRE4qjShevg19dQYi2JsLawSZ6gApug==}
engines: {node: '>=18.0.0'}
@@ -1174,6 +1236,9 @@ packages:
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@fontsource/ibm-plex-mono@5.3.0':
+ resolution: {integrity: sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==}
+
'@grpc/grpc-js@1.14.4':
resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==}
engines: {node: '>=12.10.0'}
@@ -2196,6 +2261,34 @@ packages:
engines: {bun: '>=1.3.0', node: ^20.19.0 || >=22.12.0}
hasBin: true
+ '@react-three/fiber@9.7.0':
+ resolution: {integrity: sha512-EWm9FwcaOZQu/ExFW5rggoCMM1NJet5YbxVxKaOE+KSncrjU0Wx7017qSyGFvupviK89nMYGCWU3BIK4dI1clw==}
+ peerDependencies:
+ expo: '>=43.0'
+ expo-asset: '>=8.4'
+ expo-file-system: '>=11.0'
+ expo-gl: '>=11.0'
+ react: '>=19 <19.3'
+ react-dom: '>=19 <19.3'
+ react-native: '>=0.78'
+ three: '>=0.156'
+ peerDependenciesMeta:
+ expo:
+ optional: true
+ expo-asset:
+ optional: true
+ expo-file-system:
+ optional: true
+ expo-gl:
+ optional: true
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+
+ '@rolldown/pluginutils@1.0.0-rc.3':
+ resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==}
+
'@rollup/rollup-android-arm-eabi@4.57.1':
resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==}
cpu: [arm]
@@ -2491,12 +2584,27 @@ packages:
cpu: [arm64]
os: [win32]
+ '@tweenjs/tween.js@23.1.3':
+ resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
+
'@tybys/wasm-util@0.10.3':
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
'@types/babel__code-frame@7.27.0':
resolution: {integrity: sha512-Dwlo+LrxDx/0SpfmJ/BKveHf7QXWvLBLc+x03l5sbzykj3oB9nHygCpSECF1a+s+QIxbghe+KHqC90vGtxLRAA==}
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -2527,15 +2635,34 @@ packages:
'@types/prompts@2.4.9':
resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==}
+ '@types/react-dom@19.2.4':
+ resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react-reconciler@0.28.9':
+ resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==}
+ peerDependencies:
+ '@types/react': '*'
+
'@types/react@19.2.14':
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
'@types/semver@7.7.1':
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
+ '@types/stats.js@0.17.4':
+ resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
+
+ '@types/three@0.185.3':
+ resolution: {integrity: sha512-8TqTn1+fjPWuJ4mR6Igtg56DCf9b5EeAlwhb5xaa6WlBrsg7SvG0NQbyctGRkHCKwg0uftfCspOEsTXelgktMA==}
+
'@types/vscode@1.120.0':
resolution: {integrity: sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==}
+ '@types/webxr@0.5.24':
+ resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
+
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
@@ -2543,6 +2670,12 @@ packages:
resolution: {integrity: sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@vitejs/plugin-react@5.2.0':
+ resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+
'@vitest/expect@4.1.7':
resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==}
@@ -2858,6 +2991,9 @@ packages:
buffer@5.6.0:
resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==}
+ buffer@6.0.3:
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
@@ -3209,6 +3345,9 @@ packages:
picomatch:
optional: true
+ fflate@0.8.3:
+ resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
+
figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'}
@@ -3255,6 +3394,20 @@ packages:
forwarded-parse@2.1.2:
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
+ framer-motion@12.43.0:
+ resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==}
+ peerDependencies:
+ '@emotion/is-prop-valid': '*'
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@emotion/is-prop-valid':
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+
fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
@@ -3464,6 +3617,11 @@ packages:
peerDependencies:
ws: '*'
+ its-fine@2.0.0:
+ resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==}
+ peerDependencies:
+ react: ^19.0.0
+
jiti@2.7.0:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
@@ -3713,6 +3871,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
+ meshoptimizer@1.1.1:
+ resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==}
+
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@@ -3755,6 +3916,12 @@ packages:
module-details-from-path@1.0.4:
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
+ motion-dom@12.43.0:
+ resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==}
+
+ motion-utils@12.39.0:
+ resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==}
+
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@@ -3932,6 +4099,11 @@ packages:
resolution: {integrity: sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==}
hasBin: true
+ playwright-core@1.62.1:
+ resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==}
+ engines: {node: '>=20'}
+ hasBin: true
+
pngjs@7.0.0:
resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
engines: {node: '>=14.19.0'}
@@ -3984,12 +4156,30 @@ packages:
react-devtools-core@7.0.1:
resolution: {integrity: sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw==}
+ react-dom@19.2.5:
+ resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
+ peerDependencies:
+ react: ^19.2.5
+
react-reconciler@0.33.0:
resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==}
engines: {node: '>=0.10.0'}
peerDependencies:
react: ^19.2.0
+ react-refresh@0.18.0:
+ resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
+ engines: {node: '>=0.10.0'}
+
+ react-use-measure@2.1.7:
+ resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
+ peerDependencies:
+ react: '>=16.13'
+ react-dom: '>=16.13'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react@19.2.5:
resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
engines: {node: '>=0.10.0'}
@@ -4183,6 +4373,11 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
+ suspend-react@0.1.3:
+ resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==}
+ peerDependencies:
+ react: '>=17.0'
+
tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
@@ -4204,6 +4399,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ three@0.185.1:
+ resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==}
+
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -4296,6 +4494,11 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -4516,6 +4719,24 @@ packages:
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+ zustand@5.0.14:
+ resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=18.0.0'
+ immer: '>=9.0.6'
+ react: '>=18.0.0'
+ use-sync-external-store: '>=1.2.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+
snapshots:
'@alcalzone/ansi-tokenize@0.3.0':
@@ -4768,6 +4989,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@babel/helper-plugin-utils@7.29.7': {}
+
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-string-parser@7.29.7': {}
@@ -4791,6 +5014,16 @@ snapshots:
dependencies:
'@babel/types': 7.29.7
+ '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.29.7
+
'@babel/runtime@7.29.2': {}
'@babel/template@7.28.6':
@@ -5023,6 +5256,8 @@ snapshots:
- debug
- supports-color
+ '@dimforge/rapier3d-compat@0.12.0': {}
+
'@effect/platform-node-shared@4.0.0-beta.70(effect@4.0.0-beta.70)':
dependencies:
'@types/ws': 8.18.1
@@ -5351,6 +5586,8 @@ snapshots:
'@eslint/core': 0.17.0
levn: 0.4.1
+ '@fontsource/ibm-plex-mono@5.3.0': {}
+
'@grpc/grpc-js@1.14.4':
dependencies:
'@grpc/proto-loader': 0.8.1
@@ -6137,6 +6374,28 @@ snapshots:
oxc-parser: 0.132.0
sade: 1.8.1
+ '@react-three/fiber@9.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(three@0.185.1)':
+ dependencies:
+ '@babel/runtime': 7.29.2
+ '@types/webxr': 0.5.24
+ base64-js: 1.5.1
+ buffer: 6.0.3
+ its-fine: 2.0.0(@types/react@19.2.14)(react@19.2.5)
+ react: 19.2.5
+ react-use-measure: 2.1.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ scheduler: 0.27.0
+ suspend-react: 0.1.3(react@19.2.5)
+ three: 0.185.1
+ use-sync-external-store: 1.6.0(react@19.2.5)
+ zustand: 5.0.14(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
+ optionalDependencies:
+ react-dom: 19.2.5(react@19.2.5)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+
+ '@rolldown/pluginutils@1.0.0-rc.3': {}
+
'@rollup/rollup-android-arm-eabi@4.57.1':
optional: true
@@ -6347,6 +6606,8 @@ snapshots:
'@turbo/windows-arm64@2.9.7':
optional: true
+ '@tweenjs/tween.js@23.1.3': {}
+
'@tybys/wasm-util@0.10.3':
dependencies:
tslib: 2.8.1
@@ -6354,6 +6615,27 @@ snapshots:
'@types/babel__code-frame@7.27.0': {}
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.29.7
+
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -6382,20 +6664,53 @@ snapshots:
'@types/node': 25.6.0
kleur: 3.0.3
+ '@types/react-dom@19.2.4(@types/react@19.2.14)':
+ dependencies:
+ '@types/react': 19.2.14
+
+ '@types/react-reconciler@0.28.9(@types/react@19.2.14)':
+ dependencies:
+ '@types/react': 19.2.14
+
'@types/react@19.2.14':
dependencies:
csstype: 3.2.3
'@types/semver@7.7.1': {}
+ '@types/stats.js@0.17.4': {}
+
+ '@types/three@0.185.3':
+ dependencies:
+ '@dimforge/rapier3d-compat': 0.12.0
+ '@tweenjs/tween.js': 23.1.3
+ '@types/stats.js': 0.17.4
+ '@types/webxr': 0.5.24
+ fflate: 0.8.3
+ meshoptimizer: 1.1.1
+
'@types/vscode@1.120.0': {}
+ '@types/webxr@0.5.24': {}
+
'@types/ws@8.18.1':
dependencies:
'@types/node': 25.6.0
'@typescript-eslint/types@8.59.3': {}
+ '@vitejs/plugin-react@5.2.0(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0))':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.0)
+ '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.0)
+ '@rolldown/pluginutils': 1.0.0-rc.3
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.18.0
+ vite: 7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0)
+ transitivePeerDependencies:
+ - supports-color
+
'@vitest/expect@4.1.7':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -6722,6 +7037,11 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
+ buffer@6.0.3:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
@@ -7135,6 +7455,8 @@ snapshots:
optionalDependencies:
picomatch: 4.0.4
+ fflate@0.8.3: {}
+
figures@6.1.0:
dependencies:
is-unicode-supported: 2.1.0
@@ -7178,6 +7500,15 @@ snapshots:
forwarded-parse@2.1.2: {}
+ framer-motion@12.43.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ dependencies:
+ motion-dom: 12.43.0
+ motion-utils: 12.39.0
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+
fs-extra@7.0.1:
dependencies:
graceful-fs: 4.2.11
@@ -7380,6 +7711,13 @@ snapshots:
dependencies:
ws: 8.20.0
+ its-fine@2.0.0(@types/react@19.2.14)(react@19.2.5):
+ dependencies:
+ '@types/react-reconciler': 0.28.9(@types/react@19.2.14)
+ react: 19.2.5
+ transitivePeerDependencies:
+ - '@types/react'
+
jiti@2.7.0: {}
js-tokens@4.0.0: {}
@@ -7563,6 +7901,8 @@ snapshots:
merge2@1.4.1: {}
+ meshoptimizer@1.1.1: {}
+
micromatch@4.0.8:
dependencies:
braces: 3.0.3
@@ -7598,6 +7938,12 @@ snapshots:
module-details-from-path@1.0.4: {}
+ motion-dom@12.43.0:
+ dependencies:
+ motion-utils: 12.39.0
+
+ motion-utils@12.39.0: {}
+
mri@1.2.0: {}
mrmime@2.0.1: {}
@@ -7878,6 +8224,8 @@ snapshots:
dependencies:
pngjs: 7.0.0
+ playwright-core@1.62.1: {}
+
pngjs@7.0.0: {}
postcss@8.5.6:
@@ -7931,11 +8279,24 @@ snapshots:
- bufferutil
- utf-8-validate
+ react-dom@19.2.5(react@19.2.5):
+ dependencies:
+ react: 19.2.5
+ scheduler: 0.27.0
+
react-reconciler@0.33.0(react@19.2.5):
dependencies:
react: 19.2.5
scheduler: 0.27.0
+ react-refresh@0.18.0: {}
+
+ react-use-measure@2.1.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ dependencies:
+ react: 19.2.5
+ optionalDependencies:
+ react-dom: 19.2.5(react@19.2.5)
+
react@19.2.5: {}
read-yaml-file@1.1.0:
@@ -8135,6 +8496,10 @@ snapshots:
dependencies:
has-flag: 4.0.0
+ suspend-react@0.1.3(react@19.2.5):
+ dependencies:
+ react: 19.2.5
+
tagged-tag@1.0.0: {}
tar@7.5.20:
@@ -8157,6 +8522,8 @@ snapshots:
source-map-support: 0.5.21
optional: true
+ three@0.185.1: {}
+
tinybench@2.9.0: {}
tinyexec@1.1.1: {}
@@ -8232,6 +8599,10 @@ snapshots:
dependencies:
punycode: 2.3.1
+ use-sync-external-store@1.6.0(react@19.2.5):
+ dependencies:
+ react: 19.2.5
+
util-deprecate@1.0.2: {}
uuid@14.0.0: {}
@@ -8470,3 +8841,9 @@ snapshots:
zod: 4.3.6
zod@4.3.6: {}
+
+ zustand@5.0.14(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)):
+ optionalDependencies:
+ '@types/react': 19.2.14
+ react: 19.2.5
+ use-sync-external-store: 1.6.0(react@19.2.5)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 959ff85c4..5b7a6dfc3 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,4 +1,5 @@
packages:
+ - "apps/*"
- "packages/*"
minimumReleaseAge: 7200