feat(rnotes): add real-time Yjs collaboration, comments, and suggestions
Replace whole-content Automerge sync with character-level Yjs CRDT for NOTE-type notes. Adds cursor presence, inline comments with threaded replies, and track-changes suggesting mode. - Custom Yjs WebSocket provider bridging over existing rSpace WS - Server-side yjs-sync/yjs-awareness message relay (pure broadcast) - y-indexeddb for offline persistence, periodic plaintext sync to Automerge - Comment mark + panel with resolve/reply/delete - Suggestion insert/delete marks with accept/reject support - Schema v3→v4 (collabEnabled, comments fields) - Collab toolbar: comment button, suggesting toggle, peer indicators Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b51dac1b22
commit
a736321189
120
bun.lock
120
bun.lock
|
|
@ -25,9 +25,14 @@
|
||||||
"@tiptap/extension-underline": "^3.20.0",
|
"@tiptap/extension-underline": "^3.20.0",
|
||||||
"@tiptap/pm": "^3.20.0",
|
"@tiptap/pm": "^3.20.0",
|
||||||
"@tiptap/starter-kit": "^3.20.0",
|
"@tiptap/starter-kit": "^3.20.0",
|
||||||
|
"@tiptap/y-tiptap": "^3.0.2",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
"@x402/core": "^2.3.1",
|
"@x402/core": "^2.3.1",
|
||||||
"@x402/evm": "^2.5.0",
|
"@x402/evm": "^2.5.0",
|
||||||
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
"@xterm/xterm": "^6.0.0",
|
||||||
"cron-parser": "^5.5.0",
|
"cron-parser": "^5.5.0",
|
||||||
|
"h3-js": "^4.4.0",
|
||||||
"hono": "^4.11.7",
|
"hono": "^4.11.7",
|
||||||
"imapflow": "^1.0.170",
|
"imapflow": "^1.0.170",
|
||||||
"jose": "^6.0.11",
|
"jose": "^6.0.11",
|
||||||
|
|
@ -39,11 +44,16 @@
|
||||||
"perfect-arrows": "^0.3.7",
|
"perfect-arrows": "^0.3.7",
|
||||||
"perfect-freehand": "^1.2.2",
|
"perfect-freehand": "^1.2.2",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"sharp": "^0.33.0",
|
"sharp": "^0.33.0",
|
||||||
"web-push": "^3.6.7",
|
"web-push": "^3.6.7",
|
||||||
|
"y-indexeddb": "^9.0.12",
|
||||||
|
"y-prosemirror": "^1.3.7",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.8.2",
|
||||||
|
"yjs": "^13.6.30",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/mailparser": "^3.4.0",
|
"@types/mailparser": "^3.4.0",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/nodemailer": "^6.4.0",
|
"@types/nodemailer": "^6.4.0",
|
||||||
|
|
@ -261,6 +271,8 @@
|
||||||
|
|
||||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
|
"@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="],
|
||||||
|
|
||||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||||
|
|
||||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||||
|
|
@ -539,6 +551,8 @@
|
||||||
|
|
||||||
"@tiptap/starter-kit": ["@tiptap/starter-kit@3.20.0", "", { "dependencies": { "@tiptap/core": "^3.20.0", "@tiptap/extension-blockquote": "^3.20.0", "@tiptap/extension-bold": "^3.20.0", "@tiptap/extension-bullet-list": "^3.20.0", "@tiptap/extension-code": "^3.20.0", "@tiptap/extension-code-block": "^3.20.0", "@tiptap/extension-document": "^3.20.0", "@tiptap/extension-dropcursor": "^3.20.0", "@tiptap/extension-gapcursor": "^3.20.0", "@tiptap/extension-hard-break": "^3.20.0", "@tiptap/extension-heading": "^3.20.0", "@tiptap/extension-horizontal-rule": "^3.20.0", "@tiptap/extension-italic": "^3.20.0", "@tiptap/extension-link": "^3.20.0", "@tiptap/extension-list": "^3.20.0", "@tiptap/extension-list-item": "^3.20.0", "@tiptap/extension-list-keymap": "^3.20.0", "@tiptap/extension-ordered-list": "^3.20.0", "@tiptap/extension-paragraph": "^3.20.0", "@tiptap/extension-strike": "^3.20.0", "@tiptap/extension-text": "^3.20.0", "@tiptap/extension-underline": "^3.20.0", "@tiptap/extensions": "^3.20.0", "@tiptap/pm": "^3.20.0" } }, "sha512-W4+1re35pDNY/7rpXVg+OKo/Fa4Gfrn08Bq3E3fzlJw6gjE3tYU8dY9x9vC2rK9pd9NOp7Af11qCFDaWpohXkw=="],
|
"@tiptap/starter-kit": ["@tiptap/starter-kit@3.20.0", "", { "dependencies": { "@tiptap/core": "^3.20.0", "@tiptap/extension-blockquote": "^3.20.0", "@tiptap/extension-bold": "^3.20.0", "@tiptap/extension-bullet-list": "^3.20.0", "@tiptap/extension-code": "^3.20.0", "@tiptap/extension-code-block": "^3.20.0", "@tiptap/extension-document": "^3.20.0", "@tiptap/extension-dropcursor": "^3.20.0", "@tiptap/extension-gapcursor": "^3.20.0", "@tiptap/extension-hard-break": "^3.20.0", "@tiptap/extension-heading": "^3.20.0", "@tiptap/extension-horizontal-rule": "^3.20.0", "@tiptap/extension-italic": "^3.20.0", "@tiptap/extension-link": "^3.20.0", "@tiptap/extension-list": "^3.20.0", "@tiptap/extension-list-item": "^3.20.0", "@tiptap/extension-list-keymap": "^3.20.0", "@tiptap/extension-ordered-list": "^3.20.0", "@tiptap/extension-paragraph": "^3.20.0", "@tiptap/extension-strike": "^3.20.0", "@tiptap/extension-text": "^3.20.0", "@tiptap/extension-underline": "^3.20.0", "@tiptap/extensions": "^3.20.0", "@tiptap/pm": "^3.20.0" } }, "sha512-W4+1re35pDNY/7rpXVg+OKo/Fa4Gfrn08Bq3E3fzlJw6gjE3tYU8dY9x9vC2rK9pd9NOp7Af11qCFDaWpohXkw=="],
|
||||||
|
|
||||||
|
"@tiptap/y-tiptap": ["@tiptap/y-tiptap@3.0.2", "", { "dependencies": { "lib0": "^0.2.100" }, "peerDependencies": { "prosemirror-model": "^1.7.1", "prosemirror-state": "^1.2.3", "prosemirror-view": "^1.9.10", "y-protocols": "^1.0.1", "yjs": "^13.5.38" } }, "sha512-flMn/YW6zTbc6cvDaUPh/NfLRTXDIqgpBUkYzM74KA1snqQwhOMjnRcnpu4hDFrTnPO6QGzr99vRyXEA7M44WA=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||||
|
|
@ -557,6 +571,8 @@
|
||||||
|
|
||||||
"@types/nodemailer": ["@types/nodemailer@6.4.22", "", { "dependencies": { "@types/node": "*" } }, "sha512-HV16KRsW7UyZBITE07B62k8PRAKFqRSFXn1T7vslurVjN761tMDBhk5Lbt17ehyTzK6XcyJnAgUpevrvkcVOzw=="],
|
"@types/nodemailer": ["@types/nodemailer@6.4.22", "", { "dependencies": { "@types/node": "*" } }, "sha512-HV16KRsW7UyZBITE07B62k8PRAKFqRSFXn1T7vslurVjN761tMDBhk5Lbt17ehyTzK6XcyJnAgUpevrvkcVOzw=="],
|
||||||
|
|
||||||
|
"@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||||
|
|
||||||
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
|
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
|
||||||
|
|
@ -573,6 +589,10 @@
|
||||||
|
|
||||||
"@x402/fetch": ["@x402/fetch@2.5.0", "", { "dependencies": { "@x402/core": "~2.5.0", "viem": "^2.39.3", "zod": "^3.24.2" } }, "sha512-D2jH3bn0nf8w9Jg3Vxo+6reE6Z9GickzkSIw+udITJFvsrGOpfjZvhcTeflLcthCODk4Nuu9Oe8x7Q3NLUdaRQ=="],
|
"@x402/fetch": ["@x402/fetch@2.5.0", "", { "dependencies": { "@x402/core": "~2.5.0", "viem": "^2.39.3", "zod": "^3.24.2" } }, "sha512-D2jH3bn0nf8w9Jg3Vxo+6reE6Z9GickzkSIw+udITJFvsrGOpfjZvhcTeflLcthCODk4Nuu9Oe8x7Q3NLUdaRQ=="],
|
||||||
|
|
||||||
|
"@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="],
|
||||||
|
|
||||||
|
"@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="],
|
||||||
|
|
||||||
"@zone-eu/mailsplit": ["@zone-eu/mailsplit@5.4.8", "", { "dependencies": { "libbase64": "1.3.0", "libmime": "5.3.7", "libqp": "2.1.1" } }, "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA=="],
|
"@zone-eu/mailsplit": ["@zone-eu/mailsplit@5.4.8", "", { "dependencies": { "libbase64": "1.3.0", "libmime": "5.3.7", "libqp": "2.1.1" } }, "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA=="],
|
||||||
|
|
||||||
"abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="],
|
"abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="],
|
||||||
|
|
@ -583,9 +603,9 @@
|
||||||
|
|
||||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"apg-js": ["apg-js@4.4.0", "", {}, "sha512-fefmXFknJmtgtNEXfPwZKYkMFX4Fyeyz+fNF6JWp87biGOPslJbCBVU158zvKRZfHBKnJDy8CMM40oLFGkXT8Q=="],
|
"apg-js": ["apg-js@4.4.0", "", {}, "sha512-fefmXFknJmtgtNEXfPwZKYkMFX4Fyeyz+fNF6JWp87biGOPslJbCBVU158zvKRZfHBKnJDy8CMM40oLFGkXT8Q=="],
|
||||||
|
|
||||||
|
|
@ -623,6 +643,10 @@
|
||||||
|
|
||||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
|
||||||
|
|
||||||
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
||||||
|
|
||||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
@ -647,6 +671,8 @@
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
||||||
|
|
||||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
|
|
||||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
@ -657,6 +683,8 @@
|
||||||
|
|
||||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||||
|
|
||||||
|
"dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
|
||||||
|
|
||||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||||
|
|
||||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||||
|
|
@ -671,7 +699,7 @@
|
||||||
|
|
||||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"encoding-japanese": ["encoding-japanese@2.2.0", "", {}, "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A=="],
|
"encoding-japanese": ["encoding-japanese@2.2.0", "", {}, "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A=="],
|
||||||
|
|
||||||
|
|
@ -705,6 +733,8 @@
|
||||||
|
|
||||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||||
|
|
||||||
|
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||||
|
|
||||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|
@ -721,6 +751,8 @@
|
||||||
|
|
||||||
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
@ -733,6 +765,8 @@
|
||||||
|
|
||||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"h3-js": ["h3-js@4.4.0", "", {}, "sha512-DvJh07MhGgY2KcC4OeZc8SSyA+ZXpdvoh6uCzGpoKvWtZxJB+g6VXXC1+eWYkaMIsLz7J/ErhOalHCpcs1KYog=="],
|
||||||
|
|
||||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
@ -773,6 +807,8 @@
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"isomorphic.js": ["isomorphic.js@0.2.5", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="],
|
||||||
|
|
||||||
"isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="],
|
"isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="],
|
||||||
|
|
||||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||||
|
|
@ -791,6 +827,8 @@
|
||||||
|
|
||||||
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
|
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
|
||||||
|
|
||||||
|
"lib0": ["lib0@0.2.117", "", { "dependencies": { "isomorphic.js": "^0.2.4" }, "bin": { "0serve": "bin/0serve.js", "0gentesthtml": "bin/gentesthtml.js", "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js" } }, "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw=="],
|
||||||
|
|
||||||
"libbase64": ["libbase64@1.3.0", "", {}, "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg=="],
|
"libbase64": ["libbase64@1.3.0", "", {}, "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg=="],
|
||||||
|
|
||||||
"libmime": ["libmime@5.3.7", "", { "dependencies": { "encoding-japanese": "2.2.0", "iconv-lite": "0.6.3", "libbase64": "1.3.0", "libqp": "2.1.1" } }, "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw=="],
|
"libmime": ["libmime@5.3.7", "", { "dependencies": { "encoding-japanese": "2.2.0", "iconv-lite": "0.6.3", "libbase64": "1.3.0", "libqp": "2.1.1" } }, "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw=="],
|
||||||
|
|
@ -803,6 +841,8 @@
|
||||||
|
|
||||||
"linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="],
|
"linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="],
|
||||||
|
|
||||||
|
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||||
|
|
||||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||||
|
|
||||||
"lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="],
|
"lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="],
|
||||||
|
|
@ -849,14 +889,22 @@
|
||||||
|
|
||||||
"ox": ["ox@0.12.4", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q=="],
|
"ox": ["ox@0.12.4", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q=="],
|
||||||
|
|
||||||
|
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||||
|
|
||||||
|
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||||
|
|
||||||
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
||||||
|
|
||||||
|
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
|
||||||
|
|
||||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||||
|
|
||||||
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||||
|
|
||||||
"parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="],
|
"parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="],
|
||||||
|
|
||||||
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||||
|
|
@ -877,6 +925,12 @@
|
||||||
|
|
||||||
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
|
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
|
||||||
|
|
||||||
|
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
|
||||||
|
|
||||||
|
"playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
|
||||||
|
|
||||||
|
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
"postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
|
"postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
|
||||||
|
|
@ -929,14 +983,20 @@
|
||||||
|
|
||||||
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
|
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
|
||||||
|
|
||||||
|
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
||||||
|
|
||||||
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
||||||
|
|
||||||
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
|
"require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
|
||||||
|
|
||||||
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||||
|
|
||||||
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
||||||
|
|
@ -955,6 +1015,8 @@
|
||||||
|
|
||||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
|
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
|
||||||
|
|
||||||
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
|
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
|
||||||
|
|
||||||
"sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
|
"sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
|
||||||
|
|
@ -979,13 +1041,13 @@
|
||||||
|
|
||||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
|
@ -1037,14 +1099,30 @@
|
||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||||
|
|
||||||
|
"y-indexeddb": ["y-indexeddb@9.0.12", "", { "dependencies": { "lib0": "^0.2.74" }, "peerDependencies": { "yjs": "^13.0.0" } }, "sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg=="],
|
||||||
|
|
||||||
|
"y-prosemirror": ["y-prosemirror@1.3.7", "", { "dependencies": { "lib0": "^0.2.109" }, "peerDependencies": { "prosemirror-model": "^1.7.1", "prosemirror-state": "^1.2.3", "prosemirror-view": "^1.9.10", "y-protocols": "^1.0.1", "yjs": "^13.5.38" } }, "sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg=="],
|
||||||
|
|
||||||
|
"y-protocols": ["y-protocols@1.0.7", "", { "dependencies": { "lib0": "^0.2.85" }, "peerDependencies": { "yjs": "^13.0.0" } }, "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
|
||||||
|
|
||||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
||||||
|
|
||||||
|
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
||||||
|
|
||||||
|
"yjs": ["yjs@13.6.30", "", { "dependencies": { "lib0": "^0.2.99" } }, "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ=="],
|
||||||
|
|
||||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@automerge/automerge/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
"@automerge/automerge/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||||
|
|
@ -1059,6 +1137,12 @@
|
||||||
|
|
||||||
"@encryptid/sdk/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
"@encryptid/sdk/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||||
|
|
||||||
"@openfort/openfort-node/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
"@openfort/openfort-node/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||||
|
|
||||||
"ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
@ -1089,17 +1173,7 @@
|
||||||
|
|
||||||
"mailparser/nodemailer": ["nodemailer@7.0.13", "", {}, "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw=="],
|
"mailparser/nodemailer": ["nodemailer@7.0.13", "", {}, "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw=="],
|
||||||
|
|
||||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
|
||||||
|
|
||||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
|
||||||
|
|
||||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
|
||||||
|
|
||||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||||
|
|
||||||
|
|
@ -1107,14 +1181,14 @@
|
||||||
|
|
||||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
|
|
||||||
"ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
|
"ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
|
||||||
|
|
||||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
|
||||||
|
|
||||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
|
||||||
|
|
||||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
|
||||||
|
|
||||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
|
|
||||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* TipTap mark extension for inline comments.
|
||||||
|
*
|
||||||
|
* Applies a highlight to selected text and associates it with a comment thread
|
||||||
|
* stored in Automerge. The mark position is synced via Yjs (as part of the doc content),
|
||||||
|
* while the thread data (messages, resolved state) lives in Automerge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Mark, mergeAttributes } from '@tiptap/core';
|
||||||
|
|
||||||
|
export const CommentMark = Mark.create({
|
||||||
|
name: 'comment',
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
threadId: { default: null },
|
||||||
|
resolved: { default: false },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'span[data-thread-id]',
|
||||||
|
getAttrs: (el) => {
|
||||||
|
const element = el as HTMLElement;
|
||||||
|
return {
|
||||||
|
threadId: element.getAttribute('data-thread-id'),
|
||||||
|
resolved: element.getAttribute('data-resolved') === 'true',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'span',
|
||||||
|
mergeAttributes(
|
||||||
|
{
|
||||||
|
class: `comment-highlight${HTMLAttributes.resolved ? ' resolved' : ''}`,
|
||||||
|
'data-thread-id': HTMLAttributes.threadId,
|
||||||
|
'data-resolved': HTMLAttributes.resolved ? 'true' : 'false',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,308 @@
|
||||||
|
/**
|
||||||
|
* <notes-comment-panel> — Right sidebar panel for viewing/managing inline comments.
|
||||||
|
*
|
||||||
|
* Shows threaded comments anchored to highlighted text in the editor.
|
||||||
|
* Comment thread data is stored in Automerge, while the highlight mark
|
||||||
|
* position is stored in Yjs (part of the document content).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Editor } from '@tiptap/core';
|
||||||
|
import type { DocumentId } from '../../../shared/local-first/document';
|
||||||
|
|
||||||
|
interface CommentMessage {
|
||||||
|
id: string;
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
text: string;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommentThread {
|
||||||
|
id: string;
|
||||||
|
anchor: string;
|
||||||
|
resolved: boolean;
|
||||||
|
messages: CommentMessage[];
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotebookDoc {
|
||||||
|
items: Record<string, {
|
||||||
|
comments?: Record<string, CommentThread>;
|
||||||
|
[key: string]: any;
|
||||||
|
}>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotesCommentPanel extends HTMLElement {
|
||||||
|
private shadow: ShadowRoot;
|
||||||
|
private _noteId: string | null = null;
|
||||||
|
private _doc: any = null;
|
||||||
|
private _subscribedDocId: string | null = null;
|
||||||
|
private _activeThreadId: string | null = null;
|
||||||
|
private _editor: Editor | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.shadow = this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
set noteId(v: string | null) { this._noteId = v; this.render(); }
|
||||||
|
set doc(v: any) { this._doc = v; this.render(); }
|
||||||
|
set subscribedDocId(v: string | null) { this._subscribedDocId = v; }
|
||||||
|
set activeThreadId(v: string | null) { this._activeThreadId = v; this.render(); }
|
||||||
|
set editor(v: Editor | null) { this._editor = v; }
|
||||||
|
|
||||||
|
private getThreads(): CommentThread[] {
|
||||||
|
if (!this._doc || !this._noteId) return [];
|
||||||
|
const item = this._doc.items?.[this._noteId];
|
||||||
|
if (!item?.comments) return [];
|
||||||
|
return Object.values(item.comments as Record<string, CommentThread>)
|
||||||
|
.sort((a, b) => a.createdAt - b.createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private render() {
|
||||||
|
const threads = this.getThreads();
|
||||||
|
if (threads.length === 0 && !this._activeThreadId) {
|
||||||
|
this.shadow.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esc = (s: string) => { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
|
||||||
|
const timeAgo = (ts: number) => {
|
||||||
|
const diff = Date.now() - ts;
|
||||||
|
if (diff < 60000) return 'just now';
|
||||||
|
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
||||||
|
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
||||||
|
return `${Math.floor(diff / 86400000)}d ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.shadow.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host { display: block; }
|
||||||
|
.panel { border-left: 1px solid var(--rs-border, #e5e7eb); padding: 12px; font-family: system-ui, sans-serif; font-size: 13px; max-height: 80vh; overflow-y: auto; }
|
||||||
|
.panel-title { font-weight: 600; font-size: 14px; margin-bottom: 12px; color: var(--rs-text-primary, #111); display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.thread { margin-bottom: 16px; padding: 10px; border-radius: 8px; background: var(--rs-bg-surface, #fff); border: 1px solid var(--rs-border-subtle, #f0f0f0); cursor: pointer; transition: border-color 0.15s; }
|
||||||
|
.thread:hover { border-color: var(--rs-border, #e5e7eb); }
|
||||||
|
.thread.active { border-color: var(--rs-primary, #3b82f6); }
|
||||||
|
.thread.resolved { opacity: 0.6; }
|
||||||
|
.thread-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
||||||
|
.thread-author { font-weight: 600; color: var(--rs-text-primary, #111); }
|
||||||
|
.thread-time { color: var(--rs-text-muted, #999); font-size: 11px; }
|
||||||
|
.thread-actions { display: flex; gap: 4px; }
|
||||||
|
.thread-action { padding: 2px 6px; border: none; background: none; color: var(--rs-text-secondary, #666); cursor: pointer; font-size: 11px; border-radius: 4px; }
|
||||||
|
.thread-action:hover { background: var(--rs-bg-hover, #f5f5f5); }
|
||||||
|
.message { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--rs-border-subtle, #f0f0f0); }
|
||||||
|
.message-author { font-weight: 500; font-size: 12px; color: var(--rs-text-secondary, #666); }
|
||||||
|
.message-text { margin-top: 2px; color: var(--rs-text-primary, #111); line-height: 1.4; }
|
||||||
|
.reply-form { margin-top: 8px; display: flex; gap: 6px; }
|
||||||
|
.reply-input { flex: 1; padding: 6px 8px; border: 1px solid var(--rs-input-border, #ddd); border-radius: 6px; font-size: 12px; background: var(--rs-input-bg, #fff); color: var(--rs-input-text, #111); }
|
||||||
|
.reply-input:focus { border-color: var(--rs-primary, #3b82f6); outline: none; }
|
||||||
|
.reply-btn { padding: 6px 10px; border: none; background: var(--rs-primary, #3b82f6); color: #fff; border-radius: 6px; font-size: 12px; cursor: pointer; font-weight: 500; }
|
||||||
|
.reply-btn:hover { opacity: 0.9; }
|
||||||
|
</style>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">
|
||||||
|
<span>Comments (${threads.filter(t => !t.resolved).length})</span>
|
||||||
|
</div>
|
||||||
|
${threads.map(thread => `
|
||||||
|
<div class="thread ${thread.id === this._activeThreadId ? 'active' : ''} ${thread.resolved ? 'resolved' : ''}" data-thread="${thread.id}">
|
||||||
|
<div class="thread-header">
|
||||||
|
<span class="thread-author">${esc(thread.messages[0]?.authorName || 'Anonymous')}</span>
|
||||||
|
<span class="thread-time">${timeAgo(thread.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
${thread.messages.map(msg => `
|
||||||
|
<div class="message">
|
||||||
|
<div class="message-author">${esc(msg.authorName)}</div>
|
||||||
|
<div class="message-text">${esc(msg.text)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
${thread.messages.length === 0 ? '<div class="message"><div class="message-text" style="color: var(--rs-text-muted, #999)">Click to add a comment...</div></div>' : ''}
|
||||||
|
<div class="reply-form">
|
||||||
|
<input class="reply-input" placeholder="Reply..." data-thread="${thread.id}">
|
||||||
|
<button class="reply-btn" data-reply="${thread.id}">Reply</button>
|
||||||
|
</div>
|
||||||
|
<div class="thread-actions">
|
||||||
|
<button class="thread-action" data-resolve="${thread.id}">${thread.resolved ? 'Re-open' : 'Resolve'}</button>
|
||||||
|
<button class="thread-action" data-delete="${thread.id}">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.wireEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private wireEvents() {
|
||||||
|
// Click thread to scroll editor to it
|
||||||
|
this.shadow.querySelectorAll('.thread[data-thread]').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
const threadId = (el as HTMLElement).dataset.thread;
|
||||||
|
if (!threadId || !this._editor) return;
|
||||||
|
this._activeThreadId = threadId;
|
||||||
|
// Find the comment mark in the editor and scroll to it
|
||||||
|
this._editor.state.doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText) return;
|
||||||
|
const mark = node.marks.find(m => m.type.name === 'comment' && m.attrs.threadId === threadId);
|
||||||
|
if (mark) {
|
||||||
|
this._editor!.commands.setTextSelection(pos);
|
||||||
|
this._editor!.commands.scrollIntoView();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reply
|
||||||
|
this.shadow.querySelectorAll('[data-reply]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const threadId = (btn as HTMLElement).dataset.reply;
|
||||||
|
if (!threadId) return;
|
||||||
|
const input = this.shadow.querySelector(`input[data-thread="${threadId}"]`) as HTMLInputElement;
|
||||||
|
const text = input?.value?.trim();
|
||||||
|
if (!text) return;
|
||||||
|
this.addReply(threadId, text);
|
||||||
|
input.value = '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reply on Enter
|
||||||
|
this.shadow.querySelectorAll('.reply-input').forEach(input => {
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if ((e as KeyboardEvent).key === 'Enter') {
|
||||||
|
e.stopPropagation();
|
||||||
|
const threadId = (input as HTMLInputElement).dataset.thread;
|
||||||
|
const text = (input as HTMLInputElement).value.trim();
|
||||||
|
if (threadId && text) {
|
||||||
|
this.addReply(threadId, text);
|
||||||
|
(input as HTMLInputElement).value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
input.addEventListener('click', (e) => e.stopPropagation());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resolve / re-open
|
||||||
|
this.shadow.querySelectorAll('[data-resolve]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const threadId = (btn as HTMLElement).dataset.resolve;
|
||||||
|
if (threadId) this.toggleResolve(threadId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
this.shadow.querySelectorAll('[data-delete]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const threadId = (btn as HTMLElement).dataset.delete;
|
||||||
|
if (threadId) this.deleteThread(threadId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private addReply(threadId: string, text: string) {
|
||||||
|
if (!this._noteId || !this._subscribedDocId) return;
|
||||||
|
const runtime = (window as any).__rspaceOfflineRuntime;
|
||||||
|
if (!runtime?.isInitialized) return;
|
||||||
|
|
||||||
|
let authorName = 'Anonymous';
|
||||||
|
let authorId = 'anon';
|
||||||
|
try {
|
||||||
|
const sess = JSON.parse(localStorage.getItem('encryptid_session') || '{}');
|
||||||
|
authorName = sess?.username || sess?.displayName || 'Anonymous';
|
||||||
|
authorId = sess?.userId || sess?.sub || 'anon';
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const noteId = this._noteId;
|
||||||
|
runtime.change(this._subscribedDocId as DocumentId, 'Add comment reply', (d: NotebookDoc) => {
|
||||||
|
const item = d.items[noteId];
|
||||||
|
if (!item?.comments?.[threadId]) return;
|
||||||
|
const thread = item.comments[threadId] as any;
|
||||||
|
if (!thread.messages) thread.messages = [];
|
||||||
|
thread.messages.push({
|
||||||
|
id: `m_${Date.now()}`,
|
||||||
|
authorId,
|
||||||
|
authorName,
|
||||||
|
text,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._doc = runtime.get(this._subscribedDocId as DocumentId);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleResolve(threadId: string) {
|
||||||
|
if (!this._noteId || !this._subscribedDocId) return;
|
||||||
|
const runtime = (window as any).__rspaceOfflineRuntime;
|
||||||
|
if (!runtime?.isInitialized) return;
|
||||||
|
|
||||||
|
const noteId = this._noteId;
|
||||||
|
runtime.change(this._subscribedDocId as DocumentId, 'Toggle comment resolve', (d: NotebookDoc) => {
|
||||||
|
const item = d.items[noteId];
|
||||||
|
if (!item?.comments?.[threadId]) return;
|
||||||
|
(item.comments[threadId] as any).resolved = !(item.comments[threadId] as any).resolved;
|
||||||
|
});
|
||||||
|
this._doc = runtime.get(this._subscribedDocId as DocumentId);
|
||||||
|
|
||||||
|
// Also update the mark in the editor
|
||||||
|
if (this._editor) {
|
||||||
|
const thread = this._doc?.items?.[this._noteId]?.comments?.[threadId];
|
||||||
|
if (thread) {
|
||||||
|
this._editor.state.doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText) return;
|
||||||
|
const mark = node.marks.find(m => m.type.name === 'comment' && m.attrs.threadId === threadId);
|
||||||
|
if (mark) {
|
||||||
|
const { tr } = this._editor!.state;
|
||||||
|
tr.removeMark(pos, pos + node.nodeSize, mark);
|
||||||
|
tr.addMark(pos, pos + node.nodeSize,
|
||||||
|
this._editor!.schema.marks.comment.create({ threadId, resolved: thread.resolved })
|
||||||
|
);
|
||||||
|
this._editor!.view.dispatch(tr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteThread(threadId: string) {
|
||||||
|
if (!this._noteId || !this._subscribedDocId) return;
|
||||||
|
const runtime = (window as any).__rspaceOfflineRuntime;
|
||||||
|
if (!runtime?.isInitialized) return;
|
||||||
|
|
||||||
|
const noteId = this._noteId;
|
||||||
|
runtime.change(this._subscribedDocId as DocumentId, 'Delete comment thread', (d: NotebookDoc) => {
|
||||||
|
const item = d.items[noteId];
|
||||||
|
if (item?.comments?.[threadId]) {
|
||||||
|
delete (item.comments as any)[threadId];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._doc = runtime.get(this._subscribedDocId as DocumentId);
|
||||||
|
|
||||||
|
// Remove the comment mark from the editor
|
||||||
|
if (this._editor) {
|
||||||
|
const { state } = this._editor;
|
||||||
|
const { tr } = state;
|
||||||
|
state.doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText) return;
|
||||||
|
const mark = node.marks.find(m => m.type.name === 'comment' && m.attrs.threadId === threadId);
|
||||||
|
if (mark) {
|
||||||
|
tr.removeMark(pos, pos + node.nodeSize, mark);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (tr.docChanged) {
|
||||||
|
this._editor.view.dispatch(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._activeThreadId === threadId) this._activeThreadId = null;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('notes-comment-panel', NotesCommentPanel);
|
||||||
|
|
@ -30,6 +30,14 @@ import type { ImportExportDialog } from './import-export-dialog';
|
||||||
import { SpeechDictation } from '../../../lib/speech-dictation';
|
import { SpeechDictation } from '../../../lib/speech-dictation';
|
||||||
import { TourEngine } from '../../../shared/tour-engine';
|
import { TourEngine } from '../../../shared/tour-engine';
|
||||||
import { ViewHistory } from '../../../shared/view-history.js';
|
import { ViewHistory } from '../../../shared/view-history.js';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import { IndexeddbPersistence } from 'y-indexeddb';
|
||||||
|
import { ySyncPlugin, yUndoPlugin, yCursorPlugin } from '@tiptap/y-tiptap';
|
||||||
|
import { RSpaceYjsProvider } from '../yjs-ws-provider';
|
||||||
|
import { CommentMark } from './comment-mark';
|
||||||
|
import { SuggestionInsertMark, SuggestionDeleteMark } from './suggestion-marks';
|
||||||
|
import { createSuggestionPlugin } from './suggestion-plugin';
|
||||||
|
import './comment-panel';
|
||||||
|
|
||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
|
|
||||||
|
|
@ -149,6 +157,15 @@ class FolkNotesApp extends HTMLElement {
|
||||||
private editorUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
private editorUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
private dictation: SpeechDictation | null = null;
|
private dictation: SpeechDictation | null = null;
|
||||||
|
|
||||||
|
// Yjs collaboration state
|
||||||
|
private ydoc: Y.Doc | null = null;
|
||||||
|
private yjsProvider: RSpaceYjsProvider | null = null;
|
||||||
|
private yIndexedDb: IndexeddbPersistence | null = null;
|
||||||
|
private yjsPlainTextTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
// Comments/suggestions state
|
||||||
|
private suggestingMode = false;
|
||||||
|
|
||||||
// Audio recording (AUDIO note view)
|
// Audio recording (AUDIO note view)
|
||||||
private audioRecorder: MediaRecorder | null = null;
|
private audioRecorder: MediaRecorder | null = null;
|
||||||
private audioSegments: { id: string; text: string; timestamp: number; isFinal: boolean }[] = [];
|
private audioSegments: { id: string; text: string; timestamp: number; isFinal: boolean }[] = [];
|
||||||
|
|
@ -620,7 +637,9 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
updated_at: noteItem.updatedAt ? new Date(noteItem.updatedAt).toISOString() : new Date().toISOString(),
|
updated_at: noteItem.updatedAt ? new Date(noteItem.updatedAt).toISOString() : new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update editor content if different (remote change)
|
// Skip content replacement when Yjs is active — Yjs handles content sync
|
||||||
|
if (!this.ydoc) {
|
||||||
|
// Legacy mode: update editor content if different (remote change)
|
||||||
const remoteContent = noteItem.content || "";
|
const remoteContent = noteItem.content || "";
|
||||||
const currentContent = noteItem.contentFormat === 'tiptap-json'
|
const currentContent = noteItem.contentFormat === 'tiptap-json'
|
||||||
? JSON.stringify(this.editor.getJSON())
|
? JSON.stringify(this.editor.getJSON())
|
||||||
|
|
@ -642,6 +661,7 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
this.isRemoteUpdate = false;
|
this.isRemoteUpdate = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update title input if it exists
|
// Update title input if it exists
|
||||||
const titleInput = this.shadow.querySelector('#note-title-input') as HTMLInputElement;
|
const titleInput = this.shadow.querySelector('#note-title-input') as HTMLInputElement;
|
||||||
|
|
@ -1010,6 +1030,8 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
}
|
}
|
||||||
|
|
||||||
private mountTiptapEditor(note: Note, isEditable: boolean, isDemo: boolean) {
|
private mountTiptapEditor(note: Note, isEditable: boolean, isDemo: boolean) {
|
||||||
|
const useYjs = !isDemo && isEditable;
|
||||||
|
|
||||||
this.contentZone.innerHTML = `
|
this.contentZone.innerHTML = `
|
||||||
<div class="editor-wrapper">
|
<div class="editor-wrapper">
|
||||||
<input class="editable-title" id="note-title-input" value="${this.esc(note.title)}" placeholder="Note title...">
|
<input class="editable-title" id="note-title-input" value="${this.esc(note.title)}" placeholder="Note title...">
|
||||||
|
|
@ -1021,6 +1043,134 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
const container = this.shadow.getElementById('tiptap-container');
|
const container = this.shadow.getElementById('tiptap-container');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
|
if (useYjs) {
|
||||||
|
this.mountTiptapWithYjs(note, container);
|
||||||
|
} else {
|
||||||
|
this.mountTiptapLegacy(note, isEditable, isDemo, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor!.registerPlugin(createSlashCommandPlugin(this.editor!, this.shadow));
|
||||||
|
|
||||||
|
container.addEventListener('slash-insert-image', () => {
|
||||||
|
if (!this.editor) return;
|
||||||
|
const { from } = this.editor.view.state.selection;
|
||||||
|
const coords = this.editor.view.coordsAtPos(from);
|
||||||
|
const rect = new DOMRect(coords.left, coords.top, 0, coords.bottom - coords.top);
|
||||||
|
this.showUrlPopover(rect, 'Enter image URL...').then(url => {
|
||||||
|
if (url) this.editor!.chain().focus().setImage({ src: url }).run();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('slash-create-typed-note', ((e: CustomEvent) => {
|
||||||
|
const { type } = e.detail || {};
|
||||||
|
if (type && this.selectedNotebook) {
|
||||||
|
this.createNoteViaSync({ type });
|
||||||
|
}
|
||||||
|
}) as EventListener);
|
||||||
|
|
||||||
|
this.wireTitleInput(note, isEditable, isDemo);
|
||||||
|
this.attachToolbarListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mount TipTap with Yjs collaboration (real-time co-editing). */
|
||||||
|
private mountTiptapWithYjs(note: Note, container: HTMLElement) {
|
||||||
|
const runtime = (window as any).__rspaceOfflineRuntime;
|
||||||
|
const spaceSlug = runtime?.space || this.space;
|
||||||
|
const roomName = `rnotes:${spaceSlug}:${note.id}`;
|
||||||
|
|
||||||
|
// Create Y.Doc
|
||||||
|
this.ydoc = new Y.Doc();
|
||||||
|
const fragment = this.ydoc.getXmlFragment('content');
|
||||||
|
|
||||||
|
// IndexedDB persistence for offline
|
||||||
|
this.yIndexedDb = new IndexeddbPersistence(roomName, this.ydoc);
|
||||||
|
|
||||||
|
// Connect Yjs provider over rSpace WebSocket
|
||||||
|
if (runtime?.isInitialized) {
|
||||||
|
this.yjsProvider = new RSpaceYjsProvider(note.id, this.ydoc, runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content migration: if Y.Doc fragment is empty and Automerge has content
|
||||||
|
this.yIndexedDb.on('synced', () => {
|
||||||
|
if (fragment.length === 0 && note.content) {
|
||||||
|
// Migrate existing content into Yjs by creating a temp editor, setting content, then destroying
|
||||||
|
let content: any = '';
|
||||||
|
if (note.content_format === 'tiptap-json') {
|
||||||
|
try { content = JSON.parse(note.content); } catch { content = note.content; }
|
||||||
|
} else {
|
||||||
|
content = note.content;
|
||||||
|
}
|
||||||
|
if (this.editor && content) {
|
||||||
|
this.editor.commands.setContent(content, { emitUpdate: false });
|
||||||
|
}
|
||||||
|
// Mark as collab-enabled in Automerge
|
||||||
|
if (this.doc?.items?.[note.id]) {
|
||||||
|
this.updateNoteField(note.id, 'collabEnabled', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create editor with Yjs sync/undo plugins registered directly
|
||||||
|
this.editor = new Editor({
|
||||||
|
element: container,
|
||||||
|
editable: true,
|
||||||
|
extensions: [
|
||||||
|
StarterKit.configure({
|
||||||
|
codeBlock: false,
|
||||||
|
heading: { levels: [1, 2, 3, 4] },
|
||||||
|
undoRedo: false, // Yjs has its own undo/redo
|
||||||
|
}),
|
||||||
|
Link.configure({ openOnClick: false }),
|
||||||
|
Image, TaskList, TaskItem.configure({ nested: true }),
|
||||||
|
Placeholder.configure({ placeholder: 'Start writing, or type / for commands...' }),
|
||||||
|
CodeBlockLowlight.configure({ lowlight }), Typography, Underline,
|
||||||
|
CommentMark,
|
||||||
|
SuggestionInsertMark,
|
||||||
|
SuggestionDeleteMark,
|
||||||
|
],
|
||||||
|
onSelectionUpdate: () => { this.updateToolbarState(); },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Yjs sync and undo plugins
|
||||||
|
this.editor.registerPlugin(ySyncPlugin(fragment));
|
||||||
|
this.editor.registerPlugin(yUndoPlugin());
|
||||||
|
|
||||||
|
// Register suggestion plugin for track-changes mode
|
||||||
|
const suggestionPlugin = createSuggestionPlugin(
|
||||||
|
() => this.suggestingMode,
|
||||||
|
() => {
|
||||||
|
const s = this.getSessionInfo();
|
||||||
|
return { authorId: s.userId, authorName: s.username };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.editor.registerPlugin(suggestionPlugin);
|
||||||
|
|
||||||
|
// Register cursor presence plugin
|
||||||
|
if (this.yjsProvider) {
|
||||||
|
const cursorPlugin = yCursorPlugin(
|
||||||
|
this.yjsProvider.awareness,
|
||||||
|
{ cursorBuilder: this.buildCollabCursor.bind(this) }
|
||||||
|
);
|
||||||
|
this.editor.registerPlugin(cursorPlugin);
|
||||||
|
|
||||||
|
// Set local user state on awareness
|
||||||
|
const session = this.getSessionInfo();
|
||||||
|
this.yjsProvider.awareness.setLocalStateField('user', {
|
||||||
|
name: session.username || 'Anonymous',
|
||||||
|
color: this.userColor(session.userId || 'anon'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodic plaintext sync to Automerge (for search indexing)
|
||||||
|
this.yjsPlainTextTimer = setInterval(() => {
|
||||||
|
if (!this.editor || !this.editorNoteId) return;
|
||||||
|
const plain = this.editor.getText();
|
||||||
|
this.updateNoteField(this.editorNoteId, 'contentPlain', plain);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mount TipTap without Yjs (demo mode / read-only). */
|
||||||
|
private mountTiptapLegacy(note: Note, isEditable: boolean, isDemo: boolean, container: HTMLElement) {
|
||||||
let content: any = '';
|
let content: any = '';
|
||||||
if (note.content) {
|
if (note.content) {
|
||||||
if (note.content_format === 'tiptap-json') {
|
if (note.content_format === 'tiptap-json') {
|
||||||
|
|
@ -1062,29 +1212,44 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
},
|
},
|
||||||
onSelectionUpdate: () => { this.updateToolbarState(); },
|
onSelectionUpdate: () => { this.updateToolbarState(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.editor.registerPlugin(createSlashCommandPlugin(this.editor, this.shadow));
|
|
||||||
|
|
||||||
container.addEventListener('slash-insert-image', () => {
|
|
||||||
if (!this.editor) return;
|
|
||||||
const { from } = this.editor.view.state.selection;
|
|
||||||
const coords = this.editor.view.coordsAtPos(from);
|
|
||||||
const rect = new DOMRect(coords.left, coords.top, 0, coords.bottom - coords.top);
|
|
||||||
this.showUrlPopover(rect, 'Enter image URL...').then(url => {
|
|
||||||
if (url) this.editor!.chain().focus().setImage({ src: url }).run();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for slash-create-typed-note events
|
|
||||||
container.addEventListener('slash-create-typed-note', ((e: CustomEvent) => {
|
|
||||||
const { type } = e.detail || {};
|
|
||||||
if (type && this.selectedNotebook) {
|
|
||||||
this.createNoteViaSync({ type });
|
|
||||||
}
|
}
|
||||||
}) as EventListener);
|
|
||||||
|
|
||||||
this.wireTitleInput(note, isEditable, isDemo);
|
/** Build a DOM element for remote collaborator cursor. */
|
||||||
this.attachToolbarListeners();
|
private buildCollabCursor(user: { name: string; color: string }) {
|
||||||
|
const cursor = document.createElement('span');
|
||||||
|
cursor.className = 'collab-cursor';
|
||||||
|
cursor.style.borderLeftColor = user.color;
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'collab-cursor-label';
|
||||||
|
label.style.backgroundColor = user.color;
|
||||||
|
label.textContent = user.name;
|
||||||
|
cursor.appendChild(label);
|
||||||
|
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Derive a stable color from a user ID string. */
|
||||||
|
private userColor(id: string): string {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < id.length; i++) {
|
||||||
|
hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
const hue = Math.abs(hash) % 360;
|
||||||
|
return `hsl(${hue}, 70%, 50%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get session info for cursor display. */
|
||||||
|
private getSessionInfo(): { username: string; userId: string } {
|
||||||
|
try {
|
||||||
|
const sess = JSON.parse(localStorage.getItem('encryptid_session') || '{}');
|
||||||
|
return {
|
||||||
|
username: sess?.username || sess?.displayName || 'Anonymous',
|
||||||
|
userId: sess?.userId || sess?.sub || 'anon',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { username: 'Anonymous', userId: 'anon' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private mountCodeEditor(note: Note, isEditable: boolean, isDemo: boolean) {
|
private mountCodeEditor(note: Note, isEditable: boolean, isDemo: boolean) {
|
||||||
|
|
@ -1533,6 +1698,22 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
clearTimeout(this.editorUpdateTimer);
|
clearTimeout(this.editorUpdateTimer);
|
||||||
this.editorUpdateTimer = null;
|
this.editorUpdateTimer = null;
|
||||||
}
|
}
|
||||||
|
if (this.yjsPlainTextTimer) {
|
||||||
|
clearInterval(this.yjsPlainTextTimer);
|
||||||
|
this.yjsPlainTextTimer = null;
|
||||||
|
}
|
||||||
|
if (this.yjsProvider) {
|
||||||
|
this.yjsProvider.destroy();
|
||||||
|
this.yjsProvider = null;
|
||||||
|
}
|
||||||
|
if (this.yIndexedDb) {
|
||||||
|
this.yIndexedDb.destroy();
|
||||||
|
this.yIndexedDb = null;
|
||||||
|
}
|
||||||
|
if (this.ydoc) {
|
||||||
|
this.ydoc.destroy();
|
||||||
|
this.ydoc = null;
|
||||||
|
}
|
||||||
if (this.dictation) {
|
if (this.dictation) {
|
||||||
this.dictation.destroy();
|
this.dictation.destroy();
|
||||||
this.dictation = null;
|
this.dictation = null;
|
||||||
|
|
@ -1549,6 +1730,7 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
this.audioRecorder.stop();
|
this.audioRecorder.stop();
|
||||||
}
|
}
|
||||||
this.audioRecorder = null;
|
this.audioRecorder = null;
|
||||||
|
this.suggestingMode = false;
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
this.editor.destroy();
|
this.editor.destroy();
|
||||||
this.editor = null;
|
this.editor = null;
|
||||||
|
|
@ -1609,6 +1791,16 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
${btn('summarize', 'Summarize Note')}
|
${btn('summarize', 'Summarize Note')}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="toolbar-sep"></div>
|
||||||
|
<div class="toolbar-group collab-tools">
|
||||||
|
<button class="toolbar-btn" data-cmd="comment" title="Add Comment">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 2.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5l-3 3v-11a1 1 0 0 1 1-1z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="toolbar-btn suggest-toggle" data-cmd="toggleSuggesting" title="Toggle Suggesting Mode">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M11.5 1.5l3 3-9 9H2.5v-3z"/><path d="M9.5 3.5l3 3"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="collab-peers" id="collab-peers" title="Connected peers"></span>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1714,6 +1906,8 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
case 'redo': this.editor.chain().focus().redo().run(); break;
|
case 'redo': this.editor.chain().focus().redo().run(); break;
|
||||||
case 'mic': this.toggleDictation(btn); break;
|
case 'mic': this.toggleDictation(btn); break;
|
||||||
case 'summarize': this.summarizeNote(btn); break;
|
case 'summarize': this.summarizeNote(btn); break;
|
||||||
|
case 'comment': this.addComment(); break;
|
||||||
|
case 'toggleSuggesting': this.toggleSuggestingMode(btn); break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1732,6 +1926,71 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Add a comment on the current selection. */
|
||||||
|
private addComment() {
|
||||||
|
if (!this.editor) return;
|
||||||
|
const { from, to, empty } = this.editor.state.selection;
|
||||||
|
if (empty) return; // Need selected text
|
||||||
|
|
||||||
|
const threadId = `c_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const session = this.getSessionInfo();
|
||||||
|
|
||||||
|
// Apply comment mark to the selection
|
||||||
|
this.editor.chain().focus()
|
||||||
|
.setMark('comment', { threadId, resolved: false })
|
||||||
|
.run();
|
||||||
|
|
||||||
|
// Create thread in Automerge
|
||||||
|
const noteId = this.editorNoteId;
|
||||||
|
if (noteId && this.doc?.items?.[noteId] && this.subscribedDocId) {
|
||||||
|
const runtime = (window as any).__rspaceOfflineRuntime;
|
||||||
|
if (runtime?.isInitialized) {
|
||||||
|
runtime.change(this.subscribedDocId as DocumentId, 'Add comment thread', (d: NotebookDoc) => {
|
||||||
|
const item = d.items[noteId] as any;
|
||||||
|
if (!item.comments) item.comments = {};
|
||||||
|
item.comments[threadId] = {
|
||||||
|
id: threadId,
|
||||||
|
anchor: `${from}-${to}`,
|
||||||
|
resolved: false,
|
||||||
|
messages: [],
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.doc = runtime.get(this.subscribedDocId as DocumentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open comment panel
|
||||||
|
this.showCommentPanel(threadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggle between editing and suggesting modes. */
|
||||||
|
private toggleSuggestingMode(btn: HTMLElement) {
|
||||||
|
this.suggestingMode = !this.suggestingMode;
|
||||||
|
btn.classList.toggle('active', this.suggestingMode);
|
||||||
|
btn.title = this.suggestingMode ? 'Switch to Editing Mode' : 'Switch to Suggesting Mode';
|
||||||
|
|
||||||
|
// Update editor's editable state to reflect mode
|
||||||
|
const container = this.shadow.getElementById('tiptap-container');
|
||||||
|
if (container) {
|
||||||
|
container.classList.toggle('suggesting-mode', this.suggestingMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show comment panel for a specific thread. */
|
||||||
|
private showCommentPanel(threadId?: string) {
|
||||||
|
let panel = this.shadow.querySelector('notes-comment-panel') as any;
|
||||||
|
if (!panel) {
|
||||||
|
panel = document.createElement('notes-comment-panel');
|
||||||
|
this.metaZone.appendChild(panel);
|
||||||
|
}
|
||||||
|
panel.noteId = this.editorNoteId;
|
||||||
|
panel.doc = this.doc;
|
||||||
|
panel.subscribedDocId = this.subscribedDocId;
|
||||||
|
panel.activeThreadId = threadId || null;
|
||||||
|
panel.editor = this.editor;
|
||||||
|
}
|
||||||
|
|
||||||
private toggleDictation(btn: HTMLElement) {
|
private toggleDictation(btn: HTMLElement) {
|
||||||
if (this.dictation?.isRecording) {
|
if (this.dictation?.isRecording) {
|
||||||
this.dictation.stop();
|
this.dictation.stop();
|
||||||
|
|
@ -1814,6 +2073,58 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
else if (this.editor.isActive('heading', { level: 4 })) headingSelect.value = '4';
|
else if (this.editor.isActive('heading', { level: 4 })) headingSelect.value = '4';
|
||||||
else headingSelect.value = 'paragraph';
|
else headingSelect.value = 'paragraph';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update comment button state (active when text is selected)
|
||||||
|
const commentBtn = toolbar.querySelector('[data-cmd="comment"]') as HTMLElement;
|
||||||
|
if (commentBtn) {
|
||||||
|
commentBtn.classList.toggle('active', this.editor.isActive('comment'));
|
||||||
|
const { empty } = this.editor.state.selection;
|
||||||
|
commentBtn.style.opacity = empty ? '0.4' : '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update suggesting mode toggle
|
||||||
|
const suggestBtn = toolbar.querySelector('[data-cmd="toggleSuggesting"]') as HTMLElement;
|
||||||
|
if (suggestBtn) {
|
||||||
|
suggestBtn.classList.toggle('active', this.suggestingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update peers count
|
||||||
|
this.updatePeersIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePeersIndicator() {
|
||||||
|
const peersEl = this.shadow.getElementById('collab-peers');
|
||||||
|
if (!peersEl || !this.yjsProvider) {
|
||||||
|
if (peersEl) peersEl.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const states = this.yjsProvider.awareness.getStates();
|
||||||
|
const peerCount = states.size - 1; // Exclude self
|
||||||
|
if (peerCount > 0) {
|
||||||
|
peersEl.style.display = 'inline-flex';
|
||||||
|
peersEl.innerHTML = '';
|
||||||
|
let shown = 0;
|
||||||
|
for (const [clientId, state] of states) {
|
||||||
|
if (clientId === this.ydoc?.clientID) continue;
|
||||||
|
if (shown >= 3) break;
|
||||||
|
const user = state.user || { name: '?', color: '#888' };
|
||||||
|
const dot = document.createElement('span');
|
||||||
|
dot.className = 'peer-dot';
|
||||||
|
dot.style.backgroundColor = user.color;
|
||||||
|
dot.title = user.name;
|
||||||
|
peersEl.appendChild(dot);
|
||||||
|
shown++;
|
||||||
|
}
|
||||||
|
if (peerCount > 3) {
|
||||||
|
const more = document.createElement('span');
|
||||||
|
more.className = 'peer-more';
|
||||||
|
more.textContent = `+${peerCount - 3}`;
|
||||||
|
peersEl.appendChild(more);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
peersEl.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ──
|
// ── Helpers ──
|
||||||
|
|
@ -2769,6 +3080,95 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
.tiptap-container .tiptap .hljs-attr { color: #ffcb6b; }
|
.tiptap-container .tiptap .hljs-attr { color: #ffcb6b; }
|
||||||
.tiptap-container .tiptap .hljs-tag { color: #f07178; }
|
.tiptap-container .tiptap .hljs-tag { color: #f07178; }
|
||||||
.tiptap-container .tiptap .hljs-type { color: #ffcb6b; }
|
.tiptap-container .tiptap .hljs-type { color: #ffcb6b; }
|
||||||
|
|
||||||
|
/* ── Collaboration: Remote Cursors ── */
|
||||||
|
.collab-cursor {
|
||||||
|
position: relative;
|
||||||
|
border-left: 2px solid;
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-right: -1px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.collab-cursor-label {
|
||||||
|
position: absolute;
|
||||||
|
top: -1.4em;
|
||||||
|
left: -1px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px 3px 3px 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
/* y-prosemirror remote selection highlight */
|
||||||
|
.ProseMirror .yRemoteSelection {
|
||||||
|
background-color: var(--user-color, rgba(99, 102, 241, 0.2));
|
||||||
|
}
|
||||||
|
.ProseMirror .yRemoteSelectionHead {
|
||||||
|
position: absolute;
|
||||||
|
border-left: 2px solid var(--user-color, #6366f1);
|
||||||
|
height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Collaboration: Peers Indicator ── */
|
||||||
|
.collab-tools { display: flex; align-items: center; gap: 4px; }
|
||||||
|
.collab-peers {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
.peer-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid var(--rs-bg-surface, #fff);
|
||||||
|
}
|
||||||
|
.peer-more {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--rs-text-muted);
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Collaboration: Comment Highlights ── */
|
||||||
|
.tiptap-container .tiptap .comment-highlight {
|
||||||
|
background: rgba(250, 204, 21, 0.25);
|
||||||
|
border-bottom: 2px solid rgba(250, 204, 21, 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.tiptap-container .tiptap .comment-highlight:hover {
|
||||||
|
background: rgba(250, 204, 21, 0.4);
|
||||||
|
}
|
||||||
|
.tiptap-container .tiptap .comment-highlight.resolved {
|
||||||
|
background: rgba(250, 204, 21, 0.08);
|
||||||
|
border-bottom-color: rgba(250, 204, 21, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Collaboration: Suggestions ── */
|
||||||
|
.tiptap-container .tiptap .suggestion-insert {
|
||||||
|
color: #16a34a;
|
||||||
|
background: rgba(22, 163, 74, 0.08);
|
||||||
|
border-bottom: 1px dashed #16a34a;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.tiptap-container .tiptap .suggestion-delete {
|
||||||
|
color: #dc2626;
|
||||||
|
background: rgba(220, 38, 38, 0.08);
|
||||||
|
text-decoration: line-through;
|
||||||
|
text-decoration-color: #dc2626;
|
||||||
|
}
|
||||||
|
.tiptap-container.suggesting-mode {
|
||||||
|
border-left: 3px solid #f59e0b;
|
||||||
|
}
|
||||||
|
.suggest-toggle.active {
|
||||||
|
background: #f59e0b !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
/**
|
||||||
|
* TipTap mark extensions for track-changes suggestions.
|
||||||
|
*
|
||||||
|
* SuggestionInsert: wraps text that was inserted in suggesting mode (green underline).
|
||||||
|
* SuggestionDelete: wraps text that was marked for deletion in suggesting mode (red strikethrough).
|
||||||
|
*
|
||||||
|
* Both marks are stored in the Yjs document and sync in real-time.
|
||||||
|
* Accept/reject logic is handled by the suggestion-plugin.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Mark, mergeAttributes } from '@tiptap/core';
|
||||||
|
|
||||||
|
export const SuggestionInsertMark = Mark.create({
|
||||||
|
name: 'suggestionInsert',
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
suggestionId: { default: null },
|
||||||
|
authorId: { default: null },
|
||||||
|
authorName: { default: null },
|
||||||
|
createdAt: { default: null },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'span[data-suggestion-insert]',
|
||||||
|
getAttrs: (el) => {
|
||||||
|
const element = el as HTMLElement;
|
||||||
|
return {
|
||||||
|
suggestionId: element.getAttribute('data-suggestion-id'),
|
||||||
|
authorId: element.getAttribute('data-author-id'),
|
||||||
|
authorName: element.getAttribute('data-author-name'),
|
||||||
|
createdAt: Number(element.getAttribute('data-created-at')) || null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'span',
|
||||||
|
mergeAttributes({
|
||||||
|
class: 'suggestion-insert',
|
||||||
|
'data-suggestion-insert': '',
|
||||||
|
'data-suggestion-id': HTMLAttributes.suggestionId,
|
||||||
|
'data-author-id': HTMLAttributes.authorId,
|
||||||
|
'data-author-name': HTMLAttributes.authorName,
|
||||||
|
'data-created-at': HTMLAttributes.createdAt,
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SuggestionDeleteMark = Mark.create({
|
||||||
|
name: 'suggestionDelete',
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
suggestionId: { default: null },
|
||||||
|
authorId: { default: null },
|
||||||
|
authorName: { default: null },
|
||||||
|
createdAt: { default: null },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'span[data-suggestion-delete]',
|
||||||
|
getAttrs: (el) => {
|
||||||
|
const element = el as HTMLElement;
|
||||||
|
return {
|
||||||
|
suggestionId: element.getAttribute('data-suggestion-id'),
|
||||||
|
authorId: element.getAttribute('data-author-id'),
|
||||||
|
authorName: element.getAttribute('data-author-name'),
|
||||||
|
createdAt: Number(element.getAttribute('data-created-at')) || null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'span',
|
||||||
|
mergeAttributes({
|
||||||
|
class: 'suggestion-delete',
|
||||||
|
'data-suggestion-delete': '',
|
||||||
|
'data-suggestion-id': HTMLAttributes.suggestionId,
|
||||||
|
'data-author-id': HTMLAttributes.authorId,
|
||||||
|
'data-author-name': HTMLAttributes.authorName,
|
||||||
|
'data-created-at': HTMLAttributes.createdAt,
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
/**
|
||||||
|
* ProseMirror plugin that intercepts transactions in "suggesting" mode
|
||||||
|
* and converts them into track-changes marks instead of direct edits.
|
||||||
|
*
|
||||||
|
* In suggesting mode:
|
||||||
|
* - Insertions → wraps inserted text with `suggestionInsert` mark
|
||||||
|
* - Deletions → converts to `suggestionDelete` mark instead of deleting
|
||||||
|
*
|
||||||
|
* Accept/reject operations remove marks (and optionally the text).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Plugin, PluginKey, type Transaction } from '@tiptap/pm/state';
|
||||||
|
import type { Editor } from '@tiptap/core';
|
||||||
|
|
||||||
|
const pluginKey = new PluginKey('suggestion-plugin');
|
||||||
|
|
||||||
|
interface SuggestionPluginState {
|
||||||
|
suggesting: boolean;
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the suggestion mode ProseMirror plugin.
|
||||||
|
* @param getSuggesting - callback that returns current suggesting mode state
|
||||||
|
* @param getAuthor - callback that returns { authorId, authorName }
|
||||||
|
*/
|
||||||
|
export function createSuggestionPlugin(
|
||||||
|
getSuggesting: () => boolean,
|
||||||
|
getAuthor: () => { authorId: string; authorName: string },
|
||||||
|
): Plugin {
|
||||||
|
return new Plugin({
|
||||||
|
key: pluginKey,
|
||||||
|
|
||||||
|
filterTransaction(tr: Transaction, state) {
|
||||||
|
if (!getSuggesting()) return true;
|
||||||
|
if (!tr.docChanged) return true;
|
||||||
|
if (tr.getMeta('suggestion-accept') || tr.getMeta('suggestion-reject')) return true;
|
||||||
|
if (tr.getMeta('suggestion-applied')) return true;
|
||||||
|
|
||||||
|
// Intercept the transaction and convert it to suggestion marks
|
||||||
|
const { authorId, authorName } = getAuthor();
|
||||||
|
const suggestionId = `s_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
|
||||||
|
// We need to rebuild the transaction with suggestion marks
|
||||||
|
const newTr = state.tr;
|
||||||
|
let blocked = false;
|
||||||
|
|
||||||
|
tr.steps.forEach((step, i) => {
|
||||||
|
const stepMap = step.getMap();
|
||||||
|
let hasInsert = false;
|
||||||
|
let hasDelete = false;
|
||||||
|
|
||||||
|
stepMap.forEach((oldStart: number, oldEnd: number, newStart: number, newEnd: number) => {
|
||||||
|
if (newEnd > newStart) hasInsert = true;
|
||||||
|
if (oldEnd > oldStart) hasDelete = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasInsert && !hasDelete) {
|
||||||
|
// Pure insertion — let it through but add the suggestionInsert mark
|
||||||
|
blocked = true;
|
||||||
|
const doc = tr.docs[i];
|
||||||
|
stepMap.forEach((_oldStart: number, _oldEnd: number, newStart: number, newEnd: number) => {
|
||||||
|
if (newEnd > newStart) {
|
||||||
|
const insertedText = tr.docs[i + 1]?.textBetween(newStart, newEnd, '', '') || '';
|
||||||
|
if (insertedText) {
|
||||||
|
const insertMark = state.schema.marks.suggestionInsert.create({
|
||||||
|
suggestionId,
|
||||||
|
authorId,
|
||||||
|
authorName,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
newTr.insertText(insertedText, newStart);
|
||||||
|
newTr.addMark(newStart, newStart + insertedText.length, insertMark);
|
||||||
|
newTr.setMeta('suggestion-applied', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (hasDelete && !hasInsert) {
|
||||||
|
// Pure deletion — convert to suggestionDelete mark instead
|
||||||
|
blocked = true;
|
||||||
|
stepMap.forEach((oldStart: number, oldEnd: number) => {
|
||||||
|
if (oldEnd > oldStart) {
|
||||||
|
const deleteMark = state.schema.marks.suggestionDelete.create({
|
||||||
|
suggestionId,
|
||||||
|
authorId,
|
||||||
|
authorName,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
newTr.addMark(oldStart, oldEnd, deleteMark);
|
||||||
|
newTr.setMeta('suggestion-applied', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (hasInsert && hasDelete) {
|
||||||
|
// Replacement (delete + insert) — mark old text as deleted, new text as inserted
|
||||||
|
blocked = true;
|
||||||
|
stepMap.forEach((oldStart: number, oldEnd: number, newStart: number, newEnd: number) => {
|
||||||
|
if (oldEnd > oldStart) {
|
||||||
|
const deleteMark = state.schema.marks.suggestionDelete.create({
|
||||||
|
suggestionId,
|
||||||
|
authorId,
|
||||||
|
authorName,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
newTr.addMark(oldStart, oldEnd, deleteMark);
|
||||||
|
}
|
||||||
|
if (newEnd > newStart) {
|
||||||
|
const insertedText = tr.docs[i + 1]?.textBetween(newStart, newEnd, '', '') || '';
|
||||||
|
if (insertedText) {
|
||||||
|
const insertMark = state.schema.marks.suggestionInsert.create({
|
||||||
|
suggestionId,
|
||||||
|
authorId,
|
||||||
|
authorName,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
// Insert after the "deleted" text
|
||||||
|
const insertPos = oldEnd;
|
||||||
|
newTr.insertText(insertedText, insertPos);
|
||||||
|
newTr.addMark(insertPos, insertPos + insertedText.length, insertMark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
newTr.setMeta('suggestion-applied', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (blocked && newTr.docChanged) {
|
||||||
|
// Dispatch our modified transaction instead
|
||||||
|
// We need to use view.dispatch in the next tick
|
||||||
|
setTimeout(() => {
|
||||||
|
const view = (state as any).view;
|
||||||
|
if (view) view.dispatch(newTr);
|
||||||
|
}, 0);
|
||||||
|
return false; // Block the original transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
return !blocked;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept a suggestion by removing the mark.
|
||||||
|
* - For insertions: remove the mark (text stays)
|
||||||
|
* - For deletions: remove the text and the mark
|
||||||
|
*/
|
||||||
|
export function acceptSuggestion(editor: Editor, suggestionId: string) {
|
||||||
|
const { state } = editor;
|
||||||
|
const { doc, tr } = state;
|
||||||
|
|
||||||
|
// Find all marks with this suggestionId
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText) return;
|
||||||
|
|
||||||
|
// Check for suggestionDelete marks — accept = remove the text
|
||||||
|
const deleteMark = node.marks.find(
|
||||||
|
m => m.type.name === 'suggestionDelete' && m.attrs.suggestionId === suggestionId
|
||||||
|
);
|
||||||
|
if (deleteMark) {
|
||||||
|
tr.delete(pos, pos + node.nodeSize);
|
||||||
|
tr.setMeta('suggestion-accept', true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for suggestionInsert marks — accept = remove the mark, keep text
|
||||||
|
const insertMark = node.marks.find(
|
||||||
|
m => m.type.name === 'suggestionInsert' && m.attrs.suggestionId === suggestionId
|
||||||
|
);
|
||||||
|
if (insertMark) {
|
||||||
|
tr.removeMark(pos, pos + node.nodeSize, insertMark);
|
||||||
|
tr.setMeta('suggestion-accept', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tr.docChanged) {
|
||||||
|
editor.view.dispatch(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject a suggestion by reverting it.
|
||||||
|
* - For insertions: remove the text and the mark
|
||||||
|
* - For deletions: remove the mark (text stays)
|
||||||
|
*/
|
||||||
|
export function rejectSuggestion(editor: Editor, suggestionId: string) {
|
||||||
|
const { state } = editor;
|
||||||
|
const { doc, tr } = state;
|
||||||
|
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText) return;
|
||||||
|
|
||||||
|
// Check for suggestionInsert marks — reject = remove text + mark
|
||||||
|
const insertMark = node.marks.find(
|
||||||
|
m => m.type.name === 'suggestionInsert' && m.attrs.suggestionId === suggestionId
|
||||||
|
);
|
||||||
|
if (insertMark) {
|
||||||
|
tr.delete(pos, pos + node.nodeSize);
|
||||||
|
tr.setMeta('suggestion-reject', true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for suggestionDelete marks — reject = remove the mark, keep text
|
||||||
|
const deleteMark = node.marks.find(
|
||||||
|
m => m.type.name === 'suggestionDelete' && m.attrs.suggestionId === suggestionId
|
||||||
|
);
|
||||||
|
if (deleteMark) {
|
||||||
|
tr.removeMark(pos, pos + node.nodeSize, deleteMark);
|
||||||
|
tr.setMeta('suggestion-reject', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tr.docChanged) {
|
||||||
|
editor.view.dispatch(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept all suggestions in the document.
|
||||||
|
*/
|
||||||
|
export function acceptAllSuggestions(editor: Editor) {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
editor.state.doc.descendants((node) => {
|
||||||
|
if (!node.isText) return;
|
||||||
|
for (const mark of node.marks) {
|
||||||
|
if (mark.type.name === 'suggestionInsert' || mark.type.name === 'suggestionDelete') {
|
||||||
|
ids.add(mark.attrs.suggestionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const id of ids) {
|
||||||
|
acceptSuggestion(editor, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject all suggestions in the document.
|
||||||
|
*/
|
||||||
|
export function rejectAllSuggestions(editor: Editor) {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
editor.state.doc.descendants((node) => {
|
||||||
|
if (!node.isText) return;
|
||||||
|
for (const mark of node.marks) {
|
||||||
|
if (mark.type.name === 'suggestionInsert' || mark.type.name === 'suggestionDelete') {
|
||||||
|
ids.add(mark.attrs.suggestionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const id of ids) {
|
||||||
|
rejectSuggestion(editor, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,22 @@ export interface SourceRef {
|
||||||
contentHash?: string; // For conflict detection on re-import
|
contentHash?: string; // For conflict detection on re-import
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CommentMessage {
|
||||||
|
id: string;
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
text: string;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommentThread {
|
||||||
|
id: string;
|
||||||
|
anchor: string; // serialized position info for the comment mark
|
||||||
|
resolved: boolean;
|
||||||
|
messages: CommentMessage[];
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NoteItem {
|
export interface NoteItem {
|
||||||
id: string;
|
id: string;
|
||||||
notebookId: string;
|
notebookId: string;
|
||||||
|
|
@ -42,6 +58,8 @@ export interface NoteItem {
|
||||||
summaryModel?: string;
|
summaryModel?: string;
|
||||||
openNotebookSourceId?: string;
|
openNotebookSourceId?: string;
|
||||||
sourceRef?: SourceRef;
|
sourceRef?: SourceRef;
|
||||||
|
collabEnabled?: boolean;
|
||||||
|
comments?: Record<string, CommentThread>;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
|
|
@ -102,12 +120,12 @@ export function connectionsDocId(space: string) {
|
||||||
export const notebookSchema: DocSchema<NotebookDoc> = {
|
export const notebookSchema: DocSchema<NotebookDoc> = {
|
||||||
module: 'notes',
|
module: 'notes',
|
||||||
collection: 'notebooks',
|
collection: 'notebooks',
|
||||||
version: 3,
|
version: 4,
|
||||||
init: (): NotebookDoc => ({
|
init: (): NotebookDoc => ({
|
||||||
meta: {
|
meta: {
|
||||||
module: 'notes',
|
module: 'notes',
|
||||||
collection: 'notebooks',
|
collection: 'notebooks',
|
||||||
version: 3,
|
version: 4,
|
||||||
spaceSlug: '',
|
spaceSlug: '',
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
},
|
},
|
||||||
|
|
@ -130,6 +148,7 @@ export const notebookSchema: DocSchema<NotebookDoc> = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// v2→v3: sourceRef field is optional, no migration needed
|
// v2→v3: sourceRef field is optional, no migration needed
|
||||||
|
// v3→v4: collabEnabled + comments fields are optional, no migration needed
|
||||||
return doc;
|
return doc;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
/**
|
||||||
|
* Custom Yjs WebSocket provider that bridges over the existing rSpace WebSocket.
|
||||||
|
*
|
||||||
|
* Instead of opening a separate y-websocket connection, this provider sends
|
||||||
|
* Yjs sync/awareness messages as JSON payloads through the rSpace runtime's
|
||||||
|
* WebSocket, using `yjs-sync` and `yjs-awareness` message types.
|
||||||
|
*
|
||||||
|
* The server simply relays these messages to all other peers in the same space.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import { writeSyncStep1, writeUpdate, readSyncMessage } from 'y-protocols/sync';
|
||||||
|
import {
|
||||||
|
Awareness,
|
||||||
|
encodeAwarenessUpdate,
|
||||||
|
applyAwarenessUpdate,
|
||||||
|
removeAwarenessStates,
|
||||||
|
} from 'y-protocols/awareness';
|
||||||
|
import {
|
||||||
|
createEncoder,
|
||||||
|
toUint8Array,
|
||||||
|
length as encoderLength,
|
||||||
|
} from 'lib0/encoding';
|
||||||
|
import { createDecoder } from 'lib0/decoding';
|
||||||
|
|
||||||
|
/** Minimal interface for the rSpace runtime's custom message API. */
|
||||||
|
interface RuntimeBridge {
|
||||||
|
sendCustom(msg: Record<string, any>): void;
|
||||||
|
onCustomMessage(type: string, cb: (msg: any) => void): () => void;
|
||||||
|
onConnect(cb: () => void): () => void;
|
||||||
|
onDisconnect(cb: () => void): () => void;
|
||||||
|
isOnline: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RSpaceYjsProvider {
|
||||||
|
readonly doc: Y.Doc;
|
||||||
|
readonly awareness: Awareness;
|
||||||
|
readonly noteId: string;
|
||||||
|
|
||||||
|
private runtime: RuntimeBridge;
|
||||||
|
private unsubs: (() => void)[] = [];
|
||||||
|
private connected = false;
|
||||||
|
private synced = false;
|
||||||
|
|
||||||
|
constructor(noteId: string, ydoc: Y.Doc, runtime: RuntimeBridge) {
|
||||||
|
this.noteId = noteId;
|
||||||
|
this.doc = ydoc;
|
||||||
|
this.runtime = runtime;
|
||||||
|
this.awareness = new Awareness(ydoc);
|
||||||
|
|
||||||
|
this.setupListeners();
|
||||||
|
|
||||||
|
if (runtime.isOnline) {
|
||||||
|
this.onConnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupListeners(): void {
|
||||||
|
// Listen for Yjs sync messages from other peers
|
||||||
|
this.unsubs.push(
|
||||||
|
this.runtime.onCustomMessage('yjs-sync', (msg: any) => {
|
||||||
|
if (msg.noteId !== this.noteId) return;
|
||||||
|
this.handleSyncMessage(msg.data);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen for awareness messages from other peers
|
||||||
|
this.unsubs.push(
|
||||||
|
this.runtime.onCustomMessage('yjs-awareness', (msg: any) => {
|
||||||
|
if (msg.noteId !== this.noteId) return;
|
||||||
|
this.handleAwarenessMessage(msg.data);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen for connect/disconnect
|
||||||
|
this.unsubs.push(
|
||||||
|
this.runtime.onConnect(() => this.onConnect())
|
||||||
|
);
|
||||||
|
this.unsubs.push(
|
||||||
|
this.runtime.onDisconnect(() => this.onDisconnect())
|
||||||
|
);
|
||||||
|
|
||||||
|
// When local doc changes, send update to peers
|
||||||
|
const updateHandler = (update: Uint8Array, origin: any) => {
|
||||||
|
if (origin === 'remote') return; // Don't echo back remote updates
|
||||||
|
this.sendDocUpdate(update);
|
||||||
|
};
|
||||||
|
this.doc.on('update', updateHandler);
|
||||||
|
this.unsubs.push(() => this.doc.off('update', updateHandler));
|
||||||
|
|
||||||
|
// When local awareness changes, broadcast
|
||||||
|
const awarenessHandler = (changes: {
|
||||||
|
added: number[];
|
||||||
|
updated: number[];
|
||||||
|
removed: number[];
|
||||||
|
}, origin: string | null) => {
|
||||||
|
if (origin === 'remote') return;
|
||||||
|
const changedClients = changes.added.concat(changes.updated).concat(changes.removed);
|
||||||
|
const update = encodeAwarenessUpdate(this.awareness, changedClients);
|
||||||
|
this.runtime.sendCustom({
|
||||||
|
type: 'yjs-awareness',
|
||||||
|
noteId: this.noteId,
|
||||||
|
data: Array.from(update),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.awareness.on('update', awarenessHandler);
|
||||||
|
this.unsubs.push(() => this.awareness.off('update', awarenessHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
private onConnect(): void {
|
||||||
|
if (this.connected) return;
|
||||||
|
this.connected = true;
|
||||||
|
|
||||||
|
// Send initial sync step 1 (state vector)
|
||||||
|
const encoder = createEncoder();
|
||||||
|
writeSyncStep1(encoder, this.doc);
|
||||||
|
this.runtime.sendCustom({
|
||||||
|
type: 'yjs-sync',
|
||||||
|
noteId: this.noteId,
|
||||||
|
data: Array.from(toUint8Array(encoder)),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send full awareness state
|
||||||
|
const awarenessUpdate = encodeAwarenessUpdate(
|
||||||
|
this.awareness,
|
||||||
|
[this.doc.clientID],
|
||||||
|
);
|
||||||
|
this.runtime.sendCustom({
|
||||||
|
type: 'yjs-awareness',
|
||||||
|
noteId: this.noteId,
|
||||||
|
data: Array.from(awarenessUpdate),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDisconnect(): void {
|
||||||
|
this.connected = false;
|
||||||
|
this.synced = false;
|
||||||
|
|
||||||
|
// Remove all remote awareness states on disconnect
|
||||||
|
const states = Array.from(this.awareness.getStates().keys())
|
||||||
|
.filter(client => client !== this.doc.clientID);
|
||||||
|
removeAwarenessStates(this.awareness, states, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSyncMessage(data: number[]): void {
|
||||||
|
const decoder = createDecoder(new Uint8Array(data));
|
||||||
|
const encoder = createEncoder();
|
||||||
|
const messageType = readSyncMessage(
|
||||||
|
decoder, encoder, this.doc, 'remote'
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the response encoder has content, send it back
|
||||||
|
if (encoderLength(encoder) > 0) {
|
||||||
|
this.runtime.sendCustom({
|
||||||
|
type: 'yjs-sync',
|
||||||
|
noteId: this.noteId,
|
||||||
|
data: Array.from(toUint8Array(encoder)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// After receiving sync step 2, we're synced
|
||||||
|
if (messageType === 1) { // syncStep2
|
||||||
|
this.synced = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAwarenessMessage(data: number[]): void {
|
||||||
|
applyAwarenessUpdate(
|
||||||
|
this.awareness,
|
||||||
|
new Uint8Array(data),
|
||||||
|
'remote',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendDocUpdate(update: Uint8Array): void {
|
||||||
|
if (!this.connected) return;
|
||||||
|
const encoder = createEncoder();
|
||||||
|
writeUpdate(encoder, update);
|
||||||
|
this.runtime.sendCustom({
|
||||||
|
type: 'yjs-sync',
|
||||||
|
noteId: this.noteId,
|
||||||
|
data: Array.from(toUint8Array(encoder)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSynced(): boolean {
|
||||||
|
return this.synced;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
// Remove local awareness state
|
||||||
|
removeAwarenessStates(
|
||||||
|
this.awareness,
|
||||||
|
[this.doc.clientID],
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
// Clean up all listeners
|
||||||
|
for (const unsub of this.unsubs) {
|
||||||
|
try { unsub(); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
this.unsubs = [];
|
||||||
|
this.connected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"@tiptap/extension-underline": "^3.20.0",
|
"@tiptap/extension-underline": "^3.20.0",
|
||||||
"@tiptap/pm": "^3.20.0",
|
"@tiptap/pm": "^3.20.0",
|
||||||
"@tiptap/starter-kit": "^3.20.0",
|
"@tiptap/starter-kit": "^3.20.0",
|
||||||
|
"@tiptap/y-tiptap": "^3.0.2",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@x402/core": "^2.3.1",
|
"@x402/core": "^2.3.1",
|
||||||
"@x402/evm": "^2.5.0",
|
"@x402/evm": "^2.5.0",
|
||||||
|
|
@ -59,7 +60,10 @@
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"sharp": "^0.33.0",
|
"sharp": "^0.33.0",
|
||||||
"web-push": "^3.6.7",
|
"web-push": "^3.6.7",
|
||||||
"yaml": "^2.8.2"
|
"y-indexeddb": "^9.0.12",
|
||||||
|
"y-prosemirror": "^1.3.7",
|
||||||
|
"yaml": "^2.8.2",
|
||||||
|
"yjs": "^13.6.30"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
|
|
|
||||||
|
|
@ -2838,6 +2838,20 @@ const server = Bun.serve<WSData>({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Yjs collaboration relay (pure relay — server doesn't parse Yjs binary) ──
|
||||||
|
if (msg.type === "yjs-sync" || msg.type === "yjs-awareness") {
|
||||||
|
const clients = communityClients.get(communitySlug);
|
||||||
|
if (clients) {
|
||||||
|
const relay = JSON.stringify({ ...msg, peerId });
|
||||||
|
for (const [cid, client] of clients) {
|
||||||
|
if (cid !== peerId && client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(relay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Legacy canvas protocol (no docId) ──
|
// ── Legacy canvas protocol (no docId) ──
|
||||||
if (msg.type === "sync" && Array.isArray(msg.data)) {
|
if (msg.type === "sync" && Array.isArray(msg.data)) {
|
||||||
if (ws.data.readOnly) {
|
if (ws.data.readOnly) {
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,34 @@ export class RSpaceOfflineRuntime {
|
||||||
return () => { this.#statusListeners.delete(cb); };
|
return () => { this.#statusListeners.delete(cb); };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a custom message through the WebSocket (for Yjs, etc.).
|
||||||
|
*/
|
||||||
|
sendCustom(msg: Record<string, any>): void {
|
||||||
|
this.#sync.sendCustom(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a handler for custom WS message types. Returns unsubscribe fn.
|
||||||
|
*/
|
||||||
|
onCustomMessage(type: string, cb: (msg: any) => void): () => void {
|
||||||
|
return this.#sync.onCustomMessage(type, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for WebSocket connect events.
|
||||||
|
*/
|
||||||
|
onConnect(cb: () => void): () => void {
|
||||||
|
return this.#sync.onConnect(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for WebSocket disconnect events.
|
||||||
|
*/
|
||||||
|
onDisconnect(cb: () => void): () => void {
|
||||||
|
return this.#sync.onDisconnect(cb);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flush all pending writes to IndexedDB. Call on beforeunload.
|
* Flush all pending writes to IndexedDB. Call on beforeunload.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,9 @@ export class DocSyncManager {
|
||||||
// Pending doc-list requests
|
// Pending doc-list requests
|
||||||
#docListCallbacks = new Map<string, DocListCallback>();
|
#docListCallbacks = new Map<string, DocListCallback>();
|
||||||
|
|
||||||
|
// Custom message handlers (for Yjs, etc.)
|
||||||
|
#customMessageListeners = new Map<string, Set<(msg: any) => void>>();
|
||||||
|
|
||||||
constructor(opts: DocSyncManagerOptions) {
|
constructor(opts: DocSyncManagerOptions) {
|
||||||
this.#documents = opts.documents;
|
this.#documents = opts.documents;
|
||||||
this.#store = opts.store ?? null;
|
this.#store = opts.store ?? null;
|
||||||
|
|
@ -462,6 +465,30 @@ export class DocSyncManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an arbitrary JSON message through the WebSocket.
|
||||||
|
* Used by Yjs provider and other custom protocols.
|
||||||
|
*/
|
||||||
|
sendCustom(msg: Record<string, any>): void {
|
||||||
|
if (this.#ws?.readyState === WebSocket.OPEN) {
|
||||||
|
this.#ws.send(JSON.stringify(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a handler for custom message types (e.g. 'yjs-sync', 'yjs-awareness').
|
||||||
|
* Returns an unsubscribe function.
|
||||||
|
*/
|
||||||
|
onCustomMessage(type: string, cb: (msg: any) => void): () => void {
|
||||||
|
let set = this.#customMessageListeners.get(type);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set();
|
||||||
|
this.#customMessageListeners.set(type, set);
|
||||||
|
}
|
||||||
|
set.add(cb);
|
||||||
|
return () => { set!.delete(cb); };
|
||||||
|
}
|
||||||
|
|
||||||
#handleMessage(raw: ArrayBuffer | string): void {
|
#handleMessage(raw: ArrayBuffer | string): void {
|
||||||
try {
|
try {
|
||||||
const data = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
|
const data = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
|
||||||
|
|
@ -483,6 +510,16 @@ export class DocSyncManager {
|
||||||
case 'pong':
|
case 'pong':
|
||||||
// Keep-alive acknowledged
|
// Keep-alive acknowledged
|
||||||
break;
|
break;
|
||||||
|
default: {
|
||||||
|
// Dispatch to custom message listeners
|
||||||
|
const listeners = this.#customMessageListeners.get(msg.type);
|
||||||
|
if (listeners) {
|
||||||
|
for (const cb of listeners) {
|
||||||
|
try { cb(msg); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[DocSyncManager] Failed to handle message:', e);
|
console.error('[DocSyncManager] Failed to handle message:', e);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue