Skip to content

feat: upgrade Remix to 3.0.0-alpha.4 and resync package docs#64

Open
kentcdodds wants to merge 7 commits intomainfrom
cursor/remix-v3-alpha-readiness-3c13
Open

feat: upgrade Remix to 3.0.0-alpha.4 and resync package docs#64
kentcdodds wants to merge 7 commits intomainfrom
cursor/remix-v3-alpha-readiness-3c13

Conversation

@kentcdodds
Copy link
Copy Markdown
Member

@kentcdodds kentcdodds commented Mar 26, 2026

Summary

  • upgrade the repo to remix@3.0.0-alpha.4
  • migrate the app to the alpha.4 fetch-router controller/handler APIs and the new data-table table/column APIs
  • remove the temporary custom JSX runtime and update the client to work directly with the default remix/component runtime
  • keep the D1 integration working by aligning the custom adapter/compiler with the alpha.4 DatabaseAdapter contract
  • delete the old hand-maintained Remix docs tree and resync docs/agents/remix from the upstream alpha.4 package markdown with one directory per package and upstream README.md files

Testing

  • bun run lint
  • bun run typecheck
  • bun run build
  • manual browser smoke test for /, /login, and unauthenticated /chat redirect

Walkthrough

Home page after default runtime migration
Login page after default runtime migration
Chat redirect after default runtime migration

Open in Web Open in Cursor 

cursoragent and others added 3 commits March 26, 2026 21:46
createElement(type, props) merges variadic rest children onto props; with
no rest args that becomes [] and wipes props.children, producing a blank UI.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@cursor cursor bot changed the title feat: upgrade Remix to 3.0.0-alpha.4 feat: upgrade Remix to 3.0.0-alpha.4 and resync package docs Mar 26, 2026
@kentcdodds kentcdodds marked this pull request as ready for review March 26, 2026 22:25
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 26, 2026

🔎 Preview deployed: https://epicflare-pr-64.kentcdodds.workers.dev

Worker: epicflare-pr-64
D1: epicflare-pr-64-db
KV: epicflare-pr-64-oauth-kv

Mocks:

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@cursor cursor bot temporarily deployed to preview-64 March 26, 2026 22:35 Inactive
@kentcdodds
Copy link
Copy Markdown
Member Author

@cursoragent please check CI and fix issues.

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 26, 2026

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Delete button mix overridden, losing all event handlers
    • Moved delete button styles into the double-check mix and ensured the confirm handler is invoked so click/blur handlers are preserved and deletion fires.

Create PR

Preview (03cfc173b6)
diff --git a/bun.lock b/bun.lock
--- a/bun.lock
+++ b/bun.lock
@@ -13,7 +13,7 @@
         "@modelcontextprotocol/sdk": "1.26.0",
         "agents": "^0.7.6",
         "get-port": "^7.1.0",
-        "remix": "3.0.0-alpha.3",
+        "remix": "3.0.0-alpha.4",
         "workers-ai-provider": "^3.1.2",
         "zod": "^4.3.6",
       },
@@ -306,35 +306,45 @@
 
     "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="],
 
-    "@remix-run/async-context-middleware": ["@remix-run/async-context-middleware@0.1.3", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0" } }, "sha512-z2hTnQspCWsKka1QxAu5qf+IRbMYwTBmwL28zCk8nIPcYxG19rDbBeRmfQZ9hxHes72XVDu3Fd3ArLTy8bivdw=="],
+    "@remix-run/async-context-middleware": ["@remix-run/async-context-middleware@0.2.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-ebnPM2OFM/BO3GSoRKs2i5+lgY/fIunzpL8/RhuEuP9a04cQYXMbEzbdae+qBHTgtsVdPrqeGr/lwlF5FNnQZA=="],
 
-    "@remix-run/component": ["@remix-run/component@0.5.0", "", { "dependencies": { "@remix-run/interaction": "^0.5.0" } }, "sha512-xRLOcgwWKZxFdj63bWi3/snC2uxkm978B49EGEv1/G43iBFksYjS4ADanfYxREvQjMlaCqSUH0ZTZsJbGsz3PQ=="],
+    "@remix-run/auth": ["@remix-run/auth@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-TFsK40jymT7iZyGWOjoqlFFmILdDXy3jYBoI1mohjdqrp8SVWbm7htiSbdd+j8DGBUEbxMTvKcaVG0bPA1M8VA=="],
 
-    "@remix-run/compression-middleware": ["@remix-run/compression-middleware@0.1.3", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-pKsKtIzW/yjCbftSQRYfvnKKdq65Nu+YEhbQooiXtmd1Ub4inHSrzeA/vca3sQkBnBr/eRA9uujxIszRe+6LAg=="],
+    "@remix-run/auth-middleware": ["@remix-run/auth-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-QZlI4MyU8VTVI6WB4T4MWWqtbYrIUiiBG3mgdJUXjEYMBBoxVAkT/6rT3YErmhX+SVARP2zC46qzujrbYcQLCQ=="],
 
+    "@remix-run/component": ["@remix-run/component@0.6.0", "", { "dependencies": { "@types/dom-navigation": "^1.0.7" } }, "sha512-hmIUdPnBtONeOH8bbLuOVGSgFD/MAUNvpH0Xwgo27GByBIjQhOm25Tt9R04HQslDhHAzwNVnWeQkr3j9eyXYhQ=="],
+
+    "@remix-run/compression-middleware": ["@remix-run/compression-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-vE+d4a04Tq9fO5KBmNtMD5GKJEt6U++B1yufEdBMNe5ROBQkpPWSuKlhkRcZyoV8SPEI2HbkKMtQ8QXfGJR11g=="],
+
     "@remix-run/cookie": ["@remix-run/cookie@0.5.1", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-gbeZfVd1AKRlFj3IJWcIcR6zqVGz2XGJhR+mcqYiWnYt6KM8oUGtc82dsc4qZnWWA1f0nM4/He9wrU4GjB0pag=="],
 
-    "@remix-run/data-schema": ["@remix-run/data-schema@0.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0" } }, "sha512-PzpYP9P19cb8bS7Y9+MSxyAWGy0n13sx1lYOMoKI+iEx7pdB2ZLaiidwcn1l6AeR6gjVIpuzyM6/UONG3bzboA=="],
+    "@remix-run/cop-middleware": ["@remix-run/cop-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-5rMNrBswTJJLPsu1nUfXubgVf7XBTRfPNfd/4QQHaXRdhXVWVxU9HbNJrOjd7iNbBWuZeaJxSdDepbQLnuBkTg=="],
 
-    "@remix-run/data-table": ["@remix-run/data-table@0.1.0", "", { "dependencies": { "@remix-run/data-schema": "^0.1.0" } }, "sha512-u70INxiF9pThV2LcP7y37G7eq16OXWrA+HXS8qNplQgJJd1zhwZrODOtfF9U+PKLiWQkBj+bEeh7QhOlvGsVDg=="],
+    "@remix-run/cors-middleware": ["@remix-run/cors-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/headers": "^0.19.0" } }, "sha512-x5nyxDhWEOnznqetDhtj5WYoHrKpbiZ8K6BCGQ2+DFjLYHoyWCayciUNrqOVSIWc5XdV6KlKHcg1TyQvW/cS4w=="],
 
-    "@remix-run/data-table-mysql": ["@remix-run/data-table-mysql@0.1.0", "", { "dependencies": { "@remix-run/data-table": "^0.1.0" }, "peerDependencies": { "mysql2": "^3.15.3" }, "optionalPeers": ["mysql2"] }, "sha512-lzghxTYZDHODNisIJWkq4IkcGsb1pUrp9WGtlNEVWrXWC6aArOiojR3PW4arFwNG5ddKJPRfwot+ySc2YZy5RQ=="],
+    "@remix-run/csrf-middleware": ["@remix-run/csrf-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-EZAV9UZrx3qdYu3ov9NgFVJchw6kOLYI2uRB+e3WBVW1phruHKc42EpdJIZ6fU0J9Of0Lfo4X4VTM7O1/u1cWw=="],
 
-    "@remix-run/data-table-postgres": ["@remix-run/data-table-postgres@0.1.0", "", { "dependencies": { "@remix-run/data-table": "^0.1.0" }, "peerDependencies": { "pg": "^8.16.3" }, "optionalPeers": ["pg"] }, "sha512-mqzARY5tOFVLjFAArryuLQ93M8IdjVnLKnD1VQyJHFbHQF7Zbr8+exv7Hp2hZ1TIGAhZiepGjjC8Re631yJ1Jw=="],
+    "@remix-run/data-schema": ["@remix-run/data-schema@0.2.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0" } }, "sha512-bXgdWcWZS6TkbVO9Piwn035iw0LczfSTFrCW4Vd1k57x/4j3eWZwfHBr3lXdw5whefacs3m3pVZ8RUiu8NtBeA=="],
 
-    "@remix-run/data-table-sqlite": ["@remix-run/data-table-sqlite@0.1.0", "", { "dependencies": { "@remix-run/data-table": "^0.1.0" }, "peerDependencies": { "better-sqlite3": "^12.4.1" }, "optionalPeers": ["better-sqlite3"] }, "sha512-hFtmz9haMr3p/aFWL5D1zpJsgAnNdLVCF6HvXBmtK4m3NdLmM1eNhblpB/SfPGNEODsmx4Jvcaof57dfaLuKgA=="],
+    "@remix-run/data-table": ["@remix-run/data-table@0.2.0", "", {}, "sha512-6QbdlKER0F0rX3XkTHC7ZPBgu/dyGvvgrKRbraBD9iGHTE7AED1emo3/YWsDJUeFzy7INNJaUIpNm9jNcgL8Kg=="],
 
