From 7eb60ebcf247a7d3cdb104057104b268762f6858 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 26 Nov 2025 04:08:08 -0800 Subject: [PATCH] feat: update mulTmux terminal tool and improve shape utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates to collaborative terminal integration and various shape improvements across the canvas. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- _redirects | 29 +- .../server/src/websocket/TerminalHandler.ts | 4 +- package-lock.json | 1382 +++++++++++++++-- src/App.tsx | 38 +- src/automerge/AutomergeToTLStore.ts | 53 +- src/automerge/CloudflareAdapter.ts | 70 +- src/automerge/MinimalSanitization.ts | 7 +- src/automerge/useAutomergeStoreV2.ts | 265 ++-- src/automerge/useAutomergeSyncRepo.ts | 218 ++- src/components/FathomMeetingsPanel.tsx | 3 +- src/components/StandardizedToolWrapper.tsx | 43 +- src/css/crypto-auth.css | 71 +- src/css/obsidian-browser.css | 30 + src/css/obsidian-toolbar.css | 9 +- src/css/starred-boards.css | 35 +- src/css/style.css | 102 +- src/css/user-profile.css | 5 +- src/public/_redirects | 29 +- src/routes/Board.tsx | 130 +- src/routes/Dashboard.tsx | 4 +- src/routes/Default.tsx | 6 +- src/routes/Presentations.tsx | 4 +- src/routes/Resilience.tsx | 2 +- src/shapes/ChatBoxShapeUtil.tsx | 7 +- src/shapes/EmbedShapeUtil.tsx | 604 ++++--- src/shapes/MarkdownShapeUtil.tsx | 238 ++- src/shapes/MultmuxShapeUtil.tsx | 493 ++++++ src/shapes/PromptShapeUtil.tsx | 119 +- src/tools/MultmuxTool.ts | 11 + src/ui/CustomToolbar.tsx | 74 +- src/ui/overrides.tsx | 2 +- worker/AutomergeDurableObject.ts | 111 +- 32 files changed, 3279 insertions(+), 919 deletions(-) create mode 100644 src/shapes/MultmuxShapeUtil.tsx create mode 100644 src/tools/MultmuxTool.ts diff --git a/_redirects b/_redirects index 7ca73b2..5584709 100644 --- a/_redirects +++ b/_redirects @@ -1,14 +1,25 @@ # Cloudflare Pages redirects and rewrites # This file handles SPA routing and URL rewrites (replaces vercel.json rewrites) -# SPA fallback - all routes should serve index.html +# Specific route rewrites (matching vercel.json) +# Handle both with and without trailing slashes +/board/* /index.html 200 +/board /index.html 200 +/board/ /index.html 200 +/inbox /index.html 200 +/inbox/ /index.html 200 +/contact /index.html 200 +/contact/ /index.html 200 +/presentations /index.html 200 +/presentations/ /index.html 200 +/presentations/* /index.html 200 +/dashboard /index.html 200 +/dashboard/ /index.html 200 +/login /index.html 200 +/login/ /index.html 200 +/debug /index.html 200 +/debug/ /index.html 200 + +# SPA fallback - all routes should serve index.html (must be last) /* /index.html 200 -# Specific route rewrites (matching vercel.json) -/board/* /index.html 200 -/board /index.html 200 -/inbox /index.html 200 -/contact /index.html 200 -/presentations /index.html 200 -/dashboard /index.html 200 - diff --git a/multmux/packages/server/src/websocket/TerminalHandler.ts b/multmux/packages/server/src/websocket/TerminalHandler.ts index 30e4e7d..548ca52 100644 --- a/multmux/packages/server/src/websocket/TerminalHandler.ts +++ b/multmux/packages/server/src/websocket/TerminalHandler.ts @@ -51,11 +51,11 @@ export class TerminalHandler { ws.send(JSON.stringify(message)); }; - terminal.onData(onData); + const dataListener = terminal.onData(onData); // Clean up on disconnect ws.on('close', () => { - terminal.off('data', onData); + dataListener.dispose(); this.handleDisconnect(clientId); }); } diff --git a/package-lock.json b/package-lock.json index 4d1f49c..4c7ae7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "jeffemmett", "version": "1.0.0", "license": "ISC", + "workspaces": [ + "multmux/packages/*" + ], "dependencies": { "@anthropic-ai/sdk": "^0.33.1", "@automerge/automerge": "^3.1.1", @@ -68,9 +71,94 @@ "wrangler": "^4.33.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, + "multmux/packages/cli": { + "name": "@multmux/cli", + "version": "0.1.0", + "dependencies": { + "blessed": "^0.1.81", + "chalk": "^4.1.2", + "commander": "^11.1.0", + "node-fetch": "^2.7.0", + "ora": "^5.4.1", + "ws": "^8.16.0" + }, + "bin": { + "multmux": "dist/index.js" + }, + "devDependencies": { + "@types/blessed": "^0.1.25", + "@types/node": "^20.0.0", + "@types/node-fetch": "^2.6.9", + "@types/ws": "^8.5.10", + "tsx": "^4.7.0", + "typescript": "^5.0.0" + } + }, + "multmux/packages/cli/node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "multmux/packages/cli/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "multmux/packages/cli/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, + "license": "MIT" + }, + "multmux/packages/server": { + "name": "@multmux/server", + "version": "0.1.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.0", + "nanoid": "^3.3.7", + "node-pty": "^1.0.0", + "ws": "^8.16.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^20.0.0", + "@types/ws": "^8.5.10", + "tsx": "^4.7.0", + "typescript": "^5.0.0" + } + }, + "multmux/packages/server/node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "multmux/packages/server/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, + "license": "MIT" + }, "node_modules/@ai-sdk/provider": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", @@ -851,9 +939,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -868,9 +956,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -885,9 +973,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -902,9 +990,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -919,9 +1007,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -936,9 +1024,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -953,9 +1041,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -970,9 +1058,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -987,9 +1075,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -1004,9 +1092,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -1021,9 +1109,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -1038,9 +1126,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -1055,9 +1143,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -1072,9 +1160,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -1089,9 +1177,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -1106,9 +1194,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -1123,9 +1211,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -1140,9 +1228,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -1157,9 +1245,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -1174,9 +1262,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -1191,9 +1279,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -1208,9 +1296,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -1225,9 +1313,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -1242,9 +1330,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -1259,9 +1347,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -1276,9 +1364,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -2334,6 +2422,14 @@ "npm": ">=7.0.0" } }, + "node_modules/@multmux/cli": { + "resolved": "multmux/packages/cli", + "link": true + }, + "node_modules/@multmux/server": { + "resolved": "multmux/packages/server", + "link": true + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -5347,6 +5443,27 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/blessed": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@types/blessed/-/blessed-0.1.26.tgz", + "integrity": "sha512-TAFZ4PF1bU0uVy86NTVAyhtC8BBWhjQLUfIS2N5P7YTJil+PtxpdPZ0AmpmJWO8pZ7RYPrcbGYZW0sVYVCEVLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/codemirror": { "version": "0.0.108", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz", @@ -5356,12 +5473,32 @@ "@types/tern": "*" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/core-js": { "version": "2.5.8", "resolved": "https://registry.npmjs.org/@types/core-js/-/core-js-2.5.8.tgz", "integrity": "sha512-VgnAj6tIAhJhZdJ8/IpxdatM8G4OD3VWGlp6xIxUGENZlpbob9Ty4VVdC1FIEp0aK6DBscDDjyzy5FB60TuNqg==", "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5410,6 +5547,32 @@ "@types/estree": "*" } }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -5425,6 +5588,13 @@ "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -5485,6 +5655,13 @@ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "license": "MIT" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -5522,6 +5699,13 @@ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "license": "MIT" }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", @@ -5529,6 +5713,13 @@ "license": "MIT", "optional": true }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/rbush": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz", @@ -5556,6 +5747,39 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/tern": { "version": "0.23.9", "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", @@ -5583,6 +5807,16 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@uiw/copy-to-clipboard": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.17.tgz", @@ -5816,6 +6050,19 @@ "node": ">=12" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -5938,7 +6185,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5971,6 +6217,12 @@ "node": ">=10" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/asn1js": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", @@ -6209,6 +6461,18 @@ "dev": true, "license": "MIT" }, + "node_modules/blessed": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz", + "integrity": "sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==", + "license": "MIT", + "bin": { + "blessed": "bin/tput.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/blockstore-core": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blockstore-core/-/blockstore-core-2.0.2.tgz", @@ -6258,6 +6522,57 @@ "npm": ">=7.0.0" } }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -6404,6 +6719,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/camel-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", @@ -6518,7 +6849,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -6535,7 +6865,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -6643,6 +6972,30 @@ "node": ">= 10.0" } }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -6664,6 +7017,15 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/cloudflare-workers-unfurl": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/cloudflare-workers-unfurl/-/cloudflare-workers-unfurl-0.0.7.tgz", @@ -6768,6 +7130,27 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6784,6 +7167,12 @@ "node": ">=18" } }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/core-js": { "version": "3.45.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", @@ -6795,6 +7184,19 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cose-base": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", @@ -7619,6 +8021,18 @@ "node": ">=4.0.0" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", @@ -7645,6 +8059,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -7654,6 +8077,16 @@ "node": ">=6" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -7833,6 +8266,12 @@ "node": ">= 0.4" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.213", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz", @@ -7854,6 +8293,15 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -7933,6 +8381,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -7943,6 +8433,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", @@ -8017,6 +8513,15 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -8072,6 +8577,76 @@ "node": ">=6" } }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", @@ -8164,6 +8739,39 @@ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", "license": "MIT" }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/fission-bloom-filters": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/fission-bloom-filters/-/fission-bloom-filters-1.7.1.tgz", @@ -8247,6 +8855,15 @@ "node": ">= 12.20" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fp-ts": { "version": "2.16.11", "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.11.tgz", @@ -8260,6 +8877,15 @@ "integrity": "sha512-0tLU0FOedVY7lrvN4LK0DVj6FTuYM0pWDpN97/8UTZE2lx1+OwX8+2uL7IOWc2PmktYTHQjMT6FvZZ3SGCdZdg==", "license": "CC0-1.0" }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -8356,6 +8982,19 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -8480,7 +9119,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9008,6 +9646,22 @@ "entities": "^2.0.0" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -9202,6 +9856,15 @@ "fp-ts": "^2.5.0" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/ipfs-core-types": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/ipfs-core-types/-/ipfs-core-types-0.13.0.tgz", @@ -9453,6 +10116,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -9477,6 +10149,18 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "license": "MIT" }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-url": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", @@ -10017,6 +10701,22 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -10421,6 +11121,24 @@ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "license": "MIT" }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-options": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", @@ -10465,6 +11183,15 @@ "license": "(MPL-2.0 OR Apache-2.0)", "optional": true }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -11062,6 +11789,15 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -11207,6 +11943,12 @@ "npm": ">=7.0.0" } }, + "node_modules/nan": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.1.tgz", + "integrity": "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -11246,6 +11988,15 @@ "undici": "*" } }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -11374,6 +12125,16 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.17.0" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -11406,6 +12167,27 @@ "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -11413,6 +12195,18 @@ "dev": true, "license": "MIT" }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -11428,6 +12222,21 @@ "integrity": "sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q==", "license": "MIT" }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/onnx-proto": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz", @@ -11540,6 +12349,29 @@ } } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/orderedmap": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", @@ -11633,6 +12465,15 @@ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "license": "MIT" }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/pascal-case": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", @@ -11643,6 +12484,12 @@ "tslib": "^2.0.3" } }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -12034,6 +12881,19 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -12100,6 +12960,21 @@ "node": ">=6.0.0" } }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -12246,6 +13121,42 @@ "performance-now": "^2.1.0" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rbush": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", @@ -12883,6 +13794,16 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -12890,6 +13811,19 @@ "deprecated": "https://github.com/lydell/resolve-url#deprecated", "license": "MIT" }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -13085,12 +14019,93 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "license": "MIT" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", @@ -13157,6 +14172,78 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -13299,6 +14386,15 @@ "node": ">=0.1.14" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stoppable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", @@ -13636,6 +14732,15 @@ "react-dom": "^18.2.0 || ^19.0.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -13709,6 +14814,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -13727,6 +14852,19 @@ "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", "license": "Unlicense" }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", @@ -13969,6 +15107,15 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -14105,6 +15252,15 @@ "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", @@ -14133,6 +15289,15 @@ "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", "license": "MIT" }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -14290,48 +15455,6 @@ "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7" } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" - } - }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -14360,6 +15483,15 @@ "node": ">=12" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/src/App.tsx b/src/App.tsx index 1e49ef9..7ac10ae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import "tldraw/tldraw.css" import "@/css/style.css" import { Default } from "@/routes/Default" -import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom" +import { BrowserRouter, Route, Routes, Navigate, useParams } from "react-router-dom" import { Contact } from "@/routes/Contact" import { Board } from "./routes/Board" import { Inbox } from "./routes/Inbox" @@ -67,6 +67,14 @@ const OptionalAuthRoute = ({ children }: { children: React.ReactNode }) => { return <>{children}; }; +/** + * Component to redirect board URLs without trailing slashes + */ +const RedirectBoardSlug = () => { + const { slug } = useParams<{ slug: string }>(); + return ; +}; + /** * Main App with context providers */ @@ -101,46 +109,56 @@ const AppWithProviders = () => { + {/* Redirect routes without trailing slashes to include them */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* Auth routes */} - } /> - + } /> + {/* Optional auth routes */} } /> - } /> - } /> - } /> - } /> - } /> - } /> - diff --git a/src/automerge/AutomergeToTLStore.ts b/src/automerge/AutomergeToTLStore.ts index 3bec1b9..9c3f937 100644 --- a/src/automerge/AutomergeToTLStore.ts +++ b/src/automerge/AutomergeToTLStore.ts @@ -28,10 +28,11 @@ export function applyAutomergePatchesToTLStore( const existingRecord = getRecordFromStore(store, id) - // CRITICAL: For shapes, get coordinates from store's current state BEFORE any patch processing - // This ensures we preserve coordinates even if patches don't include them + // CRITICAL: For shapes, get coordinates and parentId from store's current state BEFORE any patch processing + // This ensures we preserve coordinates and parent relationships even if patches don't include them // This is especially important when patches come back after store.put operations let storeCoordinates: { x?: number; y?: number } = {} + let storeParentId: string | undefined = undefined if (existingRecord && existingRecord.typeName === 'shape') { const storeX = (existingRecord as any).x const storeY = (existingRecord as any).y @@ -41,15 +42,21 @@ export function applyAutomergePatchesToTLStore( if (typeof storeY === 'number' && !isNaN(storeY) && storeY !== null && storeY !== undefined) { storeCoordinates.y = storeY } + // CRITICAL: Preserve parentId from store (might be a frame or group!) + const existingParentId = (existingRecord as any).parentId + if (existingParentId && typeof existingParentId === 'string') { + storeParentId = existingParentId + } } // CRITICAL: If record doesn't exist in store yet, try to get it from Automerge document // This prevents coordinates from defaulting to 0,0 when patches create new records let automergeRecord: any = null + let automergeParentId: string | undefined = undefined if (!existingRecord && automergeDoc && automergeDoc.store && automergeDoc.store[id]) { try { automergeRecord = automergeDoc.store[id] - // Extract coordinates from Automerge record if it's a shape + // Extract coordinates and parentId from Automerge record if it's a shape if (automergeRecord && automergeRecord.typeName === 'shape') { const docX = automergeRecord.x const docY = automergeRecord.y @@ -59,6 +66,10 @@ export function applyAutomergePatchesToTLStore( if (typeof docY === 'number' && !isNaN(docY) && docY !== null && docY !== undefined) { storeCoordinates.y = docY } + // CRITICAL: Preserve parentId from Automerge document (might be a frame!) + if (automergeRecord.parentId && typeof automergeRecord.parentId === 'string') { + automergeParentId = automergeRecord.parentId + } } } catch (e) { // If we can't read from Automerge doc, continue without it @@ -324,6 +335,22 @@ export function applyAutomergePatchesToTLStore( } as TLRecord } } + + // CRITICAL: Preserve parentId from store or Automerge document + // This prevents shapes from losing their frame/group parent relationships + // which causes them to reset to (0,0) on the page instead of maintaining their position in the frame + // Priority: store parentId (most reliable), then Automerge parentId, then patch value + const preservedParentId = storeParentId || automergeParentId + if (preservedParentId !== undefined) { + const patchedParentId = (currentRecord as any).parentId + // If patch didn't include parentId, or it's missing/default, use the preserved parentId + if (!patchedParentId || (patchedParentId === 'page:page' && preservedParentId !== 'page:page')) { + updatedObjects[id] = { + ...currentRecord, + parentId: preservedParentId + } as TLRecord + } + } } // CRITICAL: Re-check typeName after patch application to ensure it's still correct @@ -371,6 +398,18 @@ export function applyAutomergePatchesToTLStore( // put / remove the records in the store // Log patch application for debugging console.log(`🔧 AutomergeToTLStore: Applying ${patches.length} patches, ${toPut.length} records to put, ${toRemove.length} records to remove`) + + // DEBUG: Log shape updates being applied to store + toPut.forEach(record => { + if (record.typeName === 'shape' && (record as any).props?.w) { + console.log(`🔧 AutomergeToTLStore: Putting shape ${(record as any).type} ${record.id}:`, { + w: (record as any).props.w, + h: (record as any).props.h, + x: (record as any).x, + y: (record as any).y + }) + } + }) if (failedRecords.length > 0) { console.log({ patches, toPut: toPut.length, failed: failedRecords.length }) @@ -534,7 +573,13 @@ export function sanitizeRecord(record: any): TLRecord { // Ensure meta is a mutable copy to preserve all properties (including text for rectangles) sanitized.meta = { ...sanitized.meta } } - if (!sanitized.index) sanitized.index = 'a1' + // CRITICAL: IndexKey must follow tldraw's fractional indexing format + // Valid format: starts with 'a' followed by digits, optionally followed by uppercase letters + // Examples: "a1", "a2", "a10", "a1V" (fractional between a1 and a2) + // Invalid: "c1", "b1", "z999" (must start with 'a') + if (!sanitized.index || typeof sanitized.index !== 'string' || !/^a\d+[A-Z]*$/.test(sanitized.index)) { + sanitized.index = 'a1' + } if (!sanitized.parentId) sanitized.parentId = 'page:page' if (!sanitized.props || typeof sanitized.props !== 'object') sanitized.props = {} diff --git a/src/automerge/CloudflareAdapter.ts b/src/automerge/CloudflareAdapter.ts index 811a5e1..a913e84 100644 --- a/src/automerge/CloudflareAdapter.ts +++ b/src/automerge/CloudflareAdapter.ts @@ -166,6 +166,7 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { private websocket: WebSocket | null = null private roomId: string | null = null public peerId: PeerId | undefined = undefined + public sessionId: string | null = null // Track our session ID private readyPromise: Promise private readyResolve: (() => void) | null = null private keepAliveInterval: NodeJS.Timeout | null = null @@ -175,12 +176,19 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { private reconnectDelay: number = 1000 private isConnecting: boolean = false private onJsonSyncData?: (data: any) => void + private onPresenceUpdate?: (userId: string, data: any, senderId?: string, userName?: string, userColor?: string) => void - constructor(workerUrl: string, roomId?: string, onJsonSyncData?: (data: any) => void) { + constructor( + workerUrl: string, + roomId?: string, + onJsonSyncData?: (data: any) => void, + onPresenceUpdate?: (userId: string, data: any, senderId?: string, userName?: string, userColor?: string) => void + ) { super() this.workerUrl = workerUrl this.roomId = roomId || 'default-room' this.onJsonSyncData = onJsonSyncData + this.onPresenceUpdate = onPresenceUpdate this.readyPromise = new Promise((resolve) => { this.readyResolve = resolve }) @@ -209,11 +217,13 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { // Use the room ID from constructor or default // Add sessionId as a query parameter as required by AutomergeDurableObject const sessionId = peerId || `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + this.sessionId = sessionId // Store our session ID for filtering echoes + // Convert https:// to wss:// or http:// to ws:// const protocol = this.workerUrl.startsWith('https://') ? 'wss://' : 'ws://' const baseUrl = this.workerUrl.replace(/^https?:\/\//, '') const wsUrl = `${protocol}${baseUrl}/connect/${this.roomId}?sessionId=${sessionId}` - + this.isConnecting = true // Add a small delay to ensure the server is ready @@ -267,12 +277,21 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { this.sendPong() return } - + // Handle test messages if (message.type === 'test') { console.log('🔌 CloudflareAdapter: Received test message:', message.message) return } + + // Handle presence updates from other clients + if (message.type === 'presence') { + // Pass senderId, userName, and userColor so we can create proper instance_presence records + if (this.onPresenceUpdate && message.userId && message.data) { + this.onPresenceUpdate(message.userId, message.data, message.senderId, message.userName, message.userColor) + } + return + } // Convert the message to the format expected by Automerge if (message.type === 'sync' && message.data) { @@ -283,14 +302,20 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { documentIdType: typeof message.documentId }) - // JSON sync is deprecated - all data flows through Automerge sync protocol - // Old format content is converted server-side and saved to R2 in Automerge format - // Skip JSON sync messages - they should not be sent anymore + // JSON sync for real-time collaboration + // When we receive TLDraw changes from other clients, apply them locally const isJsonDocumentData = message.data && typeof message.data === 'object' && message.data.store - + if (isJsonDocumentData) { - console.warn('⚠️ CloudflareAdapter: Received JSON sync message (deprecated). Ignoring - all data should flow through Automerge sync protocol.') - return // Don't process JSON sync messages + console.log('📥 CloudflareAdapter: Received JSON sync message with store data') + + // Call the JSON sync callback to apply changes + if (this.onJsonSyncData) { + this.onJsonSyncData(message.data) + } else { + console.warn('⚠️ No JSON sync callback registered') + } + return // JSON sync handled } // Validate documentId - Automerge requires a valid Automerge URL format @@ -376,6 +401,18 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { } send(message: Message): void { + // Only log non-presence messages to reduce console spam + if (message.type !== 'presence') { + console.log('📤 CloudflareAdapter.send() called:', { + messageType: message.type, + dataType: (message as any).data?.constructor?.name || typeof (message as any).data, + dataLength: (message as any).data?.byteLength || (message as any).data?.length, + documentId: (message as any).documentId, + hasTargetId: !!message.targetId, + hasSenderId: !!message.senderId + }) + } + if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { // Check if this is a binary sync message from Automerge Repo if (message.type === 'sync' && (message as any).data instanceof ArrayBuffer) { @@ -396,7 +433,10 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { this.websocket.send((message as any).data.buffer) } else { // Handle text-based messages (backward compatibility and control messages) - console.log('📤 Sending WebSocket message:', message.type) + // Only log non-presence messages + if (message.type !== 'presence') { + console.log('📤 Sending WebSocket message:', message.type) + } // Debug: Log patch content if it's a patch message if (message.type === 'patch' && (message as any).patches) { console.log('🔍 Sending patches:', (message as any).patches.length, 'patches') @@ -411,10 +451,12 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { this.websocket.send(JSON.stringify(message)) } } else { - console.warn('⚠️ CloudflareAdapter: Cannot send message - WebSocket not open', { - messageType: message.type, - readyState: this.websocket?.readyState - }) + if (message.type !== 'presence') { + console.warn('⚠️ CloudflareAdapter: Cannot send message - WebSocket not open', { + messageType: message.type, + readyState: this.websocket?.readyState + }) + } } } diff --git a/src/automerge/MinimalSanitization.ts b/src/automerge/MinimalSanitization.ts index 600f589..c2862c8 100644 --- a/src/automerge/MinimalSanitization.ts +++ b/src/automerge/MinimalSanitization.ts @@ -20,8 +20,11 @@ function minimalSanitizeRecord(record: any): any { if (typeof sanitized.isLocked !== 'boolean') sanitized.isLocked = false if (typeof sanitized.opacity !== 'number') sanitized.opacity = 1 if (!sanitized.meta || typeof sanitized.meta !== 'object') sanitized.meta = {} - // Validate and fix index property - must be a valid IndexKey (like 'a1', 'a2', etc.) - if (!sanitized.index || typeof sanitized.index !== 'string' || !/^[a-z]\d+$/.test(sanitized.index)) { + // CRITICAL: IndexKey must follow tldraw's fractional indexing format + // Valid format: starts with 'a' followed by digits, optionally followed by uppercase letters + // Examples: "a1", "a2", "a10", "a1V" (fractional between a1 and a2) + // Invalid: "c1", "b1", "z999" (must start with 'a') + if (!sanitized.index || typeof sanitized.index !== 'string' || !/^a\d+[A-Z]*$/.test(sanitized.index)) { sanitized.index = 'a1' } if (!sanitized.parentId) sanitized.parentId = 'page:page' diff --git a/src/automerge/useAutomergeStoreV2.ts b/src/automerge/useAutomergeStoreV2.ts index d9c9c3f..0691a7b 100644 --- a/src/automerge/useAutomergeStoreV2.ts +++ b/src/automerge/useAutomergeStoreV2.ts @@ -6,12 +6,13 @@ import { RecordsDiff, } from "@tldraw/tldraw" import { createTLSchema, defaultBindingSchemas, defaultShapeSchemas } from "@tldraw/tlschema" -import { useEffect, useState } from "react" +import { useEffect, useState, useRef } from "react" import { DocHandle, DocHandleChangePayload } from "@automerge/automerge-repo" import { useLocalAwareness, useRemoteAwareness, } from "@automerge/automerge-repo-react-hooks" +import throttle from "lodash.throttle" import { applyAutomergePatchesToTLStore, sanitizeRecord } from "./AutomergeToTLStore.js" import { applyTLStoreChangesToAutomerge } from "./TLStoreToAutomerge.js" @@ -128,11 +129,13 @@ import { FathomMeetingsBrowserShape } from "@/shapes/FathomMeetingsBrowserShapeU export function useAutomergeStoreV2({ handle, userId: _userId, + adapter, }: { handle: DocHandle userId: string + adapter?: any }): TLStoreWithStatus { - console.log("useAutomergeStoreV2 called with handle:", !!handle) + console.log("useAutomergeStoreV2 called with handle:", !!handle, "adapter:", !!adapter) // Create a custom schema that includes all the custom shapes const customSchema = createTLSchema({ @@ -202,67 +205,46 @@ export function useAutomergeStoreV2({ const unsubs: (() => void)[] = [] - // A hacky workaround to prevent local changes from being applied twice - // once into the automerge doc and then back again. + // Track local changes to prevent echoing them back + // Simple boolean flag: set to true when making local changes, + // then reset on the NEXT Automerge change event (which is the echo) let isLocalChange = false - // Helper function to manually trigger sync after document changes - // The Automerge Repo doesn't auto-broadcast because our WebSocket setup doesn't use peer discovery - const triggerSync = () => { - try { - console.log('🔄 triggerSync() called') - const repo = (handle as any).repo - console.log('🔍 repo:', !!repo, 'handle:', !!handle, 'documentId:', handle?.documentId) + // Helper function to broadcast changes via JSON sync + // DISABLED: This causes last-write-wins conflicts + // Automerge should handle sync automatically via binary protocol + // We're keeping this function but disabling all actual broadcasting + const broadcastJsonSync = (changedRecords: any[]) => { + // TEMPORARY FIX: Manually broadcast changes via WebSocket since Automerge Repo sync isn't working + // This sends the full changed records as JSON to other clients + // TODO: Fix Automerge Repo's binary sync protocol to work properly - if (repo) { - console.log('🔍 repo.networkSubsystem:', !!repo.networkSubsystem) - console.log('🔍 repo.networkSubsystem.syncDoc:', typeof repo.networkSubsystem?.syncDoc) - console.log('🔍 repo.networkSubsystem.adapters:', !!repo.networkSubsystem?.adapters) + if (!changedRecords || changedRecords.length === 0) { + return + } - // Try multiple approaches to trigger sync + console.log(`📤 Broadcasting ${changedRecords.length} changed records via manual JSON sync`) - // Approach 1: Use networkSubsystem.syncDoc if available - if (repo.networkSubsystem && typeof repo.networkSubsystem.syncDoc === 'function') { - console.log('🔄 Triggering sync via networkSubsystem.syncDoc()') - repo.networkSubsystem.syncDoc(handle.documentId) - } - // Approach 2: Broadcast to all network adapters directly - else if (repo.networkSubsystem && repo.networkSubsystem.adapters) { - console.log('🔄 Broadcasting sync to all network adapters') - const adapters = Array.from(repo.networkSubsystem.adapters.values()) - console.log('🔍 Found adapters:', adapters.length) - adapters.forEach((adapter: any) => { - console.log('🔍 Adapter has send:', typeof adapter?.send) - if (adapter && typeof adapter.send === 'function') { - // Send a sync message via the adapter - // The adapter should handle converting this to the right format - console.log('📤 Sending sync via adapter') - adapter.send({ - type: 'sync', - documentId: handle.documentId, - data: handle.doc() - }) - } - }) - } - // Approach 3: Emit an event to trigger sync - else if (repo.emit && typeof repo.emit === 'function') { - console.log('🔄 Emitting document change event') - repo.emit('change', { documentId: handle.documentId, doc: handle.doc() }) - } - else { - console.warn('⚠️ No known method to trigger sync broadcast found') - } - } else { - console.warn('⚠️ No repo found on handle') - } - } catch (error) { - console.error('❌ Error triggering manual sync:', error) + if (adapter && typeof (adapter as any).send === 'function') { + // Send changes to other clients via the network adapter + (adapter as any).send({ + type: 'sync', + data: { + store: Object.fromEntries(changedRecords.map(r => [r.id, r])) + }, + documentId: handle?.documentId, + timestamp: Date.now() + }) + } else { + console.warn('⚠️ Cannot broadcast - adapter not available') } } // Listen for changes from Automerge and apply them to TLDraw const automergeChangeHandler = (payload: DocHandleChangePayload) => { + // Skip the immediate echo of our own local changes + // This flag is set when we update Automerge from TLDraw changes + // and gets reset after skipping one change event (the echo) if (isLocalChange) { isLocalChange = false return @@ -449,7 +431,7 @@ export function useAutomergeStoreV2({ // Throttle position-only updates (x/y changes) to reduce automerge saves during movement let positionUpdateQueue: RecordsDiff | null = null let positionUpdateTimeout: NodeJS.Timeout | null = null - const POSITION_UPDATE_THROTTLE_MS = 1000 // Save position updates every 1 second + const POSITION_UPDATE_THROTTLE_MS = 100 // Save position updates every 100ms for real-time feel const flushPositionUpdates = () => { if (positionUpdateQueue && handle) { @@ -464,13 +446,14 @@ export function useAutomergeStoreV2({ applyTLStoreChangesToAutomerge(doc, queuedChanges) }) // Trigger sync to broadcast position updates - triggerSync() - setTimeout(() => { - isLocalChange = false - }, 100) + const changedRecords = [ + ...Object.values(queuedChanges.added || {}), + ...Object.values(queuedChanges.updated || {}), + ...Object.values(queuedChanges.removed || {}) + ] + broadcastJsonSync(changedRecords) } catch (error) { console.error("Error applying throttled position updates to Automerge:", error) - isLocalChange = false } }) } @@ -855,7 +838,14 @@ export function useAutomergeStoreV2({ if (filteredTotalChanges === 0) { return } - + + // CRITICAL: Skip broadcasting changes that came from remote sources to prevent feedback loops + // Only broadcast changes that originated from user interactions (source === 'user') + if (source === 'remote') { + console.log('🔄 Skipping broadcast for remote change to prevent feedback loop') + return + } + // CRITICAL: Filter out x/y coordinate changes for pinned-to-view shapes // When a shape is pinned, its x/y coordinates change to stay in the same screen position, // but we want to keep the original coordinates static in Automerge @@ -989,7 +979,39 @@ export function useAutomergeStoreV2({ // Check if this is a position-only update that should be throttled const isPositionOnly = isPositionOnlyUpdate(finalFilteredChanges) - + + // Log what type of change this is for debugging + const changeType = Object.keys(finalFilteredChanges.added || {}).length > 0 ? 'added' : + Object.keys(finalFilteredChanges.removed || {}).length > 0 ? 'removed' : + isPositionOnly ? 'position-only' : 'property-change' + + // DEBUG: Log dimension changes for shapes + if (finalFilteredChanges.updated) { + Object.entries(finalFilteredChanges.updated).forEach(([id, recordTuple]: [string, any]) => { + const isTuple = Array.isArray(recordTuple) && recordTuple.length === 2 + const oldRecord = isTuple ? recordTuple[0] : null + const newRecord = isTuple ? recordTuple[1] : recordTuple + if (newRecord?.typeName === 'shape') { + const oldProps = oldRecord?.props || {} + const newProps = newRecord?.props || {} + if (oldProps.w !== newProps.w || oldProps.h !== newProps.h) { + console.log(`🔍 Shape dimension change detected for ${newRecord.type} ${id}:`, { + oldDims: { w: oldProps.w, h: oldProps.h }, + newDims: { w: newProps.w, h: newProps.h }, + source + }) + } + } + }) + } + + console.log(`🔍 Change detected: ${changeType}, will ${isPositionOnly ? 'throttle' : 'broadcast immediately'}`, { + added: Object.keys(finalFilteredChanges.added || {}).length, + updated: Object.keys(finalFilteredChanges.updated || {}).length, + removed: Object.keys(finalFilteredChanges.removed || {}).length, + source + }) + if (isPositionOnly && positionUpdateQueue === null) { // Start a new queue for position updates positionUpdateQueue = finalFilteredChanges @@ -1046,9 +1068,9 @@ export function useAutomergeStoreV2({ if (positionUpdateQueue) { flushPositionUpdates() } - + // CRITICAL: Don't skip changes - always save them to ensure consistency - // The isLocalChange flag is only used to prevent feedback loops from Automerge changes + // The local change timestamp is only used to prevent immediate feedback loops // We should always save TLDraw changes, even if they came from Automerge sync // This ensures that all shapes (notes, rectangles, etc.) are consistently persisted @@ -1106,13 +1128,14 @@ export function useAutomergeStoreV2({ applyTLStoreChangesToAutomerge(doc, queuedChanges) }) // Trigger sync to broadcast eraser changes - triggerSync() - setTimeout(() => { - isLocalChange = false - }, 100) + const changedRecords = [ + ...Object.values(queuedChanges.added || {}), + ...Object.values(queuedChanges.updated || {}), + ...Object.values(queuedChanges.removed || {}) + ] + broadcastJsonSync(changedRecords) } catch (error) { console.error('❌ Error applying queued eraser changes:', error) - isLocalChange = false } } }, 50) // Check every 50ms for faster response @@ -1136,17 +1159,19 @@ export function useAutomergeStoreV2({ updated: { ...(queuedChanges.updated || {}), ...(finalFilteredChanges.updated || {}) }, removed: { ...(queuedChanges.removed || {}), ...(finalFilteredChanges.removed || {}) } } - + requestAnimationFrame(() => { isLocalChange = true handle.change((doc) => { applyTLStoreChangesToAutomerge(doc, mergedChanges) }) // Trigger sync to broadcast merged changes - triggerSync() - setTimeout(() => { - isLocalChange = false - }, 100) + const changedRecords = [ + ...Object.values(mergedChanges.added || {}), + ...Object.values(mergedChanges.updated || {}), + ...Object.values(mergedChanges.removed || {}) + ] + broadcastJsonSync(changedRecords) }) return @@ -1154,22 +1179,21 @@ export function useAutomergeStoreV2({ // OPTIMIZED: Use requestIdleCallback to defer Automerge changes when browser is idle // This prevents blocking mouse interactions without queuing changes const applyChanges = () => { - // Set flag to prevent feedback loop when this change comes back from Automerge + // Mark to prevent feedback loop when this change comes back from Automerge isLocalChange = true handle.change((doc) => { applyTLStoreChangesToAutomerge(doc, finalFilteredChanges) }) - // CRITICAL: Manually trigger Automerge Repo to broadcast changes + // CRITICAL: Manually trigger JSON sync broadcast to other clients // Use requestAnimationFrame to defer this slightly so the change is fully processed - requestAnimationFrame(triggerSync) - - // Reset flag after a short delay to allow Automerge change handler to process - // This prevents feedback loops while ensuring all changes are saved - setTimeout(() => { - isLocalChange = false - }, 100) + const changedRecords = [ + ...Object.values(finalFilteredChanges.added || {}), + ...Object.values(finalFilteredChanges.updated || {}), + ...Object.values(finalFilteredChanges.removed || {}) + ] + requestAnimationFrame(() => broadcastJsonSync(changedRecords)) } // Use requestIdleCallback if available to apply changes when browser is idle @@ -1192,8 +1216,6 @@ export function useAutomergeStoreV2({ const docAfter = handle.doc() } catch (error) { console.error("Error applying TLDraw changes to Automerge:", error) - // Reset flag on error to prevent getting stuck - isLocalChange = false } } }, { @@ -1229,9 +1251,6 @@ export function useAutomergeStoreV2({ handle.change((doc) => { applyTLStoreChangesToAutomerge(doc, queuedChanges) }) - setTimeout(() => { - isLocalChange = false - }, 100) } } } @@ -1403,23 +1422,73 @@ export function useAutomergePresence(params: { name: string color: string } + adapter?: any }) { - const { handle, store, userMetadata } = params - - // Simple presence implementation - useEffect(() => { - if (!handle || !store) return + const { handle, store, userMetadata, adapter } = params + const presenceRef = useRef>(new Map()) - const updatePresence = () => { - // Basic presence update logic - console.log("Updating presence for user:", userMetadata.userId) + // Broadcast local presence to other clients + useEffect(() => { + if (!handle || !store || !adapter) { + return } - updatePresence() - }, [handle, store, userMetadata]) + // Listen for changes to instance_presence records in the store + // These represent user cursors, selections, etc. + const handleStoreChange = () => { + if (!store) return + + const allRecords = store.allRecords() + + // Filter for ALL presence-related records + // instance_presence: Contains user cursor, name, color - THIS IS WHAT WE NEED! + // instance_page_state: Contains selections, editing state + // pointer: Contains pointer position + const presenceRecords = allRecords.filter((r: any) => { + const isPresenceType = r.typeName === 'instance_presence' || + r.typeName === 'instance_page_state' || + r.typeName === 'pointer' + + const hasPresenceId = r.id?.startsWith('instance_presence:') || + r.id?.startsWith('instance_page_state:') || + r.id?.startsWith('pointer:') + + return isPresenceType || hasPresenceId + }) + + if (presenceRecords.length > 0) { + // Send presence update via WebSocket + try { + const presenceData: any = {} + presenceRecords.forEach((record: any) => { + presenceData[record.id] = record + }) + + adapter.send({ + type: 'presence', + userId: userMetadata.userId, + userName: userMetadata.name, + userColor: userMetadata.color, + data: presenceData + }) + } catch (error) { + console.error('Error broadcasting presence:', error) + } + } + } + + // Throttle presence updates to avoid overwhelming the network + const throttledUpdate = throttle(handleStoreChange, 100) + + const unsubscribe = store.listen(throttledUpdate, { scope: 'all' }) + + return () => { + unsubscribe() + } + }, [handle, store, userMetadata, adapter]) return { updatePresence: () => {}, - presence: {}, + presence: presenceRef.current, } } diff --git a/src/automerge/useAutomergeSyncRepo.ts b/src/automerge/useAutomergeSyncRepo.ts index fed926b..06a146d 100644 --- a/src/automerge/useAutomergeSyncRepo.ts +++ b/src/automerge/useAutomergeSyncRepo.ts @@ -1,5 +1,5 @@ import { useMemo, useEffect, useState, useCallback, useRef } from "react" -import { TLStoreSnapshot } from "@tldraw/tldraw" +import { TLStoreSnapshot, InstancePresenceRecordType } from "@tldraw/tldraw" import { CloudflareNetworkAdapter } from "./CloudflareAdapter" import { useAutomergeStoreV2, useAutomergePresence } from "./useAutomergeStoreV2" import { TLStoreWithStatus } from "@tldraw/tldraw" @@ -35,6 +35,7 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus const [isLoading, setIsLoading] = useState(true) const handleRef = useRef(null) const storeRef = useRef(null) + const adapterRef = useRef(null) const lastSentHashRef = useRef(null) const isMouseActiveRef = useRef(false) const pendingSaveRef = useRef(false) @@ -91,17 +92,120 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus handleRef.current = handle }, [handle]) - // JSON sync is deprecated - all data now flows through Automerge sync protocol - // Old format content is converted server-side and saved to R2 in Automerge format - // This callback is kept for backwards compatibility but should not be used - const applyJsonSyncData = useCallback((_data: TLStoreSnapshot) => { - console.warn('⚠️ JSON sync callback called but JSON sync is deprecated. All data should flow through Automerge sync protocol.') - // Don't apply JSON sync - let Automerge sync handle everything - return + // JSON sync callback - receives changed records from other clients + // Apply to Automerge document which will emit patches to update the store + const applyJsonSyncData = useCallback((data: TLStoreSnapshot) => { + const currentHandle = handleRef.current + if (!currentHandle || !data?.store) { + console.warn('⚠️ Cannot apply JSON sync - no handle or data') + return + } + + const changedRecordCount = Object.keys(data.store).length + console.log(`📥 Applying ${changedRecordCount} changed records from JSON sync to Automerge document`) + + // Log shape dimension changes for debugging + Object.entries(data.store).forEach(([id, record]: [string, any]) => { + if (record?.typeName === 'shape' && (record.props?.w || record.props?.h)) { + console.log(`📥 Receiving shape update for ${record.type} ${id}:`, { + w: record.props.w, + h: record.props.h, + x: record.x, + y: record.y + }) + } + }) + + // Apply changes to the Automerge document + // This will trigger patches which will update the TLDraw store + currentHandle.change((doc: any) => { + if (!doc.store) { + doc.store = {} + } + // Merge the changed records into the Automerge document + Object.entries(data.store).forEach(([id, record]) => { + doc.store[id] = record + }) + }) + + console.log(`✅ Applied ${changedRecordCount} records to Automerge document - patches will update store`) + }, []) + + // Presence update callback - applies presence from other clients + // Presence is ephemeral (cursors, selections) and goes directly to the store + // Note: This callback is passed to the adapter but accesses storeRef which updates later + const applyPresenceUpdate = useCallback((userId: string, presenceData: any, senderId?: string, userName?: string, userColor?: string) => { + // CRITICAL: Don't apply our own presence back to ourselves (avoid echo) + // Use senderId (sessionId) instead of userId since multiple users can have the same userId + const currentAdapter = adapterRef.current + const ourSessionId = currentAdapter?.sessionId + + if (senderId && ourSessionId && senderId === ourSessionId) { + return + } + + // Access the CURRENT store ref (not captured in closure) + const currentStore = storeRef.current + + if (!currentStore) { + return + } + + try { + // CRITICAL: Transform remote user's instance/pointer/page_state into a proper instance_presence record + // TLDraw expects instance_presence records for remote users, not their local instance records + + // Extract data from the presence message + const pointerRecord = presenceData['pointer:pointer'] + const pageStateRecord = presenceData['instance_page_state:page:page'] + const instanceRecord = presenceData['instance:instance'] + + if (!pointerRecord) { + return + } + + // Create a proper instance_presence record for this remote user + // Use senderId to create a unique presence ID for each session + const presenceId = InstancePresenceRecordType.createId(senderId || userId) + + const instancePresence = InstancePresenceRecordType.create({ + id: presenceId, + currentPageId: pageStateRecord?.pageId || 'page:page', // Default to main page + userId: userId, + userName: userName || userId, // Use provided userName or fall back to userId + color: userColor || '#000000', // Use provided color or default to black + cursor: { + x: pointerRecord.x || 0, + y: pointerRecord.y || 0, + type: pointerRecord.type || 'default', + rotation: pointerRecord.rotation || 0 + }, + chatMessage: '', // Empty by default + lastActivityTimestamp: Date.now() + }) + + // Apply the instance_presence record using mergeRemoteChanges for atomic updates + currentStore.mergeRemoteChanges(() => { + currentStore.put([instancePresence]) + }) + + console.log(`✅ Applied instance_presence for remote user ${userId}`) + } catch (error) { + console.error('❌ Error applying presence:', error) + } }, []) const { repo, adapter } = useMemo(() => { - const adapter = new CloudflareNetworkAdapter(workerUrl, roomId, applyJsonSyncData) + const adapter = new CloudflareNetworkAdapter( + workerUrl, + roomId, + applyJsonSyncData, + applyPresenceUpdate + ) + + // Store adapter ref for use in callbacks + adapterRef.current = adapter + const repo = new Repo({ network: [adapter], // Enable sharing of all documents with all peers @@ -114,7 +218,7 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus }) return { repo, adapter } - }, [workerUrl, roomId, applyJsonSyncData]) + }, [workerUrl, roomId, applyJsonSyncData, applyPresenceUpdate]) // Initialize Automerge document handle useEffect(() => { @@ -184,65 +288,18 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus // Continue anyway - user can still create new content } - console.log("Found/Created Automerge handle via Repo:", { - handleId: handle.documentId, - isReady: handle.isReady(), - roomId: roomId - }) - - // Wait for the handle to be ready - await handle.whenReady() - - // Initialize document with default store if it's new/empty - const currentDoc = handle.doc() as any - if (!currentDoc || !currentDoc.store || Object.keys(currentDoc.store).length === 0) { - console.log("📝 Document is new/empty - initializing with default store") - - // Try to load initial data from server for new documents - try { - const response = await fetch(`${workerUrl}/room/${roomId}`) - if (response.ok) { - const serverDoc = await response.json() as TLStoreSnapshot - const serverRecordCount = Object.keys(serverDoc.store || {}).length - - if (serverDoc.store && serverRecordCount > 0) { - console.log(`📥 Loading ${serverRecordCount} records from server into new document`) - handle.change((doc: any) => { - // Initialize store if it doesn't exist - if (!doc.store) { - doc.store = {} - } - // Copy all records from server document - Object.entries(serverDoc.store).forEach(([id, record]) => { - doc.store[id] = record - }) - }) - console.log(`✅ Initialized Automerge document with ${serverRecordCount} records from server`) - } else { - console.log("📥 Server document is empty - document will start empty") - } - } else if (response.status === 404) { - console.log("📥 No document found on server (404) - starting with empty document") - } else { - console.warn(`⚠️ Failed to load document from server: ${response.status} ${response.statusText}`) - } - } catch (error) { - console.error("❌ Error loading initial document from server:", error) - // Continue anyway - document will start empty and sync via WebSocket - } - } else { - const existingRecordCount = Object.keys(currentDoc.store || {}).length - console.log(`✅ Document already has ${existingRecordCount} records - ready to sync`) - } - + // Verify final document state const finalDoc = handle.doc() as any const finalStoreKeys = finalDoc?.store ? Object.keys(finalDoc.store).length : 0 const finalShapeCount = finalDoc?.store ? Object.values(finalDoc.store).filter((r: any) => r?.typeName === 'shape').length : 0 - - console.log("Automerge handle initialized:", { + + console.log("✅ Automerge handle initialized and ready:", { + handleId: handle.documentId, + isReady: handle.isReady(), hasDoc: !!finalDoc, storeKeys: finalStoreKeys, - shapeCount: finalShapeCount + shapeCount: finalShapeCount, + roomId: roomId }) setHandle(handle) @@ -260,6 +317,10 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus return () => { mounted = false + // Disconnect adapter on unmount to clean up WebSocket connection + if (adapter) { + adapter.disconnect?.() + } } }, [repo, adapter, roomId, workerUrl]) @@ -576,26 +637,42 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus } }, [handle, roomId, workerUrl, generateDocHash]) + // Generate a unique color for each user based on their userId + const generateUserColor = (userId: string): string => { + // Use a simple hash of the userId to generate a consistent color + let hash = 0 + for (let i = 0; i < userId.length; i++) { + hash = userId.charCodeAt(i) + ((hash << 5) - hash) + } + + // Generate a vibrant color using HSL (hue varies, saturation and lightness fixed for visibility) + const hue = hash % 360 + return `hsl(${hue}, 70%, 50%)` + } + // Get user metadata for presence const userMetadata: { userId: string; name: string; color: string } = (() => { if (user && 'userId' in user) { + const uid = (user as { userId: string; name: string; color?: string }).userId return { - userId: (user as { userId: string; name: string; color?: string }).userId, + userId: uid, name: (user as { userId: string; name: string; color?: string }).name, - color: (user as { userId: string; name: string; color?: string }).color || '#000000' + color: (user as { userId: string; name: string; color?: string }).color || generateUserColor(uid) } } + const uid = user?.id || 'anonymous' return { - userId: user?.id || 'anonymous', + userId: uid, name: user?.name || 'Anonymous', - color: '#000000' + color: generateUserColor(uid) } })() // Use useAutomergeStoreV2 to create a proper TLStore instance that syncs with Automerge const storeWithStatus = useAutomergeStoreV2({ handle: handle || null as any, - userId: userMetadata.userId + userId: userMetadata.userId, + adapter: adapter // Pass adapter for JSON sync broadcasting }) // Update store ref when store is available @@ -609,7 +686,8 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus const presence = useAutomergePresence({ handle: handle || null, store: storeWithStatus.store || null, - userMetadata + userMetadata, + adapter: adapter // Pass adapter for presence broadcasting }) return { diff --git a/src/components/FathomMeetingsPanel.tsx b/src/components/FathomMeetingsPanel.tsx index 804a73a..982eae6 100644 --- a/src/components/FathomMeetingsPanel.tsx +++ b/src/components/FathomMeetingsPanel.tsx @@ -435,7 +435,8 @@ export function FathomMeetingsPanel({ onClose, onMeetingSelect, shapeMode = fals cursor: apiKey ? 'pointer' : 'not-allowed', position: 'relative', zIndex: 10002, - pointerEvents: 'auto' + pointerEvents: 'auto', + touchAction: 'manipulation' }} > Save & Load Meetings diff --git a/src/components/StandardizedToolWrapper.tsx b/src/components/StandardizedToolWrapper.tsx index 362be05..18fa631 100644 --- a/src/components/StandardizedToolWrapper.tsx +++ b/src/components/StandardizedToolWrapper.tsx @@ -169,6 +169,9 @@ export const StandardizedToolWrapper: React.FC = ( transition: 'background-color 0.15s ease, color 0.15s ease', pointerEvents: 'auto', flexShrink: 0, + touchAction: 'manipulation', // Prevent double-tap zoom, improve touch responsiveness + padding: '8px', // Increase touch target size without changing visual size + margin: '-8px', // Negative margin to maintain visual spacing } const minimizeButtonStyle: React.CSSProperties = { @@ -215,12 +218,13 @@ export const StandardizedToolWrapper: React.FC = ( minHeight: '32px', backgroundColor: '#f8f9fa', flexShrink: 0, + touchAction: 'manipulation', // Improve touch responsiveness } const tagStyle: React.CSSProperties = { backgroundColor: '#007acc', color: 'white', - padding: '2px 6px', + padding: '4px 8px', // Increased padding for better touch target borderRadius: '12px', fontSize: '10px', fontWeight: '500', @@ -228,6 +232,8 @@ export const StandardizedToolWrapper: React.FC = ( alignItems: 'center', gap: '4px', cursor: tagsEditable ? 'pointer' : 'default', + touchAction: 'manipulation', // Improve touch responsiveness + minHeight: '24px', // Ensure adequate touch target height } const tagInputStyle: React.CSSProperties = { @@ -245,13 +251,15 @@ export const StandardizedToolWrapper: React.FC = ( color: 'white', border: 'none', borderRadius: '12px', - padding: '2px 8px', + padding: '4px 10px', // Increased padding for better touch target fontSize: '10px', fontWeight: '500', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '4px', + touchAction: 'manipulation', // Improve touch responsiveness + minHeight: '24px', // Ensure adequate touch target height } const handleTagClick = (tag: string) => { @@ -318,6 +326,13 @@ export const StandardizedToolWrapper: React.FC = ( const handleButtonClick = (e: React.MouseEvent, action: () => void) => { e.stopPropagation() + e.preventDefault() + action() + } + + const handleButtonTouch = (e: React.TouchEvent, action: () => void) => { + e.stopPropagation() + e.preventDefault() action() } @@ -380,6 +395,8 @@ export const StandardizedToolWrapper: React.FC = ( onClick={(e) => handleButtonClick(e, onPinToggle)} onPointerDown={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} + onTouchStart={(e) => handleButtonTouch(e, onPinToggle)} + onTouchEnd={(e) => e.stopPropagation()} title={isPinnedToView ? "Unpin from view" : "Pin to view"} aria-label={isPinnedToView ? "Unpin from view" : "Pin to view"} > @@ -398,6 +415,12 @@ export const StandardizedToolWrapper: React.FC = ( }} onPointerDown={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} + onTouchStart={(e) => { + if (onMinimize) { + handleButtonTouch(e, onMinimize) + } + }} + onTouchEnd={(e) => e.stopPropagation()} title="Minimize" aria-label="Minimize" disabled={!onMinimize} @@ -409,6 +432,8 @@ export const StandardizedToolWrapper: React.FC = ( onClick={(e) => handleButtonClick(e, onClose)} onPointerDown={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} + onTouchStart={(e) => handleButtonTouch(e, onClose)} + onTouchEnd={(e) => e.stopPropagation()} title="Close" aria-label="Close" > @@ -429,9 +454,10 @@ export const StandardizedToolWrapper: React.FC = ( {/* Tags at the bottom */} {(tags.length > 0 || (tagsEditable && isSelected)) && ( -
e.stopPropagation()} + onTouchStart={(e) => e.stopPropagation()} onClick={(e) => { if (tagsEditable && !isEditingTags && e.target === e.currentTarget) { setIsEditingTags(true) @@ -446,6 +472,11 @@ export const StandardizedToolWrapper: React.FC = ( e.stopPropagation() handleTagClick(tag) }} + onTouchEnd={(e) => { + e.stopPropagation() + e.preventDefault() + handleTagClick(tag) + }} title={tagsEditable ? "Click to remove tag" : undefined} > {tag.replace('#', '')} @@ -480,6 +511,12 @@ export const StandardizedToolWrapper: React.FC = ( setIsEditingTags(true) }} onPointerDown={(e) => e.stopPropagation()} + onTouchStart={(e) => { + e.stopPropagation() + e.preventDefault() + setIsEditingTags(true) + }} + onTouchEnd={(e) => e.stopPropagation()} title="Add tag" > + Add diff --git a/src/css/crypto-auth.css b/src/css/crypto-auth.css index 06e9032..ad9b71b 100644 --- a/src/css/crypto-auth.css +++ b/src/css/crypto-auth.css @@ -284,77 +284,76 @@ } /* Dark mode support */ -@media (prefers-color-scheme: dark) { - .crypto-login-container { + +html.dark .crypto-login-container { background: #2d3748; border-color: #4a5568; } - .crypto-login-container h2 { +html.dark .crypto-login-container h2 { color: #f7fafc; } - .crypto-info { +html.dark .crypto-info { background: #4a5568; border-left-color: #63b3ed; } - .crypto-info p { +html.dark .crypto-info p { color: #e2e8f0; } - .form-group label { +html.dark .form-group label { color: #e2e8f0; } - .form-group input { +html.dark .form-group input { background: #4a5568; border-color: #718096; color: #f7fafc; } - .form-group input:focus { +html.dark .form-group input:focus { border-color: #63b3ed; box-shadow: 0 0 0 3px rgba(99, 179, 237, 0.1); } - .form-group input:disabled { +html.dark .form-group input:disabled { background-color: #2d3748; color: #a0aec0; } - .existing-users { +html.dark .existing-users { background: #4a5568; border-color: #718096; } - .existing-users h3 { +html.dark .existing-users h3 { color: #e2e8f0; } - .user-option { +html.dark .user-option { background: #2d3748; border-color: #718096; } - .user-option:hover:not(:disabled) { +html.dark .user-option:hover:not(:disabled) { border-color: #63b3ed; background: #2c5282; } - .user-option.selected { +html.dark .user-option.selected { border-color: #63b3ed; background: #2c5282; } - .user-name { +html.dark .user-name { color: #e2e8f0; } - .user-status { +html.dark .user-status { color: #a0aec0; } -} /* Test Component Styles */ .crypto-test-container { @@ -558,20 +557,19 @@ } /* Dark mode for login button */ -@media (prefers-color-scheme: dark) { - .login-button { + +html.dark .login-button { background: linear-gradient(135deg, #63b3ed 0%, #3182ce 100%); } - .login-button:hover { +html.dark .login-button:hover { background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%); } - .login-modal { +html.dark .login-modal { background: #2d3748; border: 1px solid #4a5568; } -} /* Debug Component Styles */ .crypto-debug-container { @@ -637,59 +635,58 @@ } /* Dark mode for test component */ -@media (prefers-color-scheme: dark) { - .crypto-test-container { + +html.dark .crypto-test-container { background: #2d3748; border-color: #4a5568; } - .crypto-test-container h2 { +html.dark .crypto-test-container h2 { color: #f7fafc; } - .test-results h3 { +html.dark .test-results h3 { color: #e2e8f0; } - .results-list { +html.dark .results-list { background: #4a5568; border-color: #718096; } - .result-item { +html.dark .result-item { color: #e2e8f0; border-bottom-color: #718096; } - .test-info { +html.dark .test-info { background: #2c5282; border-left-color: #63b3ed; } - .test-info h3 { +html.dark .test-info h3 { color: #90cdf4; } - .test-info ul { +html.dark .test-info ul { color: #e2e8f0; } - .crypto-debug-container { +html.dark .crypto-debug-container { background: #4a5568; border-color: #718096; } - .crypto-debug-container h2 { +html.dark .crypto-debug-container h2 { color: #e2e8f0; } - .debug-input { +html.dark .debug-input { background: #2d3748; border-color: #718096; color: #f7fafc; } - .debug-results h3 { +html.dark .debug-results h3 { color: #e2e8f0; - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/css/obsidian-browser.css b/src/css/obsidian-browser.css index f098f41..5d41b9c 100644 --- a/src/css/obsidian-browser.css +++ b/src/css/obsidian-browser.css @@ -1237,3 +1237,33 @@ mark { background-color: #fafafa; overscroll-behavior: contain; } + +/* Mobile Touch Interaction Improvements for Obsidian Browser */ +.obsidian-browser button, +.connect-vault-button, +.close-button, +.view-button, +.disconnect-vault-button, +.select-all-button, +.bulk-import-button, +.tag-button, +.retry-button, +.load-vault-button, +.method-button, +.submit-button, +.back-button, +.folder-picker-button, +.clear-search-button { + touch-action: manipulation; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1); +} + +/* Ensure adequate touch target sizes on mobile */ +@media (max-width: 768px) { + .obsidian-browser button, + .view-button, + .tag-button { + min-height: 44px; + padding: 10px 16px; + } +} diff --git a/src/css/obsidian-toolbar.css b/src/css/obsidian-toolbar.css index 3d47395..86f6f01 100644 --- a/src/css/obsidian-toolbar.css +++ b/src/css/obsidian-toolbar.css @@ -47,22 +47,21 @@ } /* Dark mode support */ -@media (prefers-color-scheme: dark) { - .obsidian-toolbar-button { + +html.dark .obsidian-toolbar-button { background: #2d2d2d; border-color: #404040; color: #e0e0e0; } - .obsidian-toolbar-button:hover { +html.dark .obsidian-toolbar-button:hover { background: #3d3d3d; border-color: #007acc; color: #007acc; } - .obsidian-toolbar-button:active { +html.dark .obsidian-toolbar-button:active { background: #1a3a5c; border-color: #005a9e; color: #005a9e; } -} diff --git a/src/css/starred-boards.css b/src/css/starred-boards.css index 8e0616e..aa25671 100644 --- a/src/css/starred-boards.css +++ b/src/css/starred-boards.css @@ -484,15 +484,15 @@ } /* Dark mode support */ -@media (prefers-color-scheme: dark) { - .dashboard-container { + +html.dark .dashboard-container { background: #1a1a1a; } .dashboard-header, .starred-boards-section, .quick-actions-section, - .auth-required { +html.dark .auth-required { background: #2d2d2d; color: #e9ecef; } @@ -501,53 +501,53 @@ .section-header h2, .quick-actions-section h2, .board-title, - .action-card h3 { +html.dark .action-card h3 { color: #e9ecef; } .dashboard-header p, .empty-state, .board-meta, - .action-card p { +html.dark .action-card p { color: #adb5bd; } .board-card, - .action-card { +html.dark .action-card { background: #3a3a3a; border-color: #495057; } .board-card:hover, - .action-card:hover { +html.dark .action-card:hover { border-color: #6c757d; } - .board-slug { +html.dark .board-slug { background: #495057; color: #adb5bd; } - .star-board-button { +html.dark .star-board-button { background: linear-gradient(135deg, #63b3ed 0%, #3182ce 100%); color: white; border: none; } - .star-board-button:hover { +html.dark .star-board-button:hover { background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%); color: white; transform: translateY(-1px); box-shadow: 0 2px 8px rgba(99, 179, 237, 0.3); } - .star-board-button.starred { +html.dark .star-board-button.starred { background: #6B7280; color: white; border: none; } - .star-board-button.starred:hover { +html.dark .star-board-button.starred:hover { background: #4B5563; color: white; transform: translateY(-1px); @@ -555,33 +555,32 @@ } /* Dark mode popup styles */ - .star-popup-success { +html.dark .star-popup-success { background: #1e4d2b; color: #d4edda; border: 1px solid #2d5a3d; } - .star-popup-error { +html.dark .star-popup-error { background: #4a1e1e; color: #f8d7da; border: 1px solid #5a2d2d; } - .star-popup-info { +html.dark .star-popup-info { background: #1e4a4a; color: #d1ecf1; border: 1px solid #2d5a5a; } - .board-screenshot { +html.dark .board-screenshot { background: #495057; border-bottom-color: #6c757d; } - .screenshot-image { +html.dark .screenshot-image { background: #495057; } -} /* Responsive design */ @media (max-width: 768px) { diff --git a/src/css/style.css b/src/css/style.css index c58949f..20113b9 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -2,6 +2,27 @@ :root { --border-radius: 10px; + --bg-color: #ffffff; + --text-color: #24292e; + --border-color: #e1e4e8; + --code-bg: #e4e9ee; + --code-color: #38424c; + --hover-bg: #f6f8fa; + --tool-bg: #f5f5f5; + --tool-text: #333333; + --tool-border: #d0d0d0; +} + +html.dark { + --bg-color: #1a1a1a; + --text-color: #e4e4e4; + --border-color: #404040; + --code-bg: #2d2d2d; + --code-color: #e4e4e4; + --hover-bg: #2d2d2d; + --tool-bg: #3a3a3a; + --tool-text: #e0e0e0; + --tool-border: #555555; } html, @@ -11,6 +32,9 @@ body { min-height: 100vh; min-height: -webkit-fill-available; height: 100%; + background-color: var(--bg-color); + color: var(--text-color); + transition: background-color 0.3s ease, color 0.3s ease; } video { @@ -28,7 +52,7 @@ main { font-family: "Recursive"; font-variation-settings: "MONO" 1; font-variation-settings: "CASL" 1; - color: #24292e; + color: var(--text-color); } h1 { @@ -92,9 +116,9 @@ pre>code { } code { - background-color: #e4e9ee; + background-color: var(--code-bg); width: 100%; - color: #38424c; + color: var(--code-color); padding: 0.2em 0.4em; border-radius: 4px; } @@ -809,4 +833,76 @@ p:has(+ ol) { padding-right: 0.1em; padding-left: 0.1em; color: #fc8958; +} + +/* Mobile Touch Interaction Improvements */ +button, +input[type="button"], +input[type="submit"], +[role="button"], +.clickable { + touch-action: manipulation; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1); +} + +/* Ensure adequate touch target sizes on mobile */ +@media (max-width: 768px) { + button, + input[type="button"], + input[type="submit"], + [role="button"] { + min-height: 44px; + min-width: 44px; + } +} + +/* ======================================== + Tool/Shape Consistent Grey Backgrounds + ======================================== */ + +/* Apply consistent grey background to all custom shapes/tools */ +.chat-container, +.embed-container, +.markdown-container, +.prompt-container, +.obs-note-container, +.transcription-container, +.holon-container, +.video-chat-container, +.slide-container, +.fathom-meetings-browser-container, +.obsidian-browser-container, +.holon-browser-container, +.multmux-container { + background-color: var(--tool-bg) !important; + color: var(--tool-text) !important; + border: 1px solid var(--tool-border) !important; +} + +/* Input fields within tools */ +.chat-container input, +.chat-container textarea, +.prompt-container input, +.prompt-container textarea, +.markdown-container input, +.markdown-container textarea, +.embed-container input { + background-color: var(--bg-color) !important; + color: var(--text-color) !important; + border: 1px solid var(--tool-border) !important; +} + +/* Buttons within tools */ +.chat-container button, +.prompt-container button, +.embed-container button { + background-color: var(--code-bg) !important; + color: var(--code-color) !important; + border: 1px solid var(--tool-border) !important; +} + +.chat-container button:hover, +.prompt-container button:hover, +.embed-container button:hover { + background-color: var(--hover-bg) !important; } \ No newline at end of file diff --git a/src/css/user-profile.css b/src/css/user-profile.css index 8869ac4..7e4155b 100644 --- a/src/css/user-profile.css +++ b/src/css/user-profile.css @@ -37,13 +37,12 @@ } /* Dark mode support */ -@media (prefers-color-scheme: dark) { - .custom-user-profile { + +html.dark .custom-user-profile { background: rgba(45, 45, 45, 0.9); border-color: rgba(255, 255, 255, 0.1); color: #e9ecef; } -} /* Animations */ @keyframes profileSlideIn { diff --git a/src/public/_redirects b/src/public/_redirects index 7ca73b2..5584709 100644 --- a/src/public/_redirects +++ b/src/public/_redirects @@ -1,14 +1,25 @@ # Cloudflare Pages redirects and rewrites # This file handles SPA routing and URL rewrites (replaces vercel.json rewrites) -# SPA fallback - all routes should serve index.html +# Specific route rewrites (matching vercel.json) +# Handle both with and without trailing slashes +/board/* /index.html 200 +/board /index.html 200 +/board/ /index.html 200 +/inbox /index.html 200 +/inbox/ /index.html 200 +/contact /index.html 200 +/contact/ /index.html 200 +/presentations /index.html 200 +/presentations/ /index.html 200 +/presentations/* /index.html 200 +/dashboard /index.html 200 +/dashboard/ /index.html 200 +/login /index.html 200 +/login/ /index.html 200 +/debug /index.html 200 +/debug/ /index.html 200 + +# SPA fallback - all routes should serve index.html (must be last) /* /index.html 200 -# Specific route rewrites (matching vercel.json) -/board/* /index.html 200 -/board /index.html 200 -/inbox /index.html 200 -/contact /index.html 200 -/presentations /index.html 200 -/dashboard /index.html 200 - diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx index 3a297cf..05c6e59 100644 --- a/src/routes/Board.tsx +++ b/src/routes/Board.tsx @@ -1,7 +1,7 @@ import { useAutomergeSync } from "@/automerge/useAutomergeSync" import { AutomergeHandleProvider } from "@/context/AutomergeHandleContext" import { useMemo, useEffect, useState, useRef } from "react" -import { Tldraw, Editor, TLShapeId, TLRecord } from "tldraw" +import { Tldraw, Editor, TLShapeId, TLRecord, useTldrawUser, TLUserPreferences } from "tldraw" import { useParams } from "react-router-dom" import { ChatBoxTool } from "@/tools/ChatBoxTool" import { ChatBoxShape } from "@/shapes/ChatBoxShapeUtil" @@ -41,6 +41,8 @@ import { FathomMeetingsTool } from "@/tools/FathomMeetingsTool" import { HolonBrowserShape } from "@/shapes/HolonBrowserShapeUtil" import { ObsidianBrowserShape } from "@/shapes/ObsidianBrowserShapeUtil" import { FathomMeetingsBrowserShape } from "@/shapes/FathomMeetingsBrowserShapeUtil" +import { MultmuxTool } from "@/tools/MultmuxTool" +import { MultmuxShape } from "@/shapes/MultmuxShapeUtil" // Location shape removed - no longer needed import { lockElement, @@ -81,6 +83,7 @@ const customShapeUtils = [ HolonBrowserShape, ObsidianBrowserShape, FathomMeetingsBrowserShape, + MultmuxShape, ] const customTools = [ ChatBoxTool, @@ -95,6 +98,7 @@ const customTools = [ TranscriptionTool, HolonTool, FathomMeetingsTool, + MultmuxTool, ] export function Board() { @@ -180,18 +184,95 @@ export function Board() { }); }, [roomId]) + // Generate a stable user ID that persists across sessions + const uniqueUserId = useMemo(() => { + if (!session.username) return undefined + + // Use localStorage to persist user ID across sessions + const storageKey = `tldraw-user-id-${session.username}` + let userId = localStorage.getItem(storageKey) + + if (!userId) { + // Create a new user ID if one doesn't exist + userId = `${session.username}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + localStorage.setItem(storageKey, userId) + } + + return userId + }, [session.username]) + + // Generate a unique color for each user based on their userId + const generateUserColor = (userId: string): string => { + let hash = 0 + for (let i = 0; i < userId.length; i++) { + hash = userId.charCodeAt(i) + ((hash << 5) - hash) + } + const hue = hash % 360 + return `hsl(${hue}, 70%, 50%)` + } + + // Get current dark mode state from DOM + const getColorScheme = (): 'light' | 'dark' => { + return document.documentElement.classList.contains('dark') ? 'dark' : 'light' + } + + // Set up user preferences for TLDraw collaboration + const [userPreferences, setUserPreferences] = useState(() => ({ + id: uniqueUserId || 'anonymous', + name: session.username || 'Anonymous', + color: uniqueUserId ? generateUserColor(uniqueUserId) : '#000000', + colorScheme: getColorScheme(), + })) + + // Update user preferences when session changes + useEffect(() => { + if (uniqueUserId) { + setUserPreferences({ + id: uniqueUserId, + name: session.username || 'Anonymous', + color: generateUserColor(uniqueUserId), + colorScheme: getColorScheme(), + }) + } + }, [uniqueUserId, session.username]) + + // Listen for dark mode changes and update tldraw color scheme + useEffect(() => { + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === 'class') { + const newColorScheme = getColorScheme() + setUserPreferences(prev => ({ + ...prev, + colorScheme: newColorScheme, + })) + } + }) + }) + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'] + }) + + return () => observer.disconnect() + }, []) + + // Create the user object for TLDraw + const user = useTldrawUser({ userPreferences, setUserPreferences }) + const storeConfig = useMemo( () => ({ uri: `${WORKER_URL}/connect/${roomId}`, assets: multiplayerAssetStore, shapeUtils: [...defaultShapeUtils, ...customShapeUtils], bindingUtils: [...defaultBindingUtils], - user: session.authed ? { - id: session.username, - name: session.username, + user: session.authed && uniqueUserId ? { + id: uniqueUserId, + name: session.username, // Display name (can be duplicate) } : undefined, }), - [roomId, session.authed, session.username], + [roomId, session.authed, session.username, uniqueUserId], ) // Use Automerge sync for all environments @@ -414,22 +495,52 @@ export function Board() { } // Also check for shapes on other pages - const shapesOnOtherPages = storeShapes.filter((s: any) => s.parentId && s.parentId !== currentPageId) + // CRITICAL: Only count shapes that are DIRECT children of other pages, not frame/group children + const shapesOnOtherPages = storeShapes.filter((s: any) => + s.parentId && + s.parentId.startsWith('page:') && // Only page children + s.parentId !== currentPageId + ) if (shapesOnOtherPages.length > 0) { console.log(`📊 Board: ${shapesOnOtherPages.length} shapes exist on other pages (not current page ${currentPageId})`) // Find which page has the most shapes + // CRITICAL: Only count shapes that are DIRECT children of pages, not frame/group children const pageShapeCounts = new Map() storeShapes.forEach((s: any) => { - if (s.parentId) { + if (s.parentId && s.parentId.startsWith('page:')) { pageShapeCounts.set(s.parentId, (pageShapeCounts.get(s.parentId) || 0) + 1) } }) // Also check for shapes with no parentId or invalid parentId - const shapesWithInvalidParent = storeShapes.filter((s: any) => !s.parentId || (s.parentId && !allPages.find((p: any) => p.id === s.parentId))) + // CRITICAL: Frame and group children have parentId like "frame:..." or "group:...", not page IDs + // Only consider a parentId invalid if: + // 1. It's missing/null/undefined + // 2. It references a page that doesn't exist (starts with "page:" but page not found) + // 3. It references a shape that doesn't exist (starts with "shape:" but shape not found) + // DO NOT consider frame/group parentIds as invalid! + const shapesWithInvalidParent = storeShapes.filter((s: any) => { + if (!s.parentId) return true // Missing parentId + + // Check if it's a page reference + if (s.parentId.startsWith('page:')) { + // Only invalid if the page doesn't exist + return !allPages.find((p: any) => p.id === s.parentId) + } + + // Check if it's a shape reference (frame, group, etc.) + if (s.parentId.startsWith('shape:')) { + // Check if the parent shape exists in the store + const parentShape = storeShapes.find((shape: any) => shape.id === s.parentId) + return !parentShape // Invalid if parent shape doesn't exist + } + + // Any other format is invalid + return true + }) if (shapesWithInvalidParent.length > 0) { - console.warn(`📊 Board: ${shapesWithInvalidParent.length} shapes have invalid or missing parentId. Fixing...`) + console.warn(`📊 Board: ${shapesWithInvalidParent.length} shapes have truly invalid or missing parentId. Fixing...`) // Fix shapes with invalid parentId by assigning them to current page // CRITICAL: Preserve x and y coordinates when fixing parentId // This prevents coordinates from being reset when patches come back from Automerge @@ -771,6 +882,7 @@ export function Board() {
- Open Board diff --git a/src/routes/Default.tsx b/src/routes/Default.tsx index 5b0b969..96113c3 100644 --- a/src/routes/Default.tsx +++ b/src/routes/Default.tsx @@ -3,8 +3,8 @@ export function Default() {
Jeff Emmett

Hello! 👋🍄

@@ -44,7 +44,7 @@ export function Default() {

Talks

You can find my presentations and slides on the{" "} - presentations page. + presentations page.

  1. diff --git a/src/routes/Presentations.tsx b/src/routes/Presentations.tsx index b94090d..f160fab 100644 --- a/src/routes/Presentations.tsx +++ b/src/routes/Presentations.tsx @@ -12,8 +12,8 @@ export function Presentations() { support collective action and community self-organization.

    - For more of my work, check out my main page or - get in touch. + For more of my work, check out my main page or + get in touch.

diff --git a/src/routes/Resilience.tsx b/src/routes/Resilience.tsx index a8b2ca1..9804349 100644 --- a/src/routes/Resilience.tsx +++ b/src/routes/Resilience.tsx @@ -125,7 +125,7 @@ export function Resilience() { Topic: Building Community Resilience in an Age of Crisis

- ← Back to all presentations + ← Back to all presentations

diff --git a/src/shapes/ChatBoxShapeUtil.tsx b/src/shapes/ChatBoxShapeUtil.tsx index ae87638..6653ef8 100644 --- a/src/shapes/ChatBoxShapeUtil.tsx +++ b/src/shapes/ChatBoxShapeUtil.tsx @@ -132,10 +132,11 @@ export const ChatBox: React.FC = ({ setUsername(newUsername) localStorage.setItem("chatUsername", newUsername) } - fetchMessages(roomId) - const interval = setInterval(() => fetchMessages(roomId), 2000) + // DISABLED: Chat polling disabled until Telegram channels integration via Holons + // fetchMessages(roomId) + // const interval = setInterval(() => fetchMessages(roomId), 2000) - return () => clearInterval(interval) + // return () => clearInterval(interval) }, [roomId]) useEffect(() => { diff --git a/src/shapes/EmbedShapeUtil.tsx b/src/shapes/EmbedShapeUtil.tsx index a368c1f..3fdbd5b 100644 --- a/src/shapes/EmbedShapeUtil.tsx +++ b/src/shapes/EmbedShapeUtil.tsx @@ -1,9 +1,7 @@ -import { BaseBoxShapeUtil, TLBaseShape } from "tldraw" +import { BaseBoxShapeUtil, TLBaseShape, HTMLContainer } from "tldraw" import { useCallback, useState } from "react" -//import Embed from "react-embed" - - -//TODO: FIX PEN AND MOBILE INTERACTION WITH EDITING EMBED URL - DEFAULT TO TEXT SELECTED +import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper" +import { usePinnedToView } from "../hooks/usePinnedToView" export type IEmbedShape = TLBaseShape< "Embed", @@ -11,11 +9,11 @@ export type IEmbedShape = TLBaseShape< w: number h: number url: string | null - isMinimized?: boolean + pinnedToView: boolean + tags: string[] interactionState?: { scrollPosition?: { x: number; y: number } - currentTime?: number // for videos - // other state you want to sync + currentTime?: number } } > @@ -31,12 +29,10 @@ const transformUrl = (url: string): string => { // Google Maps if (url.includes("google.com/maps") || url.includes("goo.gl/maps")) { - // If it's already an embed URL, return as is if (url.includes("google.com/maps/embed")) { return url } - // Handle directions const directionsMatch = url.match(/dir\/([^\/]+)\/([^\/]+)/) if (directionsMatch || url.includes("/dir/")) { const origin = url.match(/origin=([^&]+)/)?.[1] || directionsMatch?.[1] @@ -52,13 +48,11 @@ const transformUrl = (url: string): string => { } } - // Extract place ID const placeMatch = url.match(/[?&]place_id=([^&]+)/) if (placeMatch) { return `https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2!2d0!3d0!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s${placeMatch[1]}!2s!5e0!3m2!1sen!2s!4v1` } - // For all other map URLs return `https://www.google.com/maps/embed/v1/place?key=${ import.meta.env.VITE_GOOGLE_MAPS_API_KEY }&q=${encodeURIComponent(url)}` @@ -71,15 +65,13 @@ const transformUrl = (url: string): string => { if (xMatch) { const [, username, tweetId] = xMatch if (tweetId) { - // For tweets return `https://platform.x.com/embed/Tweet.html?id=${tweetId}` } else { - // For profiles, return about:blank and handle display separately return "about:blank" } } - // Medium - return about:blank to prevent iframe loading + // Medium - return about:blank if (url.includes("medium.com")) { return "about:blank" } @@ -93,29 +85,24 @@ const transformUrl = (url: string): string => { } const getDefaultDimensions = (url: string): { w: number; h: number } => { - // YouTube default dimensions (16:9 ratio) if (url.match(/(?:youtube\.com|youtu\.be)/)) { return { w: 800, h: 450 } } - // Twitter/X default dimensions if (url.match(/(?:twitter\.com|x\.com)/)) { if (url.match(/\/status\/|\/tweets\//)) { - return { w: 800, h: 600 } // For individual tweets + return { w: 800, h: 600 } } } - // Google Maps default dimensions if (url.includes("google.com/maps") || url.includes("goo.gl/maps")) { return { w: 800, h: 600 } } - // Gather.town default dimensions if (url.includes("gather.town")) { return { w: 800, h: 600 } } - // Default dimensions for other embeds return { w: 800, h: 600 } } @@ -124,14 +111,13 @@ const getFaviconUrl = (url: string): string => { const urlObj = new URL(url) return `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=32` } catch { - return '' // Return empty if URL is invalid + return '' } } const getDisplayTitle = (url: string): string => { try { const urlObj = new URL(url) - // Handle special cases if (urlObj.hostname.includes('youtube.com')) { return 'YouTube' } @@ -141,48 +127,70 @@ const getDisplayTitle = (url: string): string => { if (urlObj.hostname.includes('google.com/maps')) { return 'Google Maps' } - // Default: return clean hostname return urlObj.hostname.replace('www.', '') } catch { - return url // Return original URL if parsing fails + return url } } export class EmbedShape extends BaseBoxShapeUtil { static override type = "Embed" + // Embed theme color: Yellow (Rainbow) + static readonly PRIMARY_COLOR = "#eab308" + getDefaultProps(): IEmbedShape["props"] { return { url: null, w: 800, h: 600, - isMinimized: false, + pinnedToView: false, + tags: ['embed'], } } indicator(shape: IEmbedShape) { return ( - ) } component(shape: IEmbedShape) { - // Ensure shape props exist with defaults const props = shape.props || {} const url = props.url || "" - const isMinimized = props.isMinimized || false - + const [isMinimized, setIsMinimized] = useState(false) const isSelected = this.editor.getSelectedShapeIds().includes(shape.id) - + const [inputUrl, setInputUrl] = useState(url) const [error, setError] = useState("") - const [copyStatus, setCopyStatus] = useState(false) + + // Use the pinning hook + usePinnedToView(this.editor, shape.id, shape.props.pinnedToView) + + const handleClose = () => { + this.editor.deleteShape(shape.id) + } + + const handleMinimize = () => { + setIsMinimized(!isMinimized) + } + + const handlePinToggle = () => { + this.editor.updateShape({ + id: shape.id, + type: shape.type, + props: { + ...shape.props, + pinnedToView: !shape.props.pinnedToView, + }, + }) + } const handleSubmit = useCallback( (e: React.FormEvent) => { @@ -192,7 +200,6 @@ export class EmbedShape extends BaseBoxShapeUtil { ? inputUrl : `https://${inputUrl}` - // Basic URL validation const isValidUrl = completedUrl.match(/(^\w+:|^)\/\//) if (!isValidUrl) { setError("Invalid URL") @@ -222,352 +229,268 @@ export class EmbedShape extends BaseBoxShapeUtil { }) } - const contentStyle = { - pointerEvents: isSelected ? "none" as const : "all" as const, - width: "100%", - height: "100%", - border: "1px solid #D3D3D3", - backgroundColor: "#FFFFFF", - display: "flex", - justifyContent: "center", - alignItems: "center", - overflow: "hidden", - } - - const wrapperStyle = { - position: 'relative' as const, - width: `${shape.props.w}px`, - height: `${shape.props.isMinimized ? 40 : shape.props.h}px`, - backgroundColor: "#F0F0F0", - borderRadius: "4px", - transition: "height 0.3s, width 0.3s", - overflow: "hidden", - } - - // Update control button styles - const controlButtonStyle = { - border: "none", - background: "#666666", // Grey background - color: "white", // White text - padding: "4px 12px", - margin: "0 4px", - borderRadius: "4px", - cursor: "pointer", - fontSize: "12px", - pointerEvents: "all" as const, - whiteSpace: "nowrap" as const, - transition: "background-color 0.2s", - "&:hover": { - background: "#4D4D4D", // Darker grey on hover - } - } - - const controlsContainerStyle = { - position: "absolute" as const, - top: "8px", - right: "8px", - display: "flex", - gap: "8px", - zIndex: 1, - } - - const handleToggleMinimize = (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - this.editor.updateShape({ - id: shape.id, - type: "Embed", - props: { - ...shape.props, - isMinimized: !shape.props.isMinimized, - }, - }) - } - - const controls = (url: string) => ( -
- - - + // Custom header content with URL info + const headerContent = url ? ( +
+ { + (e.target as HTMLImageElement).style.display = 'none' + }} + /> + + {getDisplayTitle(url)} +
+ ) : ( + Embed ) - // For minimized state, show URL and all controls - if (shape.props.url && shape.props.isMinimized) { + // For empty state - URL input form + if (!url) { return ( -
-
+ { + this.editor.updateShape({ + id: shape.id, + type: 'Embed', + props: { + ...shape.props, + tags: newTags, + } + }) }} + tagsEditable={true} > - { - // Hide broken favicon - (e.target as HTMLImageElement).style.display = 'none' - }} - />
- - {getDisplayTitle(shape.props.url)} - - - {shape.props.url} - -
- {controls(shape.props.url)} -
-
- ) - } - - // For empty state - if (!shape.props.url) { - return ( -
- {controls("")} -
{ - e.preventDefault() - e.stopPropagation() - const input = e.currentTarget.querySelector('input') - input?.focus() - }} - > -
{ + e.preventDefault() + e.stopPropagation() + const input = e.currentTarget.querySelector('input') + input?.focus() }} - onClick={(e) => e.stopPropagation()} > - setInputUrl(e.target.value)} - placeholder="Enter URL to embed" + { - if (e.key === "Enter") { - handleSubmit(e) - } - }} - onPointerDown={(e) => { - e.stopPropagation() - e.currentTarget.focus() - }} - /> - {error && ( -
{error}
- )} -
-
-
+ onClick={(e) => e.stopPropagation()} + > + setInputUrl(e.target.value)} + placeholder="Enter URL to embed..." + style={{ + width: "100%", + padding: "15px", + border: "1px solid #ccc", + borderRadius: "4px", + fontSize: "16px", + touchAction: 'manipulation', + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleSubmit(e) + } + }} + onPointerDown={(e) => { + e.stopPropagation() + e.currentTarget.focus() + }} + /> + {error && ( +
{error}
+ )} + +
+ + ) } // For medium.com and twitter profile views - if (shape.props.url?.includes("medium.com") || - (shape.props.url && shape.props.url.match(/(?:twitter\.com|x\.com)\/[^\/]+$/))) { + if (url.includes("medium.com") || + (url && url.match(/(?:twitter\.com|x\.com)\/[^\/]+$/))) { return ( -
- {controls(shape.props.url)} -
+ { + this.editor.updateShape({ + id: shape.id, + type: 'Embed', + props: { + ...shape.props, + tags: newTags, + } + }) }} + tagsEditable={true} > -

- Medium's content policy does not allow for embedding articles in - iframes. -

- - Open article in new tab → - -
-
+

+ This content cannot be embedded in an iframe. +

+ +
+ + ) } // For normal embed view return ( -
-
+ { + this.editor.updateShape({ + id: shape.id, + type: 'Embed', + props: { + ...shape.props, + tags: newTags, + } + }) }} + tagsEditable={true} > - {controls(shape.props.url)} -
- {!shape.props.isMinimized && ( - <> -
-