@treenity/react 3.0.0 → 3.0.2
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 +97 -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 +18 -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 +9 -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 +8 -7
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.js +25 -20
- 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 +2 -2
- 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 +12 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +123 -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 +14 -2
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +146 -11
- 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/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/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 +11 -4
- 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 +56 -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 +56 -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 +69 -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 +70 -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.d.ts.map +1 -1
- package/dist/mods/treenity/preview.js +3 -4
- package/dist/mods/treenity/preview.js.map +1 -1
- package/dist/mods/treenity/ref-view.js +3 -2
- 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 +3 -2
- 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 +22 -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 +76 -8
- package/src/AclEditor.tsx +11 -18
- package/src/ActionCards.tsx +224 -0
- package/src/App.tsx +232 -385
- package/src/ComponentSection.tsx +113 -0
- package/src/ErrorBoundary.tsx +40 -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/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 +12 -1
- package/src/client-tree.ts +18 -12
- 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 +51 -30
- 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 +122 -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 +2 -2
- 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 +121 -0
- package/src/fiber-tree.ts +112 -0
- package/src/hooks.ts +161 -13
- package/src/idb.ts +1 -1
- package/src/lib/minimd.ts +7 -1
- package/src/lib/sanitize-href.ts +13 -0
- package/src/lib/to-plain.ts +21 -0
- package/src/main.tsx +14 -4
- package/src/mods/clients.ts +3 -0
- package/src/mods/editor-ui/FieldLabel.tsx +125 -0
- package/src/mods/editor-ui/client.ts +1 -1
- package/src/mods/editor-ui/default-edit.tsx +101 -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 +146 -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 +148 -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 +7 -8
- package/src/mods/treenity/ref-view.tsx +12 -7
- package/src/mods/treenity/schema-form.tsx +1 -1
- package/src/mods/treenity/seed.ts +3 -2
- package/src/mods/treenity/type-view.tsx +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 +25 -0
- 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/index.html +0 -14
- package/src/remote-tree.test.ts +0 -142
- 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 { SSE_CONNECTED, SSE_DISCONNECTED, 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 ──
|
|
@@ -274,6 +109,29 @@ export function App() {
|
|
|
274
109
|
const [filter, setFilter] = useState('');
|
|
275
110
|
const [showHidden, setShowHidden] = useState(false);
|
|
276
111
|
const [toastMsg, setToastMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
|
|
112
|
+
const [sseDown, setSseDown] = useState(false);
|
|
113
|
+
const sseDownTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
114
|
+
|
|
115
|
+
// TODO: remove debounce, extract from App code, remove debounce
|
|
116
|
+
// SSE connection indicator — debounce disconnect by 2s to avoid flicker
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const onConnect = () => {
|
|
119
|
+
if (sseDownTimer.current) { clearTimeout(sseDownTimer.current); sseDownTimer.current = undefined; }
|
|
120
|
+
setSseDown(false);
|
|
121
|
+
};
|
|
122
|
+
const onDisconnect = () => {
|
|
123
|
+
if (!sseDownTimer.current) {
|
|
124
|
+
sseDownTimer.current = setTimeout(() => { sseDownTimer.current = undefined; setSseDown(true); }, 2000);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
window.addEventListener(SSE_CONNECTED, onConnect);
|
|
128
|
+
window.addEventListener(SSE_DISCONNECTED, onDisconnect);
|
|
129
|
+
return () => {
|
|
130
|
+
window.removeEventListener(SSE_CONNECTED, onConnect);
|
|
131
|
+
window.removeEventListener(SSE_DISCONNECTED, onDisconnect);
|
|
132
|
+
if (sseDownTimer.current) clearTimeout(sseDownTimer.current);
|
|
133
|
+
};
|
|
134
|
+
}, []);
|
|
277
135
|
|
|
278
136
|
// Granular: only re-render App when root node appears/disappears
|
|
279
137
|
const hasRootNode = useSyncExternalStore(
|
|
@@ -322,7 +180,7 @@ export function App() {
|
|
|
322
180
|
function onKeyDown(e: KeyboardEvent) {
|
|
323
181
|
const meta = e.metaKey || e.ctrlKey;
|
|
324
182
|
if (!meta) return;
|
|
325
|
-
if (document.querySelector('
|
|
183
|
+
if (document.querySelector('[data-slot="dialog-overlay"]')) return;
|
|
326
184
|
if (e.key === '/' && selected) {
|
|
327
185
|
e.preventDefault();
|
|
328
186
|
setAddingComponentAt(selected);
|
|
@@ -348,12 +206,8 @@ export function App() {
|
|
|
348
206
|
}, [showToast]);
|
|
349
207
|
|
|
350
208
|
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
|
|
209
|
+
const { items: children } = await tree.getChildren(path);
|
|
210
|
+
cache.putMany(children, path);
|
|
357
211
|
setLoaded((prev) => new Set(prev).add(path));
|
|
358
212
|
}, []);
|
|
359
213
|
|
|
@@ -400,60 +254,15 @@ export function App() {
|
|
|
400
254
|
})();
|
|
401
255
|
}, [authed, loadChildren, root, mode]);
|
|
402
256
|
|
|
403
|
-
//
|
|
257
|
+
// Server event subscription — module-level, refs provide stable access to current state
|
|
404
258
|
useEffect(() => {
|
|
405
259
|
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
|
-
},
|
|
260
|
+
startEvents({
|
|
261
|
+
loadChildren,
|
|
262
|
+
getExpanded: () => expandedRef.current,
|
|
263
|
+
getSelected: () => selectedRef.current,
|
|
455
264
|
});
|
|
456
|
-
return
|
|
265
|
+
return stopEvents;
|
|
457
266
|
}, [authed, loadChildren]);
|
|
458
267
|
|
|
459
268
|
const handleSelect = useCallback(
|
|
@@ -562,10 +371,12 @@ export function App() {
|
|
|
562
371
|
[loadChildren, showToast],
|
|
563
372
|
);
|
|
564
373
|
|
|
565
|
-
const roots = hasRootNode ? [root] : [];
|
|
374
|
+
const roots = hasRootNode ? [root, '/local'] : ['/local'];
|
|
375
|
+
|
|
376
|
+
const [rootPromptOpen, setRootPromptOpen] = useState(false);
|
|
377
|
+
const [rootPromptType, setRootPromptType] = useState('root');
|
|
566
378
|
|
|
567
|
-
const handleCreateRoot = useCallback(async () => {
|
|
568
|
-
const type = prompt('Root node $type:', 'root');
|
|
379
|
+
const handleCreateRoot = useCallback(async (type: string) => {
|
|
569
380
|
if (!type) return;
|
|
570
381
|
try {
|
|
571
382
|
await tree.set({ $path: '/', $type: type } as NodeData);
|
|
@@ -580,37 +391,28 @@ export function App() {
|
|
|
580
391
|
}, []);
|
|
581
392
|
|
|
582
393
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
583
|
-
const [menuOpen, setMenuOpen] = useState(false);
|
|
584
394
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
585
|
-
const menuRef = useRef<HTMLDivElement>(null);
|
|
586
395
|
|
|
587
396
|
// Re-auth as anon + show login modal when session expires mid-use
|
|
588
397
|
useEffect(() => {
|
|
589
398
|
const handler = async () => {
|
|
590
399
|
if (showLoginModal) return;
|
|
591
400
|
clearToken();
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
401
|
+
try {
|
|
402
|
+
const { token, userId } = await trpc.anonLogin.mutate();
|
|
403
|
+
setToken(token);
|
|
404
|
+
setAuthed(userId);
|
|
405
|
+
setShowLoginModal(true);
|
|
406
|
+
} catch {
|
|
407
|
+
toast.error('Server unavailable');
|
|
408
|
+
}
|
|
596
409
|
};
|
|
597
410
|
window.addEventListener(AUTH_EXPIRED_EVENT, handler);
|
|
598
411
|
return () => window.removeEventListener(AUTH_EXPIRED_EVENT, handler);
|
|
599
412
|
}, [showLoginModal]);
|
|
600
413
|
|
|
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
414
|
const handleLogout = async () => {
|
|
612
415
|
clearToken();
|
|
613
|
-
setMenuOpen(false);
|
|
614
416
|
const { token, userId } = await trpc.anonLogin.mutate();
|
|
615
417
|
setToken(token);
|
|
616
418
|
setAuthed(userId);
|
|
@@ -619,7 +421,6 @@ export function App() {
|
|
|
619
421
|
|
|
620
422
|
const handleClearCache = () => {
|
|
621
423
|
cache.clear();
|
|
622
|
-
setMenuOpen(false);
|
|
623
424
|
showToast('Cache cleared');
|
|
624
425
|
location.reload();
|
|
625
426
|
};
|
|
@@ -635,7 +436,10 @@ export function App() {
|
|
|
635
436
|
}, [mode, handleSelect]);
|
|
636
437
|
|
|
637
438
|
if (!authChecked) return null;
|
|
638
|
-
if (!authed
|
|
439
|
+
if (!authed) return <LoginScreen onLogin={(uid) => setAuthed(uid)} />;
|
|
440
|
+
|
|
441
|
+
const isAnon = authed.startsWith('anon:');
|
|
442
|
+
const needsLogin = isAnon || showLoginModal;
|
|
639
443
|
if (mode === 'view') return <NavigateProvider value={navigate}><ViewPage path={viewPath} /></NavigateProvider>;
|
|
640
444
|
if (mode === 'preview') return <NavigateProvider value={navigate}><ViewPage path={viewPath} editorLink /></NavigateProvider>;
|
|
641
445
|
|
|
@@ -645,13 +449,11 @@ export function App() {
|
|
|
645
449
|
|
|
646
450
|
if (error) {
|
|
647
451
|
return (
|
|
648
|
-
<div className="
|
|
649
|
-
<div className="
|
|
650
|
-
<
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
<button onClick={() => location.reload()}>Retry</button>
|
|
654
|
-
</div>
|
|
452
|
+
<div className="flex h-screen items-center justify-center bg-background">
|
|
453
|
+
<div className="flex flex-col items-center gap-3 text-center">
|
|
454
|
+
<span className="text-4xl">⚠</span>
|
|
455
|
+
<p className="text-sm text-red-400">{error}</p>
|
|
456
|
+
<Button variant="outline" size="sm" onClick={() => location.reload()}>Retry</Button>
|
|
655
457
|
</div>
|
|
656
458
|
</div>
|
|
657
459
|
);
|
|
@@ -659,95 +461,111 @@ export function App() {
|
|
|
659
461
|
|
|
660
462
|
return (
|
|
661
463
|
<NavigateProvider value={navigate}>
|
|
662
|
-
|
|
663
|
-
<div className=
|
|
664
|
-
|
|
665
|
-
|
|
464
|
+
{sseDown && (
|
|
465
|
+
<div className="fixed top-0 inset-x-0 z-50 bg-yellow-500 text-black text-center text-sm py-1">
|
|
466
|
+
Reconnecting to server…
|
|
467
|
+
</div>
|
|
468
|
+
)}
|
|
469
|
+
<div className="flex h-screen bg-background text-foreground overflow-hidden">
|
|
470
|
+
<ResizablePanelGroup orientation="horizontal" className="h-full">
|
|
471
|
+
<ResizablePanel
|
|
472
|
+
defaultSize={28}
|
|
473
|
+
minSize={150}
|
|
474
|
+
maxSize={450}
|
|
475
|
+
className="flex flex-col border-r border-border"
|
|
476
|
+
>
|
|
477
|
+
{/* Header */}
|
|
478
|
+
<div className="flex items-center gap-2 px-3 py-2.5 border-b border-border/50 shrink-0">
|
|
666
479
|
<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>
|
|
480
|
+
{!sidebarCollapsed && <span className="text-sm font-semibold tracking-tight">Treenity</span>}
|
|
481
|
+
{!sidebarCollapsed && root !== '/' && (
|
|
482
|
+
<Button variant="ghost" size="sm" className="h-5 px-1.5 font-mono text-[10px] text-muted-foreground" onClick={() => setRoot('/')}>
|
|
483
|
+
⌂ {root}
|
|
484
|
+
</Button>
|
|
485
|
+
)}
|
|
486
|
+
{!sidebarCollapsed && roots.length === 0 && (
|
|
487
|
+
<Button variant="ghost" size="sm" className="h-5 text-[10px]" onClick={() => { setRootPromptType('root'); setRootPromptOpen(true); }}>
|
|
488
|
+
Create root
|
|
489
|
+
</Button>
|
|
490
|
+
)}
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
{/* Search */}
|
|
494
|
+
{!sidebarCollapsed && (
|
|
495
|
+
<div className="flex items-center gap-1 px-2 py-1.5 shrink-0">
|
|
496
|
+
<Input
|
|
497
|
+
ref={searchRef}
|
|
498
|
+
placeholder="Search nodes..."
|
|
499
|
+
value={filter}
|
|
500
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
501
|
+
className="h-7 text-xs bg-muted/50 border-border"
|
|
502
|
+
/>
|
|
503
|
+
<Button
|
|
504
|
+
variant={showHidden ? 'secondary' : 'ghost'}
|
|
505
|
+
size="sm"
|
|
506
|
+
className="h-7 w-7 p-0 text-xs text-muted-foreground shrink-0"
|
|
507
|
+
onClick={() => setShowHidden(v => !v)}
|
|
508
|
+
title={showHidden ? 'Hide _ prefixed nodes' : 'Show _ prefixed nodes'}
|
|
509
|
+
>
|
|
510
|
+
_
|
|
511
|
+
</Button>
|
|
737
512
|
</div>
|
|
738
513
|
)}
|
|
739
|
-
</div>
|
|
740
|
-
</div>
|
|
741
514
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
515
|
+
{/* Tree */}
|
|
516
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
|
517
|
+
<Tree
|
|
518
|
+
roots={roots}
|
|
519
|
+
expanded={expanded}
|
|
520
|
+
loaded={loaded}
|
|
521
|
+
selected={selected}
|
|
522
|
+
filter={filter}
|
|
523
|
+
showHidden={showHidden}
|
|
524
|
+
onSelect={handleSelect}
|
|
525
|
+
onExpand={handleExpand}
|
|
526
|
+
onCreateChild={handleCreateChild}
|
|
527
|
+
onDelete={handleDelete}
|
|
528
|
+
onMove={handleMove}
|
|
529
|
+
/>
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
{/* Footer */}
|
|
533
|
+
<div className="flex items-center justify-between px-3 py-1.5 border-t border-border/50 text-[11px] text-muted-foreground shrink-0">
|
|
534
|
+
<span className="truncate">
|
|
535
|
+
{authed?.startsWith('anon:') ? `anon:${authed.slice(5, 13)}` : authed} · <NodeCount /> nodes
|
|
536
|
+
</span>
|
|
537
|
+
<DropdownMenu>
|
|
538
|
+
<DropdownMenuTrigger asChild>
|
|
539
|
+
<Button variant="ghost" size="sm" className="h-5 w-5 p-0 text-muted-foreground hover:text-foreground">
|
|
540
|
+
☰
|
|
541
|
+
</Button>
|
|
542
|
+
</DropdownMenuTrigger>
|
|
543
|
+
<DropdownMenuContent align="end" side="top" className="w-36">
|
|
544
|
+
<DropdownMenuItem onClick={handleLogout}>
|
|
545
|
+
{authed?.startsWith('anon:') ? 'Login' : 'Logout'}
|
|
546
|
+
</DropdownMenuItem>
|
|
547
|
+
<DropdownMenuItem onClick={handleClearCache}>
|
|
548
|
+
Clear cache
|
|
549
|
+
</DropdownMenuItem>
|
|
550
|
+
</DropdownMenuContent>
|
|
551
|
+
</DropdownMenu>
|
|
552
|
+
</div>
|
|
553
|
+
</ResizablePanel>
|
|
554
|
+
|
|
555
|
+
<ResizableHandle withHandle />
|
|
556
|
+
|
|
557
|
+
<ResizablePanel defaultSize={72} minSize={40}>
|
|
558
|
+
<Inspector
|
|
559
|
+
path={selected}
|
|
560
|
+
currentUserId={authed ?? undefined}
|
|
561
|
+
onDelete={handleDelete}
|
|
562
|
+
onAddComponent={handleAddComponent}
|
|
563
|
+
onSelect={handleSelect}
|
|
564
|
+
onSetRoot={handleSetRoot}
|
|
565
|
+
toast={showToast}
|
|
566
|
+
/>
|
|
567
|
+
</ResizablePanel>
|
|
568
|
+
</ResizablePanelGroup>
|
|
751
569
|
|
|
752
570
|
{creatingAt && <TypePicker onSelect={handlePickType} onCancel={() => setCreatingAt(null)} />}
|
|
753
571
|
|
|
@@ -756,19 +574,48 @@ export function App() {
|
|
|
756
574
|
title="Add Component"
|
|
757
575
|
nameLabel="Component name"
|
|
758
576
|
action="Add"
|
|
577
|
+
autoName
|
|
759
578
|
onSelect={handlePickComponent}
|
|
760
579
|
onCancel={() => setAddingComponentAt(null)}
|
|
761
580
|
/>
|
|
762
581
|
)}
|
|
763
582
|
|
|
764
|
-
{
|
|
583
|
+
<AlertDialog open={rootPromptOpen} onOpenChange={setRootPromptOpen}>
|
|
584
|
+
<AlertDialogContent>
|
|
585
|
+
<AlertDialogHeader>
|
|
586
|
+
<AlertDialogTitle>Create root node</AlertDialogTitle>
|
|
587
|
+
</AlertDialogHeader>
|
|
588
|
+
<Input
|
|
589
|
+
value={rootPromptType}
|
|
590
|
+
onChange={(e) => setRootPromptType(e.target.value)}
|
|
591
|
+
placeholder="$type"
|
|
592
|
+
className="font-mono"
|
|
593
|
+
onKeyDown={(e) => {
|
|
594
|
+
if (e.key === 'Enter') {
|
|
595
|
+
setRootPromptOpen(false);
|
|
596
|
+
handleCreateRoot(rootPromptType);
|
|
597
|
+
}
|
|
598
|
+
}}
|
|
599
|
+
/>
|
|
600
|
+
<AlertDialogFooter>
|
|
601
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
602
|
+
<AlertDialogAction onClick={() => handleCreateRoot(rootPromptType)}>Create</AlertDialogAction>
|
|
603
|
+
</AlertDialogFooter>
|
|
604
|
+
</AlertDialogContent>
|
|
605
|
+
</AlertDialog>
|
|
606
|
+
|
|
607
|
+
{needsLogin && (
|
|
765
608
|
<LoginModal
|
|
766
609
|
onLogin={(uid) => { setAuthed(uid); setShowLoginModal(false); }}
|
|
767
|
-
onClose={() => setShowLoginModal(false)}
|
|
610
|
+
onClose={isAnon ? undefined : () => setShowLoginModal(false)}
|
|
768
611
|
/>
|
|
769
612
|
)}
|
|
770
613
|
|
|
771
|
-
{toastMsg &&
|
|
614
|
+
{toastMsg && (
|
|
615
|
+
<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'}`}>
|
|
616
|
+
{toastMsg.text}
|
|
617
|
+
</div>
|
|
618
|
+
)}
|
|
772
619
|
</div>
|
|
773
620
|
</NavigateProvider>
|
|
774
621
|
);
|