@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,281 +1,281 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from "react";
|
|
2
|
-
import { X, Copy, Check } from "lucide-react";
|
|
3
|
-
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
-
import { useMesh } from "../../hooks/useMesh";
|
|
5
|
-
import { clearInvite } from "../../stores/mesh";
|
|
6
|
-
import type { ServerMessage } from "@lattice/shared";
|
|
7
|
-
|
|
8
|
-
type Tab = "generate" | "enter";
|
|
9
|
-
type PairStatus = "idle" | "connecting" | "paired" | "failed";
|
|
10
|
-
|
|
11
|
-
interface PairingDialogProps {
|
|
12
|
-
isOpen: boolean;
|
|
13
|
-
onClose: () => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function PairingDialog(props: PairingDialogProps) {
|
|
17
|
-
var ws = useWebSocket();
|
|
18
|
-
var mesh = useMesh();
|
|
19
|
-
var [tab, setTab] = useState<Tab>("generate");
|
|
20
|
-
var [pairCode, setPairCode] = useState("");
|
|
21
|
-
var [pairStatus, setPairStatus] = useState<PairStatus>("idle");
|
|
22
|
-
var [pairError, setPairError] = useState<string | null>(null);
|
|
23
|
-
var [copied, setCopied] = useState(false);
|
|
24
|
-
|
|
25
|
-
var handleKeyDown = useCallback(function (e: KeyboardEvent) {
|
|
26
|
-
if (e.key === "Escape") {
|
|
27
|
-
props.onClose();
|
|
28
|
-
}
|
|
29
|
-
}, [props.onClose]);
|
|
30
|
-
|
|
31
|
-
useEffect(function () {
|
|
32
|
-
if (!props.isOpen) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
document.addEventListener("keydown", handleKeyDown);
|
|
36
|
-
return function () {
|
|
37
|
-
document.removeEventListener("keydown", handleKeyDown);
|
|
38
|
-
};
|
|
39
|
-
}, [props.isOpen, handleKeyDown]);
|
|
40
|
-
|
|
41
|
-
useEffect(function () {
|
|
42
|
-
if (!props.isOpen) {
|
|
43
|
-
clearInvite();
|
|
44
|
-
setPairCode("");
|
|
45
|
-
setPairStatus("idle");
|
|
46
|
-
setPairError(null);
|
|
47
|
-
setCopied(false);
|
|
48
|
-
setTab("generate");
|
|
49
|
-
}
|
|
50
|
-
}, [props.isOpen]);
|
|
51
|
-
|
|
52
|
-
useEffect(function () {
|
|
53
|
-
if (pairStatus !== "connecting") {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function handler(msg: ServerMessage) {
|
|
58
|
-
if (msg.type === "mesh:paired") {
|
|
59
|
-
setPairStatus("paired");
|
|
60
|
-
setPairError(null);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
ws.subscribe("mesh:paired", handler);
|
|
65
|
-
return function () {
|
|
66
|
-
ws.unsubscribe("mesh:paired", handler);
|
|
67
|
-
};
|
|
68
|
-
}, [ws, pairStatus]);
|
|
69
|
-
|
|
70
|
-
function handleGenerateInvite() {
|
|
71
|
-
clearInvite();
|
|
72
|
-
mesh.generateInvite();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function handlePair() {
|
|
76
|
-
var trimmed = pairCode.trim();
|
|
77
|
-
if (!trimmed) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
setPairStatus("connecting");
|
|
81
|
-
setPairError(null);
|
|
82
|
-
|
|
83
|
-
var timeout = setTimeout(function () {
|
|
84
|
-
setPairStatus(function (prev) {
|
|
85
|
-
if (prev === "connecting") {
|
|
86
|
-
setPairError("Pairing timed out. Check the code and try again.");
|
|
87
|
-
return "failed";
|
|
88
|
-
}
|
|
89
|
-
return prev;
|
|
90
|
-
});
|
|
91
|
-
}, 30000);
|
|
92
|
-
|
|
93
|
-
ws.send({ type: "mesh:pair", code: trimmed });
|
|
94
|
-
|
|
95
|
-
return function () {
|
|
96
|
-
clearTimeout(timeout);
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function handleCopyCode() {
|
|
101
|
-
if (!mesh.inviteCode) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
navigator.clipboard.writeText(mesh.inviteCode).then(function () {
|
|
105
|
-
setCopied(true);
|
|
106
|
-
setTimeout(function () { setCopied(false); }, 2000);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!props.isOpen) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<div
|
|
116
|
-
role="dialog"
|
|
117
|
-
aria-modal="true"
|
|
118
|
-
aria-label="Pair a node"
|
|
119
|
-
className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/65 backdrop-blur-sm"
|
|
120
|
-
onClick={props.onClose}
|
|
121
|
-
>
|
|
122
|
-
<div
|
|
123
|
-
className="w-[440px] max-w-[calc(100vw-24px)] rounded-xl border border-base-300 bg-base-200 overflow-hidden shadow-2xl"
|
|
124
|
-
onClick={function (e) { e.stopPropagation(); }}
|
|
125
|
-
>
|
|
126
|
-
<div className="flex items-center justify-between px-5 py-4 border-b border-base-300">
|
|
127
|
-
<div className="text-[14px] font-semibold text-base-content">Pair a Node</div>
|
|
128
|
-
<button
|
|
129
|
-
onClick={props.onClose}
|
|
130
|
-
aria-label="Close"
|
|
131
|
-
className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content"
|
|
132
|
-
>
|
|
133
|
-
<X size={14} />
|
|
134
|
-
</button>
|
|
135
|
-
</div>
|
|
136
|
-
|
|
137
|
-
<div className="flex border-b border-base-300">
|
|
138
|
-
{(["generate", "enter"] as Tab[]).map(function (t) {
|
|
139
|
-
var label = t === "generate" ? "Generate Invite" : "Enter Code";
|
|
140
|
-
var isActive = tab === t;
|
|
141
|
-
return (
|
|
142
|
-
<button
|
|
143
|
-
key={t}
|
|
144
|
-
onClick={function () { setTab(t); }}
|
|
145
|
-
className={
|
|
146
|
-
"flex-1 px-4 py-2.5 text-[13px] cursor-pointer transition-colors duration-[120ms] border-b-2 " +
|
|
147
|
-
(isActive
|
|
148
|
-
? "font-semibold text-base-content border-primary"
|
|
149
|
-
: "font-normal text-base-content/40 border-transparent hover:text-base-content/70")
|
|
150
|
-
}
|
|
151
|
-
>
|
|
152
|
-
{label}
|
|
153
|
-
</button>
|
|
154
|
-
);
|
|
155
|
-
})}
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
<div className="p-5">
|
|
159
|
-
{tab === "generate" && (
|
|
160
|
-
<div>
|
|
161
|
-
<div className="text-[12px] text-base-content/40 mb-4 leading-relaxed">
|
|
162
|
-
Generate an invite code on this machine and share it with the other node.
|
|
163
|
-
The code encodes this node's address and a one-time auth token.
|
|
164
|
-
</div>
|
|
165
|
-
|
|
166
|
-
{!mesh.inviteCode && (
|
|
167
|
-
<button
|
|
168
|
-
onClick={handleGenerateInvite}
|
|
169
|
-
className="btn btn-primary btn-sm"
|
|
170
|
-
>
|
|
171
|
-
Generate Invite Code
|
|
172
|
-
</button>
|
|
173
|
-
)}
|
|
174
|
-
|
|
175
|
-
{mesh.inviteCode && (
|
|
176
|
-
<div>
|
|
177
|
-
<div className="flex items-center gap-2 px-3.5 py-2.5 rounded bg-base-100 border border-base-300 mb-4">
|
|
178
|
-
<code className="flex-1 font-mono text-[14px] font-semibold text-base-content tracking-[0.08em] break-all">
|
|
179
|
-
{mesh.inviteCode}
|
|
180
|
-
</code>
|
|
181
|
-
<button
|
|
182
|
-
onClick={handleCopyCode}
|
|
183
|
-
title="Copy code"
|
|
184
|
-
className={
|
|
185
|
-
"btn btn-xs gap-1 flex-shrink-0 " +
|
|
186
|
-
(copied ? "btn-success" : "btn-ghost border border-base-300")
|
|
187
|
-
}
|
|
188
|
-
>
|
|
189
|
-
{copied ? <Check size={12} /> : <Copy size={12} />}
|
|
190
|
-
{copied ? "Copied!" : "Copy"}
|
|
191
|
-
</button>
|
|
192
|
-
</div>
|
|
193
|
-
|
|
194
|
-
{mesh.inviteQr && (
|
|
195
|
-
<div className="flex justify-center mb-4">
|
|
196
|
-
<img
|
|
197
|
-
src={mesh.inviteQr}
|
|
198
|
-
alt="QR code for invite"
|
|
199
|
-
className="w-40 h-40 rounded border border-base-300"
|
|
200
|
-
style={{ imageRendering: "pixelated" }}
|
|
201
|
-
/>
|
|
202
|
-
</div>
|
|
203
|
-
)}
|
|
204
|
-
|
|
205
|
-
<button
|
|
206
|
-
onClick={handleGenerateInvite}
|
|
207
|
-
className="text-[12px] text-base-content/40 underline cursor-pointer"
|
|
208
|
-
>
|
|
209
|
-
Generate new code
|
|
210
|
-
</button>
|
|
211
|
-
</div>
|
|
212
|
-
)}
|
|
213
|
-
</div>
|
|
214
|
-
)}
|
|
215
|
-
|
|
216
|
-
{tab === "enter" && (
|
|
217
|
-
<div>
|
|
218
|
-
<div className="text-[12px] text-base-content/40 mb-4 leading-relaxed">
|
|
219
|
-
Paste the invite code generated on the other node to pair with it.
|
|
220
|
-
</div>
|
|
221
|
-
|
|
222
|
-
<input
|
|
223
|
-
type="text"
|
|
224
|
-
value={pairCode}
|
|
225
|
-
onChange={function (e) {
|
|
226
|
-
setPairCode(e.target.value);
|
|
227
|
-
if (pairStatus !== "idle") {
|
|
228
|
-
setPairStatus("idle");
|
|
229
|
-
setPairError(null);
|
|
230
|
-
}
|
|
231
|
-
}}
|
|
232
|
-
onKeyDown={function (e) {
|
|
233
|
-
if (e.key === "Enter") {
|
|
234
|
-
handlePair();
|
|
235
|
-
}
|
|
236
|
-
}}
|
|
237
|
-
placeholder="LTCE-XXXX-XXXX"
|
|
238
|
-
disabled={pairStatus === "connecting" || pairStatus === "paired"}
|
|
239
|
-
className="input input-bordered w-full bg-base-100 text-base-content font-mono text-[14px] tracking-[0.06em] mb-3 focus:border-primary"
|
|
240
|
-
/>
|
|
241
|
-
|
|
242
|
-
{pairStatus === "idle" && (
|
|
243
|
-
<button
|
|
244
|
-
onClick={handlePair}
|
|
245
|
-
disabled={!pairCode.trim()}
|
|
246
|
-
className={
|
|
247
|
-
"btn btn-sm " +
|
|
248
|
-
(pairCode.trim() ? "btn-primary" : "btn-ghost border border-base-300 cursor-not-allowed")
|
|
249
|
-
}
|
|
250
|
-
>
|
|
251
|
-
Pair
|
|
252
|
-
</button>
|
|
253
|
-
)}
|
|
254
|
-
|
|
255
|
-
{pairStatus === "connecting" && (
|
|
256
|
-
<div className="flex items-center gap-2 text-[13px] text-base-content/40">
|
|
257
|
-
<span
|
|
258
|
-
className="w-3 h-3 rounded-full border-2 border-primary border-t-transparent inline-block"
|
|
259
|
-
style={{ animation: "spin 0.6s linear infinite" }}
|
|
260
|
-
/>
|
|
261
|
-
Connecting...
|
|
262
|
-
</div>
|
|
263
|
-
)}
|
|
264
|
-
|
|
265
|
-
{pairStatus === "paired" && (
|
|
266
|
-
<div className="flex items-center gap-1.5 text-[13px] font-semibold text-success">
|
|
267
|
-
<Check size={14} />
|
|
268
|
-
Paired successfully!
|
|
269
|
-
</div>
|
|
270
|
-
)}
|
|
271
|
-
|
|
272
|
-
{pairStatus === "failed" && pairError && (
|
|
273
|
-
<div className="text-[12px] text-error mt-2">{pairError}</div>
|
|
274
|
-
)}
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
279
|
-
</div>
|
|
280
|
-
);
|
|
281
|
-
}
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { X, Copy, Check } from "lucide-react";
|
|
3
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
+
import { useMesh } from "../../hooks/useMesh";
|
|
5
|
+
import { clearInvite } from "../../stores/mesh";
|
|
6
|
+
import type { ServerMessage } from "@lattice/shared";
|
|
7
|
+
|
|
8
|
+
type Tab = "generate" | "enter";
|
|
9
|
+
type PairStatus = "idle" | "connecting" | "paired" | "failed";
|
|
10
|
+
|
|
11
|
+
interface PairingDialogProps {
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function PairingDialog(props: PairingDialogProps) {
|
|
17
|
+
var ws = useWebSocket();
|
|
18
|
+
var mesh = useMesh();
|
|
19
|
+
var [tab, setTab] = useState<Tab>("generate");
|
|
20
|
+
var [pairCode, setPairCode] = useState("");
|
|
21
|
+
var [pairStatus, setPairStatus] = useState<PairStatus>("idle");
|
|
22
|
+
var [pairError, setPairError] = useState<string | null>(null);
|
|
23
|
+
var [copied, setCopied] = useState(false);
|
|
24
|
+
|
|
25
|
+
var handleKeyDown = useCallback(function (e: KeyboardEvent) {
|
|
26
|
+
if (e.key === "Escape") {
|
|
27
|
+
props.onClose();
|
|
28
|
+
}
|
|
29
|
+
}, [props.onClose]);
|
|
30
|
+
|
|
31
|
+
useEffect(function () {
|
|
32
|
+
if (!props.isOpen) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
36
|
+
return function () {
|
|
37
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
38
|
+
};
|
|
39
|
+
}, [props.isOpen, handleKeyDown]);
|
|
40
|
+
|
|
41
|
+
useEffect(function () {
|
|
42
|
+
if (!props.isOpen) {
|
|
43
|
+
clearInvite();
|
|
44
|
+
setPairCode("");
|
|
45
|
+
setPairStatus("idle");
|
|
46
|
+
setPairError(null);
|
|
47
|
+
setCopied(false);
|
|
48
|
+
setTab("generate");
|
|
49
|
+
}
|
|
50
|
+
}, [props.isOpen]);
|
|
51
|
+
|
|
52
|
+
useEffect(function () {
|
|
53
|
+
if (pairStatus !== "connecting") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handler(msg: ServerMessage) {
|
|
58
|
+
if (msg.type === "mesh:paired") {
|
|
59
|
+
setPairStatus("paired");
|
|
60
|
+
setPairError(null);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ws.subscribe("mesh:paired", handler);
|
|
65
|
+
return function () {
|
|
66
|
+
ws.unsubscribe("mesh:paired", handler);
|
|
67
|
+
};
|
|
68
|
+
}, [ws, pairStatus]);
|
|
69
|
+
|
|
70
|
+
function handleGenerateInvite() {
|
|
71
|
+
clearInvite();
|
|
72
|
+
mesh.generateInvite();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handlePair() {
|
|
76
|
+
var trimmed = pairCode.trim();
|
|
77
|
+
if (!trimmed) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setPairStatus("connecting");
|
|
81
|
+
setPairError(null);
|
|
82
|
+
|
|
83
|
+
var timeout = setTimeout(function () {
|
|
84
|
+
setPairStatus(function (prev) {
|
|
85
|
+
if (prev === "connecting") {
|
|
86
|
+
setPairError("Pairing timed out. Check the code and try again.");
|
|
87
|
+
return "failed";
|
|
88
|
+
}
|
|
89
|
+
return prev;
|
|
90
|
+
});
|
|
91
|
+
}, 30000);
|
|
92
|
+
|
|
93
|
+
ws.send({ type: "mesh:pair", code: trimmed });
|
|
94
|
+
|
|
95
|
+
return function () {
|
|
96
|
+
clearTimeout(timeout);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleCopyCode() {
|
|
101
|
+
if (!mesh.inviteCode) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
navigator.clipboard.writeText(mesh.inviteCode).then(function () {
|
|
105
|
+
setCopied(true);
|
|
106
|
+
setTimeout(function () { setCopied(false); }, 2000);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!props.isOpen) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
role="dialog"
|
|
117
|
+
aria-modal="true"
|
|
118
|
+
aria-label="Pair a node"
|
|
119
|
+
className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/65 backdrop-blur-sm"
|
|
120
|
+
onClick={props.onClose}
|
|
121
|
+
>
|
|
122
|
+
<div
|
|
123
|
+
className="w-[440px] max-w-[calc(100vw-24px)] rounded-xl border border-base-300 bg-base-200 overflow-hidden shadow-2xl"
|
|
124
|
+
onClick={function (e) { e.stopPropagation(); }}
|
|
125
|
+
>
|
|
126
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-base-300">
|
|
127
|
+
<div className="text-[14px] font-semibold text-base-content">Pair a Node</div>
|
|
128
|
+
<button
|
|
129
|
+
onClick={props.onClose}
|
|
130
|
+
aria-label="Close"
|
|
131
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content"
|
|
132
|
+
>
|
|
133
|
+
<X size={14} />
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div className="flex border-b border-base-300">
|
|
138
|
+
{(["generate", "enter"] as Tab[]).map(function (t) {
|
|
139
|
+
var label = t === "generate" ? "Generate Invite" : "Enter Code";
|
|
140
|
+
var isActive = tab === t;
|
|
141
|
+
return (
|
|
142
|
+
<button
|
|
143
|
+
key={t}
|
|
144
|
+
onClick={function () { setTab(t); }}
|
|
145
|
+
className={
|
|
146
|
+
"flex-1 px-4 py-2.5 text-[13px] cursor-pointer transition-colors duration-[120ms] border-b-2 " +
|
|
147
|
+
(isActive
|
|
148
|
+
? "font-semibold text-base-content border-primary"
|
|
149
|
+
: "font-normal text-base-content/40 border-transparent hover:text-base-content/70")
|
|
150
|
+
}
|
|
151
|
+
>
|
|
152
|
+
{label}
|
|
153
|
+
</button>
|
|
154
|
+
);
|
|
155
|
+
})}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="p-5">
|
|
159
|
+
{tab === "generate" && (
|
|
160
|
+
<div>
|
|
161
|
+
<div className="text-[12px] text-base-content/40 mb-4 leading-relaxed">
|
|
162
|
+
Generate an invite code on this machine and share it with the other node.
|
|
163
|
+
The code encodes this node's address and a one-time auth token.
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{!mesh.inviteCode && (
|
|
167
|
+
<button
|
|
168
|
+
onClick={handleGenerateInvite}
|
|
169
|
+
className="btn btn-primary btn-sm"
|
|
170
|
+
>
|
|
171
|
+
Generate Invite Code
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{mesh.inviteCode && (
|
|
176
|
+
<div>
|
|
177
|
+
<div className="flex items-center gap-2 px-3.5 py-2.5 rounded bg-base-100 border border-base-300 mb-4">
|
|
178
|
+
<code className="flex-1 font-mono text-[14px] font-semibold text-base-content tracking-[0.08em] break-all">
|
|
179
|
+
{mesh.inviteCode}
|
|
180
|
+
</code>
|
|
181
|
+
<button
|
|
182
|
+
onClick={handleCopyCode}
|
|
183
|
+
title="Copy code"
|
|
184
|
+
className={
|
|
185
|
+
"btn btn-xs gap-1 flex-shrink-0 " +
|
|
186
|
+
(copied ? "btn-success" : "btn-ghost border border-base-300")
|
|
187
|
+
}
|
|
188
|
+
>
|
|
189
|
+
{copied ? <Check size={12} /> : <Copy size={12} />}
|
|
190
|
+
{copied ? "Copied!" : "Copy"}
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{mesh.inviteQr && (
|
|
195
|
+
<div className="flex justify-center mb-4">
|
|
196
|
+
<img
|
|
197
|
+
src={mesh.inviteQr}
|
|
198
|
+
alt="QR code for invite"
|
|
199
|
+
className="w-40 h-40 rounded border border-base-300"
|
|
200
|
+
style={{ imageRendering: "pixelated" }}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
<button
|
|
206
|
+
onClick={handleGenerateInvite}
|
|
207
|
+
className="text-[12px] text-base-content/40 underline cursor-pointer"
|
|
208
|
+
>
|
|
209
|
+
Generate new code
|
|
210
|
+
</button>
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{tab === "enter" && (
|
|
217
|
+
<div>
|
|
218
|
+
<div className="text-[12px] text-base-content/40 mb-4 leading-relaxed">
|
|
219
|
+
Paste the invite code generated on the other node to pair with it.
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<input
|
|
223
|
+
type="text"
|
|
224
|
+
value={pairCode}
|
|
225
|
+
onChange={function (e) {
|
|
226
|
+
setPairCode(e.target.value);
|
|
227
|
+
if (pairStatus !== "idle") {
|
|
228
|
+
setPairStatus("idle");
|
|
229
|
+
setPairError(null);
|
|
230
|
+
}
|
|
231
|
+
}}
|
|
232
|
+
onKeyDown={function (e) {
|
|
233
|
+
if (e.key === "Enter") {
|
|
234
|
+
handlePair();
|
|
235
|
+
}
|
|
236
|
+
}}
|
|
237
|
+
placeholder="LTCE-XXXX-XXXX"
|
|
238
|
+
disabled={pairStatus === "connecting" || pairStatus === "paired"}
|
|
239
|
+
className="input input-bordered w-full bg-base-100 text-base-content font-mono text-[14px] tracking-[0.06em] mb-3 focus:border-primary"
|
|
240
|
+
/>
|
|
241
|
+
|
|
242
|
+
{pairStatus === "idle" && (
|
|
243
|
+
<button
|
|
244
|
+
onClick={handlePair}
|
|
245
|
+
disabled={!pairCode.trim()}
|
|
246
|
+
className={
|
|
247
|
+
"btn btn-sm " +
|
|
248
|
+
(pairCode.trim() ? "btn-primary" : "btn-ghost border border-base-300 cursor-not-allowed")
|
|
249
|
+
}
|
|
250
|
+
>
|
|
251
|
+
Pair
|
|
252
|
+
</button>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{pairStatus === "connecting" && (
|
|
256
|
+
<div className="flex items-center gap-2 text-[13px] text-base-content/40">
|
|
257
|
+
<span
|
|
258
|
+
className="w-3 h-3 rounded-full border-2 border-primary border-t-transparent inline-block"
|
|
259
|
+
style={{ animation: "spin 0.6s linear infinite" }}
|
|
260
|
+
/>
|
|
261
|
+
Connecting...
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{pairStatus === "paired" && (
|
|
266
|
+
<div className="flex items-center gap-1.5 text-[13px] font-semibold text-success">
|
|
267
|
+
<Check size={14} />
|
|
268
|
+
Paired successfully!
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{pairStatus === "failed" && pairError && (
|
|
273
|
+
<div className="text-[12px] text-error mt-2">{pairError}</div>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|