@cryptiklemur/lattice 0.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/.editorconfig +12 -0
- package/.github/workflows/release.yml +44 -0
- package/.impeccable.md +66 -0
- package/.releaserc.json +32 -0
- package/.serena/project.yml +138 -0
- package/CLAUDE.md +35 -0
- package/CONTRIBUTING.md +93 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bun.lock +1459 -0
- package/bunfig.toml +2 -0
- package/client/index.html +32 -0
- package/client/package.json +37 -0
- package/client/public/icons/icon-192.svg +11 -0
- package/client/public/icons/icon-512.svg +11 -0
- package/client/public/manifest.json +24 -0
- package/client/public/sw.js +61 -0
- package/client/src/App.tsx +28 -0
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
- package/client/src/components/chat/ChatInput.tsx +241 -0
- package/client/src/components/chat/ChatView.tsx +727 -0
- package/client/src/components/chat/Message.tsx +362 -0
- package/client/src/components/chat/ModelSelector.tsx +87 -0
- package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
- package/client/src/components/chat/StatusBar.tsx +50 -0
- package/client/src/components/chat/ToolGroup.tsx +129 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
- package/client/src/components/chat/toolSummary.ts +41 -0
- package/client/src/components/dashboard/DashboardView.tsx +219 -0
- package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
- package/client/src/components/mesh/NodeBadge.tsx +24 -0
- package/client/src/components/mesh/PairingDialog.tsx +281 -0
- package/client/src/components/panels/FileBrowser.tsx +241 -0
- package/client/src/components/panels/StickyNotes.tsx +187 -0
- package/client/src/components/panels/Terminal.tsx +128 -0
- package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
- package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
- package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
- package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
- package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
- package/client/src/components/project-settings/ProjectRules.tsx +277 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
- package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
- package/client/src/components/settings/Appearance.tsx +151 -0
- package/client/src/components/settings/ClaudeSettings.tsx +151 -0
- package/client/src/components/settings/Environment.tsx +185 -0
- package/client/src/components/settings/GlobalMcp.tsx +207 -0
- package/client/src/components/settings/GlobalSkills.tsx +125 -0
- package/client/src/components/settings/MeshStatus.tsx +145 -0
- package/client/src/components/settings/SettingsView.tsx +57 -0
- package/client/src/components/settings/SkillMarketplace.tsx +175 -0
- package/client/src/components/settings/mcp-shared.tsx +194 -0
- package/client/src/components/settings/skill-shared.tsx +177 -0
- package/client/src/components/setup/SetupWizard.tsx +750 -0
- package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
- package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
- package/client/src/components/sidebar/ProjectRail.tsx +291 -0
- package/client/src/components/sidebar/SearchFilter.tsx +52 -0
- package/client/src/components/sidebar/SessionList.tsx +384 -0
- package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
- package/client/src/components/sidebar/Sidebar.tsx +209 -0
- package/client/src/components/sidebar/UserIsland.tsx +59 -0
- package/client/src/components/sidebar/UserMenu.tsx +101 -0
- package/client/src/components/ui/CommandPalette.tsx +321 -0
- package/client/src/components/ui/ErrorBoundary.tsx +56 -0
- package/client/src/components/ui/IconPicker.tsx +209 -0
- package/client/src/components/ui/LatticeLogomark.tsx +19 -0
- package/client/src/components/ui/PopupMenu.tsx +98 -0
- package/client/src/components/ui/SaveFooter.tsx +38 -0
- package/client/src/components/ui/Toast.tsx +112 -0
- package/client/src/hooks/useMesh.ts +89 -0
- package/client/src/hooks/useProjectSettings.ts +56 -0
- package/client/src/hooks/useProjects.ts +66 -0
- package/client/src/hooks/useSaveState.ts +59 -0
- package/client/src/hooks/useSession.ts +317 -0
- package/client/src/hooks/useSidebar.ts +74 -0
- package/client/src/hooks/useSkills.ts +30 -0
- package/client/src/hooks/useTheme.ts +114 -0
- package/client/src/hooks/useWebSocket.ts +26 -0
- package/client/src/main.tsx +10 -0
- package/client/src/providers/WebSocketProvider.tsx +146 -0
- package/client/src/router.tsx +391 -0
- package/client/src/stores/mesh.ts +78 -0
- package/client/src/stores/session.ts +322 -0
- package/client/src/stores/sidebar.ts +336 -0
- package/client/src/stores/theme.ts +44 -0
- package/client/src/styles/global.css +167 -0
- package/client/src/styles/theme-vars.css +18 -0
- package/client/src/themes/index.ts +79 -0
- package/client/src/utils/findDuplicateKeys.ts +12 -0
- package/client/tsconfig.json +14 -0
- package/client/vite.config.ts +20 -0
- package/package.json +46 -0
- package/server/package.json +22 -0
- package/server/src/auth/passphrase.ts +48 -0
- package/server/src/config.ts +55 -0
- package/server/src/daemon.ts +338 -0
- package/server/src/features/ralph-loop.ts +173 -0
- package/server/src/features/scheduler.ts +281 -0
- package/server/src/features/sticky-notes.ts +102 -0
- package/server/src/handlers/chat.ts +194 -0
- package/server/src/handlers/fs.ts +84 -0
- package/server/src/handlers/loop.ts +37 -0
- package/server/src/handlers/mesh.ts +125 -0
- package/server/src/handlers/notes.ts +45 -0
- package/server/src/handlers/project-settings.ts +174 -0
- package/server/src/handlers/scheduler.ts +47 -0
- package/server/src/handlers/session.ts +159 -0
- package/server/src/handlers/settings.ts +109 -0
- package/server/src/handlers/skills.ts +380 -0
- package/server/src/handlers/terminal.ts +70 -0
- package/server/src/identity.ts +26 -0
- package/server/src/index.ts +190 -0
- package/server/src/mesh/connector.ts +209 -0
- package/server/src/mesh/discovery.ts +123 -0
- package/server/src/mesh/pairing.ts +94 -0
- package/server/src/mesh/peers.ts +52 -0
- package/server/src/mesh/proxy.ts +103 -0
- package/server/src/mesh/session-sync.ts +107 -0
- package/server/src/project/context-breakdown.ts +289 -0
- package/server/src/project/file-browser.ts +106 -0
- package/server/src/project/project-files.ts +267 -0
- package/server/src/project/registry.ts +57 -0
- package/server/src/project/sdk-bridge.ts +566 -0
- package/server/src/project/session.ts +432 -0
- package/server/src/project/terminal.ts +69 -0
- package/server/src/tls.ts +51 -0
- package/server/src/ws/broadcast.ts +31 -0
- package/server/src/ws/router.ts +104 -0
- package/server/src/ws/server.ts +2 -0
- package/server/tsconfig.json +16 -0
- package/shared/package.json +11 -0
- package/shared/src/constants.ts +7 -0
- package/shared/src/index.ts +4 -0
- package/shared/src/messages.ts +638 -0
- package/shared/src/models.ts +136 -0
- package/shared/src/project-settings.ts +45 -0
- package/shared/tsconfig.json +11 -0
- package/themes/amoled.json +20 -0
- package/themes/ayu-light.json +9 -0
- package/themes/catppuccin-latte.json +9 -0
- package/themes/catppuccin-mocha.json +9 -0
- package/themes/clay-light.json +10 -0
- package/themes/clay.json +10 -0
- package/themes/dracula.json +9 -0
- package/themes/everforest-light.json +9 -0
- package/themes/everforest.json +9 -0
- package/themes/github-light.json +9 -0
- package/themes/gruvbox-dark.json +9 -0
- package/themes/gruvbox-light.json +9 -0
- package/themes/monokai.json +9 -0
- package/themes/nord-light.json +9 -0
- package/themes/nord.json +9 -0
- package/themes/one-dark.json +9 -0
- package/themes/one-light.json +9 -0
- package/themes/rose-pine-dawn.json +9 -0
- package/themes/rose-pine.json +9 -0
- package/themes/solarized-dark.json +9 -0
- package/themes/solarized-light.json +9 -0
- package/themes/tokyo-night-light.json +9 -0
- package/themes/tokyo-night.json +9 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { X, Copy, Check } from "lucide-react";
|
|
3
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
+
import { useMesh } from "../../hooks/useMesh";
|
|
5
|
+
import { useSaveState } from "../../hooks/useSaveState";
|
|
6
|
+
import { SaveFooter } from "../ui/SaveFooter";
|
|
7
|
+
import type { ServerMessage, SettingsDataMessage, LatticeConfig } from "@lattice/shared";
|
|
8
|
+
|
|
9
|
+
interface NodeSettingsModalProps {
|
|
10
|
+
isOpen: boolean;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function NodeSettingsModal({ isOpen, onClose }: NodeSettingsModalProps) {
|
|
15
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
16
|
+
var { nodes } = useMesh();
|
|
17
|
+
var save = useSaveState();
|
|
18
|
+
|
|
19
|
+
var localNode = nodes.find(function (n) { return n.isLocal; });
|
|
20
|
+
var nodeId = localNode ? localNode.id : "";
|
|
21
|
+
|
|
22
|
+
var [config, setConfig] = useState<LatticeConfig | null>(null);
|
|
23
|
+
var [name, setName] = useState("");
|
|
24
|
+
var [port, setPort] = useState(7654);
|
|
25
|
+
var [tls, setTls] = useState(false);
|
|
26
|
+
var [debug, setDebug] = useState(false);
|
|
27
|
+
var [copied, setCopied] = useState(false);
|
|
28
|
+
var copyTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
29
|
+
|
|
30
|
+
useEffect(function () {
|
|
31
|
+
if (!isOpen) return;
|
|
32
|
+
|
|
33
|
+
function handleData(msg: ServerMessage) {
|
|
34
|
+
if (msg.type !== "settings:data") return;
|
|
35
|
+
var data = msg as SettingsDataMessage;
|
|
36
|
+
var cfg = data.config;
|
|
37
|
+
setConfig(cfg);
|
|
38
|
+
|
|
39
|
+
if (save.saving) {
|
|
40
|
+
save.confirmSave();
|
|
41
|
+
} else {
|
|
42
|
+
setName(cfg.name);
|
|
43
|
+
setPort(cfg.port);
|
|
44
|
+
setTls(cfg.tls);
|
|
45
|
+
setDebug(cfg.debug);
|
|
46
|
+
save.resetFromServer();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
subscribe("settings:data", handleData);
|
|
51
|
+
send({ type: "settings:get" });
|
|
52
|
+
|
|
53
|
+
return function () {
|
|
54
|
+
unsubscribe("settings:data", handleData);
|
|
55
|
+
};
|
|
56
|
+
}, [isOpen]);
|
|
57
|
+
|
|
58
|
+
useEffect(function () {
|
|
59
|
+
return function () {
|
|
60
|
+
if (copyTimeout.current) clearTimeout(copyTimeout.current);
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
function handleSave() {
|
|
65
|
+
save.startSave();
|
|
66
|
+
send({
|
|
67
|
+
type: "settings:update",
|
|
68
|
+
settings: { name, port, tls, debug },
|
|
69
|
+
} as any);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleCopyId() {
|
|
73
|
+
if (!nodeId) return;
|
|
74
|
+
navigator.clipboard.writeText(nodeId);
|
|
75
|
+
setCopied(true);
|
|
76
|
+
if (copyTimeout.current) clearTimeout(copyTimeout.current);
|
|
77
|
+
copyTimeout.current = setTimeout(function () { setCopied(false); }, 2000);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!isOpen) return null;
|
|
81
|
+
|
|
82
|
+
var inputClass = "w-full h-9 px-3 bg-base-300 border border-base-content/15 rounded-xl text-base-content text-[13px] focus:border-primary focus-visible:outline-none transition-colors duration-[120ms]";
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
|
|
86
|
+
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
|
87
|
+
<div className="relative bg-base-200 border border-base-content/15 rounded-2xl shadow-2xl w-full max-w-md mx-4 overflow-hidden">
|
|
88
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-base-content/15">
|
|
89
|
+
<h2 className="text-[15px] font-mono font-bold text-base-content">Node Settings</h2>
|
|
90
|
+
<button
|
|
91
|
+
onClick={onClose}
|
|
92
|
+
aria-label="Close"
|
|
93
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content"
|
|
94
|
+
>
|
|
95
|
+
<X size={16} />
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="px-5 py-4 space-y-4 max-h-[70vh] overflow-y-auto">
|
|
100
|
+
<div>
|
|
101
|
+
<label htmlFor="node-id" className="block text-[12px] font-semibold text-base-content/40 mb-1.5">Node ID</label>
|
|
102
|
+
<div className="flex items-center gap-2">
|
|
103
|
+
<div className="flex-1 h-9 px-3 bg-base-300/50 border border-base-content/10 rounded-xl font-mono text-[11px] text-base-content/50 flex items-center truncate select-all">
|
|
104
|
+
{nodeId || "Loading..."}
|
|
105
|
+
</div>
|
|
106
|
+
<button
|
|
107
|
+
onClick={handleCopyId}
|
|
108
|
+
disabled={!nodeId}
|
|
109
|
+
aria-label="Copy node ID"
|
|
110
|
+
className="btn btn-ghost btn-sm btn-square text-base-content/30 hover:text-base-content flex-shrink-0"
|
|
111
|
+
>
|
|
112
|
+
{copied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div>
|
|
118
|
+
<label htmlFor="node-name" className="block text-[12px] font-semibold text-base-content/40 mb-1.5">Node Name</label>
|
|
119
|
+
<input
|
|
120
|
+
id="node-name"
|
|
121
|
+
type="text"
|
|
122
|
+
value={name}
|
|
123
|
+
onChange={function (e) { setName(e.target.value); save.markDirty(); }}
|
|
124
|
+
placeholder="My Node"
|
|
125
|
+
className={inputClass}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div>
|
|
130
|
+
<label htmlFor="node-port" className="block text-[12px] font-semibold text-base-content/40 mb-1.5">Port</label>
|
|
131
|
+
<input
|
|
132
|
+
id="node-port"
|
|
133
|
+
type="number"
|
|
134
|
+
value={port}
|
|
135
|
+
onChange={function (e) {
|
|
136
|
+
var val = parseInt(e.target.value, 10);
|
|
137
|
+
if (!isNaN(val)) { setPort(val); save.markDirty(); }
|
|
138
|
+
}}
|
|
139
|
+
min={1}
|
|
140
|
+
max={65535}
|
|
141
|
+
className={inputClass + " font-mono"}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="flex items-center justify-between py-1">
|
|
146
|
+
<div>
|
|
147
|
+
<div className="text-[12px] font-semibold text-base-content/40">TLS</div>
|
|
148
|
+
<div className="text-[11px] text-base-content/30">Encrypt connections between nodes</div>
|
|
149
|
+
</div>
|
|
150
|
+
<input
|
|
151
|
+
type="checkbox"
|
|
152
|
+
checked={tls}
|
|
153
|
+
onChange={function (e) { setTls(e.target.checked); save.markDirty(); }}
|
|
154
|
+
className="toggle toggle-sm toggle-primary"
|
|
155
|
+
aria-label="Enable TLS"
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div className="flex items-center justify-between py-1">
|
|
160
|
+
<div>
|
|
161
|
+
<div className="text-[12px] font-semibold text-base-content/40">Debug Mode</div>
|
|
162
|
+
<div className="text-[11px] text-base-content/30">Enable verbose logging</div>
|
|
163
|
+
</div>
|
|
164
|
+
<input
|
|
165
|
+
type="checkbox"
|
|
166
|
+
checked={debug}
|
|
167
|
+
onChange={function (e) { setDebug(e.target.checked); save.markDirty(); }}
|
|
168
|
+
className="toggle toggle-sm toggle-primary"
|
|
169
|
+
aria-label="Enable debug mode"
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div className="px-5 py-3 border-t border-base-content/15">
|
|
175
|
+
<SaveFooter dirty={save.dirty} saving={save.saving} saveState={save.saveState} onSave={handleSave} />
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Settings, Plug, Sparkles, Terminal } from "lucide-react";
|
|
3
|
+
import { PopupMenu, PopupMenuItem } from "../ui/PopupMenu";
|
|
4
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
5
|
+
|
|
6
|
+
interface ProjectDropdownProps {
|
|
7
|
+
anchorRef: React.RefObject<HTMLElement | null>;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ProjectDropdown(props: ProjectDropdownProps) {
|
|
12
|
+
var sidebar = useSidebar();
|
|
13
|
+
|
|
14
|
+
var items: PopupMenuItem[] = [
|
|
15
|
+
{ id: "project-settings", label: "Project Settings", icon: <Settings size={14} /> },
|
|
16
|
+
{ id: "mcp", label: "MCP Servers", icon: <Plug size={14} /> },
|
|
17
|
+
{ id: "skills", label: "Skills", icon: <Sparkles size={14} /> },
|
|
18
|
+
{ id: "environment", label: "Environment", icon: <Terminal size={14} /> },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function handleSelect(id: string) {
|
|
22
|
+
if (id === "project-settings") {
|
|
23
|
+
sidebar.openProjectSettings("general");
|
|
24
|
+
} else if (id === "mcp") {
|
|
25
|
+
sidebar.openProjectSettings("mcp");
|
|
26
|
+
} else if (id === "skills") {
|
|
27
|
+
sidebar.openProjectSettings("skills");
|
|
28
|
+
} else if (id === "environment") {
|
|
29
|
+
sidebar.openProjectSettings("environment");
|
|
30
|
+
}
|
|
31
|
+
props.onClose();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<PopupMenu
|
|
36
|
+
items={items}
|
|
37
|
+
onSelect={handleSelect}
|
|
38
|
+
onClose={props.onClose}
|
|
39
|
+
anchorRef={props.anchorRef}
|
|
40
|
+
position="below"
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { Plus } from "lucide-react";
|
|
3
|
+
import type { ProjectInfo, NodeInfo } from "@lattice/shared";
|
|
4
|
+
import { LatticeLogomark } from "../ui/LatticeLogomark";
|
|
5
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
6
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
7
|
+
|
|
8
|
+
function getProjectInitials(title: string): string {
|
|
9
|
+
var words = title.trim().split(/[\s\-_]+/);
|
|
10
|
+
if (words.length >= 3) {
|
|
11
|
+
return (words[0][0] + words[1][0] + words[2][0]).toUpperCase();
|
|
12
|
+
}
|
|
13
|
+
if (words.length === 2) {
|
|
14
|
+
return (words[0][0] + words[1][0]).toUpperCase();
|
|
15
|
+
}
|
|
16
|
+
return title.slice(0, 3).toUpperCase();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ProjectGroup {
|
|
20
|
+
slug: string;
|
|
21
|
+
title: string;
|
|
22
|
+
nodes: Array<{ nodeId: string; nodeName: string; online: boolean; path: string }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function groupProjectsBySlug(projects: ProjectInfo[], nodes: NodeInfo[]): ProjectGroup[] {
|
|
26
|
+
var groups = new Map<string, ProjectGroup>();
|
|
27
|
+
for (var i = 0; i < projects.length; i++) {
|
|
28
|
+
var p = projects[i];
|
|
29
|
+
var existing = groups.get(p.slug);
|
|
30
|
+
var node = nodes.find(function (n) { return n.id === p.nodeId; });
|
|
31
|
+
var nodeEntry = {
|
|
32
|
+
nodeId: p.nodeId,
|
|
33
|
+
nodeName: p.nodeName,
|
|
34
|
+
online: node ? node.online : false,
|
|
35
|
+
path: p.path,
|
|
36
|
+
};
|
|
37
|
+
if (existing) {
|
|
38
|
+
existing.nodes.push(nodeEntry);
|
|
39
|
+
} else {
|
|
40
|
+
groups.set(p.slug, { slug: p.slug, title: p.title, nodes: [nodeEntry] });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return Array.from(groups.values());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ContextMenuState {
|
|
47
|
+
visible: boolean;
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
slug: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ProjectButtonProps {
|
|
54
|
+
group: ProjectGroup;
|
|
55
|
+
isActive: boolean;
|
|
56
|
+
onClick: () => void;
|
|
57
|
+
onContextMenu: (e: React.MouseEvent, slug: string) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ProjectButton(props: ProjectButtonProps) {
|
|
61
|
+
var [hovered, setHovered] = useState(false);
|
|
62
|
+
var [tooltipTop, setTooltipTop] = useState(0);
|
|
63
|
+
var initials = getProjectInitials(props.group.title);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="relative flex items-center">
|
|
67
|
+
{props.isActive && (
|
|
68
|
+
<div
|
|
69
|
+
className="absolute -left-3 w-[3px] bg-base-content rounded-r-full pointer-events-none"
|
|
70
|
+
style={{ height: "32px", top: "50%", transform: "translateY(-50%)" }}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
<button
|
|
75
|
+
onClick={props.onClick}
|
|
76
|
+
onContextMenu={function (e) {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
props.onContextMenu(e, props.group.slug);
|
|
79
|
+
}}
|
|
80
|
+
onMouseEnter={function (e) {
|
|
81
|
+
var rect = e.currentTarget.getBoundingClientRect();
|
|
82
|
+
setTooltipTop(rect.top + rect.height / 2);
|
|
83
|
+
setHovered(true);
|
|
84
|
+
}}
|
|
85
|
+
onMouseLeave={function () { setHovered(false); }}
|
|
86
|
+
className={
|
|
87
|
+
"w-[42px] h-[42px] flex items-center justify-center text-[11px] font-bold tracking-[0.03em] cursor-pointer transition-all duration-[120ms] flex-shrink-0 " +
|
|
88
|
+
(props.isActive
|
|
89
|
+
? "rounded-xl bg-primary text-primary-content"
|
|
90
|
+
: hovered
|
|
91
|
+
? "rounded-xl bg-base-200 text-base-content/60"
|
|
92
|
+
: "rounded-full bg-base-200 text-base-content/60")
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
{initials}
|
|
96
|
+
</button>
|
|
97
|
+
|
|
98
|
+
<div className="absolute bottom-0 right-0 flex gap-[2px] pointer-events-none">
|
|
99
|
+
{props.group.nodes.map(function (n) {
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
key={n.nodeId}
|
|
103
|
+
className={
|
|
104
|
+
"w-[11px] h-[11px] rounded-full border-[1.5px] border-base-100 " +
|
|
105
|
+
(n.online ? "bg-success" : "bg-error")
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
})}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{hovered && (
|
|
113
|
+
<div
|
|
114
|
+
className="pointer-events-none z-[9000] bg-base-300 border border-base-content/20 rounded px-2 py-1 text-xs text-base-content whitespace-nowrap shadow-xl"
|
|
115
|
+
style={{
|
|
116
|
+
position: "fixed",
|
|
117
|
+
left: "calc(64px + 8px)",
|
|
118
|
+
top: tooltipTop + "px",
|
|
119
|
+
transform: "translateY(-50%)",
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
{props.group.title}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface ProjectRailProps {
|
|
130
|
+
projects: ProjectInfo[];
|
|
131
|
+
nodes: NodeInfo[];
|
|
132
|
+
activeProjectSlug: string | null;
|
|
133
|
+
onSelectProject: (slug: string) => void;
|
|
134
|
+
onDashboardClick: () => void;
|
|
135
|
+
isDashboardActive: boolean;
|
|
136
|
+
dimmed?: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function ProjectRail(props: ProjectRailProps) {
|
|
140
|
+
var ws = useWebSocket();
|
|
141
|
+
var sidebar = useSidebar();
|
|
142
|
+
var groups = groupProjectsBySlug(props.projects, props.nodes);
|
|
143
|
+
var [contextMenu, setContextMenu] = useState<ContextMenuState>({
|
|
144
|
+
visible: false,
|
|
145
|
+
x: 0,
|
|
146
|
+
y: 0,
|
|
147
|
+
slug: null,
|
|
148
|
+
});
|
|
149
|
+
var menuRef = useRef<HTMLDivElement>(null);
|
|
150
|
+
|
|
151
|
+
useEffect(
|
|
152
|
+
function () {
|
|
153
|
+
if (!contextMenu.visible) return;
|
|
154
|
+
|
|
155
|
+
function handleClick() {
|
|
156
|
+
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
160
|
+
if (e.key === "Escape") {
|
|
161
|
+
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function handleScroll() {
|
|
166
|
+
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
window.addEventListener("click", handleClick);
|
|
170
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
171
|
+
window.addEventListener("scroll", handleScroll, true);
|
|
172
|
+
|
|
173
|
+
return function () {
|
|
174
|
+
window.removeEventListener("click", handleClick);
|
|
175
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
176
|
+
window.removeEventListener("scroll", handleScroll, true);
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
[contextMenu.visible]
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
function handleContextMenu(e: React.MouseEvent, slug: string) {
|
|
183
|
+
setContextMenu({ visible: true, x: e.clientX, y: e.clientY, slug: slug });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div
|
|
188
|
+
className={
|
|
189
|
+
"w-16 flex-shrink-0 flex flex-col items-center pt-3 pb-16 gap-2 bg-base-100 border-r border-base-300 overflow-y-auto overflow-x-hidden scrollbar-hidden " +
|
|
190
|
+
(props.dimmed ? "opacity-60" : "")
|
|
191
|
+
}
|
|
192
|
+
>
|
|
193
|
+
<div className="relative flex items-center">
|
|
194
|
+
{props.isDashboardActive && (
|
|
195
|
+
<div
|
|
196
|
+
className="absolute -left-3 w-[3px] bg-base-content rounded-r-full pointer-events-none"
|
|
197
|
+
style={{ height: "32px", top: "50%", transform: "translateY(-50%)" }}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
<button
|
|
201
|
+
onClick={props.onDashboardClick}
|
|
202
|
+
className={
|
|
203
|
+
"w-[42px] h-[42px] flex items-center justify-center cursor-pointer transition-all duration-[120ms] flex-shrink-0 " +
|
|
204
|
+
(props.isDashboardActive
|
|
205
|
+
? "rounded-xl bg-primary text-primary-content"
|
|
206
|
+
: "rounded-full bg-base-200 text-base-content/60 hover:rounded-xl hover:bg-primary/20 hover:text-primary")
|
|
207
|
+
}
|
|
208
|
+
title="Lattice Dashboard"
|
|
209
|
+
>
|
|
210
|
+
<LatticeLogomark size={22} />
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
|
|
215
|
+
|
|
216
|
+
{groups.map(function (group) {
|
|
217
|
+
return (
|
|
218
|
+
<ProjectButton
|
|
219
|
+
key={group.slug}
|
|
220
|
+
group={group}
|
|
221
|
+
isActive={props.activeProjectSlug === group.slug}
|
|
222
|
+
onClick={function () { props.onSelectProject(group.slug); }}
|
|
223
|
+
onContextMenu={handleContextMenu}
|
|
224
|
+
/>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
227
|
+
|
|
228
|
+
{groups.length > 0 && (
|
|
229
|
+
<div className="w-6 h-px bg-base-300 my-0.5 flex-shrink-0" />
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
<button
|
|
233
|
+
onClick={function () {}}
|
|
234
|
+
className="w-[42px] h-[42px] flex items-center justify-center rounded-full border-2 border-dashed border-base-content/25 text-base-content/20 transition-colors duration-[120ms] flex-shrink-0 cursor-not-allowed opacity-60"
|
|
235
|
+
title="Add project (coming soon)"
|
|
236
|
+
disabled
|
|
237
|
+
>
|
|
238
|
+
<Plus size={18} />
|
|
239
|
+
</button>
|
|
240
|
+
|
|
241
|
+
<div className="flex-1" />
|
|
242
|
+
|
|
243
|
+
{contextMenu.visible && (
|
|
244
|
+
<div
|
|
245
|
+
ref={menuRef}
|
|
246
|
+
role="menu"
|
|
247
|
+
aria-label="Project actions"
|
|
248
|
+
onClick={function (e) { e.stopPropagation(); }}
|
|
249
|
+
className="fixed z-[9999] bg-base-300 border border-base-content/20 rounded-lg shadow-2xl py-1 min-w-[160px]"
|
|
250
|
+
style={{ left: contextMenu.x + "px", top: contextMenu.y + "px" }}
|
|
251
|
+
>
|
|
252
|
+
<button
|
|
253
|
+
role="menuitem"
|
|
254
|
+
className="w-full text-left px-3 py-1.5 text-sm text-base-content hover:bg-base-content/10 transition-colors"
|
|
255
|
+
onClick={function () {
|
|
256
|
+
if (contextMenu.slug) {
|
|
257
|
+
sidebar.setActiveProjectSlug(contextMenu.slug);
|
|
258
|
+
sidebar.openProjectSettings("general");
|
|
259
|
+
}
|
|
260
|
+
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
Project Settings
|
|
264
|
+
</button>
|
|
265
|
+
<button
|
|
266
|
+
role="menuitem"
|
|
267
|
+
className="w-full text-left px-3 py-1.5 text-sm text-base-content hover:bg-base-content/10 transition-colors"
|
|
268
|
+
onClick={function () {
|
|
269
|
+
if (contextMenu.slug) {
|
|
270
|
+
ws.send({ type: "session:create", projectSlug: contextMenu.slug });
|
|
271
|
+
}
|
|
272
|
+
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
New Session
|
|
276
|
+
</button>
|
|
277
|
+
<div className="my-1 h-px bg-base-content/10" />
|
|
278
|
+
<button
|
|
279
|
+
role="menuitem"
|
|
280
|
+
className="w-full text-left px-3 py-1.5 text-sm text-error hover:bg-error/10 transition-colors"
|
|
281
|
+
onClick={function () {
|
|
282
|
+
setContextMenu(function (prev) { return { ...prev, visible: false }; });
|
|
283
|
+
}}
|
|
284
|
+
>
|
|
285
|
+
Remove Project
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useRef, useEffect } from "react";
|
|
2
|
+
import { Search, X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface SearchFilterProps {
|
|
5
|
+
value: string;
|
|
6
|
+
onChange: (value: string) => void;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SearchFilter(props: SearchFilterProps) {
|
|
12
|
+
var inputRef = useRef<HTMLInputElement>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(function () {
|
|
15
|
+
if (inputRef.current) {
|
|
16
|
+
inputRef.current.focus();
|
|
17
|
+
}
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
21
|
+
if (e.key === "Escape") {
|
|
22
|
+
props.onClose();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="px-2 pb-1.5 flex-shrink-0">
|
|
28
|
+
<div className="flex items-center gap-1.5 bg-base-300 border border-base-content/15 rounded-md px-2 h-7 focus-within:border-primary transition-colors duration-[120ms]">
|
|
29
|
+
<Search size={12} className="text-base-content/30 flex-shrink-0" />
|
|
30
|
+
<input
|
|
31
|
+
ref={inputRef}
|
|
32
|
+
type="text"
|
|
33
|
+
value={props.value}
|
|
34
|
+
onChange={function (e) { props.onChange(e.target.value); }}
|
|
35
|
+
onKeyDown={handleKeyDown}
|
|
36
|
+
placeholder={props.placeholder || "Search..."}
|
|
37
|
+
className="flex-1 bg-transparent text-base-content text-[13px] outline-none min-w-0"
|
|
38
|
+
spellCheck={false}
|
|
39
|
+
/>
|
|
40
|
+
{props.value.length > 0 && (
|
|
41
|
+
<button
|
|
42
|
+
onClick={function () { props.onChange(""); }}
|
|
43
|
+
aria-label="Clear search"
|
|
44
|
+
className="text-base-content/30 hover:text-base-content flex-shrink-0"
|
|
45
|
+
>
|
|
46
|
+
<X size={11} />
|
|
47
|
+
</button>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|