@treenity/react 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (379) hide show
  1. package/README.md +91 -0
  2. package/dist/AclEditor.d.ts +1 -1
  3. package/dist/AclEditor.d.ts.map +1 -1
  4. package/dist/AclEditor.js +5 -5
  5. package/dist/AclEditor.js.map +1 -1
  6. package/dist/ActionCards.d.ts +9 -0
  7. package/dist/ActionCards.d.ts.map +1 -0
  8. package/dist/ActionCards.js +96 -0
  9. package/dist/ActionCards.js.map +1 -0
  10. package/dist/App.d.ts.map +1 -1
  11. package/dist/App.js +97 -185
  12. package/dist/App.js.map +1 -1
  13. package/dist/ComponentSection.d.ts +15 -0
  14. package/dist/ComponentSection.d.ts.map +1 -0
  15. package/dist/ComponentSection.js +25 -0
  16. package/dist/ComponentSection.js.map +1 -0
  17. package/dist/ErrorBoundary.d.ts +18 -0
  18. package/dist/ErrorBoundary.d.ts.map +1 -0
  19. package/dist/ErrorBoundary.js +18 -0
  20. package/dist/ErrorBoundary.js.map +1 -0
  21. package/dist/Inspector.d.ts +1 -0
  22. package/dist/Inspector.d.ts.map +1 -1
  23. package/dist/Inspector.js +22 -347
  24. package/dist/Inspector.js.map +1 -1
  25. package/dist/Login.d.ts +8 -0
  26. package/dist/Login.d.ts.map +1 -0
  27. package/dist/Login.js +45 -0
  28. package/dist/Login.js.map +1 -0
  29. package/dist/NodeEditor.d.ts +11 -0
  30. package/dist/NodeEditor.d.ts.map +1 -0
  31. package/dist/NodeEditor.js +157 -0
  32. package/dist/NodeEditor.js.map +1 -0
  33. package/dist/Tree.d.ts +1 -0
  34. package/dist/Tree.d.ts.map +1 -1
  35. package/dist/Tree.js +8 -27
  36. package/dist/Tree.js.map +1 -1
  37. package/dist/bind/engine.js +1 -1
  38. package/dist/bind/engine.js.map +1 -1
  39. package/dist/bind/eval.d.ts +1 -1
  40. package/dist/bind/eval.d.ts.map +1 -1
  41. package/dist/bind/hook.d.ts +1 -1
  42. package/dist/bind/hook.d.ts.map +1 -1
  43. package/dist/bind/hook.js +1 -1
  44. package/dist/bind/hook.js.map +1 -1
  45. package/dist/cache.d.ts +1 -1
  46. package/dist/cache.d.ts.map +1 -1
  47. package/dist/cache.js +9 -0
  48. package/dist/cache.js.map +1 -1
  49. package/dist/client-tree.d.ts +1 -2
  50. package/dist/client-tree.d.ts.map +1 -1
  51. package/dist/client-tree.js +12 -5
  52. package/dist/client-tree.js.map +1 -1
  53. package/dist/client.d.ts +1 -1
  54. package/dist/client.d.ts.map +1 -1
  55. package/dist/client.js +2 -4
  56. package/dist/client.js.map +1 -1
  57. package/dist/components/ConfirmDialog.d.ts +9 -0
  58. package/dist/components/ConfirmDialog.d.ts.map +1 -0
  59. package/dist/components/ConfirmDialog.js +6 -0
  60. package/dist/components/ConfirmDialog.js.map +1 -0
  61. package/dist/components/ConfirmPopover.d.ts +8 -0
  62. package/dist/components/ConfirmPopover.d.ts.map +1 -0
  63. package/dist/components/ConfirmPopover.js +9 -0
  64. package/dist/components/ConfirmPopover.js.map +1 -0
  65. package/dist/components/PathBreadcrumb.d.ts +5 -0
  66. package/dist/components/PathBreadcrumb.d.ts.map +1 -0
  67. package/dist/components/PathBreadcrumb.js +16 -0
  68. package/dist/components/PathBreadcrumb.js.map +1 -0
  69. package/dist/components/lib/utils.d.ts +3 -0
  70. package/dist/components/lib/utils.d.ts.map +1 -0
  71. package/dist/components/lib/utils.js +6 -0
  72. package/dist/components/lib/utils.js.map +1 -0
  73. package/dist/components/ui/accordion.js +1 -1
  74. package/dist/components/ui/accordion.js.map +1 -1
  75. package/dist/components/ui/alert-dialog.d.ts +19 -0
  76. package/dist/components/ui/alert-dialog.d.ts.map +1 -0
  77. package/dist/components/ui/alert-dialog.js +42 -0
  78. package/dist/components/ui/alert-dialog.js.map +1 -0
  79. package/dist/components/ui/badge.js +1 -1
  80. package/dist/components/ui/badge.js.map +1 -1
  81. package/dist/components/ui/breadcrumb.d.ts +12 -0
  82. package/dist/components/ui/breadcrumb.d.ts.map +1 -0
  83. package/dist/components/ui/breadcrumb.js +28 -0
  84. package/dist/components/ui/breadcrumb.js.map +1 -0
  85. package/dist/components/ui/button.d.ts +8 -7
  86. package/dist/components/ui/button.d.ts.map +1 -1
  87. package/dist/components/ui/button.js +25 -20
  88. package/dist/components/ui/button.js.map +1 -1
  89. package/dist/components/ui/card.d.ts +10 -0
  90. package/dist/components/ui/card.d.ts.map +1 -0
  91. package/dist/components/ui/card.js +25 -0
  92. package/dist/components/ui/card.js.map +1 -0
  93. package/dist/components/ui/checkbox.js +1 -1
  94. package/dist/components/ui/checkbox.js.map +1 -1
  95. package/dist/components/ui/collapsible.d.ts +6 -0
  96. package/dist/components/ui/collapsible.d.ts.map +1 -0
  97. package/dist/components/ui/collapsible.js +13 -0
  98. package/dist/components/ui/collapsible.js.map +1 -0
  99. package/dist/components/ui/command.d.ts +19 -0
  100. package/dist/components/ui/command.d.ts.map +1 -0
  101. package/dist/components/ui/command.js +35 -0
  102. package/dist/components/ui/command.js.map +1 -0
  103. package/dist/components/ui/dialog.d.ts.map +1 -1
  104. package/dist/components/ui/dialog.js +1 -1
  105. package/dist/components/ui/dialog.js.map +1 -1
  106. package/dist/components/ui/drawer.js +1 -1
  107. package/dist/components/ui/drawer.js.map +1 -1
  108. package/dist/components/ui/dropdown-menu.d.ts +26 -0
  109. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  110. package/dist/components/ui/dropdown-menu.js +52 -0
  111. package/dist/components/ui/dropdown-menu.js.map +1 -0
  112. package/dist/components/ui/form-field.d.ts +7 -0
  113. package/dist/components/ui/form-field.d.ts.map +1 -0
  114. package/dist/components/ui/form-field.js +17 -0
  115. package/dist/components/ui/form-field.js.map +1 -0
  116. package/dist/components/ui/input.js +1 -1
  117. package/dist/components/ui/input.js.map +1 -1
  118. package/dist/components/ui/label.js +1 -1
  119. package/dist/components/ui/label.js.map +1 -1
  120. package/dist/components/ui/pagination.d.ts +14 -0
  121. package/dist/components/ui/pagination.d.ts.map +1 -0
  122. package/dist/components/ui/pagination.js +30 -0
  123. package/dist/components/ui/pagination.js.map +1 -0
  124. package/dist/components/ui/popover.js +2 -2
  125. package/dist/components/ui/popover.js.map +1 -1
  126. package/dist/components/ui/progress.js +1 -1
  127. package/dist/components/ui/progress.js.map +1 -1
  128. package/dist/components/ui/resizable.d.ts +8 -0
  129. package/dist/components/ui/resizable.d.ts.map +1 -0
  130. package/dist/components/ui/resizable.js +14 -0
  131. package/dist/components/ui/resizable.js.map +1 -0
  132. package/dist/components/ui/scroll-area.d.ts +6 -0
  133. package/dist/components/ui/scroll-area.d.ts.map +1 -0
  134. package/dist/components/ui/scroll-area.js +13 -0
  135. package/dist/components/ui/scroll-area.js.map +1 -0
  136. package/dist/components/ui/select.js +1 -1
  137. package/dist/components/ui/select.js.map +1 -1
  138. package/dist/components/ui/separator.d.ts +5 -0
  139. package/dist/components/ui/separator.d.ts.map +1 -0
  140. package/dist/components/ui/separator.js +9 -0
  141. package/dist/components/ui/separator.js.map +1 -0
  142. package/dist/components/ui/sheet.d.ts +15 -0
  143. package/dist/components/ui/sheet.d.ts.map +1 -0
  144. package/dist/components/ui/sheet.js +40 -0
  145. package/dist/components/ui/sheet.js.map +1 -0
  146. package/dist/components/ui/skeleton.d.ts +3 -0
  147. package/dist/components/ui/skeleton.d.ts.map +1 -0
  148. package/dist/components/ui/skeleton.js +7 -0
  149. package/dist/components/ui/skeleton.js.map +1 -0
  150. package/dist/components/ui/slider.js +1 -1
  151. package/dist/components/ui/slider.js.map +1 -1
  152. package/dist/components/ui/switch.js +1 -1
  153. package/dist/components/ui/switch.js.map +1 -1
  154. package/dist/components/ui/table.d.ts +11 -0
  155. package/dist/components/ui/table.d.ts.map +1 -0
  156. package/dist/components/ui/table.js +29 -0
  157. package/dist/components/ui/table.js.map +1 -0
  158. package/dist/components/ui/tabs.d.ts +12 -0
  159. package/dist/components/ui/tabs.d.ts.map +1 -0
  160. package/dist/components/ui/tabs.js +29 -0
  161. package/dist/components/ui/tabs.js.map +1 -0
  162. package/dist/components/ui/textarea.js +2 -2
  163. package/dist/components/ui/textarea.js.map +1 -1
  164. package/dist/components/ui/toggle-group.d.ts +10 -0
  165. package/dist/components/ui/toggle-group.d.ts.map +1 -0
  166. package/dist/components/ui/toggle-group.js +23 -0
  167. package/dist/components/ui/toggle-group.js.map +1 -0
  168. package/dist/components/ui/toggle.d.ts +10 -0
  169. package/dist/components/ui/toggle.d.ts.map +1 -0
  170. package/dist/components/ui/toggle.js +27 -0
  171. package/dist/components/ui/toggle.js.map +1 -0
  172. package/dist/components/ui/tooltip.js +1 -1
  173. package/dist/components/ui/tooltip.js.map +1 -1
  174. package/dist/context/index.d.ts +27 -10
  175. package/dist/context/index.d.ts.map +1 -1
  176. package/dist/context/index.js +43 -36
  177. package/dist/context/index.js.map +1 -1
  178. package/dist/events.d.ts +12 -0
  179. package/dist/events.d.ts.map +1 -0
  180. package/dist/events.js +123 -0
  181. package/dist/events.js.map +1 -0
  182. package/dist/fiber-tree.d.ts +3 -0
  183. package/dist/fiber-tree.d.ts.map +1 -0
  184. package/dist/fiber-tree.js +93 -0
  185. package/dist/fiber-tree.js.map +1 -0
  186. package/dist/hooks.d.ts +14 -2
  187. package/dist/hooks.d.ts.map +1 -1
  188. package/dist/hooks.js +146 -11
  189. package/dist/hooks.js.map +1 -1
  190. package/dist/idb.d.ts +1 -1
  191. package/dist/idb.d.ts.map +1 -1
  192. package/dist/lib/minimd.d.ts.map +1 -1
  193. package/dist/lib/minimd.js +8 -1
  194. package/dist/lib/minimd.js.map +1 -1
  195. package/dist/lib/sanitize-href.d.ts +3 -0
  196. package/dist/lib/sanitize-href.d.ts.map +1 -0
  197. package/dist/lib/sanitize-href.js +14 -0
  198. package/dist/lib/sanitize-href.js.map +1 -0
  199. package/dist/lib/to-plain.d.ts +2 -0
  200. package/dist/lib/to-plain.d.ts.map +1 -0
  201. package/dist/lib/to-plain.js +21 -0
  202. package/dist/lib/to-plain.js.map +1 -0
  203. package/dist/main.d.ts +1 -1
  204. package/dist/main.d.ts.map +1 -1
  205. package/dist/main.js +11 -4
  206. package/dist/main.js.map +1 -1
  207. package/dist/mods/clients.d.ts +3 -0
  208. package/dist/mods/clients.d.ts.map +1 -0
  209. package/dist/mods/clients.js +4 -0
  210. package/dist/mods/clients.js.map +1 -0
  211. package/dist/mods/editor-ui/FieldLabel.d.ts +15 -0
  212. package/dist/mods/editor-ui/FieldLabel.d.ts.map +1 -0
  213. package/dist/mods/editor-ui/FieldLabel.js +56 -0
  214. package/dist/mods/editor-ui/FieldLabel.js.map +1 -0
  215. package/dist/mods/editor-ui/client.d.ts +1 -1
  216. package/dist/mods/editor-ui/client.d.ts.map +1 -1
  217. package/dist/mods/editor-ui/client.js +1 -1
  218. package/dist/mods/editor-ui/client.js.map +1 -1
  219. package/dist/mods/editor-ui/default-edit.d.ts +2 -0
  220. package/dist/mods/editor-ui/default-edit.d.ts.map +1 -0
  221. package/dist/mods/editor-ui/default-edit.js +56 -0
  222. package/dist/mods/editor-ui/default-edit.js.map +1 -0
  223. package/dist/mods/editor-ui/default-view.d.ts +8 -1
  224. package/dist/mods/editor-ui/default-view.d.ts.map +1 -1
  225. package/dist/mods/editor-ui/default-view.js +8 -5
  226. package/dist/mods/editor-ui/default-view.js.map +1 -1
  227. package/dist/mods/editor-ui/dir-view.js +0 -2
  228. package/dist/mods/editor-ui/dir-view.js.map +1 -1
  229. package/dist/mods/editor-ui/empty-placeholder.d.ts +5 -0
  230. package/dist/mods/editor-ui/empty-placeholder.d.ts.map +1 -0
  231. package/dist/mods/editor-ui/empty-placeholder.js +14 -0
  232. package/dist/mods/editor-ui/empty-placeholder.js.map +1 -0
  233. package/dist/mods/editor-ui/form-field.d.ts +17 -0
  234. package/dist/mods/editor-ui/form-field.d.ts.map +1 -0
  235. package/dist/mods/editor-ui/form-field.js +69 -0
  236. package/dist/mods/editor-ui/form-field.js.map +1 -0
  237. package/dist/mods/editor-ui/form-fields.d.ts +1 -2
  238. package/dist/mods/editor-ui/form-fields.d.ts.map +1 -1
  239. package/dist/mods/editor-ui/form-fields.js +56 -60
  240. package/dist/mods/editor-ui/form-fields.js.map +1 -1
  241. package/dist/mods/editor-ui/layout-view.js +3 -2
  242. package/dist/mods/editor-ui/layout-view.js.map +1 -1
  243. package/dist/mods/editor-ui/list-items.js +1 -1
  244. package/dist/mods/editor-ui/list-items.js.map +1 -1
  245. package/dist/mods/editor-ui/node-utils.d.ts +2 -2
  246. package/dist/mods/editor-ui/node-utils.d.ts.map +1 -1
  247. package/dist/mods/editor-ui/node-utils.js +4 -5
  248. package/dist/mods/editor-ui/node-utils.js.map +1 -1
  249. package/dist/mods/editor-ui/type-picker.d.ts +15 -0
  250. package/dist/mods/editor-ui/type-picker.d.ts.map +1 -0
  251. package/dist/mods/editor-ui/type-picker.js +70 -0
  252. package/dist/mods/editor-ui/type-picker.js.map +1 -0
  253. package/dist/mods/editor-ui/user-view.js +1 -1
  254. package/dist/mods/editor-ui/user-view.js.map +1 -1
  255. package/dist/mods/servers.d.ts +1 -0
  256. package/dist/mods/servers.d.ts.map +1 -0
  257. package/dist/mods/servers.js +4 -0
  258. package/dist/mods/servers.js.map +1 -0
  259. package/dist/mods/treenity/groups/index.js +1 -1
  260. package/dist/mods/treenity/groups/index.js.map +1 -1
  261. package/dist/mods/treenity/preview.d.ts.map +1 -1
  262. package/dist/mods/treenity/preview.js +3 -4
  263. package/dist/mods/treenity/preview.js.map +1 -1
  264. package/dist/mods/treenity/ref-view.js +3 -2
  265. package/dist/mods/treenity/ref-view.js.map +1 -1
  266. package/dist/mods/treenity/schema-form.js +1 -1
  267. package/dist/mods/treenity/schema-form.js.map +1 -1
  268. package/dist/mods/treenity/seed.js +3 -2
  269. package/dist/mods/treenity/seed.js.map +1 -1
  270. package/dist/mods/treenity/type-view.js +1 -1
  271. package/dist/mods/treenity/type-view.js.map +1 -1
  272. package/dist/schema-loader.d.ts +1 -1
  273. package/dist/schema-loader.d.ts.map +1 -1
  274. package/dist/schema-loader.js +1 -1
  275. package/dist/schema-loader.js.map +1 -1
  276. package/dist/symbols.d.ts +5 -0
  277. package/dist/symbols.d.ts.map +1 -0
  278. package/dist/symbols.js +22 -0
  279. package/dist/symbols.js.map +1 -0
  280. package/dist/trpc.d.ts +10 -3
  281. package/dist/trpc.d.ts.map +1 -1
  282. package/package.json +76 -8
  283. package/src/AclEditor.tsx +11 -18
  284. package/src/ActionCards.tsx +224 -0
  285. package/src/App.tsx +232 -385
  286. package/src/ComponentSection.tsx +113 -0
  287. package/src/ErrorBoundary.tsx +40 -0
  288. package/src/Inspector.css +54 -0
  289. package/src/Inspector.tsx +73 -793
  290. package/src/Login.tsx +97 -0
  291. package/src/NodeEditor.tsx +300 -0
  292. package/src/Tree.css +91 -0
  293. package/src/Tree.tsx +40 -43
  294. package/src/bind/engine.ts +1 -1
  295. package/src/bind/eval.ts +1 -1
  296. package/src/bind/hook.ts +1 -1
  297. package/src/bind/pipes.ts +1 -1
  298. package/src/cache.ts +12 -1
  299. package/src/client-tree.ts +18 -12
  300. package/src/client.ts +2 -4
  301. package/src/components/ConfirmDialog.tsx +34 -0
  302. package/src/components/ConfirmPopover.tsx +41 -0
  303. package/src/components/PathBreadcrumb.tsx +36 -0
  304. package/src/components/lib/utils.ts +6 -0
  305. package/src/components/lib/utils.ts.bak +6 -0
  306. package/src/components/ui/accordion.tsx +1 -1
  307. package/src/components/ui/alert-dialog.tsx +189 -0
  308. package/src/components/ui/badge.tsx +1 -1
  309. package/src/components/ui/breadcrumb.tsx +108 -0
  310. package/src/components/ui/button.tsx +51 -30
  311. package/src/components/ui/card.tsx +91 -0
  312. package/src/components/ui/checkbox.tsx +1 -1
  313. package/src/components/ui/collapsible.tsx +31 -0
  314. package/src/components/ui/command.tsx +177 -0
  315. package/src/components/ui/dialog.tsx +1 -2
  316. package/src/components/ui/drawer.tsx +1 -1
  317. package/src/components/ui/dropdown-menu.tsx +256 -0
  318. package/src/components/ui/form-field.tsx +37 -0
  319. package/src/components/ui/input.tsx +1 -1
  320. package/src/components/ui/label.tsx +1 -1
  321. package/src/components/ui/pagination.tsx +122 -0
  322. package/src/components/ui/popover.tsx +2 -2
  323. package/src/components/ui/progress.tsx +1 -1
  324. package/src/components/ui/resizable.tsx +47 -0
  325. package/src/components/ui/scroll-area.tsx +55 -0
  326. package/src/components/ui/select.tsx +1 -1
  327. package/src/components/ui/separator.tsx +27 -0
  328. package/src/components/ui/sheet.tsx +140 -0
  329. package/src/components/ui/skeleton.tsx +13 -0
  330. package/src/components/ui/slider.tsx +1 -1
  331. package/src/components/ui/switch.tsx +1 -1
  332. package/src/components/ui/table.tsx +115 -0
  333. package/src/components/ui/tabs.tsx +88 -0
  334. package/src/components/ui/textarea.tsx +2 -2
  335. package/src/components/ui/toggle-group.tsx +82 -0
  336. package/src/components/ui/toggle.tsx +46 -0
  337. package/src/components/ui/tooltip.tsx +1 -1
  338. package/src/context/index.tsx +75 -42
  339. package/src/events.ts +121 -0
  340. package/src/fiber-tree.ts +112 -0
  341. package/src/hooks.ts +161 -13
  342. package/src/idb.ts +1 -1
  343. package/src/lib/minimd.ts +7 -1
  344. package/src/lib/sanitize-href.ts +13 -0
  345. package/src/lib/to-plain.ts +21 -0
  346. package/src/main.tsx +14 -4
  347. package/src/mods/clients.ts +3 -0
  348. package/src/mods/editor-ui/FieldLabel.tsx +125 -0
  349. package/src/mods/editor-ui/client.ts +1 -1
  350. package/src/mods/editor-ui/default-edit.tsx +101 -0
  351. package/src/mods/editor-ui/default-view.tsx +13 -8
  352. package/src/mods/editor-ui/dir-view.tsx +2 -2
  353. package/src/mods/editor-ui/editor-ui.css +174 -0
  354. package/src/mods/editor-ui/empty-placeholder.tsx +39 -0
  355. package/src/mods/editor-ui/form-field.tsx +146 -0
  356. package/src/mods/editor-ui/form-fields.tsx +132 -113
  357. package/src/mods/editor-ui/layout-view.tsx +4 -2
  358. package/src/mods/editor-ui/list-items.tsx +2 -2
  359. package/src/mods/editor-ui/node-utils.ts +4 -5
  360. package/src/mods/editor-ui/type-picker.tsx +148 -0
  361. package/src/mods/editor-ui/user-view.tsx +1 -1
  362. package/src/mods/servers.ts +2 -0
  363. package/src/mods/treenity/groups/index.tsx +1 -1
  364. package/src/mods/treenity/preview.tsx +7 -8
  365. package/src/mods/treenity/ref-view.tsx +12 -7
  366. package/src/mods/treenity/schema-form.tsx +1 -1
  367. package/src/mods/treenity/seed.ts +3 -2
  368. package/src/mods/treenity/type-view.tsx +1 -1
  369. package/src/remote-tree.ts +1 -1
  370. package/src/root.css +117 -0
  371. package/src/schema-loader.ts +1 -1
  372. package/src/symbols.ts +25 -0
  373. package/src/bind/bind.test.ts +0 -316
  374. package/src/cache.test.ts +0 -139
  375. package/src/client-tree.test.ts +0 -116
  376. package/src/index.html +0 -14
  377. package/src/remote-tree.test.ts +0 -142
  378. package/src/style.css +0 -1269
  379. package/src/vite-env.d.ts +0 -3
package/src/App.tsx CHANGED
@@ -1,10 +1,26 @@
1
- import { isOfType, type NodeData } from '@treenity/core/core';
2
- import { applyPatch, type Operation } from 'fast-json-patch';
1
+ import {
2
+ AlertDialog,
3
+ AlertDialogAction,
4
+ AlertDialogCancel,
5
+ AlertDialogContent,
6
+ AlertDialogFooter,
7
+ AlertDialogHeader,
8
+ AlertDialogTitle,
9
+ } from '#components/ui/alert-dialog';
10
+ import { Button } from '#components/ui/button';
11
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '#components/ui/dropdown-menu';
12
+ import { Input } from '#components/ui/input';
13
+ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '#components/ui/resizable';
14
+ import { TypePicker } from '#mods/editor-ui/type-picker';
15
+ import type { NodeData } from '@treenity/core';
3
16
  import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
17
+ import { toast } from 'sonner';
4
18
  import * as cache from './cache';
5
19
  import { tree } from './client';
20
+ import { SSE_CONNECTED, SSE_DISCONNECTED, startEvents, stopEvents } from './events';
6
21
  import { NavigateProvider } from './hooks';
7
22
  import { Inspector } from './Inspector';
23
+ import { LoginModal, LoginScreen } from './Login';
8
24
  import { Tree } from './Tree';
