@treenity/mods 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 (486) hide show
  1. package/board/board.test.ts +212 -0
  2. package/board/client.ts +1 -0
  3. package/board/seed.ts +26 -0
  4. package/board/server.ts +5 -0
  5. package/board/types.ts +87 -0
  6. package/board/view.tsx +574 -0
  7. package/brahman/CLAUDE.md +18 -0
  8. package/brahman/brahman.test.ts +855 -0
  9. package/brahman/client.ts +2 -0
  10. package/brahman/helpers.ts +374 -0
  11. package/brahman/server.ts +2 -0
  12. package/brahman/service.ts +328 -0
  13. package/brahman/types.ts +727 -0
  14. package/brahman/view.tsx +73 -0
  15. package/brahman/views/action-cards.tsx +615 -0
  16. package/brahman/views/bot-view.tsx +76 -0
  17. package/brahman/views/chat-editor.tsx +782 -0
  18. package/brahman/views/chat-preview.tsx +266 -0
  19. package/brahman/views/menu-editor.tsx +451 -0
  20. package/brahman/views/page-layout.tsx +285 -0
  21. package/brahman/views/tstring-input.tsx +84 -0
  22. package/cafe/CLAUDE.md +7 -0
  23. package/cafe/seed.ts +8 -0
  24. package/cafe/server.ts +2 -0
  25. package/cafe/types.ts +32 -0
  26. package/canary/seed.ts +8 -0
  27. package/canary/server.ts +3 -0
  28. package/canary/service.ts +101 -0
  29. package/canary/types.ts +16 -0
  30. package/dist/board/client.d.ts +2 -0
  31. package/dist/board/client.d.ts.map +1 -0
  32. package/dist/board/client.js +2 -0
  33. package/dist/board/client.js.map +1 -0
  34. package/dist/board/seed.d.ts +2 -0
  35. package/dist/board/seed.d.ts.map +1 -0
  36. package/dist/board/seed.js +23 -0
  37. package/dist/board/seed.js.map +1 -0
  38. package/dist/board/server.d.ts +3 -0
  39. package/dist/board/server.d.ts.map +1 -0
  40. package/dist/board/server.js +5 -0
  41. package/dist/board/server.js.map +1 -0
  42. package/dist/board/types.d.ts +45 -0
  43. package/dist/board/types.d.ts.map +1 -0
  44. package/dist/board/types.js +82 -0
  45. package/dist/board/types.js.map +1 -0
  46. package/dist/board/view.d.ts +2 -0
  47. package/dist/board/view.d.ts.map +1 -0
  48. package/dist/board/view.js +254 -0
  49. package/dist/board/view.js.map +1 -0
  50. package/dist/brahman/client.d.ts +3 -0
  51. package/dist/brahman/client.d.ts.map +1 -0
  52. package/dist/brahman/client.js +3 -0
  53. package/dist/brahman/client.js.map +1 -0
  54. package/dist/brahman/helpers.d.ts +51 -0
  55. package/dist/brahman/helpers.d.ts.map +1 -0
  56. package/dist/brahman/helpers.js +321 -0
  57. package/dist/brahman/helpers.js.map +1 -0
  58. package/dist/brahman/server.d.ts +3 -0
  59. package/dist/brahman/server.d.ts.map +1 -0
  60. package/dist/brahman/server.js +3 -0
  61. package/dist/brahman/server.js.map +1 -0
  62. package/dist/brahman/service.d.ts +2 -0
  63. package/dist/brahman/service.d.ts.map +1 -0
  64. package/dist/brahman/service.js +310 -0
  65. package/dist/brahman/service.js.map +1 -0
  66. package/dist/brahman/types.d.ts +335 -0
  67. package/dist/brahman/types.d.ts.map +1 -0
  68. package/dist/brahman/types.js +633 -0
  69. package/dist/brahman/types.js.map +1 -0
  70. package/dist/brahman/view.d.ts +2 -0
  71. package/dist/brahman/view.d.ts.map +1 -0
  72. package/dist/brahman/view.js +47 -0
  73. package/dist/brahman/view.js.map +1 -0
  74. package/dist/brahman/views/action-cards.d.ts +60 -0
  75. package/dist/brahman/views/action-cards.d.ts.map +1 -0
  76. package/dist/brahman/views/action-cards.js +283 -0
  77. package/dist/brahman/views/action-cards.js.map +1 -0
  78. package/dist/brahman/views/bot-view.d.ts +5 -0
  79. package/dist/brahman/views/bot-view.d.ts.map +1 -0
  80. package/dist/brahman/views/bot-view.js +14 -0
  81. package/dist/brahman/views/bot-view.js.map +1 -0
  82. package/dist/brahman/views/chat-editor.d.ts +5 -0
  83. package/dist/brahman/views/chat-editor.d.ts.map +1 -0
  84. package/dist/brahman/views/chat-editor.js +306 -0
  85. package/dist/brahman/views/chat-editor.js.map +1 -0
  86. package/dist/brahman/views/chat-preview.d.ts +5 -0
  87. package/dist/brahman/views/chat-preview.d.ts.map +1 -0
  88. package/dist/brahman/views/chat-preview.js +145 -0
  89. package/dist/brahman/views/chat-preview.js.map +1 -0
  90. package/dist/brahman/views/menu-editor.d.ts +11 -0
  91. package/dist/brahman/views/menu-editor.d.ts.map +1 -0
  92. package/dist/brahman/views/menu-editor.js +171 -0
  93. package/dist/brahman/views/menu-editor.js.map +1 -0
  94. package/dist/brahman/views/page-layout.d.ts +5 -0
  95. package/dist/brahman/views/page-layout.d.ts.map +1 -0
  96. package/dist/brahman/views/page-layout.js +114 -0
  97. package/dist/brahman/views/page-layout.js.map +1 -0
  98. package/dist/brahman/views/tstring-input.d.ts +15 -0
  99. package/dist/brahman/views/tstring-input.d.ts.map +1 -0
  100. package/dist/brahman/views/tstring-input.js +23 -0
  101. package/dist/brahman/views/tstring-input.js.map +1 -0
  102. package/dist/cafe/seed.d.ts +2 -0
  103. package/dist/cafe/seed.d.ts.map +1 -0
  104. package/dist/cafe/seed.js +7 -0
  105. package/dist/cafe/seed.js.map +1 -0
  106. package/dist/cafe/server.d.ts +3 -0
  107. package/dist/cafe/server.d.ts.map +1 -0
  108. package/dist/cafe/server.js +3 -0
  109. package/dist/cafe/server.js.map +1 -0
  110. package/dist/cafe/types.d.ts +2 -0
  111. package/dist/cafe/types.d.ts.map +1 -0
  112. package/dist/cafe/types.js +31 -0
  113. package/dist/cafe/types.js.map +1 -0
  114. package/dist/canary/seed.d.ts +2 -0
  115. package/dist/canary/seed.d.ts.map +1 -0
  116. package/dist/canary/seed.js +7 -0
  117. package/dist/canary/seed.js.map +1 -0
  118. package/dist/canary/server.d.ts +4 -0
  119. package/dist/canary/server.d.ts.map +1 -0
  120. package/dist/canary/server.js +4 -0
  121. package/dist/canary/server.js.map +1 -0
  122. package/dist/canary/service.d.ts +2 -0
  123. package/dist/canary/service.d.ts.map +1 -0
  124. package/dist/canary/service.js +101 -0
  125. package/dist/canary/service.js.map +1 -0
  126. package/dist/canary/types.d.ts +9 -0
  127. package/dist/canary/types.d.ts.map +1 -0
  128. package/dist/canary/types.js +13 -0
  129. package/dist/canary/types.js.map +1 -0
  130. package/dist/doc/client.d.ts +3 -0
  131. package/dist/doc/client.d.ts.map +1 -0
  132. package/dist/doc/client.js +5 -0
  133. package/dist/doc/client.js.map +1 -0
  134. package/dist/doc/fs-codec.d.ts +2 -0
  135. package/dist/doc/fs-codec.d.ts.map +1 -0
  136. package/dist/doc/fs-codec.js +44 -0
  137. package/dist/doc/fs-codec.js.map +1 -0
  138. package/dist/doc/markdown.d.ts +13 -0
  139. package/dist/doc/markdown.d.ts.map +1 -0
  140. package/dist/doc/markdown.js +250 -0
  141. package/dist/doc/markdown.js.map +1 -0
  142. package/dist/doc/prefab.d.ts +2 -0
  143. package/dist/doc/prefab.d.ts.map +1 -0
  144. package/dist/doc/prefab.js +23 -0
  145. package/dist/doc/prefab.js.map +1 -0
  146. package/dist/doc/renderers.d.ts +2 -0
  147. package/dist/doc/renderers.d.ts.map +1 -0
  148. package/dist/doc/renderers.js +94 -0
  149. package/dist/doc/renderers.js.map +1 -0
  150. package/dist/doc/seed.d.ts +2 -0
  151. package/dist/doc/seed.d.ts.map +1 -0
  152. package/dist/doc/seed.js +9 -0
  153. package/dist/doc/seed.js.map +1 -0
  154. package/dist/doc/server.d.ts +6 -0
  155. package/dist/doc/server.d.ts.map +1 -0
  156. package/dist/doc/server.js +6 -0
  157. package/dist/doc/server.js.map +1 -0
  158. package/dist/doc/slash-command.d.ts +3 -0
  159. package/dist/doc/slash-command.d.ts.map +1 -0
  160. package/dist/doc/slash-command.js +123 -0
  161. package/dist/doc/slash-command.js.map +1 -0
  162. package/dist/doc/slash-menu.d.ts +15 -0
  163. package/dist/doc/slash-menu.d.ts.map +1 -0
  164. package/dist/doc/slash-menu.js +54 -0
  165. package/dist/doc/slash-menu.js.map +1 -0
  166. package/dist/doc/text.d.ts +2 -0
  167. package/dist/doc/text.d.ts.map +1 -0
  168. package/dist/doc/text.js +22 -0
  169. package/dist/doc/text.js.map +1 -0
  170. package/dist/doc/toolbar.d.ts +5 -0
  171. package/dist/doc/toolbar.d.ts.map +1 -0
  172. package/dist/doc/toolbar.js +15 -0
  173. package/dist/doc/toolbar.js.map +1 -0
  174. package/dist/doc/treenity-block-view.d.ts +2 -0
  175. package/dist/doc/treenity-block-view.d.ts.map +1 -0
  176. package/dist/doc/treenity-block-view.js +37 -0
  177. package/dist/doc/treenity-block-view.js.map +1 -0
  178. package/dist/doc/treenity-block.d.ts +3 -0
  179. package/dist/doc/treenity-block.d.ts.map +1 -0
  180. package/dist/doc/treenity-block.js +27 -0
  181. package/dist/doc/treenity-block.js.map +1 -0
  182. package/dist/doc/types.d.ts +2 -0
  183. package/dist/doc/types.d.ts.map +1 -0
  184. package/dist/doc/types.js +10 -0
  185. package/dist/doc/types.js.map +1 -0
  186. package/dist/launcher/client.d.ts +5 -0
  187. package/dist/launcher/client.d.ts.map +1 -0
  188. package/dist/launcher/client.js +5 -0
  189. package/dist/launcher/client.js.map +1 -0
  190. package/dist/launcher/icons.d.ts +2 -0
  191. package/dist/launcher/icons.d.ts.map +1 -0
  192. package/dist/launcher/icons.js +57 -0
  193. package/dist/launcher/icons.js.map +1 -0
  194. package/dist/launcher/seed.d.ts +2 -0
  195. package/dist/launcher/seed.d.ts.map +1 -0
  196. package/dist/launcher/seed.js +36 -0
  197. package/dist/launcher/seed.js.map +1 -0
  198. package/dist/launcher/server.d.ts +3 -0
  199. package/dist/launcher/server.d.ts.map +1 -0
  200. package/dist/launcher/server.js +3 -0
  201. package/dist/launcher/server.js.map +1 -0
  202. package/dist/launcher/types.d.ts +21 -0
  203. package/dist/launcher/types.d.ts.map +1 -0
  204. package/dist/launcher/types.js +47 -0
  205. package/dist/launcher/types.js.map +1 -0
  206. package/dist/launcher/view.d.ts +2 -0
  207. package/dist/launcher/view.d.ts.map +1 -0
  208. package/dist/launcher/view.js +187 -0
  209. package/dist/launcher/view.js.map +1 -0
  210. package/dist/launcher/widgets.d.ts +2 -0
  211. package/dist/launcher/widgets.d.ts.map +1 -0
  212. package/dist/launcher/widgets.js +73 -0
  213. package/dist/launcher/widgets.js.map +1 -0
  214. package/dist/mindmap/branch.d.ts +17 -0
  215. package/dist/mindmap/branch.d.ts.map +1 -0
  216. package/dist/mindmap/branch.js +47 -0
  217. package/dist/mindmap/branch.js.map +1 -0
  218. package/dist/mindmap/client.d.ts +3 -0
  219. package/dist/mindmap/client.d.ts.map +1 -0
  220. package/dist/mindmap/client.js +3 -0
  221. package/dist/mindmap/client.js.map +1 -0
  222. package/dist/mindmap/radial-tree.d.ts +14 -0
  223. package/dist/mindmap/radial-tree.d.ts.map +1 -0
  224. package/dist/mindmap/radial-tree.js +184 -0
  225. package/dist/mindmap/radial-tree.js.map +1 -0
  226. package/dist/mindmap/sidebar.d.ts +8 -0
  227. package/dist/mindmap/sidebar.d.ts.map +1 -0
  228. package/dist/mindmap/sidebar.js +24 -0
  229. package/dist/mindmap/sidebar.js.map +1 -0
  230. package/dist/mindmap/types.d.ts +8 -0
  231. package/dist/mindmap/types.d.ts.map +1 -0
  232. package/dist/mindmap/types.js +10 -0
  233. package/dist/mindmap/types.js.map +1 -0
  234. package/dist/mindmap/use-tree-data.d.ts +14 -0
  235. package/dist/mindmap/use-tree-data.d.ts.map +1 -0
  236. package/dist/mindmap/use-tree-data.js +95 -0
  237. package/dist/mindmap/use-tree-data.js.map +1 -0
  238. package/dist/mindmap/view.d.ts +3 -0
  239. package/dist/mindmap/view.d.ts.map +1 -0
  240. package/dist/mindmap/view.js +141 -0
  241. package/dist/mindmap/view.js.map +1 -0
  242. package/dist/sensor-demo/client.d.ts +2 -0
  243. package/dist/sensor-demo/client.d.ts.map +1 -0
  244. package/dist/sensor-demo/client.js +2 -0
  245. package/dist/sensor-demo/client.js.map +1 -0
  246. package/dist/sensor-demo/server.d.ts +2 -0
  247. package/dist/sensor-demo/server.d.ts.map +1 -0
  248. package/dist/sensor-demo/server.js +2 -0
  249. package/dist/sensor-demo/server.js.map +1 -0
  250. package/dist/sensor-demo/service.d.ts +2 -0
  251. package/dist/sensor-demo/service.d.ts.map +1 -0
  252. package/dist/sensor-demo/service.js +27 -0
  253. package/dist/sensor-demo/service.js.map +1 -0
  254. package/dist/sensor-demo/types.d.ts +15 -0
  255. package/dist/sensor-demo/types.d.ts.map +1 -0
  256. package/dist/sensor-demo/types.js +18 -0
  257. package/dist/sensor-demo/types.js.map +1 -0
  258. package/dist/sensor-demo/view.d.ts +2 -0
  259. package/dist/sensor-demo/view.d.ts.map +1 -0
  260. package/dist/sensor-demo/view.js +33 -0
  261. package/dist/sensor-demo/view.js.map +1 -0
  262. package/dist/sensor-generator/action.d.ts +2 -0
  263. package/dist/sensor-generator/action.d.ts.map +1 -0
  264. package/dist/sensor-generator/action.js +23 -0
  265. package/dist/sensor-generator/action.js.map +1 -0
  266. package/dist/sensor-generator/client.d.ts +2 -0
  267. package/dist/sensor-generator/client.d.ts.map +1 -0
  268. package/dist/sensor-generator/client.js +2 -0
  269. package/dist/sensor-generator/client.js.map +1 -0
  270. package/dist/sensor-generator/server.d.ts +2 -0
  271. package/dist/sensor-generator/server.d.ts.map +1 -0
  272. package/dist/sensor-generator/server.js +2 -0
  273. package/dist/sensor-generator/server.js.map +1 -0
  274. package/dist/sensor-generator/view.d.ts +2 -0
  275. package/dist/sensor-generator/view.d.ts.map +1 -0
  276. package/dist/sensor-generator/view.js +64 -0
  277. package/dist/sensor-generator/view.js.map +1 -0
  278. package/dist/sim/client.d.ts +2 -0
  279. package/dist/sim/client.d.ts.map +1 -0
  280. package/dist/sim/client.js +2 -0
  281. package/dist/sim/client.js.map +1 -0
  282. package/dist/sim/seed.d.ts +2 -0
  283. package/dist/sim/seed.d.ts.map +1 -0
  284. package/dist/sim/seed.js +50 -0
  285. package/dist/sim/seed.js.map +1 -0
  286. package/dist/sim/server.d.ts +3 -0
  287. package/dist/sim/server.d.ts.map +1 -0
  288. package/dist/sim/server.js +3 -0
  289. package/dist/sim/server.js.map +1 -0
  290. package/dist/sim/service.d.ts +4 -0
  291. package/dist/sim/service.d.ts.map +1 -0
  292. package/dist/sim/service.js +528 -0
  293. package/dist/sim/service.js.map +1 -0
  294. package/dist/sim/types.d.ts +63 -0
  295. package/dist/sim/types.d.ts.map +1 -0
  296. package/dist/sim/types.js +57 -0
  297. package/dist/sim/types.js.map +1 -0
  298. package/dist/sim/view.d.ts +2 -0
  299. package/dist/sim/view.d.ts.map +1 -0
  300. package/dist/sim/view.js +205 -0
  301. package/dist/sim/view.js.map +1 -0
  302. package/dist/table/client.d.ts +4 -0
  303. package/dist/table/client.d.ts.map +1 -0
  304. package/dist/table/client.js +4 -0
  305. package/dist/table/client.js.map +1 -0
  306. package/dist/table/edit.d.ts +2 -0
  307. package/dist/table/edit.d.ts.map +1 -0
  308. package/dist/table/edit.js +115 -0
  309. package/dist/table/edit.js.map +1 -0
  310. package/dist/table/server.d.ts +2 -0
  311. package/dist/table/server.d.ts.map +1 -0
  312. package/dist/table/server.js +2 -0
  313. package/dist/table/server.js.map +1 -0
  314. package/dist/table/types.d.ts +18 -0
  315. package/dist/table/types.d.ts.map +1 -0
  316. package/dist/table/types.js +13 -0
  317. package/dist/table/types.js.map +1 -0
  318. package/dist/table/use-debounced-sync.d.ts +8 -0
  319. package/dist/table/use-debounced-sync.d.ts.map +1 -0
  320. package/dist/table/use-debounced-sync.js +56 -0
  321. package/dist/table/use-debounced-sync.js.map +1 -0
  322. package/dist/table/view.d.ts +2 -0
  323. package/dist/table/view.d.ts.map +1 -0
  324. package/dist/table/view.js +199 -0
  325. package/dist/table/view.js.map +1 -0
  326. package/dist/tasks/server.d.ts +2 -0
  327. package/dist/tasks/server.d.ts.map +1 -0
  328. package/dist/tasks/server.js +2 -0
  329. package/dist/tasks/server.js.map +1 -0
  330. package/dist/tasks/types.d.ts +22 -0
  331. package/dist/tasks/types.d.ts.map +1 -0
  332. package/dist/tasks/types.js +34 -0
  333. package/dist/tasks/types.js.map +1 -0
  334. package/dist/three/client.d.ts +2 -0
  335. package/dist/three/client.d.ts.map +1 -0
  336. package/dist/three/client.js +2 -0
  337. package/dist/three/client.js.map +1 -0
  338. package/dist/three/seed.d.ts +2 -0
  339. package/dist/three/seed.d.ts.map +1 -0
  340. package/dist/three/seed.js +45 -0
  341. package/dist/three/seed.js.map +1 -0
  342. package/dist/three/server.d.ts +3 -0
  343. package/dist/three/server.d.ts.map +1 -0
  344. package/dist/three/server.js +3 -0
  345. package/dist/three/server.js.map +1 -0
  346. package/dist/three/types.d.ts +178 -0
  347. package/dist/three/types.d.ts.map +1 -0
  348. package/dist/three/types.js +209 -0
  349. package/dist/three/types.js.map +1 -0
  350. package/dist/three/view.d.ts +2 -0
  351. package/dist/three/view.d.ts.map +1 -0
  352. package/dist/three/view.js +307 -0
  353. package/dist/three/view.js.map +1 -0
  354. package/dist/todo/client.d.ts +3 -0
  355. package/dist/todo/client.d.ts.map +1 -0
  356. package/dist/todo/client.js +3 -0
  357. package/dist/todo/client.js.map +1 -0
  358. package/dist/todo/seed.d.ts +2 -0
  359. package/dist/todo/seed.d.ts.map +1 -0
  360. package/dist/todo/seed.js +8 -0
  361. package/dist/todo/seed.js.map +1 -0
  362. package/dist/todo/server.d.ts +3 -0
  363. package/dist/todo/server.d.ts.map +1 -0
  364. package/dist/todo/server.js +3 -0
  365. package/dist/todo/server.js.map +1 -0
  366. package/dist/todo/types.d.ts +15 -0
  367. package/dist/todo/types.d.ts.map +1 -0
  368. package/dist/todo/types.js +29 -0
  369. package/dist/todo/types.js.map +1 -0
  370. package/dist/todo/view.d.ts +2 -0
  371. package/dist/todo/view.d.ts.map +1 -0
  372. package/dist/todo/view.js +27 -0
  373. package/dist/todo/view.js.map +1 -0
  374. package/dist/whisper/client.d.ts +2 -0
  375. package/dist/whisper/client.d.ts.map +1 -0
  376. package/dist/whisper/client.js +2 -0
  377. package/dist/whisper/client.js.map +1 -0
  378. package/dist/whisper/inbox.d.ts +2 -0
  379. package/dist/whisper/inbox.d.ts.map +1 -0
  380. package/dist/whisper/inbox.js +50 -0
  381. package/dist/whisper/inbox.js.map +1 -0
  382. package/dist/whisper/route.d.ts +10 -0
  383. package/dist/whisper/route.d.ts.map +1 -0
  384. package/dist/whisper/route.js +154 -0
  385. package/dist/whisper/route.js.map +1 -0
  386. package/dist/whisper/seed.d.ts +2 -0
  387. package/dist/whisper/seed.d.ts.map +1 -0
  388. package/dist/whisper/seed.js +14 -0
  389. package/dist/whisper/seed.js.map +1 -0
  390. package/dist/whisper/server.d.ts +5 -0
  391. package/dist/whisper/server.d.ts.map +1 -0
  392. package/dist/whisper/server.js +5 -0
  393. package/dist/whisper/server.js.map +1 -0
  394. package/dist/whisper/service.d.ts +2 -0
  395. package/dist/whisper/service.d.ts.map +1 -0
  396. package/dist/whisper/service.js +26 -0
  397. package/dist/whisper/service.js.map +1 -0
  398. package/dist/whisper/types.d.ts +38 -0
  399. package/dist/whisper/types.d.ts.map +1 -0
  400. package/dist/whisper/types.js +45 -0
  401. package/dist/whisper/types.js.map +1 -0
  402. package/dist/whisper/view.d.ts +2 -0
  403. package/dist/whisper/view.d.ts.map +1 -0
  404. package/dist/whisper/view.js +40 -0
  405. package/dist/whisper/view.js.map +1 -0
  406. package/doc/CLAUDE.md +49 -0
  407. package/doc/client.ts +5 -0
  408. package/doc/editor.css +283 -0
  409. package/doc/fs-codec.test.ts +119 -0
  410. package/doc/fs-codec.ts +50 -0
  411. package/doc/markdown.test.ts +152 -0
  412. package/doc/markdown.ts +284 -0
  413. package/doc/prefab.ts +26 -0
  414. package/doc/renderers.tsx +126 -0
  415. package/doc/seed.ts +10 -0
  416. package/doc/server.ts +5 -0
  417. package/doc/slash-command.ts +136 -0
  418. package/doc/slash-menu.tsx +91 -0
  419. package/doc/text.ts +20 -0
  420. package/doc/toolbar.tsx +86 -0
  421. package/doc/treenity-block-view.tsx +116 -0
  422. package/doc/treenity-block.ts +31 -0
  423. package/doc/types.ts +10 -0
  424. package/launcher/client.ts +4 -0
  425. package/launcher/icons.tsx +234 -0
  426. package/launcher/launcher.css +64 -0
  427. package/launcher/seed.ts +41 -0
  428. package/launcher/server.ts +2 -0
  429. package/launcher/types.ts +53 -0
  430. package/launcher/view.tsx +401 -0
  431. package/launcher/widgets.tsx +171 -0
  432. package/mindmap/branch.tsx +163 -0
  433. package/mindmap/client.ts +2 -0
  434. package/mindmap/mindmap.css +243 -0
  435. package/mindmap/sidebar.tsx +127 -0
  436. package/mindmap/types.ts +10 -0
  437. package/mindmap/view.tsx +247 -0
  438. package/package.json +75 -0
  439. package/sensor-demo/CLAUDE.md +3 -0
  440. package/sensor-demo/client.ts +1 -0
  441. package/sensor-demo/server.ts +1 -0
  442. package/sensor-demo/service.ts +27 -0
  443. package/sensor-demo/types.ts +20 -0
  444. package/sensor-demo/view.tsx +64 -0
  445. package/sensor-generator/CLAUDE.md +3 -0
  446. package/sensor-generator/action.ts +28 -0
  447. package/sensor-generator/client.ts +1 -0
  448. package/sensor-generator/server.ts +1 -0
  449. package/sensor-generator/view.tsx +107 -0
  450. package/sim/CLAUDE.md +16 -0
  451. package/sim/client.ts +1 -0
  452. package/sim/seed.ts +55 -0
  453. package/sim/server.ts +2 -0
  454. package/sim/service.ts +594 -0
  455. package/sim/sim.test.ts +282 -0
  456. package/sim/types.ts +87 -0
  457. package/sim/view.tsx +446 -0
  458. package/table/client.ts +3 -0
  459. package/table/edit.tsx +241 -0
  460. package/table/server.ts +1 -0
  461. package/table/types.ts +22 -0
  462. package/table/use-debounced-sync.ts +67 -0
  463. package/table/view.tsx +339 -0
  464. package/tasks/CLAUDE.md +6 -0
  465. package/tasks/server.ts +1 -0
  466. package/tasks/types.ts +36 -0
  467. package/three/CLAUDE.md +6 -0
  468. package/three/client.ts +1 -0
  469. package/three/seed.ts +54 -0
  470. package/three/server.ts +2 -0
  471. package/three/types.ts +238 -0
  472. package/three/view.tsx +453 -0
  473. package/todo/client.ts +2 -0
  474. package/todo/seed.ts +9 -0
  475. package/todo/server.ts +2 -0
  476. package/todo/types.ts +32 -0
  477. package/todo/view.tsx +67 -0
  478. package/whisper/CLAUDE.md +16 -0
  479. package/whisper/client.ts +1 -0
  480. package/whisper/inbox.ts +54 -0
  481. package/whisper/route.ts +182 -0
  482. package/whisper/seed.ts +15 -0
  483. package/whisper/server.ts +4 -0
  484. package/whisper/service.ts +29 -0
  485. package/whisper/types.ts +51 -0
  486. package/whisper/view.tsx +81 -0