+    "@remix-run/data-table-mysql": ["@remix-run/data-table-mysql@0.2.0", "", { "dependencies": { "@remix-run/data-table": "^0.2.0" }, "peerDependencies": { "mysql2": "^3.15.3" }, "optionalPeers": ["mysql2"] }, "sha512-voTcYUjG12PWlrWIYSqoa+C3WK9Q6TVKPrqnvGKY9s6EVBil9e+glaP2JOM8BbzL0hGuKPifdbrDGfGpGitdQw=="],
+
+    "@remix-run/data-table-postgres": ["@remix-run/data-table-postgres@0.2.0", "", { "dependencies": { "@remix-run/data-table": "^0.2.0" }, "peerDependencies": { "pg": "^8.16.3" }, "optionalPeers": ["pg"] }, "sha512-03v+zpWL4lMJr34PZ76PWFpjIa4K6Uq3NLBqyvN4ojPpm6J7TGsFwPy6sInRm3HjoS4gPSCEmmQ7dUovWa58EA=="],
+
+    "@remix-run/data-table-sqlite": ["@remix-run/data-table-sqlite@0.2.0", "", { "dependencies": { "@remix-run/data-table": "^0.2.0" }, "peerDependencies": { "better-sqlite3": "^12.4.1" }, "optionalPeers": ["better-sqlite3"] }, "sha512-TAQ0u8YnmO3WdnuZULPhc+fgZbKZZPutXiPLf8c9NDhXSC41FlcvfQHTKUxBPVhqdi9mhGiblHHNp6BsVu11dg=="],
+
     "@remix-run/fetch-proxy": ["@remix-run/fetch-proxy@0.7.1", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-rPLfOpAaCXtm1dLI45uIPKERNbXbrh0P9AJc1sliz8pWd/McaFYjdr5KzB4QrFSfPvEt/Wmy6F2521qB1kK0ug=="],
 
-    "@remix-run/fetch-router": ["@remix-run/fetch-router@0.17.0", "", { "dependencies": { "@remix-run/route-pattern": "^0.19.0", "@remix-run/session": "^0.4.1" } }, "sha512-3FeJGrTqrKKCvZdQWijbCXTEHKcdttkLFbI2ogfpZ+iDYSNZ9036wgDXuuoZqg6d+D0E8Unhk5ZwrLKDCd/hOw=="],
+    "@remix-run/fetch-router": ["@remix-run/fetch-router@0.18.0", "", { "dependencies": { "@remix-run/route-pattern": "^0.20.0" } }, "sha512-9Z4JgLH9/jD8jiVvAY9LZR+VoZxPJOQ7pENTBJoSo91PZOkPXfCxQWhPhAwYCP8z+/0FV4ZWSg1DmPPoU2f0UQ=="],
 
     "@remix-run/file-storage": ["@remix-run/file-storage@0.13.3", "", { "dependencies": { "@remix-run/fs": "^0.4.2", "@remix-run/lazy-file": "^5.0.2" } }, "sha512-HBDz9RRsFRvI6EoeasklxH/NleGy0QZBXBcA4gQBW8ueucop21TQI4wvGlhZmXcnJ3nP4RkhdF2Gff2/HD5eiA=="],
 
     "@remix-run/file-storage-s3": ["@remix-run/file-storage-s3@0.1.0", "", { "dependencies": { "@remix-run/file-storage": "^0.13.3", "aws4fetch": "^1.0.20" } }, "sha512-r80An7nSFidK/0xn9O9/HxfUcgxVpM4kprnTGr6pGhKdgbaTCEtA+U5ETYGfeedFxhDcT+7ue+4Fv/VxeIvFwQ=="],
 
-    "@remix-run/form-data-middleware": ["@remix-run/form-data-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0", "@remix-run/form-data-parser": "^0.15.0" } }, "sha512-WZfP1U6lDoipkfjcd0V39HJeTPMTX2WyaPcOBTbBHS0kapIZiHYm6RpGLhE8U58652i3TBh/zzvAczJIbFV2AA=="],
+    "@remix-run/form-data-middleware": ["@remix-run/form-data-middleware@0.2.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/form-data-parser": "^0.16.0" } }, "sha512-oaBcYvyP/U2GrDdFyQUh2pNCshvKwPFBoUup1Bz3NHnMzym4OkZJgaBi4o7HIJC33QudnqI7Mp7TW1pRg0CvIA=="],
 
-    "@remix-run/form-data-parser": ["@remix-run/form-data-parser@0.15.0", "", { "dependencies": { "@remix-run/multipart-parser": "^0.14.2" } }, "sha512-sQP4r9218TWmow6Nt252VjKE674dRi4Z8WTnWUxJJG8I/qNfnGZubZ8LgyE0dR9z1gfaEpkd19MfYMLiOTOkJQ=="],
+    "@remix-run/form-data-parser": ["@remix-run/form-data-parser@0.16.0", "", { "dependencies": { "@remix-run/multipart-parser": "^0.15.0" } }, "sha512-k4QCgCyPURpqe+9Rual1GBJ8Ab6ri82Clfh5ooxq03jKQ1TyQZT5xh+XP0R05+Bqg5bzAZl7r2BpmDEvfoRTWw=="],
 
     "@remix-run/fs": ["@remix-run/fs@0.4.2", "", { "dependencies": { "@remix-run/lazy-file": "^5.0.2", "@remix-run/mime": "^0.4.0" } }, "sha512-z3W2L+iUwgZ7i0S379SYQ8veOe2Weqs+JajmyTCqSVzbmMUniH3qQ6SAYr3FjbrKtLLWHN3SpK4XtFv57VzbLA=="],
 
@@ -342,33 +352,31 @@
 
     "@remix-run/html-template": ["@remix-run/html-template@0.3.0", "", {}, "sha512-aAMx68udtIk0fmCpCXHYscVeCDsRVEmEgh4XvtusPr3vkHu3jn4gx5oAxgsPXPdDmmD/d75SYyI0m/F+aLz5iQ=="],
 
-    "@remix-run/interaction": ["@remix-run/interaction@0.5.0", "", {}, "sha512-Z2ja9/7TfMHt/wzWq425GI2xj6QxW4E3OHZ8In81uytZKIuWaI6Pn3v8qyMrInwnBEaLcfcbeQVCiExgHU8D4A=="],
-
     "@remix-run/lazy-file": ["@remix-run/lazy-file@5.0.2", "", { "dependencies": { "@remix-run/mime": "^0.4.0" } }, "sha512-52Bo5dTV+EDwrUMS3mjeR+Sly85aHeN3fnNTeaflqzlCMWJwr2pX+y6/3mTDtRdxmTWF1MGQAoeayzfPb4zZJg=="],
 
