@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/Inspector.tsx CHANGED
@@ -1,26 +1,18 @@
1
1
  // Inspector — view + edit panel for selected node (Unity-style inspector)
2
- // Shell only: delegates rendering to registered views, provides generic edit UI
3
-
2
+ // Shell only: header, rendered view, delegates editing to NodeEditor
3
+
4
+ import './Inspector.css';
5
+ import { ConfirmDialog } from '#components/ConfirmDialog';
6
+ import { PathBreadcrumb } from '#components/PathBreadcrumb';
7
+ import { Badge } from '#components/ui/badge';
8
+ import { Button } from '#components/ui/button';
9
+ import { ScrollArea } from '#components/ui/scroll-area';
4
10
  import { Render, RenderContext } from '#context';
5
- import {
6
- getActions,
7
- getActionSchema,
8
- getComponents,
9
- getPlainFields,
10
- getSchema,
11
- getViewContexts,
12
- pickDefaultContext,
13
- } from '#mods/editor-ui/node-utils';
14
- import { type ComponentData, type GroupPerm, type NodeData, resolve, resolveExact } from '@treenity/core/core';
15
- import type { TypeSchema } from '@treenity/core/schema/types';
16
- import { createElement, useEffect, useRef, useState } from 'react';
17
- import { AclEditor } from './AclEditor';
18
- import * as cache from './cache';
19
- import { set, usePath } from './hooks';
20
- import { useSchema } from './schema-loader';
21
- import { trpc } from './trpc';
22
-
23
- type AnyClass = { new(): Record<string, unknown> };
11
+ import { getViewContexts, pickDefaultContext } from '#mods/editor-ui/node-utils';
12
+ import { useState } from 'react';
13
+ import { ErrorBoundary } from './ErrorBoundary';
14
+ import { usePath } from './hooks';
15
+ import { NodeEditor } from './NodeEditor';
24
16
 
25
17
  type Props = {
26
18
  path: string | null;
@@ -32,553 +24,24 @@ type Props = {
32
24
  toast: (msg: string) => void;
33
25
  };
34
26
 
35
- // Breadcrumb from path
36
- function Breadcrumb({ path, onSelect }: { path: string; onSelect: (p: string) => void }) {
37
- if (path === '/')
38
- return (
39
- <div className="editor-breadcrumb">
40
- <span>/</span>
41
- </div>
42
- );
43
- const parts = path.split('/').filter(Boolean);
44
- const crumbs: { label: string; path: string }[] = [{ label: '/', path: '/' }];
45
- let cur = '';
46
- for (const p of parts) {
47
- cur += '/' + p;
48
- crumbs.push({ label: p, path: cur });
49
- }
50
- return (
51
- <div className="editor-breadcrumb">
52
- {crumbs.map((c, i) => (
53
- <span key={c.path}>
54
- {i > 0 && <span className="sep">/</span>}
55
- <span onClick={() => onSelect(c.path)}>{c.label === '/' ? 'root' : c.label}</span>
56
- </span>
57
- ))}
58
- </div>
59
- );
60
- }
61
-
62
- // Pretty-print action result value
63
- function ResultView({ value }: { value: unknown }) {
64
- if (value === undefined || value === null) return null;
65
- if (typeof value !== 'object')
66
- return <span className="font-mono text-[11px]">{String(value)}</span>;
67
-
68
- // Object/array with typed $type → render via Render
69
- if ('$type' in (value as any)) {
70
- return <Render value={value as ComponentData} />;
71
- }
72
-
73
- // Plain object — key/value pairs
74
- const entries = Object.entries(value as Record<string, unknown>);
75
- if (entries.length === 0) return <span className="text-muted-foreground text-[11px]">empty</span>;
76
-
77
- return (
78
- <div className="flex flex-col gap-0.5">
79
- {entries.map(([k, v]) => (
80
- <div key={k} className="flex gap-2 text-[11px]">
81
- <span className="text-muted-foreground shrink-0">{k}</span>
82
- <span className="font-mono text-foreground/80 truncate">
83
- {typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v ?? '')}
84
- </span>
85
- </div>
86
- ))}
87
- </div>
88
- );
89
- }
90
-
91
- // Action pills — compact action buttons that expand on click
92
- function ActionCardList({
93
- path,
94
- componentName,
95
- compType,
96
- toast,
97
- onActionComplete,
98
- }: {
99
- path: string;
100
- componentName: string;
101
- compType: string;
102
- compData: Record<string, unknown>;
103
- toast: (msg: string) => void;
104
- onActionComplete?: () => void;
105
- }) {
106
- const schema = useSchema(compType);
107
- const [expanded, setExpanded] = useState<string | null>(null);
108
- const [paramsText, setParamsText] = useState<Record<string, string>>({});
109
- const [schemaData, setSchemaData] = useState<Record<string, Record<string, unknown>>>({});
110
- const [running, setRunning] = useState<string | null>(null);
111
- const [results, setResults] = useState<Record<string, { ok: boolean; value: unknown }>>({});
112
- const [resultMode, setResultMode] = useState<Record<string, 'pretty' | 'json'>>({});
113
-
114
- if (schema === undefined) return null;
115
-
116
- const actions = getActions(compType, schema);
117
- if (actions.length === 0) return null;
118
-
119
- async function run(a: string) {
120
- setRunning(a);
121
- try {
122
- const actionSchema = getActionSchema(compType, a);
123
- let data: unknown = {};
124
- if (actionSchema) {
125
- data = schemaData[a] ?? {};
126
- } else {
127
- const raw = (paramsText[a] ?? '').trim();
128
- if (raw && raw !== '{}') {
129
- try { data = JSON.parse(raw); }
130
- catch { toast('Invalid JSON params'); setRunning(null); return; }
131
- }
132
- }
133
- const result = await trpc.execute.mutate({ path, key: componentName, action: a, data });
134
- const fresh = (await trpc.get.query({ path, watch: true })) as NodeData | undefined;
135
- if (fresh) cache.put(fresh);
136
- onActionComplete?.();
137
- setResults((prev) => ({ ...prev, [a]: { ok: true, value: result } }));
138
- setExpanded(a);
139
- } catch (e) {
140
- setResults((prev) => ({
141
- ...prev,
142
- [a]: { ok: false, value: e instanceof Error ? e.message : String(e) },
143
- }));
144
- setExpanded(a);
145
- } finally {
146
- setRunning(null);
147
- }
148
- }
149
-
150
- return (
151
- <div className="action-pills">
152
- <div className="flex flex-wrap gap-1.5">
153
- {actions.map((a) => (
154
- <button
155
- key={a}
156
- className={`action-pill${expanded === a ? ' active' : ''}${running === a ? ' running' : ''}`}
157
- onClick={() => setExpanded(expanded === a ? null : a)}
158
- >
159
- {running === a ? '...' : a}
160
- {results[a] && !results[a].ok && expanded !== a && (
161
- <span className="ml-1 text-destructive">!</span>
162
- )}
163
- {results[a]?.ok && expanded !== a && (
164
- <span className="ml-1 text-primary/60">✓</span>
165
- )}
166
- </button>
167
- ))}
168
- </div>
169
-
170
- {expanded && (() => {
171
- const a = expanded;
172
- const actionSchema = getActionSchema(compType, a);
173
- const hasParams = actionSchema !== null && Object.keys(actionSchema.properties).length > 0;
174
- const noParams = actionSchema !== null && Object.keys(actionSchema.properties).length === 0;
175
- const result = results[a];
176
- const mode = resultMode[a] ?? 'pretty';
177
-
178
- return (
179
- <div className="action-detail">
180
- {/* Params section */}
181
- {hasParams && (
182
- <div className="flex flex-col gap-1.5 mb-2">
183
- {Object.entries(actionSchema!.properties).map(([field, prop]) => {
184
- const p = prop as { type: string; title?: string; format?: string };
185
- const val = (schemaData[a] ?? {})[field];
186
- const setField = (v: unknown) =>
187
- setSchemaData((prev) => ({
188
- ...prev,
189
- [a]: { ...(prev[a] ?? {}), [field]: v },
190
- }));
191
- return (
192
- <div key={field} className="action-detail-field">
193
- <label>{p.title ?? field}</label>
194
- {p.type === 'number' || p.format === 'number' ? (
195
- <input type="number" value={String(val ?? 0)}
196
- onChange={(e) => setField(Number(e.target.value))} />
197
- ) : p.type === 'boolean' ? (
198
- <label className="flex items-center gap-1.5 cursor-pointer">
199
- <input type="checkbox" checked={!!val} className="w-auto"
200
- onChange={(e) => setField(e.target.checked)} />
201
- <span className="text-[11px]">{val ? 'true' : 'false'}</span>
202
- </label>
203
- ) : (
204
- <input value={String(val ?? '')}
205
- onChange={(e) => setField(e.target.value)} />
206
- )}
207
- </div>
208
- );
209
- })}
210
- </div>
211
- )}
212
-
213
- {/* Free-form JSON params for untyped actions */}
214
- {!hasParams && !noParams && (
215
- <textarea
216
- className="action-params-input mb-2"
217
- value={paramsText[a] ?? '{}'}
218
- onChange={(e) => setParamsText((prev) => ({ ...prev, [a]: e.target.value }))}
219
- spellCheck={false}
220
- rows={2}
221
- />
222
- )}
223
-
224
- {/* Run button */}
225
- <button
226
- className="action-run-btn"
227
- disabled={running !== null}
228
- onClick={() => run(a)}
229
- >
230
- {running === a ? '...' : '▶'} {a}
231
- </button>
232
-
233
- {/* Result */}
234
- {result && (
235
- <div className={`action-result-box${result.ok ? '' : ' error'}`}>
236
- {!result.ok ? (
237
- <span className="text-destructive font-mono text-[11px]">{String(result.value)}</span>
238
- ) : result.value === undefined || result.value === null ? (
239
- <span className="text-primary text-[11px]">✓ done</span>
240
- ) : (
241
- <>
242
- <div className="flex items-center justify-between mb-1">
243
- <span className="text-[10px] text-muted-foreground uppercase tracking-wider">Result</span>
244
- {typeof result.value === 'object' && (
245
- <div className="flex gap-0.5">
246
- <button
247
- className={`action-mode-btn${mode === 'pretty' ? ' active' : ''}`}
248
- onClick={() => setResultMode((p) => ({ ...p, [a]: 'pretty' }))}
249
- >View</button>
250
- <button
251
- className={`action-mode-btn${mode === 'json' ? ' active' : ''}`}
252
- onClick={() => setResultMode((p) => ({ ...p, [a]: 'json' }))}
253
- >JSON</button>
254
- </div>
255
- )}
256
- </div>
257
- {mode === 'json' ? (
258
- <pre className="text-[11px] font-mono text-foreground/60 whitespace-pre-wrap break-all leading-relaxed">
259
- {JSON.stringify(result.value, null, 2)}
260
- </pre>
261
- ) : (
262
- <ResultView value={result.value} />
263
- )}
264
- </>
265
- )}
266
- </div>
267
- )}
268
- </div>
269
- );
270
- })()}
271
- </div>
272
- );
273
- }
274
-
275
- function renderField(
276
- name: string,
277
- fieldSchema: {
278
- type: string;
279
- label: string;
280
- placeholder?: string;
281
- readOnly?: boolean;
282
- enum?: string[];
283
- items?: { type?: string; properties?: Record<string, unknown> };
284
- refType?: string;
285
- },
286
- plainData: Record<string, unknown>,
287
- setPlainData: (fn: (prev: Record<string, unknown>) => Record<string, unknown>) => void,
288
- ) {
289
- const ctx = fieldSchema.readOnly ? 'react' : 'react:form';
290
- const handler = resolveExact(fieldSchema.type, ctx) ?? resolveExact('string', ctx);
291
- if (!handler)
292
- return (
293
- <div key={name} className="text-[--danger] text-xs">
294
- No form handler: {fieldSchema.type}
295
- </div>
296
- );
297
- const fieldData: { $type: string; [k: string]: unknown } = {
298
- $type: fieldSchema.type,
299
- value: plainData[name],
300
- label: fieldSchema.label,
301
- placeholder: fieldSchema.placeholder,
302
- };
303
- if (fieldSchema.items) fieldData.items = fieldSchema.items;
304
- if (fieldSchema.enum) fieldData.enum = fieldSchema.enum;
305
- if (fieldSchema.refType) fieldData.refType = fieldSchema.refType;
306
- const isComplex = fieldSchema.type === 'object' || fieldSchema.type === 'array';
307
- return (
308
- <div key={name} className={isComplex ? 'field stack' : 'field'}>
309
- {fieldSchema.type !== 'boolean' && <label>{fieldSchema.label}</label>}
310
- {createElement(handler as any, {
311
- value: fieldData,
312
- onChange: fieldSchema.readOnly
313
- ? undefined
314
- : (next: { value: unknown }) => setPlainData((prev) => ({ ...prev, [name]: next.value })),
315
- })}
316
- </div>
317
- );
318
- }
319
-
320
- // Inline string-array editor for raw component fields without schema
321
- function StringArrayField({
322
- value,
323
- onChange,
324
- }: {
325
- value: unknown[];
326
- onChange: (next: unknown[]) => void;
327
- }) {
328
- const [input, setInput] = useState('');
329
- const isStrings = value.every((v) => typeof v === 'string');
330
-
331
- if (!isStrings) {
332
- return (
333
- <textarea
334
- value={JSON.stringify(value, null, 2)}
335
- onChange={(e) => {
336
- try {
337
- onChange(JSON.parse(e.target.value));
338
- } catch {
339
- /* typing */
340
- }
341
- }}
342
- />
343
- );
344
- }
345
-
346
- const tags = value as string[];
347
- return (
348
- <div className="flex-1 space-y-1">
349
- <div className="flex flex-wrap gap-1">
350
- {tags.map((tag, i) => (
351
- <span
352
- key={i}
353
- className="inline-flex items-center gap-0.5 text-[11px] font-mono bg-muted text-foreground/70 px-1.5 py-0.5 rounded"
354
- >
355
- {tag}
356
- <button
357
- type="button"
358
- className="ml-0.5 border-0 bg-transparent p-0 text-muted-foreground/40 hover:text-foreground leading-none cursor-pointer"
359
- onClick={() => onChange(tags.filter((_, j) => j !== i))}
360
- >
361
- ×
362
- </button>
363
- </span>
364
- ))}
365
- </div>
366
- <input
367
- className="text-xs w-full"
368
- placeholder="Add item..."
369
- value={input}
370
- onChange={(e) => setInput(e.target.value)}
371
- onKeyDown={(e) => {
372
- if (e.key !== 'Enter') return;
373
- e.preventDefault();
374
- const t = input.trim();
375
- if (t && !tags.includes(t)) onChange([...tags, t]);
376
- setInput('');
377
- }}
378
- />
379
- </div>
380
- );
381
- }
382
-
383
- function NodeCard({
384
- path,
385
- type,
386
- onChangeType,
387
- }: {
388
- path: string;
389
- type: string;
390
- onChangeType: (t: string) => void;
391
- }) {
392
- const [open, setOpen] = useState(false);
393
- return (
394
- <div className="card">
395
- <div
396
- className="card-header cursor-pointer select-none"
397
- onClick={() => setOpen((v) => !v)}
398
- >
399
- <span>Node</span>
400
- <span className="flex items-center gap-2 normal-case tracking-normal font-normal text-[11px] font-mono text-foreground/50">
401
- {path}
402
- <span className="text-primary">{type}</span>
403
- {open ? (
404
- <svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m6 9 6 6 6-6"/></svg>
405
- ) : (
406
- <svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m9 18 6-6-6-6"/></svg>
407
- )}
408
- </span>
409
- </div>
410
- {open && (
411
- <div className="card-body">
412
- <div className="field">
413
- <label>$path</label>
414
- <input value={path} readOnly />
415
- </div>
416
- <div className="field">
417
- <label>$type</label>
418
- <input value={type} onChange={(e) => onChangeType(e.target.value)} />
419
- </div>
420
- </div>
421
- )}
422
- </div>
423
- );
424
- }
425
-
426
- function ComponentBody({
427
- ctype, cdata, setCD, path, componentName, toast, onActionComplete,
428
- }: {
429
- ctype: string;
430
- cdata: Record<string, unknown>;
431
- setCD: (fn: (prev: Record<string, unknown>) => Record<string, unknown>) => void;
432
- path: string;
433
- componentName: string;
434
- toast: (msg: string) => void;
435
- onActionComplete?: () => void;
436
- }) {
437
- const cschema = useSchema(ctype);
438
- if (cschema === undefined) return null;
439
-
440
- return (
441
- <div className="card-body">
442
- {cschema && Object.keys(cschema.properties).length > 0 ? (
443
- Object.entries(cschema.properties).map(([field, prop]) => {
444
- const p = prop as {
445
- type: string; title: string; format?: string; description?: string;
446
- readOnly?: boolean; enum?: string[]; items?: { type?: string; properties?: Record<string, unknown> };
447
- refType?: string;
448
- };
449
- return renderField(field, {
450
- type: p.format ?? p.type, label: p.title ?? field, placeholder: p.description,
451
- readOnly: p.readOnly, enum: p.enum, items: p.items, refType: p.refType,
452
- }, cdata, setCD);
453
- })
454
- ) : Object.keys(cdata).length > 0 ? (
455
- Object.entries(cdata).map(([k, v]) => (
456
- <div key={k} className={`field${Array.isArray(v) || (typeof v === 'object' && v !== null) ? ' stack' : ''}`}>
457
- <label>{k}</label>
458
- {typeof v === 'boolean' ? (
459
- <label className="flex items-center gap-2 cursor-pointer">
460
- <input type="checkbox" checked={!!cdata[k]} className="w-auto"
461
- onChange={(e) => setCD((prev) => ({ ...prev, [k]: e.target.checked }))} />
462
- {cdata[k] ? 'true' : 'false'}
463
- </label>
464
- ) : typeof v === 'number' ? (
465
- <input type="number" value={String(cdata[k] ?? 0)}
466
- onChange={(e) => setCD((prev) => ({ ...prev, [k]: Number(e.target.value) }))} />
467
- ) : Array.isArray(v) ? (
468
- <StringArrayField value={cdata[k] as unknown[]}
469
- onChange={(next) => setCD((prev) => ({ ...prev, [k]: next }))} />
470
- ) : typeof v === 'object' ? (
471
- (() => {
472
- const h = resolve('object', 'react:form');
473
- return h
474
- ? createElement(h as any, {
475
- value: { $type: 'object', value: cdata[k] },
476
- onChange: (next: { value: unknown }) => setCD((prev) => ({ ...prev, [k]: next.value })),
477
- })
478
- : <pre className="text-[11px] font-mono text-foreground/60">{JSON.stringify(cdata[k], null, 2)}</pre>;
479
- })()
480
- ) : (
481
- <input value={String(cdata[k] ?? '')}
482
- onChange={(e) => setCD((prev) => ({ ...prev, [k]: e.target.value }))} />
483
- )}
484
- </div>
485
- ))
486
- ) : (
487
- <pre className="text-[11px] font-mono text-foreground/60 bg-muted/30 rounded p-2 whitespace-pre-wrap">
488
- {JSON.stringify(cdata, null, 2)}
489
- </pre>
490
- )}
491
- <ActionCardList path={path} componentName={componentName} compType={ctype} compData={cdata} toast={toast} onActionComplete={onActionComplete} />
492
- </div>
493
- );
494
- }
495
-
496
27
  export function Inspector({ path, currentUserId, onDelete, onAddComponent, onSelect, onSetRoot, toast }: Props) {
497
28
  const node = usePath(path);
498
-
499
- const [context, setContext] = useState('react');
29
+ const [confirmDelete, setConfirmDelete] = useState(false);
500
30
  const [editing, setEditing] = useState(false);
501
- const [nodeType, setNodeType] = useState('');
502
- const [compTexts, setCompTexts] = useState<Record<string, string>>({});
503
- const [compData, setCompData] = useState<Record<string, Record<string, unknown>>>({});
504
- const [plainData, setPlainData] = useState<Record<string, unknown>>({});
505
- const [tab, setTab] = useState<'properties' | 'json'>('properties');
506
- const [jsonText, setJsonText] = useState('');
507
- const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
508
- const [aclOwner, setAclOwner] = useState('');
509
- const [aclRules, setAclRules] = useState<GroupPerm[]>([]);
510
- const [dirty, setDirty] = useState(false);
511
- const [stale, setStale] = useState(false);
512
- const syncedPathRef = useRef<string | null>(null);
513
- const syncedRevRef = useRef<unknown>(null);
514
-
515
- function syncFromNode(n: NodeData) {
516
- setNodeType(n.$type);
517
- setAclOwner((n.$owner as string) ?? '');
518
- setAclRules(n.$acl ? [...(n.$acl as GroupPerm[])] : []);
519
- const texts: Record<string, string> = {};
520
- const cdata: Record<string, Record<string, unknown>> = {};
521
- for (const [name, comp] of getComponents(n)) {
522
- texts[name] = JSON.stringify(comp, null, 2);
523
- const d: Record<string, unknown> = {};
524
- for (const [k, v] of Object.entries(comp)) {
525
- if (!k.startsWith('$')) d[k] = v;
526
- }
527
- cdata[name] = d;
528
- }
529
- setCompTexts(texts);
530
- setCompData(cdata);
531
- setPlainData(getPlainFields(n));
532
- setJsonText(JSON.stringify(n, null, 2));
533
- setTab('properties');
534
- }
535
-
536
- useEffect(() => {
537
- if (!node) return;
538
-
539
- const pathChanged = node.$path !== syncedPathRef.current;
540
- if (pathChanged) {
541
- setContext(pickDefaultContext(node.$type));
542
- syncFromNode(node);
543
- syncedPathRef.current = node.$path;
544
- syncedRevRef.current = node.$rev;
545
- setDirty(false);
546
- setStale(false);
547
- return;
548
- }
549
-
550
- if (node.$rev !== syncedRevRef.current) {
551
- if (dirty) {
552
- setStale(true);
553
- } else {
554
- syncFromNode(node);
555
- syncedRevRef.current = node.$rev;
556
- }
557
- }
558
- }, [node?.$path, node?.$rev]);
559
-
560
- // Dirty-tracking wrappers — mark form as edited on any user change
561
- const dSetNodeType: typeof setNodeType = (v) => { setNodeType(v); setDirty(true); };
562
- const dSetCompData: typeof setCompData = (v) => { setCompData(v); setDirty(true); };
563
- const dSetPlainData: typeof setPlainData = (v) => { setPlainData(v); setDirty(true); };
564
- const dSetJsonText: typeof setJsonText = (v) => { setJsonText(v); setDirty(true); };
565
- const dSetAclOwner: typeof setAclOwner = (v) => { setAclOwner(v); setDirty(true); };
566
- const dSetAclRules: typeof setAclRules = (v) => { setAclRules(v); setDirty(true); };
31
+ const [context, setContext] = useState('react');
567
32
 
568
- function handleReset() {
569
- if (!node) return;
570
- const current = cache.get(node.$path) ?? node;
571
- syncFromNode(current);
572
- syncedRevRef.current = current.$rev;
573
- setDirty(false);
574
- setStale(false);
33
+ // Reset context when path changes
34
+ const [prevPath, setPrevPath] = useState(path);
35
+ if (path !== prevPath) {
36
+ setPrevPath(path);
37
+ if (node) setContext(pickDefaultContext(node.$type));
575
38
  }
576
39
 
577
40
  if (!node) {
578
41
  return (
579
- <div className="editor">
580
- <div className="editor-empty">
581
- <div className="icon">&#9741;</div>
42
+ <div className="flex flex-1 flex-col overflow-hidden bg-background">
43
+ <div className="flex flex-1 flex-col items-center justify-center gap-2 text-muted-foreground/40">
44
+ <div className="text-[32px] opacity-30">&#9741;</div>
582
45
  <p>Select a node to inspect</p>
583
46
  </div>
584
47
  </div>
@@ -586,272 +49,89 @@ export function Inspector({ path, currentUserId, onDelete, onAddComponent, onSel
586
49
  }
587
50
 
588
51
  const nodeName = node.$path === '/' ? '/' : node.$path.slice(node.$path.lastIndexOf('/') + 1);
589
- const components = getComponents(node);
590
52
  const viewContexts = getViewContexts(node.$type, node);
591
- const schemaHandler = resolve(node.$type, 'schema');
592
- const schema = schemaHandler ? (schemaHandler() as TypeSchema) : null;
593
-
594
- // Main component: when the node IS the component (its $type has a registered class).
595
- // Show the class's fields (with defaults as fallback for unset fields).
596
- const mainCompCls = resolve(node.$type, 'class') as AnyClass | null;
597
- const mainCompDefaults = mainCompCls ? new mainCompCls() : null;
598
-
599
- async function handleSave() {
600
- if (!node) return;
601
- let toSave: NodeData;
602
- if (tab === 'json') {
603
- try {
604
- toSave = JSON.parse(jsonText);
605
- } catch {
606
- toast('Invalid JSON');
607
- return;
608
- }
609
- } else {
610
- toSave = { $path: node.$path, $type: nodeType, ...plainData };
611
- if (aclOwner) toSave.$owner = aclOwner;
612
- if (aclRules.length > 0) toSave.$acl = aclRules;
613
- for (const [name, comp] of components) {
614
- const ctype = (comp as ComponentData).$type;
615
- const cschema = getSchema(ctype);
616
- const cd = compData[name];
617
- if ((cschema || (cd && Object.keys(cd).length > 0)) && cd) {
618
- toSave[name] = { $type: ctype, ...cd };
619
- } else {
620
- const text = compTexts[name];
621
- if (text === undefined) continue;
622
- try {
623
- toSave[name] = JSON.parse(text);
624
- } catch {
625
- toast(`Invalid JSON in component: ${name}`);
626
- return;
627
- }
628
- }
629
- }
630
- }
631
- await set(toSave);
632
- const fresh = cache.get(node.$path);
633
- if (fresh) {
634
- syncFromNode(fresh);
635
- syncedRevRef.current = fresh.$rev;
636
- }
637
- setDirty(false);
638
- setStale(false);
639
- toast('Saved');
640
- }
641
-
642
- function handleAdd() {
643
- if (!node) return;
644
- onAddComponent(node.$path);
645
- }
646
-
647
- function handleRemoveComponent(name: string) {
648
- if (!node) return;
649
- const next = { ...node };
650
- delete next[name];
651
- set(next);
652
- }
653
-
654
- function toggleCollapse(name: string) {
655
- setCollapsed((prev) => {
656
- const next = new Set(prev);
657
- if (next.has(name)) next.delete(name);
658
- else next.add(name);
659
- return next;
660
- });
661
- }
662
53
 
663
54
  return (
664
55
  <div className="editor">
665
56
  {/* Header */}
666
- <div className="editor-header">
667
- <Breadcrumb path={node.$path} onSelect={onSelect} />
668
- <div className="editor-title">
57
+ <div className="px-6 pt-4 pb-3 border-b border-border bg-card shrink-0">
58
+ <PathBreadcrumb path={node.$path} onSelect={onSelect} />
59
+ <div className="flex items-center gap-2.5 flex-wrap">
669
60
  <h2>{nodeName}</h2>
670
- <span className="editor-type-badge">{node.$type}</span>
61
+ <Badge variant="outline" className="font-mono text-[10px]">{node.$type}</Badge>
671
62
  <a
672
63
  href={node.$path}
673
64
  target="_blank"
674
65
  rel="noopener"
675
- className="text-[11px] text-[--text-3] hover:text-[--accent] no-underline"
66
+ className="text-[11px] text-muted-foreground hover:text-primary no-underline"
676
67
  >
677
68
  View &#8599;
678
69
  </a>
679
70
  {onSetRoot && (
680
- <button
681
- className="sm ghost text-[11px]"
682
- onClick={() => onSetRoot(node.$path)}
683
- title="Focus subtree"
684
- >
71
+ <Button variant="ghost" size="sm" className="h-6 px-1.5 text-[11px]" onClick={() => onSetRoot(node.$path)} title="Focus subtree">
685
72
  &#8962;
686
- </button>
73
+ </Button>
687
74
  )}
688
75
  {viewContexts.length > 1 && (
689
- <span className="context-buttons">
76
+ <span className="flex gap-0.5">
690
77
  {viewContexts.map((c) => (
691
- <button
78
+ <Button
692
79
  key={c}
693
- className={`sm context-btn${context === c ? ' active' : ''}`}
80
+ variant={context === c ? 'default' : 'ghost'}
81
+ size="sm"
82
+ className="h-6 px-2 text-[11px]"
694
83
  onClick={() => setContext(c)}
695
84
  >
696
- {c}
697
- </button>
85
+ {c.replace('react:', '')}
86
+ </Button>
698
87
  ))}
699
88
  </span>
700
89
  )}
701
- <span className="spacer" />
702
- <button className={editing ? 'sm' : 'sm primary'} onClick={() => setEditing(!editing)}>
90
+ <span className="flex-1" />
91
+ <Button variant={editing ? 'ghost' : 'default'} size="sm" className="h-7" onClick={() => setEditing(!editing)}>
703
92
  {editing ? 'Close' : 'Edit'}
704
- </button>
705
- <button
706
- className="sm danger"
707
- onClick={() => {
708
- if (confirm(`Delete ${node.$path}?`)) onDelete(node.$path);
709
- }}
93
+ </Button>
94
+ <Button
95
+ variant="destructive"
96
+ size="sm"
97
+ className="h-7"
98
+ onClick={() => setConfirmDelete(true)}
710
99
  >
711
100
  Delete
712
- </button>
101
+ </Button>
102
+ <ConfirmDialog
103
+ open={confirmDelete}
104
+ onOpenChange={setConfirmDelete}
105
+ title={`Delete ${node.$path}?`}
106
+ description="This action cannot be undone."
107
+ variant="destructive"
108
+ onConfirm={() => onDelete(node.$path)}
109
+ />
713
110
  </div>
714
111
  </div>
715
112
 
716
113
  {/* Rendered view */}
717
- <div className="editor-body">
718
- <RenderContext name={context}>
719
- <div className="node-view">
720
- <Render value={node} />
721
- </div>
722
- </RenderContext>
723
- </div>
724
-
725
- {/* Slide-out edit panel */}
726
- <div className={`edit-panel${editing ? ' open' : ''}`}>
727
- <div className="edit-panel-header">
728
- <span>Edit {nodeName}</span>
729
- <button className="sm ghost" onClick={() => setEditing(false)}>
730
- &#10005;
731
- </button>
732
- </div>
733
-
734
- <div className="edit-panel-tabs">
735
- <button
736
- className={`editor-tab${tab === 'properties' ? ' active' : ''}`}
737
- onClick={() => setTab('properties')}
738
- >
739
- Properties
740
- </button>
741
- <button
742
- className={`editor-tab${tab === 'json' ? ' active' : ''}`}
743
- onClick={() => {
744
- setTab('json');
745
- setJsonText(JSON.stringify({ ...node, ...plainData }, null, 2));
746
- }}
747
- >
748
- JSON
749
- </button>
750
- </div>
751
-
752
- <div className="edit-panel-body">
753
- {tab === 'properties' ? (
754
- <>
755
- <NodeCard path={node.$path} type={nodeType} onChangeType={dSetNodeType} />
756
-
757
- <AclEditor
758
- path={node.$path}
759
- owner={aclOwner}
760
- rules={aclRules}
761
- currentUserId={currentUserId}
762
- onChange={(o, r) => {
763
- dSetAclOwner(o);
764
- dSetAclRules(r);
765
- }}
766
- />
767
-
768
- {(mainCompCls || schema) && (
769
- <div className="card">
770
- <div className="card-header">{node.$type}</div>
771
- <ComponentBody
772
- ctype={node.$type}
773
- cdata={plainData}
774
- setCD={(fn) => dSetPlainData(fn)}
775
- path={node.$path}
776
- componentName=""
777
- toast={toast}
778
- onActionComplete={handleReset}
779
- />
780
- </div>
781
- )}
782
-
783
- {components.map(([name, comp]) => (
784
- <div key={name} className="card">
785
- <div className="card-header cursor-pointer select-none" onClick={() => toggleCollapse(name)}>
786
- <span className="font-mono text-[12px]">{name}</span>
787
- <span className="flex items-center gap-2">
788
- <span className="component-type">{(comp as ComponentData).$type}</span>
789
- <button
790
- className="sm danger"
791
- onClick={(e) => { e.stopPropagation(); handleRemoveComponent(name); }}
792
- >
793
- Remove
794
- </button>
795
- </span>
796
- </div>
797
- {!collapsed.has(name) && (
798
- <ComponentBody
799
- ctype={(comp as ComponentData).$type}
800
- cdata={compData[name] ?? {}}
801
- setCD={(fn) => dSetCompData((prev) => ({ ...prev, [name]: fn(prev[name] ?? {}) }))}
802
- path={node.$path}
803
- componentName={name}
804
- toast={toast}
805
- onActionComplete={handleReset}
806
- />
807
- )}
808
- </div>
809
- ))}
810
-
811
- {!schema && !mainCompDefaults && Object.keys(plainData).length > 0 && (
812
- <div className="card">
813
- <div className="card-header">Data</div>
814
- <div className="card-body">
815
- {Object.entries(plainData).map(([k, v]) => (
816
- <div key={k} className="field">
817
- <label>{k}</label>
818
- <input
819
- value={typeof v === 'string' ? v : JSON.stringify(v)}
820
- onChange={(e) =>
821
- dSetPlainData((prev) => ({ ...prev, [k]: e.target.value }))
822
- }
823
- />
824
- </div>
825
- ))}
826
- </div>
827
- </div>
828
- )}
829
- </>
830
- ) : (
831
- <div className="json-view">
832
- <textarea
833
- value={jsonText}
834
- onChange={(e) => dSetJsonText(e.target.value)}
835
- spellCheck={false}
836
- />
837
- </div>
838
- )}
114
+ <ScrollArea className="flex-1">
115
+ <div className="p-4">
116
+ <ErrorBoundary>
117
+ <RenderContext name={context}>
118
+ <div className="node-view">
119
+ <Render value={node} />
120
+ </div>
121
+ </RenderContext>
122
+ </ErrorBoundary>
839
123
  </div>
124
+ </ScrollArea>
840
125
 
841
- <div className="edit-panel-actions">
842
- {stale && (
843
- <button className="ghost" onClick={handleReset} title="Node updated externally">
844
- Reset
845
- </button>
846
- )}
847
- <button className="primary" onClick={handleSave}>
848
- Save
849
- </button>
850
- {tab === 'properties' && (
851
- <button onClick={handleAdd}>+ Component</button>
852
- )}
853
- </div>
854
- </div>
126
+ {/* Slide-out edit panel */}
127
+ <NodeEditor
128
+ node={node}
129
+ open={editing}
130
+ onClose={() => setEditing(false)}
131
+ currentUserId={currentUserId}
132
+ toast={toast}
133
+ onAddComponent={onAddComponent}
134
+ />
855
135
  </div>
856
136
  );
857
137
  }