@treenity/react 3.0.0 → 3.0.1

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 (369) 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 +71 -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 +17 -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 +4 -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 +9 -8
  86. package/dist/components/ui/button.d.ts.map +1 -1
  87. package/dist/components/ui/button.js +26 -21
  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 +1 -1
  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 +10 -0
  179. package/dist/events.d.ts.map +1 -0
  180. package/dist/events.js +78 -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 +5 -2
  187. package/dist/hooks.d.ts.map +1 -1
  188. package/dist/hooks.js +66 -6
  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/to-plain.d.ts +2 -0
  193. package/dist/lib/to-plain.d.ts.map +1 -0
  194. package/dist/lib/to-plain.js +21 -0
  195. package/dist/lib/to-plain.js.map +1 -0
  196. package/dist/main.d.ts +1 -1
  197. package/dist/main.d.ts.map +1 -1
  198. package/dist/main.js +4 -3
  199. package/dist/main.js.map +1 -1
  200. package/dist/mods/clients.d.ts +3 -0
  201. package/dist/mods/clients.d.ts.map +1 -0
  202. package/dist/mods/clients.js +4 -0
  203. package/dist/mods/clients.js.map +1 -0
  204. package/dist/mods/editor-ui/FieldLabel.d.ts +15 -0
  205. package/dist/mods/editor-ui/FieldLabel.d.ts.map +1 -0
  206. package/dist/mods/editor-ui/FieldLabel.js +55 -0
  207. package/dist/mods/editor-ui/FieldLabel.js.map +1 -0
  208. package/dist/mods/editor-ui/client.d.ts +1 -1
  209. package/dist/mods/editor-ui/client.d.ts.map +1 -1
  210. package/dist/mods/editor-ui/client.js +1 -1
  211. package/dist/mods/editor-ui/client.js.map +1 -1
  212. package/dist/mods/editor-ui/default-edit.d.ts +2 -0
  213. package/dist/mods/editor-ui/default-edit.d.ts.map +1 -0
  214. package/dist/mods/editor-ui/default-edit.js +54 -0
  215. package/dist/mods/editor-ui/default-edit.js.map +1 -0
  216. package/dist/mods/editor-ui/default-view.d.ts +8 -1
  217. package/dist/mods/editor-ui/default-view.d.ts.map +1 -1
  218. package/dist/mods/editor-ui/default-view.js +8 -5
  219. package/dist/mods/editor-ui/default-view.js.map +1 -1
  220. package/dist/mods/editor-ui/dir-view.js +0 -2
  221. package/dist/mods/editor-ui/dir-view.js.map +1 -1
  222. package/dist/mods/editor-ui/empty-placeholder.d.ts +5 -0
  223. package/dist/mods/editor-ui/empty-placeholder.d.ts.map +1 -0
  224. package/dist/mods/editor-ui/empty-placeholder.js +14 -0
  225. package/dist/mods/editor-ui/empty-placeholder.js.map +1 -0
  226. package/dist/mods/editor-ui/form-field.d.ts +17 -0
  227. package/dist/mods/editor-ui/form-field.d.ts.map +1 -0
  228. package/dist/mods/editor-ui/form-field.js +68 -0
  229. package/dist/mods/editor-ui/form-field.js.map +1 -0
  230. package/dist/mods/editor-ui/form-fields.d.ts +1 -2
  231. package/dist/mods/editor-ui/form-fields.d.ts.map +1 -1
  232. package/dist/mods/editor-ui/form-fields.js +56 -60
  233. package/dist/mods/editor-ui/form-fields.js.map +1 -1
  234. package/dist/mods/editor-ui/layout-view.js +3 -2
  235. package/dist/mods/editor-ui/layout-view.js.map +1 -1
  236. package/dist/mods/editor-ui/list-items.js +1 -1
  237. package/dist/mods/editor-ui/list-items.js.map +1 -1
  238. package/dist/mods/editor-ui/node-utils.d.ts +2 -2
  239. package/dist/mods/editor-ui/node-utils.d.ts.map +1 -1
  240. package/dist/mods/editor-ui/node-utils.js +4 -5
  241. package/dist/mods/editor-ui/node-utils.js.map +1 -1
  242. package/dist/mods/editor-ui/type-picker.d.ts +15 -0
  243. package/dist/mods/editor-ui/type-picker.d.ts.map +1 -0
  244. package/dist/mods/editor-ui/type-picker.js +69 -0
  245. package/dist/mods/editor-ui/type-picker.js.map +1 -0
  246. package/dist/mods/editor-ui/user-view.js +1 -1
  247. package/dist/mods/editor-ui/user-view.js.map +1 -1
  248. package/dist/mods/servers.d.ts +1 -0
  249. package/dist/mods/servers.d.ts.map +1 -0
  250. package/dist/mods/servers.js +4 -0
  251. package/dist/mods/servers.js.map +1 -0
  252. package/dist/mods/treenity/groups/index.js +1 -1
  253. package/dist/mods/treenity/groups/index.js.map +1 -1
  254. package/dist/mods/treenity/preview.js +1 -1
  255. package/dist/mods/treenity/preview.js.map +1 -1
  256. package/dist/mods/treenity/ref-view.js +1 -1
  257. package/dist/mods/treenity/ref-view.js.map +1 -1
  258. package/dist/mods/treenity/schema-form.js +1 -1
  259. package/dist/mods/treenity/schema-form.js.map +1 -1
  260. package/dist/mods/treenity/seed.js +1 -1
  261. package/dist/mods/treenity/seed.js.map +1 -1
  262. package/dist/mods/treenity/type-view.js +1 -1
  263. package/dist/mods/treenity/type-view.js.map +1 -1
  264. package/dist/schema-loader.d.ts +1 -1
  265. package/dist/schema-loader.d.ts.map +1 -1
  266. package/dist/schema-loader.js +1 -1
  267. package/dist/schema-loader.js.map +1 -1
  268. package/dist/symbols.d.ts +5 -0
  269. package/dist/symbols.d.ts.map +1 -0
  270. package/dist/symbols.js +16 -0
  271. package/dist/symbols.js.map +1 -0
  272. package/dist/trpc.d.ts +10 -3
  273. package/dist/trpc.d.ts.map +1 -1
  274. package/package.json +74 -8
  275. package/src/AclEditor.tsx +11 -18
  276. package/src/ActionCards.tsx +224 -0
  277. package/src/App.tsx +204 -385
  278. package/src/ComponentSection.tsx +113 -0
  279. package/src/ErrorBoundary.tsx +37 -0
  280. package/src/Inspector.css +54 -0
  281. package/src/Inspector.tsx +73 -793
  282. package/src/Login.tsx +97 -0
  283. package/src/NodeEditor.tsx +300 -0
  284. package/src/Tree.css +91 -0
  285. package/src/Tree.tsx +40 -43
  286. package/src/bind/bind.test.ts +1 -1
  287. package/src/bind/engine.ts +1 -1
  288. package/src/bind/eval.ts +1 -1
  289. package/src/bind/hook.ts +1 -1
  290. package/src/bind/pipes.ts +1 -1
  291. package/src/cache.ts +5 -1
  292. package/src/client-tree.test.ts +1 -1
  293. package/src/client-tree.ts +22 -16
  294. package/src/client.ts +2 -4
  295. package/src/components/ConfirmDialog.tsx +34 -0
  296. package/src/components/ConfirmPopover.tsx +41 -0
  297. package/src/components/PathBreadcrumb.tsx +36 -0
  298. package/src/components/lib/utils.ts +6 -0
  299. package/src/components/lib/utils.ts.bak +6 -0
  300. package/src/components/ui/accordion.tsx +1 -1
  301. package/src/components/ui/alert-dialog.tsx +189 -0
  302. package/src/components/ui/badge.tsx +1 -1
  303. package/src/components/ui/breadcrumb.tsx +108 -0
  304. package/src/components/ui/button.tsx +53 -31
  305. package/src/components/ui/card.tsx +91 -0
  306. package/src/components/ui/checkbox.tsx +1 -1
  307. package/src/components/ui/collapsible.tsx +31 -0
  308. package/src/components/ui/command.tsx +177 -0
  309. package/src/components/ui/dialog.tsx +1 -2
  310. package/src/components/ui/drawer.tsx +1 -1
  311. package/src/components/ui/dropdown-menu.tsx +256 -0
  312. package/src/components/ui/form-field.tsx +37 -0
  313. package/src/components/ui/input.tsx +1 -1
  314. package/src/components/ui/label.tsx +1 -1
  315. package/src/components/ui/pagination.tsx +127 -0
  316. package/src/components/ui/popover.tsx +2 -2
  317. package/src/components/ui/progress.tsx +1 -1
  318. package/src/components/ui/resizable.tsx +47 -0
  319. package/src/components/ui/scroll-area.tsx +55 -0
  320. package/src/components/ui/select.tsx +1 -1
  321. package/src/components/ui/separator.tsx +27 -0
  322. package/src/components/ui/sheet.tsx +140 -0
  323. package/src/components/ui/skeleton.tsx +13 -0
  324. package/src/components/ui/slider.tsx +1 -1
  325. package/src/components/ui/switch.tsx +1 -1
  326. package/src/components/ui/table.tsx +115 -0
  327. package/src/components/ui/tabs.tsx +88 -0
  328. package/src/components/ui/textarea.tsx +1 -1
  329. package/src/components/ui/toggle-group.tsx +82 -0
  330. package/src/components/ui/toggle.tsx +46 -0
  331. package/src/components/ui/tooltip.tsx +1 -1
  332. package/src/context/index.tsx +75 -42
  333. package/src/events.ts +81 -0
  334. package/src/fiber-tree.ts +112 -0
  335. package/src/hooks.ts +88 -9
  336. package/src/idb.ts +1 -1
  337. package/src/lib/to-plain.ts +21 -0
  338. package/src/main.tsx +3 -1
  339. package/src/mods/clients.ts +3 -0
  340. package/src/mods/editor-ui/FieldLabel.tsx +124 -0
  341. package/src/mods/editor-ui/client.ts +1 -1
  342. package/src/mods/editor-ui/default-edit.tsx +99 -0
  343. package/src/mods/editor-ui/default-view.tsx +13 -8
  344. package/src/mods/editor-ui/dir-view.tsx +2 -2
  345. package/src/mods/editor-ui/editor-ui.css +174 -0
  346. package/src/mods/editor-ui/empty-placeholder.tsx +39 -0
  347. package/src/mods/editor-ui/form-field.tsx +144 -0
  348. package/src/mods/editor-ui/form-fields.tsx +132 -113
  349. package/src/mods/editor-ui/layout-view.tsx +4 -2
  350. package/src/mods/editor-ui/list-items.tsx +2 -2
  351. package/src/mods/editor-ui/node-utils.ts +4 -5
  352. package/src/mods/editor-ui/type-picker.tsx +147 -0
  353. package/src/mods/editor-ui/user-view.tsx +1 -1
  354. package/src/mods/servers.ts +2 -0
  355. package/src/mods/treenity/groups/index.tsx +1 -1
  356. package/src/mods/treenity/preview.tsx +1 -1
  357. package/src/mods/treenity/ref-view.tsx +1 -1
  358. package/src/mods/treenity/schema-form.tsx +1 -1
  359. package/src/mods/treenity/seed.ts +1 -1
  360. package/src/mods/treenity/type-view.tsx +1 -1
  361. package/src/optimistic.test.ts +111 -0
  362. package/src/remote-tree.test.ts +1 -1
  363. package/src/remote-tree.ts +1 -1
  364. package/src/root.css +117 -0
  365. package/src/schema-loader.ts +1 -1
  366. package/src/symbols.ts +18 -0
  367. package/src/index.html +0 -14
  368. package/src/style.css +0 -1269
  369. 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 { 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 ──
@@ -322,7 +157,7 @@ export function App() {
322
157
  function onKeyDown(e: KeyboardEvent) {
323
158
  const meta = e.metaKey || e.ctrlKey;
324
159
  if (!meta) return;
325
- if (document.querySelector('.type-picker-overlay')) return;
160
+ if (document.querySelector('[data-slot="dialog-overlay"]')) return;
326
161
  if (e.key === '/' && selected) {
327
162
  e.preventDefault();
328
163
  setAddingComponentAt(selected);
@@ -348,12 +183,8 @@ export function App() {
348
183
  }, [showToast]);
349
184
 
350
185
  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
186
+ const { items: children } = await tree.getChildren(path);
187
+ cache.putMany(children, path);
357
188
  setLoaded((prev) => new Set(prev).add(path));
358
189
  }, []);
359
190
 
@@ -400,60 +231,15 @@ export function App() {
400
231
  })();
401
232
  }, [authed, loadChildren, root, mode]);
402
233
 
403
- // Live subscription — server push cache
234
+ // Server event subscription — module-level, refs provide stable access to current state
404
235
  useEffect(() => {
405
236
  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
- },
237
+ startEvents({
238
+ loadChildren,
239
+ getExpanded: () => expandedRef.current,
240
+ getSelected: () => selectedRef.current,
455
241
  });
456
- return () => sub.unsubscribe();
242
+ return stopEvents;
457
243
  }, [authed, loadChildren]);
458
244
 
459
245
  const handleSelect = useCallback(
@@ -562,10 +348,12 @@ export function App() {
562
348
  [loadChildren, showToast],
563
349
  );
564
350
 
565
- const roots = hasRootNode ? [root] : [];
351
+ const roots = hasRootNode ? [root, '/local'] : ['/local'];
352
+
353
+ const [rootPromptOpen, setRootPromptOpen] = useState(false);
354
+ const [rootPromptType, setRootPromptType] = useState('root');
566
355
 
567
- const handleCreateRoot = useCallback(async () => {
568
- const type = prompt('Root node $type:', 'root');
356
+ const handleCreateRoot = useCallback(async (type: string) => {
569
357
  if (!type) return;
570
358
  try {
571
359
  await tree.set({ $path: '/', $type: type } as NodeData);
@@ -580,37 +368,28 @@ export function App() {
580
368
  }, []);
581
369
 
582
370
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
583
- const [menuOpen, setMenuOpen] = useState(false);
584
371
  const [showLoginModal, setShowLoginModal] = useState(false);
585
- const menuRef = useRef<HTMLDivElement>(null);
586
372
 
587
373
  // Re-auth as anon + show login modal when session expires mid-use
588
374
  useEffect(() => {
589
375
  const handler = async () => {
590
376
  if (showLoginModal) return;
591
377
  clearToken();
592
- const { token, userId } = await trpc.anonLogin.mutate();
593
- setToken(token);
594
- setAuthed(userId);
595
- setShowLoginModal(true);
378
+ try {
379
+ const { token, userId } = await trpc.anonLogin.mutate();
380
+ setToken(token);
381
+ setAuthed(userId);
382
+ setShowLoginModal(true);
383
+ } catch {
384
+ toast.error('Server unavailable');
385
+ }
596
386
  };
597
387
  window.addEventListener(AUTH_EXPIRED_EVENT, handler);
598
388
  return () => window.removeEventListener(AUTH_EXPIRED_EVENT, handler);
599
389
  }, [showLoginModal]);
600
390
 
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
391
  const handleLogout = async () => {
612
392
  clearToken();
613
- setMenuOpen(false);
614
393
  const { token, userId } = await trpc.anonLogin.mutate();
615
394
  setToken(token);
616
395
  setAuthed(userId);
@@ -619,7 +398,6 @@ export function App() {
619
398
 
620
399
  const handleClearCache = () => {
621
400
  cache.clear();
622
- setMenuOpen(false);
623
401
  showToast('Cache cleared');
624
402
  location.reload();
625
403
  };
@@ -635,7 +413,10 @@ export function App() {
635
413
  }, [mode, handleSelect]);
636
414
 
637
415
  if (!authChecked) return null;
638
- if (!authed || authed.startsWith('anon:')) return <LoginScreen onLogin={(uid) => setAuthed(uid)} />;
416
+ if (!authed) return <LoginScreen onLogin={(uid) => setAuthed(uid)} />;
417
+
418
+ const isAnon = authed.startsWith('anon:');
419
+ const needsLogin = isAnon || showLoginModal;
639
420
  if (mode === 'view') return <NavigateProvider value={navigate}><ViewPage path={viewPath} /></NavigateProvider>;
640
421
  if (mode === 'preview') return <NavigateProvider value={navigate}><ViewPage path={viewPath} editorLink /></NavigateProvider>;
641
422
 
@@ -645,13 +426,11 @@ export function App() {
645
426
 
646
427
  if (error) {
647
428
  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>
429
+ <div className="flex h-screen items-center justify-center bg-background">
430
+ <div className="flex flex-col items-center gap-3 text-center">
431
+ <span className="text-4xl">&#9888;</span>
432
+ <p className="text-sm text-red-400">{error}</p>
433
+ <Button variant="outline" size="sm" onClick={() => location.reload()}>Retry</Button>
655
434
  </div>
656
435
  </div>
657
436
  );
@@ -659,95 +438,106 @@ export function App() {
659
438
 
660
439
  return (
661
440
  <NavigateProvider value={navigate}>
662
- <div className="app">
663
- <div className={`sidebar${sidebarCollapsed ? ' collapsed' : ''}`}>
664
- <div className="sidebar-header">
665
- <span className="logo">
441
+ <div className="flex h-screen bg-background text-foreground overflow-hidden">
442
+ <ResizablePanelGroup orientation="horizontal" className="h-full">
443
+ <ResizablePanel
444
+ defaultSize={28}
445
+ minSize={150}
446
+ maxSize={450}
447
+ className="flex flex-col border-r border-border"
448
+ >
449
+ {/* Header */}
450
+ <div className="flex items-center gap-2 px-3 py-2.5 border-b border-border/50 shrink-0">
666
451
  <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>
452
+ {!sidebarCollapsed && <span className="text-sm font-semibold tracking-tight">Treenity</span>}
453
+ {!sidebarCollapsed && root !== '/' && (
454
+ <Button variant="ghost" size="sm" className="h-5 px-1.5 font-mono text-[10px] text-muted-foreground" onClick={() => setRoot('/')}>
455
+ &#8962; {root}
456
+ </Button>
457
+ )}
458
+ {!sidebarCollapsed && roots.length === 0 && (
459
+ <Button variant="ghost" size="sm" className="h-5 text-[10px]" onClick={() => { setRootPromptType('root'); setRootPromptOpen(true); }}>
460
+ Create root
461
+ </Button>
462
+ )}
463
+ </div>
464
+
465
+ {/* Search */}
466
+ {!sidebarCollapsed && (
467
+ <div className="flex items-center gap-1 px-2 py-1.5 shrink-0">
468
+ <Input
469
+ ref={searchRef}
470
+ placeholder="Search nodes..."
471
+ value={filter}
472
+ onChange={(e) => setFilter(e.target.value)}
473
+ className="h-7 text-xs bg-muted/50 border-border"
474
+ />
475
+ <Button
476
+ variant={showHidden ? 'secondary' : 'ghost'}
477
+ size="sm"
478
+ className="h-7 w-7 p-0 text-xs text-muted-foreground shrink-0"
479
+ onClick={() => setShowHidden(v => !v)}
480
+ title={showHidden ? 'Hide _ prefixed nodes' : 'Show _ prefixed nodes'}
481
+ >
482
+ _
483
+ </Button>
737
484
  </div>
738
485
  )}
739
- </div>
740
- </div>
741
486
 
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
- />
487
+ {/* Tree */}
488
+ <div className="flex-1 overflow-y-auto overflow-x-hidden">
489
+ <Tree
490
+ roots={roots}
491
+ expanded={expanded}
492
+ loaded={loaded}
493
+ selected={selected}
494
+ filter={filter}
495
+ showHidden={showHidden}
496
+ onSelect={handleSelect}
497
+ onExpand={handleExpand}
498
+ onCreateChild={handleCreateChild}
499
+ onDelete={handleDelete}
500
+ onMove={handleMove}
501
+ />
502
+ </div>
503
+
504
+ {/* Footer */}
505
+ <div className="flex items-center justify-between px-3 py-1.5 border-t border-border/50 text-[11px] text-muted-foreground shrink-0">
506
+ <span className="truncate">
507
+ {authed?.startsWith('anon:') ? `anon:${authed.slice(5, 13)}` : authed} &middot; <NodeCount /> nodes
508
+ </span>
509
+ <DropdownMenu>
510
+ <DropdownMenuTrigger asChild>
511
+ <Button variant="ghost" size="sm" className="h-5 w-5 p-0 text-muted-foreground hover:text-foreground">
512
+ &#9776;
513
+ </Button>
514
+ </DropdownMenuTrigger>
515
+ <DropdownMenuContent align="end" side="top" className="w-36">
516
+ <DropdownMenuItem onClick={handleLogout}>
517
+ {authed?.startsWith('anon:') ? 'Login' : 'Logout'}
518
+ </DropdownMenuItem>
519
+ <DropdownMenuItem onClick={handleClearCache}>
520
+ Clear cache
521
+ </DropdownMenuItem>
522
+ </DropdownMenuContent>
523
+ </DropdownMenu>
524
+ </div>
525
+ </ResizablePanel>
526
+
527
+ <ResizableHandle withHandle />
528
+
529
+ <ResizablePanel defaultSize={72} minSize={40}>
530
+ <Inspector
531
+ path={selected}
532
+ currentUserId={authed ?? undefined}
533
+ onDelete={handleDelete}
534
+ onAddComponent={handleAddComponent}
535
+ onSelect={handleSelect}
536
+ onSetRoot={handleSetRoot}
537
+ toast={showToast}
538
+ />
539
+ </ResizablePanel>
540
+ </ResizablePanelGroup>
751
541
 
752
542
  {creatingAt && <TypePicker onSelect={handlePickType} onCancel={() => setCreatingAt(null)} />}
753
543
 
@@ -756,19 +546,48 @@ export function App() {
756
546
  title="Add Component"
757
547
  nameLabel="Component name"
758
548
  action="Add"
549
+ autoName
759
550
  onSelect={handlePickComponent}
760
551
  onCancel={() => setAddingComponentAt(null)}
761
552
  />
762
553
  )}
763
554
 
764
- {showLoginModal && (
555
+ <AlertDialog open={rootPromptOpen} onOpenChange={setRootPromptOpen}>
556
+ <AlertDialogContent>
557
+ <AlertDialogHeader>
558
+ <AlertDialogTitle>Create root node</AlertDialogTitle>
559
+ </AlertDialogHeader>
560
+ <Input
561
+ value={rootPromptType}
562
+ onChange={(e) => setRootPromptType(e.target.value)}
563
+ placeholder="$type"
564
+ className="font-mono"
565
+ onKeyDown={(e) => {
566
+ if (e.key === 'Enter') {
567
+ setRootPromptOpen(false);
568
+ handleCreateRoot(rootPromptType);
569
+ }
570
+ }}
571
+ />
572
+ <AlertDialogFooter>
573
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
574
+ <AlertDialogAction onClick={() => handleCreateRoot(rootPromptType)}>Create</AlertDialogAction>
575
+ </AlertDialogFooter>
576
+ </AlertDialogContent>
577
+ </AlertDialog>
578
+
579
+ {needsLogin && (
765
580
  <LoginModal
766
581
  onLogin={(uid) => { setAuthed(uid); setShowLoginModal(false); }}
767
- onClose={() => setShowLoginModal(false)}
582
+ onClose={isAnon ? undefined : () => setShowLoginModal(false)}
768
583
  />
769
584
  )}
770
585
 
771
- {toastMsg && <div className={`toast ${toastMsg.type === 'error' ? 'toast-error' : ''}`}>{toastMsg.text}</div>}
586
+ {toastMsg && (
587
+ <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'}`}>
588
+ {toastMsg.text}
589
+ </div>
590
+ )}
772
591
  </div>
773
592
  </NavigateProvider>
774
593
  );