@@ -0,0 +1,782 @@
1
+ // Chat editor — WYSIWYG Telegram-style editor for page actions
2
+ // Registered as react:chat:edit for brahman.page
3
+ // Same visual as PageChatPreview but with inline editing
4
+
5
+ import {
6
+ closestCenter,
7
+ DndContext,
8
+ type DragEndEvent,
9
+ KeyboardSensor,
10
+ PointerSensor,
11
+ useSensor,
12
+ useSensors,
13
+ } from '@dnd-kit/core';
14
+ import {
15
+ arrayMove,
16
+ SortableContext,
17
+ sortableKeyboardCoordinates,
18
+ useSortable,
19
+ verticalListSortingStrategy,
20
+ } from '@dnd-kit/sortable';
21
+ import { CSS } from '@dnd-kit/utilities';
22
+ import type { NodeData } from '@treenity/core';
23
+ import { getDefaults } from '@treenity/core/comp';
24
+ import { set, useChildren, usePath } from '@treenity/react/hooks';
25
+ import { trpc } from '@treenity/react/trpc';
26
+ import { Camera, File, GripVertical, Mic, MoreHorizontal, Plus, Trash2, Video, X } from 'lucide-react';
27
+ import { useCallback, useEffect, useRef, useState } from 'react';
28
+ import { createPortal } from 'react-dom';
29
+ import { ACTION_TYPES, MENU_TYPES, type MenuButton, type MenuRow, type MenuType, type TString } from '../types';
30
+ import { actionIcon, actionSummary } from './action-cards';
31
+ import { TStringLineInput, tstringPreview } from './tstring-input';
32
+
33
+ // ── Helpers ──
34
+
35
+ function tstr(ts: TString | undefined): string {
36
+ if (!ts) return '';
37
+ return ts.ru || ts.en || Object.values(ts).find(v => v) || '';
38
+ }
39
+
40
+ function primaryLang(ts: TString | undefined): string {
41
+ if (!ts) return 'ru';
42
+ if (ts.ru) return 'ru';
43
+ if (ts.en) return 'en';
44
+ return Object.keys(ts)[0] ?? 'ru';
45
+ }
46
+
47
+ // ── TgHtml — safe Telegram HTML renderer ──
48
+
49
+ const TG_TAGS: Record<string, string> = {
50
+ b: 'font-bold', strong: 'font-bold',
51
+ i: 'italic', em: 'italic',
52
+ u: 'underline', ins: 'underline',
53
+ s: 'line-through', strike: 'line-through', del: 'line-through',
54
+ code: 'font-mono text-[13px] bg-[#1a2636] px-1 rounded',
55
+ pre: 'font-mono text-[13px] bg-[#1a2636] p-2 rounded block overflow-x-auto',
56
+ blockquote: 'border-l-2 border-[#3d6a99] pl-2 italic',
57
+ };
58
+
59
+ function domToReact(node: Node, key: number): React.ReactNode {
60
+ if (node.nodeType === Node.TEXT_NODE) return node.textContent;
61
+ if (node.nodeType !== Node.ELEMENT_NODE) return null;
62
+ const el = node as Element;
63
+ const tag = el.tagName.toLowerCase();
64
+ if (tag === 'br') return <br key={key} />;
65
+ if (tag === 'a') {
66
+ const href = el.getAttribute('href') || '#';
67
+ return <a key={key} href={href} target="_blank" rel="noopener noreferrer" className="text-[#5b9bd5] underline">{Array.from(el.childNodes).map(domToReact)}</a>;
68
+ }
69
+ const cls = TG_TAGS[tag];
70
+ if (cls) return <span key={key} className={cls}>{Array.from(el.childNodes).map(domToReact)}</span>;
71
+ return Array.from(el.childNodes).map(domToReact);
72
+ }
73
+
74
+ function TgHtml({ text }: { text: string }) {
75
+ if (!/<[a-z][\s>]/i.test(text)) return <>{text}</>;
76
+ const doc = new DOMParser().parseFromString(text, 'text/html');
77
+ return <>{Array.from(doc.body.childNodes).map(domToReact)}</>;
78
+ }
79
+
80
+ // ── Editable text — click to edit, blur to save ──
81
+
82
+ // Sanitize browser-generated HTML back to Telegram-safe subset
83
+ const ALLOWED_TAGS = new Set(['b', 'strong', 'i', 'em', 'u', 'ins', 's', 'strike', 'del', 'code', 'pre', 'a', 'blockquote', 'br']);
84
+
85
+ function sanitizeHtml(html: string): string {
86
+ const doc = new DOMParser().parseFromString(html, 'text/html');
87
+
88
+ function walk(node: Node): string {
89
+ if (node.nodeType === Node.TEXT_NODE) return node.textContent ?? '';
90
+ if (node.nodeType !== Node.ELEMENT_NODE) return '';
91
+ const el = node as Element;
92
+ const tag = el.tagName.toLowerCase();
93
+ const inner = Array.from(el.childNodes).map(walk).join('');
94
+
95
+ if (tag === 'br') return '\n';
96
+ if (tag === 'div' || tag === 'p') return inner ? inner + '\n' : '';
97
+ if (!ALLOWED_TAGS.has(tag)) return inner;
98
+ if (tag === 'a') {
99
+ const href = el.getAttribute('href');
100
+ return href ? `<a href="${href}">${inner}</a>` : inner;
101
+ }
102
+ return `<${tag}>${inner}</${tag}>`;
103
+ }
104
+
105
+ return walk(doc.body).replace(/\n$/, '');
106
+ }
107
+
108
+ function EditableText({
109
+ value,
110
+ onChange,
111
+ placeholder = 'Type message...',
112
+ }: {
113
+ value: TString;
114
+ onChange: (ts: TString) => void;
115
+ placeholder?: string;
116
+ }) {
117
+ const lang = primaryLang(value);
118
+ const text = value[lang] ?? '';
119
+ const ref = useRef<HTMLDivElement>(null);
120
+ const saving = useRef(false);
121
+
122
+ function save() {
123
+ if (saving.current || !ref.current) return;
124
+ saving.current = true;
125
+ const html = ref.current.innerHTML;
126
+ const clean = sanitizeHtml(html);
127
+ if (clean !== text) onChange({ ...value, [lang]: clean });
128
+ saving.current = false;
129
+ }
130
+
131
+ return (
132
+ <div
133
+ ref={ref}
134
+ contentEditable
135
+ suppressContentEditableWarning
136
+ className="outline-none min-h-[1.25rem] cursor-text
137
+ focus:ring-1 focus:ring-[#3d6a99] rounded transition-all
138
+ empty:before:content-[attr(data-placeholder)] empty:before:text-[#6c7883] empty:before:italic"
139
+ data-placeholder={placeholder}
140
+ dangerouslySetInnerHTML={{ __html: text || '' }}
141
+ onBlur={save}
142
+ />
143
+ );
144
+ }
145
+
146
+ // ── Bubble shells ──
147
+
148
+ function BotBubble({ children, first, tools }: { children: React.ReactNode; first?: boolean; tools?: React.ReactNode }) {
149
+ return (
150
+ <div className="flex flex-col items-start max-w-[85%] group/bubble">
151
+ {first && <span className="text-[11px] font-semibold text-[#5b9bd5] mb-0.5 ml-1">Bot</span>}
152
+ <div className="relative bg-[#182533] text-[#e1e3e6] text-sm rounded-lg rounded-tl-sm px-3 py-2 whitespace-pre-wrap break-words w-full">
153
+ {children}
154
+ {tools && (
155
+ <div className="absolute -right-1 -top-1 opacity-0 group-hover/bubble:opacity-100 transition-opacity">
156
+ {tools}
157
+ </div>
158
+ )}
159
+ </div>
160
+ </div>
161
+ );
162
+ }
163
+
164
+ function UserBubble({ children }: { children: React.ReactNode }) {
165
+ return (
166
+ <div className="flex justify-end">
167
+ <div className="bg-[#2b5278] text-[#e1e3e6] text-sm rounded-lg rounded-tr-sm px-3 py-2 max-w-[70%]
168
+ border border-dashed border-[#3d6a99] opacity-60">
169
+ {children}
170
+ </div>
171
+ </div>
172
+ );
173
+ }
174
+
175
+ function SystemPill({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) {
176
+ return (
177
+ <div className="flex justify-center">
178
+ <div
179
+ className={`flex items-center gap-1.5 text-[11px] text-[#6c7883] bg-[#131c26] rounded-full px-3 py-1 ${
180
+ onClick ? 'cursor-pointer hover:bg-[#1a2636] hover:text-[#e1e3e6] transition-colors' : ''
181
+ }`}
182
+ onClick={onClick}
183
+ >
184
+ {children}
185
+ </div>
186
+ </div>
187
+ );
188
+ }
189
+
190
+ // ── Settings popover ──
191
+
192
+ function SettingsPopover({
193
+ node,
194
+ onUpdate,
195
+ onDelete,
196
+ onClose,
197
+ }: {
198
+ node: NodeData;
199
+ onUpdate: (patch: Record<string, unknown>) => void;
200
+ onDelete: () => void;
201
+ onClose: () => void;
202
+ }) {
203
+ const type = node.$type;
204
+
205
+ return createPortal(
206
+ <div className="fixed inset-0 z-50" onClick={onClose}>
207
+ <div
208
+ className="absolute bg-[#1c2733] border border-[#2b3945] rounded-lg shadow-xl p-3 space-y-2 w-64"
209
+ style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
210
+ onClick={e => e.stopPropagation()}
211
+ >
212
+ <div className="flex items-center justify-between mb-1">
213
+ <span className="text-xs font-medium text-[#e1e3e6]">
214
+ {ACTION_TYPES.find(a => a.type === type)?.label ?? type.split('.').at(-1)}
215
+ </span>
216
+ <button type="button" onClick={onClose} className="text-[#6c7883] hover:text-[#e1e3e6]">
217
+ <X className="h-3.5 w-3.5" />
218
+ </button>
219
+ </div>
220
+
221
+ {/* Type-specific settings */}
222
+ {type === 'brahman.action.message' && (
223
+ <>
224
+ <SettingSelect label="Menu" value={(node.menuType as string) ?? 'none'}
225
+ options={MENU_TYPES.map(m => ({ value: m.value, label: m.label }))}
226
+ onChange={v => onUpdate({ menuType: v })} />
227
+ <SettingCheckbox label="Disable links" checked={!!node.disableLinks}
228
+ onChange={v => onUpdate({ disableLinks: v })} />
229
+ </>
230
+ )}
231
+
232
+ {type === 'brahman.action.question' && (
233
+ <>
234
+ <SettingSelect label="Input type" value={(node.inputType as string) ?? 'text'}
235
+ options={[{ value: 'text', label: 'Text' }, { value: 'photo', label: 'Photo' }]}
236
+ onChange={v => onUpdate({ inputType: v })} />
237
+ <SettingInput label="Save to" value={(node.saveTo as string) ?? ''}
238
+ placeholder="session.field" onChange={v => onUpdate({ saveTo: v })} />
239
+ </>
240
+ )}
241
+
242
+ {type === 'brahman.action.file' && (
243
+ <SettingSelect label="Send as" value={(node.asType as string) ?? ''}
244
+ options={[
245
+ { value: '', label: 'Auto' }, { value: 'photo', label: 'Photo' },
246
+ { value: 'document', label: 'Document' }, { value: 'video', label: 'Video' },
247
+ { value: 'audio', label: 'Audio' }, { value: 'voice', label: 'Voice' },
248
+ ]}
249
+ onChange={v => onUpdate({ asType: v })} />
250
+ )}
251
+
252
+ <div className="pt-1 border-t border-[#2b3945]">
253
+ <button
254
+ type="button"
255
+ onClick={onDelete}
256
+ className="flex items-center gap-1.5 text-xs text-red-400 hover:text-red-300 py-1"
257
+ >
258
+ <Trash2 className="h-3 w-3" /> Delete action
259
+ </button>
260
+ </div>
261
+ </div>
262
+ </div>,
263
+ document.body,
264
+ );
265
+ }
266
+
267
+ function SettingSelect({ label, value, options, onChange }: {
268
+ label: string; value: string; options: { value: string; label: string }[]; onChange: (v: string) => void;
269
+ }) {
270
+ return (
271
+ <div className="flex items-center justify-between gap-2">
272
+ <span className="text-[11px] text-[#6c7883]">{label}</span>
273
+ <select
274
+ className="bg-[#0e1621] border border-[#2b3945] rounded text-xs text-[#e1e3e6] px-2 py-1"
275
+ value={value} onChange={e => onChange(e.target.value)}
276
+ >
277
+ {options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
278
+ </select>
279
+ </div>
280
+ );
281
+ }
282
+
283
+ function SettingCheckbox({ label, checked, onChange }: {
284
+ label: string; checked: boolean; onChange: (v: boolean) => void;
285
+ }) {
286
+ return (
287
+ <label className="flex items-center justify-between gap-2 cursor-pointer">
288
+ <span className="text-[11px] text-[#6c7883]">{label}</span>
289
+ <input type="checkbox" checked={checked} onChange={e => onChange(e.target.checked)}
290
+ className="rounded border-[#2b3945]" />
291
+ </label>
292
+ );
293
+ }
294
+
295
+ function SettingInput({ label, value, placeholder, onChange }: {
296
+ label: string; value: string; placeholder?: string; onChange: (v: string) => void;
297
+ }) {
298
+ return (
299
+ <div className="flex items-center justify-between gap-2">
300
+ <span className="text-[11px] text-[#6c7883] shrink-0">{label}</span>
301
+ <input
302
+ className="bg-[#0e1621] border border-[#2b3945] rounded text-xs text-[#e1e3e6] px-2 py-1 w-32 font-mono"
303
+ value={value} placeholder={placeholder} onChange={e => onChange(e.target.value)}
304
+ />
305
+ </div>
306
+ );
307
+ }
308
+
309
+ // ── Editable inline buttons ──
310
+
311
+ function EditableButtons({
312
+ rows,
313
+ menuType,
314
+ onChangeRows,
315
+ }: {
316
+ rows: MenuRow[];
317
+ menuType: MenuType;
318
+ onChangeRows: (rows: MenuRow[]) => void;
319
+ }) {
320
+ const [editBtn, setEditBtn] = useState<{ ri: number; bi: number } | null>(null);
321
+
322
+ if (!rows?.length || menuType === 'none' || menuType === 'remove') return null;
323
+ const isReply = menuType === 'keyboard' || menuType === 'force_reply';
324
+
325
+ let nextId = 1;
326
+ for (const row of rows) for (const btn of row.buttons) if (btn.id >= nextId) nextId = btn.id + 1;
327
+
328
+ function addButton(ri: number) {
329
+ const newRows = rows.map(r => ({ buttons: [...r.buttons] }));
330
+ newRows[ri].buttons.push({ id: nextId, title: { ru: '', en: '' }, tags: [] });
331
+ onChangeRows(newRows);
332
+ }
333
+
334
+ function addRow() {
335
+ onChangeRows([...rows, { buttons: [{ id: nextId, title: { ru: '', en: '' }, tags: [] }] }]);
336
+ }
337
+
338
+ function updateButton(ri: number, bi: number, btn: MenuButton) {
339
+ const newRows = rows.map(r => ({ buttons: [...r.buttons] }));
340
+ newRows[ri].buttons[bi] = btn;
341
+ onChangeRows(newRows);
342
+ }
343
+
344
+ function deleteButton(ri: number, bi: number) {
345
+ const newRows = rows.map(r => ({ buttons: [...r.buttons] }));
346
+ newRows[ri].buttons.splice(bi, 1);
347
+ onChangeRows(newRows.filter(r => r.buttons.length > 0));
348
+ setEditBtn(null);
349
+ }
350
+
351
+ return (
352
+ <div className={`flex flex-col gap-0.5 ${isReply ? 'w-full mt-2' : 'max-w-[85%] mt-0.5'}`}>
353
+ {rows.map((row, ri) => (
354
+ <div key={ri} className="flex gap-0.5">
355
+ {row.buttons.map((btn, bi) => {
356
+ const text = tstringPreview(btn.title, 24) || '...';
357
+ return (
358
+ <div
359
+ key={btn.id}
360
+ className={`flex-1 text-center text-xs py-1.5 px-2 rounded truncate cursor-pointer
361
+ hover:ring-1 hover:ring-[#5b9bd5] transition-all
362
+ ${isReply
363
+ ? 'bg-[#1c2733] text-[#e1e3e6] border border-[#2b3945]'
364
+ : 'bg-[#2b5278] text-[#e1e3e6]'
365
+ }`}
366
+ onClick={() => setEditBtn({ ri, bi })}
367
+ >
368
+ {btn.url ? `🔗 ${text}` : text}
369
+ </div>
370
+ );
371
+ })}
372
+ <button
373
+ type="button"
374
+ onClick={() => addButton(ri)}
375
+ className="text-[#6c7883] hover:text-[#e1e3e6] px-1 transition-colors"
376
+ title="Add button"
377
+ >
378
+ <Plus className="h-3 w-3" />
379
+ </button>
380
+ </div>
381
+ ))}
382
+ <button
383
+ type="button"
384
+ onClick={addRow}
385
+ className="text-[10px] text-[#6c7883] hover:text-[#e1e3e6] py-0.5 transition-colors"
386
+ >
387
+ + row
388
+ </button>
389
+
390
+ {/* Button edit modal */}
391
+ {editBtn && rows[editBtn.ri]?.buttons[editBtn.bi] && (
392
+ <ButtonEditPopover
393
+ button={rows[editBtn.ri].buttons[editBtn.bi]}
394
+ onSave={btn => updateButton(editBtn.ri, editBtn.bi, btn)}
395
+ onDelete={() => deleteButton(editBtn.ri, editBtn.bi)}
396
+ onClose={() => setEditBtn(null)}
397
+ />
398
+ )}
399
+ </div>
400
+ );
401
+ }
402
+
403
+ function ButtonEditPopover({
404
+ button,
405
+ onSave,
406
+ onDelete,
407
+ onClose,
408
+ }: {
409
+ button: MenuButton;
410
+ onSave: (btn: MenuButton) => void;
411
+ onDelete: () => void;
412
+ onClose: () => void;
413
+ }) {
414
+ const [draft, setDraft] = useState(() => ({ ...button }));
415
+
416
+ return createPortal(
417
+ <div className="fixed inset-0 z-50" onClick={onClose}>
418
+ <div
419
+ className="absolute bg-[#1c2733] border border-[#2b3945] rounded-lg shadow-xl p-3 space-y-2 w-72"
420
+ style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
421
+ onClick={e => e.stopPropagation()}
422
+ >
423
+ <div className="flex items-center justify-between mb-1">
424
+ <span className="text-xs font-medium text-[#e1e3e6]">Edit button</span>
425
+ <button type="button" onClick={onClose} className="text-[#6c7883] hover:text-[#e1e3e6]">
426
+ <X className="h-3.5 w-3.5" />
427
+ </button>
428
+ </div>
429
+
430
+ <div>
431
+ <span className="text-[11px] text-[#6c7883]">Title</span>
432
+ <TStringLineInput
433
+ value={draft.title}
434
+ onChange={title => setDraft(d => ({ ...d, title }))}
435
+ langs={['ru', 'en']}
436
+ />
437
+ </div>
438
+
439
+ <div className="flex items-center gap-2">
440
+ <span className="text-[11px] text-[#6c7883] shrink-0">URL</span>
441
+ <input
442
+ className="flex-1 bg-[#0e1621] border border-[#2b3945] rounded text-xs text-[#e1e3e6] px-2 py-1"
443
+ placeholder="https://..."
444
+ value={draft.url ?? ''}
445
+ onChange={e => setDraft(d => ({ ...d, url: e.target.value || undefined }))}
446
+ />
447
+ </div>
448
+
449
+ <div className="flex justify-between pt-1 border-t border-[#2b3945]">
450
+ <button type="button" onClick={onDelete}
451
+ className="text-xs text-red-400 hover:text-red-300 flex items-center gap-1">
452
+ <Trash2 className="h-3 w-3" /> Delete
453
+ </button>
454
+ <div className="flex gap-1">
455
+ <button type="button" onClick={onClose}
456
+ className="text-xs text-[#6c7883] hover:text-[#e1e3e6] px-2 py-1">Cancel</button>
457
+ <button type="button" onClick={() => { onSave(draft); onClose(); }}
458
+ className="text-xs bg-[#2b5278] text-[#e1e3e6] px-3 py-1 rounded hover:bg-[#3d6a99]">Save</button>
459
+ </div>
460
+ </div>
461
+ </div>
462
+ </div>,
463
+ document.body,
464
+ );
465
+ }
466
+
467
+ // ── Sortable action wrapper ──
468
+
469
+ function SortableAction({
470
+ node,
471
+ isFirstBubble,
472
+ onDelete,
473
+ }: {
474
+ node: NodeData;
475
+ isFirstBubble: boolean;
476
+ onDelete: () => void;
477
+ }) {
478
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: node.$path });
479
+ const n = usePath(node.$path);
480
+ const [showSettings, setShowSettings] = useState(false);
481
+
482
+ if (!n) return null;
483
+
484
+ function update(patch: Record<string, unknown>) {
485
+ if (!n) return;
486
+ set({ ...n, ...patch } as NodeData);
487
+ }
488
+
489
+ const style = {
490
+ transform: CSS.Transform.toString(transform),
491
+ transition,
492
+ opacity: isDragging ? 0.4 : 1,
493
+ };
494
+
495
+ const settingsBtn = (
496
+ <button
497
+ type="button"
498
+ onClick={e => { e.stopPropagation(); setShowSettings(true); }}
499
+ className="bg-[#1c2733] border border-[#2b3945] rounded-full p-1 text-[#6c7883] hover:text-[#e1e3e6] transition-colors"
500
+ >
501
+ <MoreHorizontal className="h-3 w-3" />
502
+ </button>
503
+ );
504
+
505
+ const dragHandle = (
506
+ <div
507
+ {...attributes}
508
+ {...listeners}
509
+ className="absolute -left-5 top-1/2 -translate-y-1/2 opacity-0 group-hover/action:opacity-100
510
+ cursor-grab text-[#6c7883] hover:text-[#e1e3e6] transition-opacity"
511
+ >
512
+ <GripVertical className="h-3.5 w-3.5" />
513
+ </div>
514
+ );
515
+
516
+ const type = n.$type;
517
+
518
+ const content = (() => {
519
+ switch (type) {
520
+ case 'brahman.action.message': {
521
+ const menuType = (n.menuType as MenuType) ?? 'none';
522
+ const rows = (n.rows as MenuRow[]) ?? [];
523
+ return (
524
+ <div className="flex flex-col items-start gap-0">
525
+ <BotBubble first={isFirstBubble} tools={settingsBtn}>
526
+ <EditableText
527
+ value={(n.text as TString) ?? {}}
528
+ onChange={text => update({ text })}
529
+ placeholder="Type message..."
530
+ />
531
+ </BotBubble>
532
+ <EditableButtons
533
+ rows={rows}
534
+ menuType={menuType}
535
+ onChangeRows={rows => update({ rows })}
536
+ />
537
+ </div>
538
+ );
539
+ }
540
+
541
+ case 'brahman.action.question': {
542
+ const inputType = (n.inputType as string) ?? 'text';
543
+ return (
544
+ <div className="space-y-1.5">
545
+ <BotBubble first={isFirstBubble} tools={settingsBtn}>
546
+ <EditableText
547
+ value={(n.text as TString) ?? {}}
548
+ onChange={text => update({ text })}
549
+ placeholder="Type question..."
550
+ />
551
+ </BotBubble>
552
+ <UserBubble>
553
+ {inputType === 'photo'
554
+ ? <span className="flex items-center gap-1"><Camera className="h-3.5 w-3.5" /> Photo</span>
555
+ : <span className="italic text-xs">User types answer...</span>
556
+ }
557
+ </UserBubble>
558
+ </div>
559
+ );
560
+ }
561
+
562
+ case 'brahman.action.file': {
563
+ const asType = (n.asType as string) || 'document';
564
+ const icons: Record<string, React.ReactNode> = {
565
+ photo: <Camera className="h-8 w-8" />,
566
+ video: <Video className="h-8 w-8" />,
567
+ audio: <Mic className="h-8 w-8" />,
568
+ voice: <Mic className="h-8 w-8" />,
569
+ };
570
+ return (
571
+ <BotBubble first={isFirstBubble} tools={settingsBtn}>
572
+ <div className="flex flex-col items-center gap-1 py-2 text-[#6c7883]">
573
+ {icons[asType] ?? <File className="h-8 w-8" />}
574
+ <span className="text-xs">{asType}</span>
575
+ </div>
576
+ </BotBubble>
577
+ );
578
+ }
579
+
580
+ case 'brahman.action.selectlang': {
581
+ return (
582
+ <div className="flex flex-col items-start gap-0.5">
583
+ <BotBubble first={isFirstBubble} tools={settingsBtn}>
584
+ <EditableText
585
+ value={(n.text as TString) ?? {}}
586
+ onChange={text => update({ text })}
587
+ placeholder="Choose language"
588
+ />
589
+ </BotBubble>
590
+ <div className="flex gap-0.5 max-w-[85%]">
591
+ <div className="flex-1 text-center text-xs py-1.5 px-2 rounded bg-[#2b5278] text-[#e1e3e6]">🇷🇺 RU</div>
592
+ <div className="flex-1 text-center text-xs py-1.5 px-2 rounded bg-[#2b5278] text-[#e1e3e6]">🇬🇧 EN</div>
593
+ </div>
594
+ </div>
595
+ );
596
+ }
597
+
598
+ // All other actions → system pills
599
+ default: {
600
+ const summary = actionSummary(n);
601
+ const label = type.split('.').at(-1) ?? type;
602
+ return (
603
+ <SystemPill onClick={() => setShowSettings(true)}>
604
+ {actionIcon(type)}
605
+ <span>{label}{summary ? `: ${summary}` : ''}</span>
606
+ </SystemPill>
607
+ );
608
+ }
609
+ }
610
+ })();
611
+
612
+ return (
613
+ <div ref={setNodeRef} style={style} className="relative group/action">
614
+ {dragHandle}
615
+ {content}
616
+ {showSettings && (
617
+ <SettingsPopover
618
+ node={n}
619
+ onUpdate={update}
620
+ onDelete={onDelete}
621
+ onClose={() => setShowSettings(false)}
622
+ />
623
+ )}
624
+ </div>
625
+ );
626
+ }
627
+
628
+ // ── Action palette (compact, chat-themed) ──
629
+
630
+ function ChatActionPalette({ onSelect }: { onSelect: (type: string) => void }) {
631
+ const [open, setOpen] = useState(false);
632
+
633
+ return (
634
+ <div className="flex justify-center">
635
+ <div className="relative">
636
+ <button
637
+ type="button"
638
+ onClick={() => setOpen(!open)}
639
+ className="flex items-center gap-1 text-[11px] text-[#6c7883] hover:text-[#e1e3e6]
640
+ bg-[#131c26] rounded-full px-3 py-1 transition-colors"
641
+ >
642
+ <Plus className="h-3 w-3" /> Add action
643
+ </button>
644
+
645
+ {open && (
646
+ <div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-1 z-50 w-52
647
+ bg-[#1c2733] border border-[#2b3945] rounded-lg shadow-xl py-1 max-h-64 overflow-y-auto">
648
+ {ACTION_TYPES.map(at => (
649
+ <button
650
+ key={at.type}
651
+ type="button"
652
+ className="w-full text-left px-3 py-1.5 text-xs text-[#e1e3e6] hover:bg-[#2b3945] flex items-center gap-2"
653
+ onClick={() => { onSelect(at.type); setOpen(false); }}
654
+ >
655
+ {actionIcon(at.type)}
656
+ {at.label}
657
+ </button>
658
+ ))}
659
+ </div>
660
+ )}
661
+ </div>
662
+ </div>
663
+ );
664
+ }
665
+
666
+ // ── Main component ──
667
+
668
+ export function PageChatEditor({ value }: { value: NodeData }) {
669
+ const node = usePath(value.$path);
670
+ const actionsPath = value.$path + '/_actions';
671
+ const children = useChildren(actionsPath, { watch: true, watchNew: true });
672
+
673
+ const positions: string[] = (node?.positions as string[]) ?? [];
674
+ const tracked = new Set(positions);
675
+ const sorted = [
676
+ ...positions.map(p => children.find(c => c.$path === p)).filter((c): c is NodeData => !!c),
677
+ ...children.filter(c => !tracked.has(c.$path) && c.$type?.startsWith('brahman.action.')),
678
+ ];
679
+
680
+ // ── Command editing ──
681
+ const command = (node?.command as string) ?? '';
682
+ const [localCmd, setLocalCmd] = useState(command);
683
+ const cmdTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
684
+ useEffect(() => { setLocalCmd(command); }, [command]);
685
+
686
+ const saveNode = useCallback((patch: Record<string, unknown>) => {
687
+ if (!node) return;
688
+ set({ ...node, ...patch } as NodeData);
689
+ }, [node]);
690
+
691
+ // ── DnD ──
692
+ const sensors = useSensors(
693
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
694
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
695
+ );
696
+
697
+ function handleDragEnd(event: DragEndEvent) {
698
+ const { active, over } = event;
699
+ if (!over || active.id === over.id) return;
700
+ const oldIndex = sorted.findIndex(c => c.$path === active.id);
701
+ const newIndex = sorted.findIndex(c => c.$path === over.id);
702
+ if (oldIndex === -1 || newIndex === -1) return;
703
+ saveNode({ positions: arrayMove(sorted.map(c => c.$path), oldIndex, newIndex) });
704
+ }
705
+
706
+ // ── CRUD ──
707
+ async function addAction(type: string) {
708
+ const id = Date.now().toString(36);
709
+ const childPath = `${actionsPath}/${id}`;
710
+ await trpc.set.mutate({ node: { $path: actionsPath, $type: 'dir' } as NodeData });
711
+ const defaults = getDefaults(type);
712
+ await trpc.set.mutate({ node: { $path: childPath, $type: type, ...defaults } as NodeData });
713
+ saveNode({ positions: [...positions, childPath] });
714
+ }
715
+
716
+ async function removeAction(path: string) {
717
+ await trpc.remove.mutate({ path });
718
+ saveNode({ positions: positions.filter(p => p !== path) });
719
+ }
720
+
721
+ let hadBotBubble = false;
722
+
723
+ return (
724
+ <div className="bg-[#0e1621] rounded-xl p-4 space-y-2.5 min-h-[200px] max-w-md mx-auto">
725
+ {/* Command header — editable */}
726
+ <div className="flex justify-center mb-2">
727
+ <input
728
+ className="text-[11px] text-[#6c7883] bg-[#131c26] rounded-full px-3 py-1 font-mono
729
+ text-center border-none outline-none focus:ring-1 focus:ring-[#3d6a99] w-32"
730
+ placeholder="/command..."
731
+ value={localCmd}
732
+ onChange={e => {
733
+ setLocalCmd(e.target.value);
734
+ clearTimeout(cmdTimer.current);
735
+ cmdTimer.current = setTimeout(() => saveNode({ command: e.target.value }), 400);
736
+ }}
737
+ onBlur={() => {
738
+ clearTimeout(cmdTimer.current);
739
+ if (localCmd !== command) saveNode({ command: localCmd });
740
+ }}
741
+ />
742
+ </div>
743
+
744
+ {/* Actions with DnD */}
745
+ <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
746
+ <SortableContext items={sorted.map(c => c.$path)} strategy={verticalListSortingStrategy}>
747
+ <div className="space-y-2.5">
748
+ {sorted.length === 0 && (
749
+ <div className="text-[#6c7883] text-xs text-center py-8">Click + to add actions</div>
750
+ )}
751
+
752
+ {sorted.map(child => {
753
+ const isBubbleType = ['brahman.action.message', 'brahman.action.question',
754
+ 'brahman.action.file', 'brahman.action.selectlang'].includes(child.$type);
755
+ const isFirstBubble = isBubbleType && !hadBotBubble;
756
+ if (isBubbleType) hadBotBubble = true;
757
+
758
+ return (
759
+ <SortableAction
760
+ key={child.$path}
761
+ node={child}
762
+ isFirstBubble={isFirstBubble}
763
+ onDelete={() => removeAction(child.$path)}
764
+ />
765
+ );
766
+ })}
767
+ </div>
768
+ </SortableContext>
769
+ </DndContext>
770
+
771
+ {/* Add action */}
772
+ <ChatActionPalette onSelect={addAction} />
773
+
774
+ {/* Input bar mockup */}
775
+ <div className="flex items-center gap-2 mt-4 pt-3 border-t border-[#1c2733]">
776
+ <div className="flex-1 bg-[#1c2733] rounded-full px-3 py-1.5 text-xs text-[#6c7883]">
777
+ Message...
778
+ </div>
779
+ </div>
780
+ </div>
781
+ );
782
+ }