9
25
  import { AUTH_EXPIRED_EVENT, clearToken, getToken, setToken, trpc } from './trpc';
10
26
  import { ViewPage } from './ViewPage';
@@ -12,201 +28,6 @@ import { ViewPage } from './ViewPage';
12
28
  // Hydrate from IDB before first render — fires bump() when done → reactive re-render
13
29
  cache.hydrate();
14
30
 
15
- type TypeInfo = { type: string; label: string };
16
-
17
- async function loadTypes(): Promise<TypeInfo[]> {
18
- const { items } = (await trpc.getChildren.query({ path: '/sys/types', limit: 0, depth: 99 })) as {
19
- items: NodeData[];
20
- total: number;
21
- };
22
- return items
23
- .filter((n) => isOfType(n, 'type'))
24
- .map((n) => {
25
- const schema = n.schema as { $type: string; title?: string } | undefined;
26
- const typeName = n.$path.slice('/sys/types/'.length).replace(/\//g, '.');
27
- return { type: typeName, label: schema?.title ?? typeName };
28
- });
29
- }
30
-
31
- function TypePicker({
32
- onSelect,
33
- onCancel,
34
- title = 'Create Node',
35
- nameLabel = 'Node name',
36
- action = 'Create',
37
- }: {
38
- onSelect: (name: string, type: string) => void;
39
- onCancel: () => void;
40
- title?: string;
41
- nameLabel?: string;
42
- action?: string;
43
- }) {
44
- const [types, setTypes] = useState<TypeInfo[]>([]);
45
- const [loading, setLoading] = useState(true);
46
- const [error, setError] = useState<string | null>(null);
47
- const [filter, setFilter] = useState('');
48
- const [name, setName] = useState('');
49
- const [selectedType, setSelectedType] = useState<string | null>(null);
50
- const nameRef = useRef<HTMLInputElement>(null);
51
-
52
- useEffect(() => {
53
- loadTypes()
54
- .then(setTypes)
55
- .catch((err) => {
56
- console.error('Failed to load types:', err);
57
- setError('Failed to load types');
58
- })
59
- .finally(() => setLoading(false));
60
- }, []);
61
- useEffect(() => {
62
- nameRef.current?.focus();
63
- }, []);
64
-
65
- const lf = filter.toLowerCase();
66
- const filtered = types.filter(
67
- (t) => t.type.toLowerCase().includes(lf) || t.label.toLowerCase().includes(lf),
68
- );
69
-
70
- return (
71
- <div className="type-picker-overlay" onClick={onCancel}>
72
- <div className="type-picker" onClick={(e) => e.stopPropagation()}>
73
- <div className="type-picker-header">{title}</div>
74
- <div className="type-picker-search">
75
- <input
76
- ref={nameRef}
77
- placeholder={nameLabel}
78
- value={name}
79
- onChange={(e) => setName(e.target.value)}
80
- />
81
- <input
82
- placeholder="Filter types..."
83
- value={filter}
84
- onChange={(e) => setFilter(e.target.value)}
85
- />
86
- </div>
87
- <div className="type-picker-list">
88
- {filtered.map((t) => (
89
- <div
90
- key={t.type}
91
- className={`type-picker-item${selectedType === t.type ? ' active' : ''}`}
92
- onClick={() => setSelectedType(t.type)}
93
- >
94
- <span className="type-name">{t.type}</span>
95
- {t.label !== t.type && <span className="type-label">{t.label}</span>}
96
- </div>
97
- ))}
98
- {loading && (
99
- <div className="p-3 text-[--text-3] text-[13px]">Loading types...</div>
100
- )}
101
- {error && (
102
- <div className="p-3 text-[--danger] text-[13px]">{error}</div>
103
- )}
104
- {!loading && !error && filtered.length === 0 && (
105
- <div className="p-3 text-[--text-3] text-[13px]">No types found</div>
106
- )}
107
- </div>
108
- <div className="type-picker-footer">
109
- <button onClick={onCancel}>Cancel</button>
110
- <button
111
- className="primary"
112
- disabled={!name || !selectedType}
113
- onClick={() => onSelect(name, selectedType!)}
114
- >
115
- {action}
116
- {name ? ` "${name}"` : ''}
117
- {selectedType ? ` as ${selectedType}` : ''}
118
- </button>
119
- </div>
120
- </div>
121
- </div>
122
- );
123
- }
124
-
125
- function LoginForm({ onLogin }: { onLogin: (userId: string) => void }) {
126
- const [mode, setMode] = useState<'login' | 'register'>('login');
127
- const [userId, setUserId] = useState('');
128
- const [password, setPassword] = useState('');
129
- const [err, setErr] = useState<string | null>(null);
130
- const [loading, setLoading] = useState(false);
131
-
132
- async function handleSubmit(e: React.FormEvent) {
133
- e.preventDefault();
134
- if (!userId.trim() || !password) return;
135
- setLoading(true);
136
- setErr(null);
137
- try {
138
- const fn = mode === 'register' ? trpc.register : trpc.login;
139
- const res = await fn.mutate({ userId: userId.trim(), password });
140
- setToken(res.token);
141
- onLogin(res.userId);
142
- } catch (e) {
143
- setErr(e instanceof Error ? e.message : 'Failed');
144
- } finally {
145
- setLoading(false);
146
- }
147
- }
148
-
149
- return (
150
- <form className="login-box" onSubmit={handleSubmit}>
151
- <div className="login-logo">
152
- <img src="/treenity.svg" alt="" width="32" height="32" />
153
- Treenity
154
- </div>
155
- <div className="field">
156
- <label>User ID</label>
157
- <input
158
- autoFocus
159
- placeholder="Enter your user ID"
160
- value={userId}
161
- onChange={(e) => setUserId(e.target.value)}
162
- />
163
- </div>
164
- <div className="field">
165
- <label>Password</label>
166
- <input
167
- type="password"
168
- placeholder="Enter password"
169
- value={password}
170
- onChange={(e) => setPassword(e.target.value)}
171
- />
172
- </div>
173
- {err && <div className="login-error">{err}</div>}
174
- <button className="primary" type="submit" disabled={loading || !userId.trim() || !password}>
175
- {loading ? '...' : mode === 'register' ? 'Create account' : 'Sign in'}
176
- </button>
177
- <button
178
- type="button"
179
- className="ghost"
180
- onClick={() => {
181
- setMode((m) => (m === 'login' ? 'register' : 'login'));
182
- setErr(null);
183
- }}
184
- >
185
- {mode === 'login' ? 'No account? Register' : 'Have an account? Sign in'}
186
- </button>
187
- </form>
188
- );
189
- }
190
-
191
- function LoginScreen({ onLogin }: { onLogin: (userId: string) => void }) {
192
- return (
193
- <div className="login-screen">
194
- <LoginForm onLogin={onLogin} />
195
- </div>
196
- );
197
- }
198
-
199
- function LoginModal({ onLogin, onClose }: { onLogin: (userId: string) => void; onClose: () => void }) {
200
- return (
201
- <div className="login-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
202
- <div className="login-modal">
203
- <button className="login-modal-close" onClick={onClose}>&times;</button>
204
- <LoginForm onLogin={onLogin} />
205
- </div>
206
- </div>
207
- );
208
- }
209
-
210
31
  // Isolated component — global subscription re-renders only this, not the entire App
211
32
  function NodeCount() {
212
33
  return <>{useSyncExternalStore(cache.subscribeGlobal, cache.size)}</>;
@@ -215,28 +36,42 @@ function NodeCount() {
215
36
  export function App() {
216
37
  const [authed, setAuthed] = useState<string | null>(null);
217
38
  const [authChecked, setAuthChecked] = useState(false);
39
+ const retryTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
218
40
 
219
- useEffect(() => {
220
- (async () => {
221
- const token = getToken();
222
- if (!token) {
223
- // Auto-create anonymous session
41
+ const initAuth = useCallback(async () => {
42
+ const token = getToken();
43
+ if (!token) {
44
+ try {
224
45
  const { token: anonToken, userId } = await trpc.anonLogin.mutate();
225
46
  setToken(anonToken);
226
47
  setAuthed(userId);
227
48
  setAuthChecked(true);
228
- return;
229
- }
230
- try {
231
- const res = await trpc.me.query();
232
- setAuthed(res?.userId ?? null);
233
- if (!res) clearToken();
234
49
  } catch {
50
+ toast.error('Server unavailable, retrying…');
51
+ retryTimer.current = setTimeout(initAuth, 3000);
52
+ }
53
+ return;
54
+ }
55
+ try {
56
+ const res = await trpc.me.query();
57
+ setAuthed(res?.userId ?? null);
58
+ if (!res) clearToken();
59
+ setAuthChecked(true);
60
+ } catch (e: any) {
61
+ const isAuthError = e?.data?.code === 'UNAUTHORIZED' || e?.data?.httpStatus === 401;
62
+ if (isAuthError) {
235
63
  clearToken();
236
- } finally {
237
64
  setAuthChecked(true);
65
+ } else {
66
+ toast.error('Server unavailable, retrying…');
67
+ retryTimer.current = setTimeout(initAuth, 3000);
238
68
  }
239
- })();
69
+ }
70
+ }, []);
71
+
72
+ useEffect(() => {
73
+ initAuth();
74
+ return () => clearTimeout(retryTimer.current);
240
75
  }, []);
241
76
 
242
77
  // ── Route detection ──
@@ -274,6 +109,29 @@ export function App() {
274
109
  const [filter, setFilter] = useState('');
275
110
  const [showHidden, setShowHidden] = useState(false);
276
111
  const [toastMsg, setToastMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
112
+ const [sseDown, setSseDown] = useState(false);
113
+ const sseDownTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
114
+
115
+ // TODO: remove debounce, extract from App code, remove debounce
116
+ // SSE connection indicator — debounce disconnect by 2s to avoid flicker
117
+ useEffect(() => {
118
+ const onConnect = () => {
119
+ if (sseDownTimer.current) { clearTimeout(sseDownTimer.current); sseDownTimer.current = undefined; }
120
+ setSseDown(false);
121
+ };
122
+ const onDisconnect = () => {
123
+ if (!sseDownTimer.current) {
124
+ sseDownTimer.current = setTimeout(() => { sseDownTimer.current = undefined; setSseDown(true); }, 2000);
125
+ }
126
+ };
127
+ window.addEventListener(SSE_CONNECTED, onConnect);
128
+ window.addEventListener(SSE_DISCONNECTED, onDisconnect);
129
+ return () => {
130
+ window.removeEventListener(SSE_CONNECTED, onConnect);
131
+ window.removeEventListener(SSE_DISCONNECTED, onDisconnect);
132
+ if (sseDownTimer.current) clearTimeout(sseDownTimer.current);
133
+ };
134
+ }, []);
277
135
 
278
136
  // Granular: only re-render App when root node appears/disappears
279
137
  const hasRootNode = useSyncExternalStore(
@@ -322,7 +180,7 @@ export function App() {
322
180
  function onKeyDown(e: KeyboardEvent) {
323
181
  const meta = e.metaKey || e.ctrlKey;
324
182
  if (!meta) return;
325
- if (document.querySelector('.type-picker-overlay')) return;
183
+ if (document.querySelector('[data-slot="dialog-overlay"]')) return;
326
184
  if (e.key === '/' && selected) {
327
185
  e.preventDefault();
328
186
  setAddingComponentAt(selected);
@@ -348,12 +206,8 @@ export function App() {
348
206
  }, [showToast]);
349
207
 
350
208
  const loadChildren = useCallback(async (path: string) => {
351
- const { items: children } = (await trpc.getChildren.query({
352
- path,
353
- watch: true,
354
- watchNew: true,
355
- })) as { items: NodeData[]; total: number };
356
- cache.putMany(children, path); // Use specific parent path so query mounts index them correctly
209
+ const { items: children } = await tree.getChildren(path);
210
+ cache.putMany(children, path);
357
211
  setLoaded((prev) => new Set(prev).add(path));
358
212
  }, []);
359
213
 
@@ -400,60 +254,15 @@ export function App() {
400
254
  })();
401
255
  }, [authed, loadChildren, root, mode]);
402
256
 
403
- // Live subscription — server push cache
257
+ // Server event subscription — module-level, refs provide stable access to current state
404
258
  useEffect(() => {
405
259
  if (!authed) return;
406
- const sub = trpc.events.subscribe(undefined as void, {
407
- onData(event) {
408
- if (event.type === 'reconnect') {
409
- if (!event.preserved) {
410
- // Watches lost — force useChildren hooks to re-fetch and re-register
411
- cache.signalReconnect();
412
- // Re-register tree watches for expanded paths (editor mode)
413
- for (const path of expandedRef.current) loadChildren(path);
414
- // Re-watch the currently selected node
415
- if (selectedRef.current) {
416
- trpc.get.query({ path: selectedRef.current, watch: true }).then(n => {
417
- if (n) cache.put(n as NodeData);
418
- });
419
- }
420
- }
421
- return;
422
- }
423
- if (event.type === 'set') {
424
- cache.put({ $path: event.path, ...event.node } as NodeData);
425
- if (event.addVps) event.addVps.forEach((vp: string) => cache.addToParent(event.path, vp));
426
- if (event.rmVps) event.rmVps.forEach((vp: string) => cache.removeFromParent(event.path, vp));
427
- } else if (event.type === 'patch') {
428
- const existing = cache.get(event.path);
429
- if (existing && event.patches) {
430
- try {
431
- const { newDocument } = applyPatch(structuredClone(existing), event.patches as Operation[]);
432
- cache.put(newDocument as NodeData);
433
- } catch (e) {
434
- console.error('Failed to apply patches, fetching full node:', e);
435
- trpc.get.query({ path: event.path }).then((n) => {
436
- if (n) cache.put(n as NodeData);
437
- });
438
- }
439
- } else {
440
- trpc.get.query({ path: event.path }).then((n) => {
441
- if (n) cache.put(n as NodeData);
442
- });
443
- }
444
- if (event.addVps) event.addVps.forEach((vp: string) => cache.addToParent(event.path, vp));
445
- if (event.rmVps) event.rmVps.forEach((vp: string) => cache.removeFromParent(event.path, vp));
446
- } else if (event.type === 'remove') {
447
- // Try to remove from anywhere
448
- if (event.rmVps && event.rmVps.length > 0) {
449
- event.rmVps.forEach((vp: string) => cache.removeFromParent(event.path, vp));
450
- } else {
451
- cache.remove(event.path);
452
- }
453
- }
454
- },
260
+ startEvents({
261
+ loadChildren,
262
+ getExpanded: () => expandedRef.current,
263
+ getSelected: () => selectedRef.current,
455
264
  });
456
- return () => sub.unsubscribe();
265
+ return stopEvents;
457
266
  }, [authed, loadChildren]);
458
267
 
459
268
  const handleSelect = useCallback(
@@ -562,10 +371,12 @@ export function App() {
562
371
  [loadChildren, showToast],
563
372
  );
564
373
 
565
- const roots = hasRootNode ? [root] : [];
374
+ const roots = hasRootNode ? [root, '/local'] : ['/local'];
375
+
376
+ const [rootPromptOpen, setRootPromptOpen] = useState(false);
377
+ const [rootPromptType, setRootPromptType] = useState('root');
566
378
 
567
- const handleCreateRoot = useCallback(async () => {
568
- const type = prompt('Root node $type:', 'root');
379
+ const handleCreateRoot = useCallback(async (type: string) => {
569
380
  if (!type) return;
570
381
  try {
571
382
  await tree.set({ $path: '/', $type: type } as NodeData);
@@ -580,37 +391,28 @@ export function App() {
580
391
  }, []);
581
392
 
582
393
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
583
- const [menuOpen, setMenuOpen] = useState(false);
584
394
  const [showLoginModal, setShowLoginModal] = useState(false);
585
- const menuRef = useRef<HTMLDivElement>(null);
586
395
 
587
396
  // Re-auth as anon + show login modal when session expires mid-use
588
397
  useEffect(() => {
589
398
  const handler = async () => {
590
399
  if (showLoginModal) return;
591
400
  clearToken();
592
- const { token, userId } = await trpc.anonLogin.mutate();
593
- setToken(token);
594
- setAuthed(userId);
595
- setShowLoginModal(true);
401
+ try {
402
+ const { token, userId } = await trpc.anonLogin.mutate();
403
+ setToken(token);
404
+ setAuthed(userId);
405
+ setShowLoginModal(true);
406
+ } catch {
407
+ toast.error('Server unavailable');
408
+ }
596
409
  };
597
410
  window.addEventListener(AUTH_EXPIRED_EVENT, handler);
598
411
  return () => window.removeEventListener(AUTH_EXPIRED_EVENT, handler);
599
412
  }, [showLoginModal]);
600
413
 
601
- // Close menu on outside click
602
- useEffect(() => {
603
- if (!menuOpen) return;
604
- const onDown = (e: MouseEvent) => {
605
- if (menuRef.current && !menuRef.current.contains(e.target as HTMLElement)) setMenuOpen(false);
606
- };
607
- document.addEventListener('mousedown', onDown);
608
- return () => document.removeEventListener('mousedown', onDown);
609
- }, [menuOpen]);
610
-
611
414
  const handleLogout = async () => {
612
415
  clearToken();
613
- setMenuOpen(false);
614
416
  const { token, userId } = await trpc.anonLogin.mutate();
615
417
  setToken(token);
616
418
  setAuthed(userId);
@@ -619,7 +421,6 @@ export function App() {
619
421
 
620
422
  const handleClearCache = () => {
621
423
  cache.clear();
622
- setMenuOpen(false);
623
424
  showToast('Cache cleared');
624
425
  location.reload();
625
426
  };
@@ -635,7 +436,10 @@ export function App() {
635
436
  }, [mode, handleSelect]);
636
437
 
637
438
  if (!authChecked) return null;
638
- if (!authed || authed.startsWith('anon:')) return <LoginScreen onLogin={(uid) => setAuthed(uid)} />;
439
+ if (!authed) return <LoginScreen onLogin={(uid) => setAuthed(uid)} />;
440
+
441
+ const isAnon = authed.startsWith('anon:');
442
+ const needsLogin = isAnon || showLoginModal;
639
443
  if (mode === 'view') return <NavigateProvider value={navigate}><ViewPage path={viewPath} /></NavigateProvider>;
640
444
  if (mode === 'preview') return <NavigateProvider value={navigate}><ViewPage path={viewPath} editorLink /></NavigateProvider>;
641
445
 
@@ -645,13 +449,11 @@ export function App() {
645
449
 
646
450
  if (error) {
647
451
  return (
648
- <div className="app">
649
- <div className="editor">
650
- <div className="editor-empty">
651
- <div className="icon">&#9888;</div>
652
- <p className="text-[--danger]">{error}</p>
653
- <button onClick={() => location.reload()}>Retry</button>
654
- </div>
452
+ <div className="flex h-screen items-center justify-center bg-background">
453
+ <div className="flex flex-col items-center gap-3 text-center">
454
+ <span className="text-4xl">&#9888;</span>
455
+ <p className="text-sm text-red-400">{error}</p>
456
+ <Button variant="outline" size="sm" onClick={() => location.reload()}>Retry</Button>
655
457
  </div>
656
458
  </div>
657
459
  );
@@ -659,95 +461,111 @@ export function App() {
659
461
 
660
462
  return (
661
463
  <NavigateProvider value={navigate}>
662
- <div className="app">
663
- <div className={`sidebar${sidebarCollapsed ? ' collapsed' : ''}`}>
664
- <div className="sidebar-header">
665
- <span className="logo">
464
+ {sseDown && (
465
+ <div className="fixed top-0 inset-x-0 z-50 bg-yellow-500 text-black text-center text-sm py-1">
466
+ Reconnecting to server…
467
+ </div>
468
+ )}
469
+ <div className="flex h-screen bg-background text-foreground overflow-hidden">
470
+ <ResizablePanelGroup orientation="horizontal" className="h-full">
471
+ <ResizablePanel
472
+ defaultSize={28}
473
+ minSize={150}
474
+ maxSize={450}
475
+ className="flex flex-col border-r border-border"
476
+ >
477
+ {/* Header */}
478
+ <div className="flex items-center gap-2 px-3 py-2.5 border-b border-border/50 shrink-0">
666
479
  <img src="/treenity.svg" alt="" width="20" height="20" />
667
- {!sidebarCollapsed && 'Treenity'}
668
- </span>
669
- {!sidebarCollapsed && root !== '/' && (
670
- <button
671
- className="sm ghost font-mono text-[11px]"
672
- onClick={() => setRoot('/')}
673
- title="Back to global root"
674
- >
675
- &#8962; {root}
676
- </button>
677
- )}
678
- {!sidebarCollapsed && roots.length === 0 && (
679
- <button className="sm" onClick={handleCreateRoot}>
680
- Create root
681
- </button>
682
- )}
683
- <button
684
- className="sm ghost sidebar-collapse-btn"
685
- onClick={() => setSidebarCollapsed(v => !v)}
686
- title={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
687
- >
688
- {sidebarCollapsed ? '\u25B6' : '\u25C0'}
689
- </button>
690
- </div>
691
- <div className="sidebar-search">
692
- <input
693
- ref={searchRef}
694
- placeholder="Search nodes..."
695
- value={filter}
696
- onChange={(e) => setFilter(e.target.value)}
697
- />
698
- <button
699
- className="sidebar-search-toggle"
700
- data-active={showHidden || undefined}
701
- onClick={() => setShowHidden(v => !v)}
702
- title={showHidden ? 'Hide _ prefixed nodes' : 'Show _ prefixed nodes'}
703
- >
704
- _
705
- </button>
706
- </div>
707
- <div className="sidebar-tree">
708
- <Tree
709
- roots={roots}
710
- expanded={expanded}
711
- loaded={loaded}
712
- selected={selected}
713
- filter={filter}
714
- showHidden={showHidden}
715
- onSelect={handleSelect}
716
- onExpand={handleExpand}
717
- onCreateChild={handleCreateChild}
718
- onDelete={handleDelete}
719
- onMove={handleMove}
720
- />
721
- </div>
722
- <div className="sidebar-footer" ref={menuRef}>
723
- <span>
724
- {authed?.startsWith('anon:') ? `anon:${authed.slice(5, 13)}` : authed} &middot; <NodeCount /> nodes
725
- </span>
726
- <button className="sm ghost" onClick={() => setMenuOpen(v => !v)}>
727
- &#9776;
728
- </button>
729
- {menuOpen && (
730
- <div className="sidebar-menu">
731
- <button onClick={handleLogout}>
732
- {authed?.startsWith('anon:') ? 'Login' : 'Logout'}
733
- </button>
734
- <button onClick={handleClearCache}>
735
- Clear cache
736
- </button>
480
+ {!sidebarCollapsed && <span className="text-sm font-semibold tracking-tight">Treenity</span>}
481
+ {!sidebarCollapsed && root !== '/' && (
482
+ <Button variant="ghost" size="sm" className="h-5 px-1.5 font-mono text-[10px] text-muted-foreground" onClick={() => setRoot('/')}>
483
+ &#8962; {root}
484
+ </Button>
485
+ )}
486
+ {!sidebarCollapsed && roots.length === 0 && (
487
+ <Button variant="ghost" size="sm" className="h-5 text-[10px]" onClick={() => { setRootPromptType('root'); setRootPromptOpen(true); }}>
488
+ Create root
489
+ </Button>
490
+ )}
491
+ </div>
492
+
493
+ {/* Search */}
494
+ {!sidebarCollapsed && (
495
+ <div className="flex items-center gap-1 px-2 py-1.5 shrink-0">
496
+ <Input
497
+ ref={searchRef}
498
+ placeholder="Search nodes..."
499
+ value={filter}
500
+ onChange={(e) => setFilter(e.target.value)}
501
+ className="h-7 text-xs bg-muted/50 border-border"
502
+ />
503
+ <Button
504
+ variant={showHidden ? 'secondary' : 'ghost'}
505
+ size="sm"
506
+ className="h-7 w-7 p-0 text-xs text-muted-foreground shrink-0"
507
+ onClick={() => setShowHidden(v => !v)}
508
+ title={showHidden ? 'Hide _ prefixed nodes' : 'Show _ prefixed nodes'}
509
+ >
510
+ _
511
+ </Button>
737
512
  </div>
738
513
  )}
739
- </div>
740
- </div>
741
514
 
742
- <Inspector
743
- path={selected}
744
- currentUserId={authed ?? undefined}
745
- onDelete={handleDelete}
746
- onAddComponent={handleAddComponent}
747
- onSelect={handleSelect}
748
- onSetRoot={handleSetRoot}
749
- toast={showToast}
750
- />
515
+ {/* Tree */}
516
+ <div className="flex-1 overflow-y-auto overflow-x-hidden">
517
+ <Tree
518
+ roots={roots}
519
+ expanded={expanded}
520
+ loaded={loaded}
521
+ selected={selected}
522
+ filter={filter}
523
+ showHidden={showHidden}
524
+ onSelect={handleSelect}
525
+ onExpand={handleExpand}
526
+ onCreateChild={handleCreateChild}
527
+ onDelete={handleDelete}
528
+ onMove={handleMove}
529
+ />
530
+ </div>
531
+
532
+ {/* Footer */}
533
+ <div className="flex items-center justify-between px-3 py-1.5 border-t border-border/50 text-[11px] text-muted-foreground shrink-0">
534
+ <span className="truncate">
535
+ {authed?.startsWith('anon:') ? `anon:${authed.slice(5, 13)}` : authed} &middot; <NodeCount /> nodes
536
+ </span>
537
+ <DropdownMenu>
538
+ <DropdownMenuTrigger asChild>
539
+ <Button variant="ghost" size="sm" className="h-5 w-5 p-0 text-muted-foreground hover:text-foreground">
540
+ &#9776;
541
+ </Button>
542
+ </DropdownMenuTrigger>
543
+ <DropdownMenuContent align="end" side="top" className="w-36">
544
+ <DropdownMenuItem onClick={handleLogout}>
545
+ {authed?.startsWith('anon:') ? 'Login' : 'Logout'}
546
+ </DropdownMenuItem>
547
+ <DropdownMenuItem onClick={handleClearCache}>
548
+ Clear cache
549
+ </DropdownMenuItem>
550
+ </DropdownMenuContent>
551
+ </DropdownMenu>
552
+ </div>
553
+ </ResizablePanel>
554
+
555
+ <ResizableHandle withHandle />
556
+
557
+ <ResizablePanel defaultSize={72} minSize={40}>
558
+ <Inspector
559
+ path={selected}
560
+ currentUserId={authed ?? undefined}
561
+ onDelete={handleDelete}
562
+ onAddComponent={handleAddComponent}
563
+ onSelect={handleSelect}
564
+ onSetRoot={handleSetRoot}
565
+ toast={showToast}
566
+ />
567
+ </ResizablePanel>
568
+ </ResizablePanelGroup>
751
569
 
752
570
  {creatingAt && <TypePicker onSelect={handlePickType} onCancel={() => setCreatingAt(null)} />}
753
571
 
@@ -756,19 +574,48 @@ export function App() {
756
574
  title="Add Component"
757
575
  nameLabel="Component name"
758
576
  action="Add"
577
+ autoName
759
578
  onSelect={handlePickComponent}
760
579
  onCancel={() => setAddingComponentAt(null)}
761
580
  />
762
581
  )}
763
582
 
764
- {showLoginModal && (
583
+ <AlertDialog open={rootPromptOpen} onOpenChange={setRootPromptOpen}>
584
+ <AlertDialogContent>
585
+ <AlertDialogHeader>
586
+ <AlertDialogTitle>Create root node</AlertDialogTitle>
587
+ </AlertDialogHeader>
588
+ <Input
589
+ value={rootPromptType}
590
+ onChange={(e) => setRootPromptType(e.target.value)}
591
+ placeholder="$type"
592
+ className="font-mono"
593
+ onKeyDown={(e) => {
594
+ if (e.key === 'Enter') {
595
+ setRootPromptOpen(false);
596
+ handleCreateRoot(rootPromptType);
597
+ }
598
+ }}
599
+ />
600
+ <AlertDialogFooter>
601
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
602
+ <AlertDialogAction onClick={() => handleCreateRoot(rootPromptType)}>Create</AlertDialogAction>
603
+ </AlertDialogFooter>
604
+ </AlertDialogContent>
605
+ </AlertDialog>
606
+
607
+ {needsLogin && (
765
608
  <LoginModal
766
609
  onLogin={(uid) => { setAuthed(uid); setShowLoginModal(false); }}
767
- onClose={() => setShowLoginModal(false)}
610
+ onClose={isAnon ? undefined : () => setShowLoginModal(false)}
768
611
  />
769
612
  )}
770
613
 
771
- {toastMsg && <div className={`toast ${toastMsg.type === 'error' ? 'toast-error' : ''}`}>{toastMsg.text}</div>}
614
+ {toastMsg && (
615
+ <div className={`fixed bottom-4 left-1/2 -translate-x-1/2 z-50 px-4 py-2 rounded-lg text-sm ${toastMsg.type === 'error' ? 'bg-destructive/20 text-destructive border border-destructive/30' : 'bg-primary/20 text-primary border border-primary/30'}`}>
616
+ {toastMsg.text}
617
+ </div>
618
+ )}
772
619
  </div>
773
620
  </NavigateProvider>
774
621
  );