diff --git a/README.md b/README.md
index fe7a7b1..9b004c9 100644
--- a/README.md
+++ b/README.md
@@ -6,11 +6,6 @@ Yes, another rewrite was needed. Again.
     - Yes all of it
 - Client
     - Overview
-    - Search
     - Graph
     - Calendar
     - Todo
-    - Collection pages
-    - Note pages
-        - Lexical
-        - Excalidraw
diff --git a/package.json b/package.json
index 32c1b8b..ce12a62 100644
--- a/package.json
+++ b/package.json
@@ -9,25 +9,47 @@
     "test": "vitest run"
   },
   "dependencies": {
+    "@excalidraw/excalidraw": "^0.18.0",
+    "@lexical/react": "^0.28.0",
+    "@lexical/utils": "^0.28.0",
+    "@lexical/yjs": "^0.28.0",
+    "@mdi/js": "^7.4.47",
+    "@mdi/react": "^1.6.1",
+    "@radix-ui/react-alert-dialog": "^1.1.6",
+    "@radix-ui/react-avatar": "^1.1.3",
     "@radix-ui/react-dialog": "^1.1.6",
+    "@radix-ui/react-dropdown-menu": "^2.1.6",
+    "@radix-ui/react-label": "^2.1.2",
+    "@radix-ui/react-popover": "^1.1.6",
+    "@radix-ui/react-radio-group": "^1.2.3",
     "@radix-ui/react-separator": "^1.1.2",
     "@radix-ui/react-slot": "^1.1.2",
+    "@radix-ui/react-toggle": "^1.1.2",
+    "@radix-ui/react-toggle-group": "^1.1.2",
     "@radix-ui/react-tooltip": "^1.1.8",
     "@tailwindcss/vite": "^4.0.6",
     "@tanstack/react-router": "^1.114.3",
     "@tanstack/react-router-devtools": "^1.114.3",
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
+    "cmdk": "1.0.0",
+    "lexical": "^0.28.0",
     "lucide-react": "^0.483.0",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
     "tailwind-merge": "^3.0.2",
     "tailwindcss": "^4.0.6",
-    "tw-animate-css": "^1.2.4"
+    "tw-animate-css": "^1.2.4",
+    "y-excalidraw": "^2.0.12",
+    "y-indexeddb": "^9.0.12",
+    "y-websocket": "^2.1.0",
+    "yjs": "^13.6.24",
+    "zod": "^3.24.2"
   },
   "devDependencies": {
     "@serwist/vite": "^9.0.12",
     "@serwist/window": "^9.0.12",
+    "@tanstack/router-plugin": "^1.114.25",
     "@testing-library/dom": "^10.4.0",
     "@testing-library/react": "^16.2.0",
     "@types/react": "^19.0.8",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0d04a95..d40d79e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,21 +8,63 @@ importers:
 
   .:
     dependencies:
+      '@excalidraw/excalidraw':
+        specifier: ^0.18.0
+        version: 0.18.0(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@lexical/react':
+        specifier: ^0.28.0
+        version: 0.28.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(yjs@13.6.24)
+      '@lexical/utils':
+        specifier: ^0.28.0
+        version: 0.28.0
+      '@lexical/yjs':
+        specifier: ^0.28.0
+        version: 0.28.0(yjs@13.6.24)
+      '@mdi/js':
+        specifier: ^7.4.47
+        version: 7.4.47
+      '@mdi/react':
+        specifier: ^1.6.1
+        version: 1.6.1
+      '@radix-ui/react-alert-dialog':
+        specifier: ^1.1.6
+        version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-avatar':
+        specifier: ^1.1.3
+        version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       '@radix-ui/react-dialog':
         specifier: ^1.1.6
         version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-dropdown-menu':
+        specifier: ^2.1.6
+        version: 2.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-label':
+        specifier: ^2.1.2
+        version: 2.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-popover':
+        specifier: ^1.1.6
+        version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-radio-group':
+        specifier: ^1.2.3
+        version: 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       '@radix-ui/react-separator':
         specifier: ^1.1.2
         version: 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       '@radix-ui/react-slot':
         specifier: ^1.1.2
         version: 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-toggle':
+        specifier: ^1.1.2
+        version: 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-toggle-group':
+        specifier: ^1.1.2
+        version: 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       '@radix-ui/react-tooltip':
         specifier: ^1.1.8
         version: 1.1.8(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       '@tailwindcss/vite':
         specifier: ^4.0.6
-        version: 4.0.14(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2))
+        version: 4.0.14(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3))
       '@tanstack/react-router':
         specifier: ^1.114.3
         version: 1.114.25(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -35,6 +77,12 @@ importers:
       clsx:
         specifier: ^2.1.1
         version: 2.1.1
+      cmdk:
+        specifier: 1.0.0
+        version: 1.0.0(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      lexical:
+        specifier: ^0.28.0
+        version: 0.28.0
       lucide-react:
         specifier: ^0.483.0
         version: 0.483.0(react@19.0.0)
@@ -53,13 +101,31 @@ importers:
       tw-animate-css:
         specifier: ^1.2.4
         version: 1.2.4
+      y-excalidraw:
+        specifier: ^2.0.12
+        version: 2.0.12(@excalidraw/excalidraw@0.18.0(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(yjs@13.6.24)
+      y-indexeddb:
+        specifier: ^9.0.12
+        version: 9.0.12(yjs@13.6.24)
+      y-websocket:
+        specifier: ^2.1.0
+        version: 2.1.0(yjs@13.6.24)
+      yjs:
+        specifier: ^13.6.24
+        version: 13.6.24
+      zod:
+        specifier: ^3.24.2
+        version: 3.24.2
     devDependencies:
       '@serwist/vite':
         specifier: ^9.0.12
-        version: 9.0.12(typescript@5.8.2)(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2))
+        version: 9.0.12(typescript@5.8.2)(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3))
       '@serwist/window':
         specifier: ^9.0.12
         version: 9.0.12(typescript@5.8.2)
+      '@tanstack/router-plugin':
+        specifier: ^1.114.25
+        version: 1.114.25(@tanstack/react-router@1.114.25(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3))
       '@testing-library/dom':
         specifier: ^10.4.0
         version: 10.4.0
@@ -74,7 +140,7 @@ importers:
         version: 19.0.4(@types/react@19.0.11)
       '@vitejs/plugin-react':
         specifier: ^4.3.4
-        version: 4.3.4(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2))
+        version: 4.3.4(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3))
       jsdom:
         specifier: ^26.0.0
         version: 26.0.0
@@ -86,10 +152,10 @@ importers:
         version: 5.8.2
       vite:
         specifier: ^6.1.0
-        version: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)
+        version: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3)
       vitest:
         specifier: ^3.0.5
-        version: 3.0.9(jiti@2.4.2)(jsdom@26.0.0)(lightningcss@1.29.2)
+        version: 3.0.9(@types/debug@4.1.12)(jiti@2.4.2)(jsdom@26.0.0)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3)
       web-vitals:
         specifier: ^4.2.4
         version: 4.2.4
@@ -158,6 +224,18 @@ packages:
     engines: {node: '>=6.0.0'}
     hasBin: true
 
+  '@babel/plugin-syntax-jsx@7.25.9':
+    resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0-0
+
+  '@babel/plugin-syntax-typescript@7.25.9':
+    resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0-0
+
   '@babel/plugin-transform-react-jsx-self@7.25.9':
     resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==}
     engines: {node: '>=6.9.0'}
@@ -186,6 +264,9 @@ packages:
     resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==}
     engines: {node: '>=6.9.0'}
 
+  '@braintree/sanitize-url@6.0.2':
+    resolution: {integrity: sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==}
+
   '@csstools/color-helpers@5.0.2':
     resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==}
     engines: {node: '>=18'}
@@ -364,6 +445,25 @@ packages:
     cpu: [x64]
     os: [win32]
 
+  '@excalidraw/excalidraw@0.18.0':
+    resolution: {integrity: sha512-QkIiS+5qdy8lmDWTKsuy0sK/fen/LRDtbhm2lc2xcFcqhv2/zdg95bYnl+wnwwXGHo7kEmP65BSiMHE7PJ3Zpw==}
+    peerDependencies:
+      react: ^17.0.2 || ^18.2.0 || ^19.0.0
+      react-dom: ^17.0.2 || ^18.2.0 || ^19.0.0
+
+  '@excalidraw/laser-pointer@1.3.1':
+    resolution: {integrity: sha512-psA1z1N2qeAfsORdXc9JmD2y4CmDwmuMRxnNdJHZexIcPwaNEyIpNcelw+QkL9rz9tosaN9krXuKaRqYpRAR6g==}
+
+  '@excalidraw/markdown-to-text@0.1.2':
+    resolution: {integrity: sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==}
+
+  '@excalidraw/mermaid-to-excalidraw@1.1.2':
+    resolution: {integrity: sha512-hAFv/TTIsOdoy0dL5v+oBd297SQ+Z88gZ5u99fCIFuEMHfQuPgLhU/ztKhFSTs7fISwVo6fizny/5oQRR3d4tQ==}
+
+  '@excalidraw/random-username@1.1.0':
+    resolution: {integrity: sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA==}
+    engines: {node: '>=10'}
+
   '@floating-ui/core@1.6.9':
     resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
 
@@ -401,13 +501,109 @@ packages:
   '@jridgewell/trace-mapping@0.3.25':
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
 
+  '@lexical/clipboard@0.28.0':
+    resolution: {integrity: sha512-LYqion+kAwFQJStA37JAEMxTL/m1WlZbotDfM/2WuONmlO0yWxiyRDI18oeCwhBD6LQQd9c3Ccxp9HFwUG1AVw==}
+
+  '@lexical/code@0.28.0':
+    resolution: {integrity: sha512-9LOKSWdRhxqAKRq5yveNC21XKtW4h2rmFNTucwMWZ9vLu9xteOHEwZdO1Qv82PFUmgCpAhg6EntmnZu9xD3K7Q==}
+
+  '@lexical/devtools-core@0.28.0':
+    resolution: {integrity: sha512-Fk4itAjZ+MqTYXN84aE5RDf+wQX67N5nyo3JVxQTFZGAghx7Ux1xLWHB25zzD0YfjMtJ0NQROAbE3xdecZzxcQ==}
+    peerDependencies:
+      react: '>=17.x'
+      react-dom: '>=17.x'
+
+  '@lexical/dragon@0.28.0':
+    resolution: {integrity: sha512-T6T8YaHnhU863ruuqmRHTLUYa8sfg/ArYcrnNGZGfpvvFTfFjpWb/ELOvOWo8N6Y/4fnSLjQ20aXexVW1KcTBQ==}
+
+  '@lexical/hashtag@0.28.0':
+    resolution: {integrity: sha512-zcqX9Qna4lj96bAUfwSQSVEhYQ0O5erSjrIhOVqEgeQ5ubz0EvqnnMbbwNHIb2n6jzSwAvpD/3UZJZtolh+zVg==}
+
+  '@lexical/history@0.28.0':
+    resolution: {integrity: sha512-CHzDxaGDn6qCFFhU0YKP1B8sgEb++0Ksqsj6BfDL/6TMxoLNQwRQhP3BUNNXl1kvUhxTQZgk3b9MjJZRaFKG9Q==}
+
+  '@lexical/html@0.28.0':
+    resolution: {integrity: sha512-ayb0FPxr55Ko99/d9ewbfrApul4L0z+KpU2ZG03im7EvUPVLyIGLx4S0QguMDvQh0Vu+eJ7/EESuonDs5BCe3A==}
+
+  '@lexical/link@0.28.0':
+    resolution: {integrity: sha512-T5VKxpOnML5DcXv2lW3Le0vjNlcbdohZjS9f6PAvm6eX8EzBKDpLQCopr1/0KGdlLd1QrzQsykQrdU7ieC4LRg==}
+
+  '@lexical/list@0.28.0':
+    resolution: {integrity: sha512-3a8QcZ75n2TLxP+xkSPJ2V15jsysMLMe0YoObG+ew/sioVelIU8GciYsWBo5GgQmwSzJNQJeK5cJ9p1b71z2cg==}
+
+  '@lexical/mark@0.28.0':
+    resolution: {integrity: sha512-v5PzmTACsJrw3GvNZy2rgPxrNn9InLvLFoKqrSlNhhyvYNIAcuC4KVy00LKLja43Gw/fuB3QwKohYfAtM3yR3g==}
+
+  '@lexical/markdown@0.28.0':
+    resolution: {integrity: sha512-F3JXClqN4cjmXYLDK0IztxkbZuqkqS/AVbxnhGvnDYHQ9Gp8l7BonczhOiPwmJCDubJrAACP0L9LCqyt0jDRFw==}
+
+  '@lexical/offset@0.28.0':
+    resolution: {integrity: sha512-/SMDQgBPeWM936t04mtH6UAn3xAjP/meu9q136bcT3S7p7V8ew9JfNp9aznTPTx+2W3brJORAvUow7Xn1fSHmw==}
+
+  '@lexical/overflow@0.28.0':
+    resolution: {integrity: sha512-ppmhHXEZVicBm05w9EVflzwFavTVNAe4q0bkabWUeW0IoCT3Vg2A3JT7PC9ypmp+mboUD195foFEr1BBSv1Y8Q==}
+
+  '@lexical/plain-text@0.28.0':
+    resolution: {integrity: sha512-Jj2dCMDEfRuVetfDKcUes8J5jvAfZrLnILFlHxnu7y+lC+7R/NR403DYb3NJ8H7+lNiH1K15+U2K7ewbjxS6KQ==}
+
+  '@lexical/react@0.28.0':
+    resolution: {integrity: sha512-dWPnxrKrbQFjNqExqnaAsV0UEUgw/5M1ZYRWd5FGBGjHqVTCaX2jNHlKLMA68Od0VPIoOX2Zy1TYZ8ZKtsj5Dg==}
+    peerDependencies:
+      react: '>=17.x'
+      react-dom: '>=17.x'
+
+  '@lexical/rich-text@0.28.0':
+    resolution: {integrity: sha512-y+vUWI+9uFupIb9UvssKU/DKcT9dFUZuQBu7utFkLadxCNyXQHeRjxzjzmvFiM3DBV0guPUDGu5VS5TPnIA+OA==}
+
+  '@lexical/selection@0.28.0':
+    resolution: {integrity: sha512-AJDi67Nsexyejzp4dEQSVoPov4P+FJ0t1v6DxUU+YmcvV56QyJQi6ue0i/xd8unr75ZufzLsAC0cDJJCEI7QDA==}
+
+  '@lexical/table@0.28.0':
+    resolution: {integrity: sha512-HMPCwXdj0sRWdlDzsHcNWRgbeKbEhn3L8LPhFnTq7q61gZ4YW2umdmuvQFKnIBcKq49drTH8cUwZoIwI8+AEEw==}
+
+  '@lexical/text@0.28.0':
+    resolution: {integrity: sha512-PT/A2RZv+ktn7SG/tJkOpGlYE6zjOND59VtRHnV/xciZ+jEJVaqAHtWjhbWibAIZQAkv/O7UouuDqzDaNTSGAA==}
+
+  '@lexical/utils@0.28.0':
+    resolution: {integrity: sha512-Qw00DjkS1nRK7DLSgqJpJ77Ti2AuiOQ6m5eM38YojoWXkVmoxqKAUMaIbVNVKqjFgrQvKFF46sXxIJPbUQkB0w==}
+
+  '@lexical/yjs@0.28.0':
+    resolution: {integrity: sha512-rKHpUEd3nrvMY7ghmOC0AeGSYT7YIviba+JViaOzrCX4/Wtv5C/3Sl7Io12Z9k+s1BKmy7C28bOdQHvRWaD7vQ==}
+    peerDependencies:
+      yjs: '>=13.5.22'
+
+  '@mdi/js@7.4.47':
+    resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==}
+
+  '@mdi/react@1.6.1':
+    resolution: {integrity: sha512-4qZeDcluDFGFTWkHs86VOlHkm6gnKaMql13/gpIcUQ8kzxHgpj31NuCkD8abECVfbULJ3shc7Yt4HJ6Wu6SN4w==}
+
   '@pkgjs/parseargs@0.11.0':
     resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
     engines: {node: '>=14'}
 
+  '@radix-ui/primitive@1.0.0':
+    resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==}
+
+  '@radix-ui/primitive@1.0.1':
+    resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
+
   '@radix-ui/primitive@1.1.1':
     resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
 
+  '@radix-ui/react-alert-dialog@1.1.6':
+    resolution: {integrity: sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-arrow@1.1.2':
     resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==}
     peerDependencies:
@@ -421,6 +617,52 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-avatar@1.1.3':
+    resolution: {integrity: sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-collection@1.0.1':
+    resolution: {integrity: sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-collection@1.1.2':
+    resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-compose-refs@1.0.0':
+    resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-compose-refs@1.0.1':
+    resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-compose-refs@1.1.1':
     resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==}
     peerDependencies:
@@ -430,6 +672,20 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-context@1.0.0':
+    resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-context@1.0.1':
+    resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-context@1.1.1':
     resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
     peerDependencies:
@@ -439,6 +695,19 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-dialog@1.0.5':
+    resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-dialog@1.1.6':
     resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==}
     peerDependencies:
@@ -452,6 +721,33 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-direction@1.0.0':
+    resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-direction@1.1.0':
+    resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-dismissable-layer@1.0.5':
+    resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-dismissable-layer@1.1.5':
     resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==}
     peerDependencies:
@@ -465,6 +761,28 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-dropdown-menu@2.1.6':
+    resolution: {integrity: sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-focus-guards@1.0.1':
+    resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-focus-guards@1.1.1':
     resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
     peerDependencies:
@@ -474,6 +792,19 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-focus-scope@1.0.4':
+    resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-focus-scope@1.1.2':
     resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==}
     peerDependencies:
@@ -487,6 +818,20 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-id@1.0.0':
+    resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-id@1.0.1':
+    resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-id@1.1.0':
     resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
     peerDependencies:
@@ -496,6 +841,45 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-label@2.1.2':
+    resolution: {integrity: sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-menu@2.1.6':
+    resolution: {integrity: sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-popover@1.1.6':
+    resolution: {integrity: sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-popper@1.2.2':
     resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==}
     peerDependencies:
@@ -509,6 +893,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-portal@1.0.4':
+    resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-portal@1.1.4':
     resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==}
     peerDependencies:
@@ -522,6 +919,25 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-presence@1.0.0':
+    resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-presence@1.0.1':
+    resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-presence@1.1.2':
     resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==}
     peerDependencies:
@@ -535,6 +951,25 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-primitive@1.0.1':
+    resolution: {integrity: sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-primitive@1.0.3':
+    resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-primitive@2.0.2':
     resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==}
     peerDependencies:
@@ -548,6 +983,38 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-radio-group@1.2.3':
+    resolution: {integrity: sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-roving-focus@1.0.2':
+    resolution: {integrity: sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-roving-focus@1.1.2':
+    resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-separator@1.1.2':
     resolution: {integrity: sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==}
     peerDependencies:
@@ -561,6 +1028,20 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-slot@1.0.1':
+    resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-slot@1.0.2':
+    resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-slot@1.1.2':
     resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==}
     peerDependencies:
@@ -570,6 +1051,38 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-tabs@1.0.2':
+    resolution: {integrity: sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-toggle-group@1.1.2':
+    resolution: {integrity: sha512-JBm6s6aVG/nwuY5eadhU2zDi/IwYS0sDM5ZWb4nymv/hn3hZdkw+gENn0LP4iY1yCd7+bgJaCwueMYJIU3vk4A==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-toggle@1.1.2':
+    resolution: {integrity: sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/react-tooltip@1.1.8':
     resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==}
     peerDependencies:
@@ -583,6 +1096,20 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/react-use-callback-ref@1.0.0':
+    resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-use-callback-ref@1.0.1':
+    resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-use-callback-ref@1.1.0':
     resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
     peerDependencies:
@@ -592,6 +1119,20 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-use-controllable-state@1.0.0':
+    resolution: {integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-use-controllable-state@1.0.1':
+    resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-use-controllable-state@1.1.0':
     resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
     peerDependencies:
@@ -601,6 +1142,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-use-escape-keydown@1.0.3':
+    resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-use-escape-keydown@1.1.0':
     resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
     peerDependencies:
@@ -610,6 +1160,20 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-use-layout-effect@1.0.0':
+    resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==}
+    peerDependencies:
+      react: ^16.8 || ^17.0 || ^18.0
+
+  '@radix-ui/react-use-layout-effect@1.0.1':
+    resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-use-layout-effect@1.1.0':
     resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
     peerDependencies:
@@ -619,6 +1183,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/react-use-previous@1.1.0':
+    resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/react-use-rect@1.1.0':
     resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
     peerDependencies:
@@ -894,9 +1467,47 @@ packages:
       csstype:
         optional: true
 
+  '@tanstack/router-generator@1.114.25':
+    resolution: {integrity: sha512-KfPdXm9+zGPrEjcdDkkSbZpDvx8rOSD9sS0cQn6y82jqoSeHlzC0K3bSVElsAmS1uh7WXR+PNDJra+nHUdPhaQ==}
+    engines: {node: '>=12'}
+    peerDependencies:
+      '@tanstack/react-router': ^1.114.25
+    peerDependenciesMeta:
+      '@tanstack/react-router':
+        optional: true
+
+  '@tanstack/router-plugin@1.114.25':
+    resolution: {integrity: sha512-4SIvBzgX6TzwgW5OO6Knx4/vX8AocXnfQhXW7dzsNBgzt5WnI4dzoPvp6p9p+Hqo0AjJ2WndpEYq7fMl5BhA4Q==}
+    engines: {node: '>=12'}
+    peerDependencies:
+      '@rsbuild/core': '>=1.0.2'
+      '@tanstack/react-router': ^1.114.25
+      vite: '>=5.0.0 || >=6.0.0'
+      vite-plugin-solid: ^2.11.2
+      webpack: '>=5.92.0'
+    peerDependenciesMeta:
+      '@rsbuild/core':
+        optional: true
+      '@tanstack/react-router':
+        optional: true
+      vite:
+        optional: true
+      vite-plugin-solid:
+        optional: true
+      webpack:
+        optional: true
+
+  '@tanstack/router-utils@1.114.12':
+    resolution: {integrity: sha512-W4tltvM9FQuDEJejz/JJD3q/pVHBXBb8VmA77pZlj4IBW97RnUNy8CUwZUgSYcb9OReoO4i/VjjQCUq9ZdiDmg==}
+    engines: {node: '>=12'}
+
   '@tanstack/store@0.7.0':
     resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==}
 
+  '@tanstack/virtual-file-routes@1.114.12':
+    resolution: {integrity: sha512-aR13V1kSE/kUkP4a8snmqvj82OUlR5Q/rzxICmObLCsERGfzikUc4wquOy1d/RzJgsLb8o+FiOjSWynt4T7Jhg==}
+    engines: {node: '>=12'}
+
   '@testing-library/dom@10.4.0':
     resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
     engines: {node: '>=18'}
@@ -931,9 +1542,27 @@ packages:
   '@types/babel__traverse@7.20.6':
     resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
 
+  '@types/d3-scale-chromatic@3.1.0':
+    resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
+
+  '@types/d3-scale@4.0.9':
+    resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
+
+  '@types/d3-time@3.0.4':
+    resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+  '@types/debug@4.1.12':
+    resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
+
   '@types/estree@1.0.6':
     resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
 
+  '@types/mdast@3.0.15':
+    resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
+
+  '@types/ms@2.1.0':
+    resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
+
   '@types/react-dom@19.0.4':
     resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==}
     peerDependencies:
@@ -945,6 +1574,9 @@ packages:
   '@types/trusted-types@2.0.7':
     resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
 
+  '@types/unist@2.0.11':
+    resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
   '@vitejs/plugin-react@4.3.4':
     resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==}
     engines: {node: ^14.18.0 || >=16.0.0}
@@ -980,6 +1612,21 @@ packages:
   '@vitest/utils@3.0.9':
     resolution: {integrity: sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==}
 
+  abstract-leveldown@6.2.3:
+    resolution: {integrity: sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==}
+    engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
+
+  abstract-leveldown@6.3.0:
+    resolution: {integrity: sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ==}
+    engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
+
+  acorn@8.14.1:
+    resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+
   agent-base@7.1.3:
     resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
     engines: {node: '>= 14'}
@@ -1004,6 +1651,14 @@ packages:
     resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
     engines: {node: '>=12'}
 
+  ansis@3.17.0:
+    resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==}
+    engines: {node: '>=14'}
+
+  anymatch@3.1.3:
+    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+    engines: {node: '>= 8'}
+
   aria-hidden@1.2.4:
     resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
     engines: {node: '>=10'}
@@ -1015,20 +1670,43 @@ packages:
     resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
     engines: {node: '>=12'}
 
+  async-limiter@1.0.1:
+    resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
+
   asynckit@0.4.0:
     resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
 
+  babel-dead-code-elimination@1.0.9:
+    resolution: {integrity: sha512-JLIhax/xullfInZjtu13UJjaLHDeTzt3vOeomaSUdO/nAMEL/pWC/laKrSvWylXMnVWyL5bpmG9njqBZlUQOdg==}
+
   balanced-match@1.0.2:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
 
+  base64-js@1.5.1:
+    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+  binary-extensions@2.3.0:
+    resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+    engines: {node: '>=8'}
+
   brace-expansion@2.0.1:
     resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
 
+  braces@3.0.3:
+    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+    engines: {node: '>=8'}
+
+  browser-fs-access@0.29.1:
+    resolution: {integrity: sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw==}
+
   browserslist@4.24.4:
     resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==}
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
 
+  buffer@5.7.1:
+    resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+
   cac@6.7.14:
     resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
     engines: {node: '>=8'}
@@ -1040,6 +1718,9 @@ packages:
   caniuse-lite@1.0.30001706:
     resolution: {integrity: sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==}
 
+  canvas-roundrect-polyfill@0.0.1:
+    resolution: {integrity: sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw==}
+
   chai@5.2.0:
     resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
     engines: {node: '>=12'}
@@ -1048,17 +1729,34 @@ packages:
     resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
     engines: {node: '>=10'}
 
+  character-entities@2.0.2:
+    resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
+
   check-error@2.1.1:
     resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
     engines: {node: '>= 16'}
 
+  chokidar@3.6.0:
+    resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+    engines: {node: '>= 8.10.0'}
+
   class-variance-authority@0.7.1:
     resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
 
+  clsx@1.1.1:
+    resolution: {integrity: sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==}
+    engines: {node: '>=6'}
+
   clsx@2.1.1:
     resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
     engines: {node: '>=6'}
 
+  cmdk@1.0.0:
+    resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==}
+    peerDependencies:
+      react: ^18.0.0
+      react-dom: ^18.0.0
+
   color-convert@2.0.1:
     resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
     engines: {node: '>=7.0.0'}
@@ -1070,6 +1768,14 @@ packages:
     resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
     engines: {node: '>= 0.8'}
 
+  commander@7.2.0:
+    resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+    engines: {node: '>= 10'}
+
+  commander@8.3.0:
+    resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+    engines: {node: '>= 12'}
+
   common-tags@1.8.2:
     resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
     engines: {node: '>=4.0.0'}
@@ -1077,6 +1783,18 @@ packages:
   convert-source-map@2.0.0:
     resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
 
+  cose-base@1.0.3:
+    resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
+
+  crc-32@0.3.0:
+    resolution: {integrity: sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==}
+    engines: {node: '>=0.8'}
+
+  cross-env@7.0.3:
+    resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
+    engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
+    hasBin: true
+
   cross-spawn@7.0.6:
     resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
     engines: {node: '>= 8'}
@@ -1088,10 +1806,164 @@ packages:
   csstype@3.1.3:
     resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
 
+  cytoscape-cose-bilkent@4.1.0:
+    resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==}
+    peerDependencies:
+      cytoscape: ^3.2.0
+
+  cytoscape@3.31.1:
+    resolution: {integrity: sha512-Hx5Mtb1+hnmAKaZZ/7zL1Y5HTFYOjdDswZy/jD+1WINRU8KVi1B7+vlHdsTwY+VCFucTreoyu1RDzQJ9u0d2Hw==}
+    engines: {node: '>=0.10'}
+
+  d3-array@2.12.1:
+    resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==}
+
+  d3-array@3.2.4:
+    resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+    engines: {node: '>=12'}
+
+  d3-axis@3.0.0:
+    resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
+    engines: {node: '>=12'}
+
+  d3-brush@3.0.0:
+    resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
+    engines: {node: '>=12'}
+
+  d3-chord@3.0.1:
+    resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
+    engines: {node: '>=12'}
+
+  d3-color@3.1.0:
+    resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+    engines: {node: '>=12'}
+
+  d3-contour@4.0.2:
+    resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
+    engines: {node: '>=12'}
+
+  d3-delaunay@6.0.4:
+    resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
+    engines: {node: '>=12'}
+
+  d3-dispatch@3.0.1:
+    resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
+    engines: {node: '>=12'}
+
+  d3-drag@3.0.0:
+    resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
+    engines: {node: '>=12'}
+
+  d3-dsv@3.0.1:
+    resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
+    engines: {node: '>=12'}
+    hasBin: true
+
+  d3-ease@3.0.1:
+    resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+    engines: {node: '>=12'}
+
+  d3-fetch@3.0.1:
+    resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
+    engines: {node: '>=12'}
+
+  d3-force@3.0.0:
+    resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
+    engines: {node: '>=12'}
+
+  d3-format@3.1.0:
+    resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
+    engines: {node: '>=12'}
+
+  d3-geo@3.1.1:
+    resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
+    engines: {node: '>=12'}
+
+  d3-hierarchy@3.1.2:
+    resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
+    engines: {node: '>=12'}
+
+  d3-interpolate@3.0.1:
+    resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+    engines: {node: '>=12'}
+
+  d3-path@1.0.9:
+    resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
+
+  d3-path@3.1.0:
+    resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+    engines: {node: '>=12'}
+
+  d3-polygon@3.0.1:
+    resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
+    engines: {node: '>=12'}
+
+  d3-quadtree@3.0.1:
+    resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
+    engines: {node: '>=12'}
+
+  d3-random@3.0.1:
+    resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
+    engines: {node: '>=12'}
+
+  d3-sankey@0.12.3:
+    resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==}
+
+  d3-scale-chromatic@3.1.0:
+    resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
+    engines: {node: '>=12'}
+
+  d3-scale@4.0.2:
+    resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+    engines: {node: '>=12'}
+
+  d3-selection@3.0.0:
+    resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
+    engines: {node: '>=12'}
+
+  d3-shape@1.3.7:
+    resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==}
+
+  d3-shape@3.2.0:
+    resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+    engines: {node: '>=12'}
+
+  d3-time-format@4.1.0:
+    resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+    engines: {node: '>=12'}
+
+  d3-time@3.1.0:
+    resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+    engines: {node: '>=12'}
+
+  d3-timer@3.0.1:
+    resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+    engines: {node: '>=12'}
+
+  d3-transition@3.0.1:
+    resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
+    engines: {node: '>=12'}
+    peerDependencies:
+      d3-selection: 2 - 3
+
+  d3-zoom@3.0.0:
+    resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
+    engines: {node: '>=12'}
+
+  d3@7.9.0:
+    resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
+    engines: {node: '>=12'}
+
+  dagre-d3-es@7.0.10:
+    resolution: {integrity: sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==}
+
   data-urls@5.0.0:
     resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
     engines: {node: '>=18'}
 
+  dayjs@1.11.13:
+    resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
+
   debug@4.4.0:
     resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
     engines: {node: '>=6.0'}
@@ -1104,10 +1976,21 @@ packages:
   decimal.js@10.5.0:
     resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
 
+  decode-named-character-reference@1.1.0:
+    resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
+
   deep-eql@5.0.2:
     resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
     engines: {node: '>=6'}
 
+  deferred-leveldown@5.3.0:
+    resolution: {integrity: sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==}
+    engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
+
+  delaunator@5.0.1:
+    resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
+
   delayed-stream@1.0.0:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
@@ -1123,9 +2006,20 @@ packages:
   detect-node-es@1.1.0:
     resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
 
+  diff@5.2.0:
+    resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
+    engines: {node: '>=0.3.1'}
+
+  diff@7.0.0:
+    resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==}
+    engines: {node: '>=0.3.1'}
+
   dom-accessibility-api@0.5.16:
     resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
 
+  dompurify@3.1.6:
+    resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==}
+
   dunder-proto@1.0.1:
     resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
     engines: {node: '>= 0.4'}
@@ -1136,12 +2030,20 @@ packages:
   electron-to-chromium@1.5.120:
     resolution: {integrity: sha512-oTUp3gfX1gZI+xfD2djr2rzQdHCwHzPQrrK0CD7WpTdF0nPdQ/INcRVjWgLdCT4a9W3jFObR9DAfsuyFQnI8CQ==}
 
+  elkjs@0.9.3:
+    resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==}
+
   emoji-regex@8.0.0:
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
 
   emoji-regex@9.2.2:
     resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
 
+  encoding-down@6.3.0:
+    resolution: {integrity: sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==}
+    engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
+
   enhanced-resolve@5.18.1:
     resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
     engines: {node: '>=10.13.0'}
@@ -1150,6 +2052,10 @@ packages:
     resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
     engines: {node: '>=0.12'}
 
+  errno@0.1.8:
+    resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
+    hasBin: true
+
   es-define-property@1.0.1:
     resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
     engines: {node: '>= 0.4'}
@@ -1169,6 +2075,10 @@ packages:
     resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
     engines: {node: '>= 0.4'}
 
+  es6-promise-pool@2.5.0:
+    resolution: {integrity: sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA==}
+    engines: {node: '>=0.10.0'}
+
   esbuild@0.25.1:
     resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==}
     engines: {node: '>=18'}
@@ -1185,6 +2095,10 @@ packages:
     resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==}
     engines: {node: '>=12.0.0'}
 
+  fill-range@7.1.1:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+    engines: {node: '>=8'}
+
   foreground-child@3.3.1:
     resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
     engines: {node: '>=14'}
@@ -1193,6 +2107,10 @@ packages:
     resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
     engines: {node: '>= 6'}
 
+  fractional-indexing@3.2.0:
+    resolution: {integrity: sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==}
+    engines: {node: ^14.13.1 || >=16.0.0}
+
   fsevents@2.3.3:
     resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1201,6 +2119,10 @@ packages:
   function-bind@1.1.2:
     resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
 
+  fuzzy@0.1.3:
+    resolution: {integrity: sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==}
+    engines: {node: '>= 0.6.0'}
+
   gensync@1.0.0-beta.2:
     resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
     engines: {node: '>=6.9.0'}
@@ -1217,6 +2139,13 @@ packages:
     resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
     engines: {node: '>= 0.4'}
 
+  get-tsconfig@4.10.0:
+    resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
+
+  glob-parent@5.1.2:
+    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+    engines: {node: '>= 6'}
+
   glob@10.4.5:
     resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
     hasBin: true
@@ -1225,6 +2154,9 @@ packages:
     resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
     engines: {node: '>=4'}
 
+  glur@1.1.2:
+    resolution: {integrity: sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==}
+
   goober@2.1.16:
     resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==}
     peerDependencies:
@@ -1237,6 +2169,9 @@ packages:
   graceful-fs@4.2.11:
     resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
 
+  hachure-fill@0.5.2:
+    resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
+
   has-flag@4.0.0:
     resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
     engines: {node: '>=8'}
@@ -1272,16 +2207,57 @@ packages:
   idb@8.0.2:
     resolution: {integrity: sha512-CX70rYhx7GDDQzwwQMDwF6kDRQi5vVs6khHUumDrMecBylKkwvZ8HWvKV08AGb7VbpoGCWUQ4aHzNDgoUiOIUg==}
 
+  ieee754@1.2.1:
+    resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+  image-blob-reduce@3.0.1:
+    resolution: {integrity: sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q==}
+
+  immediate@3.3.0:
+    resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==}
+
+  immutable@4.3.7:
+    resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==}
+
+  inherits@2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+  internmap@1.0.1:
+    resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==}
+
+  internmap@2.0.3:
+    resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+    engines: {node: '>=12'}
+
+  is-binary-path@2.1.0:
+    resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+    engines: {node: '>=8'}
+
+  is-extglob@2.1.1:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+    engines: {node: '>=0.10.0'}
+
   is-fullwidth-code-point@3.0.0:
     resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
     engines: {node: '>=8'}
 
+  is-glob@4.0.3:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+    engines: {node: '>=0.10.0'}
+
+  is-number@7.0.0:
+    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+    engines: {node: '>=0.12.0'}
+
   is-potential-custom-element-name@1.0.1:
     resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
 
   isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
 
+  isomorphic.js@0.2.5:
+    resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
+
   jackspeak@3.4.3:
     resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
 
@@ -1289,6 +2265,24 @@ packages:
     resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
     hasBin: true
 
+  jotai-scope@0.7.2:
+    resolution: {integrity: sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg==}
+    peerDependencies:
+      jotai: '>=2.9.2'
+      react: '>=17.0.0'
+
+  jotai@2.11.0:
+    resolution: {integrity: sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==}
+    engines: {node: '>=12.20.0'}
+    peerDependencies:
+      '@types/react': '>=17.0.0'
+      react: '>=17.0.0'
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      react:
+        optional: true
+
   js-tokens@4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
 
@@ -1311,9 +2305,77 @@ packages:
     engines: {node: '>=6'}
     hasBin: true
 
+  katex@0.16.21:
+    resolution: {integrity: sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==}
+    hasBin: true
+
+  khroma@2.1.0:
+    resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
+
+  kleur@4.1.5:
+    resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+    engines: {node: '>=6'}
+
   kolorist@1.8.0:
     resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
 
+  layout-base@1.0.2:
+    resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==}
+
+  level-codec@9.0.2:
+    resolution: {integrity: sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==}
+    engines: {node: '>=6'}
+    deprecated: Superseded by level-transcoder (https://github.com/Level/community#faq)
+
+  level-concat-iterator@2.0.1:
+    resolution: {integrity: sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==}
+    engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
+
+  level-errors@2.0.1:
+    resolution: {integrity: sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==}
+    engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
+
+  level-iterator-stream@4.0.2:
+    resolution: {integrity: sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==}
+    engines: {node: '>=6'}
+
+  level-js@5.0.2:
+    resolution: {integrity: sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==}
+    deprecated: Superseded by browser-level (https://github.com/Level/community#faq)
+
+  level-packager@5.1.1:
+    resolution: {integrity: sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==}
+    engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
+
+  level-supports@1.0.1:
+    resolution: {integrity: sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==}
+    engines: {node: '>=6'}
+
+  level@6.0.1:
+    resolution: {integrity: sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==}
+    engines: {node: '>=8.6.0'}
+
+  leveldown@5.6.0:
+    resolution: {integrity: sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==}
+    engines: {node: '>=8.6.0'}
+    deprecated: Superseded by classic-level (https://github.com/Level/community#faq)
+
+  levelup@4.4.0:
+    resolution: {integrity: sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==}
+    engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
+
+  lexical@0.28.0:
+    resolution: {integrity: sha512-dLE3O1PZg0TlZxRQo9YDpjCjDUj8zluGyBO9MHdjo21qZmMUNrxQPeCRt8fn2s5l4HKYFQ1YNgl7k1pOJB/vZQ==}
+
+  lib0@0.2.99:
+    resolution: {integrity: sha512-vwztYuUf1uf/1zQxfzRfO5yzfNKhTtgOByCruuiQQxWQXnPb8Itaube5ylofcV0oM0aKal9Mv+S1s1Ky0UYP1w==}
+    engines: {node: '>=16'}
+    hasBin: true
+
   lightningcss-darwin-arm64@1.29.2:
     resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==}
     engines: {node: '>= 12.0.0'}
@@ -1378,9 +2440,22 @@ packages:
     resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
     engines: {node: '>= 12.0.0'}
 
+  lodash-es@4.17.21:
+    resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
+
+  lodash.debounce@4.0.8:
+    resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+
   lodash.sortby@4.7.0:
     resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
 
+  lodash.throttle@4.1.1:
+    resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
+
+  loose-envify@1.4.0:
+    resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+    hasBin: true
+
   loupe@3.1.3:
     resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
 
@@ -1390,6 +2465,9 @@ packages:
   lru-cache@5.1.1:
     resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
 
+  ltgt@2.2.1:
+    resolution: {integrity: sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==}
+
   lucide-react@0.483.0:
     resolution: {integrity: sha512-WldsY17Qb/T3VZdMnVQ9C3DDIP7h1ViDTHVdVGnLZcvHNg30zH/MTQ04RTORjexoGmpsXroiQXZ4QyR0kBy0FA==}
     peerDependencies:
@@ -1406,6 +2484,78 @@ packages:
     resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
     engines: {node: '>= 0.4'}
 
+  mdast-util-from-markdown@1.3.1:
+    resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==}
+
+  mdast-util-to-string@3.2.0:
+    resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==}
+
+  mermaid@10.9.3:
+    resolution: {integrity: sha512-V80X1isSEvAewIL3xhmz/rVmc27CVljcsbWxkxlWJWY/1kQa4XOABqpDl2qQLGKzpKm6WbTfUEKImBlUfFYArw==}
+
+  micromark-core-commonmark@1.1.0:
+    resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==}
+
+  micromark-factory-destination@1.1.0:
+    resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==}
+
+  micromark-factory-label@1.1.0:
+    resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==}
+
+  micromark-factory-space@1.1.0:
+    resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==}
+
+  micromark-factory-title@1.1.0:
+    resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==}
+
+  micromark-factory-whitespace@1.1.0:
+    resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==}
+
+  micromark-util-character@1.2.0:
+    resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==}
+
+  micromark-util-chunked@1.1.0:
+    resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==}
+
+  micromark-util-classify-character@1.1.0:
+    resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==}
+
+  micromark-util-combine-extensions@1.1.0:
+    resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==}
+
+  micromark-util-decode-numeric-character-reference@1.1.0:
+    resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==}
+
+  micromark-util-decode-string@1.1.0:
+    resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==}
+
+  micromark-util-encode@1.1.0:
+    resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==}
+
+  micromark-util-html-tag-name@1.2.0:
+    resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==}
+
+  micromark-util-normalize-identifier@1.1.0:
+    resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==}
+
+  micromark-util-resolve-all@1.1.0:
+    resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==}
+
+  micromark-util-sanitize-uri@1.2.0:
+    resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==}
+
+  micromark-util-subtokenize@1.1.0:
+    resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==}
+
+  micromark-util-symbol@1.1.0:
+    resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==}
+
+  micromark-util-types@1.1.0:
+    resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==}
+
+  micromark@3.2.0:
+    resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==}
+
   mime-db@1.52.0:
     resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
     engines: {node: '>= 0.6'}
@@ -1422,26 +2572,70 @@ packages:
     resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
     engines: {node: '>=16 || 14 >=14.17'}
 
+  mri@1.2.0:
+    resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
+    engines: {node: '>=4'}
+
   ms@2.1.3:
     resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
 
+  multimath@2.0.0:
+    resolution: {integrity: sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==}
+
   nanoid@3.3.11:
     resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     hasBin: true
 
+  nanoid@3.3.3:
+    resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
+  nanoid@4.0.2:
+    resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
+    engines: {node: ^14 || ^16 || >=18}
+    hasBin: true
+
+  napi-macros@2.0.0:
+    resolution: {integrity: sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==}
+
+  node-gyp-build@4.1.1:
+    resolution: {integrity: sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==}
+    hasBin: true
+
   node-releases@2.0.19:
     resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
 
+  non-layered-tidy-tree-layout@2.0.2:
+    resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==}
+
+  normalize-path@3.0.0:
+    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+    engines: {node: '>=0.10.0'}
+
   nwsapi@2.2.19:
     resolution: {integrity: sha512-94bcyI3RsqiZufXjkr3ltkI86iEl+I7uiHVDtcq9wJUTwYQJ5odHDeSzkkrRzi80jJ8MaeZgqKjH1bAWAFw9bA==}
 
+  object-assign@4.1.1:
+    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+    engines: {node: '>=0.10.0'}
+
+  open-color@1.9.1:
+    resolution: {integrity: sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw==}
+
   package-json-from-dist@1.0.1:
     resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
 
+  pako@2.0.3:
+    resolution: {integrity: sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw==}
+
   parse5@7.2.1:
     resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==}
 
+  path-data-parser@0.1.0:
+    resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==}
+
   path-key@3.1.1:
     resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
     engines: {node: '>=8'}
@@ -1457,13 +2651,46 @@ packages:
     resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
     engines: {node: '>= 14.16'}
 
+  perfect-freehand@1.2.0:
+    resolution: {integrity: sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw==}
+
+  pica@7.1.1:
+    resolution: {integrity: sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ==}
+
   picocolors@1.1.1:
     resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
 
+  picomatch@2.3.1:
+    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+    engines: {node: '>=8.6'}
+
+  png-chunk-text@1.0.0:
+    resolution: {integrity: sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw==}
+
+  png-chunks-encode@1.0.0:
+    resolution: {integrity: sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA==}
+
+  png-chunks-extract@1.0.0:
+    resolution: {integrity: sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==}
+
+  points-on-curve@0.2.0:
+    resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==}
+
+  points-on-curve@1.0.1:
+    resolution: {integrity: sha512-3nmX4/LIiyuwGLwuUrfhTlDeQFlAhi7lyK/zcRNGhalwapDWgAGR82bUpmn2mA03vII3fvNCG8jAONzKXwpxAg==}
+
+  points-on-path@0.2.1:
+    resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==}
+
   postcss@8.5.3:
     resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
     engines: {node: ^10 || ^12 || >=14}
 
+  prettier@3.5.3:
+    resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
+    engines: {node: '>=14'}
+    hasBin: true
+
   pretty-bytes@6.1.1:
     resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
     engines: {node: ^14.13.1 || >=16.0.0}
@@ -1472,15 +2699,37 @@ packages:
     resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
 
+  prismjs@1.30.0:
+    resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
+    engines: {node: '>=6'}
+
+  prop-types@15.8.1:
+    resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+
+  prr@1.0.1:
+    resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
+
   punycode@2.3.1:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
 
+  pwacompat@2.0.17:
+    resolution: {integrity: sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w==}
+
   react-dom@19.0.0:
     resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==}
     peerDependencies:
       react: ^19.0.0
 
+  react-error-boundary@3.1.4:
+    resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==}
+    engines: {node: '>=10', npm: '>=6'}
+    peerDependencies:
+      react: '>=16.13.1'
+
+  react-is@16.13.1:
+    resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
   react-is@17.0.2:
     resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
 
@@ -1498,6 +2747,16 @@ packages:
       '@types/react':
         optional: true
 
+  react-remove-scroll@2.5.5:
+    resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   react-remove-scroll@2.6.3:
     resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==}
     engines: {node: '>=10'}
@@ -1522,20 +2781,52 @@ packages:
     resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
     engines: {node: '>=0.10.0'}
 
+  readable-stream@3.6.2:
+    resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+    engines: {node: '>= 6'}
+
+  readdirp@3.6.0:
+    resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+    engines: {node: '>=8.10.0'}
+
   regenerator-runtime@0.14.1:
     resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
 
+  resolve-pkg-maps@1.0.0:
+    resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+  robust-predicates@3.0.2:
+    resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
+
   rollup@4.36.0:
     resolution: {integrity: sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
 
+  roughjs@4.6.4:
+    resolution: {integrity: sha512-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw==}
+
   rrweb-cssom@0.8.0:
     resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
 
+  rw@1.3.3:
+    resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
+
+  sade@1.8.1:
+    resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
+    engines: {node: '>=6'}
+
+  safe-buffer@5.2.1:
+    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
   safer-buffer@2.1.2:
     resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
 
+  sass@1.51.0:
+    resolution: {integrity: sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==}
+    engines: {node: '>=12.0.0'}
+    hasBin: true
+
   saxes@6.0.0:
     resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
     engines: {node: '>=v12.22.7'}
@@ -1580,6 +2871,9 @@ packages:
     resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
     engines: {node: '>=14'}
 
+  sliced@1.0.1:
+    resolution: {integrity: sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==}
+
   solid-js@1.9.5:
     resolution: {integrity: sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw==}
 
@@ -1605,6 +2899,9 @@ packages:
     resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
     engines: {node: '>=12'}
 
+  string_decoder@1.3.0:
+    resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+
   strip-ansi@6.0.1:
     resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
     engines: {node: '>=8'}
@@ -1613,6 +2910,9 @@ packages:
     resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
     engines: {node: '>=12'}
 
+  stylis@4.3.6:
+    resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
+
   supports-color@7.2.0:
     resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
     engines: {node: '>=8'}
@@ -1661,6 +2961,10 @@ packages:
     resolution: {integrity: sha512-aRGIbCIF3teodtUFAYSdQONVmDRy21REM3o6JnqWn5ZkQBJJ4gHxhw6OfwQ+WkSAi3ASamrS4N4nyazWx6uTYg==}
     hasBin: true
 
+  to-regex-range@5.0.1:
+    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+    engines: {node: '>=8.0'}
+
   tough-cookie@5.1.2:
     resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
     engines: {node: '>=16'}
@@ -1672,9 +2976,21 @@ packages:
     resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==}
     engines: {node: '>=18'}
 
+  ts-dedent@2.2.0:
+    resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
+    engines: {node: '>=6.10'}
+
   tslib@2.8.1:
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
 
+  tsx@4.19.3:
+    resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==}
+    engines: {node: '>=18.0.0'}
+    hasBin: true
+
+  tunnel-rat@0.1.2:
+    resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==}
+
   tw-animate-css@1.2.4:
     resolution: {integrity: sha512-yt+HkJB41NAvOffe4NweJU6fLqAlVx/mBX6XmHRp15kq0JxTtOKaIw8pVSWM1Z+n2nXtyi7cW6C9f0WG/F/QAQ==}
 
@@ -1683,6 +2999,13 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
+  unist-util-stringify-position@3.0.3:
+    resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==}
+
+  unplugin@2.2.0:
+    resolution: {integrity: sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==}
+    engines: {node: '>=18.12.0'}
+
   update-browserslist-db@1.1.3:
     resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
     hasBin: true
@@ -1714,6 +3037,18 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
 
+  util-deprecate@1.0.2:
+    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+  uuid@9.0.1:
+    resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
+    hasBin: true
+
+  uvu@0.5.6:
+    resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==}
+    engines: {node: '>=8'}
+    hasBin: true
+
   vite-node@3.0.9:
     resolution: {integrity: sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==}
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -1794,6 +3129,9 @@ packages:
   web-vitals@4.2.4:
     resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
 
+  web-worker@1.5.0:
+    resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==}
+
   webidl-conversions@4.0.2:
     resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
 
@@ -1801,6 +3139,12 @@ packages:
     resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
     engines: {node: '>=12'}
 
+  webpack-virtual-modules@0.6.2:
+    resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+
+  webworkify@1.5.0:
+    resolution: {integrity: sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==}
+
   whatwg-encoding@3.1.1:
     resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
     engines: {node: '>=18'}
@@ -1834,6 +3178,17 @@ packages:
     resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
     engines: {node: '>=12'}
 
+  ws@6.2.3:
+    resolution: {integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: ^5.0.2
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
   ws@8.18.1:
     resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==}
     engines: {node: '>=10.0.0'}
@@ -1853,12 +3208,65 @@ packages:
   xmlchars@2.2.0:
     resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
 
+  xtend@4.0.2:
+    resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
+    engines: {node: '>=0.4'}
+
+  y-excalidraw@2.0.12:
+    resolution: {integrity: sha512-/dp0MUSD7WC4TFXsv9DyXxeg+CQoSM4iwh9UpLx8+VFwAg47F3O9KW62C/Jkuq7Rt3y9MAmKC2rE8beZOHCGaw==}
+    peerDependencies:
+      '@excalidraw/excalidraw': ^0.17.6
+      yjs: ^13.6.19
+
+  y-indexeddb@9.0.12:
+    resolution: {integrity: sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==}
+    engines: {node: '>=16.0.0', npm: '>=8.0.0'}
+    peerDependencies:
+      yjs: ^13.0.0
+
+  y-leveldb@0.1.2:
+    resolution: {integrity: sha512-6ulEn5AXfXJYi89rXPEg2mMHAyyw8+ZfeMMdOtBbV8FJpQ1NOrcgi6DTAcXof0dap84NjHPT2+9d0rb6cFsjEg==}
+    peerDependencies:
+      yjs: ^13.0.0
+
+  y-protocols@1.0.6:
+    resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==}
+    engines: {node: '>=16.0.0', npm: '>=8.0.0'}
+    peerDependencies:
+      yjs: ^13.0.0
+
+  y-websocket@2.1.0:
+    resolution: {integrity: sha512-WHYDRqomaGkkaujtowCDwL8KYk+t1zQCGIgKyvxvchhjTQlMgWXRHJK+FDEcWmHA7I7o/4fy0eniOrtmz0e4mA==}
+    engines: {node: '>=16.0.0', npm: '>=8.0.0'}
+    hasBin: true
+    peerDependencies:
+      yjs: ^13.5.6
+
   yallist@3.1.1:
     resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
 
+  yjs@13.6.24:
+    resolution: {integrity: sha512-xn/pYLTZa3uD1uDG8lpxfLRo5SR/rp0frdASOl2a71aYNvUXdWcLtVL91s2y7j+Q8ppmjZ9H3jsGVgoFMbT2VA==}
+    engines: {node: '>=16.0.0', npm: '>=8.0.0'}
+
   zod@3.24.2:
     resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
 
+  zustand@4.5.6:
+    resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==}
+    engines: {node: '>=12.7.0'}
+    peerDependencies:
+      '@types/react': '>=16.8'
+      immer: '>=9.0.6'
+      react: '>=16.8'
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      immer:
+        optional: true
+      react:
+        optional: true
+
 snapshots:
 
   '@ampproject/remapping@2.3.0':
@@ -1951,6 +3359,16 @@ snapshots:
     dependencies:
       '@babel/types': 7.26.10
 
+  '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10)':
+    dependencies:
+      '@babel/core': 7.26.10
+      '@babel/helper-plugin-utils': 7.26.5
+
+  '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.10)':
+    dependencies:
+      '@babel/core': 7.26.10
+      '@babel/helper-plugin-utils': 7.26.5
+
   '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.10)':
     dependencies:
       '@babel/core': 7.26.10
@@ -1988,6 +3406,8 @@ snapshots:
       '@babel/helper-string-parser': 7.25.9
       '@babel/helper-validator-identifier': 7.25.9
 
+  '@braintree/sanitize-url@6.0.2': {}
+
   '@csstools/color-helpers@5.0.2': {}
 
   '@csstools/css-calc@2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)':
@@ -2083,6 +3503,61 @@ snapshots:
   '@esbuild/win32-x64@0.25.1':
     optional: true
 
+  '@excalidraw/excalidraw@0.18.0(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@braintree/sanitize-url': 6.0.2
+      '@excalidraw/laser-pointer': 1.3.1
+      '@excalidraw/mermaid-to-excalidraw': 1.1.2
+      '@excalidraw/random-username': 1.1.0
+      '@radix-ui/react-popover': 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-tabs': 1.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      browser-fs-access: 0.29.1
+      canvas-roundrect-polyfill: 0.0.1
+      clsx: 1.1.1
+      cross-env: 7.0.3
+      es6-promise-pool: 2.5.0
+      fractional-indexing: 3.2.0
+      fuzzy: 0.1.3
+      image-blob-reduce: 3.0.1
+      jotai: 2.11.0(@types/react@19.0.11)(react@19.0.0)
+      jotai-scope: 0.7.2(jotai@2.11.0(@types/react@19.0.11)(react@19.0.0))(react@19.0.0)
+      lodash.debounce: 4.0.8
+      lodash.throttle: 4.1.1
+      nanoid: 3.3.3
+      open-color: 1.9.1
+      pako: 2.0.3
+      perfect-freehand: 1.2.0
+      pica: 7.1.1
+      png-chunk-text: 1.0.0
+      png-chunks-encode: 1.0.0
+      png-chunks-extract: 1.0.0
+      points-on-curve: 1.0.1
+      pwacompat: 2.0.17
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+      roughjs: 4.6.4
+      sass: 1.51.0
+      tunnel-rat: 0.1.2(@types/react@19.0.11)(react@19.0.0)
+    transitivePeerDependencies:
+      - '@types/react'
+      - '@types/react-dom'
+      - immer
+      - supports-color
+
+  '@excalidraw/laser-pointer@1.3.1': {}
+
+  '@excalidraw/markdown-to-text@0.1.2': {}
+
+  '@excalidraw/mermaid-to-excalidraw@1.1.2':
+    dependencies:
+      '@excalidraw/markdown-to-text': 0.1.2
+      mermaid: 10.9.3
+      nanoid: 4.0.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@excalidraw/random-username@1.1.0': {}
+
   '@floating-ui/core@1.6.9':
     dependencies:
       '@floating-ui/utils': 0.2.9
@@ -2126,11 +3601,184 @@ snapshots:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.0
 
+  '@lexical/clipboard@0.28.0':
+    dependencies:
+      '@lexical/html': 0.28.0
+      '@lexical/list': 0.28.0
+      '@lexical/selection': 0.28.0
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/code@0.28.0':
+    dependencies:
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+      prismjs: 1.30.0
+
+  '@lexical/devtools-core@0.28.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@lexical/html': 0.28.0
+      '@lexical/link': 0.28.0
+      '@lexical/mark': 0.28.0
+      '@lexical/table': 0.28.0
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+
+  '@lexical/dragon@0.28.0':
+    dependencies:
+      lexical: 0.28.0
+
+  '@lexical/hashtag@0.28.0':
+    dependencies:
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/history@0.28.0':
+    dependencies:
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/html@0.28.0':
+    dependencies:
+      '@lexical/selection': 0.28.0
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/link@0.28.0':
+    dependencies:
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/list@0.28.0':
+    dependencies:
+      '@lexical/selection': 0.28.0
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/mark@0.28.0':
+    dependencies:
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/markdown@0.28.0':
+    dependencies:
+      '@lexical/code': 0.28.0
+      '@lexical/link': 0.28.0
+      '@lexical/list': 0.28.0
+      '@lexical/rich-text': 0.28.0
+      '@lexical/text': 0.28.0
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/offset@0.28.0':
+    dependencies:
+      lexical: 0.28.0
+
+  '@lexical/overflow@0.28.0':
+    dependencies:
+      lexical: 0.28.0
+
+  '@lexical/plain-text@0.28.0':
+    dependencies:
+      '@lexical/clipboard': 0.28.0
+      '@lexical/selection': 0.28.0
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/react@0.28.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(yjs@13.6.24)':
+    dependencies:
+      '@lexical/devtools-core': 0.28.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@lexical/dragon': 0.28.0
+      '@lexical/hashtag': 0.28.0
+      '@lexical/history': 0.28.0
+      '@lexical/link': 0.28.0
+      '@lexical/list': 0.28.0
+      '@lexical/mark': 0.28.0
+      '@lexical/markdown': 0.28.0
+      '@lexical/overflow': 0.28.0
+      '@lexical/plain-text': 0.28.0
+      '@lexical/rich-text': 0.28.0
+      '@lexical/table': 0.28.0
+      '@lexical/text': 0.28.0
+      '@lexical/utils': 0.28.0
+      '@lexical/yjs': 0.28.0(yjs@13.6.24)
+      lexical: 0.28.0
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+      react-error-boundary: 3.1.4(react@19.0.0)
+    transitivePeerDependencies:
+      - yjs
+
+  '@lexical/rich-text@0.28.0':
+    dependencies:
+      '@lexical/clipboard': 0.28.0
+      '@lexical/selection': 0.28.0
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/selection@0.28.0':
+    dependencies:
+      lexical: 0.28.0
+
+  '@lexical/table@0.28.0':
+    dependencies:
+      '@lexical/clipboard': 0.28.0
+      '@lexical/utils': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/text@0.28.0':
+    dependencies:
+      lexical: 0.28.0
+
+  '@lexical/utils@0.28.0':
+    dependencies:
+      '@lexical/list': 0.28.0
+      '@lexical/selection': 0.28.0
+      '@lexical/table': 0.28.0
+      lexical: 0.28.0
+
+  '@lexical/yjs@0.28.0(yjs@13.6.24)':
+    dependencies:
+      '@lexical/offset': 0.28.0
+      '@lexical/selection': 0.28.0
+      lexical: 0.28.0
+      yjs: 13.6.24
+
+  '@mdi/js@7.4.47': {}
+
+  '@mdi/react@1.6.1':
+    dependencies:
+      prop-types: 15.8.1
+
   '@pkgjs/parseargs@0.11.0':
     optional: true
 
+  '@radix-ui/primitive@1.0.0':
+    dependencies:
+      '@babel/runtime': 7.26.10
+
+  '@radix-ui/primitive@1.0.1':
+    dependencies:
+      '@babel/runtime': 7.26.10
+
   '@radix-ui/primitive@1.1.1': {}
 
