From 5f3ac7cf1b9312e9ed5241588102d69d4c79f668 Mon Sep 17 00:00:00 2001 From: Marc Bouchenoire Date: Mon, 18 May 2026 09:23:33 +0200 Subject: [PATCH 1/5] Set up new example --- examples/nextjs-code-review/.env.example | 1 + examples/nextjs-code-review/.gitignore | 12 + examples/nextjs-code-review/.prettierrc | 11 + .../nextjs-code-review/liveblocks.config.ts | 20 + examples/nextjs-code-review/next.config.ts | 8 + examples/nextjs-code-review/package-lock.json | 4075 +++++++++++++++++ examples/nextjs-code-review/package.json | 31 + .../nextjs-code-review/postcss.config.mjs | 7 + .../nextjs-code-review/src/app/Providers.tsx | 39 + .../src/app/api/liveblocks-auth/route.ts | 34 + .../src/app/api/users/route.ts | 20 + .../src/app/api/users/search/route.ts | 20 + .../nextjs-code-review/src/app/layout.tsx | 33 + examples/nextjs-code-review/src/app/page.tsx | 42 + .../src/components/CodeReview.tsx | 83 + .../src/components/FileDiffSection.tsx | 195 + .../src/components/InlineComposer.tsx | 44 + .../src/components/Loading.tsx | 11 + examples/nextjs-code-review/src/database.ts | 78 + examples/nextjs-code-review/src/pr-data.ts | 203 + .../nextjs-code-review/src/styles/globals.css | 19 + examples/nextjs-code-review/tsconfig.json | 43 + examples/nextjs-code-review/vercel.json | 4 + 23 files changed, 5033 insertions(+) create mode 100644 examples/nextjs-code-review/.env.example create mode 100644 examples/nextjs-code-review/.gitignore create mode 100644 examples/nextjs-code-review/.prettierrc create mode 100644 examples/nextjs-code-review/liveblocks.config.ts create mode 100644 examples/nextjs-code-review/next.config.ts create mode 100644 examples/nextjs-code-review/package-lock.json create mode 100644 examples/nextjs-code-review/package.json create mode 100644 examples/nextjs-code-review/postcss.config.mjs create mode 100644 examples/nextjs-code-review/src/app/Providers.tsx create mode 100644 examples/nextjs-code-review/src/app/api/liveblocks-auth/route.ts create mode 100644 examples/nextjs-code-review/src/app/api/users/route.ts create mode 100644 examples/nextjs-code-review/src/app/api/users/search/route.ts create mode 100644 examples/nextjs-code-review/src/app/layout.tsx create mode 100644 examples/nextjs-code-review/src/app/page.tsx create mode 100644 examples/nextjs-code-review/src/components/CodeReview.tsx create mode 100644 examples/nextjs-code-review/src/components/FileDiffSection.tsx create mode 100644 examples/nextjs-code-review/src/components/InlineComposer.tsx create mode 100644 examples/nextjs-code-review/src/components/Loading.tsx create mode 100644 examples/nextjs-code-review/src/database.ts create mode 100644 examples/nextjs-code-review/src/pr-data.ts create mode 100644 examples/nextjs-code-review/src/styles/globals.css create mode 100644 examples/nextjs-code-review/tsconfig.json create mode 100644 examples/nextjs-code-review/vercel.json diff --git a/examples/nextjs-code-review/.env.example b/examples/nextjs-code-review/.env.example new file mode 100644 index 0000000000..15b93b52c3 --- /dev/null +++ b/examples/nextjs-code-review/.env.example @@ -0,0 +1 @@ +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-code-review/.gitignore b/examples/nextjs-code-review/.gitignore new file mode 100644 index 0000000000..3a68e0cfc9 --- /dev/null +++ b/examples/nextjs-code-review/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +node_modules +.env +.env.* +!.env.example +*.tsbuildinfo +.vercel +.next +out +next-env.d.ts +# Turborepo +.turbo diff --git a/examples/nextjs-code-review/.prettierrc b/examples/nextjs-code-review/.prettierrc new file mode 100644 index 0000000000..0699872430 --- /dev/null +++ b/examples/nextjs-code-review/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "tabWidth": 2, + "useTabs": false, + "singleQuote": false, + "jsxSingleQuote": false, + "arrowParens": "always", + "bracketSpacing": true, + "bracketSameLine": false, + "trailingComma": "es5" +} diff --git a/examples/nextjs-code-review/liveblocks.config.ts b/examples/nextjs-code-review/liveblocks.config.ts new file mode 100644 index 0000000000..78a31957bc --- /dev/null +++ b/examples/nextjs-code-review/liveblocks.config.ts @@ -0,0 +1,20 @@ +declare global { + interface Liveblocks { + UserMeta: { + id: string; + info: { + name: string; + avatar: string; + color: string; + }; + }; + + ThreadMetadata: { + filePath: string; + lineNumber: number; + side: "deletions" | "additions"; + }; + } +} + +export {}; diff --git a/examples/nextjs-code-review/next.config.ts b/examples/nextjs-code-review/next.config.ts new file mode 100644 index 0000000000..76ffe244df --- /dev/null +++ b/examples/nextjs-code-review/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + turbopack: { root: __dirname }, + reactStrictMode: true, +}; + +export default nextConfig; diff --git a/examples/nextjs-code-review/package-lock.json b/examples/nextjs-code-review/package-lock.json new file mode 100644 index 0000000000..457d84dd50 --- /dev/null +++ b/examples/nextjs-code-review/package-lock.json @@ -0,0 +1,4075 @@ +{ + "name": "@liveblocks-examples/nextjs-code-review", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@liveblocks-examples/nextjs-code-review", + "license": "Apache-2.0", + "dependencies": { + "@liveblocks/client": "^3.18.4", + "@liveblocks/node": "^3.18.4", + "@liveblocks/react": "^3.18.4", + "@liveblocks/react-ui": "^3.18.4", + "@pierre/diffs": "^1.1.22", + "@tailwindcss/postcss": "^4.3.0", + "next": "^16.1.6", + "postcss": "^8.5.14", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-error-boundary": "^4.0.13", + "tailwindcss": "^4.3.0" + }, + "devDependencies": { + "@types/node": "^20.4.10", + "@types/react": "^18.3.27", + "prettier": "^3.3.3", + "typescript": "^5.4.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" + }, + "node_modules/@liveblocks/client": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/@liveblocks/client/-/client-3.19.1.tgz", + "integrity": "sha512-+h645g0o7jCYhkg6b3P8rR8iOacNfIre+GqBkP8G/7WuSMquiEkwW4Ub+N5F74wqC+ssuQWLo6QdJ0ICkWlI5w==", + "dependencies": { + "@liveblocks/core": "3.19.1" + } + }, + "node_modules/@liveblocks/core": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/@liveblocks/core/-/core-3.19.1.tgz", + "integrity": "sha512-OPRPJLq/TkFatL+5ECZQaAKijwCF9j9pQDC9MZGBzD9suvcJ9/rvnkYDVko6JI9HKpLmvkPQJNxC13IpsPUAtw==", + "peerDependencies": { + "@types/json-schema": "^7" + } + }, + "node_modules/@liveblocks/node": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/@liveblocks/node/-/node-3.19.1.tgz", + "integrity": "sha512-7N5u6inEos5OgBa5DOnrQvIn6p9PimsKOFpJx1NQEkmsLT/5hh2XyWU53ENE3Txpi6d9HySKHQ9+4k8PsscDPQ==", + "dependencies": { + "@liveblocks/core": "3.19.1", + "@stablelib/base64": "^1.0.1", + "fast-sha256": "^1.3.0", + "marked": "^15.0.11", + "node-fetch": "^2.6.1" + } + }, + "node_modules/@liveblocks/react": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/@liveblocks/react/-/react-3.19.1.tgz", + "integrity": "sha512-torDKWpXqyu7F/YpsCNTREzZtQVaZq7NzXW9czMFRJtLTjgyGU5H+5WbtRLvnZpEnYtIAerl19qBWUs7I+IE4w==", + "dependencies": { + "@liveblocks/client": "3.19.1", + "@liveblocks/core": "3.19.1" + }, + "peerDependencies": { + "@types/react": "^18 || ^19", + "@types/react-dom": "^18 || ^19", + "react": "^18 || ^19 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/@liveblocks/react-ui/-/react-ui-3.19.1.tgz", + "integrity": "sha512-65AfVSh7VbayPv6iosTl5UVC5mCYJH9kjTOMlzyYpHtQ2XW3VCn7s1/MmsN206qtq+0qZYoVy9CNocUx6KXoMA==", + "dependencies": { + "@floating-ui/react-dom": "^2.1.0", + "@liveblocks/client": "3.19.1", + "@liveblocks/core": "3.19.1", + "@liveblocks/react": "3.19.1", + "frimousse": "^0.2.0", + "marked": "^15.0.11", + "radix-ui": "^1.4.0", + "slate": "^0.110.2", + "slate-history": "^0.110.3", + "slate-hyperscript": "^0.100.0", + "slate-react": "^0.110.3" + }, + "peerDependencies": { + "@types/react": "^18 || ^19", + "@types/react-dom": "^18 || ^19", + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui/node_modules/radix-ui/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@next/env": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@pierre/diffs": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/@pierre/diffs/-/diffs-1.1.22.tgz", + "integrity": "sha512-1Iv7kdl6OABFCd1n2HQbGUiRHouXPaHoIjcb7Lwg8zeJKY5ph+cESFcGEyIiwW0NCbKGtYS2bTnXmI+Eze5dwg==", + "dependencies": { + "@pierre/theme": "0.0.28", + "@shikijs/transformers": "^3.0.0", + "diff": "8.0.3", + "hast-util-to-html": "9.0.5", + "lru_map": "0.4.1", + "shiki": "^3.0.0" + }, + "peerDependencies": { + "react": "^18.3.1 || ^19.0.0", + "react-dom": "^18.3.1 || ^19.0.0" + } + }, + "node_modules/@pierre/theme": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/@pierre/theme/-/theme-0.0.28.tgz", + "integrity": "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw==", + "engines": { + "vscode": "^1.0.0" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" + }, + "node_modules/@shikijs/core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.23.0.tgz", + "integrity": "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==" + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.0.tgz", + "integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "postcss": "^8.5.10", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "peer": true + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/direction": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz", + "integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + }, + "node_modules/frimousse": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/frimousse/-/frimousse-0.2.0.tgz", + "integrity": "sha512-viSrsVQWKR4Q7xzC0lkx3Wu9i1+IHrth0QXn0nlIIJXpltwUnjkGXSTuoW7WHI5aJ4z49WR8E/pyQizFjlNtTA==", + "workspaces": [ + ".", + "site" + ], + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/is-hotkey": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz", + "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==" + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru_map": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.4.1.tgz", + "integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", + "dependencies": { + "@next/env": "16.2.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.6", + "@next/swc-darwin-x64": "16.2.6", + "@next/swc-linux-arm64-gnu": "16.2.6", + "@next/swc-linux-arm64-musl": "16.2.6", + "@next/swc-linux-x64-gnu": "16.2.6", + "@next/swc-linux-x64-musl": "16.2.6", + "@next/swc-win32-arm64-msvc": "16.2.6", + "@next/swc-win32-x64-msvc": "16.2.6", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-error-boundary": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", + "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/engine-javascript": "3.23.0", + "@shikijs/engine-oniguruma": "3.23.0", + "@shikijs/langs": "3.23.0", + "@shikijs/themes": "3.23.0", + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/slate": { + "version": "0.110.2", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.110.2.tgz", + "integrity": "sha512-4xGULnyMCiEQ0Ml7JAC1A6HVE6MNpPJU7Eq4cXh1LxlrR0dFXC3XC+rNfQtUJ7chHoPkws57x7DDiWiZAt+PBA==", + "dependencies": { + "immer": "^10.0.3", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, + "node_modules/slate-history": { + "version": "0.110.3", + "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.110.3.tgz", + "integrity": "sha512-sgdff4Usdflmw5ZUbhDkxFwCBQ2qlDKMMkF93w66KdV48vHOgN2BmLrf+2H8SdX8PYIpP/cTB0w8qWC2GwhDVA==", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, + "node_modules/slate-hyperscript": { + "version": "0.100.0", + "resolved": "https://registry.npmjs.org/slate-hyperscript/-/slate-hyperscript-0.100.0.tgz", + "integrity": "sha512-fb2KdAYg6RkrQGlqaIi4wdqz3oa0S4zKNBJlbnJbNOwa23+9FLD6oPVx9zUGqCSIpy+HIpOeqXrg0Kzwh/Ii4A==", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, + "node_modules/slate-react": { + "version": "0.110.3", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.110.3.tgz", + "integrity": "sha512-AS8PPjwmsFS3Lq0MOEegLVlFoxhyos68G6zz2nW4sh3WeTXV7pX0exnwtY1a/docn+J3LGQO11aZXTenPXA/kg==", + "dependencies": { + "@juggle/resize-observer": "^3.4.0", + "direction": "^1.0.4", + "is-hotkey": "^0.2.0", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.21", + "scroll-into-view-if-needed": "^3.1.0", + "tiny-invariant": "1.3.1" + }, + "peerDependencies": { + "react": ">=18.2.0", + "react-dom": ">=18.2.0", + "slate": ">=0.99.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/examples/nextjs-code-review/package.json b/examples/nextjs-code-review/package.json new file mode 100644 index 0000000000..66204582e9 --- /dev/null +++ b/examples/nextjs-code-review/package.json @@ -0,0 +1,31 @@ +{ + "name": "@liveblocks-examples/nextjs-code-review", + "description": "This example shows how to build a GitHub-like code review experience with Liveblocks and Next.js.", + "license": "Apache-2.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@liveblocks/client": "^3.18.4", + "@liveblocks/node": "^3.18.4", + "@liveblocks/react": "^3.18.4", + "@liveblocks/react-ui": "^3.18.4", + "@pierre/diffs": "^1.1.22", + "@tailwindcss/postcss": "^4.3.0", + "next": "^16.1.6", + "postcss": "^8.5.14", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-error-boundary": "^4.0.13", + "tailwindcss": "^4.3.0" + }, + "devDependencies": { + "@types/node": "^20.4.10", + "@types/react": "^18.3.27", + "prettier": "^3.3.3", + "typescript": "^5.4.5" + } +} diff --git a/examples/nextjs-code-review/postcss.config.mjs b/examples/nextjs-code-review/postcss.config.mjs new file mode 100644 index 0000000000..61e36849cf --- /dev/null +++ b/examples/nextjs-code-review/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/examples/nextjs-code-review/src/app/Providers.tsx b/examples/nextjs-code-review/src/app/Providers.tsx new file mode 100644 index 0000000000..ab514ed167 --- /dev/null +++ b/examples/nextjs-code-review/src/app/Providers.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { LiveblocksProvider } from "@liveblocks/react"; +import { PropsWithChildren, Suspense } from "react"; + +export function Providers({ children }: PropsWithChildren) { + return ( + { + const searchParams = new URLSearchParams( + userIds.map((userId) => ["userIds", userId]) + ); + const response = await fetch(`/api/users?${searchParams}`); + + if (!response.ok) { + throw new Error("Problem resolving users"); + } + + const users = await response.json(); + return users; + }} + resolveMentionSuggestions={async ({ text }) => { + const response = await fetch( + `/api/users/search?text=${encodeURIComponent(text)}` + ); + + if (!response.ok) { + throw new Error("Problem resolving mention suggestions"); + } + + const userIds = await response.json(); + return userIds; + }} + > + {children} + + ); +} diff --git a/examples/nextjs-code-review/src/app/api/liveblocks-auth/route.ts b/examples/nextjs-code-review/src/app/api/liveblocks-auth/route.ts new file mode 100644 index 0000000000..19bb7da2fe --- /dev/null +++ b/examples/nextjs-code-review/src/app/api/liveblocks-auth/route.ts @@ -0,0 +1,34 @@ +import { Liveblocks } from "@liveblocks/node"; +import { getRandomUser } from "@/database"; +import { NextRequest, NextResponse } from "next/server"; + +/** + * Authenticating your Liveblocks application + * https://liveblocks.io/docs/authentication + */ + +const liveblocks = new Liveblocks({ + secret: process.env.LIVEBLOCKS_SECRET_KEY!, +}); + +export async function POST(request: NextRequest) { + if (!process.env.LIVEBLOCKS_SECRET_KEY) { + return new NextResponse("Missing LIVEBLOCKS_SECRET_KEY", { status: 403 }); + } + + // Get the current user's unique id and info from your database + const user = getRandomUser(); + + // Create a session for the current user (access token auth) + // userInfo is made available in Liveblocks user hooks, e.g. useSelf + const session = liveblocks.prepareSession(user.id, { + userInfo: user.info, + }); + + // Use a naming pattern to allow access to rooms with a wildcard + session.allow(`liveblocks:examples:*`, session.FULL_ACCESS); + + // Authorize the user and return the result + const { status, body } = await session.authorize(); + return new NextResponse(body, { status }); +} diff --git a/examples/nextjs-code-review/src/app/api/users/route.ts b/examples/nextjs-code-review/src/app/api/users/route.ts new file mode 100644 index 0000000000..77c2fd40e7 --- /dev/null +++ b/examples/nextjs-code-review/src/app/api/users/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getUser } from "@/database"; + +/** + * Get users' info from their ID + * For `resolveUsers` in liveblocks.config.ts + */ + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const userIds = searchParams.getAll("userIds"); + + if (!userIds || !Array.isArray(userIds)) { + return new NextResponse("Missing or invalid userIds", { status: 400 }); + } + + return NextResponse.json( + userIds.map((userId) => getUser(userId)?.info || null) + ); +} diff --git a/examples/nextjs-code-review/src/app/api/users/search/route.ts b/examples/nextjs-code-review/src/app/api/users/search/route.ts new file mode 100644 index 0000000000..07bfc3ba72 --- /dev/null +++ b/examples/nextjs-code-review/src/app/api/users/search/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getUsers } from "@/database"; + +/** + * Returns a list of user IDs from a partial search input + * For `resolveMentionSuggestions` in liveblocks.config.ts + */ + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const text = searchParams.get("text"); + + const filteredUserIds = getUsers() + .filter((user) => + text ? user.info.name.toLowerCase().includes(text.toLowerCase()) : true + ) + .map((user) => user.id); + + return NextResponse.json(filteredUserIds); +} diff --git a/examples/nextjs-code-review/src/app/layout.tsx b/examples/nextjs-code-review/src/app/layout.tsx new file mode 100644 index 0000000000..2919d34ea8 --- /dev/null +++ b/examples/nextjs-code-review/src/app/layout.tsx @@ -0,0 +1,33 @@ +import "../styles/globals.css"; +import { Providers } from "./Providers"; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + Liveblocks + + + + + + + {children} + + + ); +} diff --git a/examples/nextjs-code-review/src/app/page.tsx b/examples/nextjs-code-review/src/app/page.tsx new file mode 100644 index 0000000000..f01b8c6b72 --- /dev/null +++ b/examples/nextjs-code-review/src/app/page.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useMemo } from "react"; +import { useSearchParams } from "next/navigation"; +import { RoomProvider } from "@liveblocks/react/suspense"; +import { ClientSideSuspense } from "@liveblocks/react"; +import { ErrorBoundary } from "react-error-boundary"; +import { CodeReview } from "../components/CodeReview"; +import { Loading } from "../components/Loading"; + +export default function Page() { + const roomId = useExampleRoomId("liveblocks:examples:nextjs-code-review"); + + return ( + + There was an error while getting threads. + } + > + }> + + + + + ); +} + +/** + * This function is used when deploying an example on liveblocks.io. + * You can ignore it completely if you run the example locally. + */ +function useExampleRoomId(roomId: string) { + const params = useSearchParams(); + const exampleId = params?.get("exampleId"); + + const exampleRoomId = useMemo(() => { + return exampleId ? `${roomId}-${exampleId}` : roomId; + }, [roomId, exampleId]); + + return exampleRoomId; +} diff --git a/examples/nextjs-code-review/src/components/CodeReview.tsx b/examples/nextjs-code-review/src/components/CodeReview.tsx new file mode 100644 index 0000000000..8658b759ac --- /dev/null +++ b/examples/nextjs-code-review/src/components/CodeReview.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useState } from "react"; +import { useThreads, useCreateThread } from "@liveblocks/react/suspense"; +import type { ComposerSubmitComment } from "@liveblocks/react-ui"; +import type { AnnotationSide } from "@pierre/diffs/react"; +import { PR_FILES, PR_TITLE, PR_BRANCH, PR_BASE } from "../pr-data"; +import { FileDiffSection } from "./FileDiffSection"; + +interface PendingComposer { + filePath: string; + lineNumber: number; + side: AnnotationSide; +} + +export function CodeReview() { + const { threads } = useThreads(); + const createThread = useCreateThread(); + const [pendingComposer, setPendingComposer] = + useState(null); + + function handleCreateThread( + filePath: string, + lineNumber: number, + side: AnnotationSide, + body: ComposerSubmitComment["body"] + ) { + createThread({ + body, + metadata: { filePath, lineNumber, side }, + }); + setPendingComposer(null); + } + + return ( +
+
+
+