-    "@remix-run/logger-middleware": ["@remix-run/logger-middleware@0.1.3", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0" } }, "sha512-6gxz1XC2lJYQS3Oz1pZzxpuoLowwd2PSpimMaQnkk0fZ7hHYxx7uV+FSs2Z3fue6kYvZ+IxSUe8Wy52V2r4LxA=="],
+    "@remix-run/logger-middleware": ["@remix-run/logger-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-gFDyYn8o5ddjQoEbrc8CK6PPq3lzrOX6BCqlvM+QVTMJ5/2aHgfMbQdXnWtbeiWJJrpODpABrSNfjLkHjaK4og=="],
 
-    "@remix-run/method-override-middleware": ["@remix-run/method-override-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0" } }, "sha512-gYFsdY0eIStTpsqGnF/22YracUmS8cZlef6KsBKOVf1nOI9wwwbRrj/DWLMQsWt22YSBMuPYZW5NLKEmXvJRZw=="],
+    "@remix-run/method-override-middleware": ["@remix-run/method-override-middleware@0.1.5", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-HYQG6YR4l0rYMJ0MXth9SWfeSCwnqHIrWgMQpZHKS/MT7roX7EAfknrl9vzjQWsVfkwl6RrSD30SrLPONj3tsg=="],
 
     "@remix-run/mime": ["@remix-run/mime@0.4.0", "", {}, "sha512-O6TcTL6CtuX82Q8BHqAere5O+0hYcrzSgY9whsDOBuqbW753Rczprs2jYw3qCDSo0kLxykW4ys3qgZcdgZ+chw=="],
 
-    "@remix-run/multipart-parser": ["@remix-run/multipart-parser@0.14.2", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-yDq9ql4Xz92bRG/Sgl4cg2dRlxxC6A40XBy/oyDhy76hJtTQvgyzx9sfPXYPxcfL1BtqljC+sYHE0PvjmQhSfw=="],
+    "@remix-run/multipart-parser": ["@remix-run/multipart-parser@0.15.0", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-/Ugo6k2bN7gh7Ybyhe7R/NrkD075fHrEfVf17P+NNC9rlWHBQCOSyJ4V8n4wtoG8umeImagU/AuHuUuTwzvHww=="],
 
     "@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.13.0", "", {}, "sha512-1EsNo0ZpgXu/90AWoRZf/oE3RVTUS80tiTUpt+hv5pjtAkw7icN4WskDwz/KdAw5ARbJLMhZBrO1NqThmy/McA=="],
 
     "@remix-run/response": ["@remix-run/response@0.3.2", "", { "dependencies": { "@remix-run/headers": "^0.19.0", "@remix-run/html-template": "^0.3.0", "@remix-run/mime": "^0.4.0" } }, "sha512-GkFqVq5E7Do6rMKTBjgoNyJlrsLrqYg+TDlCDrXoZ3v8O2RlSI14+bCF8lGQHy15DWX2pizVj6R0e6NmjcuLuA=="],
 
-    "@remix-run/route-pattern": ["@remix-run/route-pattern@0.19.0", "", {}, "sha512-RXKaIJ2Lx01uyZc0iw+yLzowFCa1/NuB8jN7QTo4QUe2CaUGtvPGdhgrTUp75lyNNCSJIrM9SaAJ6c1pjZdmoA=="],
+    "@remix-run/route-pattern": ["@remix-run/route-pattern@0.20.0", "", {}, "sha512-TEdJ5eFn40St26oyaRYGI1FWeXDvlEzkbvollM8Xit0qIuDFyFG03Okvvfc1s8KgR9sYULDPjxJIzk6xvIRR9A=="],
 
     "@remix-run/session": ["@remix-run/session@0.4.1", "", {}, "sha512-Bm6aKYgutb/raHZ3laloz8g/Qu7f3CeK3o4gUVDMxtEiAdWCzJamwHoTpGOc5+g1Kuy7z85v4M6nGrF06MFDSg=="],
 
-    "@remix-run/session-middleware": ["@remix-run/session-middleware@0.1.4", "", { "dependencies": { "@remix-run/cookie": "^0.5.1", "@remix-run/fetch-router": "^0.17.0", "@remix-run/session": "^0.4.1" } }, "sha512-qqLmf7mG88h+Ge8pWiJMO8+t9nfQMuO/Zx2W68IwB7Cpt+b6PDpB++i3dd/KLlsjJ43XPMoT2ydmo+eQMgBX3g=="],
+    "@remix-run/session-middleware": ["@remix-run/session-middleware@0.2.0", "", { "dependencies": { "@remix-run/cookie": "^0.5.1", "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-kRAr0inELyXeOnUhlaM/eKIe9x1RZsOGbglb7Keb1+Lf6KxWxNMwv1ymmHBfae2ossjQTN/QUaABwQgX/uk3DQ=="],
 
     "@remix-run/session-storage-memcache": ["@remix-run/session-storage-memcache@0.1.0", "", { "dependencies": { "@remix-run/session": "^0.4.1" } }, "sha512-k853rpHncdTJUwdk0hqd+gZ2OONZLNdOUJBKdJB+MehxrVv1TtacDnA+Xs3kh+IVwUrsTmBhED+GHSUocMATUg=="],
 
     "@remix-run/session-storage-redis": ["@remix-run/session-storage-redis@0.1.0", "", { "dependencies": { "@remix-run/session": "^0.4.1" }, "peerDependencies": { "redis": "^5.10.0" }, "optionalPeers": ["redis"] }, "sha512-MovUS1E98wDHP8zsESJGm3ySB7iiOhd+3usxyXXM2sbF9gIe6r1bdAXXirGIoC8AEq1v8IiFE5u5ipo7PX0UHQ=="],
 
-    "@remix-run/static-middleware": ["@remix-run/static-middleware@0.4.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0", "@remix-run/fs": "^0.4.2", "@remix-run/html-template": "^0.3.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-aL5ngFG57uPXTEDaH0uP/cKDpYkLMTtmPjK+SR1ugS654ORk8WTD4Ajf56QekMykCvCnO6PkgFAruUyKkwDNMg=="],
+    "@remix-run/static-middleware": ["@remix-run/static-middleware@0.4.5", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/fs": "^0.4.2", "@remix-run/html-template": "^0.3.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-jxEbrQMDWcUgmv/2NSv4pahvaW3I4jxqZUMDQ7/VfkcgeJKLFk15vgp4BJ5vLDKKMvwVcgM0Ccbw4Y3Ev2zemA=="],
 
     "@remix-run/tar-parser": ["@remix-run/tar-parser@0.7.0", "", {}, "sha512-PW8JxEUzaGcnqxC5hBI8L9lK/Qz3oad6IGKZ+NExI3L7urVJUux+yCBrsme79DMBgS6hL+lgd/5LPFA5fSwF9A=="],
 
@@ -396,6 +404,8 @@
 
     "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
 
+    "@types/dom-navigation": ["@types/dom-navigation@1.0.7", "", {}, "sha512-Di4W+i2faYquHUnyWUg3bBQp5pTNvjDDA7mIYfD/1WlLgan6sKkeVjGbdL78K0CuNEk5Pfc/c0rfelwkz10mnQ=="],
+
     "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
 
     "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@@ -952,7 +962,7 @@
 
     "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
 
-    "remix": ["remix@3.0.0-alpha.3", "", { "dependencies": { "@remix-run/async-context-middleware": "^0.1.3", "@remix-run/component": "^0.5.0", "@remix-run/compression-middleware": "^0.1.3", "@remix-run/cookie": "^0.5.1", "@remix-run/data-schema": "^0.1.0", "@remix-run/data-table": "^0.1.0", "@remix-run/data-table-mysql": "^0.1.0", "@remix-run/data-table-postgres": "^0.1.0", "@remix-run/data-table-sqlite": "^0.1.0", "@remix-run/fetch-proxy": "^0.7.1", "@remix-run/fetch-router": "^0.17.0", "@remix-run/file-storage": "^0.13.3", "@remix-run/file-storage-s3": "^0.1.0", "@remix-run/form-data-middleware": "^0.1.4", "@remix-run/form-data-parser": "^0.15.0", "@remix-run/fs": "^0.4.2", "@remix-run/headers": "^0.19.0", "@remix-run/html-template": "^0.3.0", "@remix-run/interaction": "^0.5.0", "@remix-run/lazy-file": "^5.0.2", "@remix-run/logger-middleware": "^0.1.3", "@remix-run/method-override-middleware": "^0.1.4", "@remix-run/mime": "^0.4.0", "@remix-run/multipart-parser": "^0.14.2", "@remix-run/node-fetch-server": "^0.13.0", "@remix-run/response": "^0.3.2", "@remix-run/route-pattern": "^0.19.0", "@remix-run/session": "^0.4.1", "@remix-run/session-middleware": "^0.1.4", "@remix-run/session-storage-memcache": "^0.1.0", "@remix-run/session-storage-redis": "^0.1.0", "@remix-run/static-middleware": "^0.4.4", "@remix-run/tar-parser": "^0.7.0" } }, "sha512-RIctAYR7OW3oYzAGclLhgltrRtKviIdnCVwoLcPDicOjV4I2mJ9AEi8YXl2+hGPupzNNEUcrDtoICd7xNuMptg=="],
+    "remix": ["remix@3.0.0-alpha.4", "", { "dependencies": { "@remix-run/async-context-middleware": "^0.2.0", "@remix-run/auth": "^0.1.0", "@remix-run/auth-middleware": "^0.1.0", "@remix-run/component": "^0.6.0", "@remix-run/compression-middleware": "^0.1.4", "@remix-run/cookie": "^0.5.1", "@remix-run/cop-middleware": "^0.1.0", "@remix-run/cors-middleware": "^0.1.0", "@remix-run/csrf-middleware": "^0.1.0", "@remix-run/data-schema": "^0.2.0", "@remix-run/data-table": "^0.2.0", "@remix-run/data-table-mysql": "^0.2.0", "@remix-run/data-table-postgres": "^0.2.0", "@remix-run/data-table-sqlite": "^0.2.0", "@remix-run/fetch-proxy": "^0.7.1", "@remix-run/fetch-router": "^0.18.0", "@remix-run/file-storage": "^0.13.3", "@remix-run/file-storage-s3": "^0.1.0", "@remix-run/form-data-middleware": "^0.2.0", "@remix-run/form-data-parser": "^0.16.0", "@remix-run/fs": "^0.4.2", "@remix-run/headers": "^0.19.0", "@remix-run/html-template": "^0.3.0", "@remix-run/lazy-file": "^5.0.2", "@remix-run/logger-middleware": "^0.1.4", "@remix-run/method-override-middleware": "^0.1.5", "@remix-run/mime": "^0.4.0", "@remix-run/multipart-parser": "^0.15.0", "@remix-run/node-fetch-server": "^0.13.0", "@remix-run/response": "^0.3.2", "@remix-run/route-pattern": "^0.20.0", "@remix-run/session": "^0.4.1", "@remix-run/session-middleware": "^0.2.0", "@remix-run/session-storage-memcache": "^0.1.0", "@remix-run/session-storage-redis": "^0.1.0", "@remix-run/static-middleware": "^0.4.5", "@remix-run/tar-parser": "^0.7.0" } }, "sha512-fvYHPm8QbrbL09wmmddrnknyntB+fl4bLmpECpK32cOoJW3pMoMvESBB1qRAQe+yWgTexVjp5G3BGOmmtYOX0A=="],
 
     "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
 

diff --git a/client/app.tsx b/client/app.tsx
--- a/client/app.tsx
+++ b/client/app.tsx
@@ -1,199 +1,184 @@
-import { type Handle } from 'remix/component'
-import { clientRoutes } from './routes/index.tsx'
-import {
-	getPathname,
-	listenToRouterNavigation,
-	Router,
-} from './client-router.tsx'
-import {
-	fetchSessionInfo,
-	type SessionInfo,
-	type SessionStatus,
-} from './session.ts'
-import { buildAuthLink } from './auth-links.ts'
-import { colors, mq, spacing, typography } from './styles/tokens.ts'
-
+import { css, type Handle } from 'remix/component';
+import { clientRoutes } from './routes/index.tsx';
+import { getPathname, listenToRouterNavigation, Router, } from './client-router.tsx';
+import { fetchSessionInfo, type SessionInfo, type SessionStatus, } from './session.ts';
+import { buildAuthLink } from './auth-links.ts';
+import { colors, mq, spacing, typography } from './styles/tokens.ts';
 export function App(handle: Handle) {
-	let session: SessionInfo | null = null
-	let sessionStatus: SessionStatus = 'idle'
-	let sessionRefreshInFlight = false
-	let sessionRefreshQueued = false
-	let currentPathname = getPathname()
-
-	function queueSessionRefresh() {
-		sessionRefreshQueued = true
-		if (sessionRefreshInFlight) return
-
-		// Preserve current nav state during refreshes after first load.
-		if (sessionStatus === 'idle') {
-			sessionStatus = 'loading'
-			handle.update()
-		}
-
-		sessionRefreshQueued = false
-		sessionRefreshInFlight = true
-		handle.queueTask(async (signal) => {
-			const nextSession = await fetchSessionInfo(signal)
-			sessionRefreshInFlight = false
-			if (signal.aborted) return
-			session = nextSession
-			sessionStatus = 'ready'
-			handle.update()
-			if (sessionRefreshQueued) {
-				queueSessionRefresh()
-			}
-		})
-		if (sessionStatus !== 'loading') {
-			handle.update()
-		}
-	}
-
-	handle.queueTask(() => {
-		queueSessionRefresh()
-	})
-	listenToRouterNavigation(handle, () => {
-		currentPathname = getPathname()
-		queueSessionRefresh()
-		handle.update()
-	})
-
-	const navLinkCss = {
-		color: colors.primaryText,
-		fontWeight: typography.fontWeight.medium,
-		textDecoration: 'none',
-		'&:hover': {
-			textDecoration: 'underline',
-		},
-	}
-
-	const navHomeLinkCss = {
-		...navLinkCss,
-		display: 'flex',
-		alignItems: 'center',
-		lineHeight: 0,
-		'&:hover': {
-			textDecoration: 'none',
-			opacity: 0.85,
-		},
-	}
-
-	const logOutButtonCss = {
-		padding: `${spacing.xs} ${spacing.md}`,
-		borderRadius: '999px',
-		border: `1px solid ${colors.border}`,
-		backgroundColor: 'transparent',
-		color: colors.text,
-		fontWeight: typography.fontWeight.medium,
-		cursor: 'pointer',
-	}
-
-	return () => {
-		const isChatLayout = currentPathname.startsWith('/chat')
-		const sessionEmail = session?.email ?? ''
-		const isSessionReady = sessionStatus === 'ready'
-		const isLoggedIn = isSessionReady && Boolean(sessionEmail)
-		const showAuthLinks = isSessionReady && !isLoggedIn
-		const oauthRedirectTo =
-			typeof window !== 'undefined' && currentPathname === '/oauth/authorize'
-				? `${currentPathname}${window.location.search}`
-				: null
-		const loginHref = buildAuthLink('/login', oauthRedirectTo)
-		const signupHref = buildAuthLink('/signup', oauthRedirectTo)
-
-		return (
-			<main
-				css={{
-					maxWidth: isChatLayout ? 'none' : '52rem',
-					width: '100%',
-					margin: isChatLayout ? 0 : '0 auto',
-					padding: isChatLayout
-						? `${spacing.lg} ${spacing.xl} ${spacing.sm}`
-						: spacing['2xl'],
-					minHeight: isChatLayout ? '100vh' : undefined,
-					fontFamily: typography.fontFamily,
-					boxSizing: 'border-box',
-					[mq.tablet]: {
-						padding: isChatLayout
-							? `${spacing.sm} ${spacing.sm} 0`
-							: spacing.md,
-					},
-				}}
-			>
-				<nav
-					css={{
-						display: 'flex',
-						alignItems: 'center',
-						gap: spacing.md,
-						flexWrap: 'wrap',
-						marginBottom: isChatLayout ? spacing.lg : spacing.xl,
-						[mq.tablet]: {
-							gap: spacing.sm,
-							marginBottom: isChatLayout ? spacing.sm : spacing.md,
-						},
-					}}
-				>
-					<a href="/" css={navHomeLinkCss} aria-label="Home">
-						<img
-							src="/logo.png"
-							alt=""
-							width={112}
-							height={28}
-							css={{
-								display: 'block',
-								height: '1.35em',
-								width: 'auto',
-							}}
-						/>
+    let session: SessionInfo | null = null;
+    let sessionStatus: SessionStatus = 'idle';
+    let sessionRefreshInFlight = false;
+    let sessionRefreshQueued = false;
+    let currentPathname = getPathname();
+    function queueSessionRefresh() {
+        sessionRefreshQueued = true;
+        if (sessionRefreshInFlight)
+            return;
+        // Preserve current nav state during refreshes after first load.
+        if (sessionStatus === 'idle') {
+            sessionStatus = 'loading';
+            handle.update();
+        }
+        sessionRefreshQueued = false;
+        sessionRefreshInFlight = true;
+        handle.queueTask(async (signal) => {
+            const nextSession = await fetchSessionInfo(signal);
+            sessionRefreshInFlight = false;
+            if (signal.aborted)
+                return;
+            session = nextSession;
+            sessionStatus = 'ready';
+            handle.update();
+            if (sessionRefreshQueued) {
+                queueSessionRefresh();
+            }
+        });
+        if (sessionStatus !== 'loading') {
+            handle.update();
+        }
+    }
+    handle.queueTask(() => {
+        queueSessionRefresh();
+    });
+    listenToRouterNavigation(handle, () => {
+        currentPathname = getPathname();
+        queueSessionRefresh();
+        handle.update();
+    });
+    const navLinkCss = {
+        color: colors.primaryText,
+        fontWeight: typography.fontWeight.medium,
+        textDecoration: 'none',
+        '&:hover': {
+            textDecoration: 'underline',
+        },
+    };
+    const navHomeLinkCss = {
+        ...navLinkCss,
+        display: 'flex',
+        alignItems: 'center',
+        lineHeight: 0,
+        '&:hover': {
+            textDecoration: 'none',
+            opacity: 0.85,
+        },
+    };
+    const logOutButtonCss = {
+        padding: `${spacing.xs} ${spacing.md}`,
+        borderRadius: '999px',
+        border: `1px solid ${colors.border}`,
+        backgroundColor: 'transparent',
+        color: colors.text,
+        fontWeight: typography.fontWeight.medium,
+        cursor: 'pointer',
+    };
+    return () => {
+        const isChatLayout = currentPathname.startsWith('/chat');
+        const sessionEmail = session?.email ?? '';
+        const isSessionReady = sessionStatus === 'ready';
+        const isLoggedIn = isSessionReady && Boolean(sessionEmail);
+        const showAuthLinks = isSessionReady && !isLoggedIn;
+        const oauthRedirectTo = typeof window !== 'undefined' && currentPathname === '/oauth/authorize'
+            ? `${currentPathname}${window.location.search}`
+            : null;
+        const loginHref = buildAuthLink('/login', oauthRedirectTo);
+        const signupHref = buildAuthLink('/signup', oauthRedirectTo);
+        return (<main mix={[
+            css({
+                maxWidth: isChatLayout ? 'none' : '52rem',
+                width: '100%',
+                margin: isChatLayout ? 0 : '0 auto',
+                padding: isChatLayout
+                    ? `${spacing.lg} ${spacing.xl} ${spacing.sm}`
+                    : spacing['2xl'],
+                minHeight: isChatLayout ? '100vh' : undefined,
+                fontFamily: typography.fontFamily,
+                boxSizing: 'border-box',
+                [mq.tablet]: {
+                    padding: isChatLayout
+                        ? `${spacing.sm} ${spacing.sm} 0`
+                        : spacing.md,
+                },
+            })
+        ]}>
+				<nav mix={[
+            css({
+                display: 'flex',
+                alignItems: 'center',
+                gap: spacing.md,
+                flexWrap: 'wrap',
+                marginBottom: isChatLayout ? spacing.lg : spacing.xl,
+                [mq.tablet]: {
+                    gap: spacing.sm,
+                    marginBottom: isChatLayout ? spacing.sm : spacing.md,
+                },
+            })
+        ]}>
+					<a href="/" aria-label="Home" mix={[
+            css(navHomeLinkCss)
+        ]}>
+						<img src="/logo.png" alt="" width={112} height={28} mix={[
+            css({
+                display: 'block',
+                height: '1.35em',
+                width: 'auto',
+            })
+        ]}/>
 					</a>
-					{showAuthLinks ? (
-						<>
-							<a href={loginHref} css={navLinkCss}>
+					{showAuthLinks ? (<>
+							<a href={loginHref} mix={[
+                css(navLinkCss)
+            ]}>
 								Login
 							</a>
-							<a href={signupHref} css={navLinkCss}>
+							<a href={signupHref} mix={[
+                css(navLinkCss)
+            ]}>
 								Signup
 							</a>
-						</>
-					) : null}
-					{isLoggedIn ? (
-						<>
-							<a href="/chat" css={navLinkCss}>
+						</>) : null}
+					{isLoggedIn ? (<>
+							<a href="/chat" mix={[
+                css(navLinkCss)
+            ]}>
 								Chat
 							</a>
-							<a href="/account" css={navLinkCss}>
+							<a href="/account" mix={[
+                css(navLinkCss)
+            ]}>
 								{sessionEmail}
 							</a>
-							<form method="post" action="/logout" css={{ margin: 0 }}>
-								<button type="submit" css={logOutButtonCss}>
+							<form method="post" action="/logout" mix={[
+                css({ margin: 0 })
+            ]}>
+								<button type="submit" mix={[
+                css(logOutButtonCss)
+            ]}>
 									Log out
 								</button>
 							</form>
-						</>
-					) : null}
+						</>) : null}
 				</nav>
-				<Router
-					setup={{
-						routes: clientRoutes,
-						fallback: (
-							<section>
-								<h2
-									css={{
-										fontSize: typography.fontSize.lg,
-										fontWeight: typography.fontWeight.semibold,
-										marginBottom: spacing.sm,
-										color: colors.text,
-									}}
-								>
+				<Router setup={{
+                routes: clientRoutes,
+                fallback: (<section>
+								<h2 mix={[
+                    css({
+                        fontSize: typography.fontSize.lg,
+                        fontWeight: typography.fontWeight.semibold,
+                        marginBottom: spacing.sm,
+                        color: colors.text,
+                    })
+                ]}>
 									Not Found
 								</h2>
-								<p css={{ color: colors.textMuted }}>
+								<p mix={[
+                    css({ color: colors.textMuted })
+                ]}>
 									That route does not exist.
 								</p>
-							</section>
-						),
-					}}
-				/>
-			</main>
-		)
-	}
+							</section>),
+            }}/>
+			</main>);
+    };
 }

diff --git a/client/client-router.tsx b/client/client-router.tsx
--- a/client/client-router.tsx
+++ b/client/client-router.tsx
@@ -1,4 +1,4 @@
-import { type Handle } from 'remix/component'
+import { addEventListeners, type Handle } from 'remix/component'
 
 type RouterSetup = {
 	routes: Record<string, JSX.Element>
@@ -251,7 +251,7 @@
 
 export function listenToRouterNavigation(handle: Handle, listener: () => void) {
 	ensureRouter()
-	handle.on(routerEvents, {
+	addEventListeners(routerEvents, handle.signal, {
 		navigate: () => listener(),
 	})
 }

diff --git a/client/counter.tsx b/client/counter.tsx
--- a/client/counter.tsx
+++ b/client/counter.tsx
@@ -1,49 +1,36 @@
-import { type Handle } from 'remix/component'
-import {
-	colors,
-	radius,
-	spacing,
-	transitions,
-	typography,
-} from './styles/tokens.ts'
-
+import { css, type Handle, on } from 'remix/component';
+import { colors, radius, spacing, transitions, typography, } from './styles/tokens.ts';
 type CounterSetup = {
-	initial?: number
-}
-
+    initial?: number;
+};
 export function Counter(handle: Handle, setup: CounterSetup = {}) {
-	let count = setup.initial ?? 0
-
-	function increment() {
-		count += 1
-		handle.update()
-	}
-
-	return () => (
-		<button
-			type="button"
-			css={{
-				padding: `${spacing.sm} ${spacing.lg}`,
-				borderRadius: radius.full,
-				border: `1px solid ${colors.border}`,
-				backgroundColor: colors.primary,
-				color: colors.onPrimary,
-				fontSize: typography.fontSize.base,
-				fontWeight: typography.fontWeight.semibold,
-				cursor: 'pointer',
-				transition: `transform ${transitions.fast}, background-color ${transitions.normal}`,
-				'&:hover': {
-					backgroundColor: colors.primaryHover,
-					transform: 'translateY(-1px)',
-				},
-				'&:active': {
-					backgroundColor: colors.primaryActive,
-					transform: 'translateY(0)',
-				},
-			}}
-			on={{ click: increment }}
-		>
+    let count = setup.initial ?? 0;
+    function increment() {
+        count += 1;
+        handle.update();
+    }
+    return () => (<button type="button" mix={[
+        css({
+            padding: `${spacing.sm} ${spacing.lg}`,
+            borderRadius: radius.full,
+            border: `1px solid ${colors.border}`,
+            backgroundColor: colors.primary,
+            color: colors.onPrimary,
+            fontSize: typography.fontSize.base,
+            fontWeight: typography.fontWeight.semibold,
+            cursor: 'pointer',
+            transition: `transform ${transitions.fast}, background-color ${transitions.normal}`,
+            '&:hover': {
+                backgroundColor: colors.primaryHover,
+                transform: 'translateY(-1px)',
+            },
+            '&:active': {
+                backgroundColor: colors.primaryActive,
+                transform: 'translateY(0)',
+            },
+        }),
+        on("click", increment)
+    ]}>
 			Count: {count}
-		</button>
-	)
+		</button>);
 }

diff --git a/client/double-check.ts b/client/double-check.ts
--- a/client/double-check.ts
+++ b/client/double-check.ts
@@ -1,26 +1,14 @@
 import { type Handle } from 'remix/component'
 
-type BlurHandler = (event: FocusEvent) => void
-type ClickHandler = (event: MouseEvent) => void
-
 type ButtonLikeProps = {
+	mix?: Array<unknown>
+	onConfirm?: (event: MouseEvent) => void
 	on?: {
-		blur?: BlurHandler
-		click?: ClickHandler
+		click?: (event: MouseEvent) => void
 	}
 	[key: string]: unknown
 }
 
-function callAll<Event>(
-	...handlers: Array<((event: Event) => void) | undefined>
-) {
-	return (event: Event) => {
-		for (const handler of handlers) {
-			handler?.(event)
-		}
-	}
-}
-
 export function createDoubleCheck(handle: Handle) {
 	let doubleCheck = false
 
@@ -39,29 +27,36 @@
 		},
 		getButtonProps<Props extends ButtonLikeProps>(props?: Props): Props {
 			const buttonProps = props ?? ({} as Props)
+			const {
+				mix: inputMix,
+				onConfirm,
+				on: onOverrides,
+				...rest
+			} = buttonProps as ButtonLikeProps
+			const mix = [...(inputMix ?? [])]
+			const confirmHandler = onConfirm ?? onOverrides?.click
 
-			const onBlur: BlurHandler = () => {
-				setDoubleCheck(false)
-			}
+			mix.push({
+				handleEvent(handle) {
+					handle.addEventListener('blur', () => {
+						setDoubleCheck(false)
+					})
 
-			const onClick: ClickHandler = (event) => {
-				if (!doubleCheck) {
-					event.preventDefault()
-					setDoubleCheck(true)
-					return
-				}
+					handle.addEventListener('click', (event) => {
+						if (!doubleCheck) {
+							event.preventDefault()
+							setDoubleCheck(true)
+							return
+						}
+						setDoubleCheck(false)
+						confirmHandler?.(event)
+					})
+				},
+			})
 
-				buttonProps.on?.click?.(event)
-				setDoubleCheck(false)
-			}
-
 			return {
-				...buttonProps,
-				on: {
-					...buttonProps.on,
-					blur: callAll(onBlur, buttonProps.on?.blur),
-					click: onClick,
-				},
+				...rest,
+				mix,
 			}
 		},
 	}

diff --git a/client/editable-text.tsx b/client/editable-text.tsx
--- a/client/editable-text.tsx
+++ b/client/editable-text.tsx
@@ -1,171 +1,148 @@
-import { type Handle } from 'remix/component'
-
+import { css, type Handle, on } from 'remix/component';
 type EditableTextProps = {
-	id: string
-	ariaLabel: string
-	value: string
-	emptyText?: string
-	buttonCss?: Record<string, unknown>
-	inputCss?: Record<string, unknown>
-	onSave: (value: string) => Promise<boolean> | boolean
-}
-
+    id: string;
+    ariaLabel: string;
+    value: string;
+    emptyText?: string;
+    buttonCss?: Record<string, unknown>;
+    inputCss?: Record<string, unknown>;
+    onSave: (value: string) => Promise<boolean> | boolean;
+};
 const inheritTextStyles = {
-	fontSize: 'inherit',
-	fontStyle: 'inherit',
-	fontWeight: 'inherit',
-	fontFamily: 'inherit',
-	textAlign: 'inherit',
-	lineHeight: 'inherit',
-	color: 'inherit',
-} as const
-
+    fontSize: 'inherit',
+    fontStyle: 'inherit',
+    fontWeight: 'inherit',
+    fontFamily: 'inherit',
+    textAlign: 'inherit',
+    lineHeight: 'inherit',
+    color: 'inherit',
+} as const;
 export function EditableText(handle: Handle) {
-	let isEditing = false
-	let draftValue = ''
-	let isSaving = false
-
-	function focusInput(inputId: string) {
-		void handle.queueTask(async () => {
-			const input = document.getElementById(inputId)
-			if (!(input instanceof HTMLInputElement)) return
-			input.focus()
-			input.select()
-		})
-	}
-
-	function focusButton(buttonId: string) {
-		void handle.queueTask(async () => {
-			const button = document.getElementById(buttonId)
-			if (!(button instanceof HTMLButtonElement)) return
-			button.focus()
-		})
-	}
-
-	return (props: EditableTextProps) => {
-		const buttonId = `${props.id}-button`
-
-		function startEditing() {
-			if (isSaving) return
-			draftValue = props.value
-			isEditing = true
-			handle.update()
-			focusInput(props.id)
-		}
-
-		function cancelEditing() {
-			if (isSaving) return
-			draftValue = props.value
-			isEditing = false
-			handle.update()
-			focusButton(buttonId)
-		}
-
-		async function submitEditing(event: SubmitEvent) {
-			event.preventDefault()
-			if (isSaving) return
-			const nextValue = draftValue.trim()
-			if (!nextValue) return
-
-			isSaving = true
-			handle.update()
-			let didSave = false
-			try {
-				didSave = await props.onSave(nextValue)
-			} catch (error) {
-				isSaving = false
-				handle.update()
-				throw error
-			}
-			isSaving = false
-			if (!didSave) {
-				handle.update()
-				return
-			}
-
-			isEditing = false
-			handle.update()
... diff truncated: showing 800 of 27929 lines
Open in Web Open in Cursor 

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Destructured on property shadows imported on function
    • Renamed the destructured on property to avoid shadowing the imported on helper, preventing runtime crashes when no on override is provided.
Preview (724bba772f)
diff --git a/bun.lock b/bun.lock
--- a/bun.lock
+++ b/bun.lock
@@ -13,7 +13,7 @@
         "@modelcontextprotocol/sdk": "1.26.0",
         "agents": "^0.7.6",
         "get-port": "^7.1.0",
-        "remix": "3.0.0-alpha.3",
+        "remix": "3.0.0-alpha.4",
         "workers-ai-provider": "^3.1.2",
         "zod": "^4.3.6",
       },
@@ -306,35 +306,45 @@
 
     "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="],
 
-    "@remix-run/async-context-middleware": ["@remix-run/async-context-middleware@0.1.3", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0" } }, "sha512-z2hTnQspCWsKka1QxAu5qf+IRbMYwTBmwL28zCk8nIPcYxG19rDbBeRmfQZ9hxHes72XVDu3Fd3ArLTy8bivdw=="],
+    "@remix-run/async-context-middleware": ["@remix-run/async-context-middleware@0.2.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-ebnPM2OFM/BO3GSoRKs2i5+lgY/fIunzpL8/RhuEuP9a04cQYXMbEzbdae+qBHTgtsVdPrqeGr/lwlF5FNnQZA=="],
 
-    "@remix-run/component": ["@remix-run/component@0.5.0", "", { "dependencies": { "@remix-run/interaction": "^0.5.0" } }, "sha512-xRLOcgwWKZxFdj63bWi3/snC2uxkm978B49EGEv1/G43iBFksYjS4ADanfYxREvQjMlaCqSUH0ZTZsJbGsz3PQ=="],
+    "@remix-run/auth": ["@remix-run/auth@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-TFsK40jymT7iZyGWOjoqlFFmILdDXy3jYBoI1mohjdqrp8SVWbm7htiSbdd+j8DGBUEbxMTvKcaVG0bPA1M8VA=="],
 
-    "@remix-run/compression-middleware": ["@remix-run/compression-middleware@0.1.3", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-pKsKtIzW/yjCbftSQRYfvnKKdq65Nu+YEhbQooiXtmd1Ub4inHSrzeA/vca3sQkBnBr/eRA9uujxIszRe+6LAg=="],
+    "@remix-run/auth-middleware": ["@remix-run/auth-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-QZlI4MyU8VTVI6WB4T4MWWqtbYrIUiiBG3mgdJUXjEYMBBoxVAkT/6rT3YErmhX+SVARP2zC46qzujrbYcQLCQ=="],
 
+    "@remix-run/component": ["@remix-run/component@0.6.0", "", { "dependencies": { "@types/dom-navigation": "^1.0.7" } }, "sha512-hmIUdPnBtONeOH8bbLuOVGSgFD/MAUNvpH0Xwgo27GByBIjQhOm25Tt9R04HQslDhHAzwNVnWeQkr3j9eyXYhQ=="],
+
+    "@remix-run/compression-middleware": ["@remix-run/compression-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-vE+d4a04Tq9fO5KBmNtMD5GKJEt6U++B1yufEdBMNe5ROBQkpPWSuKlhkRcZyoV8SPEI2HbkKMtQ8QXfGJR11g=="],
+
     "@remix-run/cookie": ["@remix-run/cookie@0.5.1", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-gbeZfVd1AKRlFj3IJWcIcR6zqVGz2XGJhR+mcqYiWnYt6KM8oUGtc82dsc4qZnWWA1f0nM4/He9wrU4GjB0pag=="],
 
-    "@remix-run/data-schema": ["@remix-run/data-schema@0.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0" } }, "sha512-PzpYP9P19cb8bS7Y9+MSxyAWGy0n13sx1lYOMoKI+iEx7pdB2ZLaiidwcn1l6AeR6gjVIpuzyM6/UONG3bzboA=="],
+    "@remix-run/cop-middleware": ["@remix-run/cop-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-5rMNrBswTJJLPsu1nUfXubgVf7XBTRfPNfd/4QQHaXRdhXVWVxU9HbNJrOjd7iNbBWuZeaJxSdDepbQLnuBkTg=="],
 
-    "@remix-run/data-table": ["@remix-run/data-table@0.1.0", "", { "dependencies": { "@remix-run/data-schema": "^0.1.0" } }, "sha512-u70INxiF9pThV2LcP7y37G7eq16OXWrA+HXS8qNplQgJJd1zhwZrODOtfF9U+PKLiWQkBj+bEeh7QhOlvGsVDg=="],
+    "@remix-run/cors-middleware": ["@remix-run/cors-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/headers": "^0.19.0" } }, "sha512-x5nyxDhWEOnznqetDhtj5WYoHrKpbiZ8K6BCGQ2+DFjLYHoyWCayciUNrqOVSIWc5XdV6KlKHcg1TyQvW/cS4w=="],
 
-    "@remix-run/data-table-mysql": ["@remix-run/data-table-mysql@0.1.0", "", { "dependencies": { "@remix-run/data-table": "^0.1.0" }, "peerDependencies": { "mysql2": "^3.15.3" }, "optionalPeers": ["mysql2"] }, "sha512-lzghxTYZDHODNisIJWkq4IkcGsb1pUrp9WGtlNEVWrXWC6aArOiojR3PW4arFwNG5ddKJPRfwot+ySc2YZy5RQ=="],
+    "@remix-run/csrf-middleware": ["@remix-run/csrf-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-EZAV9UZrx3qdYu3ov9NgFVJchw6kOLYI2uRB+e3WBVW1phruHKc42EpdJIZ6fU0J9Of0Lfo4X4VTM7O1/u1cWw=="],
 
-    "@remix-run/data-table-postgres": ["@remix-run/data-table-postgres@0.1.0", "", { "dependencies": { "@remix-run/data-table": "^0.1.0" }, "peerDependencies": { "pg": "^8.16.3" }, "optionalPeers": ["pg"] }, "sha512-mqzARY5tOFVLjFAArryuLQ93M8IdjVnLKnD1VQyJHFbHQF7Zbr8+exv7Hp2hZ1TIGAhZiepGjjC8Re631yJ1Jw=="],
+    "@remix-run/data-schema": ["@remix-run/data-schema@0.2.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0" } }, "sha512-bXgdWcWZS6TkbVO9Piwn035iw0LczfSTFrCW4Vd1k57x/4j3eWZwfHBr3lXdw5whefacs3m3pVZ8RUiu8NtBeA=="],
 
-    "@remix-run/data-table-sqlite": ["@remix-run/data-table-sqlite@0.1.0", "", { "dependencies": { "@remix-run/data-table": "^0.1.0" }, "peerDependencies": { "better-sqlite3": "^12.4.1" }, "optionalPeers": ["better-sqlite3"] }, "sha512-hFtmz9haMr3p/aFWL5D1zpJsgAnNdLVCF6HvXBmtK4m3NdLmM1eNhblpB/SfPGNEODsmx4Jvcaof57dfaLuKgA=="],
+    "@remix-run/data-table": ["@remix-run/data-table@0.2.0", "", {}, "sha512-6QbdlKER0F0rX3XkTHC7ZPBgu/dyGvvgrKRbraBD9iGHTE7AED1emo3/YWsDJUeFzy7INNJaUIpNm9jNcgL8Kg=="],
 
+    "@remix-run/data-table-mysql": ["@remix-run/data-table-mysql@0.2.0", "", { "dependencies": { "@remix-run/data-table": "^0.2.0" }, "peerDependencies": { "mysql2": "^3.15.3" }, "optionalPeers": ["mysql2"] }, "sha512-voTcYUjG12PWlrWIYSqoa+C3WK9Q6TVKPrqnvGKY9s6EVBil9e+glaP2JOM8BbzL0hGuKPifdbrDGfGpGitdQw=="],
+
+    "@remix-run/data-table-postgres": ["@remix-run/data-table-postgres@0.2.0", "", { "dependencies": { "@remix-run/data-table": "^0.2.0" }, "peerDependencies": { "pg": "^8.16.3" }, "optionalPeers": ["pg"] }, "sha512-03v+zpWL4lMJr34PZ76PWFpjIa4K6Uq3NLBqyvN4ojPpm6J7TGsFwPy6sInRm3HjoS4gPSCEmmQ7dUovWa58EA=="],
+
+    "@remix-run/data-table-sqlite": ["@remix-run/data-table-sqlite@0.2.0", "", { "dependencies": { "@remix-run/data-table": "^0.2.0" }, "peerDependencies": { "better-sqlite3": "^12.4.1" }, "optionalPeers": ["better-sqlite3"] }, "sha512-TAQ0u8YnmO3WdnuZULPhc+fgZbKZZPutXiPLf8c9NDhXSC41FlcvfQHTKUxBPVhqdi9mhGiblHHNp6BsVu11dg=="],
+
     "@remix-run/fetch-proxy": ["@remix-run/fetch-proxy@0.7.1", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-rPLfOpAaCXtm1dLI45uIPKERNbXbrh0P9AJc1sliz8pWd/McaFYjdr5KzB4QrFSfPvEt/Wmy6F2521qB1kK0ug=="],
 
-    "@remix-run/fetch-router": ["@remix-run/fetch-router@0.17.0", "", { "dependencies": { "@remix-run/route-pattern": "^0.19.0", "@remix-run/session": "^0.4.1" } }, "sha512-3FeJGrTqrKKCvZdQWijbCXTEHKcdttkLFbI2ogfpZ+iDYSNZ9036wgDXuuoZqg6d+D0E8Unhk5ZwrLKDCd/hOw=="],
+    "@remix-run/fetch-router": ["@remix-run/fetch-router@0.18.0", "", { "dependencies": { "@remix-run/route-pattern": "^0.20.0" } }, "sha512-9Z4JgLH9/jD8jiVvAY9LZR+VoZxPJOQ7pENTBJoSo91PZOkPXfCxQWhPhAwYCP8z+/0FV4ZWSg1DmPPoU2f0UQ=="],
 
     "@remix-run/file-storage": ["@remix-run/file-storage@0.13.3", "", { "dependencies": { "@remix-run/fs": "^0.4.2", "@remix-run/lazy-file": "^5.0.2" } }, "sha512-HBDz9RRsFRvI6EoeasklxH/NleGy0QZBXBcA4gQBW8ueucop21TQI4wvGlhZmXcnJ3nP4RkhdF2Gff2/HD5eiA=="],
 
     "@remix-run/file-storage-s3": ["@remix-run/file-storage-s3@0.1.0", "", { "dependencies": { "@remix-run/file-storage": "^0.13.3", "aws4fetch": "^1.0.20" } }, "sha512-r80An7nSFidK/0xn9O9/HxfUcgxVpM4kprnTGr6pGhKdgbaTCEtA+U5ETYGfeedFxhDcT+7ue+4Fv/VxeIvFwQ=="],
 
-    "@remix-run/form-data-middleware": ["@remix-run/form-data-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0", "@remix-run/form-data-parser": "^0.15.0" } }, "sha512-WZfP1U6lDoipkfjcd0V39HJeTPMTX2WyaPcOBTbBHS0kapIZiHYm6RpGLhE8U58652i3TBh/zzvAczJIbFV2AA=="],
+    "@remix-run/form-data-middleware": ["@remix-run/form-data-middleware@0.2.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/form-data-parser": "^0.16.0" } }, "sha512-oaBcYvyP/U2GrDdFyQUh2pNCshvKwPFBoUup1Bz3NHnMzym4OkZJgaBi4o7HIJC33QudnqI7Mp7TW1pRg0CvIA=="],
 
