@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.
- package/dist/AclEditor.d.ts +11 -0
- package/dist/AclEditor.d.ts.map +1 -0
- package/dist/AclEditor.js +152 -0
- package/dist/AclEditor.js.map +1 -0
- package/dist/App.d.ts +2 -0
- package/dist/App.d.ts.map +1 -0
- package/dist/App.js +521 -0
- package/dist/App.js.map +1 -0
- package/dist/Inspector.d.ts +12 -0
- package/dist/Inspector.d.ts.map +1 -0
- package/dist/Inspector.js +360 -0
- package/dist/Inspector.js.map +1 -0
- package/dist/Tree.d.ts +16 -0
- package/dist/Tree.d.ts.map +1 -0
- package/dist/Tree.js +100 -0
- package/dist/Tree.js.map +1 -0
- package/dist/ViewPage.d.ts +5 -0
- package/dist/ViewPage.d.ts.map +1 -0
- package/dist/ViewPage.js +13 -0
- package/dist/ViewPage.js.map +1 -0
- package/dist/bind/computed.d.ts +9 -0
- package/dist/bind/computed.d.ts.map +1 -0
- package/dist/bind/computed.js +61 -0
- package/dist/bind/computed.js.map +1 -0
- package/dist/bind/engine.d.ts +3 -0
- package/dist/bind/engine.d.ts.map +1 -0
- package/dist/bind/engine.js +184 -0
- package/dist/bind/engine.js.map +1 -0
- package/dist/bind/eval.d.ts +13 -0
- package/dist/bind/eval.d.ts.map +1 -0
- package/dist/bind/eval.js +97 -0
- package/dist/bind/eval.js.map +1 -0
- package/dist/bind/hook.d.ts +8 -0
- package/dist/bind/hook.d.ts.map +1 -0
- package/dist/bind/hook.js +99 -0
- package/dist/bind/hook.js.map +1 -0
- package/dist/bind/parse.d.ts +19 -0
- package/dist/bind/parse.d.ts.map +1 -0
- package/dist/bind/parse.js +86 -0
- package/dist/bind/parse.js.map +1 -0
- package/dist/bind/pipes.d.ts +4 -0
- package/dist/bind/pipes.d.ts.map +1 -0
- package/dist/bind/pipes.js +43 -0
- package/dist/bind/pipes.js.map +1 -0
- package/dist/cache.d.ts +27 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +236 -0
- package/dist/cache.js.map +1 -0
- package/dist/client-tree.d.ts +9 -0
- package/dist/client-tree.d.ts.map +1 -0
- package/dist/client-tree.js +14 -0
- package/dist/client-tree.js.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -0
- package/dist/components/ui/accordion.d.ts +8 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +18 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/badge.d.ts +10 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +19 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/button.d.ts +11 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +31 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +7 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +18 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +37 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +14 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +35 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/input.d.ts +4 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +7 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +8 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/popover.d.ts +11 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +26 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/progress.d.ts +5 -0
- package/dist/components/ui/progress.d.ts.map +1 -0
- package/dist/components/ui/progress.js +9 -0
- package/dist/components/ui/progress.js.map +1 -0
- package/dist/components/ui/select.d.ts +16 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +39 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/slider.d.ts +5 -0
- package/dist/components/ui/slider.d.ts.map +1 -0
- package/dist/components/ui/slider.js +15 -0
- package/dist/components/ui/slider.js.map +1 -0
- package/dist/components/ui/sonner.d.ts +4 -0
- package/dist/components/ui/sonner.d.ts.map +1 -0
- package/dist/components/ui/sonner.js +21 -0
- package/dist/components/ui/sonner.js.map +1 -0
- package/dist/components/ui/switch.d.ts +7 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +9 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +4 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +7 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +8 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +18 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/context/index.d.ts +31 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +98 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +2 -0
- package/dist/context.js.map +1 -0
- package/dist/hooks.d.ts +21 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +156 -0
- package/dist/hooks.js.map +1 -0
- package/dist/idb.d.ts +13 -0
- package/dist/idb.d.ts.map +1 -0
- package/dist/idb.js +67 -0
- package/dist/idb.js.map +1 -0
- package/dist/lib/minimd.d.ts +3 -0
- package/dist/lib/minimd.d.ts.map +1 -0
- package/dist/lib/minimd.js +97 -0
- package/dist/lib/minimd.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/load-client.d.ts +2 -0
- package/dist/load-client.d.ts.map +1 -0
- package/dist/load-client.js +6 -0
- package/dist/load-client.js.map +1 -0
- package/dist/main.d.ts +4 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +16 -0
- package/dist/main.js.map +1 -0
- package/dist/mods/editor-ui/client.d.ts +6 -0
- package/dist/mods/editor-ui/client.d.ts.map +1 -0
- package/dist/mods/editor-ui/client.js +8 -0
- package/dist/mods/editor-ui/client.js.map +1 -0
- package/dist/mods/editor-ui/default-view.d.ts +2 -0
- package/dist/mods/editor-ui/default-view.d.ts.map +1 -0
- package/dist/mods/editor-ui/default-view.js +71 -0
- package/dist/mods/editor-ui/default-view.js.map +1 -0
- package/dist/mods/editor-ui/dir-view.d.ts +2 -0
- package/dist/mods/editor-ui/dir-view.d.ts.map +1 -0
- package/dist/mods/editor-ui/dir-view.js +42 -0
- package/dist/mods/editor-ui/dir-view.js.map +1 -0
- package/dist/mods/editor-ui/form-fields.d.ts +6 -0
- package/dist/mods/editor-ui/form-fields.d.ts.map +1 -0
- package/dist/mods/editor-ui/form-fields.js +401 -0
- package/dist/mods/editor-ui/form-fields.js.map +1 -0
- package/dist/mods/editor-ui/layout-view.d.ts +2 -0
- package/dist/mods/editor-ui/layout-view.d.ts.map +1 -0
- package/dist/mods/editor-ui/layout-view.js +22 -0
- package/dist/mods/editor-ui/layout-view.js.map +1 -0
- package/dist/mods/editor-ui/list-items.d.ts +2 -0
- package/dist/mods/editor-ui/list-items.d.ts.map +1 -0
- package/dist/mods/editor-ui/list-items.js +38 -0
- package/dist/mods/editor-ui/list-items.js.map +1 -0
- package/dist/mods/editor-ui/node-utils.d.ts +10 -0
- package/dist/mods/editor-ui/node-utils.d.ts.map +1 -0
- package/dist/mods/editor-ui/node-utils.js +76 -0
- package/dist/mods/editor-ui/node-utils.js.map +1 -0
- package/dist/mods/editor-ui/user-view.d.ts +2 -0
- package/dist/mods/editor-ui/user-view.d.ts.map +1 -0
- package/dist/mods/editor-ui/user-view.js +47 -0
- package/dist/mods/editor-ui/user-view.js.map +1 -0
- package/dist/mods/treenity/client.d.ts +4 -0
- package/dist/mods/treenity/client.d.ts.map +1 -0
- package/dist/mods/treenity/client.js +6 -0
- package/dist/mods/treenity/client.js.map +1 -0
- package/dist/mods/treenity/groups/index.d.ts +2 -0
- package/dist/mods/treenity/groups/index.d.ts.map +1 -0
- package/dist/mods/treenity/groups/index.js +27 -0
- package/dist/mods/treenity/groups/index.js.map +1 -0
- package/dist/mods/treenity/preview.d.ts +6 -0
- package/dist/mods/treenity/preview.d.ts.map +1 -0
- package/dist/mods/treenity/preview.js +95 -0
- package/dist/mods/treenity/preview.js.map +1 -0
- package/dist/mods/treenity/ref-view.d.ts +2 -0
- package/dist/mods/treenity/ref-view.d.ts.map +1 -0
- package/dist/mods/treenity/ref-view.js +29 -0
- package/dist/mods/treenity/ref-view.js.map +1 -0
- package/dist/mods/treenity/schema-form.d.ts +2 -0
- package/dist/mods/treenity/schema-form.d.ts.map +1 -0
- package/dist/mods/treenity/schema-form.js +38 -0
- package/dist/mods/treenity/schema-form.js.map +1 -0
- package/dist/mods/treenity/seed.d.ts +2 -0
- package/dist/mods/treenity/seed.d.ts.map +1 -0
- package/dist/mods/treenity/seed.js +53 -0
- package/dist/mods/treenity/seed.js.map +1 -0
- package/dist/mods/treenity/server.d.ts +2 -0
- package/dist/mods/treenity/server.d.ts.map +1 -0
- package/dist/mods/treenity/server.js +2 -0
- package/dist/mods/treenity/server.js.map +1 -0
- package/dist/mods/treenity/type-view.d.ts +2 -0
- package/dist/mods/treenity/type-view.d.ts.map +1 -0
- package/dist/mods/treenity/type-view.js +36 -0
- package/dist/mods/treenity/type-view.js.map +1 -0
- package/dist/remote-tree.d.ts +6 -0
- package/dist/remote-tree.d.ts.map +1 -0
- package/dist/remote-tree.js +18 -0
- package/dist/remote-tree.js.map +1 -0
- package/dist/schema-loader.d.ts +19 -0
- package/dist/schema-loader.d.ts.map +1 -0
- package/dist/schema-loader.js +63 -0
- package/dist/schema-loader.js.map +1 -0
- package/dist/trpc.d.ts +187 -0
- package/dist/trpc.d.ts.map +1 -0
- package/dist/trpc.js +21 -0
- package/dist/trpc.js.map +1 -0
- package/package.json +88 -0
- package/src/AclEditor.tsx +330 -0
- package/src/App.tsx +775 -0
- package/src/CLAUDE.md +16 -0
- package/src/Inspector.tsx +857 -0
- package/src/Tree.tsx +237 -0
- package/src/ViewPage.tsx +45 -0
- package/src/bind/bind.test.ts +316 -0
- package/src/bind/computed.ts +64 -0
- package/src/bind/engine.ts +198 -0
- package/src/bind/eval.ts +108 -0
- package/src/bind/hook.ts +112 -0
- package/src/bind/parse.ts +104 -0
- package/src/bind/pipes.ts +71 -0
- package/src/cache.test.ts +139 -0
- package/src/cache.ts +244 -0
- package/src/client-tree.test.ts +116 -0
- package/src/client-tree.ts +24 -0
- package/src/client.ts +11 -0
- package/src/components/ui/accordion.tsx +63 -0
- package/src/components/ui/badge.tsx +27 -0
- package/src/components/ui/button.tsx +44 -0
- package/src/components/ui/checkbox.tsx +19 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/drawer.tsx +132 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/popover.tsx +86 -0
- package/src/components/ui/progress.tsx +30 -0
- package/src/components/ui/select.tsx +189 -0
- package/src/components/ui/slider.tsx +62 -0
- package/src/components/ui/sonner.tsx +32 -0
- package/src/components/ui/switch.tsx +34 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/context/index.tsx +131 -0
- package/src/context.ts +1 -0
- package/src/hooks.ts +208 -0
- package/src/idb.ts +80 -0
- package/src/index.html +14 -0
- package/src/lib/minimd.css +28 -0
- package/src/lib/minimd.ts +95 -0
- package/src/lib/utils.ts +6 -0
- package/src/load-client.ts +5 -0
- package/src/main.tsx +22 -0
- package/src/mods/editor-ui/CLAUDE.md +3 -0
- package/src/mods/editor-ui/client.ts +8 -0
- package/src/mods/editor-ui/default-view.tsx +148 -0
- package/src/mods/editor-ui/dir-view.tsx +91 -0
- package/src/mods/editor-ui/form-fields.tsx +861 -0
- package/src/mods/editor-ui/layout-view.tsx +62 -0
- package/src/mods/editor-ui/list-items.tsx +63 -0
- package/src/mods/editor-ui/node-utils.ts +84 -0
- package/src/mods/editor-ui/user-view.tsx +101 -0
- package/src/mods/treenity/CLAUDE.md +7 -0
- package/src/mods/treenity/client.ts +6 -0
- package/src/mods/treenity/groups/index.tsx +65 -0
- package/src/mods/treenity/preview.tsx +133 -0
- package/src/mods/treenity/ref-view.tsx +87 -0
- package/src/mods/treenity/schema-form.tsx +65 -0
- package/src/mods/treenity/seed.ts +56 -0
- package/src/mods/treenity/server.ts +1 -0
- package/src/mods/treenity/type-view.tsx +116 -0
- package/src/remote-tree.test.ts +142 -0
- package/src/remote-tree.ts +25 -0
- package/src/schema-loader.ts +84 -0
- package/src/style.css +1269 -0
- package/src/trpc.ts +27 -0
- package/src/vite-env.d.ts +3 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Cache contract tests — plain Map implementation
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert';
|
|
4
|
+
import { beforeEach, describe, it } from 'node:test';
|
|
5
|
+
import * as cache from './cache';
|
|
6
|
+
|
|
7
|
+
describe('cache', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
cache.clear();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('put() creates entry, get() returns node', () => {
|
|
13
|
+
cache.put({ $path: '/a', $type: 'x', v: 1 } as any);
|
|
14
|
+
const n = cache.get('/a');
|
|
15
|
+
assert.ok(n);
|
|
16
|
+
assert.strictEqual(n!.v, 1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('second put() updates value', () => {
|
|
20
|
+
cache.put({ $path: '/a', $type: 'x', v: 1 } as any);
|
|
21
|
+
cache.put({ $path: '/a', $type: 'x', v: 2 } as any);
|
|
22
|
+
const n = cache.get('/a');
|
|
23
|
+
assert.strictEqual(n!.v, 2);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('put() with fewer keys replaces node', () => {
|
|
27
|
+
cache.put({ $path: '/a', $type: 'x', old: 1, keep: 2 } as any);
|
|
28
|
+
cache.put({ $path: '/a', $type: 'x', keep: 3 } as any);
|
|
29
|
+
const n = cache.get('/a') as any;
|
|
30
|
+
assert.strictEqual(n.keep, 3);
|
|
31
|
+
assert.strictEqual('old' in n, false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('subscribePath fires on put()', () => {
|
|
35
|
+
let called = 0;
|
|
36
|
+
cache.subscribePath('/a', () => called++);
|
|
37
|
+
cache.put({ $path: '/a', $type: 'x' } as any);
|
|
38
|
+
assert.strictEqual(called, 1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('subscribeChildren fires on child put()', () => {
|
|
42
|
+
let called = 0;
|
|
43
|
+
cache.subscribeChildren('/', () => called++);
|
|
44
|
+
cache.put({ $path: '/child', $type: 'x' } as any);
|
|
45
|
+
assert.ok(called >= 1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('getSnapshot() returns plain cloneable object', () => {
|
|
49
|
+
cache.put({ $path: '/a', $type: 'x', v: 42 } as any);
|
|
50
|
+
const s = cache.getSnapshot('/a');
|
|
51
|
+
assert.ok(s);
|
|
52
|
+
assert.strictEqual(s!.v, 42);
|
|
53
|
+
const cloned = structuredClone(s);
|
|
54
|
+
assert.strictEqual(cloned.v, 42);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('getSnapshot() returns a copy, not the same reference', () => {
|
|
58
|
+
cache.put({ $path: '/a', $type: 'x', v: 42 } as any);
|
|
59
|
+
const s = cache.getSnapshot('/a');
|
|
60
|
+
assert.notStrictEqual(s, cache.get('/a'));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('notifyPath fires path subs', () => {
|
|
64
|
+
cache.put({ $path: '/a', $type: 'x' } as any);
|
|
65
|
+
let called = 0;
|
|
66
|
+
cache.subscribePath('/a', () => called++);
|
|
67
|
+
cache.notifyPath('/a');
|
|
68
|
+
assert.strictEqual(called, 1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('getChildren returns sorted children', () => {
|
|
72
|
+
cache.put({ $path: '/b', $type: 'x' } as any);
|
|
73
|
+
cache.put({ $path: '/a', $type: 'x' } as any);
|
|
74
|
+
const kids = cache.getChildren('/');
|
|
75
|
+
assert.strictEqual(kids.length, 2);
|
|
76
|
+
assert.strictEqual(kids[0].$path, '/a');
|
|
77
|
+
assert.strictEqual(kids[1].$path, '/b');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('remove() deletes node and fires subs', () => {
|
|
81
|
+
cache.put({ $path: '/a', $type: 'x' } as any);
|
|
82
|
+
let called = 0;
|
|
83
|
+
cache.subscribePath('/a', () => called++);
|
|
84
|
+
cache.remove('/a');
|
|
85
|
+
assert.strictEqual(cache.get('/a'), undefined);
|
|
86
|
+
assert.strictEqual(called, 1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('putMany() stores all nodes, fires subs', () => {
|
|
90
|
+
let parentFired = 0;
|
|
91
|
+
cache.subscribeChildren('/p', () => parentFired++);
|
|
92
|
+
cache.putMany([
|
|
93
|
+
{ $path: '/p/a', $type: 'x' } as any,
|
|
94
|
+
{ $path: '/p/b', $type: 'x' } as any,
|
|
95
|
+
], '/p');
|
|
96
|
+
assert.ok(cache.get('/p/a'));
|
|
97
|
+
assert.ok(cache.get('/p/b'));
|
|
98
|
+
assert.ok(parentFired >= 1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('direct mutation of get() result changes cached value', () => {
|
|
102
|
+
cache.put({ $path: '/a', $type: 'x', v: 1 } as any);
|
|
103
|
+
const n = cache.get('/a') as any;
|
|
104
|
+
n.v = 99;
|
|
105
|
+
assert.strictEqual(cache.get('/a')!.v, 99);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('onNodePut callback fires on every put()', () => {
|
|
109
|
+
const paths: string[] = [];
|
|
110
|
+
cache.onNodePut((p) => paths.push(p));
|
|
111
|
+
cache.put({ $path: '/a', $type: 'x' } as any);
|
|
112
|
+
cache.put({ $path: '/b', $type: 'x' } as any);
|
|
113
|
+
assert.deepStrictEqual(paths, ['/a', '/b']);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('nested component update works', () => {
|
|
117
|
+
cache.put({ $path: '/a', $type: 'x', comp: { $type: 'y', count: 0, label: 'hi' } } as any);
|
|
118
|
+
cache.put({ $path: '/a', $type: 'x', comp: { $type: 'y', count: 5, label: 'hi' } } as any);
|
|
119
|
+
assert.strictEqual((cache.get('/a') as any).comp.count, 5);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('adding a new nested component', () => {
|
|
123
|
+
cache.put({ $path: '/a', $type: 'x' } as any);
|
|
124
|
+
cache.put({ $path: '/a', $type: 'x', comp: { $type: 'y', v: 1 } } as any);
|
|
125
|
+
assert.strictEqual((cache.get('/a') as any).comp.v, 1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('removing a nested component', () => {
|
|
129
|
+
cache.put({ $path: '/a', $type: 'x', comp: { $type: 'y', v: 1 } } as any);
|
|
130
|
+
cache.put({ $path: '/a', $type: 'x' } as any);
|
|
131
|
+
assert.strictEqual('comp' in (cache.get('/a') as any), false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('array replaced wholesale', () => {
|
|
135
|
+
cache.put({ $path: '/a', $type: 'x', items: [1, 2, 3] } as any);
|
|
136
|
+
cache.put({ $path: '/a', $type: 'x', items: [4, 5] } as any);
|
|
137
|
+
assert.deepStrictEqual([...(cache.get('/a') as any).items], [4, 5]);
|
|
138
|
+
});
|
|
139
|
+
});
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// Treenity Client Cache — reactive node store
|
|
2
|
+
// useSyncExternalStore-friendly: stable snapshots, targeted notifications
|
|
3
|
+
// IDB persistence: fire-and-forget writes, hydrate() on startup.
|
|
4
|
+
|
|
5
|
+
import type { NodeData } from '@treenity/core/core';
|
|
6
|
+
import * as idb from './idb';
|
|
7
|
+
|
|
8
|
+
type Sub = () => void;
|
|
9
|
+
|
|
10
|
+
const nodes = new Map<string, NodeData>();
|
|
11
|
+
// Explicit parent -> children index. This allows nodes to have their real $path
|
|
12
|
+
// while still appearing as children of virtual folders like query mounts.
|
|
13
|
+
const parentIndex = new Map<string, Set<string>>();
|
|
14
|
+
const pathSubs = new Map<string, Set<Sub>>();
|
|
15
|
+
const childSubs = new Map<string, Set<Sub>>();
|
|
16
|
+
const globalSubs = new Set<Sub>();
|
|
17
|
+
const childSnap = new Map<string, NodeData[]>();
|
|
18
|
+
let version = 0;
|
|
19
|
+
|
|
20
|
+
// lastUpdated: timestamp of last put() per path.
|
|
21
|
+
// Used for reconnect refresh ordering (most recently viewed first).
|
|
22
|
+
const lastUpdated = new Map<string, number>();
|
|
23
|
+
export const getLastUpdated = (path: string) => lastUpdated.get(path) ?? 0;
|
|
24
|
+
|
|
25
|
+
function addSub(map: Map<string, Set<Sub>>, key: string, cb: Sub): () => void {
|
|
26
|
+
if (!map.has(key)) map.set(key, new Set());
|
|
27
|
+
map.get(key)!.add(cb);
|
|
28
|
+
return () => {
|
|
29
|
+
const s = map.get(key);
|
|
30
|
+
if (s) {
|
|
31
|
+
s.delete(cb);
|
|
32
|
+
if (!s.size) map.delete(key);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fire(map: Map<string, Set<Sub>>, key: string) {
|
|
38
|
+
const s = map.get(key);
|
|
39
|
+
if (s) for (const cb of s) cb();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function bump() {
|
|
43
|
+
version++;
|
|
44
|
+
for (const cb of globalSubs) cb();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parentOf(p: string): string | null {
|
|
48
|
+
if (p === '/') return null;
|
|
49
|
+
const i = p.lastIndexOf('/');
|
|
50
|
+
return i <= 0 ? '/' : p.slice(0, i);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Reads ──
|
|
54
|
+
|
|
55
|
+
export const get = (path: string) => nodes.get(path);
|
|
56
|
+
export const has = (path: string) => nodes.has(path);
|
|
57
|
+
export const size = () => nodes.size;
|
|
58
|
+
export const getVersion = () => version;
|
|
59
|
+
|
|
60
|
+
export function getChildren(parent: string): NodeData[] {
|
|
61
|
+
let snap = childSnap.get(parent);
|
|
62
|
+
if (snap) return snap;
|
|
63
|
+
|
|
64
|
+
const out: NodeData[] = [];
|
|
65
|
+
const children = parentIndex.get(parent);
|
|
66
|
+
|
|
67
|
+
if (children) {
|
|
68
|
+
for (const p of children) {
|
|
69
|
+
const n = nodes.get(p);
|
|
70
|
+
if (n) out.push(n);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
// Fallback: If not indexed explicitly, find children by string prefix
|
|
74
|
+
const prefix = parent === '/' ? '/' : parent + '/';
|
|
75
|
+
for (const [p, n] of nodes) {
|
|
76
|
+
if (p === parent || !p.startsWith(prefix)) continue;
|
|
77
|
+
const rest = parent === '/' ? p.slice(1) : p.slice(prefix.length);
|
|
78
|
+
if (rest && !rest.includes('/')) out.push(n);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
out.sort((a, b) => a.$path.localeCompare(b.$path));
|
|
83
|
+
childSnap.set(parent, out);
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Parent Index Management ──
|
|
88
|
+
|
|
89
|
+
export function addToParent(path: string, parent: string) {
|
|
90
|
+
if (!parentIndex.has(parent)) parentIndex.set(parent, new Set());
|
|
91
|
+
if (!parentIndex.get(parent)!.has(path)) {
|
|
92
|
+
parentIndex.get(parent)!.add(path);
|
|
93
|
+
childSnap.delete(parent);
|
|
94
|
+
fire(childSubs, parent);
|
|
95
|
+
bump();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function removeFromParent(path: string, parent: string) {
|
|
100
|
+
const children = parentIndex.get(parent);
|
|
101
|
+
if (children && children.has(path)) {
|
|
102
|
+
children.delete(path);
|
|
103
|
+
childSnap.delete(parent);
|
|
104
|
+
fire(childSubs, parent);
|
|
105
|
+
bump();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Writes ──
|
|
110
|
+
|
|
111
|
+
export function put(node: NodeData, virtualParent?: string) {
|
|
112
|
+
nodes.set(node.$path, node);
|
|
113
|
+
const p = virtualParent ?? parentOf(node.$path);
|
|
114
|
+
if (p !== null) {
|
|
115
|
+
if (!parentIndex.has(p)) parentIndex.set(p, new Set());
|
|
116
|
+
parentIndex.get(p)!.add(node.$path);
|
|
117
|
+
childSnap.delete(p);
|
|
118
|
+
}
|
|
119
|
+
fire(pathSubs, node.$path);
|
|
120
|
+
if (p !== null) fire(childSubs, p);
|
|
121
|
+
bump();
|
|
122
|
+
for (const h of putHooks) h(node.$path);
|
|
123
|
+
|
|
124
|
+
const ts = Date.now();
|
|
125
|
+
lastUpdated.set(node.$path, ts);
|
|
126
|
+
idb.save({ path: node.$path, data: node, lastUpdated: ts, virtualParent }).catch(() => {});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function putMany(items: NodeData[], virtualParent?: string) {
|
|
130
|
+
const dirty = new Set<string>();
|
|
131
|
+
if (virtualParent) {
|
|
132
|
+
if (!parentIndex.has(virtualParent)) parentIndex.set(virtualParent, new Set());
|
|
133
|
+
dirty.add(virtualParent);
|
|
134
|
+
}
|
|
135
|
+
const ts = Date.now();
|
|
136
|
+
const idbEntries: idb.IDBEntry[] = [];
|
|
137
|
+
for (const n of items) {
|
|
138
|
+
nodes.set(n.$path, n);
|
|
139
|
+
lastUpdated.set(n.$path, ts);
|
|
140
|
+
fire(pathSubs, n.$path);
|
|
141
|
+
const p = virtualParent ?? parentOf(n.$path);
|
|
142
|
+
if (p !== null) {
|
|
143
|
+
if (!parentIndex.has(p)) parentIndex.set(p, new Set());
|
|
144
|
+
parentIndex.get(p)!.add(n.$path);
|
|
145
|
+
dirty.add(p);
|
|
146
|
+
childSnap.delete(p);
|
|
147
|
+
}
|
|
148
|
+
idbEntries.push({ path: n.$path, data: n, lastUpdated: ts, virtualParent });
|
|
149
|
+
}
|
|
150
|
+
for (const p of dirty) {
|
|
151
|
+
childSnap.delete(p);
|
|
152
|
+
fire(childSubs, p);
|
|
153
|
+
}
|
|
154
|
+
if (items.length || dirty.size) bump();
|
|
155
|
+
idb.saveMany(idbEntries).catch(() => {});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function remove(path: string, virtualParent?: string) {
|
|
159
|
+
nodes.delete(path);
|
|
160
|
+
lastUpdated.delete(path);
|
|
161
|
+
const p = virtualParent ?? parentOf(path);
|
|
162
|
+
if (p !== null) {
|
|
163
|
+
parentIndex.get(p)?.delete(path);
|
|
164
|
+
childSnap.delete(p);
|
|
165
|
+
}
|
|
166
|
+
fire(pathSubs, path);
|
|
167
|
+
if (p !== null) fire(childSubs, p);
|
|
168
|
+
bump();
|
|
169
|
+
idb.del(path).catch(() => {});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Subscriptions ──
|
|
173
|
+
|
|
174
|
+
export const subscribePath = (path: string, cb: Sub) => addSub(pathSubs, path, cb);
|
|
175
|
+
export const subscribeChildren = (parent: string, cb: Sub) => addSub(childSubs, parent, cb);
|
|
176
|
+
export const subscribeGlobal = (cb: Sub): (() => void) => {
|
|
177
|
+
globalSubs.add(cb);
|
|
178
|
+
return () => globalSubs.delete(cb);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// ── Per-put hook (used by bind engine) ──
|
|
182
|
+
const putHooks = new Set<(path: string) => void>();
|
|
183
|
+
export function onNodePut(cb: (path: string) => void): () => void {
|
|
184
|
+
putHooks.add(cb);
|
|
185
|
+
return () => putHooks.delete(cb);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Extra accessors ──
|
|
189
|
+
export function notifyPath(path: string) { fire(pathSubs, path); }
|
|
190
|
+
export function getSnapshot(path: string): NodeData | undefined {
|
|
191
|
+
const node = nodes.get(path);
|
|
192
|
+
if (!node) return undefined;
|
|
193
|
+
return structuredClone(node);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── SSE Reconnect ──
|
|
197
|
+
// Generation counter — bumped when SSE reconnects with preserved=false.
|
|
198
|
+
// useChildren depends on this to re-fetch and re-register watches.
|
|
199
|
+
let sseGen = 0;
|
|
200
|
+
const genSubs = new Set<Sub>();
|
|
201
|
+
export const getSSEGen = () => sseGen;
|
|
202
|
+
export function subscribeSSEGen(cb: Sub) {
|
|
203
|
+
genSubs.add(cb);
|
|
204
|
+
return () => genSubs.delete(cb);
|
|
205
|
+
}
|
|
206
|
+
export function signalReconnect() {
|
|
207
|
+
sseGen++;
|
|
208
|
+
for (const cb of genSubs) cb();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Bulk ──
|
|
212
|
+
|
|
213
|
+
export function clear() {
|
|
214
|
+
nodes.clear();
|
|
215
|
+
parentIndex.clear();
|
|
216
|
+
childSnap.clear();
|
|
217
|
+
lastUpdated.clear();
|
|
218
|
+
bump();
|
|
219
|
+
idb.clearAll().catch(() => {});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Populate cache from IDB on startup — no IDB writes triggered.
|
|
223
|
+
// Call before first render for instant stale paint.
|
|
224
|
+
export async function hydrate(): Promise<void> {
|
|
225
|
+
try {
|
|
226
|
+
const entries = await idb.loadAll();
|
|
227
|
+
for (const { data, lastUpdated: ts, virtualParent } of entries) {
|
|
228
|
+
nodes.set(data.$path, data);
|
|
229
|
+
lastUpdated.set(data.$path, ts);
|
|
230
|
+
const p = virtualParent ?? parentOf(data.$path);
|
|
231
|
+
if (p !== null) {
|
|
232
|
+
if (!parentIndex.has(p)) parentIndex.set(p, new Set());
|
|
233
|
+
parentIndex.get(p)!.add(data.$path);
|
|
234
|
+
childSnap.delete(p);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (entries.length) bump();
|
|
238
|
+
} catch {
|
|
239
|
+
// IDB unavailable (private browsing, etc.) — continue without persistence
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Expose raw Map for Tree component (read-only contract)
|
|
244
|
+
export const raw = () => nodes;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { NodeData } from '@treenity/core/core';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
import { createClientTree } from './client-tree';
|
|
5
|
+
|
|
6
|
+
// ── Mock tRPC client ──
|
|
7
|
+
|
|
8
|
+
function createMockTrpc(backing: Map<string, NodeData>) {
|
|
9
|
+
let calls = 0;
|
|
10
|
+
|
|
11
|
+
const mock = {
|
|
12
|
+
get calls() { return calls; },
|
|
13
|
+
resetCalls() { calls = 0; },
|
|
14
|
+
|
|
15
|
+
get: {
|
|
16
|
+
query: async ({ path }: { path: string }) => {
|
|
17
|
+
calls++;
|
|
18
|
+
return backing.get(path);
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
getChildren: {
|
|
22
|
+
query: async ({ path }: { path: string; limit?: number; offset?: number }) => {
|
|
23
|
+
calls++;
|
|
24
|
+
const prefix = path === '/' ? '/' : path + '/';
|
|
25
|
+
const items = [...backing.values()].filter(
|
|
26
|
+
n => n.$path.startsWith(prefix) && n.$path !== path
|
|
27
|
+
&& n.$path.slice(prefix.length).indexOf('/') === -1,
|
|
28
|
+
);
|
|
29
|
+
return { items, total: items.length };
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
set: {
|
|
33
|
+
mutate: async ({ node }: { node: Record<string, unknown> }) => {
|
|
34
|
+
calls++;
|
|
35
|
+
backing.set(node.$path as string, node as NodeData);
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
remove: {
|
|
39
|
+
mutate: async ({ path }: { path: string }) => {
|
|
40
|
+
calls++;
|
|
41
|
+
backing.delete(path);
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return mock;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Tests ──
|
|
50
|
+
|
|
51
|
+
describe('createClientTree — unified client tree', () => {
|
|
52
|
+
it('/local/* paths stay in memory, never hit tRPC', async () => {
|
|
53
|
+
const mock = createMockTrpc(new Map());
|
|
54
|
+
const { tree: store } = createClientTree(mock as any);
|
|
55
|
+
|
|
56
|
+
await store.set({ $path: '/local/ui/theme', $type: 'theme', dark: true } as NodeData);
|
|
57
|
+
mock.resetCalls();
|
|
58
|
+
|
|
59
|
+
const node = await store.get('/local/ui/theme');
|
|
60
|
+
assert.equal(mock.calls, 0, 'tRPC should not be called for /local paths');
|
|
61
|
+
assert.equal(node?.$type, 'theme');
|
|
62
|
+
assert.equal((node as any).dark, true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('non-local paths route through tRPC', async () => {
|
|
66
|
+
const backing = new Map<string, NodeData>();
|
|
67
|
+
backing.set('/orders/1', { $path: '/orders/1', $type: 'order', total: 42 } as NodeData);
|
|
68
|
+
const mock = createMockTrpc(backing);
|
|
69
|
+
const { tree: store } = createClientTree(mock as any);
|
|
70
|
+
|
|
71
|
+
const node = await store.get('/orders/1');
|
|
72
|
+
assert.ok(mock.calls > 0, 'tRPC should be called for remote paths');
|
|
73
|
+
assert.equal((node as any).total, 42);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('getChildren merges local and remote children', async () => {
|
|
77
|
+
const backing = new Map<string, NodeData>();
|
|
78
|
+
backing.set('/cloud', { $path: '/cloud', $type: 'dir' } as NodeData);
|
|
79
|
+
const mock = createMockTrpc(backing);
|
|
80
|
+
const { tree: store } = createClientTree(mock as any);
|
|
81
|
+
|
|
82
|
+
// Write a local node
|
|
83
|
+
await store.set({ $path: '/local', $type: 'dir' } as NodeData);
|
|
84
|
+
|
|
85
|
+
// getChildren('/') should return both
|
|
86
|
+
const { items } = await store.getChildren('/');
|
|
87
|
+
const paths = items.map((n: { $path: string }) => n.$path).sort();
|
|
88
|
+
assert.ok(paths.includes('/local'), 'should include local children');
|
|
89
|
+
assert.ok(paths.includes('/cloud'), 'should include remote children');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('remove /local/* does not call tRPC', async () => {
|
|
93
|
+
const mock = createMockTrpc(new Map());
|
|
94
|
+
const { tree: store } = createClientTree(mock as any);
|
|
95
|
+
|
|
96
|
+
await store.set({ $path: '/local/temp', $type: 'tmp' } as NodeData);
|
|
97
|
+
mock.resetCalls();
|
|
98
|
+
|
|
99
|
+
await store.remove('/local/temp');
|
|
100
|
+
// filterStore tries both, but remote remove is harmless no-op
|
|
101
|
+
const node = await store.get('/local/temp');
|
|
102
|
+
assert.equal(node, undefined, '/local/temp should be gone');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('cached remote: second get skips tRPC', async () => {
|
|
106
|
+
const backing = new Map<string, NodeData>();
|
|
107
|
+
backing.set('/x', { $path: '/x', $type: 'test' } as NodeData);
|
|
108
|
+
const mock = createMockTrpc(backing);
|
|
109
|
+
const { tree: store } = createClientTree(mock as any);
|
|
110
|
+
|
|
111
|
+
await store.get('/x'); // populates cache
|
|
112
|
+
mock.resetCalls();
|
|
113
|
+
await store.get('/x'); // should hit cache
|
|
114
|
+
assert.equal(mock.calls, 0, 'second get should come from cache');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Unified Client Tree — the browser as a peer node.
|
|
2
|
+
// /local/* paths live in browser memory only (never hit the network).
|
|
3
|
+
// Everything else routes through tRPC to the server.
|
|
4
|
+
// Same Tree interface everywhere — components don't know where data lives.
|
|
5
|
+
|
|
6
|
+
import { createFilterTree, createMemoryTree } from '@treenity/core/tree';
|
|
7
|
+
import { withCache } from '@treenity/core/tree/cache';
|
|
8
|
+
import { createRemoteTree } from './remote-tree';
|
|
9
|
+
import type { trpc } from './trpc';
|
|
10
|
+
|
|
11
|
+
type TrpcClient = typeof trpc;
|
|
12
|
+
|
|
13
|
+
export function createClientTree(client: TrpcClient) {
|
|
14
|
+
const local = createMemoryTree();
|
|
15
|
+
const remote = withCache(createRemoteTree(client));
|
|
16
|
+
|
|
17
|
+
const tree = createFilterTree(
|
|
18
|
+
local,
|
|
19
|
+
remote,
|
|
20
|
+
(node) => node.$path.startsWith('/local'),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return { tree, local, remote };
|
|
24
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
// The browser peer node — singleton client tree.
|
|
3
|
+
// /local/* in memory, everything else via tRPC.
|
|
4
|
+
|
|
5
|
+
import { createClientTree } from './client-tree';
|
|
6
|
+
import { trpc } from './trpc';
|
|
7
|
+
|
|
8
|
+
export const { tree, local, remote } = createClientTree(trpc);
|
|
9
|
+
|
|
10
|
+
// Dev: expose tree for console debugging (e.g. __tree.get('/local/ui/theme'))
|
|
11
|
+
if (import.meta.env?.DEV) (globalThis as any).__tree = tree;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { cn } from '#lib/utils';
|
|
2
|
+
import { ChevronDownIcon } from 'lucide-react';
|
|
3
|
+
import { Accordion as AccordionPrimitive } from 'radix-ui';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
|
|
6
|
+
function Accordion({
|
|
7
|
+
...props
|
|
8
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
|
9
|
+
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function AccordionItem({
|
|
13
|
+
className,
|
|
14
|
+
...props
|
|
15
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
|
16
|
+
return (
|
|
17
|
+
<AccordionPrimitive.Item
|
|
18
|
+
data-slot="accordion-item"
|
|
19
|
+
className={cn("border-b last:border-b-0", className)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function AccordionTrigger({
|
|
26
|
+
className,
|
|
27
|
+
children,
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
|
30
|
+
return (
|
|
31
|
+
<AccordionPrimitive.Header className="flex">
|
|
32
|
+
<AccordionPrimitive.Trigger
|
|
33
|
+
data-slot="accordion-trigger"
|
|
34
|
+
className={cn(
|
|
35
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
|
42
|
+
</AccordionPrimitive.Trigger>
|
|
43
|
+
</AccordionPrimitive.Header>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function AccordionContent({
|
|
48
|
+
className,
|
|
49
|
+
children,
|
|
50
|
+
...props
|
|
51
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
|
52
|
+
return (
|
|
53
|
+
<AccordionPrimitive.Content
|
|
54
|
+
data-slot="accordion-content"
|
|
55
|
+
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
|
59
|
+
</AccordionPrimitive.Content>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cn } from '#lib/utils';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { type HTMLAttributes } from 'react';
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'border-transparent bg-primary text-primary-foreground shadow',
|
|
11
|
+
secondary: 'border-transparent bg-secondary text-secondary-foreground',
|
|
12
|
+
destructive: 'border-transparent bg-destructive text-destructive-foreground shadow',
|
|
13
|
+
outline: 'text-foreground',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: { variant: 'default' },
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export interface BadgeProps
|
|
21
|
+
extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
|
22
|
+
|
|
23
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
24
|
+
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cn } from '#lib/utils';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { type ButtonHTMLAttributes, forwardRef } from 'react';
|
|
4
|
+
|
|
5
|
+
const buttonVariants = cva(
|
|
6
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
|
11
|
+
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
|
12
|
+
outline:
|
|
13
|
+
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
|
14
|
+
outlined:
|
|
15
|
+
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
|
16
|
+
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
|
17
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
18
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
19
|
+
primary: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
|
20
|
+
transparent: 'bg-transparent hover:bg-accent hover:text-accent-foreground',
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default: 'h-9 px-4 py-2',
|
|
24
|
+
sm: 'h-8 rounded-md px-3 text-xs',
|
|
25
|
+
lg: 'h-10 rounded-md px-8',
|
|
26
|
+
icon: 'h-9 w-9',
|
|
27
|
+
'icon-sm': 'h-8 w-8',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: { variant: 'default', size: 'default' },
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export interface ButtonProps
|
|
35
|
+
extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}
|
|
36
|
+
|
|
37
|
+
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
38
|
+
({ className, variant, size, ...props }, ref) => (
|
|
39
|
+
<button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
Button.displayName = 'Button';
|
|
43
|
+
|
|
44
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { cn } from '#lib/utils';
|
|
2
|
+
import { forwardRef, type InputHTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
const Checkbox = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(
|
|
5
|
+
({ className, ...props }, ref) => (
|
|
6
|
+
<input
|
|
7
|
+
type="checkbox"
|
|
8
|
+
className={cn(
|
|
9
|
+
'h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 accent-primary',
|
|
10
|
+
className,
|
|
11
|
+
)}
|
|
12
|
+
ref={ref}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
Checkbox.displayName = 'Checkbox';
|
|
18
|
+
|
|
19
|
+
export { Checkbox };
|