+ {PR_TITLE} +

+ + Open + +
+
+ + + + {PR_BRANCH} + + + + {PR_BASE} + + + + {PR_FILES.length} file{PR_FILES.length !== 1 ? "s" : ""} changed + + + {threads.length} comment{threads.length !== 1 ? "s" : ""} + +
+
+ +
+ {PR_FILES.map((file) => ( + t.metadata.filePath === file.path + )} + pendingComposer={pendingComposer} + onOpenComposer={setPendingComposer} + onCloseComposer={() => setPendingComposer(null)} + onCreateThread={handleCreateThread} + /> + ))} +
+
+ ); +} diff --git a/examples/nextjs-code-review/src/components/FileDiffSection.tsx b/examples/nextjs-code-review/src/components/FileDiffSection.tsx new file mode 100644 index 0000000000..a9de60ed16 --- /dev/null +++ b/examples/nextjs-code-review/src/components/FileDiffSection.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { useState } from "react"; +import { Thread } from "@liveblocks/react-ui"; +import type { ComposerSubmitComment } from "@liveblocks/react-ui"; +import type { ThreadData } from "@liveblocks/client"; +import { + MultiFileDiff, + GutterUtilitySlotStyles, + type AnnotationSide, + type DiffLineAnnotation, +} from "@pierre/diffs/react"; +import type { PrFile } from "../pr-data"; +import { InlineComposer } from "./InlineComposer"; + +type AnnotationMetadata = + | { type: "thread"; threadId: string } + | { type: "composer" }; + +interface PendingComposer { + filePath: string; + lineNumber: number; + side: AnnotationSide; +} + +interface Props { + file: PrFile; + threads: ThreadData[]; + pendingComposer: PendingComposer | null; + onOpenComposer: (composer: PendingComposer) => void; + onCloseComposer: () => void; + onCreateThread: ( + filePath: string, + lineNumber: number, + side: AnnotationSide, + body: ComposerSubmitComment["body"] + ) => void; +} + +export function FileDiffSection({ + file, + threads, + pendingComposer, + onOpenComposer, + onCloseComposer, + onCreateThread, +}: Props) { + const [collapsed, setCollapsed] = useState(false); + + const fileThreads = threads.filter( + (t) => t.metadata.filePath === file.path + ); + + const stats = computeStats(file.oldContent, file.newContent); + + const lineAnnotations: DiffLineAnnotation[] = [ + ...fileThreads.map((thread) => ({ + side: thread.metadata.side as AnnotationSide, + lineNumber: thread.metadata.lineNumber, + metadata: { type: "thread" as const, threadId: thread.id }, + })), + ...(pendingComposer?.filePath === file.path + ? [ + { + side: pendingComposer.side, + lineNumber: pendingComposer.lineNumber, + metadata: { type: "composer" as const }, + }, + ] + : []), + ]; + + return ( +
+
setCollapsed((c) => !c)} + > + + {collapsed ? "▶" : "▼"} + + + {file.path} + + {file.status === "added" && ( + + New + + )} + + + +{stats.additions} + + + -{stats.deletions} + + + {fileThreads.length > 0 && ( + + {fileThreads.length} comment{fileThreads.length !== 1 ? "s" : ""} + + )} +
+ + {!collapsed && ( + { + const { metadata } = annotation; + if (metadata.type === "composer") { + if (!pendingComposer) return null; + return ( + + ); + } + + const thread = fileThreads.find( + (t) => t.id === metadata.threadId + ); + if (!thread) return null; + return ( +
+
+ +
+
+ ); + }} + renderGutterUtility={(getHoveredLine) => ( + + )} + /> + )} +
+ ); +} + +function computeStats( + oldContent: string, + newContent: string +): { additions: number; deletions: number } { + if (!oldContent) { + return { additions: newContent.split("\n").length, deletions: 0 }; + } + if (!newContent) { + return { additions: 0, deletions: oldContent.split("\n").length }; + } + + const oldLines = new Set(oldContent.split("\n")); + const newLines = new Set(newContent.split("\n")); + + let additions = 0; + let deletions = 0; + + for (const line of newContent.split("\n")) { + if (!oldLines.has(line)) additions++; + } + for (const line of oldContent.split("\n")) { + if (!newLines.has(line)) deletions++; + } + + return { additions, deletions }; +} diff --git a/examples/nextjs-code-review/src/components/InlineComposer.tsx b/examples/nextjs-code-review/src/components/InlineComposer.tsx new file mode 100644 index 0000000000..cdfc96259e --- /dev/null +++ b/examples/nextjs-code-review/src/components/InlineComposer.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { Composer } from "@liveblocks/react-ui"; +import type { ComposerSubmitComment } from "@liveblocks/react-ui"; +import type { AnnotationSide } from "@pierre/diffs/react"; + +interface Props { + filePath: string; + lineNumber: number; + side: AnnotationSide; + onSubmit: ( + filePath: string, + lineNumber: number, + side: AnnotationSide, + body: ComposerSubmitComment["body"] + ) => void; + onClose: () => void; +} + +export function InlineComposer({ + filePath, + lineNumber, + side, + onSubmit, + onClose, +}: Props) { + return ( +
+ { + event.preventDefault(); + onSubmit(filePath, lineNumber, side, body); + }} + /> + +
+ ); +} diff --git a/examples/nextjs-code-review/src/components/Loading.tsx b/examples/nextjs-code-review/src/components/Loading.tsx new file mode 100644 index 0000000000..7c062ba191 --- /dev/null +++ b/examples/nextjs-code-review/src/components/Loading.tsx @@ -0,0 +1,11 @@ +export function Loading() { + return ( +
+ Loading +
+ ); +} diff --git a/examples/nextjs-code-review/src/database.ts b/examples/nextjs-code-review/src/database.ts new file mode 100644 index 0000000000..8978a1a009 --- /dev/null +++ b/examples/nextjs-code-review/src/database.ts @@ -0,0 +1,78 @@ +const USER_INFO: Liveblocks["UserMeta"][] = [ + { + id: "charlie.layne@example.com", + info: { + name: "Charlie Layne", + color: "#D583F0", + avatar: "https://liveblocks.io/avatars/avatar-1.png", + }, + }, + { + id: "mislav.abha@example.com", + info: { + name: "Mislav Abha", + color: "#F08385", + avatar: "https://liveblocks.io/avatars/avatar-2.png", + }, + }, + { + id: "tatum.paolo@example.com", + info: { + name: "Tatum Paolo", + color: "#F0D885", + avatar: "https://liveblocks.io/avatars/avatar-3.png", + }, + }, + { + id: "anjali.wanda@example.com", + info: { + name: "Anjali Wanda", + color: "#85EED6", + avatar: "https://liveblocks.io/avatars/avatar-4.png", + }, + }, + { + id: "jody.hekla@example.com", + info: { + name: "Jody Hekla", + color: "#85BBF0", + avatar: "https://liveblocks.io/avatars/avatar-5.png", + }, + }, + { + id: "emil.joyce@example.com", + info: { + name: "Emil Joyce", + color: "#8594F0", + avatar: "https://liveblocks.io/avatars/avatar-6.png", + }, + }, + { + id: "jory.quispe@example.com", + info: { + name: "Jory Quispe", + color: "#85DBF0", + avatar: "https://liveblocks.io/avatars/avatar-7.png", + }, + }, + { + id: "quinn.elton@example.com", + info: { + name: "Quinn Elton", + color: "#87EE85", + avatar: "https://liveblocks.io/avatars/avatar-8.png", + }, + }, +]; + +export function getRandomUser() { + return USER_INFO[Math.floor(Math.random() * USER_INFO.length)]; +} + +export function getUser(id: string) { + return USER_INFO.find((u) => u.id === id) || null; +} + +export function getUsers() { + return USER_INFO; +} diff --git a/examples/nextjs-code-review/src/pr-data.ts b/examples/nextjs-code-review/src/pr-data.ts new file mode 100644 index 0000000000..8103bf784d --- /dev/null +++ b/examples/nextjs-code-review/src/pr-data.ts @@ -0,0 +1,203 @@ +export interface PrFile { + path: string; + status: "added" | "modified"; + oldContent: string; + newContent: string; +} + +export const PR_TITLE = "feat: Add JWT-based authentication"; +export const PR_BRANCH = "feat/jwt-auth"; +export const PR_BASE = "main"; +export const PR_AUTHOR = "mislav.abha@example.com"; + +export const PR_FILES: PrFile[] = [ + { + path: "src/lib/auth.ts", + status: "added", + oldContent: "", + newContent: `import { SignJWT, jwtVerify } from "jose"; + +const SECRET = new TextEncoder().encode( + process.env.JWT_SECRET ?? "dev-secret-change-in-production" +); + +export interface TokenPayload { + userId: string; + email: string; + role: "admin" | "user"; +} + +export async function signToken(payload: TokenPayload): Promise { + return new SignJWT({ ...payload }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("7d") + .sign(SECRET); +} + +export async function verifyToken( + token: string +): Promise { + try { + const { payload } = await jwtVerify(token, SECRET); + return payload as unknown as TokenPayload; + } catch { + return null; + } +} + +export function parseAuthHeader(header: string | null): string | null { + if (!header?.startsWith("Bearer ")) return null; + return header.slice(7); +} +`, + }, + { + path: "src/components/LoginForm.tsx", + status: "added", + oldContent: "", + newContent: `"use client"; + +import { useState } from "react"; + +interface Props { + onSuccess: (token: string) => void; +} + +export function LoginForm({ onSuccess }: Props) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + setLoading(true); + setError(null); + + try { + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + const data = await response.json(); + setError(data.error ?? "Login failed"); + return; + } + + const { token } = await response.json(); + onSuccess(token); + } catch { + setError("An unexpected error occurred"); + } finally { + setLoading(false); + } + } + + return ( +
+

Sign in

+ + + {error !== null &&

{error}

} + +
+ ); +} +`, + }, + { + path: "src/middleware.ts", + status: "modified", + oldContent: `import { NextRequest, NextResponse } from "next/server"; + +const PUBLIC_PATHS = ["/login", "/api/auth"]; + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + if (PUBLIC_PATHS.some((path) => pathname.startsWith(path))) { + return NextResponse.next(); + } + + const session = request.cookies.get("session")?.value; + + if (!session) { + return NextResponse.redirect(new URL("/login", request.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/((?!_next|favicon.ico).*)"], +}; +`, + newContent: `import { NextRequest, NextResponse } from "next/server"; +import { parseAuthHeader, verifyToken } from "@/lib/auth"; + +const PUBLIC_PATHS = ["/login", "/api/auth"]; + +export async function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + if (PUBLIC_PATHS.some((path) => pathname.startsWith(path))) { + return NextResponse.next(); + } + + const token = + parseAuthHeader(request.headers.get("authorization")) ?? + request.cookies.get("token")?.value; + + if (!token) { + return NextResponse.redirect(new URL("/login", request.url)); + } + + const payload = await verifyToken(token); + + if (!payload) { + const response = NextResponse.redirect(new URL("/login", request.url)); + response.cookies.delete("token"); + return response; + } + + const requestHeaders = new Headers(request.headers); + requestHeaders.set("x-user-id", payload.userId); + requestHeaders.set("x-user-role", payload.role); + + return NextResponse.next({ + request: { headers: requestHeaders }, + }); +} + +export const config = { + matcher: ["/((?!_next|favicon.ico).*)"], +}; +`, + }, +]; diff --git a/examples/nextjs-code-review/src/styles/globals.css b/examples/nextjs-code-review/src/styles/globals.css new file mode 100644 index 0000000000..4783e7e5a7 --- /dev/null +++ b/examples/nextjs-code-review/src/styles/globals.css @@ -0,0 +1,19 @@ +@import "tailwindcss"; +@import "@liveblocks/react-ui/styles.css"; +@import "@liveblocks/react-ui/styles/dark/media-query.css"; + +html, +body { + margin: 0; + padding: 0; +} + +.lb-root { + --lb-accent: #0969da; +} + +@media (prefers-color-scheme: dark) { + .lb-root { + --lb-accent: #58a6ff; + } +} diff --git a/examples/nextjs-code-review/tsconfig.json b/examples/nextjs-code-review/tsconfig.json new file mode 100644 index 0000000000..d9fed622cc --- /dev/null +++ b/examples/nextjs-code-review/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "baseUrl": ".", + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/nextjs-code-review/vercel.json b/examples/nextjs-code-review/vercel.json new file mode 100644 index 0000000000..5d7edc9113 --- /dev/null +++ b/examples/nextjs-code-review/vercel.json @@ -0,0 +1,4 @@ +{ + "installCommand": "npm install", + "buildCommand": "npm run build" +} From 8779d16352fedc48ed881e005a1a10104e1595bc Mon Sep 17 00:00:00 2001 From: Marc Bouchenoire Date: Mon, 18 May 2026 09:23:33 +0200 Subject: [PATCH 2/5] Move to `CodeView` and improve annotations --- .../nextjs-code-review/liveblocks.config.ts | 4 +- examples/nextjs-code-review/package-lock.json | 39 +- examples/nextjs-code-review/package.json | 3 +- .../nextjs-code-review/src/annotationUtils.ts | 89 +++ .../src/components/CodeReview.tsx | 568 ++++++++++++++++-- .../src/components/FileDiffSection.tsx | 195 ------ .../src/components/InlineComposer.tsx | 21 +- .../nextjs-code-review/src/searchUtils.ts | 23 + 8 files changed, 666 insertions(+), 276 deletions(-) create mode 100644 examples/nextjs-code-review/src/annotationUtils.ts delete mode 100644 examples/nextjs-code-review/src/components/FileDiffSection.tsx create mode 100644 examples/nextjs-code-review/src/searchUtils.ts diff --git a/examples/nextjs-code-review/liveblocks.config.ts b/examples/nextjs-code-review/liveblocks.config.ts index 78a31957bc..84ba390cde 100644 --- a/examples/nextjs-code-review/liveblocks.config.ts +++ b/examples/nextjs-code-review/liveblocks.config.ts @@ -11,8 +11,10 @@ declare global { ThreadMetadata: { filePath: string; + lineContent: string; + contextBefore: string; + contextAfter: string; lineNumber: number; - side: "deletions" | "additions"; }; } } diff --git a/examples/nextjs-code-review/package-lock.json b/examples/nextjs-code-review/package-lock.json index 457d84dd50..b3884ac2e1 100644 --- a/examples/nextjs-code-review/package-lock.json +++ b/examples/nextjs-code-review/package-lock.json @@ -11,7 +11,8 @@ "@liveblocks/node": "^3.18.4", "@liveblocks/react": "^3.18.4", "@liveblocks/react-ui": "^3.18.4", - "@pierre/diffs": "^1.1.22", + "@pierre/diffs": "1.2.0-beta.6", + "@pierre/trees": "1.0.0-beta.3", "@tailwindcss/postcss": "^4.3.0", "next": "^16.1.6", "postcss": "^8.5.14", @@ -2047,9 +2048,9 @@ } }, "node_modules/@pierre/diffs": { - "version": "1.1.22", - "resolved": "https://registry.npmjs.org/@pierre/diffs/-/diffs-1.1.22.tgz", - "integrity": "sha512-1Iv7kdl6OABFCd1n2HQbGUiRHouXPaHoIjcb7Lwg8zeJKY5ph+cESFcGEyIiwW0NCbKGtYS2bTnXmI+Eze5dwg==", + "version": "1.2.0-beta.6", + "resolved": "https://registry.npmjs.org/@pierre/diffs/-/diffs-1.2.0-beta.6.tgz", + "integrity": "sha512-34Xpk0NjBnH6O6nzFHfAICA7T0nolfN1+5PxlrKmzLzuhDT86kdLgm2E/NTywAEIKKzViNvTMyxvbQ6H+RZWMQ==", "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", @@ -2071,6 +2072,19 @@ "vscode": "^1.0.0" } }, + "node_modules/@pierre/trees": { + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@pierre/trees/-/trees-1.0.0-beta.3.tgz", + "integrity": "sha512-gfV7V1AoceIwTSFwiiWl/89gNtJROyo2dFeYYuAkT4F3AbE+ajCIGZEICBw1ygmVxVetF9Kq1Xpjz8BhXXZwTQ==", + "dependencies": { + "preact": "11.0.0-beta.0", + "preact-render-to-string": "6.6.5" + }, + "peerDependencies": { + "react": "^18.3.1 || ^19.0.0", + "react-dom": "^18.3.1 || ^19.0.0" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -3510,6 +3524,23 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "11.0.0-beta.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-11.0.0-beta.0.tgz", + "integrity": "sha512-IcODoASASYwJ9kxz7+MJeiJhvLriwSb4y4mHIyxdgaRZp6kPUud7xytrk/6GZw8U3y6EFJaRb5wi9SrEK+8+lg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.6.5.tgz", + "integrity": "sha512-O6MHzYNIKYaiSX3bOw0gGZfEbOmlIDtDfWwN1JJdc/T3ihzRT6tGGSEWE088dWrEDGa1u7101q+6fzQnO9XCPA==", + "peerDependencies": { + "preact": ">=10 || >= 11.0.0-0" + } + }, "node_modules/prettier": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", diff --git a/examples/nextjs-code-review/package.json b/examples/nextjs-code-review/package.json index 66204582e9..f8dc06d91d 100644 --- a/examples/nextjs-code-review/package.json +++ b/examples/nextjs-code-review/package.json @@ -13,7 +13,8 @@ "@liveblocks/node": "^3.18.4", "@liveblocks/react": "^3.18.4", "@liveblocks/react-ui": "^3.18.4", - "@pierre/diffs": "^1.1.22", + "@pierre/diffs": "1.2.0-beta.6", + "@pierre/trees": "1.0.0-beta.3", "@tailwindcss/postcss": "^4.3.0", "next": "^16.1.6", "postcss": "^8.5.14", diff --git a/examples/nextjs-code-review/src/annotationUtils.ts b/examples/nextjs-code-review/src/annotationUtils.ts new file mode 100644 index 0000000000..560187af98 --- /dev/null +++ b/examples/nextjs-code-review/src/annotationUtils.ts @@ -0,0 +1,89 @@ +const CONTEXT_LINES = 3; + +interface AnnotationAnchor { + lineContent: string; + contextBefore: string; + contextAfter: string; + lineNumber: number; +} + +export interface ResolvedAnnotation { + lineNumber: number; + side: "additions" | "deletions"; +} + +export function extractLineContext( + content: string, + lineNumber: number +): { lineContent: string; contextBefore: string; contextAfter: string } { + const lines = content.split("\n"); + const idx = lineNumber - 1; + return { + lineContent: lines[idx] ?? "", + contextBefore: lines + .slice(Math.max(0, idx - CONTEXT_LINES), idx) + .join("\n"), + contextAfter: lines + .slice(idx + 1, Math.min(lines.length, idx + 1 + CONTEXT_LINES)) + .join("\n"), + }; +} + +// Searches `lines` for the best match for `anchor` using context scoring. +// Returns a 1-indexed line number, or null if no match. +function findBestMatch( + anchor: AnnotationAnchor, + lines: string[] +): number | null { + const contextBefore = anchor.contextBefore + ? anchor.contextBefore.split("\n") + : []; + const contextAfter = anchor.contextAfter + ? anchor.contextAfter.split("\n") + : []; + + let bestScore = -1; + let bestLine: number | null = null; + + for (let i = 0; i < lines.length; i++) { + if (lines[i] !== anchor.lineContent) continue; + + let score = 0; + for (let j = 0; j < contextBefore.length; j++) { + const idx = i - contextBefore.length + j; + if (idx >= 0 && lines[idx] === contextBefore[j]) score++; + } + for (let j = 0; j < contextAfter.length; j++) { + const idx = i + 1 + j; + if (idx < lines.length && lines[idx] === contextAfter[j]) score++; + } + + // Break ties by proximity to original line number. + const proximity = 1 / (1 + Math.abs(i + 1 - anchor.lineNumber)); + const total = score + proximity; + + if (total > bestScore) { + bestScore = total; + bestLine = i + 1; + } + } + + return bestLine; +} + +// Maps stored anchor metadata to a current { lineNumber, side } by searching +// newContent first (→ additions), then oldContent (→ deletions). +// Returns null when the annotated line no longer exists (outdated). +export function resolveAnnotation( + anchor: AnnotationAnchor, + oldContent: string, + newContent: string +): ResolvedAnnotation | null { + const newLine = findBestMatch(anchor, newContent.split("\n")); + if (newLine !== null) return { lineNumber: newLine, side: "additions" }; + + const oldLine = findBestMatch(anchor, oldContent.split("\n")); + if (oldLine !== null) return { lineNumber: oldLine, side: "deletions" }; + + return null; +} diff --git a/examples/nextjs-code-review/src/components/CodeReview.tsx b/examples/nextjs-code-review/src/components/CodeReview.tsx index 8658b759ac..5f531b125b 100644 --- a/examples/nextjs-code-review/src/components/CodeReview.tsx +++ b/examples/nextjs-code-review/src/components/CodeReview.tsx @@ -1,16 +1,83 @@ "use client"; -import { useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useThreads, useCreateThread } from "@liveblocks/react/suspense"; -import type { ComposerSubmitComment } from "@liveblocks/react-ui"; -import type { AnnotationSide } from "@pierre/diffs/react"; +import { Thread } from "@liveblocks/react-ui"; +import { + parseDiffFromFile, + type DiffLineAnnotation, + type CodeViewDiffItem, + type CodeViewItem, + type CodeViewOptions, + type FileDiffMetadata, + type LineAnnotation, +} from "@pierre/diffs"; +import { CodeView, type CodeViewHandle } from "@pierre/diffs/react"; +import { FileTree, useFileTree } from "@pierre/trees/react"; import { PR_FILES, PR_TITLE, PR_BRANCH, PR_BASE } from "../pr-data"; -import { FileDiffSection } from "./FileDiffSection"; +import { InlineComposer } from "./InlineComposer"; +import { extractLineContext, resolveAnnotation } from "../annotationUtils"; +import { getDiffSearchMatches } from "../searchUtils"; +type AnnotationMetadata = + | { type: "thread"; threadId: string } + | { + type: "composer"; + filePath: string; + lineContent: string; + contextBefore: string; + contextAfter: string; + lineNumber: number; + }; + +// Side is UI-only state: positions the composer in the right diff column. +// It is not stored in ThreadMetadata. interface PendingComposer { filePath: string; + lineContent: string; + contextBefore: string; + contextAfter: string; lineNumber: number; - side: AnnotationSide; + side: "additions" | "deletions"; +} + +type CodeViewInstance = NonNullable< + ReturnType["getInstance"]> +>; + +// Sorted to match the file tree's alphabetical display order so scrolling down +// in the tree always scrolls down in the code view and vice versa. +const SORTED_FILES = [...PR_FILES].sort((a, b) => + a.path.localeCompare(b.path, undefined, { sensitivity: "base" }) +); +const FILE_PATHS = SORTED_FILES.map((f) => f.path); +const GIT_STATUS = SORTED_FILES.map((f) => ({ path: f.path, status: f.status })); +const LAYOUT_PADDING = 11; +const FILE_PATH_SET = new Set(FILE_PATHS); + +const diffCache = new Map(); + +function diffItemId(path: string): string { + return `diff:${path}`; +} + +function getCachedDiff(file: { + path: string; + oldContent: string; + newContent: string; +}): FileDiffMetadata { + const cacheKey = `${file.path}:${file.oldContent.length}:${file.newContent.length}`; + const cached = diffCache.get(cacheKey); + if (cached) return cached; + const diff = { + ...parseDiffFromFile( + { name: file.path, contents: file.oldContent }, + { name: file.path, contents: file.newContent } + ), + cacheKey, + }; + diffCache.set(cacheKey, diff); + return diff; } export function CodeReview() { @@ -18,66 +85,453 @@ export function CodeReview() { const createThread = useCreateThread(); const [pendingComposer, setPendingComposer] = useState(null); + const [fileSearchQuery, setFileSearchQuery] = useState(""); + const [diffSearchVisible, setDiffSearchVisible] = useState(false); + const [diffSearchQuery, setDiffSearchQuery] = useState(""); + const [diffSearchMatchIndex, setDiffSearchMatchIndex] = useState(0); + + const codeViewRef = useRef>(null); + // True while a tree-click scroll animation is in flight; silences handleScroll + // so it doesn't fight the animation by re-selecting an intermediate file. + const programmaticScrollRef = useRef(false); + const programmaticScrollTimerRef = useRef | null>(null); + const diffSearchInputRef = useRef(null); + + const filteredFilePaths = useMemo( + () => + fileSearchQuery + ? FILE_PATHS.filter((p) => + p.toLowerCase().includes(fileSearchQuery.toLowerCase()) + ) + : FILE_PATHS, + [fileSearchQuery] + ); + + const { model: treeModel } = useFileTree({ + flattenEmptyDirectories: true, + gitStatus: GIT_STATUS, + initialExpansion: "open", + itemHeight: 30, + paths: FILE_PATHS, + }); + + useEffect(() => { + treeModel.resetPaths(filteredFilePaths); + }, [treeModel, filteredFilePaths]); + + const threadsByFile = useMemo(() => { + const map = new Map(); + for (const thread of threads) { + const { filePath } = thread.metadata; + if (!filePath) continue; + const group = map.get(filePath); + if (group) { + group.push(thread); + } else { + map.set(filePath, [thread]); + } + } + return map; + }, [threads]); - function handleCreateThread( - filePath: string, - lineNumber: number, - side: AnnotationSide, - body: ComposerSubmitComment["body"] - ) { - createThread({ - body, - metadata: { filePath, lineNumber, side }, + const items = useMemo((): CodeViewDiffItem[] => { + return SORTED_FILES.map((file) => { + const fileDiff = getCachedDiff(file); + const fileThreads = threadsByFile.get(file.path) ?? []; + const annotations: DiffLineAnnotation[] = []; + + for (const thread of fileThreads) { + const resolved = resolveAnnotation( + thread.metadata, + file.oldContent, + file.newContent + ); + if (!resolved) continue; + annotations.push({ + lineNumber: resolved.lineNumber, + side: resolved.side, + metadata: { type: "thread", threadId: thread.id }, + }); + } + + if (pendingComposer?.filePath === file.path) { + annotations.push({ + lineNumber: pendingComposer.lineNumber, + side: pendingComposer.side, + metadata: { + type: "composer", + filePath: file.path, + lineContent: pendingComposer.lineContent, + contextBefore: pendingComposer.contextBefore, + contextAfter: pendingComposer.contextAfter, + lineNumber: pendingComposer.lineNumber, + }, + }); + } + + const versionStr = annotations + .map((a) => + a.metadata?.type === "thread" ? a.metadata.threadId : "composer" + ) + .join(","); + let version = 0; + for (let i = 0; i < versionStr.length; i++) { + version = (version * 31 + versionStr.charCodeAt(i)) >>> 0; + } + + return { + id: diffItemId(file.path), + type: "diff", + fileDiff, + annotations, + version, + }; }); - setPendingComposer(null); + }, [threadsByFile, pendingComposer]); + + const diffSearchMatches = useMemo( + () => getDiffSearchMatches(diffSearchQuery, items), + [diffSearchQuery, items] + ); + + useEffect(() => { + setDiffSearchMatchIndex(0); + }, [diffSearchQuery]); + + useEffect(() => { + if (!diffSearchMatches.length) return; + const match = diffSearchMatches[diffSearchMatchIndex]; + if (!match) return; + codeViewRef.current?.scrollTo({ + type: "line", + id: match.itemId, + lineNumber: match.lineNumber, + side: match.side, + align: "center", + behavior: "smooth-auto", + }); + }, [diffSearchMatchIndex, diffSearchMatches]); + + useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if ((event.metaKey || event.ctrlKey) && event.key === "f") { + event.preventDefault(); + setDiffSearchVisible(true); + setTimeout(() => diffSearchInputRef.current?.focus(), 0); + } + if (event.key === "Escape") { + setDiffSearchVisible(false); + setDiffSearchQuery(""); + } + } + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); + + const scrollToFile = useCallback((path: string) => { + const itemId = diffItemId(path); + programmaticScrollRef.current = true; + if (programmaticScrollTimerRef.current) { + clearTimeout(programmaticScrollTimerRef.current); + } + let attempts = 0; + const tryScroll = () => { + const handle = codeViewRef.current; + const viewer = handle?.getInstance(); + if (handle && viewer && viewer.getTopForItem(itemId) != null) { + handle.scrollTo({ + type: "item", + id: itemId, + behavior: "smooth-auto", + offset: LAYOUT_PADDING, + }); + programmaticScrollTimerRef.current = setTimeout(() => { + programmaticScrollRef.current = false; + }, 600); + return; + } + if (attempts++ < 6) { + requestAnimationFrame(tryScroll); + } else { + programmaticScrollRef.current = false; + } + }; + tryScroll(); + }, []); + + const handleTreeClick = useCallback( + (event: React.MouseEvent) => { + for (const target of event.nativeEvent.composedPath()) { + if ( + !("getAttribute" in target) || + typeof (target as Element).getAttribute !== "function" + ) + continue; + const path = (target as Element).getAttribute("data-item-path"); + if (path && FILE_PATH_SET.has(path)) { + scrollToFile(path); + return; + } + } + }, + [scrollToFile] + ); + + const handleScroll = useCallback( + (scrollTop: number, viewer: CodeViewInstance) => { + if (programmaticScrollRef.current) return; + let activePath: string | null = null; + for (const path of FILE_PATHS) { + const itemTop = viewer.getTopForItem(diffItemId(path)); + if (itemTop != null && itemTop <= scrollTop + 1) { + activePath = path; + } + } + if (!activePath) return; + const selected = treeModel.getSelectedPaths(); + if (selected.length === 1 && selected[0] === activePath) return; + for (const path of selected) { + treeModel.getItem(path)?.deselect(); + } + treeModel.getItem(activePath)?.select(); + }, + [treeModel] + ); + + const options = useMemo( + (): CodeViewOptions => ({ + diffIndicators: "bars", + diffStyle: "split", + enableLineSelection: false, + hunkSeparators: "simple", + itemMetrics: { diffHeaderHeight: 40 }, + layout: { + gap: 12, + paddingBottom: LAYOUT_PADDING, + paddingTop: LAYOUT_PADDING, + }, + lineDiffType: "char", + stickyHeaders: true, + themeType: "system", + onLineClick: (props, context) => { + if (props.type !== "diff-line" || context.type !== "diff") return; + const file = SORTED_FILES.find((f) => diffItemId(f.path) === context.item.id); + if (!file) return; + const side = props.annotationSide; + const content = side === "additions" ? file.newContent : file.oldContent; + const { lineContent, contextBefore, contextAfter } = extractLineContext( + content, + props.lineNumber + ); + setPendingComposer({ + filePath: file.path, + lineContent, + contextBefore, + contextAfter, + lineNumber: props.lineNumber, + side, + }); + }, + }), + [] + ); + + const renderAnnotation = useCallback( + ( + annotation: + | LineAnnotation + | DiffLineAnnotation, + item: CodeViewItem + ) => { + if (!("side" in annotation) || item.type !== "diff") return null; + const { metadata } = annotation; + if (!metadata) return null; + + if (metadata.type === "composer") { + return ( + { + createThread({ + body, + metadata: { + filePath: metadata.filePath, + lineContent: metadata.lineContent, + contextBefore: metadata.contextBefore, + contextAfter: metadata.contextAfter, + lineNumber: metadata.lineNumber, + }, + }); + setPendingComposer(null); + }} + onClose={() => setPendingComposer(null)} + /> + ); + } + + const thread = threads.find((t) => t.id === metadata.threadId); + if (!thread) return null; + + return ( +
+
+ +
+
+ ); + }, + [threads, createThread] + ); + + const renderCustomHeader = useCallback( + (item: CodeViewItem) => { + if (item.type !== "diff") return null; + const file = SORTED_FILES.find((f) => diffItemId(f.path) === item.id); + if (!file) return null; + const fileThreadCount = (threadsByFile.get(file.path) ?? []).length; + + return ( +
+ + {file.path} + + {file.status === "added" && ( + + New + + )} + {fileThreadCount > 0 && ( + + {fileThreadCount} comment{fileThreadCount !== 1 ? "s" : ""} + + )} +
+ ); + }, + [threadsByFile] + ); + + const validThreadCount = useMemo( + () => + [...threadsByFile.values()].reduce((sum, group) => sum + group.length, 0), + [threadsByFile] + ); + + function navigateDiffSearch(direction: 1 | -1) { + if (!diffSearchMatches.length) return; + setDiffSearchMatchIndex( + (i) => + (i + direction + diffSearchMatches.length) % diffSearchMatches.length + ); } return ( -
-
-
-

- {PR_TITLE} -

- - Open - +
+
- -
- {PR_FILES.map((file) => ( - t.metadata.filePath === file.path - )} - pendingComposer={pendingComposer} - onOpenComposer={setPendingComposer} - onCloseComposer={() => setPendingComposer(null)} - onCreateThread={handleCreateThread} + +
+
+
+

+ {PR_TITLE} +

+ + Open + +
+
+ + + + {PR_BRANCH} + + + + {PR_BASE} + + + {PR_FILES.length} files changed + + {validThreadCount} comment{validThreadCount !== 1 ? "s" : ""} + +
+
+ {diffSearchVisible && ( +
+ setDiffSearchQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + navigateDiffSearch(e.shiftKey ? -1 : 1); + } + }} + placeholder="Search in diff…" + className="flex-1 min-w-0 px-2 py-1 text-xs rounded-md bg-white dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200 placeholder-zinc-400 dark:placeholder-zinc-500 border border-zinc-200 dark:border-zinc-700 focus:outline-none focus:border-zinc-400 dark:focus:border-zinc-500" + /> + + {diffSearchQuery && diffSearchMatches.length === 0 + ? "No results" + : diffSearchMatches.length > 0 + ? `${diffSearchMatchIndex + 1} / ${diffSearchMatches.length}` + : ""} + + + + +
+ )} +
+ - ))} +
-
+ ); } diff --git a/examples/nextjs-code-review/src/components/FileDiffSection.tsx b/examples/nextjs-code-review/src/components/FileDiffSection.tsx deleted file mode 100644 index a9de60ed16..0000000000 --- a/examples/nextjs-code-review/src/components/FileDiffSection.tsx +++ /dev/null @@ -1,195 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Thread } from "@liveblocks/react-ui"; -import type { ComposerSubmitComment } from "@liveblocks/react-ui"; -import type { ThreadData } from "@liveblocks/client"; -import { - MultiFileDiff, - GutterUtilitySlotStyles, - type AnnotationSide, - type DiffLineAnnotation, -} from "@pierre/diffs/react"; -import type { PrFile } from "../pr-data"; -import { InlineComposer } from "./InlineComposer"; - -type AnnotationMetadata = - | { type: "thread"; threadId: string } - | { type: "composer" }; - -interface PendingComposer { - filePath: string; - lineNumber: number; - side: AnnotationSide; -} - -interface Props { - file: PrFile; - threads: ThreadData[]; - pendingComposer: PendingComposer | null; - onOpenComposer: (composer: PendingComposer) => void; - onCloseComposer: () => void; - onCreateThread: ( - filePath: string, - lineNumber: number, - side: AnnotationSide, - body: ComposerSubmitComment["body"] - ) => void; -} - -export function FileDiffSection({ - file, - threads, - pendingComposer, - onOpenComposer, - onCloseComposer, - onCreateThread, -}: Props) { - const [collapsed, setCollapsed] = useState(false); - - const fileThreads = threads.filter( - (t) => t.metadata.filePath === file.path - ); - - const stats = computeStats(file.oldContent, file.newContent); - - const lineAnnotations: DiffLineAnnotation[] = [ - ...fileThreads.map((thread) => ({ - side: thread.metadata.side as AnnotationSide, - lineNumber: thread.metadata.lineNumber, - metadata: { type: "thread" as const, threadId: thread.id }, - })), - ...(pendingComposer?.filePath === file.path - ? [ - { - side: pendingComposer.side, - lineNumber: pendingComposer.lineNumber, - metadata: { type: "composer" as const }, - }, - ] - : []), - ]; - - return ( -
-
setCollapsed((c) => !c)} - > - - {collapsed ? "▶" : "▼"} - - - {file.path} - - {file.status === "added" && ( - - New - - )} - - - +{stats.additions} - - - -{stats.deletions} - - - {fileThreads.length > 0 && ( - - {fileThreads.length} comment{fileThreads.length !== 1 ? "s" : ""} - - )} -
- - {!collapsed && ( - { - const { metadata } = annotation; - if (metadata.type === "composer") { - if (!pendingComposer) return null; - return ( - - ); - } - - const thread = fileThreads.find( - (t) => t.id === metadata.threadId - ); - if (!thread) return null; - return ( -
-
- -
-
- ); - }} - renderGutterUtility={(getHoveredLine) => ( - - )} - /> - )} -
- ); -} - -function computeStats( - oldContent: string, - newContent: string -): { additions: number; deletions: number } { - if (!oldContent) { - return { additions: newContent.split("\n").length, deletions: 0 }; - } - if (!newContent) { - return { additions: 0, deletions: oldContent.split("\n").length }; - } - - const oldLines = new Set(oldContent.split("\n")); - const newLines = new Set(newContent.split("\n")); - - let additions = 0; - let deletions = 0; - - for (const line of newContent.split("\n")) { - if (!oldLines.has(line)) additions++; - } - for (const line of oldContent.split("\n")) { - if (!newLines.has(line)) deletions++; - } - - return { additions, deletions }; -} diff --git a/examples/nextjs-code-review/src/components/InlineComposer.tsx b/examples/nextjs-code-review/src/components/InlineComposer.tsx index cdfc96259e..190bf2cacb 100644 --- a/examples/nextjs-code-review/src/components/InlineComposer.tsx +++ b/examples/nextjs-code-review/src/components/InlineComposer.tsx @@ -2,35 +2,20 @@ import { Composer } from "@liveblocks/react-ui"; import type { ComposerSubmitComment } from "@liveblocks/react-ui"; -import type { AnnotationSide } from "@pierre/diffs/react"; interface Props { - filePath: string; - lineNumber: number; - side: AnnotationSide; - onSubmit: ( - filePath: string, - lineNumber: number, - side: AnnotationSide, - body: ComposerSubmitComment["body"] - ) => void; + onSubmit: (body: ComposerSubmitComment["body"]) => void; onClose: () => void; } -export function InlineComposer({ - filePath, - lineNumber, - side, - onSubmit, - onClose, -}: Props) { +export function InlineComposer({ onSubmit, onClose }: Props) { return (
{ event.preventDefault(); - onSubmit(filePath, lineNumber, side, body); + onSubmit(body); }} /> + ); + }, + [toggleCollapsed] + ); + const renderCustomHeader = useCallback( (item: CodeViewItem) => { if (item.type !== "diff") return null; @@ -390,17 +706,17 @@ export function CodeReview() { const fileThreadCount = (threadsByFile.get(file.path) ?? []).length; return ( -
- +
+ {file.path} {file.status === "added" && ( - + New )} {fileThreadCount > 0 && ( - + {fileThreadCount} comment{fileThreadCount !== 1 ? "s" : ""} )} @@ -410,128 +726,195 @@ export function CodeReview() { [threadsByFile] ); - const validThreadCount = useMemo( - () => - [...threadsByFile.values()].reduce((sum, group) => sum + group.length, 0), - [threadsByFile] - ); - - function navigateDiffSearch(direction: 1 | -1) { - if (!diffSearchMatches.length) return; - setDiffSearchMatchIndex( - (i) => - (i + direction + diffSearchMatches.length) % diffSearchMatches.length - ); - } - return ( -
- -
-
-
-

- {PR_TITLE} -

- - Open - -
-
- - - - {PR_BRANCH} - - - - {PR_BASE} - - - {PR_FILES.length} files changed - - {validThreadCount} comment{validThreadCount !== 1 ? "s" : ""} - -
-
- {diffSearchVisible && ( -
- setDiffSearchQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - navigateDiffSearch(e.shiftKey ? -1 : 1); - } - }} - placeholder="Search in diff…" - className="flex-1 min-w-0 px-2 py-1 text-xs rounded-md bg-white dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200 placeholder-zinc-400 dark:placeholder-zinc-500 border border-zinc-200 dark:border-zinc-700 focus:outline-none focus:border-zinc-400 dark:focus:border-zinc-500" - /> - - {diffSearchQuery && diffSearchMatches.length === 0 - ? "No results" - : diffSearchMatches.length > 0 - ? `${diffSearchMatchIndex + 1} / ${diffSearchMatches.length}` - : ""} - - +
+
+ + + +
+ +
+
+ ); +} + +function Header() { + return ( +
+
+
+ + Liveblocks + +

+ {PR_TITLE} +

+ + Open + +
+
+ + {PR_BRANCH} + + into + + {PR_BASE} + + {PR_FILES.length} files +
+
+
+ ); +} + +function CommentsSidebar({ + resolvedThreads, + scrollToFile, + scrollToThread, +}: { + resolvedThreads: ResolvedThread[]; + scrollToFile: (path: string) => void; + scrollToThread: (thread: ResolvedThread) => void; +}) { + if (resolvedThreads.length === 0) { + return ( +
+ +
+ + No comments yet + +

+ Hover over a line and click the{" "} + + + {" "} + button to add code comments. +

+ ); + } + + return ( +
+ {SORTED_FILES.map((file) => { + const fileThreads = resolvedThreads.filter( + (thread) => thread.filePath === file.path + ); + if (fileThreads.length === 0) return null; + + return ( +
+ +
+ {fileThreads.map((thread) => ( + + ))} +
+
+ ); + })}
); } diff --git a/examples/nextjs-code-review/src/components/InlineComposer.tsx b/examples/nextjs-code-review/src/components/InlineComposer.tsx deleted file mode 100644 index 190bf2cacb..0000000000 --- a/examples/nextjs-code-review/src/components/InlineComposer.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import { Composer } from "@liveblocks/react-ui"; -import type { ComposerSubmitComment } from "@liveblocks/react-ui"; - -interface Props { - onSubmit: (body: ComposerSubmitComment["body"]) => void; - onClose: () => void; -} - -export function InlineComposer({ onSubmit, onClose }: Props) { - return ( -
- { - event.preventDefault(); - onSubmit(body); - }} - /> - -
- ); -} diff --git a/examples/nextjs-code-review/src/searchUtils.ts b/examples/nextjs-code-review/src/searchUtils.ts deleted file mode 100644 index 0afc7e29d3..0000000000 --- a/examples/nextjs-code-review/src/searchUtils.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface SearchMatch { - itemId: string; - lineNumber: number; - side: "additions" | "deletions"; -} - -export function getDiffSearchMatches( - query: string, - items: readonly { id: string; fileDiff: { additionLines: string[] } }[] -): SearchMatch[] { - if (!query.trim()) return []; - const lowerQuery = query.toLowerCase(); - const matches: SearchMatch[] = []; - for (const item of items) { - const lines = item.fileDiff.additionLines; - for (let i = 0; i < lines.length; i++) { - if (lines[i].toLowerCase().includes(lowerQuery)) { - matches.push({ itemId: item.id, lineNumber: i + 1, side: "additions" }); - } - } - } - return matches; -} diff --git a/examples/nextjs-code-review/src/styles/globals.css b/examples/nextjs-code-review/src/styles/globals.css index 4783e7e5a7..dfceec7a09 100644 --- a/examples/nextjs-code-review/src/styles/globals.css +++ b/examples/nextjs-code-review/src/styles/globals.css @@ -2,18 +2,121 @@ @import "@liveblocks/react-ui/styles.css"; @import "@liveblocks/react-ui/styles/dark/media-query.css"; -html, body { - margin: 0; - padding: 0; + background: var(--background); + color: var(--foreground); + font-family: + var(--font-geist-sans), var(--font-geist), ui-sans-serif, system-ui, + sans-serif; + -webkit-font-smoothing: antialiased; } .lb-root { --lb-accent: #0969da; } +.code-review-shell { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --border-opaque: color-mix( + in oklch, + var(--border) 100%, + var(--diffshub-sidebar-bg) 0% + ); + --color-border: var(--border); + --color-border-opaque: var(--border-opaque); + --diffshub-sidebar-bg: light-dark(oklch(98.5% 0 0), oklch(20.5% 0 0)); + --diffs-font-family: + var(--font-geist-mono), "SFMono-Regular", Consolas, "Liberation Mono", + Menlo, monospace; + --cv-gutter-vertical: 0px; + --cv-gutter-horizontal: 0px; + --cv-mini-gutter-vertical: 0px; + --cv-mini-gutter-horizontal: 0px; + contain: strict; + color-scheme: light; +} + +.code-review-icon-button { + display: inline-flex; + width: 2rem; + height: 2rem; + align-items: center; + justify-content: center; + border: 1px solid transparent; + border-radius: 0.375rem; + color: var(--muted-foreground); + font-size: 0.875rem; + line-height: 1; + transition: + background-color 120ms ease, + border-color 120ms ease, + color 120ms ease; +} + +.code-review-icon-button:hover:not(:disabled), +.code-review-icon-button[aria-pressed="true"] { + background: transparent; + color: var(--muted-foreground); +} + +.code-review-icon-button:disabled { + cursor: not-allowed; + opacity: 0.35; +} + +.code-review-icon-button::-webkit-details-marker, +.code-review-icon-button::marker { + display: none; + content: ""; +} + +.code-review-tab-button { + display: inline-flex; + width: 2rem; + height: 2rem; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + color: var(--muted-foreground); + box-shadow: none; + transition: + background-color 120ms ease, + color 120ms ease; +} + +.code-review-tab-button:hover, +.code-review-tab-button[aria-selected="true"] { + background: var(--muted); + color: var(--foreground); +} + @media (prefers-color-scheme: dark) { .lb-root { --lb-accent: #58a6ff; } + + .code-review-shell { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --border-opaque: color-mix( + in oklch, + var(--diffshub-sidebar-bg) 90%, + oklch(1 0 0) + ); + --color-border: oklch(1 0 0 / 10%); + --color-border-opaque: var(--border-opaque); + color-scheme: dark; + } } diff --git a/examples/nextjs-code-review/tsconfig.json b/examples/nextjs-code-review/tsconfig.json index d9fed622cc..888ad12e4f 100644 --- a/examples/nextjs-code-review/tsconfig.json +++ b/examples/nextjs-code-review/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es2018", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -25,9 +21,7 @@ } ], "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] } }, "include": [ @@ -37,7 +31,5 @@ "**/*.tsx", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } From ee353f589666ccd836e201574cf9c7b4e46ecd65 Mon Sep 17 00:00:00 2001 From: Marc Bouchenoire Date: Mon, 18 May 2026 11:26:01 +0200 Subject: [PATCH 4/5] Update dummy data --- .../src/components/CodeReview.tsx | 145 +- examples/nextjs-code-review/src/diff.ts | 1396 +++++++++++++++++ examples/nextjs-code-review/src/pr-data.ts | 203 --- 3 files changed, 1503 insertions(+), 241 deletions(-) create mode 100644 examples/nextjs-code-review/src/diff.ts delete mode 100644 examples/nextjs-code-review/src/pr-data.ts diff --git a/examples/nextjs-code-review/src/components/CodeReview.tsx b/examples/nextjs-code-review/src/components/CodeReview.tsx index f8d8c39d5f..9cacc314c3 100644 --- a/examples/nextjs-code-review/src/components/CodeReview.tsx +++ b/examples/nextjs-code-review/src/components/CodeReview.tsx @@ -12,7 +12,7 @@ import type { CSSProperties } from "react"; import { useThreads, useCreateThread } from "@liveblocks/react/suspense"; import { Composer, Thread } from "@liveblocks/react-ui"; import { - parseDiffFromFile, + parsePatchFiles, type AnnotationSide, type CodeViewDiffItem, type CodeViewItem, @@ -24,14 +24,10 @@ import { type SelectedLineRange, } from "@pierre/diffs"; import { CodeView, type CodeViewHandle } from "@pierre/diffs/react"; -import { - IconCodeStyleBg, - IconConvoFill, - IconFileTree, - IconPlus, -} from "@pierre/icons"; +import { IconConvoFill, IconFileTree, IconPlus } from "@pierre/icons"; import { FileTree, useFileTree } from "@pierre/trees/react"; -import { PR_FILES, PR_TITLE, PR_BRANCH, PR_BASE } from "../pr-data"; +import { DIFF } from "../diff"; +import type { DiffFile } from "../diff"; type DiffStyle = "split" | "unified"; type SidebarTab = "files" | "comments"; @@ -106,9 +102,36 @@ function useMediaQuery(query: string): boolean { } const CONTEXT_LINES = 3; -const SORTED_FILES = [...PR_FILES].sort((a, b) => - a.path.localeCompare(b.path, undefined, { sensitivity: "base" }) -); +/** + * Sort files the same way the FileTree component does: directories before + * files, then alphabetically within each level (case-insensitive). This keeps + * the code view scroll position in sync with the tree selection. + */ +function treeSort(a: DiffFile, b: DiffFile): number { + const aParts = a.path.split("/"); + const bParts = b.path.split("/"); + const len = Math.max(aParts.length, bParts.length); + + for (let i = 0; i < len; i++) { + const aSeg = aParts[i]; + const bSeg = bParts[i]; + + if (aSeg === undefined) return -1; + if (bSeg === undefined) return 1; + + const aIsDir = i < aParts.length - 1; + const bIsDir = i < bParts.length - 1; + + if (aIsDir !== bIsDir) return aIsDir ? -1 : 1; + + const cmp = aSeg.localeCompare(bSeg, undefined, { sensitivity: "base" }); + if (cmp !== 0) return cmp; + } + + return 0; +} + +const SORTED_FILES = [...DIFF.changes].sort(treeSort); const FILE_PATHS = SORTED_FILES.map((f) => f.path); const GIT_STATUS = SORTED_FILES.map((f) => ({ path: f.path, @@ -155,40 +178,85 @@ function diffItemId(path: string): string { function getCachedDiff(file: { path: string; - oldContent: string; - newContent: string; + patch: string; }): FileDiffMetadata { - const cacheKey = `${file.path}:${file.oldContent.length}:${file.newContent.length}`; + const cacheKey = `${file.path}:${file.patch.length}`; const cached = diffCache.get(cacheKey); if (cached) return cached; - const diff = { - ...parseDiffFromFile( - { name: file.path, contents: file.oldContent }, - { name: file.path, contents: file.newContent } - ), - cacheKey, - }; + // parsePatchFiles requires a full git diff header; patches from GitHub's API + // only include hunk content starting with @@, so we prepend the header. + const fullPatch = `diff --git a/${file.path} b/${file.path}\n--- a/${file.path}\n+++ b/${file.path}\n${file.patch}`; + const parsed = parsePatchFiles(fullPatch, cacheKey); + const diff = + parsed[0]?.files[0] ?? + parsePatchFiles( + `diff --git a/${file.path} b/${file.path}\n--- a/${file.path}\n+++ b/${file.path}\n`, + cacheKey + )[0]!.files[0]!; diffCache.set(cacheKey, diff); return diff; } +/** + * Reconstruct a sparse line array (1-indexed) from a unified diff patch + * string. Only lines visible in the diff (changed + context) are present. + * Used for annotation anchor matching — annotations are always placed on + * visible diff lines, so this gives us sufficient coverage. + */ +function linesFromPatch( + patch: string, + side: "additions" | "deletions" +): string[] { + const lines: string[] = []; + let lineNum = 0; + + for (const raw of patch.split("\n")) { + if (raw.startsWith("@@")) { + // Parse hunk header: @@ -oldStart,oldCount +newStart,newCount @@ + const match = + side === "additions" ? raw.match(/\+(\d+)/) : raw.match(/-(\d+)/); + lineNum = match ? parseInt(match[1], 10) : 1; + continue; + } + if (side === "additions") { + if (raw.startsWith("+")) { + lines[lineNum - 1] = raw.slice(1); + lineNum++; + } else if (raw.startsWith(" ")) { + lines[lineNum - 1] = raw.slice(1); + lineNum++; + } + } else { + if (raw.startsWith("-")) { + lines[lineNum - 1] = raw.slice(1); + lineNum++; + } else if (raw.startsWith(" ")) { + lines[lineNum - 1] = raw.slice(1); + lineNum++; + } + } + } + return lines; +} + function extractLineContext( - content: string, + lines: string[], lineNumber: number ): { lineContent: string; contextBefore: string; contextAfter: string; } { - const lines = content.split("\n"); const index = lineNumber - 1; return { lineContent: lines[index] ?? "", contextBefore: lines .slice(Math.max(0, index - CONTEXT_LINES), index) + .filter(Boolean) .join("\n"), contextAfter: lines .slice(index + 1, Math.min(lines.length, index + 1 + CONTEXT_LINES)) + .filter(Boolean) .join("\n"), }; } @@ -234,13 +302,18 @@ function findBestAnnotationMatch( function resolveAnnotation( anchor: AnnotationAnchor, - oldContent: string, - newContent: string + patch: string ): ResolvedAnnotation | null { - const newLine = findBestAnnotationMatch(anchor, newContent.split("\n")); + const newLine = findBestAnnotationMatch( + anchor, + linesFromPatch(patch, "additions") + ); if (newLine !== null) return { lineNumber: newLine, side: "additions" }; - const oldLine = findBestAnnotationMatch(anchor, oldContent.split("\n")); + const oldLine = findBestAnnotationMatch( + anchor, + linesFromPatch(patch, "deletions") + ); if (oldLine !== null) return { lineNumber: oldLine, side: "deletions" }; return null; @@ -365,11 +438,7 @@ export function CodeReview() { for (const file of SORTED_FILES) { const fileThreads = threadsByFile.get(file.path) ?? []; for (const thread of fileThreads) { - const resolved = resolveAnnotation( - thread.metadata, - file.oldContent, - file.newContent - ); + const resolved = resolveAnnotation(thread.metadata, file.patch); if (!resolved) continue; const range = getThreadRange(thread.metadata, resolved); const side = getRangeEndSide(range) ?? resolved.side; @@ -536,9 +605,9 @@ export function CodeReview() { if (!side) return; const file = SORTED_FILES.find((f) => diffItemId(f.path) === item.id); if (!file) return; - const content = side === "additions" ? file.newContent : file.oldContent; + const lines = linesFromPatch(file.patch, side); const { lineContent, contextBefore, contextAfter } = extractLineContext( - content, + lines, range.end ); const key = `draft-${nextComposerKeyRef.current++}`; @@ -818,7 +887,7 @@ function Header() { Liveblocks

- {PR_TITLE} + {DIFF.title}

Open @@ -826,13 +895,13 @@ function Header() {
- {PR_BRANCH} + {DIFF.from} into - {PR_BASE} + {DIFF.to} - {PR_FILES.length} files + {DIFF.changes.length} files
diff --git a/examples/nextjs-code-review/src/diff.ts b/examples/nextjs-code-review/src/diff.ts new file mode 100644 index 0000000000..d033038bc7 --- /dev/null +++ b/examples/nextjs-code-review/src/diff.ts @@ -0,0 +1,1396 @@ +export interface Diff { + title: string; + from: string; + to: string; + changes: DiffFile[]; +} + +export interface DiffFile { + path: string; + status: "added" | "modified" | "renamed" | "deleted"; + oldPath?: string; + patch: string; +} + +export const DIFF: Diff = { + title: "Add new API for server-side React Flow mutations", + from: "mutate-flow", + to: "main", + changes: [ + { + path: "CHANGELOG.md", + status: "modified", + patch: `@@ -18,7 +18,12 @@ + For full upgrade instructions, see the + JSON snapshot, only mutating what changed. + - \`initialStorage\` accepts \`LiveObject.from()\` result directly. + ++### \`@liveblocks/react-flow\` ++ ++- New \`mutateFlow()\` API for reading and mutating React Flow data from a Node.js ++ backend. Import from \`@liveblocks/react-flow/node\`. ++ + ### \`@liveblocks/zustand\` and \`@liveblocks/redux\` + + - Fix: Initial storage seeding no longer creates an undo frame.`, + }, + { + path: "examples/nextjs-react-flow-ai/app/flowchart/agent.ts", + status: "modified", + patch: `@@ -1,28 +1,25 @@ + "use server"; + + import { openai } from "@ai-sdk/openai"; +-import { Liveblocks, LiveMap } from "@liveblocks/node"; ++import { Liveblocks } from "@liveblocks/node"; ++import { mutateFlow } from "@liveblocks/react-flow/node"; + import { generateText, stepCountIs, tool } from "ai"; + import dedent from "dedent"; + import { nanoid } from "nanoid"; + import { z } from "zod"; +-import { +- LiveblocksNode, +- toLiveblocksEdge, +- toLiveblocksNode, +-} from "@liveblocks/react-flow"; + import { createAgentUser } from "../api/database"; + import { + BLOCK_COLORS, + BLOCK_SHAPES, + DEFAULT_BLOCK_SIZE, + FLOWCHART_EDGE_TYPE, + FLOWCHART_STORAGE_KEY, +- FlowchartFlow, ++ FlowchartEdge, + FlowchartNode, + createFlowchartEdge, + createFlowchartNode, + easeInOutCubic, ++ getBoundsFromNodes, + getEdgeHandlesForNodes, + getMidpoint, + getNodeCenter, +@@ -31,7 +28,6 @@ import { + sleep, + type BlockColor, + type Bounds, +- type Frame, + type Point, + } from "./shared"; + +@@ -68,46 +64,6 @@ const edgeDataSchema = z.object({ + label: z.string().optional(), + }); + +-function getLiveblocksNodeFrame(node: LiveblocksNode): Frame { +- return { +- position: node.get("position") as Point, +- width: node.get("width") ?? undefined, +- height: node.get("height") ?? undefined, +- }; +-} +- +-function getLiveblocksNodeSize(node: LiveblocksNode) { +- return getNodeSize(getLiveblocksNodeFrame(node)); +-} +- +-function getLiveblocksNodeCenter(node: LiveblocksNode): Point { +- return getNodeCenter(getLiveblocksNodeFrame(node)); +-} +- +-function getBoundsFromLiveblocksNodes( +- nodes: LiveMap> +-): Bounds | null { +- let minX = Infinity; +- let minY = Infinity; +- let maxX = -Infinity; +- let maxY = -Infinity; +- let hasNodes = false; +- +- for (const node of nodes.values()) { +- const position = node.get("position") as Point; +- const { width, height } = getLiveblocksNodeSize(node); +- +- minX = Math.min(minX, position.x); +- minY = Math.min(minY, position.y); +- maxX = Math.max(maxX, position.x + width); +- maxY = Math.max(maxY, position.y + height); +- +- hasNodes = true; +- } +- +- return hasNodes ? { minX, minY, maxX, maxY } : null; +-} +- + async function runFlowchartAgent(roomId: string, prompt: string) { + const agentUser = createAgentUser(); + let lastCursor: Point | null = null; +@@ -152,39 +108,36 @@ async function runFlowchartAgent(roomId: string, prompt: string) { + return run; + }; + +- await liveblocks.mutateStorage(roomId, async ({ root }) => { +- const flow = root.get(FLOWCHART_STORAGE_KEY) as FlowchartFlow | undefined; +- +- if (!flow) { +- return; +- } +- +- const nodes = flow.get("nodes"); +- const edges = flow.get("edges"); +- +- const bounds: Bounds = getBoundsFromLiveblocksNodes(nodes) ?? { +- minX: -DEFAULT_BOUNDS_RADIUS, +- minY: -DEFAULT_BOUNDS_RADIUS, +- maxX: DEFAULT_BOUNDS_RADIUS, +- maxY: DEFAULT_BOUNDS_RADIUS, +- }; +- +- await setPresence({ cursor: getRandomPointInBounds(bounds) }); +- +- let thinkingIntervalId: ReturnType | undefined = +- setInterval(() => { +- void setPresence({ cursor: getRandomPointInBounds(bounds) }); +- }, CURSOR_THINKING_INTERVAL); +- +- function stopThinkingInterval() { +- clearInterval(thinkingIntervalId); +- lastThinking = false; +- } +- +- try { +- await generateText({ +- model: openai("gpt-5.4-nano"), +- system: dedent\` ++ await mutateFlow( ++ { ++ client: liveblocks, ++ roomId, ++ storageKey: FLOWCHART_STORAGE_KEY, ++ }, ++ async (flow) => { ++ const bounds: Bounds = getBoundsFromNodes(flow.nodes) ?? { ++ minX: -DEFAULT_BOUNDS_RADIUS, ++ minY: -DEFAULT_BOUNDS_RADIUS, ++ maxX: DEFAULT_BOUNDS_RADIUS, ++ maxY: DEFAULT_BOUNDS_RADIUS, ++ }; ++ ++ await setPresence({ cursor: getRandomPointInBounds(bounds) }); ++ ++ let thinkingIntervalId: ReturnType | undefined = ++ setInterval(() => { ++ void setPresence({ cursor: getRandomPointInBounds(bounds) }); ++ }, CURSOR_THINKING_INTERVAL); ++ ++ function stopThinkingInterval() { ++ clearInterval(thinkingIntervalId); ++ lastThinking = false; ++ } ++ ++ try { ++ await generateText({ ++ model: openai("gpt-5.4-nano"), ++ system: dedent\` + You edit a live collaborative React Flow flowchart. + + Node shape: { id, position: { x, y }, width, height, data: { label, shape, color } }.`, + }, + { + path: "examples/nextjs-react-flow-ai/app/flowchart/shared.ts", + status: "modified", + patch: `@@ -27,7 +27,6 @@ export const FLOWCHART_EDGE_TYPE = "smoothstep" as const; + export const BLOCK_HANDLE_SIDES = ["top", "right", "bottom", "left"] as const; + + export type Point = { x: number; y: number }; +-export type Frame = { position: Point; width?: number; height?: number }; + export type Bounds = { minX: number; minY: number; maxX: number; maxY: number }; + + export type BlockShape = (typeof BLOCK_SHAPES)[number]; +@@ -62,14 +61,14 @@ export function blockTargetHandleId( + return \`tgt-\${side}\`; + } + +-export function getNodeSize(node: Pick) { ++export function getNodeSize(node: FlowchartNode) { + return { + width: node.width ?? DEFAULT_BLOCK_SIZE, + height: node.height ?? DEFAULT_BLOCK_SIZE, + }; + } + +-export function getNodeCenter(node: Frame): Point { ++export function getNodeCenter(node: FlowchartNode): Point { + const { width, height } = getNodeSize(node); + + return { +@@ -78,8 +77,32 @@ export function getNodeCenter(node: Frame): Point { + }; + } + ++export function getBoundsFromNodes( ++ nodes: readonly FlowchartNode[] ++): Bounds | null { ++ let minX = Infinity; ++ let minY = Infinity; ++ let maxX = -Infinity; ++ let maxY = -Infinity; ++ let hasNodes = false; ++ ++ for (const node of nodes) { ++ const { position } = node; ++ const { width, height } = getNodeSize(node); ++ ++ minX = Math.min(minX, position.x); ++ minY = Math.min(minY, position.y); ++ maxX = Math.max(maxX, position.x + width); ++ maxY = Math.max(maxY, position.y + height); ++ ++ hasNodes = true; ++ } ++ ++ return hasNodes ? { minX, minY, maxX, maxY } : null; ++} ++ + export function flowPointToNormalized( +- node: Frame, ++ node: FlowchartNode, + flowX: number, + flowY: number + ): Point { +@@ -91,7 +114,10 @@ export function flowPointToNormalized( + }; + } + +-export function normalizedToFlowPoint(node: Frame, normalized: Point): Point { ++export function normalizedToFlowPoint( ++ node: FlowchartNode, ++ normalized: Point ++): Point { + const { width, height } = getNodeSize(node); + + return { +@@ -101,7 +127,7 @@ export function normalizedToFlowPoint(node: Frame, normalized: Point): Point { + } + + export function getNodeAtFlowPoint( +- nodes: FlowchartNode[], ++ nodes: readonly FlowchartNode[], + flow: Point + ): FlowchartNode | undefined { + return nodes.find((node) => { +@@ -130,8 +156,8 @@ export function easeInOutCubic(t: number) { + } + + export function getEdgeHandlesForNodes( +- sourceNode: Frame, +- targetNode: Frame ++ sourceNode: FlowchartNode, ++ targetNode: FlowchartNode + ): { + sourceHandle: BlockSourceHandleId; + targetHandle: BlockTargetHandleId;`, + }, + { + path: "examples/nextjs-react-flow-ai/package.json", + status: "modified", + patch: `@@ -11,11 +11,11 @@ + }, + "dependencies": { + "@ai-sdk/openai": "^3.0.48", +- "@liveblocks/client": "^3.17.0", +- "@liveblocks/node": "^3.17.0", +- "@liveblocks/react": "^3.17.0", +- "@liveblocks/react-flow": "^3.17.0", +- "@liveblocks/react-ui": "^3.17.0", ++ "@liveblocks/client": "3.18.0", ++ "@liveblocks/node": "3.18.0", ++ "@liveblocks/react": "3.18.0", ++ "@liveblocks/react-flow": "3.18.0", ++ "@liveblocks/react-ui": "3.18.0", + "@xyflow/react": "^12.10.1", + "ai": "^6.0.136", + "dedent": "^1.7.2",`, + }, + { + path: "packages/liveblocks-react-flow/package.json", + status: "modified", + patch: `@@ -7,6 +7,6 @@ + "type": "module", + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "exports": { + ".": { + "import": { +@@ -19,6 +24,17 @@ + "default": "./dist/index.cjs" + } + }, ++ "./node": { ++ "import": { ++ "types": "./dist/node.d.ts", ++ "default": "./dist/node.js" ++ }, ++ "require": { ++ "types": "./dist/node.d.cts", ++ "module": "./dist/node.js", ++ "default": "./dist/node.cjs" ++ } ++ }, + "./styles.css": { + "types": "./styles.css.d.cts", + "default": "./styles.css" +@@ -37,6 +53,6 @@ + "build": "rollup --config rollup.config.js", + "start": "npm run dev", + "format": "(eslint --fix src/ || true) && stylelint --fix src/styles/ && prettier --write src/", + "lint": "eslint src/ && stylelint src/styles/", + "test": "npx liveblocks dev -p 1154 -c 'vitest run --coverage'", + "test:ci": "vitest run", +@@ -64,6 +81,7 @@ + }, + "devDependencies": { + "@liveblocks/eslint-config": "*", ++ "@liveblocks/node": "*", + "@liveblocks/rollup-config": "*", + "@liveblocks/vitest-config": "*", + "@testing-library/jest-dom": "^6.4.6",`, + }, + { + path: "packages/liveblocks-react-flow/rollup.config.js", + status: "modified", + patch: `@@ -7,7 +7,7 @@ import pkg from "./package.json" with { type: "json" }; + + export default createConfig({ + pkg, +- entries: ["src/index.ts"], ++ entries: ["src/index.ts", "src/node.ts"], + styles: [ + { + entry: "src/styles/index.css",`, + }, + { + path: "packages/liveblocks-react-flow/src/__tests__/mutate-flow-from-backend.test.ts", + status: "added", + patch: `@@ -0,0 +1,297 @@ ++import type { JsonObject } from "@liveblocks/core"; ++import { createClient, nanoid } from "@liveblocks/core"; ++import { Liveblocks } from "@liveblocks/node"; ++import type { Node } from "@xyflow/react"; ++import { describe, expect, onTestFinished, test, vi } from "vitest"; ++ ++import { mutateFlow } from "../node"; ++ ++const DEV_SERVER = "http://localhost:1154"; ++ ++const client = new Liveblocks({ ++ secret: "sk_localdev", ++ baseUrl: DEV_SERVER, ++}); ++ ++/** ++ * Creates a room on the dev server and optionally initializes its storage. ++ */ ++async function initRoom(storage?: Record): Promise { ++ const roomId = \`room-\${nanoid()}\`; ++ ++ await fetch(\`\${DEV_SERVER}/v2/rooms\`, { ++ method: "POST", ++ headers: { ++ Authorization: "Bearer sk_localdev", ++ "Content-Type": "application/json", ++ }, ++ body: JSON.stringify({ id: roomId }), ++ }); ++ ++ if (storage) { ++ await fetch( ++ \`\${DEV_SERVER}/v2/rooms/\${encodeURIComponent(roomId)}/storage\`, ++ { ++ method: "POST", ++ headers: { ++ Authorization: "Bearer sk_localdev", ++ "Content-Type": "application/json", ++ }, ++ body: JSON.stringify({ ++ liveblocksType: "LiveObject", ++ data: storage, ++ }), ++ } ++ ); ++ } ++ ++ return roomId; ++} ++ ++async function getStorage(roomId: string): Promise> { ++ const resp = await fetch( ++ \`\${DEV_SERVER}/v2/rooms/\${encodeURIComponent(roomId)}/storage?format=json\`, ++ { headers: { Authorization: "Bearer sk_localdev" } } ++ ); ++ return (await resp.json()) as Record; ++} ++ ++/** ++ * Connects a live observer client to a room via WebSocket and returns a ++ * function that collects storage update batches. Useful for verifying that ++ * mutations emit the expected number of ops. ++ */ ++async function connectObserver(roomId: string) { ++ const res = await fetch(\`\${DEV_SERVER}/v2/authorize-user\`, { ++ method: "POST", ++ headers: { ++ Authorization: "Bearer sk_localdev", ++ "Content-Type": "application/json", ++ }, ++ body: JSON.stringify({ ++ userId: \`observer-\${nanoid()}\`, ++ userInfo: {}, ++ permissions: { [roomId]: ["room:write"] }, ++ }), ++ }); ++ const { token } = (await res.json()) as { token: string }; ++ ++ const liveClient = createClient({ ++ baseUrl: DEV_SERVER, ++ authEndpoint: () => Promise.resolve({ token }), ++ polyfills: { WebSocket: globalThis.WebSocket }, ++ }); ++ ++ const { room, leave } = liveClient.enterRoom(roomId, { ++ initialPresence: {}, ++ initialStorage: {}, ++ }); ++ ++ onTestFinished(leave); ++ ++ // Wait for connection + initial storage sync ++ await vi.waitUntil(() => room.getStatus() === "connected"); ++ await room.getStorage(); ++ ++ const batches: number[] = []; ++ room.events.storageBatch.subscribe((updates) => { ++ batches.push(updates.length); ++ }); ++ ++ return { ++ room, ++ /** Returns array where each element = number of updates in one batch */ ++ getBatches: () => batches, ++ }; ++} ++ ++// --------------------------------------------------------------------------- ++// Tests ++// --------------------------------------------------------------------------- ++ ++describe("mutateFlow", () => { ++ // -- Empty room / initialization -- ++ ++ test("auto-initializes empty flow storage when room is empty", async () => { ++ const roomId = await initRoom(); ++ ++ await mutateFlow({ client, roomId }, (flow) => { ++ expect(flow.nodes).toEqual([]); ++ expect(flow.edges).toEqual([]); ++ }); ++ }); ++ ++ test("reads existing nodes and edges from storage", async () => { ++ const roomId = await initRoom({ ++ flow: { ++ liveblocksType: "LiveObject", ++ data: { ++ nodes: { ++ liveblocksType: "LiveMap", ++ data: { ++ n1: { ++ liveblocksType: "LiveObject", ++ data: { ++ id: "n1", ++ position: { ++ liveblocksType: "LiveObject", ++ data: { x: 0, y: 0 }, ++ }, ++ data: { ++ liveblocksType: "LiveObject", ++ data: { label: "Hello" }, ++ }, ++ }, ++ }, ++ }, ++ }, ++ edges: { ++ liveblocksType: "LiveMap", ++ data: { ++ e1: { ++ liveblocksType: "LiveObject", ++ data: { id: "e1", source: "n1", target: "n2" }, ++ }, ++ }, ++ }, ++ }, ++ }, ++ }); ++ ++ await mutateFlow({ client, roomId }, (flow) => { ++ const nodes = flow.nodes; ++ expect(nodes).toHaveLength(1); ++ expect(nodes[0]).toMatchObject({ id: "n1", data: { label: "Hello" } }); ++ ++ const edges = flow.edges; ++ expect(edges).toHaveLength(1); ++ expect(edges[0]).toMatchObject({ id: "e1", source: "n1", target: "n2" }); ++ }); ++ }); ++ ++ // -- toJSON -- ++ ++ test("toJSON returns both nodes and edges", async () => { ++ const roomId = await initRoom(); ++ ++ await mutateFlow({ client, roomId }, (flow) => { ++ flow.addNode({ ++ id: "n1", ++ type: "default", ++ position: { x: 0, y: 0 }, ++ data: { label: "A" }, ++ }); ++ flow.addEdge({ id: "e1", source: "n1", target: "n2" }); ++ ++ const { nodes, edges } = flow; ++ expect(nodes).toHaveLength(1); ++ expect(nodes[0]).toMatchObject({ id: "n1" }); ++ expect(edges).toHaveLength(1); ++ expect(edges[0]).toMatchObject({ id: "e1" }); ++ ++ // JSON.stringify calls toJSON() automatically ++ expect(JSON.parse(JSON.stringify(flow))).toEqual({ ++ nodes: [ ++ { ++ id: "n1", ++ type: "default", ++ position: { x: 0, y: 0 }, ++ data: { label: "A" }, ++ }, ++ ], ++ edges: [{ id: "e1", source: "n1", target: "n2" }], ++ }); ++ }); ++ }); ++ ++ // -- getNode / getEdge -- ++ ++ test("getNode returns a single node by id", async () => { ++ const roomId = await initRoom(); ++ ++ await mutateFlow({ client, roomId }, (flow) => { ++ flow.addNode({ ++ type: "default", ++ id: "n1", ++ position: { x: 0, y: 0 }, ++ data: { label: "A" }, ++ }); ++ expect(flow.getNode("n1")).toMatchObject({ id: "n1" }); ++ expect(flow.getNode("nope")).toBeUndefined(); ++ }); ++ }); ++ ++ test("getEdge returns a single edge by id", async () => { ++ const roomId = await initRoom(); ++ ++ await mutateFlow({ client, roomId }, (flow) => { ++ flow.addEdge({ id: "e1", source: "n1", target: "n2" }); ++ expect(flow.getEdge("e1")).toMatchObject({ id: "e1" }); ++ expect(flow.getEdge("nope")).toBeUndefined(); ++ }); ++ }); ++ ++ // -- addNode / addNodes -- ++ ++ test("addNode adds a node to an empty flow", async () => { ++ const roomId = await initRoom(); ++ ++ await mutateFlow({ client, roomId }, (flow) => { ++ flow.addNode({ ++ id: "n1", ++ type: "input", ++ position: { x: 10, y: 20 }, ++ data: { label: "New" }, ++ }); ++ ++ const nodes = flow.nodes; ++ expect(nodes).toHaveLength(1); ++ expect(nodes[0]).toMatchObject({ ++ id: "n1", ++ type: "input", ++ position: { x: 10, y: 20 }, ++ data: { label: "New" }, ++ }); ++ }); ++ ++ // Verify storage was persisted ++ const storage = await getStorage(roomId); ++ expect(storage).toMatchObject({ ++ flow: { nodes: { n1: { id: "n1", type: "input" } } }, ++ }); ++ }); ++ ++ test("addNodes adds multiple nodes at once", async () => { ++ const roomId = await initRoom(); ++ ++ await mutateFlow({ client, roomId }, (flow) => { ++ flow.addNodes([ ++ { ++ id: "a", ++ type: "default", ++ position: { x: 0, y: 0 }, ++ data: { label: "one" }, ++ }, ++ { ++ id: "b", ++ type: "default", ++ position: { x: 1, y: 1 }, ++ data: { label: "two" }, ++ }, ++ ]); ++ ++ expect(flow.nodes).toHaveLength(2); ++ }); ++ }); ++ ++ // -- updateNode -- ++ ++ test("updateNode with partial object", async () => { ++ const roomId = await initRoom(); ++ ++ await mutateFlow({ client, roomId }, (flow) => { ++ flow.addNode({ ++ type: "default", ++ id: "n1", ++ position: { x: 0, y: 0 }, ++ data: { label: "Old" },`, + }, + { + path: "packages/liveblocks-react-flow/src/constants.ts", + status: "deleted", + patch: `@@ -1,41 +0,0 @@ +-import type { SyncMode } from "@liveblocks/core"; +-import type { Edge, Node } from "@xyflow/react"; +- +-export const DEFAULT_STORAGE_KEY = "flow"; +- +-// React Flow specific versions of \`SyncConfig\` that only allow keys that are actually exposed by React Flow. +-type NodeSyncConfig = { [K in keyof Node]?: SyncMode }; +-type EdgeSyncConfig = { [K in keyof Edge]?: SyncMode }; +- +-export const NODE_BASE_CONFIG = { +- // Local-only (not synced) +- selected: false, +- dragging: false, +- measured: false, +- resizing: false, +- +- // Atomic (synced as plain Json) +- position: "atomic", +- sourcePosition: "atomic", +- targetPosition: "atomic", +- extent: "atomic", +- origin: "atomic", +- handles: "atomic", +- +- // Note: the \`data\` key is intentionally left out of this base config, as it +- // is expected to be provided by the end user +-} as const satisfies NodeSyncConfig; +- +-export const EDGE_BASE_CONFIG = { +- // Local-only (not synced) +- selected: false, +- +- // Atomic (synced as plain Json) +- markerStart: "atomic", +- markerEnd: "atomic", +- label: "atomic", +- labelBgPadding: "atomic", +- +- // Note: the \`data\` key is intentionally left out of this base config, as it +- // is expected to be provided by the end user +-} as const satisfies EdgeSyncConfig;`, + }, + { + path: "packages/liveblocks-react-flow/src/flow.ts", + status: "modified", + patch: `@@ -28,11 +28,12 @@ import { addEdge as defaultAddEdge } from "@xyflow/react"; + import { useEffect, useMemo } from "react"; + + import { ++ buildEdgeConfigCache, ++ buildNodeConfigCache, + DEFAULT_STORAGE_KEY, +- EDGE_BASE_CONFIG, +- NODE_BASE_CONFIG, +-} from "./constants"; +-import { toLiveblocksInternalEdge, toLiveblocksInternalNode } from "./helpers"; ++ toLiveblocksInternalEdge, ++ toLiveblocksInternalNode, ++} from "./helpers"; + import type { + EdgeSyncConfig, + InternalLiveblocksEdge, +@@ -72,42 +73,6 @@ type LiveblocksFlowSuspenseResult< + E extends Edge = BuiltInEdge, + > = Extract, { isLoading: false }>; + +-function mergeAndBuildDataConfigCache( +- base: SyncConfig, +- data?: Record +-): (type: string | undefined) => SyncConfig { +- if (!data) return () => base; +- +- const dataFallback = data["*"]; +- const fallback = dataFallback ? { ...base, data: dataFallback } : base; +- +- // Pre-compute full node/edge sync configs for all explicitly declared types +- const cache = new Map(); +- for (const type in data) { +- if (type === "*") continue; +- const specific = data[type]; +- if (!specific) continue; +- const dataConfig: SyncConfig = { ...dataFallback, ...specific }; +- cache.set(type, { ...base, data: dataConfig }); +- } +- +- return (type) => cache.get(type) || fallback; +-} +- +-function buildNodeConfigCache( +- /** The user-provided node data sync configuration, if any. */ +- nodeDataConfig?: NodeSyncConfig +-): (type: string | undefined) => SyncConfig { +- return mergeAndBuildDataConfigCache(NODE_BASE_CONFIG, nodeDataConfig); +-} +- +-function buildEdgeConfigCache( +- /** The user-provided edge data sync configuration, if any. */ +- edgeDataConfig?: EdgeSyncConfig +-): (type: string | undefined) => SyncConfig { +- return mergeAndBuildDataConfigCache(EDGE_BASE_CONFIG, edgeDataConfig); +-} +- + type UseLiveblocksFlowOptions = { + nodes?: { + /**`, + }, + { + path: "packages/liveblocks-react-flow/src/helpers.ts", + status: "modified", + patch: `@@ -1,14 +1,85 @@ +-import type { JsonObject, SyncConfig } from "@liveblocks/core"; ++import type { JsonObject, SyncConfig, SyncMode } from "@liveblocks/core"; + import { LiveObject } from "@liveblocks/core"; + import type { Edge, Node } from "@xyflow/react"; + +-import { EDGE_BASE_CONFIG, NODE_BASE_CONFIG } from "./constants"; +-import type { +- InternalLiveblocksEdge, +- InternalLiveblocksNode, +- LiveblocksEdge, +- LiveblocksNode, +-} from "./types"; ++import type { InternalLiveblocksEdge, InternalLiveblocksNode } from "./types"; ++ ++export const DEFAULT_STORAGE_KEY = "flow"; ++ ++// React Flow specific versions of \`SyncConfig\` that only allow keys that are actually exposed by React Flow. ++type NodeSyncConfig = { [K in keyof Node]?: SyncMode }; ++type EdgeSyncConfig = { [K in keyof Edge]?: SyncMode }; ++ ++export const NODE_BASE_CONFIG = { ++ // Local-only (not synced) ++ selected: false, ++ dragging: false, ++ measured: false, ++ resizing: false, ++ ++ // Atomic (synced as plain Json) ++ position: "atomic", ++ sourcePosition: "atomic", ++ targetPosition: "atomic", ++ extent: "atomic", ++ origin: "atomic", ++ handles: "atomic", ++ ++ // Note: the \`data\` key is intentionally left out of this base config, as it ++ // is expected to be provided by the end user ++} as const satisfies NodeSyncConfig; ++ ++export const EDGE_BASE_CONFIG = { ++ // Local-only (not synced) ++ selected: false, ++ ++ // Atomic (synced as plain Json) ++ markerStart: "atomic", ++ markerEnd: "atomic", ++ label: "atomic", ++ labelBgPadding: "atomic", ++ ++ // Note: the \`data\` key is intentionally left out of this base config, as it ++ // is expected to be provided by the end user ++} as const satisfies EdgeSyncConfig; ++ ++/** ++ * Merges a base config with per-type user data configs, returning a lookup ++ * function that resolves the full SyncConfig for a given type string. ++ */ ++export function buildFlowDataConfigCache( ++ base: SyncConfig, ++ data?: Record ++): (type: string | undefined) => SyncConfig { ++ if (!data) return () => base; ++ ++ const dataFallback = data["*"]; ++ const fallback = dataFallback ? { ...base, data: dataFallback } : base; ++ ++ // Pre-compute full sync configs for all explicitly declared types ++ const cache = new Map(); ++ for (const type in data) { ++ if (type === "*") continue; ++ const specific = data[type]; ++ if (!specific) continue; ++ const dataConfig: SyncConfig = { ...dataFallback, ...specific }; ++ cache.set(type, { ...base, data: dataConfig }); ++ } ++ ++ return (type) => cache.get(type) || fallback; ++} ++ ++export function buildNodeConfigCache( ++ nodeDataConfig?: Record ++): (type: string | undefined) => SyncConfig { ++ return buildFlowDataConfigCache(NODE_BASE_CONFIG, nodeDataConfig); ++} ++ ++export function buildEdgeConfigCache( ++ edgeDataConfig?: Record ++): (type: string | undefined) => SyncConfig { ++ return buildFlowDataConfigCache(EDGE_BASE_CONFIG, edgeDataConfig); ++} + + export function toLiveblocksInternalNode( + node: N, +@@ -29,39 +100,3 @@ export function toLiveblocksInternalEdge( + config + ) as InternalLiveblocksEdge; + } +- +-/** +- * @experimental +- * +- * Converts a React Flow \`Node\` into a Liveblocks Storage version. +- * Keys marked \`false\` in config are set as local-only (not synced). +- * Keys marked \`"atomic"\` are stored as plain Json (no deep wrapping). +- * All other keys are deep-liveified (objects→LiveObject, arrays→LiveList). +- */ +-export function toLiveblocksNode( +- node: N, +- config?: SyncConfig +-): LiveblocksNode { +- return toLiveblocksInternalNode(node, { +- ...NODE_BASE_CONFIG, +- data: config, +- }) as unknown as LiveblocksNode; +-} +- +-/** +- * @experimental +- * +- * Converts a React Flow \`Edge\` into a Liveblocks Storage version. +- * Keys marked \`false\` in config are set as local-only (not synced). +- * Keys marked \`"atomic"\` are stored as plain Json (no deep wrapping). +- * All other keys are deep-liveified (objects→LiveObject, arrays→LiveList). +- */ +-export function toLiveblocksEdge( +- edge: E, +- config?: SyncConfig +-): LiveblocksEdge { +- return toLiveblocksInternalEdge(edge, { +- ...EDGE_BASE_CONFIG, +- data: config, +- }) as unknown as LiveblocksEdge; +-}`, + }, + { + path: "packages/liveblocks-react-flow/src/index.ts", + status: "modified", + patch: `@@ -7,7 +7,6 @@ detectDupes(PKG_NAME, PKG_VERSION, PKG_FORMAT); + export type { CursorsCursorProps, CursorsProps } from "./cursors"; + export { Cursors } from "./cursors"; + export { useLiveblocksFlow } from "./flow"; +-export { toLiveblocksEdge, toLiveblocksNode } from "./helpers"; + export type { + EdgeSyncConfig, + LiveblocksEdge,`, + }, + { + path: "packages/liveblocks-react-flow/src/node.ts", + status: "added", + patch: `@@ -0,0 +1,294 @@ ++import type { JsonObject, LsonObject } from "@liveblocks/core"; ++import { LiveMap, LiveObject } from "@liveblocks/core"; ++import type { BuiltInEdge, BuiltInNode, Edge, Node } from "@xyflow/react"; ++ ++import { ++ buildEdgeConfigCache, ++ buildNodeConfigCache, ++ DEFAULT_STORAGE_KEY, ++ toLiveblocksInternalEdge, ++ toLiveblocksInternalNode, ++} from "./helpers"; ++import type { ++ EdgeSyncConfig, ++ InternalLiveblocksFlow, ++ NodeSyncConfig, ++} from "./types"; ++ ++/** ++ * A minimal interface for the Liveblocks Node client — just the ++ * \`mutateStorage\` method we actually need. This avoids importing ++ * \`@liveblocks/node\` as a dependency. ++ */ ++interface ILiveblocksClient { ++ mutateStorage( ++ roomId: string, ++ callback: (context: { ++ root: LiveObject; ++ }) => void | Promise ++ ): Promise; ++} ++ ++/** Options for \`mutateFlow()\`. */ ++export interface MutateFlowOptions< ++ N extends Node = BuiltInNode, ++ E extends Edge = BuiltInEdge, ++> { ++ client: ILiveblocksClient; ++ roomId: string; ++ storageKey?: string; ++ nodes?: { sync?: NodeSyncConfig }; ++ edges?: { sync?: EdgeSyncConfig }; ++} ++ ++export interface MutableFlow { ++ /** The current list of nodes. */ ++ readonly nodes: readonly N[]; ++ /** The current list of edges. */ ++ readonly edges: readonly E[]; ++ /** Returns a plain object snapshot with \`nodes\` and \`edges\` arrays. */ ++ toJSON(): { ++ nodes: readonly N[]; ++ edges: readonly E[]; ++ }; ++ ++ /** Returns a single node by ID, or \`undefined\` if not found. */ ++ getNode(id: string): N | undefined; ++ /** Returns a single edge by ID, or \`undefined\` if not found. */ ++ getEdge(id: string): E | undefined; ++ ++ /** Adds a node. If a node with the same ID already exists, it is replaced. */ ++ addNode(node: N): void; ++ /** Adds multiple nodes. Existing nodes with the same IDs are replaced. */ ++ addNodes(nodes: N[]): void; ++ /** Updates a node by merging a partial object. No-op if the node does not exist. */ ++ updateNode(id: string, partial: Partial): void; ++ /** Updates a node using an updater function. Always return a new object, never mutate in-place. No-op if the node does not exist. */ ++ updateNode(id: string, updater: (node: N) => N): void; ++ /** Updates a node's \`data\` by merging a partial object. No-op if the node does not exist. */ ++ updateNodeData(id: string, partial: Partial): void; ++ /** Updates a node's \`data\` using an updater function. Always return a new object, never mutate in-place. No-op if the node does not exist. */ ++ updateNodeData( ++ id: string, ++ updater: (data: D) => D ++ ): void; ++ /** Removes a node by ID. */ ++ removeNode(id: string): void; ++ /** Removes multiple nodes by ID. */ ++ removeNodes(ids: string[]): void; ++ ++ /** Adds an edge. If an edge with the same ID already exists, it is replaced. */ ++ addEdge(edge: E): void; ++ /** Adds multiple edges. Existing edges with the same IDs are replaced. */ ++ addEdges(edges: E[]): void; ++ /** Updates an edge by merging a partial object. No-op if the edge does not exist. */ ++ updateEdge(id: string, partial: Partial): void; ++ /** Updates an edge using an updater function. Always return a new object, never mutate in-place. No-op if the edge does not exist. */ ++ updateEdge(id: string, updater: (edge: E) => E): void; ++ /** Updates an edge's \`data\` by merging a partial object. No-op if the edge does not exist. */ ++ updateEdgeData(id: string, partial: Partial>): void; ++ /** Updates an edge's \`data\` using an updater function. Always return a new object, never mutate in-place. No-op if the edge does not exist. */ ++ updateEdgeData( ++ id: string, ++ updater: (data: D) => D ++ ): void; ++ /** Removes an edge by ID. */ ++ removeEdge(id: string): void; ++ /** Removes multiple edges by ID. */ ++ removeEdges(ids: string[]): void; ++} ++ ++/** ++ * Opens a flow (a collection of React Flow nodes and edges) for reading and ++ * mutating, then automatically flushes all changes when the callback ++ * completes. ++ * ++ * @example ++ * \`\`\`ts ++ * await mutateFlow({ client, roomId: "my-room" }, (flow) => { ++ * flow.addNode({ id: "1", position: { x: 0, y: 0 }, data: {} }); ++ * flow.updateNodeData("1", { label: "Hello" }); ++ * }); ++ * \`\`\` ++ */ ++export async function mutateFlow< ++ N extends Node = BuiltInNode, ++ E extends Edge = BuiltInEdge, ++>( ++ options: MutateFlowOptions, ++ callback: (flow: MutableFlow) => void | Promise ++): Promise { ++ const { client, roomId } = options; ++ const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY; ++ ++ const getNodeSyncConfig = buildNodeConfigCache(options.nodes?.sync); ++ const getEdgeSyncConfig = buildEdgeConfigCache(options.edges?.sync); ++ ++ const nodeListCache = new WeakMap, N[]>(); ++ const edgeListCache = new WeakMap, E[]>(); ++ ++ await client.mutateStorage(roomId, async ({ root }) => { ++ let flow = root.get(storageKey) as InternalLiveblocksFlow | undefined; ++ if (!flow) { ++ const newFlow = new LiveObject({ ++ nodes: new LiveMap(), ++ edges: new LiveMap(), ++ }) satisfies InternalLiveblocksFlow; ++ root.set(storageKey, newFlow); ++ flow = newFlow; ++ } ++ ++ const nodesLiveMap = flow.get("nodes"); ++ const edgesLiveMap = flow.get("edges"); ++ ++ function getNodes(): readonly N[] { ++ const nodeMap = nodesLiveMap.toJSON() as unknown as Record; ++ if (!nodeListCache.has(nodeMap)) { ++ // TODO (LB-3665): To support sub-nodes, this function will need to emit nodes ++ // in topological order (parents before children), deferring any node with a ++ // parentId until its parent has been emitted. ++ nodeListCache.set(nodeMap, Object.values(nodeMap)); ++ } ++ return nodeListCache.get(nodeMap)!; ++ } ++ ++ function getEdges(): readonly E[] { ++ const edgeMap = edgesLiveMap.toJSON() as unknown as Record; ++ if (!edgeListCache.has(edgeMap)) { ++ edgeListCache.set(edgeMap, Object.values(edgeMap)); ++ } ++ return edgeListCache.get(edgeMap)!; ++ } ++ ++ function getNode(id: string) { ++ return nodesLiveMap.get(id)?.toJSON() as N | undefined; ++ } ++ function getEdge(id: string) { ++ return edgesLiveMap.get(id)?.toJSON() as E | undefined; ++ } ++ ++ function upsertNode(id: string, newNode: N) { ++ const existing = nodesLiveMap.get(id); ++ const syncConfig = getNodeSyncConfig(newNode.type); ++ if (!existing) { ++ nodesLiveMap.set(id, toLiveblocksInternalNode(newNode, syncConfig)); ++ } else { ++ existing.reconcile(newNode as unknown as JsonObject, syncConfig); ++ } ++ } ++ ++ function upsertEdge(id: string, newEdge: E) { ++ const existing = edgesLiveMap.get(id); ++ const syncConfig = getEdgeSyncConfig(newEdge.type); ++ if (!existing) { ++ edgesLiveMap.set(id, toLiveblocksInternalEdge(newEdge, syncConfig)); ++ } else { ++ existing.reconcile(newEdge as unknown as JsonObject, syncConfig); ++ } ++ } ++ ++ const mutableFlow: MutableFlow = { ++ get nodes() { ++ return getNodes(); ++ }, ++ get edges() { ++ return getEdges(); ++ }, ++ toJSON() { ++ return { nodes: getNodes(), edges: getEdges() }; ++ }, ++ getNode, ++ getEdge, ++ ++ addNode(node: N) { ++ upsertNode(node.id, node); ++ }, ++ addNodes(nodes: N[]) { ++ for (const node of nodes) { ++ mutableFlow.addNode(node); ++ } ++ }, ++ updateNode(id: string, partialOrUpdater: Partial | ((node: N) => N)) { ++ const oldNode = getNode(id); ++ if (!oldNode) return; ++ ++ let newNode: N; ++ if (typeof partialOrUpdater === "function") { ++ newNode = partialOrUpdater(oldNode); ++ } else { ++ newNode = { ...oldNode, ...partialOrUpdater }; ++ } ++ return upsertNode(id, newNode); ++ }, ++ updateNodeData( ++ id: string, ++ partialOrUpdater: ++ | Partial ++ | ((data: D) => D) ++ ) { ++ return mutableFlow.updateNode(id, (node) => { ++ const currData = node.data ?? ({} as N["data"]); ++ const newData = ++ typeof partialOrUpdater === "function" ++ ? partialOrUpdater(currData) ++ : { ...currData, ...partialOrUpdater }; ++ return { ...node, data: newData }; ++ }); ++ }, ++ removeNode(id: string) { ++ nodesLiveMap.delete(id); ++ }, ++ removeNodes(ids: string[]) { ++ for (const id of ids) { ++ nodesLiveMap.delete(id); ++ } ++ }, ++ ++ addEdge(edge: E) { ++ upsertEdge(edge.id, edge); ++ }, ++ addEdges(edges: E[]) { ++ for (const edge of edges) { ++ mutableFlow.addEdge(edge); ++ } ++ }, ++ updateEdge(id: string, partialOrUpdater: Partial | ((edge: E) => E)) { ++ const oldEdge = getEdge(id); ++ if (!oldEdge) return; ++ ++ let newEdge: E; ++ if (typeof partialOrUpdater === "function") { ++ newEdge = partialOrUpdater(oldEdge); ++ } else { ++ newEdge = { ...oldEdge, ...partialOrUpdater }; ++ } ++ return upsertEdge(id, newEdge); ++ }, ++ updateEdgeData( ++ id: string, ++ partialOrUpdater: ++ | Partial> ++ | ((data: D) => D) ++ ) { ++ return mutableFlow.updateEdge(id, (edge) => { ++ const currData = edge.data; ++ const newData = ++ typeof partialOrUpdater === "function" ++ ? partialOrUpdater(currData) ++ : { ...currData, ...partialOrUpdater }; ++ return { ...edge, data: newData }; ++ }); ++ }, ++ removeEdge(id: string) { ++ edgesLiveMap.delete(id); ++ }, ++ removeEdges(ids: string[]) { ++ for (const id of ids) { ++ edgesLiveMap.delete(id); ++ } ++ }, ++ }; ++ ++ await callback(mutableFlow); ++ }); ++}`, + }, + { + path: "packages/liveblocks-react-flow/src/types.ts", + status: "modified", + patch: `@@ -10,6 +10,6 @@ import type { + } from "@liveblocks/core"; + import type { BuiltInEdge, BuiltInNode, Edge, Node } from "@xyflow/react"; + +-import type { EDGE_BASE_CONFIG, NODE_BASE_CONFIG } from "./constants"; ++import type { EDGE_BASE_CONFIG, NODE_BASE_CONFIG } from "./helpers"; + + export type { SyncConfig, SyncMode };`, + }, + { + path: "packages/liveblocks-react-flow/test-d/mutate-flow.test-d.ts", + status: "added", + patch: `@@ -0,0 +1,145 @@ ++/* eslint-disable */ ++ ++import type { Edge, Node } from "@xyflow/react"; ++import { expectError, expectType } from "tsd"; ++ ++import type { MutableFlow } from "../dist/node"; ++ ++// -- Custom types used by the tests below -- ++ ++type CustomNodeData = { label: string; priority: number }; ++type CustomNode = Node; ++ ++type CustomEdgeData = { weight: number }; ++type CustomEdge = Edge; ++ ++/** ++ * MutableFlow with custom node/edge types — getters ++ */ ++{ ++ const flow = {} as MutableFlow; ++ ++ expectType(flow.nodes); ++ expectType(flow.edges); ++ expectType<{ nodes: readonly CustomNode[]; edges: readonly CustomEdge[] }>( ++ flow.toJSON() ++ ); ++ expectType(flow.getNode("n1")); ++ expectType(flow.getEdge("e1")); ++} ++ ++/** ++ * MutableFlow — addNode requires correct shape ++ */ ++{ ++ const flow = {} as MutableFlow; ++ ++ // Correct node should be accepted ++ flow.addNode({ ++ id: "n1", ++ type: "task", ++ position: { x: 0, y: 0 }, ++ data: { label: "Hello", priority: 1 }, ++ }); ++ ++ // Missing required data field should error ++ expectError( ++ flow.addNode({ ++ id: "n2", ++ type: "task", ++ position: { x: 0, y: 0 }, ++ data: { label: "Hello" }, ++ }) ++ ); ++ ++ // Wrong node type should error ++ expectError( ++ flow.addNode({ ++ id: "n3", ++ type: "wrong", ++ position: { x: 0, y: 0 }, ++ data: { label: "Hello", priority: 1 }, ++ }) ++ ); ++} ++ ++/** ++ * MutableFlow — addEdge requires correct shape ++ */ ++{ ++ const flow = {} as MutableFlow; ++ ++ // Correct edge should be accepted ++ flow.addEdge({ ++ id: "e1", ++ type: "weighted", ++ source: "n1", ++ target: "n2", ++ data: { weight: 5 }, ++ }); ++ ++ // Wrong edge type should error ++ expectError( ++ flow.addEdge({ ++ id: "e2", ++ type: "wrong", ++ source: "n1", ++ target: "n2", ++ data: { weight: 5 }, ++ }) ++ ); ++} ++ ++/** ++ * MutableFlow — updateNode ++ */ ++{ ++ const flow = {} as MutableFlow; ++ ++ // Partial update ++ flow.updateNode("n1", { position: { x: 10, y: 20 } }); ++ ++ // Updater function receives the correct type ++ flow.updateNode("n1", (node) => { ++ expectType(node); ++ return { ...node, position: { x: 0, y: 0 } }; ++ }); ++} ++ ++/** ++ * MutableFlow — updateNodeData ++ */ ++{ ++ const flow = {} as MutableFlow; ++ ++ // Partial data update with known key ++ flow.updateNodeData("n1", { priority: 2 }); ++ ++ // Unknown data key should error ++ expectError(flow.updateNodeData("n1", { unknown: true })); ++ ++ // Updater function receives the correct data type ++ flow.updateNodeData("n1", (data) => { ++ expectType(data); ++ return { ...data, priority: data.priority + 1 }; ++ }); ++} ++ ++/** ++ * MutableFlow — updateEdgeData ++ */ ++{ ++ const flow = {} as MutableFlow; ++ ++ // Partial data update with known key ++ flow.updateEdgeData("e1", { weight: 5 }); ++ ++ // Unknown data key should error ++ expectError(flow.updateEdgeData("e1", { unknown: true })); ++ ++ // Updater function receives possibly-undefined data (edge data is optional in React Flow) ++ flow.updateEdgeData("e1", (data) => { ++ expectType(data); ++ return { ...data!, weight: data!.weight + 1 }; ++ }); ++}`, + }, + ], +}; diff --git a/examples/nextjs-code-review/src/pr-data.ts b/examples/nextjs-code-review/src/pr-data.ts deleted file mode 100644 index 8103bf784d..0000000000 --- a/examples/nextjs-code-review/src/pr-data.ts +++ /dev/null @@ -1,203 +0,0 @@ -export interface PrFile { - path: string; - status: "added" | "modified"; - oldContent: string; - newContent: string; -} - -export const PR_TITLE = "feat: Add JWT-based authentication"; -export const PR_BRANCH = "feat/jwt-auth"; -export const PR_BASE = "main"; -export const PR_AUTHOR = "mislav.abha@example.com"; - -export const PR_FILES: PrFile[] = [ - { - path: "src/lib/auth.ts", - status: "added", - oldContent: "", - newContent: `import { SignJWT, jwtVerify } from "jose"; - -const SECRET = new TextEncoder().encode( - process.env.JWT_SECRET ?? "dev-secret-change-in-production" -); - -export interface TokenPayload { - userId: string; - email: string; - role: "admin" | "user"; -} - -export async function signToken(payload: TokenPayload): Promise { - return new SignJWT({ ...payload }) - .setProtectedHeader({ alg: "HS256" }) - .setIssuedAt() - .setExpirationTime("7d") - .sign(SECRET); -} - -export async function verifyToken( - token: string -): Promise { - try { - const { payload } = await jwtVerify(token, SECRET); - return payload as unknown as TokenPayload; - } catch { - return null; - } -} - -export function parseAuthHeader(header: string | null): string | null { - if (!header?.startsWith("Bearer ")) return null; - return header.slice(7); -} -`, - }, - { - path: "src/components/LoginForm.tsx", - status: "added", - oldContent: "", - newContent: `"use client"; - -import { useState } from "react"; - -interface Props { - onSuccess: (token: string) => void; -} - -export function LoginForm({ onSuccess }: Props) { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - async function handleSubmit(event: React.FormEvent) { - event.preventDefault(); - setLoading(true); - setError(null); - - try { - const response = await fetch("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password }), - }); - - if (!response.ok) { - const data = await response.json(); - setError(data.error ?? "Login failed"); - return; - } - - const { token } = await response.json(); - onSuccess(token); - } catch { - setError("An unexpected error occurred"); - } finally { - setLoading(false); - } - } - - return ( -
-

Sign in

- - - {error !== null &&

{error}

} - -
- ); -} -`, - }, - { - path: "src/middleware.ts", - status: "modified", - oldContent: `import { NextRequest, NextResponse } from "next/server"; - -const PUBLIC_PATHS = ["/login", "/api/auth"]; - -export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - if (PUBLIC_PATHS.some((path) => pathname.startsWith(path))) { - return NextResponse.next(); - } - - const session = request.cookies.get("session")?.value; - - if (!session) { - return NextResponse.redirect(new URL("/login", request.url)); - } - - return NextResponse.next(); -} - -export const config = { - matcher: ["/((?!_next|favicon.ico).*)"], -}; -`, - newContent: `import { NextRequest, NextResponse } from "next/server"; -import { parseAuthHeader, verifyToken } from "@/lib/auth"; - -const PUBLIC_PATHS = ["/login", "/api/auth"]; - -export async function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - if (PUBLIC_PATHS.some((path) => pathname.startsWith(path))) { - return NextResponse.next(); - } - - const token = - parseAuthHeader(request.headers.get("authorization")) ?? - request.cookies.get("token")?.value; - - if (!token) { - return NextResponse.redirect(new URL("/login", request.url)); - } - - const payload = await verifyToken(token); - - if (!payload) { - const response = NextResponse.redirect(new URL("/login", request.url)); - response.cookies.delete("token"); - return response; - } - - const requestHeaders = new Headers(request.headers); - requestHeaders.set("x-user-id", payload.userId); - requestHeaders.set("x-user-role", payload.role); - - return NextResponse.next({ - request: { headers: requestHeaders }, - }); -} - -export const config = { - matcher: ["/((?!_next|favicon.ico).*)"], -}; -`, - }, -]; From 9c37973bff83d85c461ef6313bec5535beb10687 Mon Sep 17 00:00:00 2001 From: Marc Bouchenoire Date: Mon, 18 May 2026 16:28:59 +0200 Subject: [PATCH 5/5] Continue working on code view --- .../src/components/CodeReview.tsx | 360 +++++++++++++----- 1 file changed, 273 insertions(+), 87 deletions(-) diff --git a/examples/nextjs-code-review/src/components/CodeReview.tsx b/examples/nextjs-code-review/src/components/CodeReview.tsx index 9cacc314c3..679d9838e7 100644 --- a/examples/nextjs-code-review/src/components/CodeReview.tsx +++ b/examples/nextjs-code-review/src/components/CodeReview.tsx @@ -10,7 +10,7 @@ import { } from "react"; import type { CSSProperties } from "react"; import { useThreads, useCreateThread } from "@liveblocks/react/suspense"; -import { Composer, Thread } from "@liveblocks/react-ui"; +import { AvatarStack, Composer, Thread } from "@liveblocks/react-ui"; import { parsePatchFiles, type AnnotationSide, @@ -26,6 +26,12 @@ import { import { CodeView, type CodeViewHandle } from "@pierre/diffs/react"; import { IconConvoFill, IconFileTree, IconPlus } from "@pierre/icons"; import { FileTree, useFileTree } from "@pierre/trees/react"; +import { + createFileTreeIconResolver, + getBuiltInFileIconColor, + getBuiltInSpriteSheet, +} from "@pierre/trees"; +import type { FileTreeRowDecorationRenderer } from "@pierre/trees"; import { DIFF } from "../diff"; import type { DiffFile } from "../diff"; @@ -138,8 +144,19 @@ const GIT_STATUS = SORTED_FILES.map((f) => ({ status: f.status, })); const FILE_PATH_SET = new Set(FILE_PATHS); +const DIFF_STATS = new Map( + SORTED_FILES.map((f) => { + let added = 0; + let removed = 0; + for (const line of f.patch.split("\n")) { + if (line.startsWith("+") && !line.startsWith("+++")) added++; + else if (line.startsWith("-") && !line.startsWith("---")) removed++; + } + return [f.path, { added, removed }] as const; + }) +); const LAYOUT_PADDING = 0; -const CODE_VIEW_FILE_TREE_ITEM_HEIGHT = 24; +const CODE_VIEW_FILE_TREE_ITEM_HEIGHT = 30; const CODE_VIEW_CUSTOM_CSS = ` [data-diffs-header] { @@ -158,11 +175,20 @@ const CODE_VIEW_CUSTOM_CSS = ` background-color: var(--color-border-opaque); } } + +:host { + interpolate-size: allow-keywords; + transition: height 250ms ease; + overflow: hidden; +} + +[data-gutter-utility-slot] { + pointer-events: none; +} `; const FILE_TREE_STYLES: CSSProperties & CustomProperties = { "--trees-bg-override": "var(--diffshub-sidebar-bg)", - "--trees-density-override": 0.8, "--trees-selected-fg-override": "light-dark(#1c1c1e, #f0f0f2)", "--trees-padding-inline-override": 8, "--trees-bg-muted": "light-dark(#f5f5f5, #262626)", @@ -170,6 +196,9 @@ const FILE_TREE_STYLES: CSSProperties & CustomProperties = { "--trees-git-renamed-color-override": "light-dark(#007aff, #007aff)", }; +const { resolveIcon: resolveFileIcon } = createFileTreeIconResolver("standard"); +const FILE_TREE_SPRITE_SHEET = getBuiltInSpriteSheet("standard"); + const diffCache = new Map(); function diffItemId(path: string): string { @@ -376,6 +405,27 @@ export function CodeReview() { useState(null); const [selectedLines, setSelectedLines] = useState(null); + const [collapsedItems, setCollapsedItems] = useState(() => new Set()); + const [expandedResolvedThreadIds, setExpandedResolvedThreadIds] = useState( + () => new Set() + ); + const previouslyResolvedIdsRef = useRef(new Set()); + useEffect(() => { + const prevResolved = previouslyResolvedIdsRef.current; + const nowResolved = new Set( + threads.filter((t) => t.resolved).map((t) => t.id) + ); + previouslyResolvedIdsRef.current = nowResolved; + const newlyResolved = threads + .filter((t) => t.resolved && !prevResolved.has(t.id)) + .map((t) => t.id); + if (newlyResolved.length === 0) return; + setExpandedResolvedThreadIds((prev) => { + const next = new Set(prev); + for (const id of newlyResolved) next.delete(id); + return next; + }); + }, [threads]); const [fileSearchQuery, setFileSearchQuery] = useState(""); const [activeSidebarTab, setActiveSidebarTab] = useState("files"); const isWideLayout = useMediaQuery("(min-width: 768px)"); @@ -403,19 +453,14 @@ export function CodeReview() { [fileSearchQuery] ); - const { model: treeModel } = useFileTree({ - flattenEmptyDirectories: true, - gitStatus: GIT_STATUS, - initialExpansion: "open", - initialVisibleRowCount, - itemHeight: CODE_VIEW_FILE_TREE_ITEM_HEIGHT, - paths: FILE_PATHS, - stickyFolders: true, - }); - useEffect(() => { - treeModel.resetPaths(filteredFilePaths); - }, [treeModel, filteredFilePaths]); + if (document.querySelector("[data-pierre-trees-sprite]")) return; + const container = document.createElement("div"); + container.setAttribute("data-pierre-trees-sprite", ""); + container.style.display = "none"; + container.innerHTML = FILE_TREE_SPRITE_SHEET; + document.body.insertAdjacentElement("afterbegin", container); + }, []); const threadsByFile = useMemo(() => { const map = new Map(); @@ -432,6 +477,53 @@ export function CodeReview() { return map; }, [threads]); + const commentCountByFile = useMemo(() => { + const map = new Map(); + for (const [path, fileThreads] of threadsByFile) { + let count = 0; + for (const thread of fileThreads) { + for (const comment of thread.comments) { + if (!comment.deletedAt) count++; + } + } + if (count > 0) map.set(path, count); + } + return map; + }, [threadsByFile]); + + const commentCountByFileRef = useRef(commentCountByFile); + commentCountByFileRef.current = commentCountByFile; + + const renderRowDecoration = useCallback( + (context) => { + if (context.item.kind !== "file") return null; + const count = commentCountByFileRef.current.get(context.item.path) ?? 0; + if (count === 0) return null; + const label = `${count} comment${count !== 1 ? "s" : ""}`; + return { text: label, title: label }; + }, + [] + ); + + const { model: treeModel } = useFileTree({ + flattenEmptyDirectories: true, + gitStatus: GIT_STATUS, + initialExpansion: "open", + initialVisibleRowCount, + itemHeight: CODE_VIEW_FILE_TREE_ITEM_HEIGHT, + paths: FILE_PATHS, + renderRowDecoration, + stickyFolders: true, + }); + + useEffect(() => { + treeModel.resetPaths(filteredFilePaths); + }, [treeModel, filteredFilePaths]); + + useEffect(() => { + treeModel.setGitStatus([...GIT_STATUS]); + }, [treeModel, commentCountByFile]); + const resolvedThreads = useMemo((): ResolvedThread[] => { const nextResolvedThreads: ResolvedThread[] = []; @@ -500,7 +592,9 @@ export function CodeReview() { : annotation.metadata.key ) .join(","); + const isCollapsed = collapsedItems.has(diffItemId(file.path)); let version = fileDiff.cacheKey?.length ?? 0; + if (isCollapsed) version = (version * 31 + 1) >>> 0; for (let i = 0; i < versionStr.length; i++) { version = (version * 31 + versionStr.charCodeAt(i)) >>> 0; } @@ -511,9 +605,14 @@ export function CodeReview() { fileDiff, annotations, version, + collapsed: isCollapsed, }; }); - }, [fileDiffs, resolvedThreads, pendingComposer]); + }, [fileDiffs, resolvedThreads, pendingComposer, collapsedItems]); + + const itemsRef = useRef(items); + itemsRef.current = items; + const selectedItemIdRef = useRef(null); const diffStyle: DiffStyle = isWideLayout ? "split" : "unified"; @@ -644,14 +743,16 @@ export function CodeReview() { stickyHeaders: true, themeType: "system", unsafeCSS: CODE_VIEW_CUSTOM_CSS, - onGutterUtilityClick(range, context) { - if (context.item.type !== "diff") return; - createDraftComposer(range, context.item); - }, onLineSelectionEnd(range) { if (range == null) { setSelectedLines(null); + return; } + const item = itemsRef.current.find( + (i) => i.id === selectedItemIdRef.current + ); + if (item?.type !== "diff") return; + createDraftComposer(range, item); }, }), [createDraftComposer, diffStyle] @@ -669,7 +770,20 @@ export function CodeReview() { if (metadata.type === "composer") { return ( -
+
{ + if (event.currentTarget.contains(event.relatedTarget as Node)) + return; + const editor = event.currentTarget.querySelector( + "[contenteditable='true']" + ); + if (!editor?.textContent?.trim()) { + setPendingComposer(null); + setSelectedLines(null); + } + }} + > { @@ -700,99 +814,167 @@ export function CodeReview() { const thread = threads.find((t) => t.id === metadata.threadId); if (!thread) return null; + const isExpanded = + !thread.resolved || expandedResolvedThreadIds.has(thread.id); + return (
setSelectedLines({ id: item.id, range: metadata.range }) } > - +
+ {thread.resolved && ( + + )} + {isExpanded && ( + + )} +
); }, - [threads, createThread] + [threads, createThread, expandedResolvedThreadIds] ); const toggleCollapsed = useCallback( (item: CodeViewItem) => { - const viewer = codeViewRef.current; - const instance = viewer?.getInstance(); - if (!viewer || !instance) return; - const itemTop = instance.getTopForItem(item.id); - item.collapsed = item.collapsed !== true; - item.version = typeof item.version === "number" ? item.version + 1 : 1; - if (!viewer.updateItem(item)) return; - if (itemTop != null && itemTop < instance.getScrollTop()) { - viewer.scrollTo({ type: "item", id: item.id, align: "start" }); + const instance = codeViewRef.current?.getInstance(); + const itemTop = instance?.getTopForItem(item.id); + const scrollTop = instance?.getScrollTop(); + setCollapsedItems((prev) => { + const next = new Set(prev); + if (next.has(item.id)) next.delete(item.id); + else next.add(item.id); + return next; + }); + if (itemTop != null && scrollTop != null && itemTop < scrollTop) { + codeViewRef.current?.scrollTo({ + type: "item", + id: item.id, + align: "start", + }); } }, [] ); - const renderHeaderPrefix = useCallback( + const renderCustomHeader = useCallback( (item: CodeViewItem) => { if (item.type !== "diff") return null; + const file = SORTED_FILES.find((f) => diffItemId(f.path) === item.id); + if (!file) return null; const emptyDiff = item.fileDiff.splitLineCount === 0 && item.fileDiff.unifiedLineCount === 0; + const commentCount = commentCountByFile.get(file.path) ?? 0; + const stats = DIFF_STATS.get(file.path) ?? { added: 0, removed: 0 }; + const lastSlash = file.path.lastIndexOf("/"); + const fileName = + lastSlash === -1 ? file.path : file.path.slice(lastSlash + 1); + const dirPath = lastSlash === -1 ? "" : file.path.slice(0, lastSlash + 1); + const fileIcon = resolveFileIcon("file-tree-icon-file", file.path); + const fileIconColor = getBuiltInFileIconColor(fileIcon.token ?? ""); return ( - - ); - }, - [toggleCollapsed] - ); - - const renderCustomHeader = useCallback( - (item: CodeViewItem) => { - if (item.type !== "diff") return null; - const file = SORTED_FILES.find((f) => diffItemId(f.path) === item.id); - if (!file) return null; - const fileThreadCount = (threadsByFile.get(file.path) ?? []).length; - - return ( -
- - {file.path} - - {file.status === "added" && ( - - New - - )} - {fileThreadCount > 0 && ( - - {fileThreadCount} comment{fileThreadCount !== 1 ? "s" : ""} + + + + + +
+ + {fileName} - )} + {dirPath && ( + + {dirPath} + + )} +
+
+ {(stats.added > 0 || stats.removed > 0) && ( + + {stats.added > 0 && ( + + +{stats.added} + + )} + {stats.removed > 0 && ( + + -{stats.removed} + + )} + + )} + {commentCount > 0 && ( + + {commentCount} comment{commentCount !== 1 ? "s" : ""} + + )} +
); }, - [threadsByFile] + [commentCountByFile, toggleCollapsed] ); return ( @@ -867,11 +1049,14 @@ export function CodeReview() { items={items} options={options} selectedLines={selectedLines} - onSelectedLinesChange={setSelectedLines} + onSelectedLinesChange={(value) => { + setSelectedLines(value); + selectedItemIdRef.current = value?.id ?? null; + if (value === null) setPendingComposer(null); + }} onScroll={handleScroll} renderAnnotation={renderAnnotation} renderCustomHeader={renderCustomHeader} - renderHeaderPrefix={renderHeaderPrefix} />
@@ -894,16 +1079,17 @@ function Header() {
- + {DIFF.from} into - + {DIFF.to} {DIFF.changes.length} files
+ ); } @@ -949,7 +1135,7 @@ function CommentsSidebar({