@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,861 @@
1
+ // react:form handlers — editable fields for inspector panel
2
+ import * as cache from '#cache';
3
+ import { tree as clientStore } from '#client';
4
+ import { useSchema } from '#schema-loader';
5
+ // react view handlers — readOnly display for same types
6
+ // Covers: string, text, textarea, number, integer, boolean, array, object, image, uri, url, select, timestamp, path
7
+ import { register, resolve as resolveHandler } from '@treenity/core/core';
8
+ import dayjs from 'dayjs';
9
+ import { X } from 'lucide-react';
10
+ import { createElement, useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
11
+
12
+ type FP = {
13
+ value: {
14
+ $type: string;
15
+ value: unknown;
16
+ label?: string;
17
+ placeholder?: string;
18
+ enum?: string[];
19
+ items?: { type?: string };
20
+ refType?: string; // component type — field can hold ref or embedded value of this type
21
+ };
22
+ onChange?: (next: any) => void;
23
+ };
24
+
25
+ // ── View handlers (react context) — readOnly display ──
26
+
27
+ function StringView({ value }: FP) {
28
+ return <span className="text-xs text-foreground/70">{String(value.value ?? '')}</span>;
29
+ }
30
+
31
+ function NumberView({ value }: FP) {
32
+ return <span className="text-xs font-mono text-foreground/70">{String(value.value ?? 0)}</span>;
33
+ }
34
+
35
+ function BooleanView({ value }: FP) {
36
+ return <span className="text-xs text-foreground/70">{value.value ? 'true' : 'false'}</span>;
37
+ }
38
+
39
+ function ImageView({ value }: FP) {
40
+ const src = typeof value.value === 'string' ? value.value : '';
41
+ return src ? <img src={src} className="max-w-full max-h-[120px] rounded object-contain" /> : null;
42
+ }
43
+
44
+ function UriView({ value }: FP) {
45
+ const url = String(value.value ?? '');
46
+ return url
47
+ ? <a href={url} target="_blank" rel="noopener" className="text-xs text-primary hover:underline truncate block">{url}</a>
48
+ : null;
49
+ }
50
+
51
+ function TimestampView({ value }: FP) {
52
+ const ts = Number(value.value ?? 0);
53
+ const formatted = ts ? dayjs(ts > 1e12 ? ts : ts * 1000).format('YYYY-MM-DD HH:mm:ss') : '—';
54
+ return <span className="text-xs font-mono text-foreground/70">{formatted}</span>;
55
+ }
56
+
57
+ function ArrayView({ value }: FP) {
58
+ const arr = Array.isArray(value.value) ? (value.value as unknown[]) : [];
59
+ if (arr.length === 0) return <span className="text-xs text-muted-foreground">[]</span>;
60
+ if (arr.every((v) => typeof v === 'string')) {
61
+ return (
62
+ <div className="flex flex-wrap gap-1">
63
+ {(arr as string[]).map((tag, i) => (
64
+ <span key={i} className="text-[11px] font-mono bg-muted text-foreground/70 px-1.5 py-0.5 rounded">{tag}</span>
65
+ ))}
66
+ </div>
67
+ );
68
+ }
69
+ return (
70
+ <pre className="text-[11px] font-mono text-foreground/70 bg-muted rounded p-2 overflow-auto max-h-[200px]">
71
+ {JSON.stringify(arr, null, 2)}
72
+ </pre>
73
+ );
74
+ }
75
+
76
+ function ObjectView({ value }: FP) {
77
+ const obj = (typeof value.value === 'object' && value.value !== null ? value.value : {}) as Record<string, unknown>;
78
+ const entries = Object.entries(obj);
79
+ if (entries.length === 0) return <span className="text-xs text-muted-foreground">{'{}'}</span>;
80
+ return (
81
+ <div className="space-y-0.5">
82
+ {entries.map(([k, v]) => (
83
+ <div key={k} className="flex gap-2 text-[11px]">
84
+ <span className="font-mono text-muted-foreground shrink-0">{k}:</span>
85
+ <span className="font-mono text-foreground/70 truncate">
86
+ {typeof v === 'object' ? JSON.stringify(v) : String(v ?? '')}
87
+ </span>
88
+ </div>
89
+ ))}
90
+ </div>
91
+ );
92
+ }
93
+
94
+ // ── Form handlers (react:form context) — editable ──
95
+
96
+ function StringForm({ value, onChange }: FP) {
97
+ // enum → select dropdown
98
+ if (value.enum && value.enum.length > 0) {
99
+ return (
100
+ <select
101
+ value={String(value.value ?? '')}
102
+ onChange={(e) => onChange?.({ ...value, value: e.target.value })}
103
+ >
104
+ <option value="">—</option>
105
+ {value.enum.map((v) => (
106
+ <option key={v} value={v}>{v}</option>
107
+ ))}
108
+ </select>
109
+ );
110
+ }
111
+
112
+ return (
113
+ <input
114
+ value={String(value.value ?? '')}
115
+ placeholder={value.placeholder}
116
+ onChange={(e) => onChange?.({ ...value, value: e.target.value })}
117
+ />
118
+ );
119
+ }
120
+
121
+ function TextForm({ value, onChange }: FP) {
122
+ return (
123
+ <textarea
124
+ value={String(value.value ?? '')}
125
+ placeholder={value.placeholder}
126
+ onChange={(e) => onChange?.({ ...value, value: e.target.value })}
127
+ />
128
+ );
129
+ }
130
+
131
+ function NumberForm({ value, onChange }: FP) {
132
+ return (
133
+ <input
134
+ type="number"
135
+ value={String(value.value ?? 0)}
136
+ onChange={(e) => onChange?.({ ...value, value: Number(e.target.value) })}
137
+ />
138
+ );
139
+ }
140
+
141
+ function IntegerForm({ value, onChange }: FP) {
142
+ return (
143
+ <input
144
+ type="number"
145
+ step="1"
146
+ value={String(value.value ?? 0)}
147
+ onChange={(e) => onChange?.({ ...value, value: Math.round(Number(e.target.value)) })}
148
+ />
149
+ );
150
+ }
151
+
152
+ function BooleanForm({ value, onChange }: FP) {
153
+ return (
154
+ <label className="flex items-center gap-2 cursor-pointer">
155
+ <input
156
+ type="checkbox"
157
+ className="w-auto"
158
+ checked={!!value.value}
159
+ onChange={(e) => onChange?.({ ...value, value: e.target.checked })}
160
+ />
161
+ <span className="text-xs text-muted-foreground">{value.label}</span>
162
+ </label>
163
+ );
164
+ }
165
+
166
+ function ImageForm({ value, onChange }: FP) {
167
+ const src = typeof value.value === 'string' ? value.value : '';
168
+ return (
169
+ <div className="space-y-2">
170
+ <input
171
+ value={src}
172
+ placeholder="Image URL"
173
+ onChange={(e) => onChange?.({ ...value, value: e.target.value })}
174
+ />
175
+ {src && <img src={src} className="max-w-full max-h-[120px] rounded object-contain" />}
176
+ </div>
177
+ );
178
+ }
179
+
180
+ function UriForm({ value, onChange }: FP) {
181
+ const url = String(value.value ?? '');
182
+ return (
183
+ <div className="space-y-1">
184
+ <input
185
+ value={url}
186
+ placeholder={value.placeholder ?? 'https://...'}
187
+ onChange={(e) => onChange?.({ ...value, value: e.target.value })}
188
+ />
189
+ {url && (
190
+ <a href={url} target="_blank" rel="noopener" className="text-[10px] text-primary hover:underline truncate block">
191
+ {url}
192
+ </a>
193
+ )}
194
+ </div>
195
+ );
196
+ }
197
+
198
+ function TimestampForm({ value, onChange }: FP) {
199
+ const ts = Number(value.value ?? 0);
200
+ const formatted = ts ? dayjs(ts > 1e12 ? ts : ts * 1000).format('YYYY-MM-DD HH:mm:ss') : '—';
201
+ return (
202
+ <div className="flex items-center gap-2">
203
+ <input
204
+ type="number"
205
+ className="flex-1"
206
+ value={String(ts)}
207
+ onChange={(e) => onChange?.({ ...value, value: Number(e.target.value) })}
208
+ />
209
+ <span className="text-[10px] text-muted-foreground whitespace-nowrap">{formatted}</span>
210
+ </div>
211
+ );
212
+ }
213
+
214
+ function SelectForm({ value, onChange }: FP) {
215
+ const opts = value.enum ?? [];
216
+ return (
217
+ <select
218
+ value={String(value.value ?? '')}
219
+ onChange={(e) => onChange?.({ ...value, value: e.target.value })}
220
+ >
221
+ <option value="">—</option>
222
+ {opts.map((v) => (
223
+ <option key={v} value={v}>{v}</option>
224
+ ))}
225
+ </select>
226
+ );
227
+ }
228
+
229
+ function ObjectForm({ value, onChange }: FP) {
230
+ const [mode, setMode] = useState<'fields' | 'json'>('fields');
231
+ const [jsonDraft, setJsonDraft] = useState('');
232
+ const [jsonError, setJsonError] = useState(false);
233
+ const [newKey, setNewKey] = useState('');
234
+ const obj = (typeof value.value === 'object' && value.value !== null ? value.value : {}) as Record<
235
+ string,
236
+ unknown
237
+ >;
238
+ const emit = (next: Record<string, unknown>) => onChange?.({ ...value, value: next });
239
+ const entries = Object.entries(obj);
240
+
241
+ const modeToggle = (
242
+ <div className="flex gap-1 mb-1">
243
+ <button
244
+ type="button"
245
+ className={`border-0 px-2 py-0.5 text-[10px] rounded ${mode === 'fields' ? 'bg-muted text-foreground' : 'bg-transparent text-muted-foreground hover:text-foreground'}`}
246
+ onClick={() => setMode('fields')}
247
+ >
248
+ Fields
249
+ </button>
250
+ <button
251
+ type="button"
252
+ className={`border-0 px-2 py-0.5 text-[10px] rounded ${mode === 'json' ? 'bg-muted text-foreground' : 'bg-transparent text-muted-foreground hover:text-foreground'}`}
253
+ onClick={() => {
254
+ setJsonDraft(JSON.stringify(obj, null, 2));
255
+ setJsonError(false);
256
+ setMode('json');
257
+ }}
258
+ >
259
+ JSON
260
+ </button>
261
+ </div>
262
+ );
263
+
264
+ if (mode === 'json') {
265
+ return (
266
+ <div className="rounded border border-border/50 bg-muted/30 p-2">
267
+ {modeToggle}
268
+ <textarea
269
+ className={`text-[11px] min-h-[60px] ${jsonError ? 'border-destructive' : ''}`}
270
+ value={jsonDraft}
271
+ onChange={(e) => {
272
+ setJsonDraft(e.target.value);
273
+ try {
274
+ const parsed = JSON.parse(e.target.value);
275
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
276
+ emit(parsed);
277
+ setJsonError(false);
278
+ } else {
279
+ setJsonError(true);
280
+ }
281
+ } catch {
282
+ setJsonError(true);
283
+ }
284
+ }}
285
+ />
286
+ </div>
287
+ );
288
+ }
289
+
290
+ return (
291
+ <div className="rounded border border-border/50 bg-muted/30 p-2">
292
+ {modeToggle}
293
+
294
+ {entries.length > 0 && (
295
+ <div className="space-y-1 mb-1.5">
296
+ {entries.map(([k, v]) => (
297
+ <div key={k} className="flex gap-1.5 items-start group">
298
+ <span className="text-[11px] font-mono text-muted-foreground pt-[5px] min-w-[50px] truncate shrink-0">
299
+ {k}
300
+ </span>
301
+ {typeof v === 'boolean' ? (
302
+ <input
303
+ type="checkbox"
304
+ className="w-auto mt-1.5"
305
+ checked={v}
306
+ onChange={(e) => emit({ ...obj, [k]: e.target.checked })}
307
+ />
308
+ ) : typeof v === 'number' ? (
309
+ <input
310
+ type="number"
311
+ className="flex-1 min-w-0 text-[11px]"
312
+ value={String(v)}
313
+ onChange={(e) => emit({ ...obj, [k]: Number(e.target.value) })}
314
+ />
315
+ ) : typeof v === 'string' ? (
316
+ <input
317
+ className="flex-1 min-w-0 text-[11px]"
318
+ value={v}
319
+ onChange={(e) => emit({ ...obj, [k]: e.target.value })}
320
+ />
321
+ ) : (
322
+ <textarea
323
+ className="flex-1 min-w-0 text-[11px] font-mono min-h-[40px]"
324
+ value={JSON.stringify(v, null, 2)}
325
+ onChange={(e) => {
326
+ try {
327
+ emit({ ...obj, [k]: JSON.parse(e.target.value) });
328
+ } catch {
329
+ /* typing */
330
+ }
331
+ }}
332
+ />
333
+ )}
334
+ <button
335
+ type="button"
336
+ className="border-0 bg-transparent p-0 mt-1 text-muted-foreground/30 opacity-0 group-hover:opacity-100 hover:text-destructive shrink-0 cursor-pointer transition-opacity"
337
+ onClick={() => {
338
+ const next = { ...obj };
339
+ delete next[k];
340
+ emit(next);
341
+ }}
342
+ >
343
+ <X className="h-3 w-3" />
344
+ </button>
345
+ </div>
346
+ ))}
347
+ </div>
348
+ )}
349
+
350
+ <div className="flex gap-1 items-center border-t border-border/30 pt-1.5">
351
+ <input
352
+ className="flex-1 min-w-0 text-[11px] bg-transparent border-dashed"
353
+ placeholder="new key..."
354
+ value={newKey}
355
+ onChange={(e) => setNewKey(e.target.value)}
356
+ onKeyDown={(e) => {
357
+ if (e.key !== 'Enter') return;
358
+ e.preventDefault();
359
+ const k = newKey.trim();
360
+ if (k && !(k in obj)) {
361
+ emit({ ...obj, [k]: '' });
362
+ setNewKey('');
363
+ }
364
+ }}
365
+ />
366
+ <button
367
+ type="button"
368
+ className="border-0 bg-transparent p-0 text-[11px] text-muted-foreground hover:text-foreground shrink-0 cursor-pointer"
369
+ onClick={() => {
370
+ const k = newKey.trim();
371
+ if (k && !(k in obj)) {
372
+ emit({ ...obj, [k]: '' });
373
+ setNewKey('');
374
+ }
375
+ }}
376
+ >
377
+ +
378
+ </button>
379
+ </div>
380
+ </div>
381
+ );
382
+ }
383
+
384
+ function ArrayForm({ value, onChange }: FP) {
385
+ const [input, setInput] = useState('');
386
+ const arr = Array.isArray(value.value) ? (value.value as unknown[]) : [];
387
+ const itemType = value.items?.type ?? 'string';
388
+ const emit = (next: unknown[]) => onChange?.({ ...value, value: next });
389
+
390
+ if (itemType === 'string') {
391
+ return (
392
+ <div className="rounded border border-border/50 bg-muted/30 p-2 space-y-1.5">
393
+ {arr.length > 0 && (
394
+ <div className="flex flex-wrap gap-1">
395
+ {(arr as string[]).map((tag, i) => (
396
+ <span
397
+ key={i}
398
+ className="inline-flex items-center gap-0.5 text-[11px] font-mono bg-muted text-foreground/70 px-1.5 py-0.5 rounded"
399
+ >
400
+ {tag}
401
+ <button
402
+ type="button"
403
+ className="ml-0.5 border-0 bg-transparent p-0 text-muted-foreground/40 hover:text-foreground leading-none cursor-pointer"
404
+ onClick={() => emit(arr.filter((_, j) => j !== i))}
405
+ >
406
+ ×
407
+ </button>
408
+ </span>
409
+ ))}
410
+ </div>
411
+ )}
412
+ <input
413
+ className="text-[11px] bg-transparent border-dashed"
414
+ placeholder="add item..."
415
+ value={input}
416
+ onChange={(e) => setInput(e.target.value)}
417
+ onKeyDown={(e) => {
418
+ if (e.key !== 'Enter') return;
419
+ e.preventDefault();
420
+ const t = input.trim();
421
+ if (t && !(arr as string[]).includes(t)) emit([...arr, t]);
422
+ setInput('');
423
+ }}
424
+ />
425
+ </div>
426
+ );
427
+ }
428
+
429
+ if (itemType === 'number') {
430
+ return (
431
+ <div className="rounded border border-border/50 bg-muted/30 p-2 space-y-1">
432
+ {arr.map((item, i) => (
433
+ <div key={i} className="flex gap-1 items-center group">
434
+ <input
435
+ type="number"
436
+ className="flex-1 text-[11px]"
437
+ value={String(item ?? 0)}
438
+ onChange={(e) => emit(arr.map((v, j) => (j === i ? Number(e.target.value) : v)))}
439
+ />
440
+ <button
441
+ type="button"
442
+ className="border-0 bg-transparent p-0 text-muted-foreground/30 opacity-0 group-hover:opacity-100 hover:text-destructive cursor-pointer transition-opacity"
443
+ onClick={() => emit(arr.filter((_, j) => j !== i))}
444
+ >
445
+ <X className="h-3 w-3" />
446
+ </button>
447
+ </div>
448
+ ))}
449
+ <button
450
+ type="button"
451
+ className="border-0 bg-transparent p-0 text-[11px] text-muted-foreground hover:text-foreground cursor-pointer"
452
+ onClick={() => emit([...arr, 0])}
453
+ >
454
+ + add
455
+ </button>
456
+ </div>
457
+ );
458
+ }
459
+
460
+ // object/other — textarea fallback
461
+ return (
462
+ <textarea
463
+ value={JSON.stringify(arr, null, 2)}
464
+ onChange={(e) => {
465
+ try {
466
+ emit(JSON.parse(e.target.value));
467
+ } catch {
468
+ /* let user keep typing */
469
+ }
470
+ }}
471
+ />
472
+ );
473
+ }
474
+
475
+ // ── Path field — node reference with drag-and-drop + tree picker ──
476
+
477
+ function PathView({ value }: FP) {
478
+ const path = String(value.value ?? '');
479
+ if (!path) return <span className="text-xs text-muted-foreground">—</span>;
480
+ return <span className="text-xs font-mono text-primary truncate block">{path}</span>;
481
+ }
482
+
483
+ // Compact tree picker dropdown for selecting a node path
484
+ // Lazy-loads children via trpc on expand, caches into front/cache
485
+ export function MiniTree({ onSelect, onClose }: { onSelect: (path: string) => void; onClose: () => void }) {
486
+ const ref = useRef<HTMLDivElement>(null);
487
+ const [expanded, setExpanded] = useState<Set<string>>(new Set(['/']));
488
+ const [loaded, setLoaded] = useState<Set<string>>(new Set());
489
+ const [filter, setFilter] = useState('');
490
+
491
+ // subscribe to cache changes for reactivity
492
+ useSyncExternalStore(
493
+ useCallback((cb: () => void) => cache.subscribeGlobal(cb), []),
494
+ useCallback(() => cache.getVersion(), []),
495
+ );
496
+
497
+ // Load root children on mount
498
+ useEffect(() => {
499
+ fetchChildren('/');
500
+ }, []);
501
+
502
+ // Close on outside click
503
+ useEffect(() => {
504
+ const handler = (e: MouseEvent) => {
505
+ if (ref.current && !ref.current.contains(e.target as Node)) onClose();
506
+ };
507
+ document.addEventListener('mousedown', handler);
508
+ return () => document.removeEventListener('mousedown', handler);
509
+ }, [onClose]);
510
+
511
+ async function fetchChildren(path: string) {
512
+ if (loaded.has(path)) return;
513
+ const { items } = await clientStore.getChildren(path);
514
+ cache.putMany(items, path);
515
+ setLoaded((prev) => new Set(prev).add(path));
516
+ }
517
+
518
+ function toggleExpand(path: string) {
519
+ setExpanded((prev) => {
520
+ const next = new Set(prev);
521
+ if (next.has(path)) {
522
+ next.delete(path);
523
+ } else {
524
+ next.add(path);
525
+ fetchChildren(path);
526
+ }
527
+ return next;
528
+ });
529
+ }
530
+
531
+ const nodes = cache.raw();
532
+ const lf = filter.toLowerCase();
533
+
534
+ function getKids(path: string): string[] {
535
+ return cache.getChildren(path).map((n) => n.$path).filter((p) => p !== path).sort();
536
+ }
537
+
538
+ function matchesFilter(path: string): boolean {
539
+ if (!lf) return true;
540
+ const n = nodes.get(path);
541
+ if (!n) return false;
542
+ const name = path === '/' ? '/' : path.slice(path.lastIndexOf('/') + 1);
543
+ return name.toLowerCase().includes(lf) || n.$type.toLowerCase().includes(lf);
544
+ }
545
+
546
+ function hasMatch(path: string): boolean {
547
+ if (matchesFilter(path)) return true;
548
+ for (const c of getKids(path)) {
549
+ if (hasMatch(c)) return true;
550
+ }
551
+ return false;
552
+ }
553
+
554
+ function renderNode(path: string, depth: number) {
555
+ if (!hasMatch(path)) return null;
556
+ const n = nodes.get(path);
557
+ if (!n) return null;
558
+ const name = path === '/' ? '/' : path.slice(path.lastIndexOf('/') + 1);
559
+ const kids = getKids(path);
560
+ const isExp = expanded.has(path);
561
+ const hasKids = kids.length > 0 || !loaded.has(path);
562
+
563
+ return (
564
+ <div key={path}>
565
+ <div
566
+ className="flex items-center gap-1 px-1 py-0.5 hover:bg-muted/60 cursor-pointer rounded text-[11px]"
567
+ style={{ paddingLeft: depth * 12 + 4 }}
568
+ onClick={() => onSelect(path)}
569
+ >
570
+ {hasKids ? (
571
+ <span
572
+ className="text-muted-foreground w-3 text-center shrink-0 cursor-pointer"
573
+ onClick={(e) => {
574
+ e.stopPropagation();
575
+ toggleExpand(path);
576
+ }}
577
+ >
578
+ {isExp ? '\u25BE' : '\u25B8'}
579
+ </span>
580
+ ) : (
581
+ <span className="w-3 shrink-0" />
582
+ )}
583
+ <span className="truncate">{name}</span>
584
+ <span className="text-muted-foreground text-[10px] ml-auto shrink-0">
585
+ {n.$type.includes('.') ? n.$type.slice(n.$type.lastIndexOf('.') + 1) : n.$type}
586
+ </span>
587
+ </div>
588
+ {isExp && kids.map((c) => renderNode(c, depth + 1))}
589
+ </div>
590
+ );
591
+ }
592
+
593
+ const rootKids = getKids('/');
594
+ const rootNode = nodes.get('/');
595
+
596
+ return (
597
+ <div
598
+ ref={ref}
599
+ className="absolute z-50 mt-1 w-64 max-h-60 overflow-auto bg-popover border border-border rounded-lg shadow-lg"
600
+ >
601
+ <div className="p-1.5 border-b border-border">
602
+ <input
603
+ className="text-[11px] w-full"
604
+ placeholder="Filter..."
605
+ value={filter}
606
+ onChange={(e) => setFilter(e.target.value)}
607
+ autoFocus
608
+ />
609
+ </div>
610
+ <div className="p-1">
611
+ {rootNode && renderNode('/', 0)}
612
+ {!rootNode && rootKids.map((r) => renderNode(r, 0))}
613
+ </div>
614
+ </div>
615
+ );
616
+ }
617
+
618
+ // Inline typed editor for embedded object values
619
+ function EmbeddedFields({ data, type, setData }: {
620
+ data: Record<string, unknown>;
621
+ type: string;
622
+ setData: (fn: (prev: Record<string, unknown>) => Record<string, unknown>) => void;
623
+ }) {
624
+ const schema = useSchema(type);
625
+ if (schema === undefined) return null; // loading
626
+
627
+ if (schema && Object.keys(schema.properties).length > 0) {
628
+ return (
629
+ <div className="space-y-1.5">
630
+ {Object.entries(schema.properties).map(([field, prop]) => {
631
+ const p = prop as {
632
+ type: string; title: string; format?: string; description?: string;
633
+ readOnly?: boolean; enum?: string[]; items?: { type?: string };
634
+ };
635
+ const fieldType = p.format ?? p.type;
636
+ if (fieldType === 'path') return null; // avoid infinite nesting for now
637
+ const handler = resolveHandler(fieldType, 'react:form') ?? resolveHandler('string', 'react:form');
638
+ if (!handler) return null;
639
+ const fieldData = {
640
+ $type: fieldType,
641
+ value: data[field],
642
+ label: p.title ?? field,
643
+ placeholder: p.description,
644
+ ...(p.items ? { items: p.items } : {}),
645
+ ...(p.enum ? { enum: p.enum } : {}),
646
+ };
647
+ return (
648
+ <div key={field} className="field">
649
+ {fieldType !== 'boolean' && <label>{p.title ?? field}</label>}
650
+ {createElement(handler as any, {
651
+ value: fieldData,
652
+ onChange: p.readOnly
653
+ ? undefined
654
+ : (next: { value: unknown }) => setData((prev) => ({ ...prev, [field]: next.value })),
655
+ })}
656
+ </div>
657
+ );
658
+ })}
659
+ </div>
660
+ );
661
+ }
662
+
663
+ // No schema — render plain key/value fields
664
+ const entries = Object.entries(data).filter(([k]) => !k.startsWith('$'));
665
+ if (entries.length === 0) return null;
666
+ return (
667
+ <div className="space-y-1">
668
+ {entries.map(([k, v]) => (
669
+ <div key={k} className="field">
670
+ <label>{k}</label>
671
+ <input
672
+ className="text-[11px]"
673
+ value={typeof v === 'string' ? v : JSON.stringify(v)}
674
+ onChange={(e) => setData((prev) => ({ ...prev, [k]: e.target.value }))}
675
+ />
676
+ </div>
677
+ ))}
678
+ </div>
679
+ );
680
+ }
681
+
682
+ function PathForm({ value, onChange }: FP) {
683
+ const raw = value.value;
684
+ const refType = value.refType; // expected component type from schema
685
+ const isValue = typeof raw === 'object' && raw !== null;
686
+ const refPath = isValue ? String((raw as Record<string, unknown>).$path ?? '') : String(raw ?? '');
687
+ const embeddedType = isValue ? String((raw as Record<string, unknown>).$type ?? '') : '';
688
+ const effectiveType = embeddedType || refType || '';
689
+ const [mode, setMode] = useState<'ref' | 'val'>(isValue ? 'val' : 'ref');
690
+ const [dragOver, setDragOver] = useState(false);
691
+ const [pickerOpen, setPickerOpen] = useState(false);
692
+ const wrapRef = useRef<HTMLDivElement>(null);
693
+
694
+ function setRef(p: string) {
695
+ onChange?.({ ...value, value: p });
696
+ }
697
+
698
+ async function setByValue(p: string) {
699
+ const node = await clientStore.get(p);
700
+ if (!node) return;
701
+ const copy: Record<string, unknown> = {};
702
+ for (const [k, v] of Object.entries(node)) {
703
+ if (k === '$rev') continue;
704
+ copy[k] = v;
705
+ }
706
+ onChange?.({ ...value, value: copy });
707
+ }
708
+
709
+ // Create empty embedded object from refType schema
710
+ function createEmpty() {
711
+ if (!refType) return;
712
+ onChange?.({ ...value, value: { $type: refType } });
713
+ }
714
+
715
+ function applyNode(path: string) {
716
+ if (mode === 'val') setByValue(path);
717
+ else setRef(path);
718
+ }
719
+
720
+ function updateEmbedded(fn: (prev: Record<string, unknown>) => Record<string, unknown>) {
721
+ if (!isValue) return;
722
+ const obj = raw as Record<string, unknown>;
723
+ onChange?.({ ...value, value: fn({ ...obj }) });
724
+ }
725
+
726
+ const hasValue = isValue || !!refPath;
727
+
728
+ return (
729
+ <div ref={wrapRef} className="relative">
730
+ <div
731
+ className={`rounded border transition-colors ${
732
+ dragOver ? 'border-primary ring-2 ring-primary/30 bg-primary/5' : 'border-border'
733
+ }`}
734
+ onDragOver={(e) => {
735
+ if (e.dataTransfer.types.includes('application/treenity-path')) {
736
+ e.preventDefault();
737
+ e.dataTransfer.dropEffect = 'link';
738
+ setDragOver(true);
739
+ }
740
+ }}
741
+ onDragLeave={() => setDragOver(false)}
742
+ onDrop={(e) => {
743
+ e.preventDefault();
744
+ setDragOver(false);
745
+ const dropped = e.dataTransfer.getData('application/treenity-path');
746
+ if (dropped) applyNode(dropped);
747
+ }}
748
+ >
749
+ {/* Header row: mode switch + input + controls */}
750
+ <div className="flex items-center gap-1">
751
+ {/* Mode toggle — always visible */}
752
+ <button
753
+ type="button"
754
+ className={`border-0 bg-transparent p-0 px-1 cursor-pointer shrink-0 text-[10px] font-bold font-mono ${
755
+ mode === 'val' ? 'text-amber-500' : 'text-primary'
756
+ }`}
757
+ onClick={() => {
758
+ if (mode === 'ref') {
759
+ setMode('val');
760
+ if (refPath) setByValue(refPath);
761
+ else if (!isValue) createEmpty();
762
+ } else {
763
+ setMode('ref');
764
+ if (refPath) setRef(refPath);
765
+ }
766
+ }}
767
+ title={mode === 'val' ? 'Value mode — embeds node data' : 'Ref mode — stores path'}
768
+ >
769
+ {mode}
770
+ </button>
771
+
772
+ {/* Type badge when refType is known */}
773
+ {refType && (
774
+ <span className="text-[9px] text-muted-foreground font-mono shrink-0">
775
+ {refType.includes('.') ? refType.slice(refType.lastIndexOf('.') + 1) : refType}
776
+ </span>
777
+ )}
778
+
779
+ {isValue ? (
780
+ <span className="flex-1 min-w-0 text-[11px] font-mono text-foreground/70 truncate py-1">
781
+ {refPath && <span className="text-muted-foreground">{refPath}</span>}
782
+ {embeddedType && (
783
+ <span className="ml-1 text-amber-500">{embeddedType}</span>
784
+ )}
785
+ </span>
786
+ ) : (
787
+ <input
788
+ className="flex-1 min-w-0 text-[11px] font-mono border-0 bg-transparent"
789
+ value={refPath}
790
+ placeholder="drop or pick a node"
791
+ onChange={(e) => setRef(e.target.value)}
792
+ />
793
+ )}
794
+
795
+ {hasValue && (
796
+ <button
797
+ type="button"
798
+ className="border-0 bg-transparent p-0 px-0.5 text-muted-foreground/40 hover:text-foreground cursor-pointer shrink-0"
799
+ onClick={() => { onChange?.({ ...value, value: '' }); setMode('ref'); }}
800
+ >
801
+ <X className="h-3 w-3" />
802
+ </button>
803
+ )}
804
+ <button
805
+ type="button"
806
+ className="border-0 bg-transparent p-0 px-1 text-muted-foreground hover:text-foreground cursor-pointer shrink-0 text-[11px]"
807
+ onClick={() => setPickerOpen((v) => !v)}
808
+ title="Browse tree"
809
+ >
810
+ &#9776;
811
+ </button>
812
+ </div>
813
+
814
+ {/* Inline typed editor for embedded value */}
815
+ {isValue && (
816
+ <div className="border-t border-border/50 p-2">
817
+ <EmbeddedFields
818
+ data={raw as Record<string, unknown>}
819
+ type={effectiveType}
820
+ setData={updateEmbedded}
821
+ />
822
+ </div>
823
+ )}
824
+ </div>
825
+
826
+ {pickerOpen && (
827
+ <MiniTree
828
+ onSelect={(p) => { applyNode(p); setPickerOpen(false); }}
829
+ onClose={() => setPickerOpen(false)}
830
+ />
831
+ )}
832
+ </div>
833
+ );
834
+ }
835
+
836
+ // ── Registration ──
837
+ // Each entry: [type, viewHandler, formHandler]
838
+
839
+ const fields: [string, Function, Function][] = [
840
+ ['string', StringView, StringForm],
841
+ ['text', StringView, TextForm],
842
+ ['textarea', StringView, TextForm],
843
+ ['number', NumberView, NumberForm],
844
+ ['integer', NumberView, IntegerForm],
845
+ ['boolean', BooleanView, BooleanForm],
846
+ ['array', ArrayView, ArrayForm],
847
+ ['object', ObjectView, ObjectForm],
848
+ ['image', ImageView, ImageForm],
849
+ ['uri', UriView, UriForm],
850
+ ['url', UriView, UriForm],
851
+ ['select', StringView, SelectForm],
852
+ ['timestamp', TimestampView, TimestampForm],
853
+ ['path', PathView, PathForm],
854
+ ];
855
+
856
+ export function registerFormFields() {
857
+ for (const [type, view, form] of fields) {
858
+ register(type, 'react', view as any);
859
+ register(type, 'react:form', form as any);
860
+ }
861
+ }