@treenity/react 3.0.1 → 3.0.3
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.
- package/README.md +1 -1
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +33 -7
- package/dist/App.js.map +1 -1
- package/dist/ComponentSection.js +1 -1
- package/dist/ComponentSection.js.map +1 -1
- package/dist/ErrorBoundary.d.ts.map +1 -1
- package/dist/ErrorBoundary.js +2 -1
- package/dist/ErrorBoundary.js.map +1 -1
- package/dist/Treenity.d.ts +15 -0
- package/dist/Treenity.d.ts.map +1 -0
- package/dist/Treenity.js +17 -0
- package/dist/Treenity.js.map +1 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +5 -0
- package/dist/cache.js.map +1 -1
- package/dist/client-tree.d.ts.map +1 -1
- package/dist/client-tree.js +2 -2
- package/dist/client-tree.js.map +1 -1
- package/dist/components/ui/button.d.ts +2 -2
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.js +3 -3
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/pagination.d.ts +2 -2
- package/dist/components/ui/pagination.d.ts.map +1 -1
- package/dist/components/ui/pagination.js +3 -3
- package/dist/components/ui/pagination.js.map +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/events.d.ts +2 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +47 -2
- package/dist/events.js.map +1 -1
- package/dist/fiber-tree.d.ts.map +1 -1
- package/dist/fiber-tree.js.map +1 -1
- package/dist/hooks.d.ts +9 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +80 -5
- package/dist/hooks.js.map +1 -1
- package/dist/lib/minimd.d.ts.map +1 -1
- package/dist/lib/minimd.js +8 -1
- package/dist/lib/minimd.js.map +1 -1
- package/dist/lib/sanitize-href.d.ts +3 -0
- package/dist/lib/sanitize-href.d.ts.map +1 -0
- package/dist/lib/sanitize-href.js +14 -0
- package/dist/lib/sanitize-href.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +14 -5
- package/dist/main.js.map +1 -1
- package/dist/mods/editor-ui/FieldLabel.d.ts.map +1 -1
- package/dist/mods/editor-ui/FieldLabel.js +2 -1
- package/dist/mods/editor-ui/FieldLabel.js.map +1 -1
- package/dist/mods/editor-ui/default-edit.js +4 -2
- package/dist/mods/editor-ui/default-edit.js.map +1 -1
- package/dist/mods/editor-ui/form-field.d.ts.map +1 -1
- package/dist/mods/editor-ui/form-field.js +3 -2
- package/dist/mods/editor-ui/form-field.js.map +1 -1
- package/dist/mods/editor-ui/type-picker.d.ts.map +1 -1
- package/dist/mods/editor-ui/type-picker.js +2 -1
- package/dist/mods/editor-ui/type-picker.js.map +1 -1
- package/dist/mods/treenity/preview.d.ts.map +1 -1
- package/dist/mods/treenity/preview.js +2 -3
- package/dist/mods/treenity/preview.js.map +1 -1
- package/dist/mods/treenity/ref-view.js +2 -1
- package/dist/mods/treenity/ref-view.js.map +1 -1
- package/dist/mods/treenity/seed.js +3 -2
- package/dist/mods/treenity/seed.js.map +1 -1
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +11 -5
- package/dist/symbols.js.map +1 -1
- package/package.json +4 -2
- package/src/App.tsx +29 -1
- package/src/ComponentSection.tsx +1 -1
- package/src/ErrorBoundary.tsx +6 -3
- package/src/Treenity.tsx +32 -0
- package/src/cache.ts +7 -0
- package/src/client-tree.ts +7 -7
- package/src/components/ui/button.tsx +4 -5
- package/src/components/ui/pagination.tsx +4 -9
- package/src/components/ui/textarea.tsx +1 -1
- package/src/events.ts +46 -6
- package/src/fiber-tree.ts +3 -3
- package/src/hooks.ts +73 -4
- package/src/lib/minimd.ts +7 -1
- package/src/lib/sanitize-href.ts +13 -0
- package/src/main.tsx +23 -11
- package/src/mods/editor-ui/FieldLabel.tsx +5 -4
- package/src/mods/editor-ui/default-edit.tsx +6 -4
- package/src/mods/editor-ui/form-field.tsx +4 -2
- package/src/mods/editor-ui/type-picker.tsx +3 -2
- package/src/mods/treenity/preview.tsx +6 -7
- package/src/mods/treenity/ref-view.tsx +11 -6
- package/src/mods/treenity/seed.ts +3 -2
- package/src/symbols.ts +12 -5
- package/src/bind/bind.test.ts +0 -316
- package/src/cache.test.ts +0 -139
- package/src/client-tree.test.ts +0 -116
- package/src/optimistic.test.ts +0 -111
- package/src/remote-tree.test.ts +0 -142
package/src/hooks.ts
CHANGED
|
@@ -99,7 +99,10 @@ export function useChildren(parentPath: string, opts?: WatchOpts) {
|
|
|
99
99
|
|
|
100
100
|
trpc.getChildren
|
|
101
101
|
.query({ path: parentPath, limit: opts?.limit, watch: opts?.watch, watchNew: opts?.watchNew })
|
|
102
|
-
.then((result: any) =>
|
|
102
|
+
.then((result: any) => {
|
|
103
|
+
if (result.truncated) console.warn(`[tree] Children of ${parentPath} truncated — results may be incomplete`);
|
|
104
|
+
cache.putMany(result.items as NodeData[], parentPath);
|
|
105
|
+
});
|
|
103
106
|
}, [parentPath, gen, opts?.limit, opts?.watch, opts?.watchNew]);
|
|
104
107
|
|
|
105
108
|
return useSyncExternalStore(
|
|
@@ -111,10 +114,17 @@ export function useChildren(parentPath: string, opts?: WatchOpts) {
|
|
|
111
114
|
// ── set: optimistic update + server persist ──
|
|
112
115
|
|
|
113
116
|
export async function set(next: NodeData) {
|
|
117
|
+
const prev = cache.get(next.$path);
|
|
114
118
|
cache.put(next);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
try {
|
|
120
|
+
await tree.set(next);
|
|
121
|
+
const fresh = await tree.get(next.$path);
|
|
122
|
+
if (fresh) cache.put(fresh);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
// F15: rollback optimistic cache on server reject (validation, ACL, OCC)
|
|
125
|
+
if (prev) cache.put(prev); else cache.remove(next.$path);
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
// ── execute: action caller ──
|
|
@@ -168,6 +178,65 @@ export function useCanWrite(path: string | null): boolean {
|
|
|
168
178
|
return (perm & W) !== 0;
|
|
169
179
|
}
|
|
170
180
|
|
|
181
|
+
// ── useAutoSave: throttled auto-persist for editable views ──
|
|
182
|
+
// First save fires after 500ms (responsive), subsequent saves throttled to 2s.
|
|
183
|
+
// Returns [localData, setField, dirty] — local updates are instant, server writes are batched.
|
|
184
|
+
|
|
185
|
+
// TODO: check why unused
|
|
186
|
+
export function useAutoSave(node: NodeData) {
|
|
187
|
+
const [local, setLocal] = useState<Record<string, unknown>>({});
|
|
188
|
+
const dirtyRef = useRef(false);
|
|
189
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
190
|
+
const savingRef = useRef(false);
|
|
191
|
+
const pendingRef = useRef(false);
|
|
192
|
+
const lastSaveRef = useRef(0);
|
|
193
|
+
const nodeRef = useRef(node);
|
|
194
|
+
nodeRef.current = node;
|
|
195
|
+
|
|
196
|
+
const flush = useCallback(async () => {
|
|
197
|
+
if (savingRef.current) { pendingRef.current = true; return; }
|
|
198
|
+
if (!dirtyRef.current) return;
|
|
199
|
+
savingRef.current = true;
|
|
200
|
+
try {
|
|
201
|
+
const merged = { ...nodeRef.current, ...local };
|
|
202
|
+
delete merged.$rev; // skip OCC — force-write
|
|
203
|
+
await set(merged);
|
|
204
|
+
dirtyRef.current = false;
|
|
205
|
+
lastSaveRef.current = Date.now();
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error('[autoSave] failed:', e);
|
|
208
|
+
} finally {
|
|
209
|
+
savingRef.current = false;
|
|
210
|
+
if (pendingRef.current) {
|
|
211
|
+
pendingRef.current = false;
|
|
212
|
+
flush();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}, [local]);
|
|
216
|
+
|
|
217
|
+
const setField = useCallback((field: string, value: unknown) => {
|
|
218
|
+
setLocal(prev => ({ ...prev, [field]: value }));
|
|
219
|
+
dirtyRef.current = true;
|
|
220
|
+
|
|
221
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
222
|
+
const elapsed = Date.now() - lastSaveRef.current;
|
|
223
|
+
const delay = elapsed > 2000 ? 500 : 2000; // first batch fast, then throttle
|
|
224
|
+
timerRef.current = setTimeout(() => { timerRef.current = null; flush(); }, delay);
|
|
225
|
+
}, [flush]);
|
|
226
|
+
|
|
227
|
+
// Flush on unmount
|
|
228
|
+
useEffect(() => () => {
|
|
229
|
+
if (timerRef.current) { clearTimeout(timerRef.current); flush(); }
|
|
230
|
+
}, [flush]);
|
|
231
|
+
|
|
232
|
+
// Reset local on node path change
|
|
233
|
+
useEffect(() => { setLocal({}); dirtyRef.current = false; }, [node.$path]);
|
|
234
|
+
|
|
235
|
+
const merged = useMemo(() => ({ ...node, ...local }), [node, local]);
|
|
236
|
+
|
|
237
|
+
return [merged, setField, dirtyRef.current] as const;
|
|
238
|
+
}
|
|
239
|
+
|
|
171
240
|
// ── Internals ──
|
|
172
241
|
|
|
173
242
|
const AsyncGenFn = Object.getPrototypeOf(async function* () { }).constructor;
|
package/src/lib/minimd.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Minimal markdown → HTML for chat bubbles (~50 lines)
|
|
2
2
|
// Covers: headers, bold, italic, inline code, code blocks, links, lists, blockquotes, hr
|
|
3
3
|
import './minimd.css';
|
|
4
|
+
import { sanitizeHref } from './sanitize-href';
|
|
4
5
|
|
|
5
6
|
const esc = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
6
7
|
|
|
@@ -11,7 +12,12 @@ function inline(s: string): string {
|
|
|
11
12
|
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
|
12
13
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
13
14
|
.replace(/_(.+?)_/g, '<em>$1</em>')
|
|
14
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
15
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
|
|
16
|
+
const safe = sanitizeHref(url);
|
|
17
|
+
if (!safe) return text;
|
|
18
|
+
const safeUrl = safe.replace(/"/g, '"');
|
|
19
|
+
return `<a href="${safeUrl}" target="_blank" rel="noopener">${text}</a>`;
|
|
20
|
+
});
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
export function minimd(src: string): string {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Allowlist-based href sanitizer — blocks javascript:, data:, vbscript:, etc.
|
|
2
|
+
// Strips control characters before protocol check to defeat browser bypass vectors
|
|
3
|
+
// (e.g. "java\tscript:" → browsers collapse to "javascript:")
|
|
4
|
+
|
|
5
|
+
const SAFE_PROTOCOL = /^(https?:|mailto:|tel:|\/|#)/i;
|
|
6
|
+
|
|
7
|
+
/** Returns sanitized URL string, or null if the protocol is unsafe. */
|
|
8
|
+
export function sanitizeHref(url: string): string | null {
|
|
9
|
+
const trimmed = url.replace(/[\x00-\x20]+/g, '');
|
|
10
|
+
if (!trimmed) return null;
|
|
11
|
+
if (SAFE_PROTOCOL.test(trimmed)) return url.trim();
|
|
12
|
+
return null;
|
|
13
|
+
}
|
package/src/main.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
3
|
import { enablePatches } from 'immer';
|
|
4
|
-
import {
|
|
4
|
+
import { type ReactNode } from 'react';
|
|
5
5
|
import { createRoot } from 'react-dom/client';
|
|
6
6
|
import { App } from './App';
|
|
7
7
|
import './load-client';
|
|
@@ -12,13 +12,25 @@ enablePatches();
|
|
|
12
12
|
|
|
13
13
|
const queryClient = new QueryClient();
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
// StrictMode off: FlowGram inversify container breaks on double-mount
|
|
16
|
+
// https://github.com/bytedance/flowgram.ai/issues/402
|
|
17
|
+
const Strict = ({ children }: { children: ReactNode }) => children;
|
|
18
|
+
|
|
19
|
+
/** Mount Treenity UI into a DOM element */
|
|
20
|
+
export function boot(el: HTMLElement | string = '#root') {
|
|
21
|
+
const root = typeof el === 'string' ? document.querySelector(el) : el;
|
|
22
|
+
if (!root) throw new Error(`Treenity boot: element "${el}" not found`);
|
|
23
|
+
createRoot(root as HTMLElement).render(
|
|
24
|
+
<Strict>
|
|
25
|
+
<QueryClientProvider client={queryClient}>
|
|
26
|
+
<App />
|
|
27
|
+
<Toaster />
|
|
28
|
+
</QueryClientProvider>
|
|
29
|
+
</Strict>,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Auto-boot when loaded directly (not imported)
|
|
34
|
+
if (typeof document !== 'undefined' && document.getElementById('root')) {
|
|
35
|
+
boot('#root');
|
|
36
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
DropdownMenuSeparator,
|
|
9
9
|
DropdownMenuTrigger,
|
|
10
10
|
} from '#components/ui/dropdown-menu';
|
|
11
|
+
import { Input } from '#components/ui/input';
|
|
11
12
|
import { isRef } from '@treenity/core';
|
|
12
13
|
import { useState } from 'react';
|
|
13
14
|
|
|
@@ -101,8 +102,8 @@ export function RefEditor({ value, onChange }: {
|
|
|
101
102
|
<div className="flex flex-col gap-1 flex-1 min-w-0">
|
|
102
103
|
<div className="flex items-center gap-1">
|
|
103
104
|
<span className="text-[9px] text-muted-foreground shrink-0 w-5">$ref</span>
|
|
104
|
-
<
|
|
105
|
-
className="flex-1 min-w-0"
|
|
105
|
+
<Input
|
|
106
|
+
className="h-7 text-xs flex-1 min-w-0"
|
|
106
107
|
value={value.$ref}
|
|
107
108
|
onChange={(e) => onChange(hasMap ? { $ref: e.target.value, $map: value.$map } : { $ref: e.target.value })}
|
|
108
109
|
placeholder="path"
|
|
@@ -111,8 +112,8 @@ export function RefEditor({ value, onChange }: {
|
|
|
111
112
|
{hasMap && (
|
|
112
113
|
<div className="flex items-center gap-1">
|
|
113
114
|
<span className="text-[9px] text-muted-foreground shrink-0 w-5">$map</span>
|
|
114
|
-
<
|
|
115
|
-
className="flex-1 min-w-0"
|
|
115
|
+
<Input
|
|
116
|
+
className="h-7 text-xs flex-1 min-w-0"
|
|
116
117
|
value={value.$map ?? ''}
|
|
117
118
|
onChange={(e) => onChange({ $ref: value.$ref, $map: e.target.value })}
|
|
118
119
|
placeholder="field"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Checkbox } from '#components/ui/checkbox';
|
|
2
|
+
import { Input } from '#components/ui/input';
|
|
1
3
|
import { useSchema } from '#schema-loader';
|
|
2
4
|
import { type ComponentData, isRef, register, resolve } from '@treenity/core';
|
|
3
5
|
import { createElement } from 'react';
|
|
@@ -57,12 +59,12 @@ function DefaultEditForm({ value, onChange }: { value: ComponentData; onChange?:
|
|
|
57
59
|
<FieldLabel label={k} value={v} onChange={onCh} />
|
|
58
60
|
{typeof v === 'boolean' ? (
|
|
59
61
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
60
|
-
<
|
|
61
|
-
onChange={(e) => setData((prev) => ({ ...prev, [k]: e.target.checked }))} />
|
|
62
|
+
<Checkbox checked={!!data[k]}
|
|
63
|
+
onChange={(e) => setData((prev) => ({ ...prev, [k]: (e.target as HTMLInputElement).checked }))} />
|
|
62
64
|
{data[k] ? 'true' : 'false'}
|
|
63
65
|
</label>
|
|
64
66
|
) : typeof v === 'number' ? (
|
|
65
|
-
<
|
|
67
|
+
<Input type="number" className="h-7 text-xs" value={String(data[k] ?? 0)}
|
|
66
68
|
onChange={(e) => setData((prev) => ({ ...prev, [k]: Number(e.target.value) }))} />
|
|
67
69
|
) : Array.isArray(v) ? (
|
|
68
70
|
<StringArrayField value={data[k] as unknown[]}
|
|
@@ -78,7 +80,7 @@ function DefaultEditForm({ value, onChange }: { value: ComponentData; onChange?:
|
|
|
78
80
|
: <pre className="text-[11px] font-mono text-foreground/60">{JSON.stringify(data[k], null, 2)}</pre>;
|
|
79
81
|
})()
|
|
80
82
|
) : (
|
|
81
|
-
<
|
|
83
|
+
<Input className="h-7 text-xs" value={String(data[k] ?? '')}
|
|
82
84
|
onChange={(e) => setData((prev) => ({ ...prev, [k]: e.target.value }))} />
|
|
83
85
|
)}
|
|
84
86
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Button } from '#components/ui/button';
|
|
2
2
|
import { Input } from '#components/ui/input';
|
|
3
|
+
import { Textarea } from '#components/ui/textarea';
|
|
3
4
|
import { isRef, resolveExact } from '@treenity/core';
|
|
4
5
|
import { createElement, useState } from 'react';
|
|
5
6
|
import { FieldLabel, RefEditor } from './FieldLabel';
|
|
@@ -42,7 +43,7 @@ export function renderField(
|
|
|
42
43
|
const handler = resolveExact(fieldSchema.type, ctx) ?? resolveExact('string', ctx);
|
|
43
44
|
if (!handler)
|
|
44
45
|
return (
|
|
45
|
-
<div key={name} className="text-
|
|
46
|
+
<div key={name} className="text-destructive text-xs">
|
|
46
47
|
No form handler: {fieldSchema.type}
|
|
47
48
|
</div>
|
|
48
49
|
);
|
|
@@ -91,7 +92,8 @@ export function StringArrayField({
|
|
|
91
92
|
|
|
92
93
|
if (!isStrings) {
|
|
93
94
|
return (
|
|
94
|
-
<
|
|
95
|
+
<Textarea
|
|
96
|
+
className="min-h-16 text-xs font-mono"
|
|
95
97
|
value={JSON.stringify(value, null, 2)}
|
|
96
98
|
onChange={(e) => {
|
|
97
99
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Button } from '#components/ui/button';
|
|
2
2
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '#components/ui/command';
|
|
3
3
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '#components/ui/dialog';
|
|
4
|
+
import { Input } from '#components/ui/input';
|
|
4
5
|
import { trpc } from '#trpc';
|
|
5
6
|
import { isOfType, type NodeData } from '@treenity/core';
|
|
6
7
|
import { useEffect, useRef, useState } from 'react';
|
|
@@ -123,9 +124,9 @@ export function TypePicker({
|
|
|
123
124
|
</Command>
|
|
124
125
|
|
|
125
126
|
<div className="px-4 py-3 border-t border-border">
|
|
126
|
-
<
|
|
127
|
+
<Input
|
|
127
128
|
ref={nameRef}
|
|
128
|
-
className="
|
|
129
|
+
className="h-8 text-sm"
|
|
129
130
|
placeholder={nameLabel}
|
|
130
131
|
value={name}
|
|
131
132
|
onChange={(e) => { setName(e.target.value); setNameManual(true); }}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Reusable type preview — Storybook-like: context switcher + live render + schema form editor
|
|
2
2
|
|
|
3
|
+
import { Button } from '#components/ui/button';
|
|
3
4
|
import { Render, RenderContext } from '#context';
|
|
4
5
|
import { type ComponentData, getContextsForType, type NodeData } from '@treenity/core';
|
|
5
6
|
import { useMemo, useState } from 'react';
|
|
@@ -94,17 +95,15 @@ export function TypePreview({ typeName, properties }: {
|
|
|
94
95
|
<div>
|
|
95
96
|
<div className="flex gap-1.5 mb-2">
|
|
96
97
|
{reactContexts.map(c => (
|
|
97
|
-
<
|
|
98
|
+
<Button
|
|
98
99
|
key={c}
|
|
100
|
+
variant={previewCtx === c ? 'default' : 'outline'}
|
|
101
|
+
size="sm"
|
|
102
|
+
className="h-auto rounded-full px-2.5 py-0.5 text-xs font-mono"
|
|
99
103
|
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
104
|
>
|
|
106
105
|
{c}
|
|
107
|
-
</
|
|
106
|
+
</Button>
|
|
108
107
|
))}
|
|
109
108
|
</div>
|
|
110
109
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Ref node/component view — shows target path, resolve button, inline preview
|
|
2
2
|
|
|
3
|
+
import { Button } from '#components/ui/button';
|
|
3
4
|
import { Render } from '#context';
|
|
4
5
|
import { usePath } from '#hooks';
|
|
5
6
|
import { type NodeData, register } from '@treenity/core';
|
|
@@ -45,20 +46,24 @@ function RefDisplay({ target, onSelect }: { target: string; onSelect?: (p: strin
|
|
|
45
46
|
<span className="text-sm font-mono text-primary">{target}</span>
|
|
46
47
|
|
|
47
48
|
{onSelect && (
|
|
48
|
-
<
|
|
49
|
-
|
|
49
|
+
<Button
|
|
50
|
+
variant="outline"
|
|
51
|
+
size="sm"
|
|
52
|
+
className="h-auto px-2 py-0.5 text-xs"
|
|
50
53
|
onClick={() => onSelect(target)}
|
|
51
54
|
>
|
|
52
55
|
Go to
|
|
53
|
-
</
|
|
56
|
+
</Button>
|
|
54
57
|
)}
|
|
55
58
|
|
|
56
|
-
<
|
|
57
|
-
|
|
59
|
+
<Button
|
|
60
|
+
variant="outline"
|
|
61
|
+
size="sm"
|
|
62
|
+
className="h-auto px-2 py-0.5 text-xs"
|
|
58
63
|
onClick={() => setResolved(!resolved)}
|
|
59
64
|
>
|
|
60
65
|
{resolved ? 'Collapse' : 'Resolve'}
|
|
61
|
-
</
|
|
66
|
+
</Button>
|
|
62
67
|
</div>
|
|
63
68
|
|
|
64
69
|
{/* Resolved target */}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type NodeData, R, S } from '@treenity/core';
|
|
1
|
+
import { type NodeData, A, R, S, W } from '@treenity/core';
|
|
2
2
|
import { registerPrefab } from '@treenity/core/mod';
|
|
3
3
|
|
|
4
4
|
registerPrefab('core', 'seed', [
|
|
5
5
|
{ $path: 'sys', $type: 'treenity.system' },
|
|
6
|
-
{ $path: 'auth', $type: 'dir' },
|
|
6
|
+
{ $path: 'auth', $type: 'dir', $acl: [{ g: 'admins', p: R | W | A | S }, { g: 'public', p: 0 }] },
|
|
7
7
|
{ $path: 'auth/users', $type: 'mount-point',
|
|
8
8
|
connection: { $type: 'connection', db: 'treenity', collection: 'users' },
|
|
9
9
|
mount: { $type: 't.mount.mongo' },
|
|
@@ -20,6 +20,7 @@ registerPrefab('core', 'seed', [
|
|
|
20
20
|
{ $path: 'auth/sessions', $type: 'mount-point',
|
|
21
21
|
connection: { $type: 'connection', db: 'treenity', collection: 'sessions' },
|
|
22
22
|
mount: { $type: 't.mount.mongo' },
|
|
23
|
+
$acl: [{ g: 'admins', p: R | W | A | S }, { g: 'authenticated', p: 0 }, { g: 'public', p: 0 }],
|
|
23
24
|
},
|
|
24
25
|
{ $path: 'mnt', $type: 'dir' },
|
|
25
26
|
{ $path: 'mnt/orders', $type: 't.mount.mongo',
|
package/src/symbols.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
// Symbol-based component location metadata.
|
|
2
|
-
// Stamped on deserialization (cache.put).
|
|
2
|
+
// Stamped on deserialization (cache.put). Non-enumerable so they're
|
|
3
|
+
// invisible to structuredClone, spread, and JSON/keys/entries.
|
|
3
4
|
|
|
4
5
|
import { isComponent, type NodeData } from '@treenity/core';
|
|
5
6
|
|
|
6
7
|
export const $key = Symbol.for('treenity.$key');
|
|
7
8
|
export const $node = Symbol.for('treenity.$node');
|
|
8
9
|
|
|
10
|
+
function hide(obj: object, sym: symbol, value: unknown): void {
|
|
11
|
+
Object.defineProperty(obj, sym, { value, enumerable: false, writable: false, configurable: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
export function stampNode(node: NodeData): void {
|
|
10
|
-
(node as any)[$
|
|
11
|
-
|
|
15
|
+
if ((node as any)[$node] === node) return;
|
|
16
|
+
|
|
17
|
+
hide(node, $key, '');
|
|
18
|
+
hide(node, $node, node);
|
|
12
19
|
|
|
13
20
|
for (const [k, v] of Object.entries(node)) {
|
|
14
21
|
if (k.startsWith('$') || !isComponent(v)) continue;
|
|
15
|
-
(v
|
|
16
|
-
(v
|
|
22
|
+
hide(v, $key, k);
|
|
23
|
+
hide(v, $node, node);
|
|
17
24
|
}
|
|
18
25
|
}
|