@treenity/react 3.0.0

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 (297) hide show
  1. package/dist/AclEditor.d.ts +11 -0
  2. package/dist/AclEditor.d.ts.map +1 -0
  3. package/dist/AclEditor.js +152 -0
  4. package/dist/AclEditor.js.map +1 -0
  5. package/dist/App.d.ts +2 -0
  6. package/dist/App.d.ts.map +1 -0
  7. package/dist/App.js +521 -0
  8. package/dist/App.js.map +1 -0
  9. package/dist/Inspector.d.ts +12 -0
  10. package/dist/Inspector.d.ts.map +1 -0
  11. package/dist/Inspector.js +360 -0
  12. package/dist/Inspector.js.map +1 -0
  13. package/dist/Tree.d.ts +16 -0
  14. package/dist/Tree.d.ts.map +1 -0
  15. package/dist/Tree.js +100 -0
  16. package/dist/Tree.js.map +1 -0
  17. package/dist/ViewPage.d.ts +5 -0
  18. package/dist/ViewPage.d.ts.map +1 -0
  19. package/dist/ViewPage.js +13 -0
  20. package/dist/ViewPage.js.map +1 -0
  21. package/dist/bind/computed.d.ts +9 -0
  22. package/dist/bind/computed.d.ts.map +1 -0
  23. package/dist/bind/computed.js +61 -0
  24. package/dist/bind/computed.js.map +1 -0
  25. package/dist/bind/engine.d.ts +3 -0
  26. package/dist/bind/engine.d.ts.map +1 -0
  27. package/dist/bind/engine.js +184 -0
  28. package/dist/bind/engine.js.map +1 -0
  29. package/dist/bind/eval.d.ts +13 -0
  30. package/dist/bind/eval.d.ts.map +1 -0
  31. package/dist/bind/eval.js +97 -0
  32. package/dist/bind/eval.js.map +1 -0
  33. package/dist/bind/hook.d.ts +8 -0
  34. package/dist/bind/hook.d.ts.map +1 -0
  35. package/dist/bind/hook.js +99 -0
  36. package/dist/bind/hook.js.map +1 -0
  37. package/dist/bind/parse.d.ts +19 -0
  38. package/dist/bind/parse.d.ts.map +1 -0
  39. package/dist/bind/parse.js +86 -0
  40. package/dist/bind/parse.js.map +1 -0
  41. package/dist/bind/pipes.d.ts +4 -0
  42. package/dist/bind/pipes.d.ts.map +1 -0
  43. package/dist/bind/pipes.js +43 -0
  44. package/dist/bind/pipes.js.map +1 -0
  45. package/dist/cache.d.ts +27 -0
  46. package/dist/cache.d.ts.map +1 -0
  47. package/dist/cache.js +236 -0
  48. package/dist/cache.js.map +1 -0
  49. package/dist/client-tree.d.ts +9 -0
  50. package/dist/client-tree.d.ts.map +1 -0
  51. package/dist/client-tree.js +14 -0
  52. package/dist/client-tree.js.map +1 -0
  53. package/dist/client.d.ts +2 -0
  54. package/dist/client.d.ts.map +1 -0
  55. package/dist/client.js +10 -0
  56. package/dist/client.js.map +1 -0
  57. package/dist/components/ui/accordion.d.ts +8 -0
  58. package/dist/components/ui/accordion.d.ts.map +1 -0
  59. package/dist/components/ui/accordion.js +18 -0
  60. package/dist/components/ui/accordion.js.map +1 -0
  61. package/dist/components/ui/badge.d.ts +10 -0
  62. package/dist/components/ui/badge.d.ts.map +1 -0
  63. package/dist/components/ui/badge.js +19 -0
  64. package/dist/components/ui/badge.js.map +1 -0
  65. package/dist/components/ui/button.d.ts +11 -0
  66. package/dist/components/ui/button.d.ts.map +1 -0
  67. package/dist/components/ui/button.js +31 -0
  68. package/dist/components/ui/button.js.map +1 -0
  69. package/dist/components/ui/checkbox.d.ts +4 -0
  70. package/dist/components/ui/checkbox.d.ts.map +1 -0
  71. package/dist/components/ui/checkbox.js +7 -0
  72. package/dist/components/ui/checkbox.js.map +1 -0
  73. package/dist/components/ui/dialog.d.ts +18 -0
  74. package/dist/components/ui/dialog.d.ts.map +1 -0
  75. package/dist/components/ui/dialog.js +37 -0
  76. package/dist/components/ui/dialog.js.map +1 -0
  77. package/dist/components/ui/drawer.d.ts +14 -0
  78. package/dist/components/ui/drawer.d.ts.map +1 -0
  79. package/dist/components/ui/drawer.js +35 -0
  80. package/dist/components/ui/drawer.js.map +1 -0
  81. package/dist/components/ui/input.d.ts +4 -0
  82. package/dist/components/ui/input.d.ts.map +1 -0
  83. package/dist/components/ui/input.js +7 -0
  84. package/dist/components/ui/input.js.map +1 -0
  85. package/dist/components/ui/label.d.ts +5 -0
  86. package/dist/components/ui/label.d.ts.map +1 -0
  87. package/dist/components/ui/label.js +8 -0
  88. package/dist/components/ui/label.js.map +1 -0
  89. package/dist/components/ui/popover.d.ts +11 -0
  90. package/dist/components/ui/popover.d.ts.map +1 -0
  91. package/dist/components/ui/popover.js +26 -0
  92. package/dist/components/ui/popover.js.map +1 -0
  93. package/dist/components/ui/progress.d.ts +5 -0
  94. package/dist/components/ui/progress.d.ts.map +1 -0
  95. package/dist/components/ui/progress.js +9 -0
  96. package/dist/components/ui/progress.js.map +1 -0
  97. package/dist/components/ui/select.d.ts +16 -0
  98. package/dist/components/ui/select.d.ts.map +1 -0
  99. package/dist/components/ui/select.js +39 -0
  100. package/dist/components/ui/select.js.map +1 -0
  101. package/dist/components/ui/slider.d.ts +5 -0
  102. package/dist/components/ui/slider.d.ts.map +1 -0
  103. package/dist/components/ui/slider.js +15 -0
  104. package/dist/components/ui/slider.js.map +1 -0
  105. package/dist/components/ui/sonner.d.ts +4 -0
  106. package/dist/components/ui/sonner.d.ts.map +1 -0
  107. package/dist/components/ui/sonner.js +21 -0
  108. package/dist/components/ui/sonner.js.map +1 -0
  109. package/dist/components/ui/switch.d.ts +7 -0
  110. package/dist/components/ui/switch.d.ts.map +1 -0
  111. package/dist/components/ui/switch.js +9 -0
  112. package/dist/components/ui/switch.js.map +1 -0
  113. package/dist/components/ui/textarea.d.ts +4 -0
  114. package/dist/components/ui/textarea.d.ts.map +1 -0
  115. package/dist/components/ui/textarea.js +7 -0
  116. package/dist/components/ui/textarea.js.map +1 -0
  117. package/dist/components/ui/tooltip.d.ts +8 -0
  118. package/dist/components/ui/tooltip.d.ts.map +1 -0
  119. package/dist/components/ui/tooltip.js +18 -0
  120. package/dist/components/ui/tooltip.js.map +1 -0
  121. package/dist/context/index.d.ts +31 -0
  122. package/dist/context/index.d.ts.map +1 -0
  123. package/dist/context/index.js +98 -0
  124. package/dist/context/index.js.map +1 -0
  125. package/dist/context.d.ts +2 -0
  126. package/dist/context.d.ts.map +1 -0
  127. package/dist/context.js +2 -0
  128. package/dist/context.js.map +1 -0
  129. package/dist/hooks.d.ts +21 -0
  130. package/dist/hooks.d.ts.map +1 -0
  131. package/dist/hooks.js +156 -0
  132. package/dist/hooks.js.map +1 -0
  133. package/dist/idb.d.ts +13 -0
  134. package/dist/idb.d.ts.map +1 -0
  135. package/dist/idb.js +67 -0
  136. package/dist/idb.js.map +1 -0
  137. package/dist/lib/minimd.d.ts +3 -0
  138. package/dist/lib/minimd.d.ts.map +1 -0
  139. package/dist/lib/minimd.js +97 -0
  140. package/dist/lib/minimd.js.map +1 -0
  141. package/dist/lib/utils.d.ts +3 -0
  142. package/dist/lib/utils.d.ts.map +1 -0
  143. package/dist/lib/utils.js +6 -0
  144. package/dist/lib/utils.js.map +1 -0
  145. package/dist/load-client.d.ts +2 -0
  146. package/dist/load-client.d.ts.map +1 -0
  147. package/dist/load-client.js +6 -0
  148. package/dist/load-client.js.map +1 -0
  149. package/dist/main.d.ts +4 -0
  150. package/dist/main.d.ts.map +1 -0
  151. package/dist/main.js +16 -0
  152. package/dist/main.js.map +1 -0
  153. package/dist/mods/editor-ui/client.d.ts +6 -0
  154. package/dist/mods/editor-ui/client.d.ts.map +1 -0
  155. package/dist/mods/editor-ui/client.js +8 -0
  156. package/dist/mods/editor-ui/client.js.map +1 -0
  157. package/dist/mods/editor-ui/default-view.d.ts +2 -0
  158. package/dist/mods/editor-ui/default-view.d.ts.map +1 -0
  159. package/dist/mods/editor-ui/default-view.js +71 -0
  160. package/dist/mods/editor-ui/default-view.js.map +1 -0
  161. package/dist/mods/editor-ui/dir-view.d.ts +2 -0
  162. package/dist/mods/editor-ui/dir-view.d.ts.map +1 -0
  163. package/dist/mods/editor-ui/dir-view.js +42 -0
  164. package/dist/mods/editor-ui/dir-view.js.map +1 -0
  165. package/dist/mods/editor-ui/form-fields.d.ts +6 -0
  166. package/dist/mods/editor-ui/form-fields.d.ts.map +1 -0
  167. package/dist/mods/editor-ui/form-fields.js +401 -0
  168. package/dist/mods/editor-ui/form-fields.js.map +1 -0
  169. package/dist/mods/editor-ui/layout-view.d.ts +2 -0
  170. package/dist/mods/editor-ui/layout-view.d.ts.map +1 -0
  171. package/dist/mods/editor-ui/layout-view.js +22 -0
  172. package/dist/mods/editor-ui/layout-view.js.map +1 -0
  173. package/dist/mods/editor-ui/list-items.d.ts +2 -0
  174. package/dist/mods/editor-ui/list-items.d.ts.map +1 -0
  175. package/dist/mods/editor-ui/list-items.js +38 -0
  176. package/dist/mods/editor-ui/list-items.js.map +1 -0
  177. package/dist/mods/editor-ui/node-utils.d.ts +10 -0
  178. package/dist/mods/editor-ui/node-utils.d.ts.map +1 -0
  179. package/dist/mods/editor-ui/node-utils.js +76 -0
  180. package/dist/mods/editor-ui/node-utils.js.map +1 -0
  181. package/dist/mods/editor-ui/user-view.d.ts +2 -0
  182. package/dist/mods/editor-ui/user-view.d.ts.map +1 -0
  183. package/dist/mods/editor-ui/user-view.js +47 -0
  184. package/dist/mods/editor-ui/user-view.js.map +1 -0
  185. package/dist/mods/treenity/client.d.ts +4 -0
  186. package/dist/mods/treenity/client.d.ts.map +1 -0
  187. package/dist/mods/treenity/client.js +6 -0
  188. package/dist/mods/treenity/client.js.map +1 -0
  189. package/dist/mods/treenity/groups/index.d.ts +2 -0
  190. package/dist/mods/treenity/groups/index.d.ts.map +1 -0
  191. package/dist/mods/treenity/groups/index.js +27 -0
  192. package/dist/mods/treenity/groups/index.js.map +1 -0
  193. package/dist/mods/treenity/preview.d.ts +6 -0
  194. package/dist/mods/treenity/preview.d.ts.map +1 -0
  195. package/dist/mods/treenity/preview.js +95 -0
  196. package/dist/mods/treenity/preview.js.map +1 -0
  197. package/dist/mods/treenity/ref-view.d.ts +2 -0
  198. package/dist/mods/treenity/ref-view.d.ts.map +1 -0
  199. package/dist/mods/treenity/ref-view.js +29 -0
  200. package/dist/mods/treenity/ref-view.js.map +1 -0
  201. package/dist/mods/treenity/schema-form.d.ts +2 -0
  202. package/dist/mods/treenity/schema-form.d.ts.map +1 -0
  203. package/dist/mods/treenity/schema-form.js +38 -0
  204. package/dist/mods/treenity/schema-form.js.map +1 -0
  205. package/dist/mods/treenity/seed.d.ts +2 -0
  206. package/dist/mods/treenity/seed.d.ts.map +1 -0
  207. package/dist/mods/treenity/seed.js +53 -0
  208. package/dist/mods/treenity/seed.js.map +1 -0
  209. package/dist/mods/treenity/server.d.ts +2 -0
  210. package/dist/mods/treenity/server.d.ts.map +1 -0
  211. package/dist/mods/treenity/server.js +2 -0
  212. package/dist/mods/treenity/server.js.map +1 -0
  213. package/dist/mods/treenity/type-view.d.ts +2 -0
  214. package/dist/mods/treenity/type-view.d.ts.map +1 -0
  215. package/dist/mods/treenity/type-view.js +36 -0
  216. package/dist/mods/treenity/type-view.js.map +1 -0
  217. package/dist/remote-tree.d.ts +6 -0
  218. package/dist/remote-tree.d.ts.map +1 -0
  219. package/dist/remote-tree.js +18 -0
  220. package/dist/remote-tree.js.map +1 -0
  221. package/dist/schema-loader.d.ts +19 -0
  222. package/dist/schema-loader.d.ts.map +1 -0
  223. package/dist/schema-loader.js +63 -0
  224. package/dist/schema-loader.js.map +1 -0
  225. package/dist/trpc.d.ts +187 -0
  226. package/dist/trpc.d.ts.map +1 -0
  227. package/dist/trpc.js +21 -0
  228. package/dist/trpc.js.map +1 -0
  229. package/package.json +88 -0
  230. package/src/AclEditor.tsx +330 -0
  231. package/src/App.tsx +775 -0
  232. package/src/CLAUDE.md +16 -0
  233. package/src/Inspector.tsx +857 -0
  234. package/src/Tree.tsx +237 -0
  235. package/src/ViewPage.tsx +45 -0
  236. package/src/bind/bind.test.ts +316 -0
  237. package/src/bind/computed.ts +64 -0
  238. package/src/bind/engine.ts +198 -0
  239. package/src/bind/eval.ts +108 -0
  240. package/src/bind/hook.ts +112 -0
  241. package/src/bind/parse.ts +104 -0
  242. package/src/bind/pipes.ts +71 -0
  243. package/src/cache.test.ts +139 -0
  244. package/src/cache.ts +244 -0
  245. package/src/client-tree.test.ts +116 -0
  246. package/src/client-tree.ts +24 -0
  247. package/src/client.ts +11 -0
  248. package/src/components/ui/accordion.tsx +63 -0
  249. package/src/components/ui/badge.tsx +27 -0
  250. package/src/components/ui/button.tsx +44 -0
  251. package/src/components/ui/checkbox.tsx +19 -0
  252. package/src/components/ui/dialog.tsx +156 -0
  253. package/src/components/ui/drawer.tsx +132 -0
  254. package/src/components/ui/input.tsx +19 -0
  255. package/src/components/ui/label.tsx +21 -0
  256. package/src/components/ui/popover.tsx +86 -0
  257. package/src/components/ui/progress.tsx +30 -0
  258. package/src/components/ui/select.tsx +189 -0
  259. package/src/components/ui/slider.tsx +62 -0
  260. package/src/components/ui/sonner.tsx +32 -0
  261. package/src/components/ui/switch.tsx +34 -0
  262. package/src/components/ui/textarea.tsx +17 -0
  263. package/src/components/ui/tooltip.tsx +56 -0
  264. package/src/context/index.tsx +131 -0
  265. package/src/context.ts +1 -0
  266. package/src/hooks.ts +208 -0
  267. package/src/idb.ts +80 -0
  268. package/src/index.html +14 -0
  269. package/src/lib/minimd.css +28 -0
  270. package/src/lib/minimd.ts +95 -0
  271. package/src/lib/utils.ts +6 -0
  272. package/src/load-client.ts +5 -0
  273. package/src/main.tsx +22 -0
  274. package/src/mods/editor-ui/CLAUDE.md +3 -0
  275. package/src/mods/editor-ui/client.ts +8 -0
  276. package/src/mods/editor-ui/default-view.tsx +148 -0
  277. package/src/mods/editor-ui/dir-view.tsx +91 -0
  278. package/src/mods/editor-ui/form-fields.tsx +861 -0
  279. package/src/mods/editor-ui/layout-view.tsx +62 -0
  280. package/src/mods/editor-ui/list-items.tsx +63 -0
  281. package/src/mods/editor-ui/node-utils.ts +84 -0
  282. package/src/mods/editor-ui/user-view.tsx +101 -0
  283. package/src/mods/treenity/CLAUDE.md +7 -0
  284. package/src/mods/treenity/client.ts +6 -0
  285. package/src/mods/treenity/groups/index.tsx +65 -0
  286. package/src/mods/treenity/preview.tsx +133 -0
  287. package/src/mods/treenity/ref-view.tsx +87 -0
  288. package/src/mods/treenity/schema-form.tsx +65 -0
  289. package/src/mods/treenity/seed.ts +56 -0
  290. package/src/mods/treenity/server.ts +1 -0
  291. package/src/mods/treenity/type-view.tsx +116 -0
  292. package/src/remote-tree.test.ts +142 -0
  293. package/src/remote-tree.ts +25 -0
  294. package/src/schema-loader.ts +84 -0
  295. package/src/style.css +1269 -0
  296. package/src/trpc.ts +27 -0
  297. package/src/vite-env.d.ts +3 -0
