diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 1dae7758..24cfba3a 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -24,6 +24,7 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@tldraw/tldraw": "^4.5.10", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", @@ -2102,6 +2103,106 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -2125,6 +2226,116 @@ } } }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -2199,6 +2410,34 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", @@ -2364,31 +2603,18 @@ } } }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", - "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.4" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -2405,13 +2631,13 @@ } } }, - "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.4" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -2428,30 +2654,21 @@ } } }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -2468,13 +2685,13 @@ } } }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2486,22 +2703,13 @@ } } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", @@ -2518,14 +2726,13 @@ } } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", @@ -2542,37 +2749,30 @@ } } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", "license": "MIT", "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -2589,7 +2789,7 @@ } } }, - "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", @@ -2607,10 +2807,10 @@ } } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -2619,8 +2819,9 @@ "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { @@ -2638,33 +2839,26 @@ } } }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -2681,55 +2875,24 @@ } } }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", "license": "MIT", "dependencies": { + "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2746,20 +2909,20 @@ } } }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", @@ -2776,16 +2939,18 @@ } } }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", @@ -2793,7 +2958,8 @@ "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -2810,7 +2976,7 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", @@ -2828,81 +2994,117 @@ } } }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "license": "MIT", "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2913,64 +3115,108 @@ } } }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.1" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2987,109 +3233,691 @@ } } }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", - "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", - "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", - "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", - "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", - "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", - "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", "license": "MIT", - "optional": true, + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, "os": [ - "freebsd" + "linux" ] }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", - "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", @@ -3098,12 +3926,110 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", - "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", "cpu": [ - "arm" + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" ], "dev": true, "license": "MIT", @@ -3112,10 +4038,148 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", - "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", "cpu": [ "arm64" ], @@ -3123,13 +4187,16 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", - "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", "cpu": [ "arm64" ], @@ -3137,209 +4204,246 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", - "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", "cpu": [ - "loong64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", - "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", "cpu": [ - "loong64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "freebsd" + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", - "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", "cpu": [ - "ppc64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", - "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", - "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", - "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", - "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", "cpu": [ - "s390x" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", - "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], "cpu": [ - "x64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", - "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", - "cpu": [ - "x64" - ], + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", - "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", - "cpu": [ - "x64" - ], + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, - "os": [ - "openbsd" - ] + "dependencies": { + "tslib": "^2.4.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", - "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", - "cpu": [ - "arm64" - ], + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, - "os": [ - "openharmony" - ] + "dependencies": { + "tslib": "^2.4.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", - "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", - "cpu": [ - "arm64" - ], + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", - "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", - "cpu": [ - "ia32" - ], + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "tslib": "^2.4.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", - "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", - "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", "cpu": [ "x64" ], @@ -3348,418 +4452,664 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@tailwindcss/node": { + "node_modules/@tailwindcss/vite": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", - "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", - "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, "engines": { - "node": ">= 20" + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-x64": "4.2.2", - "@tailwindcss/oxide-freebsd-x64": "4.2.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-x64-musl": "4.2.2", - "@tailwindcss/oxide-wasm32-wasi": "4.2.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" } }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", - "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", - "cpu": [ - "arm64" - ], + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/runtime": "^7.12.5" + }, "engines": { - "node": ">= 20" + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tiptap/core": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.5.tgz", + "integrity": "sha512-L1lhWz6ujGny8LduTJ7MBWYhzigwOvfUJUrJ7IzOJSuy3+OAzisdGDD1GV7LEO/hU0Hr2Mkm1wajRIHExvS9HQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.22.5.tgz", + "integrity": "sha512-ajyP5W8fG5Hrru47T/eF3xMKOpNvWofgNJqBTeNuGl02sYxsy9a4EunyFxudsaZP9WW3VOD4SaIWr5+MqpbnOQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.22.5.tgz", + "integrity": "sha512-l/uDtpJISiFFyfctvnODNWBN/XPZI1jVZRacTRDDnSn8+x6KQ7G2qgFYueU7KvVJGDFVT39Iio56mcFRG/Pozg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.22.5.tgz", + "integrity": "sha512-yrNlFQQJY5MmhBpmD8tnmaSmyUQrEvgyPKa3bzVeWEhDSG1CW4A0ZSMx3hrA9yFO0HWfw3IJmvSCycEZQBalpQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.22.5.tgz", + "integrity": "sha512-cf54fG9AybU8NgPMv1TOcoqAkELeRc/VpnSCt/rIJZphWQx9nsFmrtkrlCatrIcCaGtNZYwlHlMnC5LVVMu0uA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.22.5" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.22.5.tgz", + "integrity": "sha512-mwDNOJC9rYbDu/JcqrN4dbUQRklJU8Fuk2raxD/IvFw9qUIcPCmxQ2XT9UTKmZz/Ju7Kdy72fss6XpgWv6gLAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.22.5.tgz", + "integrity": "sha512-d123kCfLdJTi4fue1m0+TNFztDkmIRSZGZmGu6H9KqwG5Q7IzjT9o8lzRsz+pXxYqHvqgYmXoEpM6srbzXx/Ag==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.22.5.tgz", + "integrity": "sha512-8NJERd+pCtvSuEP4C4WMGYmRRCV12ePZL7bC+QUdFlbdXg+kNZS0zZ7hh879tYA0Kidbi8rWWD1Tx+H2ezkmMw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.22.5.tgz", + "integrity": "sha512-Mp40DaFrY3sEUVtFqmxrR0BmU4G3k8GCYYNGqNa9OqWv7BrcFDC03V2n3okESDKt4MKkzhQQmypq+ouLy8dLfA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.22.5" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.22.5.tgz", + "integrity": "sha512-dhem4sTPhyQgQ+pFp2Oud4k4FSQz9PVMgeQAC9288SmGwxBkJNveDAw6sKTMrumqDvwkJrtslXIupq9TZYQnzg==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.22.5.tgz", + "integrity": "sha512-4WkMu7qqjbsm8hCQS+8X+la1wjriN0SKoRdvpfKH33qM50MB34tYJuGLAO+y7TTh4MMMco3AZCKPBL5JVMqNIg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.22.5" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.22.5.tgz", + "integrity": "sha512-n0R2mUVYZU2AVbJhg/WcY9+zx690wVwvsItHJf0DrYbf1tCYHx+PRHUt/AoXk6u8BSmnkb8/FDziS8m3mjfpSg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.22.5.tgz", + "integrity": "sha512-hjyEG4947PAhMBfP1G6B0QAh6+y9mp2C5BQmNjprA05/lQzDAT7KFZzNh8ZVp3ol6aICKq/N1gFOW9Dc/9FUOw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", - "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@tiptap/extension-highlight": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.22.5.tgz", + "integrity": "sha512-byWruAOKcqRN0OuzVSKqLLrced3M9AZaR2pD1BV3aUZHzMzeBjLBfByh8s4lExH2Z547xQUdHHnUflBQ572I5A==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", - "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.22.5.tgz", + "integrity": "sha512-vUV0/ugIbXOc8SJib0h8UMhgcqZXWu/dkEhlswZN4VVven1o5enkfxEiDw+OyIJHi5rUkrdhsQ/KTxG/Xb7X8A==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", - "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@tiptap/extension-italic": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.22.5.tgz", + "integrity": "sha512-4T8baSiLkeIymTgEwirxDFt5YgYofkP3m1+MGYdGy2HKcOK+1vpvlPhEO1X5qtZngtJW5S4+njKjinRg52A4PA==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", - "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/@tiptap/extension-link": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.22.5.tgz", + "integrity": "sha512-d671MvF3GPKoS2OVxjIlQ7hIE7MS3hREdR+d4cvnnoiLLD+ZJ6KgDnxmWqF0a1s4qxLWK2KxKRSOIfYGE31QWQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", - "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@tiptap/extension-list": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.22.5.tgz", + "integrity": "sha512-cVO3ZHCgxAWZ4zrFSs81FO2nyCk1wb2EHkpLpW98FzbJLkN9rDkazhW99P3HRWy/CvUldOT+8ecI1YrQtBojMg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", - "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@tiptap/extension-list-item": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.22.5.tgz", + "integrity": "sha512-W7uTmyKLhlsvuTPLv+8WwnsY+mlikBFIoLSvVcBaFt4MwpsZ+DeB6KQg02Y7tbtaAnG7rXu9Fvw2QORh2P728A==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", - "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.22.5.tgz", + "integrity": "sha512-cGUnxJ0y515e1bVHNjUmbx7oWHoEon59w6BA5N2KwV9iW2mZZchlTX4yxJSOX+ixeVRChsa7YwC3Z1jUZ6AMEg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", - "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.22.5.tgz", + "integrity": "sha512-OXdh4k4CNrukwiSdWdEQ49uvgnqvR0Z9aNSP4HI5/kZQ/Te1NtRtYCpUrzWyO/7CtjcCisXHti0o9C/TV8YMbQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", - "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.22.5.tgz", + "integrity": "sha512-52KCto4+XKpnBWpIufspWLyq4UWxAWC72ANPdGuIhbi72NRTabiTbTVN40uwGSPkyakeESG0/vKdWJCVvB4f0g==", "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, - "engines": { - "node": ">=14.0.0" + "peerDependencies": { + "@tiptap/core": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.8.1", - "dev": true, - "inBundle": true, + "node_modules/@tiptap/extension-strike": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.22.5.tgz", + "integrity": "sha512-42WrrFK5gOom/0znH85x12Mw5IQ/6O6DWdyUWoRIrNA/qJpuHtU8oVU+bIgU2tuomMGHruRjIzgBQv5sBjEtww==", "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.8.1", - "dev": true, - "inBundle": true, + "node_modules/@tiptap/extension-text": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.22.5.tgz", + "integrity": "sha512-bzpDOdAEo1JeoVZDIyV0oY0jGXkEG+AzF70SzHoRSjOvFDtKWunyXf9eO1OnOr2/fmMcckT2qwUBNBMQplWBzw==", "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "dev": true, - "inBundle": true, + "node_modules/@tiptap/extension-underline": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.5.tgz", + "integrity": "sha512-9ut09rJD0iEbS6sk7yd2j6IwuFDLTNmDEGTDLodvqAfi+bq7ddsTDv0YviXoZaA9sdHAdTEVr2ITy2m6WK5jpA==", "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "dev": true, - "inBundle": true, + "node_modules/@tiptap/extensions": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.5.tgz", + "integrity": "sha512-Ifg4MzKCj3uRqe3ieTwYnomu2y4p7EXr2avVSKZYfh12i2dyWe2Gkn1KuZDREANVE+gHqFlQjJRYzhJFwzSCrg==", "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, "funding": { "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "dev": true, - "inBundle": true, + "node_modules/@tiptap/pm": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.5.tgz", + "integrity": "sha512-Cr9Mv4igxvI2tKMiahw48sZxva3PfDzypErH8IB82N+9qa9n9ygVMt0BOaDg53hLKxEEVeYr2S/wCcJIVFgBTw==", "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "prosemirror-changeset": "^2.3.0", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", - "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@tiptap/react": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.22.5.tgz", + "integrity": "sha512-36WHEs+vPmB//V1ff7Ujcnpz7Ey5g8lhpI/0+hoanSbdiPMTQ7qZVWwMovIkMKDlqWVp2fxBgeYM1861jyFzTw==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.22.5", + "@tiptap/extension-floating-menu": "^3.22.5" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/react/node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 20" + "node": ">=6.0.0" } }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", - "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@tiptap/starter-kit": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.22.5.tgz", + "integrity": "sha512-LZ/LYbwH6rnDi5DnRyagkuNsYAVyhM+yJvvz+ZuYA0JkPiTXJV86J5PWSKew8M0gVfMHcNVtKjfQCvViFCeIgw==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.22.5", + "@tiptap/extension-blockquote": "^3.22.5", + "@tiptap/extension-bold": "^3.22.5", + "@tiptap/extension-bullet-list": "^3.22.5", + "@tiptap/extension-code": "^3.22.5", + "@tiptap/extension-code-block": "^3.22.5", + "@tiptap/extension-document": "^3.22.5", + "@tiptap/extension-dropcursor": "^3.22.5", + "@tiptap/extension-gapcursor": "^3.22.5", + "@tiptap/extension-hard-break": "^3.22.5", + "@tiptap/extension-heading": "^3.22.5", + "@tiptap/extension-horizontal-rule": "^3.22.5", + "@tiptap/extension-italic": "^3.22.5", + "@tiptap/extension-link": "^3.22.5", + "@tiptap/extension-list": "^3.22.5", + "@tiptap/extension-list-item": "^3.22.5", + "@tiptap/extension-list-keymap": "^3.22.5", + "@tiptap/extension-ordered-list": "^3.22.5", + "@tiptap/extension-paragraph": "^3.22.5", + "@tiptap/extension-strike": "^3.22.5", + "@tiptap/extension-text": "^3.22.5", + "@tiptap/extension-underline": "^3.22.5", + "@tiptap/extensions": "^3.22.5", + "@tiptap/pm": "^3.22.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tldraw/editor": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@tldraw/editor/-/editor-4.5.10.tgz", + "integrity": "sha512-kM11sDK1ADXATE/wthBDY3ZOriT5Nzpieq1EoPz/HwSMVFuLq5NVmaOPKF+Pt/n43T1S5eLOfMOc7i3zojNc3g==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@tiptap/core": "^3.12.1", + "@tiptap/pm": "^3.12.1", + "@tiptap/react": "^3.12.1", + "@tldraw/state": "4.5.10", + "@tldraw/state-react": "4.5.10", + "@tldraw/store": "4.5.10", + "@tldraw/tlschema": "4.5.10", + "@tldraw/utils": "4.5.10", + "@tldraw/validate": "4.5.10", + "@use-gesture/react": "^10.3.1", + "classnames": "^2.5.1", + "eventemitter3": "^4.0.7", + "idb": "^7.1.1", + "is-plain-object": "^5.0.0", + "rbush": "^3.0.1" + }, + "peerDependencies": { + "react": "^18.2.0 || ^19.2.1", + "react-dom": "^18.2.0 || ^19.2.1" + } + }, + "node_modules/@tldraw/state": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@tldraw/state/-/state-4.5.10.tgz", + "integrity": "sha512-c7l3/5T16M0p7EJ2GLjpCA2B69abn1790spjsHsfaIWlEzeo29LmTNa906dtkx8b6qPrSUguX6ibE5mEOw7IzA==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" + "dependencies": { + "@tldraw/utils": "4.5.10" } }, - "node_modules/@tailwindcss/vite": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", - "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", - "dev": true, + "node_modules/@tldraw/state-react": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@tldraw/state-react/-/state-react-4.5.10.tgz", + "integrity": "sha512-gT173dPZUmHksVGDFFNlinai/CfIgvY3ECXGGXMEwpISbHpxInn1u6U7E60z/hmG7MQ2e6kKQq1H7w+VkMeQmg==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.2.2", - "@tailwindcss/oxide": "4.2.2", - "tailwindcss": "4.2.2" + "@tldraw/state": "4.5.10", + "@tldraw/utils": "4.5.10" }, "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" + "react": "^18.2.0 || ^19.2.1", + "react-dom": "^18.2.0 || ^19.2.1" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, + "node_modules/@tldraw/store": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@tldraw/store/-/store-4.5.10.tgz", + "integrity": "sha512-qxOCIRslNBO/02wyN+spC0iwJmNrtEFsPd2j9DoDWJqF1dh1+tneNCbrjJLe/kQHEP4mYugI3jIGPbHrqIr07g==", "license": "MIT", - "peer": true, "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" + "@tldraw/state": "4.5.10", + "@tldraw/utils": "4.5.10" }, - "engines": { - "node": ">=18" + "peerDependencies": { + "react": "^18.2.0 || ^19.2.1" } }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", - "dev": true, - "license": "MIT", + "node_modules/@tldraw/tldraw": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@tldraw/tldraw/-/tldraw-4.5.10.tgz", + "integrity": "sha512-eRCTfb/OgxuYSXeVPyCKRvs3MDfqQBKwXJBrEU52obwxcwJxt5C1TrUMKjR0fC3VqAZaLoi9ZZj6CRFRRqXEDw==", + "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" + "tldraw": "4.5.10" }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" + "peerDependencies": { + "react": "^18.2.0 || ^19.2.1", + "react-dom": "^18.2.0 || ^19.2.1" } }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", - "dev": true, + "node_modules/@tldraw/tlschema": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@tldraw/tlschema/-/tlschema-4.5.10.tgz", + "integrity": "sha512-QXrCy0+jc5c1Ld5kvvRkUQscX7/bVPL86H+KWz4Of4ajRevketjBGEH6AgoDOGzDoYXwDOrkYjR1W70Xb8/tQA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" + "@tldraw/state": "4.5.10", + "@tldraw/store": "4.5.10", + "@tldraw/utils": "4.5.10", + "@tldraw/validate": "4.5.10" }, "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "react": "^18.2.0 || ^19.2.1", + "react-dom": "^18.2.0 || ^19.2.1" + } + }, + "node_modules/@tldraw/utils": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@tldraw/utils/-/utils-4.5.10.tgz", + "integrity": "sha512-aRxpxmx0lIJx/xuwMaBgNxgcYi+okOElWSdZnnV+ZdUrqAfavpe5e1U6qoOdIhn9/iVfOz+PfmEINUNObi/eLg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "jittered-fractional-indexing": "^1.0.0", + "lodash.isequal": "^4.5.0", + "lodash.isequalwith": "^4.4.0", + "lodash.throttle": "^4.1.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@tldraw/validate": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@tldraw/validate/-/validate-4.5.10.tgz", + "integrity": "sha512-yEdFMXBD1OoyQrwBz4kgsw6ZtuxA1VL9vDGlsEhmUZx9y8VWODpEDIFMiIZOIBrYB33RLD6Md0tei66AoWKklw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@tldraw/utils": "4.5.10" } }, "node_modules/@types/aria-query": { @@ -3915,7 +5265,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -3934,12 +5283,36 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -4459,6 +5832,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4832,6 +6211,12 @@ "@types/estree": "^1.0.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -4891,6 +6276,15 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fractional-indexing": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fractional-indexing/-/fractional-indexing-3.2.0.tgz", + "integrity": "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==", + "license": "CC0-1.0", + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5042,6 +6436,15 @@ "node": ">=12.0.0" } }, + "node_modules/hotkeys-js": { + "version": "3.13.15", + "resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.15.tgz", + "integrity": "sha512-gHh8a/cPTCpanraePpjRxyIlxDFrIhYqjuh01UHWEwDpglJKCnvLW8kqSx5gQtOuSsJogNZXLhOdbSExpgUiqg==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -5065,6 +6468,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -5137,6 +6546,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -5160,6 +6578,18 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jittered-fractional-indexing": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/jittered-fractional-indexing/-/jittered-fractional-indexing-1.0.1.tgz", + "integrity": "sha512-OpKFkVr4hU5ivd1ZCjZfHvVpWekraJvcePcMusBmgBmCVQK5JiRCA+4TT1vAUTLqGD9MkhqFwO0l3QspvlZgzw==", + "license": "CC0-1.0", + "dependencies": { + "fractional-indexing": "^3.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5520,6 +6950,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, "node_modules/loadjs": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loadjs/-/loadjs-4.3.0.tgz", @@ -5532,6 +6968,31 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isequalwith": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz", + "integrity": "sha512-dcZON0IalGBpRmJBmMkaoV7d3I80R2O+FrzsZyHdNSFrANq/cgDqKQNmAHE8UEj4+QYWwwhkQOVdLHiAopzlsQ==", + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -5599,9 +7060,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6978,12 +8437,145 @@ "node": ">=6" } }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/rangetouch": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.1.tgz", "integrity": "sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==", "license": "MIT" }, + "node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, "node_modules/re-resizable": { "version": "6.11.2", "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz", @@ -7667,6 +9259,32 @@ "node": ">=14.0.0" } }, + "node_modules/tldraw": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/tldraw/-/tldraw-4.5.10.tgz", + "integrity": "sha512-dVtS8MVuB3EJ5Gs75mBOxZ8XrKhRMizXrqP5MK3Xv05eJsqUe0R8GqEzowObqYFlu1kkUqIWNkVByUcvYFr+pg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@tiptap/core": "^3.12.1", + "@tiptap/extension-code": "^3.12.1", + "@tiptap/extension-highlight": "^3.12.1", + "@tiptap/extension-list": "^3.12.1", + "@tiptap/pm": "^3.12.1", + "@tiptap/react": "^3.12.1", + "@tiptap/starter-kit": "^3.12.1", + "@tldraw/editor": "4.5.10", + "@tldraw/store": "4.5.10", + "classnames": "^2.5.1", + "hotkeys-js": "^3.13.9", + "idb": "^7.1.1", + "lz-string": "^1.5.0", + "radix-ui": "^1.4.2" + }, + "peerDependencies": { + "react": "^18.2.0 || ^19.2.1", + "react-dom": "^18.2.0 || ^19.2.1" + } + }, "node_modules/tldts": { "version": "7.0.28", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz", @@ -7967,6 +9585,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/desktop/package.json b/desktop/package.json index 9c17d06a..da56a553 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@tldraw/tldraw": "^4.5.10", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", diff --git a/desktop/src/apps/ProjectsApp/ProjectMembers.tsx b/desktop/src/apps/ProjectsApp/ProjectMembers.tsx index 8a36af1a..8d7ee027 100644 --- a/desktop/src/apps/ProjectsApp/ProjectMembers.tsx +++ b/desktop/src/apps/ProjectsApp/ProjectMembers.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { projectsApi, type Project, type ProjectMember } from "@/lib/projects"; import { AddAgentDialog } from "./AddAgentDialog"; +import { canvasApi } from "./canvas/canvas-api"; export function ProjectMembers({ project, onChanged }: { project: Project; onChanged: () => void }) { const [members, setMembers] = useState([]); @@ -45,18 +46,37 @@ export function ProjectMembers({ project, onChanged }: { project: Project; onCha {m.member_kind === "clone" ? ` · ${m.memory_seed}` : ""} - +
+ {(m.member_kind === "native" || m.member_kind === "clone") && ( + + )} + +
))} diff --git a/desktop/src/apps/ProjectsApp/ProjectWorkspace.tsx b/desktop/src/apps/ProjectsApp/ProjectWorkspace.tsx index 01928fba..d981546c 100644 --- a/desktop/src/apps/ProjectsApp/ProjectWorkspace.tsx +++ b/desktop/src/apps/ProjectsApp/ProjectWorkspace.tsx @@ -7,9 +7,10 @@ import { ProjectBoard } from "./board/ProjectBoard"; import { TaskModal } from "./board/TaskModal"; import { FilesApp } from "@/apps/FilesApp"; import { MessagesApp } from "@/apps/MessagesApp"; +import { CanvasView } from "./canvas/CanvasView"; -type Tab = "board" | "tasks" | "files" | "messages" | "members" | "activity"; -const TABS: Tab[] = ["board", "tasks", "files", "messages", "members", "activity"]; +type Tab = "board" | "canvas" | "tasks" | "files" | "messages" | "members" | "activity"; +const TABS: Tab[] = ["board", "canvas", "tasks", "files", "messages", "members", "activity"]; function readTaskParam(): string | null { if (typeof window === "undefined") return null; @@ -94,6 +95,7 @@ export function ProjectWorkspace({ project, onChanged }: { project: Project; onC )} )} + {tab === "canvas" && } {tab === "tasks" && } {tab === "files" && ( { + global.fetch = vi.fn(); +}); + +describe("canvasApi", () => { + it("listElements GETs the right URL", async () => { + (fetch as any).mockResolvedValue({ + ok: true, + json: async () => ({ elements: [] }), + }); + const r = await canvasApi.listElements("prj-1"); + expect(fetch).toHaveBeenCalledWith("/api/projects/prj-1/canvas/elements"); + expect(r).toEqual([]); + }); + + it("addElement POSTs body", async () => { + (fetch as any).mockResolvedValue({ + ok: true, + json: async () => ({ element: { id: "cve-1", kind: "note" } }), + }); + const r = await canvasApi.addElement("prj-1", { + kind: "note", x: 1, y: 2, w: 3, h: 4, payload: { text: "x" }, + }); + expect(r.id).toBe("cve-1"); + const call = (fetch as any).mock.calls[0]; + expect(call[0]).toBe("/api/projects/prj-1/canvas/elements"); + expect(call[1].method).toBe("POST"); + }); + + it("deleteElement returns true on 204", async () => { + (fetch as any).mockResolvedValue({ ok: true, status: 204 }); + const r = await canvasApi.deleteElement("prj-1", "cve-1"); + expect(r).toBe(true); + }); +}); diff --git a/desktop/src/apps/ProjectsApp/__tests__/canvas-store.test.ts b/desktop/src/apps/ProjectsApp/__tests__/canvas-store.test.ts new file mode 100644 index 00000000..5e4d16f1 --- /dev/null +++ b/desktop/src/apps/ProjectsApp/__tests__/canvas-store.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { createCanvasStore } from "../canvas/canvas-store"; + +describe("canvas-store", () => { + it("seeds elements", () => { + const store = createCanvasStore(); + store.getState().seed([ + { id: "cve-1", kind: "note", x: 0, y: 0, w: 1, h: 1, + rotation: 0, z_index: 0, payload: {}, project_id: "p", + author_kind: "user", author_id: "u", + created_at: 0, updated_at: 0, deleted_at: null } as any, + ]); + expect(store.getState().elements["cve-1"].kind).toBe("note"); + }); + + it("upsert replaces by id", () => { + const store = createCanvasStore(); + store.getState().upsert({ id: "x", x: 1 } as any); + store.getState().upsert({ id: "x", x: 99 } as any); + expect(store.getState().elements["x"].x).toBe(99); + }); + + it("remove drops the element", () => { + const store = createCanvasStore(); + store.getState().upsert({ id: "x" } as any); + store.getState().remove("x"); + expect(store.getState().elements["x"]).toBeUndefined(); + }); +}); diff --git a/desktop/src/apps/ProjectsApp/canvas/CanvasBoard.tsx b/desktop/src/apps/ProjectsApp/canvas/CanvasBoard.tsx new file mode 100644 index 00000000..bbf9292b --- /dev/null +++ b/desktop/src/apps/ProjectsApp/canvas/CanvasBoard.tsx @@ -0,0 +1,163 @@ +import { useEffect, useMemo, useRef } from "react"; +import { Tldraw, Editor, createTLStore, defaultShapeUtils, TLShape } from "@tldraw/tldraw"; +import "@tldraw/tldraw/tldraw.css"; + +import { canvasApi, CanvasElement } from "./canvas-api"; +import { createCanvasStore } from "./canvas-store"; +import { subscribeCanvasStream } from "./canvas-sse"; +import { TaosNoteShapeUtil } from "./shapes/NoteShape"; +import { TaosLinkShapeUtil } from "./shapes/LinkShape"; +import { TaosImageShapeUtil } from "./shapes/ImageShape"; + +interface CanvasBoardProps { + projectId: string; + projectSlug: string; +} + +const CUSTOM_SHAPE_UTILS = [TaosNoteShapeUtil, TaosLinkShapeUtil, TaosImageShapeUtil]; + +export function CanvasBoard({ projectId, projectSlug }: CanvasBoardProps) { + const cacheRef = useRef(createCanvasStore()); + const editorRef = useRef(null); + + // In tldraw v4, shapeUtils are passed to , not createTLStore + const store = useMemo( + () => createTLStore({ defaultName: `canvas-${projectId}` }), + [projectId], + ); + + // Initial load + SSE subscription + useEffect(() => { + let cancelled = false; + (async () => { + const elements = await canvasApi.listElements(projectId); + if (cancelled) return; + cacheRef.current.getState().seed(elements); + hydrateEditor(editorRef.current, elements, projectSlug); + })(); + const unsub = subscribeCanvasStream(projectId, cacheRef.current); + return () => { + cancelled = true; + unsub(); + }; + }, [projectId, projectSlug]); + + // Re-hydrate on local cache changes (SSE-driven updates from agents) + useEffect(() => { + const unsub = cacheRef.current.subscribe((s) => { + hydrateEditor(editorRef.current, Object.values(s.elements), projectSlug); + }); + return unsub; + }, [projectSlug]); + + return ( +
+ { + editorRef.current = editor; + // Send local user edits to backend + editor.store.listen( + (entry) => { + if (entry.source !== "user") return; + const added = entry.changes.added as Record; + for (const shape of Object.values(added)) { + if (!shape || !shape.id.startsWith("shape:")) continue; + pushAdd(projectId, shape).catch(console.warn); + } + const updated = entry.changes.updated as Record; + for (const pair of Object.values(updated)) { + if (!pair) continue; + const next = pair[1]; + if (!next.id.startsWith("shape:")) continue; + pushUpdate(projectId, next).catch(console.warn); + } + const removed = entry.changes.removed as Record; + for (const shape of Object.values(removed)) { + if (!shape || !shape.id.startsWith("shape:")) continue; + const elementId = shape.id.replace(/^shape:/, ""); + canvasApi.deleteElement(projectId, elementId).catch(console.warn); + } + }, + { source: "user", scope: "document" }, + ); + }} + /> +
+ ); +} + +function hydrateEditor( + editor: Editor | null, + elements: CanvasElement[], + projectSlug: string, +) { + if (!editor) return; + editor.run(() => { + const wantedIds = new Set(elements.map((e) => `shape:${e.id}`)); + const existing = editor.getCurrentPageShapes(); + const toRemove = existing.filter((s) => !wantedIds.has(s.id) && s.id.toString().startsWith("shape:")); + if (toRemove.length) editor.deleteShapes(toRemove.map((s) => s.id) as any); + + for (const el of elements) { + const id = `shape:${el.id}` as TLShape["id"]; + const existingShape = editor.getShape(id); + const newShape = elementToShape(el, projectSlug); + if (existingShape) { + editor.updateShape(newShape); + } else { + editor.createShape(newShape); + } + } + }); +} + +function elementToShape(el: CanvasElement, projectSlug: string): any { + const baseProps = { + w: el.w, h: el.h, + taos_kind: el.kind, + taos_payload: el.payload, + taos_author_id: el.author_id, + taos_author_kind: el.author_kind, + }; + return { + id: `shape:${el.id}`, + type: shapeType(el.kind), + x: el.x, y: el.y, rotation: el.rotation, + props: el.kind === "image" + ? { ...baseProps, project_slug: projectSlug } + : baseProps, + }; +} + +function shapeType(kind: string): string { + if (kind === "note") return "taos-note"; + if (kind === "link") return "taos-link"; + if (kind === "image") return "taos-image"; + return "geo"; +} + +async function pushAdd(projectId: string, shape: TLShape) { + if (!shape.id.toString().startsWith("shape:")) return; + const props: any = shape.props; + await canvasApi.addElement(projectId, { + id: shape.id.toString().replace(/^shape:/, ""), + kind: (props.taos_kind ?? "user_shape") as any, + x: shape.x, y: shape.y, + w: props.w ?? 100, h: props.h ?? 100, + rotation: shape.rotation, + payload: props.taos_payload ?? { tldraw_shape: shape }, + }); +} + +async function pushUpdate(projectId: string, shape: TLShape) { + const elementId = shape.id.toString().replace(/^shape:/, ""); + const props: any = shape.props; + await canvasApi.updateElement(projectId, elementId, { + x: shape.x, y: shape.y, + w: props.w, h: props.h, + rotation: shape.rotation, + payload: props.taos_payload, + }); +} diff --git a/desktop/src/apps/ProjectsApp/canvas/CanvasView.tsx b/desktop/src/apps/ProjectsApp/canvas/CanvasView.tsx new file mode 100644 index 00000000..5af95e11 --- /dev/null +++ b/desktop/src/apps/ProjectsApp/canvas/CanvasView.tsx @@ -0,0 +1,11 @@ +import { CanvasBoard } from "./CanvasBoard"; + +export function CanvasView({ + projectId, projectSlug, +}: { projectId: string; projectSlug: string }) { + return ( +
+ +
+ ); +} diff --git a/desktop/src/apps/ProjectsApp/canvas/canvas-api.ts b/desktop/src/apps/ProjectsApp/canvas/canvas-api.ts new file mode 100644 index 00000000..79a2f6f4 --- /dev/null +++ b/desktop/src/apps/ProjectsApp/canvas/canvas-api.ts @@ -0,0 +1,100 @@ +export type CanvasElementKind = "note" | "link" | "image" | "user_shape"; + +export interface CanvasElement { + id: string; + project_id: string; + kind: CanvasElementKind; + author_kind: "user" | "agent"; + author_id: string; + x: number; + y: number; + w: number; + h: number; + rotation: number; + z_index: number; + payload: Record; + created_at: number; + updated_at: number; + deleted_at: number | null; +} + +export interface CanvasElementInput { + id?: string; + kind: CanvasElementKind; + x: number; + y: number; + w: number; + h: number; + rotation?: number; + z_index?: number; + payload: Record; +} + +async function jsonOrThrow(r: Response): Promise { + if (!r.ok) { + const body = await r.text(); + throw new Error(`canvas-api ${r.status}: ${body}`); + } + return r.json() as Promise; +} + +export const canvasApi = { + async listElements(projectId: string): Promise { + const r = await fetch(`/api/projects/${projectId}/canvas/elements`); + const body = await jsonOrThrow<{ elements: CanvasElement[] }>(r); + return body.elements; + }, + + async addElement( + projectId: string, input: CanvasElementInput, + ): Promise { + const r = await fetch(`/api/projects/${projectId}/canvas/elements`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(input), + }); + const body = await jsonOrThrow<{ element: CanvasElement }>(r); + return body.element; + }, + + async updateElement( + projectId: string, elementId: string, patch: Partial, + ): Promise { + const r = await fetch( + `/api/projects/${projectId}/canvas/elements/${elementId}`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(patch), + }, + ); + const body = await jsonOrThrow<{ element: CanvasElement }>(r); + return body.element; + }, + + async deleteElement(projectId: string, elementId: string): Promise { + const r = await fetch( + `/api/projects/${projectId}/canvas/elements/${elementId}`, + { method: "DELETE" }, + ); + return r.ok; + }, + + async setPermission( + projectId: string, agentId: string, canEdit: boolean, + ): Promise { + const r = await fetch( + `/api/projects/${projectId}/canvas/permissions/${agentId}`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ can_edit_canvas: canEdit }), + }, + ); + if (!r.ok) throw new Error(`setPermission failed: ${r.status}`); + }, + + snapshotPngUrl(projectId: string): string { + return `/api/projects/${projectId}/canvas/snapshot.png`; + }, +}; diff --git a/desktop/src/apps/ProjectsApp/canvas/canvas-sse.ts b/desktop/src/apps/ProjectsApp/canvas/canvas-sse.ts new file mode 100644 index 00000000..acd85851 --- /dev/null +++ b/desktop/src/apps/ProjectsApp/canvas/canvas-sse.ts @@ -0,0 +1,46 @@ +import type { CanvasState } from "./canvas-store"; +import type { StoreApi } from "zustand"; +import type { CanvasElement } from "./canvas-api"; + +export interface CanvasEvent { + type: + | "canvas.element_added" + | "canvas.element_updated" + | "canvas.element_deleted" + | "canvas.permission_changed"; + project_id: string; + payload: Record; + ts: number; +} + +export function subscribeCanvasStream( + projectId: string, + store: StoreApi, + onPermissionChanged?: (agentId: string, canEdit: boolean) => void, +): () => void { + const es = new EventSource(`/api/projects/${projectId}/canvas/stream`); + es.onmessage = (msg) => { + let evt: CanvasEvent; + try { + evt = JSON.parse(msg.data) as CanvasEvent; + } catch { + return; + } + if (evt.type === "canvas.element_added" || evt.type === "canvas.element_updated") { + const el = evt.payload.element as CanvasElement | undefined; + if (el) store.getState().upsert(el); + } else if (evt.type === "canvas.element_deleted") { + const id = evt.payload.element_id as string | undefined; + if (id) store.getState().remove(id); + } else if (evt.type === "canvas.permission_changed") { + const agentId = evt.payload.agent_id as string | undefined; + const canEdit = !!evt.payload.can_edit_canvas; + if (agentId && onPermissionChanged) onPermissionChanged(agentId, canEdit); + } + }; + es.onerror = () => { + // Browser auto-reconnects on transient errors; close on hard failure + // and let the caller open a fresh subscription on remount. + }; + return () => es.close(); +} diff --git a/desktop/src/apps/ProjectsApp/canvas/canvas-store.ts b/desktop/src/apps/ProjectsApp/canvas/canvas-store.ts new file mode 100644 index 00000000..7ae7d6ac --- /dev/null +++ b/desktop/src/apps/ProjectsApp/canvas/canvas-store.ts @@ -0,0 +1,28 @@ +import { create } from "zustand"; +import type { CanvasElement } from "./canvas-api"; + +export interface CanvasState { + elements: Record; + seed: (elements: CanvasElement[]) => void; + upsert: (element: CanvasElement) => void; + remove: (elementId: string) => void; + clear: () => void; +} + +export function createCanvasStore() { + return create((set) => ({ + elements: {}, + seed: (elements) => + set(() => ({ + elements: Object.fromEntries(elements.map((e) => [e.id, e])), + })), + upsert: (element) => + set((s) => ({ elements: { ...s.elements, [element.id]: element } })), + remove: (elementId) => + set((s) => { + const { [elementId]: _, ...rest } = s.elements; + return { elements: rest }; + }), + clear: () => set({ elements: {} }), + })); +} diff --git a/desktop/src/apps/ProjectsApp/canvas/shapes/ImageShape.tsx b/desktop/src/apps/ProjectsApp/canvas/shapes/ImageShape.tsx new file mode 100644 index 00000000..a0d2bdc5 --- /dev/null +++ b/desktop/src/apps/ProjectsApp/canvas/shapes/ImageShape.tsx @@ -0,0 +1,63 @@ +import { HTMLContainer, ShapeUtil, TLBaseShape, Rectangle2d, T } from "@tldraw/tldraw"; + +export type TaosImageShape = TLBaseShape< + "taos-image", + { + w: number; + h: number; + taos_kind: "image"; + taos_payload: { file_id: string; alt: string; mime: string }; + taos_author_id: string; + taos_author_kind: "user" | "agent"; + project_slug: string; + } +>; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class TaosImageShapeUtil extends ShapeUtil { + static override type = "taos-image" as const; + static override props = { + w: T.number, h: T.number, + taos_kind: T.literal("image"), + taos_payload: T.object({ file_id: T.string, alt: T.string, mime: T.string }), + taos_author_id: T.string, + taos_author_kind: T.literalEnum("user", "agent"), + project_slug: T.string, + }; + + override getDefaultProps(): TaosImageShape["props"] { + return { + w: 240, h: 240, taos_kind: "image", + taos_payload: { file_id: "", alt: "", mime: "image/png" }, + taos_author_id: "user", taos_author_kind: "user", + project_slug: "", + }; + } + override getGeometry(shape: TaosImageShape) { + return new Rectangle2d({ width: shape.props.w, height: shape.props.h, isFilled: true }); + } + override component(shape: TaosImageShape) { + const p = shape.props.taos_payload; + const slug = shape.props.project_slug; + const src = p.file_id ? `/api/projects/${slug}/files/canvas/${p.file_id}` : ""; + return ( + + {src ? ( + {p.alt} + ) : ( +
+ (no image) +
+ )} +
+ ); + } + override indicator(shape: TaosImageShape) { + return ; + } + override canResize() { return false; } +} diff --git a/desktop/src/apps/ProjectsApp/canvas/shapes/LinkShape.tsx b/desktop/src/apps/ProjectsApp/canvas/shapes/LinkShape.tsx new file mode 100644 index 00000000..a1d10fcc --- /dev/null +++ b/desktop/src/apps/ProjectsApp/canvas/shapes/LinkShape.tsx @@ -0,0 +1,87 @@ +import { HTMLContainer, ShapeUtil, TLBaseShape, Rectangle2d, T } from "@tldraw/tldraw"; + +export type TaosLinkShape = TLBaseShape< + "taos-link", + { + w: number; + h: number; + taos_kind: "link"; + taos_payload: { + url: string; title: string; description: string; + preview_image_url: string; favicon_url: string; fetched_at: number; + }; + taos_author_id: string; + taos_author_kind: "user" | "agent"; + } +>; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class TaosLinkShapeUtil extends ShapeUtil { + static override type = "taos-link" as const; + static override props = { + w: T.number, h: T.number, + taos_kind: T.literal("link"), + taos_payload: T.object({ + url: T.string, title: T.string, description: T.string, + preview_image_url: T.string, favicon_url: T.string, fetched_at: T.number, + }), + taos_author_id: T.string, + taos_author_kind: T.literalEnum("user", "agent"), + }; + + override getDefaultProps(): TaosLinkShape["props"] { + return { + w: 320, h: 120, + taos_kind: "link", + taos_payload: { + url: "", title: "", description: "", + preview_image_url: "", favicon_url: "", fetched_at: 0, + }, + taos_author_id: "user", + taos_author_kind: "user", + }; + } + override getGeometry(shape: TaosLinkShape) { + return new Rectangle2d({ width: shape.props.w, height: shape.props.h, isFilled: true }); + } + override component(shape: TaosLinkShape) { + const p = shape.props.taos_payload; + return ( + + {p.preview_image_url && ( + { (e.target as HTMLImageElement).style.display = "none"; }} + /> + )} +
+
+ {p.favicon_url && } + {(() => { try { return new URL(p.url).hostname; } catch { return p.url; } })()} +
+
+ {p.title || p.url} +
+
+ {p.description} +
+
+
+ ); + } + override indicator(shape: TaosLinkShape) { + return ; + } + override canResize() { return false; } +} diff --git a/desktop/src/apps/ProjectsApp/canvas/shapes/NoteShape.tsx b/desktop/src/apps/ProjectsApp/canvas/shapes/NoteShape.tsx new file mode 100644 index 00000000..d5270faa --- /dev/null +++ b/desktop/src/apps/ProjectsApp/canvas/shapes/NoteShape.tsx @@ -0,0 +1,76 @@ +import { + HTMLContainer, + ShapeUtil, + TLBaseShape, + Rectangle2d, + T, +} from "@tldraw/tldraw"; + +export type TaosNoteShape = TLBaseShape< + "taos-note", + { + w: number; + h: number; + taos_kind: "note"; + taos_payload: { text: string; color: string; font_size: number }; + taos_author_id: string; + taos_author_kind: "user" | "agent"; + } +>; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class TaosNoteShapeUtil extends ShapeUtil { + static override type = "taos-note" as const; + static override props = { + w: T.number, h: T.number, + taos_kind: T.literal("note"), + taos_payload: T.object({ + text: T.string, color: T.string, font_size: T.number, + }), + taos_author_id: T.string, + taos_author_kind: T.literalEnum("user", "agent"), + }; + + override getDefaultProps(): TaosNoteShape["props"] { + return { + w: 200, h: 100, + taos_kind: "note", + taos_payload: { text: "", color: "yellow", font_size: 14 }, + taos_author_id: "user", + taos_author_kind: "user", + }; + } + override getGeometry(shape: TaosNoteShape) { + return new Rectangle2d({ width: shape.props.w, height: shape.props.h, isFilled: true }); + } + override component(shape: TaosNoteShape) { + const { text, color, font_size } = shape.props.taos_payload; + const bg = COLOR_MAP[color] ?? COLOR_MAP.yellow; + return ( + + {shape.props.taos_author_kind === "agent" && ( +
+ by @{shape.props.taos_author_id} +
+ )} + {text} +
+ ); + } + override indicator(shape: TaosNoteShape) { + return ; + } + override canResize() { return false; } +} + +const COLOR_MAP: Record = { + yellow: "#FFF3A1", blue: "#BEDDFF", green: "#C8F0BE", + pink: "#FFC8E0", grey: "#E0E0E0", +}; diff --git a/desktop/src/lib/projects.ts b/desktop/src/lib/projects.ts index 188b408e..ab5e4c6d 100644 --- a/desktop/src/lib/projects.ts +++ b/desktop/src/lib/projects.ts @@ -17,6 +17,7 @@ export type ProjectMember = { source_agent_id: string | null; memory_seed: "none" | "snapshot" | "empty"; added_at: number; + can_edit_canvas?: boolean; }; export type ProjectTask = { diff --git a/desktop/tests/canvas-e2e.spec.ts b/desktop/tests/canvas-e2e.spec.ts new file mode 100644 index 00000000..262aedeb --- /dev/null +++ b/desktop/tests/canvas-e2e.spec.ts @@ -0,0 +1,61 @@ +// Playwright is not yet scaffolded in this repo (no @playwright/test dep, +// no playwright.config). To run this suite, first wire up Playwright: +// npm install -D @playwright/test +// npx playwright install +// npx playwright init # generate playwright.config.ts +// Then start the FastAPI dev server + `npm run dev`, and: +// npx playwright test canvas-e2e.spec.ts +// +// Until then this file documents the canonical end-to-end coverage we +// want for the per-project canvas board: a user-side hydration path +// (REST seeded → canvas tab renders) and a live SSE path (REST POST +// from outside the page → element appears without reload). + +import { test, expect } from "@playwright/test"; + +test.describe("Project canvas board", () => { + test("user adds note via API, sees it on canvas tab after reload", async ({ + page, request, + }) => { + const created = await request.post("/api/projects", { + data: { name: "E2E Canvas", slug: "e2e-canvas", description: "" }, + }); + expect(created.ok()).toBeTruthy(); + const project = await created.json(); + + await request.post( + `/api/projects/${project.id}/canvas/elements`, + { data: { kind: "note", x: 100, y: 100, w: 200, h: 100, + payload: { text: "hello-from-test", color: "yellow", font_size: 14 } } }, + ); + + await page.goto("/"); + await page.click(`text=${project.name}`); + await page.click("role=tab[name=/canvas/i]"); + + await expect(page.locator(".tl-container")).toBeVisible({ timeout: 5000 }); + await expect(page.getByText("hello-from-test")).toBeVisible({ timeout: 5000 }); + }); + + test("agent adds note via REST → user sees it without reload (SSE)", async ({ + page, request, + }) => { + const created = await request.post("/api/projects", { + data: { name: "E2E SSE", slug: "e2e-sse", description: "" }, + }); + const project = await created.json(); + + await page.goto("/"); + await page.click(`text=${project.name}`); + await page.click("role=tab[name=/canvas/i]"); + await expect(page.locator(".tl-container")).toBeVisible({ timeout: 5000 }); + + await request.post( + `/api/projects/${project.id}/canvas/elements`, + { data: { kind: "note", x: 50, y: 50, w: 150, h: 80, + payload: { text: "live-from-agent", color: "blue", font_size: 14 } } }, + ); + + await expect(page.getByText("live-from-agent")).toBeVisible({ timeout: 3000 }); + }); +}); diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts index 4e99ffd3..430ae111 100644 --- a/desktop/vite.config.ts +++ b/desktop/vite.config.ts @@ -8,6 +8,8 @@ export default defineConfig({ environment: "jsdom", globals: true, setupFiles: ["./vitest.setup.ts"], + // *.spec.ts is reserved for Playwright e2e specs; vitest uses *.test.ts + exclude: ["**/node_modules/**", "**/dist/**", "tests/**"], }, plugins: [react(), tailwindcss()], base: "/desktop/", diff --git a/pyproject.toml b/pyproject.toml index bb5e2b9d..c4a5c25d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,9 @@ dependencies = [ "aiosqlite>=0.20.0", "psutil>=5.9.0", "python-multipart>=0.0.9", + # Project canvas board renders low-fidelity PNG snapshots for vision + # agents (tinyagentos/projects/canvas/render.py). + "Pillow>=10.0", # Model torrent mesh — every instance is a potential peer so # downloads distribute across the swarm rather than hammering a # single mirror. Worker install scripts install the OS-level diff --git a/tests/projects/test_canvas_integration.py b/tests/projects/test_canvas_integration.py new file mode 100644 index 00000000..c7930bfb --- /dev/null +++ b/tests/projects/test_canvas_integration.py @@ -0,0 +1,158 @@ +import asyncio +import json +import pytest +import pytest_asyncio +from fastapi import FastAPI +from httpx import AsyncClient, ASGITransport + +from tinyagentos.projects.events import ProjectEventBroker +from tinyagentos.projects.project_store import ProjectStore +from tinyagentos.projects.canvas.store import ProjectCanvasStore +from tinyagentos.projects.canvas.snapshotter import CanvasSnapshotter +from tinyagentos.projects.folders import ensure_project_layout +from tinyagentos.routes.project_canvas import router as canvas_router + + +@pytest_asyncio.fixture +async def app_env(tmp_path): + db = tmp_path / "db.sqlite" + data_root = tmp_path / "data" + broker = ProjectEventBroker() + ps = ProjectStore(db); await ps.init() + cs = ProjectCanvasStore(db, broker=broker); await cs.init() + p = await ps.create_project(name="Alpha", slug="alpha", created_by="u") + ensure_project_layout(data_root, p["slug"], p["name"]) + snap = CanvasSnapshotter( + project_store=ps, canvas_store=cs, broker=broker, + data_root=data_root, debounce_seconds=0.05, + ) + await snap.start() + await snap._ensure_subscribed(p["id"]) + + app = FastAPI() + app.state.project_store = ps + app.state.project_canvas_store = cs + app.state.canvas_snapshotter = snap + app.state.project_event_broker = broker + app.state.projects_root = data_root + app.include_router(canvas_router) + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c: + yield c, ps, p, data_root, app + await snap.stop() + await cs.close() + await ps.close() + + +async def _collect_canvas_sse(app, project_id, *, stop_on: str, timeout: float = 3.0) -> list[str]: + """Drive canvas SSE endpoint over raw ASGI; stop when stop_on appears in a line.""" + scope = { + "type": "http", + "asgi": {"version": "3.0"}, + "http_version": "1.1", + "method": "GET", + "headers": [(b"accept", b"text/event-stream")], + "scheme": "http", + "path": f"/api/projects/{project_id}/canvas/stream", + "raw_path": f"/api/projects/{project_id}/canvas/stream".encode(), + "query_string": b"", + "server": ("testserver", 80), + "client": ("127.0.0.1", 1234), + "root_path": "", + } + + lines: list[str] = [] + done = asyncio.Event() + + async def receive(): + await done.wait() + return {"type": "http.disconnect"} + + async def send(message): + if message["type"] == "http.response.body": + body = message.get("body", b"") + if body: + for line in body.decode().split("\n"): + stripped = line.rstrip("\r") + if stripped: + lines.append(stripped) + if stop_on in stripped: + done.set() + + task = asyncio.create_task(app(scope, receive, send)) + try: + await asyncio.wait_for(asyncio.shield(done.wait()), timeout=timeout) + except asyncio.TimeoutError: + pass + finally: + task.cancel() + try: + await task + except (asyncio.CancelledError, Exception): + pass + + return lines + + +@pytest.mark.asyncio +async def test_post_then_snapshot_then_sse(app_env): + c, _, p, data_root, app = app_env + + # Post an element after a brief delay so SSE is already listening + async def post_after(): + await asyncio.sleep(0.1) + await c.post(f"/api/projects/{p['id']}/canvas/elements", json={ + "kind": "note", "x": 0, "y": 0, "w": 100, "h": 50, "payload": {"text": "live"}}) + + poster = asyncio.create_task(post_after()) + lines = await _collect_canvas_sse( + app, p["id"], stop_on="canvas.element_added", timeout=3.0 + ) + await poster + + data_lines = [l for l in lines if l.startswith("data:")] + assert any("canvas.element_added" in l for l in data_lines), ( + f"canvas.element_added not found; lines: {lines}" + ) + + # snapshot file gets written within debounce + target = data_root / p["slug"] / "canvas" / "board.tldr" + for _ in range(20): + if target.exists(): + break + await asyncio.sleep(0.05) + assert target.exists(), f"snapshot not written to {target}" + body = json.loads(target.read_text()) + assert any(k.startswith("shape:") for k in body["store"].keys()) + + +@pytest.mark.asyncio +async def test_permission_matrix(app_env): + from tinyagentos.projects.canvas.store import CanvasPermissionError + c, ps, p, _, app = app_env + cs = app.state.project_canvas_store + await ps.add_member(p["id"], "agent-1", member_kind="native") + + el = await cs.add_element( + project_id=p["id"], author_kind="user", author_id="u", + element={"kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"text": "x"}}, + ) + # Default: agent cannot update + with pytest.raises(CanvasPermissionError): + await cs.update_element( + project_id=p["id"], element_id=el["id"], patch={"x": 9}, + author_kind="agent", author_id="agent-1", + ) + + # Toggle on via REST + r = await c.patch( + f"/api/projects/{p['id']}/canvas/permissions/agent-1", + json={"can_edit_canvas": True}, + ) + assert r.status_code == 200 + + # Now agent can update + updated = await cs.update_element( + project_id=p["id"], element_id=el["id"], patch={"x": 9}, + author_kind="agent", author_id="agent-1", + ) + assert updated["x"] == 9 diff --git a/tests/projects/test_canvas_mcp_tools.py b/tests/projects/test_canvas_mcp_tools.py new file mode 100644 index 00000000..e2358a18 --- /dev/null +++ b/tests/projects/test_canvas_mcp_tools.py @@ -0,0 +1,91 @@ +import pytest +import pytest_asyncio +from pathlib import Path + +from tinyagentos.projects.events import ProjectEventBroker +from tinyagentos.projects.project_store import ProjectStore +from tinyagentos.projects.canvas.store import ProjectCanvasStore +from tinyagentos.projects.canvas.snapshotter import CanvasSnapshotter +from tinyagentos.projects.canvas import mcp_tools as ct +from tinyagentos.projects.folders import ensure_project_layout + + +@pytest_asyncio.fixture +async def env(tmp_path): + db = tmp_path / "db.sqlite" + data_root = tmp_path / "data" + broker = ProjectEventBroker() + ps = ProjectStore(db); await ps.init() + cs = ProjectCanvasStore(db, broker=broker); await cs.init() + p = await ps.create_project(name="Alpha", slug="alpha", created_by="u") + ensure_project_layout(data_root, p["slug"], p["name"]) + snap = CanvasSnapshotter( + project_store=ps, canvas_store=cs, broker=broker, + data_root=data_root, debounce_seconds=0.05, + ) + await snap.start() + await ps.add_member(p["id"], "agent-1", member_kind="native") + ctx = ct.CanvasToolContext( + project_store=ps, canvas_store=cs, snapshotter=snap, data_root=data_root, + ) + yield p, ctx, ps + await snap.stop() + await cs.close() + await ps.close() + + +@pytest.mark.asyncio +async def test_canvas_add_note_creates_element(env): + p, ctx, _ = env + res = await ct.canvas_add_note( + ctx, project_id=p["id"], agent_id="agent-1", + text="agent-said-hello", x=10, y=20, + ) + assert res["element"]["kind"] == "note" + assert res["element"]["author_kind"] == "agent" + assert res["element"]["author_id"] == "agent-1" + + +@pytest.mark.asyncio +async def test_canvas_update_denied_without_permission(env): + p, ctx, _ = env + note = await ct.canvas_add_note( + ctx, project_id=p["id"], agent_id="agent-1", + text="x", x=0, y=0, + ) + res = await ct.canvas_update_element( + ctx, project_id=p["id"], agent_id="agent-1", + element_id=note["element"]["id"], patch={"x": 99}, + ) + assert res["error"] == "permission_denied" + + +@pytest.mark.asyncio +async def test_canvas_update_succeeds_with_permission(env): + p, ctx, ps = env + note = await ct.canvas_add_note( + ctx, project_id=p["id"], agent_id="agent-1", + text="x", x=0, y=0, + ) + await ps._db.execute( + "UPDATE project_members SET can_edit_canvas = 1 " + "WHERE project_id = ? AND member_id = ?", + (p["id"], "agent-1"), + ) + await ps._db.commit() + res = await ct.canvas_update_element( + ctx, project_id=p["id"], agent_id="agent-1", + element_id=note["element"]["id"], patch={"x": 99}, + ) + assert res["element"]["x"] == 99 + + +@pytest.mark.asyncio +async def test_canvas_get_snapshot_png_writes_file(env): + p, ctx, _ = env + await ct.canvas_add_note( + ctx, project_id=p["id"], agent_id="agent-1", text="x", x=0, y=0, + ) + res = await ct.canvas_get_snapshot_png(ctx, project_id=p["id"]) + assert "file_path" in res + assert Path(res["file_path"]).exists() diff --git a/tests/projects/test_canvas_render.py b/tests/projects/test_canvas_render.py new file mode 100644 index 00000000..c63428b5 --- /dev/null +++ b/tests/projects/test_canvas_render.py @@ -0,0 +1,31 @@ +import io +from pathlib import Path + +import pytest +from PIL import Image + +from tinyagentos.projects.canvas.render import render_snapshot_png + + +def test_render_empty_canvas_returns_blank_png(tmp_path): + out = tmp_path / "snap.png" + elements: list[dict] = [] + render_snapshot_png(elements=elements, output_path=out) + assert out.exists() + img = Image.open(out) + assert img.size[0] > 0 and img.size[1] > 0 + + +def test_render_with_note(tmp_path): + out = tmp_path / "snap.png" + elements = [{ + "id": "cve-1", "kind": "note", + "x": 50, "y": 50, "w": 200, "h": 100, "rotation": 0, "z_index": 0, + "author_id": "u", "author_kind": "user", + "payload": {"text": "hello world", "color": "yellow"}, + }] + render_snapshot_png(elements=elements, output_path=out) + assert out.exists() + img = Image.open(out) + assert img.size[0] >= 600 + assert img.size[1] >= 400 diff --git a/tests/projects/test_canvas_snapshotter.py b/tests/projects/test_canvas_snapshotter.py new file mode 100644 index 00000000..75e57143 --- /dev/null +++ b/tests/projects/test_canvas_snapshotter.py @@ -0,0 +1,67 @@ +import asyncio +import json +from pathlib import Path + +import pytest +import pytest_asyncio + +from tinyagentos.projects.events import ProjectEventBroker +from tinyagentos.projects.project_store import ProjectStore +from tinyagentos.projects.canvas.store import ProjectCanvasStore +from tinyagentos.projects.canvas.snapshotter import CanvasSnapshotter +from tinyagentos.projects.folders import ensure_project_layout + + +@pytest_asyncio.fixture +async def env(tmp_path): + db = tmp_path / "db.sqlite" + data_root = tmp_path / "data" + broker = ProjectEventBroker() + ps = ProjectStore(db); await ps.init() + cs = ProjectCanvasStore(db, broker=broker); await cs.init() + p = await ps.create_project(name="Alpha", slug="alpha", created_by="u") + ensure_project_layout(data_root, p["slug"], p["name"]) + snap = CanvasSnapshotter( + project_store=ps, canvas_store=cs, broker=broker, + data_root=data_root, debounce_seconds=0.05, + ) + await snap.start() + yield ps, cs, snap, p, data_root + await snap.stop() + await cs.close() + await ps.close() + + +@pytest.mark.asyncio +async def test_add_element_writes_tldr_within_debounce(env): + ps, cs, snap, p, data_root = env + # Ensure subscribed before the publish so the dirty flag is set + await snap._ensure_subscribed(p["id"]) + await cs.add_element( + project_id=p["id"], author_kind="user", author_id="u", + element={"kind": "note", "x": 0, "y": 0, "w": 100, "h": 50, + "payload": {"text": "hi", "color": "yellow", "font_size": 14}}, + ) + target = data_root / p["slug"] / "canvas" / "board.tldr" + for _ in range(20): + if target.exists(): + break + await asyncio.sleep(0.05) + assert target.exists() + body = json.loads(target.read_text()) + assert "store" in body + shape_keys = [k for k in body["store"].keys() if k.startswith("shape:")] + assert len(shape_keys) == 1 + + +@pytest.mark.asyncio +async def test_export_now_synchronous(env): + ps, cs, snap, p, data_root = env + await cs.add_element( + project_id=p["id"], author_kind="user", author_id="u", + element={"kind": "note", "x": 1, "y": 1, "w": 10, "h": 10, + "payload": {"text": "x"}}, + ) + path = await snap.export_now(p["id"]) + assert path is not None + assert path.exists() diff --git a/tests/projects/test_canvas_store.py b/tests/projects/test_canvas_store.py new file mode 100644 index 00000000..02d93c63 --- /dev/null +++ b/tests/projects/test_canvas_store.py @@ -0,0 +1,228 @@ +import pytest +import pytest_asyncio + +from tinyagentos.projects.canvas.store import ProjectCanvasStore + + +@pytest_asyncio.fixture +async def store(tmp_path): + s = ProjectCanvasStore(tmp_path / "canvas.db") + await s.init() + yield s + await s.close() + + +@pytest.mark.asyncio +async def test_store_init_creates_table(store): + async with store._db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='project_canvas_elements'" + ) as cur: + row = await cur.fetchone() + assert row is not None + + +@pytest.mark.asyncio +async def test_add_note_persists_and_returns_row(store): + e = await store.add_element( + project_id="prj-aaa", + element={ + "kind": "note", + "x": 100.0, "y": 200.0, "w": 180.0, "h": 80.0, + "payload": {"text": "hello", "color": "yellow", "font_size": 14}, + }, + author_kind="user", + author_id="user-1", + ) + assert e["id"].startswith("cve-") + assert e["kind"] == "note" + assert e["author_kind"] == "user" + assert e["payload"] == {"text": "hello", "color": "yellow", "font_size": 14} + assert e["deleted_at"] is None + + +@pytest.mark.asyncio +async def test_add_element_rejects_unknown_kind(store): + with pytest.raises(ValueError, match="invalid kind"): + await store.add_element( + project_id="p", element={"kind": "doodad", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {}}, + author_kind="user", author_id="u", + ) + + +@pytest.mark.asyncio +async def test_add_element_rejects_agent_user_shape(store): + with pytest.raises(ValueError, match="agents may not emit"): + await store.add_element( + project_id="p", + element={"kind": "user_shape", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {}}, + author_kind="agent", author_id="agent-1", + ) + + +@pytest.mark.asyncio +async def test_list_elements_excludes_other_projects(store): + a = await store.add_element( + project_id="p1", author_kind="user", author_id="u", + element={"kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"text": "a"}}, + ) + await store.add_element( + project_id="p2", author_kind="user", author_id="u", + element={"kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"text": "b"}}, + ) + rows = await store.list_elements("p1") + assert [r["id"] for r in rows] == [a["id"]] + + +@pytest_asyncio.fixture +async def store_with_member(tmp_path): + """Provides a canvas store backed by the same DB as a project_members + table, so permission lookups have something to read.""" + from tinyagentos.projects.project_store import ProjectStore + db = tmp_path / "shared.db" + ps = ProjectStore(db) + await ps.init() + cs = ProjectCanvasStore(db) + await cs.init() + await ps.add_member("p1", "agent-1", member_kind="native") + yield cs, ps + await cs.close() + await ps.close() + + +@pytest.mark.asyncio +async def test_user_can_always_update(store_with_member): + cs, _ = store_with_member + e = await cs.add_element( + project_id="p1", author_kind="user", author_id="u", + element={"kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"text": "a"}}, + ) + updated = await cs.update_element( + project_id="p1", element_id=e["id"], + patch={"x": 50.0, "payload": {"text": "edited"}}, + author_kind="user", author_id="u", + ) + assert updated["x"] == 50.0 + assert updated["payload"]["text"] == "edited" + + +@pytest.mark.asyncio +async def test_agent_without_permission_cannot_update(store_with_member): + from tinyagentos.projects.canvas.store import CanvasPermissionError + cs, _ = store_with_member + e = await cs.add_element( + project_id="p1", author_kind="user", author_id="u", + element={"kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"text": "a"}}, + ) + with pytest.raises(CanvasPermissionError): + await cs.update_element( + project_id="p1", element_id=e["id"], + patch={"x": 50.0}, + author_kind="agent", author_id="agent-1", + ) + + +@pytest.mark.asyncio +async def test_agent_with_permission_can_update(store_with_member): + cs, ps = store_with_member + await cs._db.execute( + "UPDATE project_members SET can_edit_canvas = 1 WHERE project_id = ? AND member_id = ?", + ("p1", "agent-1"), + ) + await cs._db.commit() + e = await cs.add_element( + project_id="p1", author_kind="user", author_id="u", + element={"kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"text": "a"}}, + ) + updated = await cs.update_element( + project_id="p1", element_id=e["id"], + patch={"x": 50.0}, + author_kind="agent", author_id="agent-1", + ) + assert updated["x"] == 50.0 + + +@pytest.mark.asyncio +async def test_delete_element_soft_excludes_from_list(store): + e = await store.add_element( + project_id="p", author_kind="user", author_id="u", + element={"kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"text": "a"}}, + ) + await store.delete_element( + project_id="p", element_id=e["id"], + author_kind="user", author_id="u", + ) + rows = await store.list_elements("p") + assert rows == [] + raw = await store.get_element(e["id"]) + assert raw is not None + assert raw["deleted_at"] is not None + + +@pytest.mark.asyncio +async def test_delete_requires_permission(store_with_member): + from tinyagentos.projects.canvas.store import CanvasPermissionError + cs, _ = store_with_member + e = await cs.add_element( + project_id="p1", author_kind="user", author_id="u", + element={"kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"text": "a"}}, + ) + with pytest.raises(CanvasPermissionError): + await cs.delete_element( + project_id="p1", element_id=e["id"], + author_kind="agent", author_id="agent-1", + ) + + +@pytest_asyncio.fixture +async def store_with_broker(tmp_path): + from tinyagentos.projects.events import ProjectEventBroker + broker = ProjectEventBroker() + s = ProjectCanvasStore(tmp_path / "canvas.db", broker=broker) + await s.init() + yield s, broker + await s.close() + + +@pytest.mark.asyncio +async def test_add_element_publishes_event(store_with_broker): + s, broker = store_with_broker + queue = await broker.subscribe("p1") + await s.add_element( + project_id="p1", author_kind="user", author_id="u", + element={"kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"text": "x"}}, + ) + ev = await queue.get() + assert ev.kind == "canvas.element_added" + assert ev.payload["element"]["payload"]["text"] == "x" + + +@pytest.mark.asyncio +async def test_update_element_does_not_leak_across_projects(store): + """Calling update_element with the wrong project_id must not return or + mutate an element belonging to another project.""" + e = await store.add_element( + project_id="prj-A", + element={"kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"t": "a"}}, + author_kind="user", + author_id="u", + ) + with pytest.raises(ValueError, match="not found"): + await store.update_element( + project_id="prj-B", + element_id=e["id"], + patch={"x": 999}, + author_kind="user", + author_id="u", + ) + # Empty-patch fast path must also reject cross-project lookups. + with pytest.raises(ValueError, match="not found"): + await store.update_element( + project_id="prj-B", + element_id=e["id"], + patch={}, + author_kind="user", + author_id="u", + ) + # Original row is unchanged. + again = await store.get_element(e["id"]) + assert again["x"] == 0 diff --git a/tests/projects/test_canvas_unfurl.py b/tests/projects/test_canvas_unfurl.py new file mode 100644 index 00000000..5d6ad943 --- /dev/null +++ b/tests/projects/test_canvas_unfurl.py @@ -0,0 +1,46 @@ +import pytest +from unittest.mock import AsyncMock, patch + +from tinyagentos.projects.canvas.unfurl import fetch_link_metadata + + +@pytest.mark.asyncio +async def test_unfurl_extracts_og_tags(): + html = """ + + + + + """ + with patch("tinyagentos.projects.canvas.unfurl._http_get", AsyncMock(return_value=(200, html))): + meta = await fetch_link_metadata("https://x.example/page") + assert meta["title"] == "My Title" + assert meta["description"] == "My Description" + assert meta["preview_image_url"] == "https://x.example/i.png" + assert meta["favicon_url"] == "https://x.example/f.ico" + assert meta["url"] == "https://x.example/page" + + +@pytest.mark.asyncio +async def test_unfurl_falls_back_to_html_title(): + html = "Plain Title" + with patch("tinyagentos.projects.canvas.unfurl._http_get", AsyncMock(return_value=(200, html))): + meta = await fetch_link_metadata("https://x.example/page") + assert meta["title"] == "Plain Title" + assert meta["description"] == "" + + +@pytest.mark.asyncio +async def test_unfurl_handles_non_200(): + with patch("tinyagentos.projects.canvas.unfurl._http_get", AsyncMock(return_value=(500, ""))): + meta = await fetch_link_metadata("https://x.example/page") + assert meta["title"] == "https://x.example/page" + assert meta["description"] == "" + + +@pytest.mark.asyncio +async def test_unfurl_handles_timeout(): + with patch("tinyagentos.projects.canvas.unfurl._http_get", + AsyncMock(side_effect=TimeoutError)): + meta = await fetch_link_metadata("https://x.example/page") + assert meta["title"] == "https://x.example/page" diff --git a/tests/projects/test_routes_canvas.py b/tests/projects/test_routes_canvas.py new file mode 100644 index 00000000..eecf98a4 --- /dev/null +++ b/tests/projects/test_routes_canvas.py @@ -0,0 +1,188 @@ +import asyncio +import json + +import pytest +import pytest_asyncio +from fastapi import FastAPI +from httpx import AsyncClient +from httpx import ASGITransport + +from tinyagentos.projects.events import ProjectEventBroker +from tinyagentos.projects.project_store import ProjectStore +from tinyagentos.projects.canvas.store import ProjectCanvasStore +from tinyagentos.projects.canvas.snapshotter import CanvasSnapshotter +from tinyagentos.projects.folders import ensure_project_layout +from tinyagentos.routes.project_canvas import router as canvas_router + + +@pytest_asyncio.fixture +async def client(tmp_path): + db = tmp_path / "db.sqlite" + data_root = tmp_path / "data" + broker = ProjectEventBroker() + ps = ProjectStore(db); await ps.init() + cs = ProjectCanvasStore(db, broker=broker); await cs.init() + p = await ps.create_project(name="Alpha", slug="alpha", created_by="u") + ensure_project_layout(data_root, p["slug"], p["name"]) + snap = CanvasSnapshotter( + project_store=ps, canvas_store=cs, broker=broker, + data_root=data_root, debounce_seconds=0.05, + ) + await snap.start() + + app = FastAPI() + app.state.project_store = ps + app.state.project_canvas_store = cs + app.state.canvas_snapshotter = snap + app.state.project_event_broker = broker + app.state.projects_root = data_root + app.include_router(canvas_router) + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c: + yield c, p, app + await snap.stop() + await cs.close() + await ps.close() + + +@pytest.mark.asyncio +async def test_get_elements_empty(client): + c, p, _ = client + r = await c.get(f"/api/projects/{p['id']}/canvas/elements") + assert r.status_code == 200 + assert r.json() == {"elements": []} + + +@pytest.mark.asyncio +async def test_post_then_get_elements(client): + c, p, _ = client + body = {"kind": "note", "x": 1, "y": 2, "w": 100, "h": 50, + "payload": {"text": "hello", "color": "yellow", "font_size": 14}} + r = await c.post(f"/api/projects/{p['id']}/canvas/elements", json=body) + assert r.status_code == 201, r.text + elem = r.json()["element"] + assert elem["kind"] == "note" + assert elem["payload"]["text"] == "hello" + + r2 = await c.get(f"/api/projects/{p['id']}/canvas/elements") + assert len(r2.json()["elements"]) == 1 + + +@pytest.mark.asyncio +async def test_patch_element_updates_payload(client): + c, p, _ = client + r = await c.post(f"/api/projects/{p['id']}/canvas/elements", json={ + "kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"text": "a"}}) + eid = r.json()["element"]["id"] + r2 = await c.patch(f"/api/projects/{p['id']}/canvas/elements/{eid}", + json={"x": 99, "payload": {"text": "edited"}}) + assert r2.status_code == 200 + assert r2.json()["element"]["x"] == 99 + assert r2.json()["element"]["payload"]["text"] == "edited" + + +@pytest.mark.asyncio +async def test_delete_element_returns_204_and_hides(client): + c, p, _ = client + r = await c.post(f"/api/projects/{p['id']}/canvas/elements", json={ + "kind": "note", "x": 0, "y": 0, "w": 1, "h": 1, "payload": {"text": "a"}}) + eid = r.json()["element"]["id"] + r2 = await c.delete(f"/api/projects/{p['id']}/canvas/elements/{eid}") + assert r2.status_code == 204 + r3 = await c.get(f"/api/projects/{p['id']}/canvas/elements") + assert r3.json()["elements"] == [] + + +@pytest.mark.asyncio +async def test_snapshot_png_renders(client): + c, p, _ = client + await c.post(f"/api/projects/{p['id']}/canvas/elements", json={ + "kind": "note", "x": 0, "y": 0, "w": 100, "h": 50, "payload": {"text": "hi"}}) + r = await c.get(f"/api/projects/{p['id']}/canvas/snapshot.png") + assert r.status_code == 200 + assert r.headers["content-type"].startswith("image/png") + assert len(r.content) > 100 + + +@pytest.mark.asyncio +async def test_permission_toggle(client): + c, p, app = client + ps = app.state.project_store + await ps.add_member(p["id"], "agent-1", member_kind="native") + r = await c.patch( + f"/api/projects/{p['id']}/canvas/permissions/agent-1", + json={"can_edit_canvas": True}, + ) + assert r.status_code == 200 + members = await ps.list_members(p["id"]) + me = next(m for m in members if m["member_id"] == "agent-1") + assert me["can_edit_canvas"] == 1 + + +async def _collect_canvas_sse(app, project_id, n_lines, timeout=5.0): + """Drive the canvas SSE endpoint over raw ASGI and collect n_lines.""" + scope = { + "type": "http", + "asgi": {"version": "3.0"}, + "http_version": "1.1", + "method": "GET", + "headers": [(b"accept", b"text/event-stream")], + "scheme": "http", + "path": f"/api/projects/{project_id}/canvas/stream", + "raw_path": f"/api/projects/{project_id}/canvas/stream".encode(), + "query_string": b"", + "server": ("testserver", 80), + "client": ("127.0.0.1", 1234), + "root_path": "", + } + + lines: list[str] = [] + done = asyncio.Event() + + async def receive(): + await done.wait() + return {"type": "http.disconnect"} + + async def send(message): + if message["type"] == "http.response.body": + body = message.get("body", b"") + if body: + for line in body.decode().split("\n"): + stripped = line.rstrip("\r") + if stripped: + lines.append(stripped) + if len(lines) >= n_lines: + done.set() + + task = asyncio.create_task(app(scope, receive, send)) + try: + await asyncio.wait_for(asyncio.shield(done.wait()), timeout=timeout) + except asyncio.TimeoutError: + pass + finally: + task.cancel() + try: + await task + except (asyncio.CancelledError, Exception): + pass + + return lines + + +@pytest.mark.asyncio +async def test_sse_stream_emits_canvas_events(client): + c, p, app = client + + # Publish a canvas event into the broker replay buffer first + # so the SSE subscriber gets it immediately on subscribe. + from tinyagentos.projects.events import ProjectEvent + await app.state.project_event_broker.publish( + p["id"], + ProjectEvent(kind="canvas.element_added", payload={"id": "e1"}), + ) + + lines = await _collect_canvas_sse(app, p["id"], n_lines=1, timeout=3.0) + data_lines = [l for l in lines if l.startswith("data:")] + assert data_lines, f"No data: lines received; got: {lines}" + evt = json.loads(data_lines[0][5:].strip()) + assert evt["type"] == "canvas.element_added" diff --git a/tinyagentos/app.py b/tinyagentos/app.py index bb89f517..50c94f94 100644 --- a/tinyagentos/app.py +++ b/tinyagentos/app.py @@ -202,9 +202,12 @@ async def _probe_backend(backend: dict) -> dict: from tinyagentos.projects.project_store import ProjectStore from tinyagentos.projects.task_store import ProjectTaskStore from tinyagentos.projects.events import ProjectEventBroker + from tinyagentos.projects.canvas.store import ProjectCanvasStore as ProjectCanvasStoreImpl + from tinyagentos.projects.canvas.snapshotter import CanvasSnapshotter project_store = ProjectStore(data_dir / "projects.db") project_event_broker = ProjectEventBroker() project_task_store = ProjectTaskStore(data_dir / "projects.db", broker=project_event_broker) + project_canvas_store = ProjectCanvasStoreImpl(data_dir / "projects.db", broker=project_event_broker) projects_root = data_dir / "projects" chat_hub = ChatHub() canvas_store = CanvasStore(data_dir / "canvas.db") @@ -263,6 +266,7 @@ async def lifespan(app: FastAPI): await chat_channels.init() await project_store.init() await project_task_store.init() + await project_canvas_store.init() projects_root.mkdir(parents=True, exist_ok=True) await canvas_store.init() await desktop_settings.init() @@ -404,6 +408,7 @@ async def _ephemeral_sweep_loop(app: FastAPI) -> None: app.state.project_store = project_store app.state.project_task_store = project_task_store app.state.project_event_broker = project_event_broker + app.state.project_canvas_store = project_canvas_store app.state.projects_root = projects_root app.state.chat_hub = chat_hub from tinyagentos.chat.group_policy import GroupPolicy @@ -639,6 +644,20 @@ async def _reload_llm_proxy_on_catalog_change() -> None: logger.exception("beads bridge failed to start — continuing without") app.state.beads_bridge = None + try: + canvas_snapshotter = CanvasSnapshotter( + project_store=project_store, + canvas_store=project_canvas_store, + broker=project_event_broker, + data_root=projects_root, + ) + await canvas_snapshotter.start() + await canvas_snapshotter.backfill_active() + app.state.canvas_snapshotter = canvas_snapshotter + except Exception: + logger.exception("canvas snapshotter failed to start") + app.state.canvas_snapshotter = None + yield # NOTE: controller restart/shutdown does NOT touch agent containers — # agents and LiteLLM keep running independently, so there's nothing to @@ -678,6 +697,13 @@ async def _reload_llm_proxy_on_catalog_change() -> None: await bb.stop() except Exception: logger.exception("beads bridge stop failed") + cs_snap = getattr(app.state, "canvas_snapshotter", None) + if cs_snap is not None: + try: + await cs_snap.stop() + except Exception: + logger.exception("canvas snapshotter stop failed") + await project_canvas_store.close() await project_task_store.close() await project_store.close() await chat_channels.close() @@ -749,7 +775,9 @@ async def _reload_llm_proxy_on_catalog_change() -> None: app.state.project_store = project_store app.state.project_task_store = project_task_store app.state.project_event_broker = project_event_broker + app.state.project_canvas_store = project_canvas_store app.state.beads_bridge = None + app.state.canvas_snapshotter = None projects_root.mkdir(parents=True, exist_ok=True) app.state.projects_root = projects_root app.state.chat_hub = chat_hub @@ -912,6 +940,9 @@ async def _reload_llm_proxy_on_catalog_change() -> None: from tinyagentos.routes.project_files import router as project_files_router app.include_router(project_files_router) + from tinyagentos.routes.project_canvas import router as project_canvas_router + app.include_router(project_canvas_router) + from tinyagentos.routes.shared_folders import router as shared_folders_router app.include_router(shared_folders_router) diff --git a/tinyagentos/projects/canvas/__init__.py b/tinyagentos/projects/canvas/__init__.py new file mode 100644 index 00000000..e510c492 --- /dev/null +++ b/tinyagentos/projects/canvas/__init__.py @@ -0,0 +1,10 @@ +"""Per-project tldraw canvas board. + +See docs/superpowers/specs/2026-04-28-projects-canvas-board-design.md. +""" +from tinyagentos.projects.canvas.store import ( + ProjectCanvasStore, + CanvasPermissionError, +) + +__all__ = ["ProjectCanvasStore", "CanvasPermissionError"] diff --git a/tinyagentos/projects/canvas/mcp_tools.py b/tinyagentos/projects/canvas/mcp_tools.py new file mode 100644 index 00000000..aa57906b --- /dev/null +++ b/tinyagentos/projects/canvas/mcp_tools.py @@ -0,0 +1,143 @@ +"""Agent-facing handler functions for project canvases. + +These are the in-process equivalents of the MCP tools described in +docs/superpowers/specs/2026-04-28-projects-canvas-board-design.md §4. +A real MCP server registration is a follow-up; for v1, agents that +run inside the same process can call these directly. Each function +returns either {"element": ...} / {"elements": ...} on success or +{"error": , "message": } on failure. +""" +from __future__ import annotations + +import time +from dataclasses import dataclass +from pathlib import Path + +from tinyagentos.projects.canvas.store import ( + CanvasPermissionError, + ProjectCanvasStore, +) +from tinyagentos.projects.canvas.unfurl import fetch_link_metadata +from tinyagentos.projects.canvas.render import render_snapshot_png +from tinyagentos.projects.canvas.snapshotter import CanvasSnapshotter + + +@dataclass +class CanvasToolContext: + project_store: object + canvas_store: ProjectCanvasStore + snapshotter: CanvasSnapshotter + data_root: Path + + +async def canvas_list_elements(ctx: CanvasToolContext, *, project_id: str) -> dict: + elements = await ctx.canvas_store.list_elements(project_id) + return {"elements": elements} + + +async def canvas_add_note( + ctx: CanvasToolContext, *, project_id: str, agent_id: str, + text: str, x: float, y: float, color: str = "yellow", +) -> dict: + el = await ctx.canvas_store.add_element( + project_id=project_id, + author_kind="agent", author_id=agent_id, + element={ + "kind": "note", "x": float(x), "y": float(y), + "w": 200.0, "h": 100.0, + "payload": {"text": text, "color": color, "font_size": 14}, + }, + ) + return {"element": el} + + +async def canvas_add_link( + ctx: CanvasToolContext, *, project_id: str, agent_id: str, + url: str, x: float, y: float, +) -> dict: + meta = await fetch_link_metadata(url) + el = await ctx.canvas_store.add_element( + project_id=project_id, + author_kind="agent", author_id=agent_id, + element={ + "kind": "link", "x": float(x), "y": float(y), + "w": 320.0, "h": 120.0, "payload": meta, + }, + ) + return {"element": el} + + +async def canvas_add_image( + ctx: CanvasToolContext, *, project_id: str, agent_id: str, + file_id: str, x: float, y: float, alt: str = "", +) -> dict: + el = await ctx.canvas_store.add_element( + project_id=project_id, + author_kind="agent", author_id=agent_id, + element={ + "kind": "image", "x": float(x), "y": float(y), + "w": 240.0, "h": 240.0, + "payload": {"file_id": file_id, "alt": alt, "mime": "image/png"}, + }, + ) + return {"element": el} + + +async def canvas_update_element( + ctx: CanvasToolContext, *, project_id: str, agent_id: str, + element_id: str, patch: dict, +) -> dict: + try: + el = await ctx.canvas_store.update_element( + project_id=project_id, element_id=element_id, patch=patch, + author_kind="agent", author_id=agent_id, + ) + except CanvasPermissionError: + return { + "error": "permission_denied", + "message": ( + "This agent does not have edit permission on the canvas. " + "Ask the user to enable it in project settings, or message " + "them to make the change." + ), + } + except ValueError as e: + return {"error": "not_found", "message": str(e)} + return {"element": el} + + +async def canvas_delete_element( + ctx: CanvasToolContext, *, project_id: str, agent_id: str, element_id: str, +) -> dict: + try: + await ctx.canvas_store.delete_element( + project_id=project_id, element_id=element_id, + author_kind="agent", author_id=agent_id, + ) + except CanvasPermissionError: + return { + "error": "permission_denied", + "message": ( + "This agent does not have edit permission on the canvas. " + "Ask the user to enable it in project settings, or message " + "them to make the change." + ), + } + return {"ok": True} + + +async def canvas_get_snapshot_png( + ctx: CanvasToolContext, *, project_id: str, +) -> dict: + project = await ctx.project_store.get_project(project_id) + if project is None: + return {"error": "not_found", "message": "project not found"} + out_dir = ctx.data_root / project["slug"] / "files" / "canvas" + out_dir.mkdir(parents=True, exist_ok=True) + target = out_dir / f"snapshot-{int(time.time())}.png" + elements = await ctx.canvas_store.list_elements(project_id) + render_snapshot_png(elements=elements, output_path=target) + return { + "file_path": str(target), + "byte_size": target.stat().st_size, + } diff --git a/tinyagentos/projects/canvas/render.py b/tinyagentos/projects/canvas/render.py new file mode 100644 index 00000000..f8948e33 --- /dev/null +++ b/tinyagentos/projects/canvas/render.py @@ -0,0 +1,83 @@ +"""Server-side PNG rendering of a project canvas. + +This is intentionally a low-fidelity Pillow renderer — we draw bounding +boxes coloured by element kind plus first-line labels. Vision-capable +agents read this to "see" the board. A pixel-perfect tldraw render via +headless browser is a follow-up; the simple renderer is enough to +distinguish layout, kind, and labels. +""" +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +_MIN_WIDTH = 800 +_MIN_HEIGHT = 600 +_PADDING = 40 + +_KIND_FILL = { + "note": (255, 240, 140, 255), + "link": (190, 215, 255, 255), + "image": (220, 220, 220, 255), + "user_shape": (245, 245, 245, 255), +} +_KIND_OUTLINE = { + "note": (200, 180, 60, 255), + "link": (90, 130, 220, 255), + "image": (140, 140, 140, 255), + "user_shape": (180, 180, 180, 255), +} + + +def _bounds(elements: list[dict]) -> tuple[float, float, float, float]: + if not elements: + return 0, 0, _MIN_WIDTH, _MIN_HEIGHT + xs = [e["x"] for e in elements] + ys = [e["y"] for e in elements] + rights = [e["x"] + e["w"] for e in elements] + bottoms = [e["y"] + e["h"] for e in elements] + return min(xs), min(ys), max(rights), max(bottoms) + + +def _label_for(el: dict) -> str: + kind = el.get("kind", "") + payload = el.get("payload") or {} + if kind == "note": + return str(payload.get("text", ""))[:60] + if kind == "link": + return str(payload.get("title") or payload.get("url") or "")[:60] + if kind == "image": + return str(payload.get("alt", "image"))[:40] + return kind + + +def render_snapshot_png(*, elements: list[dict], output_path: Path) -> Path: + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + min_x, min_y, max_x, max_y = _bounds(elements) + width = max(_MIN_WIDTH, int(max_x - min_x) + 2 * _PADDING) + height = max(_MIN_HEIGHT, int(max_y - min_y) + 2 * _PADDING) + img = Image.new("RGB", (width, height), color=(255, 255, 255)) + draw = ImageDraw.Draw(img, "RGBA") + try: + font = ImageFont.load_default() + except Exception: + font = None + + for el in elements: + kind = el.get("kind", "user_shape") + x = int(el["x"] - min_x + _PADDING) + y = int(el["y"] - min_y + _PADDING) + w = int(el["w"]) + h = int(el["h"]) + fill = _KIND_FILL.get(kind, _KIND_FILL["user_shape"]) + outline = _KIND_OUTLINE.get(kind, _KIND_OUTLINE["user_shape"]) + draw.rectangle([x, y, x + w, y + h], fill=fill, outline=outline, width=2) + label = _label_for(el) + if label and font is not None: + draw.text((x + 6, y + 6), label, fill=(20, 20, 20), font=font) + + img.save(output_path, "PNG") + return output_path diff --git a/tinyagentos/projects/canvas/snapshotter.py b/tinyagentos/projects/canvas/snapshotter.py new file mode 100644 index 00000000..5fe4a118 --- /dev/null +++ b/tinyagentos/projects/canvas/snapshotter.py @@ -0,0 +1,223 @@ +"""Debounced .tldr snapshotter for project canvases. + +Mirrors tinyagentos/projects/beads_bridge.py: subscribe to broker, +mark dirty on canvas events, drain periodically. DB is authoritative; +the .tldr file is a derived snapshot — we never read it back. +""" +from __future__ import annotations + +import asyncio +import json +import logging +import os +from collections import defaultdict +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + +_STOP_DRAIN_TIMEOUT = 2.0 +_TLDRAW_SCHEMA_VERSION = 1 + + +class CanvasSnapshotter: + def __init__( + self, + *, + project_store, + canvas_store, + broker, + data_root: Path, + debounce_seconds: float = 0.5, + ) -> None: + self._project_store = project_store + self._canvas_store = canvas_store + self._broker = broker + self._data_root = Path(data_root) + self._debounce = float(debounce_seconds) + + self._dirty: set[str] = set() + self._locks: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock) + self._writer_task: asyncio.Task | None = None + self._broker_tasks: dict[str, asyncio.Task] = {} + self._broker_queues: dict[str, Any] = {} + self._stopped = asyncio.Event() + + async def start(self) -> None: + if self._writer_task is not None: + return + self._stopped.clear() + self._writer_task = asyncio.create_task( + self._writer_loop(), name="canvas-snapshotter" + ) + + async def stop(self) -> None: + if self._writer_task is None: + return + self._stopped.set() + try: + await asyncio.wait_for(self._writer_task, timeout=_STOP_DRAIN_TIMEOUT) + except asyncio.TimeoutError: + self._writer_task.cancel() + try: + await self._writer_task + except (asyncio.CancelledError, Exception): + pass + finally: + self._writer_task = None + for t in self._broker_tasks.values(): + t.cancel() + for t in self._broker_tasks.values(): + try: + await t + except (asyncio.CancelledError, Exception): + pass + self._broker_tasks.clear() + self._broker_queues.clear() + + def mark_dirty(self, project_id: str) -> None: + if project_id: + self._dirty.add(project_id) + + async def backfill_active(self) -> int: + try: + projects = await self._project_store.list_projects(status="active") + except Exception: + logger.exception("canvas snapshotter: list_projects failed") + return 0 + n = 0 + for p in projects: + self.mark_dirty(p["id"]) + await self._ensure_subscribed(p["id"]) + n += 1 + return n + + async def _ensure_subscribed(self, project_id: str) -> None: + if project_id in self._broker_tasks: + return + try: + queue = await self._broker.subscribe(project_id) + except Exception: + logger.exception("canvas snapshotter: subscribe failed for %s", project_id) + return + self._broker_queues[project_id] = queue + self._broker_tasks[project_id] = asyncio.create_task( + self._broker_loop(project_id, queue), + name=f"canvas-snapshotter-broker:{project_id}", + ) + + async def _broker_loop(self, project_id: str, queue: Any) -> None: + try: + while not self._stopped.is_set(): + try: + ev = await asyncio.wait_for(queue.get(), timeout=1.0) + except asyncio.TimeoutError: + continue + if str(ev.kind).startswith("canvas."): + self.mark_dirty(project_id) + except asyncio.CancelledError: + raise + except Exception: + logger.exception( + "canvas snapshotter: broker loop crashed for %s", project_id + ) + finally: + try: + await self._broker.unsubscribe(project_id, queue) + except Exception: + pass + + async def export_now(self, project_id: str) -> Path | None: + await self._ensure_subscribed(project_id) + async with self._locks[project_id]: + return await self._render_tldr(project_id) + + async def _writer_loop(self) -> None: + while not self._stopped.is_set(): + try: + await asyncio.sleep(self._debounce) + if not self._dirty: + continue + pending = list(self._dirty) + self._dirty.clear() + for project_id in pending: + try: + async with self._locks[project_id]: + await self._render_tldr(project_id) + except Exception: + logger.exception( + "canvas snapshotter: render failed for %s", project_id + ) + self._dirty.add(project_id) + except asyncio.CancelledError: + raise + except Exception: + logger.exception("canvas snapshotter: writer iteration crashed") + + async def _render_tldr(self, project_id: str) -> Path | None: + project = await self._project_store.get_project(project_id) + if project is None: + return None + slug = project["slug"] + canvas_dir = self._data_root / slug / "canvas" + canvas_dir.mkdir(parents=True, exist_ok=True) + target = canvas_dir / "board.tldr" + tmp = canvas_dir / f"board.tldr.{os.getpid()}.tmp" + + elements = await self._canvas_store.list_elements(project_id) + snapshot = _build_tldraw_snapshot(elements) + tmp.write_text(json.dumps(snapshot, separators=(",", ":"))) + os.replace(tmp, target) + return target + + +def _build_tldraw_snapshot(elements: list[dict]) -> dict: + store: dict[str, dict] = { + "document:document": { + "id": "document:document", + "typeName": "document", + "name": "", + }, + "page:page": { + "id": "page:page", + "typeName": "page", + "name": "Page 1", + "index": "a1", + }, + } + for el in elements: + store[f"shape:{el['id']}"] = { + "id": f"shape:{el['id']}", + "typeName": "shape", + "type": _tldraw_shape_type(el["kind"]), + "x": el["x"], + "y": el["y"], + "rotation": el["rotation"], + "index": "a1", + "parentId": "page:page", + "isLocked": False, + "opacity": 1, + "props": { + "w": el["w"], + "h": el["h"], + "taos_kind": el["kind"], + "taos_payload": el["payload"], + "taos_author_id": el["author_id"], + "taos_author_kind": el["author_kind"], + }, + "meta": {}, + } + return { + "schema": {"schemaVersion": 2, "storeVersion": _TLDRAW_SCHEMA_VERSION}, + "store": store, + } + + +def _tldraw_shape_type(kind: str) -> str: + if kind == "note": + return "taos-note" + if kind == "link": + return "taos-link" + if kind == "image": + return "taos-image" + return "geo" diff --git a/tinyagentos/projects/canvas/store.py b/tinyagentos/projects/canvas/store.py new file mode 100644 index 00000000..6f0e6532 --- /dev/null +++ b/tinyagentos/projects/canvas/store.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +import json +import time +from typing import TYPE_CHECKING + +from tinyagentos.base_store import BaseStore +from tinyagentos.projects.ids import new_id + +if TYPE_CHECKING: + from tinyagentos.projects.events import ProjectEventBroker + + +CANVAS_SCHEMA = """ +CREATE TABLE IF NOT EXISTS project_canvas_elements ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + kind TEXT NOT NULL, + author_kind TEXT NOT NULL, + author_id TEXT NOT NULL, + x REAL NOT NULL, + y REAL NOT NULL, + w REAL NOT NULL, + h REAL NOT NULL, + rotation REAL NOT NULL DEFAULT 0, + z_index INTEGER NOT NULL DEFAULT 0, + payload TEXT NOT NULL, + created_at REAL NOT NULL, + updated_at REAL NOT NULL, + deleted_at REAL +); +CREATE INDEX IF NOT EXISTS idx_canvas_project ON project_canvas_elements(project_id, deleted_at); +CREATE INDEX IF NOT EXISTS idx_canvas_updated ON project_canvas_elements(project_id, updated_at); +""" + +_CANVAS_JSON_FIELDS = ("payload",) +_VALID_KINDS = {"note", "link", "image", "user_shape"} +_AGENT_ALLOWED_KINDS = {"note", "link", "image"} + + +class CanvasPermissionError(PermissionError): + """Raised when an agent without can_edit_canvas tries to update/delete.""" + + +def _row_to_element(row, description) -> dict: + keys = [d[0] for d in description] + e = dict(zip(keys, row)) + for f in _CANVAS_JSON_FIELDS: + if f in e and e[f] is not None: + e[f] = json.loads(e[f]) + return e + + +class ProjectCanvasStore(BaseStore): + SCHEMA = CANVAS_SCHEMA + + def __init__(self, db_path, *, broker: "ProjectEventBroker | None" = None) -> None: + super().__init__(db_path) + self._broker = broker + + async def _publish(self, project_id: str, kind: str, payload: dict) -> None: + if self._broker is not None: + from tinyagentos.projects.events import ProjectEvent + await self._broker.publish(project_id, ProjectEvent(kind=kind, payload=payload)) + + async def get_element( + self, element_id: str, *, project_id: str | None = None + ) -> dict | None: + if project_id is None: + sql = "SELECT * FROM project_canvas_elements WHERE id = ?" + args: tuple = (element_id,) + else: + sql = ( + "SELECT * FROM project_canvas_elements " + "WHERE id = ? AND project_id = ?" + ) + args = (element_id, project_id) + async with self._db.execute(sql, args) as cur: + row = await cur.fetchone() + if row is None: + return None + return _row_to_element(row, cur.description) + + async def add_element( + self, + *, + project_id: str, + element: dict, + author_kind: str, + author_id: str, + ) -> dict: + kind = element.get("kind") + if kind not in _VALID_KINDS: + raise ValueError(f"invalid kind: {kind}") + if author_kind == "agent" and kind not in _AGENT_ALLOWED_KINDS: + raise ValueError(f"agents may not emit kind={kind}") + if author_kind not in ("user", "agent"): + raise ValueError(f"invalid author_kind: {author_kind}") + eid = element.get("id") or new_id("cve") + now = time.time() + await self._db.execute( + """INSERT INTO project_canvas_elements + (id, project_id, kind, author_kind, author_id, + x, y, w, h, rotation, z_index, payload, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + ( + eid, project_id, kind, author_kind, author_id, + float(element["x"]), float(element["y"]), + float(element["w"]), float(element["h"]), + float(element.get("rotation", 0)), + int(element.get("z_index", 0)), + json.dumps(element.get("payload") or {}), + now, now, + ), + ) + await self._db.commit() + new_el = await self.get_element(eid) + await self._publish(project_id, "canvas.element_added", {"element": new_el}) + return new_el + + async def list_elements(self, project_id: str) -> list[dict]: + async with self._db.execute( + """SELECT * FROM project_canvas_elements + WHERE project_id = ? AND deleted_at IS NULL + ORDER BY z_index ASC, created_at ASC""", + (project_id,), + ) as cur: + rows = await cur.fetchall() + desc = cur.description + return [_row_to_element(r, desc) for r in rows] + + async def _check_edit_permission( + self, project_id: str, author_kind: str, author_id: str + ) -> None: + if author_kind == "user": + return + if author_kind != "agent": + raise ValueError(f"invalid author_kind: {author_kind}") + async with self._db.execute( + "SELECT can_edit_canvas FROM project_members " + "WHERE project_id = ? AND member_id = ?", + (project_id, author_id), + ) as cur: + row = await cur.fetchone() + if row is None or not row[0]: + raise CanvasPermissionError( + f"agent {author_id} has no can_edit_canvas on project {project_id}" + ) + + async def update_element( + self, + *, + project_id: str, + element_id: str, + patch: dict, + author_kind: str, + author_id: str, + ) -> dict: + await self._check_edit_permission(project_id, author_kind, author_id) + sets: list[str] = [] + params: list = [] + for col in ("x", "y", "w", "h", "rotation", "z_index"): + if col in patch: + sets.append(f"{col} = ?") + params.append(patch[col]) + if "payload" in patch: + sets.append("payload = ?") + params.append(json.dumps(patch["payload"])) + if not sets: + existing = await self.get_element(element_id, project_id=project_id) + if existing is None: + raise ValueError(f"element not found: {element_id}") + return existing + sets.append("updated_at = ?"); params.append(time.time()) + params.append(element_id) + params.append(project_id) + await self._db.execute( + f"UPDATE project_canvas_elements SET {', '.join(sets)} " + f"WHERE id = ? AND project_id = ? AND deleted_at IS NULL", + params, + ) + await self._db.commit() + updated = await self.get_element(element_id, project_id=project_id) + if updated is None: + raise ValueError(f"element not found: {element_id}") + await self._publish(project_id, "canvas.element_updated", {"element": updated}) + return updated + + async def delete_element( + self, + *, + project_id: str, + element_id: str, + author_kind: str, + author_id: str, + ) -> None: + await self._check_edit_permission(project_id, author_kind, author_id) + now = time.time() + cur = await self._db.execute( + """UPDATE project_canvas_elements + SET deleted_at = ?, updated_at = ? + WHERE id = ? AND project_id = ? AND deleted_at IS NULL""", + (now, now, element_id, project_id), + ) + await self._db.commit() + if cur.rowcount == 1: + await self._publish( + project_id, "canvas.element_deleted", + {"element_id": element_id}, + ) diff --git a/tinyagentos/projects/canvas/unfurl.py b/tinyagentos/projects/canvas/unfurl.py new file mode 100644 index 00000000..4a367a0f --- /dev/null +++ b/tinyagentos/projects/canvas/unfurl.py @@ -0,0 +1,162 @@ +"""Lightweight URL preview fetcher. + +Returns OG/twitter card / HTML title metadata. Never raises: +on any failure (timeout, non-2xx, parse error) returns a fallback +dict with title=url and empty description. +""" +from __future__ import annotations + +import asyncio +import ipaddress +import logging +import re +import time +from html.parser import HTMLParser +from urllib.parse import urljoin, urlparse + +import httpx + +logger = logging.getLogger(__name__) + +_TIMEOUT = 5.0 +# Cap unfurl response bodies. Pages over this limit are truncated; the +# parser only needs metadata so a megabyte is generous. +_MAX_BODY_BYTES = 1_000_000 + + +async def _check_ssrf_safe(host: str) -> None: + """Resolve host and reject private / loopback / link-local addresses.""" + loop = asyncio.get_running_loop() + infos = await loop.getaddrinfo(host, None) + for _family, _type, _proto, _canon, sockaddr in infos: + ip = ipaddress.ip_address(sockaddr[0]) + if ( + ip.is_private or ip.is_loopback or ip.is_link_local + or ip.is_multicast or ip.is_reserved or ip.is_unspecified + ): + raise ValueError(f"refusing to fetch non-public address: {ip}") + + +async def _http_get(url: str) -> tuple[int, str]: + """Indirection seam for tests. Production path enforces SSRF + size cap.""" + parsed = urlparse(url) + if parsed.scheme not in ("http", "https"): + raise ValueError(f"unsupported scheme: {parsed.scheme!r}") + if not parsed.hostname: + raise ValueError("missing host") + await _check_ssrf_safe(parsed.hostname) + # follow_redirects=False so a redirect to localhost/private cannot bypass + # the resolution check above. + async with httpx.AsyncClient( + follow_redirects=False, + timeout=_TIMEOUT, + headers={"User-Agent": "taOS-canvas-unfurl/0.1"}, + ) as client: + async with client.stream("GET", url) as r: + chunks: list[bytes] = [] + total = 0 + async for chunk in r.aiter_bytes(): + total += len(chunk) + chunks.append(chunk) + if total >= _MAX_BODY_BYTES: + break + body = b"".join(chunks)[:_MAX_BODY_BYTES] + text = body.decode(r.encoding or "utf-8", errors="replace") + return r.status_code, text + + +class _MetaParser(HTMLParser): + def __init__(self): + super().__init__() + self.meta: dict[str, str] = {} + self.title: str | None = None + self._in_title = False + self.favicon: str | None = None + + def handle_starttag(self, tag, attrs): + a = dict(attrs) + if tag == "meta": + prop = (a.get("property") or a.get("name") or "").lower() + content = a.get("content") + if prop and content: + self.meta[prop] = content + elif tag == "link": + rel = (a.get("rel") or "").lower() + if "icon" in rel and a.get("href"): + self.favicon = a["href"] + elif tag == "title": + self._in_title = True + + def handle_endtag(self, tag): + if tag == "title": + self._in_title = False + + def handle_data(self, data): + if self._in_title and self.title is None: + self.title = data.strip() + + +def _parse_metadata(html: str, base_url: str) -> dict: + p = _MetaParser() + try: + p.feed(html) + except Exception: + pass + + title = ( + p.meta.get("og:title") + or p.meta.get("twitter:title") + or p.title + or base_url + ) + description = ( + p.meta.get("og:description") + or p.meta.get("twitter:description") + or p.meta.get("description") + or "" + ) + preview = p.meta.get("og:image") or p.meta.get("twitter:image") or "" + favicon = p.favicon or "" + + if preview and not re.match(r"^https?://", preview): + preview = urljoin(base_url, preview) + if favicon and not re.match(r"^https?://", favicon): + favicon = urljoin(base_url, favicon) + + return { + "url": base_url, + "title": title.strip() if isinstance(title, str) else base_url, + "description": description.strip() if isinstance(description, str) else "", + "preview_image_url": preview, + "favicon_url": favicon, + "fetched_at": time.time(), + } + + +def _fallback(url: str) -> dict: + return { + "url": url, + "title": url, + "description": "", + "preview_image_url": "", + "favicon_url": "", + "fetched_at": time.time(), + } + + +async def fetch_link_metadata(url: str) -> dict: + try: + status, body = await asyncio.wait_for(_http_get(url), timeout=_TIMEOUT + 0.5) + except (asyncio.TimeoutError, TimeoutError): + logger.info("canvas unfurl timeout for %s", url) + return _fallback(url) + except Exception: + logger.info("canvas unfurl error for %s", url, exc_info=True) + return _fallback(url) + if status < 200 or status >= 300: + return _fallback(url) + try: + return _parse_metadata(body, url) + except Exception: + logger.info("canvas unfurl parse error for %s", url, exc_info=True) + return _fallback(url) diff --git a/tinyagentos/projects/ids.py b/tinyagentos/projects/ids.py index 083b4cb2..9cbc10af 100644 --- a/tinyagentos/projects/ids.py +++ b/tinyagentos/projects/ids.py @@ -1,7 +1,7 @@ from __future__ import annotations import secrets -ID_PREFIXES = ("prj", "tsk", "cmt", "rel") +ID_PREFIXES = ("prj", "tsk", "cmt", "rel", "cve") _ALPHABET = "abcdefghijklmnopqrstuvwxyz234567" diff --git a/tinyagentos/projects/project_store.py b/tinyagentos/projects/project_store.py index 62a4bce8..d7056b95 100644 --- a/tinyagentos/projects/project_store.py +++ b/tinyagentos/projects/project_store.py @@ -30,6 +30,7 @@ role TEXT NOT NULL DEFAULT 'member', source_agent_id TEXT, memory_seed TEXT NOT NULL DEFAULT 'none', + can_edit_canvas INTEGER NOT NULL DEFAULT 0, added_at REAL NOT NULL, PRIMARY KEY (project_id, member_id) ); @@ -61,6 +62,16 @@ def _row_to_project(row, description) -> dict: class ProjectStore(BaseStore): SCHEMA = PROJECTS_SCHEMA + async def _post_init(self) -> None: + try: + await self._db.execute( + "ALTER TABLE project_members ADD COLUMN can_edit_canvas INTEGER NOT NULL DEFAULT 0" + ) + await self._db.commit() + except Exception: + # Column already exists on fresh installs (created by SCHEMA). + pass + async def create_project( self, name: str, diff --git a/tinyagentos/routes/project_canvas.py b/tinyagentos/routes/project_canvas.py new file mode 100644 index 00000000..75343590 --- /dev/null +++ b/tinyagentos/routes/project_canvas.py @@ -0,0 +1,196 @@ +"""REST API for per-project canvas boards. + +See docs/superpowers/specs/2026-04-28-projects-canvas-board-design.md. +""" +from __future__ import annotations + +import asyncio +import json +import logging +from typing import Literal + +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse, StreamingResponse, FileResponse, Response +from pydantic import BaseModel, Field + +from tinyagentos.projects.canvas.store import CanvasPermissionError +from tinyagentos.projects.canvas.unfurl import fetch_link_metadata +from tinyagentos.projects.canvas.render import render_snapshot_png + +logger = logging.getLogger(__name__) +router = APIRouter() + + +def _user_id(request: Request) -> str: + user = getattr(request.state, "user", None) + if user and isinstance(user, dict) and "id" in user: + return user["id"] + return "system" + + +class CreateElementIn(BaseModel): + kind: Literal["note", "link", "image", "user_shape"] + x: float + y: float + w: float + h: float + rotation: float = 0 + z_index: int = 0 + payload: dict = Field(default_factory=dict) + id: str | None = None + + +@router.get("/api/projects/{project_id}/canvas/elements") +async def list_canvas_elements(project_id: str, request: Request): + cs = request.app.state.project_canvas_store + elements = await cs.list_elements(project_id) + return {"elements": elements} + + +@router.post("/api/projects/{project_id}/canvas/elements", status_code=201) +async def create_canvas_element( + project_id: str, payload: CreateElementIn, request: Request, +): + cs = request.app.state.project_canvas_store + element = payload.model_dump() + if element["kind"] == "link": + url = (element.get("payload") or {}).get("url") + if not url: + return JSONResponse({"error": "link element requires payload.url"}, status_code=400) + meta = await fetch_link_metadata(url) + element["payload"] = meta + try: + new_el = await cs.add_element( + project_id=project_id, element=element, + author_kind="user", author_id=_user_id(request), + ) + except ValueError as e: + return JSONResponse({"error": str(e)}, status_code=400) + return {"element": new_el} + + +class PatchElementIn(BaseModel): + x: float | None = None + y: float | None = None + w: float | None = None + h: float | None = None + rotation: float | None = None + z_index: int | None = None + payload: dict | None = None + + +@router.patch("/api/projects/{project_id}/canvas/elements/{element_id}") +async def update_canvas_element( + project_id: str, element_id: str, payload: PatchElementIn, request: Request, +): + cs = request.app.state.project_canvas_store + patch = {k: v for k, v in payload.model_dump().items() if v is not None} + try: + updated = await cs.update_element( + project_id=project_id, element_id=element_id, patch=patch, + author_kind="user", author_id=_user_id(request), + ) + except CanvasPermissionError as e: + return JSONResponse({"error": "permission_denied", "message": str(e)}, status_code=403) + except ValueError as e: + return JSONResponse({"error": str(e)}, status_code=404) + return {"element": updated} + + +@router.delete("/api/projects/{project_id}/canvas/elements/{element_id}", status_code=204) +async def delete_canvas_element(project_id: str, element_id: str, request: Request): + cs = request.app.state.project_canvas_store + try: + await cs.delete_element( + project_id=project_id, element_id=element_id, + author_kind="user", author_id=_user_id(request), + ) + except CanvasPermissionError as e: + return JSONResponse({"error": "permission_denied", "message": str(e)}, status_code=403) + return Response(status_code=204) + + +class PermissionIn(BaseModel): + can_edit_canvas: bool + + +@router.get("/api/projects/{project_id}/canvas/snapshot.png") +async def get_canvas_png(project_id: str, request: Request): + cs = request.app.state.project_canvas_store + elements = await cs.list_elements(project_id) + project = await request.app.state.project_store.get_project(project_id) + if project is None: + return JSONResponse({"error": "project not found"}, status_code=404) + out = ( + request.app.state.projects_root + / project["slug"] / "files" / "canvas" + ) + out.mkdir(parents=True, exist_ok=True) + target = out / "snapshot.png" + render_snapshot_png(elements=elements, output_path=target) + return FileResponse(target, media_type="image/png") + + +@router.get("/api/projects/{project_id}/canvas/snapshot.tldr") +async def get_canvas_tldr(project_id: str, request: Request): + snap = request.app.state.canvas_snapshotter + path = await snap.export_now(project_id) + if path is None or not path.exists(): + return JSONResponse({"error": "project not found"}, status_code=404) + return FileResponse(path, media_type="application/json") + + +@router.patch("/api/projects/{project_id}/canvas/permissions/{agent_id}") +async def set_canvas_permission( + project_id: str, agent_id: str, payload: PermissionIn, request: Request, +): + ps = request.app.state.project_store + val = 1 if payload.can_edit_canvas else 0 + cur = await ps._db.execute( + "UPDATE project_members SET can_edit_canvas = ? " + "WHERE project_id = ? AND member_id = ?", + (val, project_id, agent_id), + ) + await ps._db.commit() + if cur.rowcount == 0: + return JSONResponse({"error": "member not found"}, status_code=404) + broker = request.app.state.project_event_broker + from tinyagentos.projects.events import ProjectEvent + await broker.publish( + project_id, + ProjectEvent( + kind="canvas.permission_changed", + payload={"agent_id": agent_id, "can_edit_canvas": bool(val)}, + ), + ) + return {"ok": True, "agent_id": agent_id, "can_edit_canvas": bool(val)} + + +@router.get("/api/projects/{project_id}/canvas/stream") +async def canvas_stream(project_id: str, request: Request): + broker = request.app.state.project_event_broker + queue = await broker.subscribe(project_id) + + async def gen(): + try: + while True: + if await request.is_disconnected(): + return + try: + ev = await asyncio.wait_for(queue.get(), timeout=10.0) + except asyncio.TimeoutError: + yield ":keepalive\n\n" + continue + if not str(ev.kind).startswith("canvas."): + continue + data = json.dumps({ + "type": ev.kind, + "project_id": project_id, + "payload": ev.payload, + "ts": ev.ts, + }) + yield f"data: {data}\n\n" + finally: + await broker.unsubscribe(project_id, queue) + + return StreamingResponse(gen(), media_type="text/event-stream")