-    "@remix-run/form-data-parser": ["@remix-run/form-data-parser@0.15.0", "", { "dependencies": { "@remix-run/multipart-parser": "^0.14.2" } }, "sha512-sQP4r9218TWmow6Nt252VjKE674dRi4Z8WTnWUxJJG8I/qNfnGZubZ8LgyE0dR9z1gfaEpkd19MfYMLiOTOkJQ=="],
+    "@remix-run/form-data-parser": ["@remix-run/form-data-parser@0.16.0", "", { "dependencies": { "@remix-run/multipart-parser": "^0.15.0" } }, "sha512-k4QCgCyPURpqe+9Rual1GBJ8Ab6ri82Clfh5ooxq03jKQ1TyQZT5xh+XP0R05+Bqg5bzAZl7r2BpmDEvfoRTWw=="],
 
     "@remix-run/fs": ["@remix-run/fs@0.4.2", "", { "dependencies": { "@remix-run/lazy-file": "^5.0.2", "@remix-run/mime": "^0.4.0" } }, "sha512-z3W2L+iUwgZ7i0S379SYQ8veOe2Weqs+JajmyTCqSVzbmMUniH3qQ6SAYr3FjbrKtLLWHN3SpK4XtFv57VzbLA=="],
 
@@ -342,33 +352,31 @@
 
     "@remix-run/html-template": ["@remix-run/html-template@0.3.0", "", {}, "sha512-aAMx68udtIk0fmCpCXHYscVeCDsRVEmEgh4XvtusPr3vkHu3jn4gx5oAxgsPXPdDmmD/d75SYyI0m/F+aLz5iQ=="],
 
