@treenity/react 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/dist/AclEditor.d.ts +1 -1
- package/dist/AclEditor.d.ts.map +1 -1
- package/dist/AclEditor.js +5 -5
- package/dist/AclEditor.js.map +1 -1
- package/dist/ActionCards.d.ts +9 -0
- package/dist/ActionCards.d.ts.map +1 -0
- package/dist/ActionCards.js +96 -0
- package/dist/ActionCards.js.map +1 -0
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +71 -185
- package/dist/App.js.map +1 -1
- package/dist/ComponentSection.d.ts +15 -0
- package/dist/ComponentSection.d.ts.map +1 -0
- package/dist/ComponentSection.js +25 -0
- package/dist/ComponentSection.js.map +1 -0
- package/dist/ErrorBoundary.d.ts +18 -0
- package/dist/ErrorBoundary.d.ts.map +1 -0
- package/dist/ErrorBoundary.js +17 -0
- package/dist/ErrorBoundary.js.map +1 -0
- package/dist/Inspector.d.ts +1 -0
- package/dist/Inspector.d.ts.map +1 -1
- package/dist/Inspector.js +22 -347
- package/dist/Inspector.js.map +1 -1
- package/dist/Login.d.ts +8 -0
- package/dist/Login.d.ts.map +1 -0
- package/dist/Login.js +45 -0
- package/dist/Login.js.map +1 -0
- package/dist/NodeEditor.d.ts +11 -0
- package/dist/NodeEditor.d.ts.map +1 -0
- package/dist/NodeEditor.js +157 -0
- package/dist/NodeEditor.js.map +1 -0
- package/dist/Tree.d.ts +1 -0
- package/dist/Tree.d.ts.map +1 -1
- package/dist/Tree.js +8 -27
- package/dist/Tree.js.map +1 -1
- package/dist/bind/engine.js +1 -1
- package/dist/bind/engine.js.map +1 -1
- package/dist/bind/eval.d.ts +1 -1
- package/dist/bind/eval.d.ts.map +1 -1
- package/dist/bind/hook.d.ts +1 -1
- package/dist/bind/hook.d.ts.map +1 -1
- package/dist/bind/hook.js +1 -1
- package/dist/bind/hook.js.map +1 -1
- package/dist/cache.d.ts +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +4 -0
- package/dist/cache.js.map +1 -1
- package/dist/client-tree.d.ts +1 -2
- package/dist/client-tree.d.ts.map +1 -1
- package/dist/client-tree.js +12 -5
- package/dist/client-tree.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -4
- package/dist/client.js.map +1 -1
- package/dist/components/ConfirmDialog.d.ts +9 -0
- package/dist/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/ConfirmDialog.js +6 -0
- package/dist/components/ConfirmDialog.js.map +1 -0
- package/dist/components/ConfirmPopover.d.ts +8 -0
- package/dist/components/ConfirmPopover.d.ts.map +1 -0
- package/dist/components/ConfirmPopover.js +9 -0
- package/dist/components/ConfirmPopover.js.map +1 -0
- package/dist/components/PathBreadcrumb.d.ts +5 -0
- package/dist/components/PathBreadcrumb.d.ts.map +1 -0
- package/dist/components/PathBreadcrumb.js +16 -0
- package/dist/components/PathBreadcrumb.js.map +1 -0
- package/dist/components/lib/utils.d.ts +3 -0
- package/dist/components/lib/utils.d.ts.map +1 -0
- package/dist/components/lib/utils.js +6 -0
- package/dist/components/lib/utils.js.map +1 -0
- package/dist/components/ui/accordion.js +1 -1
- package/dist/components/ui/accordion.js.map +1 -1
- package/dist/components/ui/alert-dialog.d.ts +19 -0
- package/dist/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/components/ui/alert-dialog.js +42 -0
- package/dist/components/ui/alert-dialog.js.map +1 -0
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/badge.js.map +1 -1
- package/dist/components/ui/breadcrumb.d.ts +12 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +28 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +9 -8
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.js +26 -21
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/card.d.ts +10 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +25 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/checkbox.js.map +1 -1
- package/dist/components/ui/collapsible.d.ts +6 -0
- package/dist/components/ui/collapsible.d.ts.map +1 -0
- package/dist/components/ui/collapsible.js +13 -0
- package/dist/components/ui/collapsible.js.map +1 -0
- package/dist/components/ui/command.d.ts +19 -0
- package/dist/components/ui/command.d.ts.map +1 -0
- package/dist/components/ui/command.js +35 -0
- package/dist/components/ui/command.js.map +1 -0
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog.js +1 -1
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/components/ui/drawer.js +1 -1
- package/dist/components/ui/drawer.js.map +1 -1
- package/dist/components/ui/dropdown-menu.d.ts +26 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +52 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/form-field.d.ts +7 -0
- package/dist/components/ui/form-field.d.ts.map +1 -0
- package/dist/components/ui/form-field.js +17 -0
- package/dist/components/ui/form-field.js.map +1 -0
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/input.js.map +1 -1
- package/dist/components/ui/label.js +1 -1
- package/dist/components/ui/label.js.map +1 -1
- package/dist/components/ui/pagination.d.ts +14 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +30 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.js +2 -2
- package/dist/components/ui/popover.js.map +1 -1
- package/dist/components/ui/progress.js +1 -1
- package/dist/components/ui/progress.js.map +1 -1
- package/dist/components/ui/resizable.d.ts +8 -0
- package/dist/components/ui/resizable.d.ts.map +1 -0
- package/dist/components/ui/resizable.js +14 -0
- package/dist/components/ui/resizable.js.map +1 -0
- package/dist/components/ui/scroll-area.d.ts +6 -0
- package/dist/components/ui/scroll-area.d.ts.map +1 -0
- package/dist/components/ui/scroll-area.js +13 -0
- package/dist/components/ui/scroll-area.js.map +1 -0
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/select.js.map +1 -1
- package/dist/components/ui/separator.d.ts +5 -0
- package/dist/components/ui/separator.d.ts.map +1 -0
- package/dist/components/ui/separator.js +9 -0
- package/dist/components/ui/separator.js.map +1 -0
- package/dist/components/ui/sheet.d.ts +15 -0
- package/dist/components/ui/sheet.d.ts.map +1 -0
- package/dist/components/ui/sheet.js +40 -0
- package/dist/components/ui/sheet.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +3 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +7 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/slider.js +1 -1
- package/dist/components/ui/slider.js.map +1 -1
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/switch.js.map +1 -1
- package/dist/components/ui/table.d.ts +11 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +29 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +12 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +29 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/components/ui/toggle-group.d.ts +10 -0
- package/dist/components/ui/toggle-group.d.ts.map +1 -0
- package/dist/components/ui/toggle-group.js +23 -0
- package/dist/components/ui/toggle-group.js.map +1 -0
- package/dist/components/ui/toggle.d.ts +10 -0
- package/dist/components/ui/toggle.d.ts.map +1 -0
- package/dist/components/ui/toggle.js +27 -0
- package/dist/components/ui/toggle.js.map +1 -0
- package/dist/components/ui/tooltip.js +1 -1
- package/dist/components/ui/tooltip.js.map +1 -1
- package/dist/context/index.d.ts +27 -10
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +43 -36
- package/dist/context/index.js.map +1 -1
- package/dist/events.d.ts +10 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +78 -0
- package/dist/events.js.map +1 -0
- package/dist/fiber-tree.d.ts +3 -0
- package/dist/fiber-tree.d.ts.map +1 -0
- package/dist/fiber-tree.js +93 -0
- package/dist/fiber-tree.js.map +1 -0
- package/dist/hooks.d.ts +5 -2
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +66 -6
- package/dist/hooks.js.map +1 -1
- package/dist/idb.d.ts +1 -1
- package/dist/idb.d.ts.map +1 -1
- package/dist/lib/to-plain.d.ts +2 -0
- package/dist/lib/to-plain.d.ts.map +1 -0
- package/dist/lib/to-plain.js +21 -0
- package/dist/lib/to-plain.js.map +1 -0
- package/dist/main.d.ts +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -3
- package/dist/main.js.map +1 -1
- package/dist/mods/clients.d.ts +3 -0
- package/dist/mods/clients.d.ts.map +1 -0
- package/dist/mods/clients.js +4 -0
- package/dist/mods/clients.js.map +1 -0
- package/dist/mods/editor-ui/FieldLabel.d.ts +15 -0
- package/dist/mods/editor-ui/FieldLabel.d.ts.map +1 -0
- package/dist/mods/editor-ui/FieldLabel.js +55 -0
- package/dist/mods/editor-ui/FieldLabel.js.map +1 -0
- package/dist/mods/editor-ui/client.d.ts +1 -1
- package/dist/mods/editor-ui/client.d.ts.map +1 -1
- package/dist/mods/editor-ui/client.js +1 -1
- package/dist/mods/editor-ui/client.js.map +1 -1
- package/dist/mods/editor-ui/default-edit.d.ts +2 -0
- package/dist/mods/editor-ui/default-edit.d.ts.map +1 -0
- package/dist/mods/editor-ui/default-edit.js +54 -0
- package/dist/mods/editor-ui/default-edit.js.map +1 -0
- package/dist/mods/editor-ui/default-view.d.ts +8 -1
- package/dist/mods/editor-ui/default-view.d.ts.map +1 -1
- package/dist/mods/editor-ui/default-view.js +8 -5
- package/dist/mods/editor-ui/default-view.js.map +1 -1
- package/dist/mods/editor-ui/dir-view.js +0 -2
- package/dist/mods/editor-ui/dir-view.js.map +1 -1
- package/dist/mods/editor-ui/empty-placeholder.d.ts +5 -0
- package/dist/mods/editor-ui/empty-placeholder.d.ts.map +1 -0
- package/dist/mods/editor-ui/empty-placeholder.js +14 -0
- package/dist/mods/editor-ui/empty-placeholder.js.map +1 -0
- package/dist/mods/editor-ui/form-field.d.ts +17 -0
- package/dist/mods/editor-ui/form-field.d.ts.map +1 -0
- package/dist/mods/editor-ui/form-field.js +68 -0
- package/dist/mods/editor-ui/form-field.js.map +1 -0
- package/dist/mods/editor-ui/form-fields.d.ts +1 -2
- package/dist/mods/editor-ui/form-fields.d.ts.map +1 -1
- package/dist/mods/editor-ui/form-fields.js +56 -60
- package/dist/mods/editor-ui/form-fields.js.map +1 -1
- package/dist/mods/editor-ui/layout-view.js +3 -2
- package/dist/mods/editor-ui/layout-view.js.map +1 -1
- package/dist/mods/editor-ui/list-items.js +1 -1
- package/dist/mods/editor-ui/list-items.js.map +1 -1
- package/dist/mods/editor-ui/node-utils.d.ts +2 -2
- package/dist/mods/editor-ui/node-utils.d.ts.map +1 -1
- package/dist/mods/editor-ui/node-utils.js +4 -5
- package/dist/mods/editor-ui/node-utils.js.map +1 -1
- package/dist/mods/editor-ui/type-picker.d.ts +15 -0
- package/dist/mods/editor-ui/type-picker.d.ts.map +1 -0
- package/dist/mods/editor-ui/type-picker.js +69 -0
- package/dist/mods/editor-ui/type-picker.js.map +1 -0
- package/dist/mods/editor-ui/user-view.js +1 -1
- package/dist/mods/editor-ui/user-view.js.map +1 -1
- package/dist/mods/servers.d.ts +1 -0
- package/dist/mods/servers.d.ts.map +1 -0
- package/dist/mods/servers.js +4 -0
- package/dist/mods/servers.js.map +1 -0
- package/dist/mods/treenity/groups/index.js +1 -1
- package/dist/mods/treenity/groups/index.js.map +1 -1
- package/dist/mods/treenity/preview.js +1 -1
- package/dist/mods/treenity/preview.js.map +1 -1
- package/dist/mods/treenity/ref-view.js +1 -1
- package/dist/mods/treenity/ref-view.js.map +1 -1
- package/dist/mods/treenity/schema-form.js +1 -1
- package/dist/mods/treenity/schema-form.js.map +1 -1
- package/dist/mods/treenity/seed.js +1 -1
- package/dist/mods/treenity/seed.js.map +1 -1
- package/dist/mods/treenity/type-view.js +1 -1
- package/dist/mods/treenity/type-view.js.map +1 -1
- package/dist/schema-loader.d.ts +1 -1
- package/dist/schema-loader.d.ts.map +1 -1
- package/dist/schema-loader.js +1 -1
- package/dist/schema-loader.js.map +1 -1
- package/dist/symbols.d.ts +5 -0
- package/dist/symbols.d.ts.map +1 -0
- package/dist/symbols.js +16 -0
- package/dist/symbols.js.map +1 -0
- package/dist/trpc.d.ts +10 -3
- package/dist/trpc.d.ts.map +1 -1
- package/package.json +74 -8
- package/src/AclEditor.tsx +11 -18
- package/src/ActionCards.tsx +224 -0
- package/src/App.tsx +204 -385
- package/src/ComponentSection.tsx +113 -0
- package/src/ErrorBoundary.tsx +37 -0
- package/src/Inspector.css +54 -0
- package/src/Inspector.tsx +73 -793
- package/src/Login.tsx +97 -0
- package/src/NodeEditor.tsx +300 -0
- package/src/Tree.css +91 -0
- package/src/Tree.tsx +40 -43
- package/src/bind/bind.test.ts +1 -1
- package/src/bind/engine.ts +1 -1
- package/src/bind/eval.ts +1 -1
- package/src/bind/hook.ts +1 -1
- package/src/bind/pipes.ts +1 -1
- package/src/cache.ts +5 -1
- package/src/client-tree.test.ts +1 -1
- package/src/client-tree.ts +22 -16
- package/src/client.ts +2 -4
- package/src/components/ConfirmDialog.tsx +34 -0
- package/src/components/ConfirmPopover.tsx +41 -0
- package/src/components/PathBreadcrumb.tsx +36 -0
- package/src/components/lib/utils.ts +6 -0
- package/src/components/lib/utils.ts.bak +6 -0
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/alert-dialog.tsx +189 -0
- package/src/components/ui/badge.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +108 -0
- package/src/components/ui/button.tsx +53 -31
- package/src/components/ui/card.tsx +91 -0
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/command.tsx +177 -0
- package/src/components/ui/dialog.tsx +1 -2
- package/src/components/ui/drawer.tsx +1 -1
- package/src/components/ui/dropdown-menu.tsx +256 -0
- package/src/components/ui/form-field.tsx +37 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/popover.tsx +2 -2
- package/src/components/ui/progress.tsx +1 -1
- package/src/components/ui/resizable.tsx +47 -0
- package/src/components/ui/scroll-area.tsx +55 -0
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/separator.tsx +27 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/ui/switch.tsx +1 -1
- package/src/components/ui/table.tsx +115 -0
- package/src/components/ui/tabs.tsx +88 -0
- package/src/components/ui/textarea.tsx +1 -1
- package/src/components/ui/toggle-group.tsx +82 -0
- package/src/components/ui/toggle.tsx +46 -0
- package/src/components/ui/tooltip.tsx +1 -1
- package/src/context/index.tsx +75 -42
- package/src/events.ts +81 -0
- package/src/fiber-tree.ts +112 -0
- package/src/hooks.ts +88 -9
- package/src/idb.ts +1 -1
- package/src/lib/to-plain.ts +21 -0
- package/src/main.tsx +3 -1
- package/src/mods/clients.ts +3 -0
- package/src/mods/editor-ui/FieldLabel.tsx +124 -0
- package/src/mods/editor-ui/client.ts +1 -1
- package/src/mods/editor-ui/default-edit.tsx +99 -0
- package/src/mods/editor-ui/default-view.tsx +13 -8
- package/src/mods/editor-ui/dir-view.tsx +2 -2
- package/src/mods/editor-ui/editor-ui.css +174 -0
- package/src/mods/editor-ui/empty-placeholder.tsx +39 -0
- package/src/mods/editor-ui/form-field.tsx +144 -0
- package/src/mods/editor-ui/form-fields.tsx +132 -113
- package/src/mods/editor-ui/layout-view.tsx +4 -2
- package/src/mods/editor-ui/list-items.tsx +2 -2
- package/src/mods/editor-ui/node-utils.ts +4 -5
- package/src/mods/editor-ui/type-picker.tsx +147 -0
- package/src/mods/editor-ui/user-view.tsx +1 -1
- package/src/mods/servers.ts +2 -0
- package/src/mods/treenity/groups/index.tsx +1 -1
- package/src/mods/treenity/preview.tsx +1 -1
- package/src/mods/treenity/ref-view.tsx +1 -1
- package/src/mods/treenity/schema-form.tsx +1 -1
- package/src/mods/treenity/seed.ts +1 -1
- package/src/mods/treenity/type-view.tsx +1 -1
- package/src/optimistic.test.ts +111 -0
- package/src/remote-tree.test.ts +1 -1
- package/src/remote-tree.ts +1 -1
- package/src/root.css +117 -0
- package/src/schema-loader.ts +1 -1
- package/src/symbols.ts +18 -0
- package/src/index.html +0 -14
- package/src/style.css +0 -1269
- package/src/vite-env.d.ts +0 -3
package/src/App.tsx
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
AlertDialog,
|
|
3
|
+
AlertDialogAction,
|
|
4
|
+
AlertDialogCancel,
|
|
5
|
+
AlertDialogContent,
|
|
6
|
+
AlertDialogFooter,
|
|
7
|
+
AlertDialogHeader,
|
|
8
|
+
AlertDialogTitle,
|
|
9
|
+
} from '#components/ui/alert-dialog';
|
|
10
|
+
import { Button } from '#components/ui/button';
|
|
11
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '#components/ui/dropdown-menu';
|
|
12
|
+
import { Input } from '#components/ui/input';
|
|
13
|
+
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '#components/ui/resizable';
|
|
14
|
+
import { TypePicker } from '#mods/editor-ui/type-picker';
|
|
15
|
+
import type { NodeData } from '@treenity/core';
|
|
3
16
|
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
17
|
+
import { toast } from 'sonner';
|
|
4
18
|
import * as cache from './cache';
|
|
5
19
|
import { tree } from './client';
|
|
20
|
+
import { startEvents, stopEvents } from './events';
|
|
6
21
|
import { NavigateProvider } from './hooks';
|
|
7
22
|
import { Inspector } from './Inspector';
|
|
23
|
+
import { LoginModal, LoginScreen } from './Login';
|
|
8
24
|
import { Tree } from './Tree';
|
|
9
25
|
import { AUTH_EXPIRED_EVENT, clearToken, getToken, setToken, trpc } from './trpc';
|
|
10
26
|
import { ViewPage } from './ViewPage';
|
|
@@ -12,201 +28,6 @@ import { ViewPage } from './ViewPage';
|
|
|
12
28
|
// Hydrate from IDB before first render — fires bump() when done → reactive re-render
|
|
13
29
|
cache.hydrate();
|
|
14
30
|
|
|
15
|
-
type TypeInfo = { type: string; label: string };
|
|
16
|
-
|
|
17
|
-
async function loadTypes(): Promise<TypeInfo[]> {
|
|
18
|
-
const { items } = (await trpc.getChildren.query({ path: '/sys/types', limit: 0, depth: 99 })) as {
|
|
19
|
-
items: NodeData[];
|
|
20
|
-
total: number;
|
|
21
|
-
};
|
|
22
|
-
return items
|
|
23
|
-
.filter((n) => isOfType(n, 'type'))
|
|
24
|
-
.map((n) => {
|
|
25
|
-
const schema = n.schema as { $type: string; title?: string } | undefined;
|
|
26
|
-
const typeName = n.$path.slice('/sys/types/'.length).replace(/\//g, '.');
|
|
27
|
-
return { type: typeName, label: schema?.title ?? typeName };
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function TypePicker({
|
|
32
|
-
onSelect,
|
|
33
|
-
onCancel,
|
|
34
|
-
title = 'Create Node',
|
|
35
|
-
nameLabel = 'Node name',
|
|
36
|
-
action = 'Create',
|
|
37
|
-
}: {
|
|
38
|
-
onSelect: (name: string, type: string) => void;
|
|
39
|
-
onCancel: () => void;
|
|
40
|
-
title?: string;
|
|
41
|
-
nameLabel?: string;
|
|
42
|
-
action?: string;
|
|
43
|
-
}) {
|
|
44
|
-
const [types, setTypes] = useState<TypeInfo[]>([]);
|
|
45
|
-
const [loading, setLoading] = useState(true);
|
|
46
|
-
const [error, setError] = useState<string | null>(null);
|
|
47
|
-
const [filter, setFilter] = useState('');
|
|
48
|
-
const [name, setName] = useState('');
|
|
49
|
-
const [selectedType, setSelectedType] = useState<string | null>(null);
|
|
50
|
-
const nameRef = useRef<HTMLInputElement>(null);
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
loadTypes()
|
|
54
|
-
.then(setTypes)
|
|
55
|
-
.catch((err) => {
|
|
56
|
-
console.error('Failed to load types:', err);
|
|
57
|
-
setError('Failed to load types');
|
|
58
|
-
})
|
|
59
|
-
.finally(() => setLoading(false));
|
|
60
|
-
}, []);
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
nameRef.current?.focus();
|
|
63
|
-
}, []);
|
|
64
|
-
|
|
65
|
-
const lf = filter.toLowerCase();
|
|
66
|
-
const filtered = types.filter(
|
|
67
|
-
(t) => t.type.toLowerCase().includes(lf) || t.label.toLowerCase().includes(lf),
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<div className="type-picker-overlay" onClick={onCancel}>
|
|
72
|
-
<div className="type-picker" onClick={(e) => e.stopPropagation()}>
|
|
73
|
-
<div className="type-picker-header">{title}</div>
|
|
74
|
-
<div className="type-picker-search">
|
|
75
|
-
<input
|
|
76
|
-
ref={nameRef}
|
|
77
|
-
placeholder={nameLabel}
|
|
78
|
-
value={name}
|
|
79
|
-
onChange={(e) => setName(e.target.value)}
|
|
80
|
-
/>
|
|
81
|
-
<input
|
|
82
|
-
placeholder="Filter types..."
|
|
83
|
-
value={filter}
|
|
84
|
-
onChange={(e) => setFilter(e.target.value)}
|
|
85
|
-
/>
|
|
86
|
-
</div>
|
|
87
|
-
<div className="type-picker-list">
|
|
88
|
-
{filtered.map((t) => (
|
|
89
|
-
<div
|
|
90
|
-
key={t.type}
|
|
91
|
-
className={`type-picker-item${selectedType === t.type ? ' active' : ''}`}
|
|
92
|
-
onClick={() => setSelectedType(t.type)}
|
|
93
|
-
>
|
|
94
|
-
<span className="type-name">{t.type}</span>
|
|
95
|
-
{t.label !== t.type && <span className="type-label">{t.label}</span>}
|
|
96
|
-
</div>
|
|
97
|
-
))}
|
|
98
|
-
{loading && (
|
|
99
|
-
<div className="p-3 text-[--text-3] text-[13px]">Loading types...</div>
|
|
100
|
-
)}
|
|
101
|
-
{error && (
|
|
102
|
-
<div className="p-3 text-[--danger] text-[13px]">{error}</div>
|
|
103
|
-
)}
|
|
104
|
-
{!loading && !error && filtered.length === 0 && (
|
|
105
|
-
<div className="p-3 text-[--text-3] text-[13px]">No types found</div>
|
|
106
|
-
)}
|
|
107
|
-
</div>
|
|
108
|
-
<div className="type-picker-footer">
|
|
109
|
-
<button onClick={onCancel}>Cancel</button>
|
|
110
|
-
<button
|
|
111
|
-
className="primary"
|
|
112
|
-
disabled={!name || !selectedType}
|
|
113
|
-
onClick={() => onSelect(name, selectedType!)}
|
|
114
|
-
>
|
|
115
|
-
{action}
|
|
116
|
-
{name ? ` "${name}"` : ''}
|
|
117
|
-
{selectedType ? ` as ${selectedType}` : ''}
|
|
118
|
-
</button>
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function LoginForm({ onLogin }: { onLogin: (userId: string) => void }) {
|
|
126
|
-
const [mode, setMode] = useState<'login' | 'register'>('login');
|
|
127
|
-
const [userId, setUserId] = useState('');
|
|
128
|
-
const [password, setPassword] = useState('');
|
|
129
|
-
const [err, setErr] = useState<string | null>(null);
|
|
130
|
-
const [loading, setLoading] = useState(false);
|
|
131
|
-
|
|
132
|
-
async function handleSubmit(e: React.FormEvent) {
|
|
133
|
-
e.preventDefault();
|
|
134
|
-
if (!userId.trim() || !password) return;
|
|
135
|
-
setLoading(true);
|
|
136
|
-
setErr(null);
|
|
137
|
-
try {
|
|
138
|
-
const fn = mode === 'register' ? trpc.register : trpc.login;
|
|
139
|
-
const res = await fn.mutate({ userId: userId.trim(), password });
|
|
140
|
-
setToken(res.token);
|
|
141
|
-
onLogin(res.userId);
|
|
142
|
-
} catch (e) {
|
|
143
|
-
setErr(e instanceof Error ? e.message : 'Failed');
|
|
144
|
-
} finally {
|
|
145
|
-
setLoading(false);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return (
|
|
150
|
-
<form className="login-box" onSubmit={handleSubmit}>
|
|
151
|
-
<div className="login-logo">
|
|
152
|
-
<img src="/treenity.svg" alt="" width="32" height="32" />
|
|
153
|
-
Treenity
|
|
154
|
-
</div>
|
|
155
|
-
<div className="field">
|
|
156
|
-
<label>User ID</label>
|
|
157
|
-
<input
|
|
158
|
-
autoFocus
|
|
159
|
-
placeholder="Enter your user ID"
|
|
160
|
-
value={userId}
|
|
161
|
-
onChange={(e) => setUserId(e.target.value)}
|
|
162
|
-
/>
|
|
163
|
-
</div>
|
|
164
|
-
<div className="field">
|
|
165
|
-
<label>Password</label>
|
|
166
|
-
<input
|
|
167
|
-
type="password"
|
|
168
|
-
placeholder="Enter password"
|
|
169
|
-
value={password}
|
|
170
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
171
|
-
/>
|
|
172
|
-
</div>
|
|
173
|
-
{err && <div className="login-error">{err}</div>}
|
|
174
|
-
<button className="primary" type="submit" disabled={loading || !userId.trim() || !password}>
|
|
175
|
-
{loading ? '...' : mode === 'register' ? 'Create account' : 'Sign in'}
|
|
176
|
-
</button>
|
|
177
|
-
<button
|
|
178
|
-
type="button"
|
|
179
|
-
className="ghost"
|
|
180
|
-
onClick={() => {
|
|
181
|
-
setMode((m) => (m === 'login' ? 'register' : 'login'));
|
|
182
|
-
setErr(null);
|
|
183
|
-
}}
|
|
184
|
-
>
|
|
185
|
-
{mode === 'login' ? 'No account? Register' : 'Have an account? Sign in'}
|
|
186
|
-
</button>
|
|
187
|
-
</form>
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function LoginScreen({ onLogin }: { onLogin: (userId: string) => void }) {
|
|
192
|
-
return (
|
|
193
|
-
<div className="login-screen">
|
|
194
|
-
<LoginForm onLogin={onLogin} />
|
|
195
|
-
</div>
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function LoginModal({ onLogin, onClose }: { onLogin: (userId: string) => void; onClose: () => void }) {
|
|
200
|
-
return (
|
|
201
|
-
<div className="login-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
|
|
202
|
-
<div className="login-modal">
|
|
203
|
-
<button className="login-modal-close" onClick={onClose}>×</button>
|
|
204
|
-
<LoginForm onLogin={onLogin} />
|
|
205
|
-
</div>
|
|
206
|
-
</div>
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
31
|
// Isolated component — global subscription re-renders only this, not the entire App
|
|
211
32
|
function NodeCount() {
|
|
212
33
|
return <>{useSyncExternalStore(cache.subscribeGlobal, cache.size)}</>;
|
|
@@ -215,28 +36,42 @@ function NodeCount() {
|
|
|
215
36
|
export function App() {
|
|
216
37
|
const [authed, setAuthed] = useState<string | null>(null);
|
|
217
38
|
const [authChecked, setAuthChecked] = useState(false);
|
|
39
|
+
const retryTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
218
40
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// Auto-create anonymous session
|
|
41
|
+
const initAuth = useCallback(async () => {
|
|
42
|
+
const token = getToken();
|
|
43
|
+
if (!token) {
|
|
44
|
+
try {
|
|
224
45
|
const { token: anonToken, userId } = await trpc.anonLogin.mutate();
|
|
225
46
|
setToken(anonToken);
|
|
226
47
|
setAuthed(userId);
|
|
227
48
|
setAuthChecked(true);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
try {
|
|
231
|
-
const res = await trpc.me.query();
|
|
232
|
-
setAuthed(res?.userId ?? null);
|
|
233
|
-
if (!res) clearToken();
|
|
234
49
|
} catch {
|
|
50
|
+
toast.error('Server unavailable, retrying…');
|
|
51
|
+
retryTimer.current = setTimeout(initAuth, 3000);
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const res = await trpc.me.query();
|
|
57
|
+
setAuthed(res?.userId ?? null);
|
|
58
|
+
if (!res) clearToken();
|
|
59
|
+
setAuthChecked(true);
|
|
60
|
+
} catch (e: any) {
|
|
61
|
+
const isAuthError = e?.data?.code === 'UNAUTHORIZED' || e?.data?.httpStatus === 401;
|
|
62
|
+
if (isAuthError) {
|
|
235
63
|
clearToken();
|
|
236
|
-
} finally {
|
|
237
64
|
setAuthChecked(true);
|
|
65
|
+
} else {
|
|
66
|
+
toast.error('Server unavailable, retrying…');
|
|
67
|
+
retryTimer.current = setTimeout(initAuth, 3000);
|
|
238
68
|
}
|
|
239
|
-
}
|
|
69
|
+
}
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
initAuth();
|
|
74
|
+
return () => clearTimeout(retryTimer.current);
|
|
240
75
|
}, []);
|
|
241
76
|
|
|
242
77
|
// ── Route detection ──
|
|
@@ -322,7 +157,7 @@ export function App() {
|
|
|
322
157
|
function onKeyDown(e: KeyboardEvent) {
|
|
323
158
|
const meta = e.metaKey || e.ctrlKey;
|
|
324
159
|
if (!meta) return;
|
|
325
|
-
if (document.querySelector('
|
|
160
|
+
if (document.querySelector('[data-slot="dialog-overlay"]')) return;
|
|
326
161
|
if (e.key === '/' && selected) {
|
|
327
162
|
e.preventDefault();
|
|
328
163
|
setAddingComponentAt(selected);
|
|
@@ -348,12 +183,8 @@ export function App() {
|
|
|
348
183
|
}, [showToast]);
|
|
349
184
|
|
|
350
185
|
const loadChildren = useCallback(async (path: string) => {
|
|
351
|
-
const { items: children } =
|
|
352
|
-
|
|
353
|
-
watch: true,
|
|
354
|
-
watchNew: true,
|
|
355
|
-
})) as { items: NodeData[]; total: number };
|
|
356
|
-
cache.putMany(children, path); // Use specific parent path so query mounts index them correctly
|
|
186
|
+
const { items: children } = await tree.getChildren(path);
|
|
187
|
+
cache.putMany(children, path);
|
|
357
188
|
setLoaded((prev) => new Set(prev).add(path));
|
|
358
189
|
}, []);
|
|
359
190
|
|
|
@@ -400,60 +231,15 @@ export function App() {
|
|
|
400
231
|
})();
|
|
401
232
|
}, [authed, loadChildren, root, mode]);
|
|
402
233
|
|
|
403
|
-
//
|
|
234
|
+
// Server event subscription — module-level, refs provide stable access to current state
|
|
404
235
|
useEffect(() => {
|
|
405
236
|
if (!authed) return;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
// Watches lost — force useChildren hooks to re-fetch and re-register
|
|
411
|
-
cache.signalReconnect();
|
|
412
|
-
// Re-register tree watches for expanded paths (editor mode)
|
|
413
|
-
for (const path of expandedRef.current) loadChildren(path);
|
|
414
|
-
// Re-watch the currently selected node
|
|
415
|
-
if (selectedRef.current) {
|
|
416
|
-
trpc.get.query({ path: selectedRef.current, watch: true }).then(n => {
|
|
417
|
-
if (n) cache.put(n as NodeData);
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
if (event.type === 'set') {
|
|
424
|
-
cache.put({ $path: event.path, ...event.node } as NodeData);
|
|
425
|
-
if (event.addVps) event.addVps.forEach((vp: string) => cache.addToParent(event.path, vp));
|
|
426
|
-
if (event.rmVps) event.rmVps.forEach((vp: string) => cache.removeFromParent(event.path, vp));
|
|
427
|
-
} else if (event.type === 'patch') {
|
|
428
|
-
const existing = cache.get(event.path);
|
|
429
|
-
if (existing && event.patches) {
|
|
430
|
-
try {
|
|
431
|
-
const { newDocument } = applyPatch(structuredClone(existing), event.patches as Operation[]);
|
|
432
|
-
cache.put(newDocument as NodeData);
|
|
433
|
-
} catch (e) {
|
|
434
|
-
console.error('Failed to apply patches, fetching full node:', e);
|
|
435
|
-
trpc.get.query({ path: event.path }).then((n) => {
|
|
436
|
-
if (n) cache.put(n as NodeData);
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
} else {
|
|
440
|
-
trpc.get.query({ path: event.path }).then((n) => {
|
|
441
|
-
if (n) cache.put(n as NodeData);
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
if (event.addVps) event.addVps.forEach((vp: string) => cache.addToParent(event.path, vp));
|
|
445
|
-
if (event.rmVps) event.rmVps.forEach((vp: string) => cache.removeFromParent(event.path, vp));
|
|
446
|
-
} else if (event.type === 'remove') {
|
|
447
|
-
// Try to remove from anywhere
|
|
448
|
-
if (event.rmVps && event.rmVps.length > 0) {
|
|
449
|
-
event.rmVps.forEach((vp: string) => cache.removeFromParent(event.path, vp));
|
|
450
|
-
} else {
|
|
451
|
-
cache.remove(event.path);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
},
|
|
237
|
+
startEvents({
|
|
238
|
+
loadChildren,
|
|
239
|
+
getExpanded: () => expandedRef.current,
|
|
240
|
+
getSelected: () => selectedRef.current,
|
|
455
241
|
});
|
|
456
|
-
return
|
|
242
|
+
return stopEvents;
|
|
457
243
|
}, [authed, loadChildren]);
|
|
458
244
|
|
|
459
245
|
const handleSelect = useCallback(
|
|
@@ -562,10 +348,12 @@ export function App() {
|
|
|
562
348
|
[loadChildren, showToast],
|
|
563
349
|
);
|
|
564
350
|
|
|
565
|
-
const roots = hasRootNode ? [root] : [];
|
|
351
|
+
const roots = hasRootNode ? [root, '/local'] : ['/local'];
|
|
352
|
+
|
|
353
|
+
const [rootPromptOpen, setRootPromptOpen] = useState(false);
|
|
354
|
+
const [rootPromptType, setRootPromptType] = useState('root');
|
|
566
355
|
|
|
567
|
-
const handleCreateRoot = useCallback(async () => {
|
|
568
|
-
const type = prompt('Root node $type:', 'root');
|
|
356
|
+
const handleCreateRoot = useCallback(async (type: string) => {
|
|
569
357
|
if (!type) return;
|
|
570
358
|
try {
|
|
571
359
|
await tree.set({ $path: '/', $type: type } as NodeData);
|
|
@@ -580,37 +368,28 @@ export function App() {
|
|
|
580
368
|
}, []);
|
|
581
369
|
|
|
582
370
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
583
|
-
const [menuOpen, setMenuOpen] = useState(false);
|
|
584
371
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
585
|
-
const menuRef = useRef<HTMLDivElement>(null);
|
|
586
372
|
|
|
587
373
|
// Re-auth as anon + show login modal when session expires mid-use
|
|
588
374
|
useEffect(() => {
|
|
589
375
|
const handler = async () => {
|
|
590
376
|
if (showLoginModal) return;
|
|
591
377
|
clearToken();
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
378
|
+
try {
|
|
379
|
+
const { token, userId } = await trpc.anonLogin.mutate();
|
|
380
|
+
setToken(token);
|
|
381
|
+
setAuthed(userId);
|
|
382
|
+
setShowLoginModal(true);
|
|
383
|
+
} catch {
|
|
384
|
+
toast.error('Server unavailable');
|
|
385
|
+
}
|
|
596
386
|
};
|
|
597
387
|
window.addEventListener(AUTH_EXPIRED_EVENT, handler);
|
|
598
388
|
return () => window.removeEventListener(AUTH_EXPIRED_EVENT, handler);
|
|
599
389
|
}, [showLoginModal]);
|
|
600
390
|
|
|
601
|
-
// Close menu on outside click
|
|
602
|
-
useEffect(() => {
|
|
603
|
-
if (!menuOpen) return;
|
|
604
|
-
const onDown = (e: MouseEvent) => {
|
|
605
|
-
if (menuRef.current && !menuRef.current.contains(e.target as HTMLElement)) setMenuOpen(false);
|
|
606
|
-
};
|
|
607
|
-
document.addEventListener('mousedown', onDown);
|
|
608
|
-
return () => document.removeEventListener('mousedown', onDown);
|
|
609
|
-
}, [menuOpen]);
|
|
610
|
-
|
|
611
391
|
const handleLogout = async () => {
|
|
612
392
|
clearToken();
|
|
613
|
-
setMenuOpen(false);
|
|
614
393
|
const { token, userId } = await trpc.anonLogin.mutate();
|
|
615
394
|
setToken(token);
|
|
616
395
|
setAuthed(userId);
|
|
@@ -619,7 +398,6 @@ export function App() {
|
|
|
619
398
|
|
|
620
399
|
const handleClearCache = () => {
|
|
621
400
|
cache.clear();
|
|
622
|
-
setMenuOpen(false);
|
|
623
401
|
showToast('Cache cleared');
|
|
624
402
|
location.reload();
|
|
625
403
|
};
|
|
@@ -635,7 +413,10 @@ export function App() {
|
|
|
635
413
|
}, [mode, handleSelect]);
|
|
636
414
|
|
|
637
415
|
if (!authChecked) return null;
|
|
638
|
-
if (!authed
|
|
416
|
+
if (!authed) return <LoginScreen onLogin={(uid) => setAuthed(uid)} />;
|
|
417
|
+
|
|
418
|
+
const isAnon = authed.startsWith('anon:');
|
|
419
|
+
const needsLogin = isAnon || showLoginModal;
|
|
639
420
|
if (mode === 'view') return <NavigateProvider value={navigate}><ViewPage path={viewPath} /></NavigateProvider>;
|
|
640
421
|
if (mode === 'preview') return <NavigateProvider value={navigate}><ViewPage path={viewPath} editorLink /></NavigateProvider>;
|
|
641
422
|
|
|
@@ -645,13 +426,11 @@ export function App() {
|
|
|
645
426
|
|
|
646
427
|
if (error) {
|
|
647
428
|
return (
|
|
648
|
-
<div className="
|
|
649
|
-
<div className="
|
|
650
|
-
<
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
<button onClick={() => location.reload()}>Retry</button>
|
|
654
|
-
</div>
|
|
429
|
+
<div className="flex h-screen items-center justify-center bg-background">
|
|
430
|
+
<div className="flex flex-col items-center gap-3 text-center">
|
|
431
|
+
<span className="text-4xl">⚠</span>
|
|
432
|
+
<p className="text-sm text-red-400">{error}</p>
|
|
433
|
+
<Button variant="outline" size="sm" onClick={() => location.reload()}>Retry</Button>
|
|
655
434
|
</div>
|
|
656
435
|
</div>
|
|
657
436
|
);
|
|
@@ -659,95 +438,106 @@ export function App() {
|
|
|
659
438
|
|
|
660
439
|
return (
|
|
661
440
|
<NavigateProvider value={navigate}>
|
|
662
|
-
<div className="
|
|
663
|
-
<
|
|
664
|
-
<
|
|
665
|
-
|
|
441
|
+
<div className="flex h-screen bg-background text-foreground overflow-hidden">
|
|
442
|
+
<ResizablePanelGroup orientation="horizontal" className="h-full">
|
|
443
|
+
<ResizablePanel
|
|
444
|
+
defaultSize={28}
|
|
445
|
+
minSize={150}
|
|
446
|
+
maxSize={450}
|
|
447
|
+
className="flex flex-col border-r border-border"
|
|
448
|
+
>
|
|
449
|
+
{/* Header */}
|
|
450
|
+
<div className="flex items-center gap-2 px-3 py-2.5 border-b border-border/50 shrink-0">
|
|
666
451
|
<img src="/treenity.svg" alt="" width="20" height="20" />
|
|
667
|
-
{!sidebarCollapsed &&
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
className="sidebar-search-toggle"
|
|
700
|
-
data-active={showHidden || undefined}
|
|
701
|
-
onClick={() => setShowHidden(v => !v)}
|
|
702
|
-
title={showHidden ? 'Hide _ prefixed nodes' : 'Show _ prefixed nodes'}
|
|
703
|
-
>
|
|
704
|
-
_
|
|
705
|
-
</button>
|
|
706
|
-
</div>
|
|
707
|
-
<div className="sidebar-tree">
|
|
708
|
-
<Tree
|
|
709
|
-
roots={roots}
|
|
710
|
-
expanded={expanded}
|
|
711
|
-
loaded={loaded}
|
|
712
|
-
selected={selected}
|
|
713
|
-
filter={filter}
|
|
714
|
-
showHidden={showHidden}
|
|
715
|
-
onSelect={handleSelect}
|
|
716
|
-
onExpand={handleExpand}
|
|
717
|
-
onCreateChild={handleCreateChild}
|
|
718
|
-
onDelete={handleDelete}
|
|
719
|
-
onMove={handleMove}
|
|
720
|
-
/>
|
|
721
|
-
</div>
|
|
722
|
-
<div className="sidebar-footer" ref={menuRef}>
|
|
723
|
-
<span>
|
|
724
|
-
{authed?.startsWith('anon:') ? `anon:${authed.slice(5, 13)}` : authed} · <NodeCount /> nodes
|
|
725
|
-
</span>
|
|
726
|
-
<button className="sm ghost" onClick={() => setMenuOpen(v => !v)}>
|
|
727
|
-
☰
|
|
728
|
-
</button>
|
|
729
|
-
{menuOpen && (
|
|
730
|
-
<div className="sidebar-menu">
|
|
731
|
-
<button onClick={handleLogout}>
|
|
732
|
-
{authed?.startsWith('anon:') ? 'Login' : 'Logout'}
|
|
733
|
-
</button>
|
|
734
|
-
<button onClick={handleClearCache}>
|
|
735
|
-
Clear cache
|
|
736
|
-
</button>
|
|
452
|
+
{!sidebarCollapsed && <span className="text-sm font-semibold tracking-tight">Treenity</span>}
|
|
453
|
+
{!sidebarCollapsed && root !== '/' && (
|
|
454
|
+
<Button variant="ghost" size="sm" className="h-5 px-1.5 font-mono text-[10px] text-muted-foreground" onClick={() => setRoot('/')}>
|
|
455
|
+
⌂ {root}
|
|
456
|
+
</Button>
|
|
457
|
+
)}
|
|
458
|
+
{!sidebarCollapsed && roots.length === 0 && (
|
|
459
|
+
<Button variant="ghost" size="sm" className="h-5 text-[10px]" onClick={() => { setRootPromptType('root'); setRootPromptOpen(true); }}>
|
|
460
|
+
Create root
|
|
461
|
+
</Button>
|
|
462
|
+
)}
|
|
463
|
+
</div>
|
|
464
|
+
|
|
465
|
+
{/* Search */}
|
|
466
|
+
{!sidebarCollapsed && (
|
|
467
|
+
<div className="flex items-center gap-1 px-2 py-1.5 shrink-0">
|
|
468
|
+
<Input
|
|
469
|
+
ref={searchRef}
|
|
470
|
+
placeholder="Search nodes..."
|
|
471
|
+
value={filter}
|
|
472
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
473
|
+
className="h-7 text-xs bg-muted/50 border-border"
|
|
474
|
+
/>
|
|
475
|
+
<Button
|
|
476
|
+
variant={showHidden ? 'secondary' : 'ghost'}
|
|
477
|
+
size="sm"
|
|
478
|
+
className="h-7 w-7 p-0 text-xs text-muted-foreground shrink-0"
|
|
479
|
+
onClick={() => setShowHidden(v => !v)}
|
|
480
|
+
title={showHidden ? 'Hide _ prefixed nodes' : 'Show _ prefixed nodes'}
|
|
481
|
+
>
|
|
482
|
+
_
|
|
483
|
+
</Button>
|
|
737
484
|
</div>
|
|
738
485
|
)}
|
|
739
|
-
</div>
|
|
740
|
-
</div>
|
|
741
486
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
487
|
+
{/* Tree */}
|
|
488
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
|
489
|
+
<Tree
|
|
490
|
+
roots={roots}
|
|
491
|
+
expanded={expanded}
|
|
492
|
+
loaded={loaded}
|
|
493
|
+
selected={selected}
|
|
494
|
+
filter={filter}
|
|
495
|
+
showHidden={showHidden}
|
|
496
|
+
onSelect={handleSelect}
|
|
497
|
+
onExpand={handleExpand}
|
|
498
|
+
onCreateChild={handleCreateChild}
|
|
499
|
+
onDelete={handleDelete}
|
|
500
|
+
onMove={handleMove}
|
|
501
|
+
/>
|
|
502
|
+
</div>
|
|
503
|
+
|
|
504
|
+
{/* Footer */}
|
|
505
|
+
<div className="flex items-center justify-between px-3 py-1.5 border-t border-border/50 text-[11px] text-muted-foreground shrink-0">
|
|
506
|
+
<span className="truncate">
|
|
507
|
+
{authed?.startsWith('anon:') ? `anon:${authed.slice(5, 13)}` : authed} · <NodeCount /> nodes
|
|
508
|
+
</span>
|
|
509
|
+
<DropdownMenu>
|
|
510
|
+
<DropdownMenuTrigger asChild>
|
|
511
|
+
<Button variant="ghost" size="sm" className="h-5 w-5 p-0 text-muted-foreground hover:text-foreground">
|
|
512
|
+
☰
|
|
513
|
+
</Button>
|
|
514
|
+
</DropdownMenuTrigger>
|
|
515
|
+
<DropdownMenuContent align="end" side="top" className="w-36">
|
|
516
|
+
<DropdownMenuItem onClick={handleLogout}>
|
|
517
|
+
{authed?.startsWith('anon:') ? 'Login' : 'Logout'}
|
|
518
|
+
</DropdownMenuItem>
|
|
519
|
+
<DropdownMenuItem onClick={handleClearCache}>
|
|
520
|
+
Clear cache
|
|
521
|
+
</DropdownMenuItem>
|
|
522
|
+
</DropdownMenuContent>
|
|
523
|
+
</DropdownMenu>
|
|
524
|
+
</div>
|
|
525
|
+
</ResizablePanel>
|
|
526
|
+
|
|
527
|
+
<ResizableHandle withHandle />
|
|
528
|
+
|
|
529
|
+
<ResizablePanel defaultSize={72} minSize={40}>
|
|
530
|
+
<Inspector
|
|
531
|
+
path={selected}
|
|
532
|
+
currentUserId={authed ?? undefined}
|
|
533
|
+
onDelete={handleDelete}
|
|
534
|
+
onAddComponent={handleAddComponent}
|
|
535
|
+
onSelect={handleSelect}
|
|
536
|
+
onSetRoot={handleSetRoot}
|
|
537
|
+
toast={showToast}
|
|
538
|
+
/>
|
|
539
|
+
</ResizablePanel>
|
|
540
|
+
</ResizablePanelGroup>
|
|
751
541
|
|
|
752
542
|
{creatingAt && <TypePicker onSelect={handlePickType} onCancel={() => setCreatingAt(null)} />}
|
|
753
543
|
|
|
@@ -756,19 +546,48 @@ export function App() {
|
|
|
756
546
|
title="Add Component"
|
|
757
547
|
nameLabel="Component name"
|
|
758
548
|
action="Add"
|
|
549
|
+
autoName
|
|
759
550
|
onSelect={handlePickComponent}
|
|
760
551
|
onCancel={() => setAddingComponentAt(null)}
|
|
761
552
|
/>
|
|
762
553
|
)}
|
|
763
554
|
|
|
764
|
-
{
|
|
555
|
+
<AlertDialog open={rootPromptOpen} onOpenChange={setRootPromptOpen}>
|
|
556
|
+
<AlertDialogContent>
|
|
557
|
+
<AlertDialogHeader>
|
|
558
|
+
<AlertDialogTitle>Create root node</AlertDialogTitle>
|
|
559
|
+
</AlertDialogHeader>
|
|
560
|
+
<Input
|
|
561
|
+
value={rootPromptType}
|
|
562
|
+
onChange={(e) => setRootPromptType(e.target.value)}
|
|
563
|
+
placeholder="$type"
|
|
564
|
+
className="font-mono"
|
|
565
|
+
onKeyDown={(e) => {
|
|
566
|
+
if (e.key === 'Enter') {
|
|
567
|
+
setRootPromptOpen(false);
|
|
568
|
+
handleCreateRoot(rootPromptType);
|
|
569
|
+
}
|
|
570
|
+
}}
|
|
571
|
+
/>
|
|
572
|
+
<AlertDialogFooter>
|
|
573
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
574
|
+
<AlertDialogAction onClick={() => handleCreateRoot(rootPromptType)}>Create</AlertDialogAction>
|
|
575
|
+
</AlertDialogFooter>
|
|
576
|
+
</AlertDialogContent>
|
|
577
|
+
</AlertDialog>
|
|
578
|
+
|
|
579
|
+
{needsLogin && (
|
|
765
580
|
<LoginModal
|
|
766
581
|
onLogin={(uid) => { setAuthed(uid); setShowLoginModal(false); }}
|
|
767
|
-
onClose={() => setShowLoginModal(false)}
|
|
582
|
+
onClose={isAnon ? undefined : () => setShowLoginModal(false)}
|
|
768
583
|
/>
|
|
769
584
|
)}
|
|
770
585
|
|
|
771
|
-
{toastMsg &&
|
|
586
|
+
{toastMsg && (
|
|
587
|
+
<div className={`fixed bottom-4 left-1/2 -translate-x-1/2 z-50 px-4 py-2 rounded-lg text-sm ${toastMsg.type === 'error' ? 'bg-destructive/20 text-destructive border border-destructive/30' : 'bg-primary/20 text-primary border border-primary/30'}`}>
|
|
588
|
+
{toastMsg.text}
|
|
589
|
+
</div>
|
|
590
|
+
)}
|
|
772
591
|
</div>
|
|
773
592
|
</NavigateProvider>
|
|
774
593
|
);
|