+  '@radix-ui/react-alert-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-dialog': 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-slot': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-arrow@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -2140,18 +3788,99 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-avatar@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-collection@1.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-compose-refs': 1.0.0(react@19.0.0)
+      '@radix-ui/react-context': 1.0.0(react@19.0.0)
+      '@radix-ui/react-primitive': 1.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-slot': 1.0.1(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+
+  '@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-slot': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-compose-refs@1.0.0(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      react: 19.0.0
+
+  '@radix-ui/react-compose-refs@1.0.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-compose-refs@1.1.1(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       react: 19.0.0
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-context@1.0.0(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      react: 19.0.0
+
+  '@radix-ui/react-context@1.0.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-context@1.1.1(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       react: 19.0.0
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-dialog@1.0.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/primitive': 1.0.1
+      '@radix-ui/react-compose-refs': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-focus-guards': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-id': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-portal': 1.0.4(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-presence': 1.0.1(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-primitive': 1.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-slot': 1.0.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      aria-hidden: 1.2.4
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+      react-remove-scroll: 2.5.5(@types/react@19.0.11)(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/primitive': 1.1.1
@@ -2174,6 +3903,31 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-direction@1.0.0(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      react: 19.0.0
+
+  '@radix-ui/react-direction@1.1.0(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
+  '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/primitive': 1.0.1
+      '@radix-ui/react-compose-refs': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 1.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/primitive': 1.1.1
@@ -2187,12 +3941,46 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-dropdown-menu@2.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-id': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-menu': 2.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-focus-guards@1.0.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       react: 19.0.0
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-compose-refs': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 1.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-focus-scope@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
@@ -2204,6 +3992,20 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-id@1.0.0(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-use-layout-effect': 1.0.0(react@19.0.0)
+      react: 19.0.0
+
+  '@radix-ui/react-id@1.0.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-id@1.1.0(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.11)(react@19.0.0)
@@ -2211,6 +4013,64 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-label@2.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-menu@2.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-collection': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-direction': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-id': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-roving-focus': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-slot': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      aria-hidden: 1.2.4
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+      react-remove-scroll: 2.6.3(@types/react@19.0.11)(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-popover@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-id': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-slot': 1.1.2(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      aria-hidden: 1.2.4
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+      react-remove-scroll: 2.6.3(@types/react@19.0.11)(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-popper@1.2.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -2229,6 +4089,16 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-portal@1.0.4(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-primitive': 1.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-portal@1.1.4(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -2239,6 +4109,25 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-presence@1.0.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-compose-refs': 1.0.0(react@19.0.0)
+      '@radix-ui/react-use-layout-effect': 1.0.0(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+
+  '@radix-ui/react-presence@1.0.1(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-compose-refs': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-presence@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
@@ -2249,6 +4138,23 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-primitive@1.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-slot': 1.0.1(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+
+  '@radix-ui/react-primitive@1.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-slot': 1.0.2(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-primitive@2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-slot': 1.1.2(@types/react@19.0.11)(react@19.0.0)
@@ -2258,6 +4164,56 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-radio-group@1.2.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-direction': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-roving-focus': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-previous': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-roving-focus@1.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/primitive': 1.0.0
+      '@radix-ui/react-collection': 1.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-compose-refs': 1.0.0(react@19.0.0)
+      '@radix-ui/react-context': 1.0.0(react@19.0.0)
+      '@radix-ui/react-direction': 1.0.0(react@19.0.0)
+      '@radix-ui/react-id': 1.0.0(react@19.0.0)
+      '@radix-ui/react-primitive': 1.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.0.0(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.0.0(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+
+  '@radix-ui/react-roving-focus@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-collection': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-direction': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-id': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-separator@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -2267,6 +4223,20 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-slot@1.0.1(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-compose-refs': 1.0.0(react@19.0.0)
+      react: 19.0.0
+
+  '@radix-ui/react-slot@1.0.2(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-compose-refs': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-slot@1.1.2(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.11)(react@19.0.0)
@@ -2274,6 +4244,46 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-tabs@1.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/primitive': 1.0.0
+      '@radix-ui/react-context': 1.0.0(react@19.0.0)
+      '@radix-ui/react-direction': 1.0.0(react@19.0.0)
+      '@radix-ui/react-id': 1.0.0(react@19.0.0)
+      '@radix-ui/react-presence': 1.0.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-primitive': 1.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-roving-focus': 1.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.0.0(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+
+  '@radix-ui/react-toggle-group@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-context': 1.1.1(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-direction': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-roving-focus': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-toggle': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
+  '@radix-ui/react-toggle@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.1
+      '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      '@types/react-dom': 19.0.4(@types/react@19.0.11)
+
   '@radix-ui/react-tooltip@1.1.8(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@radix-ui/primitive': 1.1.1
@@ -2294,12 +4304,38 @@ snapshots:
       '@types/react': 19.0.11
       '@types/react-dom': 19.0.4(@types/react@19.0.11)
 
+  '@radix-ui/react-use-callback-ref@1.0.0(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      react: 19.0.0
+
+  '@radix-ui/react-use-callback-ref@1.0.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       react: 19.0.0
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-use-controllable-state@1.0.0(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-use-callback-ref': 1.0.0(react@19.0.0)
+      react: 19.0.0
+
+  '@radix-ui/react-use-controllable-state@1.0.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
@@ -2307,6 +4343,14 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.11)(react@19.0.0)
@@ -2314,12 +4358,30 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-use-layout-effect@1.0.0(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      react: 19.0.0
+
+  '@radix-ui/react-use-layout-effect@1.0.1(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      '@babel/runtime': 7.26.10
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       react: 19.0.0
     optionalDependencies:
       '@types/react': 19.0.11
 
+  '@radix-ui/react-use-previous@1.1.0(@types/react@19.0.11)(react@19.0.0)':
+    dependencies:
+      react: 19.0.0
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   '@radix-ui/react-use-rect@1.1.0(@types/react@19.0.11)(react@19.0.0)':
     dependencies:
       '@radix-ui/rect': 1.1.0
@@ -2412,13 +4474,13 @@ snapshots:
     optionalDependencies:
       typescript: 5.8.2
 
-  '@serwist/vite@9.0.12(typescript@5.8.2)(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2))':
+  '@serwist/vite@9.0.12(typescript@5.8.2)(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3))':
     dependencies:
       '@serwist/build': 9.0.12(typescript@5.8.2)
       glob: 10.4.5
       kolorist: 1.8.0
       serwist: 9.0.12(typescript@5.8.2)
-      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)
+      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3)
       zod: 3.24.2
     optionalDependencies:
       typescript: 5.8.2
@@ -2483,13 +4545,13 @@ snapshots:
       '@tailwindcss/oxide-win32-arm64-msvc': 4.0.14
       '@tailwindcss/oxide-win32-x64-msvc': 4.0.14
 
-  '@tailwindcss/vite@4.0.14(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2))':
+  '@tailwindcss/vite@4.0.14(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3))':
     dependencies:
       '@tailwindcss/node': 4.0.14
       '@tailwindcss/oxide': 4.0.14
       lightningcss: 1.29.2
       tailwindcss: 4.0.14
-      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)
+      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3)
 
   '@tanstack/history@1.114.22': {}
 
@@ -2539,8 +4601,51 @@ snapshots:
     optionalDependencies:
       csstype: 3.1.3
 
+  '@tanstack/router-generator@1.114.25(@tanstack/react-router@1.114.25(react-dom@19.0.0(react@19.0.0))(react@19.0.0))':
+    dependencies:
+      '@tanstack/virtual-file-routes': 1.114.12
+      prettier: 3.5.3
+      tsx: 4.19.3
+      zod: 3.24.2
+    optionalDependencies:
+      '@tanstack/react-router': 1.114.25(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+
+  '@tanstack/router-plugin@1.114.25(@tanstack/react-router@1.114.25(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3))':
+    dependencies:
+      '@babel/core': 7.26.10
+      '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10)
+      '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.10)
+      '@babel/template': 7.26.9
+      '@babel/traverse': 7.26.10
+      '@babel/types': 7.26.10
+      '@tanstack/router-core': 1.114.25
+      '@tanstack/router-generator': 1.114.25(@tanstack/react-router@1.114.25(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
+      '@tanstack/router-utils': 1.114.12
+      '@tanstack/virtual-file-routes': 1.114.12
+      '@types/babel__core': 7.20.5
+      '@types/babel__template': 7.4.4
+      '@types/babel__traverse': 7.20.6
+      babel-dead-code-elimination: 1.0.9
+      chokidar: 3.6.0
+      unplugin: 2.2.0
+      zod: 3.24.2
+    optionalDependencies:
+      '@tanstack/react-router': 1.114.25(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@tanstack/router-utils@1.114.12':
+    dependencies:
+      '@babel/generator': 7.26.10
+      '@babel/parser': 7.26.10
+      ansis: 3.17.0
+      diff: 7.0.0
+
   '@tanstack/store@0.7.0': {}
 
+  '@tanstack/virtual-file-routes@1.114.12': {}
+
   '@testing-library/dom@10.4.0':
     dependencies:
       '@babel/code-frame': 7.26.2
@@ -2585,8 +4690,26 @@ snapshots:
     dependencies:
       '@babel/types': 7.26.10
 
+  '@types/d3-scale-chromatic@3.1.0': {}
+
+  '@types/d3-scale@4.0.9':
+    dependencies:
+      '@types/d3-time': 3.0.4
+
+  '@types/d3-time@3.0.4': {}
+
+  '@types/debug@4.1.12':
+    dependencies:
+      '@types/ms': 2.1.0
+
   '@types/estree@1.0.6': {}
 
+  '@types/mdast@3.0.15':
+    dependencies:
+      '@types/unist': 2.0.11
+
+  '@types/ms@2.1.0': {}
+
   '@types/react-dom@19.0.4(@types/react@19.0.11)':
     dependencies:
       '@types/react': 19.0.11
@@ -2597,14 +4720,16 @@ snapshots:
 
   '@types/trusted-types@2.0.7': {}
 
-  '@vitejs/plugin-react@4.3.4(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2))':
+  '@types/unist@2.0.11': {}
+
+  '@vitejs/plugin-react@4.3.4(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3))':
     dependencies:
       '@babel/core': 7.26.10
       '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10)
       '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10)
       '@types/babel__core': 7.20.5
       react-refresh: 0.14.2
-      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)
+      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3)
     transitivePeerDependencies:
       - supports-color
 
@@ -2615,13 +4740,13 @@ snapshots:
       chai: 5.2.0
       tinyrainbow: 2.0.0
 
-  '@vitest/mocker@3.0.9(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2))':
+  '@vitest/mocker@3.0.9(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3))':
     dependencies:
       '@vitest/spy': 3.0.9
       estree-walker: 3.0.3
       magic-string: 0.30.17
     optionalDependencies:
-      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)
+      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3)
 
   '@vitest/pretty-format@3.0.9':
     dependencies:
@@ -2648,6 +4773,26 @@ snapshots:
       loupe: 3.1.3
       tinyrainbow: 2.0.0
 
+  abstract-leveldown@6.2.3:
+    dependencies:
+      buffer: 5.7.1
+      immediate: 3.3.0
+      level-concat-iterator: 2.0.1
+      level-supports: 1.0.1
+      xtend: 4.0.2
+    optional: true
+
+  abstract-leveldown@6.3.0:
+    dependencies:
+      buffer: 5.7.1
+      immediate: 3.3.0
+      level-concat-iterator: 2.0.1
+      level-supports: 1.0.1
+      xtend: 4.0.2
+    optional: true
+
+  acorn@8.14.1: {}
+
   agent-base@7.1.3: {}
 
   ansi-regex@5.0.1: {}
@@ -2662,6 +4807,13 @@ snapshots:
 
   ansi-styles@6.2.1: {}
 
+  ansis@3.17.0: {}
+
+  anymatch@3.1.3:
+    dependencies:
+      normalize-path: 3.0.0
+      picomatch: 2.3.1
+
   aria-hidden@1.2.4:
     dependencies:
       tslib: 2.8.1
@@ -2672,14 +4824,37 @@ snapshots:
 
   assertion-error@2.0.1: {}
 
+  async-limiter@1.0.1:
+    optional: true
+
   asynckit@0.4.0: {}
 
+  babel-dead-code-elimination@1.0.9:
+    dependencies:
+      '@babel/core': 7.26.10
+      '@babel/parser': 7.26.10
+      '@babel/traverse': 7.26.10
+      '@babel/types': 7.26.10
+    transitivePeerDependencies:
+      - supports-color
+
   balanced-match@1.0.2: {}
 
+  base64-js@1.5.1:
+    optional: true
+
+  binary-extensions@2.3.0: {}
+
   brace-expansion@2.0.1:
     dependencies:
       balanced-match: 1.0.2
 
+  braces@3.0.3:
+    dependencies:
+      fill-range: 7.1.1
+
+  browser-fs-access@0.29.1: {}
+
   browserslist@4.24.4:
     dependencies:
       caniuse-lite: 1.0.30001706
@@ -2687,6 +4862,12 @@ snapshots:
       node-releases: 2.0.19
       update-browserslist-db: 1.1.3(browserslist@4.24.4)
 
+  buffer@5.7.1:
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+    optional: true
+
   cac@6.7.14: {}
 
   call-bind-apply-helpers@1.0.2:
@@ -2696,6 +4877,8 @@ snapshots:
 
   caniuse-lite@1.0.30001706: {}
 
+  canvas-roundrect-polyfill@0.0.1: {}
+
   chai@5.2.0:
     dependencies:
       assertion-error: 2.0.1
@@ -2709,14 +4892,40 @@ snapshots:
       ansi-styles: 4.3.0
       supports-color: 7.2.0
 
+  character-entities@2.0.2: {}
+
   check-error@2.1.1: {}
 
+  chokidar@3.6.0:
+    dependencies:
+      anymatch: 3.1.3
+      braces: 3.0.3
+      glob-parent: 5.1.2
+      is-binary-path: 2.1.0
+      is-glob: 4.0.3
+      normalize-path: 3.0.0
+      readdirp: 3.6.0
+    optionalDependencies:
+      fsevents: 2.3.3
+
   class-variance-authority@0.7.1:
     dependencies:
       clsx: 2.1.1
 
+  clsx@1.1.1: {}
+
   clsx@2.1.1: {}
 
+  cmdk@1.0.0(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+    dependencies:
+      '@radix-ui/react-dialog': 1.0.5(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@radix-ui/react-primitive': 1.0.3(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+    transitivePeerDependencies:
+      - '@types/react'
+      - '@types/react-dom'
+
   color-convert@2.0.1:
     dependencies:
       color-name: 1.1.4
@@ -2727,10 +4936,24 @@ snapshots:
     dependencies:
       delayed-stream: 1.0.0
 
+  commander@7.2.0: {}
+
+  commander@8.3.0: {}
+
   common-tags@1.8.2: {}
 
   convert-source-map@2.0.0: {}
 
+  cose-base@1.0.3:
+    dependencies:
+      layout-base: 1.0.2
+
+  crc-32@0.3.0: {}
+
+  cross-env@7.0.3:
+    dependencies:
+      cross-spawn: 7.0.6
+
   cross-spawn@7.0.6:
     dependencies:
       path-key: 3.1.1
@@ -2744,19 +4967,214 @@ snapshots:
 
   csstype@3.1.3: {}
 
+  cytoscape-cose-bilkent@4.1.0(cytoscape@3.31.1):
+    dependencies:
+      cose-base: 1.0.3
+      cytoscape: 3.31.1
+
+  cytoscape@3.31.1: {}
+
+  d3-array@2.12.1:
+    dependencies:
+      internmap: 1.0.1
+
+  d3-array@3.2.4:
+    dependencies:
+      internmap: 2.0.3
+
+  d3-axis@3.0.0: {}
+
+  d3-brush@3.0.0:
+    dependencies:
+      d3-dispatch: 3.0.1
+      d3-drag: 3.0.0
+      d3-interpolate: 3.0.1
+      d3-selection: 3.0.0
+      d3-transition: 3.0.1(d3-selection@3.0.0)
+
+  d3-chord@3.0.1:
+    dependencies:
+      d3-path: 3.1.0
+
+  d3-color@3.1.0: {}
+
+  d3-contour@4.0.2:
+    dependencies:
+      d3-array: 3.2.4
+
+  d3-delaunay@6.0.4:
+    dependencies:
+      delaunator: 5.0.1
+
+  d3-dispatch@3.0.1: {}
+
+  d3-drag@3.0.0:
+    dependencies:
+      d3-dispatch: 3.0.1
+      d3-selection: 3.0.0
+
+  d3-dsv@3.0.1:
+    dependencies:
+      commander: 7.2.0
+      iconv-lite: 0.6.3
+      rw: 1.3.3
+
+  d3-ease@3.0.1: {}
+
+  d3-fetch@3.0.1:
+    dependencies:
+      d3-dsv: 3.0.1
+
+  d3-force@3.0.0:
+    dependencies:
+      d3-dispatch: 3.0.1
+      d3-quadtree: 3.0.1
+      d3-timer: 3.0.1
+
+  d3-format@3.1.0: {}
+
+  d3-geo@3.1.1:
+    dependencies:
+      d3-array: 3.2.4
+
+  d3-hierarchy@3.1.2: {}
+
+  d3-interpolate@3.0.1:
+    dependencies:
+      d3-color: 3.1.0
+
+  d3-path@1.0.9: {}
+
+  d3-path@3.1.0: {}
+
+  d3-polygon@3.0.1: {}
+
+  d3-quadtree@3.0.1: {}
+
+  d3-random@3.0.1: {}
+
+  d3-sankey@0.12.3:
+    dependencies:
+      d3-array: 2.12.1
+      d3-shape: 1.3.7
+
+  d3-scale-chromatic@3.1.0:
+    dependencies:
+      d3-color: 3.1.0
+      d3-interpolate: 3.0.1
+
+  d3-scale@4.0.2:
+    dependencies:
+      d3-array: 3.2.4
+      d3-format: 3.1.0
+      d3-interpolate: 3.0.1
+      d3-time: 3.1.0
+      d3-time-format: 4.1.0
+
+  d3-selection@3.0.0: {}
+
+  d3-shape@1.3.7:
+    dependencies:
+      d3-path: 1.0.9
+
+  d3-shape@3.2.0:
+    dependencies:
+      d3-path: 3.1.0
+
+  d3-time-format@4.1.0:
+    dependencies:
+      d3-time: 3.1.0
+
+  d3-time@3.1.0:
+    dependencies:
+      d3-array: 3.2.4
+
+  d3-timer@3.0.1: {}
+
+  d3-transition@3.0.1(d3-selection@3.0.0):
+    dependencies:
+      d3-color: 3.1.0
+      d3-dispatch: 3.0.1
+      d3-ease: 3.0.1
+      d3-interpolate: 3.0.1
+      d3-selection: 3.0.0
+      d3-timer: 3.0.1
+
+  d3-zoom@3.0.0:
+    dependencies:
+      d3-dispatch: 3.0.1
+      d3-drag: 3.0.0
+      d3-interpolate: 3.0.1
+      d3-selection: 3.0.0
+      d3-transition: 3.0.1(d3-selection@3.0.0)
+
+  d3@7.9.0:
+    dependencies:
+      d3-array: 3.2.4
+      d3-axis: 3.0.0
+      d3-brush: 3.0.0
+      d3-chord: 3.0.1
+      d3-color: 3.1.0
+      d3-contour: 4.0.2
+      d3-delaunay: 6.0.4
+      d3-dispatch: 3.0.1
+      d3-drag: 3.0.0
+      d3-dsv: 3.0.1
+      d3-ease: 3.0.1
+      d3-fetch: 3.0.1
+      d3-force: 3.0.0
+      d3-format: 3.1.0
+      d3-geo: 3.1.1
+      d3-hierarchy: 3.1.2
+      d3-interpolate: 3.0.1
+      d3-path: 3.1.0
+      d3-polygon: 3.0.1
+      d3-quadtree: 3.0.1
+      d3-random: 3.0.1
+      d3-scale: 4.0.2
+      d3-scale-chromatic: 3.1.0
+      d3-selection: 3.0.0
+      d3-shape: 3.2.0
+      d3-time: 3.1.0
+      d3-time-format: 4.1.0
+      d3-timer: 3.0.1
+      d3-transition: 3.0.1(d3-selection@3.0.0)
+      d3-zoom: 3.0.0
+
+  dagre-d3-es@7.0.10:
+    dependencies:
+      d3: 7.9.0
+      lodash-es: 4.17.21
+
   data-urls@5.0.0:
     dependencies:
       whatwg-mimetype: 4.0.0
       whatwg-url: 14.2.0
 
+  dayjs@1.11.13: {}
+
   debug@4.4.0:
     dependencies:
       ms: 2.1.3
 
   decimal.js@10.5.0: {}
 
+  decode-named-character-reference@1.1.0:
+    dependencies:
+      character-entities: 2.0.2
+
   deep-eql@5.0.2: {}
 
+  deferred-leveldown@5.3.0:
+    dependencies:
+      abstract-leveldown: 6.2.3
+      inherits: 2.0.4
+    optional: true
+
+  delaunator@5.0.1:
+    dependencies:
+      robust-predicates: 3.0.2
+
   delayed-stream@1.0.0: {}
 
   dequal@2.0.3: {}
@@ -2765,8 +5183,14 @@ snapshots:
 
   detect-node-es@1.1.0: {}
 
+  diff@5.2.0: {}
+
+  diff@7.0.0: {}
+
   dom-accessibility-api@0.5.16: {}
 
+  dompurify@3.1.6: {}
+
   dunder-proto@1.0.1:
     dependencies:
       call-bind-apply-helpers: 1.0.2
@@ -2777,10 +5201,20 @@ snapshots:
 
   electron-to-chromium@1.5.120: {}
 
+  elkjs@0.9.3: {}
+
   emoji-regex@8.0.0: {}
 
   emoji-regex@9.2.2: {}
 
+  encoding-down@6.3.0:
+    dependencies:
+      abstract-leveldown: 6.3.0
+      inherits: 2.0.4
+      level-codec: 9.0.2
+      level-errors: 2.0.1
+    optional: true
+
   enhanced-resolve@5.18.1:
     dependencies:
       graceful-fs: 4.2.11
@@ -2788,6 +5222,11 @@ snapshots:
 
   entities@4.5.0: {}
 
+  errno@0.1.8:
+    dependencies:
+      prr: 1.0.1
+    optional: true
+
   es-define-property@1.0.1: {}
 
   es-errors@1.3.0: {}
@@ -2805,6 +5244,8 @@ snapshots:
       has-tostringtag: 1.0.2
       hasown: 2.0.2
 
+  es6-promise-pool@2.5.0: {}
+
   esbuild@0.25.1:
     optionalDependencies:
       '@esbuild/aix-ppc64': 0.25.1
@@ -2841,6 +5282,10 @@ snapshots:
 
   expect-type@1.2.0: {}
 
+  fill-range@7.1.1:
+    dependencies:
+      to-regex-range: 5.0.1
+
   foreground-child@3.3.1:
     dependencies:
       cross-spawn: 7.0.6
@@ -2853,11 +5298,15 @@ snapshots:
       es-set-tostringtag: 2.1.0
       mime-types: 2.1.35
 
+  fractional-indexing@3.2.0: {}
+
   fsevents@2.3.3:
     optional: true
 
   function-bind@1.1.2: {}
 
+  fuzzy@0.1.3: {}
+
   gensync@1.0.0-beta.2: {}
 
   get-intrinsic@1.3.0:
@@ -2880,6 +5329,14 @@ snapshots:
       dunder-proto: 1.0.1
       es-object-atoms: 1.1.1
 
+  get-tsconfig@4.10.0:
+    dependencies:
+      resolve-pkg-maps: 1.0.0
+
+  glob-parent@5.1.2:
+    dependencies:
+      is-glob: 4.0.3
+
   glob@10.4.5:
     dependencies:
       foreground-child: 3.3.1
@@ -2891,6 +5348,8 @@ snapshots:
 
   globals@11.12.0: {}
 
+  glur@1.1.2: {}
+
   goober@2.1.16(csstype@3.1.3):
     dependencies:
       csstype: 3.1.3
@@ -2899,6 +5358,8 @@ snapshots:
 
   graceful-fs@4.2.11: {}
 
+  hachure-fill@0.5.2: {}
+
   has-flag@4.0.0: {}
 
   has-symbols@1.1.0: {}
@@ -2935,12 +5396,44 @@ snapshots:
 
   idb@8.0.2: {}
 
+  ieee754@1.2.1:
+    optional: true
+
+  image-blob-reduce@3.0.1:
+    dependencies:
+      pica: 7.1.1
+
+  immediate@3.3.0:
+    optional: true
+
+  immutable@4.3.7: {}
+
+  inherits@2.0.4: {}
+
+  internmap@1.0.1: {}
+
+  internmap@2.0.3: {}
+
+  is-binary-path@2.1.0:
+    dependencies:
+      binary-extensions: 2.3.0
+
+  is-extglob@2.1.1: {}
+
   is-fullwidth-code-point@3.0.0: {}
 
+  is-glob@4.0.3:
+    dependencies:
+      is-extglob: 2.1.1
+
+  is-number@7.0.0: {}
+
   is-potential-custom-element-name@1.0.1: {}
 
   isexe@2.0.0: {}
 
+  isomorphic.js@0.2.5: {}
+
   jackspeak@3.4.3:
     dependencies:
       '@isaacs/cliui': 8.0.2
@@ -2949,6 +5442,16 @@ snapshots:
 
   jiti@2.4.2: {}
 
+  jotai-scope@0.7.2(jotai@2.11.0(@types/react@19.0.11)(react@19.0.0))(react@19.0.0):
+    dependencies:
+      jotai: 2.11.0(@types/react@19.0.11)(react@19.0.0)
+      react: 19.0.0
+
+  jotai@2.11.0(@types/react@19.0.11)(react@19.0.0):
+    optionalDependencies:
+      '@types/react': 19.0.11
+      react: 19.0.0
+
   js-tokens@4.0.0: {}
 
   jsdom@26.0.0:
@@ -2983,8 +5486,86 @@ snapshots:
 
   json5@2.2.3: {}
 
+  katex@0.16.21:
+    dependencies:
+      commander: 8.3.0
+
+  khroma@2.1.0: {}
+
+  kleur@4.1.5: {}
+
   kolorist@1.8.0: {}
 
+  layout-base@1.0.2: {}
+
+  level-codec@9.0.2:
+    dependencies:
+      buffer: 5.7.1
+    optional: true
+
+  level-concat-iterator@2.0.1:
+    optional: true
+
+  level-errors@2.0.1:
+    dependencies:
+      errno: 0.1.8
+    optional: true
+
+  level-iterator-stream@4.0.2:
+    dependencies:
+      inherits: 2.0.4
+      readable-stream: 3.6.2
+      xtend: 4.0.2
+    optional: true
+
+  level-js@5.0.2:
+    dependencies:
+      abstract-leveldown: 6.2.3
+      buffer: 5.7.1
+      inherits: 2.0.4
+      ltgt: 2.2.1
+    optional: true
+
+  level-packager@5.1.1:
+    dependencies:
+      encoding-down: 6.3.0
+      levelup: 4.4.0
+    optional: true
+
+  level-supports@1.0.1:
+    dependencies:
+      xtend: 4.0.2
+    optional: true
+
+  level@6.0.1:
+    dependencies:
+      level-js: 5.0.2
+      level-packager: 5.1.1
+      leveldown: 5.6.0
+    optional: true
+
+  leveldown@5.6.0:
+    dependencies:
+      abstract-leveldown: 6.2.3
+      napi-macros: 2.0.0
+      node-gyp-build: 4.1.1
+    optional: true
+
+  levelup@4.4.0:
+    dependencies:
+      deferred-leveldown: 5.3.0
+      level-errors: 2.0.1
+      level-iterator-stream: 4.0.2
+      level-supports: 1.0.1
+      xtend: 4.0.2
+    optional: true
+
+  lexical@0.28.0: {}
+
+  lib0@0.2.99:
+    dependencies:
+      isomorphic.js: 0.2.5
+
   lightningcss-darwin-arm64@1.29.2:
     optional: true
 
@@ -3030,8 +5611,18 @@ snapshots:
       lightningcss-win32-arm64-msvc: 1.29.2
       lightningcss-win32-x64-msvc: 1.29.2
 
+  lodash-es@4.17.21: {}
+
+  lodash.debounce@4.0.8: {}
+
   lodash.sortby@4.7.0: {}
 
+  lodash.throttle@4.1.1: {}
+
+  loose-envify@1.4.0:
+    dependencies:
+      js-tokens: 4.0.0
+
   loupe@3.1.3: {}
 
   lru-cache@10.4.3: {}
@@ -3040,6 +5631,9 @@ snapshots:
     dependencies:
       yallist: 3.1.1
 
+  ltgt@2.2.1:
+    optional: true
+
   lucide-react@0.483.0(react@19.0.0):
     dependencies:
       react: 19.0.0
@@ -3052,6 +5646,185 @@ snapshots:
 
   math-intrinsics@1.1.0: {}
 
+  mdast-util-from-markdown@1.3.1:
+    dependencies:
+      '@types/mdast': 3.0.15
+      '@types/unist': 2.0.11
+      decode-named-character-reference: 1.1.0
+      mdast-util-to-string: 3.2.0
+      micromark: 3.2.0
+      micromark-util-decode-numeric-character-reference: 1.1.0
+      micromark-util-decode-string: 1.1.0
+      micromark-util-normalize-identifier: 1.1.0
+      micromark-util-symbol: 1.1.0
+      micromark-util-types: 1.1.0
+      unist-util-stringify-position: 3.0.3
+      uvu: 0.5.6
+    transitivePeerDependencies:
+      - supports-color
+
+  mdast-util-to-string@3.2.0:
+    dependencies:
+      '@types/mdast': 3.0.15
+
+  mermaid@10.9.3:
+    dependencies:
+      '@braintree/sanitize-url': 6.0.2
+      '@types/d3-scale': 4.0.9
+      '@types/d3-scale-chromatic': 3.1.0
+      cytoscape: 3.31.1
+      cytoscape-cose-bilkent: 4.1.0(cytoscape@3.31.1)
+      d3: 7.9.0
+      d3-sankey: 0.12.3
+      dagre-d3-es: 7.0.10
+      dayjs: 1.11.13
+      dompurify: 3.1.6
+      elkjs: 0.9.3
+      katex: 0.16.21
+      khroma: 2.1.0
+      lodash-es: 4.17.21
+      mdast-util-from-markdown: 1.3.1
+      non-layered-tidy-tree-layout: 2.0.2
+      stylis: 4.3.6
+      ts-dedent: 2.2.0
+      uuid: 9.0.1
+      web-worker: 1.5.0
+    transitivePeerDependencies:
+      - supports-color
+
+  micromark-core-commonmark@1.1.0:
+    dependencies:
+      decode-named-character-reference: 1.1.0
+      micromark-factory-destination: 1.1.0
+      micromark-factory-label: 1.1.0
+      micromark-factory-space: 1.1.0
+      micromark-factory-title: 1.1.0
+      micromark-factory-whitespace: 1.1.0
+      micromark-util-character: 1.2.0
+      micromark-util-chunked: 1.1.0
+      micromark-util-classify-character: 1.1.0
+      micromark-util-html-tag-name: 1.2.0
+      micromark-util-normalize-identifier: 1.1.0
+      micromark-util-resolve-all: 1.1.0
+      micromark-util-subtokenize: 1.1.0
+      micromark-util-symbol: 1.1.0
+      micromark-util-types: 1.1.0
+      uvu: 0.5.6
+
+  micromark-factory-destination@1.1.0:
+    dependencies:
+      micromark-util-character: 1.2.0
+      micromark-util-symbol: 1.1.0
+      micromark-util-types: 1.1.0
+
+  micromark-factory-label@1.1.0:
+    dependencies:
+      micromark-util-character: 1.2.0
+      micromark-util-symbol: 1.1.0
+      micromark-util-types: 1.1.0
+      uvu: 0.5.6
+
+  micromark-factory-space@1.1.0:
+    dependencies:
+      micromark-util-character: 1.2.0
+      micromark-util-types: 1.1.0
+
+  micromark-factory-title@1.1.0:
+    dependencies:
+      micromark-factory-space: 1.1.0
+      micromark-util-character: 1.2.0
+      micromark-util-symbol: 1.1.0
+      micromark-util-types: 1.1.0
+
+  micromark-factory-whitespace@1.1.0:
+    dependencies:
+      micromark-factory-space: 1.1.0
+      micromark-util-character: 1.2.0
+      micromark-util-symbol: 1.1.0
+      micromark-util-types: 1.1.0
+
+  micromark-util-character@1.2.0:
+    dependencies:
+      micromark-util-symbol: 1.1.0
+      micromark-util-types: 1.1.0
+
+  micromark-util-chunked@1.1.0:
+    dependencies:
+      micromark-util-symbol: 1.1.0
+
+  micromark-util-classify-character@1.1.0:
+    dependencies:
+      micromark-util-character: 1.2.0
+      micromark-util-symbol: 1.1.0
+      micromark-util-types: 1.1.0
+
+  micromark-util-combine-extensions@1.1.0:
+    dependencies:
+      micromark-util-chunked: 1.1.0
+      micromark-util-types: 1.1.0
+
+  micromark-util-decode-numeric-character-reference@1.1.0:
+    dependencies:
+      micromark-util-symbol: 1.1.0
+
+  micromark-util-decode-string@1.1.0:
+    dependencies:
+      decode-named-character-reference: 1.1.0
+      micromark-util-character: 1.2.0
+      micromark-util-decode-numeric-character-reference: 1.1.0
+      micromark-util-symbol: 1.1.0
+
+  micromark-util-encode@1.1.0: {}
+
+  micromark-util-html-tag-name@1.2.0: {}
+
+  micromark-util-normalize-identifier@1.1.0:
+    dependencies:
+      micromark-util-symbol: 1.1.0
+
+  micromark-util-resolve-all@1.1.0:
+    dependencies:
+      micromark-util-types: 1.1.0
+
+  micromark-util-sanitize-uri@1.2.0:
+    dependencies:
+      micromark-util-character: 1.2.0
+      micromark-util-encode: 1.1.0
+      micromark-util-symbol: 1.1.0
+
+  micromark-util-subtokenize@1.1.0:
+    dependencies:
+      micromark-util-chunked: 1.1.0
+      micromark-util-symbol: 1.1.0
+      micromark-util-types: 1.1.0
+      uvu: 0.5.6
+
+  micromark-util-symbol@1.1.0: {}
+
+  micromark-util-types@1.1.0: {}
+
+  micromark@3.2.0:
+    dependencies:
+      '@types/debug': 4.1.12
+      debug: 4.4.0
+      decode-named-character-reference: 1.1.0
+      micromark-core-commonmark: 1.1.0
+      micromark-factory-space: 1.1.0
+      micromark-util-character: 1.2.0
+      micromark-util-chunked: 1.1.0
+      micromark-util-combine-extensions: 1.1.0
+      micromark-util-decode-numeric-character-reference: 1.1.0
+      micromark-util-encode: 1.1.0
+      micromark-util-normalize-identifier: 1.1.0
+      micromark-util-resolve-all: 1.1.0
+      micromark-util-sanitize-uri: 1.2.0
+      micromark-util-subtokenize: 1.1.0
+      micromark-util-symbol: 1.1.0
+      micromark-util-types: 1.1.0
+      uvu: 0.5.6
+    transitivePeerDependencies:
+      - supports-color
+
   mime-db@1.52.0: {}
 
   mime-types@2.1.35:
@@ -3064,20 +5837,49 @@ snapshots:
 
   minipass@7.1.2: {}
 
+  mri@1.2.0: {}
+
   ms@2.1.3: {}
 
+  multimath@2.0.0:
+    dependencies:
+      glur: 1.1.2
+      object-assign: 4.1.1
+
   nanoid@3.3.11: {}
 
+  nanoid@3.3.3: {}
+
+  nanoid@4.0.2: {}
+
+  napi-macros@2.0.0:
+    optional: true
+
+  node-gyp-build@4.1.1:
+    optional: true
+
   node-releases@2.0.19: {}
 
+  non-layered-tidy-tree-layout@2.0.2: {}
+
+  normalize-path@3.0.0: {}
+
   nwsapi@2.2.19: {}
 
+  object-assign@4.1.1: {}
+
+  open-color@1.9.1: {}
+
   package-json-from-dist@1.0.1: {}
 
+  pako@2.0.3: {}
+
   parse5@7.2.1:
     dependencies:
       entities: 4.5.0
 
+  path-data-parser@0.1.0: {}
+
   path-key@3.1.1: {}
 
   path-scurry@1.11.1:
@@ -3089,14 +5891,48 @@ snapshots:
 
   pathval@2.0.0: {}
 
+  perfect-freehand@1.2.0: {}
+
+  pica@7.1.1:
+    dependencies:
+      glur: 1.1.2
+      inherits: 2.0.4
+      multimath: 2.0.0
+      object-assign: 4.1.1
+      webworkify: 1.5.0
+
   picocolors@1.1.1: {}
 
+  picomatch@2.3.1: {}
+
+  png-chunk-text@1.0.0: {}
+
+  png-chunks-encode@1.0.0:
+    dependencies:
+      crc-32: 0.3.0
+      sliced: 1.0.1
+
+  png-chunks-extract@1.0.0:
+    dependencies:
+      crc-32: 0.3.0
+
+  points-on-curve@0.2.0: {}
+
+  points-on-curve@1.0.1: {}
+
+  points-on-path@0.2.1:
+    dependencies:
+      path-data-parser: 0.1.0
+      points-on-curve: 0.2.0
+
   postcss@8.5.3:
     dependencies:
       nanoid: 3.3.11
       picocolors: 1.1.1
       source-map-js: 1.2.1
 
+  prettier@3.5.3: {}
+
   pretty-bytes@6.1.1: {}
 
   pretty-format@27.5.1:
@@ -3105,13 +5941,33 @@ snapshots:
       ansi-styles: 5.2.0
       react-is: 17.0.2
 
+  prismjs@1.30.0: {}
+
+  prop-types@15.8.1:
+    dependencies:
+      loose-envify: 1.4.0
+      object-assign: 4.1.1
+      react-is: 16.13.1
+
+  prr@1.0.1:
+    optional: true
+
   punycode@2.3.1: {}
 
+  pwacompat@2.0.17: {}
+
   react-dom@19.0.0(react@19.0.0):
     dependencies:
       react: 19.0.0
       scheduler: 0.25.0
 
+  react-error-boundary@3.1.4(react@19.0.0):
+    dependencies:
+      '@babel/runtime': 7.26.10
+      react: 19.0.0
+
+  react-is@16.13.1: {}
+
   react-is@17.0.2: {}
 
   react-refresh@0.14.2: {}
@@ -3124,6 +5980,17 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.0.11
 
+  react-remove-scroll@2.5.5(@types/react@19.0.11)(react@19.0.0):
+    dependencies:
+      react: 19.0.0
+      react-remove-scroll-bar: 2.3.8(@types/react@19.0.11)(react@19.0.0)
+      react-style-singleton: 2.2.3(@types/react@19.0.11)(react@19.0.0)
+      tslib: 2.8.1
+      use-callback-ref: 1.3.3(@types/react@19.0.11)(react@19.0.0)
+      use-sidecar: 1.1.3(@types/react@19.0.11)(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+
   react-remove-scroll@2.6.3(@types/react@19.0.11)(react@19.0.0):
     dependencies:
       react: 19.0.0
@@ -3145,8 +6012,23 @@ snapshots:
 
   react@19.0.0: {}
 
+  readable-stream@3.6.2:
+    dependencies:
+      inherits: 2.0.4
+      string_decoder: 1.3.0
+      util-deprecate: 1.0.2
+    optional: true
+
+  readdirp@3.6.0:
+    dependencies:
+      picomatch: 2.3.1
+
   regenerator-runtime@0.14.1: {}
 
+  resolve-pkg-maps@1.0.0: {}
+
+  robust-predicates@3.0.2: {}
+
   rollup@4.36.0:
     dependencies:
       '@types/estree': 1.0.6
@@ -3172,10 +6054,32 @@ snapshots:
       '@rollup/rollup-win32-x64-msvc': 4.36.0
       fsevents: 2.3.3
 
+  roughjs@4.6.4:
+    dependencies:
+      hachure-fill: 0.5.2
+      path-data-parser: 0.1.0
+      points-on-curve: 0.2.0
+      points-on-path: 0.2.1
+
   rrweb-cssom@0.8.0: {}
 
+  rw@1.3.3: {}
+
+  sade@1.8.1:
+    dependencies:
+      mri: 1.2.0
+
+  safe-buffer@5.2.1:
+    optional: true
+
   safer-buffer@2.1.2: {}
 
+  sass@1.51.0:
+    dependencies:
+      chokidar: 3.6.0
+      immutable: 4.3.7
+      source-map-js: 1.2.1
+
   saxes@6.0.0:
     dependencies:
       xmlchars: 2.2.0
@@ -3206,6 +6110,8 @@ snapshots:
 
   signal-exit@4.1.0: {}
 
+  sliced@1.0.1: {}
+
   solid-js@1.9.5:
     dependencies:
       csstype: 3.1.3
@@ -3234,6 +6140,11 @@ snapshots:
       emoji-regex: 9.2.2
       strip-ansi: 7.1.0
 
+  string_decoder@1.3.0:
+    dependencies:
+      safe-buffer: 5.2.1
+    optional: true
+
   strip-ansi@6.0.1:
     dependencies:
       ansi-regex: 5.0.1
@@ -3242,6 +6153,8 @@ snapshots:
     dependencies:
       ansi-regex: 6.1.0
 
+  stylis@4.3.6: {}
+
   supports-color@7.2.0:
     dependencies:
       has-flag: 4.0.0
@@ -3274,6 +6187,10 @@ snapshots:
     dependencies:
       tldts-core: 6.1.84
 
+  to-regex-range@5.0.1:
+    dependencies:
+      is-number: 7.0.0
+
   tough-cookie@5.1.2:
     dependencies:
       tldts: 6.1.84
@@ -3286,12 +6203,38 @@ snapshots:
     dependencies:
       punycode: 2.3.1
 
+  ts-dedent@2.2.0: {}
+
   tslib@2.8.1: {}
 
+  tsx@4.19.3:
+    dependencies:
+      esbuild: 0.25.1
+      get-tsconfig: 4.10.0
+    optionalDependencies:
+      fsevents: 2.3.3
+
+  tunnel-rat@0.1.2(@types/react@19.0.11)(react@19.0.0):
+    dependencies:
+      zustand: 4.5.6(@types/react@19.0.11)(react@19.0.0)
+    transitivePeerDependencies:
+      - '@types/react'
+      - immer
+      - react
+
   tw-animate-css@1.2.4: {}
 
   typescript@5.8.2: {}
 
+  unist-util-stringify-position@3.0.3:
+    dependencies:
+      '@types/unist': 2.0.11
+
+  unplugin@2.2.0:
+    dependencies:
+      acorn: 8.14.1
+      webpack-virtual-modules: 0.6.2
+
   update-browserslist-db@1.1.3(browserslist@4.24.4):
     dependencies:
       browserslist: 4.24.4
@@ -3317,13 +6260,25 @@ snapshots:
     dependencies:
       react: 19.0.0
 
-  vite-node@3.0.9(jiti@2.4.2)(lightningcss@1.29.2):
+  util-deprecate@1.0.2:
+    optional: true
+
+  uuid@9.0.1: {}
+
+  uvu@0.5.6:
+    dependencies:
+      dequal: 2.0.3
+      diff: 5.2.0
+      kleur: 4.1.5
+      sade: 1.8.1
+
+  vite-node@3.0.9(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3):
     dependencies:
       cac: 6.7.14
       debug: 4.4.0
       es-module-lexer: 1.6.0
       pathe: 2.0.3
-      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)
+      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3)
     transitivePeerDependencies:
       - '@types/node'
       - jiti
@@ -3338,7 +6293,7 @@ snapshots:
       - tsx
       - yaml
 
-  vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2):
+  vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3):
     dependencies:
       esbuild: 0.25.1
       postcss: 8.5.3
@@ -3347,11 +6302,13 @@ snapshots:
       fsevents: 2.3.3
       jiti: 2.4.2
       lightningcss: 1.29.2
+      sass: 1.51.0
+      tsx: 4.19.3
 
-  vitest@3.0.9(jiti@2.4.2)(jsdom@26.0.0)(lightningcss@1.29.2):
+  vitest@3.0.9(@types/debug@4.1.12)(jiti@2.4.2)(jsdom@26.0.0)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3):
     dependencies:
       '@vitest/expect': 3.0.9
-      '@vitest/mocker': 3.0.9(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2))
+      '@vitest/mocker': 3.0.9(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3))
       '@vitest/pretty-format': 3.0.9
       '@vitest/runner': 3.0.9
       '@vitest/snapshot': 3.0.9
@@ -3367,10 +6324,11 @@ snapshots:
       tinyexec: 0.3.2
       tinypool: 1.0.2
       tinyrainbow: 2.0.0
-      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)
-      vite-node: 3.0.9(jiti@2.4.2)(lightningcss@1.29.2)
+      vite: 6.2.2(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3)
+      vite-node: 3.0.9(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.51.0)(tsx@4.19.3)
       why-is-node-running: 2.3.0
     optionalDependencies:
+      '@types/debug': 4.1.12
       jsdom: 26.0.0
     transitivePeerDependencies:
       - jiti
@@ -3392,10 +6350,16 @@ snapshots:
 
   web-vitals@4.2.4: {}
 
+  web-worker@1.5.0: {}
+
   webidl-conversions@4.0.2: {}
 
   webidl-conversions@7.0.0: {}
 
+  webpack-virtual-modules@0.6.2: {}
+
+  webworkify@1.5.0: {}
+
   whatwg-encoding@3.1.1:
     dependencies:
       iconv-lite: 0.6.3
@@ -3434,12 +6398,67 @@ snapshots:
       string-width: 5.1.2
       strip-ansi: 7.1.0
 
+  ws@6.2.3:
+    dependencies:
+      async-limiter: 1.0.1
+    optional: true
+
   ws@8.18.1: {}
 
   xml-name-validator@5.0.0: {}
 
   xmlchars@2.2.0: {}
 
+  xtend@4.0.2:
+    optional: true
+
+  y-excalidraw@2.0.12(@excalidraw/excalidraw@0.18.0(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(yjs@13.6.24):
+    dependencies:
+      '@excalidraw/excalidraw': 0.18.0(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      fractional-indexing: 3.2.0
+      yjs: 13.6.24
+
+  y-indexeddb@9.0.12(yjs@13.6.24):
+    dependencies:
+      lib0: 0.2.99
+      yjs: 13.6.24
+
+  y-leveldb@0.1.2(yjs@13.6.24):
+    dependencies:
+      level: 6.0.1
+      lib0: 0.2.99
+      yjs: 13.6.24
+    optional: true
+
+  y-protocols@1.0.6(yjs@13.6.24):
+    dependencies:
+      lib0: 0.2.99
+      yjs: 13.6.24
+
+  y-websocket@2.1.0(yjs@13.6.24):
+    dependencies:
+      lib0: 0.2.99
+      lodash.debounce: 4.0.8
+      y-protocols: 1.0.6(yjs@13.6.24)
+      yjs: 13.6.24
+    optionalDependencies:
+      ws: 6.2.3
+      y-leveldb: 0.1.2(yjs@13.6.24)
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
   yallist@3.1.1: {}
 
+  yjs@13.6.24:
+    dependencies:
+      lib0: 0.2.99
+
   zod@3.24.2: {}
+
+  zustand@4.5.6(@types/react@19.0.11)(react@19.0.0):
+    dependencies:
+      use-sync-external-store: 1.4.0(react@19.0.0)
+    optionalDependencies:
+      '@types/react': 19.0.11
+      react: 19.0.0
diff --git a/src/components/app_sidebar.tsx b/src/components/app_sidebar.tsx
new file mode 100644
index 0000000..bdb84a7
--- /dev/null
+++ b/src/components/app_sidebar.tsx
@@ -0,0 +1,253 @@
+import Icon from "@mdi/react";
+import { BookPlus, Calendar, ChevronsUpDown, Home, Inbox, ListTodo, LogOut, Monitor, Moon, Palette, PanelLeft, Plus, Search, Sun, Waypoints } from "lucide-react";
+import { Button } from "~/components/ui/button";
+import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, useSidebar } from "~/components/ui/sidebar";
+import { type ColorName, colors } from "~/lib/color";
+import { type IconName, icons } from "~/lib/icon";
+import { NewNoteDialog } from "~/components/note/new_note_dialog";
+import { NewCollectionDialog } from "~/components/collection/new_collection_dialog";
+import { cn } from "~/lib/utils";
+import { useCollectionNotesMetadata, useCollections } from "~/hooks/use-metadata";
+import { useIsMobile } from "~/hooks/use-mobile";
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "~/components/ui/dropdown-menu";
+import { UserAvatar } from "~/components/user/user_avatar";
+import { Link } from "@tanstack/react-router";
+import { useTheme } from "~/hooks/use-theme";
+import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
+
+export function AppSidebar() {
+    const collections = useCollections();
+
+    return (
+        <Sidebar variant="inset" collapsible="icon">
+            <SidebarContent>
+                <HeaderButtons>
+                    <NewNoteDialog>
+                        <Button variant="ghost"><Plus /></Button>
+                    </NewNoteDialog>
+                    <NewCollectionDialog>
+                        <Button variant="ghost"><BookPlus /></Button>
+                    </NewCollectionDialog>
+                </HeaderButtons>
+                <SidebarGroup>
+                    <SidebarMenu>
+                        <NavButton href="/app/" label="Overview" icon={<Home />} />
+                        <NavButton href="/app/search" label="Search" icon={<Search />} />
+                        <InboxButton />
+                        <NavButton href="/app/graph" label="Graph" icon={<Waypoints />} />
+                        <NavButton href="/app/calendar" label="Calendar" icon={<Calendar />} />
+                        <NavButton href="/app/todo" label="Todo" icon={<ListTodo />} />
+                    </SidebarMenu>
+                </SidebarGroup>
+                <SidebarGroup>
+                    <SidebarGroupLabel>Collections</SidebarGroupLabel>
+                    <SidebarGroupContent>
+                        <SidebarMenu>
+                            {collections.map((collection) => (<CollectionButton
+                                key={collection.get("id")}
+                                id={collection.get("id")}
+                                name={collection.get("name")}
+                                icon={collection.get("icon")}
+                                color={collection.get("color")}
+                            />))}
+                        </SidebarMenu>
+                    </SidebarGroupContent>
+                </SidebarGroup>
+            </SidebarContent>
+            <SidebarFooter>
+                <SidebarMenu>
+                    <SidebarToggle />
+                    <NavUser />
+                </SidebarMenu>
+            </SidebarFooter>
+        </Sidebar>
+    );
+}
+
+type NavButtonProps = {
+    href: string;
+    label: string;
+    icon?: React.ReactNode;
+    children?: React.ReactNode;
+}
+
+function NavButton(props: NavButtonProps) {
+    const { setOpenMobile } = useSidebar();
+
+    return (
+        <SidebarMenuItem>
+            <SidebarMenuButton asChild>
+                <Link to={props.href} onClick={() => setOpenMobile(false)}>
+                    {props.icon}
+                    <span>{props.label}</span>
+                </Link>
+            </SidebarMenuButton>
+            {props.children}
+        </SidebarMenuItem>
+    );
+}
+
+function HeaderButtons(props: { children: React.ReactNode }) {
+    return (
+        <div className={cn(
+            "flex flex-row justify-between",
+            "transition-[marign,opa] duration-200 ease-linear mt-0",
+            "group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:-mt-9")}>
+            {props.children}
+        </div>
+    );
+}
+
+type CollectionButtonProps = {
+    id: string;
+    name: string;
+    icon: IconName | "";
+    color: ColorName | "";
+}
+
+function CollectionButton(props: CollectionButtonProps) {
+    const icon = props.icon && icons[props.icon];
+    const color = props.color ? colors[props.color] : colors.white;
+
+    return (
+        <SidebarMenuItem>
+            <SidebarMenuButton asChild>
+                <Link
+                    to="/app/collection/$id"
+                    params={{ id: props.id }}
+                    className="w-full flex items-center gap-2 group/item"
+                >
+                    <div className="flex items-center justify-center h-4 w-4">
+                        {icon && <Icon path={icon.path} size={0.75} color={color.base} />}
+                    </div>
+                    <span
+                        className="group-hover/item:text-[var(--hover-color)]"
+                        style={{ "--hover-color": color.hover, }}
+                    >
+                        {props.name}
+                    </span>
+                </Link>
+            </SidebarMenuButton>
+        </SidebarMenuItem>
+    );
+}
+
+function SidebarToggle() {
+    const isMobile = useIsMobile();
+    const { toggleSidebar } = useSidebar()
+
+    if (isMobile) {
+        return null;
+    }
+
+    return (
+        <SidebarMenuItem>
+            <SidebarMenuButton asChild>
+                <Button
+                    data-sidebar="trigger"
+                    variant="ghost"
+                    size="icon"
+                    onClick={toggleSidebar}
+                    className="justify-start"
+                >
+                    <PanelLeft />
+                    <span>Toggle Sidebar</span>
+                </Button>
+            </SidebarMenuButton>
+        </SidebarMenuItem>
+    );
+}
+
+function InboxButton() {
+    const notes = useCollectionNotesMetadata("");
+    const count = notes.length;
+
+    return (
+        <NavButton href="/app/inbox" label="Inbox" icon={<Inbox />}>
+            <SidebarMenuBadge>{count}</SidebarMenuBadge>
+        </NavButton>
+    );
+}
+
+function NavUser() {
+    const isMobile = useIsMobile();
+    const [theme, setTheme] = useTheme();
+    // const { data: session } = useSession();
+    // const user = session?.user;
+    const user = {
+        name: "Kalle Struik",
+        email: "kalle@kallestruik.nl"
+    };
+
+    const email = user?.email ?? undefined;
+    const name = user?.name ?? undefined
+
+    function handleSignOut() {
+        //     // End the session
+        //     signOut();
+        //     // Clear all locally stored data
+        //     localStorage.clear();
+        //     indexedDB.databases().then((dbs) => {
+        //         dbs.forEach((db) => {
+        //             db.name && indexedDB.deleteDatabase(db.name);
+        //         });
+        //     });
+    }
+
+    return (
+        <SidebarMenu>
+            <SidebarMenuItem>
+                <DropdownMenu>
+                    <DropdownMenuTrigger asChild>
+                        <SidebarMenuButton
+                            size="lg"
+                            className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
+                        >
+                            <UserAvatar user={user} className="h-8 w-8 rounded-lg" />
+                            <div className="grid flex-1 text-left text-sm leading-tight">
+                                <span className="truncate font-semibold">{name}</span>
+                                <span className="truncate text-xs">{email}</span>
+                            </div>
+                            <ChevronsUpDown className="ml-auto size-4" />
+                        </SidebarMenuButton>
+                    </DropdownMenuTrigger>
+                    <DropdownMenuContent
+                        className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
+                        side={isMobile ? "top" : "right"}
+                        align="end"
+                        sideOffset={4}
+                    >
+                        <DropdownMenuLabel className="p-0 font-normal">
+                            <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
+                                <UserAvatar user={user} className="h-8 w-8 rounded-lg" />
+                                <div className="grid flex-1 text-left text-sm leading-tight">
+                                    <span className="truncate font-semibold">{name}</span>
+                                    <span className="truncate text-xs">{email}</span>
+                                </div>
+                            </div>
+                        </DropdownMenuLabel>
+                        <DropdownMenuSeparator />
+                        <DropdownMenuLabel className="py-0">
+                            <div className="flex flex-row place-content-between w-full items-center">
+                                <div className="flex flex-row items-center gap-2">
+                                    <Palette className="size-4 text-muted-foreground" />
+                                    Theme
+                                </div>
+                                <ToggleGroup type="single" size="sm" defaultValue={theme}>
+                                    <ToggleGroupItem value="system" onClick={() => setTheme("system")}><Monitor /></ToggleGroupItem>
+                                    <ToggleGroupItem value="light" onClick={() => setTheme("light")}><Sun /></ToggleGroupItem>
+                                    <ToggleGroupItem value="dark" onClick={() => setTheme("dark")}><Moon /></ToggleGroupItem>
+                                </ToggleGroup>
+                            </div>
+                        </DropdownMenuLabel>
+                        <DropdownMenuSeparator />
+                        <DropdownMenuItem onClick={handleSignOut}>
+                            <LogOut />
+                            Log out
+                        </DropdownMenuItem>
+                    </DropdownMenuContent>
+                </DropdownMenu>
+            </SidebarMenuItem>
+        </SidebarMenu>
+    );
+}
diff --git a/src/components/collection/collection_header.tsx b/src/components/collection/collection_header.tsx
new file mode 100644
index 0000000..c308270
--- /dev/null
+++ b/src/components/collection/collection_header.tsx
@@ -0,0 +1,46 @@
+import { mdiCog } from "@mdi/js";
+import Icon from "@mdi/react";
+import { colors } from "~/lib/color";
+import { icons } from "~/lib/icon";
+import { CollectionSettingsDialog } from "~/components/collection/collection_settings_dialog";
+import { Skeleton } from "~/components/ui/skeleton";
+import { useCollection } from "~/hooks/use-metadata";
+import type { CollectionId } from "~/lib/metadata";
+
+type CollectionHeaderProps = {
+    collectionId: CollectionId;
+};
+
+export function CollectionHeader(props: CollectionHeaderProps) {
+    const collection = useCollection(props.collectionId);
+
+    const color = collection ? colors[collection.get("color")] : undefined;
+    const icon = collection ? icons[collection.get("icon")] : undefined;
+
+    return (
+        <>
+            <div className="w-full bg-sidebar flex flex-row items-center">
+                <div className="p-2 flex justify-center items-center">
+                    {collection
+                        ? (icon && <Icon path={icon.path} size={1.5} color={color!.base} />)
+                        : <Skeleton className="w-9 h-9" />
+                    }
+                </div>
+                {collection
+                    ? <div className="p-2 flex-grow font-bold text-xl">{collection.get("name")}</div>
+                    : <div className="p-2 flex-grow"><Skeleton className="h-7 max-w-full w-[200px]" /></div>
+                }
+                <CollectionSettingsDialog
+                    name={collection?.get("name")}
+                    collectionId={props.collectionId}
+                >
+                    <button
+                        className="p-4 flex justify-center items-center hover:bg-sidebar-accent"
+                    >
+                        <Icon path={mdiCog} size={1} />
+                    </button>
+                </CollectionSettingsDialog>
+            </div>
+        </>
+    );
+}
diff --git a/src/components/collection/collection_settings_dialog.tsx b/src/components/collection/collection_settings_dialog.tsx
new file mode 100644
index 0000000..7d02a9f
--- /dev/null
+++ b/src/components/collection/collection_settings_dialog.tsx
@@ -0,0 +1,211 @@
+import * as Y from "yjs";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog";
+import { Input } from "~/components/ui/input";
+import { Button } from "~/components/ui/button";
+import { ColorPicker } from "../form/color_picker";
+import { Label } from "~/components/ui/label";
+import { IconPicker } from "../form/icon_picker";
+import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "~/components/ui/alert-dialog";
+import { useCollection, useCollections } from "~/hooks/use-metadata";
+import { type CollectionId, type CollectionProperty, deleteCollection } from "~/lib/metadata";
+import Icon from "@mdi/react";
+import { mdiPin, mdiPinOff, mdiPlus, mdiTrashCan } from "@mdi/js";
+import { createCollectionProperty, deleteCollectionProperty } from "~/lib/property";
+import { useEffect } from "react";
+import { PropertyTypeCombobox } from "../form/property_type_combobox";
+
+type CollectionSettingsDialogProps = {
+    name?: string;
+    collectionId: CollectionId;
+    children: React.ReactNode;
+};
+export function CollectionSettingsDialog(props: CollectionSettingsDialogProps) {
+    return (
+        <Dialog>
+            <DialogTrigger asChild>
+                {props.children}
+            </DialogTrigger>
+            <DialogContent className="max-w-[800px]">
+                <DialogHeader>
+                    <DialogTitle>{props.name} settings</DialogTitle>
+                    <DialogDescription>
+                        Settings for the {props.name} collection.
+                    </DialogDescription>
+                </DialogHeader>
+                <div className="h-[600px] overflow-scroll flex flex-col gap-4">
+                    <SettingsSection collectionId={props.collectionId} />
+                    <PropertiesSection collectionId={props.collectionId} />
+                    <DangerSection collectionId={props.collectionId} />
+                </div>
+            </DialogContent>
+        </Dialog>
+    );
+}
+
+function SettingsSection(props: { collectionId: CollectionId }) {
+    const collection = useCollection(props.collectionId)
+
+    if (!collection) {
+        return null;
+    }
+
+    return (
+        <div className="flex flex-col gap-2 p-2">
+            <div className="flex flex-row gap-2">
+                <IconPicker
+                    initialValue={collection.get("icon")}
+                    onChange={(icon) => collection.set("icon", icon ?? "")}
+                />
+                <div className="grid flex-grow items-center gap-1.5">
+                    <Label htmlFor="name">Name</Label>
+                    <Input
+                        id="name"
+                        className="flex-grow"
+                        defaultValue={collection.get("name")}
+                        placeholder="Name..."
+                        onChange={(name) => collection.set("name", name.currentTarget.value)}
+                    />
+                </div>
+            </div>
+            <ColorPicker
+                initialValue={collection.get("color")}
+                onChange={(color) => collection.set("color", color)}
+            />
+        </div>
+    );
+}
+
+function PropertiesSection(props: { collectionId: CollectionId }) {
+    const collection = useCollection(props.collectionId)
+
+    if (!collection) {
+        return null;
+    }
+
+    // Ensure that the properties array exists on the collection
+    useEffect(() => {
+        if (!collection.has("properties")) {
+            collection.set("properties", new Y.Array() as any)
+        }
+    }, [collection])
+
+    const properties = collection.get("properties");
+
+    return (
+        <div>
+            <Header label="Properties">
+                <Button
+                    variant="ghost"
+                    onClick={() => properties && createCollectionProperty(properties)}
+                >
+                    <Icon path={mdiPlus} size={.75} />
+                </Button>
+            </Header>
+            <div className="flex flex-col gap-2 p-2">
+                {properties?.map((property) =>
+                    <PropertyItem key={property.get("id")}
+                        property={property}
+                        onDelete={() => deleteCollectionProperty(properties, property.get("id"))}
+                    />
+                )}
+            </div>
+        </div>
+    );
+}
+
+type PropertyItemProps = {
+    property: CollectionProperty,
+    onDelete: () => void,
+}
+function PropertyItem(props: PropertyItemProps) {
+    const { property: prop } = props;
+
+    return (
+        <div className="flex flex-row gap-2">
+            <Input
+                className="flex-grow"
+                defaultValue={prop.get("name")}
+                placeholder="Name..."
+                onChange={(event) => prop.set("name", event.target.value)}
+            />
+            <PropertyTypeCombobox
+                initialValue={prop.get("type")}
+                onSelect={(type) => prop.set("type", type)}
+            />
+
+            <Button
+                variant="outline"
+                onClick={() => prop.set("pinned", !prop.get("pinned"))}
+            >
+                <Icon path={prop.get("pinned") ? mdiPinOff : mdiPin} size={.75} />
+            </Button>
+
+            <Button
+                variant="destructive"
+                onClick={props.onDelete}
+            >
+                <Icon path={mdiTrashCan} size={.75} />
+            </Button>
+        </div>
+    );
+}
+
+function DangerSection(props: { collectionId: CollectionId }) {
+    const collections = useCollections();
+
+    return (
+        <div>
+            <Header label="Danger zone">
+            </Header>
+            <div className="flex flex-row gap-2 p-2 items-center">
+                <div className="flex-grow flex flex-col gap-1">
+                    <span className="text-sm font-bold">Delete collection</span>
+                    <span className="text-xs text-zinc-400">Delete the collection and all items contained within. WARNING: This action is irreversible!</span>
+                </div>
+                <AlertDialog>
+                    <AlertDialogTrigger asChild>
+                        <Button
+                            variant="destructive"
+                        >
+                            Delete
+                        </Button>
+                    </AlertDialogTrigger>
+                    <AlertDialogContent>
+                        <AlertDialogHeader>
+                            <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
+                            <AlertDialogDescription>
+                                This action cannot be undone. This will permanently delete the collection and ALL items contained in it.
+                            </AlertDialogDescription>
+                        </AlertDialogHeader>
+                        <AlertDialogFooter>
+                            <AlertDialogCancel>Cancel</AlertDialogCancel>
+                            <AlertDialogAction asChild>
+                                <Button
+                                    variant="destructive"
+                                    // TODO: Navigate away from the page when clicked
+                                    onClick={() => deleteCollection(collections, props.collectionId)}
+                                >
+                                    Delete
+                                </Button>
+                            </AlertDialogAction>
+                        </AlertDialogFooter>
+                    </AlertDialogContent>
+                </AlertDialog>
+            </div>
+        </div>
+    );
+}
+
+type HeaderProps = {
+    label: string,
+    children?: React.ReactNode,
+}
+
+function Header(props: HeaderProps) {
+    return (
+        <div className="flex flex-row gap-2 pr-2 items-center">
+            <h1 className="font-bold flex-grow">{props.label}</h1>
+            {props.children}
+        </div>
+    );
+}
diff --git a/src/components/collection/new_collection_dialog.tsx b/src/components/collection/new_collection_dialog.tsx
new file mode 100644
index 0000000..d911234
--- /dev/null
+++ b/src/components/collection/new_collection_dialog.tsx
@@ -0,0 +1,76 @@
+import { useState } from "react";
+import { type ColorName } from "~/lib/color";
+import { type IconName } from "~/lib/icon";
+import { ColorPicker } from "../form/color_picker";
+import { IconPicker } from "../form/icon_picker";
+import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog";
+import { Button } from "~/components/ui/button";
+import { Label } from "~/components/ui/label";
+import { Input } from "~/components/ui/input";
+import { createCollection } from "~/lib/metadata";
+import { useCollections } from "~/hooks/use-metadata";
+import { useNavigate } from "@tanstack/react-router";
+
+type NewCollectionDialogProps = {
+    children: React.ReactNode;
+}
+
+export function NewCollectionDialog(props: NewCollectionDialogProps) {
+    const collections = useCollections();
+
+    const [name, setName] = useState("");
+    const [icon, setIcon] = useState<IconName>();
+    const [color, setColor] = useState<ColorName>("white");
+
+    const navigate = useNavigate();
+    function submit() {
+        const newCollection = createCollection(collections, {
+            name,
+            icon: icon ?? "",
+            color,
+        })
+        setName("");
+        setIcon(undefined);
+        setColor("white");
+
+        navigate({ to: "/app/collection/$id", params: { id: newCollection.get("id") } });
+    }
+
+    return (
+        <Dialog>
+            <DialogTrigger asChild>
+                {props.children}
+            </DialogTrigger>
+            <DialogContent>
+                <DialogHeader>
+                    <DialogTitle>New collection</DialogTitle>
+                    <DialogDescription>
+                        Settings for the new collection
+                    </DialogDescription>
+                </DialogHeader>
+                <div className="flex flex-col gap-2 p-2">
+                    <div className="flex flex-row gap-2">
+                        <IconPicker onChange={setIcon} />
+                        <div className="grid flex-grow items-center gap-1.5">
+                            <Label htmlFor="name">Name</Label>
+                            <Input
+                                id="name"
+                                className="flex-grow"
+                                placeholder="Name..."
+                                onChange={(name) => setName(name.target.value)}
+                            />
+                        </div>
+                    </div>
+                    <ColorPicker onChange={setColor} />
+                </div>
+                <DialogFooter>
+                    <DialogClose asChild>
+                        <Button
+                            onClick={submit}
+                        >Create</Button>
+                    </DialogClose>
+                </DialogFooter>
+            </DialogContent>
+        </Dialog>
+    );
+}
diff --git a/src/components/editor/link_icon.tsx b/src/components/editor/link_icon.tsx
new file mode 100644
index 0000000..0c3bbdb
--- /dev/null
+++ b/src/components/editor/link_icon.tsx
@@ -0,0 +1,17 @@
+import { mdiOpenInNew } from "@mdi/js";
+import Icon from "@mdi/react";
+import { cn } from "~/lib/utils";
+
+export function LinkIcon(props: { url: string }) {
+    return (
+        <a className={cn(
+            "link-icon",
+            "inline-flex align-text-bottom items-center justify-center ml-0.5",
+        )}
+            href={props.url}
+            target="_blank"
+        >
+            <Icon path={mdiOpenInNew} size={.75} />
+        </a>
+    );
+}
diff --git a/src/components/editor/task_icon.tsx b/src/components/editor/task_icon.tsx
new file mode 100644
index 0000000..0dea933
--- /dev/null
+++ b/src/components/editor/task_icon.tsx
@@ -0,0 +1,13 @@
+import Icon from "@mdi/react";
+import { cn } from "~/lib/utils";
+
+export function TaskIcon(props: { icon?: string }) {
+    return (
+        <div className={cn(
+            "task-icon",
+            "rounded border border-white align-text-bottom mr-2 select-none w-5 h-5 inline-flex justify-center items-center"
+        )}>
+            {props.icon && <Icon path={props.icon} size={1} />}
+        </div>
+    );
+}
diff --git a/src/components/editor/term_icon.tsx b/src/components/editor/term_icon.tsx
new file mode 100644
index 0000000..4dc54a4
--- /dev/null
+++ b/src/components/editor/term_icon.tsx
@@ -0,0 +1,18 @@
+import { mdiHelp } from "@mdi/js";
+import Icon from "@mdi/react";
+import { Link } from "@tanstack/react-router";
+import { cn } from "~/lib/utils";
+
+export function TermIcon(props: { term: string }) {
+    return (
+        <Link className={cn(
+            "term-icon",
+            "align-text-bottom select-none inline-flex justify-center items-center"
+        )}
+            to="/app/term/$term"
+            params={{ term: props.term }}
+        >
+            <Icon path={mdiHelp} size={.75} />
+        </Link>
+    );
+}
diff --git a/src/components/form/collection_picker.tsx b/src/components/form/collection_picker.tsx
new file mode 100644
index 0000000..f73dd0f
--- /dev/null
+++ b/src/components/form/collection_picker.tsx
@@ -0,0 +1,85 @@
+import Icon from "@mdi/react";
+import { ChevronsUpDown } from "lucide-react";
+import { useState } from "react";
+import { Button } from "~/components/ui/button";
+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
+import { useCollection, useCollections } from "~/hooks/use-metadata";
+import { colors } from "~/lib/color";
+import { icons } from "~/lib/icon";
+import { type CollectionId } from "~/lib/metadata";
+import { cn } from "~/lib/utils";
+
+type CollectionPickerProps = {
+    onChange?: (collectionId?: CollectionId) => void;
+    className?: string;
+}
+
+export function CollectionPicker(props: CollectionPickerProps) {
+    const [open, setOpen] = useState(false);
+    const [value, setValue] = useState<CollectionId | undefined>();
+
+    const collections = useCollections();
+
+    return (
+        <Popover open={open} onOpenChange={setOpen}>
+            <PopoverTrigger asChild>
+                <Button
+                    variant="outline"
+                    role="combobox"
+                    aria-expanded={open}
+                    className={cn("w-[200px] justify-between", props.className)}
+                >
+                    <Entry collectionId={value} />
+                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                </Button>
+            </PopoverTrigger>
+            <PopoverContent className="w-[200px] p-0">
+                <Command>
+                    <CommandInput placeholder="Search collections..." />
+                    <CommandList>
+                        <CommandEmpty>No collection found.</CommandEmpty>
+                        <CommandGroup>
+                            <CommandItem
+                                value="none"
+                                onSelect={() => {
+                                    setValue(undefined);
+                                    setOpen(false);
+                                    props.onChange && props.onChange(undefined);
+                                }}
+                            >
+                                <Entry collectionId={undefined} />
+                            </CommandItem>
+                            {collections.map((collection) => (
+                                <CommandItem
+                                    key={collection.get("id")}
+                                    value={collection.get("name")}
+                                    onSelect={() => {
+                                        setValue(collection.get("id"));
+                                        setOpen(false);
+                                        props.onChange && props.onChange(collection.get("id"));
+                                    }}
+                                >
+                                    <Entry collectionId={collection.get("id")} />
+                                </CommandItem>
+                            ))}
+                        </CommandGroup>
+                    </CommandList>
+                </Command>
+            </PopoverContent>
+        </Popover>
+    );
+}
+
+function Entry(props: { collectionId?: CollectionId }) {
+    const collection = useCollection(props.collectionId)
+
+    return (
+        <div className="flex flex-row w-full gap-3 items-center">
+            <div className="h-4 w-4 flex items-center justify-center">
+                {collection?.get("icon") && <Icon color={colors[collection.get("color")].base} path={icons[collection.get("icon")]!.path} size={1} />}
+            </div>
+            <span style={{ color: collection ? colors[collection.get("color")].hover : undefined }}>{collection?.get("name") ?? "None"}</span>
+        </div>
+    );
+}
diff --git a/src/components/form/color_picker.tsx b/src/components/form/color_picker.tsx
new file mode 100644
index 0000000..5bb9b9c
--- /dev/null
+++ b/src/components/form/color_picker.tsx
@@ -0,0 +1,84 @@
+import { ChevronsUpDown } from "lucide-react";
+import { useState } from "react";
+import { Button } from "~/components/ui/button";
+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
+import { type Color, type ColorName, colors } from "~/lib/color";
+import { cn } from "~/lib/utils";
+
+type ColorPickerProps = ({
+    onChange?: (color: ColorName) => void;
+    withNone?: false;
+} | {
+    onChange?: (color?: ColorName) => void;
+    withNone: true;
+}) & {
+    initialValue?: ColorName;
+    className?: string;
+}
+
+export function ColorPicker(props: ColorPickerProps) {
+    const [open, setOpen] = useState(false);
+    const [value, setValue] = useState<ColorName | undefined>(props.initialValue ?? (props.withNone ? undefined : "white"));
+
+    return (
+        <Popover open={open} onOpenChange={setOpen}>
+            <PopoverTrigger asChild>
+                <Button
+                    variant="outline"
+                    role="combobox"
+                    aria-expanded={open}
+                    className={cn("w-[200px] justify-between", props.className)}
+                >
+                    <Entry color={value ? colors[value] : undefined} />
+                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                </Button>
+            </PopoverTrigger>
+            <PopoverContent className="w-[200px] p-0">
+                <Command>
+                    <CommandInput placeholder="Search colors..." />
+                    <CommandList>
+                        <CommandEmpty>No color found.</CommandEmpty>
+                        <CommandGroup>
+                            {props.withNone && <CommandItem
+                                value={"none"}
+                                onSelect={() => {
+                                    setValue(undefined)
+                                    setOpen(false)
+                                    props.onChange && props.onChange(undefined)
+                                }}
+                            >
+                                <Entry color={undefined} />
+                            </CommandItem>}
+                            {Object.entries(colors).map(([key, value]) => (
+                                <CommandItem
+                                    key={key}
+                                    value={key}
+                                    onSelect={(currentValue) => {
+                                        setValue(currentValue as ColorName)
+                                        setOpen(false)
+                                        props.onChange && props.onChange(currentValue as ColorName)
+                                    }}
+                                >
+                                    <Entry color={value} />
+                                </CommandItem>
+                            ))}
+                        </CommandGroup>
+                    </CommandList>
+                </Command>
+            </PopoverContent>
+        </Popover>
+    );
+}
+
+function Entry(props: { color?: Color }) {
+    return (
+        <div className="flex flex-row w-full gap-2 items-center">
+            <div
+                className="h-4 w-4"
+                style={{ backgroundColor: props.color?.base }}
+            />
+            <span style={{ color: props.color?.hover }}>{props.color?.label ?? "None"}</span>
+        </div>
+    );
+}
diff --git a/src/components/form/icon_picker.tsx b/src/components/form/icon_picker.tsx
new file mode 100644
index 0000000..fa95a7f
--- /dev/null
+++ b/src/components/form/icon_picker.tsx
@@ -0,0 +1,82 @@
+import Icon from "@mdi/react";
+import { useState } from "react";
+import { Button } from "~/components/ui/button";
+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
+import { type Icon as IconType, type IconName, icons } from "~/lib/icon";
+
+type IconPickerProps = ({
+    onChange?: (icon: IconName) => void;
+    withNone?: false;
+} | {
+    onChange?: (icon?: IconName) => void;
+    withNone: true;
+}) & {
+    initialValue?: IconName;
+    className?: string;
+}
+
+// TODO: Make this faster by not showing all icons at once
+export function IconPicker(props: IconPickerProps) {
+    const [value, setValue] = useState<IconName | undefined>(props.initialValue);
+    const [open, setOpen] = useState(false);
+
+    return (
+        <Popover open={open} onOpenChange={setOpen}>
+            <PopoverTrigger asChild>
+                <Button
+                    variant="outline"
+                    role="combobox"
+                    aria-expanded={open}
+                    className="h-full aspect-square"
+                >
+                    {value && <Entry icon={icons[value] as IconType} />}
+                </Button>
+            </PopoverTrigger>
+            <PopoverContent className="w-[200px] p-0">
+                <Command>
+                    <CommandInput placeholder="Search types..." />
+                    <CommandList>
+                        <CommandEmpty>No icon found.</CommandEmpty>
+                        <CommandGroup>
+                            {props.withNone && <CommandItem
+                                value={"none"}
+                                onSelect={() => {
+                                    setValue(undefined)
+                                    setOpen(false)
+                                    props.onChange && props.onChange(undefined)
+                                }}>
+                                <Entry withLabel />
+                            </CommandItem>}
+                            {Object.entries(icons).map(([key, value]) => (
+                                <CommandItem
+                                    key={key}
+                                    value={key}
+                                    onSelect={(currentValue) => {
+                                        setValue(currentValue as IconName)
+                                        setOpen(false)
+                                        props.onChange && props.onChange(currentValue as IconName)
+                                    }}
+                                >
+                                    <Entry icon={value} withLabel />
+                                </CommandItem>
+                            ))}
+                        </CommandGroup>
+                    </CommandList>
+                </Command>
+            </PopoverContent>
+        </Popover>
+    );
+}
+
+function Entry(props: { icon?: IconType, withLabel?: boolean }) {
+    const withLabel = props.withLabel ?? false;
+    return (
+        <div className="flex flex-row w-full gap-2 items-center">
+            <div className="h-6 w-6">
+                {props.icon && <Icon path={props.icon.path} size={1} />}
+            </div>
+            {withLabel && <span>{props.icon?.name ?? "None"}</span>}
+        </div>
+    );
+}
diff --git a/src/components/form/property_type_combobox.tsx b/src/components/form/property_type_combobox.tsx
new file mode 100644
index 0000000..cd72973
--- /dev/null
+++ b/src/components/form/property_type_combobox.tsx
@@ -0,0 +1,57 @@
+import { useState } from "react"
+import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover"
+import { ChevronsUpDown } from "lucide-react"
+import { properties, type PropertyType } from "~/lib/property"
+import { Button } from "~/components/ui/button"
+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command"
+
+type PropertyTypeComboboxProps = {
+    initialValue: PropertyType
+    onSelect: (value: PropertyType) => void
+}
+
+export function PropertyTypeCombobox(props: PropertyTypeComboboxProps) {
+    const [open, setOpen] = useState(false)
+    const [value, setValue] = useState(props.initialValue)
+
+    return (
+        <Popover open={open} onOpenChange={setOpen}>
+            <PopoverTrigger asChild>
+                <Button
+                    variant="outline"
+                    role="combobox"
+                    aria-expanded={open}
+                    className="w-[200px] justify-between"
+                >
+                    {value in properties
+                        ? properties[value as PropertyType]
+                        : "Select type..."}
+                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                </Button>
+            </PopoverTrigger>
+            <PopoverContent className="w-[200px] p-0">
+                <Command>
+                    <CommandInput placeholder="Search types..." />
+                    <CommandList>
+                        <CommandEmpty>No type found.</CommandEmpty>
+                        <CommandGroup>
+                            {Object.entries(properties).map(([key, value]) => (
+                                <CommandItem
+                                    key={key}
+                                    value={key}
+                                    onSelect={(currentValue) => {
+                                        setValue(currentValue as PropertyType)
+                                        setOpen(false)
+                                        props.onSelect(currentValue as PropertyType)
+                                    }}
+                                >
+                                    {value}
+                                </CommandItem>
+                            ))}
+                        </CommandGroup>
+                    </CommandList>
+                </Command>
+            </PopoverContent>
+        </Popover>
+    )
+}
diff --git a/src/components/note/new_note_dialog.tsx b/src/components/note/new_note_dialog.tsx
new file mode 100644
index 0000000..21bd699
--- /dev/null
+++ b/src/components/note/new_note_dialog.tsx
@@ -0,0 +1,120 @@
+import { useState } from "react";
+import { Button } from "~/components/ui/button";
+import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog";
+import { Input } from "~/components/ui/input";
+import { Label } from "~/components/ui/label";
+import { ColorPicker } from "~/components/form/color_picker";
+import type { ColorName } from "~/lib/color";
+import { IconPicker } from "~/components/form/icon_picker";
+import type { IconName } from "~/lib/icon";
+import { CollectionPicker } from "~/components/form/collection_picker";
+import { type CollectionId, createNote } from "~/lib/metadata";
+import { useNotesMetadata } from "~/hooks/use-metadata";
+import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
+import { useNavigate } from "@tanstack/react-router";
+
+type NewNoteDialogProps = {
+    children: React.ReactNode;
+}
+
+export function NewNoteDialog(props: NewNoteDialogProps) {
+    const noteMetadata = useNotesMetadata();
+
+    const [name, setName] = useState("");
+    const [icon, setIcon] = useState<IconName>();
+    const [collection, setCollection] = useState<CollectionId | undefined>();
+    const [primaryColor, setPrimaryColor] = useState<ColorName | undefined>();
+    const [secondaryColor, setSecondaryColor] = useState<ColorName | undefined>();
+    const [type, setType] = useState<"text" | "canvas">("text");
+
+    const navigate = useNavigate();
+    function submit() {
+        const newNote = createNote(noteMetadata, {
+            name,
+            icon,
+            collectionId: collection,
+            primaryColor,
+            secondaryColor,
+            type,
+        });
+
+        setName("");
+        setIcon(undefined);
+        setCollection(undefined);
+        setPrimaryColor(undefined);
+        setSecondaryColor(undefined);
+        setType("text");
+
+        navigate({ to: "/app/note/$id", params: { id: newNote.get("id") } });
+    }
+
+    return (
+        <Dialog>
+            <DialogTrigger asChild>
+                {props.children}
+            </DialogTrigger>
+            <DialogContent>
+                <DialogHeader>
+                    <DialogTitle>New note</DialogTitle>
+                    <DialogDescription>
+                        Settings for the new collection
+                    </DialogDescription>
+                </DialogHeader>
+                <div className="flex flex-col gap-2 p-2">
+                    <div className="flex flex-row gap-2">
+                        <IconPicker onChange={setIcon} />
+                        <div className="grid flex-grow items-center gap-1.5">
+                            <Label htmlFor="name">Name</Label>
+                            <Input
+                                id="name"
+                                className="flex-grow"
+                                placeholder="Name..."
+                                onChange={(name) => setName(name.target.value)}
+                            />
+                        </div>
+                    </div>
+                    <div className="grid flex-grow items-center gap-1.5">
+                        <Label>Collection</Label>
+                        <CollectionPicker onChange={setCollection} className="w-full flex-grow" />
+                    </div>
+                    <div className="flex flex-row gap-2">
+                        <div className="grid flex-grow items-center gap-1.5">
+                            <Label>Primary color</Label>
+                            <ColorPicker withNone onChange={setPrimaryColor} className="w-full flex-grow" />
+                        </div>
+                        <div className="grid flex-grow items-center gap-1.5">
+                            <Label>Secondary color</Label>
+                            <ColorPicker withNone onChange={setSecondaryColor} className="w-full flex-grow" />
+                        </div>
+                    </div>
+                    <div className="grid flex-grow items-center gap-1.5">
+                        <Label>Type</Label>
+                        <RadioGroup
+                            onValueChange={setType as any}
+                            defaultValue={type}
+                            className="flex flex-row space-y-1 items-center"
+                        >
+                            <div className="flex items-center space-x-2">
+                                <RadioGroupItem value="text" id="type-text" />
+                                <Label htmlFor="type-text">Text</Label>
+                            </div>
+                            <div className="flex items-center space-x-2">
+                                <RadioGroupItem value="canvas" id="type-canvas" />
+                                <Label htmlFor="type-canvas">Canvas</Label>
+                            </div>
+                        </RadioGroup>
+                    </div>
+                </div>
+                <DialogFooter>
+                    <DialogClose asChild>
+                        <Button
+                            onClick={() => submit()}
+                        >
+                            Create
+                        </Button>
+                    </DialogClose>
+                </DialogFooter>
+            </DialogContent>
+        </Dialog>
+    );
+}
diff --git a/src/components/note/note_canvas.tsx b/src/components/note/note_canvas.tsx
new file mode 100644
index 0000000..126a722
--- /dev/null
+++ b/src/components/note/note_canvas.tsx
@@ -0,0 +1,76 @@
+import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
+import { ExcalidrawBinding, yjsToExcalidraw } from "y-excalidraw"
+import { Button, Excalidraw } from "@excalidraw/excalidraw";
+import * as Y from "yjs";
+import { useEffect, useMemo, useRef, useState } from "react";
+
+import "@excalidraw/excalidraw/index.css";
+import { useNoteMetadata } from "~/hooks/use-metadata";
+import { Settings } from "lucide-react";
+import { NoteSettingsDialog } from "./note_settings_dialog";
+import { useNoteArray, useNoteAwareness, useNoteMap } from "~/hooks/use-note";
+import { useEffectiveTheme } from "~/hooks/use-theme";
+
+export default function NoteCanvas(props: { noteId: string }) {
+    const [api, setApi] = useState<ExcalidrawImperativeAPI | null>(null);
+    const [binding, setBindings] = useState<ExcalidrawBinding | null>(null);
+    const excalidrawRef = useRef(null);
+    const metadata = useNoteMetadata(props.noteId);
+    const theme = useEffectiveTheme();
+
+    const yElements = useNoteArray("elements") as Y.Array<any>;
+    const yAssets = useNoteMap("assets");
+    const awareness = useNoteAwareness();
+
+    useEffect(() => {
+        if (!api) return;
+
+        const binding = new ExcalidrawBinding(
+            yElements,
+            yAssets,
+            api,
+            awareness,
+            // excalidraw dom is needed to override the undo/redo buttons in the UI as there is no way to override it via props in excalidraw
+            // You might need to pass {trackedOrigins: new Set()} to undomanager depending on whether your provider sets an origin or not
+            { excalidrawDom: excalidrawRef.current!, undoManager: new Y.UndoManager(yElements) },
+        );
+        setBindings(binding);
+        return () => {
+            setBindings(null);
+            binding.destroy();
+        };
+    }, [api]);
+
+    const initData = {
+        elements: yjsToExcalidraw(yElements)
+    }
+
+    const excalidraw = useMemo(() => <Excalidraw
+        initialData={initData}  // Need to set the initial data
+        excalidrawAPI={setApi}
+        onPointerUpdate={binding?.onPointerUpdate}
+        theme={theme}
+        UIOptions={{
+            canvasActions: {
+                toggleTheme: false,
+            },
+        }}
+        renderTopRightUI={() => (<>
+            {metadata && <NoteSettingsDialog noteMetadata={metadata}>
+                <Button
+                    style={{ width: "2.25rem", height: "2.25rem" }}
+                    onSelect={() => { }}
+                >
+                    <Settings />
+                </Button>
+            </NoteSettingsDialog>}
+        </>)}
+    />, []);
+    return (
+        <div className="w-full h-full" ref={excalidrawRef}>
+            {excalidraw}
+        </div>
+    );
+}
+
+
diff --git a/src/components/note/note_header.tsx b/src/components/note/note_header.tsx
new file mode 100644
index 0000000..77dfe84
--- /dev/null
+++ b/src/components/note/note_header.tsx
@@ -0,0 +1,96 @@
+import Icon from "@mdi/react";
+import { colors } from "~/lib/color";
+import { icons } from "~/lib/icon";
+import { NotePropertiesDialog } from "~/components/note/note_properties_dialog";
+import { mdiCircle, mdiCog } from "@mdi/js";
+import { NoteSettingsDialog } from "~/components/note/note_settings_dialog";
+import { useCollection, useNoteMetadata } from "~/hooks/use-metadata";
+import type { NoteId, NoteMetadata } from "~/lib/metadata";
+
+export function NoteHeader(props: { id: NoteId }) {
+    const noteMetadata = useNoteMetadata(props.id);
+
+    if (!noteMetadata) {
+        return null;
+    }
+
+    const primaryColorName = noteMetadata.get("primaryColor");
+    const secondaryColorName = noteMetadata.get("secondaryColor");
+    const primaryColor = primaryColorName ? colors[primaryColorName] : colors.white;
+    const secondaryColor = secondaryColorName ? colors[secondaryColorName] : primaryColor;
+
+    return (
+        <>
+            <div className="flex-shrink-0 w-full h-[300px] relative shadow mb-[42px]" style={{
+                background: `linear-gradient(135deg, ${primaryColor.base}, ${secondaryColor.base})`
+            }}>
+                <div className="flex flex-row justify-end">
+                    <NoteSettingsDialog noteMetadata={noteMetadata}>
+                        <button
+                            className="m-2 p-2 flex justify-center items-center bg-secondary bg-opacity-50 hover:bg-opacity-90 rounded"
+                        >
+                            <Icon path={mdiCog} size={1} />
+                        </button>
+                    </NoteSettingsDialog>
+                </div>
+                <div className="absolute left-8 bottom-[-42px] bg-sidebar rounded-xl flex items-center justify-center h-32 w-32 border-4 border-background">
+                    {noteMetadata.get("icon") && <Icon path={icons[noteMetadata.get("icon")]!.path} color={primaryColor.base} size={4} />}
+                </div>
+            </div>
+
+            <div className="ml-8 flex flex-col gap-1">
+                <div className="flex flex-row gap-1">
+                    {<input
+                        className="bg-transparent outline-none font-bold text-3xl flex-grow"
+                        defaultValue={noteMetadata.get("title")}
+                        onChange={(e) => noteMetadata.set("title", e.target.value)}
+                    />}
+                </div>
+                <Properties noteMetadata={noteMetadata} />
+            </div>
+        </>
+    );
+}
+
+function Properties(props: { noteMetadata: NoteMetadata }) {
+    const { noteMetadata } = props;
+    const collection = useCollection(noteMetadata.get("collectionId"));
+
+    if (!collection) {
+        return <span className="text-muted">No properties</span>;
+    }
+
+    const collectionProps = collection.get("properties")?.toArray();
+    if (!collectionProps) {
+        return <span className="text-muted">No properties</span>;
+    }
+
+    const pinnedProperties = collectionProps.filter(property => property.get("pinned"));
+
+    return (
+        <NotePropertiesDialog noteMetadata={noteMetadata}>
+            <button className="flex flex-row w-fit rounded gap-2 py-1 px-2 hover:bg-accent items-center">
+                {pinnedProperties.length === 0
+                    ? <span className="text-zinc-400">No pinned properties</span>
+                    : pinnedProperties.flatMap(property => {
+                        const propertyId = property.get("id");
+                        const value = noteMetadata.get("properties")
+                            ?.toArray()
+                            .find(it => it.get("propertyId") == propertyId)
+                            ?.get("value");
+                        const seperator = <Icon key={`${propertyId}-sep`} className="text-muted text-2xl" path={mdiCircle} size={.25} />;
+
+                        if (!value) {
+                            return [seperator, <span key={propertyId} className="text-muted">
+                                {property.get("name")}
+                            </span>]
+                        } else {
+                            return [seperator, <span key={propertyId} className="text-muted">
+                                {value}
+                            </span>]
+                        }
+                    }).slice(1)}
+            </button>
+        </NotePropertiesDialog>
+    );
+}
diff --git a/src/components/note/note_properties_dialog.tsx b/src/components/note/note_properties_dialog.tsx
new file mode 100644
index 0000000..f58115e
--- /dev/null
+++ b/src/components/note/note_properties_dialog.tsx
@@ -0,0 +1,114 @@
+import * as Y from "yjs";
+import { useCallback, useEffect } from "react";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog";
+import { Input } from "~/components/ui/input";
+import type { NoteMetadata, NoteProperty } from "~/lib/metadata";
+import { createNoteProperty, type PropertyType } from "~/lib/property";
+import { useCollection } from "~/hooks/use-metadata";
+
+type NotePropertiesDialogProps = {
+    noteMetadata: NoteMetadata;
+    children: React.ReactNode;
+};
+export function NotePropertiesDialog(props: NotePropertiesDialogProps) {
+    const { noteMetadata } = props;
+
+    if (!noteMetadata) {
+        return props.children;
+    }
+
+    return (
+        <Dialog>
+            <DialogTrigger asChild>
+                {props.children}
+            </DialogTrigger>
+            <DialogContent className="max-w-[800px]">
+                <DialogHeader>
+                    <DialogTitle>{noteMetadata.get("title")} properties</DialogTitle>
+                    <DialogDescription>
+                        Properties for the {noteMetadata.get("title")} note
+                    </DialogDescription>
+                </DialogHeader>
+                <div className="h-[600px] overflow-scroll flex flex-col gap-4">
+                    {noteMetadata.has("collectionId")
+                        ? <PropertiesSection noteMetadata={noteMetadata} />
+                        : <span>Only notes that belong to a collection can have properties</span>
+                    }
+                </div>
+            </DialogContent>
+        </Dialog>
+    );
+}
+
+function PropertiesSection(props: { noteMetadata: NoteMetadata }) {
+    const { noteMetadata } = props;
+    const collection = useCollection(noteMetadata.get("collectionId"));
+
+    useEffect(() => {
+        if (!noteMetadata.has("properties")) {
+            noteMetadata.set("properties", new Y.Array() as any)
+        }
+    }, [noteMetadata])
+
+    const noteProperties = noteMetadata.get("properties");
+    if (!noteProperties) {
+        return null;
+    }
+
+    if (!collection) {
+        return null;
+    }
+
+    const collectionProps = collection.get("properties")?.toArray();
+    if (!collectionProps) {
+        return null;
+    }
+
+
+
+    return (
+        <div className="flex flex-col gap-2 p-2">
+            {collectionProps.map((property) => {
+                const propertyId = property.get("id");
+                const noteProperty = noteProperties
+                    ?.toArray()
+                    .find(it => it.get("propertyId") == propertyId)
+                return <PropertyItem key={propertyId}
+                    noteProperty={noteProperty}
+                    name={property.get("name")}
+                    type={property.get("type")}
+                    create={() => createNoteProperty(noteProperties, { propertyId })}
+                />
+            })}
+        </div>
+    );
+}
+
+
+type PropertyItemProps = {
+    noteProperty?: NoteProperty;
+    name: string;
+    type: PropertyType;
+    create: () => NoteProperty;
+}
+function PropertyItem(props: PropertyItemProps) {
+    const handleChange = useCallback((value: string) => {
+        if (props.noteProperty) {
+            props.noteProperty.set("value", value);
+        } else {
+            props.create().set("value", value);
+        }
+    }, [props.noteProperty, props.create]);
+
+    return (
+        <div className="flex flex-row gap-2 items-center">
+            <span className="w-[200px]">{props.name}</span>
+            <Input
+                className="flex-grow"
+                defaultValue={props.noteProperty?.get("value")}
+                placeholder={`${props.name}...`}
+                onChange={(value) => handleChange(value.currentTarget.value)}
+            />
+        </div>
+    );
+}
diff --git a/src/components/note/note_settings_dialog.tsx b/src/components/note/note_settings_dialog.tsx
new file mode 100644
index 0000000..3652f66
--- /dev/null
+++ b/src/components/note/note_settings_dialog.tsx
@@ -0,0 +1,75 @@
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog";
+import { IconPicker } from "../form/icon_picker";
+import { Input } from "~/components/ui/input";
+import { Label } from "~/components/ui/label";
+import { ColorPicker } from "../form/color_picker";
+import type { NoteMetadata } from "~/lib/metadata";
+
+type NoteSettingsDialogProps = {
+    noteMetadata: NoteMetadata;
+    children: React.ReactNode;
+};
+
+export function NoteSettingsDialog(props: NoteSettingsDialogProps) {
+    const { noteMetadata } = props;
+    if (!noteMetadata) {
+        return props.children;
+    }
+
+    const primaryColor = noteMetadata.get("primaryColor");
+    const secondaryColor = noteMetadata.get("secondaryColor");
+
+    return (
+        <Dialog>
+            <DialogTrigger asChild>
+                {props.children}
+            </DialogTrigger>
+            <DialogContent>
+                <DialogHeader>
+                    <DialogTitle>{noteMetadata.get("title")} settings</DialogTitle>
+                    <DialogDescription>
+                        Settings for {noteMetadata.get("title")}.
+                    </DialogDescription>
+                </DialogHeader>
+                {<div className="flex flex-col gap-2 p-2">
+                    <div className="flex flex-row gap-2">
+                        <IconPicker withNone
+                            onChange={(newIcon) => noteMetadata.set("icon", newIcon ?? "")}
+                            initialValue={noteMetadata.get("icon") == "" ? undefined : noteMetadata.get("icon")}
+                        />
+                        <div className="grid flex-grow items-center gap-1.5">
+                            <Label htmlFor="name">Name</Label>
+                            <Input
+                                id="name"
+                                className="flex-grow"
+                                placeholder="Name..."
+                                defaultValue={noteMetadata.get("title")}
+                                onChange={(name) => noteMetadata.set("title", name.target.value)}
+                            />
+                        </div>
+                    </div>
+                    <div className="flex flex-row gap-2">
+                        <div className="grid flex-grow items-center gap-1.5">
+                            <Label>Primary color</Label>
+                            <ColorPicker
+                                withNone
+                                onChange={(color) => noteMetadata.set("primaryColor", color ?? "")}
+                                initialValue={primaryColor == "" ? undefined : primaryColor}
+                                className="w-full flex-grow"
+                            />
+                        </div>
+                        <div className="grid flex-grow items-center gap-1.5">
+                            <Label>Secondary color</Label>
+                            <ColorPicker
+                                withNone
+                                onChange={(color) => noteMetadata.set("secondaryColor", color ?? "")}
+                                initialValue={secondaryColor == "" ? undefined : secondaryColor}
+                                className="w-full flex-grow"
+                            />
+                        </div>
+                    </div>
+                </div>}
+            </DialogContent>
+        </Dialog>
+    );
+}
diff --git a/src/components/note/notes_grid.tsx b/src/components/note/notes_grid.tsx
new file mode 100644
index 0000000..452192d
--- /dev/null
+++ b/src/components/note/notes_grid.tsx
@@ -0,0 +1,105 @@
+import { mdiMagnify } from "@mdi/js";
+import Icon from "@mdi/react";
+import { Link } from "@tanstack/react-router";
+import { Command } from "cmdk";
+import { colors } from "~/lib/color";
+import { icons } from "~/lib/icon";
+import type { NoteMetadata } from "~/lib/metadata";
+
+type NotesGridProps = {
+    notes?: NoteMetadata[];
+    allUnpinned?: boolean;
+};
+export function NotesGrid(props: NotesGridProps) {
+    const allUnpinned = props.allUnpinned ?? false;
+
+    const pinnedNotes = props.notes?.filter(it => !allUnpinned && it.get("pinned"))?.map((note) => <DetailedNote key={note.get("id")} note={note} />);
+    const normalNotes = props.notes?.filter(it => allUnpinned || !it.get("pinned"))?.map((note) => <NormalNote key={note.get("id")} note={note} />);
+
+    return (
+        <Command className="p-4 flex flex-col gap-4 items-stretch">
+            <div className="bg-card rounded shadow flex flex-row items-center w-full max-w-[768px] mx-auto">
+                <div className="p-2 flex justify-center items-center">
+                    <Icon path={mdiMagnify} size={1.5} />
+                </div>
+                <Command.Input asChild>
+                    <input type="text" placeholder="Search..." className="flex-grow bg-transparent py-4 px-2 outline-none" />
+                </Command.Input>
+
+            </div>
+            <Command.List className="[&>div]:flex [&>div]:flex-col [&>div]:gap-4">
+                <Command.Empty>No notes found</Command.Empty>
+                <Command.Group
+                    value="pinned"
+                    className="[&>div]:grid [&>div]:w-full [&>div]:gap-4 [&>div]:grid-cols-[repeat(auto-fill,minmax(500px,1fr))]"
+                >
+                    {pinnedNotes}
+                </Command.Group>
+
+                <Command.Group
+                    value="unpinned"
+                    className="[&>div]:grid [&>div]:w-full [&>div]:gap-4 [&>div]:grid-cols-[repeat(auto-fill,minmax(300px,1fr))]"
+                >
+                    {normalNotes}
+                </Command.Group>
+            </Command.List>
+        </Command>
+    );
+}
+
+type DetailedNoteProps = {
+    note: NoteMetadata;
+}
+
+function DetailedNote(props: DetailedNoteProps) {
+    const { note } = props;
+
+    const primaryColorName = note.get("primaryColor")
+    const secondaryColorName = note.get("secondaryColor")
+
+    const primaryColor = primaryColorName ? colors[primaryColorName] : colors.white;
+    const secondaryColor = secondaryColorName ? colors[secondaryColorName] : primaryColor;
+    const icon = note.get("icon") ? icons[note.get("icon")] : undefined;
+
+    return (
+        <Command.Item asChild value={note.get("title")}>
+            <Link to="/app/note/$id" params={{ id: note.get("id") }} className="bg-card shadow shadow-black rounded overflow-hidden relative">
+                <div className="w-full h-[100px]" style={{
+                    background: `linear-gradient(135deg, ${primaryColor.base}, ${secondaryColor.base})`
+                }} />
+                {icon && <div className="inline-block bg-zinc-900 absolute top-[58px] left-4 rounded-xl p-2">
+                    <Icon path={icon.path} size={2} color={primaryColor.base} />
+                </div>}
+                <div className="px-4 pt-2 pb-4 mt-[22px]">
+                    <h1 className="ml-2 font-bold text-xl">{note.get("title")}</h1>
+                </div>
+            </Link>
+        </Command.Item>
+    );
+}
+
+type NormalNoteProps = {
+    note: NoteMetadata;
+};
+function NormalNote(props: NormalNoteProps) {
+    const { note } = props;
+
+    const colorName = note.get("primaryColor");
+
+    const color = colorName ? colors[colorName] : colors.white;
+    const icon = note.get("icon") ? icons[note.get("icon")] : undefined;
+
+    return (
+        <Command.Item asChild value={note.get("title")}>
+            <Link to="/app/note/$id" params={{ id: note.get("id") }} className="bg-card shadow rounded overflow-hidden flex flex-row">
+                <div className="w-1 h-full" style={{ backgroundColor: color.base }} />
+                <div className="flex flex-col gap-2 px-4 py-2 justify-center">
+                    <div className="flex flex-row gap-2 items-center">
+                        {icon && <Icon path={icon.path} size={1.5} color={color.base} />}
+                        <h1 className="font-bold text-xl">{note.get("title")}</h1>
+                    </div>
+                </div>
+            </Link>
+        </Command.Item>
+    );
+}
diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..59eaa6c
--- /dev/null
+++ b/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,155 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "~/lib/utils"
+import { buttonVariants } from "~/components/ui/button"
+
+function AlertDialog({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
+  return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
+}
+
+function AlertDialogTrigger({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
+  return (
+    <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
+  )
+}
+
+function AlertDialogPortal({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
+  return (
+    <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
+  )
+}
+
+function AlertDialogOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
+  return (
+    <AlertDialogPrimitive.Overlay
+      data-slot="alert-dialog-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
+  return (
+    <AlertDialogPortal>
+      <AlertDialogOverlay />
+      <AlertDialogPrimitive.Content
+        data-slot="alert-dialog-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          className
+        )}
+        {...props}
+      />
+    </AlertDialogPortal>
+  )
+}
+
+function AlertDialogHeader({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-dialog-header"
+      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogFooter({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-dialog-footer"
+      className={cn(
+        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
+  return (
+    <AlertDialogPrimitive.Title
+      data-slot="alert-dialog-title"
+      className={cn("text-lg font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
+  return (
+    <AlertDialogPrimitive.Description
+      data-slot="alert-dialog-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogAction({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
+  return (
+    <AlertDialogPrimitive.Action
+      className={cn(buttonVariants(), className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogCancel({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
+  return (
+    <AlertDialogPrimitive.Cancel
+      className={cn(buttonVariants({ variant: "outline" }), className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  AlertDialog,
+  AlertDialogPortal,
+  AlertDialogOverlay,
+  AlertDialogTrigger,
+  AlertDialogContent,
+  AlertDialogHeader,
+  AlertDialogFooter,
+  AlertDialogTitle,
+  AlertDialogDescription,
+  AlertDialogAction,
+  AlertDialogCancel,
+}
diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..205a1a1
--- /dev/null
+++ b/src/components/ui/avatar.tsx
@@ -0,0 +1,51 @@
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "~/lib/utils"
+
+function Avatar({
+  className,
+  ...props
+}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
+  return (
+    <AvatarPrimitive.Root
+      data-slot="avatar"
+      className={cn(
+        "relative flex size-8 shrink-0 overflow-hidden rounded-full",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AvatarImage({
+  className,
+  ...props
+}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
+  return (
+    <AvatarPrimitive.Image
+      data-slot="avatar-image"
+      className={cn("aspect-square size-full", className)}
+      {...props}
+    />
+  )
+}
+
+function AvatarFallback({
+  className,
+  ...props
+}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
+  return (
+    <AvatarPrimitive.Fallback
+      data-slot="avatar-fallback"
+      className={cn(
+        "bg-muted flex size-full items-center justify-center rounded-full",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
new file mode 100644
index 0000000..a4db9f0
--- /dev/null
+++ b/src/components/ui/command.tsx
@@ -0,0 +1,175 @@
+import * as React from "react"
+import { Command as CommandPrimitive } from "cmdk"
+import { SearchIcon } from "lucide-react"
+
+import { cn } from "~/lib/utils"
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogHeader,
+  DialogTitle,
+} from "~/components/ui/dialog"
+
+function Command({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive>) {
+  return (
+    <CommandPrimitive
+      data-slot="command"
+      className={cn(
+        "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function CommandDialog({
+  title = "Command Palette",
+  description = "Search for a command to run...",
+  children,
+  ...props
+}: React.ComponentProps<typeof Dialog> & {
+  title?: string
+  description?: string
+}) {
+  return (
+    <Dialog {...props}>
+      <DialogHeader className="sr-only">
+        <DialogTitle>{title}</DialogTitle>
+        <DialogDescription>{description}</DialogDescription>
+      </DialogHeader>
+      <DialogContent className="overflow-hidden p-0">
+        <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
+          {children}
+        </Command>
+      </DialogContent>
+    </Dialog>
+  )
+}
+
+function CommandInput({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Input>) {
+  return (
+    <div
+      data-slot="command-input-wrapper"
+      className="flex h-9 items-center gap-2 border-b px-3"
+    >
+      <SearchIcon className="size-4 shrink-0 opacity-50" />
+      <CommandPrimitive.Input
+        data-slot="command-input"
+        className={cn(
+          "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
+          className
+        )}
+        {...props}
+      />
+    </div>
+  )
+}
+
+function CommandList({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.List>) {
+  return (
+    <CommandPrimitive.List
+      data-slot="command-list"
+      className={cn(
+        "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function CommandEmpty({
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
+  return (
+    <CommandPrimitive.Empty
+      data-slot="command-empty"
+      className="py-6 text-center text-sm"
+      {...props}
+    />
+  )
+}
+
+function CommandGroup({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Group>) {
+  return (
+    <CommandPrimitive.Group
+      data-slot="command-group"
+      className={cn(
+        "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function CommandSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
+  return (
+    <CommandPrimitive.Separator
+      data-slot="command-separator"
+      className={cn("bg-border -mx-1 h-px", className)}
+      {...props}
+    />
+  )
+}
+
+function CommandItem({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Item>) {
+  return (
+    <CommandPrimitive.Item
+      data-slot="command-item"
+      className={cn(
+        "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function CommandShortcut({
+  className,
+  ...props
+}: React.ComponentProps<"span">) {
+  return (
+    <span
+      data-slot="command-shortcut"
+      className={cn(
+        "text-muted-foreground ml-auto text-xs tracking-widest",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export {
+  Command,
+  CommandDialog,
+  CommandInput,
+  CommandList,
+  CommandEmpty,
+  CommandGroup,
+  CommandItem,
+  CommandShortcut,
+  CommandSeparator,
+}
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..367a960
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -0,0 +1,133 @@
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "~/lib/utils"
+
+function Dialog({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Root>) {
+  return <DialogPrimitive.Root data-slot="dialog" {...props} />
+}
+
+function DialogTrigger({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
+  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
+}
+
+function DialogPortal({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
+  return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
+}
+
+function DialogClose({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Close>) {
+  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
+}
+
+function DialogOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
+  return (
+    <DialogPrimitive.Overlay
+      data-slot="dialog-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DialogContent({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Content>) {
+  return (
+    <DialogPortal data-slot="dialog-portal">
+      <DialogOverlay />
+      <DialogPrimitive.Content
+        data-slot="dialog-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
+          <XIcon />
+          <span className="sr-only">Close</span>
+        </DialogPrimitive.Close>
+      </DialogPrimitive.Content>
+    </DialogPortal>
+  )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="dialog-header"
+      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
+      {...props}
+    />
+  )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="dialog-footer"
+      className={cn(
+        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DialogTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Title>) {
+  return (
+    <DialogPrimitive.Title
+      data-slot="dialog-title"
+      className={cn("text-lg leading-none font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function DialogDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Description>) {
+  return (
+    <DialogPrimitive.Description
+      data-slot="dialog-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Dialog,
+  DialogClose,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogOverlay,
+  DialogPortal,
+  DialogTitle,
+  DialogTrigger,
+}
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..68c82bc
--- /dev/null
+++ b/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,255 @@
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+
+import { cn } from "~/lib/utils"
+
+function DropdownMenu({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
+  return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
+}
+
+function DropdownMenuPortal({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
+  return (
+    <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
+  )
+}
+
+function DropdownMenuTrigger({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
+  return (
+    <DropdownMenuPrimitive.Trigger
+      data-slot="dropdown-menu-trigger"
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuContent({
+  className,
+  sideOffset = 4,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
+  return (
+    <DropdownMenuPrimitive.Portal>
+      <DropdownMenuPrimitive.Content
+        data-slot="dropdown-menu-content"
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
+          className
+        )}
+        {...props}
+      />
+    </DropdownMenuPrimitive.Portal>
+  )
+}
+
+function DropdownMenuGroup({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
+  return (
+    <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
+  )
+}
+
+function DropdownMenuItem({
+  className,
+  inset,
+  variant = "default",
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
+  inset?: boolean
+  variant?: "default" | "destructive"
+}) {
+  return (
+    <DropdownMenuPrimitive.Item
+      data-slot="dropdown-menu-item"
+      data-inset={inset}
+      data-variant={variant}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuCheckboxItem({
+  className,
+  children,
+  checked,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
+  return (
+    <DropdownMenuPrimitive.CheckboxItem
+      data-slot="dropdown-menu-checkbox-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      checked={checked}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <DropdownMenuPrimitive.ItemIndicator>
+          <CheckIcon className="size-4" />
+        </DropdownMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </DropdownMenuPrimitive.CheckboxItem>
+  )
+}
+
+function DropdownMenuRadioGroup({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
+  return (
+    <DropdownMenuPrimitive.RadioGroup
+      data-slot="dropdown-menu-radio-group"
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuRadioItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
+  return (
+    <DropdownMenuPrimitive.RadioItem
+      data-slot="dropdown-menu-radio-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <DropdownMenuPrimitive.ItemIndicator>
+          <CircleIcon className="size-2 fill-current" />
+        </DropdownMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </DropdownMenuPrimitive.RadioItem>
+  )
+}
+
+function DropdownMenuLabel({
+  className,
+  inset,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
+  inset?: boolean
+}) {
+  return (
+    <DropdownMenuPrimitive.Label
+      data-slot="dropdown-menu-label"
+      data-inset={inset}
+      className={cn(
+        "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
+  return (
+    <DropdownMenuPrimitive.Separator
+      data-slot="dropdown-menu-separator"
+      className={cn("bg-border -mx-1 my-1 h-px", className)}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuShortcut({
+  className,
+  ...props
+}: React.ComponentProps<"span">) {
+  return (
+    <span
+      data-slot="dropdown-menu-shortcut"
+      className={cn(
+        "text-muted-foreground ml-auto text-xs tracking-widest",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuSub({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
+  return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
+}
+
+function DropdownMenuSubTrigger({
+  className,
+  inset,
+  children,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
+  inset?: boolean
+}) {
+  return (
+    <DropdownMenuPrimitive.SubTrigger
+      data-slot="dropdown-menu-sub-trigger"
+      data-inset={inset}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      <ChevronRightIcon className="ml-auto size-4" />
+    </DropdownMenuPrimitive.SubTrigger>
+  )
+}
+
+function DropdownMenuSubContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
+  return (
+    <DropdownMenuPrimitive.SubContent
+      data-slot="dropdown-menu-sub-content"
+      className={cn(
+        "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export {
+  DropdownMenu,
+  DropdownMenuPortal,
+  DropdownMenuTrigger,
+  DropdownMenuContent,
+  DropdownMenuGroup,
+  DropdownMenuLabel,
+  DropdownMenuItem,
+  DropdownMenuCheckboxItem,
+  DropdownMenuRadioGroup,
+  DropdownMenuRadioItem,
+  DropdownMenuSeparator,
+  DropdownMenuShortcut,
+  DropdownMenuSub,
+  DropdownMenuSubTrigger,
+  DropdownMenuSubContent,
+}
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000..9aa23b4
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+
+import { cn } from "~/lib/utils"
+
+function Label({
+  className,
+  ...props
+}: React.ComponentProps<typeof LabelPrimitive.Root>) {
+  return (
+    <LabelPrimitive.Root
+      data-slot="label"
+      className={cn(
+        "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Label }
diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx
new file mode 100644
index 0000000..201f7d4
--- /dev/null
+++ b/src/components/ui/popover.tsx
@@ -0,0 +1,46 @@
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "~/lib/utils"
+
+function Popover({
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
+  return <PopoverPrimitive.Root data-slot="popover" {...props} />
+}
+
+function PopoverTrigger({
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
+  return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
+}
+
+function PopoverContent({
+  className,
+  align = "center",
+  sideOffset = 4,
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
+  return (
+    <PopoverPrimitive.Portal>
+      <PopoverPrimitive.Content
+        data-slot="popover-content"
+        align={align}
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
+          className
+        )}
+        {...props}
+      />
+    </PopoverPrimitive.Portal>
+  )
+}
+
+function PopoverAnchor({
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
+  return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
+}
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx
new file mode 100644
index 0000000..b1c5667
--- /dev/null
+++ b/src/components/ui/radio-group.tsx
@@ -0,0 +1,45 @@
+"use client"
+
+import * as React from "react"
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
+import { CircleIcon } from "lucide-react"
+
+import { cn } from "~/lib/utils"
+
+function RadioGroup({
+  className,
+  ...props
+}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
+  return (
+    <RadioGroupPrimitive.Root
+      data-slot="radio-group"
+      className={cn("grid gap-3", className)}
+      {...props}
+    />
+  )
+}
+
+function RadioGroupItem({
+  className,
+  ...props
+}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
+  return (
+    <RadioGroupPrimitive.Item
+      data-slot="radio-group-item"
+      className={cn(
+        "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
+        className
+      )}
+      {...props}
+    >
+      <RadioGroupPrimitive.Indicator
+        data-slot="radio-group-indicator"
+        className="relative flex items-center justify-center"
+      >
+        <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
+      </RadioGroupPrimitive.Indicator>
+    </RadioGroupPrimitive.Item>
+  )
+}
+
+export { RadioGroup, RadioGroupItem }
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
index 3de327a..d44cb24 100644
--- a/src/components/ui/sidebar.tsx
+++ b/src/components/ui/sidebar.tsx
@@ -308,7 +308,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
             data-slot="sidebar-inset"
             className={cn(
                 "bg-background relative flex w-full flex-1 flex-col",
-                "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
+                "md:group-peer-data-[variant=inset]:m-2 md:group-peer-data-[variant=inset]:ml-0 md:group-peer-data-[variant=inset]:rounded-xl md:group-peer-data-[variant=inset]:shadow-sm",
                 className
             )}
             {...props}
diff --git a/src/components/ui/toggle-group.tsx b/src/components/ui/toggle-group.tsx
new file mode 100644
index 0000000..711481b
--- /dev/null
+++ b/src/components/ui/toggle-group.tsx
@@ -0,0 +1,71 @@
+import * as React from "react"
+import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
+import { type VariantProps } from "class-variance-authority"
+
+import { cn } from "~/lib/utils"
+import { toggleVariants } from "~/components/ui/toggle"
+
+const ToggleGroupContext = React.createContext<
+  VariantProps<typeof toggleVariants>
+>({
+  size: "default",
+  variant: "default",
+})
+
+function ToggleGroup({
+  className,
+  variant,
+  size,
+  children,
+  ...props
+}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
+  VariantProps<typeof toggleVariants>) {
+  return (
+    <ToggleGroupPrimitive.Root
+      data-slot="toggle-group"
+      data-variant={variant}
+      data-size={size}
+      className={cn(
+        "group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
+        className
+      )}
+      {...props}
+    >
+      <ToggleGroupContext.Provider value={{ variant, size }}>
+        {children}
+      </ToggleGroupContext.Provider>
+    </ToggleGroupPrimitive.Root>
+  )
+}
+
+function ToggleGroupItem({
+  className,
+  children,
+  variant,
+  size,
+  ...props
+}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
+  VariantProps<typeof toggleVariants>) {
+  const context = React.useContext(ToggleGroupContext)
+
+  return (
+    <ToggleGroupPrimitive.Item
+      data-slot="toggle-group-item"
+      data-variant={context.variant || variant}
+      data-size={context.size || size}
+      className={cn(
+        toggleVariants({
+          variant: context.variant || variant,
+          size: context.size || size,
+        }),
+        "min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
+        className
+      )}
+      {...props}
+    >
+      {children}
+    </ToggleGroupPrimitive.Item>
+  )
+}
+
+export { ToggleGroup, ToggleGroupItem }
diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx
new file mode 100644
index 0000000..6192e67
--- /dev/null
+++ b/src/components/ui/toggle.tsx
@@ -0,0 +1,47 @@
+"use client"
+
+import * as React from "react"
+import * as TogglePrimitive from "@radix-ui/react-toggle"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "~/lib/utils"
+
+const toggleVariants = cva(
+  "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
+  {
+    variants: {
+      variant: {
+        default: "bg-transparent",
+        outline:
+          "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
+      },
+      size: {
+        default: "h-9 px-2 min-w-9",
+        sm: "h-8 px-1.5 min-w-8",
+        lg: "h-10 px-2.5 min-w-10",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  }
+)
+
+function Toggle({
+  className,
+  variant,
+  size,
+  ...props
+}: React.ComponentProps<typeof TogglePrimitive.Root> &
+  VariantProps<typeof toggleVariants>) {
+  return (
+    <TogglePrimitive.Root
+      data-slot="toggle"
+      className={cn(toggleVariants({ variant, size, className }))}
+      {...props}
+    />
+  )
+}
+
+export { Toggle, toggleVariants }
diff --git a/src/components/user/user_avatar.tsx b/src/components/user/user_avatar.tsx
new file mode 100644
index 0000000..71e6307
--- /dev/null
+++ b/src/components/user/user_avatar.tsx
@@ -0,0 +1,51 @@
+import { useEffect, useState } from "react";
+import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
+
+type User = {
+    name?: string;
+    email?: string;
+    image?: string;
+};
+
+type UserAvatarProps = {
+    className?: string;
+    user?: User;
+};
+
+export function UserAvatar(props: UserAvatarProps) {
+    const email = props.user?.email ?? undefined;
+    const gravatar = useGravatarUrl(email);
+    const avatar = props.user?.image ?? gravatar;
+    const name = props.user?.name ?? undefined
+    const initials = getInitials(name);
+
+    return (
+        <Avatar className={props.className}>
+            <AvatarImage src={avatar} alt={name} />
+            <AvatarFallback className="rounded-lg">{initials}</AvatarFallback>
+        </Avatar>
+    );
+}
+
+function useGravatarUrl(email?: string, size = 80) {
+    const [url, setUrl] = useState<string>();
+    useEffect(() => {
+        if (!email) return;
+
+        const trimmedEmail = email.trim().toLowerCase();
+        crypto.subtle.digest('SHA-256', new TextEncoder().encode(trimmedEmail)).then(hashBuffer => {
+            const hashArray = Array.from(new Uint8Array(hashBuffer));
+            const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
+            setUrl(`https://www.gravatar.com/avatar/${hashHex}?s=${size}&d=identicon`);
+        });
+    }, [email, size]);
+
+    return url;
+}
+
+function getInitials(name?: string) {
+    if (!name) return "";
+
+    const parts = name.split(" ");
+    return parts.map(part => part[0]).join("");
+}
diff --git a/src/components/yjs/metadata-inspector.tsx b/src/components/yjs/metadata-inspector.tsx
new file mode 100644
index 0000000..b06dd9d
--- /dev/null
+++ b/src/components/yjs/metadata-inspector.tsx
@@ -0,0 +1,64 @@
+import * as Y from "yjs";
+import { useCollections, useNotesMetadata } from "~/hooks/use-metadata";
+
+export function MetadataInspector() {
+    const collections = useCollections()
+    const notes = useNotesMetadata()
+
+    return <div>
+        <span>Collections: <YjsInspector toRender={collections} /></span>
+        <span>Notes: <YjsInspector toRender={notes} /></span>
+    </div>
+}
+
+export function YjsInspector(props: { toRender: any }) {
+    return <YjsNode node={props.toRender} depth={0} />
+}
+
+type YjsNodeProps<T> = {
+    node: T
+    depth: number
+    label?: string
+}
+
+function YjsNode(props: YjsNodeProps<any>) {
+    if (props.node instanceof Y.Map) return <YjsMap {...props} />
+    if (props.node instanceof Y.Array) return <YjsArray {...props} />
+    if (typeof props.node == "string") return <String {...props} />
+
+    return <div>
+        {props.label}: {props.node.toString()}
+    </div>
+}
+
+function YjsMap(props: YjsNodeProps<Y.Map<any>>) {
+    const entries = Array.from(props.node.entries())
+    return <div>
+        {props.label ? `${props.label}: ` : ""}<span>{"{"}<Badge>YMap</Badge></span>
+        <div style={{ paddingLeft: "2ch" }}>
+            {entries.map(([key, item]) => <YjsNode key={key} node={item} depth={props.depth + 1} label={key} />)}
+        </div>
+        <span>{"}"}</span>
+    </div>
+}
+
+function YjsArray(props: YjsNodeProps<Y.Array<any>>) {
+    return <div>
+        {props.label ? `${props.label}: ` : ""}<span>{"["}<Badge>YArray</Badge></span>
+        <div style={{ paddingLeft: "2ch" }}>
+            {props.node.map((item, index) => <YjsNode key={index} node={item} depth={props.depth + 1} />)}
+        </div>
+        <span>{"]"}</span>
+    </div>
+}
+
+function Badge(props: { children: React.ReactNode }) {
+    return <span className="px-1 py-.5 rounded bg-accent ml-1">
+        {props.children}
+    </span>
+}
+
+function String(props: YjsNodeProps<string>) {
+    return <div>{props.label}: "{props.node}"</div>
+}
+
diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx
new file mode 100644
index 0000000..8e13dd0
--- /dev/null
+++ b/src/editor/Editor.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import * as Y from "yjs";
+import { type InitialConfigType, LexicalComposer } from "@lexical/react/LexicalComposer";
+import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
+import { CollaborationPlugin } from '@lexical/react/LexicalCollaborationPlugin';
+import { ContentEditable } from "@lexical/react/LexicalContentEditable";
+import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
+import { HeaderMarkerNode, HeaderNode } from "./nodes/header_node";
+import { ParagraphPlugin } from "./plugins/paragraph_plugin";
+import { HeaderPlugin } from "./plugins/header_plugin";
+import { SelectionPlugin } from "./plugins/selection_plugin";
+import { FormattedTextMarkerNode, FormattedTextNode } from "./nodes/formatted_text";
+import { FormattedTextPlugin } from "./plugins/formatted_text_plugin";
+import { TaskIconNode, TaskMarkerNode, TaskNode } from "./nodes/task_node";
+import { TaskPlugin } from "./plugins/task_plugin";
+import { LinkIconNode, LinkMarkerNode, LinkNode, LinkUrlNode } from "./nodes/link_node";
+import { LinkPlugin } from "./plugins/link_plugin";
+import { TermIconNode, TermMarkerNode, TermNode } from "./nodes/term_node";
+import { TermPlugin } from "./plugins/term_plugin";
+import { useCallback } from "react";
+import type { Provider } from "@lexical/yjs";
+import type { NoteId } from "~/lib/metadata";
+import type { Klass, LexicalNode } from "lexical";
+import { useNoteDoc, useNoteProviders } from "~/hooks/use-note";
+
+export function Editor(props: { noteId: NoteId }) {
+    const provider = useNoteProviders().websocket;
+    const doc = useNoteDoc();
+
+    const providerFactory = useCallback((id: string, yjsDocMap: Map<string, Y.Doc>): Provider => {
+        // Just overwrite it, because we have the doc managed externally
+        yjsDocMap.set(id, doc);
+
+        return provider as any as Provider;
+    }, [provider, doc]);
+
+    return (
+        <LexicalComposer initialConfig={initialEditorConfig}>
+            <div className="w-full relative editor-content">
+                <PlainTextPlugin
+                    contentEditable={<ContentEditable className="outline-none" />}
+                    placeholder={<div className="absolute top-0 left-0 pointer-events-none text-zinc-300">Enter some text...</div>}
+                    ErrorBoundary={LexicalErrorBoundary}
+                />
+            </div>
+            <CollaborationPlugin
+                id={`note-${props.noteId}`}
+                providerFactory={providerFactory}
+                shouldBootstrap={false}
+                excludedProperties={excludedProperties}
+            />
+
+            <ParagraphPlugin />
+            <SelectionPlugin />
+
+            <HeaderPlugin />
+            <FormattedTextPlugin />
+            <TaskPlugin />
+            <LinkPlugin />
+            <TermPlugin />
+        </LexicalComposer>
+    );
+}
+
+const excludedProperties = new Map<Klass<LexicalNode>, Set<string>>([
+    [HeaderNode, new Set(["__hasFocus"])],
+    [TermNode, new Set(["__hasFocus"])],
+    [TaskNode, new Set(["__hasFocus"])],
+    [LinkNode, new Set(["__hasFocus"])],
+    [FormattedTextNode, new Set(["__hasFocus"])],
+])
+
+const theme = {
+};
+
+const initialEditorConfig: InitialConfigType = {
+    namespace: "NoteEditor",
+    theme,
+    onError: console.error,
+    editorState: null,
+    nodes: [
+        HeaderNode, HeaderMarkerNode,
+        FormattedTextNode, FormattedTextMarkerNode,
+        TaskNode, TaskMarkerNode, TaskIconNode,
+        LinkNode, LinkMarkerNode, LinkUrlNode, LinkIconNode,
+        TermNode, TermMarkerNode, TermIconNode,
+    ],
+};
+
diff --git a/src/editor/editor_utils.ts b/src/editor/editor_utils.ts
new file mode 100644
index 0000000..b6b0413
--- /dev/null
+++ b/src/editor/editor_utils.ts
@@ -0,0 +1,26 @@
+import { $isRootOrShadowRoot, ElementNode, LexicalNode } from "lexical";
+import { $isLinkUrlNode } from "./nodes/link_node";
+import { $isTermNode } from "./nodes/term_node";
+
+export function findBlockNode(node: LexicalNode): LexicalNode {
+    let toCheck: LexicalNode | null = node
+    while (!isBlockNode(toCheck)) {
+        if ($isRootOrShadowRoot(node)) {
+            console.error("findBlockNode encountered the root node. This should not be possible!");
+            throw new Error("Could not find a block node in the parent chain");
+        }
+
+        if (toCheck === null) {
+            throw new Error("Could not find a block node in the parent chain");
+        }
+        toCheck = toCheck.getParent();
+    }
+
+    return toCheck!;
+}
+
+
+export function isBlockNode(node?: LexicalNode | null): boolean {
+    return node instanceof ElementNode && !node.isInline();
+}
+
diff --git a/src/editor/nodes/focusable_node.ts b/src/editor/nodes/focusable_node.ts
new file mode 100644
index 0000000..1e97965
--- /dev/null
+++ b/src/editor/nodes/focusable_node.ts
@@ -0,0 +1,32 @@
+import { ElementNode, type LexicalNode } from "lexical";
+
+export class FocusableNode extends ElementNode {
+    __hasFocus: boolean = false;
+
+    setFocus(hasFocus: boolean) {
+        const self = this.getWritable();
+        self.__hasFocus = hasFocus;
+    }
+
+    constructor(key?: string, hasFocus?: boolean) {
+        super(key);
+        hasFocus && (this.__hasFocus = hasFocus);
+    }
+
+    createDOM(): HTMLElement {
+        const element = document.createElement("div");
+        element.classList.toggle("focus", this.__hasFocus);
+        return element;
+    }
+
+    updateDOM(old: FocusableNode, dom: HTMLElement): boolean {
+        if (this.__hasFocus != old.__hasFocus) {
+            dom.classList.toggle("focus", this.__hasFocus);
+        }
+        return false;
+    }
+}
+
+export function $isFocusableNode(node: LexicalNode): node is FocusableNode {
+    return node instanceof FocusableNode;
+}
diff --git a/src/editor/nodes/formatted_text.ts b/src/editor/nodes/formatted_text.ts
new file mode 100644
index 0000000..689fb38
--- /dev/null
+++ b/src/editor/nodes/formatted_text.ts
@@ -0,0 +1,109 @@
+import { ElementNode, type LexicalNode, type SerializedElementNode, type Spread } from "lexical";
+import { FocusableNode } from "./focusable_node";
+import type { TextFormat } from "../serialized_editor_content";
+
+type SerializedTextFormatNode = Spread<SerializedElementNode, {
+    style: TextFormat;
+}>;
+
+export class FormattedTextNode extends FocusableNode {
+    __internalStyle: TextFormat;
+
+    getStyle(): TextFormat {
+        const self = this.getLatest();
+        return self.__internalStyle;
+    }
+
+    static getType(): string {
+        return "formatted-text";
+    }
+
+    static clone(node: FormattedTextNode): FormattedTextNode {
+        return new FormattedTextNode(node.__internalStyle, node.__key, node.__hasFocus);
+    }
+
+    constructor(style: TextFormat, key?: string, hasFocus?: boolean) {
+        super(key, hasFocus);
+        this.__internalStyle = style;
+    }
+
+    isInline(): boolean {
+        return true;
+    }
+
+    createDOM(): HTMLElement {
+        const element = super.createDOM();
+        element.classList.add("formatted-text");
+        element.classList.add(`format-${this.__internalStyle}`);
+        return element;
+    }
+
+    updateDOM(old: FormattedTextNode, dom: HTMLElement): boolean {
+        return super.updateDOM(old, dom);
+    }
+
+    exportJSON(): SerializedTextFormatNode {
+        return {
+            ...super.exportJSON(),
+            type: "formatted-text",
+            style: this.__internalStyle,
+        };
+    }
+
+    static importJSON(serializedNode: SerializedTextFormatNode): FormattedTextNode {
+        return $createFormattedTextNode(serializedNode.style);
+    }
+
+}
+
+export function $createFormattedTextNode(style: TextFormat) {
+    return new FormattedTextNode(style);
+}
+
+export function $isFormattedTextNode(node?: LexicalNode | null): node is FormattedTextNode {
+    return node instanceof FormattedTextNode;
+}
+
+export class FormattedTextMarkerNode extends ElementNode {
+    static getType(): string {
+        return "formatted-text-marker";
+    }
+
+    static clone(node: FormattedTextMarkerNode): FormattedTextMarkerNode {
+        return new FormattedTextMarkerNode(node.__key);
+    }
+
+    isInline(): boolean {
+        return true;
+    }
+
+    createDOM(): HTMLElement {
+        const element = document.createElement("span");
+        element.classList.add("formatted-text-marker");
+        return element;
+    }
+
+    updateDOM(): boolean {
+        return false;
+    }
+
+    exportJSON(): SerializedElementNode {
+        return {
+            ...super.exportJSON(),
+            type: "formatted-text-marker",
+        };
+    }
+
+    static importJSON() {
+        return $createFormattedTextMarkerNode();
+    }
+}
+
+export function $createFormattedTextMarkerNode() {
+    return new FormattedTextMarkerNode();
+}
+
+export function $isFormattedTextMarkerNode(node?: LexicalNode | null): node is FormattedTextMarkerNode {
+    return node instanceof FormattedTextMarkerNode;
+}
+
diff --git a/src/editor/nodes/header_node.ts b/src/editor/nodes/header_node.ts
new file mode 100644
index 0000000..65109ca
--- /dev/null
+++ b/src/editor/nodes/header_node.ts
@@ -0,0 +1,108 @@
+import { $createParagraphNode, ElementNode, type LexicalNode, ParagraphNode, type RangeSelection, type SerializedElementNode, type Spread } from "lexical";
+import { FocusableNode } from "./focusable_node";
+
+type SerializedHeaderNode = Spread<SerializedElementNode, {
+    level: number;
+}>;
+
+export class HeaderNode extends FocusableNode {
+    __level: number;
+
+    getLevel() {
+        const self = this.getLatest();
+        return self.__level;
+    }
+
+    static getType() {
+        return "header";
+    }
+
+    static clone(node: HeaderNode) {
+        return new HeaderNode(node.__level, node.__key, node.__hasFocus);
+    }
+
+    constructor(level: number, key?: string, hasFocus?: boolean) {
+        super(key, hasFocus);
+        this.__level = level;
+    }
+
+    createDOM(): HTMLElement {
+        const element = super.createDOM();
+        element.classList.add("header", `header-${this.__level}`);
+        return element;
+    }
+
+    updateDOM(old: HeaderNode, dom: HTMLElement): boolean {
+        return super.updateDOM(old, dom);
+    }
+
+    insertNewAfter(_selection: RangeSelection, restoreSelection?: boolean): ParagraphNode {
+        const newElement = $createParagraphNode();
+        this.insertAfter(newElement, restoreSelection);
+        return newElement;
+    }
+
+    exportJSON(): SerializedHeaderNode {
+        return {
+            ...super.exportJSON(),
+            type: "header",
+            level: this.__level,
+        };
+    }
+
+    static importJSON(serializedHeaderNode: SerializedHeaderNode) {
+        return $createHeaderNode(serializedHeaderNode.level);
+    }
+}
+
+export function $createHeaderNode(level: number) {
+    return new HeaderNode(level);
+}
+
+export function $isHeaderNode(node?: LexicalNode | null): node is HeaderNode {
+    return node instanceof HeaderNode;
+}
+
+export class HeaderMarkerNode extends ElementNode {
+    static getType() {
+        return "header-marker";
+    }
+
+    static clone(node: HeaderMarkerNode) {
+        return new HeaderMarkerNode(node.__key);
+    }
+
+    isInline(): boolean {
+        return true;
+    }
+
+    createDOM(): HTMLElement {
+        const element = document.createElement("span");
+        element.classList.add("header-marker");
+        return element;
+    }
+
+    updateDOM(): boolean {
+        return false;
+    }
+
+    exportJSON(): SerializedElementNode {
+        return {
+            ...super.exportJSON(),
+            type: "header-marker",
+        };
+    }
+
+    static importJSON() {
+        return $createHeaderMarkerNode();
+    }
+}
+
+export function $createHeaderMarkerNode() {
+    return new HeaderMarkerNode();
+}
+
+export function $isHeaderMarkerNode(node?: LexicalNode | null): node is HeaderMarkerNode {
+    return node instanceof HeaderMarkerNode;
+}
+
diff --git a/src/editor/nodes/link_node.tsx b/src/editor/nodes/link_node.tsx
new file mode 100644
index 0000000..dd87a58
--- /dev/null
+++ b/src/editor/nodes/link_node.tsx
@@ -0,0 +1,184 @@
+import { DecoratorNode, ElementNode, type LexicalNode, type SerializedElementNode, type SerializedLexicalNode, type Spread } from "lexical";
+import { FocusableNode } from "./focusable_node";
+import type { ReactNode } from "react";
+import { LinkIcon } from "~/components/editor/link_icon";
+
+type SerializedLinkNode = Spread<SerializedElementNode, {
+    url: string;
+}>;
+
+export class LinkNode extends FocusableNode {
+    __url: string;
+
+    getUrl(): string {
+        const self = this.getLatest();
+        return self.__url;
+    }
+
+    static getType() {
+        return "link";
+    }
+    static clone(node: LinkNode) {
+        return new LinkNode(node.__url, node.__key, node.__hasFocus);
+    }
+    constructor(url: string, key?: string, hasFocus?: boolean) {
+        super(key, hasFocus);
+        this.__url = url;
+    }
+
+    isInline(): boolean {
+        return true;
+    }
+    createDOM(): HTMLElement {
+        const element = super.createDOM();
+        element.classList.add("link");
+        return element;
+    }
+    updateDOM(old: LinkNode, dom: HTMLElement): boolean {
+        return super.updateDOM(old, dom);
+    }
+    exportJSON(): SerializedLinkNode {
+        return {
+            ...super.exportJSON(),
+            type: "link",
+            url: this.__url,
+        };
+    }
+    static importJSON(serializedNode: SerializedLinkNode) {
+        return $createLinkNode(serializedNode.url);
+    }
+}
+
+export function $createLinkNode(url: string) {
+    return new LinkNode(url);
+}
+
+export function $isLinkNode(node?: LexicalNode | null): node is LinkNode {
+    return node instanceof LinkNode;
+}
+
+export class LinkMarkerNode extends ElementNode {
+    static getType() {
+        return "link-marker";
+    }
+    static clone(node: LinkMarkerNode) {
+        return new LinkMarkerNode(node.__key);
+    }
+    isInline(): boolean {
+        return true;
+    }
+    createDOM(): HTMLElement {
+        const element = document.createElement("span");
+        element.classList.add("link-marker");
+        return element;
+    }
+    updateDOM(): boolean {
+        return false;
+    }
+    exportJSON(): SerializedElementNode {
+        return {
+            ...super.exportJSON(),
+            type: "link-marker",
+        };
+    }
+    static importJSON() {
+        return $createLinkMarkerNode();
+    }
+}
+
+export function $createLinkMarkerNode() {
+    return new LinkMarkerNode();
+}
+
+export function $isLinkMarkerNode(node?: LexicalNode | null): node is LinkMarkerNode {
+    return node instanceof LinkMarkerNode;
+}
+
+export class LinkUrlNode extends ElementNode {
+    static getType() {
+        return "link-url";
+    }
+    static clone(node: LinkUrlNode) {
+        return new LinkUrlNode(node.__key);
+    }
+    isInline(): boolean {
+        return true;
+    }
+    createDOM(): HTMLElement {
+        const element = document.createElement("span");
+        element.classList.add("link-url");
+        return element;
+    }
+    updateDOM(): boolean {
+        return false;
+    }
+    exportJSON(): SerializedElementNode {
+        return {
+            ...super.exportJSON(),
+            type: "link-url",
+        };
+    }
+    static importJSON() {
+        return $createLinkUrlNode();
+    }
+}
+
+export function $createLinkUrlNode() {
+    return new LinkUrlNode();
+}
+
+export function $isLinkUrlNode(node?: LexicalNode | null): node is LinkUrlNode {
+    return node instanceof LinkUrlNode;
+}
+
+type SerializedLinkIconNode = Spread<SerializedLexicalNode, {
+    url: string;
+}>;
+
+export class LinkIconNode extends DecoratorNode<ReactNode> {
+    __url: string;
+
+    getUrl() {
+        const self = this.getLatest();
+        return self.__url;
+    }
+
+    static getType() {
+        return "link-icon";
+    }
+    static clone(node: LinkIconNode) {
+        return new LinkIconNode(node.__url, node.__key);
+    }
+    constructor(url: string, key?: string) {
+        super(key);
+        this.__url = url;
+    }
+    createDOM(): HTMLElement {
+        return document.createElement("span");
+    }
+    updateDOM(): boolean {
+        return false;
+    }
+    decorate(): ReactNode {
+        return (<LinkIcon url={this.__url} />);
+    }
+    exportJSON(): SerializedLinkIconNode {
+        return {
+            ...super.exportJSON(),
+            type: "link-icon",
+            url: this.__url,
+        }
+    }
+    static importJSON(serializedNode: SerializedLinkIconNode) {
+        return $createLinkIconNode(serializedNode.url);
+    }
+}
+
+export function $createLinkIconNode(url: string) {
+    return new LinkIconNode(url);
+}
+
+export function $isLinkIconNode(node?: LexicalNode | null): node is LinkIconNode {
+    return node instanceof LinkIconNode;
+}
+
diff --git a/src/editor/nodes/task_node.tsx b/src/editor/nodes/task_node.tsx
new file mode 100644
index 0000000..e5d141d
--- /dev/null
+++ b/src/editor/nodes/task_node.tsx
@@ -0,0 +1,159 @@
+import { $createParagraphNode, DecoratorNode, ElementNode, type LexicalNode, ParagraphNode, type RangeSelection, type SerializedElementNode, type SerializedLexicalNode, type Spread } from "lexical";
+import { FocusableNode } from "./focusable_node";
+import type { ReactNode } from "react";
+import { type TaskType, taskTypes } from "../plugins/task_plugin";
+import { TaskIcon } from "~/components/editor/task_icon";
+import type { TaskLabel } from "../serialized_editor_content";
+
+type SerializedTaskNode = Spread<SerializedElementNode, {
+    taskType: TaskLabel;
+}>;
+
+export class TaskNode extends FocusableNode {
+    __taskType: TaskType;
+
+    getTaskType() {
+        const self = this.getLatest();
+        return self.__taskType;
+    }
+
+    static getType() {
+        return "task";
+    }
+    static clone(node: TaskNode) {
+        return new TaskNode(node.__taskType, node.__key, node.__hasFocus);
+    }
+
+    constructor(taskType: TaskType, key?: string, hasFocus?: boolean) {
+        super(key, hasFocus);
+        this.__taskType = taskType;
+    }
+
+    createDOM(): HTMLElement {
+        const element = super.createDOM();
+        element.classList.add("task");
+        element.setAttribute("data-task-type", this.__taskType.label);
+        return element;
+    }
+    updateDOM(old: TaskNode, dom: HTMLElement): boolean {
+        return super.updateDOM(old, dom);
+    }
+    insertNewAfter(_selection: RangeSelection, restoreSelection?: boolean): ParagraphNode {
+        const newElement = $createParagraphNode();
+        this.insertAfter(newElement, restoreSelection);
+        return newElement;
+    }
+    exportJSON(): SerializedTaskNode {
+        return {
+            ...super.exportJSON(),
+            type: "task",
+            taskType: this.__taskType.label,
+        };
+    }
+    static importJSON(serializedTaskNode: SerializedTaskNode) {
+        const taskType = taskTypes.find(it => it.label == serializedTaskNode.taskType);
+        // FIXME: Deal with task type not existing gracefully. Uncreate the node?
+        return $createTaskNode(taskType!);
+    }
+}
+
+export function $createTaskNode(taskType: TaskType) {
+    return new TaskNode(taskType);
+}
+
+export function $isTaskNode(node?: LexicalNode | null): node is TaskNode {
+    return node instanceof TaskNode;
+}
+
+export class TaskMarkerNode extends ElementNode {
+    static getType() {
+        return "task-marker";
+    }
+    static clone(node: TaskMarkerNode) {
+        return new TaskMarkerNode(node.__key);
+    }
+    isInline(): boolean {
+        return true;
+    }
+    createDOM(): HTMLElement {
+        const element = document.createElement("span");
+        element.classList.add("task-marker");
+        return element;
+    }
+    updateDOM(): boolean {
+        return false;
+    }
+    exportJSON() {
+        return {
+            ...super.exportJSON(),
+            type: "task-marker",
+        };
+    }
+    static importJSON() {
+        return $createTaskMarkerNode();
+    }
+}
+
+export function $createTaskMarkerNode() {
+    return new TaskMarkerNode();
+}
+
+export function $isTaskMarkerNode(node?: LexicalNode | null): node is TaskMarkerNode {
+    return node instanceof TaskMarkerNode;
+}
+
+
+type SerializedTaskIconNode = Spread<SerializedLexicalNode, {
+    taskType: TaskLabel;
+}>;
+
+export class TaskIconNode extends DecoratorNode<ReactNode> {
+    __taskType: TaskType;
+
+    getTaskType() {
+        const self = this.getLatest();
+        return self.__taskType;
+    }
+
+    static getType() {
+        return "task-icon";
+    }
+    static clone(node: TaskIconNode) {
+        return new TaskIconNode(node.__taskType, node.__key);
+    }
+
+    constructor(taskType: TaskType, key?: string) {
+        super(key);
+        this.__taskType = taskType;
+    }
+
+    createDOM(): HTMLElement {
+        return document.createElement("span");
+    }
+    updateDOM(): boolean {
+        return false;
+    }
+    decorate(): ReactNode {
+        return (<TaskIcon icon={this.__taskType.icon?.path} />);
+    }
+    exportJSON(): SerializedTaskIconNode {
+        return {
+            ...super.exportJSON(),
+            type: "task-icon",
+            taskType: this.__taskType.label,
+        };
+    }
+    static importJSON(serializedTaskIconNode: SerializedTaskIconNode) {
+        const taskType = taskTypes.find(it => it.label == serializedTaskIconNode.taskType);
+        // FIXME: Deal with task type not existing gracefully. Uncreate the node?
+        return $createTaskIconNode(taskType!);
+    }
+}
+
+export function $createTaskIconNode(taskType: TaskType) {
+    return new TaskIconNode(taskType);
+}
+
+export function $isTaskIconNode(node?: LexicalNode | null): node is TaskIconNode {
+    return node instanceof TaskIconNode;
+}
diff --git a/src/editor/nodes/term_node.tsx b/src/editor/nodes/term_node.tsx
new file mode 100644
index 0000000..ecff609
--- /dev/null
+++ b/src/editor/nodes/term_node.tsx
@@ -0,0 +1,142 @@
+import { DecoratorNode, ElementNode, type LexicalNode, type SerializedElementNode, type SerializedLexicalNode, type Spread } from "lexical";
+import { FocusableNode } from "./focusable_node";
+import type { ReactNode } from "react";
+import { TermIcon } from "~/components/editor/term_icon";
+
+type SerializedTermNode = Spread<SerializedElementNode, {
+    term: string;
+}>;
+
+export class TermNode extends FocusableNode {
+    __term: string;
+
+    getTerm() {
+        const self = this.getLatest();
+        return self.__term;
+    }
+    static getType() {
+        return "term";
+    }
+    static clone(node: TermNode) {
+        return new TermNode(node.__term, node.__key, node.__hasFocus);
+    }
+    constructor(term: string, key?: string, hasFocus?: boolean) {
+        super(key, hasFocus);
+        this.__term = term;
+    }
+    isInline() {
+        return true;
+    }
+    createDOM() {
+        const element = super.createDOM();
+        element.classList.add("term");
+        return element;
+    }
+    updateDOM(old: TermNode, dom: HTMLElement) {
+        return super.updateDOM(old, dom);
+    }
+    exportJSON(): SerializedTermNode {
+        return {
+            ...super.exportJSON(),
+            type: "term",
+            term: this.__term,
+        };
+    }
+    static importJSON(serializedNode: SerializedTermNode) {
+        return $createTermNode(serializedNode.term);
+    }
+}
+
+export function $createTermNode(term: string) {
+    return new TermNode(term);
+}
+export function $isTermNode(node?: LexicalNode | null): node is TermNode {
+    return node instanceof TermNode;
+}
+
+export class TermMarkerNode extends ElementNode {
+    static getType() {
+        return "term-marker";
+    }
+    static clone(node: TermMarkerNode) {
+        return new TermMarkerNode(node.__key);
+    }
+    isInline() {
+        return true;
+    }
+    createDOM() {
+        const element = document.createElement("span");
+        element.classList.add("term-marker");
+        return element;
+    }
+    updateDOM() {
+        return false;
+    }
+    exportJSON() {
+        return {
+            ...super.exportJSON(),
+            type: "term-marker",
+        };
+    }
+    static importJSON() {
+        return $createTermMarkerNode();
+    }
+}
+
+export function $createTermMarkerNode() {
+    return new TermMarkerNode();
+}
+export function $isTermMarkerNode(node?: LexicalNode | null): node is TermMarkerNode {
+    return node instanceof TermMarkerNode;
+}
+
+type SerializedTermIconNode = Spread<SerializedLexicalNode, {
+    term: string;
+}>;
+
+export class TermIconNode extends DecoratorNode<ReactNode> {
+    __term: string;
+
+    getTerm() {
+        const self = this.getLatest();
+        return self.__term;
+    }
+
+    static getType() {
+        return "term-icon";
+    }
+    static clone(node: TermIconNode) {
+        return new TermIconNode(node.__term, node.__key);
+    }
+    constructor(term: string, key?: string) {
+        super(key);
+        this.__term = term;
+    }
+    createDOM() {
+        return document.createElement("span");
+    }
+    updateDOM() {
+        return false;
+    }
+    decorate(): ReactNode {
+        return (<TermIcon term={this.__term} />);
+    }
+    exportJSON(): SerializedTermIconNode {
+        return {
+            ...super.exportJSON(),
+            type: "term-icon",
+            term: this.__term,
+        };
+    }
+    static importJSON(serializedNode: SerializedTermIconNode) {
+        return $createTermIconNode(serializedNode.term);
+    }
+}
+
+export function $createTermIconNode(term: string) {
+    return new TermIconNode(term);
+}
+export function $isTermIconNode(node?: LexicalNode | null): node is TermIconNode {
+    return node instanceof TermIconNode;
+}
+
diff --git a/src/editor/plugins/formatted_text_plugin.tsx b/src/editor/plugins/formatted_text_plugin.tsx
new file mode 100644
index 0000000..ff24e52
--- /dev/null
+++ b/src/editor/plugins/formatted_text_plugin.tsx
@@ -0,0 +1,102 @@
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import { mergeRegister } from "@lexical/utils";
+import { type LexicalEditor, SELECTION_CHANGE_COMMAND, TextNode } from "lexical";
+import { useEffect } from "react";
+import { $createFormattedTextMarkerNode, $createFormattedTextNode, $isFormattedTextMarkerNode, $isFormattedTextNode, FormattedTextMarkerNode, FormattedTextNode } from "../nodes/formatted_text";
+import { type TextFormat, textFormats } from "../serialized_editor_content";
+
+export function FormattedTextPlugin() {
+    const [editor] = useLexicalComposerContext();
+
+    useEffect(() => mergeRegister(
+        // Create formatted text
+        ...Object.keys(textFormats).map((format) =>
+            editor.registerNodeTransform(TextNode, createFormattedTextNode(format as TextFormat, editor))
+        ),
+        // Remove formatted text marker nodes when not inside formatted text node
+        editor.registerNodeTransform(FormattedTextMarkerNode, (node) => {
+            const parent = node.getParent();
+            if (!parent) return;
+
+            if ($isFormattedTextNode(parent)) {
+                const format = parent.getStyle();
+                const markerChars = textFormats[format];
+                if (node.getTextContent() === markerChars) return;
+            }
+
+            node.getChildren().reverse().forEach((child) => node.insertAfter(child));
+            node.remove();
+        }),
+        // Remove formatted text nodes without matching markers
+        editor.registerNodeTransform(FormattedTextNode, (node) => {
+            const format = node.getStyle();
+            const markerChars = textFormats[format];
+
+            const firstMarker = node.getFirstChild();
+            const lastMarker = node.getLastChild();
+            if (
+                !$isFormattedTextMarkerNode(firstMarker) ||
+                !$isFormattedTextMarkerNode(lastMarker)
+            ) {
+                node.getChildren().reverse().forEach((child) => node.insertAfter(child));
+                node.remove();
+                return;
+            }
+            const firstMarkerContent = firstMarker.getTextContent();
+            const lastMarkerContent = lastMarker.getTextContent();
+
+            if (firstMarkerContent !== markerChars || lastMarkerContent !== markerChars) {
+                node.getChildren().reverse().forEach((child) => node.insertAfter(child));
+                node.remove();
+                return;
+            }
+        }),
+        // Remove formatted text nodes without content
+        editor.registerNodeTransform(FormattedTextNode, (node) => {
+            const format = node.getStyle();
+            const formattedChars = textFormats[format];
+
+            const content = node.getTextContent();
+            if (content !== formattedChars + formattedChars) return;
+
+            node.getChildren().reverse().forEach((child) => node.insertAfter(child));
+            node.remove();
+        }),
+    ));
+
+    return null;
+}
+
+const createFormattedTextNode = (format: TextFormat, editor: LexicalEditor) => (node: TextNode) => {
+    const markerChars = textFormats[format];
+    const markerCharsLen = markerChars.length;
+
+    const content = node.getTextContent();
+
+    // TODO: search in all text nodes within the parent block node
+    const start = content.indexOf(markerChars);
+    if (start === -1) return;
+    const end = content.indexOf(markerChars, start + markerCharsLen);
+    if (end === -1) return;
+    if (start === end - markerCharsLen) return;
+
+    const startIndex = start > 0 ? 1 : 0;
+    const contentIndex = startIndex + 1;
+    const endIndex = contentIndex + 1;
+
+    const textNodes = node.splitText(start, start + markerCharsLen, end, end + markerCharsLen);
+
+    const formattedTextNode = $createFormattedTextNode(format);
+    textNodes[startIndex]!.insertBefore(formattedTextNode);
+
+
+    const startMarker = $createFormattedTextMarkerNode();
+    startMarker.append(textNodes[startIndex]!);
+
+    const endMarker = $createFormattedTextMarkerNode();
+    endMarker.append(textNodes[endIndex]!);
+
+    formattedTextNode.append(startMarker, textNodes[contentIndex]!, endMarker);
+
+    editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
+}
diff --git a/src/editor/plugins/header_plugin.tsx b/src/editor/plugins/header_plugin.tsx
new file mode 100644
index 0000000..d4afdc4
--- /dev/null
+++ b/src/editor/plugins/header_plugin.tsx
@@ -0,0 +1,78 @@
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import { $createParagraphNode, $isParagraphNode, $isTextNode, SELECTION_CHANGE_COMMAND, TextNode } from "lexical";
+import { useEffect } from "react";
+import { $createHeaderMarkerNode, $createHeaderNode, $isHeaderMarkerNode, $isHeaderNode, HeaderMarkerNode, HeaderNode } from "../nodes/header_node";
+import { mergeRegister } from "@lexical/utils";
+
+const HEADER_REGEX = /^#+ /;
+
+export function HeaderPlugin() {
+    const [editor] = useLexicalComposerContext();
+
+    useEffect(() => mergeRegister(
+        // Create a header node if the text node matches the HEADER_REGEX
+        editor.registerNodeTransform(TextNode, (textNode) => {
+            const prevNode = textNode.getPreviousSibling();
+            if (prevNode) return;
+            const paragraphNode = textNode.getParent();
+            if (!$isParagraphNode(paragraphNode)) return;
+
+            const content = textNode.getTextContent();
+            const regexMatch = content.match(HEADER_REGEX);
+            if (!regexMatch) return;
+
+            const children = paragraphNode.getChildren();
+
+            const firstTextNode = children[0];
+            if (!$isTextNode(firstTextNode)) return;
+
+            const markerLength = regexMatch[0].length;
+            const textNodes = firstTextNode.splitText(markerLength);
+
+            const headerMarkerContent = textNodes[0];
+            if (!headerMarkerContent) return;
+
+            const headerNode = $createHeaderNode(markerLength - 1);
+            const headerMarkerNode = $createHeaderMarkerNode();
+
+            headerMarkerNode.append(headerMarkerContent);
+
+            headerNode.append(headerMarkerNode);
+            headerNode.append(...textNodes.slice(1));
+            headerNode.append(...children.slice(1));
+
+            paragraphNode.replace(headerNode, true);
+
+            editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
+        }),
+        // Remove headers if they don't match the HEADER_REGEX
+        editor.registerNodeTransform(HeaderNode, (headerNode) => {
+            const content = headerNode.getTextContent();
+            if (content.match(HEADER_REGEX)) return;
+            headerNode.replace($createParagraphNode(), true);
+        }),
+        // Remove header markers if they don't match the HEADER_REGEX
+        editor.registerNodeTransform(HeaderMarkerNode, (node) => {
+            const headerNode = node.getParent();
+            const content = node.getTextContent();
+            if ($isHeaderNode(headerNode) &&
+                content.match(HEADER_REGEX) &&
+                content.length - 1 == headerNode.getLevel()) {
+                return;
+            }
+
+            node.getChildren().reverse().forEach(child => node.insertAfter(child));
+            node.remove();
+        }),
+        // Remove header nodes without a header marker
+        editor.registerNodeTransform(HeaderNode, (node) => {
+            const children = node.getChildren();
+            const headerMarker = children[0];
+            if ($isHeaderMarkerNode(headerMarker)) return;
+
+            node.replace($createParagraphNode(), true);
+        }),
+    ));
+
+    return null;
+}
diff --git a/src/editor/plugins/link_plugin.tsx b/src/editor/plugins/link_plugin.tsx
new file mode 100644
index 0000000..91df579
--- /dev/null
+++ b/src/editor/plugins/link_plugin.tsx
@@ -0,0 +1,136 @@
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import { mergeRegister } from "@lexical/utils";
+import { SELECTION_CHANGE_COMMAND, TextNode } from "lexical";
+import { useEffect } from "react";
+import { $createLinkIconNode, $createLinkMarkerNode, $createLinkNode, $createLinkUrlNode, $isLinkIconNode, $isLinkMarkerNode, $isLinkNode, $isLinkUrlNode, LinkIconNode, LinkMarkerNode, LinkNode, LinkUrlNode } from "../nodes/link_node";
+
+const LINK_REGEX = /\[(.+)\]\((.*)\)/;
+
+export function LinkPlugin() {
+    const [editor] = useLexicalComposerContext();
+
+    useEffect(() => mergeRegister(
+        // Create link node
+        editor.registerNodeTransform(TextNode, (node) => {
+            const content = node.getTextContent();
+            const matches = content.match(LINK_REGEX);
+            if (!matches) return;
+
+            const labelLength = matches[1]!.length;
+            const urlLength = matches[2]!.length;
+
+            // start
+            //  | labelStart
+            //  | | labelEnd
+            //  | |     | middle
+            //  | |     | | urlStart
+            //  | |     | | | urlEnd
+            //  | |     | | |   | end   Index of the segment
+            //  | |     | | |   | |     in the textNodes array
+            // 0|1|2    |3|4|5  |6|7 <--|
+            //  |[|label|]|(|url|)|
+            const start = content.indexOf(matches[0]);
+            const labelStart = start + 1;
+            const labelEnd = labelStart + labelLength;
+            const middle = labelEnd + 1;
+            const urlStart = middle + 1;
+            const urlEnd = urlStart + urlLength;
+            const end = urlEnd + 1;
+
+            const textNodes = node.splitText(start, labelStart, labelEnd, middle, urlStart, urlEnd, end);
+            const labelStartIndex = start > 0 ? 1 : 0;
+            const labelIndex = labelStartIndex + 1;
+            const labelEndIndex = labelIndex + 1;
+            const urlStartIndex = labelEndIndex + 1;
+            const urlIndex = urlStartIndex + 1;
+            const urlEndIndex = urlIndex + 1;
+
+            const url = matches[2]!;
+
+            const linkNode = $createLinkNode(url);
+            textNodes[labelStartIndex]!.insertBefore(linkNode);
+
+            const labelStartNode = $createLinkMarkerNode();
+            labelStartNode.append(textNodes[labelStartIndex]!);
+            const labelEndNode = $createLinkMarkerNode();
+            labelEndNode.append(textNodes[labelEndIndex]!);
+            const urlStartNode = $createLinkMarkerNode();
+            urlStartNode.append(textNodes[urlStartIndex]!);
+            const urlNode = $createLinkUrlNode();
+            urlNode.append(textNodes[urlIndex]!);
+            const urlEndNode = $createLinkMarkerNode();
+            urlEndNode.append(textNodes[urlEndIndex]!);
+            const iconNode = $createLinkIconNode(url);
+
+            linkNode.append(labelStartNode, textNodes[labelIndex]!, labelEndNode, urlStartNode, urlNode, urlEndNode, iconNode);
+
+            editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
+        }),
+        // Remove dangling link marker nodes and verify their content
+        editor.registerNodeTransform(LinkMarkerNode, (node) => {
+            const linkNode = node.getParent();
+            if ($isLinkNode(linkNode)) {
+                const content = node.getTextContent();
+                const index = node.getIndexWithinParent();
+                const parentChildCount = linkNode.getChildren().length;
+
+                if (index === 0 && content === "[") return;
+                if (index === parentChildCount - 5 && content === "]") return;
+                if (index === parentChildCount - 4 && content === "(") return;
+                if (index === parentChildCount - 2 && content === ")") return;
+            }
+
+            node.getChildren().reverse().forEach((child) => node.insertAfter(child));
+            node.remove();
+        }),
+        // Remove dangling link url nodes
+        editor.registerNodeTransform(LinkUrlNode, (node) => {
+            const linkNode = node.getParent();
+            if ($isLinkNode(linkNode)) {
+                const url = linkNode.getUrl();
+                const content = node.getTextContent();
+                if (url === content) return;
+            }
+
+            node.getChildren().reverse().forEach((child) => node.insertAfter(child));
+            node.remove();
+        }),
+        // Remove dangling link icon nodes
+        editor.registerNodeTransform(LinkIconNode, (node) => {
+            const linkNode = node.getParent();
+            if ($isLinkNode(linkNode)) {
+                const url = linkNode.getUrl();
+                const iconUrl = node.getUrl();
+                if (url === iconUrl) return;
+            }
+
+            node.remove();
+        }),
+        // Remove links with missing markers
+        editor.registerNodeTransform(LinkNode, (node) => {
+            const children = node.getChildren();
+            const startLabel = children.at(0);
+            const endLabel = children.at(-5);
+            const startUrl = children.at(-4);
+            const url = children.at(-3);
+            const endUrl = children.at(-2);
+            const icon = children.at(-1);
+
+            if (
+                $isLinkMarkerNode(startLabel) &&
+                $isLinkMarkerNode(endLabel) &&
+                $isLinkMarkerNode(startUrl) &&
+                $isLinkUrlNode(url) &&
+                $isLinkMarkerNode(endUrl) &&
+                $isLinkIconNode(icon)
+            ) {
+                return;
+            }
+
+            children.reverse().forEach((child) => node.insertAfter(child));
+            node.remove();
+        }),
+    ))
+
+    return null;
+}
diff --git a/src/editor/plugins/paragraph_plugin.tsx b/src/editor/plugins/paragraph_plugin.tsx
new file mode 100644
index 0000000..2d72068
--- /dev/null
+++ b/src/editor/plugins/paragraph_plugin.tsx
@@ -0,0 +1,48 @@
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import { CAN_USE_BEFORE_INPUT, IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI, mergeRegister } from "@lexical/utils";
+import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_NORMAL, INSERT_PARAGRAPH_COMMAND, KEY_ENTER_COMMAND } from "lexical";
+import { useEffect } from "react";
+
+export function ParagraphPlugin() {
+    const [editor] = useLexicalComposerContext();
+
+    useEffect(() => mergeRegister(
+        editor.registerCommand(INSERT_PARAGRAPH_COMMAND, () => {
+            const selection = $getSelection();
+            if (!$isRangeSelection(selection)) {
+                return false;
+            }
+            selection.insertParagraph();
+            return true;
+        }, COMMAND_PRIORITY_NORMAL),
+        editor.registerCommand<KeyboardEvent | null>(
+            KEY_ENTER_COMMAND,
+            (event) => {
+                const selection = $getSelection();
+                if (!$isRangeSelection(selection)) {
+                    return false;
+                }
+                if (event !== null) {
+                    // If we have beforeinput, then we can avoid blocking
+                    // the default behavior. This ensures that the iOS can
+                    // intercept that we're actually inserting a paragraph,
+                    // and autocomplete, autocapitalize etc work as intended.
+                    // This can also cause a strange performance issue in
+                    // Safari, where there is a noticeable pause due to
+                    // preventing the key down of enter.
+                    if (
+                        (IS_IOS || IS_SAFARI || IS_APPLE_WEBKIT) &&
+                        CAN_USE_BEFORE_INPUT
+                    ) {
+                        return false;
+                    }
+                    event.preventDefault();
+                }
+                return editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
+            },
+            COMMAND_PRIORITY_NORMAL,
+        ),
+    ))
+
+    return null;
+}
diff --git a/src/editor/plugins/selection_plugin.tsx b/src/editor/plugins/selection_plugin.tsx
new file mode 100644
index 0000000..a87aaed
--- /dev/null
+++ b/src/editor/plugins/selection_plugin.tsx
@@ -0,0 +1,49 @@
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import { mergeRegister } from "@lexical/utils";
+import { $getNodeByKey, $getSelection, COMMAND_PRIORITY_EDITOR, type LexicalNode, SELECTION_CHANGE_COMMAND } from "lexical";
+import { useEffect, useState } from "react";
+import { $isFocusableNode } from "../nodes/focusable_node";
+
+export function SelectionPlugin() {
+    const [editor] = useLexicalComposerContext();
+    const [previousSelectedNodes, setPreviousSelectedNodes] = useState<string[]>([]);
+
+    useEffect(() => mergeRegister(
+        editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
+            const selection = $getSelection();
+            const nodes = selection?.getNodes() || [];
+
+            previousSelectedNodes
+                .map((key) => $getNodeByKey(key))
+                .forEach(markTreeAsUnfocused);
+
+            nodes.forEach(markTreeAsFocused);
+            setPreviousSelectedNodes(nodes.map(n => n.getKey()));
+
+            return false;
+        }, COMMAND_PRIORITY_EDITOR),
+    ));
+
+    return null;
+}
+
+
+function markTreeAsFocused(node: LexicalNode | null) {
+    if (!node) return;
+
+    if ($isFocusableNode(node)) {
+        node.setFocus(true);
+    }
+
+    markTreeAsFocused(node.getParent());
+}
+
+function markTreeAsUnfocused(node: LexicalNode | null) {
+    if (!node) return;
+
+    if ($isFocusableNode(node)) {
+        node.setFocus(false);
+    }
+
+    markTreeAsUnfocused(node.getParent());
+}
diff --git a/src/editor/plugins/task_plugin.tsx b/src/editor/plugins/task_plugin.tsx
new file mode 100644
index 0000000..58e823d
--- /dev/null
+++ b/src/editor/plugins/task_plugin.tsx
@@ -0,0 +1,116 @@
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import { mergeRegister } from "@lexical/utils";
+import { $createParagraphNode, $isParagraphNode, $isTextNode, type LexicalEditor, SELECTION_CHANGE_COMMAND, TextNode } from "lexical";
+import { useEffect } from "react";
+import { $createTaskMarkerNode, $createTaskNode, $createTaskIconNode, TaskMarkerNode, $isTaskNode, TaskIconNode, TaskNode, $isTaskIconNode, $isTaskMarkerNode } from "../nodes/task_node";
+import { type Icon, icons } from "~/lib/icon";
+import { type TaskLabel } from "../serialized_editor_content";
+
+export type TaskType = {
+    label: TaskLabel
+    icon?: Icon
+}
+
+export const taskTypes: TaskType[] = [
+    {
+        label: "TODO",
+        icon: undefined,
+    },
+    {
+        label: "DOING",
+        icon: icons.minus,
+    },
+    {
+        label: "DONE",
+        icon: icons.check,
+    },
+    {
+        label: "IDEA",
+        icon: icons.lightbulb,
+    },
+    {
+        label: "DEADLINE",
+        icon: icons.exclamation,
+    },
+]
+
+export function TaskPlugin() {
+    const [editor] = useLexicalComposerContext();
+
+    useEffect(() => mergeRegister(
+        // Create a task node if the text node matches the TASK_REGEX
+        ...taskTypes.map(({ label }) =>
+            editor.registerNodeTransform(TextNode, createTaskNode(label, editor))
+        ),
+        // Remove task marker nodes not inside a task node or with the wrong content
+        editor.registerNodeTransform(TaskMarkerNode, (node) => {
+            const taskNode = node.getParent();
+            if ($isTaskNode(taskNode)) {
+                const taskType = taskNode.getTaskType();
+                const content = node.getTextContent();
+                if (content === taskType.label) return;
+            }
+
+            node.getChildren().reverse().forEach(child => node.insertAfter(child));
+            node.remove();
+        }),
+        // Remove task icon nodes not inside a task node
+        editor.registerNodeTransform(TaskIconNode, (node) => {
+            const taskNode = node.getParent();
+            if ($isTaskNode(taskNode)) {
+                const taskType = taskNode.getTaskType();
+                if (node.getTaskType() === taskType) return;
+            }
+
+            node.remove();
+        }),
+        // Remove task nodes without an icon or marker
+        editor.registerNodeTransform(TaskNode, (node) => {
+            const iconNode = node.getChildAtIndex(0);
+            const markerNode = node.getChildAtIndex(1);
+
+            if ($isTaskIconNode(iconNode) && $isTaskMarkerNode(markerNode)) return;
+
+            node.replace($createParagraphNode(), true);
+        }),
+    ));
+
+    return null;
+}
+
+const createTaskNode = (label: TaskLabel, editor: LexicalEditor) => (node: TextNode) => {
+    const prevNode = node.getPreviousSibling();
+    if (prevNode) return;
+    const paragraphNode = node.getParent();
+    if (!$isParagraphNode(paragraphNode)) return;
+
+    const content = node.getTextContent();
+    if (!content.startsWith(label + " ")) return;
+
+    const children = paragraphNode.getChildren();
+
+    const firstTextNode = children[0];
+    if (!$isTextNode(firstTextNode)) return;
+
+    const textNodes = firstTextNode.splitText(label.length);
+
+    const todoMarkerContent = textNodes[0];
+    if (!todoMarkerContent) return;
+
+    const taskType = taskTypes.find(it => it.label == label);
+    if (!taskType) return;
+
+    const taskNode = $createTaskNode(taskType);
+    const taskIconNode = $createTaskIconNode(taskType);
+    const taskMarkerNode = $createTaskMarkerNode();
+
+    taskMarkerNode.append(todoMarkerContent);
+
+    taskNode.append(taskIconNode);
+    taskNode.append(taskMarkerNode);
+    taskNode.append(...textNodes.slice(1));
+    taskNode.append(...children.slice(1));
+
+    paragraphNode.replace(taskNode, true);
+    editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
+}
diff --git a/src/editor/plugins/term_plugin.tsx b/src/editor/plugins/term_plugin.tsx
new file mode 100644
index 0000000..7b75ea8
--- /dev/null
+++ b/src/editor/plugins/term_plugin.tsx
@@ -0,0 +1,83 @@
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import { mergeRegister } from "@lexical/utils";
+import { SELECTION_CHANGE_COMMAND, TextNode } from "lexical";
+import { useEffect } from "react";
+import { $createTermIconNode, $createTermMarkerNode, $createTermNode, $isTermIconNode, $isTermMarkerNode, $isTermNode, TermIconNode, TermMarkerNode, TermNode } from "../nodes/term_node";
+
+const TERM_REGEX = /\[\[(.+)\]\]/;
+
+export function TermPlugin() {
+    const [editor] = useLexicalComposerContext();
+
+    useEffect(() => mergeRegister(
+        editor.registerNodeTransform(TextNode, (node) => {
+            const content = node.getTextContent();
+            const matches = content.match(TERM_REGEX);
+            if (!matches) return;
+
+            const term = matches[1]!;
+
+            const start = content.indexOf(matches[0]);
+            const end = start + matches[0].length;
+
+            const startIndex = start > 0 ? 1 : 0;
+            const contentIndex = startIndex + 1;
+            const endIndex = contentIndex + 1;
+
+            const textNodes = node.splitText(start, start + 2, end - 2, end);
+
+            const termNode = $createTermNode(term);
+            textNodes[startIndex]!.insertBefore(termNode);
+
+            const iconNode = $createTermIconNode(term);
+            termNode.append(iconNode);
+            const startMarkerNode = $createTermMarkerNode();
+            startMarkerNode.append(textNodes[startIndex]!);
+            const endMarkerNode = $createTermMarkerNode();
+            endMarkerNode.append(textNodes[endIndex]!);
+
+            termNode.append(startMarkerNode, textNodes[contentIndex]!, endMarkerNode);
+
+            editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
+        }),
+        editor.registerNodeTransform(TermMarkerNode, (node) => {
+            const termNode = node.getParent();
+            if ($isTermNode(termNode)) return;
+
+            node.getChildren().reverse().forEach((child) => node.insertAfter(child));
+            node.remove();
+        }),
+        editor.registerNodeTransform(TermIconNode, (node) => {
+            const termNode = node.getParent();
+            if ($isTermNode(termNode)) return;
+
+            node.remove();
+        }),
+        editor.registerNodeTransform(TermNode, (node) => {
+            const children = node.getChildren();
+            const content = node.getTextContent();
+            const term = node.getTerm();
+
+            const iconNode = children.at(0);
+            const startMarkerNode = children.at(1);
+            const endMarkerNode = children.at(-1);
+
+            if ($isTermIconNode(iconNode) &&
+                $isTermMarkerNode(startMarkerNode) &&
+                $isTermMarkerNode(endMarkerNode) &&
+                iconNode.getTerm() === term &&
+                startMarkerNode.getTextContent() === "[[" &&
+                endMarkerNode.getTextContent() === "]]" &&
+                content === `[[${term}]]`
+            ) {
+                return
+            }
+
+
+            node.getChildren().reverse().forEach((child) => node.insertAfter(child));
+            node.remove();
+        }),
+    ));
+
+    return null;
+}
diff --git a/src/editor/serialized_editor_content.ts b/src/editor/serialized_editor_content.ts
new file mode 100644
index 0000000..517a0a2
--- /dev/null
+++ b/src/editor/serialized_editor_content.ts
@@ -0,0 +1,263 @@
+// TODO: This needs to be removed, but some exported types need to be moved first
+import { $getRoot, $isLineBreakNode, $isParagraphNode, $isTextNode, type LexicalNode } from "lexical";
+import { $isHeaderNode } from "./nodes/header_node";
+import { $isTaskNode } from "./nodes/task_node";
+import { $isFormattedTextNode } from "./nodes/formatted_text";
+import { $isLinkNode } from "./nodes/link_node";
+import { $isTermNode } from "./nodes/term_node";
+
+export type SerializedEditorContent = {
+    content: SerializedBlockNode[];
+}
+
+type SerializedTextContainingNode =
+    SerializedTextNode
+    | SerializedFormattedTextNode
+    | SerializedLinebreakNode
+    | SerializedLinkNode
+    | SerializedTermNode;
+type SerializedBlockNode =
+    SerializedHeaderNode
+    | SerializedParagraphNode
+    | SerializedTaskNode;
+
+type SerializedTextNode = {
+    type: "text";
+    content: string;
+}
+
+type SerializedLinebreakNode = {
+    type: "linebreak";
+}
+
+type SerializedParagraphNode = {
+    type: "paragraph";
+    content: SerializedTextContainingNode[];
+}
+
+type SerializedHeaderNode = {
+    type: "header";
+    level: number;
+    content: SerializedTextContainingNode[];
+}
+
+export type TextFormat = "bold" | "italic" | "underline" | "strikethrough" | "code";
+export const textFormats: Record<TextFormat, string> = {
+    bold: "**",
+    italic: "//",
+    underline: "__",
+    strikethrough: "~~",
+    code: "`",
+};
+type SerializedFormattedTextNode = {
+    type: "formatted_text";
+    format: TextFormat;
+    content: SerializedTextContainingNode[];
+}
+
+type SerializedLinkNode = {
+    type: "link";
+    label: SerializedTextContainingNode[];
+    url: string;
+}
+
+type SerializedTermNode = {
+    type: "term";
+    term: string;
+}
+
+
+export type TaskLabel = "TODO" | "DOING" | "DONE" | "IDEA" | "DEADLINE";
+type SerializedTaskNode = {
+    type: "task";
+    label: TaskLabel;
+    content: SerializedTextContainingNode[];
+
+}
+
+export function $serializeEditorContent(): SerializedEditorContent {
+    return {
+        content: $getRoot().getChildren().map(serializeBlockNode),
+    };
+}
+
+function serializeBlockNode(node: LexicalNode): SerializedBlockNode {
+    switch (true) {
+        case $isParagraphNode(node): return {
+            type: "paragraph",
+            content: node.getChildren().map(serializeTextContainingNode),
+        }
+        case $isHeaderNode(node): return {
+            type: "header",
+            level: node.getLevel(),
+            // Slice(1) to remove the marker node
+            content: node.getChildren().slice(1).map(serializeTextContainingNode),
+        }
+        case $isTaskNode(node): return {
+            type: "task",
+            label: node.getTaskType().label,
+            // Slice(2) to remove the marker nodes
+            content: node.getChildren().slice(2).map(serializeTextContainingNode),
+        }
+        default: throw new Error(`Unknown block node type: ${node.getType()}`);
+    }
+}
+
+function serializeTextContainingNode(node: LexicalNode): SerializedTextContainingNode {
+    switch (true) {
+        case $isTextNode(node): return {
+            type: "text",
+            content: node.getTextContent(),
+        }
+        case $isFormattedTextNode(node): return {
+            type: "formatted_text",
+            format: node.getStyle(),
+            content: node.getChildren().slice(1, -1).map(serializeTextContainingNode),
+        }
+        case $isLineBreakNode(node): return {
+            type: "linebreak",
+        }
+        case $isLinkNode(node): return {
+            type: "link",
+            label: node.getChildren().slice(1, -5).map(serializeTextContainingNode),
+            url: node.getUrl(),
+
+        }
+        case $isTermNode(node): return {
+            type: "term",
+            term: node.getTerm(),
+        }
+        default: throw new Error(`Unknown text containing node type: ${node.getType()}`);
+    }
+}
+
+export function deserializeEditorContent(serialized: SerializedEditorContent) {
+    return {
+        root: {
+            type: "root",
+            children: serialized.content.map(deserializeBlockNode),
+        }
+    };
+}
+
+function deserializeBlockNode(serialized: SerializedBlockNode) {
+    switch (serialized.type) {
+        case "header": return {
+            type: "header",
+            level: serialized.level,
+            children: [
+                {
+                    type: "header-marker",
+                    children: [createTextNode(`${"#".repeat(serialized.level)} `)]
+                },
+                ...serialized.content.map(deserializeTextContainingNode)
+            ],
+        }
+        case "paragraph": return {
+            type: "paragraph",
+            children: [
+                ...serialized.content.map(deserializeTextContainingNode)
+            ],
+        }
+        case "task": return {
+            type: "task",
+            taskType: serialized.label,
+            children: [
+                {
+                    type: "task-icon",
+                    taskType: serialized.label,
+                },
+                {
+                    type: "task-marker",
+                    children: [createTextNode(`${serialized.label} `)]
+                },
+                ...serialized.content.map(deserializeTextContainingNode)
+            ],
+        }
+    }
+}
+
+function deserializeTextContainingNode(serialized: SerializedTextContainingNode): any {
+    switch (serialized.type) {
+        case "text": return createTextNode(serialized.content);
+        case "formatted_text": return {
+            type: "formatted-text",
+            style: serialized.format,
+            children: [
+                {
+                    type: "formatted-text-marker",
+                    children: [createTextNode(textFormats[serialized.format])]
+                },
+                ...serialized.content.map(deserializeTextContainingNode),
+                {
+                    type: "formatted-text-marker",
+                    children: [createTextNode(textFormats[serialized.format])]
+                },
+            ],
+        }
+        case "link": return {
+            type: "link",
+            children: [
+                {
+                    type: "link-marker",
+                    children: [createTextNode("[")],
+                },
+                ...serialized.label.map(deserializeTextContainingNode),
+                {
+                    type: "link-marker",
+                    children: [createTextNode("]")],
+                },
+                {
+                    type: "link-marker",
+                    children: [createTextNode("(")],
+                },
+                {
+                    type: "link-url",
+                    children: [createTextNode(serialized.url)],
+                },
+                {
+                    type: "link-marker",
+                    children: [createTextNode(")")],
+                },
+                {
+                    type: "link-icon",
+                    url: serialized.url,
+                }
+            ],
+        };
+        case "term": return {
+            type: "term",
+            term: serialized.term,
+            children: [
+                {
+                    type: "term-icon",
+                    term: serialized.term,
+                },
+                {
+                    type: "term-marker",
+                    children: [createTextNode("[[")]
+                },
+                createTextNode(serialized.term),
+                {
+                    type: "term-marker",
+                    children: [createTextNode("]]")]
+                },
+
+            ],
+        }
+        case "linebreak": return { type: "linebreak" };
+    }
+}
+
+function createTextNode(content: string) {
+    return {
+        type: "text",
+        text: content,
+
+        version: 1,
+        detail: 0,
+        format: 0,
+        mode: "normal",
+        style: "",
+    }
+}
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 0000000..d344e68
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1,8 @@
+import 'react';
+
+declare module 'react' {
+    // Allow CSS variables inside inline styles
+    interface CSSProperties {
+        [key: `--${string}`]: string | number
+    }
+}
diff --git a/src/hooks/use-metadata.tsx b/src/hooks/use-metadata.tsx
new file mode 100644
index 0000000..fb35ae8
--- /dev/null
+++ b/src/hooks/use-metadata.tsx
@@ -0,0 +1,65 @@
+import React, { useContext } from "react";
+import * as Y from "yjs";
+import { useYDoc } from "~/hooks/use-ydoc";
+import type { CollectionId, CollectionMetadata, CollectionsMetadata, NoteId, NoteMetadata, NotesMetadata } from "~/lib/metadata";
+import { useObserve } from "~/hooks/use-observe";
+
+const MetadataContext = React.createContext<Y.Doc | null>(null);
+
+export function MetadataProvider(props: { children: React.ReactNode }) {
+    const { ydoc } = useYDoc("metadata");
+
+    return (
+        <MetadataContext.Provider value={ydoc} >
+            {props.children}
+        </MetadataContext.Provider>
+    );
+}
+
+export function useCollections(): CollectionsMetadata {
+    const doc = useContext(MetadataContext)
+
+    if (!doc) {
+        throw new Error("useCollections must be used within a MetadataProvider")
+    }
+
+    const collections = doc.getArray("collections")
+    useObserve(collections, "deep")
+
+    return collections as any as CollectionsMetadata
+}
+
+export function useCollection(id: CollectionId | undefined): CollectionMetadata | undefined {
+    const collections = useCollections()
+    const collection = collections.toArray().find(it => it.get("id") == id)
+
+    return collection
+}
+
+
+export function useNotesMetadata(): NotesMetadata {
+    const doc = useContext(MetadataContext)
+
+    if (!doc) {
+        throw new Error("useNotes must be used within a MetadataProvider")
+    }
+
+    const notes = doc.getArray("notes")
+    useObserve(notes, "deep")
+
+    return notes as any as NotesMetadata
+}
+
+export function useCollectionNotesMetadata(collectionId: CollectionId | ""): NoteMetadata[] {
+    const notes = useNotesMetadata()
+
+    return notes.toArray().filter(it => it.get("collectionId") == collectionId)
+
+}
+
+export function useNoteMetadata(id: NoteId): NoteMetadata | undefined {
+    const notes = useNotesMetadata()
+    const note = notes.toArray().find(it => it.get("id") == id)
+
+    return note
+}
diff --git a/src/hooks/use-note.tsx b/src/hooks/use-note.tsx
new file mode 100644
index 0000000..4df18af
--- /dev/null
+++ b/src/hooks/use-note.tsx
@@ -0,0 +1,81 @@
+import React from "react";
+import * as Y from "yjs";
+import type { NoteId } from "~/lib/metadata";
+import { useYDoc } from "./use-ydoc";
+import type { WebsocketProvider } from "y-websocket";
+import type { IndexeddbPersistence } from "y-indexeddb";
+
+const NoteContext = React.createContext<{
+    doc: Y.Doc,
+    providers: {
+        indexeddb: IndexeddbPersistence,
+        websocket: WebsocketProvider,
+    }
+} | null>(null);
+
+export function NoteProvider(props: { children: React.ReactNode, id: NoteId }) {
+    const { ydoc, indexeddbProvider, websocketProvider } = useYDoc(`note-${props.id}`);
+
+    // If we don't have the provider yet, delay rendering until we do.
+    if (!indexeddbProvider || !websocketProvider) {
+        return null;
+    }
+
+    return (
+        <NoteContext.Provider value={{
+            doc: ydoc,
+            providers: {
+                indexeddb: indexeddbProvider,
+                websocket: websocketProvider,
+            }
+        }}>
+            {props.children}
+        </NoteContext.Provider>
+    );
+}
+
+export function useNoteDoc() {
+    const ctx = React.useContext(NoteContext);
+    if (!ctx) {
+        throw new Error("useNoteDoc must be used within a NoteProvider")
+    }
+    return ctx.doc;
+}
+
+export function useNoteMap(name: string) {
+    const ctx = React.useContext(NoteContext);
+    if (!ctx) {
+        throw new Error("useNoteMap must be used within a NoteProvider")
+    }
+    const map = ctx.doc.getMap(name);
+    return map;
+}
+
+export function useNoteArray(name: string) {
+    const ctx = React.useContext(NoteContext);
+    if (!ctx) {
+        throw new Error("useNoteArray must be used within a NoteProvider")
+    }
+    const array = ctx.doc.getArray(name);
+    return array;
+}
+
+export function useNoteProviders() {
+    const ctx = React.useContext(NoteContext);
+
+    if (!ctx) {
+        throw new Error("useNoteProviders must be used within a NoteProvider")
+    }
+
+    return ctx.providers;
+}
+
+export function useNoteAwareness() {
+    const ctx = React.useContext(NoteContext);
+    if (!ctx) {
+        throw new Error("useNoteAwareness must be used within a NoteProvider")
+    }
+
+    const awareness = ctx.providers.websocket.awareness;
+    return awareness;
+}
diff --git a/src/hooks/use-observe.ts b/src/hooks/use-observe.ts
new file mode 100644
index 0000000..35c6b41
--- /dev/null
+++ b/src/hooks/use-observe.ts
@@ -0,0 +1,22 @@
+import { useEffect } from "react";
+import * as Y from "yjs"
+import { useRedraw } from "~/hooks/use-redraw"
+
+export function useObserve(object: Y.AbstractType<any>, kind: "deep" | "normal" = "normal") {
+    const redraw = useRedraw();
+
+    useEffect(() => {
+        if (kind === "deep") {
+            object.observeDeep(redraw);
+        } else if (kind === "normal") {
+            object.observe(redraw);
+        }
+        return () => {
+            if (kind === "deep") {
+                object.unobserveDeep(redraw);
+            } else if (kind === "normal") {
+                object.unobserve(redraw);
+            }
+        }
+    }, []);
+}
diff --git a/src/hooks/use-redraw.ts b/src/hooks/use-redraw.ts
new file mode 100644
index 0000000..043511b
--- /dev/null
+++ b/src/hooks/use-redraw.ts
@@ -0,0 +1,7 @@
+import { useState } from "react";
+
+export function useRedraw() {
+    const [, setTick] = useState(0);
+
+    return () => setTick((tick) => tick + 1);
+}
diff --git a/src/hooks/use-theme.tsx b/src/hooks/use-theme.tsx
new file mode 100644
index 0000000..685c642
--- /dev/null
+++ b/src/hooks/use-theme.tsx
@@ -0,0 +1,47 @@
+import React, { useCallback, useContext, useLayoutEffect, useState } from "react";
+
+type Theme = "system" | "light" | "dark";
+
+const ThemeContex = React.createContext<{ theme: Theme, effectiveTheme: "light" | "dark", setTheme: (theme: Theme) => void }>({
+    theme: "system",
+    effectiveTheme: "light",
+    setTheme: () => { }
+});
+
+export function ThemeProvider(props: { children: React.ReactNode }) {
+    const [localTheme, setLocalTheme] = useState<Theme>(() => localStorage.getItem("theme") as Theme || "system");
+    const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
+
+    const effectiveTheme = localTheme === "system" ? systemTheme : localTheme;
+
+    useLayoutEffect(() => {
+        document.documentElement.setAttribute("data-theme", effectiveTheme)
+    }, [effectiveTheme])
+
+    const setTheme = useCallback((newTheme: Theme) => {
+        setLocalTheme(newTheme);
+        localStorage.setItem("theme", newTheme);
+    }, []);
+
+    return (
+        <ThemeContex.Provider value={{
+            theme: localTheme,
+            effectiveTheme,
+            setTheme
+        }}>
+            {props.children}
+        </ThemeContex.Provider>
+    );
+}
+
+export function useTheme() {
+    const { theme, setTheme } = useContext(ThemeContex);
+
+    return [theme, setTheme] as const;
+}
+
+export function useEffectiveTheme() {
+    const { effectiveTheme } = useContext(ThemeContex);
+
+    return effectiveTheme;
+}
diff --git a/src/hooks/use-ydoc.ts b/src/hooks/use-ydoc.ts
new file mode 100644
index 0000000..6beea38
--- /dev/null
+++ b/src/hooks/use-ydoc.ts
@@ -0,0 +1,33 @@
+import { useEffect, useState } from 'react';
+import * as Y from 'yjs';
+import { IndexeddbPersistence } from 'y-indexeddb'
+import { WebsocketProvider } from 'y-websocket';
+
+export function useYDoc(name: string) {
+    const [ydoc] = useState<Y.Doc>(() => new Y.Doc());
+    const [indexeddbProvider, setIndexeddbProvider] = useState<IndexeddbPersistence | null>(null);
+    const [websocketProvider, setWebsocketProvider] = useState<WebsocketProvider | null>(null);
+
+    useEffect(() => {
+        if (!ydoc) {
+            return
+        }
+
+        const indexeddbProvider = new IndexeddbPersistence(name, ydoc);
+        setIndexeddbProvider(indexeddbProvider);
+
+        const websocketProvider = new WebsocketProvider(`ws://localhost:1234`, name, ydoc, {
+            // TODO: For now we don't connect to a server yet, this obviously needs to change.
+            connect: false,
+        });
+        setWebsocketProvider(websocketProvider);
+
+        return () => {
+            indexeddbProvider.destroy();
+            websocketProvider.destroy();
+            ydoc.destroy();
+        };
+    }, [ydoc]);
+
+    return { ydoc, indexeddbProvider, websocketProvider };
+}
diff --git a/src/lib/color.ts b/src/lib/color.ts
new file mode 100644
index 0000000..f61e21a
--- /dev/null
+++ b/src/lib/color.ts
@@ -0,0 +1,26 @@
+export type ColorName = keyof typeof colors;
+export type Color = typeof colors[ColorName];
+
+// Tailwind colors
+// Base: 400
+// Hover: 300
+export const colors = {
+    red: { label: "Red", base: "#f87171", hover: "#fca5a5" },
+    orange: { label: "Orange", base: "#fb923c", hover: "#fdba74" },
+    amber: { label: "Amber", base: "#fbbf24", hover: "#fcd34d" },
+    yellow: { label: "Yellow", base: "#facc15", hover: "#fde047" },
+    lime: { label: "Lime", base: "#a3e635", hover: "#bef264" },
+    green: { label: "Green", base: "#4ade80", hover: "#86efac" },
+    emerald: { label: "Emerald", base: "#34d399", hover: "#6ee7b7" },
+    teal: { label: "Teal", base: "#2dd4bf", hover: "#5eead4" },
+    cyan: { label: "Cyan", base: "#22d3ee", hover: "#67e8f9" },
+    sky: { label: "Sky", base: "#38bdf8", hover: "#7dd3fc" },
+    blue: { label: "Blue", base: "#60a5fa", hover: "#93c5fd" },
+    indigo: { label: "Indigo", base: "#818cf8", hover: "#a5b4fc" },
+    violet: { label: "Violet", base: "#a78bfa", hover: "#c4b5fd" },
+    purple: { label: "Purple", base: "#c084fc", hover: "#d8b4fe" },
+    fuchsia: { label: "Fuchsia", base: "#e879f9", hover: "#f0abfc" },
+    pink: { label: "Pink", base: "#f472b6", hover: "#f9a8d4" },
+    rose: { label: "Rose", base: "#fb7185", hover: "#fda4af" },
+    white: { label: "White", base: "#a3a3a3", hover: "#d4d4d4" },
+};
diff --git a/src/lib/icon.ts b/src/lib/icon.ts
new file mode 100644
index 0000000..1aa3393
--- /dev/null
+++ b/src/lib/icon.ts
@@ -0,0 +1,12 @@
+import * as mdi_icons from "@mdi/js";
+
+export type IconName = string; //keyof typeof icons;
+export type Icon = typeof icons[IconName];
+
+export const icons = Object.fromEntries(Object.entries(mdi_icons).map(([key, value]) => ([
+    key.replace(/([A-Z])/g, "-$1").replace(/^mdi-/, "").toLowerCase(),
+    {
+        path: value,
+        name: key.replace("mdi", "").replace(/([A-Z])/g, " $1"),
+    }
+])));
diff --git a/src/lib/metadata.ts b/src/lib/metadata.ts
new file mode 100644
index 0000000..f1b71fe
--- /dev/null
+++ b/src/lib/metadata.ts
@@ -0,0 +1,99 @@
+import * as Y from "yjs"
+
+import type { ColorName } from "~/lib/color"
+import type { IconName } from "~/lib/icon"
+import type { YArray, YMap } from "~/lib/yjs"
+import type { PropertyType } from "~/lib/property"
+
+export type NoteId = string
+export type CollectionId = string
+export type PropertyId = string
+
+export type NotesMetadata = YArray<NoteMetadata>
+
+export type NoteMetadata = YMap<{
+    id: NoteId
+    collectionId: CollectionId | ""
+    title: string
+    icon: IconName | ""
+    primaryColor: ColorName | ""
+    secondaryColor: ColorName | ""
+    pinned: boolean
+    properties: YArray<NoteProperty>
+    type: "text" | "canvas"
+}>
+
+export type NoteProperty = YMap<{
+    propertyId: PropertyId
+    value: string
+}>
+
+export type CollectionsMetadata = YArray<CollectionMetadata>
+
+export type CollectionMetadata = YMap<{
+    id: CollectionId
+    name: string
+    color: ColorName
+    icon: IconName | ""
+    properties?: YArray<CollectionProperty>
+}>
+
+export type CollectionProperty = YMap<{
+    id: PropertyId
+    name: string
+    type: PropertyType
+    pinned: boolean
+}>
+
+export function createCollection(md: CollectionsMetadata, data: {
+    name: string,
+    color: ColorName,
+    icon: IconName,
+}) {
+    const collection = new Y.Map() as any as CollectionMetadata;
+    collection.set("id", randomUUID());
+    collection.set("name", data.name);
+    collection.set("color", data.color);
+    collection.set("icon", data.icon);
+    collection.set("properties", new Y.Array() as any);
+
+    md.push([collection])
+    return collection;
+}
+
+export function deleteCollection(md: CollectionsMetadata, id: CollectionId) {
+    const index = md.toArray().findIndex(it => it.get("id") == id);
+    if (index == -1) {
+        return;
+    }
+
+    md.delete(index, 1)
+}
+
+export function createNote(md: NotesMetadata, data: {
+    name: string,
+    icon: IconName | undefined,
+    collectionId: CollectionId | undefined,
+    primaryColor: ColorName | undefined,
+    secondaryColor: ColorName | undefined,
+    type: "text" | "canvas"
+}) {
+    const note = new Y.Map() as any as NoteMetadata;
+    note.set("id", randomUUID());
+    note.set("title", data.name);
+    note.set("icon", data.icon ?? "");
+    note.set("collectionId", data.collectionId ?? "");
+    note.set("primaryColor", data.primaryColor ?? "");
+    note.set("secondaryColor", data.secondaryColor ?? "");
+    note.set("pinned", false);
+    note.set("properties", new Y.Array() as any);
+    note.set("type", data.type);
+
+    md.push([note])
+
+    return note;
+}
+
+export function randomUUID() {
+    return crypto.randomUUID()
+}
diff --git a/src/lib/property.ts b/src/lib/property.ts
new file mode 100644
index 0000000..397de81
--- /dev/null
+++ b/src/lib/property.ts
@@ -0,0 +1,42 @@
+import { randomUUID, type CollectionProperty, type NoteProperty, type PropertyId } from "~/lib/metadata";
+import type { YArray } from "~/lib/yjs";
+import * as Y from "yjs";
+
+export const propertyTypes = ["shortText"] as const;
+
+export type PropertyType = typeof propertyTypes[number];
+
+export const properties: { [P in PropertyType]: string } = {
+    "shortText": "Short text",
+};
+
+export function createCollectionProperty(md: YArray<CollectionProperty>) {
+    const property = new Y.Map() as any as CollectionProperty;
+    property.set("id", randomUUID());
+    property.set("name", "");
+    property.set("type", "shortText");
+    property.set("pinned", false);
+
+    md.push([property]);
+    return property;
+}
+
+export function deleteCollectionProperty(md: YArray<CollectionProperty>, id: PropertyId) {
+    const index = md.toArray().findIndex((prop) => prop.get("id") === id);
+    if (index === -1) {
+        return;
+    }
+
+    md.delete(index, 1);
+}
+
+export function createNoteProperty(md: YArray<NoteProperty>, data: {
+    propertyId: PropertyId;
+}) {
+    const property = new Y.Map() as any as NoteProperty;
+    property.set("propertyId", data.propertyId);
+    property.set("value", "");
+
+    md.push([property]);
+    return property;
+}
diff --git a/src/lib/yjs.ts b/src/lib/yjs.ts
new file mode 100644
index 0000000..5c19c6b
--- /dev/null
+++ b/src/lib/yjs.ts
@@ -0,0 +1,48 @@
+import * as Y from 'yjs'
+
+export type YValue = object | boolean | string | number | Uint8Array | YMap<any> | YArray<any> | Y.Text
+
+type YValueToJSON<T> =
+    T extends YMap<any> ? YMapToJSON<T>
+    : T extends YArray<any> ? YArrayToJSON<T>
+    : T extends Y.Text ? string
+    : T
+
+type YMapToJSON<T> = T extends YMap<infer X>
+    ? { [KEY in keyof X]: YValueToJSON<X[KEY]> }
+    : never;
+
+type YArrayToJSON<T> = T extends YArray<infer X> ? YValueToJSON<X>[] : never;
+
+export type YMap<T extends { [key: string]: YValue }> = {
+    set: <X extends keyof T>(key: X, value: T[X]) => void
+    get: <X extends keyof T>(key: X) => T[X]
+    delete: <X extends keyof T>(key: X) => void
+    has: <X extends keyof T>(key: X) => boolean
+    clear: () => void
+    toJSON: () => YMapToJSON<YMap<T>>
+    forEach: (callback: <X extends keyof T>(value: T[X], key: X, map: YMap<T>) => void) => void
+    entries: () => Iterator<[string, YValue]>
+    values: () => Iterator<YValue>
+    keys: () => Iterator<keyof T>
+    clone: () => YMap<T>
+
+    size: number
+}
+
+export type YArray<T extends YValue> = {
+    insert: (index: number, content: T[]) => void
+    delete: (index: number, length: number) => void
+    push: (values: T[]) => void
+    unshift: (content: T[]) => void
+    get: (index: number) => T | undefined
+    slice: (start: number, end?: number) => T[]
+    toArray: () => T[]
+    toJSON: () => YArrayToJSON<YArray<T>>
+    forEach: (callback: (value: T, index: number, yarray: YArray<T>) => void) => void
+    map: <X>(callback: (value: T, index: number, yarray: YArray<T>) => X) => X[]
+    clone: () => YArray<T>
+
+    length: number
+}
+
diff --git a/src/main.tsx b/src/main.tsx
index ba85d7d..b209f38 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,28 +2,14 @@ import { StrictMode } from 'react'
 import ReactDOM from 'react-dom/client'
 import {
     RouterProvider,
-    createRootRoute,
-    createRoute,
     createRouter,
 } from '@tanstack/react-router'
 
 import '~/styles.css'
 import reportWebVitals from '~/reportWebVitals.ts'
 
-import { RootLayout } from '~/layouts/RootLayout'
-import { RootPage } from '~/pages/Root'
-
-const rootRoute = createRootRoute({
-    component: RootLayout,
-})
-
-const indexRoute = createRoute({
-    getParentRoute: () => rootRoute,
-    path: '/',
-    component: RootPage,
-})
-
-const routeTree = rootRoute.addChildren([indexRoute])
+// Import the generated route tree
+import { routeTree } from "./routeTree.gen";
 
 const router = createRouter({
     routeTree,
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
new file mode 100644
index 0000000..bd9a7ed
--- /dev/null
+++ b/src/routeTree.gen.ts
@@ -0,0 +1,369 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+import { Route as AppImport } from './routes/app'
+import { Route as IndexImport } from './routes/index'
+import { Route as AppIndexImport } from './routes/app/index'
+import { Route as AppTodoImport } from './routes/app/todo'
+import { Route as AppSearchImport } from './routes/app/search'
+import { Route as AppInboxImport } from './routes/app/inbox'
+import { Route as AppGraphImport } from './routes/app/graph'
+import { Route as AppCalendarImport } from './routes/app/calendar'
+import { Route as AppTermTermImport } from './routes/app/term.$term'
+import { Route as AppNoteIdImport } from './routes/app/note.$id'
+import { Route as AppCollectionIdImport } from './routes/app/collection.$id'
+
+// Create/Update Routes
+
+const AppRoute = AppImport.update({
+  id: '/app',
+  path: '/app',
+  getParentRoute: () => rootRoute,
+} as any)
+
+const IndexRoute = IndexImport.update({
+  id: '/',
+  path: '/',
+  getParentRoute: () => rootRoute,
+} as any)
+
+const AppIndexRoute = AppIndexImport.update({
+  id: '/',
+  path: '/',
+  getParentRoute: () => AppRoute,
+} as any)
+
+const AppTodoRoute = AppTodoImport.update({
+  id: '/todo',
+  path: '/todo',
+  getParentRoute: () => AppRoute,
+} as any)
+
+const AppSearchRoute = AppSearchImport.update({
+  id: '/search',
+  path: '/search',
+  getParentRoute: () => AppRoute,
+} as any)
+
+const AppInboxRoute = AppInboxImport.update({
+  id: '/inbox',
+  path: '/inbox',
+  getParentRoute: () => AppRoute,
+} as any)
+
+const AppGraphRoute = AppGraphImport.update({
+  id: '/graph',
+  path: '/graph',
+  getParentRoute: () => AppRoute,
+} as any)
+
+const AppCalendarRoute = AppCalendarImport.update({
+  id: '/calendar',
+  path: '/calendar',
+  getParentRoute: () => AppRoute,
+} as any)
+
+const AppTermTermRoute = AppTermTermImport.update({
+  id: '/term/$term',
+  path: '/term/$term',
+  getParentRoute: () => AppRoute,
+} as any)
+
+const AppNoteIdRoute = AppNoteIdImport.update({
+  id: '/note/$id',
+  path: '/note/$id',
+  getParentRoute: () => AppRoute,
+} as any)
+
+const AppCollectionIdRoute = AppCollectionIdImport.update({
+  id: '/collection/$id',
+  path: '/collection/$id',
+  getParentRoute: () => AppRoute,
+} as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/react-router' {
+  interface FileRoutesByPath {
+    '/': {
+      id: '/'
+      path: '/'
+      fullPath: '/'
+      preLoaderRoute: typeof IndexImport
+      parentRoute: typeof rootRoute
+    }
+    '/app': {
+      id: '/app'
+      path: '/app'
+      fullPath: '/app'
+      preLoaderRoute: typeof AppImport
+      parentRoute: typeof rootRoute
+    }
+    '/app/calendar': {
+      id: '/app/calendar'
+      path: '/calendar'
+      fullPath: '/app/calendar'
+      preLoaderRoute: typeof AppCalendarImport
+      parentRoute: typeof AppImport
+    }
+    '/app/graph': {
+      id: '/app/graph'
+      path: '/graph'
+      fullPath: '/app/graph'
+      preLoaderRoute: typeof AppGraphImport
+      parentRoute: typeof AppImport
+    }
+    '/app/inbox': {
+      id: '/app/inbox'
+      path: '/inbox'
+      fullPath: '/app/inbox'
+      preLoaderRoute: typeof AppInboxImport
+      parentRoute: typeof AppImport
+    }
+    '/app/search': {
+      id: '/app/search'
+      path: '/search'
+      fullPath: '/app/search'
+      preLoaderRoute: typeof AppSearchImport
+      parentRoute: typeof AppImport
+    }
+    '/app/todo': {
+      id: '/app/todo'
+      path: '/todo'
+      fullPath: '/app/todo'
+      preLoaderRoute: typeof AppTodoImport
+      parentRoute: typeof AppImport
+    }
+    '/app/': {
+      id: '/app/'
+      path: '/'
+      fullPath: '/app/'
+      preLoaderRoute: typeof AppIndexImport
+      parentRoute: typeof AppImport
+    }
+    '/app/collection/$id': {
+      id: '/app/collection/$id'
+      path: '/collection/$id'
+      fullPath: '/app/collection/$id'
+      preLoaderRoute: typeof AppCollectionIdImport
+      parentRoute: typeof AppImport
+    }
+    '/app/note/$id': {
+      id: '/app/note/$id'
+      path: '/note/$id'
+      fullPath: '/app/note/$id'
+      preLoaderRoute: typeof AppNoteIdImport
+      parentRoute: typeof AppImport
+    }
+    '/app/term/$term': {
+      id: '/app/term/$term'
+      path: '/term/$term'
+      fullPath: '/app/term/$term'
+      preLoaderRoute: typeof AppTermTermImport
+      parentRoute: typeof AppImport
+    }
+  }
+}
+
+// Create and export the route tree
+
+interface AppRouteChildren {
+  AppCalendarRoute: typeof AppCalendarRoute
+  AppGraphRoute: typeof AppGraphRoute
+  AppInboxRoute: typeof AppInboxRoute
+  AppSearchRoute: typeof AppSearchRoute
+  AppTodoRoute: typeof AppTodoRoute
+  AppIndexRoute: typeof AppIndexRoute
+  AppCollectionIdRoute: typeof AppCollectionIdRoute
+  AppNoteIdRoute: typeof AppNoteIdRoute
+  AppTermTermRoute: typeof AppTermTermRoute
+}
+
+const AppRouteChildren: AppRouteChildren = {
+  AppCalendarRoute: AppCalendarRoute,
+  AppGraphRoute: AppGraphRoute,
+  AppInboxRoute: AppInboxRoute,
+  AppSearchRoute: AppSearchRoute,
+  AppTodoRoute: AppTodoRoute,
+  AppIndexRoute: AppIndexRoute,
+  AppCollectionIdRoute: AppCollectionIdRoute,
+  AppNoteIdRoute: AppNoteIdRoute,
+  AppTermTermRoute: AppTermTermRoute,
+}
+
+const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren)
+
+export interface FileRoutesByFullPath {
+  '/': typeof IndexRoute
+  '/app': typeof AppRouteWithChildren
+  '/app/calendar': typeof AppCalendarRoute
+  '/app/graph': typeof AppGraphRoute
+  '/app/inbox': typeof AppInboxRoute
+  '/app/search': typeof AppSearchRoute
+  '/app/todo': typeof AppTodoRoute
+  '/app/': typeof AppIndexRoute
+  '/app/collection/$id': typeof AppCollectionIdRoute
+  '/app/note/$id': typeof AppNoteIdRoute
+  '/app/term/$term': typeof AppTermTermRoute
+}
+
+export interface FileRoutesByTo {
+  '/': typeof IndexRoute
+  '/app/calendar': typeof AppCalendarRoute
+  '/app/graph': typeof AppGraphRoute
+  '/app/inbox': typeof AppInboxRoute
+  '/app/search': typeof AppSearchRoute
+  '/app/todo': typeof AppTodoRoute
+  '/app': typeof AppIndexRoute
+  '/app/collection/$id': typeof AppCollectionIdRoute
+  '/app/note/$id': typeof AppNoteIdRoute
+  '/app/term/$term': typeof AppTermTermRoute
+}
+
+export interface FileRoutesById {
+  __root__: typeof rootRoute
+  '/': typeof IndexRoute
+  '/app': typeof AppRouteWithChildren
+  '/app/calendar': typeof AppCalendarRoute
+  '/app/graph': typeof AppGraphRoute
+  '/app/inbox': typeof AppInboxRoute
+  '/app/search': typeof AppSearchRoute
+  '/app/todo': typeof AppTodoRoute
+  '/app/': typeof AppIndexRoute
+  '/app/collection/$id': typeof AppCollectionIdRoute
+  '/app/note/$id': typeof AppNoteIdRoute
+  '/app/term/$term': typeof AppTermTermRoute
+}
+
+export interface FileRouteTypes {
+  fileRoutesByFullPath: FileRoutesByFullPath
+  fullPaths:
+    | '/'
+    | '/app'
+    | '/app/calendar'
+    | '/app/graph'
+    | '/app/inbox'
+    | '/app/search'
+    | '/app/todo'
+    | '/app/'
+    | '/app/collection/$id'
+    | '/app/note/$id'
+    | '/app/term/$term'
+  fileRoutesByTo: FileRoutesByTo
+  to:
+    | '/'
+    | '/app/calendar'
+    | '/app/graph'
+    | '/app/inbox'
+    | '/app/search'
+    | '/app/todo'
+    | '/app'
+    | '/app/collection/$id'
+    | '/app/note/$id'
+    | '/app/term/$term'
+  id:
+    | '__root__'
+    | '/'
+    | '/app'
+    | '/app/calendar'
+    | '/app/graph'
+    | '/app/inbox'
+    | '/app/search'
+    | '/app/todo'
+    | '/app/'
+    | '/app/collection/$id'
+    | '/app/note/$id'
+    | '/app/term/$term'
+  fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+  IndexRoute: typeof IndexRoute
+  AppRoute: typeof AppRouteWithChildren
+}
+
+const rootRouteChildren: RootRouteChildren = {
+  IndexRoute: IndexRoute,
+  AppRoute: AppRouteWithChildren,
+}
+
+export const routeTree = rootRoute
+  ._addFileChildren(rootRouteChildren)
+  ._addFileTypes<FileRouteTypes>()
+
+/* ROUTE_MANIFEST_START
+{
+  "routes": {
+    "__root__": {
+      "filePath": "__root.tsx",
+      "children": [
+        "/",
+        "/app"
+      ]
+    },
+    "/": {
+      "filePath": "index.tsx"
+    },
+    "/app": {
+      "filePath": "app.tsx",
+      "children": [
+        "/app/calendar",
+        "/app/graph",
+        "/app/inbox",
+        "/app/search",
+        "/app/todo",
+        "/app/",
+        "/app/collection/$id",
+        "/app/note/$id",
+        "/app/term/$term"
+      ]
+    },
+    "/app/calendar": {
+      "filePath": "app/calendar.tsx",
+      "parent": "/app"
+    },
+    "/app/graph": {
+      "filePath": "app/graph.tsx",
+      "parent": "/app"
+    },
+    "/app/inbox": {
+      "filePath": "app/inbox.tsx",
+      "parent": "/app"
+    },
+    "/app/search": {
+      "filePath": "app/search.tsx",
+      "parent": "/app"
+    },
+    "/app/todo": {
+      "filePath": "app/todo.tsx",
+      "parent": "/app"
+    },
+    "/app/": {
+      "filePath": "app/index.tsx",
+      "parent": "/app"
+    },
+    "/app/collection/$id": {
+      "filePath": "app/collection.$id.tsx",
+      "parent": "/app"
+    },
+    "/app/note/$id": {
+      "filePath": "app/note.$id.tsx",
+      "parent": "/app"
+    },
+    "/app/term/$term": {
+      "filePath": "app/term.$term.tsx",
+      "parent": "/app"
+    }
+  }
+}
+ROUTE_MANIFEST_END */
diff --git a/src/layouts/RootLayout.tsx b/src/routes/__root.tsx
similarity index 58%
rename from src/layouts/RootLayout.tsx
rename to src/routes/__root.tsx
index 0923a86..82d6975 100644
--- a/src/layouts/RootLayout.tsx
+++ b/src/routes/__root.tsx
@@ -1,9 +1,14 @@
-import { Outlet } from "@tanstack/react-router";
-import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
+import { createRootRoute, Outlet } from "@tanstack/react-router";
+// import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
 import { useEffect } from "react";
 import { getSerwist } from "virtual:serwist";
+import { ThemeProvider } from "~/hooks/use-theme";
 
-export function RootLayout() {
+export const Route = createRootRoute({
+    component: RootLayout,
+});
+
+function RootLayout() {
     useEffect(() => {
         const loadSerwist = async () => {
             if ("serviceWorker" in navigator) {
@@ -20,9 +25,9 @@ export function RootLayout() {
         loadSerwist();
     }, []);
     return (
-        <>
+        <ThemeProvider>
             <Outlet />
-            <TanStackRouterDevtools />
-        </>
+            {/*<TanStackRouterDevtools />*/}
+        </ThemeProvider>
     );
 }
diff --git a/src/routes/app.tsx b/src/routes/app.tsx
new file mode 100644
index 0000000..8143588
--- /dev/null
+++ b/src/routes/app.tsx
@@ -0,0 +1,52 @@
+import { createFileRoute, Outlet } from "@tanstack/react-router";
+import { PanelLeft } from "lucide-react";
+import { AppSidebar } from "~/components/app_sidebar";
+import { Button } from "~/components/ui/button";
+import { SidebarInset, SidebarProvider, useSidebar } from "~/components/ui/sidebar";
+import { MetadataProvider } from "~/hooks/use-metadata";
+import { useIsMobile } from "~/hooks/use-mobile";
+
+export const Route = createFileRoute("/app")({
+    component: AppLayout,
+});
+
+export function AppLayout() {
+    return (
+        <>
+            <MetadataProvider>
+                <SidebarProvider>
+                    <AppSidebar />
+                    <div className="flex flex-col h-svh w-full group">
+                        <SidebarInset className="flex-grow overflow-scroll">
+                            <Outlet />
+                        </SidebarInset>
+                        <MobileBar />
+                    </div>
+                </SidebarProvider>
+            </MetadataProvider>
+        </>
+    );
+}
+
+function MobileBar() {
+    const isMobile = useIsMobile();
+    const { toggleSidebar } = useSidebar();
+
+    if (!isMobile) {
+        return;
+    }
+
+    return (
+        <div className="bg-sidebar w-full p-2">
+            <Button
+                data-sidebar="trigger"
+                variant="ghost"
+                onClick={toggleSidebar}
+                className="justify-start"
+            >
+                <PanelLeft />
+            </Button>
+        </div>
+    );
+
+}
diff --git a/src/routes/app/calendar.tsx b/src/routes/app/calendar.tsx
new file mode 100644
index 0000000..834b5c3
--- /dev/null
+++ b/src/routes/app/calendar.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/app/calendar')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return <div>Hello "/app/calendar"!</div>
+}
diff --git a/src/routes/app/collection.$id.tsx b/src/routes/app/collection.$id.tsx
new file mode 100644
index 0000000..52cd700
--- /dev/null
+++ b/src/routes/app/collection.$id.tsx
@@ -0,0 +1,20 @@
+import { createFileRoute } from "@tanstack/react-router"
+import { CollectionHeader } from "~/components/collection/collection_header";
+import { NotesGrid } from "~/components/note/notes_grid";
+import { useCollectionNotesMetadata } from "~/hooks/use-metadata";
+
+export const Route = createFileRoute("/app/collection/$id")({
+    component: RouteComponent,
+})
+
+function RouteComponent() {
+    const { id } = Route.useParams();
+    const notes = useCollectionNotesMetadata(id);
+
+    return (
+        <div className="flex flex-col h-full">
+            <CollectionHeader collectionId={id} />
+            <NotesGrid notes={notes} />
+        </div>
+    );
+}
diff --git a/src/routes/app/graph.tsx b/src/routes/app/graph.tsx
new file mode 100644
index 0000000..7d9e38c
--- /dev/null
+++ b/src/routes/app/graph.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/app/graph')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return <div>Hello "/app/graph"!</div>
+}
diff --git a/src/routes/app/inbox.tsx b/src/routes/app/inbox.tsx
new file mode 100644
index 0000000..66329aa
--- /dev/null
+++ b/src/routes/app/inbox.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { NotesGrid } from '~/components/note/notes_grid';
+import { useCollectionNotesMetadata } from '~/hooks/use-metadata';
+
+export const Route = createFileRoute('/app/inbox')({
+    component: RouteComponent,
+})
+
+function RouteComponent() {
+    const notes = useCollectionNotesMetadata("")
+
+    return (
+        <NotesGrid notes={notes} />
+    );
+}
diff --git a/src/routes/app/index.tsx b/src/routes/app/index.tsx
new file mode 100644
index 0000000..d96c07c
--- /dev/null
+++ b/src/routes/app/index.tsx
@@ -0,0 +1,12 @@
+import { createFileRoute } from "@tanstack/react-router"
+import { MetadataInspector } from "~/components/yjs/metadata-inspector";
+
+export const Route = createFileRoute("/app/")({
+    component: RouteComponent,
+});
+
+function RouteComponent() {
+    return (
+        <MetadataInspector />
+    )
+}
diff --git a/src/routes/app/note.$id.tsx b/src/routes/app/note.$id.tsx
new file mode 100644
index 0000000..b2989f6
--- /dev/null
+++ b/src/routes/app/note.$id.tsx
@@ -0,0 +1,36 @@
+import { createFileRoute } from "@tanstack/react-router"
+import NoteCanvas from "~/components/note/note_canvas";
+import { NoteHeader } from "~/components/note/note_header";
+import { Editor } from "~/editor/Editor";
+import { useNoteMetadata } from "~/hooks/use-metadata";
+import { NoteProvider } from "~/hooks/use-note";
+
+export const Route = createFileRoute("/app/note/$id")({
+    component: RouteComponent,
+})
+
+function RouteComponent() {
+    const { id } = Route.useParams();
+    return (
+        <NoteProvider id={id}>
+            <Content id={id} />
+        </NoteProvider>
+    );
+}
+
+function Content(props: { id: string }) {
+    const metadata = useNoteMetadata(props.id);
+
+    switch (metadata?.get("type")) {
+        case "text": return <>
+            <NoteHeader id={props.id} />
+            <div className="flex-shrink-0 min-h-full max-w-3xl mx-auto w-full">
+                <Editor noteId={props.id} />
+            </div>
+        </>;
+        case "canvas": return <>
+            <NoteCanvas noteId={props.id} />
+        </>;
+        default: return null;
+    }
+}
diff --git a/src/routes/app/search.tsx b/src/routes/app/search.tsx
new file mode 100644
index 0000000..3c48e66
--- /dev/null
+++ b/src/routes/app/search.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute } from "@tanstack/react-router"
+import { NotesGrid } from "~/components/note/notes_grid";
+import { useNotesMetadata } from "~/hooks/use-metadata";
+
+export const Route = createFileRoute("/app/search")({
+    component: RouteComponent,
+})
+
+function RouteComponent() {
+    const notes = useNotesMetadata().toArray();
+
+    return <NotesGrid notes={notes} allUnpinned />
+}
diff --git a/src/routes/app/term.$term.tsx b/src/routes/app/term.$term.tsx
new file mode 100644
index 0000000..f29cd5d
--- /dev/null
+++ b/src/routes/app/term.$term.tsx
@@ -0,0 +1,10 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/app/term/$term')({
+    component: RouteComponent,
+})
+
+function RouteComponent() {
+    const { term } = Route.useParams();
+    return <div>Hello "/app/term/{term}"!</div>
+}
diff --git a/src/routes/app/todo.tsx b/src/routes/app/todo.tsx
new file mode 100644
index 0000000..1122ac9
--- /dev/null
+++ b/src/routes/app/todo.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/app/todo')({
+  component: RouteComponent,
+})
+
+function RouteComponent() {
+  return <div>Hello "/app/todo"!</div>
+}
diff --git a/src/pages/Root.tsx b/src/routes/index.tsx
similarity index 80%
rename from src/pages/Root.tsx
rename to src/routes/index.tsx
index 4eaf616..721a05e 100644
--- a/src/pages/Root.tsx
+++ b/src/routes/index.tsx
@@ -1,6 +1,11 @@
+import { createFileRoute, Link } from '@tanstack/react-router';
 import logo from '~/logo.svg'
 
-export function RootPage() {
+export const Route = createFileRoute("/")({
+    component: RootPage,
+});
+
+function RootPage() {
     return (
         <div className="text-center">
             <header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)]">
@@ -28,6 +33,11 @@ export function RootPage() {
                 >
                     Learn TanStack
                 </a>
+                <Link
+                    to='/app'
+                >
+                    Go to app
+                </Link>
             </header>
         </div>
     );
diff --git a/src/styles.css b/src/styles.css
index c1404e3..455cb99 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1,7 +1,7 @@
 @import "tailwindcss";
 @import "tw-animate-css";
 
-@custom-variant dark (&:is(.dark *));
+@custom-variant dark (&:is([data-theme="dark"] *));
 
 body {
   @apply m-0;
@@ -52,7 +52,7 @@ code {
   --sidebar-ring: oklch(0.708 0 0);
 }
 
-.dark {
+[data-theme="dark"] {
   --background: oklch(0.145 0 0);
   --foreground: oklch(0.985 0 0);
   --card: oklch(0.205 0 0);
diff --git a/vite.config.js b/vite.config.js
index 99b2cef..72ffeb0 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -2,12 +2,14 @@ import { defineConfig } from "vite";
 import { serwist } from "@serwist/vite";
 import viteReact from "@vitejs/plugin-react";
 import tailwindcss from "@tailwindcss/vite";
+import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
 import path from "path";
 
 
 // https://vitejs.dev/config/
 export default defineConfig({
     plugins: [
+        TanStackRouterVite(),
         viteReact(),
         tailwindcss(),
         serwist({
@@ -16,6 +18,8 @@ export default defineConfig({
             globDirectory: "dist",
             injectionPoint: "self.__SW_MANIFEST",
             rollupFormat: "iife",
+            // Insanely large max size, since the app **HAS** to function fully offline
+            maximumFileSizeToCacheInBytes: 512 * 1024 * 1024,
         }),
     ],
     server: {