@@ -0,0 +1,62 @@
1
+ // Default react:layout handler — renders node components + children
2
+ // Types can register custom react:layout handlers to override arrangement
3
+
4
+ import { Render, RenderContext } from '#context';
5
+ import { useChildren } from '#hooks';
6
+ import { type ComponentData, type NodeData, register } from '@treenity/core/core';
7
+ import { getComponents, getPlainFields } from './node-utils';
8
+
9
+ function DefaultLayout({ value }: { value: ComponentData }) {
10
+ if (!('$path' in value)) return null;
11
+ const node = value as NodeData;
12
+ const children = useChildren(node.$path);
13
+ const plain = getPlainFields(node);
14
+ const components = getComponents(node);
15
+ const hasInfo = Object.keys(plain).length > 0 || components.length > 0;
16
+
17
+ return (
18
+ <div className="node-layout">
19
+ <RenderContext name="react">
20
+ {components.map(([name, comp]) => {
21
+ const ctype = (comp as any).$type;
22
+ return (
23
+ <div key={name} className="comp-view-card">
24
+ <div className="comp-view-header">
25
+ {name}
26
+ {name !== ctype && <span className="comp-type">{ctype}</span>}
27
+ </div>
28
+ <Render value={comp as ComponentData} />
29
+ </div>
30
+ );
31
+ })}
32
+ </RenderContext>
33
+
34
+ {Object.keys(plain).length > 0 && (
35
+ <div className="node-info-bar">
36
+ {Object.entries(plain).map(([k, v]) => (
37
+ <span key={k} className="node-info-chip data">
38
+ <span className="node-info-chip-label">{k}</span>
39
+ <span className="node-info-chip-val">
40
+ {typeof v === 'string' ? v : JSON.stringify(v)}
41
+ </span>
42
+ </span>
43
+ ))}
44
+ </div>
45
+ )}
46
+
47
+ {children.length > 0 && (
48
+ <RenderContext name="react:list">
49
+ <div className="children-grid">
50
+ {children.map((child) => (
51
+ <Render key={child.$path} value={child} />
52
+ ))}
53
+ </div>
54
+ </RenderContext>
55
+ )}
56
+
57
+ {children.length === 0 && !hasInfo && <div className="node-empty">Empty node</div>}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ register('default', 'react:layout', DefaultLayout as any);
@@ -0,0 +1,63 @@
1
+ // Universal list item components for react:list context
2
+ // Self-contained: include card styling, click navigation, chevron
3
+
4
+ import { useNavigate } from '#hooks';
5
+ import type { NodeData } from '@treenity/core/core';
6
+ import { register } from '@treenity/core/core';
7
+
8
+ const TYPE_ICONS: Record<string, string> = {
9
+ root: '/',
10
+ folder: 'D',
11
+ page: 'P',
12
+ bot: 'B',
13
+ 'type-registry': 'T',
14
+ 'mount-point': 'M',
15
+ type: 'S',
16
+ user: 'U',
17
+ shop: 'S',
18
+ config: 'C',
19
+ };
20
+
21
+ function typeIcon(type: string): string {
22
+ return TYPE_ICONS[type] ?? type.charAt(0).toUpperCase();
23
+ }
24
+
25
+ export function pathName(p: string): string {
26
+ return p.slice(p.lastIndexOf('/') + 1) || '/';
27
+ }
28
+
29
+ // ── Dir list item ──
30
+
31
+ function DirListItem({ value }: { value: NodeData }) {
32
+ const navigate = useNavigate();
33
+ const meta = value.metadata as { $type: string; title?: string; description?: string } | undefined;
34
+ return (
35
+ <div className="child-card" onClick={() => navigate(value.$path)}>
36
+ <span className="child-icon">&#128193;</span>
37
+ <div className="child-info">
38
+ <span className="child-name">{meta?.title ?? pathName(value.$path)}</span>
39
+ <span className="child-type">{meta?.description ?? 'dir'}</span>
40
+ </div>
41
+ <span className="child-chevron">&#8250;</span>
42
+ </div>
43
+ );
44
+ }
45
+ register('dir', 'react:list', DirListItem as any);
46
+
47
+ // ── Default list item — fallback for any type ──
48
+
49
+ function DefaultListItem({ value }: { value: NodeData }) {
50
+ const navigate = useNavigate();
51
+ const path = (value as any).$ref ?? value.$path;
52
+ return (
53
+ <div className="child-card" onClick={() => navigate(path)}>
54
+ <span className="child-icon">{typeIcon(value.$type)}</span>
55
+ <div className="child-info">
56
+ <span className="child-name">{pathName(value.$path)}</span>
57
+ <span className="child-type">{value.$type}</span>
58
+ </div>
59
+ <span className="child-chevron">&#8250;</span>
60
+ </div>
61
+ );
62
+ }
63
+ register('default', 'react:list', DefaultListItem as any);
@@ -0,0 +1,84 @@
1
+ import { getContextsForType, type NodeData, resolve } from '@treenity/core/core';
2
+ import type { PropertySchema, TypeSchema } from '@treenity/core/schema/types';
3
+
4
+ export function getComponents(node: NodeData): [string, Record<string, unknown>][] {
5
+ return Object.entries(node).filter(
6
+ ([k, v]) => !k.startsWith('$') && typeof v === 'object' && v !== null && '$type' in v,
7
+ ) as [string, Record<string, unknown>][];
8
+ }
9
+
10
+ export function getPlainFields(node: NodeData): Record<string, unknown> {
11
+ const out: Record<string, unknown> = {};
12
+ for (const [k, v] of Object.entries(node)) {
13
+ if (k.startsWith('$')) continue;
14
+ if (typeof v === 'object' && v !== null && '$type' in v) continue;
15
+ out[k] = v;
16
+ }
17
+ return out;
18
+ }
19
+
20
+ export function getSchema(type: string): TypeSchema | null {
21
+ const h = resolve(type, 'schema');
22
+ return h ? (h() as TypeSchema) : null;
23
+ }
24
+
25
+ const INTERNAL_CONTEXTS = new Set(['schema', 'mount', 'class']);
26
+
27
+ export function getViewContexts(type: string, node?: NodeData): string[] {
28
+ const ctxs = getContextsForType(type).filter(
29
+ (c) => !INTERNAL_CONTEXTS.has(c) && !c.startsWith('action:'),
30
+ );
31
+ // Also collect view contexts from component types present on the node
32
+ if (node) {
33
+ for (const [, comp] of getComponents(node)) {
34
+ for (const c of getContextsForType((comp as any).$type)) {
35
+ if (!INTERNAL_CONTEXTS.has(c) && !c.startsWith('action:') && !ctxs.includes(c)) {
36
+ ctxs.push(c);
37
+ }
38
+ }
39
+ }
40
+ }
41
+ return ctxs;
42
+ }
43
+
44
+ export function getActions(type: string, schema?: TypeSchema | null): string[] {
45
+ const actions = getContextsForType(type)
46
+ .filter((c) => c.startsWith('action:') && !c.includes(':', 'action:'.length))
47
+ .map((c) => c.slice('action:'.length))
48
+ .filter((a) => !a.startsWith('_'));
49
+
50
+ // Schema is the client-side contract: server-only actions declared in schema.methods
51
+ // are discoverable by all clients (browser, MCP, external) without the implementation
52
+ if (schema?.methods) {
53
+ for (const name of Object.keys(schema.methods)) {
54
+ if (!name.startsWith('_') && !actions.includes(name)) actions.push(name);
55
+ }
56
+ }
57
+
58
+ return actions;
59
+ }
60
+
61
+ // Returns action param schema from generated schema.methods[action].arguments[0].
62
+ // null → no schema info (show JSON textarea fallback)
63
+ // properties={} → confirmed no params (hide params section)
64
+ // properties={…} → typed form fields
65
+ export function getActionSchema(type: string, action: string): TypeSchema | null {
66
+ const schema = getSchema(type);
67
+ if (!schema?.methods) return null;
68
+ const method = schema.methods[action];
69
+ if (!method) return null;
70
+ if (method.arguments.length === 0) return { title: action, type: 'object', properties: {} };
71
+ const arg = method.arguments[0];
72
+ if (arg.type !== 'object' || !arg.properties || Object.keys(arg.properties).length === 0)
73
+ return { title: action, type: 'object', properties: {} };
74
+ return {
75
+ title: method.title ?? action,
76
+ type: 'object',
77
+ properties: arg.properties as Record<string, PropertySchema>,
78
+ };
79
+ }
80
+
81
+ export function pickDefaultContext(type: string): string {
82
+ const ctxs = getViewContexts(type);
83
+ return ctxs.includes('react') ? 'react' : ctxs[0] ?? 'react';
84
+ }
@@ -0,0 +1,101 @@
1
+ import { type NodeData, register } from '@treenity/core/core';
2
+ import { pathName } from './list-items';
3
+ import { getComponents, getSchema } from './node-utils';
4
+
5
+ function UserView({ value }: { value: NodeData }) {
6
+ const id = pathName(value.$path);
7
+ const groups = value.groups as { $type: string; list: string[] } | undefined;
8
+ const comps = getComponents(value).filter(([n]) => n !== 'groups' && n !== 'credentials');
9
+
10
+ return (
11
+ <div className="node-default-view">
12
+ <div style={{ display: 'flex', gap: 16, alignItems: 'center', marginBottom: 16 }}>
13
+ <div
14
+ style={{
15
+ width: 56,
16
+ height: 56,
17
+ borderRadius: '50%',
18
+ background: 'var(--accent)',
19
+ display: 'flex',
20
+ alignItems: 'center',
21
+ justifyContent: 'center',
22
+ fontSize: 22,
23
+ fontWeight: 700,
24
+ color: '#fff',
25
+ flexShrink: 0,
26
+ }}
27
+ >
28
+ {id.charAt(0).toUpperCase()}
29
+ </div>
30
+ <div>
31
+ <div style={{ fontSize: 16, fontWeight: 600 }}>{id}</div>
32
+ {groups?.list && groups.list.length > 0 && (
33
+ <div style={{ display: 'flex', gap: 4, marginTop: 6, flexWrap: 'wrap' }}>
34
+ {groups.list.map((g) => (
35
+ <span
36
+ key={g}
37
+ style={{
38
+ padding: '2px 10px',
39
+ borderRadius: 12,
40
+ background: 'var(--accent-subtle)',
41
+ color: 'var(--accent)',
42
+ fontSize: 11,
43
+ fontWeight: 500,
44
+ }}
45
+ >
46
+ {g}
47
+ </span>
48
+ ))}
49
+ </div>
50
+ )}
51
+ {value.$owner && (
52
+ <div style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 4 }}>
53
+ owner: {String(value.$owner)}
54
+ </div>
55
+ )}
56
+ </div>
57
+ </div>
58
+ {comps.map(([name, comp]) => {
59
+ const ctype = (comp as any).$type;
60
+ const s = getSchema(ctype);
61
+ const fields = s ? Object.entries(s.properties) : [];
62
+ return (
63
+ <div key={name} className="comp-view-card">
64
+ <div className="comp-view-header">
65
+ {name}
66
+ {name !== ctype && <span className="comp-type">{ctype}</span>}
67
+ </div>
68
+ {fields.length > 0
69
+ ? fields.map(([k, prop]) => {
70
+ const val = (comp as any)[k];
71
+ return (
72
+ <div key={k} className="comp-view-row">
73
+ <span className="comp-view-label">{(prop as any).title}</span>
74
+ <span className="comp-view-value">
75
+ {val === undefined || val === ''
76
+ ? '—'
77
+ : typeof val === 'object'
78
+ ? JSON.stringify(val)
79
+ : String(val)}
80
+ </span>
81
+ </div>
82
+ );
83
+ })
84
+ : Object.entries(comp)
85
+ .filter(([k]) => !k.startsWith('$'))
86
+ .map(([k, v]) => (
87
+ <div key={k} className="comp-view-row">
88
+ <span className="comp-view-label">{k}</span>
89
+ <span className="comp-view-value">
90
+ {typeof v === 'string' ? v || '—' : JSON.stringify(v)}
91
+ </span>
92
+ </div>
93
+ ))}
94
+ </div>
95
+ );
96
+ })}
97
+ </div>
98
+ );
99
+ }
100
+
101
+ register('user', 'react', UserView as any);
@@ -0,0 +1,7 @@
1
+ # treenity
2
+
3
+ Системный мод платформы. Actions для `/sys` ноды: обнаружение типов (catalog, search_types, describe_type), компиляция JSX-views, деплой prefab'ов. Реестр модулей `t.mod`.
4
+
5
+ ## Types
6
+ - `treenity.system` — actions: catalog, search_types, describe_type, compile_view, deploy_prefab
7
+ - `t.mod` — name, state (discovered/loading/loaded/failed/disabled)
@@ -0,0 +1,6 @@
1
+ import './schema-form';
2
+ import './type-view';
3
+ import './ref-view';
4
+ import { registerGroups } from './groups';
5
+
6
+ registerGroups();
@@ -0,0 +1,65 @@
1
+ // Tags form handler — react:form for "tags" format
2
+
3
+ import { Badge } from '#components/ui/badge';
4
+ import { Button } from '#components/ui/button';
5
+ import { Input } from '#components/ui/input';
6
+ import { register } from '@treenity/core/core';
7
+ import { X } from 'lucide-react';
8
+ import { useState } from 'react';
9
+
10
+ type FP = {
11
+ value: { $type: string; value: unknown; label?: string };
12
+ onChange?: (next: any) => void;
13
+ };
14
+
15
+ function TagsForm({ value, onChange }: FP) {
16
+ const [input, setInput] = useState('');
17
+ const tags: string[] = Array.isArray(value.value) ? value.value : [];
18
+
19
+ function add() {
20
+ const t = input.trim();
21
+ if (!t || tags.includes(t)) return;
22
+ onChange?.({ ...value, value: [...tags, t] });
23
+ setInput('');
24
+ }
25
+
26
+ function remove(tag: string) {
27
+ onChange?.({ ...value, value: tags.filter((t) => t !== tag) });
28
+ }
29
+
30
+ return (
31
+ <div className="space-y-2">
32
+ <div className="flex flex-wrap gap-1.5">
33
+ {tags.map((tag) => (
34
+ <Badge key={tag} variant="secondary" className="gap-1 pr-1">
35
+ {tag}
36
+ <button
37
+ type="button"
38
+ onClick={() => remove(tag)}
39
+ className="ml-0.5 rounded-full hover:bg-destructive/20 hover:text-destructive p-0.5"
40
+ >
41
+ <X className="h-3 w-3" />
42
+ </button>
43
+ </Badge>
44
+ ))}
45
+ {tags.length === 0 && <span className="text-xs text-muted-foreground">No groups</span>}
46
+ </div>
47
+ <div className="flex gap-2">
48
+ <Input
49
+ className="flex-1"
50
+ placeholder="Add group..."
51
+ value={input}
52
+ onChange={(e) => setInput(e.target.value)}
53
+ onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), add())}
54
+ />
55
+ <Button variant="outline" size="sm" type="button" onClick={add}>
56
+ Add
57
+ </Button>
58
+ </div>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ export function registerGroups() {
64
+ register('tags', 'react:form', TagsForm as any);
65
+ }
@@ -0,0 +1,133 @@
1
+ // Reusable type preview — Storybook-like: context switcher + live render + schema form editor
2
+
3
+ import { Render, RenderContext } from '#context';
4
+ import { type ComponentData, getContextsForType, type NodeData } from '@treenity/core/core';
5
+ import { useMemo, useState } from 'react';
6
+
7
+ // ── Mock data generator (by JSON Schema field type) ──
8
+
9
+ const STRING_HINTS: [RegExp, string][] = [
10
+ [/title|heading|name/i, 'Amazing Feature Title'],
11
+ [/desc|subtitle|summary|text|content|body/i, 'A short description of this amazing feature that showcases its value.'],
12
+ [/button|label|cta/i, 'Get Started'],
13
+ [/url|link|href/i, 'https://example.com'],
14
+ [/image|photo|avatar|picture|src/i, 'https://picsum.photos/seed/demo/400/200'],
15
+ [/email/i, 'user@example.com'],
16
+ [/phone|tel/i, '+1-555-0123'],
17
+ [/address/i, '42 Oak Street, San Francisco'],
18
+ [/color/i, '#4c88ff'],
19
+ [/date/i, '2026-01-15'],
20
+ [/status|state/i, 'active'],
21
+ [/tag|category/i, 'featured'],
22
+ ];
23
+
24
+ function mockValue(schema: Record<string, unknown>, fieldName = ''): unknown {
25
+ const type = schema.type as string | undefined;
26
+ if (schema.default !== undefined && schema.default !== '' && schema.default !== 0) return schema.default;
27
+ if (schema.enum) return (schema.enum as unknown[])[0];
28
+
29
+ switch (type) {
30
+ case 'string': {
31
+ const fmt = schema.format as string | undefined;
32
+ if (fmt === 'date' || fmt === 'date-time') return '2026-01-15';
33
+ if (fmt === 'email') return 'user@example.com';
34
+ if (fmt === 'uri' || fmt === 'url') return 'https://example.com';
35
+ if (fmt === 'color') return '#4c88ff';
36
+ if (fmt === 'image') return 'https://picsum.photos/seed/demo/400/200';
37
+ if (fmt === 'textarea') return 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
38
+ for (const [re, val] of STRING_HINTS) if (re.test(fieldName)) return val;
39
+ return schema.title ? `Sample ${schema.title}` : 'Sample text';
40
+ }
41
+ case 'number':
42
+ case 'integer': {
43
+ const min = (schema.minimum as number) ?? 0;
44
+ const max = (schema.maximum as number) ?? 100;
45
+ return Math.round((min + max) / 2) || 42;
46
+ }
47
+ case 'boolean':
48
+ return true;
49
+ case 'array': {
50
+ const items = schema.items as Record<string, unknown> | undefined;
51
+ if (!items) return ['alpha', 'beta', 'gamma'];
52
+ return [mockValue(items, fieldName), mockValue(items, fieldName), mockValue(items, fieldName)];
53
+ }
54
+ case 'object': {
55
+ const props = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
56
+ return mockObject(props);
57
+ }
58
+ default:
59
+ return 'sample';
60
+ }
61
+ }
62
+
63
+ export function mockObject(properties: Record<string, Record<string, unknown>>): Record<string, unknown> {
64
+ const result: Record<string, unknown> = {};
65
+ for (const [k, v] of Object.entries(properties)) result[k] = mockValue(v, k);
66
+ return result;
67
+ }
68
+
69
+ // ── Preview component ──
70
+
71
+ export function TypePreview({ typeName, properties }: {
72
+ typeName: string;
73
+ properties: Record<string, Record<string, unknown>>;
74
+ }) {
75
+ const contexts = getContextsForType(typeName);
76
+ const reactContexts = contexts.filter(c => c.startsWith('react'));
77
+
78
+ const initial = useMemo(() => ({
79
+ $path: `/preview/${typeName.replace(/\./g, '/')}`,
80
+ $type: typeName,
81
+ ...mockObject(properties),
82
+ } as NodeData), [typeName]);
83
+
84
+ const [previewCtx, setPreviewCtx] = useState<string | null>(null);
85
+ const [node, setNode] = useState<NodeData>(initial);
86
+
87
+ if (reactContexts.length === 0) return null;
88
+
89
+ const handleFormChange = (next: ComponentData) => {
90
+ setNode({ ...next, $path: initial.$path, $type: initial.$type } as NodeData);
91
+ };
92
+
93
+ return (
94
+ <div>
95
+ <div className="flex gap-1.5 mb-2">
96
+ {reactContexts.map(c => (
97
+ <button
98
+ key={c}
99
+ onClick={() => setPreviewCtx(prev => prev === c ? null : c)}
100
+ className={`px-2.5 py-0.5 rounded-full text-xs font-mono border cursor-pointer transition-colors ${
101
+ previewCtx === c
102
+ ? 'bg-primary text-primary-foreground border-primary'
103
+ : 'bg-muted border-border text-muted-foreground hover:border-primary/50'
104
+ }`}
105
+ >
106
+ {c}
107
+ </button>
108
+ ))}
109
+ </div>
110
+
111
+ {previewCtx && (
112
+ <div className="flex flex-col gap-3">
113
+ <div className="border border-border rounded-lg p-3 bg-background/50">
114
+ <RenderContext name={previewCtx}>
115
+ <Render value={node} />
116
+ </RenderContext>
117
+ </div>
118
+
119
+ <details className="group">
120
+ <summary className="text-xs text-muted-foreground cursor-pointer select-none hover:text-foreground transition-colors">
121
+ Edit data
122
+ </summary>
123
+ <div className="mt-2 border border-border rounded-lg p-3 bg-muted/30">
124
+ <RenderContext name="react:form">
125
+ <Render value={node} onChange={handleFormChange} />
126
+ </RenderContext>
127
+ </div>
128
+ </details>
129
+ </div>
130
+ )}
131
+ </div>
132
+ );
133
+ }
@@ -0,0 +1,87 @@
1
+ // Ref node/component view — shows target path, resolve button, inline preview
2
+
3
+ import { Render } from '#context';
4
+ import { usePath } from '#hooks';
5
+ import { type NodeData, register } from '@treenity/core/core';
6
+ import { useState } from 'react';
7
+
8
+ // ── Node-level view (for ref nodes like /sys/autostart/xxx) ──
9
+
10
+ function RefNodeView({ value, onSelect }: { value: NodeData; onSelect?: (p: string) => void }) {
11
+ const ref = (value as any).$ref as string | undefined;
12
+ if (!ref) return <div className="text-sm text-muted-foreground">Invalid ref (no $ref)</div>;
13
+
14
+ return <RefDisplay target={ref} onSelect={onSelect} />;
15
+ }
16
+
17
+ // ── List item (compact, inside child-card grid) ──
18
+
19
+ function RefListItem({ value }: { value: NodeData }) {
20
+ const ref = (value as any).$ref as string | undefined;
21
+ const name = value.$path.split('/').at(-1) || value.$path;
22
+
23
+ return (
24
+ <>
25
+ <span className="child-icon">&#128279;</span>
26
+ <div className="child-info">
27
+ <span className="child-name">{name}</span>
28
+ <span className="child-type">{ref ?? 'ref'}</span>
29
+ </div>
30
+ </>
31
+ );
32
+ }
33
+
34
+ // ── Shared display ──
35
+
36
+ function RefDisplay({ target, onSelect }: { target: string; onSelect?: (p: string) => void }) {
37
+ const [resolved, setResolved] = useState(false);
38
+ const targetNode = usePath(resolved ? target : null);
39
+
40
+ return (
41
+ <div className="space-y-3">
42
+ {/* Link row */}
43
+ <div className="flex items-center gap-2">
44
+ <span className="text-muted-foreground text-sm">ref</span>
45
+ <span className="text-sm font-mono text-primary">{target}</span>
46
+
47
+ {onSelect && (
48
+ <button
49
+ className="text-xs px-2 py-0.5 rounded bg-muted hover:bg-muted/80 text-foreground border border-border"
50
+ onClick={() => onSelect(target)}
51
+ >
52
+ Go to
53
+ </button>
54
+ )}
55
+
56
+ <button
57
+ className="text-xs px-2 py-0.5 rounded bg-muted hover:bg-muted/80 text-foreground border border-border"
58
+ onClick={() => setResolved(!resolved)}
59
+ >
60
+ {resolved ? 'Collapse' : 'Resolve'}
61
+ </button>
62
+ </div>
63
+
64
+ {/* Resolved target */}
65
+ {resolved && (
66
+ <div className="border border-border rounded p-3 bg-muted/30">
67
+ {targetNode ? (
68
+ <div className="space-y-1">
69
+ <div className="flex items-center gap-2 text-xs text-muted-foreground mb-2">
70
+ <span className="font-mono">{targetNode.$type}</span>
71
+ <span>{targetNode.$path}</span>
72
+ </div>
73
+ <Render value={targetNode} />
74
+ </div>
75
+ ) : (
76
+ <div className="text-sm text-muted-foreground">Loading...</div>
77
+ )}
78
+ </div>
79
+ )}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ // ── Registration ──
85
+
86
+ register('ref', 'react', RefNodeView as any);
87
+ register('ref', 'react:list', RefListItem as any);
@@ -0,0 +1,65 @@
1
+ // Default react:form handler — renders form from type schema automatically
2
+ // Any type without a specific react:form handler falls here via resolve('default', 'react:form')
3
+
4
+ import { type RenderProps } from '#context';
5
+ import { register, resolve } from '@treenity/core/core';
6
+ import { createElement } from 'react';
7
+
8
+ function DefaultSchemaForm({ value, onChange }: RenderProps) {
9
+ const schemaHandler = resolve(value.$type, 'schema');
10
+ if (!schemaHandler) {
11
+ return <div className="text-xs text-muted-foreground italic">No schema for {value.$type}</div>;
12
+ }
13
+
14
+ const schema = schemaHandler() as {
15
+ title?: string;
16
+ properties?: Record<string, Record<string, unknown>>;
17
+ };
18
+ const properties = schema.properties ?? {};
19
+
20
+ if (Object.keys(properties).length === 0) {
21
+ return <div className="text-xs text-muted-foreground italic">No fields defined</div>;
22
+ }
23
+
24
+ return (
25
+ <div className="flex flex-col gap-3">
26
+ {Object.entries(properties).map(([name, fieldSchema]) => {
27
+ const fieldType = String(fieldSchema.format ?? fieldSchema.type ?? 'string');
28
+ const isReadOnly = !!fieldSchema.readOnly;
29
+ const handler = resolve(fieldType, isReadOnly ? 'react' : 'react:form');
30
+ if (!handler) {
31
+ return (
32
+ <div key={name} className="text-xs text-destructive">
33
+ No form handler for type: {fieldType}
34
+ </div>
35
+ );
36
+ }
37
+
38
+ const fieldData: Record<string, unknown> = {
39
+ $type: fieldType,
40
+ value: (value as any)[name],
41
+ label: String(fieldSchema.label ?? fieldSchema.title ?? name),
42
+ placeholder: fieldSchema.description ? String(fieldSchema.description) : undefined,
43
+ };
44
+ if (fieldSchema.items) fieldData.items = fieldSchema.items;
45
+ if (fieldSchema.enum) fieldData.enum = fieldSchema.enum;
46
+
47
+ return (
48
+ <div key={name} className="flex flex-col gap-1">
49
+ {fieldType !== 'boolean' && (
50
+ <label className="text-xs font-medium text-muted-foreground">
51
+ {fieldData.label as string}
52
+ </label>
53
+ )}
54
+ {createElement(handler as any, {
55
+ value: fieldData,
56
+ onChange: (next: any) => onChange?.({ ...value, [name]: next.value }),
57
+ })}
58
+ </div>
59
+ );
60
+ })}
61
+ </div>
62
+ );
63
+ }
64
+
65
+ register('default', 'react:form', DefaultSchemaForm as any);