@cryptiklemur/lattice 0.0.0 → 1.2.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/.github/workflows/release.yml +4 -4
- package/.releaserc.json +2 -1
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -70
- package/client/src/components/mesh/NodeBadge.tsx +24 -24
- package/client/src/components/mesh/PairingDialog.tsx +281 -281
- package/client/src/components/panels/FileBrowser.tsx +241 -241
- package/client/src/components/panels/StickyNotes.tsx +187 -187
- package/client/src/components/project-settings/ProjectMemory.tsx +471 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
- package/client/src/components/settings/Appearance.tsx +151 -151
- package/client/src/components/settings/MeshStatus.tsx +145 -145
- package/client/src/components/settings/SettingsView.tsx +57 -57
- package/client/src/components/setup/SetupWizard.tsx +750 -750
- package/client/src/components/sidebar/AddProjectModal.tsx +432 -0
- package/client/src/components/sidebar/ProjectRail.tsx +8 -4
- package/client/src/components/sidebar/SettingsSidebar.tsx +2 -1
- package/client/src/components/ui/ErrorBoundary.tsx +56 -56
- package/client/src/hooks/useSidebar.ts +16 -0
- package/client/src/router.tsx +453 -391
- package/client/src/stores/sidebar.ts +28 -0
- package/client/vite.config.ts +20 -20
- package/package.json +1 -1
- package/server/src/daemon.ts +1 -0
- package/server/src/handlers/chat.ts +194 -194
- package/server/src/handlers/fs.ts +159 -0
- package/server/src/handlers/memory.ts +179 -0
- package/server/src/handlers/settings.ts +114 -109
- package/shared/src/messages.ts +97 -2
- package/shared/src/project-settings.ts +1 -1
- package/themes/amoled.json +20 -20
- package/themes/ayu-light.json +9 -9
- package/themes/catppuccin-latte.json +9 -9
- package/themes/catppuccin-mocha.json +9 -9
- package/themes/clay-light.json +10 -10
- package/themes/clay.json +10 -10
- package/themes/dracula.json +9 -9
- package/themes/everforest-light.json +9 -9
- package/themes/everforest.json +9 -9
- package/themes/github-light.json +9 -9
- package/themes/gruvbox-dark.json +9 -9
- package/themes/gruvbox-light.json +9 -9
- package/themes/monokai.json +9 -9
- package/themes/nord-light.json +9 -9
- package/themes/nord.json +9 -9
- package/themes/one-dark.json +9 -9
- package/themes/one-light.json +9 -9
- package/themes/rose-pine-dawn.json +9 -9
- package/themes/rose-pine.json +9 -9
- package/themes/solarized-dark.json +9 -9
- package/themes/solarized-light.json +9 -9
- package/themes/tokyo-night-light.json +9 -9
- package/themes/tokyo-night.json +9 -9
- package/.serena/project.yml +0 -138
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
import { memo, useMemo, useCallback } from "react";
|
|
2
|
-
import { useTheme } from "../../hooks/useTheme";
|
|
3
|
-
import { Sun, Moon, Check } from "lucide-react";
|
|
4
|
-
import type { ThemeEntry } from "../../themes/index";
|
|
5
|
-
|
|
6
|
-
var SWATCH_KEYS = [
|
|
7
|
-
"base00", "base01", "base02", "base03",
|
|
8
|
-
"base04", "base05", "base06", "base07",
|
|
9
|
-
"base08", "base09", "base0A", "base0B",
|
|
10
|
-
"base0C", "base0D", "base0E", "base0F",
|
|
11
|
-
] as const;
|
|
12
|
-
|
|
13
|
-
var ThemeCard = memo(function ThemeCard({
|
|
14
|
-
entry,
|
|
15
|
-
active,
|
|
16
|
-
onSelect,
|
|
17
|
-
}: {
|
|
18
|
-
entry: ThemeEntry;
|
|
19
|
-
active: boolean;
|
|
20
|
-
onSelect: (id: string) => void;
|
|
21
|
-
}) {
|
|
22
|
-
var t = entry.theme;
|
|
23
|
-
|
|
24
|
-
function handleClick() {
|
|
25
|
-
onSelect(entry.id);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<button
|
|
30
|
-
onClick={handleClick}
|
|
31
|
-
className={
|
|
32
|
-
"flex flex-col gap-2 p-3 sm:p-2.5 px-3 rounded-lg border cursor-pointer text-left transition-colors duration-[120ms] relative focus-visible:ring-2 focus-visible:ring-primary " +
|
|
33
|
-
(active
|
|
34
|
-
? "border-primary bg-base-300 shadow-sm"
|
|
35
|
-
: "border-base-content/15 bg-base-300 hover:border-base-content/30")
|
|
36
|
-
}
|
|
37
|
-
>
|
|
38
|
-
{active && (
|
|
39
|
-
<div className="absolute top-1.5 right-1.5 w-3.5 h-3.5 rounded-full bg-primary flex items-center justify-center">
|
|
40
|
-
<Check size={8} className="text-primary-content" strokeWidth={1.8} />
|
|
41
|
-
</div>
|
|
42
|
-
)}
|
|
43
|
-
|
|
44
|
-
<div className="flex gap-[3px] flex-wrap w-[80px]">
|
|
45
|
-
{SWATCH_KEYS.map(function (key) {
|
|
46
|
-
return (
|
|
47
|
-
<div
|
|
48
|
-
key={key}
|
|
49
|
-
className="w-[10px] h-[10px] rounded-sm flex-shrink-0 ring-1 ring-base-content/10"
|
|
50
|
-
style={{ background: "#" + t[key] }}
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
})}
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
<div className="text-[12px] font-medium text-base-content">
|
|
57
|
-
{t.name}
|
|
58
|
-
</div>
|
|
59
|
-
</button>
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
function ThemeGroup({
|
|
64
|
-
label,
|
|
65
|
-
entries,
|
|
66
|
-
currentThemeId,
|
|
67
|
-
onSelect,
|
|
68
|
-
}: {
|
|
69
|
-
label: string;
|
|
70
|
-
entries: ThemeEntry[];
|
|
71
|
-
currentThemeId: string;
|
|
72
|
-
onSelect: (id: string) => void;
|
|
73
|
-
}) {
|
|
74
|
-
if (entries.length === 0) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div className="mb-6">
|
|
80
|
-
<div className="text-[11px] font-mono font-bold tracking-[0.1em] uppercase text-base-content/40 mb-3">
|
|
81
|
-
{label}
|
|
82
|
-
</div>
|
|
83
|
-
<div className="grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-2">
|
|
84
|
-
{entries.map(function (entry) {
|
|
85
|
-
return (
|
|
86
|
-
<ThemeCard
|
|
87
|
-
key={entry.id}
|
|
88
|
-
entry={entry}
|
|
89
|
-
active={entry.id === currentThemeId}
|
|
90
|
-
onSelect={onSelect}
|
|
91
|
-
/>
|
|
92
|
-
);
|
|
93
|
-
})}
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function Appearance() {
|
|
100
|
-
var { mode, currentThemeId, toggleMode, setTheme, themes } = useTheme();
|
|
101
|
-
|
|
102
|
-
var darkThemes = useMemo(function () {
|
|
103
|
-
return themes.filter(function (e) { return e.theme.variant === "dark"; });
|
|
104
|
-
}, [themes]);
|
|
105
|
-
|
|
106
|
-
var lightThemes = useMemo(function () {
|
|
107
|
-
return themes.filter(function (e) { return e.theme.variant === "light"; });
|
|
108
|
-
}, [themes]);
|
|
109
|
-
|
|
110
|
-
var handleThemeSelect = useCallback(function (id: string) {
|
|
111
|
-
setTheme(id);
|
|
112
|
-
}, [setTheme]);
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<div className="py-2">
|
|
116
|
-
<div className="flex items-center justify-between mb-6">
|
|
117
|
-
<div className="text-[12px] font-semibold text-base-content/40">Color Mode</div>
|
|
118
|
-
<button
|
|
119
|
-
onClick={toggleMode}
|
|
120
|
-
className="btn btn-ghost btn-sm border border-base-content/20"
|
|
121
|
-
>
|
|
122
|
-
{mode === "dark" ? (
|
|
123
|
-
<>
|
|
124
|
-
<Sun size={12} />
|
|
125
|
-
Switch to Light
|
|
126
|
-
</>
|
|
127
|
-
) : (
|
|
128
|
-
<>
|
|
129
|
-
<Moon size={12} />
|
|
130
|
-
Switch to Dark
|
|
131
|
-
</>
|
|
132
|
-
)}
|
|
133
|
-
</button>
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
<ThemeGroup
|
|
137
|
-
label="Dark Themes"
|
|
138
|
-
entries={darkThemes}
|
|
139
|
-
currentThemeId={currentThemeId}
|
|
140
|
-
onSelect={handleThemeSelect}
|
|
141
|
-
/>
|
|
142
|
-
|
|
143
|
-
<ThemeGroup
|
|
144
|
-
label="Light Themes"
|
|
145
|
-
entries={lightThemes}
|
|
146
|
-
currentThemeId={currentThemeId}
|
|
147
|
-
onSelect={handleThemeSelect}
|
|
148
|
-
/>
|
|
149
|
-
</div>
|
|
150
|
-
);
|
|
151
|
-
}
|
|
1
|
+
import { memo, useMemo, useCallback } from "react";
|
|
2
|
+
import { useTheme } from "../../hooks/useTheme";
|
|
3
|
+
import { Sun, Moon, Check } from "lucide-react";
|
|
4
|
+
import type { ThemeEntry } from "../../themes/index";
|
|
5
|
+
|
|
6
|
+
var SWATCH_KEYS = [
|
|
7
|
+
"base00", "base01", "base02", "base03",
|
|
8
|
+
"base04", "base05", "base06", "base07",
|
|
9
|
+
"base08", "base09", "base0A", "base0B",
|
|
10
|
+
"base0C", "base0D", "base0E", "base0F",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
var ThemeCard = memo(function ThemeCard({
|
|
14
|
+
entry,
|
|
15
|
+
active,
|
|
16
|
+
onSelect,
|
|
17
|
+
}: {
|
|
18
|
+
entry: ThemeEntry;
|
|
19
|
+
active: boolean;
|
|
20
|
+
onSelect: (id: string) => void;
|
|
21
|
+
}) {
|
|
22
|
+
var t = entry.theme;
|
|
23
|
+
|
|
24
|
+
function handleClick() {
|
|
25
|
+
onSelect(entry.id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<button
|
|
30
|
+
onClick={handleClick}
|
|
31
|
+
className={
|
|
32
|
+
"flex flex-col gap-2 p-3 sm:p-2.5 px-3 rounded-lg border cursor-pointer text-left transition-colors duration-[120ms] relative focus-visible:ring-2 focus-visible:ring-primary " +
|
|
33
|
+
(active
|
|
34
|
+
? "border-primary bg-base-300 shadow-sm"
|
|
35
|
+
: "border-base-content/15 bg-base-300 hover:border-base-content/30")
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
{active && (
|
|
39
|
+
<div className="absolute top-1.5 right-1.5 w-3.5 h-3.5 rounded-full bg-primary flex items-center justify-center">
|
|
40
|
+
<Check size={8} className="text-primary-content" strokeWidth={1.8} />
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
|
|
44
|
+
<div className="flex gap-[3px] flex-wrap w-[80px]">
|
|
45
|
+
{SWATCH_KEYS.map(function (key) {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
key={key}
|
|
49
|
+
className="w-[10px] h-[10px] rounded-sm flex-shrink-0 ring-1 ring-base-content/10"
|
|
50
|
+
style={{ background: "#" + t[key] }}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="text-[12px] font-medium text-base-content">
|
|
57
|
+
{t.name}
|
|
58
|
+
</div>
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function ThemeGroup({
|
|
64
|
+
label,
|
|
65
|
+
entries,
|
|
66
|
+
currentThemeId,
|
|
67
|
+
onSelect,
|
|
68
|
+
}: {
|
|
69
|
+
label: string;
|
|
70
|
+
entries: ThemeEntry[];
|
|
71
|
+
currentThemeId: string;
|
|
72
|
+
onSelect: (id: string) => void;
|
|
73
|
+
}) {
|
|
74
|
+
if (entries.length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="mb-6">
|
|
80
|
+
<div className="text-[11px] font-mono font-bold tracking-[0.1em] uppercase text-base-content/40 mb-3">
|
|
81
|
+
{label}
|
|
82
|
+
</div>
|
|
83
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-2">
|
|
84
|
+
{entries.map(function (entry) {
|
|
85
|
+
return (
|
|
86
|
+
<ThemeCard
|
|
87
|
+
key={entry.id}
|
|
88
|
+
entry={entry}
|
|
89
|
+
active={entry.id === currentThemeId}
|
|
90
|
+
onSelect={onSelect}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function Appearance() {
|
|
100
|
+
var { mode, currentThemeId, toggleMode, setTheme, themes } = useTheme();
|
|
101
|
+
|
|
102
|
+
var darkThemes = useMemo(function () {
|
|
103
|
+
return themes.filter(function (e) { return e.theme.variant === "dark"; });
|
|
104
|
+
}, [themes]);
|
|
105
|
+
|
|
106
|
+
var lightThemes = useMemo(function () {
|
|
107
|
+
return themes.filter(function (e) { return e.theme.variant === "light"; });
|
|
108
|
+
}, [themes]);
|
|
109
|
+
|
|
110
|
+
var handleThemeSelect = useCallback(function (id: string) {
|
|
111
|
+
setTheme(id);
|
|
112
|
+
}, [setTheme]);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="py-2">
|
|
116
|
+
<div className="flex items-center justify-between mb-6">
|
|
117
|
+
<div className="text-[12px] font-semibold text-base-content/40">Color Mode</div>
|
|
118
|
+
<button
|
|
119
|
+
onClick={toggleMode}
|
|
120
|
+
className="btn btn-ghost btn-sm border border-base-content/20"
|
|
121
|
+
>
|
|
122
|
+
{mode === "dark" ? (
|
|
123
|
+
<>
|
|
124
|
+
<Sun size={12} />
|
|
125
|
+
Switch to Light
|
|
126
|
+
</>
|
|
127
|
+
) : (
|
|
128
|
+
<>
|
|
129
|
+
<Moon size={12} />
|
|
130
|
+
Switch to Dark
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<ThemeGroup
|
|
137
|
+
label="Dark Themes"
|
|
138
|
+
entries={darkThemes}
|
|
139
|
+
currentThemeId={currentThemeId}
|
|
140
|
+
onSelect={handleThemeSelect}
|
|
141
|
+
/>
|
|
142
|
+
|
|
143
|
+
<ThemeGroup
|
|
144
|
+
label="Light Themes"
|
|
145
|
+
entries={lightThemes}
|
|
146
|
+
currentThemeId={currentThemeId}
|
|
147
|
+
onSelect={handleThemeSelect}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { Plus, CircleDot, Circle } from "lucide-react";
|
|
3
|
-
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
-
import { useMesh } from "../../hooks/useMesh";
|
|
5
|
-
import { PairingDialog } from "../mesh/PairingDialog";
|
|
6
|
-
import type { NodeInfo } from "@lattice/shared";
|
|
7
|
-
|
|
8
|
-
interface NodeRowProps {
|
|
9
|
-
node: NodeInfo;
|
|
10
|
-
onUnpair: (nodeId: string) => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function NodeRow(props: NodeRowProps) {
|
|
14
|
-
var [confirming, setConfirming] = useState(false);
|
|
15
|
-
|
|
16
|
-
function handleUnpair() {
|
|
17
|
-
if (!confirming) {
|
|
18
|
-
setConfirming(true);
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
props.onUnpair(props.node.id);
|
|
22
|
-
setConfirming(false);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div className="flex items-center gap-3 p-3 sm:p-2.5 px-3.5 rounded-lg border border-base-content/15 bg-base-300 mb-2">
|
|
27
|
-
{props.node.online ? (
|
|
28
|
-
<CircleDot size={10} className="text-success flex-shrink-0" aria-label="Online" />
|
|
29
|
-
) : (
|
|
30
|
-
<Circle size={10} className="text-base-content/30 flex-shrink-0" aria-label="Offline" />
|
|
31
|
-
)}
|
|
32
|
-
|
|
33
|
-
<div className="flex-1 min-w-0">
|
|
34
|
-
<div className="text-[13px] font-medium text-base-content truncate">
|
|
35
|
-
{props.node.name}
|
|
36
|
-
{props.node.isLocal && (
|
|
37
|
-
<span className="ml-1.5 text-[10px] font-semibold text-primary uppercase tracking-[0.06em]">
|
|
38
|
-
local
|
|
39
|
-
</span>
|
|
40
|
-
)}
|
|
41
|
-
</div>
|
|
42
|
-
<div className="text-[11px] text-base-content/40 truncate">
|
|
43
|
-
{props.node.address}:{props.node.port}
|
|
44
|
-
{!props.node.online && (
|
|
45
|
-
<span className="ml-2 text-base-content/30 italic">offline</span>
|
|
46
|
-
)}
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
{!props.node.isLocal && (
|
|
51
|
-
<div className="flex gap-1.5 flex-shrink-0">
|
|
52
|
-
{confirming ? (
|
|
53
|
-
<>
|
|
54
|
-
<button
|
|
55
|
-
onClick={handleUnpair}
|
|
56
|
-
className="btn btn-error btn-xs"
|
|
57
|
-
>
|
|
58
|
-
Confirm
|
|
59
|
-
</button>
|
|
60
|
-
<button
|
|
61
|
-
onClick={function () { setConfirming(false); }}
|
|
62
|
-
className="btn btn-ghost btn-xs"
|
|
63
|
-
>
|
|
64
|
-
Cancel
|
|
65
|
-
</button>
|
|
66
|
-
</>
|
|
67
|
-
) : (
|
|
68
|
-
<button
|
|
69
|
-
onClick={handleUnpair}
|
|
70
|
-
className="btn btn-ghost btn-xs border border-base-content/20 hover:btn-error hover:border-error"
|
|
71
|
-
>
|
|
72
|
-
Unpair
|
|
73
|
-
</button>
|
|
74
|
-
)}
|
|
75
|
-
</div>
|
|
76
|
-
)}
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function MeshStatus() {
|
|
82
|
-
var ws = useWebSocket();
|
|
83
|
-
var { nodes } = useMesh();
|
|
84
|
-
var [pairingOpen, setPairingOpen] = useState(false);
|
|
85
|
-
|
|
86
|
-
function handleUnpair(nodeId: string) {
|
|
87
|
-
ws.send({ type: "mesh:unpair", nodeId });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
var localNode = nodes.find(function (n) { return n.isLocal; });
|
|
91
|
-
var remoteNodes = nodes.filter(function (n) { return !n.isLocal; });
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<div className="py-2">
|
|
95
|
-
<div className="mb-5">
|
|
96
|
-
<div className="text-[12px] font-semibold text-base-content/40 mb-2 tracking-[0.06em]">
|
|
97
|
-
This Node
|
|
98
|
-
</div>
|
|
99
|
-
{localNode ? (
|
|
100
|
-
<NodeRow node={localNode} onUnpair={handleUnpair} />
|
|
101
|
-
) : (
|
|
102
|
-
<div className="text-[12px] text-base-content/40 italic">
|
|
103
|
-
Waiting for node info...
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<div className="mb-5">
|
|
109
|
-
<div className="flex items-center justify-between mb-2">
|
|
110
|
-
<div className="text-[12px] font-semibold text-base-content/40 tracking-[0.06em]">
|
|
111
|
-
Paired Nodes
|
|
112
|
-
</div>
|
|
113
|
-
<button
|
|
114
|
-
onClick={function () { setPairingOpen(true); }}
|
|
115
|
-
className="btn btn-primary btn-sm sm:btn-xs gap-1"
|
|
116
|
-
>
|
|
117
|
-
<Plus size={10} />
|
|
118
|
-
Pair New Node
|
|
119
|
-
</button>
|
|
120
|
-
</div>
|
|
121
|
-
|
|
122
|
-
{remoteNodes.length === 0 ? (
|
|
123
|
-
<div className="p-4 rounded-lg border border-dashed border-base-content/15 text-center text-[12px] text-base-content/40 italic">
|
|
124
|
-
No paired nodes yet.
|
|
125
|
-
</div>
|
|
126
|
-
) : (
|
|
127
|
-
remoteNodes.map(function (node) {
|
|
128
|
-
return (
|
|
129
|
-
<NodeRow
|
|
130
|
-
key={node.id}
|
|
131
|
-
node={node}
|
|
132
|
-
onUnpair={handleUnpair}
|
|
133
|
-
/>
|
|
134
|
-
);
|
|
135
|
-
})
|
|
136
|
-
)}
|
|
137
|
-
</div>
|
|
138
|
-
|
|
139
|
-
<PairingDialog
|
|
140
|
-
isOpen={pairingOpen}
|
|
141
|
-
onClose={function () { setPairingOpen(false); }}
|
|
142
|
-
/>
|
|
143
|
-
</div>
|
|
144
|
-
);
|
|
145
|
-
}
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Plus, CircleDot, Circle } from "lucide-react";
|
|
3
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
+
import { useMesh } from "../../hooks/useMesh";
|
|
5
|
+
import { PairingDialog } from "../mesh/PairingDialog";
|
|
6
|
+
import type { NodeInfo } from "@lattice/shared";
|
|
7
|
+
|
|
8
|
+
interface NodeRowProps {
|
|
9
|
+
node: NodeInfo;
|
|
10
|
+
onUnpair: (nodeId: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function NodeRow(props: NodeRowProps) {
|
|
14
|
+
var [confirming, setConfirming] = useState(false);
|
|
15
|
+
|
|
16
|
+
function handleUnpair() {
|
|
17
|
+
if (!confirming) {
|
|
18
|
+
setConfirming(true);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
props.onUnpair(props.node.id);
|
|
22
|
+
setConfirming(false);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex items-center gap-3 p-3 sm:p-2.5 px-3.5 rounded-lg border border-base-content/15 bg-base-300 mb-2">
|
|
27
|
+
{props.node.online ? (
|
|
28
|
+
<CircleDot size={10} className="text-success flex-shrink-0" aria-label="Online" />
|
|
29
|
+
) : (
|
|
30
|
+
<Circle size={10} className="text-base-content/30 flex-shrink-0" aria-label="Offline" />
|
|
31
|
+
)}
|
|
32
|
+
|
|
33
|
+
<div className="flex-1 min-w-0">
|
|
34
|
+
<div className="text-[13px] font-medium text-base-content truncate">
|
|
35
|
+
{props.node.name}
|
|
36
|
+
{props.node.isLocal && (
|
|
37
|
+
<span className="ml-1.5 text-[10px] font-semibold text-primary uppercase tracking-[0.06em]">
|
|
38
|
+
local
|
|
39
|
+
</span>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
<div className="text-[11px] text-base-content/40 truncate">
|
|
43
|
+
{props.node.address}:{props.node.port}
|
|
44
|
+
{!props.node.online && (
|
|
45
|
+
<span className="ml-2 text-base-content/30 italic">offline</span>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{!props.node.isLocal && (
|
|
51
|
+
<div className="flex gap-1.5 flex-shrink-0">
|
|
52
|
+
{confirming ? (
|
|
53
|
+
<>
|
|
54
|
+
<button
|
|
55
|
+
onClick={handleUnpair}
|
|
56
|
+
className="btn btn-error btn-xs"
|
|
57
|
+
>
|
|
58
|
+
Confirm
|
|
59
|
+
</button>
|
|
60
|
+
<button
|
|
61
|
+
onClick={function () { setConfirming(false); }}
|
|
62
|
+
className="btn btn-ghost btn-xs"
|
|
63
|
+
>
|
|
64
|
+
Cancel
|
|
65
|
+
</button>
|
|
66
|
+
</>
|
|
67
|
+
) : (
|
|
68
|
+
<button
|
|
69
|
+
onClick={handleUnpair}
|
|
70
|
+
className="btn btn-ghost btn-xs border border-base-content/20 hover:btn-error hover:border-error"
|
|
71
|
+
>
|
|
72
|
+
Unpair
|
|
73
|
+
</button>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function MeshStatus() {
|
|
82
|
+
var ws = useWebSocket();
|
|
83
|
+
var { nodes } = useMesh();
|
|
84
|
+
var [pairingOpen, setPairingOpen] = useState(false);
|
|
85
|
+
|
|
86
|
+
function handleUnpair(nodeId: string) {
|
|
87
|
+
ws.send({ type: "mesh:unpair", nodeId });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var localNode = nodes.find(function (n) { return n.isLocal; });
|
|
91
|
+
var remoteNodes = nodes.filter(function (n) { return !n.isLocal; });
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="py-2">
|
|
95
|
+
<div className="mb-5">
|
|
96
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2 tracking-[0.06em]">
|
|
97
|
+
This Node
|
|
98
|
+
</div>
|
|
99
|
+
{localNode ? (
|
|
100
|
+
<NodeRow node={localNode} onUnpair={handleUnpair} />
|
|
101
|
+
) : (
|
|
102
|
+
<div className="text-[12px] text-base-content/40 italic">
|
|
103
|
+
Waiting for node info...
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div className="mb-5">
|
|
109
|
+
<div className="flex items-center justify-between mb-2">
|
|
110
|
+
<div className="text-[12px] font-semibold text-base-content/40 tracking-[0.06em]">
|
|
111
|
+
Paired Nodes
|
|
112
|
+
</div>
|
|
113
|
+
<button
|
|
114
|
+
onClick={function () { setPairingOpen(true); }}
|
|
115
|
+
className="btn btn-primary btn-sm sm:btn-xs gap-1"
|
|
116
|
+
>
|
|
117
|
+
<Plus size={10} />
|
|
118
|
+
Pair New Node
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{remoteNodes.length === 0 ? (
|
|
123
|
+
<div className="p-4 rounded-lg border border-dashed border-base-content/15 text-center text-[12px] text-base-content/40 italic">
|
|
124
|
+
No paired nodes yet.
|
|
125
|
+
</div>
|
|
126
|
+
) : (
|
|
127
|
+
remoteNodes.map(function (node) {
|
|
128
|
+
return (
|
|
129
|
+
<NodeRow
|
|
130
|
+
key={node.id}
|
|
131
|
+
node={node}
|
|
132
|
+
onUnpair={handleUnpair}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
})
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<PairingDialog
|
|
140
|
+
isOpen={pairingOpen}
|
|
141
|
+
onClose={function () { setPairingOpen(false); }}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|