-    "@remix-run/interaction": ["@remix-run/interaction@0.5.0", "", {}, "sha512-Z2ja9/7TfMHt/wzWq425GI2xj6QxW4E3OHZ8In81uytZKIuWaI6Pn3v8qyMrInwnBEaLcfcbeQVCiExgHU8D4A=="],
-
     "@remix-run/lazy-file": ["@remix-run/lazy-file@5.0.2", "", { "dependencies": { "@remix-run/mime": "^0.4.0" } }, "sha512-52Bo5dTV+EDwrUMS3mjeR+Sly85aHeN3fnNTeaflqzlCMWJwr2pX+y6/3mTDtRdxmTWF1MGQAoeayzfPb4zZJg=="],
 
-    "@remix-run/logger-middleware": ["@remix-run/logger-middleware@0.1.3", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0" } }, "sha512-6gxz1XC2lJYQS3Oz1pZzxpuoLowwd2PSpimMaQnkk0fZ7hHYxx7uV+FSs2Z3fue6kYvZ+IxSUe8Wy52V2r4LxA=="],
+    "@remix-run/logger-middleware": ["@remix-run/logger-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-gFDyYn8o5ddjQoEbrc8CK6PPq3lzrOX6BCqlvM+QVTMJ5/2aHgfMbQdXnWtbeiWJJrpODpABrSNfjLkHjaK4og=="],
 
-    "@remix-run/method-override-middleware": ["@remix-run/method-override-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0" } }, "sha512-gYFsdY0eIStTpsqGnF/22YracUmS8cZlef6KsBKOVf1nOI9wwwbRrj/DWLMQsWt22YSBMuPYZW5NLKEmXvJRZw=="],
+    "@remix-run/method-override-middleware": ["@remix-run/method-override-middleware@0.1.5", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-HYQG6YR4l0rYMJ0MXth9SWfeSCwnqHIrWgMQpZHKS/MT7roX7EAfknrl9vzjQWsVfkwl6RrSD30SrLPONj3tsg=="],
 
     "@remix-run/mime": ["@remix-run/mime@0.4.0", "", {}, "sha512-O6TcTL6CtuX82Q8BHqAere5O+0hYcrzSgY9whsDOBuqbW753Rczprs2jYw3qCDSo0kLxykW4ys3qgZcdgZ+chw=="],
 
-    "@remix-run/multipart-parser": ["@remix-run/multipart-parser@0.14.2", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-yDq9ql4Xz92bRG/Sgl4cg2dRlxxC6A40XBy/oyDhy76hJtTQvgyzx9sfPXYPxcfL1BtqljC+sYHE0PvjmQhSfw=="],
+    "@remix-run/multipart-parser": ["@remix-run/multipart-parser@0.15.0", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-/Ugo6k2bN7gh7Ybyhe7R/NrkD075fHrEfVf17P+NNC9rlWHBQCOSyJ4V8n4wtoG8umeImagU/AuHuUuTwzvHww=="],
 
     "@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.13.0", "", {}, "sha512-1EsNo0ZpgXu/90AWoRZf/oE3RVTUS80tiTUpt+hv5pjtAkw7icN4WskDwz/KdAw5ARbJLMhZBrO1NqThmy/McA=="],
 
     "@remix-run/response": ["@remix-run/response@0.3.2", "", { "dependencies": { "@remix-run/headers": "^0.19.0", "@remix-run/html-template": "^0.3.0", "@remix-run/mime": "^0.4.0" } }, "sha512-GkFqVq5E7Do6rMKTBjgoNyJlrsLrqYg+TDlCDrXoZ3v8O2RlSI14+bCF8lGQHy15DWX2pizVj6R0e6NmjcuLuA=="],
 
-    "@remix-run/route-pattern": ["@remix-run/route-pattern@0.19.0", "", {}, "sha512-RXKaIJ2Lx01uyZc0iw+yLzowFCa1/NuB8jN7QTo4QUe2CaUGtvPGdhgrTUp75lyNNCSJIrM9SaAJ6c1pjZdmoA=="],
+    "@remix-run/route-pattern": ["@remix-run/route-pattern@0.20.0", "", {}, "sha512-TEdJ5eFn40St26oyaRYGI1FWeXDvlEzkbvollM8Xit0qIuDFyFG03Okvvfc1s8KgR9sYULDPjxJIzk6xvIRR9A=="],
 
     "@remix-run/session": ["@remix-run/session@0.4.1", "", {}, "sha512-Bm6aKYgutb/raHZ3laloz8g/Qu7f3CeK3o4gUVDMxtEiAdWCzJamwHoTpGOc5+g1Kuy7z85v4M6nGrF06MFDSg=="],
 
-    "@remix-run/session-middleware": ["@remix-run/session-middleware@0.1.4", "", { "dependencies": { "@remix-run/cookie": "^0.5.1", "@remix-run/fetch-router": "^0.17.0", "@remix-run/session": "^0.4.1" } }, "sha512-qqLmf7mG88h+Ge8pWiJMO8+t9nfQMuO/Zx2W68IwB7Cpt+b6PDpB++i3dd/KLlsjJ43XPMoT2ydmo+eQMgBX3g=="],
+    "@remix-run/session-middleware": ["@remix-run/session-middleware@0.2.0", "", { "dependencies": { "@remix-run/cookie": "^0.5.1", "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-kRAr0inELyXeOnUhlaM/eKIe9x1RZsOGbglb7Keb1+Lf6KxWxNMwv1ymmHBfae2ossjQTN/QUaABwQgX/uk3DQ=="],
 
     "@remix-run/session-storage-memcache": ["@remix-run/session-storage-memcache@0.1.0", "", { "dependencies": { "@remix-run/session": "^0.4.1" } }, "sha512-k853rpHncdTJUwdk0hqd+gZ2OONZLNdOUJBKdJB+MehxrVv1TtacDnA+Xs3kh+IVwUrsTmBhED+GHSUocMATUg=="],
 
     "@remix-run/session-storage-redis": ["@remix-run/session-storage-redis@0.1.0", "", { "dependencies": { "@remix-run/session": "^0.4.1" }, "peerDependencies": { "redis": "^5.10.0" }, "optionalPeers": ["redis"] }, "sha512-MovUS1E98wDHP8zsESJGm3ySB7iiOhd+3usxyXXM2sbF9gIe6r1bdAXXirGIoC8AEq1v8IiFE5u5ipo7PX0UHQ=="],
 
-    "@remix-run/static-middleware": ["@remix-run/static-middleware@0.4.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0", "@remix-run/fs": "^0.4.2", "@remix-run/html-template": "^0.3.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-aL5ngFG57uPXTEDaH0uP/cKDpYkLMTtmPjK+SR1ugS654ORk8WTD4Ajf56QekMykCvCnO6PkgFAruUyKkwDNMg=="],
+    "@remix-run/static-middleware": ["@remix-run/static-middleware@0.4.5", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/fs": "^0.4.2", "@remix-run/html-template": "^0.3.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-jxEbrQMDWcUgmv/2NSv4pahvaW3I4jxqZUMDQ7/VfkcgeJKLFk15vgp4BJ5vLDKKMvwVcgM0Ccbw4Y3Ev2zemA=="],
 
     "@remix-run/tar-parser": ["@remix-run/tar-parser@0.7.0", "", {}, "sha512-PW8JxEUzaGcnqxC5hBI8L9lK/Qz3oad6IGKZ+NExI3L7urVJUux+yCBrsme79DMBgS6hL+lgd/5LPFA5fSwF9A=="],
 
@@ -396,6 +404,8 @@
 
     "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
 
+    "@types/dom-navigation": ["@types/dom-navigation@1.0.7", "", {}, "sha512-Di4W+i2faYquHUnyWUg3bBQp5pTNvjDDA7mIYfD/1WlLgan6sKkeVjGbdL78K0CuNEk5Pfc/c0rfelwkz10mnQ=="],
+
     "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
 
     "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@@ -952,7 +962,7 @@
 
     "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
 
-    "remix": ["remix@3.0.0-alpha.3", "", { "dependencies": { "@remix-run/async-context-middleware": "^0.1.3", "@remix-run/component": "^0.5.0", "@remix-run/compression-middleware": "^0.1.3", "@remix-run/cookie": "^0.5.1", "@remix-run/data-schema": "^0.1.0", "@remix-run/data-table": "^0.1.0", "@remix-run/data-table-mysql": "^0.1.0", "@remix-run/data-table-postgres": "^0.1.0", "@remix-run/data-table-sqlite": "^0.1.0", "@remix-run/fetch-proxy": "^0.7.1", "@remix-run/fetch-router": "^0.17.0", "@remix-run/file-storage": "^0.13.3", "@remix-run/file-storage-s3": "^0.1.0", "@remix-run/form-data-middleware": "^0.1.4", "@remix-run/form-data-parser": "^0.15.0", "@remix-run/fs": "^0.4.2", "@remix-run/headers": "^0.19.0", "@remix-run/html-template": "^0.3.0", "@remix-run/interaction": "^0.5.0", "@remix-run/lazy-file": "^5.0.2", "@remix-run/logger-middleware": "^0.1.3", "@remix-run/method-override-middleware": "^0.1.4", "@remix-run/mime": "^0.4.0", "@remix-run/multipart-parser": "^0.14.2", "@remix-run/node-fetch-server": "^0.13.0", "@remix-run/response": "^0.3.2", "@remix-run/route-pattern": "^0.19.0", "@remix-run/session": "^0.4.1", "@remix-run/session-middleware": "^0.1.4", "@remix-run/session-storage-memcache": "^0.1.0", "@remix-run/session-storage-redis": "^0.1.0", "@remix-run/static-middleware": "^0.4.4", "@remix-run/tar-parser": "^0.7.0" } }, "sha512-RIctAYR7OW3oYzAGclLhgltrRtKviIdnCVwoLcPDicOjV4I2mJ9AEi8YXl2+hGPupzNNEUcrDtoICd7xNuMptg=="],
+    "remix": ["remix@3.0.0-alpha.4", "", { "dependencies": { "@remix-run/async-context-middleware": "^0.2.0", "@remix-run/auth": "^0.1.0", "@remix-run/auth-middleware": "^0.1.0", "@remix-run/component": "^0.6.0", "@remix-run/compression-middleware": "^0.1.4", "@remix-run/cookie": "^0.5.1", "@remix-run/cop-middleware": "^0.1.0", "@remix-run/cors-middleware": "^0.1.0", "@remix-run/csrf-middleware": "^0.1.0", "@remix-run/data-schema": "^0.2.0", "@remix-run/data-table": "^0.2.0", "@remix-run/data-table-mysql": "^0.2.0", "@remix-run/data-table-postgres": "^0.2.0", "@remix-run/data-table-sqlite": "^0.2.0", "@remix-run/fetch-proxy": "^0.7.1", "@remix-run/fetch-router": "^0.18.0", "@remix-run/file-storage": "^0.13.3", "@remix-run/file-storage-s3": "^0.1.0", "@remix-run/form-data-middleware": "^0.2.0", "@remix-run/form-data-parser": "^0.16.0", "@remix-run/fs": "^0.4.2", "@remix-run/headers": "^0.19.0", "@remix-run/html-template": "^0.3.0", "@remix-run/lazy-file": "^5.0.2", "@remix-run/logger-middleware": "^0.1.4", "@remix-run/method-override-middleware": "^0.1.5", "@remix-run/mime": "^0.4.0", "@remix-run/multipart-parser": "^0.15.0", "@remix-run/node-fetch-server": "^0.13.0", "@remix-run/response": "^0.3.2", "@remix-run/route-pattern": "^0.20.0", "@remix-run/session": "^0.4.1", "@remix-run/session-middleware": "^0.2.0", "@remix-run/session-storage-memcache": "^0.1.0", "@remix-run/session-storage-redis": "^0.1.0", "@remix-run/static-middleware": "^0.4.5", "@remix-run/tar-parser": "^0.7.0" } }, "sha512-fvYHPm8QbrbL09wmmddrnknyntB+fl4bLmpECpK32cOoJW3pMoMvESBB1qRAQe+yWgTexVjp5G3BGOmmtYOX0A=="],
 
     "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
 

diff --git a/client/app.tsx b/client/app.tsx
--- a/client/app.tsx
+++ b/client/app.tsx
@@ -1,199 +1,184 @@
-import { type Handle } from 'remix/component'
-import { clientRoutes } from './routes/index.tsx'
-import {
-	getPathname,
-	listenToRouterNavigation,
-	Router,
-} from './client-router.tsx'
-import {
-	fetchSessionInfo,
-	type SessionInfo,
-	type SessionStatus,
-} from './session.ts'
-import { buildAuthLink } from './auth-links.ts'
-import { colors, mq, spacing, typography } from './styles/tokens.ts'
-
+import { css, type Handle } from 'remix/component';
+import { clientRoutes } from './routes/index.tsx';
+import { getPathname, listenToRouterNavigation, Router, } from './client-router.tsx';
+import { fetchSessionInfo, type SessionInfo, type SessionStatus, } from './session.ts';
+import { buildAuthLink } from './auth-links.ts';
+import { colors, mq, spacing, typography } from './styles/tokens.ts';
 export function App(handle: Handle) {
-	let session: SessionInfo | null = null
-	let sessionStatus: SessionStatus = 'idle'
-	let sessionRefreshInFlight = false
-	let sessionRefreshQueued = false
-	let currentPathname = getPathname()
-
-	function queueSessionRefresh() {
-		sessionRefreshQueued = true
-		if (sessionRefreshInFlight) return
-
-		// Preserve current nav state during refreshes after first load.
-		if (sessionStatus === 'idle') {
-			sessionStatus = 'loading'
-			handle.update()
-		}
-
-		sessionRefreshQueued = false
-		sessionRefreshInFlight = true
-		handle.queueTask(async (signal) => {
-			const nextSession = await fetchSessionInfo(signal)
-			sessionRefreshInFlight = false
-			if (signal.aborted) return
-			session = nextSession
-			sessionStatus = 'ready'
-			handle.update()
-			if (sessionRefreshQueued) {
-				queueSessionRefresh()
-			}
-		})
-		if (sessionStatus !== 'loading') {
-			handle.update()
-		}
-	}
-
-	handle.queueTask(() => {
-		queueSessionRefresh()
-	})
-	listenToRouterNavigation(handle, () => {
-		currentPathname = getPathname()
-		queueSessionRefresh()
-		handle.update()
-	})
-
-	const navLinkCss = {
-		color: colors.primaryText,
-		fontWeight: typography.fontWeight.medium,
-		textDecoration: 'none',
-		'&:hover': {
-			textDecoration: 'underline',
-		},
-	}
-
-	const navHomeLinkCss = {
-		...navLinkCss,
-		display: 'flex',
-		alignItems: 'center',
-		lineHeight: 0,
-		'&:hover': {
-			textDecoration: 'none',
-			opacity: 0.85,
-		},
-	}
-
-	const logOutButtonCss = {
-		padding: `${spacing.xs} ${spacing.md}`,
-		borderRadius: '999px',
-		border: `1px solid ${colors.border}`,
-		backgroundColor: 'transparent',
-		color: colors.text,
-		fontWeight: typography.fontWeight.medium,
-		cursor: 'pointer',
-	}
-
-	return () => {
-		const isChatLayout = currentPathname.startsWith('/chat')
-		const sessionEmail = session?.email ?? ''
-		const isSessionReady = sessionStatus === 'ready'
-		const isLoggedIn = isSessionReady && Boolean(sessionEmail)
-		const showAuthLinks = isSessionReady && !isLoggedIn
-		const oauthRedirectTo =
-			typeof window !== 'undefined' && currentPathname === '/oauth/authorize'
-				? `${currentPathname}${window.location.search}`
-				: null
-		const loginHref = buildAuthLink('/login', oauthRedirectTo)
-		const signupHref = buildAuthLink('/signup', oauthRedirectTo)
-
-		return (
-			<main
-				css={{
-					maxWidth: isChatLayout ? 'none' : '52rem',
-					width: '100%',
-					margin: isChatLayout ? 0 : '0 auto',
-					padding: isChatLayout
-						? `${spacing.lg} ${spacing.xl} ${spacing.sm}`
-						: spacing['2xl'],
-					minHeight: isChatLayout ? '100vh' : undefined,
-					fontFamily: typography.fontFamily,
-					boxSizing: 'border-box',
-					[mq.tablet]: {
-						padding: isChatLayout
-							? `${spacing.sm} ${spacing.sm} 0`
-							: spacing.md,
-					},
-				}}
-			>
-				<nav
-					css={{
-						display: 'flex',
-						alignItems: 'center',
-						gap: spacing.md,
-						flexWrap: 'wrap',
-						marginBottom: isChatLayout ? spacing.lg : spacing.xl,
-						[mq.tablet]: {
-							gap: spacing.sm,
-							marginBottom: isChatLayout ? spacing.sm : spacing.md,
-						},
-					}}
-				>
-					<a href="/" css={navHomeLinkCss} aria-label="Home">
-						<img
-							src="/logo.png"
-							alt=""
-							width={112}
-							height={28}
-							css={{
-								display: 'block',
-								height: '1.35em',
-								width: 'auto',
-							}}
-						/>
+    let session: SessionInfo | null = null;
+    let sessionStatus: SessionStatus = 'idle';
+    let sessionRefreshInFlight = false;
+    let sessionRefreshQueued = false;
+    let currentPathname = getPathname();
+    function queueSessionRefresh() {
+        sessionRefreshQueued = true;
+        if (sessionRefreshInFlight)
+            return;
+        // Preserve current nav state during refreshes after first load.
+        if (sessionStatus === 'idle') {
+            sessionStatus = 'loading';
+            handle.update();
+        }
+        sessionRefreshQueued = false;
+        sessionRefreshInFlight = true;
+        handle.queueTask(async (signal) => {
+            const nextSession = await fetchSessionInfo(signal);
+            sessionRefreshInFlight = false;
+            if (signal.aborted)
+                return;
+            session = nextSession;
+            sessionStatus = 'ready';
+            handle.update();
+            if (sessionRefreshQueued) {
+                queueSessionRefresh();
+            }
+        });
+        if (sessionStatus !== 'loading') {
+            handle.update();
+        }
+    }
+    handle.queueTask(() => {
+        queueSessionRefresh();
+    });
+    listenToRouterNavigation(handle, () => {
+        currentPathname = getPathname();
+        queueSessionRefresh();
+        handle.update();
+    });
+    const navLinkCss = {
+        color: colors.primaryText,
+        fontWeight: typography.fontWeight.medium,
+        textDecoration: 'none',
+        '&:hover': {
+            textDecoration: 'underline',
+        },
+    };
+    const navHomeLinkCss = {
+        ...navLinkCss,
+        display: 'flex',
+        alignItems: 'center',
+        lineHeight: 0,
+        '&:hover': {
+            textDecoration: 'none',
+            opacity: 0.85,
+        },
+    };
+    const logOutButtonCss = {
+        padding: `${spacing.xs} ${spacing.md}`,
+        borderRadius: '999px',
+        border: `1px solid ${colors.border}`,
+        backgroundColor: 'transparent',
+        color: colors.text,
+        fontWeight: typography.fontWeight.medium,
+        cursor: 'pointer',
+    };
+    return () => {
+        const isChatLayout = currentPathname.startsWith('/chat');
+        const sessionEmail = session?.email ?? '';
+        const isSessionReady = sessionStatus === 'ready';
+        const isLoggedIn = isSessionReady && Boolean(sessionEmail);
+        const showAuthLinks = isSessionReady && !isLoggedIn;
+        const oauthRedirectTo = typeof window !== 'undefined' && currentPathname === '/oauth/authorize'
+            ? `${currentPathname}${window.location.search}`
+            : null;
+        const loginHref = buildAuthLink('/login', oauthRedirectTo);
+        const signupHref = buildAuthLink('/signup', oauthRedirectTo);
+        return (<main mix={[
+            css({
+                maxWidth: isChatLayout ? 'none' : '52rem',
+                width: '100%',
+                margin: isChatLayout ? 0 : '0 auto',
+                padding: isChatLayout
+                    ? `${spacing.lg} ${spacing.xl} ${spacing.sm}`
+                    : spacing['2xl'],
+                minHeight: isChatLayout ? '100vh' : undefined,
+                fontFamily: typography.fontFamily,
+                boxSizing: 'border-box',
+                [mq.tablet]: {
+                    padding: isChatLayout
+                        ? `${spacing.sm} ${spacing.sm} 0`
+                        : spacing.md,
+                },
+            })
+        ]}>
+				<nav mix={[
+            css({
+                display: 'flex',
+                alignItems: 'center',
+                gap: spacing.md,
+                flexWrap: 'wrap',
+                marginBottom: isChatLayout ? spacing.lg : spacing.xl,
+                [mq.tablet]: {
+                    gap: spacing.sm,
+                    marginBottom: isChatLayout ? spacing.sm : spacing.md,
+                },
+            })
+        ]}>
+					<a href="/" aria-label="Home" mix={[
+            css(navHomeLinkCss)
+        ]}>
+						<img src="/logo.png" alt="" width={112} height={28} mix={[
+            css({
+                display: 'block',
+                height: '1.35em',
+                width: 'auto',
+            })
+        ]}/>
 					</a>
-					{showAuthLinks ? (
-						<>
-							<a href={loginHref} css={navLinkCss}>
+					{showAuthLinks ? (<>
+							<a href={loginHref} mix={[
+                css(navLinkCss)
+            ]}>
 								Login
 							</a>
-							<a href={signupHref} css={navLinkCss}>
+							<a href={signupHref} mix={[
+                css(navLinkCss)
+            ]}>
 								Signup
 							</a>
-						</>
-					) : null}
-					{isLoggedIn ? (
-						<>
-							<a href="/chat" css={navLinkCss}>
+						</>) : null}
+					{isLoggedIn ? (<>
+							<a href="/chat" mix={[
+                css(navLinkCss)
+            ]}>
 								Chat
 							</a>
-							<a href="/account" css={navLinkCss}>
+							<a href="/account" mix={[
+                css(navLinkCss)
+            ]}>
 								{sessionEmail}
 							</a>
-							<form method="post" action="/logout" css={{ margin: 0 }}>
-								<button type="submit" css={logOutButtonCss}>
+							<form method="post" action="/logout" mix={[
+                css({ margin: 0 })
+            ]}>
+								<button type="submit" mix={[
+                css(logOutButtonCss)
+            ]}>
 									Log out
 								</button>
 							</form>
-						</>
-					) : null}
+						</>) : null}
 				</nav>
-				<Router
-					setup={{
-						routes: clientRoutes,
-						fallback: (
-							<section>
-								<h2
-									css={{
-										fontSize: typography.fontSize.lg,
-										fontWeight: typography.fontWeight.semibold,
-										marginBottom: spacing.sm,
-										color: colors.text,
-									}}
-								>
+				<Router setup={{
+                routes: clientRoutes,
+                fallback: (<section>
+								<h2 mix={[
+                    css({
+                        fontSize: typography.fontSize.lg,
+                        fontWeight: typography.fontWeight.semibold,
+                        marginBottom: spacing.sm,
+                        color: colors.text,
+                    })
+                ]}>
 									Not Found
 								</h2>
-								<p css={{ color: colors.textMuted }}>
+								<p mix={[
+                    css({ color: colors.textMuted })
+                ]}>
 									That route does not exist.
 								</p>
-							</section>
-						),
-					}}
-				/>
-			</main>
-		)
-	}
+							</section>),
+            }}/>
+			</main>);
+    };
 }

diff --git a/client/client-router.tsx b/client/client-router.tsx
--- a/client/client-router.tsx
+++ b/client/client-router.tsx
@@ -1,4 +1,4 @@
-import { type Handle } from 'remix/component'
+import { addEventListeners, type Handle } from 'remix/component'
 
 type RouterSetup = {
 	routes: Record<string, JSX.Element>
@@ -251,7 +251,7 @@
 
 export function listenToRouterNavigation(handle: Handle, listener: () => void) {
 	ensureRouter()
-	handle.on(routerEvents, {
+	addEventListeners(routerEvents, handle.signal, {
 		navigate: () => listener(),
 	})
 }

diff --git a/client/counter.tsx b/client/counter.tsx
--- a/client/counter.tsx
+++ b/client/counter.tsx
@@ -1,49 +1,36 @@
-import { type Handle } from 'remix/component'
-import {
-	colors,
-	radius,
-	spacing,
-	transitions,
-	typography,
-} from './styles/tokens.ts'
-
+import { css, type Handle, on } from 'remix/component';
+import { colors, radius, spacing, transitions, typography, } from './styles/tokens.ts';
 type CounterSetup = {
-	initial?: number
-}
-
+    initial?: number;
+};
 export function Counter(handle: Handle, setup: CounterSetup = {}) {
-	let count = setup.initial ?? 0
-
-	function increment() {
-		count += 1
-		handle.update()
-	}
-
-	return () => (
-		<button
-			type="button"
-			css={{
-				padding: `${spacing.sm} ${spacing.lg}`,
-				borderRadius: radius.full,
-				border: `1px solid ${colors.border}`,
-				backgroundColor: colors.primary,
-				color: colors.onPrimary,
-				fontSize: typography.fontSize.base,
-				fontWeight: typography.fontWeight.semibold,
-				cursor: 'pointer',
-				transition: `transform ${transitions.fast}, background-color ${transitions.normal}`,
-				'&:hover': {
-					backgroundColor: colors.primaryHover,
-					transform: 'translateY(-1px)',
-				},
-				'&:active': {
-					backgroundColor: colors.primaryActive,
-					transform: 'translateY(0)',
-				},
-			}}
-			on={{ click: increment }}
-		>
+    let count = setup.initial ?? 0;
+    function increment() {
+        count += 1;
+        handle.update();
+    }
+    return () => (<button type="button" mix={[
+        css({
+            padding: `${spacing.sm} ${spacing.lg}`,
+            borderRadius: radius.full,
+            border: `1px solid ${colors.border}`,
+            backgroundColor: colors.primary,
+            color: colors.onPrimary,
+            fontSize: typography.fontSize.base,
+            fontWeight: typography.fontWeight.semibold,
+            cursor: 'pointer',
+            transition: `transform ${transitions.fast}, background-color ${transitions.normal}`,
+            '&:hover': {
+                backgroundColor: colors.primaryHover,
+                transform: 'translateY(-1px)',
+            },
+            '&:active': {
+                backgroundColor: colors.primaryActive,
+                transform: 'translateY(0)',
+            },
+        }),
+        on("click", increment)
+    ]}>
 			Count: {count}
-		</button>
-	)
+		</button>);
 }

diff --git a/client/double-check.ts b/client/double-check.ts
--- a/client/double-check.ts
+++ b/client/double-check.ts
@@ -1,26 +1,14 @@
-import { type Handle } from 'remix/component'
+import { on, type Handle } from 'remix/component'
 
-type BlurHandler = (event: FocusEvent) => void
-type ClickHandler = (event: MouseEvent) => void
-
 type ButtonLikeProps = {
+	mix?: Array<unknown>
+	onConfirm?: (event: MouseEvent) => void
 	on?: {
-		blur?: BlurHandler
-		click?: ClickHandler
+		click?: (event: MouseEvent) => void
 	}
 	[key: string]: unknown
 }
 
-function callAll<Event>(
-	...handlers: Array<((event: Event) => void) | undefined>
-) {
-	return (event: Event) => {
-		for (const handler of handlers) {
-			handler?.(event)
-		}
-	}
-}
-
 export function createDoubleCheck(handle: Handle) {
 	let doubleCheck = false
 
@@ -39,29 +27,36 @@
 		},
 		getButtonProps<Props extends ButtonLikeProps>(props?: Props): Props {
 			const buttonProps = props ?? ({} as Props)
+			const {
+				mix: inputMix,
+				onConfirm,
+				on: onOverrides,
+				...rest
+			} = buttonProps as ButtonLikeProps
+			const mix = [...(inputMix ?? [])]
+			const confirmHandler = onConfirm ?? onOverrides?.click
 
-			const onBlur: BlurHandler = () => {
-				setDoubleCheck(false)
-			}
+			mix.push(
+				on<HTMLButtonElement, 'blur'>('blur', () => {
+					setDoubleCheck(false)
+				}),
+			)
 
-			const onClick: ClickHandler = (event) => {
-				if (!doubleCheck) {
-					event.preventDefault()
-					setDoubleCheck(true)
-					return
-				}
+			mix.push(
+				on<HTMLButtonElement, 'click'>('click', (event) => {
+					if (!doubleCheck) {
+						event.preventDefault()
+						setDoubleCheck(true)
+						return
+					}
+					setDoubleCheck(false)
+					confirmHandler?.(event)
+				}),
+			)
 
-				buttonProps.on?.click?.(event)
-				setDoubleCheck(false)
-			}
-
 			return {
-				...buttonProps,
-				on: {
-					...buttonProps.on,
-					blur: callAll(onBlur, buttonProps.on?.blur),
-					click: onClick,
-				},
+				...rest,
+				mix,
 			}
 		},
 	}

diff --git a/client/editable-text.tsx b/client/editable-text.tsx
--- a/client/editable-text.tsx
+++ b/client/editable-text.tsx
@@ -1,171 +1,148 @@
-import { type Handle } from 'remix/component'
-
+import { css, type Handle, on } from 'remix/component';
 type EditableTextProps = {
-	id: string
-	ariaLabel: string
-	value: string
-	emptyText?: string
-	buttonCss?: Record<string, unknown>
-	inputCss?: Record<string, unknown>
-	onSave: (value: string) => Promise<boolean> | boolean
-}
-
+    id: string;
+    ariaLabel: string;
+    value: string;
+    emptyText?: string;
+    buttonCss?: Record<string, unknown>;
+    inputCss?: Record<string, unknown>;
+    onSave: (value: string) => Promise<boolean> | boolean;
+};
 const inheritTextStyles = {
-	fontSize: 'inherit',
-	fontStyle: 'inherit',
-	fontWeight: 'inherit',
-	fontFamily: 'inherit',
-	textAlign: 'inherit',
-	lineHeight: 'inherit',
-	color: 'inherit',
-} as const
-
+    fontSize: 'inherit',
+    fontStyle: 'inherit',
+    fontWeight: 'inherit',
+    fontFamily: 'inherit',
+    textAlign: 'inherit',
+    lineHeight: 'inherit',
+    color: 'inherit',
+} as const;
 export function EditableText(handle: Handle) {
-	let isEditing = false
-	let draftValue = ''
-	let isSaving = false
-
-	function focusInput(inputId: string) {
-		void handle.queueTask(async () => {
-			const input = document.getElementById(inputId)
-			if (!(input instanceof HTMLInputElement)) return
-			input.focus()
-			input.select()
-		})
-	}
-
-	function focusButton(buttonId: string) {
-		void handle.queueTask(async () => {
-			const button = document.getElementById(buttonId)
-			if (!(button instanceof HTMLButtonElement)) return
-			button.focus()
-		})
-	}
-
-	return (props: EditableTextProps) => {
-		const buttonId = `${props.id}-button`
-
-		function startEditing() {
-			if (isSaving) return
-			draftValue = props.value
-			isEditing = true
-			handle.update()
-			focusInput(props.id)
-		}
-
-		function cancelEditing() {
-			if (isSaving) return
-			draftValue = props.value
-			isEditing = false
-			handle.update()
-			focusButton(buttonId)
-		}
-
-		async function submitEditing(event: SubmitEvent) {
-			event.preventDefault()
-			if (isSaving) return
-			const nextValue = draftValue.trim()
-			if (!nextValue) return
-
-			isSaving = true
-			handle.update()
-			let didSave = false
-			try {
-				didSave = await props.onSave(nextValue)
-			} catch (error) {
-				isSaving = false
-				handle.update()
-				throw error
-			}
-			isSaving = false
-			if (!didSave) {
-				handle.update()
-				return
-			}
-
-			isEditing = false
... diff truncated: showing 800 of 27873 lines

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@cursor cursor bot temporarily deployed to preview-64 March 26, 2026 22:39 Inactive
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@cursor cursor bot temporarily deployed to preview-64 March 26, 2026 22:40 Inactive
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@cursor cursor bot temporarily deployed to preview-64 March 26, 2026 22:51 Inactive
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

setDoubleCheck(false)
})
},
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undocumented handleEvent mixin breaks double-check event handling

High Severity

The handleEvent method pushed into the mix array is not a recognized Remix component mix-in interface. The documented mix-in APIs are on(), css(), ref(), and createMixin() — none of which use a raw handleEvent method. This means the framework will never call handleEvent, so the blur and click event listeners are never attached to the delete button. As a result, clicking the button does nothing (no double-check confirmation, no deletion), completely breaking the thread-delete flow. The fix is to use on('blur', ...) and on('click', ...) calls pushed into the mix array instead.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants