@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/hooks.ts CHANGED
@@ -5,10 +5,19 @@
5
5
  // execute: action caller
6
6
  // watch: universal async generator
7
7
 
8
- import { type Class, getComp, type TypeProxy } from '@treenity/core/comp';
9
- import { getComponent, type NodeData, normalizeType } from '@treenity/core/core';
8
+ import { getComponent, type NodeData, normalizeType, resolve } from '@treenity/core';
9
+ import { type Class, type TypeProxy } from '@treenity/core/comp';
10
10
  import { deriveURI, parseURI } from '@treenity/core/uri';
11
- import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
11
+ import {
12
+ createContext,
13
+ useCallback,
14
+ useContext,
15
+ useEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ useSyncExternalStore,
20
+ } from 'react';
12
21
  import * as cache from './cache';
13
22
  import { tree } from './client';
14
23
  import { trpc } from './trpc';
@@ -50,7 +59,7 @@ export function usePath<T extends object>(
50
59
  const path = isTyped ? pathOrUri : (parsed?.path ?? null);
51
60
 
52
61
  const node = useSyncExternalStore(
53
- useCallback((cb: () => void) => (path ? cache.subscribePath(path, cb) : () => {}), [path]),
62
+ useCallback((cb: () => void) => (path ? cache.subscribePath(path, cb) : () => { }), [path]),
54
63
  useCallback(() => (path ? cache.get(path) : undefined), [path]),
55
64
  );
56
65
 
@@ -90,7 +99,10 @@ export function useChildren(parentPath: string, opts?: WatchOpts) {
90
99
 
91
100
  trpc.getChildren
92
101
  .query({ path: parentPath, limit: opts?.limit, watch: opts?.watch, watchNew: opts?.watchNew })
93
- .then((result: any) => cache.putMany(result.items as NodeData[], parentPath));
102
+ .then((result: any) => {
103
+ if (result.truncated) console.warn(`[tree] Children of ${parentPath} truncated — results may be incomplete`);
104
+ cache.putMany(result.items as NodeData[], parentPath);
105
+ });
94
106
  }, [parentPath, gen, opts?.limit, opts?.watch, opts?.watchNew]);
95
107
 
96
108
  return useSyncExternalStore(
@@ -102,21 +114,153 @@ export function useChildren(parentPath: string, opts?: WatchOpts) {
102
114
  // ── set: optimistic update + server persist ──
103
115
 
104
116
  export async function set(next: NodeData) {
117
+ const prev = cache.get(next.$path);
105
118
  cache.put(next);
106
- await tree.set(next);
107
- const fresh = await tree.get(next.$path);
108
- if (fresh) cache.put(fresh);
119
+ try {
120
+ await tree.set(next);
121
+ const fresh = await tree.get(next.$path);
122
+ if (fresh) cache.put(fresh);
123
+ } catch (err) {
124
+ // F15: rollback optimistic cache on server reject (validation, ACL, OCC)
125
+ if (prev) cache.put(prev); else cache.remove(next.$path);
126
+ throw err;
127
+ }
109
128
  }
110
129
 
111
130
  // ── execute: action caller ──
112
131
 
113
132
  export const execute = (
114
- path: string, action: string, data?: unknown, type?: string, key?: string,
115
- ) => trpc.execute.mutate({ path, type, key, action, data });
133
+ pathOrUri: string, action: string, data?: unknown, type?: string, key?: string,
134
+ ) => {
135
+ let path = pathOrUri;
136
+ if (!key && pathOrUri.includes('#')) {
137
+ const parsed = parseURI(pathOrUri);
138
+ path = parsed.path;
139
+ key = parsed.key;
140
+ }
141
+
142
+ // Optimistic: resolve class from cache + registry, predict locally
143
+ const cached = cache.get(path);
144
+ if (cached) {
145
+ const compType = type ?? cached.$type;
146
+ const cls = resolve(compType, 'class');
147
+ if (cls) {
148
+ const fn = cls.prototype?.[action];
149
+ if (fn) predictOptimistic(path, cls, key, fn, data);
150
+ }
151
+ }
152
+
153
+ return trpc.execute.mutate({ path, type, key, action, data });
154
+ };
155
+
156
+ // ── useCanWrite: ACL-based write permission check ──
157
+
158
+ const W = 2;
159
+ const permCache = new Map<string, { perm: number; ts: number }>();
160
+ const PERM_TTL = 30_000; // 30s cache
161
+
162
+ export function useCanWrite(path: string | null): boolean {
163
+ const [perm, setPerm] = useState<number>(0);
164
+
165
+ useEffect(() => {
166
+ if (!path) return;
167
+ const cached = permCache.get(path);
168
+ if (cached && Date.now() - cached.ts < PERM_TTL) {
169
+ setPerm(cached.perm);
170
+ return;
171
+ }
172
+ trpc.getPerm.query({ path }).then((p) => {
173
+ permCache.set(path, { perm: p, ts: Date.now() });
174
+ setPerm(p);
175
+ }).catch(() => setPerm(0));
176
+ }, [path]);
177
+
178
+ return (perm & W) !== 0;
179
+ }
180
+
181
+ // ── useAutoSave: throttled auto-persist for editable views ──
182
+ // First save fires after 500ms (responsive), subsequent saves throttled to 2s.
183
+ // Returns [localData, setField, dirty] — local updates are instant, server writes are batched.
184
+
185
+ // TODO: check why unused
186
+ export function useAutoSave(node: NodeData) {
187
+ const [local, setLocal] = useState<Record<string, unknown>>({});
188
+ const dirtyRef = useRef(false);
189
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
190
+ const savingRef = useRef(false);
191
+ const pendingRef = useRef(false);
192
+ const lastSaveRef = useRef(0);
193
+ const nodeRef = useRef(node);
194
+ nodeRef.current = node;
195
+
196
+ const flush = useCallback(async () => {
197
+ if (savingRef.current) { pendingRef.current = true; return; }
198
+ if (!dirtyRef.current) return;
199
+ savingRef.current = true;
200
+ try {
201
+ const merged = { ...nodeRef.current, ...local };
202
+ delete merged.$rev; // skip OCC — force-write
203
+ await set(merged);
204
+ dirtyRef.current = false;
205
+ lastSaveRef.current = Date.now();
206
+ } catch (e) {
207
+ console.error('[autoSave] failed:', e);
208
+ } finally {
209
+ savingRef.current = false;
210
+ if (pendingRef.current) {
211
+ pendingRef.current = false;
212
+ flush();
213
+ }
214
+ }
215
+ }, [local]);
216
+
217
+ const setField = useCallback((field: string, value: unknown) => {
218
+ setLocal(prev => ({ ...prev, [field]: value }));
219
+ dirtyRef.current = true;
220
+
221
+ if (timerRef.current) clearTimeout(timerRef.current);
222
+ const elapsed = Date.now() - lastSaveRef.current;
223
+ const delay = elapsed > 2000 ? 500 : 2000; // first batch fast, then throttle
224
+ timerRef.current = setTimeout(() => { timerRef.current = null; flush(); }, delay);
225
+ }, [flush]);
226
+
227
+ // Flush on unmount
228
+ useEffect(() => () => {
229
+ if (timerRef.current) { clearTimeout(timerRef.current); flush(); }
230
+ }, [flush]);
231
+
232
+ // Reset local on node path change
233
+ useEffect(() => { setLocal({}); dirtyRef.current = false; }, [node.$path]);
234
+
235
+ const merged = useMemo(() => ({ ...node, ...local }), [node, local]);
236
+
237
+ return [merged, setField, dirtyRef.current] as const;
238
+ }
116
239
 
117
240
  // ── Internals ──
118
241
 
119
- const AsyncGenFn = Object.getPrototypeOf(async function* () {}).constructor;
242
+ const AsyncGenFn = Object.getPrototypeOf(async function* () { }).constructor;
243
+ const AsyncFn = Object.getPrototypeOf(async function () { }).constructor;
244
+
245
+ /** Optimistic prediction: run a sync method locally on a cloned cached node */
246
+ export function predictOptimistic<T extends object>(
247
+ path: string, cls: Class<T>, key: string | undefined,
248
+ fn: Function, data: unknown,
249
+ ): void {
250
+ if (fn instanceof AsyncFn) return;
251
+
252
+ const cached = cache.get(path);
253
+ if (!cached) return;
254
+
255
+ try {
256
+ const draft = structuredClone(cached);
257
+ const target = getComponent(draft, cls, key);
258
+ if (!target) return;
259
+
260
+ fn.call(target, data);
261
+ cache.put(draft);
262
+ } catch { /* prediction failed — server-only */ }
263
+ }
120
264
 
121
265
  function streamToAsyncIterable<T>(
122
266
  input: { path: string; type?: string; key?: string; action: string; data?: unknown },
@@ -157,7 +301,7 @@ function makeProxy<T extends object>(
157
301
  ): TypeProxy<T> {
158
302
  const type = normalizeType(cls);
159
303
  const comp = node
160
- ? (key ? getComponent(node, key) : getComp(node, cls))
304
+ ? (key ? getComponent(node, key) : getComponent(node, cls))
161
305
  : undefined;
162
306
 
163
307
  return new Proxy(comp ?? {}, {
@@ -166,7 +310,11 @@ function makeProxy<T extends object>(
166
310
  if (typeof fn === 'function') {
167
311
  if (fn instanceof AsyncGenFn)
168
312
  return (data?: unknown) => streamToAsyncIterable({ path, type, key, action: prop, data });
169
- return (data?: unknown) => trpc.execute.mutate({ path, type, key, action: prop, data });
313
+
314
+ return (data?: unknown) => {
315
+ predictOptimistic(path, cls, key, fn, data);
316
+ return trpc.execute.mutate({ path, type, key, action: prop, data });
317
+ };
170
318
  }
171
319
  return (comp as any)?.[prop];
172
320
  },
package/src/idb.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Raw IDB API, no dependencies. Fire-and-forget friendly.
3
3
  // Degrades silently if IDB unavailable (private browsing, SSR).
4
4
 
5
- import type { NodeData } from '@treenity/core/core';
5
+ import type { NodeData } from '@treenity/core';
6
6
 
7
7
  const DB_NAME = 'treenity';
8
8
  const DB_VERSION = 1;
package/src/lib/minimd.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Minimal markdown → HTML for chat bubbles (~50 lines)
2
2
  // Covers: headers, bold, italic, inline code, code blocks, links, lists, blockquotes, hr
3
3
  import './minimd.css';
4
+ import { sanitizeHref } from './sanitize-href';
4
5
 
5
6
  const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
6
7
 
@@ -11,7 +12,12 @@ function inline(s: string): string {
11
12
  .replace(/__(.+?)__/g, '<strong>$1</strong>')
12
13
  .replace(/\*(.+?)\*/g, '<em>$1</em>')
13
14
  .replace(/_(.+?)_/g, '<em>$1</em>')
14
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
15
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
16
+ const safe = sanitizeHref(url);
17
+ if (!safe) return text;
18
+ const safeUrl = safe.replace(/"/g, '&quot;');
19
+ return `<a href="${safeUrl}" target="_blank" rel="noopener">${text}</a>`;
20
+ });
15
21
  }
16
22
 
17
23
  export function minimd(src: string): string {
@@ -0,0 +1,13 @@
1
+ // Allowlist-based href sanitizer — blocks javascript:, data:, vbscript:, etc.
2
+ // Strips control characters before protocol check to defeat browser bypass vectors
3
+ // (e.g. "java\tscript:" → browsers collapse to "javascript:")
4
+
5
+ const SAFE_PROTOCOL = /^(https?:|mailto:|tel:|\/|#)/i;
6
+
7
+ /** Returns sanitized URL string, or null if the protocol is unsafe. */
8
+ export function sanitizeHref(url: string): string | null {
9
+ const trimmed = url.replace(/[\x00-\x20]+/g, '');
10
+ if (!trimmed) return null;
11
+ if (SAFE_PROTOCOL.test(trimmed)) return url.trim();
12
+ return null;
13
+ }
@@ -0,0 +1,21 @@
1
+ // Deep-clone any value into a plain JSON-safe object.
2
+ // Strips valtio proxies, preserves Dates, drops functions/symbols.
3
+
4
+ export function toPlain<T>(value: T): T {
5
+ if (value === null || value === undefined) return value;
6
+
7
+ if (value instanceof Date) return new Date(value.getTime()) as T;
8
+
9
+ if (Array.isArray(value)) return value.map(toPlain) as T;
10
+
11
+ if (typeof value === 'object') {
12
+ const out: Record<string, unknown> = {};
13
+ for (const [k, v] of Object.entries(value)) {
14
+ if (typeof v === 'function' || typeof v === 'symbol') continue;
15
+ out[k] = toPlain(v);
16
+ }
17
+ return out as T;
18
+ }
19
+
20
+ return value;
21
+ }
package/src/main.tsx CHANGED
@@ -1,22 +1,32 @@
1
1
  import 'reflect-metadata';
2
2
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
3
  import { enablePatches } from 'immer';
4
- import { StrictMode } from 'react';
4
+ import { StrictMode, type ReactNode } from 'react';
5
5
  import { createRoot } from 'react-dom/client';
6
6
  import { App } from './App';
7
7
  import './load-client';
8
- import './style.css';
8
+ import { Toaster } from './components/ui/sonner';
9
+ import './root.css';
9
10
 
10
11
  enablePatches();
11
12
 
12
13
  const queryClient = new QueryClient();
13
14
 
15
+ // StrictMode off: FlowGram inversify container breaks on double-mount
16
+ // https://github.com/bytedance/flowgram.ai/issues/402
17
+ // TODO: re-enable once FlowGram fixes React 19 StrictMode support
18
+ // const Strict = import.meta.env.VITE_STRICT_MODE !== 'false'
19
+ // ? StrictMode
20
+ // : ({ children }: { children: ReactNode }) => children;
21
+ const Strict = ({ children }: { children: ReactNode }) => children;
22
+
14
23
  const root = document.getElementById('root');
15
24
  if (!root) throw new Error('No #root element');
16
25
  createRoot(root).render(
17
- <StrictMode>
26
+ <Strict>
18
27
  <QueryClientProvider client={queryClient}>
19
28
  <App />
29
+ <Toaster />
20
30
  </QueryClientProvider>
21
- </StrictMode>,
31
+ </Strict>,
22
32
  );
@@ -0,0 +1,3 @@
1
+ // Barrel: all react mod client registrations
2
+ import './editor-ui/client';
3
+ import './treenity/client';
@@ -0,0 +1,125 @@
1
+ // FieldLabel — interactive label for Inspector fields
2
+ // Click → dropdown menu (value/$ref/$map + copy/clear), drop target for tree nodes
3
+
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuTrigger,
10
+ } from '#components/ui/dropdown-menu';
11
+ import { Input } from '#components/ui/input';
12
+ import { isRef } from '@treenity/core';
13
+ import { useState } from 'react';
14
+
15
+ type FieldMode = 'value' | 'ref' | 'map';
16
+
17
+ function getFieldMode(v: unknown): FieldMode {
18
+ if (v && typeof v === 'object' && isRef(v)) {
19
+ return (v as { $map?: string }).$map !== undefined ? 'map' : 'ref';
20
+ }
21
+ return 'value';
22
+ }
23
+
24
+ const MODE_LABELS: Record<FieldMode, string> = { value: 'val', ref: '$ref', map: '$map' };
25
+
26
+ /** Interactive field label — click for mode menu, drop target for tree nodes */
27
+ export function FieldLabel({ label, value, onChange }: {
28
+ label: string;
29
+ value: unknown;
30
+ onChange?: (next: unknown) => void;
31
+ }) {
32
+ const [dragOver, setDragOver] = useState(false);
33
+ const mode = getFieldMode(value);
34
+
35
+ function switchMode(next: FieldMode) {
36
+ if (!onChange || next === mode) return;
37
+ if (next === 'value') {
38
+ onChange(0);
39
+ } else if (next === 'ref') {
40
+ onChange({ $ref: '.' });
41
+ } else {
42
+ const r = isRef(value) ? (value as { $ref: string }).$ref : '.';
43
+ onChange({ $ref: r, $map: '' });
44
+ }
45
+ }
46
+
47
+ if (!onChange) {
48
+ return <label>{label}</label>;
49
+ }
50
+
51
+ return (
52
+ <label
53
+ className={dragOver ? 'text-primary cursor-pointer' : 'cursor-pointer'}
54
+ onDragOver={(e) => {
55
+ if (e.dataTransfer.types.includes('application/treenity-path')) {
56
+ e.preventDefault();
57
+ setDragOver(true);
58
+ }
59
+ }}
60
+ onDragLeave={() => setDragOver(false)}
61
+ onDrop={(e) => {
62
+ e.preventDefault();
63
+ setDragOver(false);
64
+ const path = e.dataTransfer.getData('application/treenity-path');
65
+ if (path && onChange) {
66
+ const existing = isRef(value) ? (value as { $map?: string }).$map : undefined;
67
+ onChange(existing !== undefined ? { $ref: path, $map: existing } : { $ref: path });
68
+ }
69
+ }}
70
+ >
71
+ <DropdownMenu>
72
+ <DropdownMenuTrigger asChild>
73
+ <span className="block overflow-hidden text-ellipsis">{label}</span>
74
+ </DropdownMenuTrigger>
75
+ <DropdownMenuContent align="start" className="min-w-[100px]">
76
+ {(['value', 'ref', 'map'] as FieldMode[]).map((m) => (
77
+ <DropdownMenuItem key={m} onClick={() => switchMode(m)}>
78
+ {mode === m ? '\u25CF ' : '\u00A0\u00A0'}{MODE_LABELS[m]}
79
+ </DropdownMenuItem>
80
+ ))}
81
+ <DropdownMenuSeparator />
82
+ <DropdownMenuItem onClick={() => navigator.clipboard.writeText(JSON.stringify(value))}>
83
+ Copy
84
+ </DropdownMenuItem>
85
+ <DropdownMenuItem onClick={() => onChange(undefined)}>
86
+ Clear
87
+ </DropdownMenuItem>
88
+ </DropdownMenuContent>
89
+ </DropdownMenu>
90
+ </label>
91
+ );
92
+ }
93
+
94
+ /** Inline ref/map editor — $ref + optional $map, compact single/double row */
95
+ export function RefEditor({ value, onChange }: {
96
+ value: { $ref: string; $map?: string };
97
+ onChange: (next: unknown) => void;
98
+ }) {
99
+ const hasMap = value.$map !== undefined;
100
+
101
+ return (
102
+ <div className="flex flex-col gap-1 flex-1 min-w-0">
103
+ <div className="flex items-center gap-1">
104
+ <span className="text-[9px] text-muted-foreground shrink-0 w-5">$ref</span>
105
+ <Input
106
+ className="h-7 text-xs flex-1 min-w-0"
107
+ value={value.$ref}
108
+ onChange={(e) => onChange(hasMap ? { $ref: e.target.value, $map: value.$map } : { $ref: e.target.value })}
109
+ placeholder="path"
110
+ />
111
+ </div>
112
+ {hasMap && (
113
+ <div className="flex items-center gap-1">
114
+ <span className="text-[9px] text-muted-foreground shrink-0 w-5">$map</span>
115
+ <Input
116
+ className="h-7 text-xs flex-1 min-w-0"
117
+ value={value.$map ?? ''}
118
+ onChange={(e) => onChange({ $ref: value.$ref, $map: e.target.value })}
119
+ placeholder="field"
120
+ />
121
+ </div>
122
+ )}
123
+ </div>
124
+ );
125
+ }
@@ -1,7 +1,7 @@
1
1
  import './default-view';
2
+ import './default-edit';
2
3
  import './layout-view';
3
4
  import './user-view';
4
- import './dir-view';
5
5
  import './list-items';
6
6
  import { registerFormFields } from './form-fields';
7
7
 
@@ -0,0 +1,101 @@
1
+ import { Checkbox } from '#components/ui/checkbox';
2
+ import { Input } from '#components/ui/input';
3
+ import { useSchema } from '#schema-loader';
4
+ import { type ComponentData, isRef, register, resolve } from '@treenity/core';
5
+ import { createElement } from 'react';
6
+ import { FieldLabel, RefEditor } from './FieldLabel';
7
+ import { renderField, StringArrayField } from './form-field';
8
+
9
+ function DefaultEditForm({ value, onChange }: { value: ComponentData; onChange?: (next: ComponentData) => void }) {
10
+ const schema = useSchema(value.$type);
11
+ if (schema === undefined) return null;
12
+
13
+ const data: Record<string, unknown> = {};
14
+ for (const [k, v] of Object.entries(value)) {
15
+ if (!k.startsWith('$')) data[k] = v;
16
+ }
17
+
18
+ const setData = (fn: (prev: Record<string, unknown>) => Record<string, unknown>) => {
19
+ if (!onChange) return;
20
+ const next = fn(data);
21
+ onChange({ ...value, ...next } as ComponentData);
22
+ };
23
+
24
+ // Schema-driven form
25
+ if (schema && Object.keys(schema.properties).length > 0) {
26
+ return (
27
+ <div className="py-0.5 pb-2.5">
28
+ {Object.entries(schema.properties).map(([field, prop]) => {
29
+ const p = prop as {
30
+ type: string; title: string; format?: string; description?: string;
31
+ readOnly?: boolean; enum?: string[]; items?: { type?: string; properties?: Record<string, unknown> };
32
+ refType?: string;
33
+ };
34
+ return renderField(field, {
35
+ type: p.format ?? p.type, label: p.title ?? field, placeholder: p.description,
36
+ readOnly: p.readOnly || !onChange, enum: p.enum, items: p.items, refType: p.refType,
37
+ }, data, setData);
38
+ })}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ // Fallback: raw field rendering
44
+ if (Object.keys(data).length > 0) {
45
+ return (
46
+ <div className="py-0.5 pb-2.5">
47
+ {Object.entries(data).map(([k, v]) => {
48
+ const onCh = (next: unknown) => setData((prev) => ({ ...prev, [k]: next }));
49
+ if (v && typeof v === 'object' && isRef(v)) {
50
+ return (
51
+ <div key={k} className="field">
52
+ <FieldLabel label={k} value={v} onChange={onCh} />
53
+ <RefEditor value={v as { $ref: string; $map?: string }} onChange={onCh} />
54
+ </div>
55
+ );
56
+ }
57
+ return (
58
+ <div key={k} className={`field${Array.isArray(v) || (typeof v === 'object' && v !== null) ? ' stack' : ''}`}>
59
+ <FieldLabel label={k} value={v} onChange={onCh} />
60
+ {typeof v === 'boolean' ? (
61
+ <label className="flex items-center gap-2 cursor-pointer">
62
+ <Checkbox checked={!!data[k]}
63
+ onChange={(e) => setData((prev) => ({ ...prev, [k]: (e.target as HTMLInputElement).checked }))} />
64
+ {data[k] ? 'true' : 'false'}
65
+ </label>
66
+ ) : typeof v === 'number' ? (
67
+ <Input type="number" className="h-7 text-xs" value={String(data[k] ?? 0)}
68
+ onChange={(e) => setData((prev) => ({ ...prev, [k]: Number(e.target.value) }))} />
69
+ ) : Array.isArray(v) ? (
70
+ <StringArrayField value={data[k] as unknown[]}
71
+ onChange={(next) => setData((prev) => ({ ...prev, [k]: next }))} />
72
+ ) : typeof v === 'object' ? (
73
+ (() => {
74
+ const h = resolve('object', 'react:form');
75
+ return h
76
+ ? createElement(h as any, {
77
+ value: { $type: 'object', value: data[k] },
78
+ onChange: (next: { value: unknown }) => setData((prev) => ({ ...prev, [k]: next.value })),
79
+ })
80
+ : <pre className="text-[11px] font-mono text-foreground/60">{JSON.stringify(data[k], null, 2)}</pre>;
81
+ })()
82
+ ) : (
83
+ <Input className="h-7 text-xs" value={String(data[k] ?? '')}
84
+ onChange={(e) => setData((prev) => ({ ...prev, [k]: e.target.value }))} />
85
+ )}
86
+ </div>
87
+ );
88
+ })}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ // Empty
94
+ return (
95
+ <pre className="text-[11px] font-mono text-foreground/60 bg-muted/30 rounded p-2 whitespace-pre-wrap">
96
+ {JSON.stringify(data, null, 2)}
97
+ </pre>
98
+ );
99
+ }
100
+
101
+ register('default', 'react:edit', DefaultEditForm as any);
@@ -1,8 +1,11 @@
1
+ import './editor-ui.css';
2
+ import { Button } from '#components/ui/button';
1
3
  import { Render, RenderContext } from '#context';
2
4
  import { useChildren } from '#hooks';
3
5
  import { trpc } from '#trpc';
4
- import { type ComponentData, type NodeData, register } from '@treenity/core/core';
6
+ import { type ComponentData, type NodeData, register } from '@treenity/core';
5
7
  import { useCallback, useState } from 'react';
8
+ import { EmptyNodePlaceholder } from './empty-placeholder';
6
9
  import { getComponents, getPlainFields, getSchema } from './node-utils';
7
10
 
8
11
  /** Fallback for components without their own react handler */
@@ -45,7 +48,7 @@ function ComponentFieldsView({ value }: { value: ComponentData }) {
45
48
  );
46
49
  }
47
50
 
48
- function GenerateViewButton({ type, sample }: { type: string; sample: NodeData }) {
51
+ export function GenerateViewButton({ type, sample, context, label }: { type: string; sample: NodeData; context?: string; label?: string }) {
49
52
  const [status, setStatus] = useState<'idle' | 'generating' | 'done' | 'error'>('idle');
50
53
  const [error, setError] = useState('');
51
54
 
@@ -58,7 +61,7 @@ function GenerateViewButton({ type, sample }: { type: string; sample: NodeData }
58
61
  await trpc.execute.mutate({
59
62
  path: '/metatron',
60
63
  action: 'task',
61
- data: { prompt: `Generate a React view for type "${type}". Sample data:\n${JSON.stringify(clean, null, 2)}` },
64
+ data: { prompt: `Generate a React view for type "${type}"${context ? ` in context "${context}"` : ''}. Sample data:\n${JSON.stringify(clean, null, 2)}` },
62
65
  });
63
66
  setStatus('done');
64
67
  } catch (err: any) {
@@ -78,12 +81,14 @@ function GenerateViewButton({ type, sample }: { type: string; sample: NodeData }
78
81
  }
79
82
 
80
83
  return (
81
- <button
84
+ <Button
85
+ variant="outline"
86
+ size="sm"
82
87
  onClick={generate}
83
- className="text-sm text-blue-400 hover:text-blue-300 border border-blue-400/30 rounded px-3 py-1.5 my-2"
88
+ className="text-blue-400 border-blue-400/30 hover:text-blue-300 my-2"
84
89
  >
85
- Generate AI View
86
- </button>
90
+ {label ?? 'Generate AI View'}
91
+ </Button>
87
92
  );
88
93
  }
89
94
 
@@ -134,7 +139,7 @@ function DefaultNodeView({ value }: { value: NodeData }) {
134
139
  </RenderContext>
135
140
  )}
136
141
 
137
- {children.length === 0 && !hasInfo && <div className="node-empty">Empty node</div>}
142
+ {children.length === 0 && !hasInfo && <EmptyNodePlaceholder value={value} />}
138
143
  </div>
139
144
  );
140
145
  }
@@ -1,6 +1,6 @@
1
1
  import { Render, RenderContext } from '#context';
2
2
  import { useChildren } from '#hooks';
3
- import { type NodeData, register } from '@treenity/core/core';
3
+ import { type NodeData } from '@treenity/core';
4
4
 
5
5
  const STATUS_COLORS: Record<string, [string, string]> = {
6
6
  draft: ['var(--accent-subtle, #1a2a3a)', 'var(--accent)'],
@@ -88,4 +88,4 @@ function FolderView({ value }: { value: NodeData }) {
88
88
  );
89
89
  }
90
90
 
91
- register('dir', 'react', FolderView as any);
91
+ // register('dir', 'react', FolderView as any);