@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/Login.tsx ADDED
@@ -0,0 +1,97 @@
1
+ import { Button } from '#components/ui/button';
2
+ import { Dialog, DialogContent } from '#components/ui/dialog';
3
+ import { Input } from '#components/ui/input';
4
+ import { Label } from '#components/ui/label';
5
+ import { useState } from 'react';
6
+ import { setToken, trpc } from './trpc';
7
+
8
+ function LoginForm({ onLogin }: { onLogin: (userId: string) => void }) {
9
+ const [mode, setMode] = useState<'login' | 'register'>('login');
10
+ const [userId, setUserId] = useState('');
11
+ const [password, setPassword] = useState('');
12
+ const [err, setErr] = useState<string | null>(null);
13
+ const [loading, setLoading] = useState(false);
14
+
15
+ async function handleSubmit(e: React.FormEvent) {
16
+ e.preventDefault();
17
+ if (!userId.trim() || !password) return;
18
+ setLoading(true);
19
+ setErr(null);
20
+ try {
21
+ const fn = mode === 'register' ? trpc.register : trpc.login;
22
+ const res = await fn.mutate({ userId: userId.trim(), password });
23
+ setToken(res.token);
24
+ onLogin(res.userId);
25
+ } catch (e) {
26
+ setErr(e instanceof Error ? e.message : 'Failed');
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ }
31
+
32
+ return (
33
+ <form className="flex flex-col gap-4 w-80 p-8 rounded-lg border border-border bg-card" onSubmit={handleSubmit}>
34
+ <div className="flex items-center justify-center gap-2 mb-2">
35
+ <img src="/treenity.svg" alt="" width="32" height="32" />
36
+ <span className="text-lg font-semibold">Treenity</span>
37
+ </div>
38
+
39
+ <div className="flex flex-col gap-1.5">
40
+ <Label htmlFor="userId">User ID</Label>
41
+ <Input
42
+ id="userId"
43
+ autoFocus
44
+ placeholder="Enter your user ID"
45
+ value={userId}
46
+ onChange={(e) => setUserId(e.target.value)}
47
+ />
48
+ </div>
49
+
50
+ <div className="flex flex-col gap-1.5">
51
+ <Label htmlFor="password">Password</Label>
52
+ <Input
53
+ id="password"
54
+ type="password"
55
+ placeholder="Enter password"
56
+ value={password}
57
+ onChange={(e) => setPassword(e.target.value)}
58
+ />
59
+ </div>
60
+
61
+ {err && <p className="text-sm text-destructive">{err}</p>}
62
+
63
+ <Button type="submit" disabled={loading || !userId.trim() || !password}>
64
+ {loading ? '...' : mode === 'register' ? 'Create account' : 'Sign in'}
65
+ </Button>
66
+
67
+ <Button
68
+ type="button"
69
+ variant="ghost"
70
+ onClick={() => {
71
+ setMode((m) => (m === 'login' ? 'register' : 'login'));
72
+ setErr(null);
73
+ }}
74
+ >
75
+ {mode === 'login' ? 'No account? Register' : 'Have an account? Sign in'}
76
+ </Button>
77
+ </form>
78
+ );
79
+ }
80
+
81
+ export function LoginScreen({ onLogin }: { onLogin: (userId: string) => void }) {
82
+ return (
83
+ <div className="flex items-center justify-center h-screen bg-background">
84
+ <LoginForm onLogin={onLogin} />
85
+ </div>
86
+ );
87
+ }
88
+
89
+ export function LoginModal({ onLogin, onClose }: { onLogin: (userId: string) => void; onClose?: () => void }) {
90
+ return (
91
+ <Dialog open onOpenChange={(open) => { if (!open && onClose) onClose(); }}>
92
+ <DialogContent className="p-0 border-none bg-transparent shadow-none max-w-fit" showCloseButton={!!onClose}>
93
+ <LoginForm onLogin={onLogin} />
94
+ </DialogContent>
95
+ </Dialog>
96
+ );
97
+ }
@@ -0,0 +1,300 @@
1
+ // NodeEditor — self-contained edit panel for a node (properties, components, actions)
2
+ // Reusable: Inspector uses it as a slide-out, but can be embedded anywhere.
3
+
4
+ import { Button } from '#components/ui/button';
5
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '#components/ui/collapsible';
6
+ import { Input } from '#components/ui/input';
7
+ import { ScrollArea } from '#components/ui/scroll-area';
8
+ import { Tabs, TabsList, TabsTrigger } from '#components/ui/tabs';
9
+ import { toPlain } from '#lib/to-plain';
10
+ import { FieldLabel, RefEditor } from '#mods/editor-ui/FieldLabel';
11
+ import { getComponents, getPlainFields, getSchema } from '#mods/editor-ui/node-utils';
12
+ import { type ComponentData, type GroupPerm, isRef, type NodeData, resolve } from '@treenity/core';
13
+ import type { TypeSchema } from '@treenity/core/schema/types';
14
+ import { ChevronRight } from 'lucide-react';
15
+ import { useEffect, useState } from 'react';
16
+ import { proxy, snapshot, useSnapshot } from 'valtio';
17
+ import { AclEditor } from './AclEditor';
18
+ import * as cache from './cache';
19
+ import { ComponentSection } from './ComponentSection';
20
+ import { set } from './hooks';
21
+
22
+ type AnyClass = { new(): Record<string, unknown> };
23
+
24
+ function NodeCard({ path, type, onChangeType }: {
25
+ path: string;
26
+ type: string;
27
+ onChangeType: (t: string) => void;
28
+ }) {
29
+ return (
30
+ <Collapsible className="border-t border-border mt-2 pt-0.5 first:border-t-0 first:mt-0 first:pt-0">
31
+ <CollapsibleTrigger className="flex w-full items-center justify-between py-2 pb-1.5 text-[11px] font-semibold text-muted-foreground uppercase tracking-wider cursor-pointer select-none">
32
+ <span>Node</span>
33
+ <span className="flex items-center gap-2 normal-case tracking-normal font-normal text-[11px] font-mono text-foreground/50">
34
+ {path}
35
+ <span className="text-primary">{type}</span>
36
+ <ChevronRight className="h-3 w-3 transition-transform duration-200 group-data-[state=open]:rotate-90" />
37
+ </span>
38
+ </CollapsibleTrigger>
39
+ <CollapsibleContent>
40
+ <div className="py-0.5 pb-2.5">
41
+ <div className="field">
42
+ <label>$path</label>
43
+ <Input className="h-7 text-xs" value={path} readOnly />
44
+ </div>
45
+ <div className="field">
46
+ <label>$type</label>
47
+ <Input className="h-7 text-xs" value={type} onChange={(e) => onChangeType(e.target.value)} />
48
+ </div>
49
+ </div>
50
+ </CollapsibleContent>
51
+ </Collapsible>
52
+ );
53
+ }
54
+
55
+ export type NodeEditorProps = {
56
+ node: NodeData;
57
+ open: boolean;
58
+ onClose: () => void;
59
+ currentUserId?: string;
60
+ toast: (msg: string) => void;
61
+ onAddComponent: (path: string) => void;
62
+ };
63
+
64
+ export function NodeEditor({ node, open, onClose, currentUserId, toast, onAddComponent }: NodeEditorProps) {
65
+ const [st] = useState(() => proxy({
66
+ nodeType: '',
67
+ compTexts: {} as Record<string, string>,
68
+ compData: {} as Record<string, Record<string, unknown>>,
69
+ plainData: {} as Record<string, unknown>,
70
+ tab: 'properties' as 'properties' | 'json',
71
+ jsonText: '',
72
+ collapsed: { $node: true } as Record<string, boolean>,
73
+ aclOwner: '',
74
+ aclRules: [] as GroupPerm[],
75
+ dirty: false,
76
+ stale: false,
77
+ syncedPath: null as string | null,
78
+ syncedRev: null as unknown,
79
+ }));
80
+ const snap = useSnapshot(st);
81
+
82
+ function syncFromNode(n: NodeData) {
83
+ st.nodeType = n.$type;
84
+ st.aclOwner = (n.$owner as string) ?? '';
85
+ st.aclRules = n.$acl ? [...(n.$acl as GroupPerm[])] : [];
86
+ const texts: Record<string, string> = {};
87
+ const cdata: Record<string, Record<string, unknown>> = {};
88
+ for (const [name, comp] of getComponents(n)) {
89
+ texts[name] = JSON.stringify(comp, null, 2);
90
+ const d: Record<string, unknown> = {};
91
+ for (const [k, v] of Object.entries(comp)) {
92
+ if (!k.startsWith('$')) d[k] = v;
93
+ }
94
+ cdata[name] = d;
95
+ }
96
+ st.compTexts = texts;
97
+ st.compData = cdata;
98
+ st.plainData = getPlainFields(n);
99
+ st.jsonText = JSON.stringify(n, null, 2);
100
+ st.tab = 'properties';
101
+ }
102
+
103
+ useEffect(() => {
104
+ const pathChanged = node.$path !== st.syncedPath;
105
+ if (pathChanged) {
106
+ syncFromNode(node);
107
+ st.syncedPath = node.$path;
108
+ st.syncedRev = node.$rev;
109
+ st.dirty = false;
110
+ st.stale = false;
111
+ return;
112
+ }
113
+
114
+ if (node.$rev !== st.syncedRev) {
115
+ if (st.dirty) {
116
+ st.stale = true;
117
+ } else {
118
+ syncFromNode(node);
119
+ st.syncedRev = node.$rev;
120
+ }
121
+ }
122
+ }, [node.$path, node.$rev]);
123
+
124
+ function handleReset() {
125
+ const current = cache.get(node.$path) ?? node;
126
+ syncFromNode(current);
127
+ st.syncedRev = current.$rev;
128
+ st.dirty = false;
129
+ st.stale = false;
130
+ }
131
+
132
+ const nodeName = node.$path === '/' ? '/' : node.$path.slice(node.$path.lastIndexOf('/') + 1);
133
+ const components = getComponents(node);
134
+ const schemaHandler = resolve(node.$type, 'schema');
135
+ const schema = schemaHandler ? (schemaHandler() as TypeSchema) : null;
136
+ const mainCompCls = resolve(node.$type, 'class') as AnyClass | null;
137
+ const mainCompDefaults = mainCompCls ? new mainCompCls() : null;
138
+
139
+ async function handleSave() {
140
+ const s = toPlain(snapshot(st));
141
+ let toSave: NodeData;
142
+ if (s.tab === 'json') {
143
+ try {
144
+ toSave = JSON.parse(s.jsonText);
145
+ } catch {
146
+ toast('Invalid JSON');
147
+ return;
148
+ }
149
+ } else {
150
+ toSave = { $path: node.$path, $type: s.nodeType, ...s.plainData } as NodeData;
151
+ if (s.aclOwner) toSave.$owner = s.aclOwner;
152
+ if (s.aclRules.length > 0) toSave.$acl = [...s.aclRules] as GroupPerm[];
153
+ for (const [name, comp] of components) {
154
+ const ctype = (comp as ComponentData).$type;
155
+ const cschema = getSchema(ctype);
156
+ const cd = s.compData[name];
157
+ if ((cschema || (cd && Object.keys(cd).length > 0)) && cd) {
158
+ toSave[name] = { $type: ctype, ...cd };
159
+ } else {
160
+ const text = s.compTexts[name];
161
+ if (text === undefined) continue;
162
+ try {
163
+ toSave[name] = JSON.parse(text);
164
+ } catch {
165
+ toast(`Invalid JSON in component: ${name}`);
166
+ return;
167
+ }
168
+ }
169
+ }
170
+ }
171
+ await set(toSave);
172
+ const fresh = cache.get(node.$path);
173
+ if (fresh) {
174
+ syncFromNode(fresh);
175
+ st.syncedRev = fresh.$rev;
176
+ }
177
+ st.dirty = false;
178
+ st.stale = false;
179
+ toast('Saved');
180
+ }
181
+
182
+ function handleRemoveComponent(name: string) {
183
+ const next = { ...node };
184
+ delete next[name];
185
+ set(next);
186
+ }
187
+
188
+ return (
189
+ <div className={`edit-panel${open ? ' open' : ''}`}>
190
+ <div className="edit-panel-header">
191
+ <span>Edit {nodeName}</span>
192
+ <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={onClose}>
193
+ &#10005;
194
+ </Button>
195
+ </div>
196
+
197
+ <Tabs value={snap.tab} onValueChange={(v) => {
198
+ st.tab = v as 'properties' | 'json';
199
+ if (v === 'json') st.jsonText = JSON.stringify({ ...node, ...st.plainData }, null, 2);
200
+ }} className="px-3 pt-2">
201
+ <TabsList className="h-8 bg-secondary">
202
+ <TabsTrigger value="properties" className="text-xs">Properties</TabsTrigger>
203
+ <TabsTrigger value="json" className="text-xs">JSON</TabsTrigger>
204
+ </TabsList>
205
+ </Tabs>
206
+
207
+ <ScrollArea className="flex-1">
208
+ <div className="p-3.5">
209
+ {snap.tab === 'properties' ? (
210
+ <>
211
+ <NodeCard path={node.$path} type={snap.nodeType} onChangeType={(v) => { st.nodeType = v; st.dirty = true; }} />
212
+ <AclEditor
213
+ path={node.$path}
214
+ owner={snap.aclOwner}
215
+ rules={snap.aclRules as GroupPerm[]}
216
+ currentUserId={currentUserId}
217
+ onChange={(o, r) => { st.aclOwner = o; st.aclRules = r; st.dirty = true; }}
218
+ />
219
+
220
+ {/* Main type section */}
221
+ <ComponentSection
222
+ node={node}
223
+ name=""
224
+ compType={node.$type}
225
+ data={snap.plainData as Record<string, unknown>}
226
+ onData={(d) => { st.plainData = d; st.dirty = true; }}
227
+ toast={toast}
228
+ onActionComplete={handleReset}
229
+ />
230
+
231
+ {/* Named components */}
232
+ {components.map(([name, comp]) => (
233
+ <ComponentSection
234
+ key={name}
235
+ node={node}
236
+ name={name}
237
+ compType={(comp as ComponentData).$type}
238
+ data={(snap.compData[name] ?? {}) as Record<string, unknown>}
239
+ onData={(d) => { st.compData[name] = d; st.dirty = true; }}
240
+ collapsed={!!snap.collapsed[name]}
241
+ onToggle={() => { st.collapsed[name] = !st.collapsed[name]; }}
242
+ onRemove={() => handleRemoveComponent(name)}
243
+ toast={toast}
244
+ onActionComplete={handleReset}
245
+ />
246
+ ))}
247
+
248
+ {/* Untyped plain data fallback */}
249
+ {!schema && !mainCompDefaults && Object.keys(snap.plainData).length > 0 && (
250
+ <div className="border-t border-border mt-2 pt-0.5 first:border-t-0 first:mt-0 first:pt-0">
251
+ <div className="flex items-center justify-between py-2 pb-1.5 text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">Data</div>
252
+ <div className="py-0.5 pb-2.5">
253
+ {Object.entries(snap.plainData).map(([k, v]) => {
254
+ const onCh = (next: unknown) => { st.plainData[k] = next; st.dirty = true; };
255
+ return (
256
+ <div key={k} className={`field${typeof v === 'object' && v !== null ? ' stack' : ''}`}>
257
+ <FieldLabel label={k} value={v} onChange={onCh} />
258
+ {typeof v === 'object' && isRef(v) ? (
259
+ <RefEditor value={v as { $ref: string; $map?: string }} onChange={onCh} />
260
+ ) : (
261
+ <Input
262
+ className="h-7 text-xs"
263
+ value={typeof v === 'string' ? v : JSON.stringify(v)}
264
+ onChange={(e) => { st.plainData[k] = e.target.value; st.dirty = true; }}
265
+ />
266
+ )}
267
+ </div>
268
+ );
269
+ })}
270
+ </div>
271
+ </div>
272
+ )}
273
+ </>
274
+ ) : (
275
+ <textarea
276
+ value={snap.jsonText}
277
+ onChange={(e) => { st.jsonText = e.target.value; st.dirty = true; }}
278
+ spellCheck={false}
279
+ />
280
+ )}
281
+ </div>
282
+ </ScrollArea>
283
+
284
+ <div className="edit-panel-actions">
285
+ {snap.stale && (
286
+ <Button variant="ghost" size="sm" onClick={handleReset} title="Node updated externally">
287
+ Reset
288
+ </Button>
289
+ )}
290
+ <Button size="sm" onClick={handleSave}>
291
+ Save
292
+ </Button>
293
+ {snap.tab === 'properties' && (
294
+ <Button variant="outline" size="sm" onClick={() => onAddComponent(node.$path)}>+ Component</Button>
295
+ )}
296
+ </div>
297
+
298
+ </div>
299
+ );
300
+ }
package/src/Tree.css ADDED
@@ -0,0 +1,91 @@
1
+ /* Tree sidebar */
2
+ .tree-node {
3
+ position: relative;
4
+ }
5
+ .tree-children {
6
+ position: relative;
7
+ padding-left: 12px;
8
+ }
9
+ .tree-children::before {
10
+ content: '';
11
+ position: absolute;
12
+ left: 15px;
13
+ top: 0;
14
+ bottom: 8px;
15
+ width: 1px;
16
+ background: var(--border-subtle);
17
+ }
18
+
19
+ .tree-row {
20
+ display: flex;
21
+ align-items: center;
22
+ padding: 1px 8px 1px 0;
23
+ cursor: pointer;
24
+ color: var(--text);
25
+ font-size: 14px;
26
+ height: 28px;
27
+ position: relative;
28
+ user-select: none;
29
+ transition: background var(--transition);
30
+ }
31
+ .tree-row:hover {
32
+ background: var(--surface-2);
33
+ }
34
+ .tree-row.selected {
35
+ background: var(--accent-subtle);
36
+ }
37
+ .tree-row.selected::before {
38
+ content: '';
39
+ position: absolute;
40
+ left: 0;
41
+ top: 2px;
42
+ bottom: 2px;
43
+ width: 2px;
44
+ background: var(--accent);
45
+ border-radius: 1px;
46
+ }
47
+
48
+ .tree-toggle {
49
+ width: 20px;
50
+ height: 28px;
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ color: var(--text-3);
55
+ font-size: 14px;
56
+ flex-shrink: 0;
57
+ transition: color var(--transition);
58
+ }
59
+ .tree-toggle:hover {
60
+ color: var(--text-2);
61
+ }
62
+
63
+ .tree-label {
64
+ flex: 1;
65
+ overflow: hidden;
66
+ text-overflow: ellipsis;
67
+ white-space: nowrap;
68
+ }
69
+ .tree-badge {
70
+ font-size: 10px;
71
+ color: var(--text-3);
72
+ padding: 1px 6px;
73
+ background: var(--surface-3);
74
+ border-radius: 10px;
75
+ flex-shrink: 0;
76
+ font-family: var(--mono);
77
+ cursor: pointer;
78
+ transition: all var(--transition);
79
+ }
80
+ .tree-badge:hover {
81
+ color: var(--text-2);
82
+ background: var(--surface-2);
83
+ }
84
+
85
+ /* Drag */
86
+ .tree-drop-above {
87
+ box-shadow: inset 0 2px 0 var(--accent);
88
+ }
89
+ .tree-drop-below {
90
+ box-shadow: inset 0 -2px 0 var(--accent);
91
+ }
package/src/Tree.tsx CHANGED
@@ -1,4 +1,13 @@
1
- import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
1
+ import './Tree.css';
2
+ import { ConfirmDialog } from '#components/ConfirmDialog';
3
+ import { Badge } from '#components/ui/badge';
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from '#components/ui/dropdown-menu';
10
+ import { useCallback, useRef, useState, useSyncExternalStore } from 'react';
2
11
  import * as cache from './cache';
3
12
 
4
13
  type TreeProps = {
@@ -47,57 +56,45 @@ function BadgeMenu({
47
56
  onCreateChild: (path: string) => void;
48
57
  onDelete?: (path: string) => void;
49
58
  }) {
50
- const [open, setOpen] = useState(false);
51
- const ref = useRef<HTMLDivElement>(null);
52
-
53
- useEffect(() => {
54
- if (!open) return;
55
- const close = (e: MouseEvent) => {
56
- if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
57
- };
58
- document.addEventListener('mousedown', close);
59
- return () => document.removeEventListener('mousedown', close);
60
- }, [open]);
59
+ const [confirmDelete, setConfirmDelete] = useState(false);
61
60
 
62
61
  return (
63
- <div ref={ref} className="tree-badge-wrap">
64
- <span
65
- className="tree-badge"
66
- title={fullType}
67
- onClick={(e) => {
68
- e.stopPropagation();
69
- setOpen(!open);
70
- }}
71
- >
72
- {typeLabel}
73
- </span>
74
- {open && (
75
- <div className="tree-menu">
76
- <button
77
- className="tree-menu-item"
78
- onClick={(e) => {
79
- e.stopPropagation();
80
- setOpen(false);
81
- onCreateChild(path);
82
- }}
62
+ <>
63
+ <DropdownMenu>
64
+ <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
65
+ <Badge
66
+ variant="secondary"
67
+ className="tree-badge cursor-pointer text-[10px] px-1.5 py-0 h-5 font-mono font-normal"
68
+ title={fullType}
83
69
  >
70
+ {typeLabel}
71
+ </Badge>
72
+ </DropdownMenuTrigger>
73
+ <DropdownMenuContent align="end" className="min-w-[120px]" onClick={(e) => e.stopPropagation()}>
74
+ <DropdownMenuItem onClick={() => onCreateChild(path)}>
84
75
  + Add child
85
- </button>
76
+ </DropdownMenuItem>
86
77
  {onDelete && (
87
- <button
88
- className="tree-menu-item danger"
89
- onClick={(e) => {
90
- e.stopPropagation();
91
- setOpen(false);
92
- if (confirm(`Delete ${path}?`)) onDelete(path);
93
- }}
78
+ <DropdownMenuItem
79
+ className="text-destructive focus:text-destructive"
80
+ onClick={() => setConfirmDelete(true)}
94
81
  >
95
82
  × Delete
96
- </button>
83
+ </DropdownMenuItem>
97
84
  )}
98
- </div>
85
+ </DropdownMenuContent>
86
+ </DropdownMenu>
87
+ {onDelete && (
88
+ <ConfirmDialog
89
+ open={confirmDelete}
90
+ onOpenChange={setConfirmDelete}
91
+ title={`Delete ${path}?`}
92
+ description="This action cannot be undone."
93
+ variant="destructive"
94
+ onConfirm={() => onDelete(path)}
95
+ />
99
96
  )}
100
- </div>
97
+ </>
101
98
  );
102
99
  }
103
100
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  import * as cache from '#cache';
6
6
  import { trpc } from '#trpc';
7
- import { isRef, type NodeData, type Ref } from '@treenity/core/core';
7
+ import { isRef, type NodeData, type Ref } from '@treenity/core';
8
8
  import { clearComputed, getComputed, setComputed } from './computed';
9
9
  import { evaluateRef, extractArgPaths, hasOnce, isCollectionRef } from './eval';
10
10
 
package/src/bind/eval.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Resolves source from $ref, applies $map pipeline
3
3
  // #field (self) and #/path.field (external) args resolved from context
4
4
 
5
- import type { NodeData, Ref } from '@treenity/core/core';
5
+ import type { NodeData, Ref } from '@treenity/core';
6
6
  import { isRefArg, type MapExpr, parseMapExpr, type PipeArg } from './parse';
7
7
  import { getPipe } from './pipes';
8
8
 
package/src/bind/hook.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as cache from '#cache';
4
4
  import { set, usePath } from '#hooks';
5
- import { isRef, type NodeData } from '@treenity/core/core';
5
+ import { isRef, type NodeData } from '@treenity/core';
6
6
  import { useCallback, useMemo, useSyncExternalStore } from 'react';
7
7
  import { useSnapshot } from 'valtio';
8
8
  import { proxy } from 'valtio/vanilla';
package/src/bind/pipes.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Pipe registry — Angular-style transforms for $map expressions
2
2
 
3
- import type { NodeData } from '@treenity/core/core';
3
+ import type { NodeData } from '@treenity/core';
4
4
 
5
5
  export type PipeFn = (input: unknown, ...args: unknown[]) => unknown;
6
6
 
package/src/cache.ts CHANGED
@@ -2,8 +2,13 @@
2
2
  // useSyncExternalStore-friendly: stable snapshots, targeted notifications
3
3
  // IDB persistence: fire-and-forget writes, hydrate() on startup.
4
4
 
5
- import type { NodeData } from '@treenity/core/core';
5
+ import type { NodeData } from '@treenity/core';
6
6
  import * as idb from './idb';
7
+ import { stampNode } from './symbols';
8
+
9
+ /** Shallow-freeze in dev mode to catch accidental cache mutation at the source */
10
+ const devFreeze: (node: NodeData) => void =
11
+ import.meta.env?.DEV ? (node) => Object.freeze(node) : () => {};
7
12
 
8
13
  type Sub = () => void;
9
14
 
@@ -109,7 +114,9 @@ export function removeFromParent(path: string, parent: string) {
109
114
  // ── Writes ──
110
115
 
111
116
  export function put(node: NodeData, virtualParent?: string) {
117
+ stampNode(node);
112
118
  nodes.set(node.$path, node);
119
+ devFreeze(node);
113
120
  const p = virtualParent ?? parentOf(node.$path);
114
121
  if (p !== null) {
115
122
  if (!parentIndex.has(p)) parentIndex.set(p, new Set());
@@ -135,7 +142,9 @@ export function putMany(items: NodeData[], virtualParent?: string) {
135
142
  const ts = Date.now();
136
143
  const idbEntries: idb.IDBEntry[] = [];
137
144
  for (const n of items) {
145
+ stampNode(n);
138
146
  nodes.set(n.$path, n);
147
+ devFreeze(n);
139
148
  lastUpdated.set(n.$path, ts);
140
149
  fire(pathSubs, n.$path);
141
150
  const p = virtualParent ?? parentOf(n.$path);
@@ -225,7 +234,9 @@ export async function hydrate(): Promise<void> {
225
234
  try {
226
235
  const entries = await idb.loadAll();
227
236
  for (const { data, lastUpdated: ts, virtualParent } of entries) {
237
+ stampNode(data);
228
238
  nodes.set(data.$path, data);
239
+ devFreeze(data);
229
240
  lastUpdated.set(data.$path, ts);
230
241
  const p = virtualParent ?? parentOf(data.$path);
231
242
  if (p !== null) {